// Node-side test of the recorder track-render instrument: executes the // actual MATLAB model through numbl (no browser, no worker) and checks the // audio. Run with: npx tsx test.ts import { readdirSync, readFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { chartIndexForPitch, renderRequest, type RenderEnv } from './core.ts'; import type { NoteSpec, TrackRenderRequest } from './types.ts'; const matlabDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'matlab'); const mfiles = readdirSync(matlabDir) .filter((f) => f.endsWith('.m')) .sort() .map((f) => ({ name: f, source: readFileSync(join(matlabDir, f), 'utf8') })); const env: RenderEnv = { mfiles, post: () => {}, checkCancel: () => {}, yieldToHost: async () => {}, }; let failures = 0; function check(label: string, ok: boolean, detail = ''): void { console.log(`${ok ? 'ok ' : 'FAIL'} ${label}${detail ? ` (${detail})` : ''}`); if (!ok) failures++; } function stats(y: Float32Array) { let peak = 0; let finite = true; for (let i = 0; i < y.length; i++) { if (!Number.isFinite(y[i])) finite = false; peak = Math.max(peak, Math.abs(y[i])); } return { peak, finite }; } function req(notes: NoteSpec[], overrides: Partial = {}): TrackRenderRequest { const end = notes.reduce((m, n) => Math.max(m, n.timeSec + n.durationSec), 0); return { notes, sampleRate: 22050, lengthSamples: Math.ceil((end + 1.0) * 22050), quality: 'preview', settings: {}, ...overrides, }; } const note = (timeSec: number, pitch: number, velocity: number, durationSec = 0.5): NoteSpec => ({ timeSec, durationSec, pitch, velocity }); // The pitch fold: in-range pitches map to their own row, out-of-range ones // keep their contour an octave in. check('C5 is chart row 1', chartIndexForPitch(72) === 1); check('A6 is chart row 14', chartIndexForPitch(93) === 14); check('C4 folds up to row 1', chartIndexForPitch(60) === 1); check('C7 folds down', chartIndexForPitch(96) === chartIndexForPitch(84)); const t0 = performance.now(); // One note: finite, audible. const a = await renderRequest('recorder', req([note(0, 79, 0.7)]), env); { const s = stats(a.audio); check('single note is finite', s.finite); check('single note is audible', s.peak > 1e-4, `peak ${s.peak.toExponential(2)}`); } // Determinism and the note cache. const b = await renderRequest('recorder', req([note(0, 79, 0.7)]), env); { let same = a.audio.length === b.audio.length; if (same) for (let i = 0; i < a.audio.length; i++) if (a.audio[i] !== b.audio[i]) { same = false; break; } check('render is deterministic', same); } { const t = performance.now(); await renderRequest('recorder', req([note(0, 79, 0.7)]), env); const ms = performance.now() - t; check('repeat render hits the note cache', ms < 250, `${ms.toFixed(0)} ms`); } // Velocity (breath) and duration change the sound. const soft = await renderRequest('recorder', req([note(0, 79, 0.2)]), env); { let diff = false; const n = Math.min(a.audio.length, soft.audio.length); for (let i = 0; i < n; i++) if (Math.abs(a.audio[i] - soft.audio[i]) > 1e-9) { diff = true; break; } check('velocity changes the sound', diff); } const long = await renderRequest('recorder', req([note(0, 79, 0.7, 1.5)]), env); { const shortTail = stats(a.audio.slice(Math.floor(1.2 * 22050))).peak; const longTail = stats(long.audio.slice(Math.floor(1.2 * 22050))).peak; check('duration changes the sound', longTail > shortTail, `long ${longTail.toExponential(2)} > short ${shortTail.toExponential(2)}`); } // A phrase: distinct pitches at their onsets, repeats from the cache. const tr = await renderRequest('recorder', req([note(0, 79, 0.7), note(0.8, 84, 0.7), note(1.6, 79, 0.7)]), env); { const s = stats(tr.audio); check('track render is finite', s.finite); const at = (sec: number) => stats(tr.audio.slice(Math.floor(sec * 22050), Math.floor((sec + 0.4) * 22050))).peak; check('energy at first onset', at(0.05) > 1e-4); check('energy at second onset', at(0.85) > 1e-4); check('energy at third onset', at(1.65) > 1e-4); } console.log(`total time: ${((performance.now() - t0) / 1000).toFixed(1)} s`); if (failures > 0) { console.error(`${failures} check(s) failed`); process.exit(1); }