<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://en.xen.wiki/index.php?action=history&amp;feed=atom&amp;title=Module%3AChordVoicings</id>
	<title>Module:ChordVoicings - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://en.xen.wiki/index.php?action=history&amp;feed=atom&amp;title=Module%3AChordVoicings"/>
	<link rel="alternate" type="text/html" href="https://en.xen.wiki/index.php?title=Module:ChordVoicings&amp;action=history"/>
	<updated>2026-06-23T22:30:14Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.43.6</generator>
	<entry>
		<id>https://en.xen.wiki/index.php?title=Module:ChordVoicings&amp;diff=216794&amp;oldid=prev</id>
		<title>Eufalesio at 13:36, 11 November 2025</title>
		<link rel="alternate" type="text/html" href="https://en.xen.wiki/index.php?title=Module:ChordVoicings&amp;diff=216794&amp;oldid=prev"/>
		<updated>2025-11-11T13:36:35Z</updated>

		<summary type="html">&lt;p&gt;&lt;/p&gt;
&lt;a href=&quot;https://en.xen.wiki/index.php?title=Module:ChordVoicings&amp;amp;diff=216794&amp;amp;oldid=216792&quot;&gt;Show changes&lt;/a&gt;</summary>
		<author><name>Eufalesio</name></author>
	</entry>
	<entry>
		<id>https://en.xen.wiki/index.php?title=Module:ChordVoicings&amp;diff=216792&amp;oldid=prev</id>
		<title>Eufalesio at 13:27, 11 November 2025</title>
		<link rel="alternate" type="text/html" href="https://en.xen.wiki/index.php?title=Module:ChordVoicings&amp;diff=216792&amp;oldid=prev"/>
		<updated>2025-11-11T13:27:30Z</updated>

		<summary type="html">&lt;p&gt;&lt;/p&gt;
&lt;a href=&quot;https://en.xen.wiki/index.php?title=Module:ChordVoicings&amp;amp;diff=216792&amp;amp;oldid=216790&quot;&gt;Show changes&lt;/a&gt;</summary>
		<author><name>Eufalesio</name></author>
	</entry>
	<entry>
		<id>https://en.xen.wiki/index.php?title=Module:ChordVoicings&amp;diff=216790&amp;oldid=prev</id>
		<title>Eufalesio at 13:20, 11 November 2025</title>
		<link rel="alternate" type="text/html" href="https://en.xen.wiki/index.php?title=Module:ChordVoicings&amp;diff=216790&amp;oldid=prev"/>
		<updated>2025-11-11T13:20:15Z</updated>

		<summary type="html">&lt;p&gt;&lt;/p&gt;
&lt;a href=&quot;https://en.xen.wiki/index.php?title=Module:ChordVoicings&amp;amp;diff=216790&amp;amp;oldid=216788&quot;&gt;Show changes&lt;/a&gt;</summary>
		<author><name>Eufalesio</name></author>
	</entry>
	<entry>
		<id>https://en.xen.wiki/index.php?title=Module:ChordVoicings&amp;diff=216788&amp;oldid=prev</id>
		<title>Eufalesio: Vibe coded v1.1</title>
		<link rel="alternate" type="text/html" href="https://en.xen.wiki/index.php?title=Module:ChordVoicings&amp;diff=216788&amp;oldid=prev"/>
		<updated>2025-11-11T13:10:13Z</updated>

		<summary type="html">&lt;p&gt;Vibe coded v1.1&lt;/p&gt;
