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); });