Audio render-hook M4: audio panel source indicator + stream-count debug view

Surfaces the render-hook diagnostics in the Audio panel.

- HookStatusView / IpcServer::hook_status(): carry audio_streams_seen +
  audio_streams[] from the hook's back-channel.
- InjectionPanel: expose hook_status() so the audio panel can read the counts.
- AudioPanel::draw_ui(HookStatusView): show the active Source (green Hooked
  vs amber Loopback); only warn about the local echo on the loopback path. Add
  the required render-stream table: one row per stream with format, cumulative
  frames, the primary tagged, and a live/idle dot derived from frame-count
  deltas. Notes overflow when the game exceeds kMaxAudioStreams slots.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 12:05:28 +02:00
parent 679b243974
commit 31390d8c15
6 changed files with 121 additions and 9 deletions

View File

@@ -1,11 +1,35 @@
#include "audio_panel.hpp"
#include <algorithm>
#include "imgui.h"
#include <mmreg.h>
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<std::uint32_t>(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<unsigned long long>(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();
}