-- 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")