> ## 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.

# Gleam

> Rockbox DSP, metadata, and playback in Gleam (Erlang target) via an erl_nif shim over the C ABI.

```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
gleam add rockbox_ffi
```

Requires **Gleam ≥ 1.0** on the **Erlang target** and **OTP 27+** (JSON is
decoded with the built-in `json` module plus `gleam/dynamic/decode` — no
`gleam_json` dependency). The NIF links the Rust static archive
(`target/release/librockbox_ffi.a`); build it with `make` (which runs
`cargo build --release -p rockbox-ffi` first if needed) before `gleam test`.

```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
make            # -> priv/rockbox_ffi_nif.so
gleam test
```

## Quick start

```gleam theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import rockbox/metadata
import rockbox/dsp
import rockbox/player
import gleam/option.{None, Some}

// --- metadata ---
let assert Ok(meta) = metadata.read("song.flac")
meta.artist        // "…"
meta.duration_ms   // 122324
metadata.probe("track.opus")   // Some("Opus")

// --- DSP (interleaved stereo int16 BitArray) ---
let d = dsp.new(44_100)
dsp.eq_enable(d, True)
dsp.set_eq_band(d, 0, 60, 0.7, 3.0)
dsp.set_replaygain(d, 0, True, 0.0)                 // 0 = track (DSP-native)
dsp.set_replaygain_gains(d, Some(-6.02), None, None, None)
let out = dsp.process(d, pcm)                        // BitArray in/out

// --- playback (needs an output device) ---
// Mutating `player` functions return the `Player`, so setup pipes:
let p =
  player.with_config(player.Config(..player.default_config(), volume: 0.8))
  |> player.set_queue([        // local paths, http(s):// URLs, or live-radio / streaming URLs
    "a.flac",
    "https://example.com/b.mp3",
    "http://stream.example.com/radio",
  ])
  |> player.set_eq_preset(player.BassBoost)   // 21 built-in EqPreset values
  |> player.set_bass(7)                       // tone controls, dB
  |> player.set_treble(4)
  |> player.set_crossfeed(player.Meier, 0, 0, 0, 0)
  |> player.set_shuffle(True)
  |> player.set_repeat(player.All)            // RepeatMode: Off / One / All
  |> player.play

player.status(p)         // Status(state: "playing", index: Some(0), ...)
player.dsp_settings(p)   // DspSettings(equalizer:, tone:, crossfeed:, ...)
```

## API

| Module | Contents |
| - | - |
| `rockbox/metadata` | `read(path) -> Result(Metadata, _)`, `probe(name) -> Option(String)` |
| `rockbox/decoder` | decode a file to interleaved-stereo S16LE PCM one chunk at a time |
| `rockbox/dsp` | EQ / tone / surround / compressor / ReplayGain, `process(d, pcm)` |
| `rockbox/player` | queue + transport + crossfade + ReplayGain + full DSP chain + shuffle/repeat |

### `rockbox/player`

Every mutating function returns the `Player`, so calls chain with `|>`. The
setters below apply the full Rockbox DSP chain live, mid-playback.

