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

@@ -8,11 +8,12 @@ pre-existing 48 kHz/2ch/float render client is detected, real non-silent audio
reaches the ring, zero overruns when consumed). The remaining step is the human
end-to-end with a guest over RPT: no local echo, guest hears audio.
Two corrections were applied during/after implementation: the
`IAudioClient::GetService` vtable index is **14**, not 13 (`SetEventHandle` is
13); and the inner hooks must be installed **proactively** (see below), not only
reactively — the original reactive-only design captured nothing on late injection,
which is the normal case.
Corrections applied during/after implementation: the `IAudioClient::GetService`
vtable index is **14**, not 13 (`SetEventHandle` is 13); the inner hooks must be
installed **proactively** (see below), not only reactively — the original
reactive-only design captured nothing on late injection, which is the normal case;
and the captured format must be **(re)published when the ring attaches**, not only
at stream registration, or the full app falls back to the echo (see below).
## Lesson: reactive-only hooking fails on late injection (the bug that broke it)
@@ -40,6 +41,25 @@ installing the `Activate` hook. If `Activate` is hooked first, the probe's own
blocks on the setup mutex the installer already holds — freezing the worker thread
(and any game thread that later calls `Activate`, which crashed the game).
## Lesson: publish the format when the ring attaches, not only at registration
A second late-binding bug only showed up in the full app, never in the probe.
The host creates the audio ring **when the operator toggles audio mirroring on**,
which is *after* injection — so the hook registers the game's primary stream while
`g_ring` is still null, and `register_render_client_locked` skips publishing the
format (there's no ring to publish to). When the ring later attaches via
`set_audio_ring`, the already-registered stream's format was never re-published,
so `format_valid` stayed 0, the host's `wait_for_format` timed out, and it fell
back to loopback (the echo) on every game. The in-process probe created the ring
*before* injecting, so it never reproduced this — `coop_audio_probe` now defaults
to creating the ring ~1.5 s **after** injecting to match the app.
Fix (commit follows): the hook stores the primary stream's format and
`republish_audio_format()` (re)publishes it whenever a ring is attached but has no
format yet — called from `set_audio_ring` and once per worker tick (the latter
also covers the host re-initializing the ring on a mirror re-toggle, which clears
`format_valid`).
## Problem
`coop_host.exe` mirrors the game's audio so Steam Remote Play Together (which