Module:Chord edo approximation: Difference between revisions
No edit summary |
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| | -- 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 | local DEFAULT_MAX_RMS = 10 -- Max RMS error in cents | ||
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 | local note_errors = {0} -- including root = 0, for math | ||
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 | ||
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 | ||
-- 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, | ||
note_errors = note_errors, | |||
mean_offset = mean, | |||
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 | 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 | ||
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. | 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 | return "No edos found within RMS error tolerance of " .. max_rms .. "¢" | ||
end | end | ||
-- | -- 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: %s · ≤ %dedo, | .. string.format('<br /><span style="font-size: 0.75em;">\'\'intervals: %s · ≤ %dedo, RMS error ≤ %g{{c}}\'\'</span>', | ||
intervals_display, max_edo, | 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|¢]])' | ||
.. ' !! | .. ' !! RMS error ([[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 | local rms_str = string.format("%.2f", r.rms) | ||
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(' | table.insert(output, string.format('| %s || %s || %s || %s || %s || %s', | ||
play_btn, edo_link, steps_str, cents_str, err_str, | play_btn, edo_link, steps_str, cents_str, err_str, rms_str)) | ||
end | end | ||