// Unit test for the two-path audio-format correlator (common/include/coop/audio_correlate.hpp). // // Models the real situation: the same game audio is captured twice -- by the render-hook at the // stream's true (unknown) rate, and by process-loopback at the known device rate (the hook signal // resampled by WASAPI's AUTOCONVERTPCM). We synthesize one continuous signal and sample it at both // rates (plus a capture-latency skew and a little noise), then assert correlate_rate() recovers the // true rate -- including the hard 44100-vs-48000 case the cadence method can misread. Pure header // logic, no device. #include #include #include #include #include #include "coop/audio_correlate.hpp" using namespace coop; namespace { int g_failures = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); if (!ok) { ++g_failures; } } constexpr double kPi = 3.14159265358979323846; // A non-periodic, correlation-friendly continuous signal s(t): a couple of incommensurate tones // plus a slow chirp, so cross-correlation has a single sharp peak (unlike a pure sine). double source(double t) { const double chirp = std::sin(2.0 * kPi * (300.0 * t + 140.0 * t * t)); return 0.5 * std::sin(2.0 * kPi * 221.0 * t) + 0.28 * std::sin(2.0 * kPi * 437.0 * t + 0.6) + 0.22 * chirp; } // Sample s(t) at `rate` for `seconds`, starting at t0 (capture-latency skew), optionally adding // white noise of amplitude `noise` (the post-mix path is not a bit-identical copy). std::vector capture(unsigned rate, double seconds, double t0, double noise, std::uint32_t seed) { const std::size_t n = static_cast(rate * seconds); std::vector out(n); std::mt19937 rng(seed); std::uniform_real_distribution jitter(-1.0f, 1.0f); for (std::size_t i = 0; i < n; ++i) { const double t = t0 + static_cast(i) / static_cast(rate); out[i] = static_cast(source(t)) + static_cast(noise) * jitter(rng); } return out; } // One scenario: true hook rate `true_rate` mixed to `device_rate`. Assert the correlator picks // true_rate confidently and that the runner-up is clearly behind. void test_case(unsigned true_rate, unsigned device_rate, const char* label) { std::printf("== %s (true %u Hz -> device %u Hz) ==\n", label, true_rate, device_rate); // The hook captures at the true rate; the loopback captures the same signal at the device // rate, started ~22 ms later (capture skew) with a little measurement noise. const std::vector hook = capture(true_rate, 0.55, 0.0, 0.0, 1); const std::vector loop = capture(device_rate, 0.55, 0.022, 0.02, 7); const RateCorrelation r = correlate_rate(hook, loop, device_rate, standard_audio_rates()); std::printf(" picked %u Hz score=%.3f runner_up=%.3f ok=%d\n", r.rate, r.score, r.runner_up, r.ok ? 1 : 0); check(r.rate == true_rate, "correlator picked the true rate"); check(r.ok, "pick is confident (clears threshold + beats runner-up)"); check(r.score > r.runner_up, "winner scores above the runner-up"); } } // namespace int main() { // The headline case: Godot/Brotato render 44100 while the endpoint mixes 48000 -- the cadence // method can misread this, the correlator must not. test_case(44100, 48000, "godot/brotato case"); test_case(48000, 48000, "rate matches device"); test_case(96000, 48000, "high-rate stream"); test_case(32000, 44100, "low-rate stream on a 44100 endpoint"); test_case(48000, 44100, "48000 stream on a 44100 endpoint"); // Downmix sanity: a stereo interleaved buffer collapses to the same mono the scalar path uses. { std::printf("== downmix stereo -> mono ==\n"); std::vector stereo = {1.0f, 3.0f, 2.0f, 4.0f, -1.0f, 1.0f}; std::vector mono; correlate_detail::downmix(stereo.data(), 3, 2, mono); check(mono.size() == 3 && std::fabs(mono[0] - 2.0f) < 1e-6 && std::fabs(mono[1] - 3.0f) < 1e-6 && std::fabs(mono[2] - 0.0f) < 1e-6, "stereo frames average to mono"); } // A pure guess with no shared signal must NOT be reported confident (loopback is unrelated noise). { std::printf("== unrelated signals are not confidently matched ==\n"); const std::vector hook = capture(44100, 0.5, 0.0, 0.0, 1); std::vector noise(static_cast(48000 * 0.5)); std::mt19937 rng(99); std::uniform_real_distribution d(-1.0f, 1.0f); for (float& x : noise) { x = d(rng); } const RateCorrelation r = correlate_rate(hook, noise, 48000, standard_audio_rates()); std::printf(" picked %u Hz score=%.3f ok=%d\n", r.rate, r.score, r.ok ? 1 : 0); check(!r.ok, "unrelated loopback is not a confident match"); } if (g_failures == 0) { std::printf("PASS audio_correlation_test\n"); return 0; } std::printf("FAILED audio_correlation_test (%d)\n", g_failures); return 1; }