Module:Chord edo approximation: Difference between revisions
No edit summary |
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 | -- expressed as % of EDO step (relative) so smaller EDOs aren't penalized for | ||
-- | -- their inherently coarser resolution. | ||
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 | ||
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 = {} | ||
local note_errors = {0} | 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 | ||
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 | ||
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(" ", 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 = ' — ' | |||
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 | local padded_steps, padded_cents, padded_errs = {}, {}, {} | ||
for | for i = 1, n_pos do | ||
table.insert( | 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 | local steps_cell = '<code>' .. table.concat(padded_steps, SEP) .. '</code>' | ||
local | local cents_cell = '<code>' .. table.concat(padded_cents, SEP) .. '</code>' | ||
local errs_cell = '<code>' .. table.concat(padded_errs, SEP) .. '</code>' | |||
-- Steps data attribute for the play button (unpadded, comma-separated) | |||
local | 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 | ||
local steps_data = table.concat(steps_data_parts, ",") | |||
local | |||
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, | play_btn, edo_link, steps_cell, cents_cell, errs_cell, rms_c, rms_r)) | ||
end | end | ||