Module:MOS tuning spectrum: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Ganaram inukshuk (talk | contribs)
New template because I hated my old code so much
 
Ganaram inukshuk (talk | contribs)
mNo edit summary
 
(46 intermediate revisions by 2 users not shown)
Line 1: Line 1:
local MOS = require("Module:MOS")
local mos      = require("Module:MOS")
local ET = require("Module:ET")
local mediants = require("Module:Mediants")
local rat = require("Module:Rational")
local tip      = require("Module:Template input parse")
local sb = require("Module:SB tree")
local utils    = require("Module:Utils")
local utils = require("Module:Utils")
local yesno    = require("Module:yesno")
local yesno = require("Module:yesno")
local getArgs  = require("Module:Arguments").getArgs
 
local p = {}
local p = {}


-- Helper function that parses entries from a semicolon-delimited string and returns them in an array
-- TODO:
-- TODO: Separate this and parse_pairs into its own module of helper functions, as they're included
-- - Add back old option from previous scale tree: toggle staggering of ratios.
-- in various modules at this point
-- - (Low priority): Force-add comments that lie outside searched depth. If such
function p.parse_entries(unparsed)
--   ratios are added, they're added in either the last column or their own
local parsed = {}
--  column appended after the last one. (to be determined)
-- for entry in string.gmatch(unparsed, "([^;]+)") do
for entry in string.gmatch(unparsed, "([^%$]+)") do
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
table.insert(parsed, trimmed) -- Add to array
end
return parsed
end


-- Helper function that parses pairs of elements separated by a colon
-- Split comments into in-range and out-of-range comments.
-- A pair must be two elements or it will be returned as an empty array
-- Iterate through the comments and determine whether the key for a comment is
function p.parse_pair(unparsed)
-- in the table of step ratios. If so, add the corresponding key-value pair into
local parsed = {}
-- the table of in-comments; if not, add to the table of out-comments.
for entry in string.gmatch(unparsed, "([^:]+)") do
function p.preprocess_comments(comments, step_ratios, depths)
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
local in_comments  = {}
table.insert(parsed, trimmed) -- Add to array
local out_comments = {}
end
local out_ratios  = {}
if #parsed == 2 then
return parsed
for key, value in pairs(comments) do
else
local key_found_in_step_ratios = utils.table_contains(step_ratios, key, function(key, ratio)
return {}
return key == string.format("%s/%s", ratio[1], ratio[2]) end
end
)
end
 
if key_found_in_step_ratios then
-- Function that takes a list of semicolon-delimited pairs and returns a map
in_comments[key] = value
-- (or dictionary or associative array) of key-value pairs
else
-- Each entry is colon-delimited as key : pair
out_comments[key] = value
function p.parse_kv_pairs(unparsed)
table.insert(out_ratios, tip.parse_numeric_pair(key, "/", true))
-- Tokenize the string of unparsed pairs
local parsed = p.parse_entries(unparsed)
-- Then tokenize the tokens into key-value pairs
local pairs_ = {}
for i = 1, #parsed do
local pair = p.parse_pair(parsed[i])
if #pair == 2 then
pairs_[pair[1]] = pair[2]
end
end
end
end
return pairs_
mediants.sort_ratios(out_ratios)
return in_comments, out_comments, out_ratios
end
end