| Function | Purpose |
| - | - |
| `new()` / `with_config(Config)` | Create a player (device default, or explicit `Config`) |
| `set_queue(paths)` / `enqueue(path)` | Set / append sources — local paths, `http(s)://`, or live-radio / streaming URLs |
| `insert(paths, InsertPosition)` / `queue()` | Insert at a position; read the current queue |
| `play` / `pause` / `toggle` / `stop` | Transport |
| `next` / `previous` / `skip_to(i)` / `seek_ms(ms)` | Navigation within the queue / track |
| `set_volume(f)` / `volume()` / `sample_rate()` | Output volume and device sample rate |
| `set_balance(bal)` / `balance()` | Stereo balance, `-100` (full left) … `+100` (full right); `0` = centre |
| `set_shuffle(Bool)` / `is_shuffle_enabled()` | Shuffle playback |
| `set_repeat(RepeatMode)` / `repeat()` | Repeat mode — `Off` / `One` / `All` |
| `set_crossfade(...)` / `set_replaygain(...)` | Crossfade behaviour; ReplayGain (player mode ints) |
| `set_eq_enabled(Bool)` / `is_eq_enabled()` | Toggle the graphic equalizer |
| `set_eq_preset(EqPreset)` | Apply one of 21 built-in presets (`Flat`, `BassBoost`, …) |
| `set_eq_band(band, cutoff_hz, q, gain_db)` / `set_eq_precut(db)` | Per-band EQ; EQ pre-cut headroom |
| `set_tone(...)` / `set_bass(db)` / `set_treble(db)` | Bass/treble tone controls |
| `set_bass_cutoff(hz)` / `set_treble_cutoff(hz)` | Tone cutoff frequencies |
| `set_crossfeed(CrossfeedMode, ...)` | Headphone crossfeed — `CrossfeedOff` / `Meier` / `CrossfeedCustom` |
| `set_surround(...)` | Surround effect |
| `set_channel_mode(ChannelMode)` / `set_stereo_width(pct)` | Channel mixing (`Stereo`, …); stereo width |
| `set_bass_enhancement(strength, precut)` / `set_fatigue_reduction(s)` | Bass enhancement; listening-fatigue reduction |
| `set_compressor(...)` / `set_dither(Bool)` / `set_pitch(ratio)` | Compressor; dithering; pitch |
| `dsp_settings()` | Read back the whole chain as a typed `DspSettings` record |
| `status()` | A `Status` snapshot |
| `resume` / `save_resume` / `clear_resume` / `load_resume(path)` | Queue + exact-position persistence |
| `import_m3u` / `load_m3u` / `export_m3u` / `m3u_read` / `m3u_write` | `.m3u` / `.m3u8` playlist I/O |

`DspSettings` is a typed record whose sections include `equalizer`, `tone`,
`surround`, `channel_mode`, `stereo_width`, `compressor`, `dither`, `pitch`,
`crossfeed`, `bass_enhancement`, and `fatigue_reduction`.

> [!TIP]
>
> These knobs map 1:1 to Rockbox's own Sound Settings. See the official
> [Rockbox manual](https://download.rockbox.org/daily/manual/rockbox-ipodvideo/rockbox-buildch6.html)
> for what each control does and its value ranges.

* `Dsp` and `Player` are opaque NIF resources, freed by the BEAM garbage
  collector — no explicit close.
* PCM crosses the boundary as a `BitArray`.

> [!WARNING]
>
> The DSP and player use **different mode integers**: `dsp.set_replaygain` →
> `0` track, `1` album, `2` shuffle, `3` off; `player.set_replaygain` → `0`
> off, `1` track, `2` album. See
> [the overview](https://rockboxzig.mintlify.site/bindings/overview.md#the-one-thing-to-know-two-replaygain-encodings).

### `rockbox/decoder`

Decode an audio file to interleaved-stereo **S16LE** PCM (a `BitArray`), one
chunk at a time, straight through the Rockbox codec engine — no output device
needed.

| Function | Purpose |
| - | - |
| `open(path) -> Decoder` | Open a decoder for a local audio file |
| `metadata(decoder) -> Metadata` | Tags + stream properties (same shape as `metadata.read`) |
| `next_chunk(decoder) -> Option(#(BitArray, Int))` | Next PCM buffer + sample rate; `None` at end of track |
| `seek_ms(decoder, ms)` / `elapsed_ms(decoder)` | Request a seek; read the codec's last-reported position |
| `finished(decoder) -> #(Bool, Int)` | `#(done, code)` — `code` `0` = clean end, negative = codec error (when `done`) |

**Decode a file to PCM**

```gleam theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import rockbox/decoder
import gleam/option.{None, Some}

let d = decoder.open("song.flac")
decoder.metadata(d).duration_ms      // 122324

// Pull chunks until the track ends.
fn drain(d) {
  case decoder.next_chunk(d) {
    Some(#(pcm, rate)) -> {
      // `pcm` is interleaved-stereo little-endian int16 at `rate` Hz.
      drain(d)
    }
    None -> Nil
  }
}
drain(d)

let #(done, code) = decoder.finished(d)   // #(True, 0) on a clean end
```

* `Decoder` is an opaque NIF resource, freed by the BEAM garbage collector —
  no explicit close.

> [!WARNING]
>
> Codec state is **process-wide — only one `Decoder` decodes at a time.**
> Opening a second decoder blocks until the first has been garbage-collected.

> [!NOTE]
>
> The Gleam and Elixir bindings share the exact same
> `rockbox_ffi_nif.{c,erl}` NIF, vendored into each project.


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