-- converts an interval string into a note value -- an interval string consists of any number of #'s and b's -- followed by a non-negative scale degree -- (string) -> (number) local intvToNote = function (str) local a, b = str:match "^([#b]*)(%d+)$" local acc = 0 a:gsub(".", function (c) acc = acc + (c == "#" and 1 or -1) end) b = tonumber(b) return acc * 7 + (b * 2 - 1) % 7 - 1 end -- converts a note value into a MIDI pitch value between 0 and 11 inclusive -- a note value is simply the number of perfect fifths from C -- (number) -> (number) local noteToMIDI = function (x) return x * 7 % 12 end -- gets the number of sharps in a note value -- (number) -> (number) local noteGetAcc = function (x) return math.floor((x + 1) / 7) end -- converts a note value into a note name with proper accidentals -- (number) -> (string) local noteToStr; do local NAME = {[0] = 'C', 'D', 'E', 'F', 'G', 'A', 'B'} noteToStr = function (x) local acc = noteGetAcc(x) return NAME[x * 4 % 7] .. ("#"):rep(acc) .. ("b"):rep(-acc) end; end -- converts a MIDI pitch value into a note name with sharp and octave name -- (number) -> (string) local MIDItoStr; do local NAME = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"} MIDItoStr = function (x) return NAME[x % 12 + 1] .. math.floor(x / 12) end; end local LH_LOWEST = -.25 -- left-hand octave offset local RH_CENTRE = 0 -- right-hand octave offset LH_LOWEST = 41 + 12 * LH_LOWEST RH_CENTRE = 60 + 12 * RH_CENTRE -- converts a chord over a bass note into a MIDI voicing -- a chord is a list of interval strings, basic order of chord tones is assumed: -- root to highest extension, followed by added/suspended tones -- members of lowest triad must use "0" as interval if omitted (or some other nil value) -- the value corresponding to key 0 indicates an optional slash bass -- a voicing consists of MIDI pitch values for the bass and four voices -- ([string], number) -> (number, [number]) local voicing; do local map = function (f, t) local z = {} for k, v in next, t do z[k] = f(v) end return z end voicing = function (chord, trsp) chord = map(function (x) return x end, chord) -- clone if chord[0] == "0" then chord[0] = nil end for i = #chord, 4, -1 do if chord[i] == "0" then table.remove(chord, i) end end assert(chord[1] and chord[1] ~= "0", "Missing bass note") local c = map(intvToNote, chord) local bass = (c[0] or c[1]) + trsp -- select notes local vo = {} local addVoice; do local selected = {} for i, v in pairs(chord) do if v == "0" then selected[i] = true end end addVoice = function (i, f) if #vo >= 4 then return end if not selected[i] and (not f or f(c[i])) then local n = noteToMIDI(c[i] + trsp) for _, v in ipairs(vo) do if n == v then return end end -- no duplicates selected[i] = true; table.insert(vo, n) end end; end local VOICES = #c -- 1st pass: suspended note addVoice(2, function (n) return n ~= intvToNote("3") and n ~= intvToNote("b3") end) -- 2nd pass: altered extensions for i = 5, VOICES do addVoice(i, function (n) return noteGetAcc(n) ~= 0 end) end -- 3rd pass: altered fifth addVoice(3, function (n) return n ~= intvToNote("5") end) -- 4th pass: other extensions for i = VOICES, 4, -1 do addVoice(i) end -- 5th pass: third, fifth for i = 2, 3 do addVoice(i) end -- 6th pass: slash bass (optional) -- if chord[0] then addVoice(0); c[0] = nil end -- 7th pass: root addVoice(1) table.sort(vo) -- raise bass note to left-hand range bass = noteToMIDI(bass) while bass < LH_LOWEST do bass = bass + 12 end -- use average pitch value for voicing local avg = 0 for _, v in ipairs(vo) do avg = avg + v end local COUNT = #vo while avg < (RH_CENTRE - 12) * COUNT do avg = avg + 12 * COUNT -- raise notes just below right-hand octave for i, v in ipairs(vo) do vo[i] = v + 12 end end while avg < RH_CENTRE * COUNT do avg = avg + 12 -- invert voicing until average pitch above right-hand octave table.insert(vo, table.remove(vo, 1) + 12) end -- swap end notes for slightly open voicing vo[1], vo[math.min(COUNT + 1, 4)] = vo[COUNT] - 12, vo[1] + 12 -- if COUNT == 4 then -- vo[1], vo[4] = vo[4] - 12, vo[1] + 12 -- else -- double one note -- local top = avg + vo[1] + 12 -- local down = avg + vo[COUNT] - 12 -- local centre = RH_CENTRE * COUNT -- if math.abs(down - centre) <= math.abs(top - centre) then -- table.insert(vo, 1, vo[COUNT] - 12) -- else -- table.insert(vo, vo[1] + 12) -- end -- end return bass, vo end; end local test = function (chord, name) -- for i, v in ipairs(chord) do print(i, intvToNote(v), noteToStr(intvToNote(v))) end for i = -7, 7 do local lh, rh = voicing(chord, i) io.write(("%14s"):format(noteToStr(i) .. name .. (chord[0] and "/" .. noteToStr(i + intvToNote(chord[0])) or ""))) io.write "\t\t\t" for _, v in ipairs(chord) do if v ~= "0" then io.write((" %3s"):format(noteToStr(intvToNote(v) + i))) end end if chord[0] and chord[0] ~= "0" then io.write((" / %3s"):format(noteToStr(intvToNote(chord[0]) + i))) end io.write "\t\t\t" io.write((" %3s"):format(MIDItoStr(lh))) for _, v in ipairs(rh) do io.write((" %3s"):format(MIDItoStr(v))) end io.write '\n' end io.write '\n' end test({"1", "3", "#5"}, "+") test({"1", "b3", "5", [0] = "2"}, "m") test({"1", "3", "5", [0] = "5"}, "") test({"1", "4", "5", "b7", "9"}, "9sus") test({"1", "3", "5", "b7", "b9", "13"}, "7(b9,13)") test({"1", "0", "5"}, "5") test({"1", "4", "5", "2"}, "sus42")
Standard input is empty
Cb+ Cb Eb G B3 G4 D#5 G5 B5
Gb+ Gb Bb D F#3 F#4 D5 F#5 A#5
Db+ Db F A C#4 F4 C#5 F5 A5
Ab+ Ab C E G#3 E4 C5 E5 G#5
Eb+ Eb G B D#3 G4 D#5 G5 B5
Bb+ Bb D F# A#3 F#4 D5 F#5 A#5
F+ F A C# F3 F4 C#5 F5 A5
C+ C E G# C4 E4 C5 E5 G#5
G+ G B D# G3 G4 D#5 G5 B5
D+ D F# A# D3 F#4 D5 F#5 A#5
A+ A C# E# A3 F4 C#5 F5 A5
E+ E G# B# E3 E4 C5 E5 G#5
B+ B D# F## B3 G4 D#5 G5 B5
F#+ F# A# C## F#3 F#4 D5 F#5 A#5
C#+ C# E# G## C#4 F4 C#5 F5 A5
Cbm/Db Cb Ebb Gb / Db C#4 F#4 D5 F#5 B5
Gbm/Ab Gb Bbb Db / Ab G#3 F#4 C#5 F#5 A5
Dbm/Eb Db Fb Ab / Eb D#3 E4 C#5 E5 G#5
Abm/Bb Ab Cb Eb / Bb A#3 G#4 D#5 G#5 B5
Ebm/F Eb Gb Bb / F F3 F#4 D#5 F#5 A#5
Bbm/C Bb Db F / C C4 F4 C#5 F5 A#5
Fm/G F Ab C / G G3 F4 C5 F5 G#5
Cm/D C Eb G / D D3 G4 D#5 G5 C6
Gm/A G Bb D / A A3 G4 D5 G5 A#5
Dm/E D F A / E E3 F4 D5 F5 A5
Am/B A C E / B B3 E4 C5 E5 A5
Em/F# E G B / F# F#3 G4 E5 G5 B5
Bm/C# B D F# / C# C#4 F#4 D5 F#5 B5
F#m/G# F# A C# / G# G#3 F#4 C#5 F#5 A5
C#m/D# C# E G# / D# D#3 E4 C#5 E5 G#5
Cb/Gb Cb Eb Gb / Gb F#3 F#4 D#5 F#5 B5
Gb/Db Gb Bb Db / Db C#4 F#4 C#5 F#5 A#5
Db/Ab Db F Ab / Ab G#3 F4 C#5 F5 G#5
Ab/Eb Ab C Eb / Eb D#3 G#4 D#5 G#5 C6
Eb/Bb Eb G Bb / Bb A#3 G4 D#5 G5 A#5
Bb/F Bb D F / F F3 F4 D5 F5 A#5
F/C F A C / C C4 F4 C5 F5 A5
C/G C E G / G G3 G4 E5 G5 C6
G/D G B D / D D3 G4 D5 G5 B5
D/A D F# A / A A3 F#4 D5 F#5 A5
A/E A C# E / E E3 E4 C#5 E5 A5
E/B E G# B / B B3 G#4 E5 G#5 B5
B/F# B D# F# / F# F#3 F#4 D#5 F#5 B5
F#/C# F# A# C# / C# C#4 F#4 C#5 F#5 A#5
C#/G# C# E# G# / G# G#3 F4 C#5 F5 G#5
Cb9sus Cb Fb Gb Bbb Db B3 F#4 C#5 E5 A5
Gb9sus Gb Cb Db Fb Ab F#3 E4 B4 C#5 G#5
Db9sus Db Gb Ab Cb Eb C#4 F#4 B4 D#5 G#5
Ab9sus Ab Db Eb Gb Bb G#3 F#4 C#5 D#5 A#5
Eb9sus Eb Ab Bb Db F D#3 F4 A#4 C#5 G#5
Bb9sus Bb Eb F Ab C A#3 F4 C5 D#5 G#5
F9sus F Bb C Eb G F3 G4 C5 D#5 A#5
C9sus C F G Bb D C4 F4 A#4 D5 G5
G9sus G C D F A G3 F4 C5 D5 A5
D9sus D G A C E D3 G4 C5 E5 A5
A9sus A D E G B A3 E4 B4 D5 G5
E9sus E A B D F# E3 F#4 B4 D5 A5
B9sus B E F# A C# B3 F#4 C#5 E5 A5
F#9sus F# B C# E G# F#3 E4 B4 C#5 G#5
C#9sus C# F# G# B D# C#4 F#4 B4 D#5 G#5
Cb7(b9,13) Cb Eb Gb Bbb Dbb Ab B3 G#4 C5 D#5 A5
Gb7(b9,13) Gb Bb Db Fb Abb Eb F#3 E4 A#4 D#5 G5
Db7(b9,13) Db F Ab Cb Ebb Bb C#4 F4 B4 D5 A#5
Ab7(b9,13) Ab C Eb Gb Bbb F G#3 F#4 C5 F5 A5
Eb7(b9,13) Eb G Bb Db Fb C D#3 E4 C5 C#5 G5
Bb7(b9,13) Bb D F Ab Cb G A#3 G4 B4 D5 G#5
F7(b9,13) F A C Eb Gb D F3 F#4 D5 D#5 A5
C7(b9,13) C E G Bb Db A C4 E4 A#4 C#5 A5
G7(b9,13) G B D F Ab E G3 F4 B4 E5 G#5
D7(b9,13) D F# A C Eb B D3 F#4 C5 D#5 B5
A7(b9,13) A C# E G Bb F# A3 F#4 A#4 C#5 G5
E7(b9,13) E G# B D F C# E3 F4 C#5 D5 G#5
B7(b9,13) B D# F# A C G# B3 G#4 C5 D#5 A5
F#7(b9,13) F# A# C# E G D# F#3 E4 A#4 D#5 G5
C#7(b9,13) C# E# G# B D A# C#4 F4 B4 D5 A#5
Cb5 Cb Gb B3 F#4 F#5 B5
Gb5 Gb Db F#3 F#4 F#5 C#6
Db5 Db Ab C#4 G#4 G#5 C#6
Ab5 Ab Eb G#3 G#4 G#5 D#6
Eb5 Eb Bb D#3 D#4 D#5 A#5
Bb5 Bb F A#3 F4 F5 A#5
F5 F C F3 F4 F5 C6
C5 C G C4 G4 G5 C6
G5 G D G3 G4 G5 D6
D5 D A D3 A4 A5 D6
A5 A E A3 E4 E5 A5
E5 E B E3 E4 E5 B5
B5 B F# B3 F#4 F#5 B5
F#5 F# C# F#3 F#4 F#5 C#6
C#5 C# G# C#4 G#4 G#5 C#6
Cbsus42 Cb Fb Gb Db B3 F#4 C#5 E5 B5
Gbsus42 Gb Cb Db Ab F#3 F#4 B4 C#5 G#5
Dbsus42 Db Gb Ab Eb C#4 F#4 C#5 D#5 G#5
Absus42 Ab Db Eb Bb G#3 G#4 C#5 D#5 A#5
Ebsus42 Eb Ab Bb F D#3 F4 A#4 D#5 G#5
Bbsus42 Bb Eb F C A#3 F4 C5 D#5 A#5
Fsus42 F Bb C G F3 G4 C5 F5 A#5
Csus42 C F G D C4 F4 C5 D5 G5
Gsus42 G C D A G3 G4 C5 D5 A5
Dsus42 D G A E D3 G4 D5 E5 A5
Asus42 A D E B A3 E4 B4 D5 A5
Esus42 E A B F# E3 F#4 B4 E5 A5
Bsus42 B E F# C# B3 F#4 C#5 E5 B5
F#sus42 F# B C# G# F#3 F#4 B4 C#5 G#5
C#sus42 C# F# G# D# C#4 F#4 C#5 D#5 G#5