Files
CoopAllTheThings/tests/audio_verify_test.cpp
BlackMark 7ada550930 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>
2026-06-23 02:50:59 +02:00

266 lines
7.9 KiB
C++

// Integration test for the two-path audio-format verifier (host/src/audio/audio_format_verifier).
//
// Launches coop_mock_game rendering a tone via WASAPI AUTOCONVERTPCM, injects coop_hook.dll late
// (so the stream is a *guess*), and runs the real verify_stream_format(): it co-captures the hook
// (pre-mix, via the ring's verify tap) and a parallel process-loopback (post-mix, device format)
// and cross-correlates them. Two scenarios:
// (a) rate: render at the device's channel count but a different rate -> recover the rate.
// (b) layout: render a DIFFERENT channel count than the device, with distinct per-channel content
// -> recover channels + bit depth + rate.
// Skips cleanly without an audio endpoint.
#include <cstdint>
#include <cstdio>
#include <string>
#include <windows.h>
#include <mmreg.h>
#include <objbase.h>
#include <tlhelp32.h>
#include "audio/audio_format_verifier.hpp"
#include "audio/process_loopback_capture.hpp" // default_render_format
#include "coop/audio_ring.hpp"
#include "coop/protocol.hpp"
#include "coop/shared_memory.hpp"
#include "coop/tool_paths.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;
}
}
void kill_stray_mock_games()
{
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE)
{
return;
}
PROCESSENTRY32W pe{};
pe.dwSize = sizeof(pe);
for (BOOL ok = Process32FirstW(snap, &pe); ok; ok = Process32NextW(snap, &pe))
{
if (_wcsicmp(pe.szExeFile, L"coop_mock_game.exe") == 0)
{
if (HANDLE h = OpenProcess(PROCESS_TERMINATE, FALSE, pe.th32ProcessID))
{
TerminateProcess(h, 0);
CloseHandle(h);
}
}
}
CloseHandle(snap);
}
bool inject(unsigned long pid)
{
const std::wstring dll = deployed_artifact_path(L"coop_hook.dll");
if (GetFileAttributesW(dll.c_str()) == INVALID_FILE_ATTRIBUTES)
{
return false;
}
const DWORD access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION |
PROCESS_VM_WRITE | PROCESS_VM_READ;
HANDLE process = OpenProcess(access, FALSE, pid);
if (process == nullptr)
{
return false;
}
const SIZE_T bytes = (dll.size() + 1) * sizeof(wchar_t);
void* remote = VirtualAllocEx(process, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
bool ok = false;
if (remote != nullptr && WriteProcessMemory(process, remote, dll.c_str(), bytes, nullptr))
{
auto load = reinterpret_cast<LPTHREAD_START_ROUTINE>(
GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW"));
if (HANDLE th = CreateRemoteThread(process, nullptr, 0, load, remote, 0, nullptr))
{
WaitForSingleObject(th, INFINITE);
DWORD code = 0;
GetExitCodeThread(th, &code);
CloseHandle(th);
ok = code != 0;
}
}
if (remote != nullptr)
{
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
}
CloseHandle(process);
return ok;
}
bool inject_retry(unsigned long pid)
{
for (int i = 0; i < 4; ++i)
{
if (inject(pid))
{
return true;
}
Sleep(300);
}
return false;
}
struct Scenario
{
unsigned rate, channels, bits;
bool distinct; // distinct per-channel content (so the channel count is recoverable)
bool recover_layout; // false = rate only (step a); true = full layout (step b)
};
// Launch the mock at the scenario's format, inject the hook late, and run the verifier. `ran` is
// set false when the environment can't support the test (launch/inject failed) so the caller skips.
FormatVerification run(const Scenario& sc, bool& ran)
{
ran = false;
FormatVerification fv;
kill_stray_mock_games();
if (sc.distinct)
{
SetEnvironmentVariableW(L"COOP_TONE_DISTINCT_CH", L"1");
}
const std::wstring exe = exe_directory() + L"coop_mock_game.exe";
std::wstring cmd = L"\"" + exe + L"\" dx11 30 " + std::to_wstring(sc.rate) + L" " +
std::to_wstring(sc.channels) + L" " + std::to_wstring(sc.bits) + L" " +
(sc.bits == 16 ? L"pcm" : L"float");
STARTUPINFOW si{};
si.cb = sizeof(si);
PROCESS_INFORMATION pi{};
const BOOL launched = CreateProcessW(exe.c_str(), cmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr,
&si, &pi);
if (sc.distinct)
{
SetEnvironmentVariableW(L"COOP_TONE_DISTINCT_CH", nullptr);
}
if (!launched)
{
return fv;
}
auto cleanup = [&] {
TerminateProcess(pi.hProcess, 0);
WaitForSingleObject(pi.hProcess, 2000);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
kill_stray_mock_games();
};
Sleep(800);
SharedMemory shm;
SharedMemory ring_shm;
if (!shm.create(shared_memory_name(pi.dwProcessId), sizeof(SharedBlock)) ||
!ring_shm.create(audio_ring_name(pi.dwProcessId), audio_ring_total_size(kAudioRingCapacity)))
{
cleanup();
return fv;
}
auto* block = shm.as<SharedBlock>();
block->version = kProtocolVersion;
block->pad_count = 0;
block->sequence.store(0, std::memory_order_relaxed);
for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) // audio subsystem only
{
block->control.subsystem_disabled[s].store(s != HookSubsys_Audio ? 1u : 0u, std::memory_order_release);
}
block->magic = kProtocolMagic;
auto* ring = ring_shm.as<AudioRingHeader>();
audio_ring_init(*ring, kAudioRingCapacity); // capture_enabled stays 0: audible + still measuring
if (!inject_retry(pi.dwProcessId))
{
cleanup();
return fv;
}
Sleep(500); // hook attaches + the pre-existing render client registers as a guess
fv = verify_stream_format(pi.dwProcessId, ring, /*window_ms=*/1400, sc.recover_layout);
ran = true;
cleanup();
return fv;
}
} // namespace
int main()
{
const bool com = SUCCEEDED(CoInitializeEx(nullptr, COINIT_MULTITHREADED));
unsigned dev_rate = 48000, dev_channels = 2;
if (WAVEFORMATEX* dev = default_render_format())
{
dev_rate = dev->nSamplesPerSec;
dev_channels = dev->nChannels;
CoTaskMemFree(dev);
}
const unsigned mismatched = (dev_rate == 44100) ? 48000u : 44100u; // guarantee a rate mismatch
std::printf("device: %u Hz / %u ch\n", dev_rate, dev_channels);
// (a) Rate: render at the device's channel count (no layout mismatch) but a different rate.
std::printf("== (a) rate recovery: %u Hz / %u ch ==\n", mismatched, dev_channels);
bool ran = false;
FormatVerification a = run({mismatched, dev_channels, 32, /*distinct=*/false, /*recover_layout=*/false}, ran);
if (!ran)
{
std::printf(" environment can't run the mock+inject -- skipping audio_verify_test.\n");
if (com)
{
CoUninitialize();
}
return 0;
}
std::printf(" ok=%d rate=%u score=%.3f\n", a.ok ? 1 : 0, a.rate, a.score);
if (!a.ok && a.rate == 0 && a.score == 0.0)
{
std::printf(" no usable co-capture (no endpoint / silent) -- skipping.\n");
if (com)
{
CoUninitialize();
}
return 0;
}
check(a.ok, "(a) verifier confidently correlated the two capture paths");
check(a.rate == mismatched, "(a) recovered the game's true rate (not the device rate)");
// (b) Layout: render 2ch with distinct per-channel content -- a layout that differs from a
// multichannel device -- and recover channels + bit depth + rate.
std::printf("== (b) layout recovery: 44100 Hz / 2 ch / 32-bit float (distinct channels) ==\n");
FormatVerification b = run({44100, 2, 32, /*distinct=*/true, /*recover_layout=*/true}, ran);
std::printf(" ok=%d rate=%u ch=%u bits=%u tag=%u score=%.3f\n", b.ok ? 1 : 0, b.rate, b.channels, b.bits,
b.format_tag, b.score);
if (b.ok || b.score > 0.0)
{
check(b.layout_ok, "(b) verifier confidently recovered the layout");
check(b.rate == 44100, "(b) recovered the true rate");
check(b.channels == 2, "(b) recovered the true channel count (2, not the device's)");
check(b.bits == 32 && b.format_tag == 3, "(b) recovered 32-bit float");
}
else
{
std::printf(" no usable co-capture for (b) -- skipping that scenario.\n");
}
if (com)
{
CoUninitialize();
}
if (g_failures == 0)
{
std::printf("PASS audio_verify_test\n");
return 0;
}
std::printf("FAILED audio_verify_test (%d)\n", g_failures);
return 1;
}