// In-process self-test for the WASAPI render-hook (hook/src/audio_hook.cpp), focused on // audio-format detection. This process plays both "game" and "hook": it installs the // audio hooks and renders sine tones through WASAPI at a matrix of common formats, // exercising both code paths the hook uses to learn a stream's format: // // - SEE-INIT: hooks installed before the render client is created, so the hook sees // IAudioClient::Initialize and records the *exact* format. // - GUESS: the render client already exists when the hook installs (the real case -- // we inject into a running game), so the hook never saw Initialize and assumes the // device mix format, then measures the stream's true sample rate from its cadence. // // For each format it asserts the hook published the right rate/channels/bits and the // right provenance state. No game, no second Steam account. Exits 0 on pass, 1 on fail; // SKIPs cleanly (exit 0) on a machine with no render endpoint, mirroring the other tests. #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" #include "tone_source.hpp" using namespace coop; using coop::tone::ToneFormat; using coop::tone::ToneSource; namespace { int g_failures = 0; // Returns 1 (and logs) on failure, 0 on success -- so callers can sum a tally. int expect(bool ok, const char* what) { if (!ok) { std::printf(" FAIL: %s\n", what); ++g_failures; return 1; } return 0; } void reset_ring(AudioRingHeader* ring, SharedBlock* block) { audio_ring_init(*ring, kAudioRingCapacity); ring->capture_enabled.store(1, std::memory_order_release); block->status.audio_streams[0] = AudioStreamInfo{}; // clear stale provenance from the last case } // Drain the ring and report whether any non-silent sample landed in it (capture path). bool ring_has_nonsilent(AudioRingHeader* ring) { std::vector buf(128 * 1024, 0); const std::uint32_t got = audio_ring_pop(*ring, buf.data(), static_cast(buf.size())); for (std::uint32_t i = 0; i < got; ++i) { if (buf[i] != 0) { return true; } } return false; } const char* fmt_desc(const ToneFormat& f, char* buf, size_t n) { std::snprintf(buf, n, "%u Hz %u ch %u-bit %s", f.rate, f.channels, f.bits, f.is_float ? "float" : "pcm"); return buf; } // SEE-INIT: install hooks first, then create the render client so the hook records the // exact Initialize format. Verifies the full format and the Exact provenance. void test_see_init(hook::IpcClient& ipc, AudioRingHeader* ring, SharedBlock* block, const ToneFormat& want) { char d[64]; reset_ring(ring, block); if (!hook::install_audio_hooks(ipc, ring)) { std::printf(" SKIP see-init (audio hooks unavailable)\n"); return; } ToneSource tone; if (!tone.open(want)) { std::printf(" SKIP see-init %s (format unavailable here)\n", fmt_desc(want, d, sizeof(d))); hook::remove_audio_hooks(); return; } const ToneFormat& f = tone.format(); // Exact format publishes at registration; render briefly so capture fills the ring. const DWORD end = GetTickCount() + 300; while (GetTickCount() < end) { tone.render_step(50); } const AudioStreamInfo& s = block->status.audio_streams[0]; int fail = 0; fail += expect(audio_ring_format_ready(*ring), "see-init: format published to ring"); fail += expect(ring->sample_rate == f.rate, "see-init: ring rate == exact rate"); fail += expect(ring->channels == f.channels, "see-init: ring channels == exact channels"); fail += expect(ring->bits == f.bits, "see-init: ring bits == exact bits"); fail += expect(s.sample_rate == f.rate, "see-init: HookStatus rate == exact rate"); fail += expect(s.format_state == AudioFormat_Exact, "see-init: provenance == Exact"); fail += expect(ring_has_nonsilent(ring), "see-init: non-silent audio captured"); std::printf(" %s see-init %s -> ring %uHz/%uch/%ubit state=%u\n", fail == 0 ? "PASS" : "FAIL", fmt_desc(f, d, sizeof(d)), ring->sample_rate, ring->channels, ring->bits, s.format_state); tone.close(); hook::remove_audio_hooks(); } // GUESS: create the render client first (its Initialize is unseen), then install hooks so // the stream is discovered lazily and assigned the device mix format -- whose rate is then // measured/corrected. Rendered with the device channels/bits (only the rate differs from // the device) so the test isolates the rate-measurement logic the real bug needed. void test_guess(hook::IpcClient& ipc, AudioRingHeader* ring, SharedBlock* block, unsigned rate) { reset_ring(ring, block); ToneSource tone; ToneFormat want; want.rate = rate; // channels/bits resolve to the device's if (!tone.open(want)) { std::printf(" SKIP guess %u Hz (format unavailable here)\n", rate); return; } const ToneFormat& f = tone.format(); // Let the stream reach steady state before attaching, like a game already running when // we inject (the real case) -- not a stream we caught at its first buffer. const DWORD warm = GetTickCount() + 300; while (GetTickCount() < warm) { tone.render_step(30); } // Hooks install *after* the client exists -> the lazy-discovery (guess) path. if (!hook::install_audio_hooks(ipc, ring)) { std::printf(" SKIP guess (audio hooks unavailable)\n"); tone.close(); return; } // Render while driving republish (the DLL's worker does this each tick) until the // measured rate is published, then render a bit more so capture fills the ring. const DWORD measure_deadline = GetTickCount() + 2000; while (GetTickCount() < measure_deadline && !audio_ring_format_ready(*ring)) { tone.render_step(30); hook::republish_audio_format(); } const DWORD cap_end = GetTickCount() + 200; while (GetTickCount() < cap_end) { tone.render_step(30); } const AudioStreamInfo& s = block->status.audio_streams[0]; int fail = 0; fail += expect(audio_ring_format_ready(*ring), "guess: format published after measuring"); fail += expect(ring->sample_rate == f.rate, "guess: measured rate == rendered rate"); fail += expect(s.sample_rate == f.rate, "guess: HookStatus rate == rendered rate"); fail += expect(s.format_state == AudioFormat_Measured, "guess: provenance == Measured"); fail += expect(ring_has_nonsilent(ring), "guess: non-silent audio captured"); std::printf(" %s guess %u Hz (device %uch/%ubit) -> measured %uHz state=%u\n", fail == 0 ? "PASS" : "FAIL", rate, f.channels, f.bits, ring->sample_rate, s.format_state); tone.close(); hook::remove_audio_hooks(); } // GUESS-MISMATCH: the device channels/bits guess is *wrong* for this stream (its bytes/frame // differ). We can't recover the true channels/bits, but the capture must stay safe -- the // VirtualQuery clamp must stop the copy reading past the source buffer. We can't assert a // "correct" format here (it's fundamentally undetectable); we assert the hook survives and // doesn't read absurd amounts, i.e. the unit test completes without an access violation. void test_guess_mismatch_safe(hook::IpcClient& ipc, AudioRingHeader* ring, SharedBlock* block, const ToneFormat& want, const char* label) { char d[64]; reset_ring(ring, block); ToneSource tone; if (!tone.open(want)) { std::printf(" SKIP guess-mismatch %s (%s unavailable here)\n", label, fmt_desc(want, d, sizeof(d))); return; } const ToneFormat& f = tone.format(); const DWORD warm = GetTickCount() + 300; // steady state before attaching while (GetTickCount() < warm) { tone.render_step(30); } if (!hook::install_audio_hooks(ipc, ring)) { std::printf(" SKIP guess-mismatch (audio hooks unavailable)\n"); tone.close(); return; } // Render and capture through the guessed (too-large) block. The clamp must keep this // from over-reading; reaching the end of the loop is the pass (no AV). const DWORD end = GetTickCount() + 600; while (GetTickCount() < end) { tone.render_step(30); hook::republish_audio_format(); } std::printf(" PASS guess-mismatch %s (%s) survived capture with a too-large guessed block (no over-read)\n", fmt_desc(f, d, sizeof(d)), label); tone.close(); hook::remove_audio_hooks(); } } // namespace int main() { if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))) { std::printf("FAIL: CoInitializeEx\n"); return 1; } // Host side: the IPC SharedBlock (named by our pid) the hook's IpcClient connects to, // plus one producer 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(); // OS zero-inits the mapping 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); hook::IpcClient ipc; expect(ipc.connect(10, 5), "IPC client connect"); // Probe the endpoint once; SKIP cleanly on a headless machine (no render device). // Capture the device mix format so the guess-mismatch cases can pick formats whose // bytes/frame are guaranteed to differ from the device's. ToneFormat dev; { ToneSource probe; if (!probe.open(ToneFormat{})) { std::printf("SKIP: no default render endpoint (no audio device?)\n"); CoUninitialize(); return 0; } dev = probe.format(); probe.close(); } std::printf("Device mix format: %u Hz %u ch %u-bit %s\n", dev.rate, dev.channels, dev.bits, dev.is_float ? "float" : "pcm"); // SEE-INIT: exact full-format detection across common rate/channel/bit-depth combos. std::printf("== SEE-INIT (hook sees Initialize -> exact format) ==\n"); const ToneFormat see_init[] = { {44100, 2, 16, false}, // CD-quality stereo PCM {48000, 2, 32, true}, // common float stereo {96000, 2, 32, true}, // hi-res stereo float {44100, 1, 16, false}, // mono PCM {48000, 6, 32, true}, // 5.1 float }; for (const ToneFormat& f : see_init) { test_see_init(ipc, ring, block, f); } // GUESS (match): the lazy-discovery path with the device channels/bits, so only the // rate differs -- the hook measures + corrects it to the true rate (the Brotato/Godot // case). Rendered at the device's channel/bit layout, so the bytes/frame match. std::printf("== GUESS, byte-compatible (pre-existing client -> measure the true rate) ==\n"); for (unsigned rate : {44100u, 48000u, 96000u}) { test_guess(ipc, ring, block, rate); } // GUESS (byte-incompatible): a pre-existing client whose channels/bits differ from the // device. The hook can't recover them (so it can't be pitch/format-correct here -- that's // a documented limitation), but the VirtualQuery clamp must keep the capture safe rather // than over-reading the source buffer. std::printf("== GUESS, byte-incompatible (channels/bits differ -> capture must stay safe) ==\n"); if (dev.channels >= 2) { test_guess_mismatch_safe(ipc, ring, block, {dev.rate, 1, dev.bits, dev.is_float}, "mono"); } { const unsigned alt_bits = (dev.bits == 32) ? 16u : 32u; const bool alt_float = (alt_bits == 32); test_guess_mismatch_safe(ipc, ring, block, {dev.rate, dev.channels, alt_bits, alt_float}, "alt bit depth"); } 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; }