Files
mumh5/test/opus-wasm.test.ts
T
kibiandClaude Opus 5.5 5d43b8e4ae Built-in Opus codec for browsers without one; cameras on by default with per-person hide
- libopus in WebAssembly takes over voice where WebCodecs audio is missing
  (Firefox on Android); tested for tone, timing and bitrate, and in a browser
  forced onto it
- Cameras in the channel show by themselves once the IP note was confirmed;
  a person's camera can be hidden from the right-click menu, all of them with
  one button; an enlarged camera leaves its tile
- CSP allows wasm-unsafe-eval for the codec

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 00:36:43 +02:00

55 lines
2.6 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { instantiateOpus, WasmEncoder, WasmDecoder } from '../src/lib/audio/opus-wasm.ts';
const opus = await instantiateOpus(readFileSync(new URL('../node_modules/@evan/opus/wasm/opus.wasm', import.meta.url)));
// One second of a 440 Hz tone in the engine's 10 ms frames
const tone = () => Array.from({ length: 100 }, (_, f) => Float32Array.from({ length: 480 }, (_, i) => 0.5 * Math.sin(2 * Math.PI * 440 * (f * 480 + i) / 48000)));
for (const frameMs of [10, 20, 40, 60]) {
test(`a tone survives encoding and decoding with ${frameMs} ms packets`, () => {
const packets: Uint8Array[] = [];
const encoder = new WasmEncoder(opus, 40000, frameMs, p => packets.push(p));
for (const frame of tone()) encoder.push(frame);
assert.equal(packets.length, Math.floor(1000 / frameMs), 'one packet per frame duration');
// Around the asked bitrate, not raw audio
const bytes = packets.reduce((n, p) => n + p.length, 0);
assert.ok(bytes > 2000 && bytes < 8000, `about 40 kbit/s: ${bytes} bytes for one second`);
const decoder = new WasmDecoder(opus);
const out = packets.map(p => decoder.decode(p)!);
assert.ok(out.every(pcm => pcm.length === 48 * frameMs), 'each packet decodes to its duration');
// The tone is still there: count the zero crossings in the second half (the codec needs a moment to settle)
const all = Float32Array.from(out.slice(out.length / 2).flatMap(pcm => [...pcm]));
let crossings = 0;
for (let i = 1; i < all.length; i++) if ((all[i - 1] < 0) !== (all[i] < 0)) crossings++;
const hz = crossings / 2 / (all.length / 48000);
assert.ok(Math.abs(hz - 440) < 15, `decoded tone at ${hz.toFixed(1)} Hz`);
const peak = all.reduce((m, v) => Math.max(m, Math.abs(v)), 0);
assert.ok(peak > 0.3 && peak < 0.8, `level kept: ${peak.toFixed(2)}`);
encoder.close();
decoder.close();
});
}
test('an unfinished frame is dropped at the end, and garbage does not decode', () => {
const packets: Uint8Array[] = [];
const encoder = new WasmEncoder(opus, 40000, 20, p => packets.push(p));
encoder.push(tone()[0]);
assert.equal(packets.length, 0);
encoder.flush();
encoder.push(tone()[0]);
assert.equal(packets.length, 0, 'the half frame before the flush is gone');
encoder.push(tone()[1]);
assert.equal(packets.length, 1);
const decoder = new WasmDecoder(opus);
assert.equal(decoder.decode(new Uint8Array(5000)), null);
assert.equal(decoder.decode(Uint8Array.of()), null);
encoder.close();
encoder.push(tone()[0]);
decoder.close();
assert.equal(decoder.decode(packets[0]), null);
});