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:
2026-06-23 02:50:59 +02:00
parent 00244bcfd7
commit 7ada550930
9 changed files with 554 additions and 138 deletions

View File

@@ -203,6 +203,15 @@ inline std::uint32_t audio_ring_available(const AudioRingHeader& h)
return static_cast<std::uint32_t>(w - r);
}
// Producer: bytes currently free (so a multi-part packet can be checked to fit before any of it is
// written -- keeps a self-describing [header][payload] framing from tearing on a full ring).
inline std::uint32_t audio_ring_free_space(const AudioRingHeader& h)
{
const std::uint64_t w = h.write_pos.load(std::memory_order_relaxed);
const std::uint64_t r = h.read_pos.load(std::memory_order_acquire);
return h.capacity - static_cast<std::uint32_t>(w - r);
}
// Consumer: copy up to `bytes` into `dst`; returns the number actually popped.
inline std::uint32_t audio_ring_pop(AudioRingHeader& h, void* dst, std::uint32_t bytes)
{