Audio: per-game persisted format overrides + auto-learn

Persist audio overrides keyed by game image name (coop_audio_overrides.ini
next to the exe) so a known-bad game is auto-corrected on its next launch:
on attach to a guessed stream the host applies any saved override, and a
manual Override now saves too. A format the hook catches exactly at
IAudioClient::Initialize is auto-saved as that game's override (ground truth);
if it overwrites a differing stored value, a warning is logged. Host-originated
log lines now reach the Log window via IpcServer::host_log (color-coded).

Validated live (harness): a pre-seeded override for coop_tone is auto-applied
over the guess (state -> manual override, 48000/2/16). audio_overrides_test
covers persist/reload/case-insensitive lookup/differing-overwrite. Trim README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 01:59:11 +02:00
parent fe6f8462ab
commit 32028cb286
12 changed files with 483 additions and 12 deletions

View File

@@ -79,8 +79,10 @@ and covers anything the hooked path doesn't (Vulkan, D3D9 — see Roadmap).
loopback fallback is always format-correct. The Audio panel shows each stream's loopback fallback is always format-correct. The Audio panel shows each stream's
format provenance (*known* / *measuring* / *measured rate* / *low-confidence* / format provenance (*known* / *measuring* / *measured rate* / *low-confidence* /
*override*) so the assumption is visible, and (under Debug details) lets the operator *override*) so the assumption is visible, and (under Debug details) lets the operator
**re-measure** the rate or **override** the format when the guess is wrong. Streams **re-measure** the rate or **override** the format when the guess is wrong. Overrides
created *after* injection are captured exactly. are **remembered per game** (and a format caught exactly at `Initialize` is auto-saved
as that game's override), so a known-bad game is corrected automatically next launch.
Streams created *after* injection are captured exactly.
- **Debug-oriented UI:** the ImGui overlay is laid out for diagnosing the - **Debug-oriented UI:** the ImGui overlay is laid out for diagnosing the
pipeline, not for end use. F1 hides it entirely so the window is a clean mirror pipeline, not for end use. F1 hides it entirely so the window is a clean mirror
for RPT; F2 frees the operator cursor; **F10 saves a PNG screenshot** (back buffer, for RPT; F2 frees the operator cursor; **F10 saves a PNG screenshot** (back buffer,
@@ -90,14 +92,6 @@ and covers anything the hooked path doesn't (Vulkan, D3D9 — see Roadmap).
### Planned (next up) ### Planned (next up)
- **Per-game persisted audio overrides + auto-learn.** The Audio panel already lets
the operator re-measure or override a stream's format (Debug details → *Fix the
primary stream*), but the override is session-only. Persist overrides **keyed by game
image name** to a small store next to the exe so a known-bad game is auto-corrected on
its next launch, and **auto-save a format caught exactly at `IAudioClient::Initialize`
as that game's override** (ground truth) so a later late-attach is corrected
automatically. If a caught format overwrites a stored override that *differs*, log a
warning.
- **Auto re-attach the same game on relaunch (session-only).** A checkbox on the - **Auto re-attach the same game on relaunch (session-only).** A checkbox on the
attached (or just-terminated) target, default off, *not* persisted, **always visible** attached (or just-terminated) target, default off, *not* persisted, **always visible**
(it's the recommended recovery path, not a diagnostic). While on and the target has (it's the recommended recovery path, not a diagnostic). While on and the target has
@@ -191,6 +185,9 @@ ctest --test-dir build -C Debug --output-on-failure
converges to the right standard rate, rejects burst windows (never commits to a wrong converges to the right standard rate, rejects burst windows (never commits to a wrong
neighbour, incl. the real 46205 misread), flags a genuinely non-standard rate neighbour, incl. the real 46205 misread), flags a genuinely non-standard rate
low-confidence instead of spinning, and ignores idle windows. Pure logic, no device. low-confidence instead of spinning, and ignores idle windows. Pure logic, no device.
- **`audio_overrides_test`** — unit test of the per-game audio override store
(persist/reload, case-insensitive lookup by image name, and the differing-overwrite
detection that drives the warning). No device.
- **`audio_hook_test`** — in-process self-test of the WASAPI render-hook's **format - **`audio_hook_test`** — in-process self-test of the WASAPI render-hook's **format
detection**, the part that gets pitch right. Using a shared configurable detection**, the part that gets pitch right. Using a shared configurable
`ToneSource` (the same render helper `coop_tone` uses), it renders tones at a matrix `ToneSource` (the same render helper `coop_tone` uses), it renders tones at a matrix

View File

@@ -19,6 +19,7 @@ add_executable(coop_host WIN32
src/capture/window_capture.cpp src/capture/window_capture.cpp
src/capture/shared_texture.cpp src/capture/shared_texture.cpp
src/audio/audio_loopback.cpp src/audio/audio_loopback.cpp
src/audio/audio_overrides.cpp
src/audio/process_loopback_capture.cpp src/audio/process_loopback_capture.cpp
src/test_harness.cpp) src/test_harness.cpp)

View File

@@ -0,0 +1,153 @@
#include "audio/audio_overrides.hpp"
#include <cctype>
#include <fstream>
#include <sstream>
#include <windows.h>
#include <mmreg.h> // WAVE_FORMAT_* (needs windows.h first)
#include "coop/tool_paths.hpp"
namespace coop
{
namespace
{
std::wstring to_lower(std::wstring s)
{
for (wchar_t& c : s)
{
c = static_cast<wchar_t>(::towlower(c));
}
return s;
}
std::string narrow(const std::wstring& w)
{
std::string s;
s.reserve(w.size());
for (wchar_t c : w) // image names + our format tokens are ASCII
{
s.push_back(static_cast<char>(c & 0x7F));
}
return s;
}
std::wstring widen(const std::string& s)
{
std::wstring w;
w.reserve(s.size());
for (char c : s)
{
w.push_back(static_cast<wchar_t>(static_cast<unsigned char>(c)));
}
return w;
}
} // namespace
AudioOverrideStore::AudioOverrideStore(std::wstring path) : path_(std::move(path))
{
if (path_.empty())
{
path_ = exe_directory() + L"coop_audio_overrides.ini";
}
}
std::wstring AudioOverrideStore::key_of(const std::wstring& image_name)
{
const std::size_t slash = image_name.find_last_of(L"\\/");
return to_lower(slash == std::wstring::npos ? image_name : image_name.substr(slash + 1));
}
void AudioOverrideStore::load()
{
map_.clear();
std::ifstream f(path_.c_str());
if (!f)
{
return;
}
std::string line;
while (std::getline(f, line))
{
// "<image> = <rate> <ch> <bits> <pcm|float>"; skip blank lines and # comments.
const std::size_t hash = line.find('#');
if (hash != std::string::npos)
{
line.resize(hash);
}
const std::size_t eq = line.find('=');
if (eq == std::string::npos)
{
continue;
}
std::string name = line.substr(0, eq);
// trim trailing/leading whitespace from the name
while (!name.empty() && std::isspace(static_cast<unsigned char>(name.back())))
{
name.pop_back();
}
std::size_t b = 0;
while (b < name.size() && std::isspace(static_cast<unsigned char>(name[b])))
{
++b;
}
name = name.substr(b);
if (name.empty())
{
continue;
}
std::istringstream vs(line.substr(eq + 1));
AudioFormatOverride fmt;
std::string tag;
vs >> fmt.rate >> fmt.channels >> fmt.bits >> tag;
if (!fmt.valid())
{
continue;
}
fmt.format_tag = (tag == "float") ? WAVE_FORMAT_IEEE_FLOAT : WAVE_FORMAT_PCM;
map_[key_of(widen(name))] = fmt;
}
}
bool AudioOverrideStore::find(const std::wstring& image_name, AudioFormatOverride& out) const
{
const auto it = map_.find(key_of(image_name));
if (it == map_.end())
{
return false;
}
out = it->second;
return true;
}
void AudioOverrideStore::set(const std::wstring& image_name, const AudioFormatOverride& fmt, bool* differed)
{
const std::wstring key = key_of(image_name);
if (differed != nullptr)
{
const auto it = map_.find(key);
*differed = (it != map_.end() && it->second != fmt);
}
map_[key] = fmt;
save();
}
void AudioOverrideStore::save() const
{
std::ofstream f(path_.c_str(), std::ios::trunc);
if (!f)
{
return;
}
f << "# CoopAllTheThings per-game audio format overrides (auto-managed)\n";
f << "# <image.exe> = <rate> <channels> <bits> <pcm|float>\n";
for (const auto& [name, fmt] : map_)
{
f << narrow(name) << " = " << fmt.rate << ' ' << fmt.channels << ' ' << fmt.bits << ' '
<< (fmt.format_tag == WAVE_FORMAT_IEEE_FLOAT ? "float" : "pcm") << '\n';
}
}
} // namespace coop

View File

@@ -0,0 +1,68 @@
// Per-game audio-format overrides, persisted next to the exe so a game whose hooked
// format was wrong/unrecoverable is auto-corrected on its next launch. Keyed by the
// game's image name (case-insensitive). Two writers: the operator's manual Override, and
// auto-learning a format the hook caught exactly at IAudioClient::Initialize (ground
// truth). A human-readable one-line-per-game text file:
//
// # CoopAllTheThings per-game audio overrides
// brotato.exe = 44100 2 32 float
// snb.exe = 48000 2 16 pcm
#pragma once
#include <cstdint>
#include <map>
#include <string>
namespace coop
{
struct AudioFormatOverride
{
std::uint32_t rate = 0;
std::uint32_t channels = 0;
std::uint32_t bits = 0;
std::uint32_t format_tag = 0; // WAVE_FORMAT_PCM (1) / WAVE_FORMAT_IEEE_FLOAT (3)
[[nodiscard]] bool valid() const
{
return rate != 0 && channels != 0 && bits != 0;
}
bool operator==(const AudioFormatOverride& o) const
{
return rate == o.rate && channels == o.channels && bits == o.bits && format_tag == o.format_tag;
}
bool operator!=(const AudioFormatOverride& o) const
{
return !(*this == o);
}
};
class AudioOverrideStore
{
public:
// `path` empty -> default (exe_dir/coop_audio_overrides.ini). Does not load yet.
explicit AudioOverrideStore(std::wstring path = {});
void load(); // (re)read the file; missing file is fine (empty store)
// Look up an override by image name (case-insensitive, basename). False if none.
[[nodiscard]] bool find(const std::wstring& image_name, AudioFormatOverride& out) const;
// Store image_name -> fmt and persist immediately. Sets *differed = true when an
// existing entry for that game differed from `fmt` (caller warns the operator).
void set(const std::wstring& image_name, const AudioFormatOverride& fmt, bool* differed = nullptr);
[[nodiscard]] const std::wstring& path() const
{
return path_;
}
private:
static std::wstring key_of(const std::wstring& image_name); // lowercased basename
void save() const;
std::wstring path_;
std::map<std::wstring, AudioFormatOverride> map_;
};
} // namespace coop

View File

@@ -1,6 +1,7 @@
#include "audio_panel.hpp" #include "audio_panel.hpp"
#include <algorithm> #include <algorithm>
#include <cstdio>
#include "imgui.h" #include "imgui.h"
@@ -68,8 +69,101 @@ ImVec4 audio_format_state_color(std::uint32_t state)
} }
} }
// Basename of an image path, narrowed to ASCII for a log line.
std::string image_basename(const std::wstring& image_path)
{
const std::size_t slash = image_path.find_last_of(L"\\/");
const std::wstring w = slash == std::wstring::npos ? image_path : image_path.substr(slash + 1);
std::string out;
out.reserve(w.size());
for (wchar_t c : w)
{
out.push_back(static_cast<char>(c & 0x7F));
}
return out;
}
} // namespace } // namespace
std::wstring AudioPanel::image_name_from_pid(DWORD pid)
{
if (pid == 0)
{
return {};
}
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (h == nullptr)
{
return {};
}
wchar_t buf[MAX_PATH] = {};
DWORD n = MAX_PATH;
std::wstring name;
if (QueryFullProcessImageNameW(h, 0, buf, &n))
{
name.assign(buf, n);
}
CloseHandle(h);
return name;
}
void AudioPanel::manage_overrides(const HookStatusView& status, DWORD pid)
{
if (pid != applied_pid_) // new target -> resolve its image name and reset session flags
{
applied_pid_ = pid;
target_image_ = image_name_from_pid(pid);
override_applied_ = false;
exact_saved_ = false;
}
if (pid == 0 || target_image_.empty() || !mirror_.running() || status.audio_streams_seen == 0)
{
return;
}
const AudioStreamInfo& s = status.audio_streams[0]; // the primary (mirrored) stream
const std::uint32_t st = s.format_state;
if (st == AudioFormat_Exact)
{
// Ground truth: persist it as this game's override (so a later late-attach is fixed).
if (!exact_saved_)
{
exact_saved_ = true;
const AudioFormatOverride fmt{s.sample_rate, s.channels, s.bits, s.format_tag};
bool differed = false;
overrides_.set(target_image_, fmt, &differed);
if (differed && logger_)
{
char msg[160];
std::snprintf(msg, sizeof(msg),
"%s: exact format %uHz/%uch/%ubit caught -> replaced a DIFFERING saved override",
image_basename(target_image_).c_str(), fmt.rate, fmt.channels, fmt.bits);
logger_(LogLevel_Warn, msg);
}
}
}
else if (st == AudioFormat_Measuring || st == AudioFormat_Measured || st == AudioFormat_LowConfidence)
{
// A guessed stream: if we have a saved override for this game, apply it.
if (!override_applied_)
{
override_applied_ = true;
AudioFormatOverride ov;
if (overrides_.find(target_image_, ov))
{
mirror_.request_op(0, AudioRingOp_Override, ov.rate, ov.channels, ov.bits, ov.format_tag);
if (logger_)
{
char msg[160];
std::snprintf(msg, sizeof(msg), "%s: applied saved audio override %uHz/%uch/%ubit",
image_basename(target_image_).c_str(), ov.rate, ov.channels, ov.bits);
logger_(LogLevel_Info, msg);
}
}
}
}
}
void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details) void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details)
{ {
DWORD pid = 0; DWORD pid = 0;
@@ -110,6 +204,8 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details)
mirror_.stop(); mirror_.stop();
} }
manage_overrides(status, pid); // auto-apply a saved override / auto-save a caught format
if (mirror_.running()) if (mirror_.running())
{ {
const AudioMirror::Source src = mirror_.source(); const AudioMirror::Source src = mirror_.source();
@@ -293,8 +389,15 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details)
const std::uint32_t tag = const std::uint32_t tag =
ov_fmt_idx_ == 1 ? static_cast<std::uint32_t>(WAVE_FORMAT_IEEE_FLOAT) ov_fmt_idx_ == 1 ? static_cast<std::uint32_t>(WAVE_FORMAT_IEEE_FLOAT)
: static_cast<std::uint32_t>(WAVE_FORMAT_PCM); : static_cast<std::uint32_t>(WAVE_FORMAT_PCM);
mirror_.request_op(0, AudioRingOp_Override, static_cast<std::uint32_t>(ov_rate_), const AudioFormatOverride fmt{static_cast<std::uint32_t>(ov_rate_),
static_cast<std::uint32_t>(ov_channels_), bits, tag); static_cast<std::uint32_t>(ov_channels_), bits, tag};
mirror_.request_op(0, AudioRingOp_Override, fmt.rate, fmt.channels, fmt.bits, fmt.format_tag);
// Persist it for this game so the correction sticks across launches.
if (!target_image_.empty())
{
overrides_.set(target_image_, fmt);
override_applied_ = true; // don't let manage_overrides re-apply an older saved value
}
} }
} }

