// In-process self-test for the WASAPI render-hook (hook/src/audio_hook.cpp). // This process plays both "game" and "hook": it installs the audio hooks, then // renders a sine tone through WASAPI exactly like a game would. With the hooks // live, that render path must (1) be discovered via the COM vtables, (2) copy // the rendered frames into the shared audio ring (non-silent), (3) silence the // local output, and (4) report exactly one render stream. No second Steam // account, no real game. Exits 0 on pass, 1 on failure. // // Requires a working default render endpoint; on a headless machine it reports // SKIP and exits 0 (mirrors audio_loopback_test). #include #include #include #include #include #include #include #include #include "audio_hook.hpp" #include "coop/audio_ring.hpp" #include "coop/protocol.hpp" #include "coop/shared_memory.hpp" #include "ipc_client.hpp" using namespace coop; namespace { constexpr double kPi = 3.14159265358979323846; int g_failures = 0; void check(bool ok, const char* what) { if (!ok) { std::printf(" FAIL: %s\n", what); ++g_failures; } } template void release(T*& p) { if (p) { p->Release(); p = nullptr; } } } // namespace int main() { if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))) { std::printf("FAIL: CoInitializeEx\n"); return 1; } // --- Host side: create the IPC SharedBlock (named by our pid) so the hook's // IpcClient can connect, and a producer audio ring with capture enabled. SharedMemory shm; if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock))) { std::printf("FAIL: create shared memory\n"); return 1; } auto* block = shm.as(); // mapping is zero-initialized by the OS block->version = kProtocolVersion; block->sequence.store(0, std::memory_order_relaxed); block->magic = kProtocolMagic; std::vector ring_storage(audio_ring_total_size(kAudioRingCapacity), 0); auto* ring = new (ring_storage.data()) AudioRingHeader(); audio_ring_init(*ring, kAudioRingCapacity); ring->capture_enabled.store(1, std::memory_order_relaxed); hook::IpcClient ipc; check(ipc.connect(10, 5), "IPC client connect"); // --- Install the render hooks BEFORE any audio client is created. --- if (!hook::install_audio_hooks(ipc, ring)) { std::printf("SKIP: could not install audio hooks (no default render endpoint?)\n"); CoUninitialize(); return 0; } // --- Game side: render a tone through WASAPI (the coop_tone render path). --- IMMDeviceEnumerator* enumerator = nullptr; IMMDevice* endpoint = nullptr; IAudioClient* client = nullptr; IAudioRenderClient* render = nullptr; WAVEFORMATEX* fmt = nullptr; HANDLE buffer_event = nullptr; bool rendered = false; do { if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), reinterpret_cast(&enumerator)))) { break; } if (FAILED(enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &endpoint))) { break; } if (FAILED(endpoint->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, reinterpret_cast(&client)))) { break; } if (FAILED(client->GetMixFormat(&fmt))) { break; } buffer_event = CreateEventW(nullptr, FALSE, FALSE, nullptr); constexpr REFERENCE_TIME kBuffer = 30 * 10000; // 30 ms if (FAILED(client->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, kBuffer, 0, fmt, nullptr))) { break; } client->SetEventHandle(buffer_event); if (FAILED(client->GetService(__uuidof(IAudioRenderClient), reinterpret_cast(&render)))) { break; } UINT32 buffer_frames = 0; client->GetBufferSize(&buffer_frames); const bool is_float = fmt->wFormatTag == WAVE_FORMAT_IEEE_FLOAT || (fmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE && reinterpret_cast(fmt)->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT); const unsigned channels = fmt->nChannels; const double rate = fmt->nSamplesPerSec; const double step = 2.0 * kPi * 440.0 / rate; auto write_frames = [&](UINT32 frames, double& phase) { BYTE* data = nullptr; if (frames == 0 || FAILED(render->GetBuffer(frames, &data))) { return; } for (UINT32 i = 0; i < frames; ++i) { const double s = std::sin(phase) * 0.25; phase += step; if (phase > 2.0 * kPi) { phase -= 2.0 * kPi; } for (unsigned c = 0; c < channels; ++c) { if (is_float) { reinterpret_cast(data)[i * channels + c] = static_cast(s); } else { reinterpret_cast(data)[i * channels + c] = static_cast(s * 32767.0); } } } render->ReleaseBuffer(frames, 0); }; double phase = 0.0; write_frames(buffer_frames, phase); // pre-roll client->Start(); const DWORD end_tick = GetTickCount() + 800; // ~0.8 s of rendering while (GetTickCount() < end_tick) { if (WaitForSingleObject(buffer_event, 200) != WAIT_OBJECT_0) { continue; } UINT32 padding = 0; if (FAILED(client->GetCurrentPadding(&padding))) { break; } write_frames(buffer_frames - padding, phase); } client->Stop(); rendered = true; } while (false); if (!rendered) { std::printf("SKIP: could not render through WASAPI on this machine\n"); release(render); release(client); release(endpoint); release(enumerator); if (fmt) { CoTaskMemFree(fmt); } if (buffer_event) { CloseHandle(buffer_event); } hook::remove_audio_hooks(); CoUninitialize(); return 0; } // --- Assertions: the hook discovered and intercepted the render path. --- std::printf("streams_seen=%u, frames_captured=%llu, ring frames_produced=%llu\n", hook::audio_streams_seen(), static_cast(hook::audio_frames_captured()), static_cast(ring->frames_produced.load())); check(hook::audio_streams_seen() == 1, "exactly one render stream observed"); check(block->status.audio_streams_seen == 1, "stream count published to HookStatus"); check(block->status.audio_streams[0].is_primary == 1, "slot 0 marked primary"); check(block->status.audio_streams[0].sample_rate == fmt->nSamplesPerSec, "primary sample rate published"); check(block->status.audio_streams[0].frames_rendered > 0, "primary frames_rendered advancing"); check(ring->frames_produced.load() > 0, "frames pushed to the audio ring"); check(hook::audio_frames_captured() > 0, "frames captured + silenced"); // The ring must hold the actual (non-silent) tone we rendered. { std::vector buf(64 * 1024, 0); const std::uint32_t got = audio_ring_pop(*ring, buf.data(), static_cast(buf.size())); bool nonsilent = false; for (std::uint32_t i = 0; i < got; ++i) { if (buf[i] != 0) { nonsilent = true; break; } } check(got > 0 && nonsilent, "ring carries non-silent captured audio"); } release(render); release(client); release(endpoint); release(enumerator); if (fmt) { CoTaskMemFree(fmt); } if (buffer_event) { CloseHandle(buffer_event); } hook::remove_audio_hooks(); CoUninitialize(); std::printf(g_failures == 0 ? "AUDIO HOOK TEST PASS\n" : "AUDIO HOOK TEST FAILED (%d)\n", g_failures); return g_failures == 0 ? 0 : 1; }