Module:MOS intervals: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Ganaram inukshuk (talk | contribs)
mNo edit summary
Ganaram inukshuk (talk | contribs)
todo
 
(166 intermediate revisions by 5 users not shown)
Line 1: Line 1:
local mos = require('Module:MOS')
local rat = require('Module:Rational')
local p = {}
local p = {}


-- Helper function that turns a mosstep into a step count as a string
local getArgs = require("Module:Arguments").getArgs
-- EG, the 3-mosstep of "LLsLsLs" becomes "2L + s"
local mos = require("Module:MOS")
function p.mos_interval_to_step_count_string(step_pattern, mossteps)
local rat = require("Module:Rational")
local tamnams = require("Module:TAMNAMS")
local L_count = 0
local yesno = require("Module:Yesno")
local s_count = 0
 
for i = 1, mossteps do
-- -- TODO:
local step string.sub(step_pattern, i, i)
-- - (High priority): Refactor code so instead of string concatenation, lines
if step == "L" then
--   are appended to a table, where table.concat() is called at the end.
L_count = L_count + 1
 
elseif step == "s" then
-- EXPERIMENTAL FEATURE: lookup table for intervals
s_count = s_count + 1
-- Mostly based off Margo Schulter's categories (without large/medium/small),
end
-- but other interpretations are possible. Plus, this only goes up to 1200c.
end
p.interval_ranges = {
    { name = "Pure unison (1:1)"        , range = 0,    0} },
local return_string = ""
    { name = "Comma/diesis"            , range = 0,  60} },
if L_count == 0 and s == 0 then
    { name = "Minor second"            , range = { 60, 125} },
return_string = "0"
    { name = "Neutral second"          , range = { 125,  170} },
elseif L_count == 1 and s == 1 then
    { name = "Major second"             , range = { 180,  240} },
return_string = "L + s"
    { name = "Interseptimal (Maj2-min3)", range = { 240,  260} },
elseif L_count == 1 and s_count == 0 then
    { name = "Minor third"             , range = { 260,  330} },
return_string = "L"
    { name = "Neutral third"           , range = { 330,  372} },
elseif L_count == 1 and s_count > 2 then
    { name = "Major third"              , range = { 372,  440} },
return_string = "L + " .. s_count .. "s"
    { name = "Interseptimal (Maj3-4)"  , range = { 440,  468} },
elseif L_count == 0 and s_count == 1 then
    { name = "Perfect fourth"           , range = { 468,  528} },
return_string = "s"
    { name = "Superfourth"              , range = { 528,  560} },
elseif L_count > 2 and s_count == 1 then
    { name = "Tritonic region"          , range = { 560,  640} },
return_string = L_count .. "L + s"
    { name = "Subfifth"                 , range = { 640,  672} },
else
    { name = "Perfect fifth"            , range = { 672,  732} },
return_string = L_count .. "L + " .. s_count .. "s"
    { name = "Interseptimal (5-min6)"  , range = { 732,  760} },
end
    { name = "Minor sixth"             , range = { 760,  828} },
return return_string
    { 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
end


function p.mos_intervals_frame(frame)
-- Main function; to be called by wrapper
function p._mos_intervals(args)
-- Default param for input mos is 5L 2s
-- Default param for input mos is 5L 2s
local input_mos = mos.parse(frame.args['Scale Signature']) or mos.new(2, 5, 2)
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 brightest and darkest modes for the mos
-- Get the scale sig
local brightest_mode = mos.brightest_mode(input_mos)
local scale_sig = mos.as_string(input_mos)
local darkest_mode = string.reverse(brightest_mode)
-- 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
-- Get the number of steps per period and equave
local steps_per_equave = (input_mos.nL + input_mos.ns)
local equave_step_count = mos.equave_step_count(input_mos)
local steps_per_period = steps_per_equave / rat.gcd(input_mos.nL, input_mos.ns)
local period_step_count = mos.period_step_count(input_mos)
-- Get the step counts for the bright and dark generators
-- Get the step counts for the bright and dark generators
local bright_gen = mos.bright_gen(input_mos)
local bright_gen_step_count = mos.bright_gen_step_count(input_mos)
local steps_per_bright_gen = bright_gen['L'] + bright_gen['s']
local dark_gen_step_count = mos.dark_gen_step_count(input_mos)
local steps_per_dark_gen = steps_per_period - steps_per_bright_gen
-- Get the scale sig
-- Create the table
local scale_sig = mos.as_string(input_mos)
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)
-- Create the table, starting with the headers
-- EXPERIMENTAL: option to show interval regions
local result = '{| class="wikitable"\n'
args["Show Interval Regions"] = yesno(args["Show Interval Regions"], false)
result = result .. '|+\n'
result = result .. scale_sig .. ' interval varieties\n'
result = result .. '! rowspan="2" |Interval class\n'
result = result .. '! colspan="2" |Large variety\n'
result = result .. '! colspan="2" |Small variety\n'
result = result .. '|-\n'
result = result .. '! Size\n'
result = result .. '! Quality\n'
result = result .. '!Size\n'
result = result .. '!Quality\n'
-- First row is the unison
-- Preprocess (verify) prefix/abbrev
result = result .. "|-\n"
args["MOS Prefix"] = tamnams.verify_prefix(input_mos, args["MOS Prefix"])
result = result .. "|'''0-mosstep (unison)'''\n"
args["MOS Abbrev"] = tamnams.verify_abbrev(input_mos, args["MOS Abbrev"])
result = result .. "|0\n"
 
