/ simulated_instruments / instrument-format
Sign in
simulated_instruments / instrument-format
instrument-format / scripts / build-exports.mjs
79 lines · 3.4 KBBlameHistoryRaw
1#!/usr/bin/env node
2// Build exported instrument bundles (format numbl-instrument-v1) for one
3// instrument repository. Run from the repo root:
4//
5// node scripts/build-exports.mjs
6//
7// For each export-src/<name>/export.json, the bundle's files are that
8// directory's .m sources (the drivers, render_note.m required) plus every
9// matlab/*.m of the repo, and the result is written to exports/<id>.json.
10// The canonical copy of this script lives in
11// simulated_instruments/instrument-format; instrument repos carry a copy so
12// they build standalone.
14import { mkdirSync, readdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
15import { join } from 'node:path';
17const ID_RE = /^[a-z0-9][a-z0-9-]*$/;
18const SRC = 'export-src';
19const OUT = 'exports';
21function fail(msg) {
22 console.error(`build-exports: ${msg}`);
23 process.exit(1);
26function checkKnobs(id, knobs) {
27 if (!Array.isArray(knobs)) fail(`${id}: 'knobs' must be an array`);
28 for (const k of knobs) {
29 if (typeof k.key !== 'string' || typeof k.label !== 'string') fail(`${id}: knobs need key and label`);
30 if (typeof k.min !== 'number' || typeof k.max !== 'number' || typeof k.default !== 'number' || !(k.min < k.max)) {
31 fail(`${id}: knob '${k.key}' needs min < max and a default`);
32 }
33 if (k.default < k.min || k.default > k.max) fail(`${id}: knob '${k.key}' default outside its range`);
34 if (k.step !== undefined && !(k.step > 0)) fail(`${id}: knob '${k.key}' step must be > 0`);
35 if (k.scale !== undefined && k.scale !== 'log') fail(`${id}: knob '${k.key}' has unknown scale`);
36 if (k.scale === 'log' && k.min <= 0) fail(`${id}: knob '${k.key}' log scale needs min > 0`);
37 }
40function mfilesIn(dir) {
41 if (!existsSync(dir)) return [];
42 return readdirSync(dir)
43 .filter((f) => f.endsWith('.m'))
44 .sort()
45 .map((name) => ({ name, content: readFileSync(join(dir, name), 'utf8') }));
48if (!existsSync(SRC)) fail(`no ${SRC}/ directory here; run from the instrument repo root`);
49const shared = mfilesIn('matlab');
50const dirs = readdirSync(SRC, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
51if (dirs.length === 0) fail(`${SRC}/ has no instrument directories`);
52mkdirSync(OUT, { recursive: true });
54for (const dir of dirs) {
55 const metaPath = join(SRC, dir, 'export.json');
56 if (!existsSync(metaPath)) fail(`${metaPath} is missing`);
57 const meta = JSON.parse(readFileSync(metaPath, 'utf8'));
58 const id = meta.id;
59 if (typeof id !== 'string' || !ID_RE.test(id)) fail(`${metaPath}: 'id' must match ${ID_RE}`);
60 if (typeof meta.name !== 'string' || meta.name === '') fail(`${id}: missing 'name'`);
61 checkKnobs(id, meta.knobs ?? []);
62 const drivers = mfilesIn(join(SRC, dir));
63 if (!drivers.some((f) => f.name === 'render_note.m')) fail(`${id}: no render_note.m in ${SRC}/${dir}/`);
64 const names = new Set(drivers.map((f) => f.name));
65 const clash = shared.find((f) => names.has(f.name));
66 if (clash) fail(`${id}: driver file '${clash.name}' collides with matlab/${clash.name}`);
67 const bundle = {
68 format: 'numbl-instrument-v1',
69 id,
70 name: meta.name,
71 ...(meta.description ? { description: meta.description } : {}),
72 ...(meta.source ? { source: meta.source } : {}),
73 knobs: meta.knobs ?? [],
74 files: [...drivers, ...shared],
75 };
76 const outPath = join(OUT, `${id}.json`);
77 writeFileSync(outPath, JSON.stringify(bundle, null, 2) + '\n');
78 console.log(`wrote ${outPath} (${bundle.files.length} files, ${bundle.knobs.length} knobs)`);
↑↓ move↵openescclose