diff --git a/host/src/audio_panel.cpp b/host/src/audio_panel.cpp index 1f7b44b..63b445b 100644 --- a/host/src/audio_panel.cpp +++ b/host/src/audio_panel.cpp @@ -1,11 +1,35 @@ #include "audio_panel.hpp" +#include + #include "imgui.h" +#include + namespace coop { -void AudioPanel::draw_ui() +namespace +{ + +const char* format_tag_name(std::uint32_t tag) +{ + switch (tag) + { + case WAVE_FORMAT_PCM: + return "PCM"; + case WAVE_FORMAT_IEEE_FLOAT: + return "float"; + case WAVE_FORMAT_EXTENSIBLE: + return "ext"; + default: + return "?"; + } +} + +} // namespace + +void AudioPanel::draw_ui(const HookStatusView& status) { const bool have_target = target_ != nullptr && IsWindow(target_); DWORD pid = 0; @@ -44,15 +68,79 @@ void AudioPanel::draw_ui() if (mirror_.running()) { + const AudioMirror::Source src = mirror_.source(); + const bool hooked = src == AudioMirror::Source::Hooked; ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "Mirroring %u Hz, %u ch", mirror_.sample_rate(), mirror_.channels()); + ImGui::Text("Source:"); + ImGui::SameLine(); + ImGui::TextColored(hooked ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f) : ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", + mirror_.source_name()); } - const std::string status = mirror_.status(); - if (!status.empty()) + const std::string mirror_status = mirror_.status(); + if (!mirror_status.empty()) { - ImGui::TextWrapped("%s", status.c_str()); + ImGui::TextWrapped("%s", mirror_status.c_str()); + } + + // Only the loopback path leaves the game audible locally (the echo); the + // hooked path silences it, so don't warn there. + if (mirror_.source() == AudioMirror::Source::Loopback) + { + ImGui::TextDisabled("Game audio also plays locally (echo). Inject the hook to remove it."); + } + + // --- Render-stream debug view ----------------------------------------- + // The hook counts every render stream the game creates, even with mirroring + // off. v1 captures only the first ("primary"); this makes multi-stream games + // obvious so we know if/when that needs revisiting. + ImGui::Separator(); + ImGui::Text("Render streams: %u", status.audio_streams_seen); + if (status.audio_streams_seen > kMaxAudioStreams) + { + ImGui::SameLine(); + ImGui::TextDisabled("(showing first %u)", kMaxAudioStreams); + } + + const std::uint32_t rows = std::min(status.audio_streams_seen, kMaxAudioStreams); + if (rows > 0 && + ImGui::BeginTable("audio_streams", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) + { + ImGui::TableSetupColumn("#"); + ImGui::TableSetupColumn("role"); + ImGui::TableSetupColumn("format"); + ImGui::TableSetupColumn("frames"); + ImGui::TableSetupColumn("live"); + ImGui::TableHeadersRow(); + for (std::uint32_t i = 0; i < rows; ++i) + { + const AudioStreamInfo& s = status.audio_streams[i]; + const bool live = s.frames_rendered > prev_frames_[i]; + prev_frames_[i] = s.frames_rendered; + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%u", i); + ImGui::TableNextColumn(); + ImGui::TextColored(s.is_primary ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f) : ImVec4(0.7f, 0.7f, 0.7f, 1.0f), + "%s", s.is_primary ? "primary" : "extra"); + ImGui::TableNextColumn(); + ImGui::Text("%u Hz %uch %u-bit %s", s.sample_rate, s.channels, s.bits, + format_tag_name(s.format_tag)); + ImGui::TableNextColumn(); + ImGui::Text("%llu", static_cast(s.frames_rendered)); + ImGui::TableNextColumn(); + if (live) + { + ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "●"); + } + else + { + ImGui::TextDisabled("idle"); + } + } + ImGui::EndTable(); } - ImGui::TextDisabled("Game audio also plays locally (double audio)."); ImGui::End(); } diff --git a/host/src/audio_panel.hpp b/host/src/audio_panel.hpp index 1e10472..f914c61 100644 --- a/host/src/audio_panel.hpp +++ b/host/src/audio_panel.hpp @@ -1,10 +1,14 @@ -// Owns the audio-mirror pipeline (WASAPI process loopback + re-render) and its -// UI. The target process is derived from the injected game's window. +// 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 "audio/audio_loopback.hpp" +#include "ipc/ipc_server.hpp" namespace coop { @@ -19,12 +23,16 @@ public: target_ = target; } - void draw_ui(); + // `status` is the hook's back-channel, for the render-stream debug view. + void draw_ui(const HookStatusView& status); private: HWND target_ = nullptr; bool enabled_ = false; AudioMirror mirror_; + + // Previous per-stream frame counts, to flag streams as live (advancing). + std::uint64_t prev_frames_[kMaxAudioStreams] = {}; }; } // namespace coop diff --git a/host/src/injection_panel.hpp b/host/src/injection_panel.hpp index 5c4ad51..bebc33c 100644 --- a/host/src/injection_panel.hpp +++ b/host/src/injection_panel.hpp @@ -33,6 +33,13 @@ public: return reinterpret_cast(server_.hook_status().game_hwnd); } + // The hook's full diagnostics back-channel (other panels read the audio + // render-stream counts from here). + [[nodiscard]] HookStatusView hook_status() const + { + return server_.hook_status(); + } + private: void refresh_processes(); void inject_selected(); diff --git a/host/src/ipc/ipc_server.cpp b/host/src/ipc/ipc_server.cpp index 20a8538..1bef035 100644 --- a/host/src/ipc/ipc_server.cpp +++ b/host/src/ipc/ipc_server.cpp @@ -66,6 +66,11 @@ HookStatusView IpcServer::hook_status() const view.raw_input_gamepad = s.raw_input_gamepad != 0; view.raw_input_gamepad_sink = s.raw_input_gamepad_sink != 0; view.dinput_loaded = s.dinput_loaded != 0; + view.audio_streams_seen = s.audio_streams_seen; + for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i) + { + view.audio_streams[i] = s.audio_streams[i]; + } return view; } diff --git a/host/src/ipc/ipc_server.hpp b/host/src/ipc/ipc_server.hpp index 5085f92..fe1607b 100644 --- a/host/src/ipc/ipc_server.hpp +++ b/host/src/ipc/ipc_server.hpp @@ -27,6 +27,10 @@ struct HookStatusView bool raw_input_gamepad = false; bool raw_input_gamepad_sink = false; bool dinput_loaded = false; + + // Audio render-hook diagnostics (for the Audio panel's stream-count view). + std::uint32_t audio_streams_seen = 0; + AudioStreamInfo audio_streams[kMaxAudioStreams] = {}; }; class IpcServer diff --git a/host/src/main.cpp b/host/src/main.cpp index a4dc856..4a11f03 100644 --- a/host/src/main.cpp +++ b/host/src/main.cpp @@ -59,7 +59,7 @@ int run() imgui.begin_frame(); coop::draw_debug_overlay(*input); injection.draw(); - audio.draw_ui(); + audio.draw_ui(injection.hook_status()); capture.draw_ui(); RECT client = {};