A modern, network-enabled music player platform built on Rockbox technology. rockboxd.tsiry-sandratraina.com
rust deno navidrome airplay libadwaita zig mpris snapcast mpd rockbox audio subsonic
README.md

rockbox-ffi (Python) #

PyPI Python FFI uv License

Python bindings for the Rockbox DSP, metadata, codecs, and playback engine, via cffi (ABI mode) over the prebuilt librockbox_ffi shared library.

📖 Sound settings reference — the equalizer, tone, crossfeed, compressor and other DSP controls mirror Rockbox's own. See the official Rockbox manual — Sound Settings.

Setup #

Build the shared library once (from the repo root):

cargo build --release -p rockbox-ffi

Then install the Python package with uv:

cd bindings/python
uv venv
uv pip install -e .
uv run python examples/smoke.py

The library is located automatically by walking up to target/release/librockbox_ffi.{dylib,so}. Override with the ROCKBOX_FFI_LIB environment variable.

Interactive console #

uv pip install -e '.[dev]'      # installs IPython
uv run python console.py

Drops into IPython with rb, metadata, Dsp, Player, the enums, and a FIXTURE sample track preloaded (falls back to the plain REPL without IPython):

metadata.read(str(FIXTURE))["title"]        # 'Speak'
p = Player(volume=0.6)
p.set_queue([str(FIXTURE)]); p.play()
p.status()["state"]                          # 'playing'

Usage #

import rockbox_ffi as rb
from rockbox_ffi import Dsp, Player, metadata
from rockbox_ffi.enums import DspReplayGainMode, ReplayGainMode, CrossfadeMode

# --- metadata ---------------------------------------------------------
meta = metadata.read("song.flac")
print(meta["artist"], "—", meta["title"], meta["duration_ms"], "ms")
print(metadata.probe("track.opus"))          # -> "Opus"

# --- DSP (interleaved stereo int16) -----------------------------------
with Dsp(44100) as dsp:
    dsp.eq_enable(True)
    dsp.set_eq_band(0, cutoff_hz=60, q=0.7, gain_db=3.0)
    dsp.set_replaygain(DspReplayGainMode.TRACK, noclip=True, preamp_db=0.0)
    dsp.set_replaygain_gains(track_gain_db=-6.02)   # halves amplitude
    processed = dsp.process(samples)                # array('h')

# --- codecs (decode a file to PCM, one chunk at a time) ---------------
from rockbox_ffi import Decoder

with Decoder("song.flac") as dec:
    print(dec.metadata()["title"])                  # tags from the open file
    for samples, sample_rate in dec.chunks():       # array('h'), Hz
        ...                                          # interleaved-stereo int16 PCM
    print(dec.finished())                           # (True, 0)  (0 = clean end)

# --- playback (needs an output device) --------------------------------
with Player(volume=0.8) as player:
    player.set_replaygain(ReplayGainMode.TRACK, preamp_db=0.0, prevent_clipping=True)
    player.set_crossfade(CrossfadeMode.ALWAYS)
    # Queue entries may be local files, http(s):// URLs to remote media,
    # or live-radio streams, or HLS / MPEG-DASH manifests (.m3u8/.mpd) — mix and match freely.
    player.set_queue(["a.flac", "https://example.com/b.mp3", "http://radio.example/stream", "https://cdn.example.com/live/main.m3u8"])
    player.play()
    print(player.status())     # {'state': 'playing', 'index': 0, ...}

API #

Module Contents
rockbox_ffi.metadata read(path) -> dict, probe(filename) -> str | None
rockbox_ffi.Dsp EQ / tone / surround / compressor / ReplayGain, process(samples)
rockbox_ffi.Decoder codec engine: metadata(), chunks() / next_chunk(), seek_ms(), finished()
rockbox_ffi.Player queue + transport + crossfade + ReplayGain, status() -> dict
rockbox_ffi.enums DspReplayGainMode, ReplayGainMode, CrossfadeMode, MixMode, …

Two ReplayGain encodings #

The DSP and player use different mode integers (a quirk of the C ABI):

  • Dsp.set_replaygain → DspReplayGainMode (TRACK=0, ALBUM=1, SHUFFLE=2, OFF=3)
  • Player.set_replaygain → ReplayGainMode (OFF=0, TRACK=1, ALBUM=2)

Use the named enums and you won't have to remember which is which.

Memory #

All heap allocations crossing the FFI boundary (JSON strings, sample buffers) are freed inside the wrappers — you never call a *_free yourself.