<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://xenreference.com/wiki/index.php?action=history&amp;feed=atom&amp;title=Module%3AChord_edo_approximation</id>
	<title>Module:Chord edo approximation - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://xenreference.com/wiki/index.php?action=history&amp;feed=atom&amp;title=Module%3AChord_edo_approximation"/>
	<link rel="alternate" type="text/html" href="https://xenreference.com/wiki/index.php?title=Module:Chord_edo_approximation&amp;action=history"/>
	<updated>2026-09-14T03:25:22Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.44.2</generator>
	<entry>
		<id>https://xenreference.com/wiki/index.php?title=Module:Chord_edo_approximation&amp;diff=7336&amp;oldid=prev</id>
		<title>Inthar: Created page with &quot;-- Chord EDO Approximations Module -- Calculates EDO approximations for JI chords like 4:5:6 or 4:5:6:7 -- Usage: {{#invoke:Chord_EDO_Approximation|main|chord=4:5:6|max_total_error=20|min_edo=5|max_edo=60}} local u = require(&quot;Module:Utils&quot;) local yesno = require(&quot;Module:Yesno&quot;) local p = {}  -- ===== CONFIGURATION VARIABLES ===== local DEFAULT_ERROR_BASE = 10        -- Constant tolerance offset (%) local DEFAULT_ERROR_QUADRATIC = 2.5  -- Multiplier on n·(n+1), where n =...&quot;</title>
		<link rel="alternate" type="text/html" href="https://xenreference.com/wiki/index.php?title=Module:Chord_edo_approximation&amp;diff=7336&amp;oldid=prev"/>
		<updated>2026-05-26T20:27:01Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;-- Chord EDO Approximations Module -- Calculates EDO approximations for JI chords like 4:5:6 or 4:5:6:7 -- Usage: {{#invoke:Chord_EDO_Approximation|main|chord=4:5:6|max_total_error=20|min_edo=5|max_edo=60}} local u = require(&amp;quot;Module:Utils&amp;quot;) local yesno = require(&amp;quot;Module:Yesno&amp;quot;) local p = {}  -- ===== CONFIGURATION VARIABLES ===== local DEFAULT_ERROR_BASE = 10        -- Constant tolerance offset (%) local DEFAULT_ERROR_QUADRATIC = 2.5  -- Multiplier on n·(n+1), where n =...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;-- Chord EDO Approximations Module&lt;br /&gt;
-- Calculates EDO approximations for JI chords like 4:5:6 or 4:5:6:7&lt;br /&gt;
-- Usage: {{#invoke:Chord_EDO_Approximation|main|chord=4:5:6|max_total_error=20|min_edo=5|max_edo=60}}&lt;br /&gt;
local u = require(&amp;quot;Module:Utils&amp;quot;)&lt;br /&gt;
local yesno = require(&amp;quot;Module:Yesno&amp;quot;)&lt;br /&gt;
local p = {}&lt;br /&gt;
&lt;br /&gt;
-- ===== CONFIGURATION VARIABLES =====&lt;br /&gt;
local DEFAULT_ERROR_BASE = 10        -- Constant tolerance offset (%)&lt;br /&gt;
local DEFAULT_ERROR_QUADRATIC = 2.5  -- Multiplier on n·(n+1), where n = number of intervals&lt;br /&gt;
local DEFAULT_MIN_EDO = 5&lt;br /&gt;
local DEFAULT_MAX_EDO = 60&lt;br /&gt;
-- ====================================&lt;br /&gt;
&lt;br /&gt;
local function cents(ratio)&lt;br /&gt;
    return 1200 * u.log2(ratio)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function round(x)&lt;br /&gt;
    local floor_x = math.floor(x)&lt;br /&gt;
    local frac = x - floor_x&lt;br /&gt;
    if frac &amp;lt; 0.5 then&lt;br /&gt;
        return floor_x&lt;br /&gt;
    elseif frac &amp;gt; 0.5 then&lt;br /&gt;
        return floor_x + 1&lt;br /&gt;
    else&lt;br /&gt;
        if floor_x % 2 == 0 then return floor_x else return floor_x + 1 end&lt;br /&gt;
    end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function gcd(a, b)&lt;br /&gt;
    while b ~= 0 do a, b = b, a % b end&lt;br /&gt;
    return a&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function parse_chord(chord_str)&lt;br /&gt;
    local notes = {}&lt;br /&gt;
    for n in string.gmatch(chord_str, &amp;quot;([^:%s]+)&amp;quot;) do&lt;br /&gt;
        local num = tonumber(n)&lt;br /&gt;
        if not num or num &amp;lt;= 0 then return nil end&lt;br /&gt;
        table.insert(notes, num)&lt;br /&gt;
    end&lt;br /&gt;
    if #notes &amp;lt; 2 then return nil end&lt;br /&gt;
    return notes&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function calculate_chord_approximation(interval_cents_list, edo)&lt;br /&gt;
    local edostep = 1200 / edo&lt;br /&gt;
    local steps = {}&lt;br /&gt;
    local abs_errors = {}&lt;br /&gt;
    local rel_errors = {}&lt;br /&gt;
    local total_abs = 0&lt;br /&gt;
    local total_rel = 0&lt;br /&gt;
&lt;br /&gt;
    for _, ic in ipairs(interval_cents_list) do&lt;br /&gt;
        local step = round(ic / edostep)&lt;br /&gt;
        local approx = step * edostep&lt;br /&gt;
        local abs_err = approx - ic&lt;br /&gt;
        local rel_err = (abs_err / edostep) * 100&lt;br /&gt;
        table.insert(steps, step)&lt;br /&gt;
        table.insert(abs_errors, abs_err)&lt;br /&gt;
        table.insert(rel_errors, rel_err)&lt;br /&gt;
        total_abs = total_abs + math.abs(abs_err)&lt;br /&gt;
        total_rel = total_rel + math.abs(rel_err)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return {&lt;br /&gt;
        steps = steps,&lt;br /&gt;
        abs_errors = abs_errors,&lt;br /&gt;
        rel_errors = rel_errors,&lt;br /&gt;
        total_abs = total_abs,&lt;br /&gt;
        total_rel = total_rel,&lt;br /&gt;
    }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function format_error(value)&lt;br /&gt;
    if value &amp;gt;= 0 then&lt;br /&gt;
        return string.format(&amp;quot;+%.2f&amp;quot;, value)&lt;br /&gt;
    else&lt;br /&gt;
        return string.format(&amp;quot;%.2f&amp;quot;, value)&lt;br /&gt;
    end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function p.main(frame)&lt;br /&gt;
    local args = frame.args&lt;br /&gt;
    local chord_str = args.chord or args[1]&lt;br /&gt;
    local chord_name = args.chord_name&lt;br /&gt;
    local max_total_error = tonumber(args.max_total_error)&lt;br /&gt;
    local min_edo = tonumber(args.min_edo) or DEFAULT_MIN_EDO&lt;br /&gt;
    local max_edo = tonumber(args.max_edo) or DEFAULT_MAX_EDO&lt;br /&gt;
&lt;br /&gt;
    if not chord_str then&lt;br /&gt;
        return &amp;quot;Error: No chord specified&amp;quot;&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local notes = parse_chord(chord_str)&lt;br /&gt;
    if not notes then&lt;br /&gt;
        return &amp;quot;Error: Invalid chord format (use &amp;#039;a:b:c&amp;#039; with positive integers, e.g. &amp;#039;4:5:6&amp;#039;)&amp;quot;&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
	if not max_total_error then&lt;br /&gt;
	    local n = #notes - 1&lt;br /&gt;
	    max_total_error = DEFAULT_ERROR_QUADRATIC * n * (n + 1) + DEFAULT_ERROR_BASE&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
    local root = notes[1]&lt;br /&gt;
    local intervals_cents = {}&lt;br /&gt;
    local interval_strs = {}&lt;br /&gt;
    for i = 2, #notes do&lt;br /&gt;
        local n, d = notes[i], root&lt;br /&gt;
        local g = gcd(n, d)&lt;br /&gt;
        local rn, rd = n / g, d / g&lt;br /&gt;
        table.insert(intervals_cents, cents(n / d))&lt;br /&gt;
        table.insert(interval_strs, string.format(&amp;quot;%d/%d&amp;quot;, rn, rd))&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local results = {}&lt;br /&gt;
    for edo = min_edo, max_edo do&lt;br /&gt;
        local data = calculate_chord_approximation(intervals_cents, edo)&lt;br /&gt;
        if data.total_rel &amp;lt;= max_total_error then&lt;br /&gt;
            data.edo = edo&lt;br /&gt;
            table.insert(results, data)&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    if #results == 0 then&lt;br /&gt;
        return &amp;quot;No edos found within total relative error tolerance of &amp;quot; .. max_total_error .. &amp;quot;%&amp;quot;&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    -- Build JI play button: raw cents, no EDO involved&lt;br /&gt;
    local ji_cents_parts = {&amp;quot;0&amp;quot;}&lt;br /&gt;
    for _, c in ipairs(intervals_cents) do&lt;br /&gt;
        table.insert(ji_cents_parts, string.format(&amp;quot;%.4f&amp;quot;, c))&lt;br /&gt;
    end&lt;br /&gt;
    local ji_cents_data = table.concat(ji_cents_parts, &amp;quot;,&amp;quot;)&lt;br /&gt;
    local ji_play_btn = string.format(&lt;br /&gt;
        &amp;#039;&amp;lt;span class=&amp;quot;edo-chord-play ji&amp;quot; data-cents=&amp;quot;%s&amp;quot; title=&amp;quot;Play %s in just intonation&amp;quot; role=&amp;quot;button&amp;quot; tabindex=&amp;quot;0&amp;quot;&amp;gt;▶&amp;lt;/span&amp;gt;&amp;#039;,&lt;br /&gt;
        ji_cents_data, chord_str)&lt;br /&gt;
&lt;br /&gt;
    local output = {}&lt;br /&gt;
    table.insert(output, &amp;#039;{| class=&amp;quot;wikitable center-all mw-collapsible sortable&amp;quot;&amp;#039;)&lt;br /&gt;
&lt;br /&gt;
    local display_name = (chord_name and chord_name ~= &amp;quot;&amp;quot;) and chord_name or chord_str&lt;br /&gt;
    local intervals_display = table.concat(interval_strs, &amp;quot;,&amp;amp;nbsp;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    local caption_main&lt;br /&gt;
    if display_name ~= chord_str then&lt;br /&gt;
        caption_main = string.format(&amp;quot;Edo&amp;amp;nbsp;approximations&amp;amp;nbsp;for&amp;amp;nbsp;%s&amp;amp;nbsp;(%s)&amp;amp;nbsp;%s&amp;quot;, display_name, chord_str, ji_play_btn)&lt;br /&gt;
    else&lt;br /&gt;
        caption_main = string.format(&amp;quot;Edo&amp;amp;nbsp;approximations&amp;amp;nbsp;for&amp;amp;nbsp;%s&amp;amp;nbsp;%s&amp;quot;, display_name, ji_play_btn)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    table.insert(output, &amp;#039;|+ style=&amp;quot;font-size: 105%;&amp;quot; | &amp;#039; .. caption_main&lt;br /&gt;
        .. string.format(&amp;#039;&amp;lt;br /&amp;gt;&amp;lt;span style=&amp;quot;font-size: 0.75em;&amp;quot;&amp;gt;\&amp;#039;\&amp;#039;intervals:&amp;amp;nbsp;%s&amp;amp;nbsp;·&amp;amp;nbsp;&amp;amp;le;&amp;amp;nbsp;%dedo,&amp;amp;nbsp;total&amp;amp;nbsp;rel&amp;amp;nbsp;error&amp;amp;nbsp;&amp;amp;le;&amp;amp;nbsp;%g%%\&amp;#039;\&amp;#039;&amp;lt;/span&amp;gt;&amp;#039;,&lt;br /&gt;
            intervals_display, max_edo, max_total_error))&lt;br /&gt;
&lt;br /&gt;
    table.insert(output, &amp;#039;|-&amp;#039;)&lt;br /&gt;
    table.insert(output, &amp;#039;! class=&amp;quot;unsortable&amp;quot; | &amp;amp;nbsp;&amp;#039;&lt;br /&gt;
        .. &amp;#039; !! Edo&amp;#039;&lt;br /&gt;
        .. &amp;#039; !! class=&amp;quot;unsortable&amp;quot; | Steps&amp;#039;&lt;br /&gt;
        .. &amp;#039; !! class=&amp;quot;unsortable&amp;quot; | Cents ([[cent|¢]])&amp;#039;&lt;br /&gt;
        .. &amp;#039; !! class=&amp;quot;unsortable&amp;quot; | Absolute errors ([[cent|¢]])&amp;#039;&lt;br /&gt;
        .. &amp;#039; !! Total abs. error ([[cent|¢]])&amp;#039;&lt;br /&gt;
        .. &amp;#039; !! Total [[Relative interval error|relative error]] ([[relative cent|%]])&amp;#039;)&lt;br /&gt;
&lt;br /&gt;
    for _, r in ipairs(results) do&lt;br /&gt;
        local edo_link = string.format(&amp;quot;[[%dedo|%d]]&amp;quot;, r.edo, r.edo)&lt;br /&gt;
&lt;br /&gt;
        local step_parts = {&amp;quot;0&amp;quot;}&lt;br /&gt;
        for _, s in ipairs(r.steps) do&lt;br /&gt;
            table.insert(step_parts, tostring(s))&lt;br /&gt;
        end&lt;br /&gt;
        local steps_str = table.concat(step_parts, &amp;quot; &amp;quot;)&lt;br /&gt;
        local steps_data = table.concat(step_parts, &amp;quot;,&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
        local edostep = 1200 / r.edo&lt;br /&gt;
        local cents_parts = {&amp;quot;0.00&amp;quot;}&lt;br /&gt;
        for _, s in ipairs(r.steps) do&lt;br /&gt;
            table.insert(cents_parts, string.format(&amp;quot;%.2f&amp;quot;, s * edostep))&lt;br /&gt;
        end&lt;br /&gt;
        local cents_str = table.concat(cents_parts, &amp;quot; &amp;quot;)&lt;br /&gt;
&lt;br /&gt;
        local err_parts = {}&lt;br /&gt;
        for _, e in ipairs(r.abs_errors) do&lt;br /&gt;
            table.insert(err_parts, format_error(e))&lt;br /&gt;
        end&lt;br /&gt;
        local err_str = table.concat(err_parts, &amp;quot; &amp;quot;)&lt;br /&gt;
&lt;br /&gt;
        local total_abs_str = string.format(&amp;quot;%.2f&amp;quot;, r.total_abs)&lt;br /&gt;
        local total_rel_str = string.format(&amp;quot;%.2f&amp;quot;, r.total_rel)&lt;br /&gt;
&lt;br /&gt;
        local play_btn = string.format(&lt;br /&gt;
            &amp;#039;&amp;lt;span class=&amp;quot;edo-chord-play&amp;quot; data-edo=&amp;quot;%d&amp;quot; data-steps=&amp;quot;%s&amp;quot; title=&amp;quot;Play %s in %dedo&amp;quot; role=&amp;quot;button&amp;quot; tabindex=&amp;quot;0&amp;quot;&amp;gt;▶&amp;lt;/span&amp;gt;&amp;#039;,&lt;br /&gt;
            r.edo, steps_data, chord_str, r.edo)&lt;br /&gt;
&lt;br /&gt;
        table.insert(output, &amp;#039;|-&amp;#039;)&lt;br /&gt;
        table.insert(output, string.format(&amp;#039;| %s || %s || %s || %s || %s || %s || %s&amp;#039;,&lt;br /&gt;
            play_btn, edo_link, steps_str, cents_str, err_str, total_abs_str, total_rel_str))&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    table.insert(output, &amp;#039;|}&amp;#039;)&lt;br /&gt;
&lt;br /&gt;
    local result = table.concat(output, &amp;#039;\n&amp;#039;)&lt;br /&gt;
    if yesno(frame.args[&amp;quot;debug&amp;quot;]) == true then&lt;br /&gt;
        result = &amp;#039;&amp;lt;syntaxhighlight lang=&amp;quot;wikitext&amp;quot;&amp;gt;&amp;#039; .. result .. &amp;#039;&amp;lt;/syntaxhighlight&amp;gt;&amp;#039;&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return frame:preprocess(result)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return p&lt;/div&gt;</summary>
		<author><name>Inthar</name></author>
	</entry>
</feed>