.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 kind | Looks like | Section |
|---|---|---|
| Comment | # ... | §2 |
| Header setting | bpm 120 | §3 |
| Scale | scale A minor | §4 |
| Row (pattern) | kick x...x...x...x... | §5 |
| Hold / rest (gate) | _ · . | §6 |
| Section header | [verse x2] · [outro x2 fade out] | §7 |
| Arrangement | arrange verse chorus | §7 |
| Instrument definition | inst lead = saw cutoff 1800 | §8 |
| Kit / module import | use 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:
- Header settings and
scaleapply globally no matter where they appear in the file. If a setting is repeated, the last occurrence wins. - Rows attach to the most recent
[section]header above them. Rows written before any section header land in an implicit section namedmain.
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.
| Keyword | Meaning | Default | Range |
|---|---|---|---|
bpm | Tempo, beats per minute. | 120 | any positive number |
beats | Beats per bar. | 4 | positive integer |
steps | Grid cells per bar (16 = sixteenth notes). Sets the required row length. | 16 | positive integer |
swing | Pushes off-beat (odd-indexed) steps later for a shuffle feel. | 0 | 0–1 |
master | Overall output gain, applied before the soft-limiter. | 1 | 0–1 (higher allowed, soft-limited) |
seed | Seeds the deterministic RNG used by humanize (see §14). | 1 | any number |
humanize | Amount 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). | 0 | 0–1 typical |
Notes:
- A non-numeric value is an error:
bpm fast→ “bpm” needs a number, got “fast”. - Timing math:
stepDuration = (60 / bpm) × beats / stepsseconds. With the defaults (bpm 120,beats 4,steps 16) one step is0.125s. swingdelays each odd-indexed step bystepDuration × swingseconds.swing 0.5is the maximum “triplet” feel; values near0.08–0.15are typical.masteris multiplied in, then a soft-limiter rounds off peaks, so values above1get louder but never hard-clip.
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]
ROOTis a note letterA–Gwith an optional#orb— no octave digit here.FLAVOURis one of the four below.baseOctaveis an optional integer; it defaults to3.- Malformed lines, a bad root, or an unknown flavour are errors. The last
scaleline wins. With noscaleline at all, the default is C major, octave 3.
| Flavour | Intervals (semitones from root) |
|---|---|
major | 0 2 4 5 7 9 11 |
minor | 0 2 3 5 7 8 10 |
dorian | 0 2 3 5 7 9 10 |
penta | 0 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):
| Degree | Note |
|---|---|
1 | A3 |
3 | C4 |
8 | A4 (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:
| Char | Step | Velocity |
|---|---|---|
o | ghost hit | 0.5 |
x | hit | 0.8 |
X | accent | 1.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:
| Token | Step | Velocity |
|---|---|---|
o x X | ghost hit / hit / accent | 0.5 / 0.8 / 1.0 |
. | rest | — |
_ | hold | — |
a note (C4, F#3, Eb2) | pitched hit | 0.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 once | 0.8 |
a count (4x, 7_, 3.) | the mark, repeated N times | as the mark |
Note names. A letter A–G (case-insensitive), an optional # (sharp) or b (flat),
then a single-digit octave that may be negative.
A4= 440 Hz; each octave number up doubles the frequency.- Valid:
C4,A1,F#3,Eb2,G#5,C-1. Enharmonics work (Db4==C#4). - The octave is exactly one digit (regex
-?\d), so the range isC-9…B9.C10is not a note token (it would be read as an unknown token → error).
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-degree3followed by one rest. The space changes the meaning entirely.Counts apply only to the five marks.
4C4is 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
kickwritten 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 aninstfor 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]:
| Suffix | Name | Meaning |
|---|---|---|
*N | roll / ratchet | the step fires N quick, evenly-spaced sub-hits inside its one step (same pitch and velocity). N is 2–9. |
? | maybe | the step plays with 50 % probability (seeded). |
?P | maybe N % | plays with P % probability; P is 1–99. |
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 token-form only. They do not work inside a grid string (
x*3...is one grid token and fails), and they do not attach to.(rest) or_(hold). - Count tokens cannot take a suffix — a count is repetition already, so
4x*3is an error. - A roll on a chord repeats the whole chord N times (all its pitches together). To make a
chord spell its pitches out one at a time, use
arp(§5h). *1,*10,?0, and?100are out of range and error.
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:
| Syntax | Name | Picks | When |
|---|---|---|---|
<a b c> | cycle | the next element, advancing once per bar (global bar index, wraps) | per bar |
[a|b|c] | choice | one 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:
- Group elements are plain atoms only: notes, degrees, chords,
o/x/X,.,_. - No nesting, no suffixes, no counts inside a group, and an empty group is an error.
- A group with one element is just that element.
choiceis seeded per event, on its own RNG channel — adding a[a|b]to one row never reshuffles another row’s choices (see §14).- The pipe
|is only meaningful inside[...].
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:
| Transform | Meaning | Effect on length |
|---|---|---|
rev | reverse the expanded step list | unchanged |
fast N | play the pattern N× within its span: out[i] = in[(i*N) mod L] | unchanged |
slow N | stretch N×: old step j lands at j·N, holds fill the gaps | becomes L·N (a multi-bar row) |
arp up / arp down | chord steps spell their pitches as quick sub-hits, ascending / descending, instead of all at once | unchanged |
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:
- Because the row-length rule applies after transforms, the input to
fastmust already be a multiple ofsteps(it keeps the same length);slow 2turns a 16-step row into a valid 32-step (two-bar) row. rev/fast/slowresolve in the parser (static).arpmarks the track and expands into sub-hits when the timeline is built, like a roll.- Compose freely:
... rev fast 2reverses, then doubles.
5i. Row generators — spread (Euclidean)
Instead of a pattern, a row can be a generator that fills the bar for you:
| Syntax | Meaning |
|---|---|
name spread K | place K hits spread as evenly as possible across the bar’s steps slots (Bjorklund / Euclidean) |
name spread K rotate R | the 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:
Kmust be1..steps.rotate Rmay be any integer (taken modsteps).spreademits plain, unpitched hits (velocity0.8) — this phase has no pitched spread.- Row transforms (§5h) may follow a generator
(
spread 7 rev). - A row may have a pattern or a
spreadgenerator, not both —spreadplus a pattern is an error.
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.
_(hold) extends the gate of the note that is currently sounding: the note keeps ringing at its sustain level through every consecutive_..(rest) closes the gate: the note enters itsreleaseand fades out..is a true note-off.- A fresh note (or hit) re-articulates — it closes the previous note’s gate and starts a new one.
So _ and . now sound different: _ sustains, . cuts. A bare note followed by
nothing special gets a one-step gate.
Gate length rules
| Situation | Gate |
|---|---|
Bare note, no trailing _ | 1 step |
Note followed by k consecutive _ | 1 + k steps (the holds extend it) |
| Next note / hit on the same row | closes the previous gate at that step |
. (rest) | closes the gate at that step |
| Section instance end | closes any still-open gate (gates don’t cross section loops) |
| Chord | all its pitches share one gate |
| Roll / arp sub-hits | each 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
samplevoice 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...
nameis\w+. Section names must be unique within a file (duplicate → error).xNis an optional positive integer (N ≥ 1) repeat count; omitted meansx1.- An optional
fade in/fade outsuffix (after thexN) ramps the section’s loudness — see below. - Rows after a section header belong to that section, until the next header.
- Rows before any
[section]go into an implicit section namedmain.
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
- A space-separated list of section names, in the order to play them.
- Repeating a name replays that section.
- If there is no
arrangeline, sections play in file order. - Every name must refer to a section that exists (locally or imported), else error.
- Live playback loops the whole arrangement; the offline/WAV renderer plays it once (plus a short release tail).
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] ...
NAMEis\w+. Defining the same name twice in one file is an error.- An explicit
instoverrides a sound of the same name from a kit or import. SOURCEis one of three forms: a waveform,preset P, orsample S.
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:
| Param | Default value |
|---|---|
cutoff | 1500 |
attack | 0.01 |
decay | 0.2 |
sustain | 0.2 |
release | 0.1 |
resonance | 1 (gentle, near-flat filter Q) |
octave | 0 (no shift) |
delay / reverb | 0 (no sends) |
pan | 0 (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.
| Param | Field | What it does |
|---|---|---|
cutoff | low-pass cutoff (Hz) | brightness — lower is darker |
resonance | filter Q | emphasis/“bite” at the cutoff (applies to whichever filter type is active) |
attack | seconds | fade-in time of the envelope |
decay | seconds | fall time after the attack peak |
sustain | 0–1 | held level after decay |
release | seconds | fade-out after the note ends |
octave | whole number | transpose by whole octaves (-1, 2, …) |
delay | 0–1 | send level into the shared delay (echo) bus |
reverb | 0–1 | send level into the shared reverb bus |
pan | -1…+1 | stereo position: -1 hard left, 0 center, +1 hard right |
glide | seconds (≥ 0) | portamento — slides the pitch from the row’s previous note into this one over min(glide, gate) seconds. 0 (default) = no slide. |
vibrato | 0–1 | pitch 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. |
unison | voices 1–7 (int) | stacks that many detuned copies of the oscillator for a wide, fat sound. 1 (default) = off. |
detune | cents | how far the unison copies spread apart. Defaults to 12 when unison > 1. |
sub | 0–1 | adds a sine one octave below at this level — chest-thumping bass. 0 (default) = off. |
fm | 0–1 | 2-operator FM (ratio 1:1): one oscillator bends another for bell/electric-piano tones. 0 (default) = off. |
hp | Hz | switches the main filter to high-pass at this cutoff (removes lows). |
bp | Hz | switches the main filter to band-pass at this cutoff. Precedence: bp > hp > the low-pass cutoff. |
hpf | Hz | an extra series high-pass on top of the main filter (thin-out). 0 (default) = off. |
wobble | Hz | a filter-cutoff LFO at this rate (±50 % of cutoff) — the dubstep “wow-wow.” 0 (default) = off. |
drive | 0–1 | tanh saturation — push it until it growls. 0 (default) = clean. |
crush | bits 1–16 | bit-crush to that many bits for a lo-fi, gritty sound. 0 (default) = off. |
chorus | 0–1 | per-voice chorus (a modulated short delay) for a shimmery, wide feel. 0 (default) = off. |
duck | 0–1 | sidechain: how much this voice ducks each time the row named kick fires (120 ms recovery). 0 (default) = off. See §10c. |
Sample-voice only:
| Param | Field | What it does |
|---|---|---|
reverse | 0 or 1 | 1 plays the sample backwards; 0 (default) forwards. |
start | 0–1 | start the sample at this fraction into the clip (0.25 = a quarter in). |
speed | rate (0.25–4) | 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)
sample NAME— the first line; names the pack.art NAME [key NOTE]— an articulation (a playing style).key NOTEmakes that note a keyswitch: playing it on the row switches the active articulation for the notes that follow, and the keyswitch note itself makes no sound. A pack with noartline gets one implicitdefaultarticulation.zone LOW-HIGH vLO-HI "file" root NOTE [loop START END] [rr N]— maps a note range (LOW-HIGH) and velocity range (vLO-HI, 0–127) to a recorded file.root NOTEis the pitch the file was recorded at; notes above the root play back faster (higher), below slower. A note picks the first zone whose note and velocity ranges contain it — that’s how velocity layers work (v0-63soft,v64-127loud).loop START ENDsustains a held note by looping the sample frames betweenSTARTandEND(in source samples) until note-off.rr Nis round-robin: repeated hits rotate throughNtakes of the same zone so they don’t sound machine-identical. The takes follow a filename convention: thezoneline names take 1 (k.wav); takes 2…N arek.2.wav,k.3.wav, … (the number is inserted before the extension). On azonelineloopmust come beforerr.
release "file"— an optional one-shot played at the note’s pitch when the note is released.amp adsr A D S R— the amplitude envelope (attack/decay/release in seconds, sustain0–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:
| Kit | Sets up |
|---|---|
synthwave | lead=brightlead, bass=reese, pad=softpad |
house | lead=stab, bass=sub, pad=organ |
chip | lead=organ, bass=stab, pad=bell |
808 | kick / 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:
- Local definitions win over imported ones; when a local definition shadows an import, a calm notice reports it (this is not an error).
- A module may define only instruments (no playable sections) and still import fine.
- Imports can nest. A circular import is an error; nesting deeper than 16 levels is an error; an unknown module is an error.
- After all imports merge, the top-level song must end up with at least one row.
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] ...
<bus>is one ofdelay,reverb,master. Unknown bus → friendly error.- Then zero or more numeric
key valuepairs (same shape asinstparams). An odd token count, an unknown key for that bus, or a non-numeric value is an error. - Omitted keys keep their defaults. Multiple
fxlines for the same bus deep-merge, last-wins per key:fx delay time 0.4thenfx delay wet 0.2keeps both; a secondfx delay time 0.6overrides onlytime.
There are three buses:
10a. fx delay — the echo bus
| Key | Range | Default | Meaning |
|---|---|---|---|
time | seconds > 0 | 0.3 | echo spacing in seconds |
sync | steps ≥ 1 | — | echo spacing as a number of steps (sync × stepDuration); wins over time when set |
feedback | 0–1 | 0.3 | how much the echo feeds back into itself (the tail length) |
wet | 0–1 | 0.5 | echo 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
| Key | Range | Default | Meaning |
|---|---|---|---|
room | 0–1 | 0.84 | size of the space — bigger is a longer, washier tail |
damp | 0–1 | 0.2 | high-frequency damping inside the room (higher = darker tail) |
wet | 0–1 | 0.35 | reverb 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
| Key | Range | Default | Meaning |
|---|---|---|---|
compress | 0–1 | 0 (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 (0–1) 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):
| Name | Sound |
|---|---|
kick | kick drum |
hat, hihat, hh | hi-hat |
clap | clap |
snare | clap (snare-style) |
| anything else | clap |
Pitched rows (contain a note, degree, or chord):
| Name | Sound |
|---|---|
bass | saw bass (cutoff 600, sustain 0) |
lead | saw lead (cutoff 1800, sustain 0.3, delay send 0.25) |
pad | triangle pad (cutoff 1200, sustain 0.6, delay send 0.2) |
| anything else | default 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.
| Preset | Character | Key numbers |
|---|---|---|
pluck | short bright saw pluck | saw, cutoff 2500, res 4, attack 0.005, decay 0.12, sustain 0, release 0.1 |
reese | fat saw bass, octave −1 | saw, cutoff 450, res 8, attack 0.01, decay 0.2, sustain 0.6, release 0.2, octave −1 |
sub | deep sine sub, octave −1 | sine, cutoff 200, attack 0.005, decay 0.2, sustain 0.8, release 0.1, octave −1 |
stab | square stab | square, cutoff 1800, res 2, attack 0.005, decay 0.18, sustain 0, release 0.08 |
softpad | mellow triangle pad | triangle, cutoff 1200, attack 0.2, decay 0.3, sustain 0.7, release 0.6, delay 0.2 |
brightlead | bright saw lead | saw, cutoff 3000, res 5, attack 0.01, decay 0.2, sustain 0.4, release 0.2, delay 0.25 |
bell | sine bell, long decay | sine, cutoff 4000, attack 0.005, decay 0.4, sustain 0, release 0.4 |
organ | sustained square organ | square, cutoff 2000, attack 0.01, decay 0.1, sustain 0.8, release 0.1 |
supersaw | huge wide unison lead | saw, unison 5, detune 18, cutoff 2600, attack 0.01, decay 0.25, sustain 0.5, release 0.25 |
wobblebass | dubstep wobble bass | saw, octave −1, cutoff 900, res 6, wobble 4, drive 0.3, sustain 0.8 |
epiano | FM electric piano | sine, fm 0.35, cutoff 2200, attack 0.005, decay 0.35, sustain 0.3, release 0.3 |
choir | wide chorused voices | triangle, unison 3, detune 10, chorus 0.6, attack 0.3, sustain 0.8, release 0.8, reverb 0.35 |
kick808 snare808 hat808 clap808 | the 808 samples as presets | the 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:
- Loudness: velocity is scaled by
1 + 0.15 × h × r(r ∈ [−1, 1)). Unchanged from before — old songs keep their exact loudness pattern. - Timing (lay-back): every hit/note is pushed later by up to
0.015 × hseconds (15 ms ath = 1). Lay-back only — never early — so the offline render and the live worklet stay sample-identical. Roll/arp sub-hits share their parent’s offset. - Pitch (notes only): the playback frequency is nudged by
±3 × hcents, so no two notes land on exactly the same pitch. - Brightness (notes only): the main filter cutoff is scaled by
1 ± 0.06 × h, so each note is a touch brighter or darker.
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 0–127 (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.
| Alias | CC | Drives audio? | Effect |
|---|---|---|---|
cutoff | 74 | yes | low-pass filter cutoff |
pan | 10 | yes | stereo position (range -1…+1) |
gain | 7 | yes | voice gain (range 0–1) |
pressure | — (channel aftertouch) | yes | amplitude / expression (range 0–1) |
cc64 (sustain) | 64 | no | stored / lane-only, silent this pass |
raw ccN | N | no | stored / 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:
midiToTrack(bytes) → textparses an SMF (Uint8Array/ArrayBuffer) and emits.tracksource, using the low-level primitives above: on-grid monophonic lines become grid rows with@Nvelocities, off-grid or polyphonic parts becometimelineblocks, pitch bend →bend, controllers →auto ccN, and a tempo change → atempoblock.trackToMidi(text) → bytesis the reverse: it parses the song, expands the timeline + lanes, and writes SMF bytes.
The pair is round-trip faithful: midiToTrack → trackToMidi → 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"
- No
atanywhere → one placement at beat 0. - Each indented
at TIMEline adds another placement. at TIMEis beats by default (at 8) or seconds with anssuffix (at 1.5s).- A layer uses either one inline
aton thelayerline or an indentedatblock — never both (using both is a parse error).
17.2 Per-placement options
An at line takes options, in any order:
| option | meaning |
|---|---|
| gain DB | per-placement level in dB; stacks with the lane fader (bare number = dB) |
| trim A B | keep only beats A..B of the clip |
| fade in|out S | linear fade in / out over S seconds (bare number or Ns) |
| loop | repeat the trimmed body until the next placement (no-op without trim) |
Note-layer placements accept only at and gain — trim/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
decibels — gain 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-power — pan 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:
| gesture | effect |
|---|---|
| drag a block’s middle | move (snapped) |
| drag a block’s edge | trim |
| drag the right edge past the body | loop |
| drag a corner dot | fade in / out |
| Ctrl+D / Cmd+D, or Alt-drag | duplicate |
| Delete / Backspace | remove |
| ← / → | nudge by one snap step (zoom-dependent) |
| click the ruler | seek / 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.