Files
2026-03-24 19:46:56 +02:00

89 lines
2.9 KiB
JavaScript

/**
* playback-processor.js — AudioWorkletProcessor for per-speaker audio playback.
*
* Runs on the dedicated audio worklet thread (off the main JS thread).
*
* Responsibilities:
* - Maintain a Float32 ring buffer (~1 second at 48 kHz).
* - Accept { pcm: Float32Array } messages (transferable ArrayBuffer) written
* by the main thread after AudioDecoder.decode() produces output.
* - Drain the ring buffer into the output channel on each process() call;
* output silence when the buffer is empty (natural jitter absorption).
*
* One instance is created per remote speaker alongside its AudioDecoder.
*/
const RING_CAPACITY = 48000; // 1 second @ 48 kHz
class PlaybackProcessor extends AudioWorkletProcessor {
constructor() {
super();
this._ring = new Float32Array(RING_CAPACITY);
this._writePos = 0; // next write index (mod RING_CAPACITY)
this._readPos = 0; // next read index (mod RING_CAPACITY)
this._available = 0; // samples currently buffered
this.port.onmessage = (e) => {
const pcm = e.data?.pcm;
if (!(pcm instanceof Float32Array) || pcm.length === 0) return;
// If the incoming chunk would overflow the ring, drop the oldest data
if (pcm.length > RING_CAPACITY) return; // chunk larger than ring — skip
if (this._available + pcm.length > RING_CAPACITY) {
// Discard oldest samples to make room
const discard = this._available + pcm.length - RING_CAPACITY;
this._readPos = (this._readPos + discard) % RING_CAPACITY;
this._available -= discard;
}
// Write samples into ring buffer (may wrap)
let toWrite = pcm.length;
let src = 0;
while (toWrite > 0) {
const chunk = Math.min(toWrite, RING_CAPACITY - this._writePos);
this._ring.set(pcm.subarray(src, src + chunk), this._writePos);
this._writePos = (this._writePos + chunk) % RING_CAPACITY;
src += chunk;
toWrite -= chunk;
}
this._available += pcm.length;
};
}
process(_inputs, outputs) {
const output = outputs[0]?.[0];
if (!output) return true;
const count = Math.min(output.length, this._available);
if (count === 0) {
// Silence when buffer is empty
output.fill(0);
return true;
}
// Drain ring buffer into output (may wrap)
let toRead = count;
let dst = 0;
while (toRead > 0) {
const chunk = Math.min(toRead, RING_CAPACITY - this._readPos);
output.set(this._ring.subarray(this._readPos, this._readPos + chunk), dst);
this._readPos = (this._readPos + chunk) % RING_CAPACITY;
dst += chunk;
toRead -= chunk;
}
this._available -= count;
// Zero-pad if we ran short (shouldn't happen, but guard for safety)
if (count < output.length) {
output.fill(0, count);
}
return true; // keep processor alive
}
}
registerProcessor('playback-processor', PlaybackProcessor);