-- Rewrite of scale tree function (has bugfixes and new formatting)
-- Re-re-rewrite of tuning spectrum
function p._scale_tree(input_mos, depth, comments)
function p._mos_tuning_spectrum(args)
local input_mos = input_mos or MOS.new(5, 2)
-- Find default ratios, if nothing was entered
local depth = depth or 5
local default_ratios, default_depths
local comments = comments or {}
local default_depth = 5
default_ratios, default_depths = mediants.find_mediants({{1,1}, {1,0}}, default_depth)
local equave = input_mos.equave
-- Extract info from args
local L = input_mos.nL -- Large steps in mos
local input_mos  = args["Input MOS"  ] or mos.new(5, 2)
local s = input_mos.ns -- Small steps in mos
local comments    = args["Comments"  ] or {}
local n = utils._gcd(L, s) -- Number of periods
local step_ratios = args["Step Ratios"] or default_ratios
local abstract_bright_gen = MOS.bright_gen(input_mos)
local depths      = args["Depths"    ] or default_depths
local step_ratios = sb.sb_tree_ratios(depth)
-- Find the deepest searched depth at which ratios were found
local depths = sb.sb_tree_depths(depth)
local deepest = mediants.deepest_depth(depths)
-- What is the equave suffix (edo, edt, edf, ed-p/q)
-- Find mos info
local equave_suffix = ""
local equave      = input_mos.equave
if rat.eq(input_mos.equave, rat.new(2)) then
local large_steps = input_mos.nL -- Large steps in mos
equave_suffix = "o"
local small_steps = input_mos.ns -- Small steps in mos
elseif rat.eq(input_mos.equave, rat.new(3)) then
local periods    = mos.period_count(input_mos) -- Number of periods
equave_suffix = "t"
local et_suffix  = mos.et_suffix(input_mos) -- Mos's ET suffix (edo, edt, edf, ed-p/q, etc)
elseif rat.eq(input_mos.equave, rat.new(3, 2)) then
equave_suffix = "f"
else
equave_suffix = rat.as_ratio(input_mos.equave)
end
-- Default comments for TAMNAMS-named step ratios
-- Default comments for TAMNAMS-named step ratios
local default_comments = {}
local default_comments = {}
local mos_as_string = MOS.as_string(input_mos)
local mos_as_string = mos.as_string(input_mos)
default_comments["1/1"] = string.format("'''Equalized %s'''", mos_as_string)
default_comments["1/1"] = string.format("'''Equalized %s'''", mos_as_string)
default_comments["4/3"] = string.format("'''Supersoft %s'''", mos_as_string)
default_comments["4/3"] = string.format("'''Supersoft %s'''", mos_as_string)
default_comments["3/2"] = string.format("'''Soft %s'''", mos_as_string)
default_comments["3/2"] = string.format("'''Soft %s'''"     , mos_as_string)
default_comments["5/3"] = string.format("'''Semisoft %s'''", mos_as_string)
default_comments["5/3"] = string.format("'''Semisoft %s'''" , mos_as_string)
default_comments["2/1"] = string.format("'''Basic %s'''", mos_as_string)
default_comments["2/1"] = string.format("'''Basic %s'''"   , mos_as_string)
default_comments["5/2"] = string.format("'''Semihard %s'''", mos_as_string)
default_comments["5/2"] = string.format("'''Semihard %s'''" , mos_as_string)
default_comments["3/1"] = string.format("'''Hard %s'''", mos_as_string)
default_comments["3/1"] = string.format("'''Hard %s'''"     , mos_as_string)
default_comments["4/1"] = string.format("'''Superhard %s'''", mos_as_string)
default_comments["4/1"] = string.format("'''Superhard %s'''", mos_as_string)
default_comments["1/0"] = string.format("'''Collapsed %s'''", mos_as_string)
default_comments["1/0"] = string.format("'''Collapsed %s'''", mos_as_string)
Line 95: Line 79:
-- Monosmall mosses and knL ns mosses are always proper, but all other mosses
-- Monosmall mosses and knL ns mosses are always proper, but all other mosses
-- are proper if the step ratio is within the soft-of-basic range
-- are proper if the step ratio is within the soft-of-basic range
if n < s then
if periods < small_steps then
default_comments["2/1"] = default_comments["2/1"] .. "<br />Scales with tunings softer than this are proper"
default_comments["2/1"] = default_comments["2/1"] .. "<br />Scales with tunings softer than this are proper"
end
end
Line 101: Line 85:
-- Produce table header for the comments
-- Produce table header for the comments
local comments_header_text = "Comments"
local comments_header_text = "Comments"
if s == 1 then
if small_steps == 1 then
comments_header_text = comments_header_text .. "<sup><abbr title=\"Every tuning produces a proper scale.\">(always proper)</abbr></sup>"
comments_header_text = comments_header_text .. "<sup><abbr title=\"Every tuning produces a proper scale.\">(always proper)</abbr></sup>"
elseif s == n and n > 1 then
elseif small_steps == periods and periods > 1 then
comments_header_text = comments_header_text .. "<sup><abbr title=\"Every true-MOS tuning produces a proper scale.\">(always proper)</abbr></sup>"
comments_header_text = comments_header_text .. "<sup><abbr title=\"Every true-mos tuning produces a proper scale.\">(always proper)</abbr></sup>"
end
end
Line 113: Line 97:
-- - Step ratio and hardness
-- - Step ratio and hardness
-- - Comments
-- - Comments
local result = "{| class=\"wikitable center-all\"\n"
local result = {}
.. "|+ style=\"font-size: 105%; white-space: nowrap;\" | " .. string.format("Scale tree and tuning spectrum of %s\n", mos_as_string)
table.insert(result, '{| class="wikitable center-all"')
.. "|-\n"
table.insert(result, '|+ style="font-size: 105%; white-space: nowrap;" | Scale tree and tuning spectrum of ' ..  mos_as_string)
.. string.format("! rowspan=\"2\" colspan=\"%d\" | Generator<sup><abbr title=\"In steps of ed%s.\">(ed%s)</abbr></sup>\n", depth + 1, equave_suffix, equave_suffix)
table.insert(result, '|-')
.. "! colspan=\"2\" | Cents\n"
table.insert(result, string.format('! rowspan="2" colspan="%d" | Generator<sup><abbr title="In steps of %s.">(%s)</abbr></sup>', deepest + 1, et_suffix, et_suffix))
.. "! colspan=\"2\" | Step ratio\n"
table.insert(result, '! colspan="2" | Cents')
.. "! rowspan=\"2\" | " .. comments_header_text .. "\n"
table.insert(result, '! colspan="2" | Step ratio')
.. "|-\n"
table.insert(result, string.format('! rowspan="2" | %s', comments_header_text))
.. "! Bright\n"
table.insert(result, '|-')
.. "! Dark\n"
table.insert(result, '! <abbr title="Chroma-positive generator">Bright</abbr>')
.. "! L:s\n"
table.insert(result, '! <abbr title="Chroma-negative generator">Dark</abbr>')
.. "! Hardness\n"
table.insert(result, '! L:s')
table.insert(result, '! Hardness')


