Module:Chord edo approximation: Difference between revisions
Jump to navigation
Jump to search
Tag: Undo |
mNo edit summary Tag: Manual revert |
||
| Line 1: | Line 1: | ||
-- | -- EDO Approximations Module | ||
-- Calculates EDO approximations for | -- Calculates EDO approximations for just intervals | ||
-- Usage: {{#invoke: | -- Usage: {{#invoke:EDO_Approximations|main|interval=3/2|tolerance=9|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 7: | ||
-- ===== CONFIGURATION VARIABLES ===== | -- ===== CONFIGURATION VARIABLES ===== | ||
local | local DEFAULT_TOLERANCE = 9.0 -- Relative error tolerance in percent | ||
local DEFAULT_MIN_EDO = 5 | local DEFAULT_MIN_EDO = 5 -- Minimum EDO to check | ||
local DEFAULT_MAX_EDO = 60 | local DEFAULT_MAX_EDO = 60 -- Maximum EDO to check | ||
-- ==================================== | -- ==================================== | ||
-- Convert a frequency ratio to cents | |||
local function cents(ratio) | local function cents(ratio) | ||
return 1200 * u.log2(ratio) | return 1200 * u.log2(ratio) | ||
end | end | ||
-- Python-compatible | -- Python-compatible rounding function (banker's rounding) | ||
local function round(x) | local function round(x) | ||
local floor_x = math.floor(x) | local floor_x = math.floor(x) | ||
local frac = x - floor_x | local frac = x - floor_x | ||
if frac < 0.5 then | if frac < 0.5 then | ||
return floor_x | return floor_x | ||
| Line 25: | Line 27: | ||
return floor_x + 1 | return floor_x + 1 | ||
else | else | ||
if floor_x % 2 == 0 then return floor_x else return floor_x + 1 end | if floor_x % 2 == 0 then | ||
return floor_x | |||
else | |||
return floor_x + 1 | |||
end | |||
end | end | ||
end | end | ||
local function | -- Find the best approximation of an interval in a given EDO | ||
local function find_best_approximation(ratio_cents, edo) | |||
return | local edostep = 1200 / edo | ||
local best_step = round(ratio_cents / edostep) | |||
local approximation_cents = best_step * edostep | |||
local absolute_error = approximation_cents - ratio_cents | |||
local relative_error = (absolute_error / edostep) * 100 | |||
return best_step, absolute_error, relative_error | |||
end | end | ||
-- | -- Calculate all EDO approximations within tolerance for a given ratio | ||
local function | local function calculate_edo_approximations(ratio, tolerance, min_edo, max_edo) | ||
local | local ratio_cents = cents(ratio) | ||
local results = {} | |||
for edo = min_edo, max_edo do | |||
local | local steps, abs_error, rel_error = find_best_approximation(ratio_cents, edo) | ||
if math.abs(rel_error) <= tolerance then | |||
table.insert(results, { | |||
edo = edo, | |||
steps = steps, | |||
abs_error = abs_error, | |||
rel_error = rel_error | |||
}) | |||
end | |||
end | end | ||
return | return results | ||
end | end | ||
-- Format a number with sign and 2 decimal places | |||
local function format_error(value) | local function format_error(value) | ||
if value >= 0 then | if value >= 0 then | ||
| Line 84: | Line 76: | ||
end | end | ||
-- Main function to generate the wikitable | |||
function p.main(frame) | function p.main(frame) | ||
local args = frame.args | local args = frame.args | ||
local | local interval_str = args.interval or args[1] | ||
local | local interval_name = args.interval_name | ||
local | local tolerance = tonumber(args.tolerance) or DEFAULT_TOLERANCE | ||
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 | ||
if not | if not interval_str then | ||
return "Error: No | return "Error: No interval specified" | ||
end | end | ||
local ratio = u.eval_num_arg(interval_str) | |||
local | if not ratio then | ||
return "Error: Invalid interval format (use format like '3/2')" | |||
end | end | ||
local results = calculate_edo_approximations(ratio, tolerance, min_edo, max_edo) | |||
local results = | |||
if #results == 0 then | if #results == 0 then | ||
return "No edos found within | return "No edos found within tolerance of " .. tolerance .. "%" | ||
end | end | ||
local output = {} | local output = {} | ||
table.insert(output, '{| class="wikitable center-all mw-collapsible sortable"') | table.insert(output, '{| class="wikitable center-all mw-collapsible sortable"') | ||
local | local ratio_cents = cents(ratio) | ||
local | local display_name = (interval_name and interval_name ~= "") and interval_name or interval_str | ||
table.insert(output, '|+ style="font-size: 105%;" | ' | |||
.. string.format('Edo approximations for %s (%.2f{{c}})<br /><span style="font-size: 0.75em;">\'\'≤ %dedo, relative error ≤ %g%%\'\'</span>', | |||
display_name, ratio_cents, max_edo, tolerance)) | |||
table.insert(output, '|-') | table.insert(output, '|-') | ||
table.insert(output, '! | table.insert(output, '! class="unsortable" | ' | ||
.. ' !! Edo' | |||
.. ' !! class="unsortable" | Step size' | |||
.. ' !! Cents ([[cent|¢]])' | |||
.. ' !! Absolute error ([[cent|¢]])' | |||
.. ' !! [[Relative interval error|Relative error]] ([[relative cent|%]])') | |||
for _, | for _, result in ipairs(results) do | ||
local edo_link = string.format("[[%dedo|%d]]", | local edo_link = string.format("[[%dedo|%d]]", result.edo, result.edo) | ||
local step_size = string.format("%d\\%d", result.steps, result.edo) | |||
local approximation_cents = result.steps * (1200 / result.edo) | |||
local approx_str = string.format("%.2f", approximation_cents) | |||
local abs_err = format_error(result.abs_error) | |||
local rel_err = format_error(result.rel_error) | |||
local | |||
local | |||
local | |||
local | |||
local | -- Play button: dyad of root (0) + the interval's step in this edo | ||
local | local steps_data = "0," .. tostring(result.steps) | ||
local play_btn = string.format( | |||
'<span class="edo-chord-play" data-edo="%d" data-steps="%s" title="Play %s in %dedo" role="button" tabindex="0">▶</span>', | |||
result.edo, steps_data, interval_str, result.edo) | |||
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', | ||
edo_link, | play_btn, edo_link, step_size, approx_str, abs_err, rel_err)) | ||
end | end | ||
Revision as of 01:31, 26 May 2026
Documentation for this module may be created at Module:Chord edo approximation/doc
-- EDO Approximations Module
-- Calculates EDO approximations for just intervals
-- Usage: {{#invoke:EDO_Approximations|main|interval=3/2|tolerance=9|min_edo=5|max_edo=60}}
local u = require("Module:Utils")
local yesno = require("Module:Yesno")
local p = {}
-- ===== CONFIGURATION VARIABLES =====
local DEFAULT_TOLERANCE = 9.0 -- Relative error tolerance in percent
local DEFAULT_MIN_EDO = 5 -- Minimum EDO to check
local DEFAULT_MAX_EDO = 60 -- Maximum EDO to check
-- ====================================
-- Convert a frequency ratio to cents
local function cents(ratio)
return 1200 * u.log2(ratio)
end
-- Python-compatible rounding function (banker's rounding)
local function round(x)
local floor_x = math.floor(x)
local frac = x - floor_x
if frac < 0.5 then
return floor_x
elseif frac > 0.5 then
return floor_x + 1
else
if floor_x % 2 == 0 then
return floor_x
else
return floor_x + 1
end
end
end
-- Find the best approximation of an interval in a given EDO
local function find_best_approximation(ratio_cents, edo)
local edostep = 1200 / edo
local best_step = round(ratio_cents / edostep)
local approximation_cents = best_step * edostep
local absolute_error = approximation_cents - ratio_cents
local relative_error = (absolute_error / edostep) * 100
return best_step, absolute_error, relative_error
end
-- Calculate all EDO approximations within tolerance for a given ratio
local function calculate_edo_approximations(ratio, tolerance, min_edo, max_edo)
local ratio_cents = cents(ratio)
local results = {}
for edo = min_edo, max_edo do
local steps, abs_error, rel_error = find_best_approximation(ratio_cents, edo)
if math.abs(rel_error) <= tolerance then
table.insert(results, {
edo = edo,
steps = steps,
abs_error = abs_error,
rel_error = rel_error
})
end
end
return results
end
-- Format a number with sign and 2 decimal places
local function format_error(value)
if value >= 0 then
return string.format("+%.2f", value)
else
return string.format("%.2f", value)
end
end
-- Main function to generate the wikitable
function p.main(frame)
local args = frame.args
local interval_str = args.interval or args[1]
local interval_name = args.interval_name
local tolerance = tonumber(args.tolerance) or DEFAULT_TOLERANCE
local min_edo = tonumber(args.min_edo) or DEFAULT_MIN_EDO
local max_edo = tonumber(args.max_edo) or DEFAULT_MAX_EDO
if not interval_str then
return "Error: No interval specified"
end
local ratio = u.eval_num_arg(interval_str)
if not ratio then
return "Error: Invalid interval format (use format like '3/2')"
end
local results = calculate_edo_approximations(ratio, tolerance, min_edo, max_edo)
if #results == 0 then
return "No edos found within tolerance of " .. tolerance .. "%"
end
local output = {}
table.insert(output, '{| class="wikitable center-all mw-collapsible sortable"')
local ratio_cents = cents(ratio)
local display_name = (interval_name and interval_name ~= "") and interval_name or interval_str
table.insert(output, '|+ style="font-size: 105%;" | '
.. string.format('Edo approximations for %s (%.2f{{c}})<br /><span style="font-size: 0.75em;">\'\'≤ %dedo, relative error ≤ %g%%\'\'</span>',
display_name, ratio_cents, max_edo, tolerance))
table.insert(output, '|-')
table.insert(output, '! class="unsortable" | '
.. ' !! Edo'
.. ' !! class="unsortable" | Step size'
.. ' !! Cents ([[cent|¢]])'
.. ' !! Absolute error ([[cent|¢]])'
.. ' !! [[Relative interval error|Relative error]] ([[relative cent|%]])')
for _, result in ipairs(results) do
local edo_link = string.format("[[%dedo|%d]]", result.edo, result.edo)
local step_size = string.format("%d\\%d", result.steps, result.edo)
local approximation_cents = result.steps * (1200 / result.edo)
local approx_str = string.format("%.2f", approximation_cents)
local abs_err = format_error(result.abs_error)
local rel_err = format_error(result.rel_error)
-- Play button: dyad of root (0) + the interval's step in this edo
local steps_data = "0," .. tostring(result.steps)
local play_btn = string.format(
'<span class="edo-chord-play" data-edo="%d" data-steps="%s" title="Play %s in %dedo" role="button" tabindex="0">▶</span>',
result.edo, steps_data, interval_str, result.edo)
table.insert(output, '|-')
table.insert(output, string.format('| %s || %s || %s || %s || %s || %s',
play_btn, edo_link, step_size, approx_str, abs_err, rel_err))
end
table.insert(output, '|}')
local result = table.concat(output, '\n')
if yesno(frame.args["debug"]) == true then
result = '<syntaxhighlight lang="wikitext">' .. result .. '</syntaxhighlight>'
end
return frame:preprocess(result)
end
return p