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>
Preserves in-progress work that had been sitting uncommitted in the working
tree (predates this session) so it is protected in git history. This is NOT a
verified/shipped fix — it is parked. Audio path is unverified on hardware and
still carries debug instrumentation.
- src/audio.cpp: drop the 512->480 resampler and retime the 0x36 frame to a
true 10ms/100Hz grid (SAMPLE_SIZE 64->60, 480-sample buffer) to chase the
~45kHz-vs-48kHz speaker underrun theory behind the crackle; dynamic speaker
sub-report offset; debug printf + Opus-frame-dump instrumentation.
- src/config.cpp: default audio_buffer_length 64->16; auto_haptics_enable
default 1 (Fallback) -> 0 (Off).
- src/oled.cpp: chunked non-blocking SPI flush (issue #7 OLED-stall angle) and
a battery-% midpoint display tweak.
- scripts/: audio debug helpers (opus-dump decode, pi audio test, sine ch1/2).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the three non-audio user issues (audio path untouched):
- #6: adaptive trigger FFB was silently dropped. state_update() copied the
RightTriggerFFB/LeftTriggerFFB params into the outgoing state but never set
the Allow{Right,Left}TriggerFFB apply-bits in byte 0, so the DS5 discarded
them on BOTH the standalone 0x31 path and the 0x36 audio-frame fold, while
direct USB worked. Mirror the host's two allow-flags in, like the rumble
flags already were (matches what the on-device Trigger Test screen does).
- #8/#9: new CtrlWake setting (default on = unchanged behavior). Set off and
controller input no longer keeps the OLED awake, so the dim/off timeouts run
during gameplay and the panel can sleep while the controller is in use; only
KEY0/KEY1 wake it.
- #9: OLED brightness (screen_brightness) now persists across a power cycle
instead of resetting to full on every boot.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BT microphone over Bluetooth: the DS5 mic now works over the dongle's BT
pairing — decoded from the controller's Opus stream to the USB capture
endpoint. Hinges on pkt[4] bit 0 (mic-enable) in the outbound 0x36 audio
report; credit to awalol (upstream) for identifying it. Mic-tagged 0x31
frames ((data[2]>>1)&1) are ALWAYS diverted out of the input path (decoded
when on, dropped when off) so Opus payload can never corrupt sticks/buttons.
Always-on via a sticky-latch keep-alive that only runs post-enumeration
(tud_mounted) so it never floods the fresh-pair handshake (which otherwise
delayed controller detection past the watchdog and tore the link down).
Toggle: bt_mic_enable config field (default on) — OLED Settings + web config.
README gains a "DualSense Microphone over Bluetooth" section;
BLUETOOTH_AUDIO_NOTES.md rewritten from "dead end" to the working mechanism.
USB 3.0 connection watchdog: auto-recovers a stalled connection (re-inquiry)
instead of hanging on the amber lightbar, for USB 3.0 ~2.4 GHz RF interference
that desensitizes the CYW43 BT radio. Re-enabled the ACL-fail / auth-fail /
create-connection-reject recovery paths. README "USB 3.0 ports & Bluetooth
interference" section with mitigations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue #5 (requested by @TerryFrench): the OLED dim/off tiers are no longer
hardcoded at 2/15 min. Two new Config_body fields screen_dim_timeout /
screen_off_timeout (minutes, 0 = tier disabled, range [0,250]) are editable
on the Settings screen (ScrDim/ScrOff) and persist to flash; defaults
preserve the 2/15 ladder and upgraders read those via the config_valid clamp.
The idle timer moved from time_us_32() to 64-bit µs so the full 250-min range
is representable without the ~71-min wrap.
Issue #6: new "trig fold" Diagnostics counter — trigger-bearing 0x02 host
reports that arrived while the speaker stream was active and were folded into
the 0x36 audio frames (via state[]) rather than sent as a standalone 0x31.
Makes trig_allow == to_bt(trig share) + fold visible, confirming the apparent
trig/tx gap is audio-path folding, not dropped reports.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Status screen: show an estimated time-to-full ("~43m") while charging,
self-calibrating from each 10% battery notch with Li-ion taper correction
(discard the partial plug-in step; 3-sample moving average; per-step weight
1.0/1.5/2.2 for bulk/80-90/90-100%). Shows "~--m" until the first full step.
Lightbar: the chosen mode + 4 favorites now persist to config flash and
stick across every screen (and through gameplay/audio). A single
lightbar_service() owns the LED via the persistent state[] block (new
state_set_led/state_get_led) with a host-override gate (g_lightbar_override)
so the host's AllowLedColor can't stomp a firmware-chosen mode. New HOST
passthrough mode (default) keeps the game in control of the LED out of the
box. The charging amber pulse (255,100,0) is folded in as top priority.
Idle ladder: keep the panel at the dim/dot tier (never full-off) while
charging so users stop unplugging to wake it (which reset the charge ETA).
Fix idle detection to deadzone stick jitter [120,140] + skip the counter
byte (idata[6]) so the dot tier engages with a controller connected
(mirrors bt.cpp's inactivity heuristic).
New Config_body fields: lightbar_mode + lb_fav_{r,g,b}[4].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Build-system, config, usb, and CI wiring on top of upstream's
v0.6.0-hotfix tree. Build will still fail until Phase C+D land bt.h
accessors that oled.cpp references; that's expected mid-rebase.
CMakeLists.txt: add src/oled.cpp + src/slots.cpp to add_executable,
link hardware_spi, restore OUTPUT_NAME ds5-bridge-oled.
src/config.h: append the 4 OLED Edition Config_body fields
(current_slot + auto_haptics_enable/gain/lowpass). Preserves
upstream's field order for the first 8 fields.
src/config.cpp:
- speaker_volume validity default -100 dB → 0 dB (footgun fix)
- 4 new validity guards for the appended fields with sensible
defaults (Fallback / 100 % / 160 Hz / slot 0)
- config_default() impl (was declared in upstream's config.h since
v0.5.4 but never defined; the OLED Edition's "Reset to defaults"
Settings item calls it)
src/usb.cpp: drop the UAC1 SET_CUR Volume → flash-config sync that
let PipeWire/Pulse silently override the user's saved Spk Vol on
every device reconnect. Borrowed from loteran/DS5Dongle commit
03fa1e4.
src/usb_descriptors.cpp: restore iSerialNumber = STRID_SERIAL.
Upstream's commit e79c762 zeroed it to work around SpecialK (#32),
but that broke Windows device identity (#100) — users lost
per-device volume / app preferences when moving USB ports. The
OLED Edition prioritizes Windows device-identity stability for
the broader user base over SpecialK compat for a narrower one.
.github/workflows/{build,release}.yml: artifact filenames use
ds5-bridge-oled.uf2 (and -debug / -picow variants) per the
OUTPUT_NAME rebrand.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>