Deckard

deck grammar

Full streamable patch language reference (@spacedevin/deck).

Line-oriented, streamable patch text (.deck). This is the language reference for @spacedevin/deck.

Package responsibilities: tokenize, parseProgram → AST, format helpers, bar selectors, Euclidean step fill, scale root/mode vocab, highlight classify, empty registries (generator id / param key / macro / gen_block dialect).

Host responsibilities: map AST → project IR (apply/emit), audio engines, ownership/skills, co-DJ, UI. Generators, builtin macro catalogs, and patch / matrix_fm dialect parsers are host-registered.

Times are in quarter-note beats. One bar = 4 beats = 16 sixteenth steps.


Lexical

  • Lines are statements. Indentation (2+ spaces or tab) nests a body under the current open block (track, clip, auto, macro, song, follow, gen_block).
  • # starts a comment to end of line — but only at column 0 or after whitespace, so a # inside a token is data. That is what makes sharp note names (scale F# minor, a track named C#maj) work.
  • Tokens: whitespace-separated; numbers accepted by isNumberToken.
  • Legacy alias: tpldeck for the version header only.

Version header

deck 1

Recommended first non-comment line. Emit writes deck 1. Distinct from track-body routing deck A|B|C|D.


Top-level statements

These are recognized by parseProgram.

