Module:ChordVoicings: Difference between revisions
Jump to navigation
Jump to search
Birth [Vibe coded v1] |
Vibe coded v1.1 |
||
| Line 1: | Line 1: | ||
-- Module:ChordVoicings ( | -- Module:ChordVoicings (optimized to avoid CPU limit) | ||
-- Usage | -- Usage: {{ChordVoicingTable|rchord=4:5:6:7}} | ||
local p = {} | local p = {} | ||
-- -------- utilities -------- | -- ---------- utilities ---------- | ||
local function split_enum(s) | local function split_enum(s) | ||
local t = {} | local t = {} | ||
| Line 22: | Line 22: | ||
return g | return g | ||
end | end | ||
local function reduce_irreducible(t) | local function reduce_irreducible(t) | ||
local g = gcd_list(t) | local g = gcd_list(t) | ||
| Line 42: | Line 29: | ||
return u | return u | ||
end | end | ||
return | local u = {} | ||
for i=1,#t do u[i]=t[i] end | |||
return u | |||
end | |||
local function odd_part(n) | |||
while n % 2 == 0 do n = math.floor(n/2) end | |||
return n | |||
end | |||
local function toodds(enum) | |||
local odds = {} | |||
for i=1,#enum do odds[i] = odd_part(enum[i]) end | |||
return odds | |||
end | end | ||
| Line 50: | Line 49: | ||
return table.concat(parts, ":") | return table.concat(parts, ":") | ||
end | end | ||
local function sort_key(t) | local function sort_key(t) | ||
local parts = {} | local parts = {} | ||
| Line 56: | Line 54: | ||
return table.concat(parts, "-") | return table.concat(parts, "-") | ||
end | end | ||
local function span_ratio(t) | local function span_ratio(t) | ||
local mn, mx = t[1], t[1] | local mn, mx = t[1], t[1] | ||
| Line 63: | Line 60: | ||
if t[i]>mx then mx=t[i] end | if t[i]>mx then mx=t[i] end | ||
end | end | ||
return mx / mn | return mx/mn | ||
end | end | ||
-- | -- two-octave rotation step | ||
local function rot2_once(t) | local function rot2_once(t) | ||
local n = #t | local n = #t | ||
| Line 73: | Line 70: | ||
u[#u+1] = t[1]*4 | u[#u+1] = t[1]*4 | ||
return reduce_irreducible(u) | return reduce_irreducible(u) | ||
end | end | ||
local function unique_in_order(t) | local function unique_in_order(t) | ||
local seen, out = {}, {} | local seen, out = {}, {} | ||
for _,x in ipairs(t) do | for _,x in ipairs(t) do if not seen[x] then seen[x]=true; out[#out+1]=x end end | ||
return out | return out | ||
end | end | ||
-- generate all non-empty subsets | -- generate all non-empty subsets (small n only) | ||
local function all_nonempty_subsets(arr) | local function all_nonempty_subsets(arr) | ||
local res = {} | local res = {} | ||
| Line 111: | Line 90: | ||
return | return | ||
end | end | ||
rec(i+1, cur) | rec(i+1, cur) | ||
cur[#cur+1] = arr[i] | cur[#cur+1] = arr[i] | ||
rec(i+1, cur) | rec(i+1, cur) | ||
| Line 123: | Line 100: | ||
local function subset_labels(nonroot_odds) | local function subset_labels(nonroot_odds) | ||
local nums = {} | local nums = {} | ||
for i,x in ipairs(nonroot_odds) do nums[i]=x end | for i,x in ipairs(nonroot_odds) do nums[i]=x end | ||
| Line 132: | Line 108: | ||
local subsets = all_nonempty_subsets(nums) | local subsets = all_nonempty_subsets(nums) | ||
table.sort(subsets, function(a,b) | table.sort(subsets, function(a,b) | ||
if #a ~= #b then return #a < #b end | if #a ~= #b then return #a < #b end | ||
| Line 145: | Line 120: | ||
labels[#labels+1] = { text=table.concat(parts, ""), set=set } | labels[#labels+1] = { text=table.concat(parts, ""), set=set } | ||
end | end | ||
return labels | return labels | ||
end | end | ||
local function apply_octaves(enum, odds, chosen_set) | local function apply_octaves(enum, odds, chosen_set) | ||
local out = {} | local out = {} | ||
for i=1,#enum do | for i=1,#enum do | ||
| Line 160: | Line 134: | ||
local function italicize(s) return "''" .. s .. "''" end | local function italicize(s) return "''" .. s .. "''" end | ||
local function bold_italic(s) return "'''''" .. s .. "'''''" end | local function bold_italic(s) return "'''''" .. s .. "'''''" end | ||
-- -------- main render -------- | -- ---------- key optimization ---------- | ||
-- For a given voicing vector V, compute the entire rotation cycle once: | |||
-- keep a map: firstOdd(V_k[1]) -> V_k for every step k in the cycle. | |||
local function build_rotation_map(start_vec) | |||
local map = {} | |||
local seen = {} | |||
local function sig(t) return sort_key(t) end -- signature for cycle detection | |||
local v = reduce_irreducible(start_vec) | |||
local first = sig(v) | |||
while true do | |||
local fodd = odd_part(v[1]) | |||
if not map[fodd] then map[fodd] = v end | |||
seen[sig(v)] = true | |||
local nextv = rot2_once(v) | |||
local id = sig(nextv) | |||
if seen[id] then break end | |||
v = nextv | |||
end | |||
return map | |||
end | |||
-- ---------- main render ---------- | |||
function p.render(frame) | function p.render(frame) | ||
local args = frame.args or frame:getParent().args | local args = frame.args or frame:getParent().args | ||
| Line 180: | Line 175: | ||
end | end | ||
local odds = toodds(enum) | local odds = toodds(enum) | ||
local root_odd = odd_part(enum[1]) | local root_odd = odd_part(enum[1]) | ||
local odd_rows = unique_in_order(odds) | local odd_rows = unique_in_order(odds) | ||
local nonroot_odds_unique = {} | local nonroot_odds_unique = {} | ||
for _,o in ipairs(odd_rows) do | for _,o in ipairs(odd_rows) do | ||
if o ~= root_odd then nonroot_odds_unique[#nonroot_odds_unique+1] = o end | if o ~= root_odd then nonroot_odds_unique[#nonroot_odds_unique+1] = o end | ||
end | end | ||
local col_specs | local col_specs = subset_labels(nonroot_odds_unique) -- {text,set} | ||
local out = {} | local out = {} | ||
| Line 201: | Line 194: | ||
end | end | ||
-- rows | -- Precompute rotation maps for every column (voicing) | ||
local rotation_maps = {} | |||
for c, spec in ipairs(col_specs) do | |||
local chosen = {} | |||
for _,od in ipairs(spec.set) do chosen[od] = true end | |||
local base_voicing = apply_octaves(enum, odds, chosen) | |||
rotation_maps[c] = build_rotation_map(base_voicing) | |||
end | |||
-- Emit rows: each cell becomes O(1) lookup | |||
for _,rowodd in ipairs(odd_rows) do | for _,rowodd in ipairs(odd_rows) do | ||
out[#out+1] = "|-" | out[#out+1] = "|-" | ||
out[#out+1] = string.format('! style="width:%s;" | On %s', width, tostring(rowodd)) | out[#out+1] = string.format('! style="width:%s;" | On %s', width, tostring(rowodd)) | ||
for | for c, spec in ipairs(col_specs) do | ||
-- | local rotated = rotation_maps[c][rowodd] | ||
-- In unusual cases where a given odd isn't in the cycle, fall back to the base voicing. | |||
if not rotated then | |||
local fallback = {} | |||
for _,od in ipairs(spec.set) do end -- noop, just to keep structure similar | |||
local chosen = {} | |||
for _,od in ipairs(spec.set) do chosen[od] = true end | |||
rotated = apply_octaves(enum, odds, chosen) | |||
rotated = reduce_irreducible(rotated) | |||
end | |||
local disp = to_str(rotated) | local disp = to_str(rotated) | ||
Revision as of 13:10, 11 November 2025
Documentation for this module may be created at Module:ChordVoicings/doc
-- Module:ChordVoicings (optimized to avoid CPU limit)
-- Usage: {{ChordVoicingTable|rchord=4:5:6:7}}
local p = {}
-- ---------- utilities ----------
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 gcd_list(t)
local g = t[1]
for i=2,#t do g = gcd(g, t[i]) end
return g
end
local function reduce_irreducible(t)
local g = gcd_list(t)
if g>1 then
local u = {}
for i=1,#t do u[i]=math.floor(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_part(n)
while n % 2 == 0 do n = math.floor(n/2) end
return n
end
local function toodds(enum)
local odds = {}
for i=1,#enum do odds[i] = odd_part(enum[i]) end
return odds
end
local function to_str(t)
local parts = {}
for i=1,#t do parts[i]=tostring(t[i]) end
return table.concat(parts, ":")
end
local function sort_key(t)
local parts = {}
for i=1,#t do parts[i]=string.format("%03d", t[i]) end
return table.concat(parts, "-")
end
local function span_ratio(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
end
-- two-octave rotation step
local function rot2_once(t)
local n = #t
local u = {}
for i=2,n do u[#u+1] = t[i] end
u[#u+1] = t[1]*4
return reduce_irreducible(u)
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
-- generate all non-empty subsets (small n only)
local function all_nonempty_subsets(arr)
local res = {}
local function rec(i, cur)
if i > #arr then
if #cur > 0 then
local add = {}
for k=1,#cur do add[k]=cur[k] end
res[#res+1] = add
end
return
end
rec(i+1, cur)
cur[#cur+1] = arr[i]
rec(i+1, cur)
cur[#cur] = nil
end
rec(1, {})
return res
end
local function subset_labels(nonroot_odds)
local nums = {}
for i,x in ipairs(nonroot_odds) do nums[i]=x end
table.sort(nums)
local labels = {}
labels[#labels+1] = { text="Root", set={} }
local subsets = all_nonempty_subsets(nums)
table.sort(subsets, function(a,b)
if #a ~= #b then return #a < #b end
for i=1,math.min(#a,#b) do
if a[i] ~= b[i] then return a[i] < b[i] end
end
return #a < #b
end)
for _,set in ipairs(subsets) do
local parts={}
for i,od in ipairs(set) do parts[i] = "'" .. tostring(od) end
labels[#labels+1] = { text=table.concat(parts, ""), set=set }
end
return labels
end
local function apply_octaves(enum, odds, chosen_set)
local out = {}
for i=1,#enum do
local v = enum[i]
if chosen_set[odds[i]] then v = v * 2 end
out[i] = v
end
return out
end
local function italicize(s) return "''" .. s .. "''" end
local function bold_italic(s) return "'''''" .. s .. "'''''" end
-- ---------- key optimization ----------
-- For a given voicing vector V, compute the entire rotation cycle once:
-- keep a map: firstOdd(V_k[1]) -> V_k for every step k in the cycle.
local function build_rotation_map(start_vec)
local map = {}
local seen = {}
local function sig(t) return sort_key(t) end -- signature for cycle detection
local v = reduce_irreducible(start_vec)
local first = sig(v)
while true do
local fodd = odd_part(v[1])
if not map[fodd] then map[fodd] = v end
seen[sig(v)] = true
local nextv = rot2_once(v)
local id = sig(nextv)
if seen[id] then break end
v = nextv
end
return map
end
-- ---------- main render ----------
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 width = args.width or "120px"
local title = args.title or "Voicings and rotations around two octaves"
local cls = args.class or "wikitable sortable"
local enum = split_enum(rchord_str)
if #enum < 3 then
return "<strong class=\"error\">rchord must have at least 3 integers</strong>"
end
local odds = toodds(enum)
local root_odd = odd_part(enum[1])
local odd_rows = unique_in_order(odds)
local nonroot_odds_unique = {}
for _,o in ipairs(odd_rows) do
if o ~= root_odd then nonroot_odds_unique[#nonroot_odds_unique+1] = o end
end
local col_specs = subset_labels(nonroot_odds_unique) -- {text,set}
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;" {{diagonal split header|Rotation|Voicing}}', width)
for _,spec in ipairs(col_specs) do
out[#out+1] = string.format('! style="width:%s;" | %s', width, spec.text)
end
-- Precompute rotation maps for every column (voicing)
local rotation_maps = {}
for c, spec in ipairs(col_specs) do
local chosen = {}
for _,od in ipairs(spec.set) do chosen[od] = true end
local base_voicing = apply_octaves(enum, odds, chosen)
rotation_maps[c] = build_rotation_map(base_voicing)
end
-- Emit rows: each cell becomes O(1) lookup
for _,rowodd in ipairs(odd_rows) do
out[#out+1] = "|-"
out[#out+1] = string.format('! style="width:%s;" | On %s', width, tostring(rowodd))
for c, spec in ipairs(col_specs) do
local rotated = rotation_maps[c][rowodd]
-- In unusual cases where a given odd isn't in the cycle, fall back to the base voicing.
if not rotated then
local fallback = {}
for _,od in ipairs(spec.set) do end -- noop, just to keep structure similar
local chosen = {}
for _,od in ipairs(spec.set) do chosen[od] = true end
rotated = apply_octaves(enum, odds, chosen)
rotated = reduce_irreducible(rotated)
end
local disp = to_str(rotated)
local sortv = sort_key(rotated)
local shown
local is_two_octaves = (span_ratio(rotated) >= 4.0)
if rowodd == root_odd and #spec.set == 0 then
shown = bold_italic(disp)
elseif not is_two_octaves then
shown = italicize(disp)
else
shown = disp
end
out[#out+1] = string.format('| data-sort-value="%s" | %s', sortv, 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