// 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 #include #include #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(0.5 * kFreq); frames += rate * 0.5 + extra; return est.feed(static_cast(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(rate), "steady rate converges confidently"); if (!(r.done && r.rate == static_cast(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; }