import assert from 'node:assert/strict'; import test from 'node:test'; import vm from 'node:vm'; import { mkdtempSync, readFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { AUDIO_PROTECTION } from '../src/runtime/audio-contract.js'; import { MasterProtectionDSP, protectionWorkletSource, loadProtectionWorklet, createProtectionNode } from '../src/runtime/audio-protection.js'; import { AudioSubsystem, instantiateSoundGraph } from '../src/runtime/audio-engine.js'; import { validateExhibit } from '../src/runtime/validator.js'; import { buildAudioAcceptance } from '../tools/build-audio-acceptance.mjs'; import { Diagnostics } from '../src/runtime/diagnostics.js'; import { SeededRNG } from '../src/runtime/rng.js'; const block = (value = 0, length = 128) => [new Float32Array(length).fill(value), new Float32Array(length).fill(value)]; const limit = 10 ** ((AUDIO_PROTECTION.ceilingDb + AUDIO_PROTECTION.toleranceDb) / 20); function processor(rate = 48000, guard = false) { let Processor; vm.runInNewContext(protectionWorkletSource(), { sampleRate: rate, AudioWorkletProcessor: class { constructor() { this.messages = []; this.port = { postMessage: data => this.messages.push(data) }; } }, registerProcessor(name, implementation) { assert.equal(name, 'xzbt-protection'); Processor = implementation; } }); return new Processor({ processorOptions: { guard } }); } test('the shipped worklet bounds overload, DC, alternating peaks, transients and silence at multiple sample rates', () => { for (const rate of [8000, 44100, 48000, 96000, 192000]) { const p = processor(rate), output = block(); let observed = 0; for (let n = 0; n < 300; n++) { const input = block(); for (let i = 0; i < 128; i++) { const t = (n * 128 + i) / rate; input[0][i] = n < 50 ? 0 : n < 100 ? 1024 : n < 150 ? 4096 * Math.sin(2 * Math.PI * 440 * t) : n < 200 ? (i % 2 ? -3.4e38 : 3.4e38) : n < 250 ? (i === 63 ? 10000 : 0.1) : 0; input[1][i] = input[0][i] * -0.5; } assert.equal(p.process([input, []], [output]), true); for (const channel of output) for (const sample of channel) { assert.ok(Number.isFinite(sample)); assert.ok(Math.abs(sample) <= limit); observed = Math.max(observed, Math.abs(sample)); } } assert.ok(observed > 0.8, `rate ${rate} must produce audio, not silently pass`); } }); test('quiet stereo audio retains its samples and polarity after exactly the lookahead delay', () => { const dsp = new MasterProtectionDSP(48000, AUDIO_PROTECTION); const input = block(0, 1024), output = block(0, 1024); for (let i = 0; i < 1024; i++) { input[0][i] = Math.sin(i) * 0.2; input[1][i] = input[0][i] * -0.5; } dsp.process(input, output); for (let c = 0; c < 2; c++) for (let i = 0; i < 1024; i++) assert.equal(output[c][i], i < dsp.delayFrames ? 0 : input[c][i - dsp.delayFrames]); }); test('gain recovers monotonically with the configured release time constant, shared across stereo', () => { const dsp = new MasterProtectionDSP(48000, AUDIO_PROTECTION), output = block(); for (let i = 0; i < 200; i++) dsp.process(block(100), output); let previous = dsp.gain; for (let i = 0; i < 400; i++) { dsp.process(block(0.1), output); assert.ok(dsp.gain >= previous - 1e-12); assert.ok(dsp.gain <= 1); previous = dsp.gain; } assert.ok(dsp.gain > 0.98); assert.deepEqual(output[0], output[1]); }); test('trace 12: nonfinite at the master mutes the entire stereo block, flushes history and warns once', () => { const p = processor(), output = block(); p.process([block(0.3)], [output]); for (const invalid of [NaN, Infinity, -Infinity]) { const input = block(0.3); input[1][127] = invalid; p.process([input], [output]); assert.ok(output.every(channel => channel.every(value => value === 0))); } assert.equal(p.messages.filter(message => message.type === 'nonfinite').length, 1); assert.equal(p.dsp.affectedBlocks, 3); p.process([block()], [output]); assert.ok(output.every(channel => channel.every(value => value === 0))); }); test('voice guards attribute once per instance and a fault mutes the mixed output even with healthy voices', () => { const master = processor(), guards = [processor(48000, true), processor(48000, true)]; master.port.onmessage({ data: { type: 'capture', id: 1, warmupFrames: 0, frames: 384 } }); for (let b = 0; b < 3; b++) { const fault = block(0, 128), output = block(); for (const guard of guards) { const input = block(0.1), guarded = block(), lane = [new Float32Array(128)]; input[0][127] = NaN; guard.process([input], [guarded, lane]); assert.ok(guarded.every(channel => channel.every(value => value === 0))); for (let i = 0; i < 128; i++) fault[0][i] += lane[0][i]; } master.process([block(10), fault], [output]); assert.ok(output.every(channel => channel.every(value => value === 0))); } for (const guard of guards) assert.equal(guard.messages.length, 1); const capture = master.messages.find(message => message.type === 'capture'); assert.equal(capture.affectedBlocks, 3); assert.equal(capture.nonfiniteSamples, 6); assert.equal(master.messages.filter(message => message.type === 'nonfinite').length, 0); }); test('capture excludes warmup and measures exact output frames including partial boundary blocks', () => { const p = processor(), output = block(); p.port.onmessage({ data: { type: 'capture', id: 12, warmupFrames: 300, frames: 257 } }); for (let n = 0; n < 4; n++) p.process([block(0.2)], [output]); assert.equal(p.messages.length, 0); p.process([block(0.2)], [output]); const result = p.messages[0]; assert.equal(result.id, 12); assert.equal(result.frames, 257); assert.equal(result.peak, Math.fround(0.2)); assert.equal(result.affectedBlocks, 0); }); function recordingSetup(load = async () => {}) { const nodes = []; const node = kind => { const result = { kind, connections: [], gain: { value: 1 }, port: { messages: [], postMessage(data) { this.messages.push(data); }, close() { this.closed = true; } }, connect(target, output = 0, input = 0) { this.connections.push({ target, output, input }); return target; }, disconnect() { this.connections = []; this.disconnected = true; } }; nodes.push(result); return result; }; const context = { state: 'suspended', sampleRate: 48000, currentTime: 0, destination: node('destination'), createGain: () => node('gain'), async resume() { this.state = 'running'; }, async close() { this.state = 'closed'; } }; const diagnostics = new Diagnostics(); const audio = new AudioSubsystem({ document: { audio: { buses: { bed: { gain: 1 } } } }, rng: new SeededRNG(42), diagnostics, contextFactory: () => context, protectionFactory: { load, create: (ctx, guard) => node(guard ? 'guard' : 'master-protection') } }); return { audio, context, diagnostics, nodes }; } test('all buses and direct sounds route through volume then final protection; guard fault lane is separate', async () => { const { audio, context, nodes } = recordingSetup(); assert.equal(await audio.unlock(), true); assert.equal(audio.busFor('direct'), audio.master); assert.equal(audio.buses.get('bed').connections[0].target, audio.master); assert.equal(audio.master.connections[0].target, audio.protection); const outputEdges = nodes.flatMap(node => node.connections.map(edge => ({ node, ...edge }))).filter(edge => edge.target === context.destination); assert.equal(outputEdges.length, 1); assert.equal(outputEdges[0].node, audio.protection); const guard = audio.createVoiceGuard({ soundId: 'probe', creationOrder: 7 }); assert.deepEqual(guard.connections[0], { target: audio.protection, output: 1, input: 1 }); guard.port.onmessage({ data: { type: 'nonfinite' } }); assert.equal(audio.diagnostics.list()[0].objectId, 'probe#7'); guard.disconnect(); guard.port.close(); audio.setMasterVolume(NaN); assert.equal(audio.master.gain.value, 0.8); await audio.dispose(); assert.ok(nodes.filter(node => node.kind !== 'destination').every(node => node.disconnected)); }); test('missing worklets and rejected resume/load fail closed, warn once, and create no voice', async () => { for (const failure of ['load', 'resume']) { const { audio, context, diagnostics, nodes } = recordingSetup(async () => { if (failure === 'load') throw new Error('load rejected'); }); if (failure === 'resume') context.resume = async () => { throw new Error('resume rejected'); }; assert.equal(await audio.unlock(), false); assert.equal(await audio.unlock(), false); assert.equal(audio.play('probe'), null); assert.equal(audio.unlocked, false); assert.equal(diagnostics.list().filter(entry => entry.code === 'WARN_AUDIO_UNAVAILABLE').length, 1); assert.ok(nodes.every(node => node.connections.length === 0)); await audio.dispose(); } await assert.rejects(loadProtectionWorklet({}), /unavailable/); }); test('concurrent unlocks share initialization; disposal during module loading cannot resurrect output', async () => { let finish, loads = 0; const pending = new Promise(resolve => { finish = resolve; }); const { audio, nodes } = recordingSetup(() => { loads++; return pending; }); const first = audio.unlock(), second = audio.unlock(); assert.equal(audio.unlocked, false); assert.equal(loads, 1); await audio.dispose(); finish(); assert.deepEqual(await Promise.all([first, second]), [false, false]); assert.equal(nodes.filter(node => node.kind === 'master-protection').length, 0); }); test('capture resolves only its own result, prevents overlap, and rejects on disposal or processor failure', async () => { for (const ending of ['complete', 'dispose', 'error']) { const { audio } = recordingSetup(); await audio.unlock(); const pending = audio.captureOutput({ seconds: 1, warmupSeconds: 0 }); await assert.rejects(audio.captureOutput(), /already running/); const request = audio.protection.port.messages[0]; assert.equal(request.frames, 48000); if (ending === 'complete') { audio.protection.port.onmessage({ data: { type: 'capture', id: request.id + 1 } }); assert.ok(audio.capturePending); audio.protection.port.onmessage({ data: { type: 'capture', id: request.id, peak: 0.8 } }); assert.equal((await pending).peak, 0.8); } else { const rejected = assert.rejects(pending, /disposed|failed/); if (ending === 'dispose') await audio.dispose(); else audio.protection.onprocessorerror(); await rejected; assert.equal(audio.unlocked, false); } await audio.dispose(); } }); test('production loader embeds the exact processor and constructs explicit stereo worklets', async () => { const previous = globalThis.AudioWorkletNode; const calls = []; try { globalThis.AudioWorkletNode = class { constructor(...args) { calls.push(args); } }; const context = { audioWorklet: { async addModule(url) { assert.equal(decodeURIComponent(url.split(',').slice(1).join(',')), protectionWorkletSource()); } } }; await loadProtectionWorklet(context); createProtectionNode(context); createProtectionNode(context, true); assert.deepEqual(calls[0][2].outputChannelCount, [2]); assert.equal(calls[0][2].numberOfInputs, 2); assert.deepEqual(calls[1][2].outputChannelCount, [2, 1]); } finally { globalThis.AudioWorkletNode = previous; } }); test('trace 14: absent AudioContext stays silent, creates no instance, and warns once', async () => { const diagnostics = new Diagnostics(); const audio = new AudioSubsystem({ document: {}, rng: new SeededRNG(42), diagnostics }); assert.equal(await audio.unlock(), false); assert.equal(await audio.unlock(), false); assert.equal(audio.play('probe'), null); assert.equal(audio.voices.size, 0); assert.equal(diagnostics.list().length, 1); assert.equal(diagnostics.list()[0].code, 'WARN_AUDIO_UNAVAILABLE'); await audio.dispose(); }); test('the twelve challenge recipes and frozen overload workload validate and instantiate deterministically', () => { for (const file of ['audio-challenge', 'audio-protection-stress']) { const document = JSON.parse(readFileSync(`exhibits/${file}.xzbt`, 'utf8')); assert.deepEqual(validateExhibit(document).errors, []); if (file === 'audio-challenge') assert.equal(Object.keys(document.sounds).length, 12); for (const [id, sound] of Object.entries(document.sounds)) { const plan = instantiateSoundGraph(document, id, { rng: new SeededRNG(42) }); assert.deepEqual(plan.errors, [], id); assert.deepEqual(plan, instantiateSoundGraph(document, id, { rng: new SeededRNG(42) })); if (sound.recipe.mode === 'oneshot') assert.ok(Number.isFinite(plan.endingBoundMs)); if (file === 'audio-protection-stress' && id === 'hit') assert.ok(plan.endingBoundMs < 2000); } } }); test('the direct-file acceptance build is deterministic, self-contained, and embeds the production processor', () => { const directory = mkdtempSync(join(tmpdir(), 'xzbt-protection-')); const first = buildAudioAcceptance(join(directory, 'first.html')); const second = buildAudioAcceptance(join(directory, 'second.html')); assert.equal(first.sha256, second.sha256); const html = readFileSync(first.outputPath, 'utf8'); assert.doesNotMatch(html, /<(script|link)[^>]+(?:src|href)=/i); assert.ok(html.includes(MasterProtectionDSP.toString())); const source = html.match(/