Module:Chord edo approximation: Difference between revisions

Pailiaq (talk | contribs)
No edit summary
Pailiaq (talk | contribs)
No edit summary
Line 1: Line 1:
-- Chord EDO Approximations Module
-- Chord EDO Approximations Module
-- Calculates EDO approximations for JI chords like 4:5:6 or 4:5:6:7
-- 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}}
-- Metric: RMS of per-note errors around the optimal reference (least-squares).
-- Usage: {{#invoke:Chord_EDO_Approximation|main|chord=4:5:6|max_rms=10|min_edo=5|max_edo=60}}
local u = require("Module:Utils")
local u = require("Module:Utils")
local yesno = require("Module:Yesno")
local yesno = require("Module:Yesno")
Line 7: Line 8:


-- ===== CONFIGURATION VARIABLES =====
-- ===== CONFIGURATION VARIABLES =====
local DEFAULT_ERROR_BASE = 10       -- Constant tolerance offset (%)
local DEFAULT_MAX_RMS = 10   -- Max RMS error in cents
local DEFAULT_ERROR_QUADRATIC = 2.5  -- Multiplier on n·(n+1), where n = number of intervals
local DEFAULT_MIN_EDO = 5
local DEFAULT_MIN_EDO = 5
local DEFAULT_MAX_EDO = 60
local DEFAULT_MAX_EDO = 60
Line 45: Line 45:
end
end


-- For a given EDO and a list of interval-cents (from root, length N-1),
-- compute the approximation. Note errors include the root (= 0) so the
-- optimal-reference math is symmetric across all N notes.
local function calculate_chord_approximation(interval_cents_list, edo)
local function calculate_chord_approximation(interval_cents_list, edo)
     local edostep = 1200 / edo
     local edostep = 1200 / edo
     local steps = {}
     local steps = {}
     local abs_errors = {}
     local abs_errors = {}   -- per-interval (root-relative), for display
     local rel_errors = {}
     local note_errors = {0} -- including root = 0, for math
    local total_abs = 0
    local total_rel = 0


     for _, ic in ipairs(interval_cents_list) do
     for _, ic in ipairs(interval_cents_list) do
Line 57: Line 58:
         local approx = step * edostep
         local approx = step * edostep
         local abs_err = approx - ic
         local abs_err = approx - ic
        local rel_err = (abs_err / edostep) * 100
         table.insert(steps, step)
         table.insert(steps, step)
         table.insert(abs_errors, abs_err)
         table.insert(abs_errors, abs_err)
         table.insert(rel_errors, rel_err)
         table.insert(note_errors, abs_err)
        total_abs = total_abs + math.abs(abs_err)
        total_rel = total_rel + math.abs(rel_err)
     end
     end
    -- Optimal reference offset: mean of note errors (least-squares optimum)
    local n = #note_errors
    local sum = 0
    for _, e in ipairs(note_errors) do sum = sum + e end
    local mean = sum / n
    -- RMS of centered errors (deviation from the best-fit transposition)
    local sq_sum = 0
    for _, e in ipairs(note_errors) do
        local d = e - mean
        sq_sum = sq_sum + d * d
    end
    local rms = math.sqrt(sq_sum / n)


     return {
     return {
         steps = steps,
         steps = steps,
         abs_errors = abs_errors,
         abs_errors = abs_errors,
         rel_errors = rel_errors,
         note_errors = note_errors,
         total_abs = total_abs,
         mean_offset = mean,
         total_rel = total_rel,
         rms = rms,
     }
     }
end
end
Line 86: Line 98:
     local chord_str = args.chord or args[1]
     local chord_str = args.chord or args[1]
     local chord_name = args.chord_name
     local chord_name = args.chord_name
     local max_total_error = tonumber(args.max_total_error)
     local max_rms = tonumber(args.max_rms) or DEFAULT_MAX_RMS
     local min_edo = tonumber(args.min_edo) or DEFAULT_MIN_EDO
     local min_edo = tonumber(args.min_edo) or DEFAULT_MIN_EDO
     local max_edo = tonumber(args.max_edo) or DEFAULT_MAX_EDO
     local max_edo = tonumber(args.max_edo) or DEFAULT_MAX_EDO
Line 98: Line 110:
         return "Error: Invalid chord format (use 'a:b:c' with positive integers, e.g. '4:5:6')"
         return "Error: Invalid chord format (use 'a:b:c' with positive integers, e.g. '4:5:6')"
     end
     end
if not max_total_error then
    local n = #notes - 1
    max_total_error = DEFAULT_ERROR_QUADRATIC * n * (n + 1) + DEFAULT_ERROR_BASE
end


     local root = notes[1]
     local root = notes[1]
Line 118: Line 125:
     for edo = min_edo, max_edo do
     for edo = min_edo, max_edo do
         local data = calculate_chord_approximation(intervals_cents, edo)
         local data = calculate_chord_approximation(intervals_cents, edo)
         if data.total_rel <= max_total_error then
         if data.rms <= max_rms then
             data.edo = edo
             data.edo = edo
             table.insert(results, data)
             table.insert(results, data)
Line 125: Line 132:


     if #results == 0 then
     if #results == 0 then
         return "No edos found within total relative error tolerance of " .. max_total_error .. "%"
         return "No edos found within RMS error tolerance of " .. max_rms .. "¢"
     end
     end


     -- Build JI play button: raw cents, no EDO involved
     -- JI play button for the caption
     local ji_cents_parts = {"0"}
     local ji_cents_parts = {"0"}
     for _, c in ipairs(intervals_cents) do
     for _, c in ipairs(intervals_cents) do
Line 152: Line 159:


     table.insert(output, '|+ style="font-size: 105%;" | ' .. caption_main
     table.insert(output, '|+ style="font-size: 105%;" | ' .. caption_main
         .. string.format('<br /><span style="font-size: 0.75em;">\'\'intervals:&nbsp;%s&nbsp;·&nbsp;&le;&nbsp;%dedo,&nbsp;total&nbsp;rel&nbsp;error&nbsp;&le;&nbsp;%g%%\'\'</span>',
         .. string.format('<br /><span style="font-size: 0.75em;">\'\'intervals:&nbsp;%s&nbsp;·&nbsp;&le;&nbsp;%dedo,&nbsp;RMS&nbsp;error&nbsp;&le;&nbsp;%g{{c}}\'\'</span>',
             intervals_display, max_edo, max_total_error))
             intervals_display, max_edo, max_rms))


     table.insert(output, '|-')
     table.insert(output, '|-')
Line 161: Line 168:
         .. ' !! class="unsortable" | Cents ([[cent|¢]])'
         .. ' !! class="unsortable" | Cents ([[cent|¢]])'
         .. ' !! class="unsortable" | Absolute errors ([[cent|¢]])'
         .. ' !! class="unsortable" | Absolute errors ([[cent|¢]])'
         .. ' !! Total abs. error ([[cent|¢]])'
         .. ' !! RMS error ([[cent|¢]])')
        .. ' !! Total [[Relative interval error|relative error]] ([[relative cent|%]])')


     for _, r in ipairs(results) do
     for _, r in ipairs(results) do
Line 187: Line 193:
         local err_str = table.concat(err_parts, " ")
         local err_str = table.concat(err_parts, " ")


         local total_abs_str = string.format("%.2f", r.total_abs)
         local rms_str = string.format("%.2f", r.rms)
        local total_rel_str = string.format("%.2f", r.total_rel)


         local play_btn = string.format(
         local play_btn = string.format(
Line 195: Line 200:


         table.insert(output, '|-')
         table.insert(output, '|-')
         table.insert(output, string.format('| %s || %s || %s || %s || %s || %s || %s',
         table.insert(output, string.format('| %s || %s || %s || %s || %s || %s',
             play_btn, edo_link, steps_str, cents_str, err_str, total_abs_str, total_rel_str))
             play_btn, edo_link, steps_str, cents_str, err_str, rms_str))
     end
     end