/ proof-of-concept / midiboot
Sign in
proof-of-concept / midiboot
midiboot / scripts / test-sim.ts
82 lines · 4.0 KBCodeBlameHistory
2 * Integration test for the simulated-instrument path: real manifests, real
3 * MATLAB sources, real numbl execution in node (no browser, no worker).
4 * Renders a woodblock strike and a bass pluck at the preview tier and
5 * checks the audio is shaped right, including that velocity actually
6 * changes the result. Run with `npm run test:sim`; slower than npm test
7 * because it runs the physics.
8 */
9import { readdirSync, readFileSync, existsSync } from 'node:fs';
10import { join } from 'node:path';
11import { parseManifest, type SimManifest } from '../src/instruments/simulated/manifest.ts';
12import { renderSimNote, type MFile } from '../src/instruments/simulated/mrender.ts';
14const ROOT = join(import.meta.dirname, '../../../simulated_instruments');
16function loadDef(name: string): { manifest: SimManifest; mfiles: MFile[] } {
17 const manifest = parseManifest(readFileSync(join(ROOT, name, 'instrument.json'), 'utf8'));
18 const mdir = join(ROOT, name, 'matlab');
19 const mfiles = readdirSync(mdir)
20 .filter((f) => f.endsWith('.m'))
21 .map((f) => ({ name: f, source: readFileSync(join(mdir, f), 'utf8') }));
22 return { manifest, mfiles };
25let failures = 0;
26function check(cond: boolean, msg: string): void {
27 console.log(`${cond ? 'ok ' : 'FAIL'} ${msg}`);
28 if (!cond) failures++;
31function peak(x: Float32Array, from = 0, to = x.length): number {
32 let p = 0;
33 for (let i = from; i < Math.min(to, x.length); i++) p = Math.max(p, Math.abs(x[i]));
34 return p;
37function finite(x: Float32Array): boolean {
38 for (let i = 0; i < x.length; i++) if (!Number.isFinite(x[i])) return false;
39 return true;
42if (!existsSync(ROOT)) {
43 console.log(`skipped: ${ROOT} not found`);
44 process.exit(0);
47const SR = 44100;
50 const { manifest, mfiles } = loadDef('woodblock');
51 const t0 = performance.now();
52 const soft = renderSimNote(manifest, mfiles, { pitch: 60, velocity: 0.25, durationSec: 0.25, sampleRate: SR, quality: 'preview', settings: {} });
53 const hard = renderSimNote(manifest, mfiles, { pitch: 60, velocity: 1.0, durationSec: 0.25, sampleRate: SR, quality: 'preview', settings: {} });
54 console.log(`woodblock: two strikes in ${((performance.now() - t0) / 1000).toFixed(1)} s`);
55 check(soft.length > SR * 0.2, `woodblock trace has audio length (${soft.length})`);
56 check(finite(soft) && finite(hard), 'woodblock samples finite');
57 check(peak(soft) > 0, 'woodblock soft strike sounds');
58 check(peak(hard) > peak(soft) * 1.5, `harder mallet is louder (${peak(hard).toExponential(2)} vs ${peak(soft).toExponential(2)})`);
59 const moved = renderSimNote(manifest, mfiles, { pitch: 60, velocity: 1.0, durationSec: 0.25, sampleRate: SR, quality: 'preview', settings: { strike_position: 0.25 } });
60 let differs = moved.length !== hard.length;
61 for (let i = 0; i < Math.min(moved.length, hard.length) && !differs; i++) if (moved[i] !== hard[i]) differs = true;
62 check(differs, 'strike position knob changes the sound');
66 const { manifest, mfiles } = loadDef('bass');
67 const t0 = performance.now();
68 const y = renderSimNote(manifest, mfiles, { pitch: 33, velocity: 0.7, durationSec: 0.5, sampleRate: SR, quality: 'preview', settings: { tail_s: 2.0 } });
69 console.log(`bass: one pluck in ${((performance.now() - t0) / 1000).toFixed(1)} s`);
70 check(finite(y), 'bass samples finite');
71 check(peak(y) > 0, 'bass pluck sounds');
72 check(Math.abs(y.length - SR * 2.0) < SR * 0.2, `bass trace spans the tail (${y.length} samples)`);
73 check(peak(y, 0, Math.round(SR * 0.5)) > peak(y, Math.round(SR * 1.5)), 'bass pluck decays');
74 // Determinism: fixed seed, same request, same samples.
75 const y2 = renderSimNote(manifest, mfiles, { pitch: 33, velocity: 0.7, durationSec: 0.5, sampleRate: SR, quality: 'preview', settings: { tail_s: 2.0 } });
76 let same = y.length === y2.length;
77 for (let i = 0; i < y.length && same; i++) if (y[i] !== y2[i]) same = false;
78 check(same, 'bass render is deterministic');
81console.log(failures === 0 ? 'simulated-instrument test passed' : `${failures} failure(s)`);
82process.exit(failures > 0 ? 1 : 0);
↑↓ move↵openescclose