// Integration test for the two-path audio-format verifier (host/src/audio/audio_format_verifier). // // Launches coop_mock_game rendering a tone at a NON-device rate (44100 on a typical 48000 endpoint, // the Godot/Brotato case), injects coop_hook.dll late (so the stream is a *guess*), then 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. Asserts it // recovers the true 44100 Hz rate -- the cadence method's hard case. Skips cleanly without an audio // endpoint / if Vulkan-free... (only needs WASAPI + a D3D11-capable mock, which the mock always is). #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; } } // namespace int main() { kill_stray_mock_games(); const bool com = SUCCEEDED(CoInitializeEx(nullptr, COINIT_MULTITHREADED)); // Render the mock at the device's CHANNEL count (so this step-(a) rate test isn't perturbed by // a channel mismatch -- that's step (b)'s job) but at a DIFFERENT standard rate than the device, // so the verifier has a real rate to recover. Default to 48000/2ch if we can't read the device. 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 game_rate = (dev_rate == 44100) ? 48000u : 44100u; // guarantee a rate mismatch std::printf(" device %u Hz / %u ch -> rendering the mock at %u Hz / %u ch (rate mismatch)\n", dev_rate, dev_channels, game_rate, dev_channels); const std::wstring exe = exe_directory() + L"coop_mock_game.exe"; std::wstring cmd = L"\"" + exe + L"\" dx11 30 " + std::to_wstring(game_rate) + L" " + std::to_wstring(dev_channels) + L" 32 float"; STARTUPINFOW si{}; si.cb = sizeof(si); PROCESS_INFORMATION pi{}; if (!CreateProcessW(exe.c_str(), cmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) { std::printf("Could not launch coop_mock_game -- skipping audio_verify_test.\n"); if (com) { CoUninitialize(); } return 0; } auto cleanup = [&] { TerminateProcess(pi.hProcess, 0); WaitForSingleObject(pi.hProcess, 2000); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); kill_stray_mock_games(); }; Sleep(800); // window + audio client up // IPC + the primary audio ring the hook produces into. SharedMemory shm; if (!shm.create(shared_memory_name(pi.dwProcessId), sizeof(SharedBlock))) { std::printf("Could not create IPC block -- skipping.\n"); cleanup(); return 0; } auto* block = shm.as(); block->version = kProtocolVersion; block->pad_count = 0; block->sequence.store(0, std::memory_order_relaxed); // Only the audio subsystem. for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) { const bool off = s != HookSubsys_Audio; block->control.subsystem_disabled[s].store(off ? 1u : 0u, std::memory_order_release); } block->magic = kProtocolMagic; SharedMemory ring_shm; if (!ring_shm.create(audio_ring_name(pi.dwProcessId), audio_ring_total_size(kAudioRingCapacity))) { std::printf("Could not create audio ring -- skipping.\n"); cleanup(); return 0; } auto* ring = ring_shm.as(); audio_ring_init(*ring, kAudioRingCapacity); // Leave capture_enabled = 0: we want the stream audible (so loopback hears it) and still being // MEASURED (so the verify tap fires), exactly the window verify_stream_format targets. if (!inject_retry(pi.dwProcessId)) { std::printf("Could not inject coop_hook.dll -- skipping.\n"); cleanup(); return 0; } Sleep(500); // let the hook attach + the pre-existing render client register as a guess // Run the real verifier: co-capture hook (pre-mix) + loopback (post-mix) and correlate. const FormatVerification fv = verify_stream_format(pi.dwProcessId, ring, /*window_ms=*/1400); std::printf(" verify: ok=%d rate=%u score=%.3f\n", fv.ok ? 1 : 0, fv.rate, fv.score); if (!fv.ok && fv.rate == 0 && fv.score == 0.0) { // No audio endpoint, or no usable audio captured (e.g. the mock's WASAPI client never // started on this machine) -> treat as a skip rather than a failure. std::printf(" no usable co-capture (no endpoint / silent) -- skipping audio_verify_test.\n"); if (com) { CoUninitialize(); } cleanup(); return 0; } check(fv.ok, "verifier confidently correlated the two capture paths"); check(fv.rate == game_rate, "verifier recovered the game's true rate (not the device rate)"); if (com) { CoUninitialize(); } cleanup(); 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; }