Compare commits

..
Author SHA1 Message Date
MarcelineVPQandClaude Opus 4.8 03647048f0 docs: handoff for the trigger-buzz fix (#11) — keep #6, kill the side-effect
Design-complete, not yet implemented. Captures the root cause (verified vs
upstream: #6 enabling the trigger apply-bits is the only haptic deviation; the
0x36 audio frame re-broadcasts state[] at ~100 Hz, re-firing the latched
trigger effect on the audio clock = the buzz), and the exact USB-faithful fix:
a state_set_frame() one-shot that emits trigger FFB once per host update then
goes trigger-neutral, plus the three call-site swaps. Includes build + HIL
verification steps. Resume on the main PC with hardware.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 12:18:49 -06:00
7 changed files with 188 additions and 168 deletions
-4
View File
@@ -8,10 +8,6 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
## [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 0200 = **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
+171
View File
@@ -0,0 +1,171 @@
# 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
View File
@@ -1,106 +0,0 @@
# 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
```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 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).
+12 -33
View File
@@ -264,20 +264,12 @@ void audio_loop() {
static float audio_buf[480 * 2];
static uint audio_buf_pos = 0;
static double spk_acc = 0.0; // speaker rate-trim accumulator (persists across calls)
// 2. 从4ch中提取ch3/ch4,转换为float输入重采样器
WDL_ResampleSample *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 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 hap_max = g_peak_hap;
@@ -316,32 +308,19 @@ void audio_loop() {
if (b > hap_max) hap_max = b;
}
#if !DISABLE_SPEAKER_PROC
{
const float spk_l = raw[i * INPUT_CHANNELS] / 32768.0f * audio_gain;
const float spk_r = raw[i * INPUT_CHANNELS + 1] / 32768.0f * audio_gain;
// Rate-trim: emit `spk_ratio` output samples per input sample on average
// (usually 1; occasionally 2 when trim>0, or 0 when trim<0). Zero-order
// hold — at ppm-scale trims a single duplicated/dropped sample is inaudible
// vs a full 480-sample buffer underrun.
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;
}
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;
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;
}
#endif
float h_l = raw[i * INPUT_CHANNELS + 2] / 32768.0f * haptics_gain;
-4
View File
@@ -121,10 +121,6 @@ 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");
-8
View File
@@ -54,14 +54,6 @@ struct __attribute__((packed)) Config_body {
// preserves the original "any controller activity wakes the screen"
// behavior. Issues #8 (dim timeout never fired during play) and #9.
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 {
+5 -13
View File
@@ -130,7 +130,7 @@ constexpr int kLbModeHost = 8;
constexpr int kNumLbModes = 9;
// Settings screen state
constexpr int kNumSettingsItems = 18; // 8 fields + 3 auto-haptic + 2 screen-timeout + BT mic + Ctrl-wake + Spk-trim + Reset + Wipe
constexpr int kNumSettingsItems = 17; // 8 fields + 3 auto-haptic + 2 screen-timeout + BT mic + Ctrl-wake + Reset + Wipe
constexpr int kSettingsAutoHapEnaIdx = 8;
constexpr int kSettingsAutoHapGainIdx = 9;
constexpr int kSettingsAutoHapLpIdx = 10;
@@ -138,9 +138,8 @@ constexpr int kSettingsScrDimIdx = 11;
constexpr int kSettingsScrOffIdx = 12;
constexpr int kSettingsBtMicIdx = 13;
constexpr int kSettingsCtrlWakeIdx = 14;
constexpr int kSettingsSpkTrimIdx = 15;
constexpr int kSettingsResetIdx = 16;
constexpr int kSettingsWipeSlotsIdx = 17;
constexpr int kSettingsResetIdx = 15;
constexpr int kSettingsWipeSlotsIdx = 16;
Config_body settings_local{};
int settings_sel = 0;
bool settings_dirty = false;
@@ -1600,12 +1599,6 @@ void settings_adjust(int delta) {
}
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 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;
}
}
}
@@ -1709,9 +1702,8 @@ __attribute__((noinline)) void format_settings_item(int idx, char* line, size_t
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 15: snprintf(line, n, "%s SpkTrim %+dHz", cur, (int)c.speaker_rate_trim - 100); break;
case 16: snprintf(line, n, "%s Reset to defaults", cur); break;
case 17: snprintf(line, n, "%s Wipe all slots", cur); break;
case 15: snprintf(line, n, "%s Reset to defaults", cur); break;
case 16: snprintf(line, n, "%s Wipe all slots", cur); break;
}
}