Skip to content

Add Swift API for source separation - #3426

Merged
csukuangfj merged 4 commits into
k2-fsa:masterfrom
csukuangfj:swift
Mar 27, 2026
Merged

csukuangfj merged 4 commits into
k2-fsa:masterfrom
csukuangfj:swift

Conversation

@csukuangfj

@csukuangfj csukuangfj commented Mar 26, 2026 •

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Offline source separation using Spleeter and UVR models; Swift API and runnable examples to split vocals/accompaniment.
  • Tests

    • Added example run scripts and integrated source-separation steps into the test workflow with automated cleanup.
  • Chores

    • Updated ignore rules to exclude generated source-separation artifacts and example binaries.

@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Mar 26, 2026
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly expands the Swift API by integrating offline audio source separation capabilities. It provides developers with tools to easily separate audio into different stems (e.g., vocals and accompaniment) using both Spleeter and UVR models, enhancing the framework's utility for audio processing applications.

Highlights

  • Swift API for Source Separation: Added a comprehensive Swift API for offline audio source separation, supporting both Spleeter and UVR models.
  • New Swift Example Applications: Introduced new Swift example applications (source-separation-spleeter.swift and source-separation-uvr.swift) to demonstrate the usage of the new source separation API.
  • Updated CI/CD Test Script: Updated the CI/CD test script (.github/scripts/test-swift.sh) to include execution and cleanup for the newly added Swift source separation examples.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@coderabbitai

coderabbitai Bot commented Mar 26, 2026 •

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds Swift bindings and example executables for offline source separation (Spleeter and UVR), build/run helper scripts, test pipeline integration, and .gitignore updates; implements AudioBuffer, SourceSeparationConfig, and SourceSeparator in Swift to read WAVs, run separation via the C API, and save stems.

Changes

Cohort / File(s) Summary
Test integration
\.github/scripts/test-swift.sh
Added steps to run run-source-separation-spleeter.sh and run-source-separation-uvr.sh and to remove generated model/artifact files after running.
Repository ignore
\.gitignore
Added sherpa-onnx-spleeter-2stems-fp16 entry.
Swift API wrappers
swift-api-examples/SherpaOnnx.swift
Introduced AudioBuffer, SourceSeparationConfig (with Spleeter/UVR options) and SourceSeparator managing C engine lifecycle; added file I/O, C-string conversion helpers, and conversion between owned buffers and C multi-channel waves.
Build/run scripts
swift-api-examples/run-source-separation-spleeter.sh, swift-api-examples/run-source-separation-uvr.sh
New Bash scripts that download required model/audio artifacts if missing, conditionally build the Swift example binaries, set DYLD_LIBRARY_PATH, and execute the examples.
Example executables
swift-api-examples/source-separation-spleeter.swift, swift-api-examples/source-separation-uvr.swift
New Swift command-line examples that configure separation models, load input WAV, time/process separation, and save up to two stems (vocals/accompaniment or vocals/non-vocals).
Examples ignore
swift-api-examples/.gitignore
Added ignore entries for source-separation-spleeter and source-separation-uvr binaries.

Sequence Diagram(s)

sequenceDiagram
    participant App as Swift App
    participant Engine as SourceSeparator (C Engine)
    participant Model as ONNX Model (Spleeter/UVR)
    participant IO as File I/O

    App->>IO: load WAV (qi-feng-le-zh.wav)
    IO-->>App: AudioBuffer (interleaved multi-channel)
    App->>Engine: process(buffer: AudioBuffer)
    Engine->>Model: run separation (ONNX inference)
    Model-->>Engine: separated stems (C output)
    Engine-->>App: [AudioBuffer] (owned stems)
    loop per stem
        App->>IO: save stem -> vocals.wav / accompaniment.wav
        IO-->>App: write result
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I shuffled through WAVs with a twitch of my nose,

Split vocals and rests where the audio flows,
Spleeter and UVR danced under moonlight,
Swift wrapped the magic and saved stems at night,
Hooray for small scripts and a rabbit's bright code!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Add Swift API for source separation' directly summarizes the main change—introducing new Swift API classes and examples for audio source separation using Spleeter and UVR models.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces Swift API wrappers and example scripts for source separation using Spleeter and UVR models. The changes include new Swift classes (SherpaOnnxMultiChannelWaveWrapper, SherpaOnnxSourceSeparationOutputWrapper, SherpaOnnxOfflineSourceSeparationWrapper) to interface with the underlying C library, along with shell scripts to build and run these examples. Review feedback focuses on improving the robustness and idiomatic Swift usage of these wrappers, specifically by advocating for failable initializers and non-optional properties to handle C pointers that can be null, simplifying deinitialization and property access, and updating the example code to properly handle optional return values. Minor improvements were also suggested for the shell scripts to use rm -f for safer file cleanup.