-- Rounding is done using string.format, to 3 decimal places (%.3f)
-- Rounding is done using string.format, to 3 decimal places (%.3f)
Line 130: Line 115:
-- Create each row of the table
-- Create each row of the table
for i = 1, #step_ratios do
for i = 1, #step_ratios do
local step_ratio = step_ratios[i]
-- Simplify step ratio before using it
local steps_per_equave = step_ratio[1] * L + step_ratio[2] * s
local step_ratio = mediants.simplify_ratio(step_ratios[i])
local steps_per_period = steps_per_equave / n
local et = ET.new(steps_per_equave, equave)
-- Produce current ET as a string
local et_as_string = mos.et_string(input_mos, step_ratio)
-- Calculate the bright gen and cent value
-- Calculate the bright gen and cent value
local bright_generator_steps = step_ratio[1] * abstract_bright_gen["L"] + step_ratio[2] * abstract_bright_gen["s"]
local bright_generator_steps = mos.bright_gen_to_et_steps (input_mos, step_ratio)
local bright_generator_cents = ET.cents(et, bright_generator_steps)
local bright_generator_cents  = mos.bright_gen_to_cents    (input_mos, step_ratio)
local bright_generator_string = mos.bright_gen_to_et_string(input_mos, step_ratio, "")
-- Calculate dark generator step count and cent value
-- Calculate dark generator step count and cent value
local dark_generator_steps = steps_per_period - bright_generator_steps
local dark_generator_steps = mos.dark_gen_to_et_steps(input_mos, step_ratio)
local dark_generator_cents = ET.cents(et, dark_generator_steps)
local dark_generator_cents = mos.dark_gen_to_cents  (input_mos, step_ratio)
-- New row
-- New row
result = result .. "|-\n"
table.insert(result, "|-")
-- Cells for bright generator, as steps in et
-- Cells for bright generator, as steps in et
-- Bright gen cell is accompanied by cells to the left and right.
-- The variable current_depth is the number of left cells; the number of
-- right cells is deepest minus current_depth.
local current_depth = depths[i]
local current_depth = depths[i]
for i = 1, depth + 1 do
local num_right_cells = deepest - current_depth
result = result .. "| "
for i = 1, current_depth do
if i == current_depth then
table.insert(result, "|")
result = result .. string.format("[[%s|%d\\%s]]", ET.as_string(et), bright_generator_steps, et.size)
end
end
table.insert(result, string.format("| [[%s|%s]]", et_as_string, bright_generator_string))
result = result .. "\n"
for i = 1, num_right_cells do
table.insert(result, "|")
end
end
-- Cells for generators in cents
-- Cells for generators in cents
result = result .. string.format("| %.3f\n", bright_generator_cents)
table.insert(result, string.format("| %.3f", bright_generator_cents))
result = result .. string.format("| %.3f\n", dark_generator_cents)
table.insert(result, string.format("| %.3f", dark_generator_cents ))
-- Cell for step ratio
-- Cell for step ratio
result = result .. string.format("| %d:%d\n", step_ratio[1], step_ratio[2])
table.insert(result, string.format("| %d:%d", step_ratio[1], step_ratio[2]))
-- Cell for hardness, with divide-by-zero check
-- Cell for hardness, with divide-by-zero check
Line 170: Line 161:
hardness = string.format("%.3f", step_ratio[1] / step_ratio[2])
hardness = string.format("%.3f", step_ratio[1] / step_ratio[2])
end
end
result = result .. string.format("| %s\n", hardness)
table.insert(result, string.format("| %s", hardness))
-- Cell for comment
-- Cell for comment
-- Default comments are on their own line before custom comments
-- Default comments are on their own line before custom comments
local key = step_ratios[i][1] .. "/" .. step_ratios[i][2] -- The step ratio is (literally and figuratively) the key to add comments!
local key = step_ratio[1] .. "/" .. step_ratio[2] -- The step ratio is (literally and figuratively) the key to add comments!
local comment = ""
local comment = ""
local default_comment = default_comments[key] or ""
local default_comment = default_comments[key] or ""
Line 183: Line 174:
comment = default_comment .. "<br />" .. custom_comment
comment = default_comment .. "<br />" .. custom_comment
end
end
result = result .. string.format("| style=\"text-align: left;\" | %s\n", comment)
table.insert(result, string.format("| style=\"text-align: left;\" | %s", comment))
end
end
result = result .. "|}"
table.insert(result, "|}")
return result
return table.concat(result, "\n")
end
end


