Files
voiceagent/tests/js/pcm-player.test.mjs

205 lines
3.9 KiB
JavaScript

import assert from "node:assert/strict";
import { test } from "node:test";
import { PCMPlayer } from
"../../src/voice_assistant/audio_gateway/web/pcm-player.js";
class FakeAudioSource {
constructor(context) {
this.context = context;
this.buffer = null;
this.onended = null;
this.startedAt = null;
this.stopped = false;
}
connect() {}
disconnect() {}
start(time) {
this.startedAt = time;
this.context.sources.push(this);
}
stop() {
this.stopped = true;
}
end() {
this.onended?.();
}
}
class FakeAudioContext {
constructor() {
this.currentTime = 0;
this.destination = {};
this.sources = [];
}
async resume() {}
createAnalyser() {
return {
fftSize: 1024,
smoothingTimeConstant: 0.65,
connect() {},
disconnect() {},
getFloatTimeDomainData(array) {
array.fill(0);
},
getByteFrequencyData(array) {
array.fill(0);
},
};
}
createBuffer(channels, length, sampleRate) {
return {
duration: length / sampleRate,
copyToChannel(samples) {
this.samples = samples;
},
};
}
createBufferSource() {
return new FakeAudioSource(this);
}
}
function pcm(values) {
const buffer = new ArrayBuffer(values.length * 2);
const view = new DataView(buffer);
values.forEach((value, index) => {
view.setInt16(index * 2, value, true);
});
return buffer;
}
test("decodes signed PCM16 correctly", () => {
const player = new PCMPlayer({
audioContext: new FakeAudioContext(),
});
const samples = player.decodePCM(
pcm([-32768, 0, 16384, 32767])
);
assert.equal(samples[0], -1);
assert.equal(samples[1], 0);
assert.equal(samples[2], 0.5);
assert.ok(samples[3] > 0.99);
});
test("schedules chunks in order", async () => {
const context = new FakeAudioContext();
const player = new PCMPlayer({
audioContext: context,
});
await player.enqueue(pcm([100, 200, 300, 400]));
await player.enqueue(pcm([500, 600, 700, 800]));
assert.equal(context.sources.length, 2);
const first = context.sources[0];
const second = context.sources[1];
assert.ok(second.startedAt >=
first.startedAt + first.buffer.duration);
});
test("waits for actual playback completion", async () => {
const context = new FakeAudioContext();
const player = new PCMPlayer({
audioContext: context,
});
await player.enqueue(pcm([100, 200]));
player.finish();
let completed = false;
const waiting = player.waitForCompletion().then(() => {
completed = true;
});
await Promise.resolve();
assert.equal(completed, false);
context.sources[0].end();
await waiting;
assert.equal(completed, true);
});
test("stop interrupts scheduled playback", async () => {
const context = new FakeAudioContext();
const player = new PCMPlayer({
audioContext: context,
});
await player.enqueue(pcm([100, 200]));
player.stop();
assert.equal(context.sources[0].stopped, true);
assert.equal(player.sources.size, 0);
assert.equal(player.playing, false);
});
test("reset allows a new playback turn", async () => {
const context = new FakeAudioContext();
const player = new PCMPlayer({
audioContext: context,
});
await player.enqueue(pcm([100, 200]));
player.finish();
context.sources[0].end();
await player.waitForCompletion();
player.reset();
await player.enqueue(pcm([300, 400]));
assert.equal(context.sources.length, 2);
assert.equal(player.finished, false);
});
test("rejects invalid PCM buffers", () => {
const player = new PCMPlayer({
audioContext: new FakeAudioContext(),
});
assert.throws(
() => player.decodePCM(new ArrayBuffer(3)),
/Invalid PCM byte length/
);
});
test("exposes playback analyser", () => {
const context = new FakeAudioContext();
const player = new PCMPlayer({
audioContext: context,
});
assert.equal(
player.getAnalyser(),
player.analyser
);
});