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:
71
README.md
71
README.md
@@ -87,12 +87,71 @@ and covers anything the hooked path doesn't (Vulkan, D3D9 — see Roadmap).
|
|||||||
|
|
||||||
### Planned (next up)
|
### Planned (next up)
|
||||||
|
|
||||||
Nothing queued — the previous backlog (bin restructure, terminated/hung detection,
|
**Audio format reliability.** The hooked audio path can mis-detect a pre-existing
|
||||||
re-attach, window-based target picker, overlay auto-layout, Audio "live" column,
|
stream's sample rate, or fall back to loopback with no explanation. Two root causes:
|
||||||
moving the synthetic-input toggle, mouse + keyboard forwarding, rumble forwarding,
|
the rate measurement is fragile, and late injection forces the guess path in the
|
||||||
per-backend input debug view, cursor release, capture metrics + latency, DX12 hooked
|
first place.
|
||||||
capture, multi-stream audio + per-stream formats) is all shipped. See Future work
|
|
||||||
for what's left.
|
- *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
|
### Future work
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user