// 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 #include #include #include #include #include #include #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( 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(); 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(); 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; }