function p.scale_tree(frame)
-- Wrapper function; to be called by template; preprocesses args
local mos = MOS.parse(frame.args["tuning"])
-- Unprocessed args:
local depth = frame.args["depth"] or 5
-- - Scale Signature (textual scalesig)
local comments_unparsed = frame.args["Comments"] or ""
-- - Depth (how deep to search for mediants)
local comments = p.parse_kv_pairs(comments_unparsed) or {}
-- - Int Limit (if present, overrides depth-based search for int-limit search)
-- - X/Y (comments for individual step ratios)
-- - First/Last Step Ratio (initial ratios for mediant search; default is
--  1/1 and 1/0 respectively)
-- Processed args:
-- - Input MOS (processed from textual scalesig)
-- - Step Ratios (table of step ratios found)
-- - Depths (table of how deep each step ratio was found at)
-- - Comments (step ratio comments placed in their own table)
function p.mos_tuning_spectrum(frame)
local args = getArgs(frame)
-- Parse scalesig as input mos, then remove scalesig
local input_mos = mos.parse(args["Scale Signature"])
args["Input MOS"] = input_mos
args["Scale Signature"] = nil
local out_str = p._scale_tree(mos, depth, comments)
-- Parse initial ratios
local debugg = yesno(frame.args["debug"])
local initial_ratios_unparsed = args["Initial Ratios"]
return frame:preprocess(debugg == true and "<pre>" .. out_str .. "</pre>" or out_str)
local initial_ratios = {}
if initial_ratios_unparsed ~= nil then
-- Parse initial ratios, then sort by ascending
initial_ratios = tip.parse_numeric_pairs(initial_ratios_unparsed, ";", "/", true)
mediants.sort_ratios(initial_ratios)
else
-- If no initial ratios were passed in, default to 1/1 and 1/0
initial_ratios = {{1,1}, {1,0}}
end
-- Then generate mediants and depths
-- If using depth-based search, parse the depth, then remove it from args
-- If using int-limit search, parse the int limit, then remove it from args,
-- and remove depth in case it was present.
local step_ratios, depths
local use_int_limit = args["Int Limit"] ~= nil
if use_int_limit then
-- Int-limit search; parse int-limit
local int_limit = tonumber(args["Int Limit"])
args["Int Limit"] = nil
-- Find ratios and depths found
step_ratios, depths = mediants.find_mediants_by_int_limit(initial_ratios, int_limit)
else
-- Depth-based search; parse depth
local depth = tonumber(args["Depth"]) or 5
-- Find ratios and depths found
step_ratios, depths = mediants.find_mediants(initial_ratios, depth)
end
args["Depth"      ] = nil
args["Step Ratios"] = step_ratios
args["Depths"    ] = depths
-- Transfer comments from args to comments
local comments = {}
for i = 1, #step_ratios do
local key = step_ratios[i][1] .. "/" .. step_ratios[i][2]
if args[key] ~= nil then
comments[key] = args[key]
args[key] = nil
end
end
args["Comments"] = comments
-- Parse debug option
local debugg = yesno(args["debug"])
-- Output
local result = p._mos_tuning_spectrum(args)
if debugg == true then
result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
end
return frame:preprocess(result)
end
 
