8 Commits

Author SHA1 Message Date
30eccf749d Apply clang-format across the whole tree
Run clang-format (the repo's .clang-format: LLVM base, 120 cols, tabs,
Allman functions) over every source file so the tree is formatter-clean.
Whitespace only -- no behavior change; full x64 + x86 suites pass.

Also set SortIncludes: false in .clang-format. Windows include order is
load-bearing (windows.h must precede tlhelp32.h / mmreg.h / xinput.h /
dinput.h; winsock2.h must precede windows.h), and the default
alphabetical sort reorders tlhelp32.h ahead of windows.h -- a build
break. Leaving order alone keeps the manual, correct grouping.
2026-07-12 11:52:53 +02:00
05039ab104 Clean up common/ comments and the log-ring strncpy warning
Comments must not document the past or reference plan circumstances:
drop the stale pointer to a never-created audio_correlate_layout.hpp,
the "step b" plan labels, the "carved out of reserved space" history
note, and a README pointer; reword a past-tense seqlock comment to
describe the failure mode in the present.

Replace the strncpy in log_ring_push with a bounded memcpy: same
semantics (truncate + NUL), but without the C4996 deprecation warning
on every host build.
2026-07-12 08:31:48 +02:00
5b2334f6e6 Fix stale comments
- 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>
2026-06-24 02:25:42 +02:00
7ada550930 Recover a guessed stream's channels + bit depth by correlation too (step b)
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>
2026-06-23 02:50:59 +02:00
00244bcfd7 Recover a guessed audio stream's rate by correlating hook vs loopback (step a)
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>
2026-06-23 02:26:06 +02:00
90f40ae479 Audio: operator re-measure + format override (host<->hook op channel)
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>
2026-06-22 01:46:59 +02:00
784c31a9b5 Capture every audio stream into its own ring and mix them on the host
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>
2026-06-21 05:45:47 +02:00
8e93312925 Audio render-hook M1: shared audio ring + protocol diag fields
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>
2026-06-19 11:49:18 +02:00