VOLTAreference

Reference / Patterns and harmony

Patterns: events over time

Patterns and harmony · playable examples

Open in the workbench →
On this page · 11 sections

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.

Listen to this example · four bars, then the tail
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):

SyntaxMeaning
a b cflat 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 , bpolyrhythm — comma-separated layers overlaid on the same span
a*2fast — play a twice within its slot
a/2slow — stretch a across 2 cycles
a*<2 4>patterned factor — one value per cycle (Pattern Applicative §4)
a!3replication — repeat a three times (bare ! repeats previous)
a@3weight — 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 .. 7inclusive 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.7inline per-event velocity factor (see Velocity and accents)
bd:3sample 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 — every n-th cycle comes from f p instead of p. f is applied to the WHOLE pattern, so a transform that changes the length keeps it: every 1 (slow 2) is exactly slow 2, and every 2 (iter 4) rotates cycle 2 by half a cycle.
  • p |> degrade — drop ~50% of (non-rest) events, fresh each cycle.
  • p |> superimpose f — overlay p with f p.
  • p |> off t f — overlay p with f p shifted later by cycle-fraction t.
  • p |> jux f — p panned hard left, f p panned hard right.
  • p |> sampleAdd src [lo,hi] — sample signal src at 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 operationTiming rule
p |> fast nDivide onsets, durations and repeat period by n.
p |> slow nMultiply those times by n, retaining later notes; equivalent to fast (1/n).
stack, par, voiceLines, mergesMaterialize 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 a Step tree (Atom/Rest/Seq/Stack/Alt/Fast/Slow/Euclid), detect_payload classifies the tokens (Pitch/Number/Sample), validate checks them, and render walks the tree one cycle at a time into MiniEvents (onset, duration, pitch_midi/value/sample, pan, velocity, variant, sample_region). A pattern that uses <…> or / spans multiple real cycles; cycle_span computes that count (LCM-based) and the whole span is compressed into [0,1) with MiniPattern::cycles recording how many cycles it represents. euclid_pulses distributes 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) is x..x..x., (5,8) x.xx.xx., (2,5) x.x.., and the n − 1 cases are Tidal's own (3,4) = xxx.. The rotation turns LEFT, as Tidal's rotL ((3,8,2) = .x..x.x.; Strudel's euclidRot turns the other way), and wraps modulo n. k = 0 is silent, and k ≥ n fills every slot (Tidal would lengthen the bar to k steps). (euclidOnsets in runtime/src/learn/checkers.ts mirrors 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's BuiltinKind (from compiler/src/builtins.rs) maps to a pat_* function: pat_rev, pat_every, pat_degrade (degrade_keep is the seeded keep test), pat_superimpose, pat_off, pat_jux, pat_sample_add. Transform arguments are resolved by resolve_transform; overlays are combined by compiler/src/lower/pattern.rs's merge_patterns (re-times all layers onto the LCM of their cycle spans via expand_events). jux sets each event's pan to ∓1. sampleAdd uses the closed-form shape_sample (deterministic hash for rand/perlin). A combinator returns a (MiniPattern, mult) pair; mult stretches the track's cycle_beats so a multi-cycle result plays at the right rate. Version 2 normalizes this pair in lower/timing.rs at pattern and transform boundaries; the pair's physical period is 1 / mult.
  • Whole-period contract: a pattern reaches a combinator already flattened — mini renders all cycle_span cycles into [0,1) and fast 2 halves the span — so event counts through fast prove nothing; tests use rev to show a combinator carries a field. A span-widening combinator returns mult = 1/total (Track.cycle_span = 1.0 / rate_mult), not its operand's mult. A function port runs f on the whole subject (combinator::transform_whole), never a one-cycle slice, which the law every 1 f == f checks. An operator guards::transparent admits on a guarded subject must Guard::reindex each 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 a Track with events: Vec<TrackEvent> (onset/duration as [0,1) cycle fractions, plus pitch_midi/value/sample/variant/sample_region/pan/velocity) and cycle_beats/cycle_seconds.
  • Random guards: compiler/src/lower/guards.rs decides where a draw stays late-bound; engine/src/lib.rs::cycle_firing_order skips an event whose guards fail, and VoiceCoords keys its per-voice draws as the unrolled list did.
  • Engine: engine/src/lib.rs::schedule_track walks each track's cursor, converting onset * cycle_beats to a beat via the tempo clock and starting a voice at its exact sample offset; event_pan makes per-event pan the fallback when no track pan is set, and event_dynamics multiplies per-event velocity into the voice gain.

Notes & limits#

  • degrade and sampleAdd's rand/perlin are deterministic: dropout and noise are pure hashes of (cycle, event index) / phase, so playback is reproducible. Rests are always kept.
  • a*2 / a/2 and Euclidean counts round to integers in the renderer.
  • sampleAdd adds 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.
  • jux writes hard L/R pan; a track-level panTrack overrides it (see ir.rs's pan-composition note and engine/src/lib.rs::event_pan).
  • Mixing pitch and sample tokens in one literal is rejected at parse time.
  • sometimesBy x f (and its sugar sometimes / often / rarely) reads the same per-event coin as degradeBy x, so each cycle it transforms exactly the events degradeBy x would drop. ply n repeats each event inside its own span and leaves rests alone. Both use the subject-first pipe rule in Language reference §9.
  • chunk n f and iter n use Tidal's cat timing on a multi-cycle input: output cycle c reads input cycle ⌊c/n⌋ with slot (or shift) c mod n, so the result spans n × pc cycles and never stacks copies of the input. iter shifts EARLY, as Tidal's and Strudel's iter do: iter 4 over a b c d plays a b c d, b c d a, c d a b, d a b c. iterBack n shifts LATE (Tidal's iter', Strudel's iterBack): d a b c follows a b c d. every reads the absolute cycle instead. chunk's slot comes from cycle ⌊c/n⌋ of f p — f sees the whole pattern — and the result spans n × lcm(pc, period of f p).
  • A function in a transform port sees the whole pattern. every, firstOf, lastOf, someCyclesBy, chunk, within, superimpose, off, jux and sometimesBy all apply f to the complete subject, as it would be applied outside the port, so every 1 f is f for every f. every n f p plays cycle k of f p when k mod n selects it and cycle k of p otherwise (Tidal's whenT); its period is the least common multiple of n, p's period and f p's. someCyclesBy x f is the same with a whole-cycle coin in place of k mod n.
  • linger n repeats the first 1/n of 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 in compiler/src/builtins.rs and compiler/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>" rev spans lcm(2,2,3) = 6 and transforms cycle c iff c mod n_c == 0 on 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 the degradeBy/sometimesBy complementarity holds per event. bite's patterned index squeezes its slice into each index event's span.
  • In legacy timing, fast !"2" normalizes to fast 2 exactly (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.5 widens 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/sometimesBy accept an inline cycle-rate LFO (sometimesBy (lfoSine (8c)) f) sampled closed-form per event; Hz rates are rejected (commensurability); re-timing combinators defer to segment 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 100000 errors 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 span

    A stack of coprime voices over the ceiling in EVENTS and the same stack over the ceiling in CYCLES differ only in that count and that unit. Where the extent is knowable from the source alone — a stack or voiceLines over 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:

BuiltinMeaning
p |> squeeze bpeach 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 |> invcomplement the window set — true ↔ gap or false step (fabricated events carry value 1, no source atom)
sew bp p1 p2hear p1 in true windows, p2 in false ones; each keeps its own rhythm (mask bp p1 ∪ mask (inv bp) p2)
stitch bp p1 p2the 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.