# GraphQL
Source: https://rockboxzig.mintlify.app/api-reference/graphql/overview
Single endpoint with queries, mutations and real-time subscriptions on port 6062.
The GraphQL server is the **best fit for UIs**: typed schema, batched
queries, and a `track:changed` / `status:changed` / `playlist:changed`
subscription stream over WebSocket.
* **Endpoint** — `http://localhost:6062/graphql`
* **WebSocket** — `ws://localhost:6062/graphql` (`graphql-ws` protocol)
* **GraphiQL** — `http://localhost:6062/graphiql`
The schema is generated from `crates/graphql/` and served by Juniper. All
client SDKs in [SDKs](/sdks/overview) wrap this transport.
## Quick examples
```graphql Now playing theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
query NowPlaying {
currentTrack {
title
artist
album
elapsed
length
}
playbackStatus { status }
}
```
```graphql Search theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
query Search($q: String!) {
search(term: $q) {
artists { name id }
albums { title artist year id }
tracks { title artist album id }
}
}
```
```graphql Play an album theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
mutation PlayAlbum($id: String!) {
playAlbum(albumId: $id, shuffle: false)
}
```
```graphql Subscribe to track changes theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
subscription OnTrack {
track {
title
artist
elapsed
length
}
}
```
## Subscriptions
Three subscriptions are exposed:
| Subscription | Payload | Fires when |
| ---------------- | ----------------------------- | ----------------------------------- |
| `track` | `Track` | The currently playing track changes |
| `playbackStatus` | `AudioStatus { status: Int }` | Stopped/playing/paused changes |
| `playlist` | `Playlist` | The live queue is mutated |
All three are pushed by the broker loop in `crates/server/src/lib.rs:start_broker()`.
## Connecting from a browser
```ts theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import { RockboxClient } from '@rockbox-zig/sdk';
const client = new RockboxClient();
client.connect();
client.on('track:changed', (t) => {
document.title = `${t.title} — ${t.artist}`;
});
```
For language-specific guides, see [SDKs](/sdks/overview).
## Schema introspection
GraphiQL ships pre-installed at
[http://localhost:6062/graphiql](http://localhost:6062/graphiql) — every
type, every field, every argument.
You can also dump the schema directly:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
npx graphql-cli get-schema -e http://localhost:6062/graphql > schema.graphql
```
# gRPC
Source: https://rockboxzig.mintlify.app/api-reference/grpc/overview
Strongly-typed gRPC and gRPC-Web on port 6061.
The gRPC server runs on **port 6061** and serves both native gRPC and
gRPC-Web (so browser clients work without a proxy).
* **Endpoint** — `localhost:6061`
* **Schema** — published on Buf:
[buf.build/tsiry/rockboxapis ↗](https://buf.build/tsiry/rockboxapis/docs/main:rockbox.v1alpha1)
* **Buf Studio playground** —
[open ↗](https://buf.build/studio/tsiry/rockboxapis/rockbox.v1alpha1.LibraryService/GetAlbums?target=http%3A%2F%2Flocalhost%3A6061\&selectedProtocol=grpc-web)
## Services
The proto definitions live under `proto/` (in
[buf.build/tsiry/rockboxapis](https://buf.build/tsiry/rockboxapis)) and
generate Rust bindings at `crates/rpc/`:
| Service | Purpose |
| ----------------- | ------------------------------------------- |
| `PlaybackService` | Transport, current/next track, seek, volume |
| `LibraryService` | Albums, artists, tracks, search |
| `PlaylistService` | Live queue + saved playlists |
| `SettingsService` | Read / update `global_settings` |
| `SoundService` | Volume + sound parameters |
| `BrowseService` | Filesystem browsing |
| `SystemService` | Version, scan, status |
## Generating clients
Use Buf to generate clients in any supported language:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
buf generate buf.build/tsiry/rockboxapis
```
Or pull the proto files directly:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
buf export buf.build/tsiry/rockboxapis -o proto/
```
## Quick test with grpcurl
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
grpcurl -plaintext localhost:6061 list
grpcurl -plaintext localhost:6061 rockbox.v1alpha1.LibraryService/GetAlbums
grpcurl -plaintext -d '{"id": ""}' \
localhost:6061 rockbox.v1alpha1.LibraryService/GetAlbum
```
## gRPC-Web from the browser
The same port speaks gRPC-Web — useful for browser apps that want a
strongly-typed binding without a translating reverse proxy. Use
[`@bufbuild/connect-web`](https://www.npmjs.com/package/@bufbuild/connect-web)
or the language equivalent.
# API overview
Source: https://rockboxzig.mintlify.app/api-reference/introduction
Four protocols, one source of truth — pick whichever fits your client.
`rockboxd` exposes the same in-process state through four independent
servers. They are all started by `start_servers()` in
`crates/server/src/lib.rs` and all share one set of mutexes around the
firmware, so a change made via gRPC is immediately visible over GraphQL,
HTTP and MPD.
| Protocol | Default port | Use it for |
| ------------- | ------------- | ----------------------------------------------------- |
| **HTTP REST** | 6063 | `curl`-able. Simple integrations, scripts, webhooks. |
| **GraphQL** | 6062 | Best fit for UIs. Subscriptions for real-time events. |
| **gRPC** | 6061 | Strongly-typed, multi-language. gRPC-Web supported. |
| **MPD** | 6600 | Existing MPD clients (`mpc`, `ncmpcpp`, MALP, …). |
| **Subsonic** | 4533 | Cassette, Symfonium, DSub, Ultrasonic, play:sub, … |
| **Jellyfin** | 8096 (opt-in) | Finamp, Findroid, Streamyfin, Amcfy Music, Symfonium. |
| **S3** | 9000 (opt-in) | Upload / delete tracks with `awscli`, `mc`, `rclone`. |
Open `http://localhost:6063` and explore.
GraphiQL at `http://localhost:6062/graphiql`.
Schema published on Buf.
Anything that speaks MPD on `localhost:6600`.
Native Jellyfin clients on `localhost:8096`.
Upload tracks via the S3 protocol on `localhost:9000`.
## Auto-generated REST pages
Every endpoint in the [HTTP REST API](/api-reference/rest/overview) has its
own page generated from the canonical OpenAPI spec
([`openapi.json`](https://github.com/tsirysndr/rockboxd/blob/master/crates/server/openapi.json)).
The spec is also served live at `http://localhost:6063/openapi.json` while
rockboxd is running.
## Authentication
All four servers are unauthenticated. They are intended for use on a
trusted LAN. If you expose Rockbox publicly, put it behind a reverse
proxy with TLS and HTTP basic auth.
## Pick a client SDK
We maintain six first-party SDKs. They wrap the GraphQL transport with
typed methods, real-time subscriptions and a plugin system.
# Jellyfin-compatible API
Source: https://rockboxzig.mintlify.app/api-reference/jellyfin/overview
Optional sidecar HTTP server that speaks the Jellyfin protocol so native Jellyfin clients can browse and stream your library.
Rockbox can act as a Jellyfin server for native Jellyfin clients on your
LAN. Internally it's a thin actix-web shim over the same `rockbox-library`
SQLite database the Subsonic API reads from — there's no separate scan,
no separate user store, and no extra daemon to manage.
## Enabling
Set `jellyfin_port` in `~/.config/rockbox.org/settings.toml`:
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
subsonic_username = "admin"
subsonic_password = "changeme"
jellyfin_port = 8096 # conventional Jellyfin port; pick any free port
```
Then `rockbox restart`. The server is **disabled** when `jellyfin_port`
is missing or when `subsonic_password` is empty — credentials are shared
with the Subsonic side.
## Discovery
When enabled, two discovery mechanisms run alongside the HTTP server:
* **mDNS**: `_jellyfin._tcp.local.` is advertised on the configured port,
with a `ID=…` TXT record matching the server's stable Jellyfin id.
* **UDP 7359**: the server binds the standard Jellyfin client-discovery
port and answers the literal probe `"Who is JellyfinServer?"` with a
JSON response containing the server's LAN URL.
Both run automatically; nothing to configure.
## Tested clients
| Client | Platform | Notes |
| ------------ | ----------- | ----------------------------------------------------------------------------------------------------- |
| Finamp | Android/iOS | Best-tested music client. Full browse + stream + scrobble. |
| Symfonium | Android | Paid. Works against the Jellyfin API. |
| Amcfy Music | Android | Triggers library refresh on `ScheduledTasks/Running`. |
| Findroid | Android | Video-focused; will show empty libraries because Rockbox is audio-only. |
| Streamyfin | Android | Polls `/Sessions`; works. |
| Official app | Android | Not supported — the official app is a WebView around the Jellyfin web UI, which Rockbox doesn't ship. |
For music, use **Finamp** (or Amcfy / Symfonium).
## Endpoint surface
The server implements enough of the [Jellyfin OpenAPI](https://api.jellyfin.org/)
to satisfy the native music clients above. All authenticated routes accept
the token via `X-Emby-Token` header, `Authorization: MediaBrowser Token="…"`,
or `?api_key=…` on streaming URLs. Query parameters work in both
camelCase (`?parentId=…`) and PascalCase (`?ParentId=…`); repeated keys
(`?includeItemTypes=Audio&includeItemTypes=MusicAlbum`) are concatenated.
| Group | Endpoints |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| System | `GET /System/Info`, `GET /System/Info/Public`, `GET /System/Endpoint` |
| Auth | `POST /Users/AuthenticateByName` (+ `authenticatebyname` lowercase alias) |
| Users | `GET /Users`, `GET /Users/Public`, `GET /Users/Me`, `GET /Users/{id}` |
| Views | `GET /Users/{id}/Views`, `GET /UserViews`, `GET /Library/MediaFolders` |
| Items | `GET /Items`, `GET /Users/{id}/Items`, `GET /Items/{id}`, `GET /Items/Latest`, `GET /Items/Suggestions`, `GET /Items/{id}/File`, `GET /Items/{id}/Download`, `GET /Items/{id}/Images/{kind}` |
| Audio | `GET /Audio/{id}/stream`, `/stream.{ext}`, `/universal` (Range-aware) |
| Artists | `GET /Artists`, `GET /Artists/AlbumArtists`, `GET /Artists/{name}` |
| Search | `GET /Search/Hints`, `GET /Items?searchTerm=…` |
| Playback | `GET POST /Items/{id}/PlaybackInfo` |
| Sessions | `GET /Sessions`, `POST /Sessions/Capabilities/Full`, `POST /Sessions/Playing{,/Progress,/Stopped}` |
| Tasks | `POST /ScheduledTasks/Running/{id}`, `POST /Library/Refresh` |
| Discovery | UDP `7359` probe responder, mDNS `_jellyfin._tcp.local.` advertisement |
Item IDs are deterministic dashed UUIDs derived from the native
`Artist/Album/Track` ids, and round-tripped via a `jf_guids` lookup
table so subsequent requests resolve back to the right row.
## Quick test
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
# Discover the server (or just `curl http://:8096/System/Info/Public`)
echo -n 'Who is JellyfinServer?' | nc -u -w1 -b 255.255.255.255 7359
# Authenticate
TOKEN=$(curl -s -X POST http://localhost:8096/Users/AuthenticateByName \
-H 'Content-Type: application/json' \
-H 'Authorization: MediaBrowser Client="curl", Device="d", DeviceId="i", Version="0.1"' \
-d '{"Username":"admin","Pw":"changeme"}' \
| jq -r .AccessToken)
# List libraries (returns the synthetic "Music" CollectionFolder)
curl -s -H "X-Emby-Token: $TOKEN" "http://localhost:8096/Users/me/Views" | jq
# List all artists
curl -s -H "X-Emby-Token: $TOKEN" \
"http://localhost:8096/Items?includeItemTypes=MusicArtist" | jq '.Items[].Name'
```
## What's not supported
* **Video** — Rockbox is an audio-only player. Video libraries and the
`/Videos/{id}/stream` endpoint family are not implemented.
* **Transcoding** — only direct play. `MediaSource.SupportsTranscoding`
is `false`; clients must support the container natively.
* **WebSocket notifications** (`/socket`) — clients fall back to polling
`/Sessions`, which is supported.
* **Multi-user** — there is a single synthetic user matching
`subsonic_username`. Token storage is real (persisted in
`jellyfin_tokens`), but every token belongs to the same user.
* **Playlists, lyrics, parental ratings, sync, live TV** — out of scope.
# MPD protocol
Source: https://rockboxzig.mintlify.app/api-reference/mpd/overview
Drop-in MPD server on port 6600 — works with every MPD client.
`rockboxd` runs a Music Player Daemon-compatible server on **port 6600**.
Any MPD client works out of the box.
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
mpc -h localhost -p 6600 status
mpc -h localhost -p 6600 update
mpc -h localhost -p 6600 search title "Money"
mpc -h localhost -p 6600 play
```
For the full client list and known limitations, see
[Clients › MPD](/clients/mpd). For the wire protocol reference itself,
see the [official MPD documentation ↗](https://mpd.readthedocs.io/en/stable/protocol.html).
## When to choose MPD over the others
* You already have an MPD client you like.
* You want a stable wire protocol with decades of community libraries.
* You want to drive Rockbox from a TUI like `ncmpcpp`.
For everything else — programmatic control, real-time UIs, custom apps —
you'll have a better time with [GraphQL](/api-reference/graphql/overview)
or one of the [SDKs](/sdks/overview).
# Get an album by id
Source: https://rockboxzig.mintlify.app/api-reference/rest/albums/get-an-album-by-id
/api-reference/openapi.json get /albums/{id}
# List all albums
Source: https://rockboxzig.mintlify.app/api-reference/rest/albums/list-all-albums
/api-reference/openapi.json get /albums
# List tracks in an album
Source: https://rockboxzig.mintlify.app/api-reference/rest/albums/list-tracks-in-an-album
/api-reference/openapi.json get /albums/{id}/tracks
# Get an artist by id
Source: https://rockboxzig.mintlify.app/api-reference/rest/artists/get-an-artist-by-id
/api-reference/openapi.json get /artists/{id}
# List albums by an artist
Source: https://rockboxzig.mintlify.app/api-reference/rest/artists/list-albums-by-an-artist
/api-reference/openapi.json get /artists/{id}/albums
# List all artists
Source: https://rockboxzig.mintlify.app/api-reference/rest/artists/list-all-artists
/api-reference/openapi.json get /artists
# List tracks by an artist
Source: https://rockboxzig.mintlify.app/api-reference/rest/artists/list-tracks-by-an-artist
/api-reference/openapi.json get /artists/{id}/tracks
# Connect to a paired Bluetooth device (Linux only)
Source: https://rockboxzig.mintlify.app/api-reference/rest/bluetooth/connect-to-a-paired-bluetooth-device-linux-only
/api-reference/openapi.json put /bluetooth/devices/{addr}/connect
# Disconnect a Bluetooth device (Linux only)
Source: https://rockboxzig.mintlify.app/api-reference/rest/bluetooth/disconnect-a-bluetooth-device-linux-only
/api-reference/openapi.json put /bluetooth/devices/{addr}/disconnect
# List paired Bluetooth devices (Linux only)
Source: https://rockboxzig.mintlify.app/api-reference/rest/bluetooth/list-paired-bluetooth-devices-linux-only
/api-reference/openapi.json get /bluetooth/devices
# Scan for Bluetooth devices (Linux only)
Source: https://rockboxzig.mintlify.app/api-reference/rest/bluetooth/scan-for-bluetooth-devices-linux-only
/api-reference/openapi.json post /bluetooth/scan
# Browse the filesystem under music_dir
Source: https://rockboxzig.mintlify.app/api-reference/rest/browse/browse-the-filesystem-under-music_dir
/api-reference/openapi.json get /browse/tree-entries
# Disconnect the active device and revert to builtin
Source: https://rockboxzig.mintlify.app/api-reference/rest/devices/disconnect-the-active-device-and-revert-to-builtin
/api-reference/openapi.json put /devices/{id}/disconnect
# Get a device by id (use 'current' for the active sink)
Source: https://rockboxzig.mintlify.app/api-reference/rest/devices/get-a-device-by-id-use-current-for-the-active-sink
/api-reference/openapi.json get /devices/{id}
# List all known output devices (discovered + virtual)
Source: https://rockboxzig.mintlify.app/api-reference/rest/devices/list-all-known-output-devices-discovered-+-virtual
/api-reference/openapi.json get /devices
# Switch the active sink to this device
Source: https://rockboxzig.mintlify.app/api-reference/rest/devices/switch-the-active-sink-to-this-device
/api-reference/openapi.json put /devices/{id}/connect
# HTTP REST
Source: https://rockboxzig.mintlify.app/api-reference/rest/overview
JSON over HTTP on port 6063. The endpoints used internally by the web UI and SDK clients.
The REST server runs on **port 6063** by default (override with
`ROCKBOX_TCP_PORT`). Every endpoint is JSON in / JSON out, except where
noted (some commands return plain-text status codes).
The full schema is published as
[OpenAPI 3.1](/api-reference/openapi.json) and rendered as one page per
endpoint in the sidebar — explore by tag or jump to a specific operation.
## Quick smoke test
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
curl -s http://localhost:6063/version
curl -s http://localhost:6063/player/status
curl -s http://localhost:6063/playlists/amount
```
## Common operations
```sh Now playing theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
curl -s http://localhost:6063/player/current-track | jq .title,.artist
```
```sh Search theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
curl -s 'http://localhost:6063/search?q=daft+punk' | jq '.tracks[].title'
```
```sh Play an album theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
ALBUM_ID=$(curl -s http://localhost:6063/albums | jq -r '.[0].id')
TRACKS=$(curl -s "http://localhost:6063/albums/$ALBUM_ID/tracks" | jq '[.[].path]')
curl -X POST -H 'Content-Type: application/json' \
-d "{\"name\":\"album\",\"tracks\":$TRACKS}" \
http://localhost:6063/playlists
curl -X PUT 'http://localhost:6063/playlists/start?start_index=0'
```
```sh Volume up theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
curl -X PUT -H 'Content-Type: application/json' \
-d '{"steps":3}' \
http://localhost:6063/player/volume
```
```sh Switch to a discovered Chromecast theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
ID=$(curl -s http://localhost:6063/devices | jq -r '.[] | select(.is_cast_device) | .id' | head -1)
curl -X PUT "http://localhost:6063/devices/$ID/connect"
```
## Notable behaviours
* **Responses sometimes return plain text.** A few mutations return a
status integer or insertion index in the body as text rather than JSON
(`/playlists`, `/playlists/{id}/tracks`, `/scan-library`). The OpenAPI
spec marks these explicitly.
* **Saved playlist mutations return `204`** with no body.
* **Bluetooth routes only exist on Linux.** They are conditionally
registered at compile time.
* **The HTTP server runs on its own thread** so actix's worker pool is
not pinned to the Rockbox cooperative scheduler. See
[Architecture › Overview](/architecture/overview) for the lifecycle.
## Server details
* Bind address — `0.0.0.0:$ROCKBOX_TCP_PORT` (default `6063`).
* CORS — permissive (`actix_cors::Cors::permissive()`) so browser-based
clients can hit it without preflight pain.
* The OpenAPI document is also served live at
`http://localhost:6063/openapi.json`.
# Adjust volume by N firmware-defined steps
Source: https://rockboxzig.mintlify.app/api-reference/rest/player/adjust-volume-by-n-firmware-defined-steps
/api-reference/openapi.json put /player/volume
# Flush PCM buffers and reload the current queue
Source: https://rockboxzig.mintlify.app/api-reference/rest/player/flush-pcm-buffers-and-reload-the-current-queue
/api-reference/openapi.json put /player/flush-and-reload-tracks
# Get current volume range and value
Source: https://rockboxzig.mintlify.app/api-reference/rest/player/get-current-volume-range-and-value
/api-reference/openapi.json get /player/volume
# Get the current byte offset in the playing file
Source: https://rockboxzig.mintlify.app/api-reference/rest/player/get-the-current-byte-offset-in-the-playing-file
/api-reference/openapi.json get /player/file-position
# Get the current output device
Source: https://rockboxzig.mintlify.app/api-reference/rest/player/get-the-current-output-device
/api-reference/openapi.json get /player
# Get the currently playing track
Source: https://rockboxzig.mintlify.app/api-reference/rest/player/get-the-currently-playing-track
/api-reference/openapi.json get /player/current-track
# Get the next queued track
Source: https://rockboxzig.mintlify.app/api-reference/rest/player/get-the-next-queued-track
/api-reference/openapi.json get /player/next-track
# Get the playback status
Source: https://rockboxzig.mintlify.app/api-reference/rest/player/get-the-playback-status
/api-reference/openapi.json get /player/status
# Hard-stop playback
Source: https://rockboxzig.mintlify.app/api-reference/rest/player/hard-stop-playback
/api-reference/openapi.json put /player/stop
# Load tracks into an external player (Cast/AirPlay)
Source: https://rockboxzig.mintlify.app/api-reference/rest/player/load-tracks-into-an-external-player-castairplay
/api-reference/openapi.json put /player/load
# Pause playback
Source: https://rockboxzig.mintlify.app/api-reference/rest/player/pause-playback
/api-reference/openapi.json put /player/pause
# Resume playback
Source: https://rockboxzig.mintlify.app/api-reference/rest/player/resume-playback
/api-reference/openapi.json put /player/resume
# Seek to an absolute position (ms)
Source: https://rockboxzig.mintlify.app/api-reference/rest/player/seek-to-an-absolute-position-ms
/api-reference/openapi.json put /player/ff-rewind
# Skip to the next track
Source: https://rockboxzig.mintlify.app/api-reference/rest/player/skip-to-the-next-track
/api-reference/openapi.json put /player/next
# Skip to the previous track
Source: https://rockboxzig.mintlify.app/api-reference/rest/player/skip-to-the-previous-track
/api-reference/openapi.json put /player/previous
# Start playback at an offset
Source: https://rockboxzig.mintlify.app/api-reference/rest/player/start-playback-at-an-offset
/api-reference/openapi.json put /player/play
# Get the live queue and its metadata
Source: https://rockboxzig.mintlify.app/api-reference/rest/playlist-queue/get-the-live-queue-and-its-metadata
/api-reference/openapi.json get /playlists/{id}
# Insert tracks into the live queue
Source: https://rockboxzig.mintlify.app/api-reference/rest/playlist-queue/insert-tracks-into-the-live-queue
/api-reference/openapi.json post /playlists/{id}/tracks
# List tracks currently in the queue
Source: https://rockboxzig.mintlify.app/api-reference/rest/playlist-queue/list-tracks-currently-in-the-queue
/api-reference/openapi.json get /playlists/{id}/tracks
# Number of tracks in the live queue
Source: https://rockboxzig.mintlify.app/api-reference/rest/playlist-queue/number-of-tracks-in-the-live-queue
/api-reference/openapi.json get /playlists/amount
# Remove tracks from the live queue
Source: https://rockboxzig.mintlify.app/api-reference/rest/playlist-queue/remove-tracks-from-the-live-queue
/api-reference/openapi.json delete /playlists/{id}/tracks
# Replace the live queue with a new playlist
Source: https://rockboxzig.mintlify.app/api-reference/rest/playlist-queue/replace-the-live-queue-with-a-new-playlist
/api-reference/openapi.json post /playlists
# Resume the saved control file playlist
Source: https://rockboxzig.mintlify.app/api-reference/rest/playlist-queue/resume-the-saved-control-file-playlist
/api-reference/openapi.json put /playlists/resume
# Resume the saved track at its previous offset
Source: https://rockboxzig.mintlify.app/api-reference/rest/playlist-queue/resume-the-saved-track-at-its-previous-offset
/api-reference/openapi.json put /playlists/resume-track
# Shuffle the live queue
Source: https://rockboxzig.mintlify.app/api-reference/rest/playlist-queue/shuffle-the-live-queue
/api-reference/openapi.json put /playlists/shuffle
# Start playback at a queue index
Source: https://rockboxzig.mintlify.app/api-reference/rest/playlist-queue/start-playback-at-a-queue-index
/api-reference/openapi.json put /playlists/start
# Add tracks to a saved playlist
Source: https://rockboxzig.mintlify.app/api-reference/rest/saved-playlists/add-tracks-to-a-saved-playlist
/api-reference/openapi.json post /saved-playlists/{id}/tracks
# Create a playlist folder
Source: https://rockboxzig.mintlify.app/api-reference/rest/saved-playlists/create-a-playlist-folder
/api-reference/openapi.json post /saved-playlists/folders
# Create a saved playlist
Source: https://rockboxzig.mintlify.app/api-reference/rest/saved-playlists/create-a-saved-playlist
/api-reference/openapi.json post /saved-playlists
# Delete a playlist folder
Source: https://rockboxzig.mintlify.app/api-reference/rest/saved-playlists/delete-a-playlist-folder
/api-reference/openapi.json delete /saved-playlists/folders/{id}
# Delete a saved playlist
Source: https://rockboxzig.mintlify.app/api-reference/rest/saved-playlists/delete-a-saved-playlist
/api-reference/openapi.json delete /saved-playlists/{id}
# Get a saved playlist
Source: https://rockboxzig.mintlify.app/api-reference/rest/saved-playlists/get-a-saved-playlist
/api-reference/openapi.json get /saved-playlists/{id}
# List playlist folders
Source: https://rockboxzig.mintlify.app/api-reference/rest/saved-playlists/list-playlist-folders
/api-reference/openapi.json get /saved-playlists/folders
# List saved playlists, optionally filtered by folder
Source: https://rockboxzig.mintlify.app/api-reference/rest/saved-playlists/list-saved-playlists-optionally-filtered-by-folder
/api-reference/openapi.json get /saved-playlists
# List track ids in a saved playlist
Source: https://rockboxzig.mintlify.app/api-reference/rest/saved-playlists/list-track-ids-in-a-saved-playlist
/api-reference/openapi.json get /saved-playlists/{id}/track-ids
# List tracks in a saved playlist
Source: https://rockboxzig.mintlify.app/api-reference/rest/saved-playlists/list-tracks-in-a-saved-playlist
/api-reference/openapi.json get /saved-playlists/{id}/tracks
# Load a saved playlist into the queue and start playing
Source: https://rockboxzig.mintlify.app/api-reference/rest/saved-playlists/load-a-saved-playlist-into-the-queue-and-start-playing
/api-reference/openapi.json post /saved-playlists/{id}/play
# Remove a track from a saved playlist
Source: https://rockboxzig.mintlify.app/api-reference/rest/saved-playlists/remove-a-track-from-a-saved-playlist
/api-reference/openapi.json delete /saved-playlists/{id}/tracks/{track_id}
# Update a saved playlist's metadata
Source: https://rockboxzig.mintlify.app/api-reference/rest/saved-playlists/update-a-saved-playlists-metadata
/api-reference/openapi.json put /saved-playlists/{id}
# Full-text search powered by Typesense
Source: https://rockboxzig.mintlify.app/api-reference/rest/search/full-text-search-powered-by-typesense
/api-reference/openapi.json get /search
# Apply a partial settings update and persist to settings.toml
Source: https://rockboxzig.mintlify.app/api-reference/rest/settings/apply-a-partial-settings-update-and-persist-to-settingstoml
/api-reference/openapi.json put /settings
# Get the global settings (in-memory snapshot)
Source: https://rockboxzig.mintlify.app/api-reference/rest/settings/get-the-global-settings-in-memory-snapshot
/api-reference/openapi.json get /settings
# Create a smart playlist with a rule criteria
Source: https://rockboxzig.mintlify.app/api-reference/rest/smart-playlists/create-a-smart-playlist-with-a-rule-criteria
/api-reference/openapi.json post /smart-playlists
# Delete a smart playlist
Source: https://rockboxzig.mintlify.app/api-reference/rest/smart-playlists/delete-a-smart-playlist
/api-reference/openapi.json delete /smart-playlists/{id}
# Get a smart playlist
Source: https://rockboxzig.mintlify.app/api-reference/rest/smart-playlists/get-a-smart-playlist
/api-reference/openapi.json get /smart-playlists/{id}
# List smart playlists
Source: https://rockboxzig.mintlify.app/api-reference/rest/smart-playlists/list-smart-playlists
/api-reference/openapi.json get /smart-playlists
# Resolve a smart playlist and start playing its tracks
Source: https://rockboxzig.mintlify.app/api-reference/rest/smart-playlists/resolve-a-smart-playlist-and-start-playing-its-tracks
/api-reference/openapi.json post /smart-playlists/{id}/play
# Resolve a smart playlist to its current matching tracks
Source: https://rockboxzig.mintlify.app/api-reference/rest/smart-playlists/resolve-a-smart-playlist-to-its-current-matching-tracks
/api-reference/openapi.json get /smart-playlists/{id}/tracks
# Update a smart playlist
Source: https://rockboxzig.mintlify.app/api-reference/rest/smart-playlists/update-a-smart-playlist
/api-reference/openapi.json put /smart-playlists/{id}
# Get global runtime status
Source: https://rockboxzig.mintlify.app/api-reference/rest/system/get-global-runtime-status
/api-reference/openapi.json get /status
# Get the running rockboxd version
Source: https://rockboxzig.mintlify.app/api-reference/rest/system/get-the-running-rockboxd-version
/api-reference/openapi.json get /version
# Get this OpenAPI document
Source: https://rockboxzig.mintlify.app/api-reference/rest/system/get-this-openapi-document
/api-reference/openapi.json get /openapi.json
# Trigger a library scan and (optionally) rebuild the search index
Source: https://rockboxzig.mintlify.app/api-reference/rest/system/trigger-a-library-scan-and-optionally-rebuild-the-search-index
/api-reference/openapi.json put /scan-library
# Get listening stats for a track
Source: https://rockboxzig.mintlify.app/api-reference/rest/track-stats/get-listening-stats-for-a-track
/api-reference/openapi.json get /track-stats/{id}
# Record a 'played' event for a track
Source: https://rockboxzig.mintlify.app/api-reference/rest/track-stats/record-a-played-event-for-a-track
/api-reference/openapi.json post /track-stats/{id}/played
# Record a 'skipped' event for a track
Source: https://rockboxzig.mintlify.app/api-reference/rest/track-stats/record-a-skipped-event-for-a-track
/api-reference/openapi.json post /track-stats/{id}/skipped
# Get a track by id
Source: https://rockboxzig.mintlify.app/api-reference/rest/tracks/get-a-track-by-id
/api-reference/openapi.json get /tracks/{id}
# List all tracks in the library
Source: https://rockboxzig.mintlify.app/api-reference/rest/tracks/list-all-tracks-in-the-library
/api-reference/openapi.json get /tracks
# Persist metadata for an HTTP stream URL
Source: https://rockboxzig.mintlify.app/api-reference/rest/tracks/persist-metadata-for-an-http-stream-url
/api-reference/openapi.json put /tracks/stream-metadata
Used by the player when a remote URL is loaded — saves title/artist/album/duration so the URL can be resolved from the DB later.
# S3-compatible API
Source: https://rockboxzig.mintlify.app/api-reference/s3/overview
Upload, list, and delete audio files in music_dir from any AWS S3 client.
`rockboxd` exposes an **S3-compatible HTTP API** on **port 9000**, so
any tool that speaks S3 — `awscli`, MinIO Client (`mc`), `rclone`,
the AWS SDKs, S3-mounted backup tools — can push audio files into
your library and remove them again. The library DB stays in sync
automatically through the filesystem watcher: every PUT triggers an
add, every DELETE triggers a remove. You don't need to call a separate
"rescan" endpoint.
## Enable it
In `~/.config/rockbox.org/settings.toml`:
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
s3_enabled = true
s3_port = 9000 # optional, default 9000
s3_host = "0.0.0.0" # optional, default "0.0.0.0"
s3_access_key = "your-access-key"
s3_secret_key = "your-secret-key"
```
The region is fixed to `us-east-1` and the bucket is fixed to
`music` — these are not configurable. Clients **must** sign with
region `us-east-1` and address objects as `s3://music/`.
If `s3_enabled` is omitted/false, or either credential is empty,
the server doesn't start (you'll see a `s3: disabled` debug log line
on startup).
## Supported operations
| Operation | Method | Path |
| --------------- | -------- | ------------------------ |
| `ListBuckets` | `GET` | `/` |
| `ListObjectsV2` | `GET` | `/music?list-type=2&...` |
| `PutObject` | `PUT` | `/music/{key}` |
| `GetObject` | `GET` | `/music/{key}` |
| `HeadObject` | `HEAD` | `/music/{key}` |
| `DeleteObject` | `DELETE` | `/music/{key}` |
`ListObjectsV2` supports `prefix`, `delimiter`, and `max-keys` (capped
at 1000). `GetObject` honours `If-Match` / `If-None-Match` against the
returned `ETag`.
## Use it with awscli
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export AWS_DEFAULT_REGION="us-east-1"
# Required for awscli v2.23+. The new default-on integrity headers send
# a STREAMING-AWS4-HMAC-SHA256-PAYLOAD body that rockbox's S3 server does
# not implement — these env vars fall back to single-shot SigV4.
export AWS_REQUEST_CHECKSUM_CALCULATION=when_required
export AWS_RESPONSE_CHECKSUM_VALIDATION=when_required
alias rbs3='aws --endpoint-url http://localhost:9000'
# Upload a single file
rbs3 s3 cp song.flac s3://music/song.flac
# Upload a directory tree (audio files only)
rbs3 s3 sync ~/Staging s3://music/ \
--exclude "*" --include "*.flac" --include "*.mp3" --include "*.m4a"
# List
rbs3 s3 ls s3://music/
rbs3 s3 ls s3://music/Albums/Vespertine/
rbs3 s3api list-objects-v2 --bucket music --prefix "Albums/" --max-keys 100
# Metadata only
rbs3 s3api head-object --bucket music --key "song.flac"
# Download
rbs3 s3 cp s3://music/song.flac ./song.flac
# Delete
rbs3 s3 rm s3://music/song.flac
rbs3 s3 rm s3://music/Albums/Old/ --recursive
```
If you can't set those env vars (older awscli, locked-down CI), use
the raw `s3api put-object` subcommand instead of `s3 cp` — it always
signs the full body in one shot:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rbs3 s3api put-object \
--bucket music \
--key "Albums/Vespertine/01 Hidden Place.flac" \
--body "/Users/me/staging/01 Hidden Place.flac"
```
## Use it with rclone
`rclone` defaults to `UNSIGNED-PAYLOAD` for non-AWS endpoints, so no
extra knobs are needed:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rclone config create rbs3 s3 \
provider=Other \
access_key_id="$AWS_ACCESS_KEY_ID" \
secret_access_key="$AWS_SECRET_ACCESS_KEY" \
endpoint=http://localhost:9000 \
region=us-east-1
rclone copy ~/Staging rbs3:music --include "*.{flac,mp3,m4a,ogg,opus}"
rclone ls rbs3:music
rclone delete rbs3:music/old-stuff
```
## Use it with MinIO Client (mc)
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
mc alias set rbs3 http://localhost:9000 \
"$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY"
mc cp song.flac rbs3/music/song.flac
mc ls rbs3/music
mc rm rbs3/music/song.flac
```
## Allowed file extensions
Uploads are restricted to recognised audio extensions:
```
mp3, ogg, flac, m4a, aac, mp4, alac, wav, wv, mpc,
aiff, aif, ac3, opus, spx, sid, ape, wma
```
A `PUT` with any other extension returns
`400 Bad Request — InvalidRequest`. The list mirrors the library
watcher's `AUDIO_EXTENSIONS`, so anything the scanner would index is
also accepted on upload.
## Limitations
* **Single-shot uploads only** — no multipart upload, no
`STREAMING-AWS4-HMAC-SHA256-PAYLOAD`. Per-PUT cap is **2 GiB**.
* **One fixed bucket** (`music`). Bucket CRUD isn't supported.
* **Header-form SigV4 only** — no presigned URLs, no query-string auth.
* **No ACLs, policies, versioning, lifecycle, tagging, or encryption
headers.** They're parsed-and-ignored, not rejected, so existing
clients won't crash.
* The watcher is the only path that mutates the library DB. Adding a
parallel "tell the DB about this S3 op" code path would race with
the watcher and double-insert.
## How sync works
```
PUT /music/Albums/X.flac
→ write to $music_dir/Albums/X.flac
↓
notify::Event::Create
↓
library/src/watcher.rs::handle_event
↓
save_audio_metadata() → SQLite INSERT
```
```
DELETE /music/Albums/X.flac
→ unlink $music_dir/Albums/X.flac
↓
notify::Event::Remove
↓
repo::track::delete_by_path() → SQLite DELETE (cascades)
```
This means new uploads appear in MPD, Subsonic, GraphQL, gRPC, and
the web UI within milliseconds without a manual rescan.
## Troubleshooting
| Symptom | Cause / fix |
| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SignatureDoesNotMatch` on PUT | awscli is sending chunked SigV4. Set `AWS_REQUEST_CHECKSUM_CALCULATION=when_required` and `AWS_RESPONSE_CHECKSUM_VALIDATION=when_required`, or use `s3api put-object`. |
| `NotImplemented: STREAMING-AWS4-HMAC-SHA256-PAYLOAD` | Same root cause as above. |
| `InvalidRequest: Only audio file extensions are accepted` | Key doesn't end in a recognised audio extension. |
| `NoSuchBucket` | Bucket name is fixed to `music`. `s3://anything-else/` won't work. |
| `RequestTimeTooSkewed` | Client clock is more than 15 minutes off from the server clock. Fix NTP. |
| `AccessDenied: Authorization header missing` | Client didn't sign the request — usually a misconfigured profile or missing `AWS_*` env vars. |
| Server doesn't start | `s3_enabled = false`, or `s3_access_key` / `s3_secret_key` is empty. Check `tracing` logs. |
## When to choose S3 over the alternatives
* You already have an S3-aware backup pipeline (`rclone sync`,
`restic`, S3 mounting tools) and want it to write to your Rockbox
library.
* You want to use the AWS SDKs from a language that doesn't have a
Rockbox SDK yet.
* You want simple, well-documented multi-language tooling that handles
retries, multipart-on-large-files (in clients that ask for it),
and concurrent uploads out of the box.
For programmatic playback control — playing, queueing, searching —
use [GraphQL](/api-reference/graphql/overview),
[gRPC](/api-reference/grpc/overview), or
[REST](/api-reference/rest/overview) instead.
# Build system
Source: https://rockboxzig.mintlify.app/architecture/build
Make → Cargo → Zig. The three-step pipeline that produces rockboxd.
Rockbox Daemon is built by three tools in series:
1. **Make** — compiles the Rockbox C firmware into static libraries.
2. **Cargo** — compiles the Rust crates into static libraries (`crate-type = ["staticlib"]`).
3. **Zig** — links everything (plus CPAL) into a single executable.
## Dependencies
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
sudo apt-get install \
libasound2-dev libfreetype6-dev libdbus-1-dev libunwind-dev \
zip protobuf-compiler cmake
```
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
sudo dnf install \
alsa-lib-devel freetype-devel libunwind-devel \
zip protobuf-compiler cmake
```
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
brew install tsirysndr/tap/rockbox
```
This installs the pre-built binary. If you want to **build from source**,
install the toolchain dependencies instead:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
brew install freetype cmake protobuf
```
You'll also need:
* **Zig** ≥ 0.16 — [ziglang.org/download](https://ziglang.org/download/)
* **Rust stable** — `rustup update stable`
* **Deno** — for the web UI build
## Full build
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
# 1. Clone with submodules
git clone https://github.com/tsirysndr/rockboxd.git
cd rockboxd
git submodule update --init --recursive
# 2. Build the web UI (embedded into the binary)
cd webui/rockbox
deno install
deno run build
cd ../..
# 3. Configure and build the C firmware (one-time setup)
mkdir -p build-lib && cd build-lib
../tools/configure --target=sdlapp --type=N \
--lcdwidth=320 --lcdheight=240 --prefix=/usr/local
cp ../autoconf/autoconf.h .
make lib
cd ..
# 4. Build Rust crates
cargo build --release -p rockbox-cli -p rockbox-server
# 5. Link everything with Zig
cd zig && zig build
```
The binary lands at `zig/zig-out/bin/rockboxd`.
## Iterating on changes
Zig only re-links when the static libraries are newer than the binary.
After editing C, run `make lib` first. After editing Rust, run
`cargo build --release` first.
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
# C change
cd build-lib && make lib && cd .. && cd zig && zig build
# Rust change
cargo build --release -p rockbox-cli -p rockbox-server && cd zig && zig build
```
**Stale binary pitfall.** If behaviour doesn't match the source, check
mtimes:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
ls -la zig/zig-out/bin/rockboxd \
build-lib/libfirmware.a \
target/release/librockbox_cli.a
```
If `rockboxd` is newer than every `.a` file, Zig considered the link
up-to-date and your change wasn't picked up.
## Verifying symbols
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
nm zig/zig-out/bin/rockboxd | grep pcm_airplay
nm zig/zig-out/bin/rockboxd | grep pcm_squeezelite
ar t target/release/librockbox_cli.a | grep airplay
ar t target/release/librockbox_cli.a | grep slim
```
## Headless build (CPAL / no SDL)
The recommended build path for desktop use is the **headless** target, which
uses CPAL for audio instead of SDL. A convenience script handles all three
steps:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
bash scripts/build-headless.sh
```
This configures and builds the firmware against `build-headless/`, compiles
the Rust crates with the `cpal` feature, and links the result with Zig.
## Embeddable library (`librockboxd.a`)
`zig build lib` produces `zig/zig-out/lib/librockboxd.a` — a fat archive that
desktop GUIs (GPUI, macOS Swift, Qt, …) can link against to boot the Rockbox
daemon in-process. The public C header is `include/rockboxd.h`.
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
# After the headless firmware + Rust embed crate:
cd zig && zig build lib
# → zig/zig-out/lib/librockboxd.a
```
## Don't re-run `tools/configure`
`build-lib/` is pre-configured for the `sdlapp` target; `build-headless/` is
pre-configured for the headless target. Re-running `tools/configure`
regenerates the Makefile and overwrites local edits. If you really need to
reconfigure, do it knowingly and review the resulting diff.
# Architecture
Source: https://rockboxzig.mintlify.app/architecture/overview
How Rockbox C, Rust and Zig fit together inside one rockboxd binary.
Web UI · GTK · GPUI · macOS (Swift) · TUI · REPL · MPD · MPRIS
gRPC :6061 · GraphQL :6062 · REST :6063 · MPD :6600
playback · library · settings · search · playlists · airplay · slim · chromecast · upnp · netstream · cpal-sink · bluetooth · discovery
audio engine · DSP · codecs · tag database
cpal · fifo · airplay · squeezelite · chromecast · snapcast\_tcp · upnp
The entire system ships as **one binary**, `rockboxd`, produced by Zig's
linker. There's no separate "rockbox-server" service, no per-feature
sidecar, no IPC.
## What links into the binary
| Artifact | Built by | Notes |
| ------------------------------------ | -------- | ------------------------------------------------------------------ |
| `build-lib/libfirmware.a` | Make | Rockbox C audio engine + DSP |
| `build-lib/librockbox.a` | Make | App layer (playlist, database, plugins) |
| Codec libraries (`librbcodec.a`, …) | Make | rbcodec + fixedpoint + skin parser |
| `target/release/librockbox_cli.a` | Cargo | CLI entry point + Rust output sinks (incl. `cpal-sink`) |
| `target/release/librockbox_server.a` | Cargo | gRPC, GraphQL, HTTP (Actix-web), MPD servers |
| `zig/zig-out/lib/librockboxd.a` | Zig | Fat static archive for embedding in desktop GUIs (`zig build lib`) |
The Zig build script (`zig/build.zig`) glues them together, ensuring force-included symbols stay in the staticlib through the link.
## Repository layout
```text theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
firmware/ Rockbox C firmware (audio engine, codecs, DSP)
target/hosted/
headless/ Headless host target — POSIX threads, CPAL audio, no SDL
sdl/ SDL host target (legacy; still used for the SDL rockboxd build)
apps/ Rockbox application layer (playlist, database, plugins)
lib/ Codec libraries (rbcodec, fixedpoint, skin_parser, tlsf)
build-lib/ Out-of-tree Make build directory (generated; do not edit)
build-headless/ Headless Make build directory (no SDL)
include/ Public C header (rockboxd.h) for the embeddable library
crates/ Rust workspace
airplay/ ALAC encoder + RAOP/RTP sender
slim/ Slim Protocol + HTTP broadcast (squeezelite multi-room)
cli/ Compiled to librockbox_cli.a (staticlib)
embed/ Embeddable desktop library — daemon boot + C ABI rb_* exports
server/ gRPC / HTTP (Actix-web) server
settings/ load_settings() — reads settings.toml, applies sinks
sys/ FFI bindings to the C firmware
library/ SQLite library management
cpal-sink/ CPAL audio sink (CoreAudio / WASAPI / ALSA)
fts5/ SQLite FTS5 search backend (feature-flag alternative to Typesense)
typesense/ Typesense client for search
netstream/ HTTP streaming (Range-request fd multiplexing)
chromecast/ Chromecast output
rpc/ gRPC definitions / generated code
graphql/ GraphQL schema and resolvers
mpd/ MPD protocol server
mpris/ MPRIS D-Bus integration
playlists/ Playlist management
tracklist/ Tracklist management
bluetooth/ Bluetooth pairing and control
discovery/ mDNS / LAN device discovery
upnp/ UPnP/DLNA support
rocksky/ Rocksky cloud sync / remote agent
types/ Shared Rust types
traits/ Shared Rust traits
zig/ Zig build script, main.zig (executable), lib.zig (embedded lib)
sdk/ Client SDKs (TypeScript, Python, Ruby, Elixir, Clojure, Gleam)
webui/rockbox/ React web UI — Tailwind CSS, built into the binary
gpui/ Desktop client (GPUI / Rust) — embeds daemon via librockboxd.a
macos/ Native macOS client (Swift / Xcode) — embeds daemon via librockboxd.a
gtk/ GTK4 desktop client
```
## Cross-cutting concerns
### macOS CPAL audio
CPAL uses CoreAudio natively on macOS. The built-in sink implementation lives
in `firmware/target/hosted/headless/pcm-cpal.c` (C side) and `crates/cpal-sink/`
(Rust side, ring buffer + resampler). No extra initialisation is required
beyond what CPAL performs at stream open time.
### SIGTERM handling
`crates/cli/src/lib.rs` overrides SIGTERM/SIGINT to kill the typesense
child process and `_exit(0)`. The default Rockbox handler in
`system-hosted.c` would otherwise loop forever waiting for a quit
event from the audio subsystem.
### Typesense subprocess
Typesense is spawned with `Stdio::piped()` and its stdout/stderr lines are
forwarded to `tracing` in background threads — this keeps the PCM stdout
stream clean when running in `fifo_path = "-"` mode.
### HTTP streaming for cloud sources
HTTP file descriptors are encoded as values `≤ -1000` (the
`STREAM_HTTP_FD_BASE` constant). `stream_open/read/lseek/close` in
`crates/netstream/` dispatch between HTTP and POSIX based on fd value, so
the rest of the firmware doesn't know it's reading from the network.
# PCM sinks
Source: https://rockboxzig.mintlify.app/architecture/pcm-sinks
How Rockbox's audio output abstraction works, and how to add a new sink.
The audio output abstraction lives in `firmware/export/pcm_sink.h`. Each
sink implements a `pcm_sink_ops` vtable:
```c theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
struct pcm_sink_ops {
void (*init)(void);
void (*postinit)(void);
void (*set_freq)(int hz);
void (*lock)(void);
void (*unlock)(void);
void (*play)(const void *data, size_t bytes);
void (*stop)(void);
};
```
## Built-in sinks
| Enum constant | Value | Implementation |
| ----------------------- | ----- | -------------------------------------------- |
| `PCM_SINK_BUILTIN` | 0 | `firmware/target/hosted/headless/pcm-cpal.c` |
| `PCM_SINK_FIFO` | 1 | `firmware/target/hosted/pcm-fifo.c` |
| `PCM_SINK_AIRPLAY` | 2 | `firmware/target/hosted/pcm-airplay.c` |
| `PCM_SINK_SQUEEZELITE` | 3 | `firmware/target/hosted/pcm-squeezelite.c` |
| `PCM_SINK_UPNP` | 4 | `firmware/target/hosted/pcm-upnp.c` |
| `PCM_SINK_CHROMECAST` | 5 | `firmware/target/hosted/pcm-chromecast.c` |
| `PCM_SINK_SNAPCAST_TCP` | 6 | `firmware/target/hosted/pcm-snapcast-tcp.c` |
| `PCM_SINK_CPAL` | 7 | `firmware/target/hosted/headless/pcm-cpal.c` |
Selection at startup happens in `crates/settings/src/lib.rs:load_settings()`,
which reads `audio_output` and calls `pcm::switch_sink()`. Rust-side
constants and helpers live in `crates/sys/src/sound/pcm.rs`.
## FIFO sink details (Snapcast)
* Pre-creates the named FIFO with `O_RDWR|O_NONBLOCK` in
`pcm_fifo_set_path()` then clears `O_NONBLOCK`. Holding a write reference
prevents readers from seeing premature EOF between tracks.
* `sink_dma_stop()` does **not** close the fd; it stays open across track
transitions.
* Startup order matters: rockboxd must start before snapserver.
## AirPlay sink details
* `pcm_airplay_connect()` is called once per `sink_dma_start()` and is
idempotent if already connected.
* The `rockbox-airplay` rlib is force-included via
`use rockbox_airplay::_link_airplay as _` in `crates/cli/src/lib.rs`.
Without that shim the linker would garbage-collect the symbols.
## Squeezelite sink details
* The DMA loop in `pcm-squeezelite.c` paces output to real time using
`CLOCK_MONOTONIC`.
* **Use `int64_t` for the nanosecond diff** — unsigned subtraction wraps
catastrophically when `tv_nsec` rolls over. This is a real bug we hit; if
you touch this code, keep it signed.
* The `rockbox-slim` rlib is force-included via
`use rockbox_slim::_link_slim as _`.
## CPAL sink details (headless)
The built-in CPAL sink (`audio_output = "builtin"`) is the default audio
backend for all platforms. It lives in
`crates/cpal-sink/` on the Rust side and `firmware/target/hosted/headless/pcm-cpal.c`
on the C side.
* **Data flow:** the firmware DMA thread calls `pcm_cpal_push(data, size)`,
which writes into a 512 KB S16LE ring buffer. The CPAL audio callback
drains the ring at the device's native rate, resampling with
linear interpolation when `in_rate ≠ out_rate` and converting i16→f32
when the device requires it.
* **Pre-warm:** on non-macOS platforms a background thread opens the ALSA /
PipeWire stream during `postinit` so it is ready before the first track
plays. `OPEN_STREAM_MTX` serialises concurrent `open_stream()` calls.
* **Volume:** per-channel multipliers are stored as f32 bits in atomics so
the CPAL callback can read them lock-free.
* **`set_freq` receives an index, not Hz** — translate via
`hw_freq_sampr[idx]` before passing to CPAL.
## Adding a new sink
1. Create `firmware/target/hosted/pcm-.c` — model on `pcm-fifo.c`.
2. Add `PCM_SINK_` to the enum in `firmware/export/pcm_sink.h`.
3. Register `&_pcm_sink` in the `sinks[]` array in `firmware/pcm.c`.
4. Add `target/hosted/pcm-.c` inside the `#if PLATFORM_HOSTED` block
in `firmware/SOURCES`.
5. Add a Rust constant `PCM_SINK_: i32` in
`crates/sys/src/sound/pcm.rs`.
6. Add a `set__*` wrapper if configuration is needed.
7. Handle the new sink in `crates/settings/src/lib.rs:load_settings()`.
8. If it has a Rust implementation in a new crate: add a
`_link_()` dummy fn and reference it from `crates/cli/src/lib.rs`
to force inclusion in the staticlib.
## Logging from a sink
Always use `tracing` from Rust. Never `eprintln!`/`println!` — they bypass
the structured log filter and pollute stdout (which breaks FIFO mode).
```rust theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
tracing::info!("airplay: session established to {}", host);
tracing::warn!("airplay: dropped frame, network slow");
tracing::error!("airplay: handshake failed: {err}");
```
Control verbosity with `RUST_LOG`, e.g.
`RUST_LOG=rockbox_airplay=debug,info rockboxd`.
# AirPlay
Source: https://rockboxzig.mintlify.app/audio-output/airplay
RAOP streaming to one or many AirPlay receivers — Apple TV, HomePod, Airport Express, shairport-sync.
Rockbox includes a pure-Rust RAOP (AirPlay 1) implementation. ALAC frames go
out over RTP/UDP; RTSP handles session setup. RTCP NTP sync packets are sent
roughly every 44 frames so receivers stay in lockstep.
## Single receiver
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
music_dir = "/path/to/Music"
audio_output = "airplay"
airplay_host = "192.168.1.50" # IP of the AirPlay receiver
airplay_port = 5000 # optional, default 5000
```
## Multi-room
Fan-out to N receivers simultaneously:
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
music_dir = "/path/to/Music"
audio_output = "airplay"
[[airplay_receivers]]
host = "192.168.1.50" # living room
port = 5000 # optional, default 5000
[[airplay_receivers]]
host = "192.168.1.51" # bedroom
# port defaults to 5000
[[airplay_receivers]]
host = "192.168.1.52" # kitchen
```
All receivers share the same `initial_rtptime`, so RTP-level synchronisation
is within one frame (\~8 ms) across the LAN.
## Compatible receivers
* Apple TV (any generation supporting AirPlay 1)
* HomePod / HomePod mini
* AirPort Express
* [shairport-sync](https://github.com/mikebrady/shairport-sync) — software
AirPlay receiver for Linux, macOS, FreeBSD, OpenWrt
* Most third-party AirPlay-1 speakers
AirPlay 2 is not implemented.
## Auto-discovery
Discovered receivers appear in the web UI device picker — click to connect
without editing the config. The mDNS service type is `_raop._tcp.local.`.
## Debugging
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
RUST_LOG=rockbox_airplay=debug rockboxd
```
Common log lines:
```
DEBUG rockbox_airplay::rtsp ANNOUNCE → 192.168.1.50:5000
DEBUG rockbox_airplay::rtsp SETUP → control=53000 timing=53001 server=53002
DEBUG rockbox_airplay::rtsp RECORD → 200 OK
DEBUG rockbox_airplay::rtp sent 352 frames seq=12345
```
## Limitations
* **No password / pairing.** AirPlay 1 receivers that require a PIN are not
supported.
* **No volume sync.** Volume changes apply only at the rockboxd side; the
receiver's hardware volume is not adjusted.
* **No AirPlay 2.** The pairing/encryption stack required for AirPlay 2 is
not implemented.
## Architecture
The RAOP stack lives in `crates/airplay/`:
| File | Responsibility |
| --------- | ------------------------------------------------------------ |
| `alac.rs` | ALAC escape/verbatim encoder — 352 stereo S16LE → 1411 bytes |
| `rtp.rs` | RTP/UDP packet sender + RTCP NTP sync |
| `rtsp.rs` | Synchronous RTSP client: ANNOUNCE → SETUP → RECORD |
The C-side sink is `firmware/target/hosted/pcm-airplay.c`.
# Built-in (CPAL)
Source: https://rockboxzig.mintlify.app/audio-output/built-in
The default. CPAL audio to your OS default device.
The built-in sink uses **CPAL** to play audio through your operating system's
default audio device. This is the default and needs no setup beyond installing
Rockbox.
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
music_dir = "/path/to/Music"
audio_output = "builtin"
```
## Selecting the output device
CPAL plays through whichever device the OS reports as default. To change it,
use your platform's audio settings:
* **macOS** — System Settings › Sound › Output
* **Linux (PipeWire / PulseAudio)** — `pavucontrol` or your DE's sound applet
* **Windows** — Sound settings › Output
There is no per-application device picker built into Rockbox for the CPAL sink.
If you need multi-device routing on Linux, point Rockbox at a PulseAudio
*null sink* and route from there.
## macOS-specific notes
CPAL uses CoreAudio natively on macOS — no additional setup is required.
If you ever see "no audio output" on macOS only, verify that the correct
output device is selected in System Settings › Sound › Output.
## Format
Output is **S16LE stereo at 44 100 Hz**. Higher-resolution sources are
dithered down by the rbcodec DSP pipeline before they reach CPAL. To change
the output sample rate, set `play_frequency` in `settings.toml` (`auto`,
`44100`, `48000`, `88200`, `96000`).
## Switching to another sink
Edit `audio_output` in `settings.toml` and `rockbox restart`, or call:
```graphql theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
mutation { connectDevice(id: "") }
```
…with a device discovered via mDNS — see
[Audio output › Overview](/audio-output/overview).
# Chromecast
Source: https://rockboxzig.mintlify.app/audio-output/chromecast
Google Cast support over WAV-over-HTTP plus the Cast control channel.
Rockbox streams audio to any Google Cast-compatible device — Google Home,
Chromecast Audio, Chromecast with Google TV, Nest Hub, or third-party
receivers — using two channels at once:
| Channel | Port | Purpose |
| ------------- | -------- | ---------------------------------------------------- |
| Cast protocol | TCP 8009 | TLS + Protobuf — playback control, queue, metadata |
| WAV over HTTP | TCP 7881 | Live `audio/wav` stream with finite `Content-Length` |
The finite content length is what lets the Chromecast show a progress bar
and auto-advance at track boundaries.
## Configuration
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
music_dir = "/path/to/Music"
audio_output = "chromecast"
chromecast_host = "192.168.1.60" # LAN IP of the target device
chromecast_port = 8009 # optional, default 8009 (Cast protocol)
chromecast_http_port = 7881 # optional, default 7881 (WAV stream)
```
## Auto-discovery
Devices on the LAN are discovered via mDNS (`_googlecast._tcp.local.`) and
appear in the web UI and desktop app device picker — clicking starts a Cast
session on demand without `audio_output = "chromecast"` in the config.
## Track metadata
Title, artist, album, duration, and album art are pushed to the device on
every track change so the "Now playing" card stays accurate.
**Network requirement**: the Chromecast must be able to reach port 7881 on
the host running rockboxd. If rockboxd is in a VM or container, forward
that port to the host (or run with `--network host`).
## Picking a device from the API
```graphql theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
query DiscoveredCastDevices {
devices {
id
name
ip
port
isCastDevice
}
}
mutation Cast {
connectDevice(id: "chromecast-living-room")
}
```
…or with the [TypeScript SDK](/sdks/typescript):
```ts theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
const devices = await client.devices.list();
const cast = devices.find((d) => d.isCastDevice);
if (cast) await client.devices.connect(cast.id);
```
## Architecture
Implementation lives in `crates/chromecast/`. See
[`crates/chromecast/README.md`](https://github.com/tsirysndr/rockboxd/blob/master/crates/chromecast/README.md)
for the protocol-level details.
# HLS + MPEG-DASH (CMAF)
Source: https://rockboxzig.mintlify.app/audio-output/hls
Live AAC-LC stream in fragmented MP4 — plays directly in any browser.
The CMAF sink encodes live audio as AAC-LC in a fragmented MP4 container and
serves it as both **HLS** and **MPEG-DASH** from the same in-memory segment
ring buffer. Any HLS- or DASH-capable client can play the stream — including
every modern browser — without installing extra software.
This is the default audio output for the [Docker image](/quickstart): the web
UI's `` element attaches to the HLS stream automatically as soon as the
active output is set to `cmaf` / `hls` / `dash`.
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
music_dir = "/path/to/Music"
audio_output = "cmaf" # also accepts "hls" or "dash"
cmaf_http_port = 7882 # optional, default 7882
cmaf_bitrate = 128000 # optional, AAC-LC bitrate in bps
```
## Endpoints
Once `rockboxd` is running with `audio_output = "cmaf"`, three endpoints are
served on `cmaf_http_port` (default `7882`):
| Path | Content |
| -------------------- | ------------------------------------------------------ |
| `/hls/master.m3u8` | HLS master playlist (one variant — AAC-LC 128 kbps) |
| `/hls/audio.m3u8` | HLS media playlist (sliding window of recent segments) |
| `/dash/manifest.mpd` | MPEG-DASH manifest (live profile, same segments) |
| `/init.mp4` | fMP4 initialisation segment |
| `/seg/{n}.m4s` | fMP4 media segments (\~2 s each) |
## Play from anywhere
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
# Browser — open the web UI; attaches automatically.
open http://localhost:6062
# CLI players
ffplay http://localhost:7882/hls/master.m3u8
vlc http://localhost:7882/hls/master.m3u8
mpv http://localhost:7882/hls/master.m3u8
# Or stream the DASH manifest
ffplay http://localhost:7882/dash/manifest.mpd
```
## How it works
```
Rockbox PCM (S16LE / 44.1 kHz / stereo)
→ fdk-aac (AAC-LC, 1024-sample frames)
→ fMP4 segmenter (86 frames ≈ 2 s per segment)
→ SegmentStore (sliding ring, last ~6 segments in memory)
→ HTTP server (/init.mp4, /seg/{n}.m4s, /hls/*.m3u8, /dash/manifest.mpd)
```
* A 2-second sliding window of segments is retained; clients always join at the
live edge.
* Between tracks the encoder emits silence segments at wall-clock cadence so
clients don't see an empty playlist (which `hls.js` would treat as a fatal
`levelEmptyError`).
* The bitrate is clamped to **32 000 – 320 000 bps**. Outside that range it is
silently clamped to the nearest endpoint.
## Mirror to disk for an external HTTP server
To serve the same artefacts from nginx, Caddy, or a CDN origin, point the sink
at a directory:
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
audio_output = "cmaf"
cmaf_segment_dir = "/var/www/rockbox-cmaf"
```
`init.mp4`, `seg/N.m4s`, `hls/master.m3u8`, `hls/audio.m3u8`, and
`dash/manifest.mpd` are all written there alongside the in-memory ring. The
in-memory ring is always authoritative; disk I/O is best-effort and never
blocks encoding.
Example nginx vhost:
```nginx theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
server {
listen 80;
location / {
root /var/www/rockbox-cmaf;
add_header Cache-Control "no-cache";
add_header Access-Control-Allow-Origin "*";
}
}
```
## Licensing note
`libfdk-aac` ships under the *Software License for The Fraunhofer FDK AAC Codec
Library for Android* — open source but **not GPL-compatible**. Redistributing
a binary that combines this sink with the GPLv2 Rockbox firmware is a license
conflict for redistribution. Personal / non-distributed use is the intended
scope.
## Switching to another sink
Edit `audio_output` in `settings.toml` and restart, or call:
```graphql theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
mutation { connectDevice(id: "") }
```
…or pick another output from the web UI's device picker. See
[Audio output › Overview](/audio-output/overview).
# Audio output overview
Source: https://rockboxzig.mintlify.app/audio-output/overview
Pick a PCM sink. Switch any time over the API.
Rockbox writes decoded PCM into a single active **sink** at a time. The sink
is selected by the `audio_output` key in `settings.toml`, but it can also be
changed at runtime — e.g. by clicking a discovered Chromecast in the device
picker.
| Sink | `audio_output` | Use it for |
| ----------------- | -------------- | -------------------------------------------------- |
| Built-in CPAL | `builtin` | Local speakers / headphones |
| HLS + DASH (CMAF) | `cmaf` | Browser playback, `ffplay`/`vlc`/`mpv`, CDN origin |
| FIFO / pipe | `fifo` | Snapcast (`pipe://`), `ffplay`, any pipe consumer |
| Snapcast TCP | `snapcast_tcp` | Snapserver `tcp://` source with auto-discovery |
| AirPlay (RAOP) | `airplay` | Apple TV, HomePod, Airport Express, shairport-sync |
| Squeezelite | `squeezelite` | Logitech-style multi-room with squeezelite clients |
| Chromecast | `chromecast` | Google Home, Chromecast Audio, Nest Hub |
| UPnP / DLNA | `upnp` | Kodi, VLC, BubbleUPnP, any UPnP MediaRenderer |
## Stream format
Every sink receives the same byte stream: **S16LE stereo PCM at 44 100 Hz**.
The Rockbox DSP pipeline dithers and downmixes higher-bit-depth decoder
output before it reaches the sink.
## Fan-out
A few sinks support sending the same stream to multiple receivers
simultaneously:
* **AirPlay** — list multiple `[[airplay_receivers]]` and they all share the
same `initial_rtptime`, keeping playback within \~8 ms across the LAN.
* **Squeezelite** — any number of squeezelite clients can attach to one
rockboxd; a `sync` packet aligns their clocks once per second.
* **Snapcast** — fan-out is handled by snapserver, not Rockbox.
For Chromecast, AirPlay and UPnP the LAN is also scanned with mDNS / SSDP at
startup; discovered devices show up in the web UI and desktop picker without
any config-file edits.
## Switching sinks at runtime
Through the GraphQL API:
```graphql theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
mutation Connect {
connectDevice(id: "chromecast-living-room")
}
```
…or through the desktop / web device picker. The active sink is stopped, the
new sink's `init` and `start` hooks run, and audio resumes within a frame.
## Architecture
Each sink implements the `pcm_sink_ops` vtable in
`firmware/export/pcm_sink.h`. The full list of sinks with file paths and
implementation notes lives in [Architecture › PCM sinks](/architecture/pcm-sinks).
# Snapcast
Source: https://rockboxzig.mintlify.app/audio-output/snapcast
Synchronised multi-room playback through Snapserver — TCP or FIFO.
Rockbox can feed [Snapcast](https://github.com/badaix/snapcast) two ways.
Both write raw **S16LE stereo PCM at 44 100 Hz** to snapserver; pick the
transport that fits your setup.
| | TCP sink | FIFO sink |
| ------------------------- | ----------------------------- | -------------------------- |
| Filesystem entry required | No | Yes (`/tmp/snapfifo`) |
| Snapserver source type | `tcp://` | `pipe://` |
| Startup order | Snapserver first | Rockbox first |
| Auto-reconnect | Yes (next play call) | n/a — FIFO stays open |
| Auto-discovery in UI | Yes (`_snapcast._tcp.local.`) | No — static virtual device |
| stdout pipe support | No | Yes (`fifo_path = "-"`) |
**Use TCP** for auto-discovery, multiple snapservers, or no filesystem
dependency. **Use FIFO** for stdout piping or the traditional pipe model.
## TCP (recommended)
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
music_dir = "/path/to/Music"
audio_output = "snapcast_tcp"
snapcast_tcp_host = "192.168.1.x" # IP of the snapserver host
snapcast_tcp_port = 4953 # default snapserver TCP source port
```
Snapserver:
```ini theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
# /etc/snapserver.conf (or /usr/local/etc/snapserver.conf on macOS)
[stream]
source = tcp://0.0.0.0:4953?name=default&sampleformat=44100:16:2
```
**Auto-discovery**: rockboxd scans `_snapcast._tcp.local.` at startup;
discovered servers appear in the web UI device picker. Click to connect —
no config file editing required.
**Startup order**: start `snapserver` first so it is already listening when
rockboxd begins playback. If the connection drops (e.g. snapserver
restarts), it is re-established automatically on the next play call.
## FIFO / pipe
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
music_dir = "/path/to/Music"
audio_output = "fifo"
fifo_path = "/tmp/snapfifo" # named FIFO for snapserver; "-" = stdout
```
Snapserver:
```ini theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
[stream]
source = pipe:///tmp/snapfifo?name=default&sampleformat=44100:16:2
```
**Startup order matters**: start `rockboxd` before `snapserver`. Rockbox
holds a permanent write reference on the FIFO so snapserver never sees a
premature EOF between tracks. If snapserver opens the FIFO first it may get
EOF and stop reading.
### stdout mode
`fifo_path = "-"` writes raw PCM to stdout — useful for piping into any
consumer:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rockboxd | ffplay -f s16le -ar 44100 -ac 2 -
```
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rockboxd | sox -t raw -r 44100 -e signed -b 16 -c 2 - -d
```
## macOS quirk
Snapserver v0.35.0 on macOS ignores the `-s` sample-format CLI flag. Use the
config file at `/usr/local/etc/snapserver.conf` instead:
```ini theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
[stream]
source = pipe:///tmp/snapfifo?name=default&sampleformat=44100:16:2
```
## Verifying it works
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
# In one terminal
snapserver --logging.filter "*:debug"
# In another
rockboxd
```
You should see `Stream: 'default' connected` in the snapserver logs within
a second of starting playback. From a snapclient host:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
snapclient -h
```
# Squeezelite
Source: https://rockboxzig.mintlify.app/audio-output/squeezelite
Slim Protocol multi-room — Rockbox impersonates Logitech Media Server.
Rockbox runs a minimal **Logitech Media Server** that any number of
[squeezelite](https://github.com/ralph-irving/squeezelite) clients can attach
to. A Slim Protocol TCP server accepts connections; an HTTP PCM broadcast
server serves the actual audio stream.
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
music_dir = "/path/to/Music"
audio_output = "squeezelite"
squeezelite_port = 3483 # Slim Protocol TCP port (default 3483)
squeezelite_http_port = 9999 # HTTP PCM broadcast port (default 9999)
```
## Connecting clients
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
squeezelite -s localhost -n "Living Room"
squeezelite -s localhost -n "Kitchen"
squeezelite -s localhost -n "Bedroom"
```
Each client gets an independent `BroadcastReceiver` cursor into the shared
buffer, so adding or removing clients never blocks the writer or interrupts
playback in other rooms.
### Selecting a specific output device
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
squeezelite -s localhost -l # list available devices
squeezelite -s localhost -o "" # system default
squeezelite -s localhost -o "Built-in Output" # specific device
```
## How it stays in sync
Rockbox sends a `sync` packet to every client once per second so they all
align to the same playback clock. The DMA loop in
`firmware/target/hosted/pcm-squeezelite.c` paces output to real time using
`CLOCK_MONOTONIC`, which keeps the broadcast buffer from drifting against
the wall clock.
## Buffer behaviour
| Property | Value |
| --------------------- | ------------------------------------------- |
| Capacity | 4 MB |
| Eviction | Oldest-first when full |
| Per-client cursor | Yes — each receiver tracks its own position |
| Lagging client policy | Skip forward (does not block the writer) |
A slow client never stalls the others.
## Slim Protocol details
Internal — useful if you're debugging or building your own client.
* **Framing**:
* client → server: `opcode[4] + u32_t length BE + payload`
* server → client: `u16_t length BE + opcode[4] + payload`
(length excludes the 2-byte length field itself)
* **STRM `'s'`** points clients at the HTTP port (defaults to 9999).
* **STMt heartbeat** from the client must be answered with `audg`, otherwise
squeezelite's 36-second watchdog will tear down the session.
* **ASCII-encoded PCM fields** in the STRM packet — squeezelite subtracts
`'0'` from `pcm_sample_size`, `pcm_sample_rate`, `pcm_channels` and
`pcm_endianness`. Correct values: `'1'` (16-bit), `'3'` (44 100 Hz),
`'2'` (stereo), `'1'` (little-endian).
## Debugging
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
RUST_LOG=rockbox_slim=debug rockboxd
```
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
squeezelite -s localhost -d slimproto=debug -d output=info
```
# UPnP / DLNA
Source: https://rockboxzig.mintlify.app/audio-output/upnp
Three independent UPnP/DLNA modes — sink, media server and renderer.
Rockbox has three UPnP/DLNA modes that can be enabled independently. They
combine freely: e.g. expose your library to BubbleUPnP **and** stream live
to Kodi at the same time.
## Mode 1 — PCM sink (push to a renderer)
Rockbox encodes live PCM as a continuous WAV-over-HTTP stream and tells a
UPnP MediaRenderer to play it via AVTransport SOAP.
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
music_dir = "/path/to/Music"
audio_output = "upnp"
upnp_renderer_url = "http://192.168.1.x:7777/AVTransport/control"
upnp_http_port = 7879 # WAV broadcast HTTP port (default 7879)
```
Track metadata (title, artist, album, album art, duration) is sent as
DIDL-Lite XML in `SetAVTransportURI` and refreshed on every track change.
**Finding `upnp_renderer_url`**: start `rockboxd` with `RUST_LOG=info` —
it scans the LAN at startup and logs
`upnp scan: found renderer "" av=http://...` for every renderer
found.
## Mode 2 — Media Server (let others browse your library)
Exposes your music library as a UPnP ContentDirectory. BubbleUPnP, Kodi,
VLC, foobar2000 and the like can browse artists / albums / tracks and pull
audio directly from Rockbox.
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
upnp_server_enabled = true
upnp_server_port = 7878 # default
upnp_friendly_name = "Rockbox" # name shown in apps
```
## Mode 3 — MediaRenderer (let others push to you)
Rockbox registers as a `MediaRenderer:1`. Any control point can push a URI
and control playback remotely. Incoming DIDL-Lite metadata is parsed and
displayed in the UI.
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
upnp_renderer_enabled = true
upnp_renderer_port = 7880 # default
upnp_friendly_name = "Rockbox"
```
## All UPnP keys
| Key | Default | Description |
| ----------------------- | ----------- | --------------------------------------------- |
| `audio_output = "upnp"` | — | Enable the PCM → WAV streaming sink |
| `upnp_renderer_url` | — | AVTransport controlURL of the target renderer |
| `upnp_http_port` | `7879` | WAV broadcast HTTP port |
| `upnp_server_enabled` | `false` | Start the ContentDirectory media server |
| `upnp_server_port` | `7878` | Media server HTTP port |
| `upnp_renderer_enabled` | `false` | Start the MediaRenderer endpoint |
| `upnp_renderer_port` | `7880` | MediaRenderer HTTP port |
| `upnp_friendly_name` | `"Rockbox"` | Display name shown to control points |
## Typical setups
Find Kodi's AVTransport URL with `RUST_LOG=info rockboxd`, then:
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
audio_output = "upnp"
upnp_renderer_url = "http://192.168.1.42:7777/AVTransport/control"
```
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
audio_output = "builtin"
upnp_server_enabled = true
upnp_friendly_name = "Living-room music"
```
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
audio_output = "builtin"
upnp_renderer_enabled = true
upnp_friendly_name = "Rockbox"
```
Now BubbleUPnP can pick "Rockbox" as the playback target.
# Crossfade
Source: https://rockboxzig.mintlify.app/audio-settings/crossfade
Overlap the end of one track with the beginning of the next.
Crossfade rolls the outgoing track into the incoming one over a configurable
window. Off by default; enable it via `crossfade` in `settings.toml`.
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
crossfade = 5 # see Mode table below
fade_on_stop = false
fade_in_delay = 2
fade_in_duration = 7
fade_out_delay = 4
fade_out_duration = 0
fade_out_mixmode = 2
```
## Mode
| Value | Mode | When the crossfade fires |
| ----- | --------------------- | ---------------------------- |
| 0 | Off | Never |
| 1 | Auto track change | At end-of-track only |
| 2 | Manual skip | When you press next/previous |
| 3 | Shuffle | While shuffle is on |
| 4 | Shuffle + manual skip | Both |
| 5 | Always | Every transition |
## Timings
| Setting | Storage | Range | Default | Description |
| ----------------- | ----------------------------- | --------------- | --------- | ---------------------------------------------- |
| Fade-in delay | `crossfade_fade_in_delay` | 0..7 s | 0 s | Silence before the fade-in begins |
| Fade-out delay | `crossfade_fade_out_delay` | 0..7 s | 0 s | Silence before the fade-out begins |
| Fade-in duration | `crossfade_fade_in_duration` | 0..15 s | 2 s | Length of the fade-in ramp |
| Fade-out duration | `crossfade_fade_out_duration` | 0..15 s | 2 s | Length of the fade-out ramp |
| Fade-out mode | `crossfade_fade_out_mixmode` | crossfade / mix | crossfade | Whether the outgoing track fades or mixes flat |
## Fade-on-stop
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
fade_on_stop = true
```
When set, pressing **stop** ramps audio out instead of cutting it.
## Notes
* Crossfade is part of the rbcodec DSP pipeline and is applied before the
sink, so it works equally well with AirPlay, Snapcast and the rest.
* Gapless playback overrides crossfade for tracks that share an album
identity — gapless seams are more important than smooth fades.
# DSP effects
Source: https://rockboxzig.mintlify.app/audio-settings/dsp
Crossfeed, Haas surround, PBE, AFR, compressor and dithering.
The rbcodec DSP pipeline runs between the codec output and the active sink.
All of these are bypassable; turn anything off by zeroing its enable flag.
## Crossfeed
Mixes a delayed and filtered portion of one channel into the other to
simulate the spatial cues you'd get from loudspeakers. Particularly useful
on headphones with hard-panned mixes (older rock/jazz).
| Setting | Storage | Range | Default |
| -------------- | -------------------------- | -------------------- | ------- |
| Type | `crossfeed` | off / meier / custom | off |
| Direct gain | `crossfeed_direct_gain` | −60..0 dB (step 5) | −15 dB |
| Cross gain | `crossfeed_cross_gain` | −120..−30 dB | −60 dB |
| HF attenuation | `crossfeed_hf_attenuation` | −240..−60 dB | −160 dB |
| HF cutoff | `crossfeed_hf_cutoff` | 500..2000 Hz | 700 Hz |
Crossfeed can cause output distortion if its settings result in a combined
level that is too high.
## Haas surround
Adds an adjustable delay between channels to widen the stereo image. Four
auxiliary controls move the perceived stage back toward the centre.
| Setting | Storage | Range | Default |
| ------------ | ------------------ | --------------------------- | ------- |
| Enable | `surround_enabled` | 0 / 5 / 8 / 10 / 15 / 30 ms | 0 (off) |
| Balance | `surround_balance` | 0..99 % | 35 % |
| f(x1) HF cut | `surround_fx1` | 600..8000 Hz (step 200) | 3400 Hz |
| f(x2) LF cut | `surround_fx2` | 40..400 Hz (step 40) | 320 Hz |
| Side only | `surround_method2` | bool | false |
| Dry/wet mix | `surround_mix` | 0..100 % | 50 % |
## Perceptual Bass Enhancement (PBE)
Group-delay correction plus a biophonic EQ to boost low-end perception.
| Setting | Storage | Range | Default |
| -------- | ------------ | --------------------- | --------- |
| Strength | `pbe` | 0..100 % (step 25) | 0 % (off) |
| Precut | `pbe_precut` | −4.5..0 dB (step 0.1) | −2.5 dB |
## Auditory Fatigue Reduction (AFR)
Reduces energy in frequency bands the human ear is most sensitive to —
helpful for long listening sessions.
| Setting | Values | Default |
| ---------- | ------------------------------ | ------- |
| AFR enable | off / weak / moderate / strong | off |
## Compressor
Reduces dynamic range so quiet passages stay audible without loud passages
clipping.
| Setting | Storage | Values / range | Default |
| ------------ | --------------- | --------------------------------------------------- | ------- |
| Threshold | `.threshold` | off / −3 / −6 / −9 / −12 / −15 / −18 / −21 / −24 dB | off |
| Makeup gain | `.makeup_gain` | off / auto | auto |
| Ratio | `.ratio` | 2:1 / 4:1 / 6:1 / 10:1 / limit | 2:1 |
| Knee | `.knee` | hard / soft | soft |
| Attack time | `.attack_time` | 0..30 ms (step 5) | 5 ms |
| Release time | `.release_time` | 100..1000 ms (step 100) | 500 ms |
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
[compressor_settings]
threshold = -24
makeup_gain = 0
ratio = 4
knee = 1
release_time = 300
attack_time = 5
```
## Dithering
Most decoders work at higher than 16-bit precision; dithering adds a small
shaped noise signal before truncation so the residual is uniform rather
than signal-correlated. Most useful with classical music and other
high-dynamic-range material.
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
dithering_enabled = true
```
* Algorithm: high-pass triangular distribution (HPTPDF)
* Noise shaper: third order, biased above \~10 kHz
# Equalizer
Source: https://rockboxzig.mintlify.app/audio-settings/equalizer
10-band parametric EQ — independent gain, frequency and Q per band.
Rockbox uses a **parametric** EQ rather than the more common graphic EQ.
Each band has independent control of gain, centre frequency and bandwidth
(Q), which buys you the same shaping power with fewer bands than a graphic
EQ would need.
Using more bands than necessary wastes CPU and adds rounding noise. Disable
or zero out bands you aren't using.
## Bands
| Band | Filter type | Default centre / cutoff | Q recommendation |
| ---- | -------------- | ------------------------------------------- | ------------------------------------------------- |
| 0 | Low-shelf | 32 Hz | 0.7 (higher Q adds an unwanted boost near cutoff) |
| 1–8 | Peaking (bell) | 64 / 125 / 250 / 500 / 1k / 2k / 4k / 8k Hz | Higher Q = narrower band |
| 9 | High-shelf | 16 000 Hz | 0.7 |
Per band:
* **Cutoff / centre frequency** — Hz
* **Gain** — dB; positive boosts, negative cuts
* **Q** — bandwidth (peak filters); 0.7 for shelves
## Top-level settings
| Setting | Storage | Type / range | Description |
| --------- | ------------ | ------------ | ----------------------------------------------------------------- |
| Enable EQ | `eq_enabled` | bool | Master on/off |
| Precut | `eq_precut` | 0..24 dB | Negative gain applied before EQ to prevent clipping when boosting |
Applied via `dsp_set_eq_precut()` and `dsp_set_eq_coefs()` in
`lib/rbcodec/dsp/eq.h`.
## TOML
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
eq_enabled = true
eq_precut = 3 # 3 dB headroom before EQ
[[eq_band_settings]] # band 0 (low shelf)
cutoff = 32
q = 7 # Q × 10 — Rockbox stores fixed-point
gain = 30 # dB × 10
[[eq_band_settings]] # band 1
cutoff = 64
q = 7
gain = 0
# ... bands 2-9
```
`q` and `gain` are stored as fixed-point (×10) in `global_settings`. The
GraphQL API accepts plain decimals — see
[Settings TOML reference](/reference/settings-toml).
## Configuring via the API
```graphql GraphQL theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
mutation EnableEqAndShapeBass {
saveSettings(input: {
eqEnabled: true
eqPrecut: -3
eqBandSettings: [
{ cutoff: 60, q: 7, gain: 3 }
{ cutoff: 200, q: 7, gain: 0 }
{ cutoff: 800, q: 7, gain: 0 }
{ cutoff: 4000, q: 7, gain: -2 }
{ cutoff: 12000, q: 7, gain: 1 }
]
}) { eqEnabled }
}
```
```ts TypeScript theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
await client.settings.save({
eqEnabled: true,
eqPrecut: -3,
eqBandSettings: [
{ cutoff: 60, q: 7, gain: 3 },
{ cutoff: 200, q: 7, gain: 0 },
{ cutoff: 800, q: 7, gain: 0 },
{ cutoff: 4000, q: 7, gain: -2 },
{ cutoff: 12000, q: 7, gain: 1 },
],
});
```
```python Python theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
await client.settings.save(
eq_enabled=True,
eq_precut=-3,
eq_band_settings=[
{"cutoff": 60, "q": 7, "gain": 3},
{"cutoff": 200, "q": 7, "gain": 0},
{"cutoff": 800, "q": 7, "gain": 0},
{"cutoff": 4000, "q": 7, "gain": -2},
{"cutoff": 12000, "q": 7, "gain": 1},
],
)
```
# Audio settings
Source: https://rockboxzig.mintlify.app/audio-settings/overview
Volume, EQ, DSP, ReplayGain, crossfade and the rest of the rbcodec pipeline.
The Rockbox DSP pipeline runs between the codec output and the active PCM
sink. Settings live in `global_settings` (in the C firmware) and are
mirrored in `settings.toml`. Most can also be changed at runtime via
GraphQL or gRPC and they persist on the next save cycle.
10-band parametric EQ — gain, centre frequency, Q per band.
Crossfeed, surround, PBE, AFR, compressor, dithering.
Track / album normalisation with optional clipping protection.
Overlap track ends with the next track's start.
## Where settings live
| Layer | Lives in | Lifetime |
| -------------------- | ------------------------------------- | -------------------- |
| Compiled-in defaults | `apps/settings_list.c` | Build-time |
| `settings.toml` | `~/.config/rockbox.org/settings.toml` | Read once at startup |
| Runtime (API) | In-memory `global_settings` | Persisted on save |
Hardware settings flow through `firmware/sound.c → sound_set_*()`. DSP
settings flow through `lib/rbcodec/dsp/*` and are applied in the PCM pipeline
before samples reach the sink.
## Volume
`global_status.volume` — decibels relative to the device's clipping point.
**0 dB** is the maximum undistorted level. Negative values reduce output;
positive values may distort. On the CPAL target, volume is implemented as a
software mixer.
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
volume_limit = 0 # ceiling in dB (default = device max)
```
## Channels & stereo
| Setting | Storage | Range / values |
| -------------------------- | ---------------- | ---------------------------------------------------- |
| Balance | `balance` | −100..+100 |
| Channel config | `channel_config` | Stereo / Mono / Custom / Mono L / R / Karaoke / Swap |
| Stereo width (when Custom) | `stereo_width` | 0..255 % |
## Pitch & time-stretch
Persisted in `global_status` so they survive across restarts. Time-stretch
uses a TDHS algorithm — best for speech, may sound rough on dense music.
| Setting | Storage | Range |
| ------------------- | ------------------------------------- | ----------- |
| Pitch | `global_status.resume_pitch` | \~50..200 % |
| Speed | `global_status.resume_speed` | \~35..250 % |
| Time-stretch enable | `global_settings.timestretch_enabled` | bool |
## Output sample rate
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
play_frequency = 0 # 0=auto, 44100, 48000, 88200, 96000
```
## UI feedback
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
beep = 0 # off / weak / moderate / strong
keyclick = 0
keyclick_repeats = false
```
# ReplayGain
Source: https://rockboxzig.mintlify.app/audio-settings/replaygain
Loudness normalisation using ReplayGain tags embedded in your files.
ReplayGain reads the `REPLAYGAIN_*` tags written by tools like
`loudgain` / `mp3gain` / `metaflac` and applies the recommended gain at
playback time. Albums are kept at consistent loudness without re-encoding
the files.
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
[replaygain_settings]
type = 0 # 0=Track 1=Album 2=Track shuffle 3=Off
noclip = true
preamp = 0
```
## Settings
| Setting | Storage | Values / range | Default | Description |
| ------- | --------- | ----------------------------------- | ------- | ----------------------------------------------------------------------- |
| Type | `.type` | track / album / track shuffle / off | shuffle | Which RG tag to apply for normalisation |
| No-Clip | `.noclip` | bool | false | If the RG adjustment would cause clipping, scale down to avoid it |
| Preamp | `.preamp` | −120..+120 dB (step 5) | 0 dB | Extra gain on top of the RG value (use with No-Clip to avoid surprises) |
Applied via `dsp_replaygain_set_settings()` in `lib/rbcodec/dsp/dsp_misc.h`.
## Modes explained
* **Track** — every track plays at its own RG-normalised level.
* **Album** — all tracks within an album share the same gain; preserves
intended quiet/loud relationships within the album.
* **Track shuffle** — Track gain when shuffling, Album gain otherwise. The
default; behaves naturally regardless of how you're listening.
* **Off** — RG tags ignored.
## Tagging your files
Rockbox does not write ReplayGain tags. Use one of:
* **`loudgain`** — modern multi-format CLI (FLAC, MP3, Opus, OGG, M4A).
* **`mp3gain`** — MP3 only, but lossless.
* **`metaflac --add-replay-gain`** — FLAC, ships with `flac`.
Example:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
loudgain -a -k -s e *.flac # tag album-mode, prevent clipping
```
After re-tagging, trigger a library rescan:
```graphql theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
mutation { scanLibrary }
```
## Configuring at runtime
```graphql GraphQL theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
mutation {
saveSettings(input: {
replaygainSettings: {
type: 1
noclip: true
preamp: 0
}
}) { replaygainSettings { type } }
}
```
```ts TypeScript theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import { ReplaygainType } from '@rockbox-zig/sdk';
await client.settings.save({
replaygainSettings: {
type: ReplaygainType.Album,
noclip: true,
preamp: 0,
},
});
```
# Clojure
Source: https://rockboxzig.mintlify.app/bindings/clojure
Rockbox DSP, metadata, and playback on the JVM via the Java Foreign Function & Memory API — no JNI.
Clojure bindings with **no JNI and no native glue to compile**: they call the
Java **Foreign Function & Memory API** (JEP 454, stable since **JDK 22**)
through interop to locate `librockbox_ffi` at runtime and bind every function
to a `MethodHandle` downcall.
```clojure theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
;; deps.edn
io.github.tsirysndr/rockbox-clj-ffi {:mvn/version "0.1.1"}
```
Requires a **JDK 22+** and the Clojure CLI. The published jar bundles a
prebuilt `librockbox_ffi` for every OS/arch and extracts the matching one at
load time — no Rust toolchain needed. `ROCKBOX_FFI_LIB` overrides; a repo
checkout falls back to `target/release`.
## Quick start
```clojure theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
(require '[rockbox.ffi.metadata :as metadata]
'[rockbox.ffi.dsp :as dsp]
'[rockbox.ffi.player :as player])
;; metadata -> map with keyword keys
(metadata/read "/music/song.flac") ; => {:title "…" :codec "FLAC" …}
(metadata/probe "song.flac") ; => "FLAC"
;; DSP (interleaved stereo int16 short-array)
(dsp/with-dsp [d 44100]
(dsp/eq-enable d true)
(dsp/set-eq-band d 0 100 0.7 3.0)
(dsp/process d samples)) ; => short-array
;; Player (queue + transport + DSP)
(player/with-player [p {:volume 0.8}]
;; Mutating fns return the handle, so setup threads cleanly with `->`.
(-> p
;; queue entries: local paths, http(s):// URLs, or live-radio / stream URLs
(player/set-queue ["/music/a.flac"
"https://example.com/b.mp3"
"http://stream.example.com/live"])
(player/set-eq-enabled true)
(player/set-eq-preset :bass-boost) ; one of 21 built-in presets
(player/set-shuffle true)
(player/set-repeat :all)
(player/play))
(:state (player/status p)))
```
## Player API
Every mutating player fn takes the handle `p` first and **returns `p`**, so a
full session — queue, DSP chain, shuffle/repeat, transport — threads with `->`.
Getters/queries (`volume`, `status`, `queue`, `dsp-settings`, `shuffle-enabled?`,
`repeat`, `eq-enabled?`, …) return their values as usual.
| Namespace | Fns |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rockbox.ffi.metadata` | `read`, `probe` |
| `rockbox.ffi.dsp` | `with-dsp`, `eq-enable`, `set-eq-band`, `set-replaygain`, `process` |
| `rockbox.ffi.player` | **lifecycle** `new-player`, `with-player`, `free` · **queue** `set-queue`, `enqueue`, `insert`, `queue` · **transport** `play`, `pause`, `toggle`, `stop`, `next`, `previous`, `skip-to`, `seek-ms` · **settings** `set-volume`, `volume`, `set-balance`, `balance`, `sample-rate`, `set-crossfade`, `set-replaygain` · **shuffle/repeat** `set-shuffle`, `shuffle-enabled?`, `set-repeat`, `repeat` · **status** `status` · **resume** `resume`, `save-resume`, `clear-resume`, `load-resume` · **playlists** `import-m3u`, `load-m3u`, `export-m3u`, `m3u-read`, `m3u-write`, `is-url?` |
| `rockbox.ffi.decoder` | `open`, `with-decoder`, `free`, `metadata`, `next-chunk`, `seek-ms`, `elapsed-ms`, `finished` |
| `rockbox.ffi.player` DSP | `set-eq-enabled`, `eq-enabled?`, `set-eq-preset`, `set-eq-band`, `set-eq-precut`, `set-tone`, `set-bass`, `set-treble`, `set-bass-cutoff`, `set-treble-cutoff`, `set-crossfeed`, `set-surround`, `set-channel-mode`, `set-stereo-width`, `set-bass-enhancement`, `set-fatigue-reduction`, `set-compressor`, `set-dither`, `set-pitch`, `dsp-settings` |
### Sources
`set-queue`, `enqueue`, and `insert` accept **local file paths**, finite
**`http(s)://` URLs**, and **live-radio / streaming URLs** in the same list —
mix them freely. `dsp-settings` returns a keywordized snapshot of the whole
chain.
### Stereo balance
`set-balance` / `balance` pan the output between the channels: the value ranges
from **-100 (full left)** to **+100 (full right)**, with **0 = centre**.
```clojure theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
(-> p (player/set-balance -30)) ; shift 30 % toward the left channel
(player/balance p) ; => -30
```
### Enums
Every enum arg accepts either the keyword or the raw int (`rockbox.ffi.enums`):
| Map | Keywords |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `eq-preset` | `:flat` `:acoustic` `:bass-boost` `:bass-reducer` `:classical` `:dance` `:deep` `:electronic` `:hip-hop` `:jazz` `:latin` `:loudness` `:lounge` `:piano` `:pop` `:rnb` `:rock` `:small-speakers` `:treble-boost` `:treble-reducer` `:vocal-boost` (21) |
| `crossfeed-mode` | `:off` `:meier` `:custom` |
| `channel-mode` | `:stereo` `:mono` `:custom` `:mono-left` `:mono-right` `:karaoke` `:swap` |
| `repeat-mode` | `:off` `:one` `:all` |
| `crossfade-mode` | `:off` `:auto-skip` `:manual-skip` `:shuffle` `:shuffle-or-manual` `:always` |
| `replaygain-mode` | `:off` `:track` `:album` (player) — see the ReplayGain warning below |
The DSP chain mirrors Rockbox's own sound settings. For what each control does
(EQ bands, crossfeed, tone/bass/treble, compressor, dithering, pitch, …) see
the official
[Rockbox manual — Sound Settings](https://download.rockbox.org/daily/manual/rockbox-ipodvideo/rockbox-buildch6.html).
## Decoder API
`rockbox.ffi.decoder` streams an audio file to **interleaved-stereo S16LE PCM**
one chunk at a time through the Rockbox codec engine — useful for feeding your
own output sink, resampler, or analysis pipeline.
* `open` takes a path and returns a handle; `free` (or `with-decoder`) releases
it when you're done.
* `metadata` returns the tags + stream properties as a keyword-keyed map (same
shape as `metadata/read`).
* `next-chunk` yields `[samples sample-rate]` — `samples` is an interleaved
stereo `short-array` — or **nil at end of track**.
* `seek-ms` requests a seek; `elapsed-ms` is the codec's last reported position.
* `finished` returns `[done code]`: `code` is `0` on a clean end and **negative
on a codec error** (valid only once `done` is true).
```clojure theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
(require '[rockbox.ffi.decoder :as decoder])
(decoder/with-decoder [d "/music/song.flac"]
(:title (decoder/metadata d)) ; => "…"
(loop [total 0]
(if-let [[samples rate] (decoder/next-chunk d)]
(do
;; samples: interleaved stereo short-array at `rate` Hz — write it out
(recur (+ total (alength samples))))
(let [[done code] (decoder/finished d)]
(when (neg? code) (throw (ex-info "codec error" {:code code})))
total)))) ; => total samples decoded
```
Codec state is **process-wide — only one decoder decodes at a time**. Opening
a second decoder blocks until the first is freed, so wrap each in
`with-decoder` (or `free` it) before starting the next.
## Notes
* Rich values (metadata, player status, `dsp-settings`) come back as maps with
**keyword keys**, parsed with `clojure.data.json`.
* Native memory is freed automatically — `with-dsp` / `with-player` free their
handle, and every `char*` / `int16*` the ABI returns is freed inside the
binding.
* Enum arguments accept either a keyword or the raw int (see
`src/rockbox/ffi/enums.clj`).
**Two ReplayGain encodings** — `dsp-replaygain-mode` (`:track` 0, `:album` 1,
`:shuffle` 2, `:off` 3) for `rockbox.ffi.dsp`, `replaygain-mode` (`:off` 0,
`:track` 1, `:album` 2) for `rockbox.ffi.player`. See
[the overview](/bindings/overview#the-one-thing-to-know-two-replaygain-encodings).
The FFM downcalls need `--enable-native-access=ALL-UNNAMED`, wired into the
project's `deps.edn` aliases. This binding lives under `rockbox.ffi.*` and
coexists cleanly with the separate `rockbox-clj` gRPC SDK (which owns the bare
`rockbox.*` namespaces).
# Elixir
Source: https://rockboxzig.mintlify.app/bindings/elixir
Rockbox DSP, metadata, and playback in Elixir via an erl_nif shim over the C ABI.
```elixir theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
# mix.exs
def deps do
[{:rockbox_ex_ffi, "~> 0.1"}]
end
```
Requires **Elixir 1.15+** and **OTP 27+** (uses the built-in `:json` module —
no `jason` dependency). The NIF links the Rust static archive
(`target/release/librockbox_ffi.a`); `mix compile` builds it automatically
through `elixir_make` (running `cargo build --release -p rockbox-ffi` first if
the archive is missing).
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
mix deps.get
mix compile
```
## Quick start
```elixir theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
# --- metadata ---
{:ok, meta} = Rockbox.Metadata.read("song.flac")
meta.artist # "…"
meta.duration_ms # 122324
Rockbox.Metadata.probe("track.opus") # "Opus"
# --- DSP (interleaved stereo int16 binary) ---
d = Rockbox.Dsp.new(44_100)
Rockbox.Dsp.eq_enable(d, true)
Rockbox.Dsp.set_eq_band(d, 0, 60, 0.7, 3.0)
Rockbox.Dsp.set_replaygain(d, 0, true, 0.0) # 0 = track (DSP-native)
Rockbox.Dsp.set_replaygain_gains(d, -6.02, nil, nil, nil)
out = Rockbox.Dsp.process(d, pcm_binary) # int16 LE in/out
# --- playback (needs an output device) ---
# Every mutating Rockbox.Player function returns the handle, so setup pipes.
# Queue entries can be local paths, http(s):// files, or live-radio / stream URLs.
alias Rockbox.Player
p = Player.new(volume: 0.8, crossfade_mode: 5) # 5 = always
p
|> Player.set_replaygain(1, 0.0, true) # 1 = track (player)
|> Player.set_queue(["a.flac", "https://example.com/b.mp3", "http://radio.example/stream"])
|> Player.set_eq_enabled(true)
|> Player.set_eq_preset(:bass_boost) # one of 21 presets
|> Player.set_bass(6) # dB, tone control
|> Player.set_crossfeed(:meier, 0, 0, 0, 0) # headphone crossfeed
|> Player.set_shuffle(true)
|> Player.set_repeat(:all) # :off | :one | :all
|> Player.play()
Player.status(p) # %{state: "playing", index: 0, ...}
Player.dsp_settings(p) # %{eq_enabled: true, bass: 6, ...} full DSP chain
```
### Decode a file to PCM
`Rockbox.Decoder` runs the Rockbox codec engine directly, yielding interleaved
stereo S16LE PCM one chunk at a time — no output device required.
```elixir theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
d = Rockbox.Decoder.open("song.flac") # nil if no codec recognises it
Rockbox.Decoder.metadata(d) # same map shape as Rockbox.Metadata.read/1
# Pull chunks until end of track.
Stream.repeatedly(fn -> Rockbox.Decoder.next_chunk(d) end)
|> Enum.take_while(&(&1 != :eof)) # {pcm_binary, sample_rate} per chunk
Rockbox.Decoder.seek_ms(d, 30_000) # jump to 30 s
Rockbox.Decoder.elapsed_ms(d) # last position reported by the codec
Rockbox.Decoder.finished(d) # {done, code}: 0 = clean end, <0 = error
```
Codec state is **process-wide — only one `Rockbox.Decoder` decodes at a
time**. Opening a second decoder blocks until the first is freed by the GC.
## API
| Module | Contents |
| ------------------ | -------------------------------------------------------------------------------------------------------- |
| `Rockbox.Metadata` | `read/1 -> {:ok, map}`, `probe/1 -> String.t()` |
| `Rockbox.Dsp` | EQ / tone / surround / compressor / ReplayGain, `process/2` |
| `Rockbox.Decoder` | `open/1`, `metadata/1`, `next_chunk/1 -> {pcm, rate} \| :eof`, `seek_ms/2`, `elapsed_ms/1`, `finished/1` |
| `Rockbox.Player` | queue + transport + crossfade + ReplayGain + full DSP chain + shuffle/repeat, `status/1 -> map` |
`Rockbox.Player` — every mutating function returns the handle (pipe-friendly),
so setup chains with `|>`; getters/queries return their values:
* **Transport**: `play/1`, `pause/1`, `toggle/1`, `stop/1`, `next/1`,
`previous/1`, `skip_to/2`, `seek_ms/2`, `set_volume/2` / `volume/1`,
`set_balance/2` / `balance/1` (stereo balance, `-100` full left …
`+100` full right, `0` = centre), `sample_rate/1`, `status/1`.
* **Sources / queue**: `set_queue/2` and `enqueue/2` accept local file paths,
`http(s)://` URLs, and live-radio / streaming URLs; plus `insert/4`,
`queue/1`, `import_m3u/4`, `load_m3u/2`, `export_m3u/2`, and resume via
`resume/1` / `save_resume/1` / `clear_resume/1`.
* **Shuffle & repeat**: `set_shuffle/2` / `shuffle_enabled?/1`;
`set_repeat/2` / `repeat/1` (`Rockbox.RepeatMode`).
* **DSP chain**: `set_eq_enabled/2` / `eq_enabled?/1`, `set_eq_preset/2`
(21 `Rockbox.EqPreset` presets), `set_eq_band/5`, `set_eq_precut/2`,
`set_tone/5` / `set_bass/2` / `set_treble/2` / `set_bass_cutoff/2` /
`set_treble_cutoff/2`, `set_crossfeed/6` (`Rockbox.CrossfeedMode`),
`set_surround/5`, `set_channel_mode/2` (`Rockbox.ChannelMode`) /
`set_stereo_width/2`, `set_bass_enhancement/3`, `set_fatigue_reduction/2`,
`set_compressor/7`, `set_dither/2`, `set_pitch/2`, and `dsp_settings/1`
(full DSP state as a map).
* **Crossfade / ReplayGain**: `set_crossfade/7`, `set_replaygain/4`.
Enum helper modules (each takes an atom **or** the raw integer):
| Module | Atoms |
| ------------------------ | ------------------------------------------------------------------------- |
| `Rockbox.RepeatMode` | `:off` `:one` `:all` |
| `Rockbox.EqPreset` | 21 presets — `:flat` `:acoustic` `:bass_boost` `:rock` `:jazz` `:pop` … |
| `Rockbox.ChannelMode` | `:stereo` `:mono` `:custom` `:mono_left` `:mono_right` `:karaoke` `:swap` |
| `Rockbox.CrossfeedMode` | `:off` `:meier` `:custom` |
| `Rockbox.InsertPosition` | queue-insert positions for `insert/4` / `import_m3u/4` |
* Handles (`Rockbox.Dsp` / `Rockbox.Player`) are NIF resources freed by the
BEAM garbage collector — no explicit close.
* PCM crosses the boundary as an **int16 LE binary**.
* Rich values come back as maps with atom keys.
* The DSP chain mirrors Rockbox's own; see the
[official Rockbox sound-settings manual](https://download.rockbox.org/daily/manual/rockbox-ipodvideo/rockbox-buildch6.html)
for what each control does.
The DSP and player use **different mode integers**:
`Rockbox.Dsp.set_replaygain/4` → `0` track, `1` album, `2` shuffle, `3` off;
`Rockbox.Player.set_replaygain/4` → `0` off, `1` track, `2` album. See
[the overview](/bindings/overview#the-one-thing-to-know-two-replaygain-encodings).
## Docs
Generate API docs locally with `mix docs` (→ `doc/index.html`), or read the
published ones at [hexdocs.pm/rockbox\_ex\_ffi](https://hexdocs.pm/rockbox_ex_ffi/).
The Elixir and Gleam bindings share the exact same
`rockbox_ffi_nif.{c,erl}` NIF, vendored into each project.
# Erlang
Source: https://rockboxzig.mintlify.app/bindings/erlang
The shared BEAM native layer — a raw erl_nif surface over the Rockbox C ABI, reused by the Elixir and Gleam bindings.
```erlang theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
%% rebar.config
{deps, [rockbox_ffi_nif]}.
```
`rockbox_ffi_nif` is the **common native layer** for every binding that runs on
the BEAM. The [Elixir](/bindings/elixir) (`rockbox_ex_ffi`) and
[Gleam](/bindings/gleam) (`rockbox_ffi`) packages both depend on it instead of
vendoring their own copy of the C shim and loader — the ergonomic wrappers live
in those packages.
Requires **OTP 27+** (JSON is decoded with the built-in `json` module). The Hex
package ships only a checksum manifest; the loader's `-on_load` hook downloads
the matching prebuilt `.so` for your platform on first use (see
[NIF delivery](#nif-delivery)).
This page documents the **raw NIF surface**. For application code prefer the
[Elixir](/bindings/elixir) or [Gleam](/bindings/gleam) wrappers — they add
named enums, pipe-friendly setters, and typed return values on top of the same
native functions.
Every function is a raw NIF: strings and paths are **UTF-8 binaries**, `*_json`
functions return raw JSON binaries (decode with OTP 27's `json` module), and
handles (`RbDecoder`, `RbPlayer`, `RbDsp`) are opaque `reference()`s freed by
the GC — there is no explicit close.
## Read metadata
```erlang theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
%% Guess the codec from the extension (no I/O)…
<<"FLAC">> = rockbox_ffi_nif:meta_probe(<<"song.flac">>),
%% …or parse the full tag / stream properties.
Json = rockbox_ffi_nif:meta_read_json(<<"/music/song.flac">>),
#{<<"title">> := Title,
<<"artist">> := Artist,
<<"duration_ms">> := DurMs,
<<"sample_rate">> := Rate} = json:decode(Json).
```
## Decode a file to PCM
`decoder_next_chunk/1` hands back interleaved-stereo little-endian `int16` PCM
until it returns `nil` at end of track.
```erlang theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
D = rockbox_ffi_nif:decoder_open(<<"/music/song.mp3">>),
%% Metadata is available straight from the open decoder.
Meta = json:decode(rockbox_ffi_nif:decoder_metadata_json(D)),
%% Pull every chunk into one PCM binary.
Drain = fun Drain(Acc) ->
case rockbox_ffi_nif:decoder_next_chunk(D) of
nil -> lists:reverse(Acc);
{Pcm, _Rate} -> Drain([Pcm | Acc])
end
end,
Pcm = iolist_to_binary(Drain([])),
%% Confirm the track ended cleanly (0 = clean, negative = codec error).
{true, 0} = rockbox_ffi_nif:decoder_finished(D).
```
Seek before decoding more:
```erlang theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rockbox_ffi_nif:decoder_seek_ms(D, 30000), %% jump to 30s
Ms = rockbox_ffi_nif:decoder_elapsed_ms(D).
```
The codec state is **process-wide** — only one `RbDecoder` can decode at a
time. `decoder_open/1` blocks until any previous decoder is freed by the GC.
## Run PCM through the DSP pipeline
```erlang theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
Dsp = rockbox_ffi_nif:dsp_new(44100),
%% Turn on the equalizer and lift a 1 kHz band by 6 dB.
rockbox_ffi_nif:dsp_eq_enable(Dsp, true),
rockbox_ffi_nif:dsp_set_eq_band(Dsp, 4, 1000, 1.0, 6.0),
Out = rockbox_ffi_nif:dsp_process(Dsp, Pcm).
```
## Play a queue
A player owns a live audio device and a background engine thread; dropping the
last reference to the handle stops playback.
```erlang theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
P = rockbox_ffi_nif:player_new(),
%% Queue local files and/or http(s) / stream URLs.
Queue = json:encode([<<"/music/a.mp3">>, <<"/music/b.flac">>]),
rockbox_ffi_nif:player_set_queue_json(P, Queue),
rockbox_ffi_nif:player_enqueue(P, <<"http://host/stream.mp3">>),
rockbox_ffi_nif:player_set_volume(P, 0.8),
rockbox_ffi_nif:player_set_balance(P, 0), %% -100 left … +100 right, 0 = centre
rockbox_ffi_nif:player_play(P),
%% Transport controls.
rockbox_ffi_nif:player_next(P),
rockbox_ffi_nif:player_seek_ms(P, 15000),
%% Poll playback state.
Status = json:decode(rockbox_ffi_nif:player_status_json(P)),
rockbox_ffi_nif:player_pause(P).
```
The player exposes the same DSP chain as the standalone pipeline —
`player_set_eq_band/5`, `player_set_tone/5`, `player_set_crossfade/7`,
`player_set_replaygain/4`, `player_set_balance/2` / `player_balance/1`, and more
(see the [module docs](https://hexdocs.pm/rockbox_ffi_nif/)).
## API
| Surface | Functions |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Metadata | `meta_probe/1`, `meta_read_json/1` |
| `RbDecoder` | `decoder_open/1`, `decoder_metadata_json/1`, `decoder_next_chunk/1`, `decoder_seek_ms/2`, `decoder_elapsed_ms/1`, `decoder_finished/1` |
| `RbDsp` | `dsp_new/1`, `dsp_eq_enable/2`, `dsp_set_eq_band/5`, `dsp_process/2`, ReplayGain / tone / surround / compressor setters |
| `RbPlayer` | `player_new/0`, queue + transport, `player_set_volume/2`, `player_set_balance/2` / `player_balance/1`, crossfade, ReplayGain, full DSP chain, `player_status_json/1` |
* Handles (`RbDecoder`, `RbDsp`, `RbPlayer`) are NIF resources freed by the BEAM
garbage collector — no explicit close.
* PCM crosses the boundary as an **int16 LE binary**.
* Rich values (`*_json`) come back as raw JSON binaries — decode with OTP 27's
built-in `json` module.
* The DSP chain mirrors Rockbox's own; see the
[official Rockbox sound-settings manual](https://download.rockbox.org/daily/manual/rockbox-ipodvideo/rockbox-buildch6.html)
for what each control does.
The DSP and player use **different ReplayGain mode integers**:
`dsp_set_replaygain/4` → `0` track, `1` album, `2` shuffle, `3` off;
`player_set_replaygain/4` → `0` off, `1` track, `2` album. See
[the overview](/bindings/overview#the-one-thing-to-know-two-replaygain-encodings).
## NIF delivery
The compiled NIF statically links the Rust `librockbox_ffi.a`, making it a
multi-megabyte shared object — too large to bundle in a Hex tarball. So the Hex
package ships only the checksum manifest, and `rockbox_ffi_nif.erl`'s `-on_load`
hook downloads the matching `rockbox_ffi_nif-.so` from the named GitHub
release into the user cache on first use, verifying it against the manifest
sha256. Both the Elixir and Gleam bindings get their native code the same way.
Prebuilt targets: `aarch64-apple-darwin`, `x86_64-apple-darwin`,
`aarch64-linux-gnu`, `x86_64-linux-gnu`, `x86_64-unknown-freebsd`,
`x86_64-unknown-netbsd`. Other platforms build from source.
## Local development
Inside a full monorepo checkout (needs the Cargo workspace + `include/` header):
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
# 1. Build the Rust static archive.
cargo build --release -p rockbox-ffi
# 2. Build the NIF into priv/rockbox_ffi_nif.so.
cd bindings/erlang && make
# 3. Compile the Erlang app.
rebar3 compile
```
The Elixir / Gleam bindings pick up this locally-built `.so` via a path
dependency on `../erlang` — the loader prefers a local `priv/*.so` over any
cached download.
## Docs
Read the published module docs at
[hexdocs.pm/rockbox\_ffi\_nif](https://hexdocs.pm/rockbox_ffi_nif/), or generate
them locally with `rebar3 ex_doc`.
# Gleam
Source: https://rockboxzig.mintlify.app/bindings/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`.
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`.
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](/bindings/overview#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.
Codec state is **process-wide — only one `Decoder` decodes at a time.**
Opening a second decoder blocks until the first has been garbage-collected.
The Gleam and Elixir bindings share the exact same
`rockbox_ffi_nif.{c,erl}` NIF, vendored into each project.
# Go
Source: https://rockboxzig.mintlify.app/bindings/go
Rockbox DSP, metadata, and playback in Go via purego over the librockbox_ffi C ABI — no cgo.
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
go get github.com/tsirysndr/rockboxd/bindings/go
```
Requires **Go 1.22+**. Calls the native engine through
[`purego`](https://github.com/ebitengine/purego) — **no cgo and no C
toolchain**; the package `dlopen`s a prebuilt `librockbox_ffi` at process
start. From a repo checkout the loader walks up to `target/release/`; override
with `ROCKBOX_FFI_LIB`.
## Quick start
```go theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
package main
import (
"fmt"
rockbox "github.com/tsirysndr/rockboxd/bindings/go"
)
func main() {
// --- metadata -----------------------------------------------------
meta, _ := rockbox.Metadata.Read("song.flac")
fmt.Println(meta.Artist, "—", meta.Title, meta.DurationMs, "ms")
label, _ := rockbox.Metadata.Probe("track.opus") // "Opus" (no I/O, extension guess)
fmt.Println(label)
// --- DSP: process interleaved stereo int16 ------------------------
dsp, _ := rockbox.NewDsp(44_100)
defer dsp.Close()
dsp.EqEnable(true)
dsp.SetEqBand(0, 60, 0.7, 3.0)
dsp.SetReplaygain(rockbox.DspReplayGainTrack, true, 0.0)
dsp.SetReplaygainGains(rockbox.Opt(-6.02), nil, nil, nil) // −6 dB ≈ half amplitude
processed, _ := dsp.Process(samples) // []int16
_ = processed
// --- playback (needs an output device) ----------------------------
// Functional-options constructor (or use NewPlayer(Config) directly):
player, _ := rockbox.New(
rockbox.WithVolume(0.8),
rockbox.WithReplayGain(rockbox.ReplayGainTrack, 0.0, true),
rockbox.WithCrossfade(rockbox.CrossfadeAlways, 0, 2000, 0, 2000, rockbox.MixCrossfade),
rockbox.WithResumeFile("state.m3u8"),
)
defer player.Close()
// DSP chain — EQ presets, tone, crossfeed, compressor, … (setters return nothing)
player.SetEqEnabled(true)
player.SetEqPreset(rockbox.EqPresetBassBoost)
// Shuffle & repeat
player.SetShuffle(true)
player.SetRepeat(rockbox.RepeatAll)
// Queue entries may be local paths, http(s):// URLs, or live-radio / streaming URLs.
player.SetQueue([]string{"a.flac", "https://example.com/b.mp3", "https://stream.example/radio"})
player.Play()
st, _ := player.Status() // &Status{State: "playing", Index: 0, Shuffle: true, Repeat: "all", ...}
fmt.Println(st.State, st.Shuffle, st.Repeat)
}
```
## Decode a file to PCM
`OpenDecoder` runs a file through the Rockbox codec engine and hands back
interleaved-stereo S16LE PCM one chunk at a time — decode without an output
device.
```go theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
dec, err := rockbox.OpenDecoder("song.flac")
if err != nil {
log.Fatal(err)
}
defer dec.Close() // safe to call twice
meta, _ := dec.Metadata() // map[string]any, same shape as Metadata.Read
fmt.Println(meta["title"])
for {
samples, rate, ok := dec.NextChunk() // []int16, sample rate, ok=false at EOF
if !ok {
break
}
_ = samples
_ = rate
// dec.SeekMs(30_000); dec.ElapsedMs()
}
if done, code := dec.Finished(); done && code < 0 {
log.Printf("codec error: %d", code) // 0 = clean end, negative = codec error
}
```
The codec engine is **process-wide**: only one `Decoder` may decode at a
time, and always from a single goroutine. A second `OpenDecoder` blocks until
the first is closed.
## API
| Symbol | Contents |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rockbox.Metadata` | `Read(path) (*Meta, error)`, `Probe(name) (string, bool)` |
| `rockbox.NewDsp` → `*Dsp` | EQ / tone / surround / compressor / ReplayGain, `Process([]int16)` |
| `rockbox.New` / `rockbox.NewPlayer` → `*Player` | **Construct:** `New(WithVolume, WithReplayGain, WithCrossfade, WithResumeFile, …)` (functional options) or `NewPlayer(Config)`. **Transport:** `Play` / `Pause` / `Toggle` / `Stop` / `Next` / `Previous` / `SkipTo` / `SeekMs`. **Queue:** `SetQueue` / `Enqueue` / `Insert` / `Queue` (local paths, `http(s)://`, live-radio / streaming URLs). **Shuffle & repeat:** `SetShuffle` / `IsShuffleEnabled`, `SetRepeat` / `Repeat`. **DSP chain:** `SetEqEnabled` / `IsEqEnabled` / `SetEqPreset` / `SetEqBand` / `SetEqPrecut`, `SetTone` / `SetBass` / `SetTreble` / `SetBassCutoff` / `SetTrebleCutoff`, `SetCrossfeed`, `SetSurround`, `SetChannelMode` / `SetStereoWidth`, `SetBassEnhancement`, `SetFatigueReduction`, `SetCompressor`, `SetDither`, `SetPitch`, `DSPSettings()`. **Volume/balance/ReplayGain/crossfade:** `SetVolume` / `Volume`, `SetBalance` / `Balance`, `SetReplaygain`, `SetCrossfade`. **Resume / m3u:** `Resume` / `SaveResume` / `ClearResume`, `LoadM3u` / `ImportM3u` / `ExportM3u`. **State:** `Status()`. |
| `rockbox.OpenDecoder` → `*Decoder` | Stream-decode a file to interleaved-stereo S16LE PCM via the Rockbox codec engine: `Metadata()`, `NextChunk() ([]int16, uint32, bool)`, `SeekMs` / `ElapsedMs`, `Finished() (bool, int32)`. **Codec state is process-wide** — only one Decoder decodes at a time, from one goroutine. |
| `rockbox.*` consts | `DspReplayGainMode`, `ReplayGainMode`, `RepeatMode` (Off/One/All), `EqPreset` (21 presets), `CrossfeedMode`, `ChannelMode`, `CrossfadeMode`, `MixMode`, `InsertPosition`, `ChannelConfig` |
* **Handles** own native resources — `defer dsp.Close()` / `defer player.Close()`
frees them.
* **Rich values** (metadata, status) decode into typed structs (`Meta`,
`Status`); optional tags are `nil`-able pointer fields.
* **Sample buffers** are `[]int16` of interleaved-stereo signed 16-bit samples.
* **DSP semantics** (EQ bands, tone, crossfeed, compressor, …) mirror Rockbox's own settings — see the [official Rockbox manual](https://download.rockbox.org/daily/manual/rockbox-ipodvideo/rockbox-buildch6.html) for what each control does.
`Dsp.SetReplaygain` and `Player.SetReplaygain` take **different mode
integers**. Always pass the named constants — `DspReplayGain*` for the DSP,
`ReplayGain*` for the player. See [the overview](/bindings/overview#the-one-thing-to-know-two-replaygain-encodings).
## Smoke test
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
cd bindings/go
go run ./examples/smoke # metadata + DSP + player checks
go test ./...
```
# Kotlin
Source: https://rockboxzig.mintlify.app/bindings/kotlin
Rockbox DSP, metadata, and playback on the JVM via the Java Foreign Function & Memory API — no JNI.
Kotlin/JVM bindings with **no JNI and no native glue to compile**: they use the
Java **Foreign Function & Memory API** (JEP 454, stable since **JDK 22**) to
locate `librockbox_ffi` at runtime and bind every function to a `MethodHandle`
downcall.
```kotlin theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
// build.gradle.kts
dependencies {
implementation("io.github.tsirysndr:rockbox-ffi:0.2.0")
}
```
Requires a **JDK 22+**. The published jar bundles a prebuilt `librockbox_ffi`
for every OS/arch — it extracts the matching one at load time, so consumers
need no Rust toolchain. `ROCKBOX_FFI_LIB` overrides; a repo checkout falls back
to `target/release`.
FFM's restricted native calls need `--enable-native-access=ALL-UNNAMED` on
the launch command (the project's Gradle tasks wire this in).
## Quick start
```kotlin theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import org.rockbox.ffi.*
// metadata
val meta = Metadata.read("/music/song.flac") // Map
println(meta["title"])
Metadata.probe("song.flac") // "FLAC"
// DSP (interleaved stereo int16)
Dsp(44_100).use { dsp ->
dsp.eqEnable(true)
dsp.setEqBand(band = 0, cutoffHz = 100, q = 0.7f, gainDb = 3.0f)
val out: ShortArray = dsp.process(samples)
}
// Player (queue + transport + DSP + shuffle/repeat)
Player(Player.Config().apply { volume = 0.8f }).use { player ->
// Sources may be local paths, http(s):// URLs, or live-radio / streaming URLs.
player.setQueue(listOf("/music/a.flac", "https://example.com/b.mp3"))
player.enqueue("http://stream.example.org/radio")
// DSP chain
player.setEqEnabled(true)
player.setEqPreset(EqPreset.BASS_BOOST) // one of 21 built-in presets
// Playback modes
player.setShuffle(true)
player.setRepeat(RepeatMode.ALL)
player.play()
println(player.status()["state"])
println(player.dspSettings()) // Map
}
```
Player setters follow Kotlin idiom — they return `Unit`, so they do **not**
chain. Construct with `Player(Config().apply { volume = 0.8f })` and call
each setter on its own line.
## Decode a file to PCM
`Decoder` streams a file through the Rockbox codec engine, yielding
interleaved-stereo S16LE chunks one at a time — no output device required.
```kotlin theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import org.rockbox.ffi.*
Decoder("/music/song.flac").use { dec ->
println(dec.metadata()["title"]) // same shape as Metadata.read
while (true) {
val chunk = dec.nextChunk() ?: break // null at end of track
val (samples: ShortArray, rate: Int) = chunk
// feed `samples` (interleaved stereo int16) at `rate` Hz to your sink
}
val (done, code) = dec.finished() // code 0 = clean end, <0 = codec error
check(done && code == 0) { "decode failed: $code" }
}
// seeking is supported mid-stream
Decoder("/music/song.flac").use { dec ->
dec.seekMs(30_000)
dec.nextChunk()
println(dec.elapsedMs())
}
```
Codec state is **process-wide — only one `Decoder` decodes at a time**.
Constructing a second `Decoder` blocks until the first is closed, so always
keep decoders inside a `use { }` block (or call `close()`) before opening the
next one.
## API surface
| Type | Selected members |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Metadata` | `read(path)` → `Map`, `probe(path)` → codec name |
| `Dsp` | `eqEnable(on)`, `setEqBand(band, cutoffHz, q, gainDb)`, `process(samples)`, `setReplaygain(mode, …)` |
| `Decoder` | `Decoder(path)` (AutoCloseable), `metadata()` → `Map`, `nextChunk()` → `Pair?`, `seekMs(ms)`, `elapsedMs()`, `finished()` → `Pair` |
| `Player` | **Sources / queue**: `setQueue(paths)`, `enqueue(path)`, `insert(paths, position, index)`, `queue()`, `loadM3u` / `importM3u` / `exportM3u` — every path may be a local file, an `http(s)://` URL, or a live-radio / streaming URL. **Transport**: `play`, `pause`, `toggle`, `stop`, `next`, `previous`, `skipTo`, `seekMs`. **Volume / balance / crossfade / replaygain**: `setVolume`, `setBalance(balance)` / `balance` (−100 … +100, 0 = centre), `setCrossfade`, `setReplaygain`. **Shuffle / repeat**: `setShuffle(on)`, `isShuffleEnabled()`, `setRepeat(mode)`, `repeat()`. **DSP chain**: `setEqEnabled` / `isEqEnabled`, `setEqPreset`, `setEqBand`, `setEqPrecut`, `setTone`, `setBass`, `setTreble`, `setBassCutoff`, `setTrebleCutoff`, `setCrossfeed`, `setSurround`, `setChannelMode`, `setStereoWidth`, `setBassEnhancement`, `setFatigueReduction`, `setCompressor`, `setDither`, `setPitch`, `dspSettings()` → `Map`. **Status / resume**: `status()`, `resume`, `saveResume`, `clearResume`. |
### Enums
* `EqPreset` — 21 built-in presets: `FLAT`, `ACOUSTIC`, `BASS_BOOST`, `BASS_REDUCER`, `CLASSICAL`, `DANCE`, `DEEP`, `ELECTRONIC`, `HIP_HOP`, `JAZZ`, `LATIN`, `LOUDNESS`, `LOUNGE`, `PIANO`, `POP`, `RNB`, `ROCK`, `SMALL_SPEAKERS`, `TREBLE_BOOST`, `TREBLE_REDUCER`, `VOCAL_BOOST`.
* `RepeatMode` — `OFF`, `ONE`, `ALL`.
* `CrossfeedMode` — `OFF`, `MEIER`, `CUSTOM`.
* `ChannelMode` — `STEREO`, `MONO`, `CUSTOM`, `MONO_LEFT`, `MONO_RIGHT`, `KARAOKE`, `SWAP`.
* `CrossfadeMode` — `OFF`, `AUTO_SKIP`, `MANUAL_SKIP`, `SHUFFLE`, `SHUFFLE_OR_MANUAL`, `ALWAYS`; `MixMode` — `CROSSFADE`, `MIX`.
* `InsertPosition` — `PREPEND`, `INSERT`, `INSERT_NEXT`, `INSERT_LAST`, `INSERT_SHUFFLED`, `INSERT_LAST_SHUFFLED`, `REPLACE`, `INDEX`.
* `ReplayGainMode` (player) — `OFF`, `TRACK`, `ALBUM`; `DspReplayGainMode` (`Dsp`) — `TRACK`, `ALBUM`, `SHUFFLE`, `OFF`.
The DSP chain mirrors Rockbox's own **Sound Settings** — parametric EQ,
crossfeed, tone controls, channel modes, surround, and the compressor. For
what each knob does and sensible ranges, see the official
[Rockbox manual](https://download.rockbox.org/daily/manual/rockbox-ipodvideo/rockbox-buildch6.html).
## Notes
* Rich values (metadata, player status) come back as `Map`,
parsed from the ABI's JSON with `org.json`.
* Sample buffers are `ShortArray` of interleaved-stereo signed 16-bit samples.
* Native memory is freed automatically — handles are `AutoCloseable`
(`use { }`), and every `char*` / `int16*` the ABI returns is freed inside the
binding.
**Two ReplayGain encodings** — `DspReplayGainMode` (TRACK=0, ALBUM=1,
SHUFFLE=2, OFF=3) for `Dsp`, `ReplayGainMode` (OFF=0, TRACK=1, ALBUM=2) for
`Player`. See [the overview](/bindings/overview#the-one-thing-to-know-two-replaygain-encodings).
# Native engine bindings
Source: https://rockboxzig.mintlify.app/bindings/overview
Call the Rockbox DSP, metadata, decoding, and playback engine in-process from ten languages, all over one shared C ABI.
The **bindings** embed the Rockbox audio engine directly in your process. They
wrap `rockbox-ffi` — a flat **C ABI** (`crates/rockbox-ffi`) — so you can parse
tags, decode files to PCM, run PCM through the DSP pipeline, and drive a
queue-based player without running a daemon or talking to any network port.
Looking to **remote-control a running `rockboxd`** over the network instead?
That is what the [Client SDKs](/sdks/overview) are for — they speak GraphQL on
port 6062. The bindings on this page are the opposite: no daemon, no sockets,
the engine runs **inside your program**.
## SDKs vs. bindings
| | [Client SDKs](/sdks/overview) | Bindings (this section) |
| -------------- | ------------------------------------ | ----------------------------------- |
| Talks to | A running `rockboxd` over GraphQL | The engine, linked in-process |
| Transport | HTTP / WebSocket on port 6062 | Native `dlopen` / FFI — no network |
| Best for | Apps that control a music server | Embedding DSP / metadata / playback |
| Package family | `rockbox-sdk`, `@rockbox-zig/sdk`, … | `rockbox-ffi` and friends |
## Four surfaces
Every binding exposes the same namespaces with matching method names:
`read(path)` returns parsed tags, duration, ReplayGain, album-art and
cuesheet offsets. `probe(filename)` guesses the codec from the extension —
no I/O.
Open a file and pull decoded **interleaved-stereo int16 PCM** one chunk at
a time through the Rockbox codec engine — `metadata()`, `next_chunk()`,
`seek_ms()`, `elapsed_ms()`, `finished()`. No audio device needed.
Construct with a sample rate, then the whole pipeline — EQ, tone,
crossfeed, surround, channel mixing, bass enhancement, compressor,
ReplayGain, … — and `process(samples)` over interleaved stereo int16.
Queue + transport, **shuffle / repeat**, crossfade, ReplayGain, **stereo
balance**, the full **DSP chain** (EQ + presets, tone, crossfeed, surround,
compressor, …), and `status()`. Needs an audio output device.
The codec state is **process-wide** — only one `Decoder` can decode at a time.
Opening a second one blocks until the first is freed (closed explicitly, or
by GC / a context manager).
## What the Player exposes
Beyond queue and transport, the player mirrors Rockbox's own sound engine —
every setter is live and reflected back by `dsp_settings()` / `status()`:
**Shuffle** (current track stays, the rest are shuffled) and **repeat**
(`off` / `one` / `all`), readable back from `status()`.
10-band parametric EQ with **21 ready-to-use presets** (Rock, Jazz, Bass
Boost, …), per-band control, and a pre-cut.
Bass/treble (with cutoffs), **crossfeed**, Haas **surround**, channel
mixing (mono / karaoke / swap), custom stereo width and **stereo balance**
(−100 full-left … +100 full-right).
**Perceptual bass enhancement**, **auditory fatigue reduction**, a
dynamic-range **compressor**, **dither** and **pitch**/speed.
Queue entries can be **local file paths**, **`http(s)://` URLs** (finite
remote files, streamed on demand), or **live-radio / streaming URLs**.
These are the same controls as Rockbox's on-device sound menu — see the
official [Rockbox manual — Sound Settings](https://download.rockbox.org/daily/manual/rockbox-ipodvideo/rockbox-buildch6.html)
(and the [Equalizer](https://download.rockbox.org/daily/manual/rockbox-ipodvideo/rockbox-buildch6.html#x11-1200006.11)
section) for what each one does.
## Pick a language
`pip install rockbox-ffi` — cffi
`go get …/bindings/go` — purego, no cgo
`npm install rockbox-ffi` — Bun / Deno / Node
`gem install rockbox_ffi` — fiddle
`{rockbox_ffi_nif, …}` — erl\_nif (shared)
`{:rockbox_ex_ffi, "~> 0.1"}` — erl\_nif
`gleam add rockbox_ffi` — erl\_nif
SwiftPM — pure-Swift dlopen
`io.github.tsirysndr:rockbox-ffi` — Java FFM
`io.github.tsirysndr/rockbox-clj-ffi` — Java FFM
On the BEAM, [Erlang](/bindings/erlang) (`rockbox_ffi_nif`) is the shared
native layer — a raw `erl_nif` surface over the C ABI. The
[Elixir](/bindings/elixir) and [Gleam](/bindings/gleam) packages depend on it
for their native code and add the ergonomic wrappers on top.
## The one thing to know: two ReplayGain encodings
The DSP and the player use **different integers** for the same ReplayGain
modes — a quirk of the underlying C ABI. Every binding ships named
enums/constants so you never have to remember which is which:
| Mode | `Dsp` (`DspReplayGainMode`) | `Player` (`ReplayGainMode`) |
| ------- | --------------------------- | --------------------------- |
| Off | 3 | 0 |
| Track | 0 | 1 |
| Album | 1 | 2 |
| Shuffle | 2 | — |
## How values cross the boundary
* **Rich values** (metadata, player status) cross as **JSON** and land as a
native map/dict/struct in your language.
* **Sample buffers** are raw interleaved-stereo **signed 16-bit** integers
(`Int16Array`, `array('h')`, `ShortArray`, a BEAM binary, …).
* **Memory is managed for you.** Every allocation the ABI hands out has a
matching free called *inside* the binding — you never call a `*_free`
yourself. Handles close via context managers, `use`/`with`, or GC finalizers.
## Build the native library
Published packages bundle a prebuilt `librockbox_ffi` for your platform, so
`pip install` / `npm install` / `gem install` need no Rust toolchain. From a
repo checkout, build it once and every binding finds it automatically:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
cargo build --release -p rockbox-ffi
# target/release/librockbox_ffi.dylib (macOS)
# target/release/librockbox_ffi.so (Linux)
# target/release/librockbox_ffi.a (static, for the BEAM NIFs)
```
The loaders walk up to `target/release/` by default. Point them elsewhere with
the `ROCKBOX_FFI_LIB` environment variable:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
export ROCKBOX_FFI_LIB=/path/to/librockbox_ffi.dylib
```
On **Linux** the shared library links ALSA — install the system
`libasound2` package at runtime.
# Python
Source: https://rockboxzig.mintlify.app/bindings/python
Rockbox DSP, metadata, and playback in Python via cffi over the librockbox_ffi C ABI.
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
pip install rockbox-ffi
# or
uv add rockbox-ffi
```
Requires **Python 3.9+**. The wheel bundles a prebuilt `librockbox_ffi` for
your platform — no Rust toolchain needed. From a repo checkout, the loader
walks up to `target/release/`; override with `ROCKBOX_FFI_LIB`.
## Quick start
```python theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import rockbox_ffi as rb
from rockbox_ffi import Dsp, Player, metadata
from rockbox_ffi.enums import (
DspReplayGainMode, ReplayGainMode, CrossfadeMode,
EqPreset, RepeatMode, CrossfeedMode,
)
# --- metadata ---------------------------------------------------------
meta = metadata.read("song.flac")
print(meta["artist"], "—", meta["title"], meta["duration_ms"], "ms")
print(metadata.probe("track.opus")) # -> "Opus" (no I/O, extension guess)
# --- DSP: process interleaved stereo int16 ----------------------------
with Dsp(44_100) 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) # −6 dB ≈ half amplitude
processed = dsp.process(samples) # array('h')
# --- playback (needs an output device) --------------------------------
# Setters return None — call them plainly, one per line (no chaining).
with Player(volume=0.8) as player:
player.set_replaygain(ReplayGainMode.TRACK, preamp_db=0.0, prevent_clipping=True)
player.set_crossfade(CrossfadeMode.ALWAYS)
# --- live DSP chain (mirrored back by player.dsp_settings()) ------
player.set_eq_enabled(True)
player.set_eq_preset(EqPreset.BASS_BOOST) # 21 presets: Flat, Rock, Jazz, …
player.set_bass(4) # tone: bass/treble in dB
player.set_treble(2)
player.set_crossfeed(CrossfeedMode.MEIER, 0, -60, -80, 700)
player.set_bass_enhancement(strength=30, precut=-30) # PBE
print(player.dsp_settings()) # -> dict of the whole chain
# --- shuffle & repeat --------------------------------------------
player.set_shuffle(True)
player.set_repeat(RepeatMode.ALL)
# --- sources: local paths, http(s):// files, live-radio URLs ------
player.set_queue([
"a.flac",
"https://example.com/b.mp3",
"https://stream.example.com/radio", # live stream
])
player.play()
print(player.status()) # {'state': 'playing', 'index': 0, ...}
```
## Decode a file to PCM
`Decoder` streams an audio file to interleaved-stereo S16LE PCM one chunk at a
time through the Rockbox codec engine — the same decoders that power playback,
without an output device.
```python theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
from rockbox_ffi import Decoder
with Decoder("song.flac") as dec:
print(dec.metadata()["title"]) # same shape as metadata.read
for samples, rate in dec.chunks(): # array('h'), Hz
... # feed samples somewhere
print(dec.finished()) # (True, 0) — clean end
# dec.seek_ms(30_000); dec.elapsed_ms() # seek / query position
```
`chunks()` (or `next_chunk()`) yields `(samples, sample_rate)` and stops at
`None` on end of track; `finished()` returns `(done, code)` where `code` is `0`
for a clean end and negative for a codec error.
**One decoder at a time.** The codec state is process-wide (global) — only one
`Decoder` can decode at once. Constructing a second one *blocks* until the
first is closed. Always use `with Decoder(...)` (or call `close()`) so the
native resource is freed; a GC finalizer is the backstop.
## 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` | streaming file → S16LE PCM via the codec engine: `metadata()`, `next_chunk()` / `chunks()`, `seek_ms(ms)`, `elapsed_ms()`, `finished() -> (done, code)` (one decoder at a time — codec state is global) |
| `rockbox_ffi.Player` | queue + transport + crossfade + ReplayGain + shuffle/repeat + full live DSP chain (10-band EQ, tone, crossfeed, surround, PBE/AFR, compressor, dither, pitch), `dsp_settings() -> dict`, `status() -> dict` |
| `rockbox_ffi.enums` | `DspReplayGainMode`, `ReplayGainMode`, `CrossfadeMode`, `MixMode`, `RepeatMode`, `EqPreset`, `ChannelMode`, `CrossfeedMode`, `InsertPosition`, … |
* **Handles** are context managers — `with Dsp(...) as dsp:` frees the native
resource on exit. A GC finalizer is the backstop.
* **Rich values** (metadata, status) come back as plain `dict`s.
* **Sample buffers** are `array('h')` of interleaved-stereo signed 16-bit
samples.
**Player DSP chain.** Every setter applies live and is reflected back by
`player.dsp_settings()`. The full surface:
**EQ** — `set_eq_enabled` / `is_eq_enabled`, `set_eq_preset(EqPreset)`
(21 presets: `FLAT`, `ROCK`, `JAZZ`, `BASS_BOOST`, …), `set_eq_band`,
`set_eq_precut`. **Tone** — `set_tone`, `set_bass`, `set_treble`,
`set_bass_cutoff`, `set_treble_cutoff`. **Spatial** —
`set_crossfeed(CrossfeedMode)`, `set_surround`, `set_channel_mode(ChannelMode)`,
`set_stereo_width`, `set_balance(balance)` / `balance()` (stereo balance,
`-100` full left … `+100` full right, `0` = centre). **Enhancers** — `set_bass_enhancement` (PBE),
`set_fatigue_reduction` (AFR), `set_compressor`, `set_dither`, `set_pitch`.
**Shuffle / repeat** — `set_shuffle` / `is_shuffle_enabled`,
`set_repeat(RepeatMode)` / `repeat()`.
For what each parameter does (and sensible ranges), see the official Rockbox
manual: Sound Settings .
`Dsp.set_replaygain` and `Player.set_replaygain` take **different mode
integers**. Always pass the named enums — `DspReplayGainMode` for the DSP,
`ReplayGainMode` for the player. See [the overview](/bindings/overview#the-one-thing-to-know-two-replaygain-encodings).
## Interactive console
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
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:
```python theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
metadata.read(str(FIXTURE))["title"] # 'Speak'
p = Player(volume=0.6)
p.set_queue([str(FIXTURE)]); p.play()
p.status()["state"] # 'playing'
```
# Ruby
Source: https://rockboxzig.mintlify.app/bindings/ruby
Rockbox DSP, metadata, and playback in Ruby via fiddle — no native extension to compile.
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
gem install rockbox_ffi
```
Requires **Ruby 2.6+**. The binding uses [`fiddle`](https://docs.ruby-lang.org/en/master/Fiddle.html)
(Ruby stdlib) to `dlopen` the shared library — **no native extension is
compiled**. The published gem bundles a prebuilt `librockbox_ffi`; from a repo
checkout the loader walks up to `target/release/` (override with
`ROCKBOX_FFI_LIB`).
## Quick start
```ruby theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
require "rockbox_ffi"
# --- metadata ---------------------------------------------------------
meta = RockboxFFI::Metadata.read("song.flac")
puts "#{meta[:artist]} — #{meta[:title]} (#{meta[:duration_ms]} ms)"
RockboxFFI::Metadata.probe("track.opus") # => "Opus"
# --- DSP (interleaved stereo int16) -----------------------------------
RockboxFFI::Dsp.open(44_100) do |dsp|
dsp.eq_enable(true)
dsp.set_eq_band(0, 60, 0.7, 3.0)
dsp.set_replaygain(RockboxFFI::DspReplayGainMode::TRACK, true, 0.0)
dsp.set_replaygain_gains(track_gain_db: -6.02) # halves amplitude
processed = dsp.process(samples) # Array
end
# --- playback (needs an output device) --------------------------------
# Mutating Player setters return self, so calls chain fluently. The queue
# accepts local file paths, http(s):// URLs, and live-radio / streaming URLs.
RockboxFFI::Player.open(volume: 0.8) do |player|
player
.set_queue(["a.flac", "https://example.com/b.mp3", "http://stream.example/live"])
.set_eq_preset(RockboxFFI::EqPreset::BASS_BOOST)
.set_shuffle(true)
.set_repeat(RockboxFFI::RepeatMode::ALL)
.set_balance(0) # -100 full left … +100 full right, 0 = centre
.play
# --- DSP chain (all setters chainable) ------------------------------
player
.set_eq_enabled(true)
.set_eq_band(0, 60, 0.7, 3.0) # band, cutoff_hz, q, gain_db
.set_eq_precut(3.0)
.set_bass(4).set_treble(2) # or set_tone(bass, treble, cuts…)
.set_crossfeed(RockboxFFI::CrossfeedMode::MEIER, 0, 0, 0, 0)
.set_channel_mode(RockboxFFI::ChannelMode::STEREO)
player.eq_enabled? # => true
player.shuffle_enabled? # => true
player.repeat # => 2 (RepeatMode::ALL)
player.dsp_settings # => {eq_enabled: true, ...}
player.status # => {state: "playing", index: 0, ...}
end
# --- decode a file to PCM (interleaved stereo int16) ------------------
RockboxFFI::Decoder.open("song.flac") do |dec|
dec.metadata[:title] # same shape as Metadata.read
dec.seek_ms(30_000) # optional: jump 30 s in
dec.each_chunk do |samples, sample_rate|
# samples: Array S16LE stereo; sample_rate: Hz
handle(samples, sample_rate)
end
done, code = dec.finished # code: 0 = clean end, negative = codec error
warn "codec error #{code}" if done && code.negative?
end
```
The Rockbox codec engine is **process-wide — only one `Decoder` decodes at a
time**. A second `Decoder.open` blocks until the first is freed (via `#close`
or the block form). Drive a decoder from a single thread.
## API
| Namespace | Contents |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RockboxFFI::Metadata` | `read(path) => Hash`, `probe(filename) => String \| nil` |
| `RockboxFFI::Dsp` | EQ / tone / surround / compressor / ReplayGain, `process(samples)` |
| `RockboxFFI::Player` | queue (`set_queue` / `enqueue` / `insert` — local paths, `http(s)://` URLs, live-radio / streaming URLs), transport, `set_volume` / `set_balance` / `balance`, `set_shuffle` / `shuffle_enabled?`, `set_repeat` / `repeat`, crossfade, ReplayGain, full DSP chain (`set_eq_enabled` / `eq_enabled?`, `set_eq_preset`, `set_eq_band`, `set_eq_precut`, `set_tone` / `set_bass` / `set_treble` / `set_bass_cutoff` / `set_treble_cutoff`, `set_crossfeed`, `set_surround`, `set_channel_mode` / `set_stereo_width`, `set_bass_enhancement`, `set_fatigue_reduction`, `set_compressor`, `set_dither`, `set_pitch`, `dsp_settings`), m3u/resume, `status => Hash`. Mutating setters return `self` for chaining. |
| `RockboxFFI::Decoder` | streaming file decoder: `open(path)` / `close`, `metadata => Hash`, `next_chunk` / `each_chunk => [samples, sample_rate]`, `seek_ms`, `elapsed_ms`, `finished => [done, code]` |
| `RockboxFFI::*Mode` | `DspReplayGainMode`, `ReplayGainMode`, `RepeatMode`, `CrossfadeMode`, `MixMode`, `CrossfeedMode`, `ChannelMode` |
| `RockboxFFI::EqPreset` | 21 built-in EQ presets (`FLAT`, `BASS_BOOST`, `ROCK`, `JAZZ`, `VOCAL_BOOST`, …) for `Player#set_eq_preset` |
* Rich values (metadata, status, `dsp_settings`) come back as **Hashes with
symbol keys**.
* Sample buffers are `Array` of interleaved-stereo signed 16-bit
samples.
* `Dsp` and `Player` own native resources. Use the block form (`.open`) to
close them automatically, or call `#close`; a GC finalizer is the backstop.
* The `Player` DSP chain mirrors 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.
`Dsp#set_replaygain` and `Player#set_replaygain` take **different mode
integers**. Use the named constants — `DspReplayGainMode` for the DSP,
`ReplayGainMode` for the player. See [the overview](/bindings/overview#the-one-thing-to-know-two-replaygain-encodings).
## Interactive console
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
bundle exec rake console # or: ./bin/console
```
Drops into IRB with `RockboxFFI` loaded and `FIXTURE` pointing at a sample
track — Tab autocompletion and syntax highlighting are on by default:
```ruby theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
RockboxFFI::Metadata.read(FIXTURE)[:title] # => "Speak"
p = RockboxFFI::Player.new(volume: 0.6)
p.set_queue([FIXTURE]); p.play
p.status[:state] # => "playing"
```
# Swift
Source: https://rockboxzig.mintlify.app/bindings/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:
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`.
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).
The static `RockboxFFI` product force-references each `rb_*` entry point with a
`-u ` 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`.
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
```
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.
**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](/bindings/overview#the-one-thing-to-know-two-replaygain-encodings).
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.
# TypeScript
Source: https://rockboxzig.mintlify.app/bindings/typescript
One typed API for the Rockbox engine across Bun, Deno, and Node.js.
TypeScript bindings for **metadata** parsing (40+ formats), the **DSP**
pipeline (EQ, tone, surround, compressor, ReplayGain, resampler), and a
queue-based **player** with crossfade, shuffle/repeat, and the full Rockbox
DSP chain (EQ presets, tone, crossfeed, surround, compressor, …). One typed
API, three runtimes.
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
npm install rockbox-ffi # Node.js (also pulls in koffi)
bun add rockbox-ffi # Bun
deno add npm:rockbox-ffi # Deno (or import npm:rockbox-ffi/deno)
```
Import the entry point for your runtime — all three expose the identical API:
| Runtime | Import | Notes |
| ------- | -------------------------------------- | ------------------------------------------------ |
| Bun | `import … from "rockbox-ffi/bun"` | uses built-in `bun:ffi` |
| Deno | `import … from "npm:rockbox-ffi/deno"` | run with `--allow-ffi --allow-read --allow-env` |
| Node.js | `import … from "rockbox-ffi/node"` | uses [`koffi`](https://koffi.dev) (a dependency) |
Prefer one import that resolves the right backend at runtime? Use
`import { load } from "rockbox-ffi"` — it returns a promise for the correct
runtime backend.
The published package bundles a prebuilt native library per platform. From a
repo checkout, point at your own build with `ROCKBOX_FFI_LIB`.
## Quick start
```ts theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import {
metadata,
Dsp,
Player,
sineStereo,
DspReplayGainMode,
ReplayGainMode,
CrossfadeMode,
EqPreset,
RepeatMode,
} from "rockbox-ffi/bun"; // or /node, or npm:rockbox-ffi/deno
// --- metadata --------------------------------------------------------
const meta = metadata.read("song.flac");
console.log(meta.artist, "—", meta.title, `${meta.duration_ms} ms`);
console.log(metadata.probe("track.opus")); // "Opus" (extension guess, no I/O)
// --- DSP: process interleaved stereo Int16 ---------------------------
const dsp = new Dsp(44_100);
dsp.eqEnable(true);
dsp.setEqBand(0, /*cutoffHz*/ 60, /*q*/ 0.7, /*gainDb*/ 3.0);
dsp.setReplaygain(DspReplayGainMode.TRACK, /*noclip*/ true, /*preampDb*/ 0.0);
dsp.setReplaygainGains(/*trackGainDb*/ -6.02); // −6 dB ≈ half amplitude
const input = sineStereo(1_000, 1.0, 44_100); // 1 s of a 1 kHz test tone
const output: Int16Array = dsp.process(input);
dsp.close();
// --- playback (needs an audio output device) -------------------------
// Mutating Player methods return `this`, so setup fluently chains.
// Queue entries may be local paths, http(s):// files, or live-radio /
// streaming URLs.
const player = new Player({ volume: 0.8, crossfadeMode: CrossfadeMode.ALWAYS });
player
.setReplaygain(ReplayGainMode.TRACK, 0.0, true)
.setQueue(["a.flac", "https://example.com/b.mp3", "http://radio.example/live"])
.setEqPreset(EqPreset.BassBoost) // one of 21 presets
.setShuffle(true)
.setRepeat(RepeatMode.All)
.play();
console.log(player.status()); // { state: "playing", index: 0, ... }
console.log(player.dspSettings()); // full DSP-chain snapshot
```
## Decode a file to PCM
`Decoder` streams one audio file through the Rockbox codec engine, yielding
interleaved-stereo S16LE PCM (`Int16Array`) one chunk at a time — no output
device required. Same `Metadata` shape as `metadata.read`.
```ts theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import { Decoder } from "rockbox-ffi/bun"; // or /node, or npm:rockbox-ffi/deno
using dec = new Decoder("song.flac"); // freed at end of scope
console.log(dec.metadata().title);
for (let chunk; (chunk = dec.nextChunk()) !== null; ) {
// chunk.samples: interleaved-stereo Int16Array; chunk.sampleRate: Hz
writePcm(chunk.samples);
}
const { done, code } = dec.finished(); // code 0 = clean end, negative = codec error
```
Seek with `dec.seekMs(ms)` and read the codec's position with `dec.elapsedMs()`.
Codec state is **process-wide — only one `Decoder` decodes at a time**.
Constructing a second `Decoder` blocks until the first is closed (via
`close()`, or automatically at the end of a `using` scope).
## API
Everything is fully typed — your editor has the details.
**`metadata`**
| Function | Returns | Description |
| -------------------------- | ---------------- | -------------------------------------------------------- |
| `metadata.read(path)` | `Metadata` | Parse tags, duration, ReplayGain, album-art/cue offsets |
| `metadata.probe(filename)` | `string \| null` | Codec label from the extension, without opening the file |
**`Dsp`** — interleaved-S16LE-stereo processor. Construct with a sample rate,
feed it `Int16Array`s, and `close()` (or `using`) when done.
| Method | Description |
| ------------------------------------------------------- | ------------------------------------------- |
| `process(samples: Int16Array): Int16Array` | Run stereo S16 frames through the pipeline |
| `setInputFrequency(hz)` | Change input rate (engages the resampler) |
| `eqEnable(on)` / `setEqBand(band, cutoffHz, q, gainDb)` | 10-band EQ (band 0 low-shelf, 9 high-shelf) |
| `setEqPrecut(db)` | Negative pre-gain to avoid EQ clipping |
**`Decoder`** — streaming file decoder. Construct with a path, pull PCM with
`nextChunk()`, and `close()` (or `using`) when done. Only one may decode at a
time (see [Decode a file to PCM](#decode-a-file-to-pcm)).
| Method | Description |
| ---------------------------------------------- | --------------------------------------------------------------- |
| `metadata()` | Tags + stream properties (same shape as `metadata.read`) |
| `nextChunk(): { samples, sampleRate } \| null` | Next interleaved-stereo `Int16Array`; `null` at end of track |
| `finished(): { done, code }` | `done` once ended; `code` 0 = clean end, negative = codec error |
| `seekMs(ms)` / `elapsedMs()` | Seek by / read position in milliseconds |
**`Player`** — queue, transport, crossfade, shuffle/repeat, the full DSP
chain, and `status()`. Every mutating method returns `this`, so calls chain
fluently (see the quick start); getters and `close()` return their own values.
| Method | Description |
| ----------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `setQueue(paths)` / `enqueue(path)` | Set / append. Entries: local path, `http(s)://`, or stream URL |
| `insert(paths, position?, index?)` | Insert at an `InsertPosition` |
| `queue()` | The current queue as an array of paths/URLs |
| `play()` / `pause()` / `toggle()` / `stop()` | Transport controls |
| `next()` / `previous()` / `skipTo(index)` / `seekMs(ms)` | Navigate within the queue |
| `setVolume(v)` / `volume()` / `sampleRate()` | Volume + output-rate access |
| `setBalance(balance)` / `balance()` | Stereo balance, −100 (full left) to +100 (full right); 0 = centre |
| `setShuffle(on)` / `isShuffleEnabled()` | Queue shuffle |
| `setRepeat(mode)` / `repeat()` | `RepeatMode` (Off / One / All) |
| `setCrossfade(mode, …)` / `setReplaygain(mode, preampDb, noclip)` | Crossfade + `ReplayGainMode` |
| `setEqEnabled(on)` / `isEqEnabled()` | Toggle the graphic EQ |
| `setEqPreset(preset)` | Apply one of 21 `EqPreset` presets |
| `setEqBand(band, cutoffHz, q, gainDb)` / `setEqPrecut(db)` | Per-band EQ (plain units) + pre-gain |
| `setTone(bassDb, trebleDb, bassHz, trebleHz)` | Bass/treble tone controls at given cutoffs |
| `setBass(db)` / `setTreble(db)` / `setBassCutoff(hz)` / `setTrebleCutoff(hz)` | Individual tone setters |
| `setCrossfeed(mode, directGain, crossGain, hfGain, hfCutoff)` | `CrossfeedMode` (Off / Meier / Custom) |
| `setSurround(delayMs, balance, cutoffLowHz, cutoffHighHz)` | Surround/soundstage effect |
| `setChannelMode(mode)` / `setStereoWidth(percent)` | `ChannelMode` + stereo-width control |
| `setBassEnhancement(strength, precut)` / `setFatigueReduction(strength)` | Bass boost + listening-fatigue reduction |
| `setCompressor(thresholdDb, makeupGain, ratio, knee, attackMs, releaseMs)` | Dynamic-range compressor |
| `setDither(on)` / `setPitch(ratio)` | Dithering + pitch/tempo ratio |
| `dspSettings()` | The current DSP-chain settings as a plain object |
| `status()` | Playback state, index, elapsed, and current track metadata |
| `resume()` / `saveResume()` / `clearResume()` | Persist / restore the queue + exact position |
| `importM3u(path, position?, index?)` / `loadM3u(path)` / `exportM3u(path)` | Playlist (.m3u/.m3u8) import / load / export |
**Enums**
Exported constant objects to pass to the setters above:
`DspReplayGainMode`, `ReplayGainMode`, `CrossfadeMode`, `MixMode`,
`CrossfeedMode`, `InsertPosition`, `ChannelConfig`, `ChannelMode`, `EqPreset`,
and `RepeatMode`.
The `EqPreset` presets, tone/crossfeed/surround/compressor controls, and
their value ranges mirror Rockbox's own sound-settings menu — see the
[official Rockbox manual](https://download.rockbox.org/daily/manual/rockbox-ipodvideo/rockbox-buildch6.html)
for what each effect does.
`Dsp.setReplaygain` and `Player.setReplaygain` use **different mode
integers**. Pass the exported enums — `DspReplayGainMode` for the DSP,
`ReplayGainMode` for the player. See [the overview](/bindings/overview#the-one-thing-to-know-two-replaygain-encodings).
# Desktop app
Source: https://rockboxzig.mintlify.app/clients/desktop
Native desktop clients on macOS (GPUI), Linux (GTK4) and Windows.
Two native desktop clients are maintained alongside `rockboxd`:
* **macOS (GPUI)** — built on Zed's native UI toolkit, ships as a `.dmg`
* **Linux (GTK4)** — distributed as a Flatpak
Both connect to a running `rockboxd` instance over GraphQL.
## macOS (GPUI)
Download the latest `.dmg` from the
[Releases page](https://github.com/tsirysndr/rockboxd/releases/latest)
or build from source:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
cd gpui
cargo run --release
```
Features:
* Full library browsing and search
* Drag-and-drop queue management
* Native macOS media keys & Now Playing card via MPRIS-equivalent integration
* Right-click context menus for tracks, albums and folders
* Dark / light mode follows the system
## Linux (GTK4)
The GTK4 client is published as a Flatpak. Build from source:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
sudo apt-get install flatpak
flatpak remote-add --if-not-exists --user flathub \
https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak install --user flathub org.flatpak.Builder
flatpak install --user flathub org.gnome.Sdk/x86_64/47
flatpak install --user flathub org.gnome.Platform/x86_64/47
flatpak install --user org.freedesktop.Sdk.Extension.rust-stable
flatpak install --user org.freedesktop.Sdk.Extension.llvm18
cd gtk
flatpak run org.flatpak.Builder \
--user --disable-rofiles-fuse --repo=repo \
flatpak_app build-aux/io.github.tsirysndr.Rockbox.json --force-clean
flatpak run org.flatpak.Builder \
--run flatpak_app build-aux/io.github.tsirysndr.Rockbox.json rockbox-gtk
```
## Connecting to a remote rockboxd
Both desktop clients accept a non-default host and port — useful when
`rockboxd` runs on a NAS or Raspberry Pi:
* macOS — Settings → Connection → Host / Port
* GTK — `Preferences` → Server
If you put rockboxd behind a reverse proxy with TLS, use the `httpUrl` /
`wsUrl` overrides under Connection settings.
# MPD clients
Source: https://rockboxzig.mintlify.app/clients/mpd
Rockbox speaks the Music Player Daemon protocol on port 6600.
`rockboxd` runs a built-in MPD server on port **6600**, so any of the dozens
of MPD clients work out of the box — `mpc`, `ncmpcpp`, MALP, M.A.L.P.,
Cantata, Mopidy, Volumio's UI, etc.
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
mpc -h localhost -p 6600 status
mpc -h localhost -p 6600 update
mpc -h localhost -p 6600 search title "money"
mpc -h localhost -p 6600 add /Music/Pink\ Floyd/Money.mp3
mpc -h localhost -p 6600 play
```
## Compatibility
The implementation lives in `crates/mpd/`. It targets the standard MPD
protocol version. Coverage:
* ✅ Status, current/next song, elapsed time
* ✅ Playback control: play/pause/next/previous/seek/stop
* ✅ Queue (`add`, `addid`, `delete`, `clear`, `shuffle`, `move`)
* ✅ Library (`list`, `find`, `search`, `lsinfo`)
* ✅ Saved playlists (`load`, `save`, `rm`, `playlistadd`)
* ✅ Volume, repeat, random, single, consume
* ✅ `idle` notifications (player, playlist, mixer, etc.)
* ⚠️ Sticker database — partial
* ⚠️ Outputs — single output exposed (the active sink)
## Configuring the bind address
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
mpd_host = "0.0.0.0"
mpd_port = 6600
```
## Recommended clients
| Client | Platform | Notes |
| --------- | ----------------------- | ------------------------------------ |
| `mpc` | CLI | Scripting, smoke-testing |
| `ncmpcpp` | TUI | The classic terminal client |
| MALP | Android | Material Design, modern feel |
| M.A.L.P. | Android (F-Droid) | F-Droid build of MALP |
| Cantata | Linux / Windows / macOS | Full GUI client |
| Mopidy | Linux | Acts as an MPD-compatible aggregator |
## Idle subscriptions
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
mpc -h localhost -p 6600 idle player playlist mixer
```
Returns one event per change. The `subscribe`/`channels` channel commands
are also implemented for client-to-client messaging.
## Library updates
`update` triggers a full rescan of `music_dir` exactly like the
[GraphQL `scanLibrary` mutation](/api-reference/graphql/library). New files
appear in MPD listings as soon as the scan finishes.
# MPRIS
Source: https://rockboxzig.mintlify.app/clients/mpris
Linux media keys and desktop integration via D-Bus.
On Linux, `rockboxd` registers itself on the session bus as
`org.mpris.MediaPlayer2.rockbox`. This makes media keys, Now Playing
applets, KDE/GNOME notifications and tools like `playerctl` Just Work.
## Capabilities
The MPRIS interface is implemented in `crates/mpris/` and exposes:
* **Player interface** — Play, Pause, PlayPause, Stop, Next, Previous, Seek,
SetPosition, Metadata, PlaybackStatus, LoopStatus, Shuffle, Volume
* **Root interface** — Identity, DesktopEntry, SupportedUriSchemes,
SupportedMimeTypes
* **TrackList interface** — partial (track-level queue introspection)
## Quick test
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
playerctl --player rockbox play-pause
playerctl --player rockbox metadata
playerctl --player rockbox position 90
```
## Wiring up media keys
GNOME and KDE pick up MPRIS players automatically — no setup needed. For
sway / Hyprland / dwm:
```text theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
# sway
bindsym XF86AudioPlay exec playerctl --player rockbox play-pause
bindsym XF86AudioNext exec playerctl --player rockbox next
bindsym XF86AudioPrev exec playerctl --player rockbox previous
```
## Now Playing applet
GNOME Shell, KDE's Media Player widget, polybar's `mpris-tail`, waybar's
`mpris` module — all consume MPRIS metadata directly.
## macOS
macOS has its own Now Playing system. Rockbox publishes to it through the
`MPNowPlayingInfoCenter` API; you don't need MPRIS on Mac. Media keys work
out of the box.
# Web UI
Source: https://rockboxzig.mintlify.app/clients/web
Browser-based controller served by rockboxd on port 6062.
The web UI is a React app served from the GraphQL HTTP listener. Open
[http://localhost:6062](http://localhost:6062) once `rockboxd` is running.
## Features
* Browse artists, albums and tracks from the tag database
* Browse the filesystem under `music_dir`
* Search powered by Typesense (instant results as you type)
* Manage the live playback queue and saved playlists
* Reorder, shuffle, repeat
* Like / unlike tracks and albums
* Pick the active output device (Chromecast, AirPlay, Snapcast TCP, …)
* Real-time "now playing" updates over a GraphQL subscription
## Building from source
The web UI ships pre-built with the `rockboxd` binary, but you can rebuild
it locally:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
cd webui/rockbox
deno install
deno run build
```
The result lands in `webui/rockbox/build/` and is embedded into rockboxd at
link time.
## Customising the bind address
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
graphql_host = "0.0.0.0" # default 0.0.0.0
graphql_port = 6062 # default 6062
```
Restart rockboxd, then reach the UI at `http://:`.
## Storybook
Component-level documentation is on Chromatic:
[storybook ↗](https://master--670ceec25af685dcdc87c0df.chromatic.com/?path=/story/components-albums--default).
# Configuration
Source: https://rockboxzig.mintlify.app/configuration
Configure Rockbox via ~/.config/rockbox.org/settings.toml.
Rockbox reads `~/.config/rockbox.org/settings.toml` once on startup. Edit the
file, then `rockbox restart`. There is no live-reload; the API is the way to
change things at runtime.
## Minimal config
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
music_dir = "/path/to/your/Music"
audio_output = "builtin"
```
`music_dir` is the only required field. `audio_output` defaults to `"builtin"`
(CPAL) if omitted.
## Top-level keys
| Key | Type | Default | Description |
| -------------- | ------ | ----------- | ----------------------------------------------------------------------------------------------------------------------- |
| `music_dir` | string | — | Absolute path to your music library |
| `audio_output` | string | `"builtin"` | One of: `builtin`, `cmaf` (alias `hls`, `dash`), `fifo`, `airplay`, `squeezelite`, `chromecast`, `snapcast_tcp`, `upnp` |
| `player_name` | string | `""` | Name advertised to MPD clients and UI |
## Output sinks
Each sink has its own configuration block. See the dedicated pages:
Default. No setup.
Plays in any browser.
FIFO or direct TCP.
Single or multi-room RAOP.
Slim Protocol multi-room.
Google Cast over WAV/HTTP.
Sink, server, renderer.
## Playback defaults
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
playlist_shuffle = false
repeat_mode = 1 # 0=Off 1=All 2=One 3=Shuffle 4=A-B
party_mode = true
```
## Equalizer
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
eq_enabled = true
[[eq_band_settings]] # band 0 — low shelf
cutoff = 0
q = 64
gain = 10
[[eq_band_settings]] # bands 1–8 — peaking
cutoff = 3
q = 125
gain = 10
# ...repeat for the remaining bands
```
The full 10-band parametric EQ is documented in
[Audio settings › Equalizer](/audio-settings/equalizer).
## Crossfade
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
crossfade = 5
fade_on_stop = false
fade_in_delay = 2
fade_in_duration = 7
fade_out_delay = 4
fade_out_duration = 0
fade_out_mixmode = 2
```
## Tone & stereo
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
bass = 0
treble = 0
bass_cutoff = 0
treble_cutoff = 0
balance = 0
stereo_width = 100
stereosw_mode = 0
channel_config = 0
surround_enabled = 0
surround_balance = 0
surround_fx1 = 0
surround_fx2 = 0
```
## ReplayGain
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
[replaygain_settings]
noclip = true
type = 0 # 0=Track 1=Album 2=Shuffle (see Replaygain page)
preamp = 0
```
## Compressor
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
[compressor_settings]
threshold = -24
makeup_gain = 0
ratio = 4
knee = 1
release_time = 300
attack_time = 5
```
## Where settings come from
There are three layers, in order of precedence:
1. **Runtime API calls** — every setting is also exposed over GraphQL/gRPC
and persists to disk on the next save cycle.
2. **`settings.toml`** — applied once at startup.
3. **Compiled-in defaults** — in `apps/settings_list.c`.
For the full settings catalogue with units, ranges and where each one is
applied, see the [Settings TOML reference](/reference/settings-toml).
# Rockbox Daemon
Source: https://rockboxzig.mintlify.app/index
A modern wrapper around the Rockbox audio engine, exposed over gRPC, GraphQL, HTTP and MPD — with multi-room AirPlay, Snapcast, Squeezelite, Chromecast and UPnP.
Rockbox Daemon is a modern take on the [Rockbox](https://www.rockbox.org) open-source
audio player firmware. The Rockbox C engine — gapless playback, DSP, 20+ codecs,
and the tag database — in a single binary (rockboxd) that expose **gRPC**,
**GraphQL**, **HTTP** and **MPD** APIs, add a **Typesense**-backed search engine,
and stream audio to **AirPlay**, **Snapcast**, **Squeezelite**, **Chromecast**
and **UPnP** receivers.
Everything ships as a single binary called `rockboxd` (all codecs statically linked).
Install Rockbox and play your first track in under five minutes.
Built-in CPAL, HLS + DASH (browser), AirPlay, Snapcast, Squeezelite, Chromecast, UPnP/DLNA.
GraphQL, HTTP REST, gRPC and MPD — pick your protocol.
TypeScript, Python, Ruby, Elixir, Clojure and Gleam.
## What's inside
Rockbox C firmware — gapless playback, parametric EQ, crossfade, ReplayGain,
PBE, Haas surround, dithering, and the rbcodec DSP pipeline.
MP3, OGG, FLAC, WAV, AAC, Opus, ALAC, Musepack, WMA, APE, Wavpack, Speex
and many more.
Synchronised playback to AirPlay receivers, Snapcast clients,
Squeezelite devices, Chromecasts and DLNA renderers — all at once.
SQLite-backed tag database, instant Typesense search, browse-by-folder
and ReplayGain-aware metadata.
gRPC on `:6061`, GraphQL on `:6062`, REST on `:6063`, MPD on `:6600`,
Subsonic on `:4533`, Jellyfin on `:8096` (opt-in),
S3-compatible upload/delete on `:9000`, plus MPRIS and UPnP/DLNA.
Native macOS (GPUI), GTK4 desktop app, React web UI, terminal TUI and
a Rockbox REPL.
## How it fits together
Web UI · GTK · GPUI · TUI · REPL · MPD · MPRIS
gRPC :6061 · GraphQL :6062 · REST :6063 · MPD :6600 · Subsonic :4533
playback · library · settings · search · playlists · airplay · slim · chromecast · upnp · netstream
audio engine · DSP · codecs · tag database
builtin · cmaf (HLS + DASH) · fifo · airplay · squeezelite · chromecast · snapcast\_tcp · upnp
## Status
Rockbox Daemon is actively developed. The audio engine, library, search, MPD
server and the AirPlay / Snapcast / Squeezelite / Chromecast sinks are all
production-ready. Mobile clients and Wasm extensions are on the roadmap.
Looking for the source? It lives at
[github.com/tsirysndr/rockboxd](https://github.com/tsirysndr/rockboxd).
Issues and PRs welcome.
# Installation
Source: https://rockboxzig.mintlify.app/installation
Install pre-built binaries or build Rockbox Daemon from source.
The recommended path is to install a pre-built binary for your platform.
Building from source is documented in [Architecture › Build](/architecture/build).
## Pre-built binaries
Pre-built packages for the latest release are on the
[Releases page](https://github.com/tsirysndr/rockboxd/releases/latest).
| Platform | Architecture | Package |
| -------- | ----------------------- | --------------- |
| Linux | x86\_64 | `.tar.gz` |
| Linux | aarch64 | `.tar.gz` |
| macOS | x86\_64 | `.pkg` |
| macOS | aarch64 (Apple Silicon) | `.pkg` + `.dmg` |
## Package managers
### Ubuntu / Debian
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
echo "deb [trusted=yes] https://apt.fury.io/tsiry/ /" | sudo tee /etc/apt/sources.list.d/fury.list
sudo apt-get update
sudo apt-get install rockbox
```
### Fedora
Add the following to `/etc/yum.repos.d/fury.repo`:
```ini theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
[fury]
name=Gemfury Private Repo
baseurl=https://yum.fury.io/tsiry/
enabled=1
gpgcheck=0
```
Then:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
sudo dnf install rockbox
```
### macOS (Homebrew)
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
brew install tsirysndr/tap/rockbox
```
### macOS desktop app (Homebrew cask)
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
brew install --cask tsirysndr/tap/rockbox
```
Installs `Rockbox.app` into `/Applications` — the native desktop client with
the daemon embedded, so no separate `rockboxd` process is needed.
### Arch Linux
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
paru -S rockboxd-bin
```
### Universal installer
The installer detects your OS and architecture and drops the right binary
under `/usr/local/bin`:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
curl -fsSL https://raw.githubusercontent.com/tsirysndr/rockboxd/HEAD/install.sh | bash
```
### Docker
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
docker pull tsiry/rockbox
```
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
docker run --rm -it \
-p 6060-6063:6060-6063 \
-p 6600:6600 \
-p 7878-7881:7878-7881 \
-p 3483:3483 \
-p 9999:9999 \
-p 1704:1704 \
-p 1705:1705 \
-p 1780:1780 \
-v $HOME/Music:/root/Music \
tsiry/rockbox
```
For AirPlay, Squeezelite and UPnP discovery (mDNS / SSDP) you'll usually want
`--network host` instead of port mapping — multicast does not cross Docker's
default bridge network.
## Run as a systemd service
Once `rockbox` is on your `PATH`:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rockbox service install # enable and start
rockbox service status # check status
rockbox service uninstall # stop and disable
```
This installs a user-level systemd unit at `~/.config/systemd/user/rockboxd.service`.
## Verifying the install
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rockbox --version
rockbox start
```
The daemon binds these ports by default:
| Service | Port | Protocol |
| ---------------- | ---- | --------------- |
| gRPC | 6061 | gRPC / gRPC-Web |
| GraphQL + Web UI | 6062 | HTTP / WS |
| HTTP REST | 6063 | HTTP |
| MPD | 6600 | MPD |
See the full [port reference](/reference/ports).
## Building from source
If you want to hack on Rockbox Daemon itself, see
[Architecture › Build](/architecture/build) for the Make + Cargo + Zig
toolchain.
## Uninstall
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
sudo apt-get remove rockbox
rockbox service uninstall # if you installed the systemd unit
```
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
sudo dnf remove rockbox
rockbox service uninstall
```
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
sudo pacman -Rns rockboxd-bin
```
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
brew uninstall rockbox
```
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
brew uninstall --cask rockbox
```
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
sudo rm /usr/local/bin/rockbox /usr/local/bin/rockboxd
sudo pkgutil --forget io.github.tsirysndr.rockbox
```
# Quickstart
Source: https://rockboxzig.mintlify.app/quickstart
Install Rockbox, point it at your music, and play your first track.
This guide gets you from zero to a running `rockboxd` with a usable web UI in
about five minutes.
## 1. Install
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
brew install tsirysndr/tap/rockbox
```
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
echo "deb [trusted=yes] https://apt.fury.io/tsiry/ /" | sudo tee /etc/apt/sources.list.d/fury.list
sudo apt-get update
sudo apt-get install rockbox
```
Add `/etc/yum.repos.d/fury.repo`:
```ini theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
[fury]
name=Gemfury Private Repo
baseurl=https://yum.fury.io/tsiry/
enabled=1
gpgcheck=0
```
Then:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
sudo dnf install rockbox
```
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
paru -S rockboxd-bin
```
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
docker run -v $HOME/Music:/root/Music \
-p 6062:6062 \
-p 7882:7882 \
tsiry/rockbox
```
Open the web UI at [http://localhost:6062](http://localhost:6062) and start
playing — no config file, no external client. The page attaches to the
HLS stream on port `7882` automatically and audio plays directly in the
browser.
Prefer the terminal? Any HLS-capable player works:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
ffplay http://localhost:7882/hls/master.m3u8
```
Port `6062` serves the web UI and GraphQL API.
Port `7882` serves the HLS playlist (`/hls/master.m3u8`) and DASH manifest
(`/dash/manifest.mpd`) — both are produced from the same in-memory CMAF
(fMP4 + AAC-LC) ring buffer.
The image also ships `snapserver` for users who prefer Snapcast multi-room.
Set `audio_output = "fifo"` in `~/.config/rockbox.org/settings.toml` and
add `-p 1704:1704 -p 1705:1705 -p 1780:1780` to the `docker run` command,
then connect with `snapclient tcp://localhost`.
For full distribution coverage, build instructions and Docker images, see
[Installation](/installation).
## 2. Point it at your music
Create `~/.config/rockbox.org/settings.toml`:
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
music_dir = "/path/to/your/Music"
audio_output = "builtin"
```
That's the minimum. `music_dir` is the only required field; everything else
has sensible defaults. See [Configuration](/configuration) for the full list.
## 3. Start `rockboxd`
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rockbox start
```
You should see logs like:
```
INFO rockbox-cli rockboxd starting...
INFO rockbox-cli graphql server listening on :6062
INFO rockbox-cli http server listening on :6063
INFO rockbox-cli mpd server listening on :6600
INFO rockbox-cli grpc server listening on :6061
```
## 4. Open a client
Browse your library and control playback at
[http://localhost:6062](http://localhost:6062).
Explore the API live at
[http://localhost:6062/graphiql](http://localhost:6062/graphiql).
Point any MPD client (`ncmpcpp`, `mpc`, MALP, etc.) at `localhost:6600`.
Quick `curl` testing — see the REST overview.
## 5. Play something
From the web UI, search for a track and hit play. From the terminal:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
mpc -h localhost -p 6600 update
mpc -h localhost -p 6600 search title "money"
mpc -h localhost -p 6600 play
```
Or with the [TypeScript SDK](/sdks/typescript):
```ts theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import { RockboxClient } from '@rockbox-zig/sdk';
const client = new RockboxClient();
const { albums } = await client.library.search('dark side');
await client.playback.playAlbum(albums[0].id, { shuffle: true });
```
## Next steps
`settings.toml` reference — every key, every default.
Send audio to AirPlay, Snapcast, Squeezelite, Chromecast or UPnP.
Build clients in TypeScript, Python, Ruby, Elixir, Clojure or Gleam.
Common gotchas and how to fix them.
# CLI reference
Source: https://rockboxzig.mintlify.app/reference/cli
rockbox and rockboxd — the two binaries you'll interact with.
There are two binaries:
* **`rockbox`** — user-facing wrapper. Starts the server, scans the
library, opens the web UI, manages the systemd service, runs JS/TS
scripts, and acts as a Bluetooth client on Linux.
* **`rockboxd`** — the daemon itself. Linked by Zig from the C firmware
* Rust crates + CPAL. Lives at `zig/zig-out/bin/rockboxd` after a build,
or `/usr/local/bin/rockboxd` after install.
Most users only ever invoke `rockbox`; `rockboxd` is the underlying
process it spawns.
## `rockbox`
```text theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rockbox [--rebuild] [SUBCOMMAND]
```
Running `rockbox` with no subcommand starts the server. The subcommands
below are dispatched in `cli/src/main.rs`.
### Global flags
| Flag | Description |
| ----------------- | --------------------------------------------- |
| `--rebuild`, `-r` | Rebuild the Typesense search index after scan |
| `-h`, `--help` | Print help |
| `-V`, `--version` | Print the version |
### Subcommands
| Subcommand | Aliases | Description |
| ------------------------------ | ------- | -------------------------------------------------------- |
| *(none)* | | Start the server |
| `start [-r]` | | Start the server |
| `scan [-d PATH] [-r]` | | Scan a library directory; `-r` rebuilds the search index |
| `webui` | `web` | Open the web UI in your browser |
| `tui` | | Start the terminal UI |
| `repl` | `shell` | Start the Rockbox REPL |
| `run ` | `x` | Run a JS/TS script via Deno against the local rockboxd |
| `open ` | | Play a local file or HTTP URL directly |
| `clear` | | Clear the current playlist |
| `service install` | | Install + enable the systemd unit |
| `service uninstall` | | Disable + remove the systemd unit |
| `service status` | | Show the unit status |
| `login ` | `auth` | Log in to Rocksky (BlueSky handle) |
| `whoami` | `me` | Show the currently logged-in user |
| `community` | | Open the Discord invite |
| `setup` | | Install host dependencies (CPAL, etc.) |
| `bluetooth scan [--timeout S]` | | Linux only — scan for Bluetooth devices |
| `bluetooth devices` | | Linux only — list known devices |
| `bluetooth connect ` | | Linux only — connect to a device |
| `bluetooth disconnect ` | | Linux only — disconnect a device |
### Examples
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
# Start the daemon
rockbox start
# Scan the default music_dir, rebuild the search index
rockbox scan -r
# Scan a specific path
rockbox scan -d "/Volumes/Music/Recently Added"
# Play a URL
rockbox open "https://stream.example.com/jazz.mp3"
# Install as a systemd user service
rockbox service install
rockbox service status
# Run a small script against a local rockboxd
rockbox run scripts/scrobble.ts
# Bluetooth (Linux)
rockbox bluetooth scan --timeout 15
rockbox bluetooth connect AA:BB:CC:DD:EE:FF
```
## `rockboxd`
The daemon. Usually you don't run it directly — `rockbox start` or the
systemd unit handles it. When you do need to invoke it manually, most
configuration is driven by environment variables and
`~/.config/rockbox.org/settings.toml`. A small set of subcommands are
also available for account management and settings sync; they exit the
process immediately without starting the Zig firmware or any servers.
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rockboxd [SUBCOMMAND]
```
### Subcommands
| Subcommand | Description |
| --------------------------------------- | ----------------------------------------------------- |
| *(none)* | Start the daemon (server + firmware) |
| `login ` | Log in to Rocksky with your Bluesky handle |
| `whoami` | Print the currently logged-in Rocksky user |
| `settings pull [--did ]` | Pull audio settings from Rocksky into `settings.toml` |
| `settings push` | Push `settings.toml` audio settings to Rocksky |
#### `login `
Opens the Rocksky OAuth authorisation URL in your default browser and
listens on `localhost:6996` for the callback. On success the token is
saved to `~/.config/rockbox.org/token`.
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rockboxd login alice.bsky.social
```
#### `whoami`
Reads the stored token and resolves the Rocksky account associated with it.
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rockboxd whoami
```
#### `settings pull`
Downloads your audio settings (equalizer, crossfade, replaygain, tone)
from Rocksky and merges them into `~/.config/rockbox.org/settings.toml`.
All other fields in `settings.toml` (e.g. `music_dir`, `audio_output`)
are left untouched. Restart `rockboxd` to apply the new settings.
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
# Pull your own settings (requires login)
rockboxd settings pull
# Pull another user's settings publicly — no login required
rockboxd settings pull --did alice.bsky.social
rockboxd settings pull --did did:plc:abc123
```
#### `settings push`
Reads the audio sections of `~/.config/rockbox.org/settings.toml` and
uploads them to Rocksky. Requires a stored token (`rockboxd login` first).
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rockboxd settings push
```
### Environment variables
| Variable | Default | Description |
| ------------------------ | ---------------------- | --------------------------------------- |
| `RUST_LOG` | `info` | Tracing filter (per-crate supported) |
| `ROCKBOX_TCP_PORT` | `6063` | HTTP REST bind port |
| `ROCKBOX_GRAPHQL_PORT` | `6062` | GraphQL bind port |
| `ROCKBOX_RPC_PORT` | `6061` | gRPC bind port |
| `ROCKBOX_MPD_PORT` | `6600` | MPD bind port |
| `ROCKBOX_LIBRARY` | `$HOME/Music` | Default music library path |
| `ROCKBOX_ADDR` | (auto-detected LAN IP) | Address advertised to external players |
| `ROCKBOX_UPDATE_LIBRARY` | unset | When `1`, rebuild Typesense on startup |
| `HOME` | (system) | Used to derive config and library paths |
### Stdout / stderr
* **Stderr** — `tracing` log output. Always safe to redirect or filter.
* **Stdout** — normally empty, **except** when `audio_output = "fifo"`
with `fifo_path = "-"`, in which case stdout is raw S16LE 44.1 kHz PCM.
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rockboxd | ffplay -f s16le -ar 44100 -ac 2 -
```
### Logging
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
RUST_LOG=debug rockboxd
RUST_LOG=rockbox_airplay=debug,rockbox_slim=debug,info rockboxd
```
Never use `eprintln!` / `println!` from inside the codebase — they
bypass the structured filter and pollute stdout (which breaks FIFO
mode). Use `tracing::{debug,info,warn,error}!` in Rust code.
### Files
| Path | Purpose |
| ----------------------------------------- | ---------------------------------------- |
| `~/.config/rockbox.org/settings.toml` | Persistent configuration |
| `~/.config/rockbox.org/library.db` | SQLite library + listening stats |
| `~/.config/rockbox.org/playlists/` | Saved playlists |
| `~/.config/rockbox.org/token` | Rocksky OAuth token (written by `login`) |
| `~/.config/systemd/user/rockboxd.service` | systemd unit (after `service install`) |
# FAQ
Source: https://rockboxzig.mintlify.app/reference/faq
Common questions about Rockbox Daemon.
[Rockbox](https://www.rockbox.org) is firmware for portable audio
players. Rockbox Daemon wraps that same C audio engine in Rust and Zig
services, exposing it on a desktop/server as a single `rockboxd` binary
with gRPC, GraphQL, HTTP and MPD APIs and multi-room output sinks.
The DSP, codecs and tag database come straight from upstream Rockbox.
Yes. Linux ARM64 builds are on the
[Releases page](https://github.com/tsirysndr/rockboxd/releases). It
runs comfortably on a Pi 4; on a Pi 3 expect Typesense indexing to be
slower on first scan but playback is fine.
MP3, OGG Vorbis, FLAC, WAV, AAC, ALAC, Opus, Musepack, WMA, APE,
Wavpack, Speex, AIFF, AC3, SID and several more — 20+ codecs total. The
codec list comes from upstream Rockbox; see
[`AUDIO_EXTENSIONS`](https://github.com/tsirysndr/rockboxd/blob/master/crates/server/src/lib.rs)
for what is auto-scanned into the library.
Not yet. Generic HTTP(S) stream URLs work — you can queue them and
playback works through the netstream layer in `crates/netstream/`. Rich
provider integrations (YouTube, Spotify, Tidal) are on the roadmap.
You can — they're great projects. Rockbox Daemon differs in that the
audio engine, DSP, parametric EQ and crossfade are the upstream Rockbox
implementation rather than ALSA's defaults. If you specifically want
Rockbox's sound (dithering, PBE, Haas surround, ReplayGain pipeline, the
EQ presets) on a desktop or server, this is one way to get it.
* **AirPlay** — pick this if you have Apple TVs / HomePods / shairport-sync
receivers. Built-in fan-out, \~8 ms tight sync.
* **Snapcast** — best when you have or are willing to deploy snapserver and
multiple snapclients. Works on every platform, very tight sync.
* **Squeezelite** — pick this if you already run squeezelite or
Logitech-style hardware. One rockboxd serves any number of squeezelite
clients with per-client cursors into a 4 MB shared buffer.
Probably. Rockbox runs an MPD-compatible server on port 6600. `mpc`,
`ncmpcpp`, MALP, M.A.L.P. and Cantata are all tested. If your client
breaks on something Rockbox-specific, please open an issue.
Yes — `rockbox service install` registers a user-level systemd unit. See
[Installation](/installation#run-as-a-systemd-service).
SQLite, in `~/.config/rockbox.org/library.db`. The
[smart playlist rules](/sdks/typescript) read from this database.
You can record `played` / `skipped` events manually from any SDK or via
the REST endpoints `POST /track-stats/{id}/played` and
`POST /track-stats/{id}/skipped`.
Simpler to deploy, simpler to debug, and the firmware/Rust boundary is
already complex enough that adding IPC on top would be a step backward.
The C audio engine and the Rust services share memory through static
libraries linked by Zig — see [Architecture](/architecture/overview).
On Linux, yes — pairing/connecting is exposed through the REST and
GraphQL APIs (and SDKs). On macOS and Windows, no first-party
integration; use the OS-level Bluetooth stack and route the built-in
CPAL output to the BT device.
Yes. Every SDK ships a Jellyfin-style plugin lifecycle:
```ts theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
const SleepTimer = (minutes: number): RockboxPlugin => ({
name: 'sleep-timer',
version: '1.0.0',
install({ events, query }) {
setTimeout(() => query('mutation { hardStop }'), minutes * 60_000);
},
});
await client.use(SleepTimer(30));
```
Wasm extensions inside `rockboxd` itself are on the roadmap.
Read the [Contributing guide](https://github.com/tsirysndr/rockboxd/blob/master/CONTRIBUTING.md),
hop into [Discord](https://discord.gg/tXPrgcPKSt), and open a PR. Build
from source with the [build instructions](/architecture/build).
# Ports
Source: https://rockboxzig.mintlify.app/reference/ports
Every TCP and UDP port rockboxd binds, plus mDNS/SSDP service types.
| Service | Port | Protocol | Override |
| ----------------------------------- | ---- | --------------- | ---------------------------------------- |
| gRPC | 6061 | gRPC / gRPC-Web | `ROCKBOX_RPC_PORT` |
| GraphQL + Web UI | 6062 | HTTP / WS | `ROCKBOX_GRAPHQL_PORT` |
| HTTP REST | 6063 | HTTP | `ROCKBOX_TCP_PORT` |
| MPD server | 6600 | MPD protocol | `ROCKBOX_MPD_PORT` |
| Subsonic / Navidrome API | 4533 | HTTP | `subsonic_port` |
| S3-compatible API | 9000 | HTTP | `s3_port` |
| Jellyfin-compatible API (opt-in) | 8096 | HTTP | `jellyfin_port` — omit to disable |
| Jellyfin client discovery | 7359 | UDP | (fixed — bound when Jellyfin is enabled) |
| CMAF (HLS + DASH) | 7882 | HTTP | `cmaf_http_port` |
| Slim Protocol (squeezelite) | 3483 | TCP | `squeezelite_port` |
| HTTP PCM stream (squeezelite) | 9999 | HTTP | `squeezelite_http_port` |
| Chromecast WAV stream | 7881 | HTTP | `chromecast_http_port` |
| UPnP MediaServer (ContentDirectory) | 7878 | HTTP / SSDP | `upnp_server_port` |
| UPnP WAV broadcast (PCM sink) | 7879 | HTTP | `upnp_http_port` |
| UPnP MediaRenderer (AVTransport) | 7880 | HTTP / SSDP | `upnp_renderer_port` |
| Snapcast TCP source (outbound only) | 4953 | TCP (client) | `snapcast_tcp_port` |
## mDNS / SSDP service types
Rockbox both **advertises** and **scans for** the following on the LAN:
| Service | Service type | Direction |
| ---------------------------- | --------------------------------------------- | -------------- |
| Rockbox itself | `_rockbox._tcp.local.` | advertise |
| Jellyfin server (this one) | `_jellyfin._tcp.local.` | advertise |
| Chromecast | `_googlecast._tcp.local.` | scan |
| AirPlay (RAOP) | `_raop._tcp.local.` | scan |
| Squeezelite players | `_slim._tcp.local.` | scan |
| Snapcast servers | `_snapcast._tcp.local.` | scan |
| UPnP renderers | `urn:schemas-upnp-org:device:MediaRenderer:1` | SSDP scan |
| UPnP media server (this one) | `urn:schemas-upnp-org:device:MediaServer:1` | SSDP advertise |
## Firewall checklist
If rockboxd is in a VM, container, or behind a firewall, **at minimum**
inbound on these is needed:
* `6062/tcp` — for the web UI and GraphQL
* `6063/tcp` — for the REST API
* `6600/tcp` — for MPD clients
* `4533/tcp` — for Subsonic/Navidrome clients (Cassette, Symfonium, DSub, …)
* `8096/tcp` — for Jellyfin clients (Finamp, Findroid, Streamyfin, Amcfy, Symfonium); also `7359/udp` if you want client auto-discovery
* `9000/tcp` — for the S3-compatible API (awscli, mc, rclone) when `s3_enabled = true`
* `7882/tcp` — for HLS / DASH in-browser playback (when `audio_output = "cmaf"`)
* `5353/udp` — mDNS (multicast)
* `1900/udp` — SSDP (multicast)
If you use Chromecast or AirPlay, the receiver must also be able to
**reach back** to rockboxd on `7881/tcp` (Chromecast WAV) or
`7879/tcp` (UPnP WAV).
# Settings sync
Source: https://rockboxzig.mintlify.app/reference/settings-sync
Back up and share your audio settings via Rocksky.
Rockbox Daemon can sync your audio settings — equalizer, crossfade, ReplayGain, and tone controls — with [Rocksky](https://rocksky.app), a social layer built on the AT Protocol (Bluesky). Once pushed, your settings are reachable from any device you own, and can optionally be made public so others can pull them by handle.
## Prerequisites
* `rockboxd` installed and on your `$PATH`
* A [Bluesky](https://bsky.app) account (used as the Rocksky identity)
## Step 1 — Log in
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rockboxd login your.handle.bsky.social
```
This opens the Rocksky authorisation page in your browser. After you approve access, a local callback server on port `6996` captures the token and writes it to `~/.config/rockbox.org/token`. You only need to do this once per machine.
Verify the login succeeded:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rockboxd whoami
```
## Step 2 — Push your settings
Configure your audio settings (`settings.toml`, or via the web/desktop UI), then upload them:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rockboxd settings push
```
The following sections of `~/.config/rockbox.org/settings.toml` are uploaded:
| Section | Fields synced |
| ---------- | ----------------------------------------------------------------------------------------------------------- |
| Equalizer | `eq_enabled`, `eq_precut`, `eq_band_settings` (cutoff / gain / Q per band) |
| Crossfade | `crossfade`, `fade_in_delay`, `fade_in_duration`, `fade_out_delay`, `fade_out_duration`, `fade_out_mixmode` |
| ReplayGain | `replaygain_settings.type`, `replaygain_settings.preamp`, `replaygain_settings.noclip` |
| Tone | `bass`, `treble`, `balance`, `channel_config` |
All other fields (`music_dir`, `audio_output`, etc.) are never uploaded.
## Step 3 — Pull your settings on another device
Log in on the second device, then pull:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rockboxd settings pull
```
This merges the downloaded values into the local `settings.toml` without touching any other fields. Restart `rockboxd` to apply them.
## Sharing settings publicly
Any user's settings can be pulled without logging in by passing their DID or Bluesky handle:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
# by handle
rockboxd settings pull --did alice.bsky.social
# by DID
rockboxd settings pull --did did:plc:abc123xyz
```
This is useful for sharing EQ presets or replication of a tuned listening profile across devices owned by different accounts.
## Token management
| File | Contents |
| ----------------------------- | ------------------------- |
| `~/.config/rockbox.org/token` | Bearer token (plain text) |
To log out, delete the token file:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rm ~/.config/rockbox.org/token
```
## Troubleshooting
**"Session expired or invalid"** — the stored token has been revoked or expired. Run `rockboxd login ` again.
**"No settings file"** on `settings push` — pull first to create the file, or configure `settings.toml` manually and run push again.
**Port 6996 already in use** — the login callback server cannot bind. Stop whatever is listening on that port and retry.
# settings.toml reference
Source: https://rockboxzig.mintlify.app/reference/settings-toml
Every key Rockbox reads from ~/.config/rockbox.org/settings.toml.
This is the canonical list of keys recognised by `rockbox_settings::load_settings()`.
Keys not listed here are ignored.
## Core
| Key | Type | Default | Description |
| -------------- | ------ | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
| `music_dir` | string | — | Absolute path to your music library |
| `audio_output` | string | `"builtin"` | `builtin` / `cmaf` (alias `hls`, `dash`) / `fifo` / `airplay` / `squeezelite` / `chromecast` / `snapcast_tcp` / `upnp` |
| `player_name` | string | `""` | Name advertised to MPD clients and the UI |
## Subsonic / Navidrome API server
| Key | Type | Default | Description |
| ------------------- | ------ | ------- | --------------------------------------------------- |
| `subsonic_username` | string | `admin` | Username clients must authenticate with |
| `subsonic_password` | string | — | Password; server is **disabled** when this is empty |
| `subsonic_port` | int | `4533` | TCP port the Subsonic API listens on |
## Jellyfin-compatible API server
| Key | Type | Default | Description |
| --------------- | ---- | ------- | ------------------------------------------------------------------------------------------------------------ |
| `jellyfin_port` | int | — | TCP port the Jellyfin-compatible API listens on. **Omit to disable the server.** Conventional value: `8096`. |
The Jellyfin server activates only when `jellyfin_port` is set. It
reuses `subsonic_username` / `subsonic_password` as its credentials and
is additionally **disabled** when the password is empty. On startup it
also binds `udp/7359` for client auto-discovery (`"Who is JellyfinServer?"`)
and advertises `_jellyfin._tcp.local.` over mDNS. See
[API reference › Jellyfin](/api-reference/jellyfin/overview) for the
endpoint surface and the list of tested native clients.
## S3-compatible API server
| Key | Type | Default | Description |
| --------------- | ------ | ----------- | ------------------------------------------------------------------ |
| `s3_enabled` | bool | `false` | Start the S3-compatible HTTP server |
| `s3_host` | string | `"0.0.0.0"` | Listen address |
| `s3_port` | int | `9000` | TCP port the S3 API listens on |
| `s3_access_key` | string | — | AWS access key ID for SigV4 auth; server **disabled** if empty |
| `s3_secret_key` | string | — | AWS secret access key for SigV4 auth; server **disabled** if empty |
The bucket name is fixed to `music` and the region is fixed to
`us-east-1`. The single bucket maps 1:1 to `music_dir`. Uploads are
accepted only for recognised audio extensions, and the library DB
stays in sync via the filesystem watcher — no rescan needed. See
[API reference › S3](/api-reference/s3/overview) for the supported
operations and client recipes.
## CMAF (HLS + DASH) sink
| Key | Type | Default | Description |
| ------------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `cmaf_http_port` | int | `7882` | HTTP port the HLS playlist + DASH manifest + fMP4 segments are served on |
| `cmaf_bitrate` | int | `128000` | AAC-LC bitrate in bits/sec (clamped to 32 000 – 320 000) |
| `cmaf_segment_dir` | string | — | Optional directory to mirror `init.mp4`, `seg/N.m4s`, and the HLS/DASH manifests to (for serving from an external HTTP server / CDN) |
The aliases `audio_output = "hls"` and `audio_output = "dash"` are also accepted
and produce the same in-memory CMAF stream.
## FIFO / pipe sink
| Key | Type | Default | Description |
| ----------- | ------ | ------------------- | ------------------------------------ |
| `fifo_path` | string | `/tmp/rockbox.fifo` | Named FIFO path, or `"-"` for stdout |
## Snapcast TCP sink
| Key | Type | Default | Description |
| ------------------- | ------ | ------- | -------------------------- |
| `snapcast_tcp_host` | string | — | snapserver host |
| `snapcast_tcp_port` | int | `4953` | snapserver TCP source port |
## AirPlay sink
| Key | Type | Default | Description |
| ------------------- | --------------- | ------- | ----------------------------------------------- |
| `airplay_host` | string | — | Single receiver IP |
| `airplay_port` | int | `5000` | Single receiver port |
| `airplay_receivers` | array of tables | — | Multi-room. Each entry: `host`, optional `port` |
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
[[airplay_receivers]]
host = "192.168.1.50"
[[airplay_receivers]]
host = "192.168.1.51"
port = 5000
```
## Squeezelite sink
| Key | Type | Default | Description |
| ----------------------- | ---- | ------- | ----------------------- |
| `squeezelite_port` | int | `3483` | Slim Protocol TCP port |
| `squeezelite_http_port` | int | `9999` | HTTP PCM broadcast port |
## Chromecast sink
| Key | Type | Default | Description |
| ---------------------- | ------ | ------- | -------------------- |
| `chromecast_host` | string | — | Target Chromecast IP |
| `chromecast_port` | int | `8009` | Cast control port |
| `chromecast_http_port` | int | `7881` | WAV HTTP stream port |
## UPnP
| Key | Type | Default | Description |
| ----------------------- | ------ | ----------- | --------------------------------------------- |
| `upnp_renderer_url` | string | — | AVTransport controlURL of the target renderer |
| `upnp_http_port` | int | `7879` | WAV broadcast HTTP port (sink mode) |
| `upnp_server_enabled` | bool | `false` | Start the ContentDirectory media server |
| `upnp_server_port` | int | `7878` | Media server HTTP port |
| `upnp_renderer_enabled` | bool | `false` | Start the MediaRenderer endpoint |
| `upnp_renderer_port` | int | `7880` | MediaRenderer HTTP port |
| `upnp_friendly_name` | string | `"Rockbox"` | Display name shown to control points |
## Playback defaults
| Key | Type | Default | Description |
| ------------------ | ---- | ------- | --------------------------------- |
| `playlist_shuffle` | bool | `false` | |
| `repeat_mode` | int | `1` | 0=Off 1=All 2=One 3=Shuffle 4=A-B |
| `party_mode` | bool | `true` | |
## Tone, stereo & channels
| Key | Type | Default | Description |
| ---------------- | ---- | ------- | --------------------------------------------------- |
| `bass` | int | `0` | dB |
| `treble` | int | `0` | dB |
| `bass_cutoff` | int | `0` | Hz |
| `treble_cutoff` | int | `0` | Hz |
| `balance` | int | `0` | −100..+100 |
| `stereo_width` | int | `100` | 0..255 % (when `channel_config = Custom`) |
| `stereosw_mode` | int | `0` | |
| `channel_config` | int | `0` | 0=Stereo 1=Mono 2=Custom 3=ML 4=MR 5=Karaoke 6=Swap |
## Surround
| Key | Type | Default | Description |
| ------------------ | ---- | ------- | ------------------------- |
| `surround_enabled` | int | `0` | 0/5/8/10/15/30 ms (0=off) |
| `surround_balance` | int | `0` | 0..99 % |
| `surround_fx1` | int | `0` | HF cutoff, Hz |
| `surround_fx2` | int | `0` | LF cutoff, Hz |
| `surround_method2` | bool | `false` | Side-only processing |
| `surround_mix` | int | `0` | 0..100 % |
## Crossfade
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
crossfade = 5
fade_on_stop = false
fade_in_delay = 2
fade_in_duration = 7
fade_out_delay = 4
fade_out_duration = 0
fade_out_mixmode = 2
```
## Equalizer
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
eq_enabled = true
eq_precut = 3 # dB headroom
[[eq_band_settings]]
cutoff = 0 # Hz (per-band)
q = 64 # × 10 fixed-point
gain = 10 # × 10 dB fixed-point
# repeat for each of the 10 bands
```
## ReplayGain
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
[replaygain_settings]
type = 0 # 0=Track 1=Album 2=Track shuffle 3=Off
noclip = true
preamp = 0 # × 10 dB fixed-point
```
## Compressor
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
[compressor_settings]
threshold = -24 # dB
makeup_gain = 0
ratio = 4
knee = 1
release_time = 300 # ms
attack_time = 5 # ms
```
## Where it's parsed
The file is read by `rockbox_settings::load_settings()` in
`crates/settings/src/lib.rs`. Unknown keys are silently ignored, so
typos won't crash startup — but they also won't take effect.
# Troubleshooting
Source: https://rockboxzig.mintlify.app/reference/troubleshooting
Common errors, the symptoms they produce, and how to fix them.
## No audio when using built-in CPAL on macOS
**Symptom:** `rockboxd` starts cleanly, the UI shows playback progressing,
but no audio is heard.
**Cause:** the CPAL audio stream failed to open, usually because the selected
output device was disconnected or the system audio stack returned an error.
**Fix:** update to the latest release. Verify the correct output device is
set in System Settings › Sound › Output. If you build from source, check that
the CPAL-based sink compiled correctly (`nm zig/zig-out/bin/rockboxd | grep pcm_cpal`).
## Snapcast: silence, then snapserver disconnects
**Symptom:** snapserver logs `Stream: 'default' eof` shortly after rockboxd starts.
**Cause:** snapserver was started **before** rockboxd, so it opened the
FIFO first and saw EOF.
**Fix:** start rockboxd first, then snapserver. Rockbox holds a permanent
write-side handle on the FIFO so snapserver never sees EOF mid-track.
For TCP mode, the order is reversed — start snapserver first.
## Squeezelite disconnects every 36 seconds
**Cause:** the `STMt` heartbeat is not being answered. squeezelite has a
36-second watchdog.
**Fix:** every `STMt` heartbeat must be answered with `audg`. This is
already the case in current builds; if you're hacking on
`crates/slim/`, don't strip that response.
## Stale binary after editing C or Rust
**Symptom:** behaviour doesn't match the source code; logs reference
old strings.
**Cause:** Zig only re-links when the static libraries are newer than
the binary.
**Fix:**
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
ls -la zig/zig-out/bin/rockboxd \
build-lib/libfirmware.a \
target/release/librockbox_cli.a
```
If `rockboxd` is newer than every `.a`, force a rebuild:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
# After C changes
cd build-lib && make lib && cd .. && cd zig && zig build
# After Rust changes
cargo build --release -p rockbox-cli -p rockbox-server && cd zig && zig build
```
## "library\_directory is not set" after fresh install
**Cause:** `~/.config/rockbox.org/settings.toml` is missing or has no
`music_dir` key.
**Fix:**
```toml theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
music_dir = "/path/to/your/Music"
```
## AirPlay receiver refuses to connect
**Symptom:** logs show `RTSP ANNOUNCE → 401` or `Receiver requires
password`.
**Cause:** the receiver requires a PIN/password (most "AirPlay 2" gear).
**Status:** AirPlay 2 pairing/encryption isn't implemented. Use a
shairport-sync receiver, an Apple TV, an Airport Express, or another
AirPlay 1 device. See [AirPlay](/audio-output/airplay).
## Chromecast plays once then stops
**Symptom:** first track plays through, queue advances, second track
gets stuck buffering.
**Cause:** the Chromecast cannot reach back to port 7881 on rockboxd's
host. Common when rockboxd is in a VM/container.
**Fix:** forward port 7881 to the host, or run the container with
`--network host`. See the [Chromecast](/audio-output/chromecast) page.
## mDNS discovery returns nothing
**Symptom:** the device picker is empty, even though receivers are on
the LAN.
**Causes & fixes:**
* **Multicast doesn't cross Docker bridges.** Use `--network host`.
* **Some Wi-Fi APs filter multicast.** Enable "multicast forwarding" or
"IGMP snooping" — vendor-specific naming.
* **Avahi/mDNS not running.** On Linux, ensure `avahi-daemon` is
running for SSDP/Bonjour to work.
## "address already in use" on startup
**Cause:** a previous rockboxd process didn't shut down cleanly, or
another service is on one of the API ports.
**Fix:**
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
lsof -i :6061 -i :6062 -i :6063 -i :6600
kill -9
```
…or change the bind ports via the env vars in
[Reference › Ports](/reference/ports).
## Logs are too quiet
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
RUST_LOG=debug rockboxd
```
Or scoped:
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
RUST_LOG=rockbox_airplay=debug,info rockboxd
RUST_LOG=rockbox_slim=debug,info rockboxd
```
Never use `eprintln!` / `println!` in the codebase — they bypass the
filter and pollute stdout (which breaks FIFO mode). All Rust logging
goes through `tracing`.
# Clojure
Source: https://rockboxzig.mintlify.app/sdks/clojure
Pipe-friendly Clojure wrapper over rockboxd's GraphQL API.
`deps.edn`:
```clojure theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
{:deps {org.clojars.tsiry/rockbox-clj {:mvn/version "0.1.2-SNAPSHOT"}}}
```
## Quick start
```clojure theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
(require '[rockbox.core :as rb]
'[rockbox.playback :as pb]
'[rockbox.library :as lib])
(def client (rb/client))
(rb/connect client)
(when-let [t (pb/current-track client)]
(println "Now playing:" (:title t) "—" (:artist t)))
(let [{:keys [albums tracks]} (lib/search client "dark side")]
(println (count albums) "albums," (count tracks) "tracks"))
(-> client
(pb/play-album "album-id" {:shuffle true}))
(rb/on client :track-changed
(fn [t] (println "▶" (:title t) "by" (:artist t))))
(rb/disconnect client)
```
## Configure
```clojure theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
(def c (rb/client)) ;; localhost:6062
(def c (rb/client {:host "192.168.1.42" :port 6062}))
(def c (rb/client {:http-url "https://music.home/graphql"
:ws-url "wss://music.home/graphql"}))
;; Builder style — every with-* fn returns a new client value
(def c (-> (rb/client)
(rb/with-host "music.home")
(rb/with-port 6062)
(rb/with-timeout 30000)
(rb/with-headers {:x-trace-id "req-123"})))
```
## Conventions
* **Action functions return the client**, so chains compose with `->`:
`(-> client (pb/play-album "id") (pb/seek 30000))`
* **Read functions return data** as plain Clojure maps with kebab-case keys.
* **Enums are keywords**: `:playing`, `:paused`, `:stopped`.
* **Events surface as callbacks *or* `core.async` channels.**
## API surface
```clojure theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rockbox.core ;; client, connect, disconnect, on, query
rockbox.playback ;; transport, play helpers
rockbox.library ;; albums, artists, tracks, search, likes
rockbox.playlist ;; live queue
rockbox.saved-playlists ;; persistent playlists
rockbox.smart-playlists ;; rule-based playlists
rockbox.sound ;; volume
rockbox.settings ;; EQ / ReplayGain / crossfade / …
rockbox.system ;; version, status
rockbox.browse ;; filesystem
rockbox.devices ;; output devices
rockbox.bluetooth ;; Linux only
```
## More
Full reference and `core.async` event channels: see the
[Clojure SDK README on GitHub ↗](https://github.com/tsirysndr/rockboxd/blob/master/sdk/clojure/README.md).
# Elixir
Source: https://rockboxzig.mintlify.app/sdks/elixir
Idiomatic Elixir SDK — pipe-friendly, builder-friendly, with messages-as-events.
```elixir theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
def deps do
[{:rockbox_ex, "~> 0.1"}]
end
```
## Quick start
```elixir theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
client = Rockbox.new()
# Optional: opens the WebSocket so subscribers receive events
{:ok, _pid} = Rockbox.connect(client)
case Rockbox.Playback.current_track(client) do
{:ok, %Rockbox.Track{} = t} -> IO.puts("▶ #{t.title} — #{t.artist}")
{:ok, nil} -> IO.puts("Nothing is playing.")
end
{:ok, results} = Rockbox.Library.search(client, "dark side")
album = List.first(results.albums)
:ok = Rockbox.Playback.play_album(client, album.id, shuffle: true)
# Events arrive as messages
:ok = Rockbox.subscribe(:track_changed)
receive do
{:rockbox, :track_changed, track} ->
IO.puts("Now: #{track.title}")
end
Rockbox.disconnect(client)
```
## Configure
```elixir theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
client = Rockbox.new() # localhost:6062
client = Rockbox.new(host: "192.168.1.42", port: 6062)
client = Rockbox.new(
http_url: "https://music.home/graphql",
ws_url: "wss://music.home/graphql"
)
```
## Highlights
* **Pipe-friendly** — every API function takes the client as its first arg.
* **Builder-friendly** — smart-playlist rules and partial settings updates compose with `|>`.
* **Tagged tuples or bangs** — `name/N → {:ok, value} | {:error, exception}`,
with a matching `name!/N` that raises.
* **Real-time events as messages** — `Rockbox.subscribe(:track_changed)` and
receive `{:rockbox, :track_changed, %Rockbox.Track{}}`.
* **Plugins** — implement `Rockbox.Plugin` and install with
`Rockbox.use_plugin/2`.
## API surface
```elixir theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
Rockbox.Playback.* # transport, current/next track, play helpers
Rockbox.Library.* # albums, artists, tracks, search, likes
Rockbox.Queue.* # live queue
Rockbox.SavedPlaylists.* # persistent playlists
Rockbox.SmartPlaylists.* # rule-based playlists
Rockbox.Sound.* # volume
Rockbox.Settings.* # EQ / ReplayGain / crossfade / …
Rockbox.System.* # version, status
Rockbox.Browse.* # filesystem
Rockbox.Devices.* # output devices
Rockbox.Bluetooth.* # Linux only
```
## More
Full reference and rule-builder DSL: see the
[Elixir SDK README on GitHub ↗](https://github.com/tsirysndr/rockboxd/blob/master/sdk/elixir/README.md).
# Gleam
Source: https://rockboxzig.mintlify.app/sdks/gleam
Type-safe Gleam SDK with a tagged Result on every call.
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
gleam add rockbox
```
## Quick start
```gleam theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import gleam/io
import gleam/list
import gleam/option.{None, Some}
import rockbox
import rockbox/library
import rockbox/playback
pub fn main() {
let client = rockbox.default_client()
case playback.current_track(client) {
Ok(Some(track)) -> io.println("▶ " <> track.title <> " — " <> track.artist)
Ok(None) -> io.println("Nothing is playing.")
Error(_) -> io.println("Could not reach rockboxd.")
}
let assert Ok(results) = library.search(client, "dark side")
case list.first(results.albums) {
Ok(album) -> {
let _ = playback.play_album(
client, album.id,
playback.play_options() |> playback.with_shuffle(True),
)
Nil
}
Error(_) -> Nil
}
}
```
## Configure
```gleam theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
let client = rockbox.default_client() // localhost:6062
let client = rockbox.at(host: "192.168.1.42", port: 6062)
let client =
rockbox.new()
|> rockbox.url("http://192.168.1.42:6062/graphql")
|> rockbox.connect
```
## Highlights
* **Pipe-friendly** — every API function takes the client as its first arg.
* **Tagged results** — every call returns
`Result(value, rockbox/error.Error)`, so `case` and `use` flows stay flat.
* **Type-safe rules DSL** — compose smart-playlist rules with
`rockbox/smart_playlists/rules` instead of hand-written JSON.
## API surface
```gleam theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
rockbox/playback
rockbox/library
rockbox/queue
rockbox/saved_playlists
rockbox/smart_playlists
rockbox/sound
rockbox/settings
rockbox/system
rockbox/browse
rockbox/devices
rockbox/bluetooth // Linux only
```
## More
Full reference and rule DSL: see the
[Gleam SDK README on GitHub ↗](https://github.com/tsirysndr/rockboxd/blob/master/sdk/gleam/README.md).
# Client SDKs
Source: https://rockboxzig.mintlify.app/sdks/overview
Six first-party SDKs. All wrap the GraphQL transport, surface real-time events, and ship a tiny plugin system.
Every SDK targets the GraphQL endpoint on **port 6062** and exposes the
same domain-namespaced API:
```
client.playback # transport, current/next track, play helpers
client.library # albums, artists, tracks, search, likes, scan
client.playlist # the active queue
client.savedPlaylists
client.smartPlaylists
client.sound # volume
client.settings # global EQ / replaygain / crossfade / shuffle
client.system # version, runtime info
client.browse # filesystem browser
client.devices # output devices (Cast, AirPlay, Snapcast)
client.bluetooth # Linux only
```
## Pick a language
`bun add @rockbox-zig/sdk`
`uv add rockbox-sdk` — async-first
`gem install rockbox`
`{:rockbox_ex, "~> 0.1"}`
`org.clojars.tsiry/rockbox-clj`
`gleam add rockbox`
## Why not just use the GraphQL transport directly?
You can — `client.query()` on every SDK is an escape hatch, and the
GraphiQL explorer at
[http://localhost:6062/graphiql](http://localhost:6062/graphiql) lets you
test queries without writing any client code. The SDKs add value when you
want:
* **Typed responses** — Pydantic models in Python, `Struct`s in Ruby,
TypeScript types, Gleam tagged unions.
* **Real-time events** — `track:changed` / `status:changed` /
`playlist:changed` over WebSocket with auto-reconnect and exponential
backoff.
* **A plugin system** — Jellyfin-style install/uninstall lifecycle for
cross-cutting features (scrobbling, notifications, sleep timer).
* **Smart-playlist rule builders** — type-safe rule DSLs (Gleam, Elixir,
Clojure).
* **Idiomatic ergonomics** — pipe-friendly in functional languages,
builder DSLs in OOP languages.
## Common patterns
```ts TypeScript theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import { RockboxClient } from '@rockbox-zig/sdk';
const client = new RockboxClient();
client.connect();
client.on('track:changed', (t) => console.log(`▶ ${t.title} — ${t.artist}`));
const { albums } = await client.library.search('dark side');
await client.playback.playAlbum(albums[0].id, { shuffle: true });
```
```python Python theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import asyncio
from rockbox_sdk import RockboxClient
async def main():
async with RockboxClient(host="localhost") as client:
await client.connect()
results = await client.library.search("dark side")
await client.playback.play_album(results.albums[0].id, shuffle=True)
asyncio.run(main())
```
```ruby Ruby theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
require "rockbox"
client = Rockbox::Client.new
client.on(:track_changed) { |t| puts "▶ #{t.title} — #{t.artist}" }
client.connect
results = client.library.search("dark side")
client.playback.play_album(results.albums.first.id, shuffle: true)
```
```elixir Elixir theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
client = Rockbox.new()
{:ok, _pid} = Rockbox.connect(client)
Rockbox.subscribe(:track_changed)
{:ok, results} = Rockbox.Library.search(client, "dark side")
album = List.first(results.albums)
:ok = Rockbox.Playback.play_album(client, album.id, shuffle: true)
```
```clojure Clojure theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
(require '[rockbox.core :as rb]
'[rockbox.playback :as pb]
'[rockbox.library :as lib])
(def client (rb/client))
(rb/connect client)
(rb/on client :track-changed
(fn [t] (println "▶" (:title t) "—" (:artist t))))
(let [{:keys [albums]} (lib/search client "dark side")]
(pb/play-album client (:id (first albums)) {:shuffle true}))
```
```gleam Gleam theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import rockbox
import rockbox/library
import rockbox/playback
pub fn main() {
let client = rockbox.default_client()
let assert Ok(results) = library.search(client, "dark side")
case list.first(results.albums) {
Ok(album) -> {
let _ = playback.play_album(
client, album.id,
playback.play_options() |> playback.with_shuffle(True),
)
Nil
}
Error(_) -> Nil
}
}
```
# Python
Source: https://rockboxzig.mintlify.app/sdks/python
Async-first Python SDK on httpx + websockets, with Pydantic models.
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
uv add rockbox-sdk
# or
pip install rockbox-sdk
```
Requires **Python 3.10+**.
## Quick start
```python theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import asyncio
from rockbox_sdk import RockboxClient, PlaybackStatus
async def main():
async with RockboxClient(host="localhost") as client:
track = await client.playback.current_track()
if track:
print(f"Now: {track.title} — {track.artist}")
if await client.playback.status() == PlaybackStatus.PAUSED:
await client.playback.resume()
asyncio.run(main())
```
## Highlights
* **Async-first** — built on `httpx` + `websockets`. Use `await` everywhere.
* **Domain-namespaced API** — `client.playback.*`, `client.library.*`, `client.sound.*`, …
* **Typed responses** — every reply is a Pydantic model with snake\_case fields.
* **Real-time events** — `connect()` opens a WebSocket and forwards
`track:changed` / `status:changed` / `playlist:changed` to listeners.
* **Builder API** — `RockboxClient.builder().host(...).port(...).build()`.
* **Plugin system** — Jellyfin-style install/uninstall lifecycle.
* **Python-friendly** — context manager, decorator listeners, dataclass inputs.
## Configure
```python theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
client = RockboxClient(host="192.168.1.42", port=6062)
# Builder
client = (
RockboxClient.builder()
.host("nas.local")
.port(6062)
.timeout(15)
.build()
)
# Full URL override
client = RockboxClient(
http_url="http://nas.local:6062/graphql",
ws_url="ws://nas.local:6062/graphql",
)
```
Always close: `await client.aclose()`, or use it as an async context
manager (`async with RockboxClient() as client:`).
## Domains
| Namespace | What it does |
| ------------------------ | -------------------------------------------- |
| `client.playback` | Transport, current/next, play helpers |
| `client.library` | Albums, artists, tracks, search, likes, scan |
| `client.playlist` | The active queue |
| `client.saved_playlists` | Persistent playlists & folders |
| `client.smart_playlists` | Rule-based playlists & stats |
| `client.sound` | Volume |
| `client.settings` | EQ / ReplayGain / crossfade / shuffle / … |
| `client.system` | Version, runtime info |
| `client.browse` | Filesystem & UPnP browser |
| `client.devices` | Cast / source devices |
| `client.bluetooth` | Bluetooth (Linux only) |
## Real-time events
```python theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
from rockbox_sdk import RockboxClient, TRACK_CHANGED, STATUS_CHANGED
async with RockboxClient() as client:
await client.connect()
@client.on(TRACK_CHANGED)
async def on_track(track):
print(f"▶ {track.title} — {track.artist}")
@client.on(STATUS_CHANGED)
def on_status(raw_status):
print(f"◐ status = {raw_status}")
await asyncio.Event().wait()
```
Convenience wrappers: `client.on_track_changed(...)`,
`client.on_status_changed(...)`, `client.on_playlist_changed(...)`.
## Plugins
```python theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
class SleepTimer:
name = "sleep-timer"
version = "1.0.0"
def __init__(self, minutes: int):
self.minutes = minutes
self._task = None
def install(self, ctx):
async def fire():
await asyncio.sleep(self.minutes * 60)
await ctx.query("mutation { hardStop }")
self._task = asyncio.create_task(fire())
def uninstall(self):
if self._task:
self._task.cancel()
await client.use(SleepTimer(30))
```
## REPL-friendly
The SDK is async-first. The recommended REPL is **IPython** — `await`
works at the top level, and you get tab-completion on Pydantic models.
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
uv run ipython
```
```python theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
In [1]: from rockbox_sdk import RockboxClient
In [2]: client = RockboxClient()
In [3]: await client.playback.status()
In [4]: track = await client.playback.current_track()
```
## More
Full reference, type catalogue and plugin examples: see the
[Python SDK README on GitHub ↗](https://github.com/tsirysndr/rockboxd/blob/master/sdk/python/README.md).
# Ruby
Source: https://rockboxzig.mintlify.app/sdks/ruby
Builder-friendly, block-friendly Ruby SDK with WebSocket subscriptions and plugins.
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
gem install rockbox
```
Or with Bundler:
```ruby theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
# Gemfile
gem "rockbox"
```
Requires **Ruby 3.0+**.
## Quick start
```ruby theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
require "rockbox"
client = Rockbox::Client.new
client.connect # opens the WebSocket — subscriptions start firing
if (track = client.playback.current_track)
puts "Now playing: #{track.title} — #{track.artist}"
end
results = client.library.search("dark side")
client.playback.play_album(results.albums.first.id, shuffle: true)
client.on(:track_changed) { |t| puts "▶ #{t.title} by #{t.artist}" }
client.disconnect
```
## Configure
```ruby theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
client = Rockbox::Client.new(host: "192.168.1.42", port: 6062)
client = Rockbox::Client.build do |c|
c.host = "192.168.1.42"
c.port = 6062
end
client = Rockbox::Client.new(
http_url: "https://music.home/graphql",
ws_url: "wss://music.home/graphql",
)
```
## Playback
```ruby theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
client.playback.status # => Integer
client.playback.status_name # => :playing | :paused | :stopped | :unknown
client.playback.current_track
client.playback.next_track
client.playback.play(elapsed: 0, offset: 0)
client.playback.pause
client.playback.resume
client.playback.next!
client.playback.previous!
client.playback.seek(60_000)
client.playback.stop
client.playback.play_track("/Music/song.mp3")
client.playback.play_album(album_id, shuffle: true)
client.playback.play_artist(artist_id)
client.playback.play_playlist(playlist_id, shuffle: true)
client.playback.play_directory("/Music/Pink Floyd", recurse: true)
client.playback.play_liked_tracks(shuffle: true)
client.playback.play_all_tracks
```
## Real-time events
```ruby theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
client.on(:track_changed) { |track| puts track.title }
client.on(:status_changed) { |status| puts status }
client.on(:playlist_changed) { |queue| puts queue.amount }
```
## Plugin system
```ruby theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
class Scrobbler
def name; "scrobbler"; end
def version; "1.0.0"; end
def description; "Scrobble played tracks"; end
def install(ctx)
ctx.events.on(:track_changed) do |track|
MyScrobbler.submit(track.title, track.artist)
end
end
end
client.use Scrobbler.new
```
## More
Full reference, type catalogue and plugin examples: see the
[Ruby SDK README on GitHub ↗](https://github.com/tsirysndr/rockboxd/blob/master/sdk/ruby/README.md).
# TypeScript
Source: https://rockboxzig.mintlify.app/sdks/typescript
Fully typed GraphQL client with real-time subscriptions and a plugin system.
```sh theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
bun add @rockbox-zig/sdk
# or
npm install @rockbox-zig/sdk
```
`rockboxd` must be running and reachable. By default the SDK connects to
`http://localhost:6062/graphql`.
## Quick start
```ts theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import { RockboxClient, PlaybackStatus } from '@rockbox-zig/sdk';
const client = new RockboxClient();
client.connect(); // optional — opens the WebSocket
const track = await client.playback.currentTrack();
if (track) console.log(`Now playing: ${track.title} — ${track.artist}`);
const { albums } = await client.library.search('dark side');
await client.playback.playAlbum(albums[0].id, { shuffle: true });
client.on('track:changed', (t) => console.log(`▶ ${t.title} by ${t.artist}`));
client.disconnect();
```
## Configuration
```ts theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
new RockboxClient(); // localhost:6062
new RockboxClient({ host: '192.168.1.42', port: 6062 });
new RockboxClient({ // behind a reverse proxy
httpUrl: 'https://music.home/graphql',
wsUrl: 'wss://music.home/graphql',
});
```
| Option | Default | Description |
| --------- | ------------------------------ | ------------------------------- |
| `host` | `localhost` | Hostname or IP of rockboxd |
| `port` | `6062` | GraphQL port |
| `httpUrl` | `http://{host}:{port}/graphql` | Override the full HTTP URL |
| `wsUrl` | `ws://{host}:{port}/graphql` | Override the full WebSocket URL |
## Domains
| Namespace | What it does |
| ----------------------- | ------------------------------------------------ |
| `client.playback` | Transport, current/next track, play helpers |
| `client.library` | Albums, artists, tracks, search, likes, scan |
| `client.playlist` | The active queue |
| `client.savedPlaylists` | Persistent playlists & folders |
| `client.smartPlaylists` | Rule-based playlists & listening stats |
| `client.sound` | Volume control |
| `client.settings` | Global EQ / ReplayGain / crossfade / shuffle / … |
| `client.system` | Version, runtime info |
| `client.browse` | Filesystem browser |
| `client.devices` | Cast / source device discovery |
## Playback shortcuts
```ts theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
await client.playback.playTrack('/Music/Pink Floyd/Wish You Were Here.mp3');
await client.playback.playAlbum('album-id', { shuffle: true });
await client.playback.playAlbum('album-id', { position: 3 });
await client.playback.playArtist('artist-id', { shuffle: true });
await client.playback.playPlaylist('playlist-id', { shuffle: true });
await client.playback.playDirectory('/Music/Jazz', { recurse: true, shuffle: true });
await client.playback.playLikedTracks({ shuffle: true });
await client.playback.playAllTracks({ shuffle: true });
```
## Queue management
```ts theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import { InsertPosition } from '@rockbox-zig/sdk';
await client.playlist.insertTracks(
['/Music/track1.mp3', '/Music/track2.mp3'],
InsertPosition.Next,
);
await client.playlist.insertTracks(paths, InsertPosition.Last);
await client.playlist.insertTracks(paths, InsertPosition.First); // replace
await client.playlist.insertDirectory('/Music/Ambient', InsertPosition.Last);
await client.playlist.insertAlbum('album-id', InsertPosition.Next);
await client.playlist.removeTrack(2);
await client.playlist.clear();
await client.playlist.shuffle();
```
| `InsertPosition` | Effect |
| ---------------- | -------------------------------------- |
| `Next` | After the currently playing track |
| `AfterCurrent` | After the last manually inserted track |
| `Last` | At the end of the queue |
| `First` | Replace the entire queue |
## Real-time events
```ts theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
client.connect();
client.on('track:changed', (t) => updateNowPlaying(t));
client.on('status:changed', (s) => setStatusBadge(s));
client.on('playlist:changed', (q) => renderQueue(q.tracks));
client.on('ws:error', (err) => console.error(err.message));
client.once('track:changed', (t) => console.log('first event:', t.title));
client.off('track:changed', handler);
```
## Plugins
Drop-in cross-cutting features. Inspired by Jellyfin's `IPlugin`.
```ts theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import type { RockboxPlugin } from '@rockbox-zig/sdk';
export const Notifications: RockboxPlugin = {
name: 'desktop-notifications',
version: '1.0.0',
install({ events }) {
if (typeof Notification === 'undefined') return;
Notification.requestPermission();
events.on('track:changed', (t) => {
new Notification(t.title, { body: `${t.artist} · ${t.album}`, icon: t.albumArt ?? undefined });
});
},
};
await client.use(Notifications);
client.installedPlugins().forEach((p) => console.log(`${p.name} v${p.version}`));
await client.unuse('desktop-notifications');
```
## Error handling
```ts theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
import { RockboxNetworkError, RockboxGraphQLError, RockboxError } from '@rockbox-zig/sdk';
try {
await client.playback.play();
} catch (err) {
if (err instanceof RockboxNetworkError) showOfflineBanner(err.message);
else if (err instanceof RockboxGraphQLError) console.error(err.errors);
else if (err instanceof RockboxError) console.error('Rockbox error:', err.message);
}
```
## Raw queries
```ts theme={"theme":{"light":"catppuccin-latte","dark":"min-dark"}}
const data = await client.query<{ rockboxVersion: string }>(`query { rockboxVersion }`);
```
Full reference, more examples and the plugin system deep-dive: see the
[SDK README on GitHub ↗](https://github.com/tsirysndr/rockboxd/blob/master/sdk/typescript/README.md).