@@ -0,0 +1,76 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const test = require("node:test");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const vm = require("node:vm");
|
||||
|
||||
const source = fs.readFileSync(path.join(__dirname, "../public/pcm-playback.worklet.js"), "utf8");
|
||||
|
||||
function fixture({ outputRate = 48000, prebufferMs = 120 } = {}) {
|
||||
const messages = [];
|
||||
let Processor;
|
||||
class AudioWorkletProcessor {
|
||||
constructor() {
|
||||
this.port = { onmessage: null, postMessage: (message) => messages.push(message) };
|
||||
}
|
||||
}
|
||||
const context = vm.createContext({
|
||||
AudioWorkletProcessor,
|
||||
Float32Array,
|
||||
ArrayBuffer,
|
||||
Math,
|
||||
sampleRate: outputRate,
|
||||
registerProcessor: (name, implementation) => {
|
||||
assert.equal(name, "pcm16-playback");
|
||||
Processor = implementation;
|
||||
},
|
||||
});
|
||||
vm.runInContext(source, context);
|
||||
const processor = new Processor({ processorOptions: { sourceSampleRate: 16000, prebufferMs } });
|
||||
const push = (values) => {
|
||||
const samples = new Float32Array(values);
|
||||
processor.port.onmessage({ data: { type: "push", samples: samples.buffer } });
|
||||
};
|
||||
const process = () => {
|
||||
const output = new Float32Array(128);
|
||||
assert.equal(processor.process([], [[output]]), true);
|
||||
return output;
|
||||
};
|
||||
return { messages, processor, push, process };
|
||||
}
|
||||
|
||||
test("waits for prebuffer then resamples continuous 16k PCM without fragment gaps", () => {
|
||||
const f = fixture({ outputRate: 48000, prebufferMs: 10 });
|
||||
f.push(new Array(120).fill(0.25));
|
||||
assert.ok(f.process().every((sample) => sample === 0), "must remain silent below the prebuffer threshold");
|
||||
f.push(new Array(400).fill(0.25));
|
||||
const first = f.process();
|
||||
const second = f.process();
|
||||
assert.ok(first.some((sample) => sample > 0.05), "buffered playback should start with a short anti-pop fade");
|
||||
assert.ok(second.every((sample) => sample > 0.12), "playback should remain continuous across source fragments");
|
||||
assert.equal(f.messages.filter((message) => message.type === "underflow").length, 0);
|
||||
});
|
||||
|
||||
test("plays 16k source at real-time duration instead of the former 1.5x speed", () => {
|
||||
const f = fixture({ outputRate: 48000, prebufferMs: 1 });
|
||||
f.push(new Array(1000).fill(0.25));
|
||||
f.process();
|
||||
assert.ok(Math.abs(f.processor.readPosition - 128 / 3) < 0.001, `consumed ${f.processor.readPosition} source samples`);
|
||||
});
|
||||
|
||||
test("reports network starvation and re-enters prebuffering instead of scheduling tiny buffers", () => {
|
||||
const f = fixture({ outputRate: 48000, prebufferMs: 0.01 });
|
||||
f.push(new Array(8).fill(0.5));
|
||||
f.process();
|
||||
assert.equal(f.messages.filter((message) => message.type === "underflow").length, 1);
|
||||
assert.ok(f.process().every((sample) => sample === 0), "starved playback should wait for a new prebuffer");
|
||||
});
|
||||
|
||||
test("drains the final response cleanly and emits a completion signal", () => {
|
||||
const f = fixture({ outputRate: 24000 });
|
||||
f.push(new Array(64).fill(-0.25));
|
||||
f.processor.port.onmessage({ data: { type: "end" } });
|
||||
f.process();
|
||||
assert.equal(f.messages.filter((message) => message.type === "drained").length, 1);
|
||||
assert.equal(f.messages.filter((message) => message.type === "underflow").length, 0);
|
||||
});
|
||||
Reference in New Issue
Block a user