/
/
/
1/**
2 * Output latency tracker with EMA smoothing and persistence.
3 *
4 * Tracks AudioContext.baseLatency + outputLatency using exponential moving
5 * average to filter browser jitter (especially Chrome). Persists the smoothed
6 * value to storage for cross-session consistency.
7 */
8const OUTPUT_LATENCY_ALPHA = 0.01;
9const OUTPUT_LATENCY_STORAGE_KEY = "sendspin-output-latency-us";
10const OUTPUT_LATENCY_PERSIST_INTERVAL_MS = 10000;
11export class OutputLatencyTracker {
12 constructor(storage) {
13 this.storage = storage;
14 this.smoothedOutputLatencyUs = null;
15 this.lastLatencyPersistAtMs = null;
16 this.loadPersisted();
17 }
18 loadPersisted() {
19 if (!this.storage)
20 return;
21 try {
22 const stored = this.storage.getItem(OUTPUT_LATENCY_STORAGE_KEY);
23 if (stored) {
24 const latency = parseFloat(stored);
25 if (!isNaN(latency) && latency >= 0) {
26 this.smoothedOutputLatencyUs = latency;
27 }
28 }
29 }
30 catch {
31 // ignore
32 }
33 }
34 persist() {
35 if (!this.storage || this.smoothedOutputLatencyUs === null)
36 return;
37 try {
38 this.storage.setItem(OUTPUT_LATENCY_STORAGE_KEY, this.smoothedOutputLatencyUs.toString());
39 }
40 catch {
41 // ignore
42 }
43 }
44 /** Get raw output latency in microseconds from AudioContext. */
45 getRawUs(audioContext) {
46 if (!audioContext)
47 return 0;
48 const baseLatency = audioContext.baseLatency ?? 0;
49 const outputLatency = audioContext.outputLatency ?? 0;
50 return (baseLatency + outputLatency) * 1000000;
51 }
52 /** Get EMA-smoothed output latency in microseconds. */
53 getSmoothedUs(audioContext) {
54 const rawLatencyUs = this.getRawUs(audioContext);
55 if (rawLatencyUs <= 0 && this.smoothedOutputLatencyUs !== null) {
56 return this.smoothedOutputLatencyUs;
57 }
58 if (this.smoothedOutputLatencyUs === null) {
59 this.smoothedOutputLatencyUs = rawLatencyUs;
60 }
61 else {
62 this.smoothedOutputLatencyUs =
63 OUTPUT_LATENCY_ALPHA * rawLatencyUs +
64 (1 - OUTPUT_LATENCY_ALPHA) * this.smoothedOutputLatencyUs;
65 }
66 const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
67 if (this.lastLatencyPersistAtMs === null ||
68 nowMs - this.lastLatencyPersistAtMs >= OUTPUT_LATENCY_PERSIST_INTERVAL_MS) {
69 this.persist();
70 this.lastLatencyPersistAtMs = nowMs;
71 }
72 return this.smoothedOutputLatencyUs;
73 }
74 /** Reset smoother (on stream change or audio context recreation). */
75 reset() {
76 this.smoothedOutputLatencyUs = null;
77 }
78}
79//# sourceMappingURL=output-latency-tracker.js.map
80