function p.tester()
--local args = {}
--return p._mos_tuning_spectrum(args)
 
local comments = {
["5/4"] = "aaa",
["2/1"] = "bbb"
}
local step_ratios = {
{1,1},
{5,4},
{4,3}
}
local in_comments = {}
local out_comments = {}
in_comments, out_comments, out_ratios = p.preprocess_comments(comments, step_ratios)
 
local args = {}
--return out_comments
return p._mos_tuning_spectrum(args)
end
end


return p
return p

Latest revision as of 21:30, 8 October 2025

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

This template displays the tuning spectrum of a given mos scale, showing various step ratios between 1:1 to 1:0 (by default).

Introspection summary for Module:MOS tuning spectrum 
Functions provided (4)
Line Function Params
20 preprocess_comments (comments, step_ratios, depths)
43 _mos_tuning_spectrum (main) (args)
197 mos_tuning_spectrum (invokable) (frame)
264 tester none
Lua modules required (6)
Variable Module Functions used
getArgs Module:Arguments getArgs
mediants Module:Mediants sort_ratios
find_mediants
deepest_depth
simplify_ratio
find_mediants_by_int_limit
mos Module:MOS new
period_count
et_suffix
as_string
et_string
bright_gen_to_et_steps
bright_gen_to_cents
bright_gen_to_et_string
dark_gen_to_et_steps
dark_gen_to_cents
parse
tip Module:Template input parse parse_numeric_pair
parse_numeric_pairs
utils Module:Utils table_contains
yesno Module:yesno yesno

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


