local p = {}
local getArgs = require("Module:Arguments").getArgs
local mos = require("Module:MOS")
local rat = require("Module:Rational")
local tamnams = require("Module:TAMNAMS")
local yesno = require("Module:Yesno")
-- -- TODO:
-- - (High priority): Refactor code so instead of string concatenation, lines
-- are appended to a table, where table.concat() is called at the end.
-- EXPERIMENTAL FEATURE: lookup table for intervals
-- Mostly based off Margo Schulter's categories (without large/medium/small),
-- but other interpretations are possible. Plus, this only goes up to 1200c.
p.interval_ranges = {
{ name = "Pure unison (1:1)" , range = { 0, 0} },
{ name = "Comma/diesis" , range = { 0, 60} },
{ name = "Minor second" , range = { 60, 125} },
{ name = "Neutral second" , range = { 125, 170} },
{ name = "Major second" , range = { 180, 240} },
{ name = "Interseptimal (Maj2-min3)", range = { 240, 260} },
{ name = "Minor third" , range = { 260, 330} },
{ name = "Neutral third" , range = { 330, 372} },
{ name = "Major third" , range = { 372, 440} },
{ name = "Interseptimal (Maj3-4)" , range = { 440, 468} },
{ name = "Perfect fourth" , range = { 468, 528} },
{ name = "Superfourth" , range = { 528, 560} },
{ name = "Tritonic region" , range = { 560, 640} },
{ name = "Subfifth" , range = { 640, 672} },
{ name = "Perfect fifth" , range = { 672, 732} },
{ name = "Interseptimal (5-min6)" , range = { 732, 760} },
{ name = "Minor sixth" , range = { 760, 828} },
{ name = "Neutral sixth" , range = { 828, 870} },
{ name = "Major sixth" , range = { 870, 940} },
{ name = "Interseptimal (Maj6-min7)", range = { 940, 960} },
{ name = "Minor seventh" , range = { 960, 1020} },
{ name = "Neutral seventh" , range = {1030, 1075} },
{ name = "Major seventh" , range = {1075, 1140} },
{ name = "Octave less comma/diesis" , range = {1140, 1200} },
{ name = "Pure octave (2:1)" , range = {1200, 1200} }
}
-- EXPERIMENTAL FEATURE: interval lookup function
function p.lookup_interval_range(cents)
for _, interval in ipairs(p.interval_ranges) do
if cents >= interval.range[1] and cents <= interval.range[2] then
return interval.name
end
end
return "Out of range"
end
-- Main function; to be called by wrapper
function p._mos_intervals(args)
-- Default param for input mos is 5L 2s
local input_mos = args["Input MOS" ] or mos.new(5, 2, 2)
local mos_prefix = args["MOS Prefix" ] or "mos"
local mos_abbrev = args["MOS Abbrev" ] or "m"
local is_collapsed = args["Is Collapsed"] == true
local show_inregs = false
-- Get the scale sig
local scale_sig = mos.as_string(input_mos)
-- Get the brightest and darkest modes as step matrices
local bright_step_matrix = mos.mode_to_step_matrix(mos.brightest_mode(input_mos))
local dark_step_matrix = mos.mode_to_step_matrix(mos.darkest_mode(input_mos))
-- Get the number of steps per period and equave
local equave_step_count = mos.equave_step_count(input_mos)
local period_step_count = mos.period_step_count(input_mos)
-- Get the step counts for the bright and dark generators
local bright_gen_step_count = mos.bright_gen_step_count(input_mos)
local dark_gen_step_count = mos.dark_gen_step_count(input_mos)
-- Create the table
local result = '{| class="wikitable mw-collapsible' .. (is_collapsed and ' mw-collapsed"\n' or '"\n')
-- Create table title
result = result
.. '|+ style="font-size: 105%; white-space: nowrap;" | ' .. string.format('Intervals of %s', scale_sig) .. '\n'
.. '|-\n'
-- Create table headers
result = result
.. '! colspan="3" | Intervals\n'
.. '! rowspan="2" | Steps<br />subtended\n'
.. '! rowspan="2" | Range in cents\n'
.. '|-\n' -- Start of second row of header cells
.. '! Generic\n'
.. '! Specific\n'
.. '! Abbrev.\n'
.. (show_inregs and '! Interval Regions\n' or '')
-- Write each row
for i = 1, #bright_step_matrix do
-- Compare the bright and dark intervals. If they're the same, then the
-- current interval class is a period interval.
local current_bright_interval = bright_step_matrix[i]
local current_dark_interval = dark_step_matrix[i]
local is_period = mos.interval_eq(current_bright_interval, current_dark_interval)
-- If it's a period interval, then there is only one row to write.
-- Otherwise, there are two rows to write, one for each size.
if is_period then
local cents = mos.interval_to_cents(current_bright_interval, input_mos, {1, 1})
local cents_formatted = string.format("%.1f{{c}}", cents)
result = result
.. "|-\n"
.. "| '''" .. i-1 .. "-" .. mos_prefix .. "step'''\n"
.. "| " .. tamnams.interval_quality(current_bright_interval, input_mos, "sentence-case", mos_prefix) .. "\n"
.. "| " .. tamnams.interval_quality(current_bright_interval, input_mos, "abbrev" , mos_abbrev) .. "\n"
.. "| <span style=\"white-space: nowrap;\">" .. mos.interval_as_string(current_bright_interval) .. "</span>\n"
.. "| " .. cents_formatted .. "\n"
.. (show_inregs and string.format("| %s\n", p.lookup_interval_range(cents)) or "")
else
-- Calculate the cent values min and max for the current intervals
local sm_min_cents = mos.interval_to_cents(current_dark_interval, input_mos, {1,1})
local sm_max_cents = mos.interval_to_cents(current_dark_interval, input_mos, {1,0})
local lg_min_cents = mos.interval_to_cents(current_bright_interval, input_mos, {1,1})
local lg_max_cents = mos.interval_to_cents(current_bright_interval, input_mos, {1,0})
-- Then sort, as the min and max may be swapped
-- This happens if the dark interval has more small steps than large steps
local sm_min_sorted = math.min(sm_min_cents, sm_max_cents)
local sm_max_sorted = math.max(sm_min_cents, sm_max_cents)
local lg_min_sorted = math.min(lg_min_cents, lg_max_cents)
local lg_max_sorted = math.max(lg_min_cents, lg_max_cents)
-- Produce text ranges for intervals
local dark_interval_range = string.format("%.1f{{c}} to %.1f{{c}}", sm_min_sorted, sm_max_sorted)
local bright_interval_range = string.format("%.1f{{c}} to %.1f{{c}}", lg_min_sorted, lg_max_sorted)
result = result
.. "|-\n"
.. '| rowspan="2" | ' .. i-1 .. '-' .. mos_prefix .. 'step\n'
.. "| " .. tamnams.interval_quality(current_dark_interval, input_mos, "sentence-case", mos_prefix) .. "\n"
.. "| " .. tamnams.interval_quality(current_dark_interval, input_mos, "abbrev" , mos_abbrev) .. "\n"
.. "| <span style=\"white-space: nowrap;\">" .. mos.interval_as_string(current_dark_interval) .. "</span>\n"
.. "| " .. dark_interval_range .. "\n"
.. (show_inregs and string.format("| %s to %s\n", p.lookup_interval_range(sm_min_sorted), p.lookup_interval_range(sm_max_sorted)) or "")
.. "|-\n"
.. "| " .. tamnams.interval_quality(current_bright_interval, input_mos, "sentence-case", mos_prefix) .. "\n"
.. "| " .. tamnams.interval_quality(current_bright_interval, input_mos, "abbrev" , mos_abbrev) .. "\n"
.. "| <span style=\"white-space: nowrap;\">" .. mos.interval_as_string(current_bright_interval) .. "</span>\n"
.. "| " .. bright_interval_range .. "\n"
.. (show_inregs and string.format("| %s to %s\n", p.lookup_interval_range(lg_min_sorted), p.lookup_interval_range(lg_max_sorted)) or "")
end
end
result = result .. "|}"
return result
end
-- Wrapper function; to be called by template
function p.mos_intervals(frame)
local args = getArgs(frame)
-- Preprocess scalesig into input mos
local input_mos = mos.parse(args["Scale Signature"])
args["Input MOS"] = input_mos
args["Scale Signature"] = nil
-- Preprocess collapse option
args["Collapsed"] = yesno(args["Collapsed"], false)
-- EXPERIMENTAL: option to show interval regions
args["Show Interval Regions"] = yesno(args["Show Interval Regions"], false)
-- Preprocess (verify) prefix/abbrev
args["MOS Prefix"] = tamnams.verify_prefix(input_mos, args["MOS Prefix"])
args["MOS Abbrev"] = tamnams.verify_abbrev(input_mos, args["MOS Abbrev"])
local result = p._mos_intervals(args)
local debugg = yesno(args["debug"])
-- Debugger option
if debugg == true then
result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
end
return frame:preprocess(result)
end
return p