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.
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_qualityis 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 aliasesmajor/minor/m/dom7/min7/… and the figured-bass markers°(dim),°7,ø/ø7(m7♭5),+(aug) and+7).scale_intervalsis the mode/scale catalogue (ionian…locrian, harmonic/melodic minor, pentatonics, chromatic, plus the augmented-2nd "exotic" family:phrygiandominant— akahijaz/freygish/spanish—doubleharmonic/byzantine, andhungarianminor).parse_keyparses"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_degreemaps^Nto60 + root + scale[N mod n] + 12·⌊N/n⌋;roman_chordbuilds 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 (VIIis B in C major, B♭ in C minor); ab/#(or♭/♯) prefix makes it chromatic, counted from the tonic's major scale, sobVIIis B♭ and#IVis 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_pitchesdispatches 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'sextract_keyreads the top-levelkey = "…"string decl and installs theKeybefore any pattern is parsed. Chord/degree/roman cells expand to NTrackEvents sharing an onset (same shapestackemits) — no IR chord node.lower/pattern.rs'sresolve_chordlowers thechordbuiltin, andresolve_in_scalelowersinScale (tonality "…") !"…"by re-parsing the pattern literal under the swapped key.lower/combinator.rsholds the compile-time pattern passes:pat_transpose(chromatic, additive onpitch_midi),pat_voice_lead(per-chord octave shiftk ∈ [-3,3]minimizing distance of chord mean to previous), andpat_arp/arp_sequence(up/down/updown/thumb, subdividing the cell's duration). - Builtins:
BuiltinKindtagsTranspose/VoiceLead/InScale/Arpare descriptor rows incompiler/src/builtins/pattern_transforms.rs(voiceLead,arpandtransposerewrite only pitched events),Chordincompiler/src/builtins/pattern_constructors.rs, andTonalityincompiler/src/builtins/tuning.rs. - IR (
ir/src/ir.rs): the only harmony artifact in the IR isModule.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.voltaandlattice.voltaall open withkey = "c minor"and voice-lead their pads withvoiceLead(phosphorandlatticeasvoiceLead (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 waykey = "…"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 —root2is the root in octave 2,fifth2the fifth aboveroot2; a bare word is octave 4, like^0. A tone the chord does not contain (seventhover a triad) is its chord scale's degree at that place. - Chord-relative degrees.
^^Nis degreeNof the current chord's chord scale, counted from its root in octave 4 (^^0the root,^^2the third,^^7the 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^^2overV7in C minor is B♮. followChords pre-maps an already-written pattern:pis 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. (followis 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,everymove 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'ssourcebeing its token inside the declaration, andAnalysis::progressionthe 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
arrangeschedule'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 declaredkey(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, thearrangetotal, 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
LivePatternnode (stepSeq … |> syncTo "transport"'s), one event per chord span. No engine node. A program that namesharmonyTensionlowers 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
pushmicro-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 barec minorwould not type-check. Default scale word ismajorif omitted. - The
Scalevalue form istonality "…", notscale "…"—scaleis the audio-range builtin (sig |> scale [lo, hi]). - No
/inversionchord suffix —/is the mini-notation slow operator. transposeis chromatic. UsetransposeDiatonicfor scale steps andfitScalefor reusable-pattern scale fitting; Theory tools and composition workflow.voiceLeadis 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 intonindependent voice-led lines (no crossing, upper-voice spacing ≤ an octave, no parallel perfects, least motion) — see Interweaving melodies "The voicing CSP". Theharmonymodule exportsparallel,leadingToneandrelativefor P/L/R chord-value transforms.arppasses 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 ponly re-resolvespwhenpis an inline!"…"literal or a key generator likerwalk; a top-level pattern name was already parsed under the outer key and is rejected; usefitScaleon 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, solydian = tonality "c lydian"may be handed toinScaleby name (production-gaps.md §31 AF2).harmonydefines first-classChord,QualityandIntervalvalues. 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 /chordretune to just ratios of their own root, while hand-spelled stacks stay 12-TET. See Tuning systems.