.track Language Reference

A precise, one-by-one reference for every keyword, setting, and value the .track parser accepts. For a gentle tutorial, see README.md; this document is the exhaustive spec.

Everything here is verified against the parser in src/parser/ and the engine defaults in src/sounds/, src/engine/, and src/ir/. Where the implemented language has rough edges, they are flagged plainly; a planned improvement — see the project roadmap.

Every fenced code block in this file is either a complete, parseable .track file or is tagged ```text / ```bash because it is prose, pseudo-syntax, or a deliberate error example. A test (src/docsExamples.test.ts) enforces this.


1. File anatomy

A .track file is plain UTF-8 text, parsed line by line. Each non-empty line is one of:

Line kindLooks likeSection
Comment# ...§2
Header settingbpm 120§3
Scalescale A minor§4
Row (pattern)kick x...x...x...x...§5
Hold / rest (gate)_ · .§6
Section header[verse x2] · [outro x2 fade out]§7
Arrangementarrange verse chorus§7
Instrument definitioninst lead = saw cutoff 1800§8
Kit / module importuse kit 808 · use "synths"§9
Mix bus (fx)fx reverb room 0.9 wet 0.4§10

Whitespace around a line is trimmed and blank lines are ignored. Line ordering is mostly free:


2. Comments

A comment is a whole line whose first non-whitespace character is #:

# anything on a line that starts with # is ignored
    # leading whitespace before the # is fine too

There are no end-of-line comments. A header line is matched by an exact keyword value regex, so trailing text makes the line fall through to row parsing and error out. The following does not parse — it is shown only as a counter-example:

bpm 120   # tempo        ← NOT a comment; this line fails to parse
kick x...x...x...x...  # backbeat   ← also fails

Put comments on their own lines.


3. Header settings

Syntax: keyword value, where value must be a number (exactly one keyword and one value, nothing else on the line). There are seven header keywords. Each may appear anywhere in the file and applies to the whole song; if repeated, the last occurrence wins.

KeywordMeaningDefaultRange
bpmTempo, beats per minute.120any positive number
beatsBeats per bar.4positive integer
stepsGrid cells per bar (16 = sixteenth notes). Sets the required row length.16positive integer
swingPushes off-beat (odd-indexed) steps later for a shuffle feel.001
masterOverall output gain, applied before the soft-limiter.101 (higher allowed, soft-limited)
seedSeeds the deterministic RNG used by humanize (see §14).1any number
humanizeAmount of seeded naturalness: loudness ±15 %·h and lay-back timing up to 15 ms·h, plus per-note pitch ±3 cents·h and brightness ±6 %·h variation — all seeded (see §14).001 typical

Notes:

A minimal song header using all seven settings:

bpm      128
beats    4
steps    16
swing    0.08
master   0.9
seed     7
humanize 0.4

kick x...x...x...x...

4. scale and degrees

A scale line lets you write degree numbers (1, 2, 3, …) instead of absolute note names, and have them snap to a key.

Syntax:

scale ROOT FLAVOUR [baseOctave]
FlavourIntervals (semitones from root)
major0 2 4 5 7 9 11
minor0 2 3 5 7 8 10
dorian0 2 3 5 7 9 10
penta0 3 5 7 10

A degree d (integer ≥ 1) resolves to rootMidi + intervals[(d−1) mod L] + 12 × floor((d−1) / L), where L is the number of intervals in the flavour. In plain terms: degrees walk up the scale, and once they run off the top they wrap into the next octave.

Worked example with scale A minor (root A, baseOctave 3):

DegreeNote
1A3
3C4
8A4 (one full octave above 1)
scale A minor

bass 1 . . . 3 . . . 5 . . . 8 . . .

Degrees and absolute note names can be mixed freely, including inside chords (see §5).


5. Rows (patterns)

A row is name pattern. The name (\w+ — letters, digits, underscore) picks the sound; the pattern says when (and what) to play.

A row’s expanded step count must be a non-zero multiple of steps. A row longer than steps is a multi-bar row: it keeps advancing across the section instead of repeating each bar (it is indexed position % rowLength, so it evolves across bars).

There are two pattern styles, chosen automatically.

5a. Grid form (one solid token)

If the pattern is a single token made only of the characters o x X . _, it is read character-by-character, one per step:

CharStepVelocity
oghost hit0.5
xhit0.8
Xaccent1.0
.rest
_hold— (see §6)
steps 16
kick  x...x...x...x...
hat   x.x.oxx.x.x.oxx.
clap  ....X.......X...

5b. Token form (whitespace-separated)

Otherwise the pattern is split on whitespace into one token per step. Each token is one of:

