125 lines
3.5 KiB
TypeScript
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);
|