77 lines
3.1 KiB
JavaScript
77 lines
3.1 KiB
JavaScript
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);
|
|
});
|