TokenStepVelocity
o x Xghost hit / hit / accent0.5 / 0.8 / 1.0
.rest
_hold
a note (C4, F#3, Eb2)pitched hit0.8
a degree (1, 2, 3, …)pitched hit (via scale)0.8
a chord (A3+C4+E4, 1+3+5)pitched hit, all atoms at once0.8
a count (4x, 7_, 3.)the mark, repeated N timesas the mark

Note names. A letter AG (case-insensitive), an optional # (sharp) or b (flat), then a single-digit octave that may be negative.

Degrees. Any bare integer ≥ 1 is a scale degree, resolved through the active scale (see §4). With no scale line, the default is C major octave 3.

Chords. Join two or more atoms with + to play them simultaneously. Atoms may be notes, degrees, or a mix: A3+C4+E4, 1+3+5, C4+5.

scale C major
steps 16
bass C2 . . . G1 . . . A1 . . . E1 . . .
lead . . 3 . 5 . . 7 . . 6 . . 5 . .
pad  1+3+5 . . . . . . . 4+6+8 . . . . . . .

5c. Count tokens

A count token is N<mark> where mark is one of . _ o x X. It expands to that mark repeated N times: 4x is four hits, 7_ is seven holds, 3. is three rests. A whole bar of sixteenth-note hits is just 16x.

steps 16
kick 4x 4x 4x 4x
hat  16x

Spacing gotcha. 3. (no space) is three rests. 3 . (with a space) is two tokens: scale-degree 3 followed by one rest. The space changes the meaning entirely.

Counts apply only to the five marks. 4C4 is not four C4 notes — it is an error. Counts do not apply to notes, degrees, or chords.

5d. Multi-bar rows

A row whose expanded length is 2 × steps, 3 × steps, … is a multi-bar row. It plays its full length across the section rather than repeating each bar, so it can evolve:

steps 16
bass C2 . . . G1 . . . A1 . . . E1 . . . F1 . . . D1 . . . G1 . . . C2 . . .

5e. Drum vs. pitched classification

A row is pitched if any of its steps is a note, degree, or chord; otherwise it is a drum row. This decides which default sound an un-inst’d name gets (see §11).

Gotcha. A row named kick written with notes (kick C2 . E2 .) is classified pitched and will not use the kick drum. If you want a pitched part to use a drum sample, define an inst for it explicitly.

5f. Token suffixes — roll and maybe

A token-form atom (a hit o/x/X, a note, a degree, or a chord) may carry up to two suffixes, in the fixed order atom[*N][?P]:

SuffixNameMeaning
*Nroll / ratchetthe step fires N quick, evenly-spaced sub-hits inside its one step (same pitch and velocity). N is 29.
?maybethe step plays with 50 % probability (seeded).
?Pmaybe N %plays with P % probability; P is 199.

The two combine as x*3?50 (“a 3-hit roll that happens half the time”). Order is fixed — the roll count comes first, the probability last.

steps 16
snare . . . . X . . . . . . . x*3 . . .
hat   x? x? x? x? x? x? x? x? x? x? x? x? x? x? x? x?
lead  C4*2 . . . E4 . . . G4 . . . C5*4 . . .
perc  x*3?50 . x?75 . x . . . x*3?50 . x?75 . x . . .

Rules and limits:

Suffixes are resolved per event when the timeline is built, so ? (maybe) is seeded: the same seed gives the same coin-flips every render (see §14).

5g. Token groups — cycle and choice

A token-form step may be a group of plain atoms instead of a single atom:

SyntaxNamePicksWhen
<a b c>cyclethe next element, advancing once per bar (global bar index, wraps)per bar
[a|b|c]choiceone element at random (seeded)per event
scale A minor
steps 16
bass <1 5 6 4> . . . <1 5 6 4> . . . <1 5 6 4> . . . <1 5 6 4> . . .
lead [1|5|8] . . . [3|5|8] . . . [1|5|8] . . . [3|5|8] . . .

A <cycle> advances on the global bar index (floor(globalFrameIndex / steps)), so a <1 5 6 4> written four times across one bar shows the same element four times in that bar, then the next element next bar — it evolves bar to bar, not step to step.

Rules:

5h. Row transforms — rev, fast, slow, arp

After the pattern, on the same line, you may append one or more transforms. They apply left to right as written:

TransformMeaningEffect on length
revreverse the expanded step listunchanged
fast Nplay the pattern within its span: out[i] = in[(i*N) mod L]unchanged
slow Nstretch : old step j lands at j·N, holds fill the gapsbecomes L·N (a multi-bar row)
arp up / arp downchord steps spell their pitches as quick sub-hits, ascending / descending, instead of all at onceunchanged

N for fast / slow is an integer ≥ 2.

steps 16
hat   x . o . x . o . x . o . x . o . rev
kick  x . . . x . . . x . . . x . . . fast 2
snare . . . . x . . . . . . . x . . . slow 2
pad   1+3+5 . . . . . . . 4+6+8 . . . . . . . arp up

Notes:

5i. Row generators — spread (Euclidean)

Instead of a pattern, a row can be a generator that fills the bar for you:

SyntaxMeaning
name spread Kplace K hits spread as evenly as possible across the bar’s steps slots (Bjorklund / Euclidean)
name spread K rotate Rthe same, rotated right by R slots
steps 16
kick spread 4
hat  spread 11
clap spread 5 rotate 2
perc spread 7 rev

With steps 16, spread 4 is four-on-the-floor (x...x...x...x...), spread 5 is x..x..x..x..x..., and spread 11 is a dense x.xx.xx.xx.xx.xx hat. Rules:


6. Gate — hold and rest

Neither . nor _ emits a trigger, but they shape the note before them. A note’s gate is how long it is held on before it releases. The note plays at its sustain level for the whole gate, then fades out over its release.

So _ and . now sound different: _ sustains, . cuts. A bare note followed by nothing special gets a one-step gate.

Gate length rules

SituationGate
Bare note, no trailing _1 step
Note followed by k consecutive _1 + k steps (the holds extend it)
Next note / hit on the same rowcloses the previous gate at that step
. (rest)closes the gate at that step
Section instance endcloses any still-open gate (gates don’t cross section loops)
Chordall its pitches share one gate
Roll / arp sub-hitseach sub-hit inherits the parent step’s gate

Gate length is counted as 1 + (number of consecutive holds after the trigger), scanned within the section instance using multi-bar indexing (pos % len). The gate never extends past the next trigger (the next trigger occupies a non-hold step) nor past the end of the section instance.

steps 16

lead  C4 _ _ _ . . . . E4 . G4 . A4 _ _ _

Here C4 holds for four steps (note + three _) then rests; E4 and G4 are short one-step stabs; A4 holds for four steps to the bar’s end. See §8d for glide, which slides between consecutive notes on a row.

Drums & samples are one-shots. A kick, hat, clap, or sample voice ignores the gate — it always plays its full recorded/enveloped length, so _ after a drum hit does nothing audible.


7. Sections & arrange

7a. Sections — [name xN]

[intro]
kick x...x...x...x...

[verse x2]
kick x...x...x...x...
clap ....X.......X...

xN repeats the section for N bars. Combined with a multi-bar row, a section can evolve across its repeats (a 32-step row inside [verse x2] plays its full two bars once).

Fades — fade in / fade out

A section header may end with fade in or fade out (after the optional xN):

[intro x2 fade in]
kick x...x...x...x...

[outro x2 fade out]
kick x...x...x...x...

The fade scales the velocity of each trigger by its position across the whole section instance: fade in ramps 0 → 1 (silent → full) from the section’s first step to its last, fade out ramps 1 → 0. The full grammar is [name xN fade in|out] — the fade word must come last, and it must be in or out (anything else → malformed section header).

Lean detail. The ramp is applied per trigger, at the trigger’s step. A held note (_) keeps the level of its trigger; the fade does not re-scale the sustain partway through the held step. So a long sustained pad inside a fade reflects the fade at its attack, not continuously. For audible fades, prefer parts that re-trigger across the section (drums, repeated stabs) over one long held chord.

7b. arrange — play order

[verse]
kick x...x...x...x...

[chorus]
kick x...x...x...x...
clap ....X.......X...

arrange verse chorus verse chorus

8. inst — custom instruments

An inst line names a sound. It produces no audio by itself — a row with the same name uses it.

inst NAME = SOURCE [key value] [key value] ...

8a. A waveform (synth)

Accepted wave tokens (case-insensitive): saw / sawtooth, square, sine, triangle / tri.

inst lead = saw cutoff 1800 resonance 6 attack 0.01 release 0.25 reverb 0.3 pan -0.2
kick x...x...x...x...

A bare wave starts from these synth defaults, which any params then override:

ParamDefault value
cutoff1500
attack0.01
decay0.2
sustain0.2
release0.1
resonance1 (gentle, near-flat filter Q)
octave0 (no shift)
delay / reverb0 (no sends)
pan0 (center)

8b. preset P

Clones a built-in preset (see §12); trailing params override its fields. The preset name is case-insensitive. Unknown preset name → error.

inst lead = preset pluck cutoff 3000
kick x...x...x...x...

8c. sample S

Plays a recorded clip instead of a synth (see §13). The sample name is case-insensitive. Unknown sample name → error. Params may follow, though most synth params don’t apply to a raw sample.

inst kick = sample 808kick
kick x...x...x...x...

8d. Instrument parameters

Params come in key value pairs. There are twenty-seven keys; each value must be a number. The first group works on any synth voice; the second group is sample-voice only.

ParamFieldWhat it does
cutofflow-pass cutoff (Hz)brightness — lower is darker
resonancefilter Qemphasis/“bite” at the cutoff (applies to whichever filter type is active)
attacksecondsfade-in time of the envelope
decaysecondsfall time after the attack peak
sustain01held level after decay
releasesecondsfade-out after the note ends
octavewhole numbertranspose by whole octaves (-1, 2, …)
delay01send level into the shared delay (echo) bus
reverb01send level into the shared reverb bus
pan-1+1stereo position: -1 hard left, 0 center, +1 hard right
glideseconds (≥ 0)portamento — slides the pitch from the row’s previous note into this one over min(glide, gate) seconds. 0 (default) = no slide.
vibrato01pitch wobble: a 5.5 Hz sine, depth ±40 × v cents, with a 0.3 s fade-in (so it blooms in after the note settles) — singing leads. 0 (default) = off.
unisonvoices 17 (int)stacks that many detuned copies of the oscillator for a wide, fat sound. 1 (default) = off.
detunecentshow far the unison copies spread apart. Defaults to 12 when unison > 1.
sub01adds a sine one octave below at this level — chest-thumping bass. 0 (default) = off.
fm012-operator FM (ratio 1:1): one oscillator bends another for bell/electric-piano tones. 0 (default) = off.
hpHzswitches the main filter to high-pass at this cutoff (removes lows).
bpHzswitches the main filter to band-pass at this cutoff. Precedence: bp > hp > the low-pass cutoff.
hpfHzan extra series high-pass on top of the main filter (thin-out). 0 (default) = off.
wobbleHza filter-cutoff LFO at this rate (±50 % of cutoff) — the dubstep “wow-wow.” 0 (default) = off.
drive01tanh saturation — push it until it growls. 0 (default) = clean.
crushbits 116bit-crush to that many bits for a lo-fi, gritty sound. 0 (default) = off.
chorus01per-voice chorus (a modulated short delay) for a shimmery, wide feel. 0 (default) = off.
duck01sidechain: how much this voice ducks each time the row named kick fires (120 ms recovery). 0 (default) = off. See §10c.

Sample-voice only:

ParamFieldWhat it does
reverse0 or 11 plays the sample backwards; 0 (default) forwards.
start01start the sample at this fraction into the clip (0.25 = a quarter in).
speedrate (0.254)playback rate; faster repitches up, slower repitches down. 1 (default) = original.

Glide (portamento). With glide set above 0, a note slides up or down from the previous note on the same row (within the same section instance) to its own pitch. Rests between the two notes don’t break the chain — it still slides from the last note actually played. For chords it slides from (and to) the first pitch. The first note of a row, or of each section instance, has nothing to slide from, so it just plays. A negative glide is clamped to 0.

scale A minor
steps 16

inst bass = saw glide 0.08 cutoff 700 sustain 0.7
bass  1 _ _ _ 5 _ _ _ 4 _ _ _ 8 _ _ _

Each new note in that bass line slides into pitch — a classic 303-style portamento.

A fat sound stacks the synthesis params — here a seven-voice supersaw lead:

scale A minor
steps 16

inst lead = saw unison 7 detune 22 cutoff 2600 drive 0.2 reverb 0.3
lead  1 . 5 . 8 . 5 . 1 . 5 . 8 . 5 .

Error cases (each shown only as a counter-example, not parseable):

inst lead = saw cutoff            ← odd token count: "cutoff" has no value
inst lead = saw volume 2          ← unknown key "volume"
inst lead = saw cutoff loud       ← non-numeric value
inst lead = wobble                ← unknown wave

The delay and reverb params are only this voice’s send levels into the two shared mix buses. The buses themselves (echo time/feedback/wet, room size/damp/wet) are configured by top-level fx lines; with no fx line they keep their defaults (delay time 0.3 s, feedback 0.3, wet 0.5; reverb Schroeder stereo room 0.84, damp 0.2, wet 0.35).

8e. sample "pack" — sampled instruments

A quoted name turns sample into a multi-sample instrument loaded from a pack.track file (a recorded violin, drum kit, etc.) instead of a built-in registry clip:

steps 4
inst vln = sample "violin"
vln  A4 C5 E5 _

The bare form (sample 808kick, §8c) still reads the built-in registry. The quoted form names a pack the host supplies at render time; pack ids contain no spaces. When no matching pack is supplied, the instrument is silent (the registry path never engages for a quoted name).

Pack file format (pack.track). A pack is its own little file with one keyword per line:

sample violin                                            ← names the pack (first line)
art sustain                                              ← an articulation
  zone A2-G3  v0-63   "vln/A2_p.wav"  root A2  loop 12000 48000
  zone A2-G3  v64-127 "vln/A2_f.wav"  root A2
art pizz key C0                                          ← "key NOTE" makes it a keyswitch
  zone A2-G4  v0-127  "vln/A2_pizz.wav" root A2  rr 2
release "vln/release.wav"                                ← one-shot played on note-off
amp adsr 0.02 0.1 0.9 0.3                                ← amp envelope (a d s r, sustain 0-1)

9. use — kits and imports

9a. use kit NAME

Applies a bundle of instrument definitions in one line. Kit names are case-insensitive.

use kit synthwave
lead 1 . 3 . 5 . 3 . 1 . 3 . 5 . 3 .

Built-in kits:

KitSets up
synthwavelead=brightlead, bass=reese, pad=softpad
houselead=stab, bass=sub, pad=organ
chiplead=organ, bass=stab, pad=bell
808kick / snare / hat / clap → the matching 808 samples

Unknown kit name → error. Your own inst lines override kit instruments of the same name.

9b. use "module"

Imports the instruments and sections defined in another quoted module. In the app that’s one of your other tabs; some bundled example modules also exist.

use "synths"

Rules:


10. fx — mix buses

An fx line configures one of the song’s shared mix buses. Like header settings, fx lines are position-independent (they may appear anywhere) and the song has exactly one of each bus. Where inst delay/reverb params set each voice’s send level, fx lines set the bus itself — the echo’s timing and feedback, the reverb’s room size, the master glue.

fx <bus> [key value] [key value] ...

There are three buses:

10a. fx delay — the echo bus

KeyRangeDefaultMeaning
timeseconds > 00.3echo spacing in seconds
syncsteps ≥ 1echo spacing as a number of steps (sync × stepDuration); wins over time when set
feedback010.3how much the echo feeds back into itself (the tail length)
wet010.5echo loudness in the mix

sync makes the echo tempo-locked: fx delay sync 3 spaces echoes three steps apart, so the delay tracks the bpm. When both sync and time are present, sync wins. Voices feed this bus through their delay send param.

bpm 124
steps 16

fx delay sync 3 feedback 0.4 wet 0.4

inst lead = preset pluck delay 0.4
lead C4 . . . E4 . . . G4 . . . C5 . . .

10b. fx reverb — the room bus

KeyRangeDefaultMeaning
room010.84size of the space — bigger is a longer, washier tail
damp010.2high-frequency damping inside the room (higher = darker tail)
wet010.35reverb loudness in the mix

A Schroeder stereo reverb (mono-in, stereo-out). Voices feed it through their reverb send.

steps 16

fx reverb room 0.9 damp 0.3 wet 0.45

inst pad = preset softpad reverb 0.4
pad 1+3+5 . . . . . . . 4+6+8 . . . . . . .

10c. fx master compress — the glue + duck sidechain

KeyRangeDefaultMeaning
compress010 (off)master-bus compression amount — 0 bypasses (the no-fx path is byte-identical)

The master compressor is a feed-forward compressor on the final stereo sum, before the soft limiter: threshold = 1 − 0.6 × compress, ratio 4:1 above threshold (attack 5 ms, release 120 ms). Turn it up to glue a busy mix together; 0 leaves the signal untouched.

steps 16

fx master compress 0.4

kick x...x...x...x...

Duck (sidechain). The classic “pumping” effect is an inst param, not an fx line. Set duck (01) on any voice and it ducks each time the row named kick fires — a fixed, kid-simple convention (the duck source is always the kick row). The duck recovers over ~120 ms, so the voice dips on every kick and swells back between hits:

steps 16

inst pad = preset choir duck 0.7

kick x...x...x...x...
pad  1+3+5 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

The pad breathes with the kick — hard on the downbeats, full in between.


11. Default name→sound mapping

If a name has no inst and isn’t set by a kit/import, the engine picks a default based on the row’s kind. Precedence is always: explicit inst > kit / import > these defaults.

Drum rows (no notes/degrees/chords):

NameSound
kickkick drum
hat, hihat, hhhi-hat
clapclap
snareclap (snare-style)
anything elseclap

Pitched rows (contain a note, degree, or chord):

NameSound
basssaw bass (cutoff 600, sustain 0)
leadsaw lead (cutoff 1800, sustain 0.3, delay send 0.25)
padtriangle pad (cutoff 1200, sustain 0.6, delay send 0.2)
anything elsedefault saw synth (cutoff 1500)

12. Built-in presets

For inst NAME = preset P. Key parameter values are listed so you can predict what your overrides change.

PresetCharacterKey numbers
pluckshort bright saw plucksaw, cutoff 2500, res 4, attack 0.005, decay 0.12, sustain 0, release 0.1
reesefat saw bass, octave −1saw, cutoff 450, res 8, attack 0.01, decay 0.2, sustain 0.6, release 0.2, octave −1
subdeep sine sub, octave −1sine, cutoff 200, attack 0.005, decay 0.2, sustain 0.8, release 0.1, octave −1
stabsquare stabsquare, cutoff 1800, res 2, attack 0.005, decay 0.18, sustain 0, release 0.08
softpadmellow triangle padtriangle, cutoff 1200, attack 0.2, decay 0.3, sustain 0.7, release 0.6, delay 0.2
brightleadbright saw leadsaw, cutoff 3000, res 5, attack 0.01, decay 0.2, sustain 0.4, release 0.2, delay 0.25
bellsine bell, long decaysine, cutoff 4000, attack 0.005, decay 0.4, sustain 0, release 0.4
organsustained square organsquare, cutoff 2000, attack 0.01, decay 0.1, sustain 0.8, release 0.1
supersawhuge wide unison leadsaw, unison 5, detune 18, cutoff 2600, attack 0.01, decay 0.25, sustain 0.5, release 0.25
wobblebassdubstep wobble basssaw, octave −1, cutoff 900, res 6, wobble 4, drive 0.3, sustain 0.8
epianoFM electric pianosine, fm 0.35, cutoff 2200, attack 0.005, decay 0.35, sustain 0.3, release 0.3
choirwide chorused voicestriangle, unison 3, detune 10, chorus 0.6, attack 0.3, sustain 0.8, release 0.8, reverb 0.35
kick808 snare808 hat808 clap808the 808 samples as presetsthe matching 808 sample

13. Built-in samples

For inst NAME = sample S (and what the 808 kit uses):

808kick · 808snare · 808hat · 808clap

Unknown sample name → error.


14. seed & humanize

The render is deterministic: the same .track text with the same seed produces a byte-identical render, every time, on every machine.

humanize h makes everything the engine plays less robotic. With h > 0 each event gets four independent, seeded micro-variations — every one a function of seed, the event’s order index, its grid position, and its row index, so the same song always renders identically:

Each variation draws from its own seeded stream, so adding or removing notes on one row never reshuffles another row’s micro-variations. h = 0 (the default) leaves everything untouched and byte-identical. Live playback loops the arrangement; the offline/WAV render plays it once plus a short release tail. Both share the same seeded RNG, so the variation is consistent.

seed 42
humanize 0.6
steps 16
hat 16x

15. A complete example

This song exercises a broad cross-section of the language: all seven headers (including seed/humanize), a scale with degrees, chords, the o/x/X velocities, count tokens, an inst with pan/reverb/delay, a kit, a multi-bar row, sections with xN, and an arrange. (For the mix buses, fade, and duck, see §10.) Comments are on their own lines. It parses.

# ── Global settings ───────────────────────────────────────
bpm      124
beats    4
steps    16
swing    0.1
master   0.9
seed     7
humanize 0.4

# ── Key: degrees below resolve through this scale ──────────
scale A minor

# ── A kit for lead/bass/pad, plus one custom instrument ────
use kit synthwave
inst melo = preset pluck cutoff 2800 reverb 0.3 delay 0.2 pan -0.15

# ── Verse: four-on-the-floor, ghost/accent hats, degree bass
[verse x2]
kick  x...x...x...x...
hat   o.x.o.X.o.x.o.X.
clap  ....X.......X...
bass  1 . . . 1 . . . 5 . . . 5 . . .
melo  8 . . 7 . . 5 . . 7 . . 8 . . .

# ── Chorus: busier kick via counts, chords, multi-bar bass ─
[chorus x2]
kick  4x 4x 4x 4x
hat   16x
clap  ....X.......X...
lead  1+3+5 . . . 4+6+8 . . . 5+7+2 . . . 1+3+5 . . .
bass  1 . 1 . 5 . 5 . 6 . 6 . 4 . 4 . 1 . 1 . 5 . 5 . 3 . 3 . 7 . 7 .

# ── Play order (loops live; renders once to WAV) ───────────
arrange verse chorus verse chorus

16. Low-level primitives

Everything above is enough to hand-author a full song. This section documents six optional, low-level primitives meant for machine-generated or imported material (e.g. the MIDI converter): explicit velocities, exact Hz pitches, absolute-time event lists, and continuous parameter/pitch/tempo curves. They are entirely invisible to hand-authoring — if you never write one, nothing about the grid language changes, and a song with no lane block renders byte-identically to before.

Four of them (timeline, bend, auto, tempo) are indentation-delimited blocks: a keyword line at column 0, followed by body lines indented under it. A body line ends the block when the indentation returns to column 0. The other two (@N, Hz) are token-level additions that drop into any row.

16a. @N — numeric velocity

Any hit (o/x/X) or pitched atom in token form may carry a @N velocity suffix, where N is an integer 0127 (MIDI range). It overrides the mark’s default velocity, mapping to N / 127 internally. It composes with the roll/maybe suffixes on the same atom.

steps 16
lead C4@100 . . . E4@30 . . . G4@127 . . . C5@64 . . .

C4@200 (out of range) and C4@5.5 (non-integer) are errors.

16b. Hz — exact-frequency pitch

A pitch atom written as <float>Hz plays that exact frequency instead of a named note or degree. It resolves to fractional MIDI, so microtonal values are exact (440Hz is A4).

steps 4
lead 440Hz _ _ _

A non-positive value (0Hz) is an error.

16c. timeline NAME — absolute-time events

A timeline block schedules notes for the instrument NAME by absolute position rather than on the grid. Each body line is TIME NOTE DURATION, where TIME and DURATION are in beats by default or in seconds with an s suffix (the two may be mixed freely in one block). The note may carry a @N velocity. Timeline events render alongside the grid.

steps 4
lead . . . .
timeline lead
  0    C4@100  0.5
  1.0s G4@90   0.5

This pass wires timeline into the offline / WAV renderer; live-playback scheduling is a planned follow-on.

16d. bend TARGET + inline -> — pitch curves & glides

A bend block is a per-target semitone pitch curve applied to every voice on the row named TARGET. Each body line is BEAT SEMITONES; values are signed (+2, -1.5). Points ramp linearly by default; append : step for a jump-and-hold.

steps 16
lead C4 _ _ _ E4->G4 . . . . . . . . . . .
bend lead
  0    0
  1.0  +2 : step

The inline A->B token in that example is a glide: a single note that starts at A and slides to B across its gate (reusing the portamento path). bend is the continuous-curve form; -> is the per-note shorthand.

16e. auto NAME CC — parameter automation

An auto block is a continuous control curve for the row named TARGET. The opener is auto NAME CC, where CC is either a raw ccN (cc74) or one of the aliases below. Each body line is BEAT VALUE; points ramp by default, : step jumps.

AliasCCDrives audio?Effect
cutoff74yeslow-pass filter cutoff
pan10yesstereo position (range -1+1)
gain7yesvoice gain (range 01)
pressure— (channel aftertouch)yesamplitude / expression (range 01)
cc64 (sustain)64nostored / lane-only, silent this pass
raw ccNNnostored / lane-only, silent this pass

The four aliases with a known audio parameter drive the sound directly; raw ccN and cc64 are stored losslessly on the lane but produce no audio this pass (round-trip targets for the MIDI converter).

steps 16
lead C4 . . . E4 . . . G4 . . . C5 . . .
auto lead cutoff
  0  40
  4  120

16f. tempo — tempo map

A tempo block makes the tempo vary over the song. Each body line is BEAT BPM; segments hold the BPM constant by default, or append : ramp to glide linearly to the next point’s BPM. The beats↔time integration drives both the offline render and live transport; with no tempo block the song keeps its exact constant-tempo render path.

steps 16
kick x...x...x...x...
tempo
  0  120
  4  140 : ramp
  8  140

(Gate length under a tempo ramp uses the constant-step approximation this pass — see the project roadmap. Likewise, swing and sub-step roll/arp offsets are computed from the initial constant step duration, so they aren’t rescaled under a tempo change — a minor, accepted approximation, consistent between offline render and live transport.)

16g. MIDI import / export (a tool, not syntax)

The library ships a pair of pure converters between Standard MIDI Files and .track source text. They are library functions, not language keywords — nothing here adds grammar:

The pair is round-trip faithful: midiToTracktrackToMidi → re-parse preserves note pitches, timing, velocity, pitch bend, CC, and tempo within tolerance (exact byte-identity is not a goal). Both are pure — bytes/text in, text/bytes out, no I/O.


17. Projects & the Timeline

17.0 Projects

A project stacks several pieces of audio into one mix: note layers (layer NAME = "file.track", rendered like any standalone song) and clip layers (layer NAME = clip "file.wav", placed audio that is never time-stretched to fit the tempo). A project is saved as a .sb file. It is its own grammar — not a song — so you parse it with parseProject(text) and render it with renderProject(spec, resolve, opts); parseTrack does not apply. v1 limit: clip layers must be .wav (mp3 is not decoded by the exporter).

17.1 Placements

Each layer places its body on the timeline one or more times via at:

# A clip layer placed twice, then note layers with placements of their own.
layer vox  = clip "vox.wav"
  at 0
  at 16
layer beat = "beat.track"
  at 8
# No `at` anywhere → one placement at beat 0.
layer bass = "bass.track"

17.2 Per-placement options

An at line takes options, in any order:

optionmeaning
gain DBper-placement level in dB; stacks with the lane fader (bare number = dB)
trim A Bkeep only beats A..B of the clip
fade in|out Slinear fade in / out over S seconds (bare number or Ns)
looprepeat the trimmed body until the next placement (no-op without trim)

Note-layer placements accept only at and gaintrim/fade/loop are clip-only (a parse error on a note layer). pan is not a placement option — it lives in the mix block (inline gain/pan written on a layer line route to the lane, not the placement). A looped or long body that would overlap the next placement is cut at that placement.

# The clip placed three times: a 1-bar slice looping under the intro,
# then the full take (cut by the next placement), then a quieter tail that fades out.
layer vox = clip "vox.wav"
  at 0 trim 0 4 loop
  at 16
  at 24 gain -6 trim 0 4 fade out 1

17.3 mix and master limit

mix is an indented block of per-layer overrides: NAME gain DB pan P. gain is in decibelsgain 0 is unity, gain -6dB is ~half the amplitude; the dB suffix is optional (a bare number is read as dB, gain -2 == gain -2dB). pan is equal-powerpan 0 is centered, -1 hard left, 1 hard right. master limit DB sets the master-bus limiter ceiling (e.g. master limit -1dB), so a hot stack of layers won’t clip.

17.4 The visual timeline editor

Open a .sb file and run smallBANG: Open Studio. Lanes (one per layer, colored) hold blocks — one block per placement. Gestures:

gestureeffect
drag a block’s middlemove (snapped)
drag a block’s edgetrim
drag the right edge past the bodyloop
drag a corner dotfade in / out
Ctrl+D / Cmd+D, or Alt-dragduplicate
Delete / Backspaceremove
← / →nudge by one snap step (zoom-dependent)
click the rulerseek / play-from-here (Alt-click: unsnapped)

Playback is live: editing structure while it plays does a seamless swap, and changing a referenced .track/.wav triggers a live rebuild. Export Project WAV renders all layers mixed down. Play must be pressed inside the Studio panel — Web Audio needs an in-frame user gesture.

18. One-screen cheat sheet

# comment            whole line only — NO end-of-line comments

HEADERS (apply globally, last wins, value must be numeric):
  bpm 124   beats 4   steps 16   swing 0–1   master 0–1   seed 7   humanize 0–1
  stepDuration = (60/bpm) × beats / steps   ·   swing delays odd steps

scale ROOT FLAVOUR [baseOctave=3]    ROOT = A–G [#/b], no octave digit
  flavours: major minor dorian penta · degree d walks up the scale, wraps octaves
  e.g. scale A minor → 1=A3, 3=C4, 8=A4

ROWS  name pattern   (length must be a multiple of steps; longer = multi-bar)
  marks:  o ghost(.5)  x hit(.8)  X accent(1.0)  . rest(note-off)  _ hold(sustains the note)
  grid form:  one solid token of o x X . _   e.g.  kick x...x...x...x...
  token form: whitespace-split, one token/step:
    notes  C4 F#3 Eb2      (letter [#/b] 1-digit octave, A4=440Hz, range C-9..B9)
    degrees 1 2 3 …        (resolved via scale; default C major oct 3)
    chords  A3+C4+E4  1+3+5  (atoms joined by +, play together)
    counts  4x 7_ 3.       (N then a mark) — GOTCHA: 3. = 3 rests, 3 . = degree 3 + rest
                             counts do NOT apply to notes/degrees (4C4 is an error)
  pitched if any note/degree/chord, else drum (a noted "kick" won't use the kick drum)

  SUFFIXES (token form, order atom[*N][?P]; not on . _ or counts/grids):
    *N    roll — N quick sub-hits in the step (N 2–9)   e.g. x*3  C4*2
    ?     maybe — plays 50% of the time (seeded)         e.g. x?
    ?P    maybe — plays P% of the time (P 1–99)          e.g. x?75   combine: x*3?50
  GROUPS (plain atoms only — no nesting/suffixes/counts inside):
    <a b c>   cycle  — next element per BAR (global, wraps)   e.g. <1 5 6 4>
    [a|b|c]   choice — one element per event, seeded          e.g. [1|5|8]
  ROW TRANSFORMS (after the pattern, left→right):
    rev            reverse the steps
    fast N         out[i]=in[(i*N)%L], N≥2, length unchanged (input must already fit)
    slow N         stretch N×, holds between, length becomes L·N (multi-bar)
    arp up|down    chord step spells its pitches as quick sub-hits (asc/desc)
  ROW GENERATOR (replaces the pattern):
    name spread K [rotate R]   Euclidean: K hits over `steps` slots, rotate right R
                               K 1..steps; unpitched (vel 0.8); transforms may follow;
                               spread + a pattern on one row = error

SECTIONS  [name]  [name xN]  [name xN fade in|out]   names unique; rows before any header → "main"
            fade in|out scales each trigger's velocity 0→1 / 1→0 across the section instance
arrange a b a …               omit = file order; live loops, WAV plays once

inst NAME = WAVE        [k v …]   saw/sawtooth · square · sine · triangle/tri
inst NAME = preset P    [k v …]   start from a preset, optionally override
inst NAME = sample S    [k v …]   play a recorded sample
  params: cutoff resonance attack decay sustain release octave delay reverb pan glide vibrato duck
          unison detune sub fm hp bp hpf wobble drive crush chorus   (synth)
          reverse start speed                                        (sample voice only)
          (delay/reverb = bus sends 0–1; pan -1..+1; octave = whole-octave shift;
           glide = portamento seconds; vibrato 0–1 = 5.5 Hz pitch wobble (0.3 s fade-in);
           unison 1–7 + detune cents = fat stack;
           hp/bp switch the main filter, precedence bp>hp>lowpass; crush = bits 1–16;
           duck 0–1 = sidechain — ducks when the `kick` row fires, 120 ms recovery)

FX MIX BUSES (top-level, position-independent, last-wins per key; omitted keys keep defaults):
  fx delay  time S | sync N(steps; tempo-synced, wins over time) | feedback 0–1 | wet 0–1   (0.3/0.3/0.5)
  fx reverb room 0–1 | damp 0–1 | wet 0–1                                                     (0.84/0.2/0.35)
  fx master compress 0–1   (0 = off; threshold 1−0.6c, 4:1, before the soft limiter)

use kit NAME    synthwave · house · chip · 808
use "module"    import instruments + sections (local shadows import w/ notice)

presets: pluck reese sub stab softpad brightlead bell organ supersaw wobblebass epiano choir
         + kick808 snare808 hat808 clap808
samples: 808kick 808snare 808hat 808clap

PROJECTS (.sb — stacks layers into one mix; not a song; parseProject/renderProject; see §17):
  layer NAME = "file.track"        note layer
  layer NAME = clip "file.wav"     clip layer (wav only, v1)
  no `at` anywhere → one placement at beat 0; else one inline `at` OR an indented `at` block,
    never both · at TIME [gain DB] [trim A B] [fade in|out S] [loop]   TIME beats or `Ns` seconds
    notes: at + gain only (trim/fade/loop are clip-only) · pan lives in `mix`, not a placement
  mix / NAME gain DB pan P   ·   master limit DB      gain bare number = dB · pan -1..1

layer vox  = clip "vox.wav"
  at 0 trim 0 4 loop
  at 16
  at 24 gain -6 trim 0 4 fade out 1
# no `at` anywhere -> one placement at beat 0
layer bass = "bass.track"
mix
  vox  gain -6dB pan 0
  bass gain -2dB pan 0
master limit -1dB

LOW-LEVEL PRIMITIVES (optional, machine-targeted — see §16):
  C4@100   velocity 0–127        440Hz   exact-frequency pitch (fractional MIDI)
  timeline NAME / TIME NOTE DUR  (beats or `s`)   bend NAME / BEAT SEMITONES   A->B inline glide
  auto NAME CC / BEAT VALUE      (cutoff pan gain pressure drive audio; raw ccN & cc64 lane-only/silent)
  tempo / BEAT BPM [: ramp]      varying tempo (no block = constant, byte-identical)

NOT yet in the language: end-of-line comments, per-voice compress (only the master compressor
  exists), per-section fx, named duck sources (the duck always follows the `kick` row),
  multiple delay taps, look-ahead compression, live-playback `timeline` scheduling.