> ## Documentation Index
> Fetch the complete documentation index at: https://rockboxzig.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Swift

> Rockbox DSP, metadata, and playback in Swift — statically bundled or dlopen'd, macOS + iOS.

The Swift package ships the engine two ways, from one shared implementation
(`RockboxFFICore` — the public API plus a `dlsym` loader). Pick the product that
fits how you want to distribute:

| Product | Native code | Runtime lookup | Use when |
| - | - | - | - |
| `RockboxFFI` | statically linked | none | you want a single self-contained binary |
| `RockboxFFIDynamic` | `dlopen`ed | file lookup | you'd rather ship the library beside your app |

Both drive the same typed `@convention(c)` closures; the loader resolves the
`rb_*` symbols from the process image (static) or from a `dlopen`ed library
(dynamic — the same approach as the Ruby and Python bindings), decided at
runtime.

Build the native artifacts once from the repo root — this produces **both**
`librockbox_ffi.a` (bundled by `RockboxFFI`) and `librockbox_ffi.dylib` (loaded
by `RockboxFFIDynamic`, which honours `ROCKBOX_FFI_LIB`, else walks up to
`target/release`):

```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
cargo build --release -p rockbox-ffi
```

```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
swift run rockbox-ffi-smoke                 # static product, no runtime lookup
swift run rockbox-ffi-play /path/to/audio   # dynamic product, plays via output device
```

## Distribution

The `bindings-release` workflow publishes reproducible, prebuilt Swift artifacts
to each GitHub Release:

> [card] RockboxFFI.xcframework.zip
>
> Static library + header for **macOS** (universal), **iOS** (arm64 device),
> and the **iOS simulator** (arm64 + x86\_64). Drop it into Xcode or reference
> it as a SwiftPM `binaryTarget`.

> [card] rockbox-ffi-swift-macos.tar.gz
>
> The SwiftPM package with the universal macOS archive vendored under `Libs/`,
> so `swift build` works turnkey off the monorepo (macOS only; iOS consumers
> use the xcframework).

> [!NOTE]
>
> The static `RockboxFFI` product force-references each `rb_*` entry point with a
> `-u <symbol>` linker flag (it reaches them via `dlsym`, so `-dead_strip` would
> otherwise drop them). If you consume the xcframework through this loader on
> iOS, keep those flags. iOS apps must also link `AudioToolbox` / `AVFoundation`
> and set up an `AVAudioSession` before using `Player`.

## Quick start

```swift theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import RockboxFFI

// metadata
let meta = try Metadata.read("/music/song.flac")   // [String: Any]
Metadata.probe("song.flac")                         // "FLAC"

// DSP (interleaved stereo int16)
let dsp = try Dsp(sampleRate: 44_100)
defer { dsp.close() }
dsp.eqEnable(true)
dsp.setEqBand(0, cutoffHz: 100, q: 0.7, gainDb: 3.0)
let out = try dsp.process(samples)

// Player (queue + transport) — mutating setters are @discardableResult and
// return Self, so they chain fluently.
var cfg = Player.Config(); cfg.volume = 0.8
let player = try Player(config: cfg)
try player
    .setQueue(["/music/a.flac", "https://example.com/b.mp3"])  // files, http(s), live streams
    .setEqEnabled(true)
    .setEqPreset(.bassBoost)
    .setShuffle(true)
    .setRepeat(.all)
    .play()
```

`setQueue` / `enqueue` / `insert` accept **local file paths**, finite
`http(s)://` URLs, and **live-radio / streaming URLs** interchangeably.

## Player DSP chain

The `Player` exposes the full Rockbox DSP pipeline. Every setter is
`@discardableResult` and returns `Self`, so they compose in one fluent chain
(the statement-style calls still compile unchanged). Read the live state back
with `dspSettings()` (a `[String: Any]` snapshot).

```swift theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
try player
    .setEqEnabled(true)
    .setEqPreset(.rock)                       // 21-case EqPreset (flat … vocalBoost)
    .setEqBand(0, cutoffHz: 100, q: 0.7, gainDb: 3.0)
    .setEqPrecut(2.0)
    .setBass(4).setTreble(2)                  // or setTone(bassDb:trebleDb:bassCutoffHz:trebleCutoffHz:)
    .setCrossfeed(.meier, directGain: 0, crossGain: 0, hfGain: 0, hfCutoff: 0)
    .setChannelMode(.stereo).setStereoWidth(120)
    .setBassEnhancement(strength: 50, precut: 0)
    .setFatigueReduction(30)

let dsp = try player.dspSettings()            // [String: Any] snapshot
```

### Player API

