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

@@ -7,6 +7,7 @@
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <windows.h>
@@ -87,6 +88,13 @@ public:
client_->GetBufferSize(&buffer_frames_);
step_ = kTwoPi * freq_hz / static_cast<double>(fmt_.rate);
// Optional: give each channel genuinely different content (a per-channel frequency scale),
// so a downstream test can *recover* the channel count by correlation (identical channels
// are ambiguous: 2ch@R looks like 1ch@2R). Off by default -> the usual single-tone source.
if (const char* d = std::getenv("COOP_TONE_DISTINCT_CH"); d != nullptr && d[0] == '1')
{
distinct_ = true;
}
write(buffer_frames_); // pre-roll
client_->Start();
return true;
@@ -220,13 +228,24 @@ private:
}
for (unsigned c = 0; c < fmt_.channels; ++c)
{
double sc = s;
if (distinct_ && c < 8)
{
// Each channel at its own frequency scale -> genuinely different content.
sc = std::sin(phase_c_[c]) * 0.25;
phase_c_[c] += step_ * (1.0 + 0.37 * static_cast<double>(c));
if (phase_c_[c] > kTwoPi)
{
phase_c_[c] -= kTwoPi;
}
}
if (float_)
{
reinterpret_cast<float*>(data)[i * fmt_.channels + c] = static_cast<float>(s);
reinterpret_cast<float*>(data)[i * fmt_.channels + c] = static_cast<float>(sc);
}
else
{
reinterpret_cast<INT16*>(data)[i * fmt_.channels + c] = static_cast<INT16>(s * 32767.0);
reinterpret_cast<INT16*>(data)[i * fmt_.channels + c] = static_cast<INT16>(sc * 32767.0);
}
}
}
@@ -243,6 +262,8 @@ private:
bool float_ = false;
double phase_ = 0.0;
double step_ = 0.0;
bool distinct_ = false; // per-channel distinct content (recoverable channel count)
double phase_c_[8] = {}; // per-channel phase when distinct_
};
} // namespace coop::tone