View File

@@ -4,10 +4,13 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <functional>
#include <string>
#include <windows.h> #include <windows.h>
#include "audio/audio_loopback.hpp" #include "audio/audio_loopback.hpp"
#include "audio/audio_overrides.hpp"
#include "ipc/ipc_server.hpp" #include "ipc/ipc_server.hpp"
namespace coop namespace coop
@@ -16,6 +19,11 @@ namespace coop
class AudioPanel class AudioPanel
{ {
public: public:
AudioPanel()
{
overrides_.load();
}
// The window whose process audio to mirror (0 if none); typically the // The window whose process audio to mirror (0 if none); typically the
// injected game's HWND. // injected game's HWND.
void set_target(HWND target) void set_target(HWND target)
@@ -23,6 +31,13 @@ public:
target_ = target; target_ = target;
} }
// Wire a sink for host-side log lines (override-overwrite warnings etc.). main
// connects this to the injection panel's Log-window channel.
void set_logger(std::function<void(std::uint32_t, const char*)> logger)
{
logger_ = std::move(logger);
}
// `status` is the hook's back-channel, for the render-stream view. With // `status` is the hook's back-channel, for the render-stream view. With
// `debug_details` on, the per-stream table is shown. // `debug_details` on, the per-stream table is shown.
void draw_ui(const HookStatusView& status, bool debug_details); void draw_ui(const HookStatusView& status, bool debug_details);
@@ -66,11 +81,24 @@ public:
#endif #endif
private: private:
// Auto-apply a stored override over a guessed stream, and auto-save a format the hook
// caught exactly at Initialize (warning if it overwrites a differing stored value).
void manage_overrides(const HookStatusView& status, DWORD pid);
static std::wstring image_name_from_pid(DWORD pid);
HWND target_ = nullptr; HWND target_ = nullptr;
DWORD dev_pid_ = 0; // test harness only: force a (windowless) target pid; 0 in production DWORD dev_pid_ = 0; // test harness only: force a (windowless) target pid; 0 in production
bool enabled_ = false; bool enabled_ = false;
AudioMirror mirror_; AudioMirror mirror_;
// Per-game persisted overrides + per-target session bookkeeping.
AudioOverrideStore overrides_;
std::function<void(std::uint32_t, const char*)> logger_;
DWORD applied_pid_ = 0; // pid the session flags below were reset for
std::wstring target_image_; // current target's image path (store keys on the basename)
bool override_applied_ = false; // applied (or confirmed none) the stored override this session
bool exact_saved_ = false; // saved the exact Initialize format this session
// Per-stream activity tracking for the "live" column. Audio buffers release in // 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 // 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 // flickers. Instead we remember when each stream last advanced and debounce the

