Audio: robust rate estimation + color-coded log levels
Harden the guessed-stream sample-rate measurement that produced wrong rates (e.g. 44100 read as ~46205). New rate_estimator.hpp measures over longer ~0.5 s windows, rejects any window that doesn't snap to a standard rate (standard rates are >8% apart, so a quantization/burst error big enough to miss one lands in no-man's-land, never on a wrong neighbour), and requires consensus across windows before committing. If consensus isn't reached it commits a low-confidence estimate (new AudioFormat_LowConfidence, shown red) rather than spinning or publishing garbage. Pure logic, unit-tested with adversarial cadences (rate_estimator_test) incl. the real 46205 bug value. Add log severity levels: hook logw/loge set LogRecord.level; the host Log window colors warnings amber and errors red. The low-confidence rate logs a warning. Protocol -> v15 (new format states); also reserves AudioFormat_Override. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,12 @@ add_executable(audio_mix_test audio_mix_test.cpp)
|
||||
target_include_directories(audio_mix_test PRIVATE ${CMAKE_SOURCE_DIR}/host/src)
|
||||
add_test(NAME audio_mix_test COMMAND audio_mix_test)
|
||||
|
||||
# Unit test for the robust sample-rate estimator (consensus / reject-non-standard /
|
||||
# low-confidence). Pure header logic fed synthetic adversarial cadences. No device.
|
||||
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 host->game mouse coordinate mapping (letterbox inverse +
|
||||
# decorated-window client offset). Header-only, no device.
|
||||
add_executable(mkb_map_test mkb_map_test.cpp)
|
||||
@@ -156,6 +162,7 @@ coop_output_subdir(tests
|
||||
mkb_ring_test
|
||||
mkb_map_test
|
||||
audio_mix_test
|
||||
rate_estimator_test
|
||||
audio_loopback_test
|
||||
audio_hook_test
|
||||
srgb_format_test
|
||||
|
||||
156
tests/rate_estimator_test.cpp
Normal file
156
tests/rate_estimator_test.cpp
Normal file
@@ -0,0 +1,156 @@
|
||||
// Unit test for the robust sample-rate estimator (hook/src/rate_estimator.hpp).
|
||||
//
|
||||
// Feeds the estimator synthetic, adversarial render cadences -- the exact failure modes
|
||||
// that made the old 200 ms single-window measurement publish a bogus rate (e.g. 44100
|
||||
// read as ~46205) -- and asserts the new estimator: converges to the right standard rate,
|
||||
// rejects burst windows and never commits to a wrong neighbour, flags a genuinely
|
||||
// non-standard rate low-confidence instead of forever spinning, and ignores idle windows.
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <initializer_list>
|
||||
|
||||
#include "rate_estimator.hpp"
|
||||
|
||||
using namespace coop::hook;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Drives an estimator with a controllable QPC clock. One feed per "window" (we use a
|
||||
// 0.5 s step that matches the estimator's window, so each feed after the first completes
|
||||
// exactly one window -- making each window's frame count individually controllable).
|
||||
struct Sim
|
||||
{
|
||||
RateEstimator est;
|
||||
static constexpr std::int64_t kFreq = 1'000'000; // 1 MHz (microseconds)
|
||||
std::int64_t qpc = 0;
|
||||
double frames = 0.0;
|
||||
|
||||
// Advance one window (0.5 s) accumulating `rate` Hz plus `extra` burst frames, feed it.
|
||||
RateEstimate window(double rate, double extra = 0.0)
|
||||
{
|
||||
qpc += static_cast<std::int64_t>(0.5 * kFreq);
|
||||
frames += rate * 0.5 + extra;
|
||||
return est.feed(static_cast<std::uint64_t>(frames), qpc, kFreq);
|
||||
}
|
||||
};
|
||||
|
||||
// Feed steady `rate` until done (or give up after `max` windows). Returns the result.
|
||||
RateEstimate run_steady(double rate, int max = 40)
|
||||
{
|
||||
Sim s;
|
||||
RateEstimate r;
|
||||
for (int i = 0; i < max; ++i)
|
||||
{
|
||||
r = s.window(rate);
|
||||
if (r.done)
|
||||
{
|
||||
return r;
|
||||
}
|
||||
}
|
||||
return r; // not done
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
// --- snap_standard_rate: the core "reject non-standard" rule -----------------
|
||||
check(snap_standard_rate(44100.0) == 44100, "snap exact 44100");
|
||||
check(snap_standard_rate(48000.0) == 48000, "snap exact 48000");
|
||||
check(snap_standard_rate(44100.0 * 1.015) == 44100, "snap 44100 within +1.5%");
|
||||
check(snap_standard_rate(96000.0 * 0.99) == 96000, "snap 96000 within -1%");
|
||||
// 46205 is the real-world bogus reading: it sits between 44100 and 48000 and must NOT
|
||||
// snap to either (this is why the new estimator rejects it instead of publishing it).
|
||||
check(snap_standard_rate(46205.0) == 0, "46205 snaps to nothing (the old bug value)");
|
||||
check(snap_standard_rate(45000.0) == 0, "45000 (non-standard) snaps to nothing");
|
||||
|
||||
// --- steady standard rates converge, confidently -----------------------------
|
||||
for (double rate : {44100.0, 48000.0, 96000.0, 22050.0})
|
||||
{
|
||||
const RateEstimate r = run_steady(rate);
|
||||
check(r.done && r.confident && r.rate == static_cast<std::uint32_t>(rate),
|
||||
"steady rate converges confidently");
|
||||
if (!(r.done && r.rate == static_cast<std::uint32_t>(rate)))
|
||||
{
|
||||
std::printf(" (rate=%.0f -> done=%d confident=%d got=%u)\n", rate, r.done, r.confident, r.rate);
|
||||
}
|
||||
}
|
||||
|
||||
// --- realistic jitter: 44100 with a small per-window wobble still snaps -------
|
||||
{
|
||||
Sim s;
|
||||
RateEstimate r;
|
||||
const double wobble[] = {+150.0, -120.0, +90.0, -150.0, +60.0, -90.0, +130.0, -40.0};
|
||||
for (int i = 0; i < 30 && !r.done; ++i)
|
||||
{
|
||||
r = s.window(44100.0, wobble[i % 8]); // ~0.3% jitter, within the snap band
|
||||
}
|
||||
check(r.done && r.confident && r.rate == 44100, "44100 with small jitter -> 44100 confident");
|
||||
}
|
||||
|
||||
// --- a burst window can't decide the rate (consensus rejects it) -------------
|
||||
// Inflate a single window enough to read as 48000 (true is 44100); the surrounding
|
||||
// clean windows must still win, proving one burst never commits to the wrong rate.
|
||||
{
|
||||
Sim s;
|
||||
(void)s.window(44100.0); // window 1: discarded (warm-up)
|
||||
// window 2: burst -- 0.5 s of 48000 instead of 44100 (extra ~1950 frames) -> reads 48000.
|
||||
const RateEstimate burst = s.window(44100.0, (48000.0 - 44100.0) * 0.5);
|
||||
check(!burst.done, "single burst window does not commit");
|
||||
// windows 3..N: clean 44100 -> consensus on 44100.
|
||||
RateEstimate r = burst;
|
||||
for (int i = 0; i < 10 && !r.done; ++i)
|
||||
{
|
||||
r = s.window(44100.0);
|
||||
}
|
||||
check(r.done && r.confident && r.rate == 44100, "burst rejected; converges to 44100, not 48000");
|
||||
}
|
||||
|
||||
// --- a genuinely non-standard rate ends as low-confidence, not a spin ---------
|
||||
{
|
||||
const RateEstimate r = run_steady(45000.0, 40);
|
||||
check(r.done && !r.confident, "non-standard 45000 -> done but LOW-confidence");
|
||||
check(r.rate >= 44600 && r.rate <= 45400, "low-confidence estimate is ~45000");
|
||||
if (r.done)
|
||||
{
|
||||
std::printf(" (45000 -> confident=%d rate=%u)\n", r.confident, r.rate);
|
||||
}
|
||||
}
|
||||
|
||||
// --- idle windows never yield a bogus rate, then real audio converges --------
|
||||
{
|
||||
Sim s;
|
||||
RateEstimate r;
|
||||
for (int i = 0; i < 6; ++i)
|
||||
{
|
||||
r = s.window(0.0); // silent: no frames advance
|
||||
check(!r.done, "idle window never commits");
|
||||
}
|
||||
for (int i = 0; i < 12 && !r.done; ++i)
|
||||
{
|
||||
r = s.window(48000.0); // audio resumes
|
||||
}
|
||||
check(r.done && r.confident && r.rate == 48000, "after idle, real audio converges to 48000");
|
||||
}
|
||||
|
||||
if (g_failures == 0)
|
||||
{
|
||||
std::printf("PASS rate_estimator_test\n");
|
||||
return 0;
|
||||
}
|
||||
std::printf("FAILED rate_estimator_test (%d)\n", g_failures);
|
||||
return 1;
|
||||
}
|
||||
Reference in New Issue
Block a user