Module:Chord edo approximation: Difference between revisions
No edit summary |
No edit summary |
||
| (18 intermediate revisions by the same user not shown) | |||
| 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), | ||
-- 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 7: | Line 9: | ||
-- ===== CONFIGURATION VARIABLES ===== | -- ===== CONFIGURATION VARIABLES ===== | ||
local | 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 48: | Line 50: | ||
local steps = {} | local steps = {} | ||
local abs_errors = {} | local abs_errors = {} | ||
local | local note_errors = {0} | ||
for _, ic in ipairs(interval_cents_list) do | for _, ic in ipairs(interval_cents_list) do | ||
| Line 56: | Line 56: | ||
local approx = step * edostep | local approx = step * edostep | ||
local abs_err = approx - ic | local abs_err = approx - ic | ||
table.insert(steps, step) | table.insert(steps, step) | ||
table.insert(abs_errors, abs_err) | table.insert(abs_errors, abs_err) | ||
table.insert( | table.insert(note_errors, abs_err) | ||
end | end | ||
local n = #note_errors | |||
local sum = 0 | |||
for _, e in ipairs(note_errors) do sum = sum + e end | |||
local mean = sum / n | |||
local sq_sum = 0 | |||
for _, e in ipairs(note_errors) do | |||
local d = e - mean | |||
sq_sum = sq_sum + d * d | |||
end | |||
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, | ||
mean_offset = mean, | |||
rms_cents = rms_cents, | |||
rms_relative = rms_relative, | |||
} | } | ||
end | end | ||
| Line 79: | 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 85: | Line 102: | ||
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 | 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 112: | Line 129: | ||
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. | if data.rms_relative <= max_rms then | ||
data.edo = edo | data.edo = edo | ||
table.insert(results, data) | table.insert(results, data) | ||
| Line 119: | Line 136: | ||
if #results == 0 then | if #results == 0 then | ||
return "No edos found within | 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 | ||
-- JI play button for the caption | |||
local ji_cents_parts = {"0"} | |||
for _, c in ipairs(intervals_cents) do | |||
table.insert(ji_cents_parts, string.format("%.4f", c)) | |||
end | |||
local ji_cents_data = table.concat(ji_cents_parts, ",") | |||
local ji_play_btn = string.format( | |||
'<span class="edo-chord-play ji" data-cents="%s" title="Play %s in just intonation" role="button" tabindex="0">▶</span>', | |||
ji_cents_data, chord_str) | |||
local output = {} | local output = {} | ||
| Line 130: | Line 184: | ||
local caption_main | local caption_main | ||
if display_name ~= chord_str then | if display_name ~= chord_str then | ||
caption_main = string.format("Edo approximations for %s (%s)", display_name, chord_str) | caption_main = string.format("Edo approximations for %s (%s) %s", display_name, chord_str, ji_play_btn) | ||
else | else | ||
caption_main = string.format("Edo approximations for %s", display_name) | caption_main = string.format("Edo approximations for %s %s", display_name, ji_play_btn) | ||
end | end | ||
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: %s · ≤ %dedo, | .. string.format('<br /><span style="font-size: 0.75em;">\'\'intervals: %s · ≤ %dedo, RMS rel. error ≤ %g%%\'\'</span>', | ||
intervals_display, max_edo, | intervals_display, max_edo, max_rms)) | ||
table.insert(output, '|-') | table.insert(output, '|-') | ||
| Line 145: | Line 199: | ||
.. ' !! class="unsortable" | Cents ([[cent|¢]])' | .. ' !! class="unsortable" | Cents ([[cent|¢]])' | ||
.. ' !! class="unsortable" | Absolute errors ([[cent|¢]])' | .. ' !! class="unsortable" | Absolute errors ([[cent|¢]])' | ||
.. ' !! | .. ' !! RMS ([[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])) | |||
table.insert( | |||
end | end | ||
local | local CODE_STYLE = ' style="padding:2px 3px;white-space:nowrap;"' | ||
local steps_cell = '<code' .. CODE_STYLE .. '>' .. table.concat(padded_steps, SEP) .. '</code>' | |||
local cents_cell = '<code' .. CODE_STYLE .. '>' .. table.concat(padded_cents, SEP) .. '</code>' | |||
local errs_cell = '<code' .. CODE_STYLE .. '>' .. table.concat(padded_errs, SEP) .. '</code>' | |||
-- Steps data attribute for the play button (unpadded, comma-separated) | |||
local steps_data_parts = {"0"} | |||
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 | local rms_r = string.format("%.2f", r.rms_relative) | ||
local play_btn = string.format( | local play_btn = string.format( | ||
| Line 180: | 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 | ||