Rockbox open source high quality audio player as a Music Player Daemon
mpris rockbox mpd libadwaita audio rust zig deno
2
fork

Configure Feed

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.

+803 -606
+95 -95
AUDIO_SETTINGS.md
··· 41 41 42 42 Determines how the left and right channels of a stereo signal are routed to the output. 43 43 44 - | Value | Behaviour | 45 - |---|---| 46 - | **Stereo** | Leave the audio signal unmodified. | 47 - | **Mono** | Combine both channels; send the result to both outputs (monophonic). | 48 - | **Custom** | Apply the stereo width specified by the **Stereo Width** setting. | 49 - | **Mono Left** | Play the left channel in both stereo channels. | 50 - | **Mono Right** | Play the right channel in both stereo channels. | 51 - | **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. | 52 - | **Swap Left & Right** | Play the left channel on the right output and vice versa. | 44 + | Value | Behaviour | 45 + | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 46 + | **Stereo** | Leave the audio signal unmodified. | 47 + | **Mono** | Combine both channels; send the result to both outputs (monophonic). | 48 + | **Custom** | Apply the stereo width specified by the **Stereo Width** setting. | 49 + | **Mono Left** | Play the left channel in both stereo channels. | 50 + | **Mono Right** | Play the right channel in both stereo channels. | 51 + | **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. | 52 + | **Swap Left & Right** | Play the left channel on the right output and vice versa. | 53 53 54 54 Applied via `sound_set_channels()` in `firmware/sound.c`. 55 55 ··· 78 78 79 79 > **Warning:** crossfeed can cause output distortion if its settings result in a combined level that is too high. 80 80 81 - | Setting | Storage field | Range | Default | Description | 82 - |---|---|---|---|---| 83 - | **Type** | `global_settings.crossfeed` | off, meier, custom | off | `meier` uses fixed sensible defaults; `custom` exposes the four parameters below | 84 - | **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 | 85 - | **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 | 86 - | **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 | 87 - | **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 | 81 + | Setting | Storage field | Range | Default | Description | 82 + | ------------------ | ------------------------------------------ | ----------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | 83 + | **Type** | `global_settings.crossfeed` | off, meier, custom | off | `meier` uses fixed sensible defaults; `custom` exposes the four parameters below | 84 + | **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 | 85 + | **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 | 86 + | **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 | 87 + | **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 | 88 88 89 89 Applied via `dsp_set_crossfeed_type()` and `dsp_set_crossfeed_cross_params()` in `lib/rbcodec/dsp/crossfeed.h`. 90 90 ··· 100 100 101 101 ### EQ bands 102 102 103 - | Band | Filter type | Default centre / cutoff | Q recommendation | 104 - |---|---|---|---| 105 - | Band 0 | Low shelf filter | 32 Hz | 0.7 (higher values add an unwanted boost near cutoff) | 106 - | Bands 1–8 | Peaking (bell) filters | 64 / 125 / 250 / 500 / 1k / 2k / 4k / 8k Hz | Higher Q = narrower range; lower Q = wider range | 107 - | Band 9 | High shelf filter | 16 000 Hz | 0.7 | 103 + | Band | Filter type | Default centre / cutoff | Q recommendation | 104 + | --------- | ---------------------- | ------------------------------------------- | ----------------------------------------------------- | 105 + | Band 0 | Low shelf filter | 32 Hz | 0.7 (higher values add an unwanted boost near cutoff) | 106 + | Bands 1–8 | Peaking (bell) filters | 64 / 125 / 250 / 500 / 1k / 2k / 4k / 8k Hz | Higher Q = narrower range; lower Q = wider range | 107 + | Band 9 | High shelf filter | 16 000 Hz | 0.7 | 108 108 109 109 **Band parameters (per band):** 110 110 - **Cutoff / Centre frequency** — where the shelving starts (shelf bands) or the centre of the affected range (peak bands). ··· 113 113 114 114 ### EQ sub-settings 115 115 116 - | Setting | Storage field | Type | Description | 117 - |---|---|---|---| 118 - | **Enable EQ** | `global_settings.eq_enabled` | bool | Master on/off switch for the software EQ | 119 - | **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 | 120 - | **Graphical EQ** | — | screen | Graphical interface for adjusting gain, centre frequency, and Q for each band | 121 - | **Simple EQ** | — | screen | Simplified view: only gain is adjustable per band | 122 - | **Advanced EQ** | — | submenu | Same parameters as Graphical EQ, via text menus | 123 - | **Save EQ Preset** | — | action | Saves current EQ configuration to a `.cfg` file | 124 - | **Browse EQ Presets** | — | screen | Lists built-in presets and any saved configurations | 116 + | Setting | Storage field | Type | Description | 117 + | --------------------- | ---------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 118 + | **Enable EQ** | `global_settings.eq_enabled` | bool | Master on/off switch for the software EQ | 119 + | **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 | 120 + | **Graphical EQ** | — | screen | Graphical interface for adjusting gain, centre frequency, and Q for each band | 121 + | **Simple EQ** | — | screen | Simplified view: only gain is adjustable per band | 122 + | **Advanced EQ** | — | submenu | Same parameters as Graphical EQ, via text menus | 123 + | **Save EQ Preset** | — | action | Saves current EQ configuration to a `.cfg` file | 124 + | **Browse EQ Presets** | — | screen | Lists built-in presets and any saved configurations | 125 125 126 126 Applied via `dsp_set_eq_precut()` and `dsp_set_eq_coefs()` in `lib/rbcodec/dsp/eq.h`. 127 127 ··· 131 131 132 132 **Storage:** `global_settings.replaygain_settings` 133 133 134 - | Setting | Storage field | Values / Range | Default | Description | 135 - |---|---|---|---|---| 136 - | **Type** | `.type` | track, album, track shuffle, off | shuffle | Which RG tag to read for normalization | 137 - | **No-Clip** | `.noclip` | bool | false | Scale down if RG adjustment would cause clipping | 138 - | **Preamp** | `.preamp` | −120..+120 dB (step 5) | 0 dB | Additional gain applied on top of the RG value | 134 + | Setting | Storage field | Values / Range | Default | Description | 135 + | ----------- | ------------- | -------------------------------- | ------- | ------------------------------------------------ | 136 + | **Type** | `.type` | track, album, track shuffle, off | shuffle | Which RG tag to read for normalization | 137 + | **No-Clip** | `.noclip` | bool | false | Scale down if RG adjustment would cause clipping | 138 + | **Preamp** | `.preamp` | −120..+120 dB (step 5) | 0 dB | Additional gain applied on top of the RG value | 139 139 140 140 Applied via `dsp_replaygain_set_settings()` in `lib/rbcodec/dsp/dsp_misc.h`. 141 141 ··· 147 147 148 148 Overlaps the end of one track with the beginning of the next. 149 149 150 - | Setting | Storage field | Range | Default | Description | 151 - |---|---|---|---|---| 152 - | **Mode** | `global_settings.crossfade` | off, auto track change, manual skip, shuffle, shuffle+manual, always | off | When crossfading is triggered | 153 - | **Fade-In Delay** | `global_settings.crossfade_fade_in_delay` | 0..7 s | 0 s | Silence before the fade-in begins | 154 - | **Fade-Out Delay** | `global_settings.crossfade_fade_out_delay` | 0..7 s | 0 s | Silence before the fade-out begins | 155 - | **Fade-In Duration** | `global_settings.crossfade_fade_in_duration` | 0..15 s | 2 s | Length of the fade-in ramp | 156 - | **Fade-Out Duration** | `global_settings.crossfade_fade_out_duration` | 0..15 s | 2 s | Length of the fade-out ramp | 157 - | **Fade-Out Mode** | `global_settings.crossfade_fade_out_mixmode` | crossfade, mix | crossfade | Whether the outgoing track fades out or mixes at a flat level | 150 + | Setting | Storage field | Range | Default | Description | 151 + | --------------------- | --------------------------------------------- | -------------------------------------------------------------------- | --------- | ------------------------------------------------------------- | 152 + | **Mode** | `global_settings.crossfade` | off, auto track change, manual skip, shuffle, shuffle+manual, always | off | When crossfading is triggered | 153 + | **Fade-In Delay** | `global_settings.crossfade_fade_in_delay` | 0..7 s | 0 s | Silence before the fade-in begins | 154 + | **Fade-Out Delay** | `global_settings.crossfade_fade_out_delay` | 0..7 s | 0 s | Silence before the fade-out begins | 155 + | **Fade-In Duration** | `global_settings.crossfade_fade_in_duration` | 0..15 s | 2 s | Length of the fade-in ramp | 156 + | **Fade-Out Duration** | `global_settings.crossfade_fade_out_duration` | 0..15 s | 2 s | Length of the fade-out ramp | 157 + | **Fade-Out Mode** | `global_settings.crossfade_fade_out_mixmode` | crossfade, mix | crossfade | Whether the outgoing track fades out or mixes at a flat level | 158 158 159 159 --- 160 160 ··· 183 183 184 184 Enabling Timestretch allows playback speed to be changed independently of pitch. Intended primarily for speech playback — may noticeably degrade the listening experience with complex music. 185 185 186 - | Setting | Storage field | Range | Default | Description | 187 - |---|---|---|---|---| 188 - | **Pitch** | `global_status.resume_pitch` | ~50..200 % | 100 % | Pitch shift without changing tempo | 189 - | **Speed** | `global_status.resume_speed` | ~35..250 % | 100 % | Playback speed without changing pitch | 190 - | **Timestretch Enable** | `global_settings.timestretch_enabled` | bool | false | Enables the TDHS time-domain algorithm; accessible via the Pitch Screen after reboot | 186 + | Setting | Storage field | Range | Default | Description | 187 + | ---------------------- | ------------------------------------- | ---------- | ------- | ------------------------------------------------------------------------------------ | 188 + | **Pitch** | `global_status.resume_pitch` | ~50..200 % | 100 % | Pitch shift without changing tempo | 189 + | **Speed** | `global_status.resume_speed` | ~35..250 % | 100 % | Playback speed without changing pitch | 190 + | **Timestretch Enable** | `global_settings.timestretch_enabled` | bool | false | Enables the TDHS time-domain algorithm; accessible via the Pitch Screen after reboot | 191 191 192 192 Applied via `sound_set_pitch()` and `dsp_timestretch_enable()` in `lib/rbcodec/dsp/tdspeed.h`. 193 193 ··· 197 197 198 198 **Storage:** `global_settings.play_frequency` | Condition: `HAVE_PLAY_FREQ` 199 199 200 - | Setting | Values | Description | 201 - |---|---|---| 200 + | Setting | Values | Description | 201 + | ------------------ | ---------------------------------------- | ---------------------------------------------------------------- | 202 202 | **Play Frequency** | auto, 44.1 kHz, 48 kHz, 88.2 kHz, 96 kHz | Output sample rate. `auto` matches the source file's native rate | 203 203 204 204 --- ··· 209 209 210 210 Implements 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: 211 211 212 - | Setting | Storage field | Range | Default | Description | 213 - |---|---|---|---|---| 214 - | **Enable** | `global_settings.surround_enabled` | 0, 5, 8, 10, 15, 30 ms | 0 (off) | Delay time for the Haas effect; 0 disables it | 215 - | **Balance** | `global_settings.surround_balance` | 0..99 % | 35 % | Left/right channel output ratio to re-centre the stage | 216 - | **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 | 217 - | **f(x2) — LF Cutoff** | `global_settings.surround_fx2` | 40..400 Hz (step 40) | 320 Hz | Lower boundary of the bypass band | 218 - | **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 | 219 - | **Dry/Wet Mix** | `global_settings.surround_mix` | 0..100 % (step 5) | 50 % | Proportion of original (dry) vs effected (wet) signal in the final output | 212 + | Setting | Storage field | Range | Default | Description | 213 + | --------------------- | ---------------------------------- | ----------------------- | ------- | ---------------------------------------------------------------------------------------------------------------- | 214 + | **Enable** | `global_settings.surround_enabled` | 0, 5, 8, 10, 15, 30 ms | 0 (off) | Delay time for the Haas effect; 0 disables it | 215 + | **Balance** | `global_settings.surround_balance` | 0..99 % | 35 % | Left/right channel output ratio to re-centre the stage | 216 + | **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 | 217 + | **f(x2) — LF Cutoff** | `global_settings.surround_fx2` | 40..400 Hz (step 40) | 320 Hz | Lower boundary of the bypass band | 218 + | **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 | 219 + | **Dry/Wet Mix** | `global_settings.surround_mix` | 0..100 % (step 5) | 50 % | Proportion of original (dry) vs effected (wet) signal in the final output | 220 220 221 221 Applied via `dsp_surround_enable()` and related functions in `lib/rbcodec/dsp/surround.h`. 222 222 ··· 228 228 229 229 Implements a group delay correction and an additional biophonic EQ to boost bass perception. 230 230 231 - | Setting | Storage field | Range | Default | Description | 232 - |---|---|---|---|---| 233 - | **PBE** | `global_settings.pbe` | 0..100 % (step 25) | 0 % (off) | Strength of the bass enhancement effect | 234 - | **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 | 231 + | Setting | Storage field | Range | Default | Description | 232 + | -------------- | ---------------------------- | --------------------- | --------- | ----------------------------------------------------------------------------------------------------------------- | 233 + | **PBE** | `global_settings.pbe` | 0..100 % (step 25) | 0 % (off) | Strength of the bass enhancement effect | 234 + | **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 | 235 235 236 236 Applied via `dsp_pbe_enable()` and `dsp_pbe_precut()` in `lib/rbcodec/dsp/pbe.h`. 237 237 ··· 243 243 244 244 Human 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. 245 245 246 - | Setting | Values | Default | 247 - |---|---|---| 248 - | **AFR Enable** | off, weak, moderate, strong | off | 246 + | Setting | Values | Default | 247 + | -------------- | --------------------------- | ------- | 248 + | **AFR Enable** | off, weak, moderate, strong | off | 249 249 250 250 Applied via `dsp_afr_enable()` in `lib/rbcodec/dsp/afr.h`. 251 251 ··· 257 257 258 258 The 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. 259 259 260 - | Setting | Storage field | Values / Range | Default | Description | 261 - |---|---|---|---|---| 262 - | **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 | 263 - | **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 | 264 - | **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 | 265 - | **Knee** | `.knee` | hard, soft | soft | **Hard knee:** transition occurs precisely at the threshold. **Soft knee:** transition is smoothed over ±3 dB around the threshold | 266 - | **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 | 267 - | **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 | 260 + | Setting | Storage field | Values / Range | Default | Description | 261 + | ---------------- | --------------- | ------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | 262 + | **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 | 263 + | **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 | 264 + | **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 | 265 + | **Knee** | `.knee` | hard, soft | soft | **Hard knee:** transition occurs precisely at the threshold. **Soft knee:** transition is smoothed over ±3 dB around the threshold | 266 + | **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 | 267 + | **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 | 268 268 269 269 Applied via `dsp_set_compressor()` in `lib/rbcodec/dsp/compressor.h`. 270 270 ··· 272 272 273 273 ## UI Audio Feedback 274 274 275 - | Setting | Storage field | Values | Default | Description | 276 - |---|---|---|---|---| 277 - | **Beep** | `global_settings.beep` | off, weak, moderate, strong | off | Audible tone on track change or key events | 278 - | **Keyclick** | `global_settings.keyclick` | off, weak, moderate, strong | off | Audio click on every key press | 279 - | **Keyclick Repeats** | `global_settings.keyclick_repeats` | bool | false | Whether held keys also produce a click | 275 + | Setting | Storage field | Values | Default | Description | 276 + | -------------------- | ---------------------------------- | --------------------------- | ------- | ------------------------------------------ | 277 + | **Beep** | `global_settings.beep` | off, weak, moderate, strong | off | Audible tone on track change or key events | 278 + | **Keyclick** | `global_settings.keyclick` | off, weak, moderate, strong | off | Audio click on every key press | 279 + | **Keyclick Repeats** | `global_settings.keyclick_repeats` | bool | false | Whether held keys also produce a click | 280 280 281 281 --- 282 282 283 283 ## Summary 284 284 285 - | Category | Setting count | 286 - |---|---| 287 - | Volume & limit | 2 | 288 - | Channel / stereo | 3 | 289 - | Crossfeed | 5 | 290 - | Software EQ (10 bands) | 12 | 291 - | ReplayGain | 3 | 292 - | Crossfade | 6 | 293 - | Dithering | 1 | 294 - | Pitch / Time-Stretch | 3 | 295 - | Output sample rate | 1 | 296 - | Haas Surround | 6 | 297 - | PBE | 2 | 298 - | AFR | 1 | 299 - | Compressor | 6 | 300 - | UI audio feedback | 3 | 301 - | **Total** | **~54** | 285 + | Category | Setting count | 286 + | ---------------------- | ------------- | 287 + | Volume & limit | 2 | 288 + | Channel / stereo | 3 | 289 + | Crossfeed | 5 | 290 + | Software EQ (10 bands) | 12 | 291 + | ReplayGain | 3 | 292 + | Crossfade | 6 | 293 + | Dithering | 1 | 294 + | Pitch / Time-Stretch | 3 | 295 + | Output sample rate | 1 | 296 + | Haas Surround | 6 | 297 + | PBE | 2 | 298 + | AFR | 1 | 299 + | Compressor | 6 | 300 + | UI audio feedback | 3 | 301 + | **Total** | **~54** |
+24 -6
CLAUDE.md
··· 1 1 # CLAUDE.md — Rockbox Zig 2 2 3 + ## Markdown formatting 4 + 5 + ### Tables 6 + 7 + 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: 8 + 9 + ```markdown 10 + | Name | Role | Notes | 11 + | ------- | --------- | ---------------------------- | 12 + | Alice | Engineer | Owns the firmware layer | 13 + | Bob | Designer | Works on the mobile UI | 14 + | Charlie | QA | Runs integration test suites | 15 + ``` 16 + 17 + When editing an existing table, re-align the whole table (not just the changed row). When adding a new table, align it before committing. 18 + 19 + --- 20 + 3 21 ## Project overview 4 22 5 23 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/Squeezelite output sinks, and a desktop/web UI. ··· 114 132 115 133 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). 116 134 117 - | Enum constant | Value | Implementation file | 118 - |--------------------|-------|---------------------------------------------| 119 - | `PCM_SINK_BUILTIN` | 0 | `firmware/target/hosted/sdl/pcm-sdl.c` | 120 - | `PCM_SINK_FIFO` | 1 | `firmware/target/hosted/pcm-fifo.c` | 121 - | `PCM_SINK_AIRPLAY` | 2 | `firmware/target/hosted/pcm-airplay.c` | 122 - | `PCM_SINK_SQUEEZELITE` | 3 | `firmware/target/hosted/pcm-squeezelite.c` | 135 + | Enum constant | Value | Implementation file | 136 + | ---------------------- | ----- | ------------------------------------------ | 137 + | `PCM_SINK_BUILTIN` | 0 | `firmware/target/hosted/sdl/pcm-sdl.c` | 138 + | `PCM_SINK_FIFO` | 1 | `firmware/target/hosted/pcm-fifo.c` | 139 + | `PCM_SINK_AIRPLAY` | 2 | `firmware/target/hosted/pcm-airplay.c` | 140 + | `PCM_SINK_SQUEEZELITE` | 3 | `firmware/target/hosted/pcm-squeezelite.c` | 123 141 124 142 `crates/settings/src/lib.rs:load_settings()` reads `audio_output` and calls `pcm::switch_sink()`. 125 143
+32 -32
HEADLESS.md
··· 45 45 46 46 Target 206 is `headlesshost`. `tools/configure` sets: 47 47 48 - | Variable | Value | Why | 49 - |---|---|---| 50 - | `TARGET` | `-DHEADLESSHOST` | Selects headless firmware path | 51 - | `APP_TYPE` | `headless_host` | Headless Make rules | 52 - | `CODECS_STATIC` | `1` | Static codec linking (see below) | 53 - | `EXTRA_DEFINES` | `-DCODECS_STATIC -DZIG_APP -DAPPLICATION` | Propagated into C flags | 54 - | `OC` | `llvm-objcopy` (from llvm@21) | Safe Mach-O symbol renaming (see below) | 48 + | Variable | Value | Why | 49 + | --------------- | ----------------------------------------- | --------------------------------------- | 50 + | `TARGET` | `-DHEADLESSHOST` | Selects headless firmware path | 51 + | `APP_TYPE` | `headless_host` | Headless Make rules | 52 + | `CODECS_STATIC` | `1` | Static codec linking (see below) | 53 + | `EXTRA_DEFINES` | `-DCODECS_STATIC -DZIG_APP -DAPPLICATION` | Propagated into C flags | 54 + | `OC` | `llvm-objcopy` (from llvm@21) | Safe Mach-O symbol renaming (see below) | 55 55 56 56 The configure script searches for `llvm@21` first, then falls back to the generic `llvm` formula. **It explicitly avoids `llvm@22`** due to a crash described below. 57 57 ··· 258 258 259 259 **Fix** (`lib/rbcodec/codecs/libm4a/demux.c`): 260 260 261 - | Old pattern | Replacement | Why it works | 262 - |---|---|---| 263 - | `stream_read(stream, 4, char filetype[4])` — result unused | `stream_read_uint32(stream)` — result stored in a `uint32_t` variable | Return value is a live read; optimizer cannot eliminate it | 264 - | 4 discarded `stream_read_uint8/int32` calls in `read_chunk_esds` (13 bytes) | `stream_skip(stream, 1/4/4/4)` | `stream_skip` calls `ci->advance_buffer` directly — no destination buffer, no dead-write to eliminate | 265 - | `stream_read(stream, codecdata_len, codecdata)` bulk read | Byte-by-byte loop: `codecdata[i] = stream_read_uint8(stream)` | Each `stream_read_uint8` return value is stored into the array element that IS used by the decoder | 261 + | Old pattern | Replacement | Why it works | 262 + | --------------------------------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | 263 + | `stream_read(stream, 4, char filetype[4])` — result unused | `stream_read_uint32(stream)` — result stored in a `uint32_t` variable | Return value is a live read; optimizer cannot eliminate it | 264 + | 4 discarded `stream_read_uint8/int32` calls in `read_chunk_esds` (13 bytes) | `stream_skip(stream, 1/4/4/4)` | `stream_skip` calls `ci->advance_buffer` directly — no destination buffer, no dead-write to eliminate | 265 + | `stream_read(stream, codecdata_len, codecdata)` bulk read | Byte-by-byte loop: `codecdata[i] = stream_read_uint8(stream)` | Each `stream_read_uint8` return value is stored into the array element that IS used by the decoder | 266 266 267 267 **Rule going forward**: in CODECS_STATIC (or any no-`-fPIC`) build, never call `stream_read(stream, N, buf)` where `buf` is a local variable not subsequently read by the caller. Use `stream_read_uint8/uint16/uint32` (live return values) or `stream_skip` (no buffer at all) for all bytes you intend to discard. 268 268 ··· 294 294 295 295 ## File Map 296 296 297 - | File | Purpose | 298 - |---|---| 299 - | `scripts/build-headless.sh` | Full build script — configure, make, cargo, zig | 300 - | `tools/configure` | Rockbox configure script; `headlesshostcc()` sets up the headless target and finds `llvm@21` | 301 - | `firmware/export/config/headlesshost.h` | C config header for the headless target | 302 - | `firmware/target/hosted/headless/` | Headless-specific C sources (PCM sink, codec loader, etc.) | 303 - | `firmware/target/hosted/headless/lc-headless.c` | `lc_static_table[]` — maps codec names to `__header_*` pointers | 304 - | `firmware/target/hosted/headless/pcm-cpal.c` | C side of the cpal PCM sink; calls `pcm_cpal_push()` / `pcm_cpal_set_sample_rate()` | 305 - | `lib/rbcodec/codecs/codecs.make` | Per-codec build rules; `CODECS_STATIC` block (line 273+) handles symbol renaming | 306 - | `lib/rbcodec/codecs/lib/codeclib.c` | `codec_init`, `codec_malloc`, `bs_clz_tab`, etc. → compiled into `libcodec.a` | 307 - | `crates/cpal-sink/src/lib.rs` | Rust cpal backend — ring buffer, resampler, stream negotiation | 308 - | `zig/build.zig` | Zig linker script — `headless` block lists all `.o` files and `.a` archives | 297 + | File | Purpose | 298 + | ----------------------------------------------- | -------------------------------------------------------------------------------------------- | 299 + | `scripts/build-headless.sh` | Full build script — configure, make, cargo, zig | 300 + | `tools/configure` | Rockbox configure script; `headlesshostcc()` sets up the headless target and finds `llvm@21` | 301 + | `firmware/export/config/headlesshost.h` | C config header for the headless target | 302 + | `firmware/target/hosted/headless/` | Headless-specific C sources (PCM sink, codec loader, etc.) | 303 + | `firmware/target/hosted/headless/lc-headless.c` | `lc_static_table[]` — maps codec names to `__header_*` pointers | 304 + | `firmware/target/hosted/headless/pcm-cpal.c` | C side of the cpal PCM sink; calls `pcm_cpal_push()` / `pcm_cpal_set_sample_rate()` | 305 + | `lib/rbcodec/codecs/codecs.make` | Per-codec build rules; `CODECS_STATIC` block (line 273+) handles symbol renaming | 306 + | `lib/rbcodec/codecs/lib/codeclib.c` | `codec_init`, `codec_malloc`, `bs_clz_tab`, etc. → compiled into `libcodec.a` | 307 + | `crates/cpal-sink/src/lib.rs` | Rust cpal backend — ring buffer, resampler, stream negotiation | 308 + | `zig/build.zig` | Zig linker script — `headless` block lists all `.o` files and `.a` archives | 309 309 310 310 --- 311 311 312 312 ## Rebuild After Changes 313 313 314 - | Changed | Command | 315 - |---|---| 316 - | Any C firmware file | `cd build-headless && make lib OC=...` then `cd zig && zig build -Dheadless=true` | 317 - | `lc-headless.c` or codec C files | Same as above | 318 - | `crates/cpal-sink/` | `cargo build --release --features cpal-sink -p rockbox-cli` then `zig build` | 319 - | Any other Rust crate | `cargo build --release -p rockbox-cli -p rockbox-server` then `zig build` | 320 - | `zig/build.zig` | `cd zig && zig build -Dheadless=true` | 321 - | Everything | `bash scripts/build-headless.sh` | 314 + | Changed | Command | 315 + | -------------------------------- | --------------------------------------------------------------------------------- | 316 + | Any C firmware file | `cd build-headless && make lib OC=...` then `cd zig && zig build -Dheadless=true` | 317 + | `lc-headless.c` or codec C files | Same as above | 318 + | `crates/cpal-sink/` | `cargo build --release --features cpal-sink -p rockbox-cli` then `zig build` | 319 + | Any other Rust crate | `cargo build --release -p rockbox-cli -p rockbox-server` then `zig build` | 320 + | `zig/build.zig` | `cd zig && zig build -Dheadless=true` | 321 + | Everything | `bash scripts/build-headless.sh` | 322 322 323 323 > **Stale binary pitfall**: `zig build` only re-links if the `.a` files are newer than the binary. Always rebuild Make/Cargo before running `zig build` after changing C or Rust code.
+23 -23
README.md
··· 174 174 175 175 ## 🔌 Ports 176 176 177 - | Service | Default port | Protocol | 178 - |---------------------------------------|--------------|-----------------| 179 - | gRPC | 6061 | gRPC / gRPC-Web | 180 - | GraphQL + Web UI | 6062 | HTTP | 181 - | HTTP REST API | 6063 | HTTP | 182 - | MPD server | 6600 | MPD protocol | 183 - | Slim Protocol (squeezelite) | 3483 | TCP | 184 - | HTTP PCM stream (squeezelite) | 9999 | HTTP | 185 - | Chromecast WAV stream | 7881 | HTTP | 186 - | UPnP Media Server (ContentDirectory) | 7878 | HTTP / SSDP | 187 - | UPnP WAV broadcast (PCM sink) | 7879 | HTTP | 188 - | UPnP MediaRenderer (AVTransport) | 7880 | HTTP / SSDP | 177 + | Service | Default port | Protocol | 178 + | ------------------------------------ | ------------ | --------------- | 179 + | gRPC | 6061 | gRPC / gRPC-Web | 180 + | GraphQL + Web UI | 6062 | HTTP | 181 + | HTTP REST API | 6063 | HTTP | 182 + | MPD server | 6600 | MPD protocol | 183 + | Slim Protocol (squeezelite) | 3483 | TCP | 184 + | HTTP PCM stream (squeezelite) | 9999 | HTTP | 185 + | Chromecast WAV stream | 7881 | HTTP | 186 + | UPnP Media Server (ContentDirectory) | 7878 | HTTP / SSDP | 187 + | UPnP WAV broadcast (PCM sink) | 7879 | HTTP | 188 + | UPnP MediaRenderer (AVTransport) | 7880 | HTTP / SSDP | 189 189 190 190 --- 191 191 ··· 412 412 413 413 #### All UPnP settings 414 414 415 - | Key | Default | Description | 416 - |----------------------------|--------------|------------------------------------------------| 417 - | `audio_output = "upnp"` | — | Enable the PCM → WAV streaming sink | 418 - | `upnp_renderer_url` | — | AVTransport controlURL of the target renderer | 419 - | `upnp_http_port` | `7879` | WAV broadcast HTTP port | 420 - | `upnp_server_enabled` | `false` | Start the ContentDirectory media server | 421 - | `upnp_server_port` | `7878` | Media server HTTP port | 422 - | `upnp_renderer_enabled` | `false` | Start the MediaRenderer endpoint | 423 - | `upnp_renderer_port` | `7880` | MediaRenderer HTTP port | 424 - | `upnp_friendly_name` | `"Rockbox"` | Display name shown to control points | 415 + | Key | Default | Description | 416 + | ----------------------- | ----------- | --------------------------------------------- | 417 + | `audio_output = "upnp"` | — | Enable the PCM → WAV streaming sink | 418 + | `upnp_renderer_url` | — | AVTransport controlURL of the target renderer | 419 + | `upnp_http_port` | `7879` | WAV broadcast HTTP port | 420 + | `upnp_server_enabled` | `false` | Start the ContentDirectory media server | 421 + | `upnp_server_port` | `7878` | Media server HTTP port | 422 + | `upnp_renderer_enabled` | `false` | Start the MediaRenderer endpoint | 423 + | `upnp_renderer_port` | `7880` | MediaRenderer HTTP port | 424 + | `upnp_friendly_name` | `"Rockbox"` | Display name shown to control points | 425 425 426 426 --- 427 427 ··· 473 473 [Releases page](https://github.com/tsirysndr/rockbox-zig/releases/latest). 474 474 475 475 | Platform | Architecture | Package | 476 - |----------|-------------------------|-----------| 476 + | -------- | ----------------------- | --------- | 477 477 | Linux | x86_64 | `.tar.gz` | 478 478 | Linux | aarch64 | `.tar.gz` | 479 479 | macOS | x86_64 | `.pkg` |
+28 -28
SNAPCAST.md
··· 5 5 6 6 Two complementary sinks are available: 7 7 8 - | Sink | Setting value | Transport | Snapserver source type | 9 - |------|--------------|-----------|------------------------| 10 - | FIFO / pipe | `audio_output = "fifo"` | Named FIFO or stdout | `pipe://` | 11 - | TCP (direct) | `audio_output = "snapcast_tcp"` | TCP socket | `tcp://` | 8 + | Sink | Setting value | Transport | Snapserver source type | 9 + | ------------ | ------------------------------- | -------------------- | ---------------------- | 10 + | FIFO / pipe | `audio_output = "fifo"` | Named FIFO or stdout | `pipe://` | 11 + | TCP (direct) | `audio_output = "snapcast_tcp"` | TCP socket | `tcp://` | 12 12 13 13 The **FIFO sink** is the traditional approach: rockboxd writes to a named pipe 14 14 that snapserver reads. The **TCP sink** connects directly to snapserver's TCP ··· 54 54 55 55 ## Choosing FIFO vs TCP 56 56 57 - | | FIFO sink | TCP sink | 58 - |---|---|---| 59 - | Filesystem entry required | Yes (`/tmp/snapfifo`) | No | 60 - | Snapserver source type | `pipe://` | `tcp://` | 61 - | Startup order sensitive | Yes — rockboxd first | Yes — snapserver first | 62 - | Reconnect on snapserver restart | No (FIFO stays open) | Yes (auto on next play) | 63 - | Auto-discovered in UI | No (static virtual device) | Yes (mDNS `_snapcast._tcp.local.`) | 64 - | stdout pipe support | Yes (`fifo_path = "-"`) | No | 65 - | Config | `fifo_path` | `snapcast_tcp_host` + `snapcast_tcp_port` | 57 + | | FIFO sink | TCP sink | 58 + | ------------------------------- | -------------------------- | ----------------------------------------- | 59 + | Filesystem entry required | Yes (`/tmp/snapfifo`) | No | 60 + | Snapserver source type | `pipe://` | `tcp://` | 61 + | Startup order sensitive | Yes — rockboxd first | Yes — snapserver first | 62 + | Reconnect on snapserver restart | No (FIFO stays open) | Yes (auto on next play) | 63 + | Auto-discovered in UI | No (static virtual device) | Yes (mDNS `_snapcast._tcp.local.`) | 64 + | stdout pipe support | Yes (`fifo_path = "-"`) | No | 65 + | Config | `fifo_path` | `snapcast_tcp_host` + `snapcast_tcp_port` | 66 66 67 67 **Use FIFO** when you want stdout piping or prefer the traditional pipe model. 68 68 ··· 106 106 `firmware/target/hosted/pcm-fifo.c` implements `struct pcm_sink`: 107 107 108 108 | Op | Implementation | 109 - |-------------------|-------------------------------------------------------------| 109 + | ----------------- | ----------------------------------------------------------- | 110 110 | `init` | `pthread_mutex_init` (recursive) | 111 111 | `postinit` | no-op | 112 112 | `set_freq` | no-op (output is always 44100 Hz; snapserver must match) | ··· 239 239 240 240 `firmware/target/hosted/pcm-tcp.c` implements `struct pcm_sink`: 241 241 242 - | Op | Implementation | 243 - |-------------------|-------------------------------------------------------------| 244 - | `init` | `pthread_mutex_init` (recursive) | 245 - | `postinit` | no-op | 246 - | `set_freq` | no-op (output is always 44100 Hz; snapserver must match) | 247 - | `lock` / `unlock` | `pthread_mutex_lock/unlock` | 248 - | `play` | `sink_dma_start` — connects if needed, spawns `tcp_thread` | 249 - | `stop` | `sink_dma_stop` — signals thread, joins; keeps socket open | 242 + | Op | Implementation | 243 + | ----------------- | ---------------------------------------------------------- | 244 + | `init` | `pthread_mutex_init` (recursive) | 245 + | `postinit` | no-op | 246 + | `set_freq` | no-op (output is always 44100 Hz; snapserver must match) | 247 + | `lock` / `unlock` | `pthread_mutex_lock/unlock` | 248 + | `play` | `sink_dma_start` — connects if needed, spawns `tcp_thread` | 249 + | `stop` | `sink_dma_stop` — signals thread, joins; keeps socket open | 250 250 251 251 `tcp_pcm_sink` is registered at index `PCM_SINK_SNAPCAST_TCP = 6` in 252 252 `firmware/pcm.c`. ··· 395 395 396 396 ### All Snapcast settings keys 397 397 398 - | Key | Type | Default | Sink | Description | 399 - |----------------------|--------|-----------------------|-------|------------------------------------------| 400 - | `audio_output` | string | `"builtin"` | both | `"fifo"` or `"snapcast_tcp"` | 401 - | `fifo_path` | string | `"/tmp/rockbox.fifo"` | FIFO | FIFO path, or `"-"` for stdout | 402 - | `snapcast_tcp_host` | string | — | TCP | IP / hostname of the snapserver machine | 403 - | `snapcast_tcp_port` | u16 | `4953` | TCP | snapserver TCP source port | 398 + | Key | Type | Default | Sink | Description | 399 + | ------------------- | ------ | --------------------- | ---- | --------------------------------------- | 400 + | `audio_output` | string | `"builtin"` | both | `"fifo"` or `"snapcast_tcp"` | 401 + | `fifo_path` | string | `"/tmp/rockbox.fifo"` | FIFO | FIFO path, or `"-"` for stdout | 402 + | `snapcast_tcp_host` | string | — | TCP | IP / hostname of the snapserver machine | 403 + | `snapcast_tcp_port` | u16 | `4953` | TCP | snapserver TCP source port | 404 404 405 405 --- 406 406
+203 -24
THREADING.md
··· 9 9 10 10 Understanding 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. 11 11 12 + 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. 13 + 12 14 --- 13 15 14 - ## The SDL Cooperative Scheduler 16 + ## The SDL Cooperative Scheduler (desktop — macOS / Linux) 15 17 16 - 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: 18 + 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: 17 19 18 20 ```c 19 21 // firmware/target/hosted/sdl/thread-sdl.c ··· 55 57 56 58 --- 57 59 60 + ## The Headless Scheduler (Android cdylib) 61 + 62 + 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. 63 + 64 + ### `system-android.c` vs `system-sdl.c` 65 + 66 + | Concern | SDL hosted | Android cdylib | 67 + | ----------------- | ----------------------------------- | ------------------------------------------------------- | 68 + | Cooperative token | `SDL_mutex *m` (single global) | `__cores[0].running` (global current-thread pointer) | 69 + | Thread creation | `SDL_CreateThread` | `pthread_create` | 70 + | Scheduler yield | `SDL_UnlockMutex` / `SDL_LockMutex` | Rockbox kernel pthread mutex round-trip | 71 + | Boot path | SDL event thread initialises audio | `rb_daemon_start` (JNI) spawns `rockbox-engine` pthread | 72 + | Power-off | SDL_QUIT event loop | `exit(0)` via `power_off()` | 73 + | stdio | terminal / controlled | piped to logcat via `redirect_stdio_to_logcat` | 74 + 75 + ### `__cores[0].running` — the critical invariant 76 + 77 + 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.** 78 + 79 + 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: 80 + - `wakeup_thread_` dereferences a stale or null function pointer → **SIGSEGV at PC=0** 81 + - Kernel scheduler state is silently corrupted 82 + - Symptoms may appear seconds later, during a track switch or settings change 83 + 84 + This is why **all firmware-mutating calls from Rust handlers must go through the firmware-command bus** (see below). 85 + 86 + ### Daemon boot sequence (Android) 87 + 88 + `rb_daemon_start(configDir, musicDir, deviceName)` in `crates/expo/src/daemon.rs`: 89 + 90 + 1. Atomically transitions `STATE`: `STOPPED → STARTING` (returns `-114` if already running). 91 + 2. Installs the tracing-android logcat subscriber (idempotent). 92 + 3. Sets env vars: `HOME`, `ROCKBOX_LIBRARY`, `TMPDIR`, `ROCKBOX_PORT`, `ROCKBOX_GRAPHQL_PORT`, `ROCKBOX_TCP_PORT`, `ROCKBOX_MPD_PORT`. 93 + 4. Spawns `rockbox-engine` pthread (2 MB stack) that calls `main_c()` wrapped in `catch_unwind`. 94 + 5. Polls TCP `127.0.0.1:<port>` every 50 ms, up to 30 s, waiting for gRPC to bind. 95 + 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. 96 + 7. Returns the bound gRPC port (positive) or a negative error code (`-110` = timeout, `-114` = already running). 97 + 98 + --- 99 + 100 + ## The Firmware-Command Bus (`crates/server/src/fw_bus.rs`) 101 + 102 + ### Problem 103 + 104 + 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. 105 + 106 + ### Solution 107 + 108 + 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()`. 109 + 110 + ``` 111 + actix worker / tonic task 112 + → fw_bus::send(FwCmd::Play { elapsed, offset, reply }) 113 + → mpsc channel 114 + → broker thread (real Rockbox kernel thread) 115 + → rb::playback::play(elapsed, offset) 116 + → reply_tx.send(()) 117 + ← fw_bus::send_and_wait blocks until reply arrives (≤ 30 s) 118 + ``` 119 + 120 + ### API 121 + 122 + | Function | Use case | 123 + | ------------------------------ | -------------------------------------------------------------------- | 124 + | `fw_bus::init()` | Call once at startup, before broker thread spawns | 125 + | `fw_bus::send(cmd)` | Fire-and-forget (no reply needed) | 126 + | `fw_bus::send_and_wait(make)` | Send + block until broker confirms (for actix `web::block` handlers) | 127 + | `fw_bus::run_on_broker(f)` | Run arbitrary closure on broker; returns `T::default()` on timeout | 128 + | `fw_bus::try_run_on_broker(f)` | Same but returns `Option<T>` — `None` on timeout | 129 + | `fw_bus::drain(rx)` | Called once per broker iteration to execute pending commands | 130 + 131 + ### `FwCmd` variants 132 + 133 + `Play`, `Pause`, `Resume`, `Next`, `Prev`, `Stop`, `FfRewind`, `FlushAndReloadTracks`, `SetCrossfade`, and `Custom(Box<dyn FnOnce() + Send>)` (escape hatch for anything not enumerated). 134 + 135 + ### Timeout behaviour 136 + 137 + 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`. 138 + 139 + ### Where fw_bus is NOT needed 140 + 141 + - Read-only status queries (`rb::playback::status()`, `rb::playback::current_track()`) — these only read atomics or copy structs; no kernel primitive is called. 142 + - Anything running inside the broker thread itself (it already owns `__running_self_entry()`). 143 + - 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. 144 + 145 + --- 146 + 58 147 ## Thread Map 59 148 60 149 ### Rockbox kernel threads (must yield) 61 150 62 - | Thread | C entry point | What it does | 63 - |--------|---------------|--------------| 64 - | `server_thread` | `server_thread.c` → `start_server()` | Spawns the actix HTTP server in a Rust OS thread, then loops yielding to the Rockbox scheduler | 65 - | `broker_thread` | `broker_thread.c` → `start_broker()` | Event loop: publishes playback state to GraphQL subscriptions, scrobbles tracks, restores playlist | 151 + | Thread | C entry point | What it does | 152 + | --------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | 153 + | `server_thread` | `server_thread.c` → `start_server()` | Spawns the actix HTTP server in a Rust OS thread, then loops yielding to the Rockbox scheduler | 154 + | `broker_thread` | `broker_thread.c` → `start_broker()` | Event loop: drains `fw_bus`, publishes playback state to GraphQL subscriptions, scrobbles tracks, restores playlist | 66 155 67 - Both threads must call `rb::system::sleep(rb::HZ)` on every loop iteration. 156 + Both threads must call `rb::system::sleep(rb::HZ)` (desktop) or equivalent on every loop iteration. 68 157 69 158 **`server_thread` pattern** (`crates/server/src/lib.rs`): 70 159 ```rust ··· 89 178 **`broker_thread` pattern** (`crates/server/src/lib.rs`): 90 179 ```rust 91 180 pub extern "C" fn start_broker() { 92 - // ... setup ... 181 + let fw_rx = fw_bus::take_receiver(); 93 182 loop { 94 - // ... do work (check playback, publish events) ... 183 + // Drain firmware commands from actix/gRPC handlers first. 184 + if let Some(rx) = &fw_rx { 185 + fw_bus::drain(rx); 186 + } 187 + // ... publish events, scrobble, restore playlist ... 95 188 thread::sleep(std::time::Duration::from_millis(100)); 96 - rb::system::sleep(rb::HZ); // yield the Rockbox mutex 189 + rb::system::sleep(rb::HZ); 97 190 } 98 191 } 99 192 ``` 100 193 101 194 ### Rust OS threads (no Rockbox scheduler involvement) 102 195 103 - 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. 196 + These are spawned with `std::thread::spawn` — they are pure OS threads and never interact with the Rockbox scheduler token or `__cores[0].running`. 197 + 198 + | Thread / component | Spawned from | Runtime | 199 + | ---------------------------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------- | 200 + | **HTTP server** (actix-web, port 6063) | `start_server()` via `thread::spawn` | `actix_rt::System` (single-thread + LocalSet per arbiter) | 201 + | **gRPC server** (tonic, port 6061) | `start_servers()` via `thread::spawn` | `tokio::Builder::new_current_thread` | 202 + | **GraphQL server** (async-graphql, port 6062) | `start_servers()` via `thread::spawn` | `tokio::Builder::new_current_thread` | 203 + | **MPD server** (port 6600) | `start_servers()` via `thread::spawn` | `tokio::Builder::new_current_thread` | 204 + | **MPRIS server** (Linux, D-Bus) | `start_servers()` via `thread::spawn` | `async_std` task | 205 + | **UPnP runtime** | `rockbox_upnp::init()` (static `OnceLock`) | `tokio::runtime::Runtime::new()` (multi-thread) | 206 + | **Device scanners** (Chromecast, AirPlay, Snapcast, UPnP, Squeezelite) | `run_http_server()` via `thread::spawn` | each creates its own `tokio::runtime::Runtime::new()` | 207 + | **Player event listener** | `run_http_server()` via `thread::spawn` | `tokio::runtime::Runtime::new()` (multi-thread) | 208 + | **Command relay** | `start_servers()` via `thread::spawn` | `reqwest::blocking` (creates its own tokio internally) | 209 + 210 + ### Android-only Rust OS threads 104 211 105 - | Thread / component | Spawned from | Runtime | 106 - |--------------------|--------------|---------| 107 - | **HTTP server** (actix-web, port 6063) | `start_server()` via `thread::spawn` | `actix_rt::System` (single-thread + LocalSet per arbiter) | 108 - | **gRPC server** (tonic, port 6061) | `start_servers()` via `thread::spawn` | `tokio::Builder::new_current_thread` | 109 - | **GraphQL server** (async-graphql, port 6062) | `start_servers()` via `thread::spawn` | `tokio::Builder::new_current_thread` | 110 - | **MPD server** (port 6600) | `start_servers()` via `thread::spawn` | `tokio::Builder::new_current_thread` | 111 - | **MPRIS server** (Linux, D-Bus) | `start_servers()` via `thread::spawn` | `async_std` task | 112 - | **UPnP runtime** | `rockbox_upnp::init()` (static `OnceLock`) | `tokio::runtime::Runtime::new()` (multi-thread) | 113 - | **Device scanners** (Chromecast, AirPlay, Snapcast, UPnP, Squeezelite) | `run_http_server()` via `thread::spawn` | each creates its own `tokio::runtime::Runtime::new()` | 114 - | **Player event listener** | `run_http_server()` via `thread::spawn` | `tokio::runtime::Runtime::new()` (multi-thread) | 115 - | **Command relay** | `start_servers()` via `thread::spawn` | `reqwest::blocking` (creates its own tokio internally) | 212 + | Thread | Spawned from | Purpose | 213 + | ------------------------------------ | --------------------------------------------- | ------------------------------------------------------------------------------- | 214 + | **`rockbox-engine`** (2 MB stack) | `rb_daemon_start` | Calls `main_c()`; owns all Rockbox kernel threads and the cooperative scheduler | 215 + | **`rockbox-library-scan`** | `spawn_library_scan()` after gRPC binds | SQLite audio scan + Rocksky enrichment | 216 + | **`stdio-logcat-reader`** (detached) | `redirect_stdio_to_logcat()` at `system_init` | Reads the stdout/stderr pipe and writes lines to logcat tag `Rockbox` | 217 + | **`rockbox-rpc`** (× 2 workers) | `RT` (`Lazy<Runtime>`) in `crates/expo` | Tokio multi-thread runtime for all tonic gRPC client calls | 116 218 117 219 --- 118 220 119 - ## Startup Sequence 221 + ## Startup Sequences 222 + 223 + ### Desktop (SDL hosted) 120 224 121 225 ``` 122 226 main.c: server_init() ··· 131 235 ├── sleep(HZ) ← Rockbox scheduler sleep on main thread (~1 s) 132 236 133 237 └── start_servers() ← called on main C thread after 1 s 238 + ├── fw_bus::init() ← create mpsc channel before broker spawns 134 239 ├── thread::spawn(gRPC server) ← Rust OS thread, new_current_thread runtime 135 240 ├── thread::spawn(GraphQL server) ← Rust OS thread, new_current_thread runtime 136 241 ├── thread::spawn(MPD server) ← Rust OS thread, new_current_thread runtime ··· 141 246 142 247 └── create_thread(broker_thread) ← Rockbox kernel thread 143 248 └── start_broker() 144 - └── loop { work + sleep + rb::system::sleep(HZ) } 249 + ├── fw_bus::take_receiver() ← claim the mpsc Receiver 250 + └── loop { fw_bus::drain + work + sleep + rb::system::sleep(HZ) } 251 + ``` 252 + 253 + ### Android cdylib 254 + 255 + ``` 256 + JNI: RockboxRpcModule.OnCreate 257 + 258 + └── rb_daemon_start(configDir, musicDir, deviceName) ← crates/expo/src/daemon.rs 259 + ├── STATE: STOPPED → STARTING 260 + ├── install_logcat_subscriber() 261 + ├── configure_environment() ← sets HOME, ROCKBOX_LIBRARY, TMPDIR, ports 262 + 263 + ├── thread::spawn("rockbox-engine", stack=2MB) 264 + │ └── main_c() ← apps/main.c 265 + │ ├── system_init() ← redirect_stdio_to_logcat + stackbegin 266 + │ ├── server_init() ← create_thread(server_thread) 267 + │ │ └── start_server() 268 + │ │ ├── load_settings() 269 + │ │ ├── fw_bus::init() 270 + │ │ ├── thread::spawn(actix HTTP server) 271 + │ │ └── loop { sleep + rb::system::sleep(HZ) } 272 + │ ├── sleep(HZ) 273 + │ ├── start_servers() 274 + │ │ ├── thread::spawn(gRPC server) 275 + │ │ ├── thread::spawn(GraphQL server) 276 + │ │ ├── thread::spawn(MPD server) 277 + │ │ └── thread::spawn(command relay) 278 + │ └── broker_init() ← create_thread(broker_thread) 279 + │ └── start_broker() 280 + │ ├── fw_bus::take_receiver() 281 + │ └── loop { fw_bus::drain + work + sleep + rb::system::sleep(HZ) } 282 + 283 + ├── wait_for_grpc(:6061, 30s) ← polls TCP connect every 50 ms 284 + ├── STATE: STARTING → RUNNING 285 + ├── rb_set_server_url("http://127.0.0.1:6061") ← if JS hasn't overridden 286 + └── spawn_library_scan(force=false) ← own OS thread + current_thread tokio runtime 145 287 ``` 146 288 147 289 --- ··· 149 291 ## Tokio Runtime Layout 150 292 151 293 Multiple independent tokio runtimes coexist; they do not share thread pools or event loops. 294 + 295 + ### Desktop 152 296 153 297 ``` 154 298 ┌─────────────────────────────────────────────────────┐ ··· 183 327 └─────────────────────────────────────────────────────┘ 184 328 ``` 185 329 330 + ### Android (additional runtimes) 331 + 332 + ``` 333 + ┌─────────────────────────────────────────────────────┐ 334 + │ rockbox-rpc Runtime (multi-thread, 2 workers) │ 335 + │ Lazy<Runtime> in crates/expo/src/lib.rs │ 336 + │ Owns: all outbound tonic gRPC client calls │ 337 + │ (rb_play, rb_pause, rb_status_json, streams, …) │ 338 + └─────────────────────────────────────────────────────┘ 339 + 340 + ┌─────────────────────────────────────────────────────┐ 341 + │ library-scan Runtime (current-thread, ephemeral) │ 342 + │ Created per spawn_library_scan() call │ 343 + │ Owns: SQLite connection pool, audio_scan, │ 344 + │ save_audio_metadata, Rocksky enrichment │ 345 + └─────────────────────────────────────────────────────┘ 346 + 347 + ┌─────────────────────────────────────────────────────┐ 348 + │ save_remote_track_metadata Runtime (current-thread)│ 349 + │ Created per call from C firmware (streamfd.c) │ 350 + │ Safe: called from a Rockbox kernel thread, which │ 351 + │ is never inside an existing async context │ 352 + └─────────────────────────────────────────────────────┘ 353 + ``` 354 + 186 355 All runtimes share a single **SQLite database** (`~/.config/rockbox.org/rockbox-library.db`). The connection pool is configured with: 187 356 - WAL journal mode (concurrent readers + one writer) 188 357 - `busy_timeout = 30 s` (serialize concurrent writers instead of failing) ··· 240 409 ### Rule 5: `SimpleBroker` is runtime-agnostic 241 410 242 411 `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. 412 + 413 + ### Rule 6: All firmware-mutating FFI from Rust handlers must go through `fw_bus` 414 + 415 + 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. 416 + 417 + Read-only queries (status, current track) that only touch atomics or copy structs are safe to call directly. 418 + 419 + ### Rule 7: `fw_bus::init()` must be called before any handler sends a command 420 + 421 + `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.
+15 -15
crates/airplay/README.md
··· 111 111 following vtable: 112 112 113 113 | Op | Implementation | 114 - |-------------------|---------------------------------------------------------------------| 114 + | ----------------- | ------------------------------------------------------------------- | 115 115 | `init` | `pthread_mutex_init` (recursive) | 116 116 | `postinit` | no-op | 117 117 | `set_freq` | no-op (sample rate is fixed at 44100 Hz) | 118 118 | `lock` / `unlock` | `pthread_mutex_lock/unlock` | 119 - | `play` | `sink_dma_start` — connects all receivers, spawns `airplay_thread` | 119 + | `play` | `sink_dma_start` — connects all receivers, spawns `airplay_thread` | 120 120 | `stop` | `sink_dma_stop` — signals thread, joins, calls `pcm_airplay_stop()` | 121 121 122 122 `airplay_pcm_sink` is registered at index `PCM_SINK_AIRPLAY = 2` in the ··· 148 148 149 149 `crates/airplay/src/lib.rs` exports these `#[no_mangle] extern "C"` functions: 150 150 151 - | C symbol | Purpose | 152 - |-----------------------------|----------------------------------------------------------| 153 - | `pcm_airplay_set_host` | Set a single receiver (clears any previous list) | 154 - | `pcm_airplay_add_receiver` | Append one receiver to the multi-room list | 155 - | `pcm_airplay_clear_receivers` | Clear the receiver list before re-configuring | 156 - | `pcm_airplay_connect` | Open RTSP + RTP sessions for all configured receivers | 157 - | `pcm_airplay_write` | Buffer PCM, encode ALAC once, fan out to every receiver | 158 - | `pcm_airplay_stop` | Send TEARDOWN to all, clear session | 159 - | `pcm_airplay_close` | Same as stop (called on sink switch) | 151 + | C symbol | Purpose | 152 + | ----------------------------- | ------------------------------------------------------- | 153 + | `pcm_airplay_set_host` | Set a single receiver (clears any previous list) | 154 + | `pcm_airplay_add_receiver` | Append one receiver to the multi-room list | 155 + | `pcm_airplay_clear_receivers` | Clear the receiver list before re-configuring | 156 + | `pcm_airplay_connect` | Open RTSP + RTP sessions for all configured receivers | 157 + | `pcm_airplay_write` | Buffer PCM, encode ALAC once, fan out to every receiver | 158 + | `pcm_airplay_stop` | Send TEARDOWN to all, clear session | 159 + | `pcm_airplay_close` | Same as stop (called on sink switch) | 160 160 161 161 `SESSION` is a `Mutex<Option<AirPlaySession>>`. `CONFIG` is a 162 162 `Mutex<AirPlayConfig>` holding `receivers: Vec<(String, u16)>`. ··· 327 327 328 328 Owns the two UDP sockets for one AirPlay endpoint: 329 329 330 - | Socket | Direction | Purpose | 331 - |--------------|-------------------------|---------------------| 332 - | `audio_sock` | → receiver audio port | RTP audio frames | 333 - | `ctrl_sock` | ↔ receiver control port | RTCP sync packets | 330 + | Socket | Direction | Purpose | 331 + | ------------ | ----------------------- | ----------------- | 332 + | `audio_sock` | → receiver audio port | RTP audio frames | 333 + | `ctrl_sock` | ↔ receiver control port | RTCP sync packets | 334 334 335 335 Also holds `ssrc` (random per receiver) and `seqnum` (wrapping u16). 336 336
+21 -21
crates/chromecast/README.md
··· 53 53 54 54 ## Module map 55 55 56 - | File | Responsibility | 57 - |---------------|-----------------------------------------------------------------------------------------| 56 + | File | Responsibility | 57 + | ------------- | -------------------------------------------------------------------------------------- | 58 58 | `src/pcm.rs` | Primary: HTTP WAV server; `BroadcastBuffer`; `cast_loop`; full C FFI surface | 59 59 | `src/lib.rs` | Secondary: `Player` trait impl; Cast command dispatch (retained, not called by server) | 60 - | `src/main.rs` | Example binary (connects to a hardcoded IP for manual testing) | 60 + | `src/main.rs` | Example binary (connects to a hardcoded IP for manual testing) | 61 61 62 62 --- 63 63 ··· 198 198 If you need to drive the Cast protocol directly (e.g. from a test binary or a 199 199 future multi-session feature), `lib.rs` provides: 200 200 201 - | Player method | Cast action | 202 - |----------------|--------------------------------------| 203 - | `play()` | `media.play()` | 204 - | `pause()` | `media.pause()` | 205 - | `resume()` | `media.play()` | 206 - | `stop()` | no-op | 207 - | `disconnect()` | `receiver.stop_app(session_id)` | 201 + | Player method | Cast action | 202 + | -------------- | ------------------------------- | 203 + | `play()` | `media.play()` | 204 + | `pause()` | `media.pause()` | 205 + | `resume()` | `media.play()` | 206 + | `stop()` | no-op | 207 + | `disconnect()` | `receiver.stop_app(session_id)` | 208 208 209 209 Next / previous are **not** routed through `lib.rs` — the server always calls 210 210 `rb::playback::next()` / `rb::playback::prev()` directly, and the `cast_loop` ··· 286 286 ### Port summary 287 287 288 288 | Port | Protocol | Purpose | 289 - |------|-----------|-----------------------------------------------------| 289 + | ---- | --------- | --------------------------------------------------- | 290 290 | 8009 | TCP / TLS | Cast control channel (Protobuf) | 291 291 | 7881 | HTTP | WAV audio stream + album art served **by rockboxd** | 292 292 ··· 297 297 298 298 ## Known limitations 299 299 300 - | Feature | Status | 301 - |---------------------------------------|---------------------------------------------| 302 - | Play / pause / resume | ✅ Implemented | 303 - | Next / previous track | ✅ Via `rb::playback::next/prev` + cast_loop | 304 - | Track metadata + album art display | ✅ Implemented | 305 - | Reconnect after output switch | ✅ Via teardown + fresh cast_loop | 306 - | Volume control | ⏳ Not yet implemented | 307 - | Seek within track | ⏳ Not yet implemented | 308 - | Multi-device fan-out | ⏳ Not yet implemented (single device only) | 300 + | Feature | Status | 301 + | ---------------------------------- | ------------------------------------------- | 302 + | Play / pause / resume | ✅ Implemented | 303 + | Next / previous track | ✅ Via `rb::playback::next/prev` + cast_loop | 304 + | Track metadata + album art display | ✅ Implemented | 305 + | Reconnect after output switch | ✅ Via teardown + fresh cast_loop | 306 + | Volume control | ⏳ Not yet implemented | 307 + | Seek within track | ⏳ Not yet implemented | 308 + | Multi-device fan-out | ⏳ Not yet implemented (single device only) | 309 309 310 310 --- 311 311 312 312 ## Dependencies 313 313 314 314 | Crate | Version | Purpose | 315 - |-------------------|-----------|-----------------------------------------------------------| 315 + | ----------------- | --------- | --------------------------------------------------------- | 316 316 | `chromecast` | 0.18.2 | Cast protocol client (Protobuf/TLS) | 317 317 | `tokio` | workspace | Async runtime for Cast background task | 318 318 | `async-trait` | workspace | `Player` trait with async methods |
+56 -56
crates/expo/README.md
··· 2 2 3 3 The mobile-side Rust crate. Two builds in one workspace: 4 4 5 - | Build | Output | Size | Purpose | 6 - |---|---|---|---| 7 - | **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. | 8 - | **`--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. | 5 + | Build | Output | Size | Purpose | 6 + | ----------------------------------------------- | ---------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 7 + | **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. | 8 + | **`--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. | 9 9 10 10 The Expo Modules wrapper at [`expo/modules/rockbox-rpc/`](../../expo/modules/rockbox-rpc/) 11 11 loads the resulting library and forwards calls through React Native. ··· 73 73 74 74 ## Surface map 75 75 76 - | Group | Examples | 77 - |-------|----------| 78 - | Init | `rb_set_server_url`, `rb_set_http_url`, `rb_ping` | 79 - | Playback | `rb_play / pause / play_pause / next / prev`, `rb_seek`, `rb_play_album / play_artist_tracks / play_track / play_directory` | 80 - | 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` | 81 - | 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` | 82 - | Sound / Settings | `rb_adjust_volume`, `rb_sound_current_json`, `rb_save_shuffle / save_repeat`, `rb_get_global_settings_json`, `rb_get_global_status_json` | 83 - | Browse | `rb_tree_get_entries_json` | 84 - | 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` | 85 - | Smart playlists | `rb_get_smart_playlists_json`, `rb_get_smart_playlist_tracks_json`, `rb_play_smart_playlist` | 86 - | Bluetooth | `rb_bluetooth_available`, `rb_get_bluetooth_devices_json`, `rb_connect_bluetooth`, `rb_disconnect_bluetooth` | 87 - | Server-streaming | `rb_subscribe_status`, `rb_subscribe_current_track`, `rb_subscribe_playlist`, `rb_subscribe_library`, `rb_subscribe_discovery(serviceName)` | 88 - | Stream pump | `rb_poll_event(subId, timeoutMs)`, `rb_unsubscribe(subId)` | 89 - | Discovery constants | `rb_rockbox_service_name`, `rb_chromecast_service_name` | 90 - | Memory | `rb_free_string` | 91 - | **Embedded daemon** | `rb_daemon_start(configDir, musicDir, deviceName)`, `rb_daemon_port`, `rb_daemon_state`, `rb_rescan_library` | 76 + | Group | Examples | 77 + | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 78 + | Init | `rb_set_server_url`, `rb_set_http_url`, `rb_ping` | 79 + | Playback | `rb_play / pause / play_pause / next / prev`, `rb_seek`, `rb_play_album / play_artist_tracks / play_track / play_directory` | 80 + | 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` | 81 + | 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` | 82 + | Sound / Settings | `rb_adjust_volume`, `rb_sound_current_json`, `rb_save_shuffle / save_repeat`, `rb_get_global_settings_json`, `rb_get_global_status_json` | 83 + | Browse | `rb_tree_get_entries_json` | 84 + | 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` | 85 + | Smart playlists | `rb_get_smart_playlists_json`, `rb_get_smart_playlist_tracks_json`, `rb_play_smart_playlist` | 86 + | Bluetooth | `rb_bluetooth_available`, `rb_get_bluetooth_devices_json`, `rb_connect_bluetooth`, `rb_disconnect_bluetooth` | 87 + | Server-streaming | `rb_subscribe_status`, `rb_subscribe_current_track`, `rb_subscribe_playlist`, `rb_subscribe_library`, `rb_subscribe_discovery(serviceName)` | 88 + | Stream pump | `rb_poll_event(subId, timeoutMs)`, `rb_unsubscribe(subId)` | 89 + | Discovery constants | `rb_rockbox_service_name`, `rb_chromecast_service_name` | 90 + | Memory | `rb_free_string` | 91 + | **Embedded daemon** | `rb_daemon_start(configDir, musicDir, deviceName)`, `rb_daemon_port`, `rb_daemon_state`, `rb_rescan_library` | 92 92 93 93 --- 94 94 ··· 292 292 The cdylib-specific firmware sources live under 293 293 `firmware/target/hosted/android/cdylib/`: 294 294 295 - | File | Role | 296 - |---|---| 297 - | `system-android.c` | Headless system_init + power_off + stdout/stderr→logcat shim | 298 - | `pcm-aaudio.c` | AAudio PCM sink (replaces SDL audio) | 299 - | `lc-android.c` | `lc_open()` / `lc_get_header()` over the static `lc_static_table[]` | 300 - | `rb_zig_compat.c` | C compat layer for the 18 `rb_*` symbols `crates/sys` would otherwise pull from `zig/src/main.zig` | 301 - | `lcd-noop.c`, `button-noop.c`, `cpuinfo-noop.c`, `audiohw-noop.c` | Stubs — UI lives in React Native, not on the device LCD | 295 + | File | Role | 296 + | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | 297 + | `system-android.c` | Headless system_init + power_off + stdout/stderr→logcat shim | 298 + | `pcm-aaudio.c` | AAudio PCM sink (replaces SDL audio) | 299 + | `lc-android.c` | `lc_open()` / `lc_get_header()` over the static `lc_static_table[]` | 300 + | `rb_zig_compat.c` | C compat layer for the 18 `rb_*` symbols `crates/sys` would otherwise pull from `zig/src/main.zig` | 301 + | `lcd-noop.c`, `button-noop.c`, `cpuinfo-noop.c`, `audiohw-noop.c` | Stubs — UI lives in React Native, not on the device LCD | 302 302 303 303 --- 304 304 ··· 371 371 The configure script reads two env vars when it sees the androidcdylib 372 372 target — set them before running if you need non-default values: 373 373 374 - | Var | Default | Purpose | 375 - |---|---|---| 376 - | `ANDROID_NDK_HOME` | _(none — required)_ | Path to NDK install root | 377 - | `ANDROID_TARGET_ABI` | `arm64-v8a` | One of `arm64-v8a` / `armeabi-v7a` / `x86_64` | 378 - | `ANDROID_API_LEVEL` | `26` | Minimum SDK; **don't go below 26** (AAudio requires it) | 374 + | Var | Default | Purpose | 375 + | -------------------- | ------------------- | ------------------------------------------------------- | 376 + | `ANDROID_NDK_HOME` | _(none — required)_ | Path to NDK install root | 377 + | `ANDROID_TARGET_ABI` | `arm64-v8a` | One of `arm64-v8a` / `armeabi-v7a` / `x86_64` | 378 + | `ANDROID_API_LEVEL` | `26` | Minimum SDK; **don't go below 26** (AAudio requires it) | 379 379 380 380 For a 32-bit ARM build, e.g.: 381 381 ··· 536 536 537 537 Tag map: 538 538 539 - | logcat tag | Source | 540 - |---|---| 541 - | `rockbox` | Rust `tracing::*` calls (default level: per-crate `debug`, see `daemon.rs::install_logcat_subscriber`) | 542 - | `Rockbox` | C firmware `printf`/`fprintf` and `DEBUGF`/`logf`/`panicf` (routed via `debug-android.c` and the stdout/stderr pipe in `system-android.c`) | 543 - | `rockbox-engine` | `system-android.c` boot diagnostics (cgroup/SELinux denials, etc.) | 544 - | `rb-system-android`, `rb-pcm-aaudio` | other cdylib C tags | 545 - | `RockboxRpc` | Kotlin Log calls in `RockboxRpcModule.kt` | 546 - | `RockboxNowPlaying` | Kotlin Log calls in `NowPlayingService.kt` | 539 + | logcat tag | Source | 540 + | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | 541 + | `rockbox` | Rust `tracing::*` calls (default level: per-crate `debug`, see `daemon.rs::install_logcat_subscriber`) | 542 + | `Rockbox` | C firmware `printf`/`fprintf` and `DEBUGF`/`logf`/`panicf` (routed via `debug-android.c` and the stdout/stderr pipe in `system-android.c`) | 543 + | `rockbox-engine` | `system-android.c` boot diagnostics (cgroup/SELinux denials, etc.) | 544 + | `rb-system-android`, `rb-pcm-aaudio` | other cdylib C tags | 545 + | `RockboxRpc` | Kotlin Log calls in `RockboxRpcModule.kt` | 546 + | `RockboxNowPlaying` | Kotlin Log calls in `NowPlayingService.kt` | 547 547 548 548 Quick capture recipe: 549 549 ··· 560 560 561 561 ## Known pitfalls 562 562 563 - | Symptom | Cause | Fix | 564 - |---|---|---| 565 - | `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) | 566 - | `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` | 567 - | `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` | 568 - | 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) | 569 - | 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 | 570 - | `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 | 571 - | 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` | 572 - | 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()` | 573 - | `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 | 574 - | 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`) | 575 - | 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 | 576 - | 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 | 577 - | 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 | 578 - | 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 | 563 + | Symptom | Cause | Fix | | | 564 + | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | ------------------------------------------------------------------------------------------------------------------------------------------------- | 565 + | `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) | | | 566 + | `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` | | | 567 + | `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` | | | 568 + | 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) | | | 569 + | 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 | | | 570 + | `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 | | | 571 + | 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` | | | 572 + | 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()` | | | 573 + | `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 | | | 574 + | 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`) | | | 575 + | 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 | 576 + | 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 | | | 577 + | 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 | | | 578 + | 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 | | | 579 579 580 580 --- 581 581
+3 -3
crates/slim/README.md
··· 209 209 `crates/sys/src/sound/pcm.rs`: 210 210 211 211 | C symbol | Rust wrapper | 212 - |--------------------------------------|------------------------------------------| 212 + | ------------------------------------ | ---------------------------------------- | 213 213 | `pcm_squeezelite_set_slim_port(u16)` | `pcm::squeezelite_set_slim_port(u16)` | 214 214 | `pcm_squeezelite_set_http_port(u16)` | `pcm::squeezelite_set_http_port(u16)` | 215 215 | `pcm_switch_sink(i32)` | `pcm::switch_sink(PCM_SINK_SQUEEZELITE)` | ··· 358 358 The Slim Protocol uses **asymmetric framing**: 359 359 360 360 | Direction | Wire format | 361 - |-----------------|-----------------------------------------------------| 361 + | --------------- | --------------------------------------------------- | 362 362 | Client → Server | `opcode[4]` + `u32 length BE` + `payload[length]` | 363 363 | Server → Client | `u16 length BE` + `opcode[4]` + `payload[length-4]` | 364 364 ··· 550 550 squeezelite handles the audio pipeline in three internal threads: 551 551 552 552 | Thread | Role | 553 - |--------------------------------------|---------------------------------------------------------------| 553 + | ------------------------------------ | ------------------------------------------------------------- | 554 554 | `stream_thread` | Reads HTTP stream into `streambuf` (2 MB ring buffer) | 555 555 | `decode_thread` | PCM "decoder": memcpy from `streambuf` → `outputbuf` (3.5 MB) | 556 556 | `output_thread` / PortAudio callback | Reads `outputbuf`, sends to audio device |
+30 -30
crates/upnp/README.md
··· 3 3 UPnP/DLNA support for Rockbox Zig. This crate provides three independent but 4 4 complementary features: 5 5 6 - | Feature | What it does | 7 - |--------------------------------------|-----------------------------------------------------------------------------------------------------| 8 - | **Media Server** (ContentDirectory) | Exposes the music library so UPnP control points (BubbleUPnP, Kodi, etc.) can browse and pull tracks | 9 - | **MediaRenderer** | Lets control points push media to Rockbox (Rockbox becomes the speaker) | 10 - | **PCM sink / WAV output** | Streams live PCM audio as WAV-over-HTTP to an external UPnP renderer (Kodi, etc.) | 6 + | Feature | What it does | 7 + | ----------------------------------- | ---------------------------------------------------------------------------------------------------- | 8 + | **Media Server** (ContentDirectory) | Exposes the music library so UPnP control points (BubbleUPnP, Kodi, etc.) can browse and pull tracks | 9 + | **MediaRenderer** | Lets control points push media to Rockbox (Rockbox becomes the speaker) | 10 + | **PCM sink / WAV output** | Streams live PCM audio as WAV-over-HTTP to an external UPnP renderer (Kodi, etc.) | 11 11 12 12 --- 13 13 ··· 40 40 41 41 ### UPnP service roles 42 42 43 - | UPnP term | Rockbox role | Description | 44 - |---------------------------------|----------------|----------------------------------------| 45 - | MediaServer / ContentDirectory | **server** | Hosts the music library for browsing | 46 - | MediaRenderer / AVTransport | **renderer** | Receives push-play commands | 47 - | Control Point | *external app* | BubbleUPnP, Kodi, Foobar2000, … | 43 + | UPnP term | Rockbox role | Description | 44 + | ------------------------------ | -------------- | ------------------------------------ | 45 + | MediaServer / ContentDirectory | **server** | Hosts the music library for browsing | 46 + | MediaRenderer / AVTransport | **renderer** | Receives push-play commands | 47 + | Control Point | *external app* | BubbleUPnP, Kodi, Foobar2000, … | 48 48 49 49 --- 50 50 ··· 123 123 124 124 ### Supported AVTransport actions 125 125 126 - | Action | Behaviour | 127 - |-----------------------|------------------------------------------------------------------------------------| 128 - | `SetAVTransportURI` | Store URI + parse DIDL-Lite metadata; open the stream in the Rockbox audio engine | 129 - | `Play` | Start or resume playback | 130 - | `Pause` | Pause/resume toggle | 131 - | `Stop` | Stop playback; clear stored metadata | 132 - | `Seek` | Seek to absolute time (REL_TIME target unit) | 133 - | `GetTransportInfo` | Return current transport state (PLAYING / PAUSED_PLAYBACK / STOPPED) | 134 - | `GetPositionInfo` | Return track URI, DIDL-Lite metadata, duration, elapsed time | 135 - | `GetMediaInfo` | Return current URI and DIDL-Lite metadata | 126 + | Action | Behaviour | 127 + | ------------------- | --------------------------------------------------------------------------------- | 128 + | `SetAVTransportURI` | Store URI + parse DIDL-Lite metadata; open the stream in the Rockbox audio engine | 129 + | `Play` | Start or resume playback | 130 + | `Pause` | Pause/resume toggle | 131 + | `Stop` | Stop playback; clear stored metadata | 132 + | `Seek` | Seek to absolute time (REL_TIME target unit) | 133 + | `GetTransportInfo` | Return current transport state (PLAYING / PAUSED_PLAYBACK / STOPPED) | 134 + | `GetPositionInfo` | Return track URI, DIDL-Lite metadata, duration, elapsed time | 135 + | `GetMediaInfo` | Return current URI and DIDL-Lite metadata | 136 136 137 137 --- 138 138 ··· 225 225 226 226 ### All UPnP settings at a glance 227 227 228 - | Key | Type | Default | Description | 229 - |--------------------------|-----------|--------------|------------------------------------------------| 230 - | `audio_output` | string | `"builtin"` | Set to `"upnp"` to use the PCM sink | 231 - | `upnp_renderer_url` | string | — | AVTransport controlURL of the target renderer | 232 - | `upnp_http_port` | integer | `7879` | Port for the WAV broadcast HTTP server | 233 - | `upnp_server_enabled` | bool | `false` | Start the ContentDirectory media server | 234 - | `upnp_server_port` | integer | `7878` | HTTP port for the media server | 235 - | `upnp_renderer_enabled` | bool | `false` | Start the MediaRenderer | 236 - | `upnp_renderer_port` | integer | `7880` | HTTP port for the renderer | 237 - | `upnp_friendly_name` | string | `"Rockbox"` | Display name shown to control points | 228 + | Key | Type | Default | Description | 229 + | ----------------------- | ------- | ----------- | --------------------------------------------- | 230 + | `audio_output` | string | `"builtin"` | Set to `"upnp"` to use the PCM sink | 231 + | `upnp_renderer_url` | string | — | AVTransport controlURL of the target renderer | 232 + | `upnp_http_port` | integer | `7879` | Port for the WAV broadcast HTTP server | 233 + | `upnp_server_enabled` | bool | `false` | Start the ContentDirectory media server | 234 + | `upnp_server_port` | integer | `7878` | HTTP port for the media server | 235 + | `upnp_renderer_enabled` | bool | `false` | Start the MediaRenderer | 236 + | `upnp_renderer_port` | integer | `7880` | HTTP port for the renderer | 237 + | `upnp_friendly_name` | string | `"Rockbox"` | Display name shown to control points | 238 238 239 239 --- 240 240
+39 -39
docs/pcm-normalization.md
··· 58 58 59 59 The coefficient `α` controls how quickly the estimate tracks changes. Crucially, **two different coefficients** are used depending on the direction of change: 60 60 61 - | Signal direction | Coefficient | Behaviour | 62 - |---|---|---| 63 - | `chunk_rms > rms_estimate` (getting louder) | `RMS_ATTACK = 0.3` | Tracks loud transients in 2–3 chunks (< 150 ms) | 64 - | `chunk_rms < rms_estimate` (getting quieter) | `RMS_RELEASE = 0.99` | Takes ~7 s to settle on a quieter signal | 61 + | Signal direction | Coefficient | Behaviour | 62 + | -------------------------------------------- | -------------------- | ----------------------------------------------- | 63 + | `chunk_rms > rms_estimate` (getting louder) | `RMS_ATTACK = 0.3` | Tracks loud transients in 2–3 chunks (< 150 ms) | 64 + | `chunk_rms < rms_estimate` (getting quieter) | `RMS_RELEASE = 0.99` | Takes ~7 s to settle on a quieter signal | 65 65 66 66 This asymmetry is essential. A fast attack means the estimate rises quickly when a loud section begins — preventing the normalizer from over-boosting and causing clipping. A slow release means the estimate falls slowly after a loud section ends — preventing the gain from shooting up during a brief quiet passage (the "pumping" or "breathing" artefact). 67 67 ··· 88 88 gain = β × gain + (1 − β) × desired_gain 89 89 ``` 90 90 91 - | Direction | Coefficient | Convergence | 92 - |---|---|---| 93 - | Gain decreasing (signal too loud) | `GAIN_ATTACK = 0.3` | Reaches target in ~3 chunks (< 150 ms) | 94 - | Gain increasing (signal too quiet) | `GAIN_RELEASE = 0.98` | Reaches target in ~3 seconds | 91 + | Direction | Coefficient | Convergence | 92 + | ---------------------------------- | --------------------- | -------------------------------------- | 93 + | Gain decreasing (signal too loud) | `GAIN_ATTACK = 0.3` | Reaches target in ~3 chunks (< 150 ms) | 94 + | Gain increasing (signal too quiet) | `GAIN_RELEASE = 0.98` | Reaches target in ~3 seconds | 95 95 96 96 The fast gain attack prevents over-shoot and clipping when a loud track suddenly follows a quiet one. The slow gain release prevents the loudness from rising abruptly during a quiet moment. 97 97 ··· 165 165 166 166 All parameters are compile-time constants in `firmware/pcm_normalizer.c`. 167 167 168 - | Constant | Value | dB equivalent | Description | 169 - |---|---|---|---| 170 - | `TARGET_RMS` | `0.35` | −9 dBFS | Target RMS loudness. Higher = louder output. | 171 - | `RMS_ATTACK` | `0.3` | — | IIR coefficient for RMS rising (loud signal). Lower = faster. | 172 - | `RMS_RELEASE` | `0.99` | — | IIR coefficient for RMS falling (quiet signal). Higher = slower. | 173 - | `GAIN_ATTACK` | `0.3` | — | IIR coefficient for gain decreasing. Lower = faster. | 174 - | `GAIN_RELEASE` | `0.98` | — | IIR coefficient for gain increasing. Higher = slower. | 175 - | `MAX_GAIN` | `10.0` | +20 dB | Maximum boost applied to quiet tracks. | 176 - | `MIN_GAIN` | `0.1` | −20 dB | Maximum cut applied to loud tracks. | 177 - | `GATE_THRESH` | `0.001` | −60 dBFS | RMS below this → treat chunk as silence. | 168 + | Constant | Value | dB equivalent | Description | 169 + | -------------- | ------- | ------------- | ---------------------------------------------------------------- | 170 + | `TARGET_RMS` | `0.35` | −9 dBFS | Target RMS loudness. Higher = louder output. | 171 + | `RMS_ATTACK` | `0.3` | — | IIR coefficient for RMS rising (loud signal). Lower = faster. | 172 + | `RMS_RELEASE` | `0.99` | — | IIR coefficient for RMS falling (quiet signal). Higher = slower. | 173 + | `GAIN_ATTACK` | `0.3` | — | IIR coefficient for gain decreasing. Lower = faster. | 174 + | `GAIN_RELEASE` | `0.98` | — | IIR coefficient for gain increasing. Higher = slower. | 175 + | `MAX_GAIN` | `10.0` | +20 dB | Maximum boost applied to quiet tracks. | 176 + | `MIN_GAIN` | `0.1` | −20 dB | Maximum cut applied to loud tracks. | 177 + | `GATE_THRESH` | `0.001` | −60 dBFS | RMS below this → treat chunk as silence. | 178 178 179 179 ### Choosing TARGET_RMS 180 180 181 181 `TARGET_RMS` is the most impactful parameter. A few reference points: 182 182 183 - | Value | dBFS | Character | 184 - |---|---|---| 185 - | `0.071` | −23 dBFS | EBU R128 broadcast standard (very conservative) | 186 - | `0.178` | −15 dBFS | Apple Music / AES streaming recommendation | 187 - | `0.200` | −14 dBFS | Spotify / YouTube streaming target | 188 - | `0.350` | −9 dBFS | **Current default** — loud and punchy | 189 - | `0.500` | −6 dBFS | Very loud; risk of clipping on loud source material | 183 + | Value | dBFS | Character | 184 + | ------- | -------- | --------------------------------------------------- | 185 + | `0.071` | −23 dBFS | EBU R128 broadcast standard (very conservative) | 186 + | `0.178` | −15 dBFS | Apple Music / AES streaming recommendation | 187 + | `0.200` | −14 dBFS | Spotify / YouTube streaming target | 188 + | `0.350` | −9 dBFS | **Current default** — loud and punchy | 189 + | `0.500` | −6 dBFS | Very loud; risk of clipping on loud source material | 190 190 191 191 ### Convergence Time Reference 192 192 193 193 IIR convergence depends on the chunk size. For a typical 4 096-byte chunk at 44 100 Hz stereo (46 ms per chunk): 194 194 195 - | Parameter | Coefficient | ~Time to move 63% of the way to target | 196 - |---|---|---| 197 - | `RMS_ATTACK` | 0.3 | 1 chunk ≈ 46 ms | 198 - | `RMS_RELEASE` | 0.99 | 100 chunks ≈ 4.6 s | 199 - | `GAIN_ATTACK` | 0.3 | 1 chunk ≈ 46 ms | 200 - | `GAIN_RELEASE` | 0.98 | 50 chunks ≈ 2.3 s | 195 + | Parameter | Coefficient | ~Time to move 63% of the way to target | 196 + | -------------- | ----------- | -------------------------------------- | 197 + | `RMS_ATTACK` | 0.3 | 1 chunk ≈ 46 ms | 198 + | `RMS_RELEASE` | 0.99 | 100 chunks ≈ 4.6 s | 199 + | `GAIN_ATTACK` | 0.3 | 1 chunk ≈ 46 ms | 200 + | `GAIN_RELEASE` | 0.98 | 50 chunks ≈ 2.3 s | 201 201 202 202 Time constant τ = `−chunk_duration / ln(α)`. For `α = 0.98` and chunk = 46 ms: τ = −46 ms / ln(0.98) ≈ 2.3 s. 203 203 ··· 257 257 258 258 Rockbox also supports ReplayGain, which is a pre-computed per-track gain stored in file tags. The two approaches are complementary: 259 259 260 - | | ReplayGain | PCM Normalizer | 261 - |---|---|---| 262 - | **Requires track analysis** | Yes (offline scan) | No | 263 - | **Works on streams / radio** | No | Yes | 264 - | **Accuracy** | Very high (full-track analysis) | Moderate (real-time estimate) | 265 - | **Artefacts** | None | Slight pumping on highly dynamic content | 266 - | **Target** | Configurable per standard | `TARGET_RMS` compile constant | 267 - | **Processing cost** | Zero at runtime | ~1–2% CPU (RMS + gain loop) | 260 + | | ReplayGain | PCM Normalizer | 261 + | ---------------------------- | ------------------------------- | ---------------------------------------- | 262 + | **Requires track analysis** | Yes (offline scan) | No | 263 + | **Works on streams / radio** | No | Yes | 264 + | **Accuracy** | Very high (full-track analysis) | Moderate (real-time estimate) | 265 + | **Artefacts** | None | Slight pumping on highly dynamic content | 266 + | **Target** | Configurable per standard | `TARGET_RMS` compile constant | 267 + | **Processing cost** | Zero at runtime | ~1–2% CPU (RMS + gain loop) | 268 268 269 269 For local music libraries, ReplayGain is generally preferred when tags are available. The PCM normalizer is the practical choice for streaming sources or when ReplayGain tags are missing. 270 270
+9 -9
expo/README.md
··· 44 44 45 45 Mirrors [`gpui/src/ui/theme.rs`](../gpui/src/ui/theme.rs): 46 46 47 - | Token | Hex | 48 - | ------------ | -------------------- | 49 - | `appBg` | `#0F1117` | 50 - | `bgCard` | `#1A1D26` | 51 - | `accent` | `#6F00FF` | 52 - | `accentSoft` | `#1A0E3D` | 53 - | `text/primary` | `#FFFFFF` | 54 - | `text/secondary` | `#9898A8` | 55 - | `border` | `rgba(255,255,255,0.16)` | 47 + | Token | Hex | 48 + | ---------------- | ------------------------ | 49 + | `appBg` | `#0F1117` | 50 + | `bgCard` | `#1A1D26` | 51 + | `accent` | `#6F00FF` | 52 + | `accentSoft` | `#1A0E3D` | 53 + | `text/primary` | `#FFFFFF` | 54 + | `text/secondary` | `#9898A8` | 55 + | `border` | `rgba(255,255,255,0.16)` | 56 56 57 57 The app icon (`assets/images/icon.png`, splash, favicon, Android adaptive 58 58 foreground) is rendered from [`gpui/assets/rockbox.svg`](../gpui/assets/rockbox.svg).
+8 -8
expo/modules/rockbox-rpc/README.md
··· 181 181 182 182 Topics surfaced through the EventEmitter base class: 183 183 184 - | Topic | Payload | 185 - |-------|---------| 186 - | `rockbox.status` | `{ status: 0|1|2 }` | 187 - | `rockbox.currentTrack` | `TrackSnapshot` | 188 - | `rockbox.playlist` | `PlaylistSnapshot` (`index`, `amount`, `tracks`) | 189 - | `rockbox.library` | full library snapshot (`unknown` — cast as needed) | 190 - | `rockbox.discovery` | `DiscoveredService` (`name`, `hostname`, `port`, `addresses[]`, `properties{}`) | 191 - | `rockbox.error` | `{ subId, stream, error }` | 184 + | Topic | Payload | | | 185 + | ---------------------- | ------------------------------------------------------------------------------- | - | ---- | 186 + | `rockbox.status` | `{ status: 0 | 1 | 2 }` | 187 + | `rockbox.currentTrack` | `TrackSnapshot` | | | 188 + | `rockbox.playlist` | `PlaylistSnapshot` (`index`, `amount`, `tracks`) | | | 189 + | `rockbox.library` | full library snapshot (`unknown` — cast as needed) | | | 190 + | `rockbox.discovery` | `DiscoveredService` (`name`, `hostname`, `port`, `addresses[]`, `properties{}`) | | | 191 + | `rockbox.error` | `{ subId, stream, error }` | | | 192 192 193 193 ## Adding a new method 194 194
+23 -23
sdk/clojure/README.md
··· 132 132 (rb/with-headers {:x-trace-id "req-123"}))) 133 133 ``` 134 134 135 - | Option | Default | Description | 136 - |---------------|-----------------------------------|-------------------------------------| 137 - | `:host` | `"localhost"` | rockboxd hostname / IP | 138 - | `:port` | `6062` | GraphQL HTTP/WS port | 139 - | `:http-url` | `http://{host}:{port}/graphql` | Override the full HTTP URL | 140 - | `:ws-url` | `ws://{host}:{port}/graphql` | Override the full WS URL | 141 - | `:timeout-ms` | `15000` | Per-request timeout | 142 - | `:headers` | `{}` | Extra HTTP headers map | 143 - | `:http-client`| (auto) | Reuse a `java.net.http.HttpClient` | 135 + | Option | Default | Description | 136 + | -------------- | ------------------------------ | ---------------------------------- | 137 + | `:host` | `"localhost"` | rockboxd hostname / IP | 138 + | `:port` | `6062` | GraphQL HTTP/WS port | 139 + | `:http-url` | `http://{host}:{port}/graphql` | Override the full HTTP URL | 140 + | `:ws-url` | `ws://{host}:{port}/graphql` | Override the full WS URL | 141 + | `:timeout-ms` | `15000` | Per-request timeout | 142 + | `:headers` | `{}` | Extra HTTP headers map | 143 + | `:http-client` | (auto) | Reuse a `java.net.http.HttpClient` | 144 144 145 145 --- 146 146 ··· 252 252 ``` 253 253 254 254 | `insert-position` keyword | Effect | 255 - |---------------------------|----------------------------------------| 255 + | ------------------------- | -------------------------------------- | 256 256 | `:next` | After the currently playing track | 257 257 | `:after-current` | After the last manually inserted track | 258 258 | `:last` | At the end of the queue | ··· 491 491 492 492 ### Event map 493 493 494 - | Event | Payload | Description | 495 - |---------------------|-------------|--------------------------------------| 496 - | `:track-changed` | track map | Currently playing track changed | 497 - | `:status-changed` | int | Playback status (0=stopped, 1=playing, 3=paused) | 498 - | `:playlist-changed` | playlist | Active queue was modified | 499 - | `:ws-open` | `nil` | WebSocket connection established | 500 - | `:ws-close` | `nil` | WebSocket connection closed | 501 - | `:ws-error` | Throwable | WebSocket / subscription error | 494 + | Event | Payload | Description | 495 + | ------------------- | --------- | ------------------------------------------------ | 496 + | `:track-changed` | track map | Currently playing track changed | 497 + | `:status-changed` | int | Playback status (0=stopped, 1=playing, 3=paused) | 498 + | `:playlist-changed` | playlist | Active queue was modified | 499 + | `:ws-open` | `nil` | WebSocket connection established | 500 + | `:ws-close` | `nil` | WebSocket connection closed | 501 + | `:ws-error` | Throwable | WebSocket / subscription error | 502 502 503 503 --- 504 504 ··· 603 603 (err/graphql-error? e) 604 604 ``` 605 605 606 - | `:type` | When thrown | 607 - |--------------------|-----------------------------------------------------------------| 608 - | `:rockbox/network` | Cannot reach rockboxd, or HTTP returned a non-2xx status | 609 - | `:rockbox/graphql` | Server returned `{errors: [...]}` in the response body | 610 - | `:rockbox/config` | Client constructed with bad config or required input missing | 606 + | `:type` | When thrown | 607 + | ------------------ | ------------------------------------------------------------ | 608 + | `:rockbox/network` | Cannot reach rockboxd, or HTTP returned a non-2xx status | 609 + | `:rockbox/graphql` | Server returned `{errors: [...]}` in the response body | 610 + | `:rockbox/config` | Client constructed with bad config or required input missing | 611 611 612 612 --- 613 613
+12 -12
sdk/clojure/examples/README.md
··· 10 10 ROCKBOX_HOST=192.168.1.42 clj -M:examples -m ex02-now-playing 11 11 ``` 12 12 13 - | File | What it shows | 14 - |-----------------------------------|-------------------------------------------------------| 15 - | `ex01_basic_playback.clj` | Pause / seek / resume in one threading-macro chain | 16 - | `ex02_now_playing.clj` | Pretty-print the currently playing track | 17 - | `ex03_library_search.clj` | Search → play first matching album shuffled | 18 - | `ex04_queue_management.clj` | Inspect and modify the live queue | 19 - | `ex05_realtime_events.clj` | WebSocket events with the callback API | 20 - | `ex06_core_async_events.clj` | Same events, consumed via `core.async` channels | 21 - | `ex07_volume_eq.clj` | Adjust volume + write a 5-band EQ preset | 22 - | `ex08_browse_filesystem.clj` | Walk `music_dir` (directories vs files) | 23 - | `ex09_plugin_scrobbler.clj` | Toy "scrobbler" plugin via `use-plugin` / event hook | 24 - | `ex10_smart_playlist.clj` | Create a smart playlist from a Clojure data rule-set | 13 + | File | What it shows | 14 + | ---------------------------- | ---------------------------------------------------- | 15 + | `ex01_basic_playback.clj` | Pause / seek / resume in one threading-macro chain | 16 + | `ex02_now_playing.clj` | Pretty-print the currently playing track | 17 + | `ex03_library_search.clj` | Search → play first matching album shuffled | 18 + | `ex04_queue_management.clj` | Inspect and modify the live queue | 19 + | `ex05_realtime_events.clj` | WebSocket events with the callback API | 20 + | `ex06_core_async_events.clj` | Same events, consumed via `core.async` channels | 21 + | `ex07_volume_eq.clj` | Adjust volume + write a 5-band EQ preset | 22 + | `ex08_browse_filesystem.clj` | Walk `music_dir` (directories vs files) | 23 + | `ex09_plugin_scrobbler.clj` | Toy "scrobbler" plugin via `use-plugin` / event hook | 24 + | `ex10_smart_playlist.clj` | Create a smart playlist from a Clojure data rule-set | 25 25 26 26 `rockboxd` must be running locally (or specify `ROCKBOX_HOST`) before the 27 27 examples can connect.
+39 -39
sdk/elixir/README.md
··· 112 112 ) 113 113 ``` 114 114 115 - | Option | Type | Default | Description | 116 - |-------------|------------------------|----------------------------------|-----------------------------------------------------| 117 - | `:host` | `String.t()` | `"localhost"` | Hostname or IP of rockboxd | 118 - | `:port` | `non_neg_integer()` | `6062` | GraphQL HTTP/WS port | 119 - | `:http_url` | `String.t()` | `http://{host}:{port}/graphql` | Override the full HTTP URL | 120 - | `:ws_url` | `String.t()` | `ws://{host}:{port}/graphql` | Override the full WebSocket URL | 121 - | `:headers` | `[{String.t(), String.t()}]` | `[]` | Extra HTTP request headers | 122 - | `:timeout` | `non_neg_integer()` | `15_000` | HTTP request timeout (ms) | 115 + | Option | Type | Default | Description | 116 + | ----------- | ---------------------------- | ------------------------------ | ------------------------------- | 117 + | `:host` | `String.t()` | `"localhost"` | Hostname or IP of rockboxd | 118 + | `:port` | `non_neg_integer()` | `6062` | GraphQL HTTP/WS port | 119 + | `:http_url` | `String.t()` | `http://{host}:{port}/graphql` | Override the full HTTP URL | 120 + | `:ws_url` | `String.t()` | `ws://{host}:{port}/graphql` | Override the full WebSocket URL | 121 + | `:headers` | `[{String.t(), String.t()}]` | `[]` | Extra HTTP request headers | 122 + | `:timeout` | `non_neg_integer()` | `15_000` | HTTP request timeout (ms) | 123 123 124 124 --- 125 125 ··· 454 454 455 455 ### Event map 456 456 457 - | Event | Payload | 458 - |----------------------|-----------------------------------------------| 459 - | `:track_changed` | `%Rockbox.Track{}` | 460 - | `:status_changed` | `:stopped | :playing | :paused` | 461 - | `:playlist_changed` | `%Rockbox.Playlist{}` | 462 - | `:ws_open` | `nil` | 463 - | `:ws_close` | `nil` | 464 - | `:ws_error` | `Exception.t()` | 457 + | Event | Payload | | | 458 + | ------------------- | --------------------- | -------- | -------- | 459 + | `:track_changed` | `%Rockbox.Track{}` | | | 460 + | `:status_changed` | `:stopped | :playing | :paused` | 461 + | `:playlist_changed` | `%Rockbox.Playlist{}` | | | 462 + | `:ws_open` | `nil` | | | 463 + | `:ws_close` | `nil` | | | 464 + | `:ws_error` | `Exception.t()` | | | 465 465 466 466 Subscribers are auto-removed when their process exits — no manual cleanup 467 467 needed. ··· 598 598 end 599 599 ``` 600 600 601 - | Exception | When raised | 602 - |---------------------------|----------------------------------------------------------| 603 - | `Rockbox.NetworkError` | HTTP request fails or returns a non-2xx status | 604 - | `Rockbox.GraphQLError` | Server returns `{ "errors": [...] }` in the response body | 605 - | `Rockbox.Error` | Base exception — rescue this to catch any SDK failure | 601 + | Exception | When raised | 602 + | ---------------------- | --------------------------------------------------------- | 603 + | `Rockbox.NetworkError` | HTTP request fails or returns a non-2xx status | 604 + | `Rockbox.GraphQLError` | Server returns `{ "errors": [...] }` in the response body | 605 + | `Rockbox.Error` | Base exception — rescue this to catch any SDK failure | 606 606 607 607 --- 608 608 ··· 634 634 635 635 ## Module map 636 636 637 - | Domain | Module | 638 - |-----------------------|---------------------------------| 639 - | Client constructor | `Rockbox`, `Rockbox.Client` | 640 - | Transport controls | `Rockbox.Playback` | 641 - | Library / search | `Rockbox.Library` | 642 - | Live queue | `Rockbox.Queue` | 643 - | Saved playlists | `Rockbox.SavedPlaylists` | 644 - | Smart playlists | `Rockbox.SmartPlaylists` | 645 - | Smart-playlist rules | `Rockbox.SmartPlaylist.Rules` | 646 - | Volume | `Rockbox.Sound` | 647 - | Settings | `Rockbox.Settings` | 648 - | System info | `Rockbox.System` | 649 - | Filesystem browser | `Rockbox.Browse` | 650 - | Output devices | `Rockbox.Devices` | 651 - | Bluetooth | `Rockbox.Bluetooth` | 652 - | Real-time events | `Rockbox.Events` | 653 - | Plugin behaviour | `Rockbox.Plugin`, `Rockbox.Plugins` | 654 - | Errors | `Rockbox.Error`, `Rockbox.NetworkError`, `Rockbox.GraphQLError` | 637 + | Domain | Module | 638 + | -------------------- | --------------------------------------------------------------- | 639 + | Client constructor | `Rockbox`, `Rockbox.Client` | 640 + | Transport controls | `Rockbox.Playback` | 641 + | Library / search | `Rockbox.Library` | 642 + | Live queue | `Rockbox.Queue` | 643 + | Saved playlists | `Rockbox.SavedPlaylists` | 644 + | Smart playlists | `Rockbox.SmartPlaylists` | 645 + | Smart-playlist rules | `Rockbox.SmartPlaylist.Rules` | 646 + | Volume | `Rockbox.Sound` | 647 + | Settings | `Rockbox.Settings` | 648 + | System info | `Rockbox.System` | 649 + | Filesystem browser | `Rockbox.Browse` | 650 + | Output devices | `Rockbox.Devices` | 651 + | Bluetooth | `Rockbox.Bluetooth` | 652 + | Real-time events | `Rockbox.Events` | 653 + | Plugin behaviour | `Rockbox.Plugin`, `Rockbox.Plugins` | 654 + | Errors | `Rockbox.Error`, `Rockbox.NetworkError`, `Rockbox.GraphQLError` | 655 655 656 656 --- 657 657
+15 -15
sdk/elixir/examples/README.md
··· 9 9 10 10 Override the host/port via env: `ROCKBOX_HOST`, `ROCKBOX_PORT`. 11 11 12 - | File | What it shows | 13 - |-----------------------------------|------------------------------------------------| 14 - | `01_basic_playback.exs` | Toggle play/pause based on current status | 15 - | `02_now_playing.exs` | Real-time event subscriptions | 16 - | `03_library_search.exs` | Full-text search across artists/albums/tracks | 17 - | `04_queue_management.exs` | Inspect and modify the live queue | 18 - | `05_saved_playlists.exs` | Persistent named playlists | 19 - | `06_smart_playlist.exs` | Builder DSL for rule-based playlists | 20 - | `07_volume_control.exs` | Volume up/down | 21 - | `08_eq_config.exs` | Equalizer configuration | 22 - | `09_browse_filesystem.exs` | Walk `music_dir` | 23 - | `10_devices.exs` | List Chromecast / AirPlay devices | 24 - | `11_bluetooth.exs` | Bluetooth scan / connect (Linux) | 25 - | `12_plugin_sleep_timer.exs` | Build a plugin with the `Rockbox.Plugin` behaviour | 26 - | `13_raw_query.exs` | Escape hatch for one-off GraphQL queries | 12 + | File | What it shows | 13 + | --------------------------- | -------------------------------------------------- | 14 + | `01_basic_playback.exs` | Toggle play/pause based on current status | 15 + | `02_now_playing.exs` | Real-time event subscriptions | 16 + | `03_library_search.exs` | Full-text search across artists/albums/tracks | 17 + | `04_queue_management.exs` | Inspect and modify the live queue | 18 + | `05_saved_playlists.exs` | Persistent named playlists | 19 + | `06_smart_playlist.exs` | Builder DSL for rule-based playlists | 20 + | `07_volume_control.exs` | Volume up/down | 21 + | `08_eq_config.exs` | Equalizer configuration | 22 + | `09_browse_filesystem.exs` | Walk `music_dir` | 23 + | `10_devices.exs` | List Chromecast / AirPlay devices | 24 + | `11_bluetooth.exs` | Bluetooth scan / connect (Linux) | 25 + | `12_plugin_sleep_timer.exs` | Build a plugin with the `Rockbox.Plugin` behaviour | 26 + | `13_raw_query.exs` | Escape hatch for one-off GraphQL queries | 27 27 28 28 For long-running examples (subscriptions, plugins) use `mix run --no-halt …`.
+37 -37
sdk/gleam/README.md
··· 111 111 |> rockbox.connect 112 112 ``` 113 113 114 - | Setter | Default | Description | 115 - |------------------|-----------------------|----------------------------------------| 116 - | `host(_, value)` | `"localhost"` | Hostname or IP of rockboxd | 117 - | `port(_, value)` | `6062` | GraphQL HTTP port | 114 + | Setter | Default | Description | 115 + | ---------------- | ---------------------- | ------------------------------------------------------ | 116 + | `host(_, value)` | `"localhost"` | Hostname or IP of rockboxd | 117 + | `port(_, value)` | `6062` | GraphQL HTTP port | 118 118 | `url(_, value)` | derived from host/port | Override the full HTTP URL (wins over `host` / `port`) | 119 119 120 120 Use `rockbox.http_url(client)` to read back the resolved URL — handy for ··· 321 321 322 322 #### Operators 323 323 324 - | Variant | Meaning | 325 - |-------------|----------------------------------------| 326 - | `Eq` | equals | 327 - | `Neq` | not equals | 328 - | `Gt` | greater than | 329 - | `Gte` | greater than or equal | 330 - | `Lt` | less than | 331 - | `Lte` | less than or equal | 332 - | `Contains` | substring match | 333 - | `Within` | duration window (e.g. `"30d"`, `"7d"`) | 324 + | Variant | Meaning | 325 + | ---------- | -------------------------------------- | 326 + | `Eq` | equals | 327 + | `Neq` | not equals | 328 + | `Gt` | greater than | 329 + | `Gte` | greater than or equal | 330 + | `Lt` | less than | 331 + | `Lte` | less than or equal | 332 + | `Contains` | substring match | 333 + | `Within` | duration window (e.g. `"30d"`, `"7d"`) | 334 334 335 335 #### OR groups and nesting 336 336 ··· 509 509 } 510 510 ``` 511 511 512 - | Variant | When raised | 513 - |-----------------|----------------------------------------------------------| 514 - | `NetworkError` | DNS, refused connection, TLS, etc. | 515 - | `HttpError` | Server returned a non-2xx HTTP response. | 516 - | `GraphQLError` | Server returned a populated `errors` array. | 517 - | `DecodeError` | Response body could not be decoded into the expected shape. | 512 + | Variant | When raised | 513 + | -------------- | ----------------------------------------------------------- | 514 + | `NetworkError` | DNS, refused connection, TLS, etc. | 515 + | `HttpError` | Server returned a non-2xx HTTP response. | 516 + | `GraphQLError` | Server returned a populated `errors` array. | 517 + | `DecodeError` | Response body could not be decoded into the expected shape. | 518 518 519 519 --- 520 520 ··· 557 557 558 558 ## Module map 559 559 560 - | Domain | Module | 561 - |-----------------------|-----------------------------------------| 562 - | Client constructor | `rockbox` | 563 - | Transport controls | `rockbox/playback` | 564 - | Library / search | `rockbox/library` | 565 - | Live queue | `rockbox/playlist` | 566 - | Saved playlists | `rockbox/saved_playlists` | 567 - | Smart playlists | `rockbox/smart_playlists` | 568 - | Smart-playlist rules | `rockbox/smart_playlists/rules` | 569 - | Volume | `rockbox/sound` | 570 - | Settings | `rockbox/settings` | 571 - | System info | `rockbox/system` | 572 - | Filesystem browser | `rockbox/browse` | 573 - | Output devices | `rockbox/devices` | 574 - | Bluetooth | `rockbox/bluetooth` | 575 - | Domain types | `rockbox/types` | 576 - | Errors | `rockbox/error` | 560 + | Domain | Module | 561 + | -------------------- | ------------------------------- | 562 + | Client constructor | `rockbox` | 563 + | Transport controls | `rockbox/playback` | 564 + | Library / search | `rockbox/library` | 565 + | Live queue | `rockbox/playlist` | 566 + | Saved playlists | `rockbox/saved_playlists` | 567 + | Smart playlists | `rockbox/smart_playlists` | 568 + | Smart-playlist rules | `rockbox/smart_playlists/rules` | 569 + | Volume | `rockbox/sound` | 570 + | Settings | `rockbox/settings` | 571 + | System info | `rockbox/system` | 572 + | Filesystem browser | `rockbox/browse` | 573 + | Output devices | `rockbox/devices` | 574 + | Bluetooth | `rockbox/bluetooth` | 575 + | Domain types | `rockbox/types` | 576 + | Errors | `rockbox/error` | 577 577 578 578 --- 579 579
+15 -15
sdk/gleam/examples/README.md
··· 13 13 browse path, sleep-timer minutes, …) declare a constant near the top of 14 14 the file; tweak it and re-run. 15 15 16 - | File | What it shows | 17 - |-----------------------------------|--------------------------------------------------------| 18 - | `example_01_basic_playback.gleam` | Toggle play/pause based on current status | 19 - | `example_02_now_playing.gleam` | Polling-based current-track watcher | 20 - | `example_03_library_search.gleam` | Full-text search across artists/albums/tracks | 21 - | `example_04_queue_management.gleam` | Inspect and modify the live queue | 22 - | `example_05_saved_playlists.gleam` | Persistent named playlists | 23 - | `example_06_smart_playlist.gleam` | Rule-based smart playlist via the `rules` builder DSL | 24 - | `example_07_volume_control.gleam` | Volume up/down | 25 - | `example_08_eq_config.gleam` | Equalizer configuration via the settings patch builder | 26 - | `example_09_browse_filesystem.gleam` | Walk `music_dir` | 27 - | `example_10_devices.gleam` | List Chromecast / AirPlay devices | 28 - | `example_11_bluetooth.gleam` | Bluetooth scan / connect (Linux) | 29 - | `example_12_sleep_timer.gleam` | Polling-based sleep timer | 30 - | `example_13_raw_query.gleam` | Escape hatch for one-off GraphQL queries | 16 + | File | What it shows | 17 + | ------------------------------------ | ------------------------------------------------------ | 18 + | `example_01_basic_playback.gleam` | Toggle play/pause based on current status | 19 + | `example_02_now_playing.gleam` | Polling-based current-track watcher | 20 + | `example_03_library_search.gleam` | Full-text search across artists/albums/tracks | 21 + | `example_04_queue_management.gleam` | Inspect and modify the live queue | 22 + | `example_05_saved_playlists.gleam` | Persistent named playlists | 23 + | `example_06_smart_playlist.gleam` | Rule-based smart playlist via the `rules` builder DSL | 24 + | `example_07_volume_control.gleam` | Volume up/down | 25 + | `example_08_eq_config.gleam` | Equalizer configuration via the settings patch builder | 26 + | `example_09_browse_filesystem.gleam` | Walk `music_dir` | 27 + | `example_10_devices.gleam` | List Chromecast / AirPlay devices | 28 + | `example_11_bluetooth.gleam` | Bluetooth scan / connect (Linux) | 29 + | `example_12_sleep_timer.gleam` | Polling-based sleep timer | 30 + | `example_13_raw_query.gleam` | Escape hatch for one-off GraphQL queries | 31 31 32 32 ## Differences from the Elixir SDK 33 33
+13 -13
sdk/python/README.md
··· 133 133 134 134 ## Domains 135 135 136 - | Namespace | What it does | 137 - | -------------------------- | ------------------------------------------------------ | 138 - | `client.playback` | Transport (`play`/`pause`/`seek`), play helpers | 139 - | `client.library` | Albums, artists, tracks, search, likes, scan | 140 - | `client.playlist` | The active queue (insert/remove/shuffle/start) | 141 - | `client.saved_playlists` | Persistent playlists & folders | 142 - | `client.smart_playlists` | Rule-based playlists & listening stats | 143 - | `client.sound` | Volume control | 144 - | `client.settings` | Global EQ / replaygain / crossfade / shuffle / … | 145 - | `client.system` | Version, runtime info | 146 - | `client.browse` | Filesystem & UPnP browser | 147 - | `client.devices` | Cast / source device discovery | 148 - | `client.bluetooth` | Bluetooth pairing & scanning (Linux only) | 136 + | Namespace | What it does | 137 + | ------------------------ | ------------------------------------------------ | 138 + | `client.playback` | Transport (`play`/`pause`/`seek`), play helpers | 139 + | `client.library` | Albums, artists, tracks, search, likes, scan | 140 + | `client.playlist` | The active queue (insert/remove/shuffle/start) | 141 + | `client.saved_playlists` | Persistent playlists & folders | 142 + | `client.smart_playlists` | Rule-based playlists & listening stats | 143 + | `client.sound` | Volume control | 144 + | `client.settings` | Global EQ / replaygain / crossfade / shuffle / … | 145 + | `client.system` | Version, runtime info | 146 + | `client.browse` | Filesystem & UPnP browser | 147 + | `client.devices` | Cast / source device discovery | 148 + | `client.bluetooth` | Bluetooth pairing & scanning (Linux only) | 149 149 150 150 ## Real-time events 151 151
+21 -21
sdk/ruby/README.md
··· 129 129 client = Rockbox.new(host: "localhost") 130 130 ``` 131 131 132 - | Option | Type | Default | Description | 133 - |----------------|----------|---------------------------------|--------------------------------------| 134 - | `host` | String | `"localhost"` | Hostname or IP of rockboxd | 135 - | `port` | Integer | `6062` | GraphQL port | 136 - | `http_url` | String | `http://{host}:{port}/graphql` | Override the full HTTP URL | 137 - | `ws_url` | String | `ws://{host}:{port}/graphql` | Override the full WebSocket URL | 138 - | `open_timeout` | Integer | `5` | HTTP connect timeout (seconds) | 139 - | `read_timeout` | Integer | `30` | HTTP read timeout (seconds) | 132 + | Option | Type | Default | Description | 133 + | -------------- | ------- | ------------------------------ | ------------------------------- | 134 + | `host` | String | `"localhost"` | Hostname or IP of rockboxd | 135 + | `port` | Integer | `6062` | GraphQL port | 136 + | `http_url` | String | `http://{host}:{port}/graphql` | Override the full HTTP URL | 137 + | `ws_url` | String | `ws://{host}:{port}/graphql` | Override the full WebSocket URL | 138 + | `open_timeout` | Integer | `5` | HTTP connect timeout (seconds) | 139 + | `read_timeout` | Integer | `30` | HTTP read timeout (seconds) | 140 140 141 141 --- 142 142 ··· 385 385 client.disconnect 386 386 ``` 387 387 388 - | Event | Payload | 389 - |--------------------|--------------------------------------| 390 - | `:track_changed` | `Rockbox::Track` | 391 - | `:status_changed` | `Integer` (`Rockbox::PlaybackStatus`)| 392 - | `:playlist_changed`| `Rockbox::Playlist` | 393 - | `:ws_open` | `nil` | 394 - | `:ws_close` | `nil` | 395 - | `:ws_error` | `Exception` | 388 + | Event | Payload | 389 + | ------------------- | ------------------------------------- | 390 + | `:track_changed` | `Rockbox::Track` | 391 + | `:status_changed` | `Integer` (`Rockbox::PlaybackStatus`) | 392 + | `:playlist_changed` | `Rockbox::Playlist` | 393 + | `:ws_open` | `nil` | 394 + | `:ws_close` | `nil` | 395 + | `:ws_error` | `Exception` | 396 396 397 397 --- 398 398 ··· 441 441 end 442 442 ``` 443 443 444 - | Class | Raised when… | 445 - |---------------------------|----------------------------------------------| 446 - | `Rockbox::Error` | Base class for every SDK error. | 447 - | `Rockbox::NetworkError` | rockboxd is unreachable / non-2xx response. | 448 - | `Rockbox::GraphQLError` | rockboxd returns a GraphQL `errors` payload. | 444 + | Class | Raised when… | 445 + | ----------------------- | -------------------------------------------- | 446 + | `Rockbox::Error` | Base class for every SDK error. | 447 + | `Rockbox::NetworkError` | rockboxd is unreachable / non-2xx response. | 448 + | `Rockbox::GraphQLError` | rockboxd returns a GraphQL `errors` payload. | 449 449 450 450 --- 451 451
+25 -25
sdk/typescript/README.md
··· 94 94 }); 95 95 ``` 96 96 97 - | Option | Type | Default | Description | 98 - |-----------|----------|--------------------------------|-----------------------------------------------------| 99 - | `host` | `string` | `"localhost"` | Hostname or IP of rockboxd | 100 - | `port` | `number` | `6062` | GraphQL port (env: `ROCKBOX_GRAPHQL_PORT`) | 101 - | `httpUrl` | `string` | `http://{host}:{port}/graphql` | Override the full HTTP URL | 102 - | `wsUrl` | `string` | `ws://{host}:{port}/graphql` | Override the full WebSocket URL | 97 + | Option | Type | Default | Description | 98 + | --------- | -------- | ------------------------------ | ------------------------------------------ | 99 + | `host` | `string` | `"localhost"` | Hostname or IP of rockboxd | 100 + | `port` | `number` | `6062` | GraphQL port (env: `ROCKBOX_GRAPHQL_PORT`) | 101 + | `httpUrl` | `string` | `http://{host}:{port}/graphql` | Override the full HTTP URL | 102 + | `wsUrl` | `string` | `ws://{host}:{port}/graphql` | Override the full WebSocket URL | 103 103 104 104 --- 105 105 ··· 321 321 await client.playlist.resume(); 322 322 ``` 323 323 324 - | `InsertPosition` | Value | Effect | 325 - |------------------|-------|------------------------------------------| 326 - | `Next` | `0` | After the currently playing track | 327 - | `AfterCurrent` | `1` | After the last manually inserted track | 328 - | `Last` | `2` | At the end of the queue | 329 - | `First` | `3` | Replace the entire queue | 324 + | `InsertPosition` | Value | Effect | 325 + | ---------------- | ----- | -------------------------------------- | 326 + | `Next` | `0` | After the currently playing track | 327 + | `AfterCurrent` | `1` | After the last manually inserted track | 328 + | `Last` | `2` | At the end of the queue | 329 + | `First` | `3` | Replace the entire queue | 330 330 331 331 --- 332 332 ··· 669 669 670 670 ### Event map 671 671 672 - | Event | Payload | Description | 673 - |--------------------|------------|---------------------------------------| 674 - | `track:changed` | `Track` | Currently playing track changed | 675 - | `status:changed` | `number` | Playback status changed (0 / 1 / 2) | 676 - | `playlist:changed` | `Playlist` | Active queue was modified | 677 - | `ws:error` | `Error` | WebSocket or subscription error | 678 - | `ws:open` | — | WebSocket connection established | 679 - | `ws:close` | — | WebSocket connection closed | 672 + | Event | Payload | Description | 673 + | ------------------ | ---------- | ----------------------------------- | 674 + | `track:changed` | `Track` | Currently playing track changed | 675 + | `status:changed` | `number` | Playback status changed (0 / 1 / 2) | 676 + | `playlist:changed` | `Playlist` | Active queue was modified | 677 + | `ws:error` | `Error` | WebSocket or subscription error | 678 + | `ws:open` | — | WebSocket connection established | 679 + | `ws:close` | — | WebSocket connection closed | 680 680 681 681 --- 682 682 ··· 846 846 } 847 847 ``` 848 848 849 - | Class | When thrown | 850 - |------------------------|----------------------------------------------------------| 851 - | `RockboxNetworkError` | `fetch` rejects or HTTP status is not 2xx | 852 - | `RockboxGraphQLError` | Server returns `{ errors: [...] }` in the response body | 853 - | `RockboxError` | Base class — catch to handle all SDK errors | 849 + | Class | When thrown | 850 + | --------------------- | ------------------------------------------------------- | 851 + | `RockboxNetworkError` | `fetch` rejects or HTTP status is not 2xx | 852 + | `RockboxGraphQLError` | Server returns `{ errors: [...] }` in the response body | 853 + | `RockboxError` | Base class — catch to handle all SDK errors | 854 854 855 855 --- 856 856
+17 -17
sdk/typescript/examples/README.md
··· 30 30 31 31 ## Index 32 32 33 - | File | Demonstrates | 34 - |---------------------------------------|---------------------------------------------------------| 35 - | `01-basic-playback.ts` | Status, transport controls, current track | 36 - | `02-now-playing.ts` | Real-time WebSocket subscriptions | 37 - | `03-library-search.ts` | Search the library and play results | 38 - | `04-queue-management.ts` | Inspect and manipulate the playback queue | 39 - | `05-saved-playlists.ts` | Create, edit, and play saved playlists | 40 - | `06-smart-playlist.ts` | Build smart playlists from rule sets | 41 - | `07-volume-control.ts` | Read `VolumeInfo` and adjust relative volume | 42 - | `08-eq-config.ts` | Configure the equalizer and replaygain | 43 - | `09-browse-filesystem.ts` | Walk `music_dir` like a tree | 44 - | `10-browse-upnp.ts` | Discover and browse UPnP media servers | 45 - | `11-bluetooth.ts` | Scan, connect, and disconnect Bluetooth devices (Linux) | 46 - | `12-devices.ts` | List and switch Chromecast / AirPlay output sinks | 47 - | `13-plugin-sleep-timer.ts` | Plugin: stop playback after N minutes | 48 - | `14-plugin-scrobbler.ts` | Plugin: log every fully-played track | 49 - | `15-cli-remote.ts` | Tiny interactive remote control in the terminal | 33 + | File | Demonstrates | 34 + | -------------------------- | ------------------------------------------------------- | 35 + | `01-basic-playback.ts` | Status, transport controls, current track | 36 + | `02-now-playing.ts` | Real-time WebSocket subscriptions | 37 + | `03-library-search.ts` | Search the library and play results | 38 + | `04-queue-management.ts` | Inspect and manipulate the playback queue | 39 + | `05-saved-playlists.ts` | Create, edit, and play saved playlists | 40 + | `06-smart-playlist.ts` | Build smart playlists from rule sets | 41 + | `07-volume-control.ts` | Read `VolumeInfo` and adjust relative volume | 42 + | `08-eq-config.ts` | Configure the equalizer and replaygain | 43 + | `09-browse-filesystem.ts` | Walk `music_dir` like a tree | 44 + | `10-browse-upnp.ts` | Discover and browse UPnP media servers | 45 + | `11-bluetooth.ts` | Scan, connect, and disconnect Bluetooth devices (Linux) | 46 + | `12-devices.ts` | List and switch Chromecast / AirPlay output sinks | 47 + | `13-plugin-sleep-timer.ts` | Plugin: stop playback after N minutes | 48 + | `14-plugin-scrobbler.ts` | Plugin: log every fully-played track | 49 + | `15-cli-remote.ts` | Tiny interactive remote control in the terminal | 50 50 51 51 Each example is self-contained — pick the one closest to what you need, copy 52 52 it into your project, and adapt.