Files
richandClaude Opus 5.5 6dc4592ed2 Initial voice pack: seven live-map voices for CoreScope
Voices taken from CoreScope feat/audio-voices (ac257569), plus a compat
shim for stock audio.js, an idempotent install/uninstall script, a
derived-image Dockerfile, and tests that run against a real public/ dir.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:39:51 -04:00

92 lines
4.5 KiB
JavaScript

/* Smoke test: load a CoreScope public/ dir's audio scripts in the order its
* index.html lists them, using a fake AudioContext, then check every pack
* voice registered, constellation stayed the default, and each voice plays
* every packet type through the real engine without throwing.
*
* node tests/smoke.js <corescope public dir, after install.sh>
*/
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const PACK_VOICES = ['metal', 'synthmetal', 'technoir', 'ambient', 'dub', 'acid', 'lofi'];
const TYPES = ['ADVERT', 'GRP_TXT', 'TXT_MSG', 'TRACE', 'REQ', 'UNKNOWN'];
function makeParam(value) {
const p = { value: value || 0 };
['setValueAtTime', 'linearRampToValueAtTime', 'exponentialRampToValueAtTime', 'setTargetAtTime', 'cancelScheduledValues']
.forEach((m) => { p[m] = () => p; });
return p;
}
class FakeCtx {
constructor() { this.state = 'running'; this.currentTime = 0; this.sampleRate = 8000; this.destination = this._node(); }
_node(extra) { return Object.assign({ connect: (t) => t, disconnect() {} }, extra); }
createGain() { return this._node({ gain: makeParam(1) }); }
createOscillator() { return this._node({ type: 'sine', frequency: makeParam(440), detune: makeParam(), start() {}, stop() {} }); }
createBiquadFilter() { return this._node({ type: 'lowpass', frequency: makeParam(350), Q: makeParam(1), gain: makeParam() }); }
createWaveShaper() { return this._node({ curve: null, oversample: 'none' }); }
createDynamicsCompressor() { return this._node({ threshold: makeParam(), knee: makeParam(), ratio: makeParam(), attack: makeParam(), release: makeParam() }); }
createStereoPanner() { return this._node({ pan: makeParam() }); }
createDelay() { return this._node({ delayTime: makeParam() }); }
createConvolver() { return this._node({ buffer: null }); }
createBufferSource() { return this._node({ buffer: null, start() {}, stop() {} }); }
createBuffer(channels, length) {
const data = Array.from({ length: channels }, () => new Float32Array(length));
return { numberOfChannels: channels, length, getChannelData: (c) => data[c] };
}
resume() {}
}
const publicDir = process.argv[2];
if (!publicDir) { console.error('usage: node tests/smoke.js <corescope public dir>'); process.exit(2); }
const html = fs.readFileSync(path.join(publicDir, 'index.html'), 'utf8');
const scripts = [...html.matchAll(/<script src="(audio[^"?]*\.js)/g)].map((m) => m[1]).filter((f) => f !== 'audio-lab.js');
const store = {};
const ctx = {
console, Math, Float32Array, Object, Array, WeakMap, Map, Set,
setTimeout: () => 0, clearTimeout() {},
localStorage: { getItem: (k) => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: (k) => { delete store[k]; } },
document: { createElement: () => ({}), body: { appendChild() {} }, addEventListener() {} },
AudioContext: FakeCtx,
};
ctx.window = ctx;
vm.createContext(ctx);
scripts.forEach((f) => vm.runInContext(fs.readFileSync(path.join(publicDir, f), 'utf8'), ctx, { filename: f }));
let failed = 0;
function test(name, fn) {
try { fn(); console.log(` ok ${name}`); } catch (e) { failed++; console.log(` FAIL ${name}: ${e.message}`); }
}
const MA = ctx.MeshAudio;
test(`scripts load in order: ${scripts.join(', ')}`, () => assert.ok(MA && MA.synthkit));
test('every pack voice registers', () => {
const names = MA.getVoiceNames();
PACK_VOICES.forEach((v) => assert.ok(names.includes(v), `missing ${v} (have ${names.join(', ')})`));
});
test('constellation stays the default voice', () => assert.strictEqual(MA.getVoiceName(), 'constellation'));
MA.setEnabled(true);
PACK_VOICES.forEach((name) => test(`${name} plays every packet type`, () => {
MA.setVoice(name);
TYPES.forEach((t) => MA.sonifyPacket({
raw: 'a1b2c3' + '00112233445566778899aabbccddeeff', observation_count: 2,
decoded: { header: { payloadTypeName: t }, payload: { lat: 40, lon: -100 }, path: { hops: ['ab'] } },
}));
// sonifyPacket swallows voice errors, so also call play() directly to surface them.
const voice = ctx._meshAudioVoices[name];
const ac = new FakeCtx();
TYPES.forEach((t) => {
const dur = voice.play(ac, ac.createGain(), { payloadBytes: [0, 0x42, 0x9a, 0xff, 0x11, 0xc3], typeName: t, hopCount: 2, obsCount: 3, payload: {}, hops: [] }, { bpm: 120, tempoMultiplier: 1 });
assert.ok(dur > 0, `${t} duration ${dur}`);
});
}));
console.log(failed ? `\n${failed} failed` : '\nall passed');
process.exit(failed ? 1 : 0);