Fix audio falling back to echo in the full app (format not published)

In the full app the host creates the audio ring only when the operator toggles
audio mirroring on -- after injection. So the hook registers the game's primary
render stream while the ring is still null, and register_render_client_locked
skips publishing the format (nothing to publish to). When the ring later
attaches via set_audio_ring, the already-registered stream's format was never
re-published: format_valid stayed 0, the host's wait_for_format timed out, and
it fell back to loopback (the echo) -- on every game, including Phantom Brave.
The in-process probe created the ring before injecting, so it never reproduced
this.

Fix: the hook stores the primary stream's format and republish_audio_format()
publishes it whenever a ring is attached but has no format yet -- called from
set_audio_ring and once per worker tick (the tick also covers the host
re-initializing the ring on a mirror re-toggle, which clears format_valid).

coop_audio_probe now creates the ring ~1.5 s AFTER injecting by default
(ring_delay_ms arg) to match the app's ordering. Verified against Phantom
Brave: the log shows "primary stream set ... no ring yet" at inject, then
"republish_audio_format: published 48000Hz/2ch/32bit" when the ring attaches,
and the host-shaped consumer then drains real audio with zero overruns.

All four tests still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 15:23:30 +02:00
parent a1b7578165
commit 4e076420bf
6 changed files with 120 additions and 21 deletions

View File

@@ -89,11 +89,15 @@ int wmain(int argc, wchar_t** argv)
{
if (argc < 2)
{
std::printf("usage: coop_audio_probe <pid> [seconds]\n");
std::printf("usage: coop_audio_probe <pid> [seconds] [ring_delay_ms]\n"
" ring_delay_ms: how long after injecting to create the audio ring\n"
" (default 1500 = reproduces the real app, which creates the ring\n"
" only when audio mirroring is toggled on; 0 = ring before inject).\n");
return 1;
}
const unsigned long pid = std::wcstoul(argv[1], nullptr, 10);
const int seconds = (argc >= 3) ? std::max(1, _wtoi(argv[2])) : 20;
const int ring_delay_ms = (argc >= 4) ? std::max(0, _wtoi(argv[3])) : 1500;
if (pid == 0)
{
std::printf("ERROR: invalid pid.\n");
@@ -113,17 +117,6 @@ int wmain(int argc, wchar_t** argv)
block->sequence.store(0, std::memory_order_relaxed);
block->magic = coop::kProtocolMagic;
// 2) Audio ring, capture enabled (mirrors AudioMirror::thread_main).
coop::SharedMemory ring_shm;
if (!ring_shm.create(coop::audio_ring_name(pid), coop::audio_ring_total_size(coop::kAudioRingCapacity)))
{
std::printf("ERROR: create audio ring mapping failed (%lu).\n", GetLastError());
return 1;
}
auto* ring = ring_shm.as<coop::AudioRingHeader>();
coop::audio_ring_init(*ring, coop::kAudioRingCapacity);
ring->capture_enabled.store(1, std::memory_order_release);
// Enable the hook's file trace (%TEMP%\coop_hook.log) for this debug session.
{
wchar_t dir[MAX_PATH] = {};
@@ -139,15 +132,49 @@ int wmain(int argc, wchar_t** argv)
}
}
// 3) Inject.
// Create the audio ring (capture enabled), mirroring AudioMirror::thread_main.
// By default we do this *after* injecting so the ordering matches the real app
// (the host creates the ring only when audio mirroring is toggled on, which is
// after the hook has already been injected and the game's stream registered).
coop::SharedMemory ring_shm;
coop::AudioRingHeader* ring = nullptr;
auto create_ring = [&]() -> bool {
if (!ring_shm.create(coop::audio_ring_name(pid),
coop::audio_ring_total_size(coop::kAudioRingCapacity)))
{
std::printf("ERROR: create audio ring mapping failed (%lu).\n", GetLastError());
return false;
}
ring = ring_shm.as<coop::AudioRingHeader>();
coop::audio_ring_init(*ring, coop::kAudioRingCapacity);
ring->capture_enabled.store(1, std::memory_order_release);
return true;
};
if (ring_delay_ms == 0 && !create_ring())
{
return 1;
}
// Inject.
std::printf("Injecting coop_hook.dll into pid %lu ...\n", pid);
if (!inject(pid, dll_path_next_to_self()))
{
return 1;
}
std::printf("Injected. Polling for %d s. Hook trace: %%TEMP%%\\coop_hook.log\n\n", seconds);
// 4) Poll + print. Drain the ring like the real host would (so it doesn't
if (ring_delay_ms > 0)
{
std::printf("Injected. Creating audio ring %d ms later (app-ordering)...\n", ring_delay_ms);
Sleep(static_cast<DWORD>(ring_delay_ms));
if (!create_ring())
{
return 1;
}
}
std::printf("Polling for %d s. Hook trace: %%TEMP%%\\coop_hook.log\n\n", seconds);
// Poll + print. Drain the ring like the real host would (so it doesn't
// overrun) and measure peak amplitude to prove we captured real audio.
const coop::HookStatus& status = block->status;
std::uint64_t prev_frames[coop::kMaxAudioStreams] = {};