type PlaybackMessage = | { kind: "config"; inputRate: number } | { kind: "audio"; samples: Float32Array; sequence: number } | { kind: "clear" | "finish"; sequence: number }; class AudioPlaybackProcessor extends AudioWorkletProcessor { private rate = 16000; private queue: Float32Array[] = []; private offset = 0; private fraction = 0; private playing = false; private fadeIn = 0; private fadeOut = 0; private lastSample = 0; private sequence = 0; private finished = false; constructor() { super(); this.port.onmessage = (event: MessageEvent) => { const data = event.data; switch (data.kind) { case "config": if (data.inputRate > 0) { this.rate = data.inputRate; } break; case "audio": this.sequence = data.sequence; this.finished = false; if (data.samples.length) { this.queue.push(data.samples); } break; case "clear": this.sequence = data.sequence; this.finished = true; this.queue = []; this.offset = 0; this.fraction = 0; this.fadeOut = 32; break; case "finish": this.sequence = data.sequence; this.finished = true; break; } }; } private bufferedMs() { return (this.queue.reduce((sum, buffer) => sum + buffer.length, -this.offset) / this.rate) * 1000; } private read(): number | null { const head = this.queue[0]; if (!head) { return null; } const a = head[this.offset] ?? 0; const b = head[this.offset + 1] ?? this.queue[1]?.[0] ?? a; return a + (b - a) * this.fraction; } private advance() { this.fraction += this.rate / sampleRate; while (this.fraction >= 1) { this.fraction--; this.offset++; } while (this.queue[0] && this.offset >= this.queue[0].length) { this.offset -= this.queue[0].length; this.queue.shift(); } } process(_inputs: Float32Array[][], outputs: Float32Array[][]) { const channels = outputs[0]; const output = channels?.[0]; if (!output) { return true; } for (let index = 0; index < output.length; index++) { let value = 0; if (!this.playing && this.queue.length && (this.finished || this.bufferedMs() >= 90)) { this.playing = true; this.fadeIn = 32; this.fadeOut = 0; } if (this.playing) { const sample = this.read(); if (sample === null) { value = this.lastSample * (1 - 1 / 32); this.lastSample = value; if (Math.abs(value) < 1e-4) { this.playing = false; this.lastSample = 0; } } else { value = sample; this.lastSample = sample; this.advance(); } if (this.fadeIn > 0) { value *= 1 - this.fadeIn-- / 32; } if (this.fadeOut > 0) { value *= this.fadeOut-- / 32; if (!this.fadeOut) { this.playing = false; this.lastSample = 0; } } } output[index] = value; const stereo = channels?.[1]; if (stereo) { stereo[index] = value; } } if (this.finished && !this.playing && !this.queue.length) { this.finished = false; this.port.postMessage({ kind: "drained", sequence: this.sequence }); } return true; } } registerProcessor("audio-playback", AudioPlaybackProcessor);