VOLTAreference

Reference / Patterns and harmony

Chords, scales and harmony

Patterns and harmony · playable examples

Open in the workbench →
On this page · 7 sections

Write chords, scale degrees and progressions, then transform their register, rhythm and melodic motion.

Hear it and change it#

A chord sounds several pitches together. A triad has three notes. c4:maj builds a C major triad; c4:min changes its middle note to make a minor triad. Major and minor are useful colors, not fixed instructions for happy or sad music.

Listen for: the change in color when the middle note moves down a semitone. Angle brackets alternate the two chords, one per bar.

Listen to this example · four bars, then the tail
tempo = 120bpm
timingVersion = 2
play "keys" "<c4:maj c4:min>" |> release 450ms |> gainTrack 0.55

Make it yours: try <c4:maj7 c4:min7> for four-note chords. The extra note adds tension and richness. Chords also add voices, so lower the track level if you layer several chord parts.

What it is#

Volta writes chords, scales, and roman-numeral progressions as first-class musical tokens instead of hand-spelled stacks. A chord literal like c4:maj7, a roman numeral like vi, or a scale degree like ^2 resolves to ordinary simultaneous pitch events at compile time, against an ambient key. Combinators then operate on the result: transpose shifts pitch, voiceLead smooths chord-to-chord motion, arp breaks a chord into successive notes. Harmony is a purely compile-time concern — it never reaches the IR as a chord payload; the runtime only ever sees stacked pitch events.

Surface#

key = "c minor"               -- ambient key; resolves roman + degree tokens

-- chord tokens: <pitch>:<quality>
pad   = !"c4:maj7 eb3:min g3:dom7 d4:min7b5"
-- roman numerals (case = default triad quality; suffix overrides)
prog  = !"i VI III V"          -- Cm Ab Eb G, against key
flat7 = !"i bVII iiø7 V7"      -- b/# prefix = chromatic; ° ø + figured bass
-- scale-degree tokens: ^N (index into the ambient scale; ^7 = octave up)
line  = !"^0 ^2 ^4 ^7"

-- expression builtins
trio  = chord "c4" "maj7"                -- = the c4:maj7 token, as a value
dor   = inScale (tonality "c dorian") !"^0 ^2 ^4"
lyd   = tonality "c lydian"              -- a Scale flows through a binding…
dor2  = inScale lyd !"^0 ^2 ^4"          -- …and `inScale` folds it by name

-- combinators
up    = transpose 12 prog                -- chromatic shift, in semitones
smooth = voiceLead (!"i VI" |> slow 2)   -- octave-shift each chord nearer prev
parts  = voiceLines 4 !"i iv v i"        -- solve it into four voice-led lines
roll  = arp "up" (chord "c4" "maj")      -- "up" | "down" | "updown" | "thumb"

