// Integration test for WASAPI process-loopback capture. Spawns coop_tone.exe (a // real process rendering a sine wave), captures its audio by PID, and verifies // non-silent audio actually arrives. Exits 0 on pass, 1 on failure. // // Requires a working default render endpoint; on a headless machine with no audio // device 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. 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; } } // 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); // --- Launch the tone generator with its stdout redirected to a pipe. --- 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 1; } SetHandleInformation(read_pipe, HANDLE_FLAG_INHERIT, 0); std::wstring cmd = L"\"" + exe_dir() + L"\\coop_tone.exe\" 8"; // ~8 s, outlives capture 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()); return 1; } CloseHandle(write_pipe); // keep only the read end int rc = 1; if (!wait_for_token(read_pipe, "TONE_RENDERING", 5000)) { std::printf("FAIL: tone generator never started rendering\n"); } else { // --- Capture the tone process's audio for ~2 s. --- ProcessLoopbackCapture capture; const bool started = capture.start(pi.dwProcessId, fmt, nullptr); std::printf("Capture start: %s, targeting pid %lu\n", started ? "ok" : "FAILED", pi.dwProcessId); Sleep(2000); const auto frames = capture.frames_captured(); const auto nonsilent = capture.nonsilent_frames(); const std::string status = capture.status(); capture.stop(); std::printf("Status: %s\n", status.c_str()); std::printf("Frames captured: %llu, non-silent: %llu\n", static_cast(frames), static_cast(nonsilent)); // Expect at least ~0.2 s of non-silent audio for a 2 s capture. const unsigned long long need = fmt->nSamplesPerSec / 5; if (nonsilent >= need) { std::printf("PASS: received %llu non-silent frames (need >= %llu)\n", static_cast(nonsilent), need); rc = 0; } else { std::printf("FAIL: too few non-silent frames (got %llu, need >= %llu)\n", static_cast(nonsilent), need); } } TerminateProcess(pi.hProcess, 0); WaitForSingleObject(pi.hProcess, 2000); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); CloseHandle(read_pipe); CoTaskMemFree(fmt); CoUninitialize(); return rc; }