harmonia is a small, pure PureScript library for functional harmony.
You name a chord by a scale degree, a quality, a tension, maybe a borrowed
mode — and it realizes that recipe against a key into pitch classes,
then threads a whole progression together by smallest motion. No
Effect, no FFI; the same code compiles unchanged on JavaScript
and on Erlang.
The three staves above are one call apart. Written once as scale degrees,
the progression is handed to reflavour — and every chord
re-derives its quality from the new mode’s own harmony. The four
movements below go deeper; each carries the PureScript that makes it.
A
cantabile
The voices lead
block — root position, leaping
voice-led — least motion
The same ii–V–I, voiced two ways. Block-voiced, the chords
leap. voiceLead instead moves to each next chord by the
least total motion: every common tone stays on its exact pitch and each
remaining voice steps to its nearest octave — so the whole progression
barely moves.
B
campanae
A line, shadowed — tintinnabuli
the melody — M-voice
+ its shadow — T-voice
Arvo Pärt’s tintinnabuli (little bells): a melody moving
stepwise, shadowed by a second voice that sounds only the notes of the home
triad. Each shadow note is the melody note snapped to the nearest triad
tone above — a pure map over the line.
C
poco a poco
The turnaround, enriched
triads — C · Am · Dm · G
+ sevenths — Cmaj7 · Am7 · Dm7 · G7
+ ninths — Cmaj9 · Am9 · Dm9 · G9
One functional skeleton — I · vi · ii · V —
read three ways. Each pass names a richer recipe: add a seventh, add
a ninth. The progression never changes; only its Quality and
Tension do, and the chord grows a note at a time.
Fine.
to play —spago install harmonia
Engraved in the browser with Verovio · MIT · Andrew Condon
-- one progression, named once by scale degree
phrase :: Phrase
phrase = Phrase (map located [ deg IMaj [], deg IVMaj [], deg VMaj [], deg IMaj [] ])
where located = Located { tonic: 0, mode: Ionian }
-- reflavour re-reads every degree in the new mode, re-deriving-- each chord's quality from that mode's own diatonic harmony:
value (reflavour Mixolydian phrase) -- C F Gm C
value (reflavour Aeolian phrase) -- Cm Fm Gm Cm
Harmonia.Graded · reflavour is total: a chord it can’t re-read comes back unchanged, under blame.
-- a ii–V–I, as recipes
cadence = [ deg IIMin7 [], deg VDom7 [], deg IMaj7 [] ]
-- realize, then voice-lead the whole progression — least total-- motion, common tones held on the very same pitch:
play cMajorKey identity cadence
-- ii7 C4 D4 F4 A4-- V7 B3 D4 F4 G4 — F4 held; A→G, C→B, D stays-- Imaj7 B3 C4 E4 G4 — B3 and G4 held
Harmonia.Voicing · play = realize + voiceLead threaded across a progression.
-- Arvo Pärt's mechanical T-voice rule: the shadow note is the-- k-th triad pitch in a chosen direction from the melody note.dataPosition = AboveInt | BelowInt
nearestTriadNote :: ArrayInt -> Position -> Int -> Int
nearestTriadNote triad pos melody = case pos ofAbove k -> fromMaybe melody (above !! (k - 1))
Below k -> fromMaybe melody (below !! (k - 1))
where
above = filter member (range (melody + 1) (melody + 24))
below = filter member (reverse (range (melody - 24) (melody - 1)))
member n = elem (mod n 12) triad
-- the whole texture is one line — a pure map over the melody:
tVoice = map (nearestTriadNote aMinor (Above1)) mVoice
-- mVoice = A B C D E D C B-- tVoice = C C E E A E E C — the triad, shadowing
from Tidal.Tintinnabuli in the live-coding engine — the Triad newtype unwrapped to its pitch classes.
-- one functional skeleton — I vi ii V — three ways.-- each pass names a richer recipe; the progression is unchanged.
triads = [ deg IMaj [], deg VIMin [], deg IIMin [], deg VMaj [] ]
sevenths = [ deg IMaj7 [], deg VIMin7 [], deg IIMin7 [], deg VDom7 [] ]
ninths = [ deg IMaj7 [Add9], deg VIMin7 [Add9], deg IIMin7 [Add9], deg VDom7 [Add9] ]
map (realize cMajorKey) ninths
-- Cmaj9 Am9 Dm9 G9 — tensions are just richer recipes
Harmonia.Chord · Quality and Tension name the colour; realize stacks the notes.