Phase 2: audio mirror via WASAPI process loopback

Mirror the real game's audio so Steam Remote Play Together (which streams the
host's own audio session) carries it to the guest. The host captures the game
by PID via WASAPI process loopback and re-renders it on the default endpoint;
the game still plays locally too (accepted "double audio" for now).

- ProcessLoopbackCapture: process-loopback capture client, frame-sink + stats.
  The completion handler must be agile (IAgileObject) or
  ActivateAudioInterfaceAsync rejects every call with E_ILLEGAL_METHOD_CALL.
- AudioMirror: wraps capture with an event-driven render client and a primed
  ring buffer; AudioPanel drives it from the injected game's window/PID.
- coop_tone: standalone WASAPI sine-wave process used as a known audio source.
- audio_loopback_test (CTest): captures coop_tone by PID and asserts non-silent
  audio arrives, so the path is verifiable without a second Steam account.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 10:21:27 +02:00
parent ba545ba64a
commit 663d86e6ec
13 changed files with 1313 additions and 3 deletions

View File

@@ -11,6 +11,7 @@
#include <winrt/Windows.Foundation.h>
#include "audio_panel.hpp"
#include "capture_panel.hpp"
#include "d3d11_window.hpp"
#include "debug_overlay.hpp"
@@ -39,6 +40,7 @@ int run()
auto input = std::make_unique<coop::XInputSource>();
coop::InjectionPanel injection;
coop::AudioPanel audio;
coop::CapturePanel capture;
if (!capture.init(window.device()))
{
@@ -50,11 +52,14 @@ int run()
{
input->poll();
injection.publish(input->pads());
capture.set_target(injection.game_hwnd());
const HWND game = injection.game_hwnd();
capture.set_target(game);
audio.set_target(game);
imgui.begin_frame();
coop::draw_debug_overlay(*input);
injection.draw();
audio.draw_ui();
capture.draw_ui();
RECT client = {};