Module:ChordVoicings: Difference between revisions

Eufalesio (talk | contribs)
Birth [Vibe coded v1]
 
Eufalesio (talk | contribs)
Vibe coded v1.1
Line 1: Line 1:
-- Module:ChordVoicings (Scribunto-safe, no // or bitwise ops)
-- Module:ChordVoicings (optimized to avoid CPU limit)
-- Usage via template: {{ChordVoicingTable|rchord=4:5:6:7}}
-- 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 odd_part(n)
    -- remove all factors of 2 using integer division compatible with Lua 5.1
    while n % 2 == 0 do n = math.floor(n / 2) end
    return n
end
local function copy(t)
    local u = {}
    for i=1,#t do u[i]=t[i] end
    return u
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 copy(t)
    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


-- rotate across two octaves: move first*4 to end, then reduce
-- 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
local function rotation_for_first_odd(start_vec, target_odd, cap)
    local t = reduce_irreducible(start_vec)
    for _=1,cap do
        local first_odd = odd_part(t[1])
        if first_odd == target_odd then return t end
        t = rot2_once(t)
    end
    return t
end
local function toodds(enum)
    local odds = {}
    for i=1,#enum do odds[i] = odd_part(enum[i]) end
    return odds
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
        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 of an array, as arrays themselves
-- 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
        -- exclude
         rec(i+1, cur)
         rec(i+1, cur)
        -- include
         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)
    -- sort the odds ascending for consistent labeling/order
     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)
    -- order by size then lexicographic
     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, nums  -- also return the sorted nonroot odds
     return labels
end
end


local function apply_octaves(enum, odds, chosen_set)
local function apply_octaves(enum, odds, chosen_set)
    -- double entries whose odd is in chosen_set (set is table {odd=>true})
     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(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 rchord_display = rchord_str
     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)


    -- columns: Root + all 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
         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, nonroot_sorted = subset_labels(nonroot_odds_unique) -- {text,set}, and sorted list
     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 _,spec in ipairs(col_specs) do
         for c, spec in ipairs(col_specs) do
             -- build chosen set for doubling
            local rotated = rotation_maps[c][rowodd]
            local chosen = {}
             -- In unusual cases where a given odd isn't in the cycle, fall back to the base voicing.
            for _,od in ipairs(spec.set) do chosen[od] = true end
            if not rotated then
 
                local fallback = {}
            local base_voicing = apply_octaves(enum, odds, chosen)
                for _,od in ipairs(spec.set) do end -- noop, just to keep structure similar
            local rotated = rotation_for_first_odd(base_voicing, rowodd, 8*#enum)
                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)