Capture every audio stream into its own ring and mix them on the host
Games with several concurrent WASAPI render streams (e.g. Spider-Man: Miles
Morales) only had their first ("primary") stream mirrored; the rest kept playing
locally and never reached the guest. Now the render-hook captures + silences EVERY
tracked stream into its own ring (coop_audio_<pid>[_<index>]), each published with
that stream's own detected format (Initialize when caught, else GetMixFormat -- the
per-stream format detection, now actually used per ring rather than only for the
primary). The host creates a ring per stream and mixes the same-format streams with
a soft clip (host/src/audio/audio_mix.hpp); streams whose format differs from the
primary are still silenced (no echo) but skipped from the mix (would need
resampling).
The single-stream case is byte-for-byte unchanged: when only one stream is active
the host passes it through without the mixer, so the common path has no overhead or
fidelity change.
Verified: new audio_mix_test covers the decode/sum/soft-clip/encode math (float32 +
int16); audio_hook_test (x64 + x86) still passes, guarding the primary
capture+silence path against regression; full build x64 + x86 clean; ctest x64
11/11, x86 3/3. Multi-stream mixing against a real multi-stream game needs a live
session to fully confirm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -30,8 +30,8 @@ namespace
|
||||
|
||||
coop::hook::IpcClient g_ipc;
|
||||
std::atomic<bool> g_running{true};
|
||||
coop::SharedMemory g_audio_shm; // the host's audio ring, opened when present
|
||||
coop::SharedMemory g_log_shm; // the host's log ring, opened when present
|
||||
coop::SharedMemory g_audio_shm[coop::kMaxAudioStreams]; // per-stream audio rings, opened when present
|
||||
coop::SharedMemory g_log_shm; // the host's log ring, opened when present
|
||||
|
||||
DWORD WINAPI worker_thread(LPVOID)
|
||||
{
|
||||
@@ -168,31 +168,40 @@ DWORD WINAPI worker_thread(LPVOID)
|
||||
coop::hook::logf("worker_thread: MKB hooks removed (host request)");
|
||||
}
|
||||
|
||||
if (audio_installed && !audio_ring_open)
|
||||
// Attach a ring per stream. The host creates up to kMaxAudioStreams rings
|
||||
// (coop_audio_<pid>[_<index>]); we open each as it appears and (re)attach it so
|
||||
// every stream is captured + silenced into its own ring for the host to mix.
|
||||
if (audio_installed)
|
||||
{
|
||||
const std::wstring name = coop::audio_ring_name(GetCurrentProcessId());
|
||||
if (!g_audio_shm.valid())
|
||||
for (unsigned i = 0; i < coop::kMaxAudioStreams; ++i)
|
||||
{
|
||||
g_audio_shm.open(name, coop::audio_ring_total_size(coop::kAudioRingCapacity));
|
||||
}
|
||||
if (g_audio_shm.valid())
|
||||
{
|
||||
auto* ring = g_audio_shm.as<coop::AudioRingHeader>();
|
||||
if (coop::audio_ring_valid(*ring))
|
||||
if (!g_audio_shm[i].valid())
|
||||
{
|
||||
coop::hook::set_audio_ring(ring);
|
||||
audio_ring_open = true;
|
||||
coop::hook::logf("worker_thread: audio ring opened");
|
||||
g_audio_shm[i].open(coop::audio_ring_name(GetCurrentProcessId(), i),
|
||||
coop::audio_ring_total_size(coop::kAudioRingCapacity));
|
||||
}
|
||||
else
|
||||
if (g_audio_shm[i].valid())
|
||||
{
|
||||
g_audio_shm.reset(); // present but not our contract; retry
|
||||
auto* ring = g_audio_shm[i].as<coop::AudioRingHeader>();
|
||||
if (coop::audio_ring_valid(*ring))
|
||||
{
|
||||
coop::hook::set_audio_ring(i, ring); // idempotent re-attach
|
||||
if (i == 0 && !audio_ring_open)
|
||||
{
|
||||
audio_ring_open = true;
|
||||
coop::hook::logf("worker_thread: audio ring 0 opened");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
g_audio_shm[i].reset(); // present but not our contract; retry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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.
|
||||
// A stream is often registered before its ring is attached (or the host re-inits
|
||||
// a ring on a mirror re-toggle, clearing its format); keep formats published so
|
||||
// the host consumes the rings instead of falling back to loopback.
|
||||
if (audio_ring_open)
|
||||
{
|
||||
coop::hook::republish_audio_format();
|
||||
|
||||
Reference in New Issue
Block a user