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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user