Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e7942ec40 | ||
|
|
3b157cb9a1 |
@@ -8,6 +8,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added (experimental — `audio/speaker-rate-trim` branch)
|
||||||
|
|
||||||
|
- **`SpkTrim` setting — fine speaker clock-drift trim to chase the periodic crackle (#7).** The DS5's DAC and the USB host's audio clock are independent ~48 kHz crystals; the residual ppm mismatch slowly drifts the controller's audio buffer, which underruns and crackles. The speaker path now delivers `48000 + trim` samples/s (via a zero-order-hold duplicate/drop accumulator) instead of exactly 48000, to null that drift. New `speaker_rate_trim` config field (stored 0–200 = **−100…+100 Hz**, default **0 Hz** = exact no-op), swept on the OLED **Settings** screen with the D-pad ▶◀ at 1 Hz/step. **Experimental / diagnostic:** if one value silences the crackle and *holds*, the drift was a static offset and this is the fix; if it nulls then creeps back, that proves the drift is dynamic and needs adaptive resampling. Tune with `scripts/sine_ch12.py` playing a steady tone and time the interval between crackles.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [0.6.11-oled-edition] — 2026-06-02
|
## [0.6.11-oled-edition] — 2026-06-02
|
||||||
|
|||||||
@@ -1,171 +0,0 @@
|
|||||||
# HANDOFF — fix the constant trigger buzz (#11) without losing #6
|
|
||||||
|
|
||||||
_Written 2026-06-05 on the laptop (no Pico on hand). Resume on the main PC with hardware._
|
|
||||||
|
|
||||||
**Status: design complete + agreed. NOT implemented, NOT built, NOT flashed.** This doc has the
|
|
||||||
full root-cause and the exact code so you can drop it in, build, and HIL-verify in one sitting.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The decision (don't re-litigate this)
|
|
||||||
|
|
||||||
- **Ground truth for the whole project:** the dongle should make the DualSense behave **as close to a
|
|
||||||
wired-USB controller as possible.** Every "should we add a setting?" gets answered with "does USB do
|
|
||||||
it? then match that" — not a new toggle.
|
|
||||||
- **Keep the #6 adaptive-trigger fix.** Users have confirmed games work with it. It was correct.
|
|
||||||
- **#11 (constant trigger buzz) is an unintended side-effect of #6**, not a reason to revert it.
|
|
||||||
- **We rejected a toggle.** A toggle treats the symptom and makes you babysit it. We fix the cause.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Root cause (verified against upstream `awalol/DS5Dongle`)
|
|
||||||
|
|
||||||
I fetched upstream and diffed the haptic path. Fork point: `0ed05d3` ("fix: rumble").
|
|
||||||
|
|
||||||
- **The only haptic-relevant deviation from upstream is #6** (`71cead4`): in `src/state_mgr.cpp` we added
|
|
||||||
```c
|
|
||||||
set_bit(state[0], 2, update.AllowRightTriggerFFB);
|
|
||||||
set_bit(state[0], 3, update.AllowLeftTriggerFFB);
|
|
||||||
```
|
|
||||||
Upstream copies the trigger FFB *params* (its `copy_if_allowed` lines are identical to ours) but
|
|
||||||
**never sets these two apply-bits**, so upstream's adaptive triggers are silently dead. We turned
|
|
||||||
them on. That's the entire difference.
|
|
||||||
- **The re-broadcast is NOT our deviation.** Both upstream and our fork stamp the full controller state
|
|
||||||
(`state_set(pkt+13, 63)`) into **every `0x36` audio frame** — it's load-bearing for keeping the
|
|
||||||
speaker/HD-haptic actuators alive (the `0x7f 0x7f` volume bytes must ride every frame; see CLAUDE.md).
|
|
||||||
- **So the buzz = (our enabled trigger bits) × (the audio-rate re-broadcast).** A host-latched trigger
|
|
||||||
effect gets re-commanded ~100×/second on the **audio clock**. Wired USB never does that — the host
|
|
||||||
drives trigger updates on **its** clock and the controller latches the last effect.
|
|
||||||
- Upstream doesn't buzz only because it never enables triggers (and pays for it with dead triggers).
|
|
||||||
Neither upstream (no triggers) nor our current master (buzzing triggers) matches USB. The fix below does.
|
|
||||||
|
|
||||||
`auto_haptics` (speaker-derived rumble) is a fork-only feature but is **off by default** — not the culprit.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How state reaches the controller — the four egress points
|
|
||||||
|
|
||||||
| Site | Report | Cadence | Carries triggers today? |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `src/main.cpp:322` | `0x31` | only on a host output report, **and only when audio is OFF** | yes (already USB-like) |
|
|
||||||
| `src/bt.cpp:636` | `0x32` | **once**, at L2CAP connect (init handshake) | init only; params are zero |
|
|
||||||
| `src/audio.cpp:172` | `0x36` | ~4 Hz keepalive (audio armed but idle) | yes — re-fires |
|
|
||||||
| `src/audio.cpp:404` | `0x36` | **~100 Hz** while audio streams | yes — re-fires (the loud buzz) |
|
|
||||||
|
|
||||||
Critical detail at `src/main.cpp:307-311`: **when audio is active the firmware deliberately does NOT send
|
|
||||||
the standalone `0x31`** — it lets the trigger FFB written into `state[]` "ride the `0x36` audio frames
|
|
||||||
instead." So during gameplay-with-audio, the 100 Hz `0x36` re-broadcast is the *only* trigger delivery
|
|
||||||
path. That is exactly the path that buzzes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The fix — one-shot trigger emit (mirrors USB cadence)
|
|
||||||
|
|
||||||
Deliver the trigger apply-bits **once per host update**, then go trigger-neutral so the controller
|
|
||||||
*latches* the effect instead of being re-hit by it. The `0x0C` apply-bits are literally named
|
|
||||||
"*Enable setting* RightTriggerFFB" — cleared means "leave triggers as-is," so the DS5 holds the last
|
|
||||||
applied effect (same as USB).
|
|
||||||
|
|
||||||
### `src/state_mgr.cpp`
|
|
||||||
|
|
||||||
1. Add a file-static one-shot (single-core: `state_update` and the frame senders all run on core 0, so
|
|
||||||
no atomics needed):
|
|
||||||
```c
|
|
||||||
static bool trigger_oneshot = false; // armed by a host trigger write, consumed by the next outbound frame
|
|
||||||
```
|
|
||||||
2. In `state_update()`, right after the two existing `set_bit(state[0], 2/3, ...)` lines, arm it:
|
|
||||||
```c
|
|
||||||
// Trigger FFB is a host-latched effect, not a continuous level: arm a one-shot so exactly ONE
|
|
||||||
// outbound frame carries the apply-bits after each host update, then state_set_frame() masks them
|
|
||||||
// off. The controller holds the latched effect instead of it being re-fired on every 0x36 audio
|
|
||||||
// frame (the #11 buzz). Mirrors wired USB. Keeps #6 fully intact.
|
|
||||||
if (update.AllowRightTriggerFFB || update.AllowLeftTriggerFFB) trigger_oneshot = true;
|
|
||||||
```
|
|
||||||
3. Add a new frame-copy next to `state_set()`:
|
|
||||||
```c
|
|
||||||
// Like state_set(), but for REPEATING outbound frames (the 0x36 audio frames, the 0x31 host echo).
|
|
||||||
// Emits the adaptive-trigger apply-bits only on the first frame after a host trigger update, then
|
|
||||||
// clears them so the effect isn't re-fired at audio rate (#11). Rumble (bits 0/1) is a continuous
|
|
||||||
// level and is intentionally left re-asserted every frame.
|
|
||||||
void state_set_frame(uint8_t *data, const uint8_t size) {
|
|
||||||
state_set(data, size);
|
|
||||||
if (trigger_oneshot) trigger_oneshot = false; // this frame carries the triggers
|
|
||||||
else if (size > 0) data[0] &= ~0x0C; // clear AllowRight/LeftTriggerFFB → hold latched effect
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `src/state_mgr.h`
|
|
||||||
Declare it:
|
|
||||||
```c
|
|
||||||
void state_set_frame(uint8_t *data, const uint8_t size);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Swap the three *repeating* senders (leave `bt.cpp:636` init as plain `state_set`)
|
|
||||||
- `src/audio.cpp:172` → `state_set_frame(pkt + 13, 63);`
|
|
||||||
- `src/audio.cpp:404` → `state_set_frame(pkt + 13, 63);`
|
|
||||||
- `src/main.cpp:322` → `state_set_frame(outputData + 3, sizeof(SetStateData));`
|
|
||||||
|
|
||||||
### Why this is safe
|
|
||||||
- Additive + 3 one-line swaps; fully reversible.
|
|
||||||
- Can't break #6: every host trigger update still produces exactly one trigger-bearing frame to the DS5.
|
|
||||||
- Can't regress rumble: only byte-0 bits 2,3 are masked; rumble bits 0,1 still re-assert every frame.
|
|
||||||
- Worst case if the buzz had another cause: it's a no-op move toward USB cadence, not a regression.
|
|
||||||
|
|
||||||
### Offsets (already confirmed in `src/utils.h:289` `SetStateData`)
|
|
||||||
- byte 0 bit 2 = `AllowRightTriggerFFB`, bit 3 = `AllowLeftTriggerFFB` → mask `0x0C`.
|
|
||||||
- `RightTriggerFFB[11]` at state byte 10; `LeftTriggerFFB[11]` at byte 21. (Params don't need zeroing —
|
|
||||||
the DS5 ignores them when the apply-bit is clear. Masking the apply-bit is sufficient.)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Build + flash (main PC)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git fetch origin && git checkout master # this fix branches off master (clean 0.6.12)
|
|
||||||
git checkout -b fix/trigger-ffb-latch
|
|
||||||
# ...apply the edits above...
|
|
||||||
|
|
||||||
# Toolchain (Ubuntu 26.04 — the apt install was started on the laptop, finish/verify it):
|
|
||||||
# sudo apt-get install -y cmake ninja-build build-essential \
|
|
||||||
# gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib python3 git
|
|
||||||
# Pico SDK 2.2.0 + export PICO_SDK_PATH, then PIN TinyUSB (or the audio config won't compile):
|
|
||||||
( cd "$PICO_SDK_PATH/lib/tinyusb" && git fetch --tags && 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 the UF2 onto the RP2350 mount
|
|
||||||
```
|
|
||||||
|
|
||||||
## HIL verification (the part that actually needs the hardware)
|
|
||||||
|
|
||||||
1. **Triggers still work (#6 intact):** in a game with adaptive triggers (or the on-dongle Trigger Test
|
|
||||||
screen), confirm resistance/effects still fire. Test **with audio active** specifically (that's the
|
|
||||||
path we changed).
|
|
||||||
2. **Buzz is gone (#11):** set a trigger effect in a game, then idle — the constant low-level
|
|
||||||
buzz/vibration should not persist.
|
|
||||||
3. **Decisive instrument:** OLED **Diagnostics** screen — watch the `trig` / `host02` counters during
|
|
||||||
the buzz scenario. (These are `g_host_out02_trig_allow` / `_to_bt` / `_folded` from `src/main.cpp`.)
|
|
||||||
Confirms whether the host is streaming trigger FFB vs a stale latch.
|
|
||||||
4. **Latch assumption check:** the fix relies on the DS5 holding the last effect when apply-bit=0. If an
|
|
||||||
effect unexpectedly *clears* between updates, the assumption is wrong — fall back to "emit on change"
|
|
||||||
(shadow-compare the trigger bytes) instead of the one-shot. (Not expected; apply-bit semantics say hold.)
|
|
||||||
|
|
||||||
Commit only after HIL passes (one-UF2-per-feature cadence). Trailer: `Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`. Branch off master, push to `origin` only. Then it can ship as 0.6.12 (update `CHANGELOG.md [Unreleased]` → Fixed).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Other open threads (unchanged, not blocking this)
|
|
||||||
|
|
||||||
- **`audio/speaker-rate-trim`** branch — experimental `SpkTrim` crackle sweep (#7). See `HANDOFF.md` on
|
|
||||||
that branch. Separate work.
|
|
||||||
- **`defaults/usb-faithful`** branch — 1000 Hz polling + mic-off defaults; needs a soak-test before merge.
|
|
||||||
- **Toolchain install** was started on the laptop (Ubuntu 26.04, apt) but not finished/verified; Pico SDK
|
|
||||||
not yet cloned there. The main PC presumably already has a working build env.
|
|
||||||
|
|
||||||
## One-paste kickoff for a fresh session on the main PC
|
|
||||||
|
|
||||||
> Read `HANDOFF-trigger-buzz.md`. We're keeping the #6 adaptive-trigger fix and removing its side-effect,
|
|
||||||
> the constant trigger buzz (#11), by delivering trigger FFB once per host update instead of re-firing it
|
|
||||||
> on every 0x36 audio frame — wired-USB cadence. The exact edits (a `state_set_frame` one-shot in
|
|
||||||
> `state_mgr.cpp` + three call-site swaps) are in the doc. Help me apply them on a branch off master,
|
|
||||||
> build the UF2, and run the HIL verification in the doc.
|
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
# 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 30–60 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 10–15 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).
|
||||||
+33
-12
@@ -264,12 +264,20 @@ void audio_loop() {
|
|||||||
|
|
||||||
static float audio_buf[480 * 2];
|
static float audio_buf[480 * 2];
|
||||||
static uint audio_buf_pos = 0;
|
static uint audio_buf_pos = 0;
|
||||||
|
static double spk_acc = 0.0; // speaker rate-trim accumulator (persists across calls)
|
||||||
// 2. 从4ch中提取ch3/ch4,转换为float输入重采样器
|
// 2. 从4ch中提取ch3/ch4,转换为float输入重采样器
|
||||||
WDL_ResampleSample *in_buf;
|
WDL_ResampleSample *in_buf;
|
||||||
int nframes = resampler.ResamplePrepare(frames, OUTPUT_CHANNELS, &in_buf);
|
int nframes = resampler.ResamplePrepare(frames, OUTPUT_CHANNELS, &in_buf);
|
||||||
|
|
||||||
const float audio_gain = mute[0] ? 0.0f : powf(10.0f, get_config().speaker_volume / 20.0f);
|
const float audio_gain = mute[0] ? 0.0f : powf(10.0f, get_config().speaker_volume / 20.0f);
|
||||||
const float haptics_gain = get_config().haptics_gain;
|
const float haptics_gain = get_config().haptics_gain;
|
||||||
|
|
||||||
|
// EXPERIMENTAL speaker clock-drift trim (audio/speaker-rate-trim). Deliver
|
||||||
|
// 48000 + trim samples/s to the DS5 instead of exactly 48000, to null the slow
|
||||||
|
// inter-crystal drift behind the crackle (#7). trim = 0 → ratio == 1.0 → exact
|
||||||
|
// no-op (byte-identical to pre-trim, since 48000/48000 is exact in double).
|
||||||
|
const int spk_trim_hz = (int)get_config().speaker_rate_trim - 100; // -100..+100 Hz
|
||||||
|
const double spk_ratio = (48000.0 + spk_trim_hz) / 48000.0;
|
||||||
uint16_t spk_max = g_peak_spk;
|
uint16_t spk_max = g_peak_spk;
|
||||||
uint16_t hap_max = g_peak_hap;
|
uint16_t hap_max = g_peak_hap;
|
||||||
|
|
||||||
@@ -308,19 +316,32 @@ void audio_loop() {
|
|||||||
if (b > hap_max) hap_max = b;
|
if (b > hap_max) hap_max = b;
|
||||||
}
|
}
|
||||||
#if !DISABLE_SPEAKER_PROC
|
#if !DISABLE_SPEAKER_PROC
|
||||||
audio_buf[audio_buf_pos++] = raw[i * INPUT_CHANNELS] / 32768.0f * audio_gain;
|
{
|
||||||
audio_buf[audio_buf_pos++] = raw[i * INPUT_CHANNELS + 1] / 32768.0f * audio_gain;
|
const float spk_l = raw[i * INPUT_CHANNELS] / 32768.0f * audio_gain;
|
||||||
if (audio_buf_pos == 480 * 2) {
|
const float spk_r = raw[i * INPUT_CHANNELS + 1] / 32768.0f * audio_gain;
|
||||||
static audio_raw_element element{};
|
// Rate-trim: emit `spk_ratio` output samples per input sample on average
|
||||||
memcpy(element.data, audio_buf, 480 * 2 * 4);
|
// (usually 1; occasionally 2 when trim>0, or 0 when trim<0). Zero-order
|
||||||
if (queue_is_full(&audio_fifo)) {
|
// hold — at ppm-scale trims a single duplicated/dropped sample is inaudible
|
||||||
queue_try_remove(&audio_fifo,NULL);
|
// vs a full 480-sample buffer underrun.
|
||||||
g_fifo_drops++;
|
spk_acc += spk_ratio;
|
||||||
|
int emit = (int)spk_acc;
|
||||||
|
spk_acc -= emit;
|
||||||
|
while (emit-- > 0) {
|
||||||
|
audio_buf[audio_buf_pos++] = spk_l;
|
||||||
|
audio_buf[audio_buf_pos++] = spk_r;
|
||||||
|
if (audio_buf_pos == 480 * 2) {
|
||||||
|
static audio_raw_element element{};
|
||||||
|
memcpy(element.data, audio_buf, 480 * 2 * 4);
|
||||||
|
if (queue_is_full(&audio_fifo)) {
|
||||||
|
queue_try_remove(&audio_fifo,NULL);
|
||||||
|
g_fifo_drops++;
|
||||||
|
}
|
||||||
|
if (!queue_try_add(&audio_fifo, &element)) {
|
||||||
|
printf("[Audio] Warning: audio_fifo add failed\n");
|
||||||
|
}
|
||||||
|
audio_buf_pos = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!queue_try_add(&audio_fifo, &element)) {
|
|
||||||
printf("[Audio] Warning: audio_fifo add failed\n");
|
|
||||||
}
|
|
||||||
audio_buf_pos = 0;
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
float h_l = raw[i * INPUT_CHANNELS + 2] / 32768.0f * haptics_gain;
|
float h_l = raw[i * INPUT_CHANNELS + 2] / 32768.0f * haptics_gain;
|
||||||
|
|||||||
@@ -121,6 +121,10 @@ void config_valid() {
|
|||||||
body->controller_wakes_display = 1;
|
body->controller_wakes_display = 1;
|
||||||
printf("[Config] controller_wakes_display invalid, defaulting to 1 (on)\n");
|
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) {
|
if (body->config_version != CONFIG_VERSION) {
|
||||||
body->config_version = CONFIG_VERSION;
|
body->config_version = CONFIG_VERSION;
|
||||||
printf("[Config] Warning: Config may breaking change\n");
|
printf("[Config] Warning: Config may breaking change\n");
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ struct __attribute__((packed)) Config_body {
|
|||||||
// preserves the original "any controller activity wakes the screen"
|
// preserves the original "any controller activity wakes the screen"
|
||||||
// behavior. Issues #8 (dim timeout never fired during play) and #9.
|
// behavior. Issues #8 (dim timeout never fired during play) and #9.
|
||||||
uint8_t controller_wakes_display;
|
uint8_t controller_wakes_display;
|
||||||
|
// EXPERIMENTAL (audio/speaker-rate-trim branch): fine speaker clock-drift trim.
|
||||||
|
// The DS5's DAC and the USB host's audio clock are independent ~48 kHz crystals;
|
||||||
|
// the residual ppm mismatch slowly drifts the DS5's buffer and produces the
|
||||||
|
// periodic crackle (#7). This nudges our delivered rate to 48000 + (trim-100) Hz
|
||||||
|
// to null that drift. Stored 0..200, offset by 100 so an erased-flash 0xFF clamps
|
||||||
|
// to 100 = 0 Hz (an exact no-op, byte-identical to pre-trim). Swept on the OLED
|
||||||
|
// Settings screen with the D-pad ▶◀, 1 Hz/step. Default 100 (0 Hz).
|
||||||
|
uint8_t speaker_rate_trim;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct __attribute__((packed)) Config {
|
struct __attribute__((packed)) Config {
|
||||||
|
|||||||
+13
-5
@@ -130,7 +130,7 @@ constexpr int kLbModeHost = 8;
|
|||||||
constexpr int kNumLbModes = 9;
|
constexpr int kNumLbModes = 9;
|
||||||
|
|
||||||
// Settings screen state
|
// Settings screen state
|
||||||
constexpr int kNumSettingsItems = 17; // 8 fields + 3 auto-haptic + 2 screen-timeout + BT mic + Ctrl-wake + Reset + Wipe
|
constexpr int kNumSettingsItems = 18; // 8 fields + 3 auto-haptic + 2 screen-timeout + BT mic + Ctrl-wake + Spk-trim + Reset + Wipe
|
||||||
constexpr int kSettingsAutoHapEnaIdx = 8;
|
constexpr int kSettingsAutoHapEnaIdx = 8;
|
||||||
constexpr int kSettingsAutoHapGainIdx = 9;
|
constexpr int kSettingsAutoHapGainIdx = 9;
|
||||||
constexpr int kSettingsAutoHapLpIdx = 10;
|
constexpr int kSettingsAutoHapLpIdx = 10;
|
||||||
@@ -138,8 +138,9 @@ constexpr int kSettingsScrDimIdx = 11;
|
|||||||
constexpr int kSettingsScrOffIdx = 12;
|
constexpr int kSettingsScrOffIdx = 12;
|
||||||
constexpr int kSettingsBtMicIdx = 13;
|
constexpr int kSettingsBtMicIdx = 13;
|
||||||
constexpr int kSettingsCtrlWakeIdx = 14;
|
constexpr int kSettingsCtrlWakeIdx = 14;
|
||||||
constexpr int kSettingsResetIdx = 15;
|
constexpr int kSettingsSpkTrimIdx = 15;
|
||||||
constexpr int kSettingsWipeSlotsIdx = 16;
|
constexpr int kSettingsResetIdx = 16;
|
||||||
|
constexpr int kSettingsWipeSlotsIdx = 17;
|
||||||
Config_body settings_local{};
|
Config_body settings_local{};
|
||||||
int settings_sel = 0;
|
int settings_sel = 0;
|
||||||
bool settings_dirty = false;
|
bool settings_dirty = false;
|
||||||
@@ -1599,6 +1600,12 @@ void settings_adjust(int delta) {
|
|||||||
}
|
}
|
||||||
case 13: c.bt_mic_enable ^= 1; break; // BT mic on/off
|
case 13: c.bt_mic_enable ^= 1; break; // BT mic on/off
|
||||||
case 14: c.controller_wakes_display ^= 1; break; // controller activity wakes OLED on/off
|
case 14: c.controller_wakes_display ^= 1; break; // controller activity wakes OLED on/off
|
||||||
|
case 15: { // speaker_rate_trim stored 0..200 = -100..+100 Hz, 1 Hz/step
|
||||||
|
int v = (int)c.speaker_rate_trim + delta;
|
||||||
|
if (v < 0) v = 0; if (v > 200) v = 200;
|
||||||
|
c.speaker_rate_trim = (uint8_t)v;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1702,8 +1709,9 @@ __attribute__((noinline)) void format_settings_item(int idx, char* line, size_t
|
|||||||
break;
|
break;
|
||||||
case 13: snprintf(line, n, "%s BT Mic %s", cur, c.bt_mic_enable ? "on" : "off"); break;
|
case 13: snprintf(line, n, "%s BT Mic %s", cur, c.bt_mic_enable ? "on" : "off"); break;
|
||||||
case 14: snprintf(line, n, "%s CtrlWake %s", cur, c.controller_wakes_display ? "on" : "off"); break;
|
case 14: snprintf(line, n, "%s CtrlWake %s", cur, c.controller_wakes_display ? "on" : "off"); break;
|
||||||
case 15: snprintf(line, n, "%s Reset to defaults", cur); break;
|
case 15: snprintf(line, n, "%s SpkTrim %+dHz", cur, (int)c.speaker_rate_trim - 100); break;
|
||||||
case 16: snprintf(line, n, "%s Wipe all slots", cur); break;
|
case 16: snprintf(line, n, "%s Reset to defaults", cur); break;
|
||||||
|
case 17: snprintf(line, n, "%s Wipe all slots", cur); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user