How it's implemented#

  • Token resolution (compiler/src/mini.rs): chord_quality is the static semitone-offset table (maj/min/dim/aug/aug7/sus2/sus4/6/ m6/7/maj7/m7/m7b5/dim7/add9/9/m9/maj9, with the spelled-out aliases major/minor/m/dom7/min7/… and the figured-bass markers ° (dim), °7, ø/ø7 (m7♭5), + (aug) and +7). scale_intervals is the mode/scale catalogue (ionian…locrian, harmonic/melodic minor, pentatonics, chromatic, plus the augmented-2nd "exotic" family: phrygiandominant — aka hijaz/freygish/spanish — doubleharmonic/byzantine, and hungarianminor). parse_key parses "c minor" → Key { root, scale }; a multi-word scale name is joined, so "e phrygian dominant" and "c harmonic minor" resolve like their single-token spellings. resolve_degree maps ^N to 60 + root + scale[N mod n] + 12·⌊N/n⌋; roman_chord builds a triad (or suffix-quality chord) on a scale degree, root placed an octave below the degree pitch. A bare numeral is diatonic to the declared scale (VII is B in C major, B♭ in C minor); a b/# (or ♭/♯) prefix makes it chromatic, counted from the tonic's major scale, so bVII is B♭ and #IV is F♯ in every key and mode. A token shaped like a numeral that does not resolve (VIII) is a diagnostic, not a silent sample name; token_pitches dispatches a mini-notation cell to one or many MIDI pitches. The set ambient key is held in module state (mini::set_key).
  • Lowering: compiler/src/lower.rs's extract_key reads the top-level key = "…" string decl and installs the Key before any pattern is parsed. Chord/degree/roman cells expand to N TrackEvents sharing an onset (same shape stack emits) — no IR chord node. lower/pattern.rs's resolve_chord lowers the chord builtin, and resolve_in_scale lowers inScale (tonality "…") !"…" by re-parsing the pattern literal under the swapped key. lower/combinator.rs holds the compile-time pattern passes: pat_transpose (chromatic, additive on pitch_midi), pat_voice_lead (per-chord octave shift k ∈ [-3,3] minimizing distance of chord mean to previous), and pat_arp / arp_sequence (up/down/updown/thumb, subdividing the cell's duration).
  • Builtins: BuiltinKind tags Transpose/VoiceLead/InScale/Arp are descriptor rows in compiler/src/builtins/pattern_transforms.rs (voiceLead, arp and transpose rewrite only pitched events), Chord in compiler/src/builtins/pattern_constructors.rs, and Tonality in compiler/src/builtins/tuning.rs.
  • IR (ir/src/ir.rs): the only harmony artifact in the IR is Module.key: Option<KeyInfo { root, scale }> — the ambient key, carried for the harmonic visualizer (The harmony Lens), not for synthesis.
  • Examples: examples/songs/phosphor.volta, morph.volta and lattice.volta all open with key = "c minor" and voice-lead their pads with voiceLead (phosphor and lattice as voiceLead (chords |> transpose 12)).

Following a progression#

Declare the song's chords once and let other parts follow them. A top-level pattern binding named progression is the song's chord track; patterns written in chord-relative terms resolve against the chord sounding at each note's onset, so editing the progression moves every follower.

tempo = 110bpm
key = "c major"
progression = !"<I vi IV V7>"

pluck = patch { source = saw (freq pitch), amp = adsr 5ms 120ms 0.4 200ms, gain = 0.15 }
sub = patch { source = sine (freq pitch), amp = adsr 5ms 80ms 0.8 150ms, gain = 0.3 }

bass = play sub !"root2 ~ root2 fifth2"           -- C2 C2 G2, then A2 A2 E3, …
arp  = play pluck (!"^^0 ^^2 ^^4 ^^7" |> fast 2)  -- each chord's own arpeggio
hook = play pluck (!"c5 d5 e5 g5" |> followChords) -- a riff that walks the changes
  • The declaration. progression = <pattern> is an ordinary pattern binding (play it or not) that also sets the chord track, the way key = "…" sets the key. Its chords are its onset groups: the pitched events sharing an onset are one chord, rooted at the token's own root (or the lowest pitch for a hand-spelled stack), and a chord holds until the next onset — a rest does not end it. Its period is the binding's own (<I vi IV V7> is four cycles). It is realised before every other value, so it may be declared anywhere.
  • Chord tones. root, third, fifth, seventh, ninth, each with an optional octave digit naming the octave of the chord's ROOT — root2 is the root in octave 2, fifth2 the fifth above root2; a bare word is octave 4, like ^0. A tone the chord does not contain (seventh over a triad) is its chord scale's degree at that place.
  • Chord-relative degrees. ^^N is degree N of the current chord's chord scale, counted from its root in octave 4 (^^0 the root, ^^2 the third, ^^7 the root an octave up, negative degrees below). The chord scale is the key's scale re-centred on the chord root — or ionian/aeolian/locrian by the chord's own third and fifth when the root is outside the key or there is no key — with the chord's own third, fifth, seventh and ninth substituted, so ^^2 over V7 in C minor is B♮.
  • followChords p re-maps an already-written pattern: p is read as written over the progression's first chord, and each pitched note moves to the same chord-scale place over the chord sounding at its onset, the root moving by the smallest interval (−5…+6 semitones). Over the first chord it is the identity; a note outside the first chord's scale keeps its distance from the root. (follow is the signal envelope follower.)
  • Timing. A follower reads the chord on the TRACK's own clock, where the pattern becomes a track (or a timeline region's content), after every timing transform — slow, fast, rev, every move a note before its chord is read. A pitch transform applied earlier (transpose 12) rides along as an offset. The follower's period widens to the common multiple of its own and the progression's (a one-cycle bass over a four-chord <…> becomes a four-cycle track). The progression repeats from cycle 0 of each track; a region's content counts from its own start.
  • No declaration is a compile error at the follower's own atom naming the fix; a declared progression with no followers changes nothing that sounds.
  • Analysis. The compile hands the declared chords to the editor through the playback sidecar (SourceMap::progression, never the IR plan). The Lens analysis (harmony_analysis::analyze_declared) then reads them as exact chords — the HARMONY lane, chip and Tonnetz show the declared track — each chord's source being its token inside the declaration, and Analysis::progression the declaration's value span, so a chord edit lands on the one place every follower reads.

