64 lines
2.4 KiB
JavaScript
Raw Normal View History

// Recent known-pilot observations. Fixed memory; no audio or content retained.
const CAPACITY = 1000;
export class PilotWindow {
constructor(windowMs) {
if (!Number.isFinite(windowMs) || windowMs <= 0 || windowMs > 100) {
throw new RangeError('pilot observation duration');
}
this.windowMs = windowMs;
this.history = new Uint8Array(CAPACITY);
this.next = 0;
this.count = 0;
this.lastEndMs = null;
this.interruptions = 0;
}
interrupt() {
this.next = 0;
this.count = 0;
this.lastEndMs = null;
this.interruptions = Math.min(Number.MAX_SAFE_INTEGER, this.interruptions + 1);
}
observe(present, endMs) {
if (typeof present !== 'boolean' || !Number.isFinite(endMs) || endMs < this.windowMs) {
this.interrupt();
return;
}
// The detector emits equal-sized, contiguous sample windows. Missing
// callbacks are unknown coverage, not evidence of missing pilot PCM.
if (this.lastEndMs !== null &&
Math.abs(endMs - this.lastEndMs - this.windowMs) > 0.001) {
this.interrupt();
}
this.history[this.next] = present ? 1 : 0;
this.next = (this.next + 1) % CAPACITY;
this.count = Math.min(CAPACITY, this.count + 1);
this.lastEndMs = endMs;
}
summary(nowMs) {
let missing = 0, run = 0, longest = 0;
const start = (this.next + CAPACITY - this.count) % CAPACITY;
for (let n = 0; n < this.count; n++) {
if (this.history[(start + n) % CAPACITY]) run = 0;
else { missing++; run++; longest = Math.max(longest, run); }
}
const age = this.lastEndMs !== null && Number.isFinite(nowMs) && nowMs >= this.lastEndMs
? nowMs - this.lastEndMs : null;
return {
windowLimitMs: CAPACITY * this.windowMs,
observedWindows: this.count,
observedMs: this.count * this.windowMs,
endMs: this.lastEndMs,
observationAgeMs: age,
coverageComplete: this.count === CAPACITY && age !== null && age <= 2 * this.windowMs,
absentFraction: this.count ? missing / this.count : null,
absenceMaxMs: this.count ? longest * this.windowMs : null,
trailingAbsenceMs: this.count ? run * this.windowMs : null,
interruptions: this.interruptions,
};
}
}