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

@@ -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)
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 +
# decorated-window client offset). Header-only, no device.
add_executable(mkb_map_test mkb_map_test.cpp)
@@ -163,6 +172,7 @@ coop_output_subdir(tests
mkb_map_test
audio_mix_test
rate_estimator_test
audio_overrides_test
audio_loopback_test
audio_hook_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;
}