local mos      = require("Module:MOS")
local mediants = require("Module:Mediants")
local tip      = require("Module:Template input parse")
local utils    = require("Module:Utils")
local yesno    = require("Module:yesno")
local getArgs  = require("Module:Arguments").getArgs

local p = {}

-- TODO:
-- - Add back old option from previous scale tree: toggle staggering of ratios.
-- - (Low priority): Force-add comments that lie outside searched depth. If such
--   ratios are added, they're added in either the last column or their own
--   column appended after the last one. (to be determined)

-- Split comments into in-range and out-of-range comments.
-- Iterate through the comments and determine whether the key for a comment is
-- in the table of step ratios. If so, add the corresponding key-value pair into
-- the table of in-comments; if not, add to the table of out-comments.
function p.preprocess_comments(comments, step_ratios, depths)
	local in_comments  = {}
	local out_comments = {}
	local out_ratios   = {}
	
	for key, value in pairs(comments) do
		local key_found_in_step_ratios = utils.table_contains(step_ratios, key, function(key, ratio)
			return key == string.format("%s/%s", ratio[1], ratio[2]) end
		)
		
		if key_found_in_step_ratios then
			in_comments[key] = value
		else
			out_comments[key] = value
			table.insert(out_ratios, tip.parse_numeric_pair(key, "/", true))
		end
	end
	mediants.sort_ratios(out_ratios)
	
	return in_comments, out_comments, out_ratios
end

