Audio: detect a pre-existing render stream's true sample rate (fix pitch)
Hooked audio mirroring played back pitch-shifted on games we inject into that render at a non-device sample rate (e.g. Godot/Brotato render 44100 Hz on a 48000 Hz endpoint via WASAPI AUTOCONVERTPCM). We attach to an already-running game, so the render-hook never saw its IAudioClient:: Initialize and assumed the device mix format -- right channels/bits, wrong rate -- so 44100 audio was rendered as 48000 (+~1.5 semitones). Fix: treat a pre-existing client's format as a guess and measure its true sample rate from the render cadence (frames/sec over a steady-state window, snapped to the nearest standard rate) before publishing it, deferring capture until verified. Discard the first measurement window so the buffer-fill burst at attach time doesn't over-count. Streams created after we inject still carry their exact Initialize format. Channels/bit-depth genuinely can't be recovered for a pre-existing client: AUTOCONVERTPCM hands GetBuffer a fixed staging buffer (no buffer stride to measure -- confirmed empirically) and WASAPI exposes no API for the format. They stay the device-mix guess, which is correct for the common case (engines render stereo float, matching the endpoint). To keep a wrong guess safe, a VirtualQuery clamp stops the capture copy from ever over-reading the source buffer when the guessed bytes/frame is too large. Surface all of this: a per-stream AudioFormatState (known / measuring / measured rate (ch/bits assumed)) in HookStatus, shown in the Audio panel for the hooked path and as "device endpoint (known)" for loopback; clear hook logs; and enriched mirror status strings. Documented in README (Limitations + Lessons learned). The loopback fallback was always correct (post-mix at the device format). Tests: extract a shared, configurable ToneSource (used by coop_tone and the hook self-test); coop_tone takes rate/channels/bits/format args. Rewrite audio_hook_test to a format matrix x both code paths -- see-init (exact) and guess (rate measured) -- plus a byte-incompatible guess that asserts the clamp keeps capture safe. The matrix caught the attach-burst over-count. audio_loopback_test now spawns coop_tone at several source formats to confirm loopback is format-agnostic. 11/11 x64 + 3/3 x86 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ namespace coop
|
||||
|
||||
// Bump whenever the layout of SharedBlock or CoopPadState changes. The hook
|
||||
// refuses to attach to a host with a mismatched version.
|
||||
inline constexpr std::uint32_t kProtocolVersion = 13;
|
||||
inline constexpr std::uint32_t kProtocolVersion = 14;
|
||||
|
||||
// 'COOP' little-endian, used to sanity-check the mapping before trusting it.
|
||||
inline constexpr std::uint32_t kProtocolMagic = 0x504F4F43u;
|
||||
@@ -51,6 +51,18 @@ inline constexpr std::uint32_t kMaxAudioStreams = 4;
|
||||
// POD (no atomics): diagnostics tolerate benign cross-process races like the
|
||||
// other HookStatus counters. frames_rendered is cumulative; the host derives
|
||||
// "live vs idle" from successive deltas.
|
||||
// How confidently the hook knows a render stream's format. A stream that existed before
|
||||
// we injected (the common case) was never seen at Initialize, so its format starts as a
|
||||
// guess (the device mix format) and its true sample rate is measured from the render
|
||||
// cadence; a stream we watched get created carries its exact Initialize format.
|
||||
enum AudioFormatState : std::uint32_t
|
||||
{
|
||||
AudioFormat_Unknown = 0, // no format determined yet
|
||||
AudioFormat_Exact = 1, // taken from the game's own IAudioClient::Initialize
|
||||
AudioFormat_Measuring = 2, // guessed (device mix format); true sample rate being measured
|
||||
AudioFormat_Measured = 3, // guessed rate measured; channels/bits assumed from the device
|
||||
};
|
||||
|
||||
struct AudioStreamInfo
|
||||
{
|
||||
std::uint32_t is_primary; // 1 = the stream the hook captures/silences
|
||||
@@ -59,6 +71,7 @@ struct AudioStreamInfo
|
||||
std::uint16_t bits;
|
||||
std::uint32_t format_tag; // WAVE_FORMAT_* of this stream
|
||||
std::uint64_t frames_rendered;
|
||||
std::uint32_t format_state; // AudioFormatState: how the format above was determined
|
||||
};
|
||||
|
||||
// Orthogonal hook subsystems the host can install/remove independently.
|
||||
|
||||
Reference in New Issue
Block a user