Module:ChordVoicings: Difference between revisions
Jump to navigation
Jump to search
Vibe coded v1.1 |
No edit summary |
||
| (2 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
-- Module:ChordVoicings ( | -- Module:ChordVoicings (ultra-lean, hard-capped) | ||
-- | -- Works fast on small chords (n ≤ 5). Rotations per column ≤ n. Columns ≤ 2^(n-1). | ||
local p = {} | local p = {} | ||
-- ---------- | -- ---------- tiny utils ---------- | ||
local function split_enum(s) | local function split_enum(s) local t={} for num in string.gmatch(s or "", "%d+") do t[#t+1]=tonumber(num) end; return t end | ||
local function gcd(a,b) while b~=0 do a,b=a%b,b end; return math.abs(a) end | |||
local function reduce(t) local g=t[1] for i=2,#t do g=gcd(g,t[i]) end; if g>1 then local u={} for i=1,#t do u[i]=t[i]/g end; return u end; local u={} for i=1,#t do u[i]=t[i] end; return u end | |||
local function odd(n) while n%2==0 do n=math.floor(n/2) end; return n end | |||
local function odds_of(t) local o={} for i=1,#t do o[i]=odd(t[i]) end; return o end | |||
local function unique_in_order(t) local seen,out={},{} for _,x in ipairs(t) do if not seen[x] then seen[x]=true; out[#out+1]=x end end; return out end | |||
end | local function to_str(t) local p={} for i=1,#t do p[i]=tostring(t[i]) end; return table.concat(p,":") end | ||
local function sort_key(t) local p={} for i=1,#t do p[i]=string.format("%03d", t[i]) end; return table.concat(p,"-") end | |||
local function span_ge_4(t) local mn, mx=t[1], t[1] for i=2,#t do if t[i]<mn then mn=t[i] end; if t[i]>mx then mx=t[i] end end; return (mx/mn) >= 4.0 end | |||
local function italic(s) return "''"..s.."''" end | |||
local function boldital(s) return "'''''"..s.."'''''" end | |||
local function rot2_once(v) local n=#v; local u={} for i=2,n do u[#u+1]=v[i] end; u[#u+1]=v[1]*4; return reduce(u) end | |||
-- apply octave raises for chosen odds (set is {odd -> true}) | |||
local function apply_octaves(enum, odds, chosen) | |||
local out={} for i=1,#enum do local x=enum[i]; if chosen[odds[i]] then x=x*2 end; out[i]=x end; return out | |||
local function | |||
end | end | ||
local function | -- Build rotation map with ≤ n steps, covering only wanted row odds | ||
local function rotation_map_capped(start_vec, want_set, n) | |||
local map = {} | |||
local v = reduce(start_vec) | |||
local | for step=1,n do | ||
local f = odd(v[1]) | |||
if want_set[f] and not map[f] then | |||
map[f] = v | |||
-- early exit: have we covered all wanted odds? | |||
local ok=true | |||
for w,_ in pairs(want_set) do if not map[w] then ok=false; break end end | |||
if ok then break end | |||
end | end | ||
return | if step==n then break end | ||
v = rot2_once(v) | |||
end | |||
return map | |||
end | end | ||
-- | -- Deterministic, non-recursive subset listing for k ≤ 4 | ||
local function | local function subset_columns(nonroot_sorted, n_total) | ||
-- total columns ≤ 2^(n-1). We’ll emit in size order: 0 (Root), 1-sets, 2-sets, 3-sets, 4-sets… | |||
local cols = { {text="Root", set={}} } | |||
local cap = math.floor(2^(n_total-1)) - 1 -- non-root subsets cap | |||
local k = #nonroot_sorted | |||
if cap <= 0 or k == 0 then return cols end | |||
end | |||
-- size-1 | |||
for i=1,math.min(k,cap>0 and k or 0) do | |||
cols[#cols+1] = { text="'"..nonroot_sorted[i], set={nonroot_sorted[i]} } | |||
end | |||
end | local used = math.min(k, cap) | ||
if used >= cap then return cols end | |||
-- | -- size-2 (i<j) | ||
for i=1,k-1 do | |||
for j=i+1,k do | |||
cols[#cols+1] = { text="'"..nonroot_sorted[i].."'"..nonroot_sorted[j], set={nonroot_sorted[i], nonroot_sorted[j]} } | |||
used = used + 1 | |||
if used >= cap then return cols end | |||
end | end | ||
end | |||
end | |||
-- size-3 (i<j<l) | |||
for i=1,k-2 do | |||
for i, | for j=i+1,k-1 do | ||
for l=j+1,k do | |||
cols[#cols+1] = { text="'"..nonroot_sorted[i].."'"..nonroot_sorted[j].."'"..nonroot_sorted[l], set={nonroot_sorted[i], nonroot_sorted[j], nonroot_sorted[l]} } | |||
used = used + 1 | |||
if used >= cap then return cols end | |||
end | |||
end | |||
end | |||
-- size-4 (i<j<l<m) — practically never hit for n≤5, but included | |||
for i=1,k-3 do | |||
for j=i+1,k-2 do | |||
for | for l=j+1,k-1 do | ||
for m=l+1,k do | |||
cols[#cols+1] = { text="'"..nonroot_sorted[i].."'"..nonroot_sorted[j].."'"..nonroot_sorted[l].."'"..nonroot_sorted[m], set={nonroot_sorted[i], nonroot_sorted[j], nonroot_sorted[l], nonroot_sorted[m]} } | |||
used = used + 1 | |||
if used >= cap then return cols end | |||
end | end | ||
end | |||
end | end | ||
end | |||
end | |||
return cols | |||
end | end | ||
function p.render(frame) | |||
local args = frame.args or frame:getParent().args | |||
local rchord_str = (args.rchord or args[1] or ""):gsub("%s+","") | |||
if rchord_str == "" then return '<strong class="error">Missing parameter: rchord</strong>' end | |||
local enum = split_enum(rchord_str) | |||
local n = #enum | |||
if n < 3 then return '<strong class="error">rchord must have at least 3 integers</strong>' end | |||
local | |||
local width = args.width or "120px" | |||
local title = args.title or "Voicings and rotations around two octaves" | |||
local cls = args.class or "wikitable sortable" | |||
local odds = odds_of(enum) | |||
local root_odd = odd(enum[1]) | |||
local odd_rows = unique_in_order(odds) | |||
-- rows we actually need | |||
local want = {}; for _,o in ipairs(odd_rows) do want[o]=true end | |||
-- non-root odds, sorted | |||
local nonroot = {} | |||
for _,o in ipairs(odd_rows) do if o ~= root_odd then nonroot[#nonroot+1] = o end end | |||
table.sort(nonroot) | |||
-- columns (Root + capped subsets) | |||
local cols = subset_columns(nonroot, n) | |||
-- header (no external template calls in header to keep it cheap) | |||
local out = {} | |||
out[#out+1] = "== " .. title .. " ==" | |||
out[#out+1] = string.format('{| class="%s" style="text-align:center;"', cls) | |||
out[#out+1] = "|+" | |||
out[#out+1] = string.format('! style="width:%s;" | Rotation/Voicing', width) | |||
for _,c in ipairs(cols) do | |||
out[#out+1] = string.format('! style="width:%s;" | %s', width, c.text) | |||
end | |||
-- precompute rotation maps for each column, with ≤ n rotations | |||
local rotation_maps = {} | |||
for i,c in ipairs(cols) do | |||
local chosen = {} | |||
for _,odv in ipairs(c.set) do chosen[odv]=true end | |||
for _, | local base = apply_octaves(enum, odds, chosen) | ||
rotation_maps[i] = rotation_map_capped(base, want, n) | |||
end | |||
-- rows | |||
for _,rowodd in ipairs(odd_rows) do | |||
for c | out[#out+1] = "|-" | ||
out[#out+1] = string.format('! style="width:%s;" | On %s', width, tostring(rowodd)) | |||
for i,c in ipairs(cols) do | |||
local r = rotation_maps[i][rowodd] | |||
if not r then -- capped cycle didn't hit this odd; show reduced base | |||
local chosen = {} | local chosen = {} | ||
for _, | for _,odv in ipairs(c.set) do chosen[odv]=true end | ||
r = reduce(apply_octaves(enum, odds, chosen)) | |||
end | |||
local s = to_str(r) | |||
local shown | |||
if rowodd == root_odd and #c.set == 0 then | |||
shown = boldital(s) | |||
elseif not span_ge_4(r) then | |||
shown = italic(s) | |||
else | |||
shown = s | |||
end | |||
out[#out+1] = string.format('| data-sort-value="%s" | %s', sort_key(r), shown) | |||
end | end | ||
end | |||
out[#out+1] = "|}" | |||
out[#out+1] = "Enumerations in italics map to an existing octave-reduced rotation." | |||
return table.concat(out, "\n") | |||
end | end | ||
return p | return p | ||
Latest revision as of 13:36, 11 November 2025
Documentation for this module may be created at Module:ChordVoicings/doc
-- Module:ChordVoicings (ultra-lean, hard-capped)
-- Works fast on small chords (n ≤ 5). Rotations per column ≤ n. Columns ≤ 2^(n-1).
local p = {}
-- ---------- tiny utils ----------
local function split_enum(s) local t={} for num in string.gmatch(s or "", "%d+") do t[#t+1]=tonumber(num) end; return t end
local function gcd(a,b) while b~=0 do a,b=a%b,b end; return math.abs(a) end
local function reduce(t) local g=t[1] for i=2,#t do g=gcd(g,t[i]) end; if g>1 then local u={} for i=1,#t do u[i]=t[i]/g end; return u end; local u={} for i=1,#t do u[i]=t[i] end; return u end
local function odd(n) while n%2==0 do n=math.floor(n/2) end; return n end
local function odds_of(t) local o={} for i=1,#t do o[i]=odd(t[i]) end; return o end
local function unique_in_order(t) local seen,out={},{} for _,x in ipairs(t) do if not seen[x] then seen[x]=true; out[#out+1]=x end end; return out end
local function to_str(t) local p={} for i=1,#t do p[i]=tostring(t[i]) end; return table.concat(p,":") end
local function sort_key(t) local p={} for i=1,#t do p[i]=string.format("%03d", t[i]) end; return table.concat(p,"-") end
local function span_ge_4(t) local mn, mx=t[1], t[1] for i=2,#t do if t[i]<mn then mn=t[i] end; if t[i]>mx then mx=t[i] end end; return (mx/mn) >= 4.0 end
local function italic(s) return "''"..s.."''" end
local function boldital(s) return "'''''"..s.."'''''" end
local function rot2_once(v) local n=#v; local u={} for i=2,n do u[#u+1]=v[i] end; u[#u+1]=v[1]*4; return reduce(u) end
-- apply octave raises for chosen odds (set is {odd -> true})
local function apply_octaves(enum, odds, chosen)
local out={} for i=1,#enum do local x=enum[i]; if chosen[odds[i]] then x=x*2 end; out[i]=x end; return out
end
-- Build rotation map with ≤ n steps, covering only wanted row odds
local function rotation_map_capped(start_vec, want_set, n)
local map = {}
local v = reduce(start_vec)
for step=1,n do
local f = odd(v[1])
if want_set[f] and not map[f] then
map[f] = v
-- early exit: have we covered all wanted odds?
local ok=true
for w,_ in pairs(want_set) do if not map[w] then ok=false; break end end
if ok then break end
end
if step==n then break end
v = rot2_once(v)
end
return map
end
-- Deterministic, non-recursive subset listing for k ≤ 4
local function subset_columns(nonroot_sorted, n_total)
-- total columns ≤ 2^(n-1). We’ll emit in size order: 0 (Root), 1-sets, 2-sets, 3-sets, 4-sets…
local cols = { {text="Root", set={}} }
local cap = math.floor(2^(n_total-1)) - 1 -- non-root subsets cap
local k = #nonroot_sorted
if cap <= 0 or k == 0 then return cols end
-- size-1
for i=1,math.min(k,cap>0 and k or 0) do
cols[#cols+1] = { text="'"..nonroot_sorted[i], set={nonroot_sorted[i]} }
end
local used = math.min(k, cap)
if used >= cap then return cols end
-- size-2 (i<j)
for i=1,k-1 do
for j=i+1,k do
cols[#cols+1] = { text="'"..nonroot_sorted[i].."'"..nonroot_sorted[j], set={nonroot_sorted[i], nonroot_sorted[j]} }
used = used + 1
if used >= cap then return cols end
end
end
-- size-3 (i<j<l)
for i=1,k-2 do
for j=i+1,k-1 do
for l=j+1,k do
cols[#cols+1] = { text="'"..nonroot_sorted[i].."'"..nonroot_sorted[j].."'"..nonroot_sorted[l], set={nonroot_sorted[i], nonroot_sorted[j], nonroot_sorted[l]} }
used = used + 1
if used >= cap then return cols end
end
end
end
-- size-4 (i<j<l<m) — practically never hit for n≤5, but included
for i=1,k-3 do
for j=i+1,k-2 do
for l=j+1,k-1 do
for m=l+1,k do
cols[#cols+1] = { text="'"..nonroot_sorted[i].."'"..nonroot_sorted[j].."'"..nonroot_sorted[l].."'"..nonroot_sorted[m], set={nonroot_sorted[i], nonroot_sorted[j], nonroot_sorted[l], nonroot_sorted[m]} }
used = used + 1
if used >= cap then return cols end
end
end
end
end
return cols
end
function p.render(frame)
local args = frame.args or frame:getParent().args
local rchord_str = (args.rchord or args[1] or ""):gsub("%s+","")
if rchord_str == "" then return '<strong class="error">Missing parameter: rchord</strong>' end
local enum = split_enum(rchord_str)
local n = #enum
if n < 3 then return '<strong class="error">rchord must have at least 3 integers</strong>' end
local width = args.width or "120px"
local title = args.title or "Voicings and rotations around two octaves"
local cls = args.class or "wikitable sortable"
local odds = odds_of(enum)
local root_odd = odd(enum[1])
local odd_rows = unique_in_order(odds)
-- rows we actually need
local want = {}; for _,o in ipairs(odd_rows) do want[o]=true end
-- non-root odds, sorted
local nonroot = {}
for _,o in ipairs(odd_rows) do if o ~= root_odd then nonroot[#nonroot+1] = o end end
table.sort(nonroot)
-- columns (Root + capped subsets)
local cols = subset_columns(nonroot, n)
-- header (no external template calls in header to keep it cheap)
local out = {}
out[#out+1] = "== " .. title .. " =="
out[#out+1] = string.format('{| class="%s" style="text-align:center;"', cls)
out[#out+1] = "|+"
out[#out+1] = string.format('! style="width:%s;" | Rotation/Voicing', width)
for _,c in ipairs(cols) do
out[#out+1] = string.format('! style="width:%s;" | %s', width, c.text)
end
-- precompute rotation maps for each column, with ≤ n rotations
local rotation_maps = {}
for i,c in ipairs(cols) do
local chosen = {}
for _,odv in ipairs(c.set) do chosen[odv]=true end
local base = apply_octaves(enum, odds, chosen)
rotation_maps[i] = rotation_map_capped(base, want, n)
end
-- rows
for _,rowodd in ipairs(odd_rows) do
out[#out+1] = "|-"
out[#out+1] = string.format('! style="width:%s;" | On %s', width, tostring(rowodd))
for i,c in ipairs(cols) do
local r = rotation_maps[i][rowodd]
if not r then -- capped cycle didn't hit this odd; show reduced base
local chosen = {}
for _,odv in ipairs(c.set) do chosen[odv]=true end
r = reduce(apply_octaves(enum, odds, chosen))
end
local s = to_str(r)
local shown
if rowodd == root_odd and #c.set == 0 then
shown = boldital(s)
elseif not span_ge_4(r) then
shown = italic(s)
else
shown = s
end
out[#out+1] = string.format('| data-sort-value="%s" | %s', sort_key(r), shown)
end
end
out[#out+1] = "|}"
out[#out+1] = "Enumerations in italics map to an existing octave-reduced rotation."
return table.concat(out, "\n")
end
return p