-- Re-re-rewrite of tuning spectrum
function p._mos_tuning_spectrum(args)
	-- Find default ratios, if nothing was entered
	local default_ratios, default_depths
	local default_depth = 5
	default_ratios, default_depths = mediants.find_mediants({{1,1}, {1,0}}, default_depth)
	
	-- Extract info from args
	local input_mos   = args["Input MOS"  ] or mos.new(5, 2)
	local comments    = args["Comments"   ] or {}
	local step_ratios = args["Step Ratios"] or default_ratios
	local depths      = args["Depths"     ] or default_depths
	
	-- Find the deepest searched depth at which ratios were found
	local deepest = mediants.deepest_depth(depths)
	
	-- Find mos info
	local equave      = input_mos.equave
	local large_steps = input_mos.nL		-- Large steps in mos
	local small_steps = input_mos.ns		-- Small steps in mos
	local periods     = mos.period_count(input_mos)	-- Number of periods
	local et_suffix   = mos.et_suffix(input_mos)	-- Mos's ET suffix (edo, edt, edf, ed-p/q, etc)
	
	-- Default comments for TAMNAMS-named step ratios
	local default_comments = {}
	local mos_as_string = mos.as_string(input_mos)
	default_comments["1/1"] = string.format("'''Equalized %s'''", mos_as_string)
	default_comments["4/3"] = string.format("'''Supersoft %s'''", mos_as_string)
	default_comments["3/2"] = string.format("'''Soft %s'''"     , mos_as_string)
	default_comments["5/3"] = string.format("'''Semisoft %s'''" , mos_as_string)
	default_comments["2/1"] = string.format("'''Basic %s'''"    , mos_as_string)
	default_comments["5/2"] = string.format("'''Semihard %s'''" , mos_as_string)
	default_comments["3/1"] = string.format("'''Hard %s'''"     , mos_as_string)
	default_comments["4/1"] = string.format("'''Superhard %s'''", mos_as_string)
	default_comments["1/0"] = string.format("'''Collapsed %s'''", mos_as_string)
	
	-- Append boundary of proper scales to basic comment, if applicable
	-- Monosmall mosses and knL ns mosses are always proper, but all other mosses
	-- are proper if the step ratio is within the soft-of-basic range
	if periods < small_steps then
		default_comments["2/1"] = default_comments["2/1"] .. "<br />Scales with tunings softer than this are proper"
	end
	
	-- Produce table header for the comments
	local comments_header_text = "Comments"
	if small_steps == 1 then
		comments_header_text = comments_header_text .. "<sup><abbr title=\"Every tuning produces a proper scale.\">(always proper)</abbr></sup>"
	elseif small_steps == periods and periods > 1 then
		comments_header_text = comments_header_text .. "<sup><abbr title=\"Every true-mos tuning produces a proper scale.\">(always proper)</abbr></sup>"
	end
	
	-- Table headers
	-- There are 6 columns:
	-- - Steps of ED
	-- - Bright and dark gens in cents
	-- - Step ratio and hardness
	-- - Comments
	local result = {}
	table.insert(result, '{| class="wikitable center-all"')
	table.insert(result, '|+ style="font-size: 105%; white-space: nowrap;" | Scale tree and tuning spectrum of ' ..  mos_as_string)
	table.insert(result, '|-')
	table.insert(result, string.format('! rowspan="2" colspan="%d" | Generator<sup><abbr title="In steps of %s.">(%s)</abbr></sup>', deepest + 1, et_suffix, et_suffix))
	table.insert(result, '! colspan="2" | Cents')
	table.insert(result, '! colspan="2" | Step ratio')
	table.insert(result, string.format('! rowspan="2" | %s', comments_header_text))
	table.insert(result, '|-')
	table.insert(result, '! <abbr title="Chroma-positive generator">Bright</abbr>')
	table.insert(result, '! <abbr title="Chroma-negative generator">Dark</abbr>')
	table.insert(result, '! L:s')
	table.insert(result, '! Hardness')

	-- Rounding is done using string.format, to 3 decimal places (%.3f)
	
	-- Create each row of the table
	for i = 1, #step_ratios do
		-- Simplify step ratio before using it
		local step_ratio = mediants.simplify_ratio(step_ratios[i])
		
		-- Produce current ET as a string
		local et_as_string = mos.et_string(input_mos, step_ratio)
		
		-- Calculate the bright gen and cent value
		local bright_generator_steps  = mos.bright_gen_to_et_steps (input_mos, step_ratio)
		local bright_generator_cents  = mos.bright_gen_to_cents    (input_mos, step_ratio)
		local bright_generator_string = mos.bright_gen_to_et_string(input_mos, step_ratio, "")
		
		-- Calculate dark generator step count and cent value
		local dark_generator_steps = mos.dark_gen_to_et_steps(input_mos, step_ratio)
		local dark_generator_cents = mos.dark_gen_to_cents   (input_mos, step_ratio)
		
		-- New row
		table.insert(result, "|-")
		
		-- Cells for bright generator, as steps in et
		-- Bright gen cell is accompanied by cells to the left and right.
		-- The variable current_depth is the number of left cells; the number of
		-- right cells is deepest minus current_depth.
		local current_depth = depths[i]
		local num_right_cells = deepest - current_depth
		for i = 1, current_depth do
			table.insert(result, "|")
		end
		table.insert(result, string.format("| [[%s|%s]]", et_as_string, bright_generator_string))
		for i = 1, num_right_cells do
			table.insert(result, "|")
		end
		
		-- Cells for generators in cents
		table.insert(result, string.format("| %.3f", bright_generator_cents))
		table.insert(result, string.format("| %.3f", dark_generator_cents  ))
		
		-- Cell for step ratio
		table.insert(result, string.format("| %d:%d", step_ratio[1], step_ratio[2]))
		
		-- Cell for hardness, with divide-by-zero check
		local hardness = ""
		if step_ratio[2] == 0 then
			hardness = "&rarr;&nbsp;&#8734;"
		else
			hardness = string.format("%.3f", step_ratio[1] / step_ratio[2])
		end
		table.insert(result, string.format("| %s", hardness))
		
		-- Cell for comment
		-- Default comments are on their own line before custom comments
		local key = step_ratio[1] .. "/" .. step_ratio[2]		-- The step ratio is (literally and figuratively) the key to add comments!
		local comment = ""
		local default_comment = default_comments[key] or ""
		local custom_comment = comments[key] or ""
		if default_comment == "" then
			comment = custom_comment
		else
			comment = default_comment .. "<br />" .. custom_comment
		end
		table.insert(result, string.format("| style=\"text-align: left;\" | %s", comment))
		
	end
	
	table.insert(result, "|}")
	return table.concat(result, "\n")
