Single source of truth for the track ("program"/"patch") grammar, which was
implemented by hand in src/engine.js and pico-cp/app.py with no cross-check and
had quietly drifted.
- docs/track-format.md: formal grammar, container (programs.json) schema with a
version field, the new per-track playback-flow model (rep/end + relative goto;
default = loop forever), normalization rules, and a list of known divergences.
- tests/: golden vectors + a runner that loads the REAL engine.js and app.py
grammar (no copies; app.py via ast extraction) and compares both against the
spec. Exit non-zero on unexpected mismatch or round-trip break -> usable as CI.
Surfaces real divergences for follow-up: default accent pattern (no =pattern)
differs web vs device and affects shipped presets; euclid not parsed on device;
vol/cd dropped on device; unknown-sound fallback; tempo clamp; empty patch.
The rep/end playback-flow vectors are the acceptance test for building that.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
183 lines
10 KiB
JSON
183 lines
10 KiB
JSON
{
|
|
"_comment": "Golden vectors for the PM track format. See docs/track-format.md. Each case: parse `in`, normalize, compare to `norm`. status: stable|divergence|new. expectFail lists impls (js/py) known to mismatch today; an UNlisted mismatch is a regression, and a listed PASS means the gap was fixed. NOTE: stable cases use EXPLICIT =patterns on every lane, because lanes with no =pattern currently produce DIFFERENT defaults on web vs device (see the 'default-pattern' case + docs §6).",
|
|
"cases": [
|
|
{
|
|
"id": "minimal",
|
|
"status": "stable",
|
|
"in": "t120;kick:4=Xxxx",
|
|
"norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null,
|
|
"lanes": [ { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] }
|
|
},
|
|
{
|
|
"id": "backbeat",
|
|
"status": "stable",
|
|
"in": "t120;kick:4=Xxxx;snare:4=.x.x;hatClosed:4/2=X.x.x.x.",
|
|
"norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null,
|
|
"lanes": [
|
|
{ "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] },
|
|
{ "sound": "snare", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [0,1,0,1] },
|
|
{ "sound": "hatClosed", "groups": [4], "sub": 2, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,0,1,0,1,0,1,0] }
|
|
] }
|
|
},
|
|
{
|
|
"id": "odd-meter-2+2+3",
|
|
"status": "stable",
|
|
"in": "t130;kick:2+2+3=x..x..x;hatClosed:2+2+3/2=X.x.X.x.X.x.x.",
|
|
"norm": { "bpm": 130, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null,
|
|
"lanes": [
|
|
{ "sound": "kick", "groups": [2,2,3], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [1,0,0,1,0,0,1] },
|
|
{ "sound": "hatClosed", "groups": [2,2,3], "sub": 2, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,0,1,0,2,0,1,0,2,0,1,0,1,0] }
|
|
] }
|
|
},
|
|
{
|
|
"id": "swing",
|
|
"status": "stable",
|
|
"in": "t150;ride:4/2s=X.x.x.x.;kick:4=X..x",
|
|
"norm": { "bpm": 150, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null,
|
|
"lanes": [
|
|
{ "sound": "ride", "groups": [4], "sub": 2, "swing": true, "poly": false, "mute": false, "gainDb": 0, "levels": [2,0,1,0,1,0,1,0] },
|
|
{ "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,0,0,1] }
|
|
] }
|
|
},
|
|
{
|
|
"id": "ghost-notes",
|
|
"status": "stable",
|
|
"in": "t92;snare:4/3=..gg.gX.gg.g",
|
|
"norm": { "bpm": 92, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null,
|
|
"lanes": [ { "sound": "snare", "groups": [4], "sub": 3, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [0,0,3,3,0,3,2,0,3,3,0,3] } ] }
|
|
},
|
|
{
|
|
"id": "polymeter",
|
|
"status": "stable",
|
|
"in": "t100;kick:4=Xxxx;claves:5=Xxxxx~",
|
|
"norm": { "bpm": 100, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null,
|
|
"lanes": [
|
|
{ "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] },
|
|
{ "sound": "claves", "groups": [5], "sub": 1, "swing": false, "poly": true, "mute": false, "gainDb": 0, "levels": [2,1,1,1,1] }
|
|
] }
|
|
},
|
|
{
|
|
"id": "disabled-lane",
|
|
"status": "stable",
|
|
"in": "t120;kick:4=Xxxx;hatClosed:4=Xxxx!",
|
|
"norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null,
|
|
"lanes": [
|
|
{ "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] },
|
|
{ "sound": "hatClosed", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": true, "gainDb": 0, "levels": [2,1,1,1] }
|
|
] }
|
|
},
|
|
{
|
|
"id": "ramp",
|
|
"status": "stable",
|
|
"in": "t80;woodblock:4=Xxxx;rmp80/4/4",
|
|
"norm": { "bpm": 80, "bars": 0, "volume": null, "countMs": 0, "ramp": { "start": 80, "amt": 4, "every": 4 }, "trainer": null, "rep": null, "end": null,
|
|
"lanes": [ { "sound": "woodblock", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] }
|
|
},
|
|
{
|
|
"id": "gap-trainer",
|
|
"status": "stable",
|
|
"in": "t100;kick:4=Xxxx;tr2/2",
|
|
"norm": { "bpm": 100, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": { "play": 2, "mute": 2 }, "rep": null, "end": null,
|
|
"lanes": [ { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] }
|
|
},
|
|
{
|
|
"id": "segment-bars",
|
|
"status": "stable",
|
|
"in": "t88;b8;kick:4=X.x.",
|
|
"norm": { "bpm": 88, "bars": 8, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null,
|
|
"lanes": [ { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,0,1,0] } ] }
|
|
},
|
|
{
|
|
"id": "gm-note-number-alias",
|
|
"status": "stable",
|
|
"in": "t120;36:4=Xxxx",
|
|
"note": "GM note 36 = kick. engine.js maps it; firmware keeps the numeric sound but plays it via SOUND_GM. Names should resolve the same.",
|
|
"expectFail": ["py"],
|
|
"norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null,
|
|
"lanes": [ { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] }
|
|
},
|
|
|
|
{
|
|
"id": "default-pattern",
|
|
"status": "divergence",
|
|
"expectFail": ["py"],
|
|
"note": "DESIGN DECISION PENDING. A lane with no =pattern produces different defaults: web plays every subdivision (off-beats normal) and accents every beat -> [2,1,2,1...]; device rests off-beats -> [2,0,1,0...]. This affects shipped presets (e.g. 'Four-on-the-floor' hatClosed:4/2). Vector pins the web behavior as provisional reference; revise once the rule is chosen.",
|
|
"in": "t120;hatClosed:4/2",
|
|
"norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null,
|
|
"lanes": [ { "sound": "hatClosed", "groups": [4], "sub": 2, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,2,1,2,1,2,1] } ] }
|
|
},
|
|
{
|
|
"id": "euclid",
|
|
"status": "divergence",
|
|
"expectFail": ["py"],
|
|
"note": "Euclid (k,n) parsed by engine.js; firmware falls back to a plain bar. Spec = engine behavior.",
|
|
"in": "t120;kick:4(3,8)",
|
|
"norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null,
|
|
"lanes": [ { "sound": "kick", "groups": [4], "sub": 2, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,0,0,1,0,0,1,0] } ] }
|
|
},
|
|
{
|
|
"id": "vol-and-countin",
|
|
"status": "divergence",
|
|
"expectFail": ["py"],
|
|
"note": "engine.js round-trips vol/cd; firmware drops them. Spec = preserve them.",
|
|
"in": "t120;vol80;cd2;kick:4=Xxxx",
|
|
"norm": { "bpm": 120, "bars": 0, "volume": 0.8, "countMs": 2000, "ramp": null, "trainer": null, "rep": null, "end": null,
|
|
"lanes": [ { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] }
|
|
},
|
|
{
|
|
"id": "unknown-sound",
|
|
"status": "divergence",
|
|
"expectFail": ["py"],
|
|
"note": "engine.js maps unknown sounds to beep; firmware keeps the raw name. Spec = beep.",
|
|
"in": "t120;blorp:4=Xxxx",
|
|
"norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null,
|
|
"lanes": [ { "sound": "beep", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] }
|
|
},
|
|
{
|
|
"id": "tempo-clamp-high",
|
|
"status": "divergence",
|
|
"expectFail": ["js"],
|
|
"note": "Firmware clamps t to [5,300]; engine.js does not. Spec = clamp everywhere.",
|
|
"in": "t999;kick:4=Xxxx",
|
|
"norm": { "bpm": 300, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null,
|
|
"lanes": [ { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] }
|
|
},
|
|
{
|
|
"id": "empty-defaults-to-beep",
|
|
"status": "divergence",
|
|
"expectFail": ["js"],
|
|
"note": "Firmware injects a default beep:4; engine.js returns no lanes (editor guards emptiness). Spec = beep:4.",
|
|
"in": "",
|
|
"norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null,
|
|
"lanes": [ { "sound": "beep", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] }
|
|
},
|
|
|
|
{
|
|
"id": "end-stop",
|
|
"status": "new",
|
|
"expectFail": ["js", "py"],
|
|
"note": "Per-track playback flow — not yet implemented anywhere.",
|
|
"in": "t120;kick:4=Xxxx;end=stop",
|
|
"norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": 1, "end": "stop",
|
|
"lanes": [ { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] }
|
|
},
|
|
{
|
|
"id": "rep-then-next",
|
|
"status": "new",
|
|
"expectFail": ["js", "py"],
|
|
"note": "Play 4 cycles then auto-advance. next ⇒ +1.",
|
|
"in": "t120;kick:4=Xxxx;rep=4;end=next",
|
|
"norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": 4, "end": 1,
|
|
"lanes": [ { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] }
|
|
},
|
|
{
|
|
"id": "relative-goto-back-two",
|
|
"status": "new",
|
|
"expectFail": ["js", "py"],
|
|
"note": "Relative goto (D.S.): after 1 cycle, jump back two tracks.",
|
|
"in": "t120;kick:4=Xxxx;end=-2",
|
|
"norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": 1, "end": -2,
|
|
"lanes": [ { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] }
|
|
}
|
|
]
|
|
}
|