Select the types of activity you want to include in your feed.
Standardize and align Markdown tables
Add Markdown table formatting guidelines to CLAUDE.md and reformat tables across documentation. Align pipes, pad cells to column widths, and normalize separator rows.
···41414242Determines how the left and right channels of a stereo signal are routed to the output.
43434444-| Value | Behaviour |
4545-|---|---|
4646-| **Stereo** | Leave the audio signal unmodified. |
4747-| **Mono** | Combine both channels; send the result to both outputs (monophonic). |
4848-| **Custom** | Apply the stereo width specified by the **Stereo Width** setting. |
4949-| **Mono Left** | Play the left channel in both stereo channels. |
5050-| **Mono Right** | Play the right channel in both stereo channels. |
5151-| **Karaoke** | Remove all sound common to both channels. Since vocals are typically equally present in both channels, this often (but not always) removes the voice track. May have other undesirable effects. |
5252-| **Swap Left & Right** | Play the left channel on the right output and vice versa. |
4444+| Value | Behaviour |
4545+| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
4646+| **Stereo** | Leave the audio signal unmodified. |
4747+| **Mono** | Combine both channels; send the result to both outputs (monophonic). |
4848+| **Custom** | Apply the stereo width specified by the **Stereo Width** setting. |
4949+| **Mono Left** | Play the left channel in both stereo channels. |
5050+| **Mono Right** | Play the right channel in both stereo channels. |
5151+| **Karaoke** | Remove all sound common to both channels. Since vocals are typically equally present in both channels, this often (but not always) removes the voice track. May have other undesirable effects. |
5252+| **Swap Left & Right** | Play the left channel on the right output and vice versa. |
53535454Applied via `sound_set_channels()` in `firmware/sound.c`.
5555···78787979> **Warning:** crossfeed can cause output distortion if its settings result in a combined level that is too high.
80808181-| Setting | Storage field | Range | Default | Description |
8282-|---|---|---|---|---|
8383-| **Type** | `global_settings.crossfeed` | off, meier, custom | off | `meier` uses fixed sensible defaults; `custom` exposes the four parameters below |
8484-| **Direct Gain** | `global_settings.crossfeed_direct_gain` | −60..0 dB (step 5) | −15 dB | How much to decrease the level of the signal travelling the direct path from a speaker to the corresponding ear |
8585-| **Cross Gain** | `global_settings.crossfeed_cross_gain` | −120..−30 dB (step 5) | −60 dB | How much to decrease the level of the signal travelling the cross path from a speaker to the opposite ear |
8686-| **HF Attenuation** | `global_settings.crossfeed_hf_attenuation` | −240..−60 dB (step 5) | −160 dB | How much the upper frequencies of the cross-path signal are dampened. Total high-frequency level is a combination of this setting and Cross Gain |
8787-| **HF Cutoff** | `global_settings.crossfeed_hf_cutoff` | 500..2000 Hz (step 100) | 700 Hz | Frequency at which the cross-path signal begins to be cut by the HF Attenuation amount |
8181+| Setting | Storage field | Range | Default | Description |
8282+| ------------------ | ------------------------------------------ | ----------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
8383+| **Type** | `global_settings.crossfeed` | off, meier, custom | off | `meier` uses fixed sensible defaults; `custom` exposes the four parameters below |
8484+| **Direct Gain** | `global_settings.crossfeed_direct_gain` | −60..0 dB (step 5) | −15 dB | How much to decrease the level of the signal travelling the direct path from a speaker to the corresponding ear |
8585+| **Cross Gain** | `global_settings.crossfeed_cross_gain` | −120..−30 dB (step 5) | −60 dB | How much to decrease the level of the signal travelling the cross path from a speaker to the opposite ear |
8686+| **HF Attenuation** | `global_settings.crossfeed_hf_attenuation` | −240..−60 dB (step 5) | −160 dB | How much the upper frequencies of the cross-path signal are dampened. Total high-frequency level is a combination of this setting and Cross Gain |
8787+| **HF Cutoff** | `global_settings.crossfeed_hf_cutoff` | 500..2000 Hz (step 100) | 700 Hz | Frequency at which the cross-path signal begins to be cut by the HF Attenuation amount |
88888989Applied via `dsp_set_crossfeed_type()` and `dsp_set_crossfeed_cross_params()` in `lib/rbcodec/dsp/crossfeed.h`.
9090···100100101101### EQ bands
102102103103-| Band | Filter type | Default centre / cutoff | Q recommendation |
104104-|---|---|---|---|
105105-| Band 0 | Low shelf filter | 32 Hz | 0.7 (higher values add an unwanted boost near cutoff) |
106106-| Bands 1–8 | Peaking (bell) filters | 64 / 125 / 250 / 500 / 1k / 2k / 4k / 8k Hz | Higher Q = narrower range; lower Q = wider range |
107107-| Band 9 | High shelf filter | 16 000 Hz | 0.7 |
103103+| Band | Filter type | Default centre / cutoff | Q recommendation |
104104+| --------- | ---------------------- | ------------------------------------------- | ----------------------------------------------------- |
105105+| Band 0 | Low shelf filter | 32 Hz | 0.7 (higher values add an unwanted boost near cutoff) |
106106+| Bands 1–8 | Peaking (bell) filters | 64 / 125 / 250 / 500 / 1k / 2k / 4k / 8k Hz | Higher Q = narrower range; lower Q = wider range |
107107+| Band 9 | High shelf filter | 16 000 Hz | 0.7 |
108108109109**Band parameters (per band):**
110110- **Cutoff / Centre frequency** — where the shelving starts (shelf bands) or the centre of the affected range (peak bands).
···113113114114### EQ sub-settings
115115116116-| Setting | Storage field | Type | Description |
117117-|---|---|---|---|
118118-| **Enable EQ** | `global_settings.eq_enabled` | bool | Master on/off switch for the software EQ |
119119-| **Precut** | `global_settings.eq_precut` | 0..24.0 dB | Global negative gain applied to decoded audio before the EQ. Prevents distortion when boosting bands. Can also be used as a volume cap. Not applied when EQ is disabled |
120120-| **Graphical EQ** | — | screen | Graphical interface for adjusting gain, centre frequency, and Q for each band |
121121-| **Simple EQ** | — | screen | Simplified view: only gain is adjustable per band |
122122-| **Advanced EQ** | — | submenu | Same parameters as Graphical EQ, via text menus |
123123-| **Save EQ Preset** | — | action | Saves current EQ configuration to a `.cfg` file |
124124-| **Browse EQ Presets** | — | screen | Lists built-in presets and any saved configurations |
116116+| Setting | Storage field | Type | Description |
117117+| --------------------- | ---------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
118118+| **Enable EQ** | `global_settings.eq_enabled` | bool | Master on/off switch for the software EQ |
119119+| **Precut** | `global_settings.eq_precut` | 0..24.0 dB | Global negative gain applied to decoded audio before the EQ. Prevents distortion when boosting bands. Can also be used as a volume cap. Not applied when EQ is disabled |
120120+| **Graphical EQ** | — | screen | Graphical interface for adjusting gain, centre frequency, and Q for each band |
121121+| **Simple EQ** | — | screen | Simplified view: only gain is adjustable per band |
122122+| **Advanced EQ** | — | submenu | Same parameters as Graphical EQ, via text menus |
123123+| **Save EQ Preset** | — | action | Saves current EQ configuration to a `.cfg` file |
124124+| **Browse EQ Presets** | — | screen | Lists built-in presets and any saved configurations |
125125126126Applied via `dsp_set_eq_precut()` and `dsp_set_eq_coefs()` in `lib/rbcodec/dsp/eq.h`.
127127···131131132132**Storage:** `global_settings.replaygain_settings`
133133134134-| Setting | Storage field | Values / Range | Default | Description |
135135-|---|---|---|---|---|
136136-| **Type** | `.type` | track, album, track shuffle, off | shuffle | Which RG tag to read for normalization |
137137-| **No-Clip** | `.noclip` | bool | false | Scale down if RG adjustment would cause clipping |
138138-| **Preamp** | `.preamp` | −120..+120 dB (step 5) | 0 dB | Additional gain applied on top of the RG value |
134134+| Setting | Storage field | Values / Range | Default | Description |
135135+| ----------- | ------------- | -------------------------------- | ------- | ------------------------------------------------ |
136136+| **Type** | `.type` | track, album, track shuffle, off | shuffle | Which RG tag to read for normalization |
137137+| **No-Clip** | `.noclip` | bool | false | Scale down if RG adjustment would cause clipping |
138138+| **Preamp** | `.preamp` | −120..+120 dB (step 5) | 0 dB | Additional gain applied on top of the RG value |
139139140140Applied via `dsp_replaygain_set_settings()` in `lib/rbcodec/dsp/dsp_misc.h`.
141141···147147148148Overlaps the end of one track with the beginning of the next.
149149150150-| Setting | Storage field | Range | Default | Description |
151151-|---|---|---|---|---|
152152-| **Mode** | `global_settings.crossfade` | off, auto track change, manual skip, shuffle, shuffle+manual, always | off | When crossfading is triggered |
153153-| **Fade-In Delay** | `global_settings.crossfade_fade_in_delay` | 0..7 s | 0 s | Silence before the fade-in begins |
154154-| **Fade-Out Delay** | `global_settings.crossfade_fade_out_delay` | 0..7 s | 0 s | Silence before the fade-out begins |
155155-| **Fade-In Duration** | `global_settings.crossfade_fade_in_duration` | 0..15 s | 2 s | Length of the fade-in ramp |
156156-| **Fade-Out Duration** | `global_settings.crossfade_fade_out_duration` | 0..15 s | 2 s | Length of the fade-out ramp |
157157-| **Fade-Out Mode** | `global_settings.crossfade_fade_out_mixmode` | crossfade, mix | crossfade | Whether the outgoing track fades out or mixes at a flat level |
150150+| Setting | Storage field | Range | Default | Description |
151151+| --------------------- | --------------------------------------------- | -------------------------------------------------------------------- | --------- | ------------------------------------------------------------- |
152152+| **Mode** | `global_settings.crossfade` | off, auto track change, manual skip, shuffle, shuffle+manual, always | off | When crossfading is triggered |
153153+| **Fade-In Delay** | `global_settings.crossfade_fade_in_delay` | 0..7 s | 0 s | Silence before the fade-in begins |
154154+| **Fade-Out Delay** | `global_settings.crossfade_fade_out_delay` | 0..7 s | 0 s | Silence before the fade-out begins |
155155+| **Fade-In Duration** | `global_settings.crossfade_fade_in_duration` | 0..15 s | 2 s | Length of the fade-in ramp |
156156+| **Fade-Out Duration** | `global_settings.crossfade_fade_out_duration` | 0..15 s | 2 s | Length of the fade-out ramp |
157157+| **Fade-Out Mode** | `global_settings.crossfade_fade_out_mixmode` | crossfade, mix | crossfade | Whether the outgoing track fades out or mixes at a flat level |
158158159159---
160160···183183184184Enabling Timestretch allows playback speed to be changed independently of pitch. Intended primarily for speech playback — may noticeably degrade the listening experience with complex music.
185185186186-| Setting | Storage field | Range | Default | Description |
187187-|---|---|---|---|---|
188188-| **Pitch** | `global_status.resume_pitch` | ~50..200 % | 100 % | Pitch shift without changing tempo |
189189-| **Speed** | `global_status.resume_speed` | ~35..250 % | 100 % | Playback speed without changing pitch |
190190-| **Timestretch Enable** | `global_settings.timestretch_enabled` | bool | false | Enables the TDHS time-domain algorithm; accessible via the Pitch Screen after reboot |
186186+| Setting | Storage field | Range | Default | Description |
187187+| ---------------------- | ------------------------------------- | ---------- | ------- | ------------------------------------------------------------------------------------ |
188188+| **Pitch** | `global_status.resume_pitch` | ~50..200 % | 100 % | Pitch shift without changing tempo |
189189+| **Speed** | `global_status.resume_speed` | ~35..250 % | 100 % | Playback speed without changing pitch |
190190+| **Timestretch Enable** | `global_settings.timestretch_enabled` | bool | false | Enables the TDHS time-domain algorithm; accessible via the Pitch Screen after reboot |
191191192192Applied via `sound_set_pitch()` and `dsp_timestretch_enable()` in `lib/rbcodec/dsp/tdspeed.h`.
193193···197197198198**Storage:** `global_settings.play_frequency` | Condition: `HAVE_PLAY_FREQ`
199199200200-| Setting | Values | Description |
201201-|---|---|---|
200200+| Setting | Values | Description |
201201+| ------------------ | ---------------------------------------- | ---------------------------------------------------------------- |
202202| **Play Frequency** | auto, 44.1 kHz, 48 kHz, 88.2 kHz, 96 kHz | Output sample rate. `auto` matches the source file's native rate |
203203204204---
···209209210210Implements the **Haas effect** with an adjustable delay time to enhance the stereo image. A full-range Haas effect creates the impression that sound starts from one channel and ends in the other. Four additional controls move the perceived stage back toward the centre:
211211212212-| Setting | Storage field | Range | Default | Description |
213213-|---|---|---|---|---|
214214-| **Enable** | `global_settings.surround_enabled` | 0, 5, 8, 10, 15, 30 ms | 0 (off) | Delay time for the Haas effect; 0 disables it |
215215-| **Balance** | `global_settings.surround_balance` | 0..99 % | 35 % | Left/right channel output ratio to re-centre the stage |
216216-| **f(x1) — HF Cutoff** | `global_settings.surround_fx1` | 600..8000 Hz (step 200) | 3400 Hz | Upper boundary of a bypass band for frequencies (mostly vocals) that are not affected by the surround processing |
217217-| **f(x2) — LF Cutoff** | `global_settings.surround_fx2` | 40..400 Hz (step 40) | 320 Hz | Lower boundary of the bypass band |
218218-| **Side Only** | `global_settings.surround_method2` | bool | false | Uses mid-side processing to apply the effect to the side channel only, leaving the centre image unmodified |
219219-| **Dry/Wet Mix** | `global_settings.surround_mix` | 0..100 % (step 5) | 50 % | Proportion of original (dry) vs effected (wet) signal in the final output |
212212+| Setting | Storage field | Range | Default | Description |
213213+| --------------------- | ---------------------------------- | ----------------------- | ------- | ---------------------------------------------------------------------------------------------------------------- |
214214+| **Enable** | `global_settings.surround_enabled` | 0, 5, 8, 10, 15, 30 ms | 0 (off) | Delay time for the Haas effect; 0 disables it |
215215+| **Balance** | `global_settings.surround_balance` | 0..99 % | 35 % | Left/right channel output ratio to re-centre the stage |
216216+| **f(x1) — HF Cutoff** | `global_settings.surround_fx1` | 600..8000 Hz (step 200) | 3400 Hz | Upper boundary of a bypass band for frequencies (mostly vocals) that are not affected by the surround processing |
217217+| **f(x2) — LF Cutoff** | `global_settings.surround_fx2` | 40..400 Hz (step 40) | 320 Hz | Lower boundary of the bypass band |
218218+| **Side Only** | `global_settings.surround_method2` | bool | false | Uses mid-side processing to apply the effect to the side channel only, leaving the centre image unmodified |
219219+| **Dry/Wet Mix** | `global_settings.surround_mix` | 0..100 % (step 5) | 50 % | Proportion of original (dry) vs effected (wet) signal in the final output |
220220221221Applied via `dsp_surround_enable()` and related functions in `lib/rbcodec/dsp/surround.h`.
222222···228228229229Implements a group delay correction and an additional biophonic EQ to boost bass perception.
230230231231-| Setting | Storage field | Range | Default | Description |
232232-|---|---|---|---|---|
233233-| **PBE** | `global_settings.pbe` | 0..100 % (step 25) | 0 % (off) | Strength of the bass enhancement effect |
234234-| **PBE Precut** | `global_settings.pbe_precut` | −4.5..0 dB (step 0.1) | −2.5 dB | Negative overall gain applied to prevent audio distortion caused by the EQ gain. Stacks with any other EQ applied |
231231+| Setting | Storage field | Range | Default | Description |
232232+| -------------- | ---------------------------- | --------------------- | --------- | ----------------------------------------------------------------------------------------------------------------- |
233233+| **PBE** | `global_settings.pbe` | 0..100 % (step 25) | 0 % (off) | Strength of the bass enhancement effect |
234234+| **PBE Precut** | `global_settings.pbe_precut` | −4.5..0 dB (step 0.1) | −2.5 dB | Negative overall gain applied to prevent audio distortion caused by the EQ gain. Stacks with any other EQ applied |
235235236236Applied via `dsp_pbe_enable()` and `dsp_pbe_precut()` in `lib/rbcodec/dsp/pbe.h`.
237237···243243244244Human hearing is more sensitive to certain frequency bands. AFR applies additional equalization and bi-shelf filtering to reduce energy in those bands, minimising the risk of temporary threshold shift (auditory fatigue) during extended listening sessions.
245245246246-| Setting | Values | Default |
247247-|---|---|---|
248248-| **AFR Enable** | off, weak, moderate, strong | off |
246246+| Setting | Values | Default |
247247+| -------------- | --------------------------- | ------- |
248248+| **AFR Enable** | off, weak, moderate, strong | off |
249249250250Applied via `dsp_afr_enable()` in `lib/rbcodec/dsp/afr.h`.
251251···257257258258The compressor reduces the dynamic range of the audio signal by progressively reducing the gain of louder signals. When the compressed signal is subsequently amplified (via makeup gain), the quiet sections become louder while the loud sections stay below clipping. This is useful for listening to dynamic material in noisy environments.
259259260260-| Setting | Storage field | Values / Range | Default | Description |
261261-|---|---|---|---|---|
262262-| **Threshold** | `.threshold` | off, −3, −6, −9, −12, −15, −18, −21, −24 dB | off | Input level above which compression begins. The maximum compression (minimum operating level) is −24 dB |
263263-| **Makeup Gain** | `.makeup_gain` | off, auto | auto | **Off:** no re-amplification after compression. **Auto:** amplifies so the loudest post-compression signal is just below clipping, restoring perceived loudness |
264264-| **Ratio** | `.ratio` | 2:1, 4:1, 6:1, 10:1, limit | 2:1 | For every N dB above threshold, the output rises by only 1 dB. **Limit** = ∞:1 — the output cannot exceed the threshold at all |
265265-| **Knee** | `.knee` | hard, soft | soft | **Hard knee:** transition occurs precisely at the threshold. **Soft knee:** transition is smoothed over ±3 dB around the threshold |
266266-| **Attack Time** | `.attack_time` | 0..30 ms (step 5) | 5 ms | Delay between the input signal exceeding the threshold and the compressor acting on it |
267267-| **Release Time** | `.release_time` | 100..1000 ms (step 100) | 500 ms | Time for the gain to recover by 10 dB after the signal drops below the threshold. A longer release time reduces "pumping" artefacts |
260260+| Setting | Storage field | Values / Range | Default | Description |
261261+| ---------------- | --------------- | ------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
262262+| **Threshold** | `.threshold` | off, −3, −6, −9, −12, −15, −18, −21, −24 dB | off | Input level above which compression begins. The maximum compression (minimum operating level) is −24 dB |
263263+| **Makeup Gain** | `.makeup_gain` | off, auto | auto | **Off:** no re-amplification after compression. **Auto:** amplifies so the loudest post-compression signal is just below clipping, restoring perceived loudness |
264264+| **Ratio** | `.ratio` | 2:1, 4:1, 6:1, 10:1, limit | 2:1 | For every N dB above threshold, the output rises by only 1 dB. **Limit** = ∞:1 — the output cannot exceed the threshold at all |
265265+| **Knee** | `.knee` | hard, soft | soft | **Hard knee:** transition occurs precisely at the threshold. **Soft knee:** transition is smoothed over ±3 dB around the threshold |
266266+| **Attack Time** | `.attack_time` | 0..30 ms (step 5) | 5 ms | Delay between the input signal exceeding the threshold and the compressor acting on it |
267267+| **Release Time** | `.release_time` | 100..1000 ms (step 100) | 500 ms | Time for the gain to recover by 10 dB after the signal drops below the threshold. A longer release time reduces "pumping" artefacts |
268268269269Applied via `dsp_set_compressor()` in `lib/rbcodec/dsp/compressor.h`.
270270···272272273273## UI Audio Feedback
274274275275-| Setting | Storage field | Values | Default | Description |
276276-|---|---|---|---|---|
277277-| **Beep** | `global_settings.beep` | off, weak, moderate, strong | off | Audible tone on track change or key events |
278278-| **Keyclick** | `global_settings.keyclick` | off, weak, moderate, strong | off | Audio click on every key press |
279279-| **Keyclick Repeats** | `global_settings.keyclick_repeats` | bool | false | Whether held keys also produce a click |
275275+| Setting | Storage field | Values | Default | Description |
276276+| -------------------- | ---------------------------------- | --------------------------- | ------- | ------------------------------------------ |
277277+| **Beep** | `global_settings.beep` | off, weak, moderate, strong | off | Audible tone on track change or key events |
278278+| **Keyclick** | `global_settings.keyclick` | off, weak, moderate, strong | off | Audio click on every key press |
279279+| **Keyclick Repeats** | `global_settings.keyclick_repeats` | bool | false | Whether held keys also produce a click |
280280281281---
282282283283## Summary
284284285285-| Category | Setting count |
286286-|---|---|
287287-| Volume & limit | 2 |
288288-| Channel / stereo | 3 |
289289-| Crossfeed | 5 |
290290-| Software EQ (10 bands) | 12 |
291291-| ReplayGain | 3 |
292292-| Crossfade | 6 |
293293-| Dithering | 1 |
294294-| Pitch / Time-Stretch | 3 |
295295-| Output sample rate | 1 |
296296-| Haas Surround | 6 |
297297-| PBE | 2 |
298298-| AFR | 1 |
299299-| Compressor | 6 |
300300-| UI audio feedback | 3 |
301301-| **Total** | **~54** |
285285+| Category | Setting count |
286286+| ---------------------- | ------------- |
287287+| Volume & limit | 2 |
288288+| Channel / stereo | 3 |
289289+| Crossfeed | 5 |
290290+| Software EQ (10 bands) | 12 |
291291+| ReplayGain | 3 |
292292+| Crossfade | 6 |
293293+| Dithering | 1 |
294294+| Pitch / Time-Stretch | 3 |
295295+| Output sample rate | 1 |
296296+| Haas Surround | 6 |
297297+| PBE | 2 |
298298+| AFR | 1 |
299299+| Compressor | 6 |
300300+| UI audio feedback | 3 |
301301+| **Total** | **~54** |
···11# CLAUDE.md — Rockbox Zig
2233+## Markdown formatting
44+55+### Tables
66+77+Always align markdown tables so that column pipes line up in raw text. Every cell in a column must be padded with trailing spaces to the width of the widest cell in that column. The separator row must use the same number of dashes as the column width. Example:
88+99+```markdown
1010+| Name | Role | Notes |
1111+| ------- | --------- | ---------------------------- |
1212+| Alice | Engineer | Owns the firmware layer |
1313+| Bob | Designer | Works on the mobile UI |
1414+| Charlie | QA | Runs integration test suites |
1515+```
1616+1717+When editing an existing table, re-align the whole table (not just the changed row). When adding a new table, align it before committing.
1818+1919+---
2020+321## Project overview
422523Rockbox 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/Squeezelite output sinks, and a desktop/web UI.
···114132115133The 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).
116134117117-| Enum constant | Value | Implementation file |
118118-|--------------------|-------|---------------------------------------------|
119119-| `PCM_SINK_BUILTIN` | 0 | `firmware/target/hosted/sdl/pcm-sdl.c` |
120120-| `PCM_SINK_FIFO` | 1 | `firmware/target/hosted/pcm-fifo.c` |
121121-| `PCM_SINK_AIRPLAY` | 2 | `firmware/target/hosted/pcm-airplay.c` |
122122-| `PCM_SINK_SQUEEZELITE` | 3 | `firmware/target/hosted/pcm-squeezelite.c` |
135135+| Enum constant | Value | Implementation file |
136136+| ---------------------- | ----- | ------------------------------------------ |
137137+| `PCM_SINK_BUILTIN` | 0 | `firmware/target/hosted/sdl/pcm-sdl.c` |
138138+| `PCM_SINK_FIFO` | 1 | `firmware/target/hosted/pcm-fifo.c` |
139139+| `PCM_SINK_AIRPLAY` | 2 | `firmware/target/hosted/pcm-airplay.c` |
140140+| `PCM_SINK_SQUEEZELITE` | 3 | `firmware/target/hosted/pcm-squeezelite.c` |
123141124142`crates/settings/src/lib.rs:load_settings()` reads `audio_output` and calls `pcm::switch_sink()`.
125143
···991010Understanding the boundary between these two classes is critical. Crossing it incorrectly (e.g. doing a plain OS-level block inside a Rockbox kernel thread) will silently starve every other Rockbox kernel thread.
11111212+The scheduler implementation differs between the **SDL hosted target** (desktop, macOS/Linux) and the **headless Android cdylib** target. Both share the same C-level `create_thread` / kernel-thread API, but the underlying concurrency primitive is different.
1313+1214---
13151414-## The SDL Cooperative Scheduler
1616+## The SDL Cooperative Scheduler (desktop — macOS / Linux)
15171616-On the `sdlapp` hosted target (macOS / Linux desktop), each Rockbox kernel thread is backed by a real SDL OS thread (`SDL_CreateThread`). However, Rockbox layers a **cooperative scheduler** on top of those OS threads using a single global SDL mutex:
1818+On the `sdlapp` hosted target, each Rockbox kernel thread is backed by a real SDL OS thread (`SDL_CreateThread`). However, Rockbox layers a **cooperative scheduler** on top of those OS threads using a single global SDL mutex:
17191820```c
1921// firmware/target/hosted/sdl/thread-sdl.c
···55575658---
57596060+## The Headless Scheduler (Android cdylib)
6161+6262+The Android cdylib target (`firmware/target/hosted/android/cdylib/`) replaces SDL entirely with plain pthreads and POSIX clock. There is **no SDL mutex**, no event loop, no LCD, no button polling.
6363+6464+### `system-android.c` vs `system-sdl.c`
6565+6666+| Concern | SDL hosted | Android cdylib |
6767+| ----------------- | ----------------------------------- | ------------------------------------------------------- |
6868+| Cooperative token | `SDL_mutex *m` (single global) | `__cores[0].running` (global current-thread pointer) |
6969+| Thread creation | `SDL_CreateThread` | `pthread_create` |
7070+| Scheduler yield | `SDL_UnlockMutex` / `SDL_LockMutex` | Rockbox kernel pthread mutex round-trip |
7171+| Boot path | SDL event thread initialises audio | `rb_daemon_start` (JNI) spawns `rockbox-engine` pthread |
7272+| Power-off | SDL_QUIT event loop | `exit(0)` via `power_off()` |
7373+| stdio | terminal / controlled | piped to logcat via `redirect_stdio_to_logcat` |
7474+7575+### `__cores[0].running` — the critical invariant
7676+7777+On the headless pthread target, the Rockbox kernel tracks the currently-executing kernel thread via the global `__cores[0].running` pointer. **Any firmware function that calls `queue_send`, `wakeup_thread`, `pcmbuf_*`, or any kernel primitive reads this pointer to find its own thread entry.**
7878+7979+If such a function is called from a non-Rockbox pthread (e.g. an actix worker or a tonic gRPC handler) the pointer resolves to the wrong thread entry. Consequences:
8080+- `wakeup_thread_` dereferences a stale or null function pointer → **SIGSEGV at PC=0**
8181+- Kernel scheduler state is silently corrupted
8282+- Symptoms may appear seconds later, during a track switch or settings change
8383+8484+This is why **all firmware-mutating calls from Rust handlers must go through the firmware-command bus** (see below).
8585+8686+### Daemon boot sequence (Android)
8787+8888+`rb_daemon_start(configDir, musicDir, deviceName)` in `crates/expo/src/daemon.rs`:
8989+9090+1. Atomically transitions `STATE`: `STOPPED → STARTING` (returns `-114` if already running).
9191+2. Installs the tracing-android logcat subscriber (idempotent).
9292+3. Sets env vars: `HOME`, `ROCKBOX_LIBRARY`, `TMPDIR`, `ROCKBOX_PORT`, `ROCKBOX_GRAPHQL_PORT`, `ROCKBOX_TCP_PORT`, `ROCKBOX_MPD_PORT`.
9393+4. Spawns `rockbox-engine` pthread (2 MB stack) that calls `main_c()` wrapped in `catch_unwind`.
9494+5. Polls TCP `127.0.0.1:<port>` every 50 ms, up to 30 s, waiting for gRPC to bind.
9595+6. On success: stores port in `LOCAL_PORT`, transitions to `RUNNING`, sets `SERVER_URL` if not already overridden by JS, spawns `rockbox-library-scan` thread.
9696+7. Returns the bound gRPC port (positive) or a negative error code (`-110` = timeout, `-114` = already running).
9797+9898+---
9999+100100+## The Firmware-Command Bus (`crates/server/src/fw_bus.rs`)
101101+102102+### Problem
103103+104104+gRPC / HTTP handlers run on actix workers or tonic tasks — plain Rust OS threads. Calling firmware FFI directly from these threads violates the `__cores[0].running` invariant and causes the SIGSEGV / scheduler corruption described above. This affects both the Android cdylib and any future hosted-pthread build.
105105+106106+### Solution
107107+108108+All kernel-mutating calls are serialised through a single `std::sync::mpsc` channel. **Only the broker thread drains this channel.** The broker IS a real Rockbox kernel thread (spawned by `apps/broker_thread.c::create_thread`), so its firmware calls always run with a valid `__running_self_entry()`.
109109+110110+```
111111+actix worker / tonic task
112112+ → fw_bus::send(FwCmd::Play { elapsed, offset, reply })
113113+ → mpsc channel
114114+ → broker thread (real Rockbox kernel thread)
115115+ → rb::playback::play(elapsed, offset)
116116+ → reply_tx.send(())
117117+ ← fw_bus::send_and_wait blocks until reply arrives (≤ 30 s)
118118+```
119119+120120+### API
121121+122122+| Function | Use case |
123123+| ------------------------------ | -------------------------------------------------------------------- |
124124+| `fw_bus::init()` | Call once at startup, before broker thread spawns |
125125+| `fw_bus::send(cmd)` | Fire-and-forget (no reply needed) |
126126+| `fw_bus::send_and_wait(make)` | Send + block until broker confirms (for actix `web::block` handlers) |
127127+| `fw_bus::run_on_broker(f)` | Run arbitrary closure on broker; returns `T::default()` on timeout |
128128+| `fw_bus::try_run_on_broker(f)` | Same but returns `Option<T>` — `None` on timeout |
129129+| `fw_bus::drain(rx)` | Called once per broker iteration to execute pending commands |
130130+131131+### `FwCmd` variants
132132+133133+`Play`, `Pause`, `Resume`, `Next`, `Prev`, `Stop`, `FfRewind`, `FlushAndReloadTracks`, `SetCrossfade`, and `Custom(Box<dyn FnOnce() + Send>)` (escape hatch for anything not enumerated).
134134+135135+### Timeout behaviour
136136+137137+The broker tick period is ~10 ms at idle (the `rb::system::sleep(HZ/10)` in the broker loop). `BROKER_TIMEOUT` is 30 s — generous because building a large playlist can take several seconds. On timeout `run_on_broker` logs a warning and returns `T::default()` rather than panicking; `try_run_on_broker` returns `None`.
138138+139139+### Where fw_bus is NOT needed
140140+141141+- Read-only status queries (`rb::playback::status()`, `rb::playback::current_track()`) — these only read atomics or copy structs; no kernel primitive is called.
142142+- Anything running inside the broker thread itself (it already owns `__running_self_entry()`).
143143+- Desktop SDL builds — the SDL mutex serialises everything anyway. The bus is still compiled in and works correctly; it just adds one channel hop of latency.
144144+145145+---
146146+58147## Thread Map
5914860149### Rockbox kernel threads (must yield)
611506262-| Thread | C entry point | What it does |
6363-|--------|---------------|--------------|
6464-| `server_thread` | `server_thread.c` → `start_server()` | Spawns the actix HTTP server in a Rust OS thread, then loops yielding to the Rockbox scheduler |
6565-| `broker_thread` | `broker_thread.c` → `start_broker()` | Event loop: publishes playback state to GraphQL subscriptions, scrobbles tracks, restores playlist |
151151+| Thread | C entry point | What it does |
152152+| --------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
153153+| `server_thread` | `server_thread.c` → `start_server()` | Spawns the actix HTTP server in a Rust OS thread, then loops yielding to the Rockbox scheduler |
154154+| `broker_thread` | `broker_thread.c` → `start_broker()` | Event loop: drains `fw_bus`, publishes playback state to GraphQL subscriptions, scrobbles tracks, restores playlist |
661556767-Both threads must call `rb::system::sleep(rb::HZ)` on every loop iteration.
156156+Both threads must call `rb::system::sleep(rb::HZ)` (desktop) or equivalent on every loop iteration.
6815769158**`server_thread` pattern** (`crates/server/src/lib.rs`):
70159```rust
···89178**`broker_thread` pattern** (`crates/server/src/lib.rs`):
90179```rust
91180pub extern "C" fn start_broker() {
9292- // ... setup ...
181181+ let fw_rx = fw_bus::take_receiver();
93182 loop {
9494- // ... do work (check playback, publish events) ...
183183+ // Drain firmware commands from actix/gRPC handlers first.
184184+ if let Some(rx) = &fw_rx {
185185+ fw_bus::drain(rx);
186186+ }
187187+ // ... publish events, scrobble, restore playlist ...
95188 thread::sleep(std::time::Duration::from_millis(100));
9696- rb::system::sleep(rb::HZ); // yield the Rockbox mutex
189189+ rb::system::sleep(rb::HZ);
97190 }
98191}
99192```
100193101194### Rust OS threads (no Rockbox scheduler involvement)
102195103103-These are spawned with `std::thread::spawn` — they are pure OS threads and never interact with the Rockbox mutex. They are free to block indefinitely.
196196+These are spawned with `std::thread::spawn` — they are pure OS threads and never interact with the Rockbox scheduler token or `__cores[0].running`.
197197+198198+| Thread / component | Spawned from | Runtime |
199199+| ---------------------------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------- |
200200+| **HTTP server** (actix-web, port 6063) | `start_server()` via `thread::spawn` | `actix_rt::System` (single-thread + LocalSet per arbiter) |
201201+| **gRPC server** (tonic, port 6061) | `start_servers()` via `thread::spawn` | `tokio::Builder::new_current_thread` |
202202+| **GraphQL server** (async-graphql, port 6062) | `start_servers()` via `thread::spawn` | `tokio::Builder::new_current_thread` |
203203+| **MPD server** (port 6600) | `start_servers()` via `thread::spawn` | `tokio::Builder::new_current_thread` |
204204+| **MPRIS server** (Linux, D-Bus) | `start_servers()` via `thread::spawn` | `async_std` task |
205205+| **UPnP runtime** | `rockbox_upnp::init()` (static `OnceLock`) | `tokio::runtime::Runtime::new()` (multi-thread) |
206206+| **Device scanners** (Chromecast, AirPlay, Snapcast, UPnP, Squeezelite) | `run_http_server()` via `thread::spawn` | each creates its own `tokio::runtime::Runtime::new()` |
207207+| **Player event listener** | `run_http_server()` via `thread::spawn` | `tokio::runtime::Runtime::new()` (multi-thread) |
208208+| **Command relay** | `start_servers()` via `thread::spawn` | `reqwest::blocking` (creates its own tokio internally) |
209209+210210+### Android-only Rust OS threads
104211105105-| Thread / component | Spawned from | Runtime |
106106-|--------------------|--------------|---------|
107107-| **HTTP server** (actix-web, port 6063) | `start_server()` via `thread::spawn` | `actix_rt::System` (single-thread + LocalSet per arbiter) |
108108-| **gRPC server** (tonic, port 6061) | `start_servers()` via `thread::spawn` | `tokio::Builder::new_current_thread` |
109109-| **GraphQL server** (async-graphql, port 6062) | `start_servers()` via `thread::spawn` | `tokio::Builder::new_current_thread` |
110110-| **MPD server** (port 6600) | `start_servers()` via `thread::spawn` | `tokio::Builder::new_current_thread` |
111111-| **MPRIS server** (Linux, D-Bus) | `start_servers()` via `thread::spawn` | `async_std` task |
112112-| **UPnP runtime** | `rockbox_upnp::init()` (static `OnceLock`) | `tokio::runtime::Runtime::new()` (multi-thread) |
113113-| **Device scanners** (Chromecast, AirPlay, Snapcast, UPnP, Squeezelite) | `run_http_server()` via `thread::spawn` | each creates its own `tokio::runtime::Runtime::new()` |
114114-| **Player event listener** | `run_http_server()` via `thread::spawn` | `tokio::runtime::Runtime::new()` (multi-thread) |
115115-| **Command relay** | `start_servers()` via `thread::spawn` | `reqwest::blocking` (creates its own tokio internally) |
212212+| Thread | Spawned from | Purpose |
213213+| ------------------------------------ | --------------------------------------------- | ------------------------------------------------------------------------------- |
214214+| **`rockbox-engine`** (2 MB stack) | `rb_daemon_start` | Calls `main_c()`; owns all Rockbox kernel threads and the cooperative scheduler |
215215+| **`rockbox-library-scan`** | `spawn_library_scan()` after gRPC binds | SQLite audio scan + Rocksky enrichment |
216216+| **`stdio-logcat-reader`** (detached) | `redirect_stdio_to_logcat()` at `system_init` | Reads the stdout/stderr pipe and writes lines to logcat tag `Rockbox` |
217217+| **`rockbox-rpc`** (× 2 workers) | `RT` (`Lazy<Runtime>`) in `crates/expo` | Tokio multi-thread runtime for all tonic gRPC client calls |
116218117219---
118220119119-## Startup Sequence
221221+## Startup Sequences
222222+223223+### Desktop (SDL hosted)
120224121225```
122226main.c: server_init()
···131235├── sleep(HZ) ← Rockbox scheduler sleep on main thread (~1 s)
132236│
133237└── start_servers() ← called on main C thread after 1 s
238238+ ├── fw_bus::init() ← create mpsc channel before broker spawns
134239 ├── thread::spawn(gRPC server) ← Rust OS thread, new_current_thread runtime
135240 ├── thread::spawn(GraphQL server) ← Rust OS thread, new_current_thread runtime
136241 ├── thread::spawn(MPD server) ← Rust OS thread, new_current_thread runtime
···141246│
142247└── create_thread(broker_thread) ← Rockbox kernel thread
143248 └── start_broker()
144144- └── loop { work + sleep + rb::system::sleep(HZ) }
249249+ ├── fw_bus::take_receiver() ← claim the mpsc Receiver
250250+ └── loop { fw_bus::drain + work + sleep + rb::system::sleep(HZ) }
251251+```
252252+253253+### Android cdylib
254254+255255+```
256256+JNI: RockboxRpcModule.OnCreate
257257+│
258258+└── rb_daemon_start(configDir, musicDir, deviceName) ← crates/expo/src/daemon.rs
259259+ ├── STATE: STOPPED → STARTING
260260+ ├── install_logcat_subscriber()
261261+ ├── configure_environment() ← sets HOME, ROCKBOX_LIBRARY, TMPDIR, ports
262262+ │
263263+ ├── thread::spawn("rockbox-engine", stack=2MB)
264264+ │ └── main_c() ← apps/main.c
265265+ │ ├── system_init() ← redirect_stdio_to_logcat + stackbegin
266266+ │ ├── server_init() ← create_thread(server_thread)
267267+ │ │ └── start_server()
268268+ │ │ ├── load_settings()
269269+ │ │ ├── fw_bus::init()
270270+ │ │ ├── thread::spawn(actix HTTP server)
271271+ │ │ └── loop { sleep + rb::system::sleep(HZ) }
272272+ │ ├── sleep(HZ)
273273+ │ ├── start_servers()
274274+ │ │ ├── thread::spawn(gRPC server)
275275+ │ │ ├── thread::spawn(GraphQL server)
276276+ │ │ ├── thread::spawn(MPD server)
277277+ │ │ └── thread::spawn(command relay)
278278+ │ └── broker_init() ← create_thread(broker_thread)
279279+ │ └── start_broker()
280280+ │ ├── fw_bus::take_receiver()
281281+ │ └── loop { fw_bus::drain + work + sleep + rb::system::sleep(HZ) }
282282+ │
283283+ ├── wait_for_grpc(:6061, 30s) ← polls TCP connect every 50 ms
284284+ ├── STATE: STARTING → RUNNING
285285+ ├── rb_set_server_url("http://127.0.0.1:6061") ← if JS hasn't overridden
286286+ └── spawn_library_scan(force=false) ← own OS thread + current_thread tokio runtime
145287```
146288147289---
···149291## Tokio Runtime Layout
150292151293Multiple independent tokio runtimes coexist; they do not share thread pools or event loops.
294294+295295+### Desktop
152296153297```
154298┌─────────────────────────────────────────────────────┐
···183327└─────────────────────────────────────────────────────┘
184328```
185329330330+### Android (additional runtimes)
331331+332332+```
333333+┌─────────────────────────────────────────────────────┐
334334+│ rockbox-rpc Runtime (multi-thread, 2 workers) │
335335+│ Lazy<Runtime> in crates/expo/src/lib.rs │
336336+│ Owns: all outbound tonic gRPC client calls │
337337+│ (rb_play, rb_pause, rb_status_json, streams, …) │
338338+└─────────────────────────────────────────────────────┘
339339+340340+┌─────────────────────────────────────────────────────┐
341341+│ library-scan Runtime (current-thread, ephemeral) │
342342+│ Created per spawn_library_scan() call │
343343+│ Owns: SQLite connection pool, audio_scan, │
344344+│ save_audio_metadata, Rocksky enrichment │
345345+└─────────────────────────────────────────────────────┘
346346+347347+┌─────────────────────────────────────────────────────┐
348348+│ save_remote_track_metadata Runtime (current-thread)│
349349+│ Created per call from C firmware (streamfd.c) │
350350+│ Safe: called from a Rockbox kernel thread, which │
351351+│ is never inside an existing async context │
352352+└─────────────────────────────────────────────────────┘
353353+```
354354+186355All runtimes share a single **SQLite database** (`~/.config/rockbox.org/rockbox-library.db`). The connection pool is configured with:
187356- WAL journal mode (concurrent readers + one writer)
188357- `busy_timeout = 30 s` (serialize concurrent writers instead of failing)
···240409### Rule 5: `SimpleBroker` is runtime-agnostic
241410242411`rockbox_graphql::simplebroker::SimpleBroker` uses `futures_channel::mpsc::UnboundedSender` for pub/sub. `publish()` is a synchronous call — it does not require or interact with any tokio runtime. It is safe to call from any thread, including Rockbox kernel threads and scanner threads with their own runtimes.
412412+413413+### Rule 6: All firmware-mutating FFI from Rust handlers must go through `fw_bus`
414414+415415+On the headless pthread target (Android cdylib) calling `rb::playback::play`, `rb::sound::set_volume`, or any function that reaches `queue_send` / `wakeup_thread` from a non-Rockbox pthread corrupts `__cores[0].running` and causes a SIGSEGV. Route every such call through `fw_bus::run_on_broker(|| ...)` from inside a `web::block(...)` closure.
416416+417417+Read-only queries (status, current track) that only touch atomics or copy structs are safe to call directly.
418418+419419+### Rule 7: `fw_bus::init()` must be called before any handler sends a command
420420+421421+`fw_bus::send()` silently drops commands if the bus hasn't been initialised. `init()` is called inside `start_servers()`, which runs before the HTTP/gRPC servers start accepting requests. On Android, the boot sequence guarantees `start_servers()` (inside `main_c()`) completes before `rb_daemon_start` returns.
···111111following vtable:
112112113113| Op | Implementation |
114114-|-------------------|---------------------------------------------------------------------|
114114+| ----------------- | ------------------------------------------------------------------- |
115115| `init` | `pthread_mutex_init` (recursive) |
116116| `postinit` | no-op |
117117| `set_freq` | no-op (sample rate is fixed at 44100 Hz) |
118118| `lock` / `unlock` | `pthread_mutex_lock/unlock` |
119119-| `play` | `sink_dma_start` — connects all receivers, spawns `airplay_thread` |
119119+| `play` | `sink_dma_start` — connects all receivers, spawns `airplay_thread` |
120120| `stop` | `sink_dma_stop` — signals thread, joins, calls `pcm_airplay_stop()` |
121121122122`airplay_pcm_sink` is registered at index `PCM_SINK_AIRPLAY = 2` in the
···148148149149`crates/airplay/src/lib.rs` exports these `#[no_mangle] extern "C"` functions:
150150151151-| C symbol | Purpose |
152152-|-----------------------------|----------------------------------------------------------|
153153-| `pcm_airplay_set_host` | Set a single receiver (clears any previous list) |
154154-| `pcm_airplay_add_receiver` | Append one receiver to the multi-room list |
155155-| `pcm_airplay_clear_receivers` | Clear the receiver list before re-configuring |
156156-| `pcm_airplay_connect` | Open RTSP + RTP sessions for all configured receivers |
157157-| `pcm_airplay_write` | Buffer PCM, encode ALAC once, fan out to every receiver |
158158-| `pcm_airplay_stop` | Send TEARDOWN to all, clear session |
159159-| `pcm_airplay_close` | Same as stop (called on sink switch) |
151151+| C symbol | Purpose |
152152+| ----------------------------- | ------------------------------------------------------- |
153153+| `pcm_airplay_set_host` | Set a single receiver (clears any previous list) |
154154+| `pcm_airplay_add_receiver` | Append one receiver to the multi-room list |
155155+| `pcm_airplay_clear_receivers` | Clear the receiver list before re-configuring |
156156+| `pcm_airplay_connect` | Open RTSP + RTP sessions for all configured receivers |
157157+| `pcm_airplay_write` | Buffer PCM, encode ALAC once, fan out to every receiver |
158158+| `pcm_airplay_stop` | Send TEARDOWN to all, clear session |
159159+| `pcm_airplay_close` | Same as stop (called on sink switch) |
160160161161`SESSION` is a `Mutex<Option<AirPlaySession>>`. `CONFIG` is a
162162`Mutex<AirPlayConfig>` holding `receivers: Vec<(String, u16)>`.
···327327328328Owns the two UDP sockets for one AirPlay endpoint:
329329330330-| Socket | Direction | Purpose |
331331-|--------------|-------------------------|---------------------|
332332-| `audio_sock` | → receiver audio port | RTP audio frames |
333333-| `ctrl_sock` | ↔ receiver control port | RTCP sync packets |
330330+| Socket | Direction | Purpose |
331331+| ------------ | ----------------------- | ----------------- |
332332+| `audio_sock` | → receiver audio port | RTP audio frames |
333333+| `ctrl_sock` | ↔ receiver control port | RTCP sync packets |
334334335335Also holds `ssrc` (random per receiver) and `seqnum` (wrapping u16).
336336
···2233The mobile-side Rust crate. Two builds in one workspace:
4455-| Build | Output | Size | Purpose |
66-|---|---|---|---|
77-| **Default** (`cargo build -p rockbox-expo`) | `staticlib` + `cdylib` | ~6 MB | Thin tonic gRPC client — controls a remote rockboxd over LAN. iOS, web, and "remote-only" Android variants use this. |
88-| **`--features embedded-daemon`** (Android only) | `cdylib` | ~48 MB | Full in-process rockboxd: C firmware + 44 statically-linked codecs + all Rust sink crates + gRPC/HTTP/GraphQL/MPD servers + AAudio sink + mDNS discovery. The phone becomes a symmetric peer of any LAN rockboxd. |
55+| Build | Output | Size | Purpose |
66+| ----------------------------------------------- | ---------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
77+| **Default** (`cargo build -p rockbox-expo`) | `staticlib` + `cdylib` | ~6 MB | Thin tonic gRPC client — controls a remote rockboxd over LAN. iOS, web, and "remote-only" Android variants use this. |
88+| **`--features embedded-daemon`** (Android only) | `cdylib` | ~48 MB | Full in-process rockboxd: C firmware + 44 statically-linked codecs + all Rust sink crates + gRPC/HTTP/GraphQL/MPD servers + AAudio sink + mDNS discovery. The phone becomes a symmetric peer of any LAN rockboxd. |
991010The Expo Modules wrapper at [`expo/modules/rockbox-rpc/`](../../expo/modules/rockbox-rpc/)
1111loads the resulting library and forwards calls through React Native.
···73737474## Surface map
75757676-| Group | Examples |
7777-|-------|----------|
7878-| Init | `rb_set_server_url`, `rb_set_http_url`, `rb_ping` |
7979-| Playback | `rb_play / pause / play_pause / next / prev`, `rb_seek`, `rb_play_album / play_artist_tracks / play_track / play_directory` |
8080-| Queue | `rb_jump_to_queue_position`, `rb_insert_tracks`, `rb_insert_track_next / last`, `rb_remove_from_queue`, `rb_shuffle_playlist`, `rb_get_playlist_current_json` |
8181-| Library | `rb_get_tracks_json`, `rb_get_artists_json`, `rb_get_album_json`, `rb_search_json`, `rb_like_track / unlike_track`, `rb_get_liked_tracks_json` |
8282-| Sound / Settings | `rb_adjust_volume`, `rb_sound_current_json`, `rb_save_shuffle / save_repeat`, `rb_get_global_settings_json`, `rb_get_global_status_json` |
8383-| Browse | `rb_tree_get_entries_json` |
8484-| Saved playlists | `rb_get_saved_playlists_json`, `rb_create_saved_playlist`, `rb_update_saved_playlist`, `rb_delete_saved_playlist`, `rb_add_track_to_playlist`, `rb_remove_track_from_playlist`, `rb_get_saved_playlist_tracks_json`, `rb_play_saved_playlist` |
8585-| Smart playlists | `rb_get_smart_playlists_json`, `rb_get_smart_playlist_tracks_json`, `rb_play_smart_playlist` |
8686-| Bluetooth | `rb_bluetooth_available`, `rb_get_bluetooth_devices_json`, `rb_connect_bluetooth`, `rb_disconnect_bluetooth` |
8787-| Server-streaming | `rb_subscribe_status`, `rb_subscribe_current_track`, `rb_subscribe_playlist`, `rb_subscribe_library`, `rb_subscribe_discovery(serviceName)` |
8888-| Stream pump | `rb_poll_event(subId, timeoutMs)`, `rb_unsubscribe(subId)` |
8989-| Discovery constants | `rb_rockbox_service_name`, `rb_chromecast_service_name` |
9090-| Memory | `rb_free_string` |
9191-| **Embedded daemon** | `rb_daemon_start(configDir, musicDir, deviceName)`, `rb_daemon_port`, `rb_daemon_state`, `rb_rescan_library` |
7676+| Group | Examples |
7777+| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7878+| Init | `rb_set_server_url`, `rb_set_http_url`, `rb_ping` |
7979+| Playback | `rb_play / pause / play_pause / next / prev`, `rb_seek`, `rb_play_album / play_artist_tracks / play_track / play_directory` |
8080+| Queue | `rb_jump_to_queue_position`, `rb_insert_tracks`, `rb_insert_track_next / last`, `rb_remove_from_queue`, `rb_shuffle_playlist`, `rb_get_playlist_current_json` |
8181+| Library | `rb_get_tracks_json`, `rb_get_artists_json`, `rb_get_album_json`, `rb_search_json`, `rb_like_track / unlike_track`, `rb_get_liked_tracks_json` |
8282+| Sound / Settings | `rb_adjust_volume`, `rb_sound_current_json`, `rb_save_shuffle / save_repeat`, `rb_get_global_settings_json`, `rb_get_global_status_json` |
8383+| Browse | `rb_tree_get_entries_json` |
8484+| Saved playlists | `rb_get_saved_playlists_json`, `rb_create_saved_playlist`, `rb_update_saved_playlist`, `rb_delete_saved_playlist`, `rb_add_track_to_playlist`, `rb_remove_track_from_playlist`, `rb_get_saved_playlist_tracks_json`, `rb_play_saved_playlist` |
8585+| Smart playlists | `rb_get_smart_playlists_json`, `rb_get_smart_playlist_tracks_json`, `rb_play_smart_playlist` |
8686+| Bluetooth | `rb_bluetooth_available`, `rb_get_bluetooth_devices_json`, `rb_connect_bluetooth`, `rb_disconnect_bluetooth` |
8787+| Server-streaming | `rb_subscribe_status`, `rb_subscribe_current_track`, `rb_subscribe_playlist`, `rb_subscribe_library`, `rb_subscribe_discovery(serviceName)` |
8888+| Stream pump | `rb_poll_event(subId, timeoutMs)`, `rb_unsubscribe(subId)` |
8989+| Discovery constants | `rb_rockbox_service_name`, `rb_chromecast_service_name` |
9090+| Memory | `rb_free_string` |
9191+| **Embedded daemon** | `rb_daemon_start(configDir, musicDir, deviceName)`, `rb_daemon_port`, `rb_daemon_state`, `rb_rescan_library` |
92929393---
9494···292292The cdylib-specific firmware sources live under
293293`firmware/target/hosted/android/cdylib/`:
294294295295-| File | Role |
296296-|---|---|
297297-| `system-android.c` | Headless system_init + power_off + stdout/stderr→logcat shim |
298298-| `pcm-aaudio.c` | AAudio PCM sink (replaces SDL audio) |
299299-| `lc-android.c` | `lc_open()` / `lc_get_header()` over the static `lc_static_table[]` |
300300-| `rb_zig_compat.c` | C compat layer for the 18 `rb_*` symbols `crates/sys` would otherwise pull from `zig/src/main.zig` |
301301-| `lcd-noop.c`, `button-noop.c`, `cpuinfo-noop.c`, `audiohw-noop.c` | Stubs — UI lives in React Native, not on the device LCD |
295295+| File | Role |
296296+| ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
297297+| `system-android.c` | Headless system_init + power_off + stdout/stderr→logcat shim |
298298+| `pcm-aaudio.c` | AAudio PCM sink (replaces SDL audio) |
299299+| `lc-android.c` | `lc_open()` / `lc_get_header()` over the static `lc_static_table[]` |
300300+| `rb_zig_compat.c` | C compat layer for the 18 `rb_*` symbols `crates/sys` would otherwise pull from `zig/src/main.zig` |
301301+| `lcd-noop.c`, `button-noop.c`, `cpuinfo-noop.c`, `audiohw-noop.c` | Stubs — UI lives in React Native, not on the device LCD |
302302303303---
304304···371371The configure script reads two env vars when it sees the androidcdylib
372372target — set them before running if you need non-default values:
373373374374-| Var | Default | Purpose |
375375-|---|---|---|
376376-| `ANDROID_NDK_HOME` | _(none — required)_ | Path to NDK install root |
377377-| `ANDROID_TARGET_ABI` | `arm64-v8a` | One of `arm64-v8a` / `armeabi-v7a` / `x86_64` |
378378-| `ANDROID_API_LEVEL` | `26` | Minimum SDK; **don't go below 26** (AAudio requires it) |
374374+| Var | Default | Purpose |
375375+| -------------------- | ------------------- | ------------------------------------------------------- |
376376+| `ANDROID_NDK_HOME` | _(none — required)_ | Path to NDK install root |
377377+| `ANDROID_TARGET_ABI` | `arm64-v8a` | One of `arm64-v8a` / `armeabi-v7a` / `x86_64` |
378378+| `ANDROID_API_LEVEL` | `26` | Minimum SDK; **don't go below 26** (AAudio requires it) |
379379380380For a 32-bit ARM build, e.g.:
381381···536536537537Tag map:
538538539539-| logcat tag | Source |
540540-|---|---|
541541-| `rockbox` | Rust `tracing::*` calls (default level: per-crate `debug`, see `daemon.rs::install_logcat_subscriber`) |
542542-| `Rockbox` | C firmware `printf`/`fprintf` and `DEBUGF`/`logf`/`panicf` (routed via `debug-android.c` and the stdout/stderr pipe in `system-android.c`) |
543543-| `rockbox-engine` | `system-android.c` boot diagnostics (cgroup/SELinux denials, etc.) |
544544-| `rb-system-android`, `rb-pcm-aaudio` | other cdylib C tags |
545545-| `RockboxRpc` | Kotlin Log calls in `RockboxRpcModule.kt` |
546546-| `RockboxNowPlaying` | Kotlin Log calls in `NowPlayingService.kt` |
539539+| logcat tag | Source |
540540+| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
541541+| `rockbox` | Rust `tracing::*` calls (default level: per-crate `debug`, see `daemon.rs::install_logcat_subscriber`) |
542542+| `Rockbox` | C firmware `printf`/`fprintf` and `DEBUGF`/`logf`/`panicf` (routed via `debug-android.c` and the stdout/stderr pipe in `system-android.c`) |
543543+| `rockbox-engine` | `system-android.c` boot diagnostics (cgroup/SELinux denials, etc.) |
544544+| `rb-system-android`, `rb-pcm-aaudio` | other cdylib C tags |
545545+| `RockboxRpc` | Kotlin Log calls in `RockboxRpcModule.kt` |
546546+| `RockboxNowPlaying` | Kotlin Log calls in `NowPlayingService.kt` |
547547548548Quick capture recipe:
549549···560560561561## Known pitfalls
562562563563-| Symptom | Cause | Fix |
564564-|---|---|---|
565565-| `embedded daemon not built into this .so (remote-only mode)` | Build script ran without `--features embedded-daemon` | Use `expo/modules/rockbox-rpc/scripts/build-android.sh` (defaults to enabled) |
566566-| `dlopen failed: cannot locate symbol "server_init"` | `CONFIG_SERVER` not defined → `apps/SOURCES` skips `server_thread.c` compilation | Set both `ROCKBOX_SERVER` and `CONFIG_SERVER` in `androidcdylib.h` |
567567-| `Codec: cannot read file` for every track | Codec naming uses Java-shell `libNAME.so` convention but `lc_static_table[]` has bare `<name>.codec` entries | Gate the `libNAME.so` override in `lib/rbcodec/metadata/metadata.h` on `!CODECS_STATIC` |
568568-| SIGSEGV in `init_mad` (or any codec init) at small fault address | `ci` symbol collision: 264-byte struct (codecs.c) merged into 8-byte pointer storage (codec_crt0.c) | Firmware-side rename: `firmware_ci` for the struct, `ci` for the pointer (both 8 bytes, same type) |
569569-| Audio plays at chipmunk speed (~9 % too fast) | `pcm_sink::set_freq` receives an INDEX into `hw_freq_sampr[]`, not Hz; AAudio gets opened at "4 Hz", silently falls back to 48 kHz | `pcm-aaudio.c::sink_set_freq` looks up `hw_freq_sampr[freq_index]` first |
570570-| `ForegroundServiceStartNotAllowedException` on play | Android 14+ blocks `startForegroundService` from background process state (`uidState: SVC`) even with `mediaPlayback` type | `RockboxNowPlayingModule.startServiceCompat` and `NowPlayingService.refreshNotification` check `ActivityManager.getMyMemoryState().importance` before promoting |
571571-| ENOENT when the GraphQL `treeGetEntries` resolver browses `Music` | Daemon set `ROCKBOX_MUSIC_DIR` but the resolvers read `ROCKBOX_LIBRARY` | Set `ROCKBOX_LIBRARY` in `daemon.rs::configure_environment` |
572572-| Library DB stays empty even after browsing works | Embedded daemon doesn't run the desktop CLI's startup scan | `daemon.rs::spawn_library_scan` runs after gRPC binds; force re-scan via `RockboxClient.rescanLibrary()` |
573573-| `Permission denied` reading `/storage/emulated/0/Music` on API 33+ | `READ_EXTERNAL_STORAGE` is ignored on `targetSdk=33+`; `READ_MEDIA_AUDIO` only grants MediaStore queries | `MANAGE_EXTERNAL_STORAGE` in manifest + `useAllFilesAccessPrompt()` opens system Settings |
574574-| Daemon dies after the app backgrounds for a few minutes | App process killed for memory; daemon dies with it | NowPlayingService is a foreground service — keep it running via `RockboxNowPlaying.start()` at app launch (called from `_layout.tsx`) |
575575-| SIGSEGV at PC=0 in `wakeup_thread_` / `queue_send` on track switches, settings updates, anything that crosses `audio_thread` ↔ `codec_thread` | Rockbox kernel uses `__cores[0].running` as global "current thread" — no TLS. Calling firmware FFI from a non-Rockbox pthread (actix worker handling a gRPC request) corrupts kernel-thread state. Same root cause as the older "stale blocker" / "pcmbuf race" symptoms — they were all surfaces of this | **Firmware-command bus** in `crates/server/src/fw_bus.rs`. Every kernel-mutating handler in `crates/server/src/handlers/{player,playlists,saved_playlists,smart_playlists,settings}.rs` wraps its FFI block in `crate::fw_bus::run_on_broker(move \|\| …)` so the calls run on the broker (a real Rockbox kernel thread) and `__running_self_entry()` resolves correctly. Read-only handlers stay direct |
576576-| pcmbuf rebuild race (separate, narrower window) | `pcmbuf_init` rewrites the chunk descriptor ring while the AAudio writer pthread may be mid-`pcmbuf_pcm_callback` | `apps/pcmbuf.c::pcmbuf_init` wrapped in `pcm_play_lock()` / `pcm_play_unlock()` — routes to our `aa_mtx` and blocks `aa_thread` for the few ms of rebuild. Kept as defence-in-depth on top of the bus fix |
577577-| Track-switch hiccup race (separate) | `codec_stop` from `halt_decoding_track` could race the codec_thread before it parked | `apps/playback.c::halt_decoding_track` does `sleep(HZ/10)` (CODECS_STATIC only) before `codec_stop()` to let the runqueue drain. Kept as defence-in-depth |
578578-| Probe / cache writes fail with `ENOENT /tmp/...` on Android | App sandbox has no writable `/tmp` | `daemon.rs::configure_environment` sets `TMPDIR=$HOME/tmp` (and `mkdir -p`s it) so `std::env::temp_dir()` resolves into the sandbox |
563563+| Symptom | Cause | Fix | | |
564564+| --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | ------------------------------------------------------------------------------------------------------------------------------------------------- |
565565+| `embedded daemon not built into this .so (remote-only mode)` | Build script ran without `--features embedded-daemon` | Use `expo/modules/rockbox-rpc/scripts/build-android.sh` (defaults to enabled) | | |
566566+| `dlopen failed: cannot locate symbol "server_init"` | `CONFIG_SERVER` not defined → `apps/SOURCES` skips `server_thread.c` compilation | Set both `ROCKBOX_SERVER` and `CONFIG_SERVER` in `androidcdylib.h` | | |
567567+| `Codec: cannot read file` for every track | Codec naming uses Java-shell `libNAME.so` convention but `lc_static_table[]` has bare `<name>.codec` entries | Gate the `libNAME.so` override in `lib/rbcodec/metadata/metadata.h` on `!CODECS_STATIC` | | |
568568+| SIGSEGV in `init_mad` (or any codec init) at small fault address | `ci` symbol collision: 264-byte struct (codecs.c) merged into 8-byte pointer storage (codec_crt0.c) | Firmware-side rename: `firmware_ci` for the struct, `ci` for the pointer (both 8 bytes, same type) | | |
569569+| Audio plays at chipmunk speed (~9 % too fast) | `pcm_sink::set_freq` receives an INDEX into `hw_freq_sampr[]`, not Hz; AAudio gets opened at "4 Hz", silently falls back to 48 kHz | `pcm-aaudio.c::sink_set_freq` looks up `hw_freq_sampr[freq_index]` first | | |
570570+| `ForegroundServiceStartNotAllowedException` on play | Android 14+ blocks `startForegroundService` from background process state (`uidState: SVC`) even with `mediaPlayback` type | `RockboxNowPlayingModule.startServiceCompat` and `NowPlayingService.refreshNotification` check `ActivityManager.getMyMemoryState().importance` before promoting | | |
571571+| ENOENT when the GraphQL `treeGetEntries` resolver browses `Music` | Daemon set `ROCKBOX_MUSIC_DIR` but the resolvers read `ROCKBOX_LIBRARY` | Set `ROCKBOX_LIBRARY` in `daemon.rs::configure_environment` | | |
572572+| Library DB stays empty even after browsing works | Embedded daemon doesn't run the desktop CLI's startup scan | `daemon.rs::spawn_library_scan` runs after gRPC binds; force re-scan via `RockboxClient.rescanLibrary()` | | |
573573+| `Permission denied` reading `/storage/emulated/0/Music` on API 33+ | `READ_EXTERNAL_STORAGE` is ignored on `targetSdk=33+`; `READ_MEDIA_AUDIO` only grants MediaStore queries | `MANAGE_EXTERNAL_STORAGE` in manifest + `useAllFilesAccessPrompt()` opens system Settings | | |
574574+| Daemon dies after the app backgrounds for a few minutes | App process killed for memory; daemon dies with it | NowPlayingService is a foreground service — keep it running via `RockboxNowPlaying.start()` at app launch (called from `_layout.tsx`) | | |
575575+| SIGSEGV at PC=0 in `wakeup_thread_` / `queue_send` on track switches, settings updates, anything that crosses `audio_thread` ↔ `codec_thread` | Rockbox kernel uses `__cores[0].running` as global "current thread" — no TLS. Calling firmware FFI from a non-Rockbox pthread (actix worker handling a gRPC request) corrupts kernel-thread state. Same root cause as the older "stale blocker" / "pcmbuf race" symptoms — they were all surfaces of this | **Firmware-command bus** in `crates/server/src/fw_bus.rs`. Every kernel-mutating handler in `crates/server/src/handlers/{player,playlists,saved_playlists,smart_playlists,settings}.rs` wraps its FFI block in `crate::fw_bus::run_on_broker(move \ | \ | …)` so the calls run on the broker (a real Rockbox kernel thread) and `__running_self_entry()` resolves correctly. Read-only handlers stay direct |
576576+| pcmbuf rebuild race (separate, narrower window) | `pcmbuf_init` rewrites the chunk descriptor ring while the AAudio writer pthread may be mid-`pcmbuf_pcm_callback` | `apps/pcmbuf.c::pcmbuf_init` wrapped in `pcm_play_lock()` / `pcm_play_unlock()` — routes to our `aa_mtx` and blocks `aa_thread` for the few ms of rebuild. Kept as defence-in-depth on top of the bus fix | | |
577577+| Track-switch hiccup race (separate) | `codec_stop` from `halt_decoding_track` could race the codec_thread before it parked | `apps/playback.c::halt_decoding_track` does `sleep(HZ/10)` (CODECS_STATIC only) before `codec_stop()` to let the runqueue drain. Kept as defence-in-depth | | |
578578+| Probe / cache writes fail with `ENOENT /tmp/...` on Android | App sandbox has no writable `/tmp` | `daemon.rs::configure_environment` sets `TMPDIR=$HOME/tmp` (and `mkdir -p`s it) so `std::env::temp_dir()` resolves into the sandbox | | |
579579580580---
581581
···33UPnP/DLNA support for Rockbox Zig. This crate provides three independent but
44complementary features:
5566-| Feature | What it does |
77-|--------------------------------------|-----------------------------------------------------------------------------------------------------|
88-| **Media Server** (ContentDirectory) | Exposes the music library so UPnP control points (BubbleUPnP, Kodi, etc.) can browse and pull tracks |
99-| **MediaRenderer** | Lets control points push media to Rockbox (Rockbox becomes the speaker) |
1010-| **PCM sink / WAV output** | Streams live PCM audio as WAV-over-HTTP to an external UPnP renderer (Kodi, etc.) |
66+| Feature | What it does |
77+| ----------------------------------- | ---------------------------------------------------------------------------------------------------- |
88+| **Media Server** (ContentDirectory) | Exposes the music library so UPnP control points (BubbleUPnP, Kodi, etc.) can browse and pull tracks |
99+| **MediaRenderer** | Lets control points push media to Rockbox (Rockbox becomes the speaker) |
1010+| **PCM sink / WAV output** | Streams live PCM audio as WAV-over-HTTP to an external UPnP renderer (Kodi, etc.) |
11111212---
1313···40404141### UPnP service roles
42424343-| UPnP term | Rockbox role | Description |
4444-|---------------------------------|----------------|----------------------------------------|
4545-| MediaServer / ContentDirectory | **server** | Hosts the music library for browsing |
4646-| MediaRenderer / AVTransport | **renderer** | Receives push-play commands |
4747-| Control Point | *external app* | BubbleUPnP, Kodi, Foobar2000, … |
4343+| UPnP term | Rockbox role | Description |
4444+| ------------------------------ | -------------- | ------------------------------------ |
4545+| MediaServer / ContentDirectory | **server** | Hosts the music library for browsing |
4646+| MediaRenderer / AVTransport | **renderer** | Receives push-play commands |
4747+| Control Point | *external app* | BubbleUPnP, Kodi, Foobar2000, … |
48484949---
5050···123123124124### Supported AVTransport actions
125125126126-| Action | Behaviour |
127127-|-----------------------|------------------------------------------------------------------------------------|
128128-| `SetAVTransportURI` | Store URI + parse DIDL-Lite metadata; open the stream in the Rockbox audio engine |
129129-| `Play` | Start or resume playback |
130130-| `Pause` | Pause/resume toggle |
131131-| `Stop` | Stop playback; clear stored metadata |
132132-| `Seek` | Seek to absolute time (REL_TIME target unit) |
133133-| `GetTransportInfo` | Return current transport state (PLAYING / PAUSED_PLAYBACK / STOPPED) |
134134-| `GetPositionInfo` | Return track URI, DIDL-Lite metadata, duration, elapsed time |
135135-| `GetMediaInfo` | Return current URI and DIDL-Lite metadata |
126126+| Action | Behaviour |
127127+| ------------------- | --------------------------------------------------------------------------------- |
128128+| `SetAVTransportURI` | Store URI + parse DIDL-Lite metadata; open the stream in the Rockbox audio engine |
129129+| `Play` | Start or resume playback |
130130+| `Pause` | Pause/resume toggle |
131131+| `Stop` | Stop playback; clear stored metadata |
132132+| `Seek` | Seek to absolute time (REL_TIME target unit) |
133133+| `GetTransportInfo` | Return current transport state (PLAYING / PAUSED_PLAYBACK / STOPPED) |
134134+| `GetPositionInfo` | Return track URI, DIDL-Lite metadata, duration, elapsed time |
135135+| `GetMediaInfo` | Return current URI and DIDL-Lite metadata |
136136137137---
138138···225225226226### All UPnP settings at a glance
227227228228-| Key | Type | Default | Description |
229229-|--------------------------|-----------|--------------|------------------------------------------------|
230230-| `audio_output` | string | `"builtin"` | Set to `"upnp"` to use the PCM sink |
231231-| `upnp_renderer_url` | string | — | AVTransport controlURL of the target renderer |
232232-| `upnp_http_port` | integer | `7879` | Port for the WAV broadcast HTTP server |
233233-| `upnp_server_enabled` | bool | `false` | Start the ContentDirectory media server |
234234-| `upnp_server_port` | integer | `7878` | HTTP port for the media server |
235235-| `upnp_renderer_enabled` | bool | `false` | Start the MediaRenderer |
236236-| `upnp_renderer_port` | integer | `7880` | HTTP port for the renderer |
237237-| `upnp_friendly_name` | string | `"Rockbox"` | Display name shown to control points |
228228+| Key | Type | Default | Description |
229229+| ----------------------- | ------- | ----------- | --------------------------------------------- |
230230+| `audio_output` | string | `"builtin"` | Set to `"upnp"` to use the PCM sink |
231231+| `upnp_renderer_url` | string | — | AVTransport controlURL of the target renderer |
232232+| `upnp_http_port` | integer | `7879` | Port for the WAV broadcast HTTP server |
233233+| `upnp_server_enabled` | bool | `false` | Start the ContentDirectory media server |
234234+| `upnp_server_port` | integer | `7878` | HTTP port for the media server |
235235+| `upnp_renderer_enabled` | bool | `false` | Start the MediaRenderer |
236236+| `upnp_renderer_port` | integer | `7880` | HTTP port for the renderer |
237237+| `upnp_friendly_name` | string | `"Rockbox"` | Display name shown to control points |
238238239239---
240240
···1010ROCKBOX_HOST=192.168.1.42 clj -M:examples -m ex02-now-playing
1111```
12121313-| File | What it shows |
1414-|-----------------------------------|-------------------------------------------------------|
1515-| `ex01_basic_playback.clj` | Pause / seek / resume in one threading-macro chain |
1616-| `ex02_now_playing.clj` | Pretty-print the currently playing track |
1717-| `ex03_library_search.clj` | Search → play first matching album shuffled |
1818-| `ex04_queue_management.clj` | Inspect and modify the live queue |
1919-| `ex05_realtime_events.clj` | WebSocket events with the callback API |
2020-| `ex06_core_async_events.clj` | Same events, consumed via `core.async` channels |
2121-| `ex07_volume_eq.clj` | Adjust volume + write a 5-band EQ preset |
2222-| `ex08_browse_filesystem.clj` | Walk `music_dir` (directories vs files) |
2323-| `ex09_plugin_scrobbler.clj` | Toy "scrobbler" plugin via `use-plugin` / event hook |
2424-| `ex10_smart_playlist.clj` | Create a smart playlist from a Clojure data rule-set |
1313+| File | What it shows |
1414+| ---------------------------- | ---------------------------------------------------- |
1515+| `ex01_basic_playback.clj` | Pause / seek / resume in one threading-macro chain |
1616+| `ex02_now_playing.clj` | Pretty-print the currently playing track |
1717+| `ex03_library_search.clj` | Search → play first matching album shuffled |
1818+| `ex04_queue_management.clj` | Inspect and modify the live queue |
1919+| `ex05_realtime_events.clj` | WebSocket events with the callback API |
2020+| `ex06_core_async_events.clj` | Same events, consumed via `core.async` channels |
2121+| `ex07_volume_eq.clj` | Adjust volume + write a 5-band EQ preset |
2222+| `ex08_browse_filesystem.clj` | Walk `music_dir` (directories vs files) |
2323+| `ex09_plugin_scrobbler.clj` | Toy "scrobbler" plugin via `use-plugin` / event hook |
2424+| `ex10_smart_playlist.clj` | Create a smart playlist from a Clojure data rule-set |
25252626`rockboxd` must be running locally (or specify `ROCKBOX_HOST`) before the
2727examples can connect.
···30303131## Index
32323333-| File | Demonstrates |
3434-|---------------------------------------|---------------------------------------------------------|
3535-| `01-basic-playback.ts` | Status, transport controls, current track |
3636-| `02-now-playing.ts` | Real-time WebSocket subscriptions |
3737-| `03-library-search.ts` | Search the library and play results |
3838-| `04-queue-management.ts` | Inspect and manipulate the playback queue |
3939-| `05-saved-playlists.ts` | Create, edit, and play saved playlists |
4040-| `06-smart-playlist.ts` | Build smart playlists from rule sets |
4141-| `07-volume-control.ts` | Read `VolumeInfo` and adjust relative volume |
4242-| `08-eq-config.ts` | Configure the equalizer and replaygain |
4343-| `09-browse-filesystem.ts` | Walk `music_dir` like a tree |
4444-| `10-browse-upnp.ts` | Discover and browse UPnP media servers |
4545-| `11-bluetooth.ts` | Scan, connect, and disconnect Bluetooth devices (Linux) |
4646-| `12-devices.ts` | List and switch Chromecast / AirPlay output sinks |
4747-| `13-plugin-sleep-timer.ts` | Plugin: stop playback after N minutes |
4848-| `14-plugin-scrobbler.ts` | Plugin: log every fully-played track |
4949-| `15-cli-remote.ts` | Tiny interactive remote control in the terminal |
3333+| File | Demonstrates |
3434+| -------------------------- | ------------------------------------------------------- |
3535+| `01-basic-playback.ts` | Status, transport controls, current track |
3636+| `02-now-playing.ts` | Real-time WebSocket subscriptions |
3737+| `03-library-search.ts` | Search the library and play results |
3838+| `04-queue-management.ts` | Inspect and manipulate the playback queue |
3939+| `05-saved-playlists.ts` | Create, edit, and play saved playlists |
4040+| `06-smart-playlist.ts` | Build smart playlists from rule sets |
4141+| `07-volume-control.ts` | Read `VolumeInfo` and adjust relative volume |
4242+| `08-eq-config.ts` | Configure the equalizer and replaygain |
4343+| `09-browse-filesystem.ts` | Walk `music_dir` like a tree |
4444+| `10-browse-upnp.ts` | Discover and browse UPnP media servers |
4545+| `11-bluetooth.ts` | Scan, connect, and disconnect Bluetooth devices (Linux) |
4646+| `12-devices.ts` | List and switch Chromecast / AirPlay output sinks |
4747+| `13-plugin-sleep-timer.ts` | Plugin: stop playback after N minutes |
4848+| `14-plugin-scrobbler.ts` | Plugin: log every fully-played track |
4949+| `15-cli-remote.ts` | Tiny interactive remote control in the terminal |
50505151Each example is self-contained — pick the one closest to what you need, copy
5252it into your project, and adapt.