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