folx-tv/client/replit_integrations/audio/audio-playback-worklet.js
2026-02-28 20:36:50 +00:00

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);