View File

@@ -94,6 +94,13 @@ public:
server_.drain_logs(std::forward<F>(emit)); server_.drain_logs(std::forward<F>(emit));
} }
// Emit a host-side line into the Log window (color-coded by level), e.g. an
// override-overwrite warning. No-op if not connected.
void host_log(std::uint32_t level, const char* text)
{
server_.host_log(level, text);
}
// --- Present-hook video path (consumed by the Video mirror panel) ---------- // --- Present-hook video path (consumed by the Video mirror panel) ----------
[[nodiscard]] unsigned long target_pid() const [[nodiscard]] unsigned long target_pid() const

View File

@@ -126,6 +126,15 @@ void IpcServer::set_subsystem_enabled(std::uint32_t subsystem, bool enabled)
} }
} }
void IpcServer::host_log(std::uint32_t level, const char* text)
{
std::scoped_lock lock(mutex_);
if (log_ring_ != nullptr)
{
log_ring_push(*log_ring_, GetCurrentProcessId(), level, GetTickCount64(), text);
}
}
void IpcServer::stop() void IpcServer::stop()
{ {
std::scoped_lock lock(mutex_); std::scoped_lock lock(mutex_);

View File

@@ -113,6 +113,10 @@ public:
} }
} }
// Push a host-originated line into the shared log ring (level = LogLevel), so it
// shows color-coded in the Log window next to the hook's lines. No-op if not started.
void host_log(std::uint32_t level, const char* text);
[[nodiscard]] bool running() const [[nodiscard]] bool running() const
{ {
return block_ != nullptr; return block_ != nullptr;

View File

@@ -285,6 +285,9 @@ int run()
return 1; return 1;
} }
capture.set_injection(&injection); // for the Present-hook (Hooked) video source capture.set_injection(&injection); // for the Present-hook (Hooked) video source
// Route the audio panel's host-side notices (override-overwrite warnings, applied
// saved overrides) into the Log window, color-coded by level.
audio.set_logger([&injection](std::uint32_t level, const char* text) { injection.host_log(level, text); });
// Controller polling + forwarding runs on its own thread so the render frame rate // Controller polling + forwarding runs on its own thread so the render frame rate
// (which can drop, especially with frame-sync) never throttles input. XInput is the // (which can drop, especially with frame-sync) never throttles input. XInput is the