| Group | Methods |
| - | - |
| Queue | `setQueue(_:)`, `enqueue(_:)`, `insert(_:position:index:)`, `queue()` |
| Transport | `play()`, `pause()`, `toggle()`, `stop()`, `next()`, `previous()`, `skip(to:)`, `seek(ms:)` |
| Settings | `setVolume(_:)`, `volume`, `setBalance(_:)`, `balance`, `sampleRate`, `setCrossfade(_:…)`, `setReplaygain(_:preampDb:preventClipping:)` |
| Shuffle/repeat | `setShuffle(_:)`, `isShuffleEnabled()`, `setRepeat(_:)`, `` `repeat`() `` |
| EQ | `setEqEnabled(_:)`, `isEqEnabled()`, `setEqPreset(_:)`, `setEqBand(_:cutoffHz:q:gainDb:)`, `setEqPrecut(_:)` |
| Tone | `setTone(bassDb:trebleDb:bassCutoffHz:trebleCutoffHz:)`, `setBass(_:)`, `setTreble(_:)`, `setBassCutoff(_:)`, `setTrebleCutoff(_:)` |
| Spatial | `setCrossfeed(_:directGain:crossGain:hfGain:hfCutoff:)`, `setSurround(delayMs:balance:cutoffLowHz:cutoffHighHz:)`, `setChannelMode(_:)`, `setStereoWidth(_:)` |
| Enhancement | `setBassEnhancement(strength:precut:)`, `setFatigueReduction(_:)`, `setCompressor(thresholdDb:makeupGain:ratio:knee:attackMs:releaseMs:)`, `setDither(_:)`, `setPitch(_:)` |
| Status/DSP | `status()`, `dspSettings()` |
| Resume / m3u | `resume()`, `saveResume()`, `clearResume()`, `importM3u(_:position:index:)`, `loadM3u(_:)`, `exportM3u(_:)` |

`setBalance(_:)` / `balance` take a stereo balance of **-100 (full left) to
+100 (full right)**, with **0 = centre**.

Relevant enums: `RepeatMode` (`off`/`one`/`all`), `EqPreset` (21 cases,
`flat` … `vocalBoost`), `CrossfeedMode` (`off`/`meier`/`custom`), `ChannelMode`
(`stereo`/`mono`/`custom`/`monoLeft`/`monoRight`/`karaoke`/`swap`),
`CrossfadeMode`, and `MixMode`.

> [!TIP]
>
> The DSP controls mirror Rockbox's own Sound Settings. For what each parameter
> does, see the [official Rockbox manual](https://download.rockbox.org/daily/manual/rockbox-ipodvideo/rockbox-buildch6.html).

## Decoder

`Decoder` is a streaming decoder: it decodes one local audio file to
interleaved-stereo **S16LE PCM**, one chunk at a time, through the Rockbox codec
engine. Open it with a path; the native handle is freed on `close()` or
`deinit`.

| Member | Returns | Notes |
| - | - | - |
| `init(path:)` | — | opens the file for decoding; throws if the codec can't |
| `metadata()` | `[String: Any]` | tags + stream properties (same shape as `Metadata.read`) |
| `nextChunk()` | `(samples, sampleRate)?` | interleaved-stereo int16; `nil` at end of track |
| `seek(ms:)` | — | request a seek to `ms` milliseconds |
| `elapsedMs` | `UInt64` | position last reported by the codec, in ms |
| `finished()` | `(done, code)` | `code` valid when `done`: `0` = clean end, `< 0` = error |
| `close()` | — | frees the handle; safe to call more than once |

```swift theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import RockboxFFI

let decoder = try Decoder(path: "/music/song.flac")
defer { decoder.close() }

let tags = try decoder.metadata()            // [String: Any]

while let (samples, sampleRate) = decoder.nextChunk() {
    // samples: [Int16] interleaved L,R,L,R…  — feed to your output / encoder
    _ = (samples, sampleRate)
}

let (done, code) = decoder.finished()        // code 0 = clean end, negative = codec error
```

> [!WARNING]
>
> The Rockbox codec state is **process-wide — only one `Decoder` decodes at a
> time**. Constructing a second `Decoder` blocks until the first is freed
> (via `close()` or `deinit`).

## Notes

* Rich values (metadata, player status) come back as `[String: Any]`, parsed
  from the ABI's JSON with `JSONSerialization`.
* Native memory is freed inside the binding — `close()` / `deinit` for handles;
  every `char*` / `int16*` return is freed after copying.

> [!WARNING]
>
> **Two ReplayGain encodings** — use `DspReplayGainMode` (track 0, album 1,
> shuffle 2, off 3) for `Dsp`, and `ReplayGainMode` (off 0, track 1, album 2)
> for `Player`. See [the overview](https://rockboxzig.mintlify.site/bindings/overview.md#the-one-thing-to-know-two-replaygain-encodings).

> [!NOTE]
>
> Keep `Sources/RockboxFFICore/Loader.swift` — and the `ffiSymbols` list in
> `Package.swift` — in sync with `include/rockbox_ffi.h`. The loader declares
> each function signature by hand, and the static product force-links each
> symbol by name.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.