Add a per-stream op channel in AudioRingHeader (op_seq + op_* fields, version 2): the host posts re-measure / override commands, the hook applies them and re-publishes (bumping format_generation), and the host rebuilds its render client live on the change. The Audio panel (under Debug details) gains a "Re-measure rate" button and a rate/channels/bit-depth/format override -- for when detection is wrong or the channels/bit-depth were unrecoverable. Also add a debug-only IPC test harness (-DCOOP_TEST_HARNESS, off by default, absent from the shipped host): a file-based command channel that drives the host's real UI code paths (inject / audio / re-measure / override / screenshot / status) for scripted validation, instead of unreliable synthetic mouse input. Used to validate live: late-attach to coop_tone@44100 -> measured 44100, promoted to hooked; override -> 2ch state, re-measure -> reconverge. Trim the README roadmap to what's left; document the harness + rate_estimator_test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
92 lines
2.6 KiB
C++
92 lines
2.6 KiB
C++
// 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 <cstdint>
|
|
|
|
#include <windows.h>
|
|
|
|
#include "audio/audio_loopback.hpp"
|
|
#include "ipc/ipc_server.hpp"
|
|
|
|
namespace coop
|
|
{
|
|
|
|
class AudioPanel
|
|
{
|
|
public:
|
|
// The window whose process audio to mirror (0 if none); typically the
|
|
// injected game's HWND.
|
|
void set_target(HWND target)
|
|
{
|
|
target_ = target;
|
|
}
|
|
|
|
// `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);
|
|
|
|
#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:
|
|
HWND target_ = nullptr;
|
|
DWORD dev_pid_ = 0; // test harness only: force a (windowless) target pid; 0 in production
|
|
bool enabled_ = false;
|
|
AudioMirror mirror_;
|
|
|
|
// 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
|