// Owns the audio-mirror pipeline (render-hook ring or WASAPI process loopback + // re-render) and its UI. The target process is derived from the injected game's // window; the render-stream debug view reads the hook's diagnostics back-channel. #pragma once #include #include #include #include #include "audio/audio_loopback.hpp" #include "audio/audio_overrides.hpp" #include "ipc/ipc_server.hpp" namespace coop { class AudioPanel { public: AudioPanel() { overrides_.load(); } // The window whose process audio to mirror (0 if none); typically the // injected game's HWND. void set_target(HWND target) { target_ = target; } // Wire a sink for host-side log lines (override-overwrite warnings etc.). main // connects this to the injection panel's Log-window channel. void set_logger(std::function logger) { logger_ = std::move(logger); } // `status` is the hook's back-channel, for the render-stream view. With // `debug_details` on, the per-stream table is shown. void draw_ui(const HookStatusView& status, bool debug_details); // UI-fit check (headless test) only: render the richest content path -- as if a // loopback mirror were running with long status/reason strings -- without a live // AudioMirror, so the fit test can measure the panel's worst-case size. Never set in // the shipping host (the render path is identical, just fed synthetic values). void dev_set_demo(bool on) { demo_ = on; } #ifdef COOP_TEST_HARNESS // Test-harness hooks (debug builds only): drive the real audio code paths and read // state back, incl. targeting a windowless process by pid (coop_tone has no window). void dev_set_enabled(bool on) { enabled_ = on; } void dev_set_pid(DWORD pid) { dev_pid_ = pid; } void dev_request_op(unsigned slot, std::uint32_t kind, std::uint32_t rate, std::uint32_t ch, std::uint32_t bits, std::uint32_t tag) { mirror_.request_op(slot, kind, rate, ch, bits, tag); } [[nodiscard]] bool dev_running() const { return mirror_.running(); } [[nodiscard]] unsigned dev_rate() const { return mirror_.sample_rate(); } [[nodiscard]] unsigned dev_channels() const { return mirror_.channels(); } [[nodiscard]] std::string dev_source() const { return mirror_.source_name(); } [[nodiscard]] std::string dev_reason() const { return mirror_.fallback_reason(); } #endif private: // Auto-apply a stored override over a guessed stream, and auto-save a format the hook // caught exactly at Initialize (warning if it overwrites a differing stored value). void manage_overrides(const HookStatusView& status, DWORD pid); static std::wstring image_name_from_pid(DWORD pid); HWND target_ = nullptr; DWORD dev_pid_ = 0; // test harness only: force a (windowless) target pid; 0 in production bool enabled_ = false; bool demo_ = false; // UI-fit test only: render the richest content with synthetic values AudioMirror mirror_; // Per-game persisted overrides + per-target session bookkeeping. AudioOverrideStore overrides_; std::function logger_; DWORD applied_pid_ = 0; // pid the session flags below were reset for std::wstring target_image_; // current target's image path (store keys on the basename) bool override_applied_ = false; // applied (or confirmed none) the stored override this session bool exact_saved_ = false; // saved the exact Initialize format this session // Per-stream activity tracking for the "live" column. Audio buffers release in // bursts, so most UI frames see no change; comparing to just the previous frame // flickers. Instead we remember when each stream last advanced and debounce the // live/idle indicator over a short window, plus a ~2 Hz frames/s estimate. std::uint64_t prev_frames_[kMaxAudioStreams] = {}; double last_active_[kMaxAudioStreams] = {}; // ImGui time a stream last advanced std::uint64_t rate_base_frames_[kMaxAudioStreams] = {}; double frames_per_s_[kMaxAudioStreams] = {}; double rate_base_time_ = 0.0; // Operator format-override editor (debug details). Applies to the primary stream. int ov_rate_ = 48000; int ov_channels_ = 2; int ov_bits_idx_ = 1; // 0 = 16-bit, 1 = 32-bit int ov_fmt_idx_ = 1; // 0 = PCM, 1 = float }; } // namespace coop