// Integration test for WASAPI process-loopback capture (the audio mirror's fallback // backend). Spawns coop_tone.exe rendering a sine wave at several source formats and // verifies non-silent audio actually arrives for each. Loopback captures the game's audio // *post-mix* at the device endpoint format, so it is format-agnostic by construction -- // whatever rate/channels the source renders, the captured audio is correct at the device // rate. This test confirms that for the common source formats. Exits 0 on pass, 1 on fail. // // Requires a working default render endpoint; on a headless machine it reports SKIP and // exits 0. #include #include #include #include #include #include #include #include "audio/process_loopback_capture.hpp" using namespace coop; namespace { // Directory of this test executable (coop_tone.exe is built alongside it). std::wstring exe_dir() { wchar_t path[MAX_PATH] = {}; GetModuleFileNameW(nullptr, path, MAX_PATH); std::wstring s(path); const size_t slash = s.find_last_of(L"\\/"); return slash == std::wstring::npos ? L"." : s.substr(0, slash); } // Read from `pipe` until `token` appears or `timeout_ms` elapses (echoing to stdout). bool wait_for_token(HANDLE pipe, const char* token, DWORD timeout_ms) { std::string acc; const DWORD end = GetTickCount() + timeout_ms; while (GetTickCount() < end) { DWORD avail = 0; if (PeekNamedPipe(pipe, nullptr, 0, nullptr, &avail, nullptr) && avail > 0) { char buf[256]; DWORD read = 0; if (ReadFile(pipe, buf, sizeof(buf) - 1, &read, nullptr) && read > 0) { acc.append(buf, read); std::fwrite(buf, 1, read, stdout); if (acc.find(token) != std::string::npos) { return true; } continue; } } Sleep(20); } return false; } // Spawn coop_tone with `tone_args` and capture its audio via process loopback for ~1.5 s. // Returns true if enough non-silent audio arrived (i.e. loopback handled this source // format correctly). `endpoint_fmt` is the device format loopback renders into. bool capture_one(const WAVEFORMATEX* endpoint_fmt, const std::wstring& tone_args, const char* label) { std::printf("--- %s ---\n", label); HANDLE read_pipe = nullptr; HANDLE write_pipe = nullptr; SECURITY_ATTRIBUTES sa = {sizeof(sa), nullptr, TRUE}; if (!CreatePipe(&read_pipe, &write_pipe, &sa, 0)) { std::printf("FAIL: CreatePipe\n"); return false; } SetHandleInformation(read_pipe, HANDLE_FLAG_INHERIT, 0); std::wstring cmd = L"\"" + exe_dir() + L"\\coop_tone.exe\" " + tone_args; STARTUPINFOW si = {}; si.cb = sizeof(si); si.dwFlags = STARTF_USESTDHANDLES; si.hStdOutput = write_pipe; si.hStdError = write_pipe; PROCESS_INFORMATION pi = {}; std::vector cmd_buf(cmd.begin(), cmd.end()); cmd_buf.push_back(L'\0'); if (!CreateProcessW(nullptr, cmd_buf.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &si, &pi)) { std::printf("FAIL: CreateProcess(coop_tone) err=%lu\n", GetLastError()); CloseHandle(read_pipe); CloseHandle(write_pipe); return false; } CloseHandle(write_pipe); // keep only the read end bool ok = false; if (!wait_for_token(read_pipe, "TONE_RENDERING", 5000)) { std::printf("FAIL: tone generator never started rendering\n"); } else { ProcessLoopbackCapture capture; const bool started = capture.start(pi.dwProcessId, endpoint_fmt, nullptr); std::printf("Capture start: %s, targeting pid %lu\n", started ? "ok" : "FAILED", pi.dwProcessId); Sleep(1500); const auto nonsilent = capture.nonsilent_frames(); capture.stop(); // Expect at least ~0.2 s of non-silent audio for a 1.5 s capture. const unsigned long long need = endpoint_fmt->nSamplesPerSec / 5; std::printf("Non-silent frames: %llu (need >= %llu)\n", static_cast(nonsilent), need); ok = nonsilent >= need; std::printf("%s\n", ok ? "PASS" : "FAIL: too few non-silent frames"); } TerminateProcess(pi.hProcess, 0); WaitForSingleObject(pi.hProcess, 2000); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); CloseHandle(read_pipe); return ok; } } // namespace int main() { if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))) { std::printf("FAIL: CoInitializeEx\n"); return 1; } WAVEFORMATEX* fmt = default_render_format(); if (!fmt) { std::printf("SKIP: no default render endpoint (no audio device?)\n"); CoUninitialize(); return 0; } std::printf("Endpoint format: %u Hz, %u ch, %u-bit\n", fmt->nSamplesPerSec, fmt->nChannels, fmt->wBitsPerSample); // Each case spawns coop_tone at a different source format; loopback should capture all // of them correctly because it captures post-mix at the device endpoint format. // Args: . ~8 s outlives capture. struct Case { std::wstring args; const char* label; }; const Case cases[] = { {L"8 440", "device default format"}, {L"8 440 44100 2 16 pcm", "44100 Hz stereo 16-bit PCM"}, {L"8 440 48000 2 32 float", "48000 Hz stereo 32-bit float"}, {L"8 660 96000 2 32 float", "96000 Hz stereo 32-bit float"}, }; int failures = 0; for (const Case& c : cases) { if (!capture_one(fmt, c.args, c.label)) { ++failures; } } CoTaskMemFree(fmt); CoUninitialize(); std::printf(failures == 0 ? "AUDIO LOOPBACK TEST PASS\n" : "AUDIO LOOPBACK TEST FAILED (%d)\n", failures); return failures == 0 ? 0 : 1; }