// Robust sample-rate estimation for a render stream whose format we had to guess. // // When we attach to an already-running game we never saw its IAudioClient::Initialize, // so we assume the device mix format and recover the *true* sample rate by timing how // fast the game renders frames. The naive version (one short ~200 ms window, snap to the // nearest standard rate, accept whatever came out) is fragile: WASAPI delivers audio in // quantized ~10 ms buffers, so one extra buffer at a window edge is a ~5% error over // 200 ms, which lands *between* standard rates (they're >8% apart) and used to be // published verbatim -- e.g. 44100 measured as ~46205. // // This estimator fixes that with three rules: // 1. Longer windows (~0.5 s) -> the per-buffer quantization error drops to ~2%. // 2. Reject a window that doesn't snap to a standard rate. Standard rates are far // enough apart that any error big enough to miss the right one lands in no-man's- // land rather than on a wrong neighbour, so a non-snapping window is simply noise. // 3. Require N consecutive windows to agree on the same standard rate (consensus) // before committing, so a one-off burst can't decide the rate. // If consensus isn't reached within a bounded number of attempts it commits the best // estimate flagged *low-confidence* (the host shows that in red and the operator can // re-measure or override) rather than spinning forever on a genuinely unusual rate. // // Pure logic (no Windows deps): fed (cumulative frames, QPC now, QPC frequency) so it // can be unit-tested with synthetic, adversarial cadences. See tests/rate_estimator_test. #pragma once #include namespace coop::hook { // Snap a measured rate to the nearest standard rate when within `tol` (fractional); // returns 0 when it doesn't land near any standard rate. The standard rates are spaced // >8% apart, so a 2% tolerance is unambiguous. inline std::uint32_t snap_standard_rate(double measured, double tol = 0.02) { static constexpr std::uint32_t kStd[] = {8000, 11025, 16000, 22050, 32000, 44100, 48000, 88200, 96000, 176400, 192000}; for (std::uint32_t s : kStd) { if (measured >= s * (1.0 - tol) && measured <= s * (1.0 + tol)) { return s; } } return 0; } // Outcome of feeding one measurement tick. struct RateEstimate { bool done = false; // a rate has been decided (stop feeding) std::uint32_t rate = 0; // the decided rate, valid when done bool confident = false; // true = consensus on a standard rate; false = low-confidence fallback }; class RateEstimator { public: // Window length, consensus count, and the attempt budget before giving up to a // low-confidence estimate. Public so a caller/test can tune them; the defaults are // what the hook ships. double window_seconds = 0.5; int needed_agree = 2; int max_attempts = 12; double min_audio_rate = 4000.0; // a window below this is treated as idle, not a sample // Feed the stream's cumulative frame count and a QPC timestamp (with its frequency). // Call repeatedly (e.g. each worker tick); returns done=false while still measuring. RateEstimate feed(std::uint64_t frames, std::int64_t now_qpc, std::int64_t freq) { if (freq <= 0) { return {}; } if (window_qpc_ == 0) { start_window(frames, now_qpc); // begin the first window return {}; } const std::int64_t dt = now_qpc - window_qpc_; if (dt < static_cast(window_seconds * static_cast(freq))) { return {}; // window still filling } const std::uint64_t df = frames - window_frames_; const double secs = static_cast(dt) / static_cast(freq); start_window(frames, now_qpc); // next window starts here const double raw = static_cast(df) / secs; if (raw < min_audio_rate) { // Stream went (near-)idle this window: can't trust it. Drop back to the // warm-up state so the next active window is discarded, not measured. primed_ = false; reset_consensus(); return {}; } if (!primed_) { // Discard the first full active window: a freshly-attached stream can deliver // its already-queued buffers in a burst, over-counting frames. primed_ = true; reset_consensus(); return {}; } ++attempts_; last_raw_ = raw; const std::uint32_t snapped = snap_standard_rate(raw); if (snapped != 0) { if (snapped == last_snapped_) { ++agree_; } else { last_snapped_ = snapped; agree_ = 1; } if (agree_ >= needed_agree) { return {true, snapped, true}; // consensus -> confident } } else { reset_consensus(); // a non-snapping window breaks the streak } if (attempts_ >= max_attempts) { // Give up on consensus: a snapped value seen along the way beats a raw one. const std::uint32_t best = last_snapped_ != 0 ? last_snapped_ : static_cast(last_raw_ + 0.5); return {true, best, false}; // low-confidence } return {}; } // Restart measurement from scratch (keeps the tunables). Used to re-measure on demand. void reset() { window_qpc_ = 0; window_frames_ = 0; primed_ = false; attempts_ = 0; last_raw_ = 0.0; reset_consensus(); } private: void start_window(std::uint64_t frames, std::int64_t qpc) { window_frames_ = frames; window_qpc_ = qpc; } void reset_consensus() { agree_ = 0; last_snapped_ = 0; } std::int64_t window_qpc_ = 0; std::uint64_t window_frames_ = 0; bool primed_ = false; std::uint32_t last_snapped_ = 0; int agree_ = 0; int attempts_ = 0; double last_raw_ = 0.0; }; } // namespace coop::hook