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>
This commit is contained in:
@@ -99,6 +99,35 @@ void drain_ring(AudioRingHeader& ring, std::vector<BYTE>& scratch)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the hook's self-describing verify stream -- a sequence of [u32 frame-count][count*stride
|
||||
// bytes] chunks -- into a ChunkedCapture. `stride` is the device block_align (what the hook padded
|
||||
// each buffer to). Stops at the first truncated/garbled record.
|
||||
ChunkedCapture parse_chunks(const std::vector<BYTE>& raw, unsigned stride)
|
||||
{
|
||||
ChunkedCapture cap;
|
||||
cap.stride = stride;
|
||||
if (stride == 0)
|
||||
{
|
||||
return cap;
|
||||
}
|
||||
std::size_t off = 0;
|
||||
while (off + sizeof(std::uint32_t) <= raw.size())
|
||||
{
|
||||
std::uint32_t count = 0;
|
||||
std::memcpy(&count, raw.data() + off, sizeof(count));
|
||||
off += sizeof(count);
|
||||
const std::size_t payload = static_cast<std::size_t>(count) * stride;
|
||||
if (count == 0 || off + payload > raw.size())
|
||||
{
|
||||
break; // truncated or garbled -> stop
|
||||
}
|
||||
cap.counts.push_back(count);
|
||||
cap.bytes.insert(cap.bytes.end(), raw.data() + off, raw.data() + off + payload);
|
||||
off += payload;
|
||||
}
|
||||
return cap;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FormatVerification verify_stream_format(DWORD pid, AudioRingHeader* ring, unsigned window_ms, bool recover_layout)
|
||||
@@ -156,31 +185,53 @@ FormatVerification verify_stream_format(DWORD pid, AudioRingHeader* ring, unsign
|
||||
ring->verify_capture.store(0, std::memory_order_release);
|
||||
drain_ring(*ring, scratch); // leave the ring clean for the real capture that follows
|
||||
|
||||
// The hook bytes are at the guessed layout = the device channels/bits (the assumption the
|
||||
// cadence path also makes). Decode both captures with that layout and correlate.
|
||||
const std::vector<float> hook_mono = to_mono(hook_bytes, dev);
|
||||
// The loopback is at the KNOWN device layout. Decode it to mono ground truth. The hook stream
|
||||
// is self-describing chunks ([count][padded payload]); parse them at the device block.
|
||||
const std::vector<float> loop_mono = to_mono(loop_bytes, dev);
|
||||
const unsigned dev_block = dev_wfx->nBlockAlign;
|
||||
const ChunkedCapture cap = parse_chunks(hook_bytes, dev_block);
|
||||
const std::size_t need = dev.rate / 5; // require >= ~200 ms of usable audio on both sides
|
||||
const std::size_t hook_frames = dev_block != 0 ? cap.bytes.size() / dev_block : 0;
|
||||
CoTaskMemFree(dev_wfx);
|
||||
|
||||
if (const char* dbg = std::getenv("COOP_VERIFY_DEBUG"); dbg != nullptr && dbg[0] == '1')
|
||||
{
|
||||
std::fprintf(stderr, "[verify] dev=%uHz/%uch/%ubit tag=%u hook_frames=%zu loop_frames=%zu\n", dev.rate,
|
||||
dev.channels, dev.bits, dev.tag, hook_mono.size(), loop_mono.size());
|
||||
std::fprintf(stderr, "[verify] dev=%uHz/%uch/%ubit blk=%u chunks=%zu hook_frames=%zu loop=%zu layout=%d\n",
|
||||
dev.rate, dev.channels, dev.bits, dev_block, cap.counts.size(), hook_frames, loop_mono.size(),
|
||||
recover_layout ? 1 : 0);
|
||||
}
|
||||
const std::size_t need = dev.rate / 5; // require >= ~200 ms of usable audio on both sides
|
||||
if (hook_mono.size() < need || loop_mono.size() < need)
|
||||
if (loop_mono.size() < need || hook_frames < need)
|
||||
{
|
||||
return result; // not enough non-silent audio captured (game quiet, or stream wasn't a guess)
|
||||
}
|
||||
|
||||
const RateCorrelation rc = correlate_rate(hook_mono, loop_mono, dev.rate, standard_audio_rates());
|
||||
result.ok = rc.ok;
|
||||
result.rate = rc.rate;
|
||||
result.score = rc.score;
|
||||
// Channels/bit-depth stay the device assumption here; step (b) recovers them.
|
||||
result.channels = dev.channels;
|
||||
result.bits = dev.bits;
|
||||
result.format_tag = dev.tag;
|
||||
if (recover_layout)
|
||||
{
|
||||
// Step (b): recover channels + bit depth too, by trying candidate de-interleavings of the
|
||||
// (de-padded) hook bytes and keeping whichever (layout, rate) correlates with the loopback.
|
||||
const FormatCorrelation fc =
|
||||
correlate_format(cap, loop_mono, dev.rate, standard_audio_rates(), standard_audio_layouts());
|
||||
result.ok = fc.ok;
|
||||
result.rate = fc.rate;
|
||||
result.score = fc.score;
|
||||
result.layout_ok = fc.ok;
|
||||
result.channels = fc.channels;
|
||||
result.bits = fc.bits;
|
||||
result.format_tag = fc.tag;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Step (a): rate only, assuming the hook layout matches the device (common stereo case), so
|
||||
// the de-padded payload is already clean device-layout audio.
|
||||
const std::vector<float> hook_mono = to_mono(cap.bytes, dev);
|
||||
const RateCorrelation rc = correlate_rate(hook_mono, loop_mono, dev.rate, standard_audio_rates());
|
||||
result.ok = rc.ok;
|
||||
result.rate = rc.rate;
|
||||
result.score = rc.score;
|
||||
result.channels = dev.channels;
|
||||
result.bits = dev.bits;
|
||||
result.format_tag = dev.tag;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user