Files
DS5Dongle-OLED-Edition-stea…/HANDOFF.md
MarcelineVPQandClaude Opus 4.8 6e7942ec40 docs: add HANDOFF.md — cross-machine pickup for the audio work
Captures branch map, the SpkTrim sweep-test protocol, open threads (#10/#11/defaults soak-test), why DS5 BT audio is hard, gotchas, and a one-paste kickoff for a fresh session. Written so the main-PC session can resume from git alone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:36:04 -06:00

7.8 KiB
Raw Permalink Blame History

HANDOFF — audio work in progress

Last updated: 2026-06-04 (written on the laptop; continuing on the main PC)

This is a working handoff so the next session — on the main PC, or a fresh Claude Code session — can pick up exactly where we left off. All the real work is committed and pushed to origin; nothing is stuck on the laptop. This file lives on the audio/speaker-rate-trim branch, so after git checkout audio/speaker-rate-trim it's right here.


TL;DR

  • The frustration that kicked this off: audio (speaker/mic/HD haptics) is the priority, not OLED features. Goal: make the dongle behave as close to a wired-USB DualSense as possible, with flaky extras opt-in.
  • We confirmed the open GitHub issues are real, synced the tree up to v0.6.11 (it was stale at 0.6.4), and split the work onto two feature branches.
  • Main thing to do next: build audio/speaker-rate-trim on the main PC, flash it, and run the SpkTrim sweep test (below) to find out whether the speaker crackle is a fixed clock offset (curable with the new knob) or live drift (needs adaptive resampling).

Branch map

Branch Commit State Needs
master 878a742 untouched, = origin/master = v0.6.11
defaults/usb-faithful 333201e pushed HIL soak-test before merging to master
audio/speaker-rate-trim 3b157cb pushed build + flash + sweep test (not yet built)

Both feature branches are pushed to origin (the fork) and tracking. upstream / upstream-fork were not touched.


What each branch contains

defaults/usb-faithful — out-of-box "acts like USB" defaults

A full audit found the firmware is already ~USB-faithful for everything that matters (input is byte-for-byte passthrough; lightbar defaults to host control; etc.). Only two defaults were flipped:

  • polling_rate_mode 0 → 2 (250 Hz → 1000 Hz / realtime) — matches a wired DS5's 1 ms latency. ⚠️ Realtime also drops the report-throttle, so this needs a 3060 min hardware soak-test (watch for BT drops / OLED Diag BT31 in/s collapse) before it merges to master. Fallback if unstable: polling_rate_mode = 1 (500 Hz).
  • bt_mic_enable 1 → 0 (mic off by default) — the BT mic has a known 2× playback bug (#10), so it's opt-in until fixed.

Both only affect fresh flashes / Reset-to-defaults; existing saved configs keep their values.

audio/speaker-rate-trim — experimental crackle fix (the active thread)

New SpkTrim setting that trims the speaker sample rate to null clock drift.

Why: the speaker path is 1:1 (no resampler) — it delivers samples at the USB host's 48 kHz clock while the DS5 consumes at its own 48 kHz crystal. Two independent clocks drift apart → the DS5's audio buffer slowly underruns → the periodic crackle (#7). 0.6.11's "retiming" fixed the average rate but can't track drift.

What it does: delivers 48000 + trim samples/s via a zero-order-hold duplicate/drop accumulator (src/audio.cpp, in the speaker accumulation loop). At ppm scale, a single duplicated/dropped sample is inaudible vs a 480-sample gap.

Files changed: src/config.h (field), src/config.cpp (clamp), src/audio.cpp (the trim), src/oled.cpp (Settings UI), CHANGELOG.md.

Config field: speaker_rate_trim, stored 0..200 = 100…+100 Hz, default 100 (= 0 Hz, an exact no-op). So a fresh flash sounds identical to 0.6.11 until you sweep it — safe to flash.

UI: OLED Settings screen, item SpkTrim ±NHz (just above "Reset to defaults"), swept with the D-pad ▶ / ◀ at 1 Hz/step. (Reset/Wipe moved to settings indices 16/17 via named constants.)


Next steps on the main PC

git fetch origin
git checkout audio/speaker-rate-trim

# ensure TinyUSB is still pinned (CLAUDE.md — 0.18 that ships with SDK 2.2.0 won't compile):
( cd "$PICO_SDK_PATH/lib/tinyusb" && git checkout 0.20.0 )

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DPICO_SDK_PATH="$PICO_SDK_PATH"
cmake --build build --target ds5-bridge        # → build/ds5-bridge-oled.uf2
# BOOTSEL the board, copy build/ds5-bridge-oled.uf2 onto the RP2350 mount

The sweep test (the whole point)

  1. OLED Settings → scroll to SpkTrim (reads +0Hz).
  2. Play a steady tone from the host: python3 scripts/sine_ch12.py 600 (10 min of 440 Hz on the speaker channels).
  3. Nudge SpkTrim with the D-pad ▶/◀ and time the interval between crackles. A longer interval = closer to the DS5's true rate. Walk it toward the value where the crackle stops. (The crackle interval is itself a tachometer: a click every ~16 s ≈ ~600 ppm of clock error.)
  4. Decision read — leave the good value running 1015 min:
    • Stays clean → static offset. The knob is the fix; we lock in a default and ship it.
    • Creeps back → dynamic drift. Proof we need adaptive resampling (continuously measure drift and vary the trim), which is the bigger but "correct" fix. The DS5 gives no buffer back-channel, so by-ear / crackle-interval is the right instrument.

Other open threads (not blocking the sweep)

  • defaults/usb-faithful soak-test — validate 1000 Hz polling stability, then merge to master (or fall back to 500 Hz).
  • #10 mic 2× playback — needs OLED Diagnostics Mic dec= (samples per opus_decode; 240 would confirm a half-rate stream) and Mic in/s (arrival rate) read with the mic enabled. Mic is off-by-default now, so it's no longer an out-of-box problem.
  • #11 constant low-level haptics — root-caused to the #6 adaptive-trigger fix (71cead4): 0.6.11 now honors host trigger-FFB, and the dongle re-asserts state[] continuously via the 0x36 audio frames, so a game's idle trigger effect buzzes constantly. Not auto-haptics (already off). Decisive read: watch the OLED Diag trig / host02 counters during the buzz — climbing = host streaming FFB (add an opt-out toggle); static = stale latch (clear FFB params when allow-bit is 0, in src/state_mgr.cpp).

Why audio is hard (context for any new session)

The DualSense's Bluetooth audio is not A2DP / not normal Bluetooth audio — Sony tunnels Opus-encoded audio inside the HID controller reports (0x31/0x36) over a proprietary, undocumented protocol with no clock negotiation. A normal PC/phone paired to a DS5 over BT gets zero controller audio; only the PS5 implements it (Sony built both ends + a dedicated radio). This dongle reverse-engineers that on a CYW43 chip over emulated SPI (hence the load-bearing 320 MHz overclock) with general-purpose BTstack. So the gap from PS5-quality is fidelity, not feasibility — the crackle and mic-2× are timing/rate bugs, not walls.


Gotchas / reminders

  • Push only to origin — never upstream / upstream-fork.
  • TinyUSB must be 0.20.0 or the build fails on TUD_AUDIO_EP_SIZE.
  • Build target is ds5-bridge; output is build/ds5-bridge-oled.uf2 (custom name).
  • SpkTrim = 0 is a byte-exact no-op, so flashing this branch can't regress audio.
  • The 0x36 packet layout and the state[] re-assertion are load-bearing (see CLAUDE.md) — don't "simplify" the audio frame.

One-paste kickoff for a fresh Claude session

I'm on branch audio/speaker-rate-trim. Read HANDOFF.md, the latest commit message, and the [Unreleased] section of CHANGELOG.md. We added an experimental SpkTrim setting to chase the speaker crackle (#7) by trimming the speaker sample rate to null clock drift between the host and the DualSense. Help me build the UF2, flash it, and run the sweep test in HANDOFF.md — find the SpkTrim value where the crackle stops, then check whether it holds (static offset = fix) or creeps back (dynamic drift = needs adaptive resampling).