/* Stateful box-filter resampling: phase and integration survive render quanta. */ class TranscriptionPCM extends AudioWorkletProcessor { constructor() { super(); this.ratio = sampleRate / 16000; this.weight = 0; this.sum = 0; this.samples = []; this.stopped = false; this.port.onmessage = ({ data }) => { if (data.type === 'flush') { this.stopped = true; if (this.weight > 0) this.samples.push(this.sum / this.weight); this.emit(); this.port.postMessage({ type: 'flushed' }); } }; } emit() { if (!this.samples.length) return; const buffer = new ArrayBuffer(this.samples.length * 2); const view = new DataView(buffer); let energy = 0; this.samples.forEach((value, index) => { const sample = Math.max(-1, Math.min(1, value)); view.setInt16(index * 2, Math.round(sample * (sample < 0 ? 32768 : 32767)), true); energy += sample * sample; }); this.port.postMessage({ type: 'pcm', buffer, level: Math.sqrt(energy / this.samples.length) }, [buffer]); this.samples = []; } process(inputs) { if (this.stopped) return false; const channels = inputs[0]; if (!channels?.length) return true; for (let i = 0; i < channels[0].length; i++) { let value = 0; for (const channel of channels) value += channel[i] / channels.length; let remaining = 1; while (remaining > 1e-9) { const take = Math.min(remaining, this.ratio - this.weight); this.sum += value * take; this.weight += take; remaining -= take; if (this.weight >= this.ratio - 1e-9) { this.samples.push(this.sum / this.ratio); this.sum = 0; this.weight = 0; if (this.samples.length >= 2400) this.emit(); } } } return true; } } registerProcessor('transcription-pcm', TranscriptionPCM);