diff --git a/README.md b/README.md index 76be27d..8840551 100644 --- a/README.md +++ b/README.md @@ -214,10 +214,12 @@ ctest --test-dir build -C Debug --output-on-failure hook's shared video texture, and **decodes the frame number out of the captured pixels** to assert the mirror sees a *monotonic, advancing* sequence for both backends (the bar for no dropped / stale / out-of-order frames — what the DX12 rotating-backbuffer bug - broke). It then injects with audio + video, checks both stream, cycles the audio - subsystem off/on (hook/unhook stress), and confirms the game never crashes and capture - resumes. This suite drove out five real audio races (see Lessons learned). Skips cleanly - without a D3D11 device. + broke). It launches the game at several **audio formats** (44100/48000/96000, PCM + + float) and asserts the hook measures each one's rate through the full inject path, then + injects with audio + video, checks both stream, cycles the audio subsystem off/on + (hook/unhook stress), and confirms the game never crashes and capture resumes. This + suite drove out five real audio races (see Lessons learned). Skips cleanly without a + D3D11 device. ### Debugging the hooks against a real game diff --git a/tests/mock_game_test.cpp b/tests/mock_game_test.cpp index d96f7ff..13bb349 100644 --- a/tests/mock_game_test.cpp +++ b/tests/mock_game_test.cpp @@ -295,6 +295,93 @@ void test_video_capture(const char* backend, ID3D11Device* device) game.kill(); } +// Launch the mock game rendering audio at `rate`/`channels`/`bits`/`fmt`, inject the audio +// hook (late, so it's the guessed path), and verify the hook MEASURES the right sample rate +// for this variant and captures non-silent audio. (Channels/bit-depth aren't recoverable for +// a guessed stream -- that's the documented limitation -- so the rate is the variant check.) +void test_audio_variant(unsigned rate, unsigned channels, unsigned bits, const wchar_t* fmt) +{ + wchar_t args[64]; + swprintf(args, static_cast(std::size(args)), L"dx11 30 %u %u %u %ls", rate, channels, bits, fmt); + std::printf("== audio variant: %u Hz %u ch %u-bit %ls ==\n", rate, channels, bits, fmt); + MockGame game = MockGame::launch(args); + if (!game.ok) + { + check(false, "launch coop_mock_game (audio variant)"); + return; + } + Sleep(800); + + // Audio subsystem only. + SharedMemory shm; + const std::uint32_t disabled = (1u << HookSubsys_Input) | (1u << HookSubsys_Focus) | + (1u << HookSubsys_Video) | (1u << HookSubsys_Mkb); + SharedBlock* block = make_ipc(shm, game.pid(), disabled); + SharedMemory ring_shm; + AudioRingHeader* ring = nullptr; + if (ring_shm.create(audio_ring_name(game.pid()), audio_ring_total_size(kAudioRingCapacity))) + { + ring = ring_shm.as(); + audio_ring_init(*ring, kAudioRingCapacity); + ring->capture_enabled.store(1, std::memory_order_release); + } + if (block == nullptr || ring == nullptr || !inject_retry(game.pid())) + { + check(false, "inject into mock game (audio variant)"); + game.kill(); + return; + } + + // Wait for the rate to be measured + published (Measured / LowConfidence / Exact), then + // drain to prove non-silent capture. + std::vector drain(kAudioRingCapacity); + std::uint32_t measured = 0; + std::uint32_t state = 0; + double peak = 0.0; + for (int i = 0; i < 120 && game.alive(); ++i) // up to ~6 s + { + Sleep(50); + std::uint32_t got = 0; + while ((got = audio_ring_pop(*ring, drain.data(), static_cast(drain.size()))) > 0) + { + if (ring->bits == 32 && ring->format_tag == 3) + { + const auto* f = reinterpret_cast(drain.data()); + for (std::uint32_t k = 0; k < got / 4; ++k) + { + peak = std::max(peak, static_cast(std::fabs(f[k]))); + } + } + else if (ring->bits == 16) + { + const auto* s = reinterpret_cast(drain.data()); + for (std::uint32_t k = 0; k < got / 2; ++k) + { + peak = std::max(peak, std::abs(s[k]) / 32768.0); + } + } + if (got < drain.size()) + { + break; + } + } + state = block->status.audio_streams[0].format_state; + if (state == AudioFormat_Measured || state == AudioFormat_LowConfidence || state == AudioFormat_Exact) + { + measured = block->status.audio_streams[0].sample_rate; + if (measured != 0 && peak > 0.01) + { + break; + } + } + } + std::printf(" measured rate=%u Hz (state=%u) peak=%.4f\n", measured, state, peak); + check(measured == rate, "hook measured this variant's sample rate"); + check(peak > 0.01, "captured non-silent audio for this variant"); + + game.kill(); +} + // Inject with audio + video, verify both stream, then cycle the audio subsystem off/on a // few times (hook/unhook stress) and confirm the game stays alive (heartbeat advances). void test_av_and_hook_cycles(ID3D11Device* device) @@ -432,6 +519,12 @@ int main() test_video_capture("dx11", device); test_video_capture("dx12", device); + + // Audio variants: the hook must measure each variant's rate through the full inject path. + test_audio_variant(44100, 2, 16, L"pcm"); + test_audio_variant(48000, 2, 32, L"float"); + test_audio_variant(96000, 2, 32, L"float"); + test_av_and_hook_cycles(device); device->Release();