Module:Chord edo approximation: Difference between revisions

Pailiaq (talk | contribs)
No edit summary
Pailiaq (talk | contribs)
No edit summary
Line 2: Line 2:
-- 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),
-- expressed as % of EDO step (relative) so that smaller EDOs aren't penalized
-- expressed as % of EDO step (relative) so smaller EDOs aren't penalized for
-- for their inherently coarser resolution.
-- 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 47: Line 46:
end
end


-- Computes both absolute (cents) and relative (% of step) RMS around the
-- optimal-reference offset (mean of per-note errors).
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 = {}   -- per-interval (root-relative), for display
     local abs_errors = {}
     local note_errors = {0} -- including root = 0, for math
     local note_errors = {0}


     for _, ic in ipairs(interval_cents_list) do
     for _, ic in ipairs(interval_cents_list) do
Line 64: Line 61:
     end
     end


    -- 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 70: Line 66:
     local mean = sum / n
     local mean = sum / n


    -- 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 94: Line 89:
         return string.format("%.2f", value)
         return string.format("%.2f", value)
     end
     end
end
-- Right-align s to width by prepending non-breaking spaces (so HTML preserves them)
local function pad_left(s, width)
    local need = width - #s
    if need <= 0 then return s end
    return string.rep("&nbsp;", need) .. s
end
end


Line 135: Line 137:
     if #results == 0 then
     if #results == 0 then
         return "No edos found within RMS tolerance of " .. max_rms .. "%"
         return "No edos found within RMS tolerance of " .. max_rms .. "%"
    end
    -- === PASS 1: precompute all formatted strings, find max width per position ===
    local n_pos = #(results[1].steps) + 1  -- +1 for the root
    local max_step_w = {}
    local max_cents_w = {}
    local max_err_w = {}
    for i = 1, n_pos do max_step_w[i] = 0; max_cents_w[i] = 0; max_err_w[i] = 0 end
    for _, r in ipairs(results) do
        local edostep = 1200 / r.edo
        local step_strs = {"0"}
        local cents_strs = {"0.00"}
        local err_strs = {"0.00"}
        for i, s in ipairs(r.steps) do
            table.insert(step_strs, tostring(s))
            table.insert(cents_strs, string.format("%.2f", s * edostep))
            table.insert(err_strs, format_error(r.abs_errors[i]))
        end
        for i = 1, n_pos do
            if #step_strs[i] > max_step_w[i] then max_step_w[i] = #step_strs[i] end
            if #cents_strs[i] > max_cents_w[i] then max_cents_w[i] = #cents_strs[i] end
            if #err_strs[i] > max_err_w[i] then max_err_w[i] = #err_strs[i] end
        end
        r._step_strs = step_strs
        r._cents_strs = cents_strs
        r._err_strs = err_strs
     end
     end


Line 172: Line 201:
         .. ' !! RMS ([[cent|¢]])'
         .. ' !! RMS ([[cent|¢]])'
         .. ' !! RMS ([[relative cent|%]])')
         .. ' !! RMS ([[relative cent|%]])')
    -- === PASS 2: emit padded, em-dash-separated cell content ===
    local SEP = '&nbsp;—&nbsp;'


     for _, r in ipairs(results) do
     for _, r in ipairs(results) do
         local edo_link = string.format("[[%dedo|%d]]", r.edo, r.edo)
         local edo_link = string.format("[[%dedo|%d]]", r.edo, r.edo)


         local step_parts = {"0"}
         local padded_steps, padded_cents, padded_errs = {}, {}, {}
         for _, s in ipairs(r.steps) do
         for i = 1, n_pos do
             table.insert(step_parts, tostring(s))
            table.insert(padded_steps, pad_left(r._step_strs[i], max_step_w[i]))
            table.insert(padded_cents, pad_left(r._cents_strs[i], max_cents_w[i]))
             table.insert(padded_errs, pad_left(r._err_strs[i], max_err_w[i]))
         end
         end
         local steps_str = table.concat(step_parts, " ")
         local steps_cell = '<code>' .. table.concat(padded_steps, SEP) .. '</code>'
         local steps_data = table.concat(step_parts, ",")
         local cents_cell = '<code>' .. table.concat(padded_cents, SEP) .. '</code>'
        local errs_cell  = '<code>' .. table.concat(padded_errs, SEP) .. '</code>'


         local edostep = 1200 / r.edo
         -- Steps data attribute for the play button (unpadded, comma-separated)
         local cents_parts = {"0.00"}
         local steps_data_parts = {"0"}
         for _, s in ipairs(r.steps) do
         for _, s in ipairs(r.steps) do table.insert(steps_data_parts, tostring(s)) end
            table.insert(cents_parts, string.format("%.2f", s * edostep))
         local steps_data = table.concat(steps_data_parts, ",")
        end
         local cents_str = table.concat(cents_parts, " ")
 
        local err_parts = {}
        for _, e in ipairs(r.abs_errors) do
            table.insert(err_parts, format_error(e))
        end
        local err_str = table.concat(err_parts, " ")


         local rms_c = string.format("%.2f", r.rms_cents)
         local rms_c = string.format("%.2f", r.rms_cents)
Line 205: Line 232:
         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 || %s',
             play_btn, edo_link, steps_str, cents_str, err_str, rms_c, rms_r))
             play_btn, edo_link, steps_cell, cents_cell, errs_cell, rms_c, rms_r))
     end
     end