Module:ChordVoicings: Difference between revisions
Vibe coded v1.1 |
No edit summary |
||
| Line 1: | Line 1: | ||
-- Module:ChordVoicings ( | -- Module:ChordVoicings (hardened against CPU limit / signal 24) | ||
-- Usage: {{ChordVoicingTable|rchord=4:5:6:7}} | -- Usage: {{ChordVoicingTable|rchord=4:5:6:7}} | ||
| Line 21: | Line 21: | ||
for i=2,#t do g = gcd(g, t[i]) end | for i=2,#t do g = gcd(g, t[i]) end | ||
return g | return g | ||
end | end | ||
| Line 37: | Line 26: | ||
while n % 2 == 0 do n = math.floor(n/2) end | while n % 2 == 0 do n = math.floor(n/2) end | ||
return n | return n | ||
end | end | ||
| Line 54: | Line 38: | ||
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 61: | Line 46: | ||
end | end | ||
return mx/mn | return mx/mn | ||
end | |||
-- memoized reduction | |||
local reduce_cache = {} | |||
local function key_of(t) return sort_key(t) end | |||
local function clone(t) | |||
local u = {} | |||
for i=1,#t do u[i]=t[i] end | |||
return u | |||
end | |||
local function reduce_irreducible(t) | |||
local k = key_of(t) | |||
local cached = reduce_cache[k] | |||
if cached then return cached end | |||
local g = gcd_list(t) | |||
local u | |||
if g>1 then | |||
u = {} | |||
for i=1,#t do u[i]=math.floor(t[i]/g) end | |||
else | |||
u = clone(t) | |||
end | |||
reduce_cache[k] = u | |||
return u | |||
end | end | ||
| Line 70: | Line 79: | ||
u[#u+1] = t[1]*4 | u[#u+1] = t[1]*4 | ||
return reduce_irreducible(u) | return reduce_irreducible(u) | ||
end | |||
local function toodds(enum) | |||
local odds = {} | |||
for i=1,#enum do odds[i] = odd_part(enum[i]) end | |||
return odds | |||
end | end | ||
| Line 78: | Line 93: | ||
end | end | ||
-- | -- subsets | ||
local function all_nonempty_subsets(arr) | local function all_nonempty_subsets(arr) | ||
local res = {} | local res = {} | ||
| Line 90: | Line 105: | ||
return | return | ||
end | end | ||
rec(i+1, cur) | rec(i+1, cur) -- exclude | ||
cur[#cur+1] = arr[i] | cur[#cur+1] = arr[i]; rec(i+1, cur); cur[#cur] = nil -- include | ||
end | end | ||
rec(1, {}) | rec(1, {}) | ||
| Line 136: | Line 149: | ||
local function bold_italic(s) return "'''''" .. s .. "'''''" end | local function bold_italic(s) return "'''''" .. s .. "'''''" end | ||
-- ---------- | -- ---------- hardened rotation cycle with cap ---------- | ||
-- | |||
- | |||
local function build_rotation_map(start_vec) | local function build_rotation_map(start_vec) | ||
local map = {} | local map = {} | ||
local seen = {} | local seen = {} | ||
local v = reduce_irreducible(start_vec) | local v = reduce_irreducible(start_vec) | ||
local | local steps, MAX = 0, 512 -- hard cap to avoid CPU overruns | ||
while true do | while true do | ||
local fodd = odd_part(v[1]) | local fodd = odd_part(v[1]) | ||
if not map[fodd] then map[fodd] = v end | if not map[fodd] then map[fodd] = v end | ||
local sig = key_of(v) | |||
if seen[sig] then break end | |||
seen[sig] = true | |||
local nextv = rot2_once(v) | local nextv = rot2_once(v) | ||
steps = steps + 1 | |||
if | if steps > MAX then break end | ||
v = nextv | v = nextv | ||
end | end | ||
| Line 170: | Line 186: | ||
local cls = args.class or "wikitable sortable" | local cls = args.class or "wikitable sortable" | ||
-- parse & sanity | |||
local enum = split_enum(rchord_str) | local enum = split_enum(rchord_str) | ||
if #enum < 3 then | if #enum < 3 then | ||
return "<strong class=\"error\">rchord must have at least 3 integers</strong>" | return "<strong class=\"error\">rchord must have at least 3 integers</strong>" | ||
end | |||
-- optional global guard for pathological inputs | |||
if #enum > 7 then | |||
return "<strong class=\"error\">rchord too large for on-wiki render (max 7 terms)</strong>" | |||
end | end | ||
| Line 179: | Line 200: | ||
local odd_rows = unique_in_order(odds) | local odd_rows = unique_in_order(odds) | ||
-- columns: Root + subsets of non-root odds | |||
local nonroot_odds_unique = {} | local nonroot_odds_unique = {} | ||
for _,o in ipairs(odd_rows) do | for _,o in ipairs(odd_rows) do | ||
| Line 185: | Line 207: | ||
local col_specs = subset_labels(nonroot_odds_unique) -- {text,set} | local col_specs = subset_labels(nonroot_odds_unique) -- {text,set} | ||
-- trivial case: if there are no non-root odds, only the Root column exists | |||
if #col_specs == 1 then | |||
local only = reduce_irreducible(enum) | |||
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) | |||
out[#out+1] = string.format('! style="width:%s;" | Root', width) | |||
for _,rowodd in ipairs(odd_rows) do | |||
out[#out+1] = "|-" | |||
out[#out+1] = string.format('! style="width:%s;" | On %s', width, tostring(rowodd)) | |||
local rotated_map = build_rotation_map(enum) | |||
local r = rotated_map[rowodd] or reduce_irreducible(enum) | |||
local disp = to_str(r) | |||
local shown = (rowodd == root_odd) and bold_italic(disp) or disp | |||
out[#out+1] = string.format('| data-sort-value="%s" | %s', sort_key(r), shown) | |||
end | |||
out[#out+1] = "|}" | |||
out[#out+1] = "Enumerations in italics map to an existing octave-reduced rotation." | |||
return table.concat(out, "\n") | |||
end | |||
-- header | |||
local out = {} | local out = {} | ||
out[#out+1] = "== " .. title .. " ==" | out[#out+1] = "== " .. title .. " ==" | ||
| Line 194: | Line 242: | ||
end | end | ||
-- Precompute rotation | -- Precompute one rotation map per column (voicing) | ||
local rotation_maps = {} | local rotation_maps = {} | ||
for c, spec in ipairs(col_specs) do | for c, spec in ipairs(col_specs) do | ||
| Line 203: | Line 251: | ||
end | end | ||
-- Emit rows | -- Emit rows | ||
for _,rowodd in ipairs(odd_rows) do | for _,rowodd in ipairs(odd_rows) do | ||
out[#out+1] = "|-" | out[#out+1] = "|-" | ||
| Line 210: | Line 258: | ||
for c, spec in ipairs(col_specs) do | for c, spec in ipairs(col_specs) do | ||
local rotated = rotation_maps[c][rowodd] | local rotated = rotation_maps[c][rowodd] | ||
if not rotated then | if not rotated then | ||
-- fallback (rare): use reduced base voicing | |||
local chosen = {} | local chosen = {} | ||
for _,od in ipairs(spec.set) do chosen[od] = true end | for _,od in ipairs(spec.set) do chosen[od] = true end | ||
rotated = apply_octaves(enum, odds, chosen) | rotated = reduce_irreducible(apply_octaves(enum, odds, chosen)) | ||
end | end | ||