/ proof-of-concept / midiboot
Sign in
proof-of-concept / midiboot
midiboot / scripts / test-gpu.ts
72 lines · 2.8 KBBlameHistoryRaw
1/**
2 * GPU smoke test for the placeholder instrawave adapter, on desktop
3 * WebGPU (Google Dawn) through instrawave's node shim: bake the drum once
4 * at the placeholder accuracy, render two notes through the midiboot
5 * Instrument interface, and check the result is shaped like audio. Run
6 * with `npm run test:gpu`; exits 0 with a note when no GPU is available.
7 */
8import { installWebGpu, errMsg } from 'instrawave/scripts/nodeWebGpu.ts';
10try {
11 console.log(await installWebGpu());
12} catch (e) {
13 console.log(`skipped: ${errMsg(e)}`);
14 process.exit(0);
17const { createInstrawaveInstruments } = await import('../src/instruments/instrawave/adapter.ts');
19let failures = 0;
20function check(cond: boolean, msg: string): void {
21 console.log(`${cond ? 'ok ' : 'FAIL'} ${msg}`);
22 if (!cond) failures++;
25const drum = createInstrawaveInstruments().find((i) => i.id === 'iw-drum')!;
26const SR = 44100;
27const len = Math.round(SR * 2.0);
28const t0 = performance.now();
29const out = (await drum.render({
30 notes: [
31 { timeSec: 0.1, durationSec: 0.25, pitch: 48, velocity: 0.9 },
32 { timeSec: 0.9, durationSec: 0.25, pitch: 55, velocity: 0.6 },
33 ],
34 sampleRate: SR,
35 lengthSamples: len,
36})) as Float32Array;
37console.log(`bake + render in ${((performance.now() - t0) / 1000).toFixed(1)} s`);
39check(out.length === len, `trace length ${out.length} = ${len}`);
40let finite = true;
41for (let i = 0; i < out.length; i++) if (!Number.isFinite(out[i])) { finite = false; break; }
42check(finite, 'all samples finite');
43const seg = (a: number, b: number) => {
44 let p = 0;
45 for (let i = Math.round(a * SR); i < Math.round(b * SR); i++) p = Math.max(p, Math.abs(out[i]));
46 return p;
47};
48// The raw trace is at mic-pressure scale (peaks around 1e-5); the app
49// normalizes per track, so the checks here are relative to the peak.
50const peakAll = seg(0, 2.0);
51check(peakAll > 1e-7, `trace is not silent (peak ${peakAll.toExponential(2)})`);
52check(seg(0, 0.09) < peakAll * 1e-3, 'silent before the first strike');
53check(seg(0.12, 0.8) > peakAll * 0.1, 'first strike sounds');
54check(seg(0.92, 1.8) > peakAll * 0.05, 'second strike sounds');
56// The render is deterministic against the same bake.
57const again = (await drum.render({
58 notes: [{ timeSec: 0.1, durationSec: 0.25, pitch: 48, velocity: 0.9 }],
59 sampleRate: SR,
60 lengthSamples: SR,
61})) as Float32Array;
62const again2 = (await drum.render({
63 notes: [{ timeSec: 0.1, durationSec: 0.25, pitch: 48, velocity: 0.9 }],
64 sampleRate: SR,
65 lengthSamples: SR,
66})) as Float32Array;
67let same = again.length === again2.length;
68if (same) for (let i = 0; i < again.length; i++) if (again[i] !== again2[i]) { same = false; break; }
69check(same, 'repeat render is bit-identical');
71console.log(failures === 0 ? 'GPU smoke test passed' : `${failures} failure(s)`);
72process.exit(failures > 0 ? 1 : 0);
↑↓ move↵openescclose