Audio: operator re-measure + format override (host<->hook op channel)

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>
This commit is contained in:
2026-06-22 01:46:59 +02:00
parent c24bfdd64f
commit 90f40ae479
13 changed files with 607 additions and 111 deletions

View File

@@ -11,8 +11,14 @@
#include <windows.h>
#include <mmreg.h>
#include <timeapi.h>
#ifdef COOP_TEST_HARNESS
#include <sstream>
#include <vector>
#endif
#include <winrt/Windows.Foundation.h>
#include "imgui.h"
@@ -27,6 +33,7 @@
#include "injection_panel.hpp"
#include "input/input_worker.hpp"
#include "log_panel.hpp"
#include "test_harness.hpp"
#include "ui/app_chrome.hpp"
namespace
@@ -82,6 +89,102 @@ void draw_screenshot_toast(double seconds_since, const std::string& name)
ImGui::End();
}
#ifdef COOP_TEST_HARNESS
std::wstring widen(const std::string& s)
{
if (s.empty())
{
return {};
}
const int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(s.size()), nullptr, 0);
std::wstring w(static_cast<std::size_t>(n), L'\0');
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(s.size()), w.data(), n);
return w;
}
// Run a test-harness command on the main thread, hitting the same code the UI buttons do.
// Returns a one-line response the driver reads back.
std::string apply_test_command(const std::string& cmd, coop::UiState& ui, coop::InjectionPanel& injection,
coop::AudioPanel& audio, coop::D3D11Window& window)
{
std::vector<std::string> tok;
{
std::istringstream is(cmd);
std::string t;
while (is >> t)
{
tok.push_back(t);
}
}
if (tok.empty())
{
return "empty";
}
const std::string& v = tok[0];
auto arg = [&](std::size_t i) -> std::string { return i < tok.size() ? tok[i] : std::string(); };
auto num = [&](std::size_t i) -> unsigned { return static_cast<unsigned>(std::strtoul(arg(i).c_str(), nullptr, 10)); };
if (v == "inject")
{
const unsigned long pid = injection.dev_inject_by_name(widen(arg(1)));
return pid != 0 ? ("ok pid " + std::to_string(pid)) : "fail no-process-or-inject-failed";
}
if (v == "audio")
{
const bool on = arg(1) == "on";
if (on)
{
audio.dev_set_pid(injection.target_pid());
}
audio.dev_set_enabled(on);
return "ok";
}
if (v == "debug")
{
ui.debug_details = (arg(1) == "on");
return "ok";
}
if (v == "remeasure")
{
audio.dev_request_op(num(1), coop::AudioRingOp_Remeasure, 0, 0, 0, 0);
return "ok";
}
if (v == "override")
{
const std::uint32_t tag = arg(5) == "float" ? static_cast<std::uint32_t>(WAVE_FORMAT_IEEE_FLOAT)
: static_cast<std::uint32_t>(WAVE_FORMAT_PCM);
audio.dev_request_op(num(1), coop::AudioRingOp_Override, num(2), num(3), num(4), tag);
return "ok";
}
if (v == "screenshot")
{
const std::wstring p = screenshot_path();
window.request_screenshot(p);
return "ok";
}
if (v == "quit")
{
ui.request_quit = true;
return "ok";
}
if (v == "status")
{
const coop::HookStatusView st = injection.hook_status();
const std::string reason = audio.dev_reason();
char buf[512];
std::snprintf(buf, sizeof(buf),
"audio_running=%d source=%s rate=%u ch=%u state=%u streams=%u inj_pid=%lu inj_state=%d "
"reason=%s",
audio.dev_running() ? 1 : 0, audio.dev_source().c_str(), audio.dev_rate(),
audio.dev_channels(), st.audio_streams[0].format_state, st.audio_streams_seen,
injection.target_pid(), static_cast<int>(injection.target_state()),
reason.empty() ? "-" : reason.c_str());
return buf;
}
return "unknown-command";
}
#endif // COOP_TEST_HARNESS
#ifdef COOP_WITH_STEAM
// Absolute path to the bundled Steam Input action manifest (next to the exe).
std::string steam_manifest_path()
@@ -208,6 +311,9 @@ int run()
coop::register_ui_settings(ui);
coop::FrameStats stats;
coop::TestHarness harness; // debug builds only; a no-op shim otherwise
harness.init();
// Frame-sync: the hook generation we last presented (so we wait for the next one).
std::uint32_t last_synced_gen = 0;
@@ -249,6 +355,13 @@ int run()
stats.tick(ImGui::GetIO().DeltaTime * 1000.0f);
log.pull(injection); // drain hook log lines even while the Log window is hidden
#ifdef COOP_TEST_HARNESS
if (std::string tcmd = harness.poll_command(); !tcmd.empty())
{
harness.write_response(apply_test_command(tcmd, ui, injection, audio, window));
}
#endif
if (ImGui::IsKeyPressed(ImGuiKey_F1, false))
{
show_overlay = !show_overlay;