Run clang-format (the repo's .clang-format: LLVM base, 120 cols, tabs, Allman functions) over every source file so the tree is formatter-clean. Whitespace only -- no behavior change; full x64 + x86 suites pass. Also set SortIncludes: false in .clang-format. Windows include order is load-bearing (windows.h must precede tlhelp32.h / mmreg.h / xinput.h / dinput.h; winsock2.h must precede windows.h), and the default alphabetical sort reorders tlhelp32.h ahead of windows.h -- a build break. Leaving order alone keeps the manual, correct grouping.
163 lines
5.3 KiB
C++
163 lines
5.3 KiB
C++
// 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 <cstdio>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <windows.h>
|
|
|
|
#include <audioclient.h>
|
|
#include <mmdeviceapi.h>
|
|
#include <mmreg.h>
|
|
|
|
#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<wchar_t> 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<unsigned long long>(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: <seconds> <freq> <rate> <channels> <bits> <float|pcm>. ~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;
|
|
}
|