Explore different ways of placing pitches within an octave, from familiar equal steps to adaptive and custom tunings.
What it is#
A second ambient binding beside key. Volta's pitch pipeline is nominal 12-TET MIDI everywhere — note names, chord tokens, scale degrees, and every pattern combinator operate on integer semitone space — and a tuning is a final re-mapping of nominal to fractional MIDI, applied at compile time at the last pitch-resolution step. The engine converts MIDI → Hz with the 12-TET formula (440·2^((m−69)/12)), so a retuned fractional value is an exact microtonal frequency on every host with no IR or runtime involvement. No tuning declaration = identity = bit-identical output to before the feature existed.
Surface#
``volta norun four alternative spellings of onetuning` declaration tuning = edo 19 -- n equal divisions of the octave (n ∈ 1..=1200) tuning = just ~C2 -- 5-limit just intonation fixed on a root pitch class tuning = tuningOf [0, 90, 193, 294, 386, 498, 590, 702, 794, 884, 996, 1088] -- explicit cents-from-root table (12 entries, -- octave-repeating, rooted on the key's root) tuning = adaptive -- ★ adaptive JI: each chord retunes to just ratios -- of its own root; melodies stay 12-TET
One `tuning` per module, like `key`. It covers, uniformly: pattern pitch
tokens, chord tokens (`c4:maj7`), roman numerals / scale degrees,
`transpose`/`arp`/`voiceLead` outputs, `trigger` in shreds, and `freq ~C4` /
`~Pitch` constants inside patches. Per-note cent offsets (`~C4 + 14Cents`,
`humanize`, unison detune) compose **on top of** the tuning.
## The systems
**`edo n`** — nominal pitches map to the *nearest* EDO degree, anchored at
A4 = MIDI 69 = 440 Hz: `m → 69 + (12/n)·round(n·(m−69)/12)`. For 19- and
31-EDO nearest-degree *is* the classical diatonic mapping: the 12-TET major
scale lands on 19-EDO degrees `0 3 6 8 11 14 17`, a chromatic semitone
(`c→c#`) is one 63.2 ¢ step, a diatonic semitone (`e→f`) two. Limitation:
input pitch is 12-TET MIDI, so enharmonic pairs the input can't distinguish
(19-EDO's C♯ ≠ D♭) collapse to one degree.
**`just ~Root`** — fixed 5-limit JI, octave-repeating on the root's pitch
class (only the class matters). Each pitch class sits at the table below
above the nearest root-class pitch; root-class pitches stay 12-TET-exact.
Classic caveat applies: fixed JI is beautiful *in* its key and wolf-ridden
outside it — that's what `adaptive` is for.
**`tuningOf [c0 … c11]`** — the same octave-repeating mechanism with your
own cents table (exactly 12 finite entries; entry 0 is the root, normally
`0`). Rooted on the ambient `key`'s root pitch class, C when no key.
**`adaptive`** — ★ adaptive just intonation. Chord tokens, roman numerals,
and the `chord` builtin are compile-time values, so each chord's members
retune to exact ratios of *that chord's* root (root itself 12-TET-anchored):
major = 4:5:6, minor = 10:12:15, maj7 = 8:10:12:15, m7 = 10:12:15:18, and
two structural septimal choices — dominant-shaped chords (major third +
minor seventh) take the harmonic seventh **7/4** (dom7 = 4:5:6:7), flat-five
chords (minor third + diminished fifth) take **7/5**. Because each chord
re-tunes to its own root, progressions modulate freely with no wolf
intervals — a thing no fixed-tuning instrument can do. **Melodic notes (and
`trigger`, and patch constants) stay 12-TET** — only material with chord
identity moves; spelling a chord by hand (`[c3,e3,g3,b3]`) deliberately
keeps equal temperament, which is also the A/B trick `syntonic.volta` uses.
The 5-limit table (cents above the root):
| pc | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ratio | 1/1 | 16/15 | 9/8 | 6/5 | 5/4 | 4/3 | 45/32 | 3/2 | 8/5 | 5/3 | 9/5 | 15/8 |
| cents | 0 | 111.7 | 203.9 | 315.6 | 386.3 | 498.0 | 590.2 | 702.0 | 813.7 | 884.4 | 1017.6 | 1088.3 |
## How it's implemented
- `compiler/src/tuning.rs` — the `Tuning` enum, the ratio tables
(`just_cents_5limit`, `adaptive_member_cents`), and the retune functions
(`retune` for chord-less pitches, `retune_event` for pattern events). The
ambient value is a thread-local mirroring `mini::CURRENT_KEY`
(`set_tuning` / `current_tuning` / `with_tuning`).
- `lower.rs::extract_tuning` — consumes the `tuning = …` decl structurally
(the `extract_tempo`/`tempoMap` pattern); installed right after
`mini::set_key`, always (with `None` when absent) so state can't leak
between compiles.
- `builtins/tuning.rs` — the descriptors for `edo`/`just`/`tuningOf`/`adaptive`
(beside `tonality` and `freq`): opaque `Hole`-typed values to the checker,
every port folded at compile time, meaningless outside the `tuning` decl —
one that reaches the signal lowerer is a lowering error. The descriptors own
arity, port names and named options; `extract_tuning` still reads the
declaration structurally. `compiler/tests/tuning_contracts.rs` pins them.
- Adaptive chord context — `MiniEvent.chord_root` + `just_cents`, stamped at
expansion (`mini.rs::chord_members` for tokens/romans,
`lower/pattern.rs::resolve_chord` / `chord_from_record` for the builtin
and `Chord` records). `pat_transpose` shifts the root with the pitch;
`pat_arp` re-stamps each arpeggiated note with its own member's context;
`voiceLead` needs nothing (octave shifts are pure).
- The three retune points — `lower/track.rs::track_event` (every pattern
event, *after* all combinators), `lower/score.rs` `Trigger` folding, and
`lower/expr.rs` `PitchConst` creation. Fractional inputs split into
`round(m)` + remainder so cent decorations ride on top.
- **No IR / runtime / engine work for pitch** — retuned fractional MIDI flows
through the same paths [[reference/glide]]'s continuous pitch already
exercises, and `PitchToHz`'s 12-TET formula is the decoder. (A declared
tuning also ships its normalized cents lattice as `Module.tuning` metadata,
for the timbre half below; no declaration leaves it `None`.)
## Deliberately not retuned
- `keytrack` reference pitches — slope anchors, not sounding pitches.
- `pitched` / `sampleAt` sample roots — they declare what a recording *is*.
- MIDI-out events — external hardware owns its tuning; fractional MIDI
rounds at the device boundary (no pitch-bend microtuning yet).
## Notes & limits
- Combinators are tuning-agnostic by design: `transpose 2` means two
*nominal* semitones, then retune — under `edo 19` you get the retuned D,
not D + 2·(12/19). Note names keep meaning what they say.
- Determinism is trivial — everything folds at compile time; golden IR
snapshots pin the retuned events (`syntonic.json`, `nineteen.json`).
- One module-level tuning; no per-pattern `inTuning` yet ([[tuning]] §5).
- Glide/slide interpolate between *retuned* endpoints (linear in fractional
MIDI) — portamento works untouched in any tuning; `nineteen.volta`'s bass
glides through the 19-EDO grid.
- Examples: `examples/songs/syntonic.volta` (adaptive-JI A/B — tempered
bars are hand-spelled stacks, just bars are chord tokens),
`examples/songs/nineteen.volta` (19-EDO).
## The other half: timbre
A tuning only fixes *where the notes are*. Consonance also depends on where a
sound's **partials** are — which is why gamelan bronze is consonant in pelog
and clangs in 12-TET. [[reference/spectral-tuning]] joins the two:
`additive … |> tuned [amount]` snaps an additive bank's partial ratios onto
this tuning's cents lattice at compile time, and `track |> spectralTune amount`
does it live to arbitrary audio. That is also the **only** reason a tuning
reaches the IR (`Module.tuning`, the normalized cents lattice) and the engine — ordinary pitch never needs
to, because it is already retuned above. Demo: `examples/songs/lamellae.volta`.