feat(audio): experimental speaker rate-trim to chase the crackle (#7)

The DS5 DAC and the USB host audio clock are independent ~48kHz crystals; the residual ppm drift slowly underruns the controller's audio buffer = the periodic crackle. The speaker path is 1:1 (no resampler), so it delivered exactly the host's 48kHz; this adds a fine rate trim that delivers 48000+(trim) samples/s via a zero-order-hold duplicate/drop accumulator to null the drift.

New speaker_rate_trim config field (stored 0..200 = -100..+100 Hz, default 100 = 0 Hz = exact no-op since 48000/48000 is exact in double). Swept on the OLED Settings screen (new 'SpkTrim' item) with the D-pad <</>> at 1 Hz/step. Reset/Wipe items shifted to idx 16/17 (named constants, action handlers auto-follow).

Diagnostic intent: if one value silences the crackle and HOLDS, drift was a static offset and this is the fix; if it nulls then creeps back, that proves dynamic drift needing adaptive resampling. Tune with scripts/sine_ch12.py + time the crackle interval.

NOT YET BUILT OR HIL-TESTED. Experimental branch only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MarcelineVPQ
2026-06-04 19:30:43 -06:00
co-authored by Claude Opus 4.8
parent 878a742bfc
commit 3b157cb9a1
5 changed files with 62 additions and 17 deletions
+4
View File
@@ -121,6 +121,10 @@ void config_valid() {
body->controller_wakes_display = 1;
printf("[Config] controller_wakes_display invalid, defaulting to 1 (on)\n");
}
if (body->speaker_rate_trim > 200) { // 0xFF erased / upgrade → 0 Hz (centered)
body->speaker_rate_trim = 100; // 100 encodes 0 Hz; actual trim = stored - 100
printf("[Config] speaker_rate_trim invalid, defaulting to 100 (0 Hz)\n");
}
if (body->config_version != CONFIG_VERSION) {
body->config_version = CONFIG_VERSION;
printf("[Config] Warning: Config may breaking change\n");