./run-source-separation-spleeter.sh
rm -rf sherpa-onnx-spleeter-*
rm vocals.wav accompaniment.wav

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using rm -f is safer as it won't cause an error if the files don't exist, making the script more robust, especially since set -e is active.

Suggested change
rm vocals.wav accompaniment.wav
rm -f vocals.wav accompaniment.wav


./run-source-separation-uvr.sh
rm -rf UVR-MDX-NET-Voc_FT.onnx
rm uvr-vocals.wav uvr-non-vocals.wav

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using rm -f is safer as it won't cause an error if the files don't exist, making the script more robust, especially since set -e is active.

Suggested change
rm uvr-vocals.wav uvr-non-vocals.wav
rm -f uvr-vocals.wav uvr-non-vocals.wav

Comment thread swift-api-examples/SherpaOnnx.swift Outdated
Comment on lines +2076 to +2080
let wave: UnsafePointer<SherpaOnnxMultiChannelWave>!

init(wave: UnsafePointer<SherpaOnnxMultiChannelWave>!) {
self.wave = wave
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using an implicitly unwrapped optional (!) for a C pointer that can be nil is unsafe. It's better to use a failable initializer (init?) and a non-optional property. This makes the code safer and more idiomatic in Swift.

Suggested change
let wave: UnsafePointer<SherpaOnnxMultiChannelWave>!
init(wave: UnsafePointer<SherpaOnnxMultiChannelWave>!) {
self.wave = wave
}
let wave: UnsafePointer<SherpaOnnxMultiChannelWave>
init?(wave: UnsafePointer<SherpaOnnxMultiChannelWave>?) {
guard let wave = wave else { return nil }
self.wave = wave
}

Comment thread swift-api-examples/SherpaOnnx.swift Outdated
Comment on lines +2082 to +2101
deinit {
if let wave {
SherpaOnnxFreeMultiChannelWave(wave)
}
}

var numChannels: Int32 {
guard let wave else { return 0 }
return wave.pointee.num_channels
}

var numSamples: Int32 {
guard let wave else { return 0 }
return wave.pointee.num_samples
}

var sampleRate: Int32 {
guard let wave else { return 0 }
return wave.pointee.sample_rate
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Following the change to a non-optional wave property, the deinit and computed properties can be simplified. The guard let checks are no longer necessary, which makes the code cleaner and more efficient.

Suggested change
deinit {
if let wave {
SherpaOnnxFreeMultiChannelWave(wave)
}
}
var numChannels: Int32 {
guard let wave else { return 0 }
return wave.pointee.num_channels
}
var numSamples: Int32 {
guard let wave else { return 0 }
return wave.pointee.num_samples
}
var sampleRate: Int32 {
guard let wave else { return 0 }
return wave.pointee.sample_rate
}
deinit {
SherpaOnnxFreeMultiChannelWave(wave)
}
var numChannels: Int32 {
return wave.pointee.num_channels
}
var numSamples: Int32 {
return wave.pointee.num_samples
}
var sampleRate: Int32 {
return wave.pointee.sample_rate
}

Comment thread swift-api-examples/SherpaOnnx.swift Outdated
Comment on lines +2106 to +2110
let output: UnsafePointer<SherpaOnnxSourceSeparationOutput>!

init(output: UnsafePointer<SherpaOnnxSourceSeparationOutput>!) {
self.output = output
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to SherpaOnnxMultiChannelWaveWrapper, it's safer to use a failable initializer and a non-optional property for output since the underlying C function can return nil.

Suggested change
let output: UnsafePointer<SherpaOnnxSourceSeparationOutput>!
init(output: UnsafePointer<SherpaOnnxSourceSeparationOutput>!) {
self.output = output
}
let output: UnsafePointer<SherpaOnnxSourceSeparationOutput>
init?(output: UnsafePointer<SherpaOnnxSourceSeparationOutput>?) {
guard let output = output else { return nil }
self.output = output
}

Comment on lines +2163 to +2167
deinit {
if let impl {
SherpaOnnxDestroyOfflineSourceSeparation(impl)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

With a non-optional impl property, the deinit can be simplified by removing the if let binding.

  deinit {
    SherpaOnnxDestroyOfflineSourceSeparation(impl)
  }

Comment thread swift-api-examples/SherpaOnnx.swift Outdated
Comment on lines +2170 to +2173
static func readWave(filename: String) -> SherpaOnnxMultiChannelWaveWrapper {
let wave = SherpaOnnxReadWaveMultiChannel(toCPointer(filename))
return SherpaOnnxMultiChannelWaveWrapper(wave: wave)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since SherpaOnnxReadWaveMultiChannel can return nil on failure, this function should return an optional SherpaOnnxMultiChannelWaveWrapper? to propagate the failure information to the caller. This avoids creating an "empty" wrapper object that might hide errors.

Suggested change
static func readWave(filename: String) -> SherpaOnnxMultiChannelWaveWrapper {
let wave = SherpaOnnxReadWaveMultiChannel(toCPointer(filename))
return SherpaOnnxMultiChannelWaveWrapper(wave: wave)
}
static func readWave(filename: String) -> SherpaOnnxMultiChannelWaveWrapper? {
let wave = SherpaOnnxReadWaveMultiChannel(toCPointer(filename))
return SherpaOnnxMultiChannelWaveWrapper(wave: wave)
}

Comment thread swift-api-examples/SherpaOnnx.swift Outdated
Comment on lines +2176 to +2190
func process(
wave: SherpaOnnxMultiChannelWaveWrapper
) -> SherpaOnnxSourceSeparationOutputWrapper {
guard let wavePtr = wave.wave else {
return SherpaOnnxSourceSeparationOutputWrapper(output: nil)
}
let output = SherpaOnnxOfflineSourceSeparationProcess(
impl,
wavePtr.pointee.samples,
wavePtr.pointee.num_channels,
wavePtr.pointee.num_samples,
wavePtr.pointee.sample_rate
)
return SherpaOnnxSourceSeparationOutputWrapper(output: output)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The SherpaOnnxOfflineSourceSeparationProcess function can return nil. To handle this gracefully, the process method should return an optional SherpaOnnxSourceSeparationOutputWrapper?. Also, with the suggested changes to SherpaOnnxMultiChannelWaveWrapper, the guard check for wave.wave is no longer needed here, as the caller would have to unwrap the optional wave object.

  func process(
    wave: SherpaOnnxMultiChannelWaveWrapper
  ) -> SherpaOnnxSourceSeparationOutputWrapper? {
    let wavePtr = wave.wave
    let output = SherpaOnnxOfflineSourceSeparationProcess(
      impl,
      wavePtr.pointee.samples,
      wavePtr.pointee.num_channels,
      wavePtr.pointee.num_samples,
      wavePtr.pointee.sample_rate
    )
    return SherpaOnnxSourceSeparationOutputWrapper(output: output)
  }

Comment on lines +26 to +34
let ss = SherpaOnnxOfflineSourceSeparationWrapper(config: &config)

let wave = SherpaOnnxOfflineSourceSeparationWrapper.readWave(
filename: "./qi-feng-le-zh.wav")
print(
"Input: channels=\(wave.numChannels), samples=\(wave.numSamples), sampleRate=\(wave.sampleRate)"
)

let output = ss.process(wave: wave)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

With the suggested changes to make the wrapper initializers and methods failable, this example code needs to be updated to handle the returned optionals. This makes the example more robust by properly handling potential initialization or processing failures.

  guard let ss = SherpaOnnxOfflineSourceSeparationWrapper(config: &config) else {
    fatalError("Failed to create SherpaOnnxOfflineSourceSeparationWrapper")
  }

  guard let wave = SherpaOnnxOfflineSourceSeparationWrapper.readWave(
    filename: "./qi-feng-le-zh.wav") else {
    fatalError("Failed to read wave file ./qi-feng-le-zh.wav")
  }
  print(
    "Input: channels=\(wave.numChannels), samples=\(wave.numSamples), sampleRate=\(wave.sampleRate)"
  )

  guard let output = ss.process(wave: wave) else {
    fatalError("Failed to process wave")
  }

Comment on lines +23 to +31
let ss = SherpaOnnxOfflineSourceSeparationWrapper(config: &config)

let wave = SherpaOnnxOfflineSourceSeparationWrapper.readWave(
filename: "./qi-feng-le-zh.wav")
print(
"Input: channels=\(wave.numChannels), samples=\(wave.numSamples), sampleRate=\(wave.sampleRate)"
)

let output = ss.process(wave: wave)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

With the suggested changes to make the wrapper initializers and methods failable, this example code needs to be updated to handle the returned optionals. This makes the example more robust by properly handling potential initialization or processing failures.

  guard let ss = SherpaOnnxOfflineSourceSeparationWrapper(config: &config) else {
    fatalError("Failed to create SherpaOnnxOfflineSourceSeparationWrapper")
  }

  guard let wave = SherpaOnnxOfflineSourceSeparationWrapper.readWave(
    filename: "./qi-feng-le-zh.wav") else {
    fatalError("Failed to read wave file ./qi-feng-le-zh.wav")
  }
  print(
    "Input: channels=\(wave.numChannels), samples=\(wave.numSamples), sampleRate=\(wave.sampleRate)"
  )

  guard let output = ss.process(wave: wave) else {
    fatalError("Failed to process wave")
  }

@csukuangfj
csukuangfj merged commit b0c562c into k2-fsa:master Mar 27, 2026
0 of 3 checks passed
@csukuangfj
csukuangfj deleted the swift branch March 27, 2026 03:48
@coderabbitai coderabbitai Bot mentioned this pull request Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant