metronome/tests/adapters/js_adapter.mjs
Me Here cb54b4d689 Preserve notation + grammar feature work (verified complete + green)
The parallel agent's full session, committed now that it's solo:
- Grammar: flam/drag/roll ornaments (f/F d/D z/Z, per-lane orns channel) across
  src/engine.js, pico-cp/pico-explorer/pico-scroll app.py, pico/main.py, rust/track-format,
  + golden vectors / conformance (tests/, rust/track-format/tests).
- Live-sync deep-sync: SysEx 0x44 SLSYNC + 0x45 LOGSYNC (docs/livesync-protocol.md, src/livesync.js).
- PM_E-2 notation: web engine (pm_e-2.html, build/deploy/index/embed wiring) + Rust device port
  (pm-ui draw_notation rewrite + LaneView.groups, pm-kit ViewMode, uisim notesim).

Verified: node tests/run.mjs 47 pass / 1 known; ./rust/run.sh green; pm-kit firmware + uisim compile.
2026-06-02 13:45:26 -05:00

52 lines
2.1 KiB
JavaScript

// JS adapter: loads the REAL grammar out of src/engine.js (no copy) and emits the
// neutral normalized structure defined in docs/track-format.md §5.
//
// engine.js has no top-level side effects (only const/function declarations) and references
// `window` only inside ensureAudio(), which we never call — so it loads cleanly in Node.
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const here = dirname(fileURLToPath(import.meta.url));
const src = readFileSync(join(here, "..", "..", "src", "engine.js"), "utf8");
// Expose the pure codec functions from engine.js's top-level scope.
globalThis.window = globalThis.window || {};
const api = new Function(src + "\n;return { patchToSetup, setupToPatch };")();
const groupsArr = (s) => String(s).split(/[^0-9]+/).filter(Boolean).map(Number);
export function normalize(patch) {
const s = api.patchToSetup(patch);
return {
bpm: s.bpm,
bars: s.bars || 0,
volume: s.volume == null ? null : s.volume,
countMs: s.countMs || 0,
ramp: s.ramp && s.ramp.on ? { start: s.ramp.startBpm, amt: s.ramp.amount, every: s.ramp.everyBars } : null,
trainer: s.trainer && s.trainer.on ? { play: s.trainer.playBars, mute: s.trainer.muteBars } : null,
end: s.end == null ? null : s.end,
rep: s.end == null ? null : (s.rep == null ? 1 : s.rep), // rep only meaningful with end; defaults to 1
lanes: (s.lanes || []).map((c) => ({
sound: c.sound,
groups: groupsArr(c.groupsStr),
sub: c.stepsPerBeat || 1,
swing: !!c.swing,
poly: !!c.poly,
mute: c.enabled === false,
gainDb: c.gainDb || 0,
levels: (c.beatsOn || []).map((v) => v | 0),
orns: (c.orns || []).map((v) => v | 0),
})),
};
}
// serialize(parse(x)) — used for the idempotency check.
export function canonical(patch) {
return api.setupToPatch(api.patchToSetup(patch));
}
// CLI: `node js_adapter.mjs '<patch>'` prints normalized JSON.
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
process.stdout.write(JSON.stringify(normalize(process.argv[2] ?? "")));
}