Roadmap: plan audio format reliability work

Document the diagnosis and plan for the unreliable hooked-audio format
detection: robust rate measurement (longer window, atomic endpoints,
consensus, reject non-standard rates), visible + red-flagged loopback
fallback with auto-promote, a re-measure button and per-game persisted
format overrides via an AudioRingHeader op channel, session-only
auto-re-attach on relaunch, and color-coded log levels. Drop the stale
"previous backlog is all shipped" note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 00:41:43 +02:00
parent f673eb660c
commit 31db2d82c6

View File

@@ -87,12 +87,71 @@ and covers anything the hooked path doesn't (Vulkan, D3D9 — see Roadmap).
### Planned (next up)
Nothing queued — the previous backlog (bin restructure, terminated/hung detection,
re-attach, window-based target picker, overlay auto-layout, Audio "live" column,
moving the synthetic-input toggle, mouse + keyboard forwarding, rumble forwarding,
per-backend input debug view, cursor release, capture metrics + latency, DX12 hooked
capture, multi-stream audio + per-stream formats) is all shipped. See Future work
for what's left.
**Audio format reliability.** The hooked audio path can mis-detect a pre-existing
stream's sample rate, or fall back to loopback with no explanation. Two root causes:
the rate measurement is fragile, and late injection forces the guess path in the
first place.
- *Why the rate is sometimes wrong.* The measurement window is only ~200 ms. WASAPI
delivers audio in quantized ~10 ms buffers, so one extra buffer at a window edge is
a ~5% error (e.g. 44100 → ~46205 Hz). The snap-to-standard tolerance is ±2%, so a
~5%-off value snaps to *nothing* and is published verbatim instead of rejected, and
it's single-shot (no averaging). Fix: **measure over a longer window (~1–1.5 s),
sample the (frames, QPC) endpoints atomically, require consensus across a few
windows, and refuse to publish a rate that doesn't land near a standard rate** —
keep measuring (or flag low-confidence) rather than committing a bogus value.
- *Why it falls back to loopback silently.* The host waits a fixed 1 s for the hook
to publish a format; the guess+measure path needs ~400–700 ms of *continuous* audio
after the ring attaches, which a momentarily-quiet game can miss. Once on loopback
it never retries, and the UI shows no reason. Fix: **keep the rings live and
auto-promote to the hooked path whenever the hook later publishes a format, surface
the concrete fallback reason** (no format in time / not renderable / hook off) in the
Audio panel + log, and **flag a low-confidence rate (non-standard, didn't snap) in
red** so a bad measurement is obvious at a glance.
- **Re-measure button.** A debug action (Audio panel) to re-run a stream's rate
measurement on demand. Needs a small host→hook control surface: add operator-control
fields to the reserved area of `AudioRingHeader` (per-stream, already shared both
ways) — a bumped `op_seq` plus a "re-measure" request. The hook resets that stream's
measurement window, returns it to *Measuring*, clears `format_valid`, and republishes
once it reconverges; the host re-inits its render client when `format_generation`
bumps (so the corrected rate takes effect live).
- **Per-game format overrides (persisted), incl. the unmeasurable channels/bit-depth.**
A dropdown/inputs in the Audio panel — **shown only under the Debug details flag**,
per stream — to set sample rate, channels, bit-depth, and PCM/float when detection is
wrong or unrecoverable, **keyed by game image name and persisted** to a small store
next to the exe, so a known-bad game is auto-corrected on its next launch. A
**rate-only** override is host-side only (re-init
the render loop at the operator's rate — the bytes are already framed correctly); a
**channels/bits** override goes through the same `AudioRingHeader` op channel so the
*hook* re-frames its capture copy (`block_align`) and drops the over-read clamp, then
the host re-inits on the `format_generation` bump. **A format caught exactly at
`IAudioClient::Initialize` is itself saved as that game's override** (ground truth),
so a later late-attach to the same game is corrected automatically; if that overwrites
a previously-stored override that *differs*, log a warning.
- **Auto re-attach the same game on relaunch (session-only).** A checkbox on the
attached (or just-terminated) target, default off, *not* persisted, and **always
visible — not gated behind Debug details** (it's the recommended recovery path, not a
diagnostic). While it's on and the target has terminated, the host watches the process
list for a process of the same image name and injects automatically the moment it
reappears — built on the existing re-attach-by-image-name path, so there's no target
to type (it's the same game that was just attached). The workflow this enables: if
audio came out wrong (late attach forced the guess), tick the box, kill and relaunch
the game, and the host re-attaches *early* — early enough to catch
`IAudioClient::Initialize` and read the exact format, no guessing. This is a
**dependable fallback, not the intended primary workflow**: hardened first-launch
detection (above) should get it right without a relaunch; auto-re-attach is the
reliable recovery when it doesn't, and never asks the operator to guess a format.
- **Color-coded log levels.** The Log window should color warnings and errors (amber /
red) so they stand out from routine lines. `LogRecord` already carries a
(currently-unused) `level` field; add severity variants to the hook's `logf` and have
the host color by level. Used by the override-overwrite warning and the fallback
reasons above.
Ordering: measurement hardening + visible/red fallback reason + log levels first
(fixes the bug outright for most cases, no protocol change) → the `AudioRingHeader` op
channel (re-measure + channels/bits override; bumps `kAudioRingVersion`) → per-game
override persistence + auto-saving caught formats → session auto-re-attach (host-only,
extends the existing re-attach path).
### Future work