class BufferedAudioWorkletProcessor extends AudioWorkletProcessor { constructor() { super(); this.bufferQueue = []; this.currentChunkOffset = 0; this.hadData = false; this.port.onmessage = (event) => { const data = event.data; if (data instanceof Float32Array) { this.hadData = true; this.bufferQueue.push(data); } else if (data === "stop") { this.bufferQueue = []; this.currentChunkOffset = 0; } }; } process(inputs, outputs) { const channel = outputs[0][0]; if (!channel) return true; const numSamples = channel.length; let outputIndex = 0; if (this.hadData && this.bufferQueue.length === 0) { this.port.postMessage({ type: "playback_ended" }); this.hadData = false; } while (outputIndex < numSamples) { if (this.bufferQueue.length > 0) { const currentChunk = this.bufferQueue[0]; const remainingSamples = currentChunk.length - this.currentChunkOffset; const samplesToCopy = Math.min(remainingSamples, numSamples - outputIndex); channel.set( currentChunk.subarray(this.currentChunkOffset, this.currentChunkOffset + samplesToCopy), outputIndex, ); this.currentChunkOffset += samplesToCopy; outputIndex += samplesToCopy; // Remove the chunk if fully consumed. if (this.currentChunkOffset >= currentChunk.length) { // current chunk finished; advance and signal UI to move highlight this.bufferQueue.shift(); this.currentChunkOffset = 0; this.port.postMessage({ type: "next_chunk" }); } } else { // If no data is available, fill the rest of the buffer with silence. channel.fill(0, outputIndex); outputIndex = numSamples; } } return true; } } registerProcessor("buffered-audio-worklet-processor", BufferedAudioWorkletProcessor);