258 lines
5.6 KiB
JavaScript
258 lines
5.6 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
|
|
);
|
|
});
|
|
|
|
|
|
test("late PCM after stop is ignored", async () => {
|
|
const context = new FakeAudioContext();
|
|
|
|
const player = new PCMPlayer({
|
|
audioContext: context,
|
|
sampleRate: 48000,
|
|
});
|
|
|
|
player.stop();
|
|
|
|
const pcm = new ArrayBuffer(8);
|
|
|
|
await assert.doesNotReject(
|
|
player.enqueue(pcm)
|
|
);
|
|
|
|
assert.equal(player.playing, false);
|
|
assert.equal(player.sources.size, 0);
|
|
});
|
|
|
|
|
|
test("uses the output sample rate declared by the server", async () => {
|
|
const context = new FakeAudioContext();
|
|
const player = new PCMPlayer({ audioContext: context });
|
|
player.configure({ encoding: "pcm_s16le", sample_rate: 24000, channels: 1 });
|
|
await player.enqueue(pcm([100, 200, 300, 400]));
|
|
assert.equal(player.sampleRate, 24000);
|
|
assert.equal(context.sources[0].buffer.duration, 4 / 24000);
|
|
});
|
|
|
|
for (const format of [
|
|
undefined,
|
|
{ encoding: "wav", sample_rate: 48000, channels: 1 },
|
|
{ encoding: "pcm_s16le", sample_rate: 48000, channels: 2 },
|
|
{ encoding: "pcm_s16le", sample_rate: "48000", channels: 1 },
|
|
{ encoding: "pcm_s16le", sample_rate: 0, channels: 1 },
|
|
{ encoding: "pcm_s16le", sample_rate: 192000, channels: 1 },
|
|
]) {
|
|
test(`rejects unsupported playback metadata: ${JSON.stringify(format)}`, () => {
|
|
const player = new PCMPlayer({ audioContext: new FakeAudioContext() });
|
|
assert.throws(() => player.configure(format), /Formato de áudio/);
|
|
});
|
|
}
|
|
|
|
test("rejects format changes while audio is scheduled", async () => {
|
|
const player = new PCMPlayer({ audioContext: new FakeAudioContext() });
|
|
await player.enqueue(pcm([100, 200]));
|
|
assert.throws(() => player.configure({
|
|
encoding: "pcm_s16le", sample_rate: 24000, channels: 1,
|
|
}), /durante a reprodução/);
|
|
});
|