View File

@@ -36,6 +36,15 @@ add_executable(rate_estimator_test rate_estimator_test.cpp)
target_include_directories(rate_estimator_test PRIVATE ${CMAKE_SOURCE_DIR}/hook/src) target_include_directories(rate_estimator_test PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)
add_test(NAME rate_estimator_test COMMAND rate_estimator_test) add_test(NAME rate_estimator_test COMMAND rate_estimator_test)
# Unit test for the per-game audio override store (persist/reload, case-insensitive
# lookup, differing-overwrite detection). Reuses the shipping source. No device.
add_executable(audio_overrides_test
audio_overrides_test.cpp
${CMAKE_SOURCE_DIR}/host/src/audio/audio_overrides.cpp)
target_include_directories(audio_overrides_test PRIVATE ${CMAKE_SOURCE_DIR}/host/src)
target_link_libraries(audio_overrides_test PRIVATE coop_common)
add_test(NAME audio_overrides_test COMMAND audio_overrides_test)
# Unit test for the host->game mouse coordinate mapping (letterbox inverse + # Unit test for the host->game mouse coordinate mapping (letterbox inverse +
# decorated-window client offset). Header-only, no device. # decorated-window client offset). Header-only, no device.
add_executable(mkb_map_test mkb_map_test.cpp) add_executable(mkb_map_test mkb_map_test.cpp)
@@ -163,6 +172,7 @@ coop_output_subdir(tests
mkb_map_test mkb_map_test
audio_mix_test audio_mix_test
rate_estimator_test rate_estimator_test
audio_overrides_test
audio_loopback_test audio_loopback_test
audio_hook_test audio_hook_test
srgb_format_test srgb_format_test

View File

@@ -0,0 +1,88 @@
// Unit test for the per-game audio override store (save/load round-trip, case-insensitive
// lookup by image name, and the differing-overwrite detection that drives the warning).
#include <cstdint>
#include <cstdio>
#include <string>
#include <windows.h>
#include <mmreg.h>
#include "audio/audio_overrides.hpp"
using namespace coop;
namespace
{
int g_failures = 0;
void check(bool ok, const char* what)
{
if (!ok)
{
std::printf("FAIL: %s\n", what);
++g_failures;
}
else
{
std::printf(" ok: %s\n", what);
}
}
std::wstring temp_path()
{
wchar_t dir[MAX_PATH] = {};
GetTempPathW(MAX_PATH, dir);
return std::wstring(dir) + L"coop_overrides_test_" + std::to_wstring(GetCurrentProcessId()) + L".ini";
}
} // namespace
int main()
{
const std::wstring path = temp_path();
DeleteFileW(path.c_str());
const AudioFormatOverride brotato{44100, 2, 32, WAVE_FORMAT_IEEE_FLOAT};
const AudioFormatOverride snb{48000, 2, 16, WAVE_FORMAT_PCM};
{
AudioOverrideStore store(path);
store.load(); // missing file -> empty
AudioFormatOverride got;
check(!store.find(L"brotato.exe", got), "empty store: no entry");
bool differed = true;
store.set(L"C:\\games\\Brotato.exe", brotato, &differed); // full path -> basename key
check(!differed, "first set: not a differing overwrite");
store.set(L"snb.exe", snb, &differed);
check(!differed, "new game set: not a differing overwrite");
// Same value again -> not differing.
store.set(L"brotato.exe", brotato, &differed);
check(!differed, "identical re-set: not differing");
// Different value -> differing (drives the warning).
const AudioFormatOverride brotato2{48000, 2, 32, WAVE_FORMAT_IEEE_FLOAT};
store.set(L"brotato.exe", brotato2, &differed);
check(differed, "changed value: flagged as differing overwrite");
}
// Reload from disk in a fresh store: persistence + case-insensitive basename lookup.
{
AudioOverrideStore store(path);
store.load();
AudioFormatOverride got;
check(store.find(L"BROTATO.EXE", got), "reload: found case-insensitively");
check(got == AudioFormatOverride{48000, 2, 32, WAVE_FORMAT_IEEE_FLOAT}, "reload: latest value persisted");
check(store.find(L"D:\\steam\\snb.exe", got) && got == snb, "reload: found by full path basename");
check(!store.find(L"unknown.exe", got), "reload: unknown game absent");
}
DeleteFileW(path.c_str());
if (g_failures == 0)
{
std::printf("PASS audio_overrides_test\n");
return 0;
}
std::printf("FAILED audio_overrides_test (%d)\n", g_failures);
return 1;
}