116 lines
3.8 KiB
JavaScript
116 lines
3.8 KiB
JavaScript
class PCM16PlaybackProcessor extends AudioWorkletProcessor {
|
|
constructor(options) {
|
|
super();
|
|
const config = options.processorOptions || {};
|
|
this.sourceSampleRate = Number(config.sourceSampleRate || 16000);
|
|
this.prebufferSamples = Math.max(1, Math.round(this.sourceSampleRate * Number(config.prebufferMs || 120) / 1000));
|
|
this.capacity = Math.max(this.sourceSampleRate * 30, this.prebufferSamples * 4);
|
|
this.ring = new Float32Array(this.capacity);
|
|
this.readPosition = 0;
|
|
this.writePosition = 0;
|
|
this.playing = false;
|
|
this.ended = false;
|
|
this.drained = false;
|
|
this.lastOutput = 0;
|
|
this.fadeInRemaining = 0;
|
|
this.fadeFrames = Math.max(1, Math.round(sampleRate * 0.005));
|
|
this.underflows = 0;
|
|
this.port.onmessage = (message) => this.onMessage(message.data || {});
|
|
}
|
|
|
|
onMessage(message) {
|
|
if (message.type === "reset") {
|
|
this.reset();
|
|
return;
|
|
}
|
|
if (message.type === "end") {
|
|
this.ended = true;
|
|
return;
|
|
}
|
|
if (message.type !== "push" || !(message.samples instanceof ArrayBuffer)) return;
|
|
const samples = new Float32Array(message.samples);
|
|
if (this.drained) this.reset();
|
|
this.ended = false;
|
|
for (let index = 0; index < samples.length; index += 1) {
|
|
if (this.writePosition - Math.floor(this.readPosition) >= this.capacity - 1) {
|
|
this.readPosition = this.writePosition - this.capacity + 2;
|
|
}
|
|
this.ring[this.writePosition % this.capacity] = samples[index];
|
|
this.writePosition += 1;
|
|
}
|
|
}
|
|
|
|
reset() {
|
|
this.readPosition = 0;
|
|
this.writePosition = 0;
|
|
this.playing = false;
|
|
this.ended = false;
|
|
this.drained = false;
|
|
this.lastOutput = 0;
|
|
this.fadeInRemaining = 0;
|
|
}
|
|
|
|
available() {
|
|
return this.writePosition - this.readPosition;
|
|
}
|
|
|
|
startWhenReady() {
|
|
if (this.playing) return true;
|
|
const available = this.available();
|
|
if (available >= this.prebufferSamples || (this.ended && available > 1)) {
|
|
this.playing = true;
|
|
this.fadeInRemaining = this.fadeFrames;
|
|
this.port.postMessage({ type: "started", bufferedSamples: Math.floor(available) });
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
finish(output, fromIndex, eventType) {
|
|
const remaining = Math.min(this.fadeFrames, output.length - fromIndex);
|
|
for (let index = 0; index < remaining; index += 1) {
|
|
output[fromIndex + index] = this.lastOutput * (1 - (index + 1) / remaining);
|
|
}
|
|
output.fill(0, fromIndex + remaining);
|
|
this.lastOutput = 0;
|
|
this.playing = false;
|
|
if (eventType === "drained") {
|
|
if (!this.drained) this.port.postMessage({ type: "drained" });
|
|
this.drained = true;
|
|
} else {
|
|
this.underflows += 1;
|
|
this.port.postMessage({ type: "underflow", count: this.underflows });
|
|
}
|
|
}
|
|
|
|
process(_inputs, outputs) {
|
|
const output = outputs[0]?.[0];
|
|
if (!output) return true;
|
|
output.fill(0);
|
|
if (!this.startWhenReady()) return true;
|
|
|
|
const sourceStep = this.sourceSampleRate / sampleRate;
|
|
for (let index = 0; index < output.length; index += 1) {
|
|
const base = Math.floor(this.readPosition);
|
|
if (base + 1 >= this.writePosition) {
|
|
this.finish(output, index, this.ended ? "drained" : "underflow");
|
|
break;
|
|
}
|
|
const fraction = this.readPosition - base;
|
|
const first = this.ring[base % this.capacity];
|
|
const second = this.ring[(base + 1) % this.capacity];
|
|
let value = first + (second - first) * fraction;
|
|
if (this.fadeInRemaining > 0) {
|
|
value *= 1 - this.fadeInRemaining / this.fadeFrames;
|
|
this.fadeInRemaining -= 1;
|
|
}
|
|
output[index] = value;
|
|
this.lastOutput = value;
|
|
this.readPosition += sourceStep;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
registerProcessor("pcm16-playback", PCM16PlaybackProcessor);
|