&lt;a href=&quot;https://en.xen.wiki/index.php?title=Module:ChordVoicings&amp;amp;diff=216788&amp;amp;oldid=216781&quot;&gt;Show changes&lt;/a&gt;</summary>
		<author><name>Eufalesio</name></author>
	</entry>
	<entry>
		<id>https://en.xen.wiki/index.php?title=Module:ChordVoicings&amp;diff=216781&amp;oldid=prev</id>
		<title>Eufalesio: Birth [Vibe coded v1]</title>
		<link rel="alternate" type="text/html" href="https://en.xen.wiki/index.php?title=Module:ChordVoicings&amp;diff=216781&amp;oldid=prev"/>
		<updated>2025-11-11T12:58:33Z</updated>

		<summary type="html">&lt;p&gt;Birth [Vibe coded v1]&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;-- Module:ChordVoicings (Scribunto-safe, no // or bitwise ops)&lt;br /&gt;
-- Usage via template: {{ChordVoicingTable|rchord=4:5:6:7}}&lt;br /&gt;
&lt;br /&gt;
local p = {}&lt;br /&gt;
&lt;br /&gt;
-- -------- utilities --------&lt;br /&gt;
local function split_enum(s)&lt;br /&gt;
    local t = {}&lt;br /&gt;
    for num in string.gmatch(s or &amp;quot;&amp;quot;, &amp;quot;%d+&amp;quot;) do&lt;br /&gt;
        t[#t+1] = tonumber(num)&lt;br /&gt;
    end&lt;br /&gt;
    return t&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function gcd(a,b)&lt;br /&gt;
    while b ~= 0 do a, b = a % b, b end&lt;br /&gt;
    return math.abs(a)&lt;br /&gt;
end&lt;br /&gt;
local function gcd_list(t)&lt;br /&gt;
    local g = t[1]&lt;br /&gt;
    for i=2,#t do g = gcd(g, t[i]) end&lt;br /&gt;
    return g&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function odd_part(n)&lt;br /&gt;
    -- remove all factors of 2 using integer division compatible with Lua 5.1&lt;br /&gt;
    while n % 2 == 0 do n = math.floor(n / 2) end&lt;br /&gt;
    return n&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function copy(t)&lt;br /&gt;
    local u = {}&lt;br /&gt;
    for i=1,#t do u[i]=t[i] end&lt;br /&gt;
    return u&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function reduce_irreducible(t)&lt;br /&gt;
    local g = gcd_list(t)&lt;br /&gt;
    if g&amp;gt;1 then&lt;br /&gt;
        local u = {}&lt;br /&gt;
        for i=1,#t do u[i]=math.floor(t[i]/g) end&lt;br /&gt;
        return u&lt;br /&gt;
    end&lt;br /&gt;
    return copy(t)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function to_str(t)&lt;br /&gt;
    local parts = {}&lt;br /&gt;
    for i=1,#t do parts[i]=tostring(t[i]) end&lt;br /&gt;
    return table.concat(parts, &amp;quot;:&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function sort_key(t)&lt;br /&gt;
    local parts = {}&lt;br /&gt;
    for i=1,#t do parts[i]=string.format(&amp;quot;%03d&amp;quot;, t[i]) end&lt;br /&gt;
    return table.concat(parts, &amp;quot;-&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function span_ratio(t)&lt;br /&gt;
    local mn, mx = t[1], t[1]&lt;br /&gt;
    for i=2,#t do&lt;br /&gt;
        if t[i]&amp;lt;mn then mn=t[i] end&lt;br /&gt;
        if t[i]&amp;gt;mx then mx=t[i] end&lt;br /&gt;
    end&lt;br /&gt;
    return mx / mn&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- rotate across two octaves: move first*4 to end, then reduce&lt;br /&gt;
local function rot2_once(t)&lt;br /&gt;
    local n = #t&lt;br /&gt;
    local u = {}&lt;br /&gt;
    for i=2,n do u[#u+1] = t[i] end&lt;br /&gt;
    u[#u+1] = t[1]*4&lt;br /&gt;
    return reduce_irreducible(u)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function rotation_for_first_odd(start_vec, target_odd, cap)&lt;br /&gt;
    local t = reduce_irreducible(start_vec)&lt;br /&gt;
    for _=1,cap do&lt;br /&gt;
        local first_odd = odd_part(t[1])&lt;br /&gt;
        if first_odd == target_odd then return t end&lt;br /&gt;
        t = rot2_once(t)&lt;br /&gt;
    end&lt;br /&gt;
    return t&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function toodds(enum)&lt;br /&gt;
    local odds = {}&lt;br /&gt;
    for i=1,#enum do odds[i] = odd_part(enum[i]) end&lt;br /&gt;
    return odds&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function unique_in_order(t)&lt;br /&gt;
    local seen, out = {}, {}&lt;br /&gt;
    for _,x in ipairs(t) do&lt;br /&gt;
        if not seen[x] then seen[x]=true; out[#out+1]=x end&lt;br /&gt;
    end&lt;br /&gt;
    return out&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- generate all non-empty subsets of an array, as arrays themselves&lt;br /&gt;
local function all_nonempty_subsets(arr)&lt;br /&gt;
    local res = {}&lt;br /&gt;
    local function rec(i, cur)&lt;br /&gt;
        if i &amp;gt; #arr then&lt;br /&gt;
            if #cur &amp;gt; 0 then&lt;br /&gt;
                local add = {}&lt;br /&gt;
                for k=1,#cur do add[k]=cur[k] end&lt;br /&gt;
                res[#res+1] = add&lt;br /&gt;
            end&lt;br /&gt;
            return&lt;br /&gt;
        end&lt;br /&gt;
        -- exclude&lt;br /&gt;
        rec(i+1, cur)&lt;br /&gt;
        -- include&lt;br /&gt;
        cur[#cur+1] = arr[i]&lt;br /&gt;
        rec(i+1, cur)&lt;br /&gt;
        cur[#cur] = nil&lt;br /&gt;
    end&lt;br /&gt;
    rec(1, {})&lt;br /&gt;
    return res&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function subset_labels(nonroot_odds)&lt;br /&gt;
    -- sort the odds ascending for consistent labeling/order&lt;br /&gt;
    local nums = {}&lt;br /&gt;
    for i,x in ipairs(nonroot_odds) do nums[i]=x end&lt;br /&gt;
    table.sort(nums)&lt;br /&gt;
&lt;br /&gt;
    local labels = {}&lt;br /&gt;
    labels[#labels+1] = { text=&amp;quot;Root&amp;quot;, set={} }&lt;br /&gt;
&lt;br /&gt;
    local subsets = all_nonempty_subsets(nums)&lt;br /&gt;
    -- order by size then lexicographic&lt;br /&gt;
    table.sort(subsets, function(a,b)&lt;br /&gt;
        if #a ~= #b then return #a &amp;lt; #b end&lt;br /&gt;
        for i=1,math.min(#a,#b) do&lt;br /&gt;
            if a[i] ~= b[i] then return a[i] &amp;lt; b[i] end&lt;br /&gt;
        end&lt;br /&gt;
        return #a &amp;lt; #b&lt;br /&gt;
    end)&lt;br /&gt;
    for _,set in ipairs(subsets) do&lt;br /&gt;
        local parts={}&lt;br /&gt;
        for i,od in ipairs(set) do parts[i] = &amp;quot;&amp;#039;&amp;quot; .. tostring(od) end&lt;br /&gt;
        labels[#labels+1] = { text=table.concat(parts, &amp;quot;&amp;quot;), set=set }&lt;br /&gt;
    end&lt;br /&gt;
    return labels, nums  -- also return the sorted nonroot odds&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function apply_octaves(enum, odds, chosen_set)&lt;br /&gt;
    -- double entries whose odd is in chosen_set (set is table {odd=&amp;gt;true})&lt;br /&gt;
    local out = {}&lt;br /&gt;
    for i=1,#enum do&lt;br /&gt;
        local v = enum[i]&lt;br /&gt;
        if chosen_set[odds[i]] then v = v * 2 end&lt;br /&gt;
        out[i] = v&lt;br /&gt;
    end&lt;br /&gt;
    return out&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function italicize(s) return &amp;quot;&amp;#039;&amp;#039;&amp;quot; .. s .. &amp;quot;&amp;#039;&amp;#039;&amp;quot; end&lt;br /&gt;
local function bold(s) return &amp;quot;&amp;#039;&amp;#039;&amp;#039;&amp;quot; .. s .. &amp;quot;&amp;#039;&amp;#039;&amp;#039;&amp;quot; end&lt;br /&gt;
local function bold_italic(s) return &amp;quot;&amp;#039;&amp;#039;&amp;#039;&amp;#039;&amp;#039;&amp;quot; .. s .. &amp;quot;&amp;#039;&amp;#039;&amp;#039;&amp;#039;&amp;#039;&amp;quot; end&lt;br /&gt;
&lt;br /&gt;
-- -------- main render --------&lt;br /&gt;
function p.render(frame)&lt;br /&gt;
    local args = frame.args or frame:getParent().args&lt;br /&gt;
    local rchord_str = (args.rchord or args[1] or &amp;quot;&amp;quot;):gsub(&amp;quot;%s+&amp;quot;, &amp;quot;&amp;quot;)&lt;br /&gt;
    if rchord_str == &amp;quot;&amp;quot; then&lt;br /&gt;
        return &amp;quot;&amp;lt;strong class=\&amp;quot;error\&amp;quot;&amp;gt;Missing parameter: rchord&amp;lt;/strong&amp;gt;&amp;quot;&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local width = args.width or &amp;quot;120px&amp;quot;&lt;br /&gt;
    local title = args.title or &amp;quot;Voicings and rotations around two octaves&amp;quot;&lt;br /&gt;
    local cls   = args.class or &amp;quot;wikitable sortable&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    local enum = split_enum(rchord_str)&lt;br /&gt;
    if #enum &amp;lt; 3 then&lt;br /&gt;
        return &amp;quot;&amp;lt;strong class=\&amp;quot;error\&amp;quot;&amp;gt;rchord must have at least 3 integers&amp;lt;/strong&amp;gt;&amp;quot;&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local rchord_display = rchord_str&lt;br /&gt;
    local odds = toodds(enum)&lt;br /&gt;
    local root_odd = odd_part(enum[1])&lt;br /&gt;
    local odd_rows = unique_in_order(odds)&lt;br /&gt;
&lt;br /&gt;
    -- columns: Root + all subsets of non-root odds&lt;br /&gt;
    local nonroot_odds_unique = {}&lt;br /&gt;
    for _,o in ipairs(odd_rows) do&lt;br /&gt;
        if o ~= root_odd then nonroot_odds_unique[#nonroot_odds_unique+1] = o end&lt;br /&gt;
    end&lt;br /&gt;
    local col_specs, nonroot_sorted = subset_labels(nonroot_odds_unique) -- {text,set}, and sorted list&lt;br /&gt;
&lt;br /&gt;
    local out = {}&lt;br /&gt;
    out[#out+1] = &amp;quot;== &amp;quot; .. title .. &amp;quot; ==&amp;quot;&lt;br /&gt;
    out[#out+1] = string.format(&amp;#039;{| class=&amp;quot;%s&amp;quot; style=&amp;quot;text-align:center;&amp;quot;&amp;#039;, cls)&lt;br /&gt;
    out[#out+1] = &amp;quot;|+&amp;quot;&lt;br /&gt;
    out[#out+1] = string.format(&amp;#039;! style=&amp;quot;width:%s;&amp;quot; {{diagonal split header|Rotation|Voicing}}&amp;#039;, width)&lt;br /&gt;
    for _,spec in ipairs(col_specs) do&lt;br /&gt;
        out[#out+1] = string.format(&amp;#039;! style=&amp;quot;width:%s;&amp;quot; | %s&amp;#039;, width, spec.text)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    -- rows&lt;br /&gt;
    for _,rowodd in ipairs(odd_rows) do&lt;br /&gt;
        out[#out+1] = &amp;quot;|-&amp;quot;&lt;br /&gt;
        out[#out+1] = string.format(&amp;#039;! style=&amp;quot;width:%s;&amp;quot; | On %s&amp;#039;, width, tostring(rowodd))&lt;br /&gt;
&lt;br /&gt;
        for _,spec in ipairs(col_specs) do&lt;br /&gt;
            -- build chosen set for doubling&lt;br /&gt;
            local chosen = {}&lt;br /&gt;
            for _,od in ipairs(spec.set) do chosen[od] = true end&lt;br /&gt;
&lt;br /&gt;
            local base_voicing = apply_octaves(enum, odds, chosen)&lt;br /&gt;
            local rotated = rotation_for_first_odd(base_voicing, rowodd, 8*#enum)&lt;br /&gt;
&lt;br /&gt;
            local disp = to_str(rotated)&lt;br /&gt;
            local sortv = sort_key(rotated)&lt;br /&gt;
            local shown&lt;br /&gt;
            local is_two_octaves = (span_ratio(rotated) &amp;gt;= 4.0)&lt;br /&gt;
&lt;br /&gt;
            if rowodd == root_odd and #spec.set == 0 then&lt;br /&gt;
                shown = bold_italic(disp)&lt;br /&gt;
            elseif not is_two_octaves then&lt;br /&gt;
                shown = italicize(disp)&lt;br /&gt;
            else&lt;br /&gt;
                shown = disp&lt;br /&gt;
            end&lt;br /&gt;
&lt;br /&gt;
            out[#out+1] = string.format(&amp;#039;| data-sort-value=&amp;quot;%s&amp;quot; | %s&amp;#039;, sortv, shown)&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    out[#out+1] = &amp;quot;|}&amp;quot;&lt;br /&gt;
    out[#out+1] = &amp;quot;Enumerations in italics map to an existing octave-reduced rotation.&amp;quot;&lt;br /&gt;
    return table.concat(out, &amp;quot;\n&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return p&lt;/div&gt;</summary>
		<author><name>Eufalesio</name></author>
	</entry>
</feed>