result = result .. "|Perfect\n"
local result = p._mos_intervals(args)
result = result .. "|0\n"
local debugg = yesno(args["debug"])
result = result .. "|Perfect\n"
-- Successive rows are the mossteps, starting at the 1-mosstep
-- Debugger option
-- Name the interval according to whether it's major/minor or
if debugg == true then
-- perf/aug/dim; for nL ns mosses, it's only major/minor for any
result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
-- non-period intervals.
local is_nL_ns = input_mos.nL == input_mos.ns
for i = 1, steps_per_equave do
-- If i corresponds to the bright generator, then the large size is
-- perfect and the small size is diminished.
-- If i corresponds to the dark generator, then the large size is
-- augmented and the small size is perfect.
-- If i corresponds to the period, then the large and small sizes are
-- the same and they're perfect.
-- For any other interval, and for generators for nL ns mosses, the
-- large size is major and the small size is minor.
if i % steps_per_period == steps_per_bright_gen and not is_nL_ns then
result = result .. "|'''" .. i .. "-mosstep'''\n"
result = result .. "|" .. p.mos_interval_to_step_count_string(brightest_mode, i) .. "\n"
result = result .. "|Perfect\n"
result = result .. "|" .. p.mos_interval_to_step_count_string(darkest_mode, i) .. "\n"
result = result .. "|Diminished\n"
elseif i % steps_per_period == steps_per_bright_gen and not is_nL_ns then
result = result .. "|'''" .. i .. "-mosstep'''\n"
result = result .. "|" .. p.mos_interval_to_step_count_string(brightest_mode, i) .. "\n"
result = result .. "|Augmented\n"
result = result .. "|" .. p.mos_interval_to_step_count_string(darkest_mode, i) .. "\n"
result = result .. "|Perfect\n"
elseif i % steps_per_period == 0 then
result = result .. "|'''" .. i .. "-mosstep (period)'''\n"
result = result .. "|" .. p.mos_interval_to_step_count_string(brightest_mode, i) .. "\n"
result = result .. "|Perfect\n"
result = result .. "|" .. p.mos_interval_to_step_count_string(darkest_mode, i) .. "\n"
result = result .. "|Perfect\n"
else
result = result .. "|" .. i .. "-mosstep\n"
result = result .. "|" .. p.mos_interval_to_step_count_string(brightest_mode, i) .. "\n"
result = result .. "|Major\n"
result = result .. "|" .. p.mos_interval_to_step_count_string(darkest_mode, i) .. "\n"
result = result .. "|Minor\n"
end
end
end
return result
return frame:preprocess(result)
end
end


return p
return p

Latest revision as of 22:12, 7 October 2025

Module documentation[view] [edit] [history] [purge]
This module should not be invoked directly; use its corresponding template instead: Template:MOS intervals.

This module generates a table showing interval varieties and qualities for a given MOS scale.

Introspection summary for Module:MOS intervals 
Functions provided (3)
Line Function Params
45 lookup_interval_range (cents)
55 _mos_intervals (main) (args)
160 mos_intervals (invokable) (frame)
Lua modules required (5)
Variable Module Functions used
getArgs Module:Arguments getArgs
mos Module:MOS new
as_string
mode_to_step_matrix
brightest_mode
darkest_mode
equave_step_count
period_step_count
bright_gen_step_count
dark_gen_step_count
interval_eq
interval_to_cents
interval_as_string
parse
rat Module:Rational dependency not used
tamnams Module:TAMNAMS interval_quality
verify_prefix
verify_abbrev
yesno Module:Yesno yesno

No function descriptions were provided. The Lua code may have further information.


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