@@ -0,0 +1,115 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user