Module:ChordVoicings: Difference between revisions

Eufalesio (talk | contribs)
Vibe coded v1.1
Eufalesio (talk | contribs)
No edit summary
Line 1: Line 1:
-- Module:ChordVoicings (optimized to avoid CPU limit)
-- 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
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
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
local function toodds(enum)
    local odds = {}
    for i=1,#enum do odds[i] = odd_part(enum[i]) end
    return odds
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


-- generate all non-empty subsets (small n only)
-- 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
        rec(i+1, cur)
        cur[#cur] = nil
     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


-- ---------- key optimization ----------
-- ---------- hardened rotation cycle with cap ----------
-- 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 function build_rotation_map(start_vec)
     local map = {}
     local map = {}
     local seen = {}
     local seen = {}
    local function sig(t) return sort_key(t) end  -- signature for cycle detection
 
     local v = reduce_irreducible(start_vec)
     local v = reduce_irreducible(start_vec)
     local first = sig(v)
     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
         seen[sig(v)] = true
 
         local sig = key_of(v)
        if seen[sig] then break end
        seen[sig] = true
 
         local nextv = rot2_once(v)
         local nextv = rot2_once(v)
         local id = sig(nextv)
         steps = steps + 1
         if seen[id] then break end
         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 maps for every column (voicing)
     -- 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: each cell becomes O(1) lookup
     -- 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]
            -- In unusual cases where a given odd isn't in the cycle, fall back to the base voicing.
             if not rotated then
             if not rotated then
                 local fallback = {}
                 -- fallback (rare): use reduced base voicing
                for _,od in ipairs(spec.set) do end -- noop, just to keep structure similar
                 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))
                rotated = reduce_irreducible(rotated)
             end
             end