#ifndef POSITION_STATE_H #define POSITION_STATE_H #include #include #include // Caller holds the position mutex. Time is monotonic milliseconds; explicit // arguments let tests cover connection/poll races without wall-clock sleeps. class ConnectionPosition { public: void allowZeroStart() { allowZero = true; } // Queue-only anchor: identity and PLAYING are already confirmed by SOAP. // No HTTP connection, PCM submission or 1.1-second stream lease is involved. void item(unsigned id, uint32_t ms, uint32_t rate, uint64_t now, uint64_t frozen = 0) { reset(id); anchored = hadAudio = heardPosition = zeroObserved = true; relative = ms; observed = started = now; reportTime = now; reportFrames = std::max(double(frozen), double(ms) * rate / 1000); lastAudible = uint64_t(reportFrames); } void itemPoll(uint32_t ms, uint64_t now) { if (!anchored || ms < relative) return; if (ms != relative) observed = now; relative = ms; } // An eligible current-stream probe is fresh evidence, unlike a leased // normal zero. Seed only the explicit rate-change reporting coordinate. void startZero(double time) { if (!allowZero || !anchored || heardPosition || pendingAudibleBase || !std::isfinite(time) || time < started) return; conservativeStart = true; observed = uint64_t(time); heardPosition = zeroObserved = true; } bool needsStartEvidence() const { return allowZero && relative == 0; } bool audioStarted() const { return anchored && heardPosition && relative > 0 && !pendingAudibleBase; } void reset(unsigned id) { if (id != stream) allowZero = false; zeroObserved = lowerStart = conservativeStart = false; stream = id; request = 0; base = relative = lastFrames = 0; anchored = hadAudio = heardPosition = pendingAudibleBase = false; audibleBase = lastAudible = 0; reportTime = 0; reportFrames = 0; slewOffset = 0; reportingModel = false; ++generation; } void connection(unsigned id, uint64_t req, uint64_t now) { if (id != stream) reset(id); zeroObserved = lowerStart = conservativeStart = false; request = req; reportTime = 0; reportFrames = 0; slewOffset = 0; reportingModel = false; // Hold the last reported position until the first PCM offset is known. base = lastFrames; audibleBase = lastAudible; relative = 0; anchored = false; pendingAudibleBase = false; started = now; ++generation; if (!hadAudio) base = 0; } void pcm(unsigned id, uint64_t req, uint64_t firstFrame, uint64_t now, bool deferAudible = false) { if (id != stream || req != request || anchored) return; base = hadAudio ? firstFrame : 0; // S7#2 (27 Sep): the first GET closed after 1.25 s, before // any positive RelTime. Its queued PCM is not an audible offset. pendingAudibleBase = deferAudible && heardPosition; audibleBase = heardPosition ? (deferAudible ? lastAudible : base) : 0; relative = 0; anchored = hadAudio = true; started = now; ++generation; } uint64_t measurementBase() const { return base; } uint64_t token() const { return generation; } void poll(uint64_t token, uint32_t ms, uint64_t now) { if (token != generation || !anchored || (ms == 0 && (!allowZero || relative != 0 || !zeroObserved))) return; // Stop may report zero before the next GET; retain the last position. // A new connection/PCM anchor already clears relative to zero. // Position reads are cached for one second. Ignore that cache after // a handoff, and reject an old RelTime larger than this GET's lifetime. if (now - started < 1100 || ms > now - started + 1000) return; if (pendingAudibleBase) { audibleBase = base; pendingAudibleBase = false; } // Repeated leased values are the same observation, not a fresh bracket. if (!heardPosition || relative != ms) observed = now; if (allowZero && ms && !relative) lowerStart = conservativeStart = true; relative = ms; if (ms) allowZero = false; // Startup-only: reconnects retain the positive-read safeguard. if (!ms) zeroObserved = true; heardPosition = true; } uint64_t frames(uint32_t rate) { lastFrames = base + uint64_t(relative) * rate / 1000; return lastFrames; } unsigned streamId() const { return stream; } bool modelAudibleFrames(uint32_t rate, double now, double candidate, double rateScale, uint64_t &result, double relSecond = NAN, double observedMono = NAN) { if (!anchored || !heardPosition || pendingAudibleBase || now < observed || !std::isfinite(now) || !std::isfinite(candidate) || !std::isfinite(rateScale) || rateScale <= 0) return false; // RelTime is floored at observation. Project its interval to now, rather // than force the model onto a leased, whole-second staircase. // Prefer the timestamp of the actual normal SOAP read, already recorded // by the probe seed path, when it corresponds to this leased value. const double sampleMs = std::isfinite(observedMono) && relSecond == double(relative) / 1000 ? observedMono * 1000 : double(observed); if (sampleMs > now) return false; const double elapsed = (double(now) - sampleMs) * rate * rateScale / 1000; const double low = double(audibleBase) + double(relative) * rate / 1000 + elapsed; const double high = low + rate; const double bounded = std::max(low, std::min(high, candidate)); if (bounded < 0 || bounded >= double(UINT64_MAX)) return false; frames(rate); // preserve the encoded handoff coordinate lastAudible = std::max(lastAudible, uint64_t(bounded)); result = lastAudible; return true; } uint64_t audibleFrames(uint32_t rate) { frames(rate); // retain the encoded-stream coordinate for handoffs const uint64_t next = audibleBase + uint64_t(relative) * rate / 1000; if (next > lastAudible) lastAudible = next; return lastAudible; } // Smooth only the audible reporting coordinate; encoded handoffs stay exact. uint64_t smoothFrames(uint32_t rate, double now, bool model, double candidate, double scale = 1, double relSecond = NAN, double observedMono = NAN) { if (!anchored || !heardPosition || (!relative && !zeroObserved) || pendingAudibleBase || !rate) return audibleFrames(rate); const double sample = std::isfinite(observedMono) && relSecond == double(relative) / 1000 ? observedMono * 1000 : double(observed); double target = audibleBase + (double(relative) / 1000 + (relative ? .5 : 0) + std::max(0., now - sample) * scale / 1000) * rate; if (model) { const double low = target - .5 * rate; target = std::clamp(candidate, low, low + rate); } // A restarted stream begins at the conservative end of its observed // interval. Do not import a speculative zero from before TRANSITIONING. if (!reportTime && lowerStart) { reportTime = now; reportFrames = target - .5 * rate; lowerStart = false; } const double dt = reportTime ? std::max(0., now - reportTime) / 1000 : 0; const double projected = reportTime ? reportFrames + dt * rate * scale : target; const double difference = target - projected; // 15% corrects the observed 650 ms phase error in 4.34 seconds. // A qualified model correction above 750 ms is a discontinuity. // Unlocked observations constrain nominal projection below; they // cannot steer it towards a midpoint that follows the polling phase. const double correction = conservativeStart ? .05 : .15; double next = !reportTime || (model && std::abs(difference) > .75 * rate) ? target : projected + std::clamp(difference, -correction * dt * rate, correction * dt * rate); // Unlocked re-anchors use the latest quantisation endpoints and // cannot accumulate unbounded phase lag. // RelTime midpoint phase follows the polling phase. Chasing each new // midpoint runs at the polling rate (and slews a whole second when // that phase crosses a tick). Keep unity/prior projection whenever // it is still consistent with the latest quantisation interval. if (!model) next = std::clamp(projected, target - .5 * rate, target + .5 * rate); reportFrames = model ? std::max(double(lastAudible), std::max(0., next)) : std::max(0., next); lastAudible = uint64_t(reportFrames); reportTime = now; reportingModel = model; slewOffset = (double(lastAudible) - target) / rate; frames(rate); return lastAudible; } const char *phase(bool prior) const { return reportingModel ? (std::abs(slewOffset) > .00005 ? "slewing" : "locked") : (prior ? "prior" : "acquiring"); } double offset() const { return slewOffset; } private: double reportTime = 0, reportFrames = 0, slewOffset = 0; bool lowerStart = false, conservativeStart = false; bool reportingModel = false, allowZero = false, zeroObserved = false; unsigned stream = 0; uint64_t request = 0, generation = 0, base = 0, started = 0, lastFrames = 0, observed = 0; uint32_t relative = 0; uint64_t audibleBase = 0, lastAudible = 0; bool anchored = false, hadAudio = false, heardPosition = false, pendingAudibleBase = false; }; #endif