end

-- Wrapper function; to be called by template; preprocesses args
-- Unprocessed args:
-- - Scale Signature (textual scalesig)
-- - Depth (how deep to search for mediants)
-- - Int Limit (if present, overrides depth-based search for int-limit search)
-- - X/Y (comments for individual step ratios)
-- - First/Last Step Ratio (initial ratios for mediant search; default is
--   1/1 and 1/0 respectively)
-- Processed args:
-- - Input MOS (processed from textual scalesig)
-- - Step Ratios (table of step ratios found)
-- - Depths (table of how deep each step ratio was found at)
-- - Comments (step ratio comments placed in their own table)
function p.mos_tuning_spectrum(frame)
	local args = getArgs(frame)
	
	-- Parse scalesig as input mos, then remove scalesig
	local input_mos = mos.parse(args["Scale Signature"])
	args["Input MOS"] = input_mos
	args["Scale Signature"] = nil
	
	-- Parse initial ratios
	local initial_ratios_unparsed = args["Initial Ratios"]
	local initial_ratios = {}
	if initial_ratios_unparsed ~= nil then
		-- Parse initial ratios, then sort by ascending
		initial_ratios = tip.parse_numeric_pairs(initial_ratios_unparsed, ";", "/", true)
		mediants.sort_ratios(initial_ratios)
	else
		-- If no initial ratios were passed in, default to 1/1 and 1/0
		initial_ratios = {{1,1}, {1,0}}
	end
	
	-- Then generate mediants and depths
	-- If using depth-based search, parse the depth, then remove it from args
	-- If using int-limit search, parse the int limit, then remove it from args,
	-- and remove depth in case it was present.
	local step_ratios, depths
	local use_int_limit = args["Int Limit"] ~= nil
	if use_int_limit then
		-- Int-limit search; parse int-limit
		local int_limit = tonumber(args["Int Limit"])
		args["Int Limit"] = nil
		
		-- Find ratios and depths found
		step_ratios, depths = mediants.find_mediants_by_int_limit(initial_ratios, int_limit)
	else 
		-- Depth-based search; parse depth
		local depth = tonumber(args["Depth"]) or 5
		
		-- Find ratios and depths found
		step_ratios, depths = mediants.find_mediants(initial_ratios, depth)
	end
	args["Depth"      ] = nil
	args["Step Ratios"] = step_ratios
	args["Depths"     ] = depths
	
	-- Transfer comments from args to comments
	local comments = {}
	for i = 1, #step_ratios do
		local key = step_ratios[i][1] .. "/" .. step_ratios[i][2]
		if args[key] ~= nil then
			comments[key] = args[key]
			args[key] = nil
		end
	end
	args["Comments"] = comments
	
	-- Parse debug option
	local debugg = yesno(args["debug"])
	
	-- Output
	local result = p._mos_tuning_spectrum(args)
	if debugg == true then
		result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
	end
	
	return frame:preprocess(result)
end

function p.tester()
	--local args = {}
	--return p._mos_tuning_spectrum(args)

	local comments = {
		["5/4"] = "aaa",
		["2/1"] = "bbb"
	}
	
	local step_ratios = {
		{1,1},
		{5,4},
		{4,3}
	}
	
	local in_comments = {}
	local out_comments = {}
	in_comments, out_comments, out_ratios = p.preprocess_comments(comments, step_ratios)

	local args = {}
	--return out_comments
	return p._mos_tuning_spectrum(args)
end

return p