113 lines
3.0 KiB
JavaScript
113 lines
3.0 KiB
JavaScript
/**
|
|
* Reusable AudioWorklet for streaming PCM16 audio playback.
|
|
* Place in public/ folder and load via audioContext.audioWorklet.addModule()
|
|
*/
|
|
class RingBuffer {
|
|
constructor(initialCapacity) {
|
|
this.capacity = initialCapacity;
|
|
this.buffer = new Float32Array(initialCapacity);
|
|
this.readIndex = 0;
|
|
this.writeIndex = 0;
|
|
this.availableData = 0;
|
|
}
|
|
|
|
push(data) {
|
|
const len = data.length;
|
|
// Auto-grow if needed
|
|
while (this.availableData + len > this.capacity) {
|
|
this.grow();
|
|
}
|
|
for (let i = 0; i < len; i++) {
|
|
this.buffer[this.writeIndex] = data[i];
|
|
this.writeIndex = (this.writeIndex + 1) % this.capacity;
|
|
this.availableData++;
|
|
}
|
|
}
|
|
|
|
grow() {
|
|
const newCapacity = this.capacity * 2;
|
|
const newBuffer = new Float32Array(newCapacity);
|
|
// Copy existing data maintaining order
|
|
for (let i = 0; i < this.availableData; i++) {
|
|
const srcIndex = (this.readIndex + i) % this.capacity;
|
|
newBuffer[i] = this.buffer[srcIndex];
|
|
}
|
|
this.buffer = newBuffer;
|
|
this.readIndex = 0;
|
|
this.writeIndex = this.availableData;
|
|
this.capacity = newCapacity;
|
|
}
|
|
|
|
pull(outputBuffer) {
|
|
const len = outputBuffer.length;
|
|
const available = Math.min(len, this.availableData);
|
|
for (let i = 0; i < available; i++) {
|
|
outputBuffer[i] = this.buffer[this.readIndex];
|
|
this.readIndex = (this.readIndex + 1) % this.capacity;
|
|
}
|
|
// Pad remaining with silence
|
|
for (let i = available; i < len; i++) {
|
|
outputBuffer[i] = 0;
|
|
}
|
|
this.availableData -= available;
|
|
return available > 0;
|
|
}
|
|
|
|
available() {
|
|
return this.availableData;
|
|
}
|
|
|
|
clear() {
|
|
this.readIndex = 0;
|
|
this.writeIndex = 0;
|
|
this.availableData = 0;
|
|
}
|
|
}
|
|
|
|
class AudioPlaybackProcessor extends AudioWorkletProcessor {
|
|
constructor() {
|
|
super();
|
|
this.ringBuffer = new RingBuffer(24000 * 30); // 30s initial capacity
|
|
this.isPlaying = false;
|
|
this.streamComplete = false;
|
|
|
|
this.port.onmessage = (event) => {
|
|
const { type, samples } = event.data;
|
|
if (type === "audio") {
|
|
this.ringBuffer.push(samples);
|
|
this.isPlaying = true;
|
|
} else if (type === "clear") {
|
|
this.ringBuffer.clear();
|
|
this.isPlaying = false;
|
|
this.streamComplete = false;
|
|
} else if (type === "streamComplete") {
|
|
this.streamComplete = true;
|
|
} else if (type === "stop") {
|
|
this.isPlaying = false;
|
|
this.streamComplete = false;
|
|
}
|
|
};
|
|
}
|
|
|
|
process(inputs, outputs) {
|
|
const output = outputs[0];
|
|
if (!output || output.length === 0) return true;
|
|
|
|
const channel = output[0];
|
|
if (this.isPlaying) {
|
|
this.ringBuffer.pull(channel);
|
|
if (this.streamComplete && this.ringBuffer.available() === 0) {
|
|
this.isPlaying = false;
|
|
this.streamComplete = false;
|
|
this.port.postMessage({ type: "ended" });
|
|
}
|
|
} else {
|
|
channel.fill(0);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
registerProcessor("audio-playback-processor", AudioPlaybackProcessor);
|
|
|