205 lines
3.9 KiB
JavaScript
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
|
|
);
|
|
});
|