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
-- Metric: RMS of per-note errors around the optimal reference (least-squares).
-- 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}}
-- expressed as % of EDO step (relative) so that smaller EDOs aren't penalized
-- for their inherently coarser resolution.
-- Usage: {{#invoke:Chord_EDO_Approximation|main|chord=4:5:6|max_rms=15|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 8: Line 10:


-- ===== CONFIGURATION VARIABLES =====
-- ===== CONFIGURATION VARIABLES =====
local DEFAULT_MAX_RMS = 10   -- Max RMS error in cents
local DEFAULT_MAX_RMS = 15   -- Max RMS error in % of EDO step
local DEFAULT_MIN_EDO = 5
local DEFAULT_MIN_EDO = 5
local DEFAULT_MAX_EDO = 60
local DEFAULT_MAX_EDO = 60
Line 45: Line 47:
end
end


-- For a given EDO and a list of interval-cents (from root, length N-1),
-- Computes both absolute (cents) and relative (% of step) RMS around the
-- compute the approximation. Note errors include the root (= 0) so the
-- optimal-reference offset (mean of per-note errors).
-- 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
Line 63: Line 64:
     end
     end


     -- Optimal reference offset: mean of note errors (least-squares optimum)
     -- Optimal-reference offset: mean of note errors (least-squares centering)
     local n = #note_errors
     local n = #note_errors
     local sum = 0
     local sum = 0
Line 69: Line 70:
     local mean = sum / n
     local mean = sum / n


     -- RMS of centered errors (deviation from the best-fit transposition)
     -- RMS of centered errors, in cents and as % of EDO step
     local sq_sum = 0
     local sq_sum = 0
     for _, e in ipairs(note_errors) do
     for _, e in ipairs(note_errors) do
Line 75: Line 76:
         sq_sum = sq_sum + d * d
         sq_sum = sq_sum + d * d
     end
     end
     local rms = math.sqrt(sq_sum / n)
     local rms_cents = math.sqrt(sq_sum / n)
    local rms_relative = (rms_cents / edostep) * 100


     return {
     return {
         steps = steps,
         steps = steps,
         abs_errors = abs_errors,
         abs_errors = abs_errors,
        note_errors = note_errors,
         mean_offset = mean,
         mean_offset = mean,
         rms = rms,
         rms_cents = rms_cents,
        rms_relative = rms_relative,
     }
     }
end
end
Line 125: Line 127:
     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.rms <= max_rms then
         if data.rms_relative <= max_rms then
             data.edo = edo
             data.edo = edo
             table.insert(results, data)
             table.insert(results, data)
Line 132: Line 134:


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


Line 159: Line 161:


     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;RMS&nbsp;error&nbsp;&le;&nbsp;%g{{c}}\'\'</span>',
         .. string.format('<br /><span style="font-size: 0.75em;">\'\'intervals:&nbsp;%s&nbsp;·&nbsp;&le;&nbsp;%dedo,&nbsp;RMS&nbsp;rel.&nbsp;error&nbsp;&le;&nbsp;%g%%\'\'</span>',
             intervals_display, max_edo, max_rms))
             intervals_display, max_edo, max_rms))


Line 168: Line 170:
         .. ' !! class="unsortable" | Cents ([[cent|¢]])'
         .. ' !! class="unsortable" | Cents ([[cent|¢]])'
         .. ' !! class="unsortable" | Absolute errors ([[cent|¢]])'
         .. ' !! class="unsortable" | Absolute errors ([[cent|¢]])'
         .. ' !! RMS error ([[cent|¢]])')
         .. ' !! RMS ([[cent|¢]])'
        .. ' !! RMS ([[relative cent|%]])')


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


         local rms_str = string.format("%.2f", r.rms)
         local rms_c = string.format("%.2f", r.rms_cents)
        local rms_r = string.format("%.2f", r.rms_relative)


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


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