Files
vocero-client/vocero-frontend/src/audio/audio-playback.worklet.ts
T

125 lines
3.5 KiB
TypeScript

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<PlaybackMessage>) => {
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);