···11+# CLAUDE.md — Rockbox Zig
22+33+## Project overview
44+55+Rockbox Zig is a modern wrapper around the [Rockbox](https://www.rockbox.org) open-source audio player firmware. It adds Rust/Zig services on top of the C firmware to expose gRPC, GraphQL, HTTP, and MPD APIs, a Typesense-backed search engine, Chromecast/AirPlay/Snapcast output sinks, and a desktop/web UI.
66+77+The binary is called **`rockboxd`**. It is a single executable built by Zig that links:
88+- The Rockbox C firmware (compiled by Make into `build-lib/libfirmware.a` and friends)
99+- Rust crates (compiled by Cargo into `target/release/librockbox_cli.a` and `librockbox_server.a`)
1010+- SDL2 for audio/event handling on the host platform
1111+1212+## Repository layout
1313+1414+```
1515+firmware/ Rockbox C firmware (audio engine, codecs, DSP)
1616+apps/ Rockbox application layer (playlist, database, plugins)
1717+lib/ Codec libraries (rbcodec, fixedpoint, skin_parser, tlsf)
1818+build-lib/ Out-of-tree Make build directory (generated; do not edit)
1919+crates/ Rust workspace
2020+ airplay/ ALAC encoder + RAOP/RTP sender (AirPlay 1 output)
2121+ cli/ Entry point compiled to librockbox_cli.a (staticlib)
2222+ server/ gRPC / HTTP server
2323+ settings/ load_settings() — reads settings.toml, applies sinks
2424+ sys/ FFI bindings to the C firmware (unsafe extern "C")
2525+ library/ Audio file scanning and SQLite library management
2626+ typesense/ Typesense client for fast music search
2727+ netstream/ HTTP streaming (Range-request based fd multiplexing)
2828+ chromecast/ Chromecast output
2929+ rpc/ gRPC definitions / generated code
3030+ graphql/ GraphQL schema and resolvers
3131+ mpd/ MPD protocol server
3232+ mpris/ MPRIS D-Bus integration
3333+ tracklist/ Playlist / tracklist management
3434+ types/ Shared Rust types
3535+ traits/ Shared Rust traits
3636+zig/ Zig build script and thin main.zig entry point
3737+```
3838+3939+## Build system
4040+4141+### Step 1 — C firmware (Make)
4242+```sh
4343+cd build-lib
4444+make lib # builds libfirmware.a, librockbox.a, codec libs
4545+```
4646+The `build-lib/` directory was pre-configured via Rockbox's `tools/configure` for the `sdlapp` target. Do **not** run `configure` again unless you know what you're doing — it regenerates the Makefile and overwrites any local edits.
4747+4848+### Step 2 — Rust crates (Cargo)
4949+```sh
5050+cargo build --release -p rockbox-cli # produces target/release/librockbox_cli.a
5151+cargo build --release -p rockbox-server # produces target/release/librockbox_server.a
5252+```
5353+Both crates have `crate-type = ["staticlib"]`. All transitive rlib dependencies are bundled into the `.a`.
5454+5555+### Step 3 — Zig linker
5656+```sh
5757+cd zig
5858+zig build # links everything into zig-out/bin/rockboxd
5959+```
6060+6161+### Quick full rebuild
6262+```sh
6363+cd build-lib && make lib && cd ..
6464+cargo build --release -p rockbox-cli -p rockbox-server
6565+cd zig && zig build
6666+```
6767+6868+### Critical: stale binary pitfall
6969+`zig build` only re-links if the `.a` files are newer than the binary. After changing C code, always run `make lib` first. After changing Rust code, run `cargo build --release`. If behavior doesn't match the code, check timestamps:
7070+```sh
7171+ls -la zig/zig-out/bin/rockboxd build-lib/libfirmware.a target/release/librockbox_cli.a
7272+```
7373+7474+## Runtime configuration
7575+7676+Settings file: `~/.config/rockbox.org/settings.toml`
7777+7878+```toml
7979+music_dir = "/path/to/Music"
8080+8181+# Audio output — pick one:
8282+audio_output = "builtin" # SDL audio (default)
8383+8484+audio_output = "fifo"
8585+fifo_path = "/tmp/snapfifo" # named FIFO for Snapcast; use "-" for stdout
8686+8787+audio_output = "airplay"
8888+airplay_host = "192.168.1.x" # RAOP receiver IP
8989+airplay_port = 5000 # optional, default 5000
9090+```
9191+9292+## PCM sink architecture
9393+9494+The audio output abstraction lives in `firmware/export/pcm_sink.h`. Each sink implements `struct pcm_sink_ops` (init / postinit / set_freq / lock / unlock / play / stop).
9595+9696+| Enum constant | Value | Implementation file |
9797+|--------------------|-------|---------------------------------------------|
9898+| `PCM_SINK_BUILTIN` | 0 | `firmware/target/hosted/sdl/pcm-sdl.c` |
9999+| `PCM_SINK_FIFO` | 1 | `firmware/target/hosted/pcm-fifo.c` |
100100+| `PCM_SINK_AIRPLAY` | 2 | `firmware/target/hosted/pcm-airplay.c` |
101101+102102+`crates/settings/src/lib.rs:load_settings()` reads `audio_output` and calls `pcm::switch_sink()`.
103103+104104+Rust constants + helpers live in `crates/sys/src/sound/pcm.rs`.
105105+106106+### FIFO sink (Snapcast)
107107+- Pre-creates the named FIFO with `O_RDWR|O_NONBLOCK` in `pcm_fifo_set_path()` then clears `O_NONBLOCK`, so a permanent writer reference is held — readers never see premature EOF between tracks.
108108+- `sink_dma_stop()` does NOT close the fd; it stays open across track transitions.
109109+- **Startup order matters**: rockboxd must start before snapserver. If snapserver opens the FIFO first it may get EOF and stop reading.
110110+- On macOS, snapserver v0.35.0 ignores the `-s` sample-format CLI flag; use `/usr/local/etc/snapserver.conf`:
111111+ ```ini
112112+ [stream]
113113+ source = pipe:///tmp/snapfifo?name=default&sampleformat=44100:16:2
114114+ ```
115115+116116+### AirPlay sink (RAOP)
117117+- `crates/airplay/` implements the full RAOP stack in pure Rust (no tokio needed).
118118+ - `alac.rs` — ALAC escape/verbatim frame encoder: 352 stereo S16LE samples → 1411-byte bitstream
119119+ - `rtp.rs` — RTP/UDP packet sender; RTCP NTP sync packets sent every ~44 frames
120120+ - `rtsp.rs` — synchronous RTSP client: ANNOUNCE (SDP) → SETUP → RECORD
121121+- `pcm_airplay_connect()` is called once per `sink_dma_start()` (idempotent if already connected).
122122+- The `rockbox-airplay` rlib must be force-included in `librockbox_cli.a` via the `use rockbox_airplay::_link_airplay as _` shim in `crates/cli/src/lib.rs`.
123123+124124+## Key cross-cutting concerns
125125+126126+### macOS SDL audio
127127+`SDL_InitSubSystem(SDL_INIT_AUDIO)` must be called explicitly on macOS because the SDL event thread (which normally does it) is `#ifndef __APPLE__`. This is done in `firmware/target/hosted/sdl/system-sdl.c` in the `#else` branch of the event-thread guard.
128128+129129+### SIGTERM handling
130130+`system-hosted.c` installs a SIGTERM handler that loops forever (waits for SDL quit event). `crates/cli/src/lib.rs` overrides SIGTERM/SIGINT with a handler that kills the typesense child and calls `_exit(0)`.
131131+132132+### Typesense subprocess
133133+Spawned in `crates/cli/src/lib.rs` with `Stdio::piped()`. stdout/stderr lines are forwarded to `tracing::debug!`/`tracing::warn!` in background threads, keeping the PCM stdout stream clean in FIFO mode.
134134+135135+### Logging — use `tracing`, never `eprintln!`/`println!`
136136+All Rust logging must use the `tracing` crate (`tracing::debug!`, `tracing::info!`, `tracing::warn!`, `tracing::error!`). **Never use `eprintln!` or `println!` for diagnostic output** in Rust code — they bypass the structured log filter, pollute stdout (breaking FIFO/pipe mode), and can't be silenced at runtime.
137137+138138+Severity guide:
139139+- `tracing::error!` — unrecoverable failures (connection refused, missing config)
140140+- `tracing::warn!` — recoverable issues (non-fatal fallbacks, unexpected-but-handled states)
141141+- `tracing::info!` — notable lifecycle events (session established, device paired)
142142+- `tracing::debug!` — per-packet/per-frame detail, protocol negotiation steps
143143+144144+`tracing` is declared as a workspace dependency in the root `Cargo.toml`; add `tracing = { workspace = true }` to any crate that needs it. Control verbosity at runtime with `RUST_LOG`, e.g. `RUST_LOG=debug rockboxd` or `RUST_LOG=rockbox_airplay=debug,info`.
145145+146146+### HTTP streaming
147147+HTTP fds are encoded as values `<= STREAM_HTTP_FD_BASE (-1000)`. `stream_open/read/lseek/close` in `crates/netstream/` dispatch between HTTP and POSIX based on fd value. The global `STREAMS` map holds `Arc<Mutex<StreamState>>` per handle so concurrent reads don't serialize on a single lock.
148148+149149+## Adding a new PCM sink
150150+151151+1. Create `firmware/target/hosted/pcm-<name>.c` — model on `pcm-fifo.c`.
152152+2. Add `PCM_SINK_<NAME>` to the enum in `firmware/export/pcm_sink.h`.
153153+3. Register `&<name>_pcm_sink` in the `sinks[]` array in `firmware/pcm.c`.
154154+4. Add `target/hosted/pcm-<name>.c` inside the `#if PLATFORM_HOSTED` block in `firmware/SOURCES`.
155155+5. Add Rust constant `PCM_SINK_<NAME>: i32` in `crates/sys/src/sound/pcm.rs`.
156156+6. Add a `set_<name>_*` wrapper if configuration is needed.
157157+7. Handle in `crates/settings/src/lib.rs:load_settings()`.
158158+8. If the sink has a Rust implementation in a new crate: add a `_link_<name>()` dummy fn and reference it from `crates/cli/src/lib.rs` to force inclusion in the staticlib.
159159+160160+## Useful commands
161161+162162+```sh
163163+# Run the daemon
164164+./zig/zig-out/bin/rockboxd
165165+166166+# Run with AirPlay debug logging
167167+RUST_LOG=debug ./zig/zig-out/bin/rockboxd
168168+169169+# Test FIFO → stdout pipe
170170+./zig/zig-out/bin/rockboxd | ffplay -f s16le -ar 44100 -ac 2 -
171171+172172+# Check binary vs library timestamps
173173+ls -la zig/zig-out/bin/rockboxd build-lib/libfirmware.a target/release/librockbox_cli.a
174174+175175+# Verify AirPlay symbols are present
176176+nm zig/zig-out/bin/rockboxd | grep pcm_airplay
177177+178178+# Verify a crate is in the staticlib
179179+ar t target/release/librockbox_cli.a | grep airplay
180180+```