StatementFormNotes
Versiondeck <n> / tpl <n>
Tempobpm <number>
Swingswing <0..1>Off-beat 16th shuffle; 0 = straight
Scale lockscale <root> <mode>root = note (C, F#, Bb) or pitch-class 0..11; modes below. scale off / none / chromatic clears (AST root -1)
Launch quantlaunch_quant <n>Scene/clip launch grid (bars), n ≥ 1
Song seedsong_seed <int>Seeds deterministic randomness (e.g. step probability)
Crossfaderxfade <x> [<y>]Both 0..1; if y omitted, y = 0.5
Main deck`main_deck live\local`
Booth mix`deck_mix <A\B\
Track`track <name…> id <id> gen <genId\macro> [ * <N\
Remove trackremove_track <id>Incremental edit; not present in full snapshots
Macro defmacro <name> [k=default …]end macroBody lines = patch dialect lines
Automationauto … + indented <beat> <value>See Automation
Master mixmaster_mix eq_lo <db> eq_mid <db> eq_hi <db>Keys any order; missing keys unchanged
Actor mixactor_mix <lane> …gain/trim, eq_*, optional mute/solo
Session scenessession_scenes <n>n ≥ 1
Session slot`session_slot <channelId> <sceneIdx> <clipId\-\
Clipclip <clipId> channel <channelId> bars <n> [name …] + indented body
Songsong + indented P<scene1> [x<repeat>] or bare scene index1-based P
Followfollow + indented P<scene> <a> <wa> [<b> <wb>]1-based P
Control directive@ <verb> …Collected into directives[]; the verb is host-interpreted. See Control directives

Scale modes

Accepted mode tokens (aliases in parentheses): major (ionian), minor (aeolian), dorian, phrygian, lydian, mixolydian, locrian, harmonic_minor, melodic_minor, pentatonic_major (penta_major, majpenta), pentatonic_minor (penta_minor, minpenta), blues.

Package helpers: parseScaleRoot, scaleRootNames, scaleModeNames, scaleIntervals.

Track header

track <displayName> id <channelId> gen <generatorId|macro> [ * <N|inf> ] [ <param> <val> … ]
  • Npattern length in bars (default 1). Channel spans N × 16 steps and repeats. inf / * infinite clears an explicit finite length.
  • Trailing key value pairs — macro parameter overrides when gen is a macro name.
  • N and the key value pairs may appear in any order after gen <id>. Emit writes N first; a * that names no valid length is an error, never a silently dropped token.
  • generatorId spellings are host-registered (registerGeneratorIdAliases). Undeclared ids pass through as-is.

Track / clip body

parseProgram stores indented body lines as token rows (except gen_block collection); parseBodyLine / parseTrackBody turn those rows into typed values. The heads below are the standard language.

Body parsing is deliberately parse-only: an absent optional is null so the host applies its own default, and there is no clamping or range checking — that is host policy, and hosts differ (one clamps an out-of-range lock, another rejects it). Range checks needing track context (note start vs * N) can't live here at all. An unrecognised head comes back as kind: "unknown" so a host dialect can claim it via registerBodyLineDialect — see DECK_EXTENSION.md.

Mix

mix gain <n> pan <n> [mute <0|1>] [solo <0|1>] [eq_lo <db>] [eq_mid <db>] [eq_hi <db>]

Boolish: 1/true/on vs 0/false/off.

Pattern length vs play cap

FormMeaning
* N on track headerPattern length (bars); loops forever
`loops <N\inf\

Compose: * 4 + loops 8 = 4-bar pattern played twice, then stops.

Steps

steps x . . . x . . . x . . . x . . .
steps euclid <hits> <len>
  • On: x X 1 · Off: . 0
  • Euclidean: Bjorklund fill (euclideanPattern in this package). Common host constraint: len = 16.

Step lock lanes (after steps)

Emitted only when a step differs from the default:

LaneRange (default)Meaning
step_vel1..127 (100)Velocity
step_prob0..1 (1)Hit probability (seeded; peers agree)
step_ratchet1..8 (1)Sub-hits over the step
step_nudge-0.5..0.5 (0)Micro-timing as a fraction of a step

A bare steps line resets locks; following lanes restore deviations. Optional host extension: step_lyric (emitted by some hosts).

Step pitch

step_pitch <midi> [ bar <selector> ]

Base MIDI for step hits when the channel has no note lines (default 36). With bar <selector>, one line per bar/group for multi-bar patterns.

Notes (piano roll)

note <midi> <startBeat> <durBeats> v <velocity> [ p <prob> ] [ r <ratchet> ] [ n <nudge> ] [ bar <selector> ] [ l <lyric> ]
  • Beats in quarter notes. For pattern length N: 0 ≤ startBeat and startBeat + durBeats ≤ N×4.
  • Optional locks (non-default only on emit): p, r, n — same semantics as step locks.
  • bar <selector>: keep startBeat < 4; expand onto matching loop bars.
  • Host optional: l <lyric> on notes / step lyric lane.

Steps vs notes: if a track block contains any note lines, steps for that channel are cleared. If it contains steps and no notes, piano notes are cleared. Playback prefers notes when any exist.

Also: notes_clear — host edit fragment that clears piano notes.

Transpose

transpose <semitones>

Integer shift applied to collected note pitches for that block.

Generator params (fixed / generic)

Hosts typically accept:

gen <snake_key> <val> …
adsr a <n> d <n> s <n> r <n>

plus legacy one-line shapes for specific engines (noise …, fm …, osc waveform …). Snake_case keys map via paramKeyToCamel / registerParamKeyAliases.

Channel FX / voice / deck routing

fx reverb_send <n> drive <n> lfo_rate <n> lfo_depth <n> cutoff <n> res <n> [filter_type <t>]
voice octave <n> arp <token> chord <token> arprate <token> inversion <token> strum <n>
deck <A|B|C|D|live> [slot <n>]

fx also accepts reverb as alias for reverb_send, and type as alias for filter_type.

Heavy generators (gen_block)

gen_block <generatorId>
  …
end gen_block

Core language collects lines until end gen_block. parseGenBlock(id, lines) returns { kind, tplHeaderId, version, raw } until a host dialect is registered. See DECK_EXTENSION.md for the registration API and common patch / matrix_fm dialects.


Bar selectors

Single token (no spaces). Used after bar on note / step_pitch. Bars are 0-indexed within the track's * N length.

SelectorMatches
even / odd0,2,4,… / 1,3,5,…
<int>that bar only
n / * / all / everyevery bar
<a>nbar % a == 0
<a>n+<b>bar % a == b
-n+<b>first b bars (0 .. b-1)
b0,b1,…explicit list

Package: parseBarSelector, barSelectorMatches.


Macros

Define (top-level):

macro <name> [key=default …]
  … patch-dialect body with $key …
end macro

Use: track … gen <name> [key val …] — expands to a gen_block patch at load (when the host registers a patch dialect + builtin/user macros). Package provides lookupMacro, expandMacroBody, registerBuiltinMacros (catalog is empty until the host fills it).


Automation

auto master_gain
  <beat> <value>

auto <channelId> gen <paramName>
  <beat> <value>

auto <channelId> mix <gain|pan|eq_lo|eq_mid|eq_hi>
  <beat> <value>

auto actor <lane> mix <gain|trim|eq_lo|eq_mid|eq_hi>
  <beat> <value>

auto master mix <eq_lo|eq_mid|eq_hi>
  <beat> <value>

Indented points are beat value pairs. Hosts interpolate on the beat timeline (beat = globalStep × 0.25 for step playback).


Session / scenes / clips

session_scenes <n>
session_slot <channelId> <sceneIdx> <clipId|->

clip <clipId> channel <channelId> bars <n> [name <display…>]
  steps …
  note …
  loops …

Clip grid length = bars × 16 steps. Clip notes may span the whole clip (bars × 4 beats). Same steps-vs-notes rule as tracks.

Song arrangement

song
  P1
  P2 x4
  3

1-based scene refs (P<n> or bare index). Optional xN repeat.

Follow actions

follow
  P1 next 1
  P2 jump 0.7 stay 0.3

P<scene1> <actionA> <weightA> [<actionB> <weightB>]. Host interprets action tokens.


Control directives (@ …)

Transient stream lines. Most are not stored in a static project document; hosts apply them for performance / co-DJ.

parseProgram collects every @ … line into directives[] as { lineNo, verb, tokens } and does not interpret the verb — that is host policy. A bare @ with no verb is an error. Hosts typically understand:

DirectiveTypical authorityEffect
@ launch scene <n>masterArm scene clips
`@ launch clip <trackId> <clipId\-\stop>`
`@ transport play\song\sequence [scene] \
@ transport previewprivateLocal preview clock
@ cue <scene>privateLoad into local cue
@ throw [scene]masterCue → shared main
`@ fx <echo\filter> on\off …`
`@ deck <A\B\C\
@ perf_step <n>Schedule surrounding block for perf step n

Any other verb is collected too, so a host may define its own without a parser change.


Format helpers

Package emit helpers (numeric spelling):

  • formatTplBeat — snap to 1/96 beat, trim zeros
  • formatTplFloat — ≤ 4 decimal places, trimmed

Streaming rules

  1. Strip comments; ignore empty lines.
  2. A line commits when its newline arrives and any open gen_block is closed.
  3. Partial trailing lines must not mutate state.
  4. Incremental merge is by channelId / clip id / automation key (host apply).

What is not language (host-only)

  • Audio engines and Web Audio graphs
  • Instrument / preset catalogs and generator default param tables
  • Builtin macro catalogs (register into the package)
  • Ownership, skills, co-DJ transport plumbing
  • HTML highlight styling (tpl-hl-*) — classify API only lives here
  • Project JSON / IR schemas beyond what the AST implies

Golden example

deck 1
bpm 118
swing 0.08
scale C minor
xfade 0.5 0.5
main_deck live

track Kick id c0 gen noise_burst
  mix gain 0.9 pan 0 eq_lo 0 eq_mid 0 eq_hi 0
  deck A
  step_pitch 36
  steps x . . . x . . . x . . . x . . .
  step_vel 120 100 100 100 70 100 100 100 100 100 100 100 90 100 100 100

track Bass id c3 gen fm * 2
  mix gain 0.85 pan 0
  voice octave -1
  fx cutoff 1200 res 0.4
  note 48 0.0 0.5 v 90
  note 50 1.0 0.5 v 85 bar even

track Lead id c4 gen patch
  gen_block patch
    osc o1 sawtooth note
    filter f1 lowpass q 4 freq 1800
    gain a1 0
    conn o1 f1 1
    conn f1 a1 1
    conn a1 out 1
    env a1.gain set 0 0 lin 0.01 0.9 lin dur 0
  end gen_block
  note 60 0 1 v 80

session_scenes 4
session_slot c0 0 clip_kick_a

clip clip_kick_a channel c0 bars 1
  steps x . . . x . . . x . . . x . . .

auto master_gain
  0 0.85
  16 0.9

master_mix eq_lo 0 eq_mid 0 eq_hi 0