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.

Add slim README for squeezelite PCM sink

+593
+593
crates/slim/README.md
··· 1 + # rockbox-slim — Squeezelite PCM sink internals 2 + 3 + This document traces the full path of a PCM audio frame from the Rockbox C 4 + firmware through the Slim Protocol and HTTP broadcast layer to one or more 5 + squeezelite instances. 6 + 7 + --- 8 + 9 + ## Table of contents 10 + 11 + 1. [Overview](#overview) 12 + 2. [Architecture diagram](#architecture-diagram) 13 + 3. [Layer 1 — PCM sink abstraction (C)](#layer-1--pcm-sink-abstraction-c) 14 + 4. [Layer 2 — DMA thread and real-time pacing (C)](#layer-2--dma-thread-and-real-time-pacing-c) 15 + 5. [Layer 3 — FFI boundary](#layer-3--ffi-boundary) 16 + 6. [Layer 4 — Broadcast buffer (Rust)](#layer-4--broadcast-buffer-rust) 17 + 7. [Layer 5 — HTTP stream server (Rust)](#layer-5--http-stream-server-rust) 18 + 8. [Layer 6 — Slim Protocol server (Rust)](#layer-6--slim-protocol-server-rust) 19 + 9. [Layer 7 — squeezelite client](#layer-7--squeezelite-client) 20 + 10. [Startup sequence](#startup-sequence) 21 + 11. [Track transition](#track-transition) 22 + 12. [Multi-room](#multi-room) 23 + 13. [Configuration](#configuration) 24 + 14. [Gotchas and non-obvious invariants](#gotchas-and-non-obvious-invariants) 25 + 26 + --- 27 + 28 + ## Overview 29 + 30 + The squeezelite sink turns rockboxd into a minimal Logitech Media Server (LMS) 31 + that speaks just enough of the Slim Protocol to command squeezelite clients to 32 + fetch a continuous raw PCM stream over HTTP. 33 + 34 + ``` 35 + rockboxd squeezelite 36 + ───────── ─────────── 37 + Codec → PCM engine audio device (CoreAudio / ALSA / …) 38 + │ ▲ 39 + pcm-squeezelite.c (C) │ 40 + │ │ 41 + pcm_squeezelite_write() [HTTP audio stream] 42 + │ │ 43 + BroadcastBuffer (Rust) ←── pcm_squeezelite_write() pushes chunks 44 + 45 + ├── HTTP server :9999 ─── one thread per squeezelite client 46 + 47 + └── Slim server :3483 ─── sends STRM, keeps connection alive 48 + ``` 49 + 50 + The PCM data is **never transcoded** — rockboxd pushes raw signed 16-bit 51 + little-endian stereo (`S16LE`) at 44 100 Hz, squeezelite's built-in `pcm` 52 + codec copies it straight to the audio device. 53 + 54 + --- 55 + 56 + ## Architecture diagram 57 + 58 + ``` 59 + ┌─────────────────────────────────────────────────────────────────┐ 60 + │ Rockbox firmware (C) │ 61 + │ │ 62 + │ codec thread ──► PCM buffer ──► pcm_play_dma_complete_callback │ 63 + │ │ │ 64 + │ squeezelite_thread (pthread) │ 65 + │ │ │ 66 + │ pcm_squeezelite_write() ◄── FFI │ 67 + └─────────────────────────────────────────────────────────────────┘ 68 + 69 + ══════════════╪══════════ FFI boundary 70 + 71 + ┌─────────────────────────────────────────────────────────────────┐ 72 + │ rockbox-slim crate (Rust) │ 73 + │ │ 74 + │ BroadcastBuffer │ 75 + │ ┌──────────────────────────────────────────────────────────┐ │ 76 + │ │ VecDeque<(seq: u64, chunk: Vec<u8>)> max 4 MB │ │ 77 + │ │ next_seq (writer cursor) │ │ 78 + │ └──────────────────────────────────────────────────────────┘ │ 79 + │ │ │ │ 80 + │ HTTP server :9999 Slim Protocol server :3483 │ 81 + │ (one thread per client) (one thread per client) │ 82 + │ │ │ │ 83 + │ BroadcastReceiver HELO → STRM → audg keepalive │ 84 + │ (per-client seq cursor) │ 85 + └────────────────────────────┬────────────────────────────────────┘ 86 + │ TCP :9999 87 + ┌──────────────┼──────────────┐ 88 + ▼ ▼ ▼ 89 + squeezelite squeezelite squeezelite 90 + "Room 1" "Room 2" "Room 3" 91 + ``` 92 + 93 + --- 94 + 95 + ## Layer 1 — PCM sink abstraction (C) 96 + 97 + **Files:** `firmware/export/pcm_sink.h`, `firmware/pcm.c` 98 + 99 + Rockbox's hosted audio output is built around `struct pcm_sink`, a vtable of 100 + seven operations: 101 + 102 + ```c 103 + struct pcm_sink_ops { 104 + void (*init)(void); 105 + void (*postinit)(void); 106 + void (*set_freq)(uint16_t freq); // called when sample rate changes 107 + void (*lock)(void); 108 + void (*unlock)(void); 109 + void (*play)(const void *addr, size_t size); // sink_dma_start 110 + void (*stop)(void); // sink_dma_stop 111 + }; 112 + ``` 113 + 114 + The squeezelite sink is registered as `PCM_SINK_SQUEEZELITE = 3`: 115 + 116 + ```c 117 + // firmware/export/pcm_sink.h 118 + enum pcm_sink_ids { 119 + PCM_SINK_BUILTIN = 0, // SDL audio 120 + PCM_SINK_FIFO = 1, // named FIFO → Snapcast 121 + PCM_SINK_AIRPLAY = 2, // RAOP/RTP → AirPlay receiver 122 + PCM_SINK_SQUEEZELITE = 3, // Slim Protocol + HTTP → squeezelite 123 + PCM_SINK_NUM 124 + }; 125 + ``` 126 + 127 + `pcm_switch_sink(PCM_SINK_SQUEEZELITE)` (called from `crates/settings`) swaps 128 + the active vtable. All subsequent `pcm_play_data()` calls drive the 129 + squeezelite sink. 130 + 131 + --- 132 + 133 + ## Layer 2 — DMA thread and real-time pacing (C) 134 + 135 + **File:** `firmware/target/hosted/pcm-squeezelite.c` 136 + 137 + ### sink_dma_start / sink_dma_stop 138 + 139 + `sink_dma_start(addr, size)` is called by the Rockbox PCM engine whenever a 140 + new track (or track segment) begins. It: 141 + 142 + 1. Calls `pcm_squeezelite_start()` — idempotent; starts the Slim and HTTP 143 + servers on first call only. 144 + 2. Captures `play_start = CLOCK_MONOTONIC` and resets `play_bytes = 0` to 145 + anchor real-time pacing. 146 + 3. Stores the initial `(addr, size)` buffer pointer. 147 + 4. Spawns `squeezelite_thread`. 148 + 149 + `sink_dma_stop()` sets `squeezelite_stop = true`, joins the thread, and calls 150 + the no-op `pcm_squeezelite_stop()`. 151 + 152 + ### squeezelite_thread — the DMA loop 153 + 154 + The thread mimics a hardware DMA interrupt loop in software: 155 + 156 + ``` 157 + loop: 158 + 1. Grab current (data, size) under mutex, clear pcm_data/pcm_size 159 + 2. pcm_squeezelite_write(data, size) → push to BroadcastBuffer 160 + 3. Real-time pace (sleep if ahead of wall clock) 161 + 4. pcm_play_dma_complete_callback() → get next (pcm_data, pcm_size) 162 + 5. pcm_play_dma_status_callback(STARTED) 163 + 6. goto loop 164 + ``` 165 + 166 + `pcm_play_dma_complete_callback()` is the hook Rockbox uses to advance the 167 + codec's read pointer — calling it at the wrong rate directly affects the 168 + playback-position counter shown in the UI. 169 + 170 + ### Real-time pacing 171 + 172 + Without pacing, the loop drains an entire track in milliseconds (the ring 173 + buffer write is a memory copy). Pacing is implemented with a running byte 174 + counter: 175 + 176 + ```c 177 + play_bytes += size; 178 + uint64_t bps = (uint64_t)current_sample_rate * 4; // S16LE stereo 179 + uint64_t expected_us = play_bytes * 1000000ULL / bps; 180 + 181 + struct timespec now; 182 + clock_gettime(CLOCK_MONOTONIC, &now); 183 + 184 + // IMPORTANT: use int64_t, not uint64_t. 185 + // tv_nsec subtraction wraps to ~10^19 when tv_nsec rolls over, 186 + // making elapsed_us astronomically large and disabling all sleeps. 187 + int64_t elapsed_us = 188 + (int64_t)(now.tv_sec - play_start.tv_sec) * 1000000LL + 189 + ((int64_t)now.tv_nsec - (int64_t)play_start.tv_nsec) / 1000LL; 190 + 191 + if (elapsed_us >= 0 && expected_us > (uint64_t)elapsed_us) 192 + usleep((useconds_t)(expected_us - (uint64_t)elapsed_us)); 193 + ``` 194 + 195 + `play_start` and `play_bytes` are **reset on every `sink_dma_start()`** (i.e. 196 + every track), so drift does not accumulate across tracks. 197 + 198 + --- 199 + 200 + ## Layer 3 — FFI boundary 201 + 202 + **Files:** `crates/sys/src/lib.rs`, `crates/sys/src/sound/pcm.rs` 203 + 204 + The C symbols exported by `pcm-squeezelite.c` are declared as `extern "C"` in 205 + `crates/sys/src/lib.rs` and wrapped with safe Rust helpers in 206 + `crates/sys/src/sound/pcm.rs`: 207 + 208 + | C symbol | Rust wrapper | 209 + |--------------------------------------|------------------------------------------| 210 + | `pcm_squeezelite_set_slim_port(u16)` | `pcm::squeezelite_set_slim_port(u16)` | 211 + | `pcm_squeezelite_set_http_port(u16)` | `pcm::squeezelite_set_http_port(u16)` | 212 + | `pcm_switch_sink(i32)` | `pcm::switch_sink(PCM_SINK_SQUEEZELITE)` | 213 + 214 + These wrappers are called from `crates/settings/src/lib.rs:load_settings()` 215 + when `audio_output = "squeezelite"` is found in `settings.toml`. 216 + 217 + ### Force-link shim 218 + 219 + `rockbox-slim` is an `rlib`, not a `staticlib`. The Rust linker would dead- 220 + strip it unless something directly references its symbols. A dummy function 221 + in `crates/cli/src/lib.rs` forces all FFI exports into `librockbox_cli.a`: 222 + 223 + ```rust 224 + // crates/cli/src/lib.rs 225 + #[allow(unused_imports)] 226 + use rockbox_slim::_link_slim as _; 227 + ``` 228 + 229 + ```rust 230 + // crates/slim/src/lib.rs 231 + #[doc(hidden)] 232 + pub fn _link_slim() {} 233 + ``` 234 + 235 + --- 236 + 237 + ## Layer 4 — Broadcast buffer (Rust) 238 + 239 + **File:** `crates/slim/src/lib.rs` 240 + 241 + ### Design 242 + 243 + The `BroadcastBuffer` stores PCM chunks as `(seq: u64, data: Vec<u8>)` tuples 244 + in a `VecDeque`. The writer holds a single monotonically-increasing 245 + `next_seq` cursor. Each reader (`BroadcastReceiver`) holds its own 246 + independent `next_seq` cursor. 247 + 248 + ``` 249 + write cursor ──────────────────────────────────────────► next_seq 250 + 251 + chunks: [(100,…), (101,…), (102,…), (103,…), (104,…)] │ 252 + ▲ ▲ ▲ │ 253 + │ │ │ │ 254 + reader C reader A reader B │ 255 + (lagging) (live) (live) │ 256 + ``` 257 + 258 + ### Push (writer) 259 + 260 + ```rust 261 + pub(crate) fn push(&self, data: &[u8]) { 262 + // assign sequence number, append chunk 263 + // evict oldest chunks while total_bytes > MAX_BUFFERED (4 MB) 264 + // notify all waiting receivers 265 + } 266 + ``` 267 + 268 + Eviction happens from the front; the `next_seq` counter is never reset, so 269 + existing receivers automatically detect that they have fallen behind. 270 + 271 + ### Subscribe (new reader) 272 + 273 + ```rust 274 + pub(crate) fn subscribe(self: &Arc<Self>) -> BroadcastReceiver { 275 + // start at the current write cursor — live stream, no stale data 276 + } 277 + ``` 278 + 279 + ### recv_blocking (reader) 280 + 281 + ```rust 282 + pub(crate) fn recv_blocking(&mut self) -> RecvResult { 283 + loop { 284 + // 1. If receiver is behind front_seq → skip forward (log warning) 285 + // 2. If chunk is available → return it, advance next_seq 286 + // 3. If closed → return Closed 287 + // 4. Otherwise → condvar.wait (release lock, sleep) 288 + } 289 + } 290 + ``` 291 + 292 + The index into the `VecDeque` is computed as 293 + `(self.next_seq - front_seq) as usize`, which is O(1). 294 + 295 + ### Capacity and eviction 296 + 297 + The buffer is capped at **4 MB ≈ 23 seconds** of S16LE stereo at 44 100 Hz. 298 + Chunks are evicted oldest-first when the cap is exceeded. A lagging reader 299 + whose `next_seq` points to an evicted chunk silently skips forward to the 300 + oldest available chunk — it experiences a brief audio discontinuity but does 301 + not block the writer or other readers. 302 + 303 + --- 304 + 305 + ## Layer 5 — HTTP stream server (Rust) 306 + 307 + **File:** `crates/slim/src/http.rs` 308 + 309 + The HTTP server accepts connections on `0.0.0.0:9999` (configurable). Each 310 + accepted connection is handled in a **dedicated thread**: 311 + 312 + ``` 313 + listener.incoming() loop 314 + 315 + └── thread::spawn per connection 316 + 317 + ├── drain_request() — read & discard HTTP request headers 318 + ├── write HTTP 200 response headers 319 + ├── buf.subscribe() — get a fresh BroadcastReceiver 320 + └── loop { recv_blocking() → write_all() } 321 + ``` 322 + 323 + ### Response headers 324 + 325 + ``` 326 + HTTP/1.0 200 OK 327 + Content-Type: audio/L16;rate=44100;channels=2 328 + Cache-Control: no-cache 329 + ``` 330 + 331 + `audio/L16` is the MIME type for raw signed 16-bit PCM. squeezelite ignores 332 + the Content-Type and uses the codec declared in the `STRM` command (`p` = 333 + raw PCM), but the header documents the wire format. 334 + 335 + ### Back-pressure 336 + 337 + squeezelite reads from the TCP socket at exactly the rate its audio device 338 + consumes PCM (176 400 bytes/s at 44 100 Hz stereo 16-bit). The OS TCP send 339 + buffer provides natural back-pressure: if squeezelite is slow, `write_all()` 340 + blocks, which causes `recv_blocking()` to hold the lock longer, which in turn 341 + causes the C DMA thread to block in `pcm_squeezelite_write()` — naturally 342 + slowing the DMA loop. 343 + 344 + --- 345 + 346 + ## Layer 6 — Slim Protocol server (Rust) 347 + 348 + **File:** `crates/slim/src/slimproto.rs` 349 + 350 + The Slim Protocol server listens on `0.0.0.0:3483` (configurable). Each 351 + squeezelite client gets its own thread. 352 + 353 + ### Packet framing 354 + 355 + The Slim Protocol uses **asymmetric framing**: 356 + 357 + | Direction | Wire format | 358 + |-----------------|-----------------------------------------------------| 359 + | Client → Server | `opcode[4]` + `u32 length BE` + `payload[length]` | 360 + | Server → Client | `u16 length BE` + `opcode[4]` + `payload[length-4]` | 361 + 362 + The `u16 length` field in server→client packets counts the opcode (4 bytes) 363 + plus the payload, but **not the 2-byte length field itself**. 364 + 365 + ### Session flow 366 + 367 + ``` 368 + squeezelite Slim server 369 + ─────────── ─────────── 370 + HELO ──────────────────────────────► 371 + (no kick — all clients share the stream) 372 + ◄────────────────────────── STRM 's' 373 + HTTP GET :9999/stream.pcm ─────────────────────────────► HTTP server 374 + ◄────────────────────── HTTP 200 + raw PCM stream 375 + STAT STMc ─────────────────────────► 376 + STAT STMs ─────────────────────────► 377 + STAT STMt ─────────────────────────► (every ~1 s) 378 + ◄─────────────────────────── audg (full volume) ← keepalive 379 + STAT STMt ─────────────────────────► 380 + ◄─────────────────────────── audg 381 + 382 + ``` 383 + 384 + ### STRM 's' payload layout 385 + 386 + The `STRM` packet instructs squeezelite where and how to fetch audio. All 387 + index fields are **ASCII-encoded**: squeezelite subtracts `'0'` before use. 388 + 389 + ``` 390 + Offset Size Value Meaning 391 + ────── ──── ────── ─────────────────────────────────────────────── 392 + 0 1 's' command: start 393 + 1 1 '1' autostart: 1 = buffer then play immediately 394 + 2 1 'p' format: raw PCM 395 + 3 1 '1' pcm_sample_size: '1'-'0'+1 = 2 bytes = 16-bit 396 + 4 1 '3' pcm_sample_rate: sample_rates[3] = 44 100 Hz 397 + 5 1 '2' pcm_channels: '2'-'0' = 2 (stereo) 398 + 6 1 '1' pcm_endianness: '1'≠'0' → little-endian 399 + 7 1 255 threshold: 255 KB before auto-start 400 + 8 1 0 spdif_enable 401 + 9 1 0 transition_period 402 + 10 1 '0' transition_type: none 403 + 11 1 0 flags 404 + 12 1 0 output_threshold 405 + 13 1 0 slaves 406 + 14 4 BE u32 replay_gain = 0x00010000 (1.0 in 16.16 fixed-point) 407 + 18 2 BE u16 server_port (HTTP audio port) 408 + 20 4 BE u32 server_ip = 0 → squeezelite uses the Slim server IP 409 + 24 * bytes "GET /stream.pcm HTTP/1.0\r\n\r\n" 410 + ``` 411 + 412 + `server_ip = 0` is a squeezelite convention: when the IP is zero, squeezelite 413 + uses the IP address it connected to for Slim Protocol. This means the HTTP 414 + server must be on the same host as the Slim server. 415 + 416 + ### audg keepalive 417 + 418 + squeezelite counts consecutive 1-second `select()` timeouts on the Slim 419 + socket. After **36 consecutive timeouts** with no data from the server it 420 + logs `"No messages from server — connection dead"` and reconnects. 421 + 422 + The fix: reply to every `STMt` (timer tick, sent by squeezelite once per 423 + second) with an `audg` (audio gain) packet: 424 + 425 + ``` 426 + Offset Size Value Meaning 427 + ────── ──── ────────── ─────────────────────────────────────── 428 + 0 4 0x00010000 left gain = 1.0 (16.16 BE fixed-point) 429 + 4 4 0x00010000 right gain = 1.0 430 + 8 1 0 do not adjust by replay gain 431 + ``` 432 + 433 + This resets squeezelite's timeout counter to zero on every tick. 434 + 435 + --- 436 + 437 + ## Layer 7 — squeezelite client 438 + 439 + squeezelite handles the audio pipeline in three internal threads: 440 + 441 + | Thread | Role | 442 + |--------------------------------------|---------------------------------------------------------------| 443 + | `stream_thread` | Reads HTTP stream into `streambuf` (2 MB ring buffer) | 444 + | `decode_thread` | PCM "decoder": memcpy from `streambuf` → `outputbuf` (3.5 MB) | 445 + | `output_thread` / PortAudio callback | Reads `outputbuf`, sends to audio device | 446 + 447 + For raw PCM (`format = 'p'`), the decode step is a straight memory copy with 448 + no transcoding, so latency between push and playback is determined solely by 449 + squeezelite's buffer thresholds (default ~255 KB ≈ 1.4 seconds at 44 100 Hz). 450 + 451 + --- 452 + 453 + ## Startup sequence 454 + 455 + ``` 456 + rockboxd starts 457 + 458 + └── load_settings() reads audio_output = "squeezelite" 459 + 460 + ├── pcm::squeezelite_set_slim_port(3483) 461 + ├── pcm::squeezelite_set_http_port(9999) 462 + └── pcm::switch_sink(PCM_SINK_SQUEEZELITE) 463 + 464 + └── swaps active pcm_sink vtable 465 + 466 + user plays a track 467 + 468 + └── sink_dma_start(addr, size) [pcm-squeezelite.c] 469 + 470 + ├── pcm_squeezelite_start() [lib.rs — idempotent] 471 + │ ├── spawn HTTP server thread on :9999 472 + │ └── spawn Slim server thread on :3483 473 + 474 + ├── reset play_start, play_bytes 475 + └── spawn squeezelite_thread 476 + 477 + squeezelite -s localhost 478 + 479 + ├── TCP connect :3483 480 + ├── send HELO 481 + ├── receive STRM 's' → TCP connect :9999 482 + ├── receive HTTP 200 483 + └── start buffering PCM 484 + ``` 485 + 486 + --- 487 + 488 + ## Track transition 489 + 490 + Between tracks, rockboxd calls `sink_dma_stop()` then `sink_dma_start()`: 491 + 492 + 1. `sink_dma_stop()`: sets `squeezelite_stop = true`, joins the DMA thread, 493 + clears `pcm_data`/`pcm_size`. The broadcast buffer is **not flushed** — 494 + squeezelite's in-flight HTTP connection continues reading. 495 + 496 + 2. `sink_dma_start()`: `pcm_squeezelite_start()` is a no-op (already started). 497 + Resets `play_start`/`play_bytes`, spawns a new DMA thread. 498 + 499 + The gap between stop and start is a few milliseconds. squeezelite's 500 + `outputbuf` (≈ 20 s capacity) absorbs the gap without a dropout. 501 + 502 + --- 503 + 504 + ## Multi-room 505 + 506 + Any number of squeezelite instances can connect simultaneously: 507 + 508 + - Each opens a Slim connection (`:3483`) and receives an identical `STRM 's'` 509 + command pointing at `:9999`. 510 + - Each opens an HTTP connection (`:9999`) and gets an independent 511 + `BroadcastReceiver` starting at the current write cursor. 512 + - Chunks flow from the single writer to all readers independently. 513 + - A slow reader skips forward to the oldest available chunk; other readers 514 + are unaffected. 515 + 516 + Squeezelite clients are not time-synchronised (no NTP/PTP layer). Clock drift 517 + between rooms is typically 100–500 ms. For tighter sync, run squeezelite with 518 + a Snapcast-compatible back-end or use the FIFO sink instead. 519 + 520 + --- 521 + 522 + ## Configuration 523 + 524 + `~/.config/rockbox.org/settings.toml`: 525 + 526 + ```toml 527 + audio_output = "squeezelite" 528 + squeezelite_port = 3483 # Slim Protocol TCP port (default) 529 + squeezelite_http_port = 9999 # HTTP PCM stream port (default) 530 + ``` 531 + 532 + To select a specific audio device in squeezelite: 533 + 534 + ```sh 535 + squeezelite -s localhost -l # list available devices 536 + squeezelite -s localhost -o "" # system default 537 + squeezelite -s localhost -o "Built-in Output" 538 + ``` 539 + 540 + Debug logging: 541 + 542 + ```sh 543 + RUST_LOG=debug ./zig/zig-out/bin/rockboxd # rockboxd side 544 + squeezelite -s localhost -d all=debug # squeezelite side 545 + ``` 546 + 547 + --- 548 + 549 + ## Gotchas and non-obvious invariants 550 + 551 + ### 1. `int64_t` for the pacing diff — never `uint64_t` 552 + 553 + `struct timespec::tv_nsec` is `long`. `(uint64_t)(now.tv_nsec - prev.tv_nsec)` 554 + wraps to ≈ 1.8 × 10¹⁹ when `tv_nsec` rolls over (once per second). This 555 + makes `elapsed_us` enormous, disables all `usleep()` calls for the rest of the 556 + track, and causes the DMA loop to drain PCM at CPU speed — advancing the 557 + playback position many times faster than real time. Always use `int64_t` for 558 + the subtraction. 559 + 560 + ### 2. squeezelite's 36-second watchdog 561 + 562 + squeezelite counts consecutive 1-second `select()` timeouts. Without an 563 + `audg` keepalive, it declares the connection dead after 36 seconds and 564 + reconnects. The reconnect is seamless but causes a brief gap in the HTTP 565 + stream. 566 + 567 + ### 3. ASCII-encoded STRM fields 568 + 569 + `pcm_sample_size`, `pcm_sample_rate`, `pcm_channels`, and `pcm_endianness` in 570 + the `STRM` payload are ASCII characters, not binary integers. squeezelite 571 + subtracts `'0'` (0x30) before use. Sending `0x03` instead of `'3'` (0x33) 572 + for the sample rate would silently select the wrong rate. 573 + 574 + ### 4. `server_ip = 0` in STRM 575 + 576 + squeezelite interprets `server_ip = 0` as "use the IP I connected to for Slim 577 + Protocol". The HTTP server must therefore be reachable on the same host as 578 + the Slim server. A non-zero `server_ip` would make squeezelite connect to 579 + that explicit IP instead. 580 + 581 + ### 5. Force-link shim 582 + 583 + `rockbox-slim` is an `rlib`. Without `use rockbox_slim::_link_slim as _` in 584 + `crates/cli/src/lib.rs`, the linker discards all its symbols and the 585 + `extern "C"` declarations in `pcm-squeezelite.c` produce undefined-reference 586 + errors at link time. 587 + 588 + ### 6. `next_seq` is never reset 589 + 590 + `BroadcastBuffer::reset()` clears the chunk queue but does **not** reset 591 + `next_seq`. This ensures that receivers created before the reset 592 + automatically skip forward to newly pushed chunks rather than getting stuck 593 + waiting for sequence numbers that will never appear.