Tension as a modulator#

harmonyTension is the song's harmonic tension as a control signal: a bare, nullary modulator typed Sig Control Normalized, usable anywhere a modulator goes — a filter cutoff through a range, a send or reverb mix, a drive amount.

key = "c major"
pad = patch { source = saw (freq pitch) |> lpf (const 300Hz + harmonyTension * 4000Hz) 0.7,
              ampEnv = adsr 5ms 50ms 0.8 100ms, gain = vel * 0.2 }
padT = play pad !"<c4:maj c#4:dim7 f4:maj g4:7>"

The filter sits low on the C chord and opens on the diminished one.

  • What it reads. The Lens analysis (The harmony Lens) of the whole compiled song: every pitched track, gated by its arrangement — a written timeline's regions, an arrange schedule's sections, or (a loose song) every track from zero. Tension per chord is interval roughness + circle-of-fifths distance from the key + out-of-key tones, scaled 0..1, against the declared key (or the detected one).
  • Update rate and shape. One step per chord span, held for the whole span and through a gap with no confident chord, with stepSeq's 5 ms anti-pop glide at each step. 0 before the first chord. For a slower move, smooth it with the existing smoother: harmonyTension |> withSlew 0.4.
  • Time. Song beat time on the transport clock, so it follows tempo changes. Its period is the song's length (the timeline's length, the arrange total, or a loose song's track-period LCM up to 256 bars), so a looping song's tension loops with it.
  • No chord. A song with no pitched notes, or none that read as a chord, gets a constant 0 and a warning.
  • How it lowers. At compile time, to the existing transport-clocked LivePattern node (stepSeq … |> syncTo "transport"'s), one event per chord span. No engine node. A program that names harmonyTension lowers twice: once to find the song's notes, once with the curve. Pitched events are compile-time patterns and never read a control signal, so the signal cannot feed back into its own analysis.
  • Approximations. The compile-time note walk reads the IR, not the scheduler: swing, nudge and push micro-timing, positional meter maps and scene-edge transition material are not applied, and chord names come from detection (chord tokens' exact spans are not consulted). Scheduler-exact timing changes which chord sounds by milliseconds at most. A track subset (harmonyTensionOf [tracks]) is not in v1.

Notes & limits#

  • The key is a string (key = "c minor"); a bare c minor would not type-check. Default scale word is major if omitted.
  • The Scale value form is tonality "…", not scale "…" — scale is the audio-range builtin (sig |> scale [lo, hi]).
  • No /inversion chord suffix — / is the mini-notation slow operator.
  • transpose is chromatic. Use transposeDiatonic for scale steps and fitScale for reusable-pattern scale fitting; Theory tools and composition workflow.
  • voiceLead is block voice-leading: it shifts each whole chord by octaves to sit nearest its predecessor's mean, spelling intact. The per-note voicing is a separate combinator, voiceLines n p, which solves the progression into n independent voice-led lines (no crossing, upper-voice spacing ≤ an octave, no parallel perfects, least motion) — see Interweaving melodies "The voicing CSP". The harmony module exports parallel, leadingTone and relative for P/L/R chord-value transforms.
  • arp passes single-pitch cells, rests, and non-pitched events through unchanged, so it is safe on mixed patterns. An unknown direction is a compile error.
  • A degree/roman token used with no ambient key is a compile error pointing the user to add key = "…".
  • inScale t p only re-resolves p when p is an inline !"…" literal or a key generator like rwalk; a top-level pattern name was already parsed under the outer key and is rejected; use fitScale on resolved pitches. The SCALE argument has no such restriction since 2026-09-01: tonality "…" is an opaque tag the elaborator inlines through a top-level binding, so lydian = tonality "c lydian" may be handed to inScale by name (production-gaps.md §31 AF2).
  • harmony defines first-class Chord, Quality and Interval values. Chord records lower to stacked pitch events. See Define your own value types and Theory tools and composition workflow for transformations and signed inversions.
  • Chord identity feeds the tuning system: under tuning = adaptive, chord tokens / romans / chord retune to just ratios of their own root, while hand-spelled stacks stay 12-TET. See Tuning systems.