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>
This commit is contained in:
@@ -391,6 +391,30 @@ HRESULT STDMETHODCALLTYPE hk_ReleaseBuffer(IAudioRenderClient* self, UINT32 num_
|
||||
}
|
||||
}
|
||||
}
|
||||
// Format-verification co-capture (host-driven, step 2a). While the host has set
|
||||
// verify_capture and this guessed stream's rate is still being MEASURED (format not yet
|
||||
// published), push the raw pre-mix bytes to the ring WITHOUT silencing, so the host can
|
||||
// capture both the hook (pre-mix) and a parallel process-loopback (post-mix) of the same
|
||||
// audio and cross-correlate them to recover the true format from ground truth. The game
|
||||
// stays audible (the host runs loopback during the measurement window anyway), and the
|
||||
// shipping no-echo capture/silence path above is left completely untouched.
|
||||
if (num_frames > 0 && (flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0)
|
||||
{
|
||||
AudioRingHeader* vring = g_rings[i].load(std::memory_order_acquire);
|
||||
if (vring != nullptr && vring->verify_capture.load(std::memory_order_relaxed) != 0 &&
|
||||
!audio_ring_format_ready(*vring) &&
|
||||
g_streams[i].assumed_format.load(std::memory_order_relaxed) != 0 && t_gb_client == self &&
|
||||
t_gb_data != nullptr && t_gb_frames == num_frames &&
|
||||
t_gb_epoch == g_hook_epoch.load(std::memory_order_acquire))
|
||||
{
|
||||
const std::uint32_t block = g_streams[i].block_align.load(std::memory_order_relaxed);
|
||||
if (block != 0)
|
||||
{
|
||||
audio_ring_push(*vring, t_gb_data, readable_bytes(t_gb_data, num_frames * block),
|
||||
num_frames);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
return g_vh_releasebuffer.original<ReleaseBufferFn>()(self, num_frames, flags);
|
||||
|
||||
Reference in New Issue
Block a user