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:
@@ -70,6 +70,14 @@ std::atomic<IAudioRenderClient*> g_self_render{nullptr};
|
||||
CapturedFormat g_mix_format;
|
||||
std::atomic<std::uint32_t> g_have_mix_format{0};
|
||||
|
||||
// The primary stream's actual format, captured when it's registered. The host
|
||||
// creates the ring only when audio mirroring is toggled on — typically *after*
|
||||
// the primary stream was already registered — so the format must be (re)published
|
||||
// to the ring whenever it attaches. (The in-process probe creates the ring before
|
||||
// injecting, so it never exercises this ordering; the full app always does.)
|
||||
// Guarded by g_setup_mutex.
|
||||
CapturedFormat g_primary_format;
|
||||
|
||||
// Per IAudioClient, the format captured at Initialize, looked up when its render
|
||||
// client is created. Setup-path only (never touched on the audio thread).
|
||||
std::unordered_map<IAudioClient*, CapturedFormat> g_client_formats;
|
||||
@@ -238,6 +246,7 @@ void register_render_client_locked(IAudioRenderClient* rc, const CapturedFormat&
|
||||
|
||||
if (slot == 0)
|
||||
{
|
||||
g_primary_format = cf;
|
||||
g_primary_block_align.store(cf.block_align, std::memory_order_relaxed);
|
||||
g_primary.store(rc, std::memory_order_release);
|
||||
AudioRingHeader* ring = g_ring.load(std::memory_order_acquire);
|
||||
@@ -472,11 +481,36 @@ bool install_audio_hooks(IpcClient& ipc, AudioRingHeader* ring)
|
||||
return static_cast<bool>(g_hk_activate);
|
||||
}
|
||||
|
||||
void republish_audio_format()
|
||||
{
|
||||
AudioRingHeader* ring = g_ring.load(std::memory_order_acquire);
|
||||
if (ring == nullptr || audio_ring_format_ready(*ring))
|
||||
{
|
||||
return; // no ring yet, or the format is already published
|
||||
}
|
||||
std::scoped_lock lock(g_setup_mutex);
|
||||
if (audio_ring_format_ready(*ring))
|
||||
{
|
||||
return; // raced with another publisher
|
||||
}
|
||||
if (g_primary.load(std::memory_order_acquire) != nullptr && g_primary_format.rate != 0)
|
||||
{
|
||||
audio_ring_set_format(*ring, g_primary_format.rate, g_primary_format.channels, g_primary_format.bits,
|
||||
g_primary_format.tag, g_primary_format.block_align);
|
||||
logf("republish_audio_format: published %uHz/%uch/%ubit to ring %p", g_primary_format.rate,
|
||||
g_primary_format.channels, g_primary_format.bits, ring);
|
||||
}
|
||||
}
|
||||
|
||||
void set_audio_ring(AudioRingHeader* ring)
|
||||
{
|
||||
g_ring.store(ring, std::memory_order_release);
|
||||
logf("set_audio_ring: ring=%p capture_enabled=%u", ring,
|
||||
ring ? ring->capture_enabled.load(std::memory_order_relaxed) : 0u);
|
||||
// The primary may already be registered (game was playing before we injected
|
||||
// and before the host created the ring); publish its format so the host stops
|
||||
// waiting and consumes the ring instead of falling back to loopback.
|
||||
republish_audio_format();
|
||||
}
|
||||
|
||||
void remove_audio_hooks()
|
||||
@@ -500,6 +534,7 @@ void remove_audio_hooks()
|
||||
g_self_client = nullptr;
|
||||
}
|
||||
g_have_mix_format.store(0, std::memory_order_relaxed);
|
||||
g_primary_format = CapturedFormat{};
|
||||
|
||||
g_registered = 0;
|
||||
g_streams_seen.store(0, std::memory_order_relaxed);
|
||||
|
||||
@@ -26,6 +26,12 @@ bool install_audio_hooks(IpcClient& ipc, AudioRingHeader* ring);
|
||||
// Attach/replace the producer ring after install (e.g. host created it late).
|
||||
void set_audio_ring(AudioRingHeader* ring);
|
||||
|
||||
// Publish the registered primary stream's format to the attached ring if it
|
||||
// isn't published yet. Idempotent; call periodically so a ring the host attaches
|
||||
// (or re-initializes on a mirror re-toggle) gets the format even though the
|
||||
// stream was registered earlier. No-op if there's no ring / no primary yet.
|
||||
void republish_audio_format();
|
||||
|
||||
// Removes all installed render hooks (best effort; used on DLL detach).
|
||||
void remove_audio_hooks();
|
||||
|
||||
|
||||
@@ -89,6 +89,13 @@ DWORD WINAPI worker_thread(LPVOID)
|
||||
}
|
||||
}
|
||||
}
|
||||
// The primary stream is often registered before the ring is attached (or the
|
||||
// host re-inits the ring on a mirror re-toggle, clearing its format); keep
|
||||
// the format published so the host consumes the ring instead of falling back.
|
||||
if (audio_ring_open)
|
||||
{
|
||||
coop::hook::republish_audio_format();
|
||||
}
|
||||
coop::hook::update_input_diagnostics(g_ipc); // refreshes each tick; registrations can change
|
||||
g_ipc.heartbeat();
|
||||
Sleep(250);
|
||||
|
||||
Reference in New Issue
Block a user