foldl = function(f, z, l)
for i = 1, #l do z = f(z, l[i]) end
return z
end
foldl1 = function(f, l)
return foldl(f, table.remove(l, 1), l)
end
foldr = function(f, z, l)
for i = #l, 1, -1 do z = f(l[i], z) end
return z
end
foldr1 = function(f, l)
return foldr(f, table.remove(l), l)
end
map = function(f, l)
local z = table.sub(l)
for k in pairs(l) do z[k] = f(rawget(l, k)) end
return z
end
filter = function(pr, l)
return foldl(function(c, v)
return pr(v) == true and table.insert(c, v) or c
end, {}, l)
end
zipWith = function(f, a, b)
local n = {}
for i = 1, math.min(#a, #b) do n[i] = f(a[i], b[i]) end
return n
end
elem = function(t, x)
return foldl(function(c, v) return c or v == x end, false, t)
end
enum = function(i, j)
local z = {}
for k = i, j do z[#z + 1] = k end
return z
end
table.join = function(a, b)
return foldl(function(c, v)
table.insert(c, v); return c
end, a, b)
end
table.sub = function(t, i, j)
local n = {}
i = i or 1
j = j or #t
table.move(t, i, j, 1, n)
return n
end
table.rotate = function(t, k)
local n = {}
for i = 1, #t do
n[i] = t[(i + k - 1) % #t + 1]
end
return n
end
tableStr = function(t, ind)
ind = ind or 0
local str = '{\n'
local mLen = string.len(#t) + 1
for k in pairs(t) do
local kStr = k
local xStr = rawget(t, k)
if type(xStr) == "boolean" then
xStr = xStr and "True" or "False"
elseif type(xStr) == "string" then
xStr = '"' .. xStr .. '"'
elseif type(xStr) == "table" then
xStr = tableStr(xStr, ind + 1)
end
str = str .. string.rep(' ', ind + mLen - string.len(kStr)) .. kStr .. ": " .. xStr .. '\n'
end
str = str .. string.rep(' ', ind) .. '}'
if ind == 0 then str = str .. '\n' end
return str
end
tableStr2 = function(t)
local str = '{'
for k in pairs(t) do
local kStr = k
local xStr = rawget(t, k)
if type(xStr) == "boolean" then
xStr = xStr and "True" or "False"
elseif type(xStr) == "string" then
xStr = '"' .. xStr .. '"'
elseif type(xStr) == "table" then
xStr = tableStr2(xStr)
end
str = str .. ' ' .. kStr .. ": " .. xStr .. ","
end
return str == '{' and "{ }" or string.sub(str, 1, string.len(str) - 1) .. " }"
end
cartProd = function(...)
local _cartProd = function(a, b)
local l = {}
for _, v1 in pairs(a) do
for _, v2 in pairs(b) do
local _v1 = v1
if type(_v1) ~= "table" then _v1 = {_v1} end
if type(v2) ~= "table" then v2 = {v2} end
l[#l + 1] = table.join(_v1, v2)
end
end
return l
end
return foldr1(_cartProd, table.pack(...))
end
lexgt = function(a, b)
for i = 1, #a do
if b[i] == nil then return 1
elseif a[i] > b[i] then return 1
elseif a[i] < b[i] then return -1
end
end
if #a < #b then return -1 end
return 0
end
revlexgt = function(a, b)
if #a > #b then return 1 end
if #a < #b then return -1 end
for i = #a, 1, -1 do
if a[i] > b[i + #b - #a] then return 1
elseif a[i] < b[i + #b - #a] then return -1
end
end
return 0
end
--main
DESC = [[
Usage: lua chords.lua filename [option]
Options:
-u: use unicode characters
-n: allow omitted extensions
-nl: allow omitted chord tones
-a: allow all avoid tones
-f: allow all chords without checking
-m: disallow all minor ninths
-s: disallow suspended chords]]
-- command line argument flags
for i = 2, #arg do
if arg[i] == "-u" then bUnicode = true
elseif arg[i] == "-n" then bNoUpper = true
elseif arg[i] == "-nl" then bNoLower = true
elseif arg[i] == "-a" then bAllowAvoid = true
elseif arg[i] == "-f" then bAllowAll = true
elseif arg[i] == "-m" then bStrict = true
elseif arg[i] == "-s" then bNoSus = true end
end
-- semitone offsets from chord root
local SEMITONES = {{0}
,{ 4, 3, 5, 2}
,{ 7, 6, 8}
,{10, 9, 11}
,{14, 13, 15}
,{17, 16, 18}
,{21, 20}}
-- chord factor strings
local CHORD_STR = {{"1"}
,{"3" , "b3" , "4" , "2"}
,{"5" , "b5" , "#5"}
,{"b7", "6" , "M7"}
,{"9" , "b9" , "#9"}
,{"11", "b11", "#11"}
,{"13", "b13"}}
if bNoUpper then for i = 5, 7 do table.insert(SEMITONES[i], -1) table.insert(CHORD_STR[i], "") end end
if bNoLower then for i = 2, 4 do table.insert(SEMITONES[i], -1) table.insert(CHORD_STR[i], "") end end
if bNoSus then
table.remove(SEMITONES[2], 4)
table.remove(SEMITONES[2], 3)
table.remove(CHORD_STR[2], 4)
table.remove(CHORD_STR[2], 3)
end
local chordStr = function(l)
local sym = "" -- chord symbol
local scale = {} -- scale table
local unalt = 4 -- extension index
-- highest extension resulting from stacking thirds
while (l[unalt] ~= -1 and unalt < 7) do unalt = unalt + 1 end
-- highest unaltered extension in the chord
while ((l[unalt] ~= SEMITONES[unalt][1] or l[unalt] == -1) and unalt > 4) do unalt = unalt - 1 end
-- do not allow abbreviated extensions sixth chords
if l[4] == 9 then unalt = 4 end
-- chord symbol for the lower structure
sym = (l[2] == 3 and "m" or "")
.. (l[4] == 11 and "M" or "")
.. (l[4] == 9 and "6" or l[4] ~= -1 and tostring(unalt * 2 - 1) or "")
.. (l[3] == 6 and "b5" or l[3] == 8 and "#5" or "")
if sym == "m6b5" then sym = "dim7" end
-- append alterations
for i = 5, 7 do
if l[i] ~= -1 then
local acc = l[i] - SEMITONES[i][1]
if acc ~= 0 or i > unalt then
-- add correct accidentals
-- acc > 0 and string.rep('#', acc) or acc < 0 and string.rep('b', -acc) or '/'
sym = sym .. (acc == 1 and '#' or acc == -1 and 'b' or '/')
.. tostring(i * 2 - 1)
end
end
end
-- special cases for the lower structure
sym = sym .. (l[2] == 5 and "sus4" or l[2] == 2 and "sus2" or "")
.. (l[2] == -1 and "no3" or "")
.. (l[3] == -1 and "no5" or "")
if sym == "" then sym = "M" end
if sym == "mb5" then sym = "dim" end
-- correctly label added chords
sym = string.gsub(sym, "^/", "add")
if foldl(function(a, b) return b ~= -1 or a end, false, table.sub(l, 5, 7)) then
sym = string.gsub(sym, "^b", "addb")
sym = string.gsub(sym, "^#", "add#")
end
if bUnicode then
sym = string.gsub(string.gsub(sym, "b", "\u{266D}"), "#", "\u{266F}")
end
-- remove omitted notes from the chord table
l = filter(function(x) return x ~= -1 end, l)
-- reduce octaves from the chord tables
scale = map(function(x) return x % 12 end, table.sub(l))
-- sort the scale table
table.sort(scale)
-- replace chord table with spellings of chord factors
local flatten = foldl(table.join, {}, CHORD_STR)
for k, v in pairs(foldl(table.join, {}, SEMITONES)) do
l = map(function(x)
return x == v and flatten[k] or x
end, l)
end
-- reorder the scale table to the largest inversion
-- scale = foldl(function(l, k)
-- local n = table.rotate(l, k)
-- n = map(function(x) return (x - n[1]) % 12 end, n)
-- return revlexgt(l, n) == 1 and l or n
-- end, scale, enum(1, #scale - 1))
-- return a 2-tuple containing formatted string and scale table
return {string.format("%-24s%-24s%s", table.concat(l, "-"), table.concat(scale, " "), sym), scale}
end
local validChord = function(l)
if bAllowAll then return true end
-- strict definition of avoid note:
-- any chord tone forming a minor ninth above another chord tone
-- but allow b9 unless M7 is present
-- also allow b13 when both b3 and b7 are present
-- but disallow minor seconds (e.g. #9 and b11, #5 and 6) and octaves (e.g. b3 and #9)
local red = map(function(x) return x % 12 end, l)
return (foldl1(function(a, b)
return a and b
end, zipWith(function(a, b)
return a == -1 or b == -1
or b - a ~= 13
or ((b == 13 and l[4] ~= 11)
or (b == 20 and l[2] == 3 and l[4] == 10))
and not bStrict
end
,table.sub(l, 1, 3), table.sub(l, 5, 7)))
and not (l[2] == 2 and l[5] == 15) -- special case
or bAllowAvoid)
and foldl1(function(a, b)
return a and b
end, zipWith(function(a, b) return a == -1 or b == -1 or b - a > 1 end
,table.sub(l, 1, 6), table.sub(l, 2, 7)))
and foldl1(function(t, x)
return t and (l[x] == -1 or not elem(table.sub(red, 1, x - 1), red[x]))
end, enum(1, 7))
end
local getChords = function()
return map(chordStr, filter(validChord, cartProd(table.unpack(SEMITONES))))
end
if not arg[1] then
print(DESC)
else
local ALL_CHORDS = {}
for i = 7, bAllowAll and 2 or 3, -1 do
if SEMITONES[i][#SEMITONES[i]] == -1 then table.remove(SEMITONES[i]) end
local chordTable = getChords()
-- this sorts the chords by the reduced scale
table.sort(chordTable, function(x, y) return lexgt(x[2], y[2]) == -1 end)
file = io.open(arg[1] .. "_" .. tostring(i * 2 - 1) .. ".txt", "w")
file:write(tableStr(map(function(x) return x[1] end, chordTable)))
file:close()
ALL_CHORDS = table.join(chordTable, ALL_CHORDS)
SEMITONES[i] = {-1}
end
-- this sorts the chords by the reduced scale
table.sort(ALL_CHORDS, function(x, y) if #x[2] > #y[2] then return false end if #y[2] > #x[2] then return true end return lexgt(x[2], y[2]) == -1 end)
-- this sorts the chords by the chord name
-- table.sort(ALL_CHORDS, function(x, y) return string.sub(x[1], 49) < string.sub(y[1], 49) end)
file = io.open(arg[1] .. "_all.txt", "w")
file:write(tableStr(map(function(x) return x[1] end, ALL_CHORDS)))
file:close()
end
Comments
0 B
|👍
/👎
0 B
|0 👍
/0 👎
0 B
|0 👍
/0 👎