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:
@@ -1,11 +1,35 @@
|
|||||||
#include "audio_panel.hpp"
|
#include "audio_panel.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
|
|
||||||
|
#include <mmreg.h>
|
||||||
|
|
||||||
namespace coop
|
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_);
|
const bool have_target = target_ != nullptr && IsWindow(target_);
|
||||||
DWORD pid = 0;
|
DWORD pid = 0;
|
||||||
@@ -44,15 +68,79 @@ void AudioPanel::draw_ui()
|
|||||||
|
|
||||||
if (mirror_.running())
|
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",
|
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "Mirroring %u Hz, %u ch",
|
||||||
mirror_.sample_rate(), mirror_.channels());
|
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();
|
const std::string mirror_status = mirror_.status();
|
||||||
if (!status.empty())
|
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();
|
ImGui::End();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
// Owns the audio-mirror pipeline (WASAPI process loopback + re-render) and its
|
// Owns the audio-mirror pipeline (render-hook ring or WASAPI process loopback +
|
||||||
// UI. The target process is derived from the injected game's window.
|
// 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
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
#include "audio/audio_loopback.hpp"
|
#include "audio/audio_loopback.hpp"
|
||||||
|
#include "ipc/ipc_server.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop
|
||||||
{
|
{
|
||||||
@@ -19,12 +23,16 @@ public:
|
|||||||
target_ = target;
|
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:
|
private:
|
||||||
HWND target_ = nullptr;
|
HWND target_ = nullptr;
|
||||||
bool enabled_ = false;
|
bool enabled_ = false;
|
||||||
AudioMirror mirror_;
|
AudioMirror mirror_;
|
||||||
|
|
||||||
|
// Previous per-stream frame counts, to flag streams as live (advancing).
|
||||||
|
std::uint64_t prev_frames_[kMaxAudioStreams] = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace coop
|
} // namespace coop
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ public:
|
|||||||
return reinterpret_cast<HWND>(server_.hook_status().game_hwnd);
|
return reinterpret_cast<HWND>(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:
|
private:
|
||||||
void refresh_processes();
|
void refresh_processes();
|
||||||
void inject_selected();
|
void inject_selected();
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ HookStatusView IpcServer::hook_status() const
|
|||||||
view.raw_input_gamepad = s.raw_input_gamepad != 0;
|
view.raw_input_gamepad = s.raw_input_gamepad != 0;
|
||||||
view.raw_input_gamepad_sink = s.raw_input_gamepad_sink != 0;
|
view.raw_input_gamepad_sink = s.raw_input_gamepad_sink != 0;
|
||||||
view.dinput_loaded = s.dinput_loaded != 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;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ struct HookStatusView
|
|||||||
bool raw_input_gamepad = false;
|
bool raw_input_gamepad = false;
|
||||||
bool raw_input_gamepad_sink = false;
|
bool raw_input_gamepad_sink = false;
|
||||||
bool dinput_loaded = 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
|
class IpcServer
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ int run()
|
|||||||
imgui.begin_frame();
|
imgui.begin_frame();
|
||||||
coop::draw_debug_overlay(*input);
|
coop::draw_debug_overlay(*input);
|
||||||
injection.draw();
|
injection.draw();
|
||||||
audio.draw_ui();
|
audio.draw_ui(injection.hook_status());
|
||||||
capture.draw_ui();
|
capture.draw_ui();
|
||||||
|
|
||||||
RECT client = {};
|
RECT client = {};
|
||||||
|
|||||||
Reference in New Issue
Block a user