// Mirrors a target process's audio so Steam Remote Play Together (which streams // THIS host's audio) carries the real game's sound, re-rendering it on the // default output endpoint. // // Two source paths (see docs/audio-render-hook-plan.md): // - Hooked: the injected render-hook copies the game's frames into a shared // audio ring AND silences the game locally, so there is no echo. Preferred. // - Loopback: WASAPI process-loopback capture of the game (the game still // plays locally, so the operator hears it twice). Automatic fallback when // the hook isn't present / doesn't publish a format in time. #pragma once #include #include #include #include #include #include "coop/audio_ring.hpp" #include "coop/shared_memory.hpp" namespace coop { class AudioMirror { public: AudioMirror() = default; ~AudioMirror(); AudioMirror(const AudioMirror&) = delete; AudioMirror& operator=(const AudioMirror&) = delete; // Start mirroring audio from `pid` (and its child processes). Replaces any // running mirror. Returns false only on synchronous setup failure; capture // errors surface asynchronously via status(). bool start(DWORD pid); void stop(); // True once the audio thread is actively mirroring (false while starting or // after a failure). [[nodiscard]] bool running() const { return running_.load(std::memory_order_acquire); } // The process currently targeted (0 if stopped). Updated synchronously by // start()/stop() so the UI can detect target changes without races. [[nodiscard]] DWORD target_pid() const { return pid_; } [[nodiscard]] unsigned sample_rate() const { return sample_rate_.load(std::memory_order_relaxed); } [[nodiscard]] unsigned channels() const { return channels_.load(std::memory_order_relaxed); } // Which capture path is active, for the UI's source indicator. enum class Source { None, Hooked, // shared audio ring from the render-hook (no echo) Loopback, // WASAPI process loopback (echo) }; [[nodiscard]] Source source() const { return source_.load(std::memory_order_relaxed); } [[nodiscard]] const char* source_name() const { switch (source()) { case Source::Hooked: return "Hooked (no echo)"; case Source::Loopback: return "Loopback (echo)"; default: return "—"; } } [[nodiscard]] std::string status() const; private: void thread_main(DWORD pid); // Returns true if it owned the session to a clean stop; false if setup failed // and the caller should fall back to the loopback path. bool run_hooked(AudioRingHeader* ring); void run_loopback(DWORD pid); bool wait_for_format(AudioRingHeader* ring, DWORD timeout_ms); bool stop_requested() const; void set_status(std::string s); std::thread thread_; HANDLE stop_event_ = nullptr; DWORD pid_ = 0; SharedMemory audio_ring_shm_; // host-created shared ring (named coop_audio_) std::atomic running_{false}; std::atomic source_{Source::None}; std::atomic sample_rate_{0}; std::atomic channels_{0}; mutable std::mutex status_mutex_; std::string status_; }; } // namespace coop