Audio render-hook M3: wire DLL + host hooked mode + loopback fallback

End-to-end plumbing of the render-hook audio path.

Hook (coop_hook.dll):
- CMake: build audio_hook.cpp, link ole32/mmdevapi, NTDDI_WIN10_CO.
- dllmain: CoInitializeEx(MTA) on the worker thread; install the audio hooks
  even before the ring exists (so streams are counted); open the host's
  coop_audio_<pid> ring when it appears and attach it (enabling capture+silence);
  remove_audio_hooks on clean detach.

Host (coop_host.exe):
- AudioMirror now creates the shared audio ring (owns capture_enabled) and tries
  the Hooked path first: waits ~1s for the hook to publish a format, then
  re-renders the game's frames from the ring with AUTOCONVERTPCM (no echo, since
  the hook silences the game locally).
- Automatic fallback: if the ring can't be created, no format arrives in time,
  or the render client won't initialize, it disables capture (so the game stays
  audible) and reverts to the existing process-loopback path (echo, no regress).
- Exposes Source (Hooked/Loopback/None) for the upcoming panel indicator.

Loopback render loop kept intact as run_loopback. Manual end-to-end (M5) and the
panel UI (M4) are next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 12:02:29 +02:00
parent 4e3814a072
commit 679b243974
4 changed files with 378 additions and 16 deletions

View File

@@ -10,6 +10,11 @@
#include <windows.h>
#include <objbase.h>
#include "audio_hook.hpp"
#include "coop/audio_ring.hpp"
#include "coop/shared_memory.hpp"
#include "focus_spoof.hpp"
#include "ipc_client.hpp"
#include "xinput_hook.hpp"
@@ -19,6 +24,7 @@ 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
DWORD WINAPI worker_thread(LPVOID)
{
@@ -28,8 +34,13 @@ DWORD WINAPI worker_thread(LPVOID)
return 0;
}
// The audio render-hook instantiates a COM enumerator on this thread.
const bool com_ok = SUCCEEDED(CoInitializeEx(nullptr, COINIT_MULTITHREADED));
bool xinput_installed = false;
bool focus_installed = false;
bool audio_installed = false;
bool audio_ring_open = false;
// Keep retrying the installs (XInput and the game window may both appear
// lazily) and beat a heartbeat so the host can show the hook is alive.
@@ -43,10 +54,39 @@ DWORD WINAPI worker_thread(LPVOID)
{
focus_installed = coop::hook::install_focus_spoof(g_ipc);
}
// Install the audio render-hook even before the host's ring exists, so
// render streams are counted for the debug view regardless; attach the
// ring (enabling capture+silence) once the host creates it.
if (com_ok && !audio_installed)
{
audio_installed = coop::hook::install_audio_hooks(g_ipc, nullptr);
}
if (audio_installed && !audio_ring_open)
{
const std::wstring name = coop::audio_ring_name(GetCurrentProcessId());
if (g_audio_shm.open(name, coop::audio_ring_total_size(coop::kAudioRingCapacity)))
{
auto* ring = g_audio_shm.as<coop::AudioRingHeader>();
if (coop::audio_ring_valid(*ring))
{
coop::hook::set_audio_ring(ring);
audio_ring_open = true;
}
else
{
g_audio_shm.reset(); // present but not our contract; retry
}
}
}
coop::hook::update_input_diagnostics(g_ipc); // refreshes each tick; registrations can change
g_ipc.heartbeat();
Sleep(250);
}
if (com_ok)
{
CoUninitialize();
}
return 0;
}
@@ -71,6 +111,7 @@ BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID reserved)
g_running.store(false, std::memory_order_relaxed);
coop::hook::remove_focus_spoof();
coop::hook::remove_xinput_hooks();
coop::hook::remove_audio_hooks();
}
break;
default: