- hook_guard.hpp top block: described removal as `hook = {}` (destroy/reset); the
model is now persistent disable_for_removal (never destroyed mid-session, the
trampoline stays alive). Updated to match.
- input_source.hpp: SteamInputSource is no longer "future" -- it exists and is
opt-in; reworded.
- audio_ring.hpp: format_generation actually bumps on every set_format (not
"reserved, v1 sets once"); verify_capture is a 4-byte atomic guarded by the
version gate (not "repurposed from a reserved byte old builds saw"); and the
SharedBlock is no longer "20-byte pads".
- audio_format_verifier.cpp: dropped a dead `(void)recover_layout;` with a stale
"step (a) only" comment -- the parameter is actually used.
Comment-only except the dead (void) cast.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends the two-path correlation from rate-only to the full layout, removing the
"channels/bit-depth assumed = device" limitation. correlate_format tries each
candidate de-interleaving (float32 / int16; mono..7.1) of the hook capture, runs
the rate correlation per layout, and keeps whichever aligns with the loopback;
a wrong de-interleaving is noise and won't.
The catch: the hook can't know a guessed stream's real frame size, so its verify
tap pads each render buffer to the device block -- which over-reads stale staging
bytes for a stream with fewer channels/bits, scrambling the audio. So the tap is
now self-describing: it prefixes each buffer with its frame count
([count][count*device_block bytes]), and the host strips the padding per candidate
layout (take the real count*real_block of each chunk) before de-interleaving.
- audio_correlate.hpp: ChunkedCapture + chunk-aware correlate_format + candidate
layouts; absolute-margin confidence gate (the true layout scores ~1.0, a truly
ambiguous alternative within ~0.001 -- 2ch@R == 1ch@2R for identical channels --
is correctly left unconfident).
- audio_hook.cpp: chunked verify tap (free-space-checked so framing can't tear).
- audio_format_verifier: parse chunks; recover_layout path. AudioMirror now corrects
the full format.
- audio_correlation_test: layout recovery from padded chunks (stereo float, 16-bit
PCM, 5.1, mono). audio_verify_test gains scenario (b): 2ch on a multichannel
endpoint with distinct per-channel content (new env-gated ToneSource mode) ->
recovers ch=2/32-bit float end-to-end.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the host attaches to an already-running game it never saw the stream's
Initialize, so the render-hook assumes the device mix format and measures only
the sample rate from the render cadence -- which a jittery game can make wrong
(intermittent pitch shift). But during the measurement window the game is still
audible, so we have the same audio twice: the hook (pre-mix, unknown format) and
a process-loopback (post-mix, the known device format). Cross-correlating them
pins the true rate from ground truth.
- common/include/coop/audio_correlate.hpp: the pure correlator. Resample the hook
by each candidate standard rate up to the device rate and score how well it
aligns with the loopback across the window (drift-detecting). audio_correlation_test
recovers every rate (score ~1.0 vs ~0.01 for wrong ones), incl. 44100-vs-48000,
and rejects unrelated signals.
- Hook measurement tap: a host-set verify_capture ring flag makes the hook push a
still-being-measured (guessed) stream's raw pre-mix bytes WITHOUT silencing, so
the host can co-capture both signals (a silenced game's loopback is silent).
Inert by default -- the shipping no-echo path is untouched.
- host/src/audio/audio_format_verifier: co-captures hook + loopback and correlates,
feeding a correction into the existing override channel. Wired into AudioMirror's
measurement window (hidden in the gap loopback already covers, so exact streams
pay nothing). audio_verify_test drives it end-to-end against coop_mock_game.
Rate vs layout are coupled (correlating the waveform needs the right channel
de-interleaving), so this step assumes the hook layout matches the device (the
common stereo-on-stereo case); recovering a different channel count / bit depth
is step b.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a per-stream op channel in AudioRingHeader (op_seq + op_* fields, version
2): the host posts re-measure / override commands, the hook applies them and
re-publishes (bumping format_generation), and the host rebuilds its render
client live on the change. The Audio panel (under Debug details) gains a
"Re-measure rate" button and a rate/channels/bit-depth/format override -- for
when detection is wrong or the channels/bit-depth were unrecoverable.
Also add a debug-only IPC test harness (-DCOOP_TEST_HARNESS, off by default,
absent from the shipped host): a file-based command channel that drives the
host's real UI code paths (inject / audio / re-measure / override / screenshot
/ status) for scripted validation, instead of unreliable synthetic mouse input.
Used to validate live: late-attach to coop_tone@44100 -> measured 44100,
promoted to hooked; override -> 2ch state, re-measure -> reconverge.
Trim the README roadmap to what's left; document the harness + rate_estimator_test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Games with several concurrent WASAPI render streams (e.g. Spider-Man: Miles
Morales) only had their first ("primary") stream mirrored; the rest kept playing
locally and never reached the guest. Now the render-hook captures + silences EVERY
tracked stream into its own ring (coop_audio_<pid>[_<index>]), each published with
that stream's own detected format (Initialize when caught, else GetMixFormat -- the
per-stream format detection, now actually used per ring rather than only for the
primary). The host creates a ring per stream and mixes the same-format streams with
a soft clip (host/src/audio/audio_mix.hpp); streams whose format differs from the
primary are still silenced (no echo) but skipped from the mix (would need
resampling).
The single-stream case is byte-for-byte unchanged: when only one stream is active
the host passes it through without the mixer, so the common path has no overhead or
fidelity change.
Verified: new audio_mix_test covers the decode/sum/soft-clip/encode math (float32 +
int16); audio_hook_test (x64 + x86) still passes, guarding the primary
capture+silence path against regression; full build x64 + x86 clean; ctest x64
11/11, x86 3/3. Multi-stream mixing against a real multi-stream game needs a live
session to fully confirm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First milestone of the injection render-hook audio path (see
docs/audio-render-hook-plan.md) that fixes the local audio echo without a
virtual device.
- common/include/coop/audio_ring.hpp: new lock-free SPSC shared-memory ring
for PCM, separate from the input/status SharedBlock. Free-running 64-bit
positions (release/acquire), format handshake, host-owned capture_enabled
gate, drop-whole-packet overrun policy.
- common/include/coop/protocol.hpp: add AudioStreamInfo + audio_streams_seen /
audio_streams[] to the always-present HookStatus for the render-stream-count
debug view; bump kProtocolVersion 3->4 (new members appended).
- tests/audio_ring_test.cpp: in-process unit test (push/pop integrity,
wrap-around, format handshake, overrun/drop). No hook or audio device.
- docs/audio-render-hook-plan.md: the green-lit design this implements.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>