Describe what happens when with notes, samples, rests, subdivisions and transformations of a repeating pattern.
Hear it and change it#
Square brackets put a smaller pattern inside one slot. In "bd [hh hh] sn hh", the two hats inside brackets share the second beat. They do not make the bar longer. hh*8 is a convenient way to play eight hats across a cycle.
Listen for: the quick pair of hats near the end of the bar.
tempo = 108bpm
timingVersion = 2
sound "bd ~ sn ~" |> gain 0.5
sound "hh hh hh [hh hh]" |> gain 0.25
Make it yours: change the last group to [hh hh hh] for three evenly spaced hits in the same amount of time. Nested groups can go deeper, but a small number of clear rhythmic ideas often makes a stronger groove.
What it is#
The pattern language is how Volta describes what happens when. A pattern literal is a mini-notation string (!"…") that the compiler parses into a list of timed events spanning one or more cycles. Tokens are pitches (c4, eb3:maj7, ^2, vi), numbers (0.3 1 1), or sample names (bd sn hh); the parser sniffs the payload type from the tokens. A line of bare names that are neither pitches nor numbers is a sample pattern: under sound an unknown bank warns UnknownSample, and on a synth track (play kick !"x x") the names warn SampleNamesOnSynth, because a synth plays only pitches and numbers (Doctor: trace a silent signal). Mini-notation supplies the rhythmic grammar — nesting, alternation, polyrhythm, speed/slow, replication, weighting, and Euclidean rhythms — and a set of named combinators (rev, every, degrade, jux, off, superimpose, …) reshape an already-parsed pattern. Continuous signal sources (sine, rand, …) can be sampled once per event via sampleAdd to bend pitch or value across a cycle.
Surface#
Mini-notation operators (inside a !"…" literal):
| Syntax | Meaning |
|---|---|
a b c | flat sequence — events split the cycle evenly |
~ . | rest (silent slot) |
[a b] | nesting — the group subdivides its slot |
<a b> | alternation — one child per cycle; a child advances only when picked (<a <b c>> = a b a c, Tidal's slowcat) |
<a@2 b> | weighted alternation — a holds 2 cycles, then b (period 3; Strudel's [a@2 b]/3; weights must sum to whole cycles) |
a , b | polyrhythm — comma-separated layers overlaid on the same span |
a*2 | fast — play a twice within its slot |
a/2 | slow — stretch a across 2 cycles |
a*<2 4> | patterned factor — one value per cycle (Pattern Applicative §4) |
a!3 | replication — repeat a three times (bare ! repeats previous) |
a@3 | weight — a takes 3 shares of its sequence's width |
a(3,8) | Euclidean — 3 onsets spread across 8 slots (x..x..x.) |
a(3,8,2) | Euclidean rotated — same, rotated 2 slots LEFT (.x..x.x.) |
a(<3 5>,8,<0 2>) | Euclid args may alternate per cycle |
0 .. 7 | inclusive integer range sugar (also 0..7; descending counts down) |
hh? | per-event coin — kept with p 0.5, fresh each cycle (bd:? stays the variant sigil) |
[a | b] | per-cycle random choice among alternatives |
a=0.7 | inline per-event velocity factor (see Velocity and accents) |
bd:3 | sample variant index (see Choose sample variants) |
Combinators are Volta expressions. Put the subject before the pipe; omit it inside a transform slot (see Read and write pattern pipes):
arpFig = !"c4 [eb4 g4] <bb4 c5> [g4 eb4]" |> sampleAdd sine [-2, 2]
arpPat = arpFig |> every 3 rev |> jux (off 0.25 rev)
hatPat = !"<c6(7,16) c6(9,16)>" |> jux rev
Direct calls such as every p 3 rev and builtin aliases follow Read and write pattern pipes; ambiguous positional calls keep legacy meaning. Named sections (up = transpose 12) and lambdas (rush = \p -> p |> fast 2) also work in transform slots: p |> every 4 up, or directly as p |> rush. Forward and imported callable aliases use their defining scope. Naming a transform preserves inline timing and random draws; a captured named pattern keeps its declaration's baked events. Calling and capture limits are in Read and write pattern pipes.
p |> rev— reverse each cycle.p |> every n f— everyn-th cycle comes fromf pinstead ofp.fis applied to the WHOLE pattern, so a transform that changes the length keeps it:every 1 (slow 2)is exactlyslow 2, andevery 2 (iter 4)rotates cycle 2 by half a cycle.p |> degrade— drop ~50% of (non-rest) events, fresh each cycle.p |> superimpose f— overlaypwithf p.p |> off t f— overlaypwithf pshifted later by cycle-fractiont.p |> jux f—ppanned hard left,f ppanned hard right.p |> sampleAdd src [lo,hi]— sample signalsrcat each event's onset, map[-1,1]→[lo,hi], and add it to the event's pitch (semitones) or value.
Signal sources usable as src for sampleAdd: sine, saw, tri, isaw, square, rand, perlin. (sine/saw/square also name oscillators; the nullary signal-source reading is selected by context.)
Timing versions#
Composable timing (version 2) is the default. No version declaration is needed:
phrase = !"c3 e3" |> fast 2
main = play "saw" (arrange [[1, phrase], [1, !"g3"]])
The first cell plays four notes at cycle positions 0, 1/4, 1/2, 3/4; the second starts at cycle 1. Extracting phrase into a binding preserves that rhythm. The setting applies to the entry's imported patterns too; a library's setting cannot change its caller's version. An unversioned library inherits the entry's version. A library that explicitly declares a version requires the entry to use that version; incompatible imports produce a diagnostic instead of silently reinterpreting their patterns. A selective import of timingVersion explicitly adopts the library's setting.
timingVersion = 2 remains accepted, but is optional. File → New Song and generated Songcraft songs omit it.
For an older composition that needs legacy timing, add timingVersion = 1. That preserves the old rate-discard behavior and ArrangeCellScale warnings. Previously unversioned files now use version 2, which can change their rhythm or loop representation. Bundled songs whose compiled musical data changed are explicitly pinned to version 1; other bundled songs use the default.
volta fmt --timing-version 1|2 --check song.volta previews a version edit; remove --check to apply. It compares against the file's current timing (the default is now 2), and refuses changed or unprovable IR even with --force. It cannot recover an old unversioned file's legacy intent; add timingVersion = 1 yourself when that is the intended behavior. See Format and modernize source.
| Version-2 operation | Timing rule |
|---|---|
p |> fast n | Divide onsets, durations and repeat period by n. |
p |> slow n | Multiply those times by n, retaining later notes; equivalent to fast (1/n). |
stack, par, voiceLines, merges | Materialize each child's rate before combining schedules over their common period. |
arrange [[n, p], …] | Give each cell n whole cycles; start p at zero, repeat it, and admit onsets before the cell's end. Rates survive; weights do not rescale p. |
seq [p, …] | Explicitly fit each child's complete materialized period into an equal slot of one cycle. seq [p] fits it into one cycle. |
There is no separate fit builtin. seq supplies that deliberate rescaling. Arrangement cells and sequence slots keep selected notes' full durations, so tails can ring into the next cell. Rests occupy time. An onset exactly at a cell's end is excluded from that cell.
In the IR a whole arrange is one track cycle of Σ n bars: cell i occupies the phase window [Σ_{j<i} n_j / N, Σ_{j≤i} n_j / N) of that cycle (N = Σ n) and its pattern repeats n_i times inside it (compiler/src/lower/pattern.rs). A weight therefore buys a longer region, not a stretched phrase: for a bend or a held note to span both bars of a two-bar cell, the phrase itself has to be two bars long — written that way, or stretched with [2, p |> slow 2]. The cell also starts p at its own first bar, not on the song's bar grid. An emitted event's duration is a fraction of that whole cycle, so its length in seconds is duration × cycle_seconds.
Positive rational factors support denominators up to 64, including 1.5, 0.75 and 4/3. For example, a one-cycle phrase at fast 1.5 materializes three copies over two cycles. Materialization copies event metadata and already-seeded choices; it does not draw randomness again. Later random transforms still operate on the events their input supplies. The engine also seeds voices by cycle/event coordinates: a different loop representation can change oscillator phase even when onsets agree. The migration proof therefore requires matching IR, not just matching schedules.
A varying factor selects onsets from constant-factor schedules inside the argument's hold windows, over their common period; it is not a tempo curve. Argument rests hold the previous value, all rests mean identity, and selected durations cross window boundaries. Version 2 rejects non-rational factors instead of snapping them to eighths. Materializations and overlays check the 65,536 ceiling before expansion — in events, and in cycles for the common period. Contract and compatibility evidence: Pattern Timing.
Placing a pattern in a song#
A pattern can be placed on the song's bar axis by a region rather than left to loop from the transport start (Write a song as a timeline):
bassIdea = !"<c2 ~ g2 ~>"
bassT = play bass [region 8bars 24bars bassIdea]
Placement does not touch timing. The pattern keeps its own period and repeats inside the container: a two-bar idea in a twenty-four-bar region plays twelve times and is not stretched into a twenty-four-bar phrase. Resizing the container loops more or less of it; rate on the region is the one explicit stretch, and rate = (4/3) stays an exact rational so a three-beat idea beside a four-beat one never drifts.
The pass a region repeats is the pattern's cycle_span — the number of base cycles one pass of its events covers, the same number a <…> or / pattern already reports. A region's offset is measured in those content units (offset = 2c enters the source two base cycles in), and repeat = [start, length] loops a chosen window of them.
What still materializes. Regions made placement lazy, not the pattern operators. Everything in the table above is unchanged inside one placed expression: stack, a varying factor, and an overlay still expand onto their common period under the 65,536-event budget before the region ever sees them. What costs nothing is independence across regions and tracks — each keeps its own cursor, and clock (p/q) aligns by proof — so a three-beat ostinato beside a four-beat loop never builds a twelve-beat pattern.
How it's implemented#
- Parser:
compiler/src/mini.rs. A lexer + recursive-descent parser builds aSteptree (Atom/Rest/Seq/Stack/Alt/Fast/Slow/Euclid),detect_payloadclassifies the tokens (Pitch/Number/Sample),validatechecks them, andrenderwalks the tree one cycle at a time intoMiniEvents (onset,duration,pitch_midi/value/sample,pan,velocity,variant,sample_region). A pattern that uses<…>or/spans multiple real cycles;cycle_spancomputes that count (LCM-based) and the whole span is compressed into[0,1)withMiniPattern::cyclesrecording how many cycles it represents.euclid_pulsesdistributes Euclidean onsets with Bjorklund's algorithm exactly as Tidal (Sound.Tidal.Bjorklund) and Strudel run it, so the first pulse is on step 0:(3,8)isx..x..x.,(5,8)x.xx.xx.,(2,5)x.x.., and then − 1cases are Tidal's own(3,4)=xxx.. The rotation turns LEFT, as Tidal'srotL((3,8,2)=.x..x.x.; Strudel'seuclidRotturns the other way), and wraps modulon.k = 0is silent, andk ≥ nfills every slot (Tidal would lengthen the bar toksteps). (euclidOnsetsinruntime/src/learn/checkers.tsmirrors it.) Chord/degree/roman tokens (c4:maj7,^2,vi) resolve to stacked pitch events here — see Chords, scales and harmony. - Combinator lowering:
compiler/src/lower/combinator.rs. Each combinator'sBuiltinKind(fromcompiler/src/builtins.rs) maps to apat_*function:pat_rev,pat_every,pat_degrade(degrade_keepis the seeded keep test),pat_superimpose,pat_off,pat_jux,pat_sample_add. Transform arguments are resolved byresolve_transform; overlays are combined bycompiler/src/lower/pattern.rs'smerge_patterns(re-times all layers onto the LCM of their cycle spans viaexpand_events).juxsets each event'spanto ∓1.sampleAdduses the closed-formshape_sample(deterministic hash forrand/perlin). A combinator returns a(MiniPattern, mult)pair;multstretches the track'scycle_beatsso a multi-cycle result plays at the right rate. Version 2 normalizes this pair inlower/timing.rsat pattern and transform boundaries; the pair's physical period is1 / mult. - Whole-period contract: a pattern reaches a combinator already flattened —
minirenders allcycle_spancycles into[0,1)andfast 2halves the span — so event counts throughfastprove nothing; tests userevto show a combinator carries a field. A span-widening combinator returnsmult = 1/total(Track.cycle_span = 1.0 / rate_mult), not its operand's mult. A function port runsfon the whole subject (combinator::transform_whole), never a one-cycle slice, which the lawevery 1 f == fchecks. An operatorguards::transparentadmits on a guarded subject mustGuard::reindexeach copy it makes, and a test that reads a random track's events bakes it first (ir::guard::bake_track). - IR:
ir/src/ir.rs. A lowered pattern becomes aTrackwithevents: Vec<TrackEvent>(onset/duration as[0,1)cycle fractions, pluspitch_midi/value/sample/variant/sample_region/pan/velocity) andcycle_beats/cycle_seconds. - Random guards:
compiler/src/lower/guards.rsdecides where a draw stays late-bound;engine/src/lib.rs::cycle_firing_orderskips an event whose guards fail, andVoiceCoordskeys its per-voice draws as the unrolled list did. - Engine:
engine/src/lib.rs::schedule_trackwalks each track's cursor, convertingonset * cycle_beatsto a beat via the tempo clock and starting a voice at its exact sample offset;event_panmakes per-eventpanthe fallback when no track pan is set, andevent_dynamicsmultiplies per-eventvelocityinto the voice gain.
Notes & limits#
degradeandsampleAdd'srand/perlinare deterministic: dropout and noise are pure hashes of (cycle, event index) / phase, so playback is reproducible. Rests are always kept.a*2/a/2and Euclidean counts round to integers in the renderer.sampleAddadds to the event payload (pitch in semitones, or numeric value); it does not create a continuously-varying control signal — the signal is sampled once, at the event's onset phase.juxwrites hard L/R pan; a track-levelpanTrackoverrides it (seeir.rs's pan-composition note andengine/src/lib.rs::event_pan).- Mixing pitch and sample tokens in one literal is rejected at parse time.
sometimesBy x f(and its sugarsometimes/often/rarely) reads the same per-event coin asdegradeBy x, so each cycle it transforms exactly the eventsdegradeBy xwould drop.ply nrepeats each event inside its own span and leaves rests alone. Both use the subject-first pipe rule in Language reference §9.chunk n fanditer nuse Tidal'scattiming on a multi-cycle input: output cyclecreads input cycle⌊c/n⌋with slot (or shift)c mod n, so the result spansn × pccycles and never stacks copies of the input.itershifts EARLY, as Tidal's and Strudel'siterdo:iter 4overa b c dplaysa b c d,b c d a,c d a b,d a b c.iterBack nshifts LATE (Tidal'siter', Strudel'siterBack):d a b cfollowsa b c d.everyreads the absolute cycle instead.chunk's slot comes from cycle⌊c/n⌋off p—fsees the whole pattern — and the result spansn × lcm(pc, period of f p).- A function in a transform port sees the whole pattern.
every,firstOf,lastOf,someCyclesBy,chunk,within,superimpose,off,juxandsometimesByall applyfto the complete subject, as it would be applied outside the port, soevery 1 fisffor everyf.every n f pplays cyclekoff pwhenk mod nselects it and cyclekofpotherwise (Tidal'swhenT); its period is the least common multiple ofn,p's period andf p's.someCyclesBy x fis the same with a whole-cycle coin in place ofk mod n. linger nrepeats the first1/nof EACH cycle, so on a multi-cycle pattern every cycle lingers on its own opening.- For the larger Tidal-parity combinator family (
iter,iterBack,linger,chunk,palindrome,bite,wchoose,humanize,degradeBy,mask,struct,ply,sometimesBy) and pattern constructors (patternOf,pure,silence,stack), see their own arms incompiler/src/builtins.rsandcompiler/src/lower/combinator.rs.
Patterned arguments (the applicative seam)#
Shipped with Pattern Applicative Stages 0–6 (2026-08-21). Every numeric combinator argument also takes a numeric pattern, and the two argument templates are Tidal's innerJoin rendered eagerly:
- Re-timing combinators (
fast,slow,every,off,iter,iterBack,linger,chunk) — three-pass specialize-and-restrict (lower/combinator.rs::specialize_by_arg): hold windows from the argument pattern (sample_step's tail-wrap rule), one render per distinct value, the true span = the LCM of the base and every specialization's span, then tile-and-restrict by onset, retaining complete durations across boundaries.every !"<2 3>" revspanslcm(2,2,3) = 6and transforms cycleciffc mod n_c == 0on the absolute cycle. - Value combinators (
degradeBy,sometimesBy,transpose,humanize,chop,ply) — expand-then-apply (sample_arg_per_event): both patterns expand to the LCM first, so index-keyed coins vary per repetition and thedegradeBy/sometimesBycomplementarity holds per event.bite's patterned index squeezes its slice into each index event's span. - In legacy timing,
fast !"2"normalizes tofast 2exactly (mult included); a varying factor always takes the structural reading (advisory at top level, where the constant spelling scales the bar); a pitch/sample argument pattern is a hard error;fast 1.5widens the period to 2 cycles (rational recovery, denom ≤ 64) instead of silently rounding. Version 2 uses the consistent rate/window contract above in every context. - Signal-valued arguments:
degradeBy/sometimesByaccept an inline cycle-rate LFO (sometimesBy (lfoSine (8c)) f) sampled closed-form per event; Hz rates are rejected (commensurability); re-timing combinators defer tosegment n sig. Budget: every span-widening fold shares ONE ceiling of 65 536 — one constant (
compiler/src/semantics/temporal.rs) read by the eager folds, the timing stage, the chooser's common-span search, authored pattern data and the bounded query plan alike.every 100000errors instead of materializing.The refusal is one sentence, and it names the operator you wrote, the count, the unit and the fix:
stack would materialize 68068 events (budget 65536) - an LCM blow-up; simplify one operand's cycle spanA
stackof coprime voices over the ceiling in EVENTS and the samestackover the ceiling in CYCLES differ only in that count and that unit. Where the extent is knowable from the source alone — astackorvoiceLinesover pattern literals — it is refused at check time, on the operand's own span, identically whether the expression is written inline, bound, imported, returned from a function, in a record field or in a list; otherwise the first fold that would allocate refuses it in the same words.
Fresh-per-cycle randomness#
Every random operator draws fresh each cycle, as in Tidal: mini ? and [a | b], someCyclesBy x f, shuffle n, scramble n, and the per-event coins of degrade/degradeBy/undegradeBy/sometimesBy — each a salted hash of (cycle, site), deterministic and seekable. degradeBy x and sometimesBy x f read the same coin, so they split each cycle's events exactly. A top-level randomCycles = N (1–64) makes every draw repeat every N cycles; randomCycles = 1 draws the same events every cycle — the stable, fixed-lattice groove. Unbounded per-cycle choice is also wchoose's runtime job.
The draws are late-bound. In a track's note pattern each governed event carries a guard (TrackEvent::guards) naming its draw and key; the scheduler and the note stream behind MIDI export, DAWproject and pattern_events evaluate it per cycle with the one implementation in ir/src/guard.rs, so the draws never repeat and a ? pattern stores one period. Guards pass only through rev, constant fast/slow, transpose, ply, every/firstOf/lastOf, superimpose, jux, off, stack and the random operators. Anywhere else — lanes, pattern arguments, arrange, chooseBy, humanize, the degradeBy family's own input, events p — the draws are unrolled: ?/|/whole-cycle draws over randomCycles cycles (8 by default), the per-event coins over randomCycles if written, else the same events every cycle. swing, nudge and once unroll a ?/|/whole-cycle track too, since they act on the track's cycle.
Stage 6 backfill#
zoom lo hi, within lo hi f, inside/outside n f, stut n fb t, press/pressBy x, hurry n (fast + per-event speed control), run n, firstOf/lastOf n f, segment n sig, rolled, brak (every second cycle squashed into its middle half), wedge t p1 p2 (proportion split — one cycle of each side squeezed into [0, t) / [t, 1)).
Structural (boolean) patterns#
The positional rest law: a rest means hold the previous value in VALUED positions (merge value side, argument slots — sample_step's tail-wrap rule) and *false* in STRUCTURAL positions. A structural position also reads Tidal's boolean spellings: t, true, x, 1 (any sounding value) are true; f, false and a numeric 0 are false, exactly as ~ is (compiler/src/mini.rs::MiniEvent::boolean_step). mask bp and struct bp always read their argument that way (window = the event's duration). At each true window struct looks up the value pattern's latest onset at or before it and stamps every event sharing that onset, one copy per member with its own velocity and suffixes, so a chord lane restructured by a boolean stays a chord (compiler/src/lower/combinator.rs::pat_struct). The completions join them:
| Builtin | Meaning |
|---|---|
p |> squeeze bp | each true window receives one full cycle of p, time-squeezed into it (the struct-flavored squeeze alignment; a multi-cycle p walks its cycles across windows) |
bp |> inv | complement the window set — true ↔ gap or false step (fabricated events carry value 1, no source atom) |
sew bp p1 p2 | hear p1 in true windows, p2 in false ones; each keeps its own rhythm (mask bp p1 ∪ mask (inv bp) p2) |
stitch bp p1 p2 | the bool pattern's rhythm: true windows sample p1, false windows p2 (struct lookup rule on both sides) |
Because the encoding is structural, boolean algebra falls out of ordinary combinators: gate \|> degradeBy .3 is a random selector, x(3,8) a Euclidean one, every 4 rev gate a cycling one. sew/stitch/wedge require both value patterns to share a payload (pitch/number/sample) and error otherwise. fit is closed as subsumed: patterned bite n i covers index-fitting and squeeze covers whole-cycle fitting.
Spans LCM-expand#
Every one of these combinators expands both sides to the LCM of their cycle spans before doing anything, exactly as the applicative seam does for a patterned argument — so a boolean longer than its input, or an input longer than its boolean, advances rather than freezing on cycle 0:
!"c5*16" |> mask !"<t ~>" -- 2-cycle result: hats, then a bar off
!"<c4 e4 g4 b4>" |> struct !"t ~ t ~" -- 4-cycle result, two stabs per bar
sew !"<t [t ~]>" a b -- 2-cycle result: the alternation advances
The result carries the widened span (cycle_span in the IR) and its rate multiplier is re-based onto it, like every span-widening combinator. An LCM blow-up is a compile error naming the combinator, not a hang.
(Before 2026-09-01 the longer side silently collapsed to its first cycle. A spelling written to work around that — a literal <[…] […] […] […]> tile — still means what it says.)
Tidal's order, booleans first#
struct, mask and squeeze take the booleans first and the pattern last, as Tidal writes them: struct "t ~ t t ~" p. The pipe is the canonical spelling and reads the other way round, p |> struct "t ~ t t ~"; both are one call. The subject-first prefix (struct "c3 e3" "t ~ t t ~") would make the booleans the pattern, so the compiler refuses it — at check time, naming the pipe spelling — whenever the booleans are all t/f/x/true/false/ 1/0 steps and the other argument holds notes, sample names or other numbers. Through bindings (struct mel bools) the lowerer refuses the same swap from the resolved events. sew bp p1 p2 and stitch bp p1 p2 put the booleans first too.
0 and f are false#
A boolean slot reads a pattern as Tidal and Strudel do: mask !"<0!4 1!12>" is a four-bar mute, the same gate as mask !"<~!4 t!12>", and "t f t t" is "t ~ t t". Every structural reader — mask, struct, squeeze, inv, sew, stitch, and signal/stepSeq reading a boolean — asks the same predicate. brakeTrack's pattern is not a boolean slot: it is read for its onsets alone, so a 0 step there is a wind-down like any other.
struct keeps whole chords#
struct's lookup takes the most-recent non-rest onset at or before each true window (wrapping to the span's last onset before the first), and then stamps every event sharing that onset — so a chord lane keeps all its members, each with its own velocity / slide / voice-tag / glide suffixes. stitch inherits this on both sides. Mind the voice pool: a triad through struct needs room for three notes plus the previous stab's release tail.
Reading a boolean as a signal#
signal bp and stepSeq bp accept a structural pattern and read it as 1/0-valued windows — t → 1, ~ and f → 0. The positional rest law's "a rest holds the previous value" is deliberately suppressed for this read (a false window has to read zero, not hold the 1 before it), which is why these two used to reject boolean patterns outright. So one pattern serves every reading:
puff = !"t ~ t t"
-- structure: play kit (chordLane |> struct puff)
-- audibility: hats |> mask puff
-- complement: play kit (rim |> struct (inv puff))
-- as a GATE: amp = smooth 9ms (signal puff)
A layered boolean (!"t ~, ~ t") merges co-onset members with an OR — the same "any true window" rule mask reads them by. A pitch pattern is still rejected by signal/stepSeq: the boolean read is for structural patterns, not a silent coercion of a mis-typed melody.