diff --git a/README.md b/README.md index 35a0b45..fe06c0b 100644 --- a/README.md +++ b/README.md @@ -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