Module:Infobox MOS: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Ganaram inukshuk (talk | contribs)
mNo edit summary
ArrowHead294 (talk | contribs)
m No need to use NBSP on ones that aren't displayed
 
(171 intermediate revisions by 5 users not shown)
Line 1: Line 1:
local p = {}
local p = {}
local utils = require('Module:Utils')
local rat = require('Module:Rational')
local mos = require('Module:MOS')
local et = require('Module:ET')
--local xp = require('Module:Xenpaper') -- No xenpaper links for now
local infobox = require('Module:Infobox')


local common_suffix = {
local getArgs = require("Module:Arguments").getArgs
['3/2'] = 'f',
local ib = require("Module:Infobox")
['2'] = 'o',
local kbvis = require("Module:Keyboard vis")
['2/1'] = 'o',
local mos = require("Module:MOS")
['3'] = 't',
local tamnams = require("Module:TAMNAMS")
['3/1'] = 't',
local tip = require("Module:Template input parse")
}
local xp = require("Module:Xenpaper")
local yesno = require("Module:Yesno")


local common_ratio = {
-- TODO: REWRITE. REFACTOR. AGAIN.
['f'] = rat.new(3, 2),
 
['o'] = 2,
-- TODO: bugfix tamnams lookup breaking for mosses with 5 steps or less; interim
['t'] = 3,
-- fix currently implemented shows smaller mosses as descending from itself.
}
 
-- Helper function
-- Concatenates the contents of two tables into one
-- This doesn't have a return value; rather, the first table passed has the
-- second table's contents added to it.
function p.concatenate_tables(t1, t2)
for i=1, #t2 do
t1[#t1 + 1] = t2[i]
end
end
 
-- Helper function
-- Annotates a section header with the equave as superscript text.
function p.annotate_section_header(input_mos, section_header)
local et_suffix = mos.et_suffix(input_mos)
return (et_suffix == "edo"
and string.format('<div style=\"margin-top: 0.6em;\"><b>%s</b></div>', section_header)
or  string.format('<div style=\"margin-top: 0.6em;\"><b>%s</b><sup><abbr title=\"In steps of %s\">(%s)</sup></div>', section_header, et_suffix, et_suffix)
)
end
 
-- Helper function
-- Create a keyboard visualization, based on the Halberstadt keyboard layout
function p.kb_vis(input_mos)
local input_mos = input_mos or mos.new(5, 2)
local brightest_mode = mos.brightest_mode(input_mos)
local vis = ""
if input_mos.nL + input_mos.ns < 40 then
vis = kbvis.vis_small(brightest_mode)
end
return {{["Header"] = vis}}
end


-- Helper function
-- Helper function
-- Adds categories
-- Adds categories
function p.categorize(tuning)
function p.categorize(input_mos)
local tuning = tuning or "5L 2s"
local input_mos = input_mos or mos.new(5, 2)
local input_mos = mos.parse(tuning)
-- Add to category of abstact mosses
-- Add to category of abstact mosses
local categories = "[[Category:Abstract MOS patterns]]"
local categories = " [[Category:Abstract MOS patterns]]"
-- Add tuning category
--categories = categories .. string.format('[[Category:%s]]', tuning)
-- Add notecount category if the notecount is greater than 3
-- Add notecount category if the notecount is greater than 3
local notecount = input_mos.nL + input_mos.ns
local notecount = input_mos.nL + input_mos.ns
if notecount > 3 then
if notecount > 3 then
categories = categories .. string.format('[[Category:%d-tone scales]]', notecount)
categories = categories .. string.format(" [[Category:%d-tone scales]]", notecount)
end
end
-- If the mos is octave-equivalent, add appropriate tamnams-named categories
-- If the mos is octave-equivalent, add appropriate tamnams-named categories
-- Otherwise, add to nonoctave category
-- Otherwise, add to nonoctave category
if rat.eq(input_mos.equave, rat.new(2)) then
if mos.is_octave_equivalent(input_mos) then
-- Caveats:
-- Caveats:
-- - Only octave-equivalent mos names are used as categories.
-- - Only octave-equivalent mos names are used as categories.
Line 47: Line 72:
-- - Mosses whose notecounts > 10 and periods < 5 are categorized under
-- - Mosses whose notecounts > 10 and periods < 5 are categorized under
--  the closest tamnams-named ancestor.
--  the closest tamnams-named ancestor.
-- - Monolarge scales aren't categorized for they're all related to one
local ancestor_mos = tamnams.find_ancestor(input_mos)
--  another. (For now.)
local tamnams_name = tamnams.lookup_name(ancestor_mos)
-- - Monosmall descendants that descend from 1L 9s aren't categorized
--  for now.
local ancestor_mos = mos.find_ancestor(input_mos)
local tamnams_name = mos.tamnams_name[mos.as_string(ancestor_mos)]
if tamnams_name == "arch(a)eotonic" then
tamnams_name = "archaeotonic"
end
if tamnams_name ~= nil then
if tamnams_name ~= nil then
categories = categories .. string.format('[[Category:%s]]', tamnams_name)
categories = categories .. string.format(" [[Category:%s]]", tamnams_name)
end
end
else
else
categories = categories .. '[[Category:Nonoctave]]'
categories = categories .. " [[Category:Nonoctave]]"
end
end
Line 71: Line 88:
-- Creates adjacent links for mos, found by +/-1 large or +/- small steps
-- Creates adjacent links for mos, found by +/-1 large or +/- small steps
function p.adjacent_links(input_mos)
function p.adjacent_links(input_mos)
local input_mos = input_mos or mos.new(5, 2)
local input_mos = input_mos or mos.new(1, 1)
local long_equave_as_text = ""
local adjacent_mosses = {
local equave_as_text = ""
mos.new(input_mos.nL - 1, input_mos.ns - 1, input_mos.equave), -- UL
if not rat.eq(input_mos.equave, 2) then
mos.new(input_mos.nL    , input_mos.ns - 1, input_mos.equave), -- U
long_equave_as_text = string.format(" (%s-equivalent)", rat.as_ratio(input_mos.equave))
mos.new(input_mos.nL + 1, input_mos.ns - 1, input_mos.equave), -- UR
equave_as_text = string.format("⟨%s⟩", rat.as_ratio(input_mos.equave))
mos.new(input_mos.nL - 1, input_mos.ns    , input_mos.equave), -- L
end
mos.new(input_mos.nL + 1, input_mos.ns    , input_mos.equave), -- R
mos.new(input_mos.nL - 1, input_mos.ns + 1, input_mos.equave), -- DL
mos.new(input_mos.nL    , input_mos.ns + 1, input_mos.equave), -- D
mos.new(input_mos.nL + 1, input_mos.ns + 1, input_mos.equave), -- DR
}
local adjacent_links = {
local adjacent_links = {
string.format("[[%dL %ds%s | ↖%dL %ds%s]]", input_mos.nL-1, input_mos.ns-1, long_equave_as_text, input_mos.nL-1, input_mos.ns-1, equave_as_text),
mos.is_valid(adjacent_mosses[1]) and string.format("[[%s|&#x2196;&nbsp;%s]]", mos.as_long_string(adjacent_mosses[1], false), mos.as_string(adjacent_mosses[1]), true) or "",
string.format("[[%dL %ds%s | ↑%dL %ds%s]]", input_mos.nL  , input_mos.ns-1, long_equave_as_text, input_mos.nL  , input_mos.ns-1, equave_as_text),
mos.is_valid(adjacent_mosses[2]) and string.format("[[%s|&#x2191;&nbsp;%s]]", mos.as_long_string(adjacent_mosses[2], false), mos.as_string(adjacent_mosses[2]), true) or "",
string.format("[[%dL %ds%s | %dL %ds%s↗]]", input_mos.nL+1, input_mos.ns-1, long_equave_as_text, input_mos.nL+1, input_mos.ns-1, equave_as_text),
mos.is_valid(adjacent_mosses[3]) and string.format("[[%s|%s&nbsp;&#x2197;]]", mos.as_long_string(adjacent_mosses[3], false), mos.as_string(adjacent_mosses[3]), true) or "",
string.format("[[%dL %ds%s | ←%dL %ds%s]]", input_mos.nL-1, input_mos.ns  , long_equave_as_text, input_mos.nL-1, input_mos.ns  , equave_as_text),
mos.is_valid(adjacent_mosses[4]) and string.format("[[%s|&#x2190;&nbsp;%s]]", mos.as_long_string(adjacent_mosses[4], false), mos.as_string(adjacent_mosses[4]), true) or "",
string.format("[[%dL %ds%s | %dL %ds%s→]]", input_mos.nL+1, input_mos.ns  , long_equave_as_text, input_mos.nL+1, input_mos.ns  , equave_as_text),
mos.is_valid(adjacent_mosses[5]) and string.format("[[%s|%s&nbsp;&#x2192;]]", mos.as_long_string(adjacent_mosses[5], false), mos.as_string(adjacent_mosses[5]), true) or "",
string.format("[[%dL %ds%s | ↙%dL %ds%s]]", input_mos.nL-1, input_mos.ns+1, long_equave_as_text, input_mos.nL-1, input_mos.ns+1, equave_as_text),
mos.is_valid(adjacent_mosses[6]) and string.format("[[%s|&#x2199;&nbsp;%s]]", mos.as_long_string(adjacent_mosses[6], false), mos.as_string(adjacent_mosses[6]), true) or "",
string.format("[[%dL %ds%s | ↓%dL %ds%s]]", input_mos.nL  , input_mos.ns+1, long_equave_as_text, input_mos.nL  , input_mos.ns+1, equave_as_text),
mos.is_valid(adjacent_mosses[7]) and string.format("[[%s|&#x2193;&nbsp;%s]]", mos.as_long_string(adjacent_mosses[7], false), mos.as_string(adjacent_mosses[7]), true) or "",
string.format("[[%dL %ds%s | %dL %ds%s↘]]", input_mos.nL+1, input_mos.ns+1, long_equave_as_text, input_mos.nL+1, input_mos.ns+1, equave_as_text),
mos.is_valid(adjacent_mosses[8]) and string.format("[[%s|%s&nbsp;&#x2198;]]", mos.as_long_string(adjacent_mosses[8], false), mos.as_string(adjacent_mosses[8]), true) or ""
}
}
for i = 1, #adjacent_links do
local gcd = utils._gcd(input_mos.nL, input_mos.ns)
local is_null_large = string.find(adjacent_links[i], "0L") and input_mos.nL == gcd
local is_null_small = string.find(adjacent_links[i], "0s") and input_mos.ns == gcd
if is_null_large or is_null_small then
adjacent_links[i] = ""
end
end
return adjacent_links
return adjacent_links
end
end


-- TODO: Cleanup and adopt mos functions
-- Helper function
-- Helper function
-- Produces section entries for scale sturcture
-- Produces section entries for scale sturcture
-- Section is returned as a jagged array and return value must be merged into
-- a larger array.
function p.scale_structure(input_mos)
function p.scale_structure(input_mos)
local input_mos = input_mos or mos.new(5, 5, 3)
local input_mos = input_mos or mos.new(5, 2)
local equave_as_text = rat.as_ratio(input_mos.equave)
local equave_as_string = mos.equave_as_string(input_mos)
local equave_in_cents = rat.cents(input_mos.equave)
local equave_in_cents = mos.equave_to_cents(input_mos)
local number_of_periods = utils._gcd(input_mos.nL, input_mos.ns)
local number_of_periods = mos.period_count(input_mos)
local period_as_text = ""
local period_as_string = ""
if number_of_periods == 1 then
if number_of_periods == 1 then
period_as_text = equave_as_text
period_as_string = equave_as_string
else
else
local ed = et.new(number_of_periods, input_mos.equave)
period_as_string = mos.reduced_period_to_et_string(input_mos, "")
period_as_text = et.backslash_display(ed, 1)
end
end
local period_in_cents = equave_in_cents / number_of_periods
local period_in_cents = equave_in_cents / number_of_periods
local scale_structure = {
local step_pattern = string.format("...%d steps...", input_mos.nL + input_mos.ns)
{"Brightest mode", mos.brightest_mode(input_mos)},
if input_mos.nL + input_mos.ns <= 40 then
{"[[Equave]] (cents)", string.format("%s (%.1f¢)", equave_as_text, equave_in_cents)},
local brightest_mode = mos.brightest_mode(input_mos)
{"[[Period]] (cents)", string.format("%s (%.1f¢)", period_as_text, period_in_cents)}
step_pattern = string.format("<abbr title=\"Brightest mode\">%s</abbr><br /><abbr title=\"Darkest mode\">%s</abbr>", brightest_mode, string.reverse(brightest_mode))
end
local section_header = "Scale structure"
local section_entries = {
{ ["Header"] = string.format("<div style=\"margin-top: 0.6em;\"><b>%s</b></div>", section_header)},
{ ["Header"] = "[[Step pattern]]", ["Data"] = step_pattern},
{ ["Header"] = "[[Equave]]"     , ["Data"] = string.format("%s (%.1f{{c}})", equave_as_string, equave_in_cents) },
{ ["Header"] = "[[Period]]"     , ["Data"] = string.format("%s (%.1f{{c}})", period_as_string, period_in_cents) }
}
}
 
return scale_structure
return section_entries
end
end


-- Helper function
-- Helper function
-- Produces generator ranges for scale
-- Produces generator ranges for scale
function p.generator_ranges(input_mos)
-- Section is returned as a jagged array and return value must be merged into
local input_mos = input_mos or mos.new(5, 2)
-- a larger array.
function p.generator_sizes(input_mos)
local input_mos = input_mos or mos.new(5, 2,3)
local bright_min_in_steps = mos.bright_gen_to_et_string(input_mos, {1, 1}, "")
local bright_max_in_steps = mos.bright_gen_to_et_string(input_mos, {1, 0}, "")
local dark_min_in_steps  = mos.dark_gen_to_et_string  (input_mos, {1, 0}, "")
local dark_max_in_steps  = mos.dark_gen_to_et_string  (input_mos, {1, 1}, "")
local number_of_periods = utils._gcd(input_mos.nL, input_mos.ns)
local bright_min_in_cents = mos.bright_gen_to_cents(input_mos, {1, 1})
local bright_max_in_cents = mos.bright_gen_to_cents(input_mos, {1, 0})
local dark_min_in_cents = mos.dark_gen_to_cents(input_mos, {1, 0})
local dark_max_in_cents = mos.dark_gen_to_cents(input_mos, {1, 1})
local bright_gen = mos.bright_gen(input_mos)
local section_header = p.annotate_section_header(input_mos, "Generator size")
local dark_gen = {
local section_entries = {
['L'] = input_mos.nL / number_of_periods - bright_gen['L'],
{ ["Header"] = section_header},
['s'] = input_mos.ns / number_of_periods - bright_gen['s']
{ ["Header"] = "[[Bright]]", ["Data"] = string.format("%s to %s (%.1f{{c}} to %.1f{{c}})", bright_min_in_steps, bright_max_in_steps, bright_min_in_cents, bright_max_in_cents) },
{ ["Header"] = "[[Dark]]"  , ["Data"] = string.format("%s to %s (%.1f{{c}} to %.1f{{c}})", dark_min_in_steps, dark_max_in_steps, dark_min_in_cents, dark_max_in_cents) },
}
}
local equalized_ed = et.new(input_mos.nL + input_mos.ns, input_mos.equave)
return section_entries
local collapsed_ed = et.new(input_mos.nL, input_mos.equave)
local bright_min_in_steps = et.backslash_display(equalized_ed, bright_gen['L'] + bright_gen['s'])
local bright_max_in_steps = et.backslash_display(collapsed_ed, bright_gen['L'])
local dark_min_in_steps  = et.backslash_display(collapsed_ed, dark_gen['L'])
local dark_max_in_steps  = et.backslash_display(equalized_ed, dark_gen['L'] + dark_gen['s'])
local bright_min_in_cents = et.cents(equalized_ed, bright_gen['L'] + bright_gen['s'])
local bright_max_in_cents = et.cents(collapsed_ed, bright_gen['L'])
local dark_min_in_cents  = et.cents(collapsed_ed, dark_gen['L'])
local dark_max_in_cents  = et.cents(equalized_ed, dark_gen['L'] + dark_gen['s'])
return {
{"[[Bright]] (cents)", string.format("%s to %s (%.1d¢ to %.1d¢)", bright_min_in_steps, bright_max_in_steps, bright_min_in_cents, bright_max_in_cents)},
{"[[Dark]] (cents)", string.format("%s to %s (%.1d¢ to %.1d¢)", dark_min_in_steps, dark_max_in_steps, dark_min_in_cents, dark_max_in_cents)},
}
end
end


-- Helper function for a helper function
-- Helper function
-- Determines what mos the given mos descends from
-- Produces section entries for tamnams info
-- as well as what step ratio that produces this scale
-- Conditions for tamnams info inclusion:
function p.find_mos_ancestor(input_mos)
-- - Scale is octave-equivalent.
local input_mos = input_mos or mos.new(7, 7)
-- - Scales within the "named range" (6-10 notes, or is 1L 1s or 2L 2s) have
--  a tamnams name.
-- - Scales with a notecount greater than 10 and no more than 5 periods have
--  a tamnams-named ancestor.
-- - Scales with a notecount greater than 10 and more than 5 periods don't have
--   a tamnams-named ancestor, but will report what nL ns mos they descend from.
-- Section is returned as a jagged array and return value must be merged into
-- a larger array.
function p.tamnams_information(input_mos)
local input_mos = input_mos or mos.new(6,6,3)
local z = input_mos.nL
-- If a mos is octave-equivalent and has 10 or fewer steps, then it has a
local w = input_mos.ns
-- tamnams name/prefix/abbrev.
local generations = 0
-- If a mos is octave-equivalent, has more than 10 steps, and is not a root
-- mos nL ns, then:
-- - If it has 5 periods or less, then its closest ancestor has a tamnams
--  name.
-- - If it has more than 5 periods, then it relates to a root mos nL ns that
--  has more than 10 steps (ancestor therefore has no tamnams name).
-- If a mos is not octave-equivalent, then it may have a tamnams name (if
-- step count is 5 or less; currently unsupported) or not.
local is_octave_equivalent  = mos.is_octave_equivalent(input_mos)
local has_tamnams_name      = tamnams.lookup_name(input_mos) ~= nil
local is_within_named_range  = mos.step_count(input_mos) <= 10
local is_within_period_count = mos.period_count(input_mos) <= 5
local is_root_mos = input_mos.nL == input_mos.ns
-- For an ancestral mos zU wv and descendant xL ys, how many steps of size
local section_header = "TAMNAMS information"
-- L and s can fit inside U and v? (basically the chunking operation)
local section_entries = nil
local lg_chunk = { nL = 1, ns = 0 }
if is_octave_equivalent and has_tamnams_name then
local sm_chunk = { nL = 0, ns = 1 }
section_entries = {
{ ["Header"] = string.format("<div style=\"margin-top: 0.6em;\"><b>%s</b></div>", section_header) },
while (z ~= w) and (z + w > 10) do
{ ["Header"] = "[[TAMNAMS#Mos_pattern_names | Name]]"    , ["Data"] = tamnams.lookup_name  (input_mos) },
local m1 = math.max(z, w)
{ ["Header"] = "[[TAMNAMS#Mos_pattern_names | Prefix]]"  , ["Data"] = tamnams.lookup_prefix(input_mos) .. "-" },
local m2 = math.min(z, w)
{ ["Header"] = "[[TAMNAMS#Mos_pattern_names | Abbrev.]]" , ["Data"] = tamnams.lookup_abbrev(input_mos) }
}
elseif is_octave_equivalent and not has_tamnams_name and not is_root_mos and not is_within_named_range then
-- Lookup closest named ancestor mos
local ancestor_mos, ratio_1, ratio_2, generations = tamnams.find_ancestor_info(input_mos)
-- For use with updating ancestor mos chunks
-- Link to ancestor mos
local z_prev = z
local ancestor_link = mos.as_link(ancestor_mos)
-- Count how many generations
-- Lookup step ratio range
generations = generations + 1
local step_ratio_range = string.format("%s:%s to %s:%s", ratio_1[1], ratio_1[2], ratio_2[1], ratio_2[2])
local range_name = tamnams.lookup_step_ratio_range(ratio_1, ratio_2)
local step_ratio_range_entry = range_name == nil and step_ratio_range or string.format("%s (%s)", step_ratio_range, range_name)
-- Update step ratios
local ancestor_name = tamnams.lookup_name(ancestor_mos)
z = m2
local ancestor_entry = ancestor_name == nil and ancestor_link or string.format("%s (%s)", ancestor_link, ancestor_name)
w = m1 - m2
-- Update large chunk
section_entries = {
local prev_lg_chunk = { nL = lg_chunk.nL, ns = lg_chunk.ns }
{ ["Header"] = string.format("<div style=\"margin-top: 0.6em;\"><b>%s</b></div>", section_header) },
lg_chunk.nL = lg_chunk.nL + sm_chunk.nL
{ ["Header"] = "Related to"  , ["Data"] = ancestor_entry },
lg_chunk.ns = lg_chunk.ns + sm_chunk.ns
{ ["Header"] = "With tunings", ["Data"] = step_ratio_range_entry }
}
-- Update small chunk
if z ~= z_prev then
sm_chunk = prev_lg_chunk
end
end
end
return mos.new(z, w, input_mos.equave), lg_chunk, sm_chunk, generations
return section_entries
end
end


-- Helper function
-- Helper function
-- Produces section entries for tamnams info
-- Adds a section for scale names
function p.tamnams_information(input_mos)
function p.other_names(other_names)
local input_mos = input_mos or mos.new(5, 2)
local other_names = other_names or {"p-chromatic", "hard diatonic"}
local scalesig = string.format("%dL %ds", input_mos.nL, input_mos.ns)
local tamnams_name = mos.tamnams_name[scalesig] or ""
local section_header = "Other names"
local tamnams_prefix = mos.tamnams_prefix[scalesig] or ""
local tamnams_abbrev = mos.tamnams_abbrev[scalesig] or ""
if input_mos.nL + input_mos.ns <= 10 and rat.eq(input_mos.equave, 2) then
if #other_names == 0 then
return {
return nil
{"[[TAMNAMS#Mos_pattern_names | Name]]", tamnams_name},
else
{"[[TAMNAMS#Mos_pattern_names | Prefix]]", tamnams_prefix},
local scale_names = ""
{"[[TAMNAMS#Mos_pattern_names | Abbrev.]]", tamnams_abbrev}
for i=1, #other_names do
}
scale_names = scale_names .. other_names[i]
elseif input_mos.nL + input_mos.ns > 10 and rat.eq(input_mos.equave, 2) then
if i ~= #other_names then
local ancestor_mos, lg_chunk, sm_chunk, generations = p.find_mos_ancestor(input_mos)
scale_names = scale_names .. "<br />"
local ancestor_scalesig = mos.as_string(ancestor_mos)
end
local ancestor_name = mos.tamnams_name[ancestor_scalesig] or "none"
local ancestor_entry = "none"
if ancestor_name ~= "none" and ancestor_scalesig == scalesig then
ancestor_entry = string.format("[[%s]] (%s)", ancestor_scalesig, ancestor_name)
end
end
return {
local section_entries = {
{"Descendant of", ancestor_entry}
{ ["Header"] = string.format("<div style=\"margin-top: 0.6em;\"><b>%s</b></div>", section_header)},
}
{ ["Header"] = "Name(s)", ["Data"] = scale_names}
else
}  
return nil
return section_entries
end
end
end
end
Line 242: Line 272:
-- Helper function
-- Helper function
-- Produces section for related scales
-- Produces section for related scales
-- Section is returned as a jagged array and return value must be merged into
-- a larger array.
function p.related_scales(input_mos)
function p.related_scales(input_mos)
local input_mos = input_mos or mos.new(5, 2)
local input_mos = input_mos or mos.new(5, 2)
local parent_mos = mos.new(math.min(input_mos.nL, input_mos.ns), math.abs(input_mos.nL-input_mos.ns), input_mos.equave)
-- Produce the mos's relatives
local soft_child_mos = mos.new(input_mos.nL+input_mos.ns, input_mos.nL, input_mos.equave)
local parent_mos, sister_mos, soft_child_mos, hard_child_mos, neutral_mos, soft_flought_mos, hard_flought_mos
local hard_child_mos = mos.new(input_mos.nL, input_mos.nL+input_mos.ns, input_mos.equave)
parent_mos = mos.parent(input_mos)
local sister_mos = mos.new(input_mos.ns, input_mos.nL, input_mos.equave)
sister_mos = mos.sister(input_mos)
soft_child_mos, hard_child_mos = mos.children(input_mos)
neutral_mos = mos.neutralized(input_mos)
soft_flought_mos, hard_flought_mos = mos.interleaved(input_mos)
local parent_scalesig = string.format("[[%s | %s]]", mos.as_long_string(parent_mos   ), mos.as_string(parent_mos    ))
-- Produce links to those relatives; parent and sister links have extra
local soft_scalesig  = string.format("[[%s | %s]]", mos.as_long_string(soft_child_mos), mos.as_string(soft_child_mos))
-- checks to make sure they're valid mosses:
local hard_scalesig   = string.format("[[%s | %s]]", mos.as_long_string(hard_child_mos), mos.as_string(hard_child_mos))
-- - If the input mos is a root mos (nL ns), then it has no parent
local sister_scalesig = string.format("[[%s | %s]]", mos.as_long_string(sister_mos    ), mos.as_string(sister_mos    ))
-- - If the input mos is a root mos, then it's its own sister
local is_nL_ns = input_mos.nL == input_mos.ns
local parent_scalesig       = is_nL_ns and "none" or mos.as_link(parent_mos)
local sister_scalesig      = is_nL_ns and (mos.as_string(input_mos) .. " (self)") or mos.as_link(sister_mos)
local soft_scalesig        = mos.as_link(soft_child_mos)
local hard_scalesig         = mos.as_link(hard_child_mos)
local neutral_scalesig      = mos.as_link(neutral_mos)
local soft_flought_scalesig = mos.as_link(soft_flought_mos)
local hard_flought_scalesig = mos.as_link(hard_flought_mos)
local gcd = utils._gcd(input_mos.nL, input_mos.ns)
local section_header = "Related MOS scales"
local is_null_large = string.find(parent_scalesig, "0L") and input_mos.nL == gcd
local section_entries = {
local is_null_small = string.find(parent_scalesig, "0s") and input_mos.ns == gcd
{ ["Header"] = string.format("<div style=\"margin-top: 0.6em;\"><b>%s</b></div>", section_header)},
if is_null_large or is_null_small then
{ ["Header"] = "[[Operations_on_MOSes#Parent_MOS | Parent]]"         , ["Data"] = parent_scalesig },
parent_scalesig = "none"
{ ["Header"] = "[[Operations_on_MOSes#Sister_MOS | Sister]]"         , ["Data"] = sister_scalesig },
end
{ ["Header"] = "[[Operations_on_MOSes#Daughter_MOS | Daughters]]"   , ["Data"] = soft_scalesig .. ", " .. hard_scalesig },
 
{ ["Header"] = "[[Operations_on_MOSes#Neutralization | Neutralized]]", ["Data"] = neutral_scalesig},
return {
{ ["Header"] = "[[Flought_scale | 2-Flought]]"                      , ["Data"] = soft_flought_scalesig .. ", " .. hard_flought_scalesig }
{"Parent", parent_scalesig},
{"[[Operations_on_MOSes#Sister_MOS | Sister]]", sister_scalesig},
{"Daughters", soft_scalesig .. ", " .. hard_scalesig}
}
}
return section_entries
end
end


-- Helper function
-- Helper function
-- Produces simple equal tunings
-- Produces simple equal tunings
-- Includes xenpaper links
function p.equal_tunings(input_mos)
function p.equal_tunings(input_mos)
local input_mos = input_mos or mos.new(5, 2)
local input_mos = input_mos or mos.new(5, 2)
local bright_gen = mos.bright_gen(input_mos)
local bright_gen = mos.bright_gen(input_mos)
local mos_as_vector = {
['L'] = input_mos.nL,
['s'] = input_mos.ns
}
local step_ratios = {
local step_ratios = {
{ 1, 1 },
{ 1, 1 },
Line 293: Line 329:
}
}
local step_ratio_names = {
local section_header = p.annotate_section_header(input_mos, "Equal tunings")
"Equalized",
local section_entries = {{ ["Header"] = section_header }}
"Supersoft",
"Soft",
"Semisoft",
"Basic",
"Semihard",
"Hard",
"Superhard",
"Collapsed"
}
local equal_tunings = {}
for i = 1, #step_ratios do
for i = 1, #step_ratios do
local step_ratio = step_ratios[i]
local step_ratio = step_ratios[i]
local ed_size = mos_as_vector['L'] * step_ratio[1] + mos_as_vector['s'] * step_ratio[2]
local ed_as_string = mos.et_string(input_mos, step_ratio)
local gen_size = bright_gen['L'] * step_ratio[1] + bright_gen['s'] * step_ratio[2]
local ed = et.new(ed_size, input_mos.equave)
local gen_in_steps = mos.bright_gen_to_et_string(input_mos, step_ratio, "")
local ed_as_text = et.as_string(ed)
local gen_in_cents = mos.bright_gen_to_cents(input_mos, step_ratio)
local gen_in_steps = et.backslash_display(ed, gen_size)
local step_ratio_name = tamnams.lookup_step_ratio(step_ratio)
local gen_in_cents = et.cents(ed, gen_size)
step_ratio_name = step_ratio_name:gsub("^%l", string.upper)
local caption = string.format("[[%s]] (L:s = %d:%d)", step_ratio_names[i], step_ratio[1], step_ratio[2])
local xenpaper_link = xp.mosstep_pattern_to_xenpaper_link(mos.brightest_mode(input_mos), step_ratios[i], input_mos.equave)
local text = string.format("[[%s | %s]] (%.1f¢)", ed_as_text, gen_in_steps, gen_in_cents)
local caption = string.format("[[%s]] [%s (<span style=\"white-space: nowrap;\">L:s = %d:%d</span>)]", step_ratio_name, xenpaper_link, step_ratio[1], step_ratio[2])
local text = string.format("[[%s|%s]] (%.1f{{c}})", ed_as_string, gen_in_steps, gen_in_cents)
table.insert(equal_tunings, { caption, text })
table.insert(section_entries, { ["Header"] = caption, ["Data"] = text })
end
end
return equal_tunings
 
return section_entries
end
end


-- New "main" function
-- New "main" function
function p._infobox_mos(tuning)
function p._infobox_mos(input_mos)
local tuning = tuning or "5L 2s"
local input_mos = input_mos or mos.new(4, 5, 3)
local tuning_parsed = mos.parse(tuning)
local other_names_unparsed = ""
local other_names_parsed = tip.parse_entries(other_names_unparsed) or tip.parse_entries(other_names_unparsed, ",")
local sections = {}
local sections = {}
local scale_structure_header = "Scale structure"
-- Keyboard visualization
local scale_structure_section = p.scale_structure(tuning_parsed)
local kb_vis = p.kb_vis(input_mos)
table.insert(sections, {scale_structure_header, scale_structure_section})
p.concatenate_tables(sections, kb_vis)
local gen_ranges_headher = "Generator ranges"
-- Scale structure section
local gen_ranges_section = p.generator_ranges(tuning_parsed)
local scale_structure = p.scale_structure(input_mos)
table.insert(sections, {gen_ranges_headher, gen_ranges_section})
p.concatenate_tables(sections, scale_structure)
local tamnams_info_header = "TAMNAMS information"
-- Interval range section
local tamnams_info_section = p.tamnams_information(tuning_parsed)
--local step_sizes = p.step_sizes(tuning_parsed)
if tamnams_info_section ~= nil then
--p.concatenate_tables(sections, step_sizes)
table.insert(sections, {tamnams_info_header, tamnams_info_section})
-- Generator sizes section
local gen_sizes = p.generator_sizes(input_mos)
p.concatenate_tables(sections, gen_sizes)
-- Tamnams info section, if applicable
local tamnams_info = p.tamnams_information(input_mos)
if tamnams_info ~= nil then
p.concatenate_tables(sections, tamnams_info)
end
end
local related_scales_header = "Related scales"
-- Other names section, if applicable
local related_scales_section = p.related_scales(tuning_parsed)
local other_names_section = p.other_names(other_names_parsed)
table.insert(sections, {related_scales_header, related_scales_section})
if other_names_section ~= nil then
p.concatenate_tables(sections, other_names_section)
end
local equal_tunings_header = "Equal tunings"
-- Related scales section
local equal_tunings_section = p.equal_tunings(tuning_parsed)
local related_scales = p.related_scales(input_mos)
table.insert(sections, {equal_tunings_header, equal_tunings_section})
p.concatenate_tables(sections, related_scales)
-- Equal tunings section
local equal_tunings = p.equal_tunings(input_mos)
p.concatenate_tables(sections, equal_tunings)


local adjacent_links = p.adjacent_links(tuning_parsed)
-- Adjacent links
local adjacent_links = p.adjacent_links(input_mos)
return infobox.build_multisection(tuning, sections, adjacent_links)
local args = {
["Adjacent Links"] = adjacent_links,
["Title"] = mos.as_long_string(input_mos),
["Rows"] = sections,
["name"] = "Infobox MOS"
}
return ib._infobox(args)
--return sections
end
end


-- Wrapper function
-- Wrapper function
function p.infobox_MOS(frame)
function p.infobox_MOS(frame)
local args = getArgs(frame)
local tuning = frame.args['Tuning']
-- "Scale Signature" is preferred; "Tuning" is supported for legacy purposes
local other_names = frame.args['Other names'] or nil
local unparsed = args["Tuning"] or args["Scale Signature"]
local debug_mode = tonumber(frame.args['debug']) == 1
local input_mos = mos.parse(unparsed)
local other_names = args["othernames"] or nil
local debug_mode = yesno(args["debug"], false)
local wtext = yesno(frame.args["wtext"] or args["wtext"])
local result = p._infobox_mos(tuning)
local result = p._infobox_mos(input_mos)
if not debug_mode then
if not debug_mode then
result = result .. p.categorize(tuning)
result = result .. p.categorize(input_mos)
end
if wtext then
result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
end
end
return result
return frame:preprocess(result)
 
end
end


return p
return p

Latest revision as of 13:55, 8 December 2025

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

This module generates an infobox providing information about a given moment of symmetry (MOS) scale.

Introspection summary for Module:Infobox MOS 
Functions provided (13)
Line Function Params
21 concatenate_tables (t1, t2)
29 annotate_section_header (input_mos, section_header)
39 kb_vis (input_mos)
54 categorize (input_mos)
89 adjacent_links (input_mos)
122 scale_structure (input_mos)
158 generator_sizes (input_mos)
193 tamnams_information (input_mos)
248 other_names (other_names)
276 related_scales (input_mos)
316 equal_tunings (input_mos)
356 _infobox_mos (main) (input_mos)
415 infobox_MOS (invokable) (frame)
Lua modules required (8)
Variable Module Functions used
getArgs Module:Arguments getArgs
ib Module:Infobox _infobox
kbvis Module:Keyboard vis vis_small
mos Module:MOS et_suffix
new
brightest_mode
is_octave_equivalent
is_valid
as_long_string
as_string
equave_as_string
equave_to_cents
period_count
reduced_period_to_et_string
bright_gen_to_et_string
dark_gen_to_et_string
bright_gen_to_cents
dark_gen_to_cents
step_count
as_link
parent
sister
children
neutralized
interleaved
bright_gen
et_string
parse
tamnams Module:TAMNAMS find_ancestor
lookup_name
lookup_prefix
lookup_abbrev
find_ancestor_info
lookup_step_ratio_range
lookup_step_ratio
tip Module:Template input parse parse_entries
xp Module:Xenpaper mosstep_pattern_to_xenpaper_link
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 ib = require("Module:Infobox")
local kbvis = require("Module:Keyboard vis")
local mos = require("Module:MOS")
local tamnams = require("Module:TAMNAMS")
local tip = require("Module:Template input parse")
local xp = require("Module:Xenpaper")
local yesno = require("Module:Yesno")

-- TODO: REWRITE. REFACTOR. AGAIN.

-- TODO: bugfix tamnams lookup breaking for mosses with 5 steps or less; interim
-- fix currently implemented shows smaller mosses as descending from itself.

-- Helper function
-- Concatenates the contents of two tables into one
-- This doesn't have a return value; rather, the first table passed has the
-- second table's contents added to it.
function p.concatenate_tables(t1, t2)
	for i=1, #t2 do
		t1[#t1 + 1] = t2[i]
	end
end

-- Helper function
-- Annotates a section header with the equave as superscript text.
function p.annotate_section_header(input_mos, section_header)
	local et_suffix = mos.et_suffix(input_mos)
	return (et_suffix == "edo" 
		and string.format('<div style=\"margin-top: 0.6em;\"><b>%s</b></div>', section_header)
		or  string.format('<div style=\"margin-top: 0.6em;\"><b>%s</b><sup><abbr title=\"In steps of %s\">(%s)</sup></div>', section_header, et_suffix, et_suffix)
	)
end

-- Helper function
-- Create a keyboard visualization, based on the Halberstadt keyboard layout
function p.kb_vis(input_mos)
	local input_mos = input_mos or mos.new(5, 2)
	
	local brightest_mode = mos.brightest_mode(input_mos)
	
	local vis = ""
	if input_mos.nL + input_mos.ns < 40 then
		vis = kbvis.vis_small(brightest_mode)
	end
	
	return {{["Header"] = vis}}
end

-- Helper function
-- Adds categories
function p.categorize(input_mos)
	local input_mos = input_mos or mos.new(5, 2)
	
	-- Add to category of abstact mosses
	local categories = " [[Category:Abstract MOS patterns]]"
	
	-- Add notecount category if the notecount is greater than 3
	local notecount = input_mos.nL + input_mos.ns
	if notecount > 3 then
		categories = categories .. string.format(" [[Category:%d-tone scales]]", notecount)
	end
	
	-- If the mos is octave-equivalent, add appropriate tamnams-named categories
	-- Otherwise, add to nonoctave category
	if mos.is_octave_equivalent(input_mos) then
		-- Caveats:
		-- - Only octave-equivalent mos names are used as categories.
		-- - Monowood and biwood are excluded (for now).
		-- - Mosses whose notecounts > 10 and periods < 5 are categorized under
		--   the closest tamnams-named ancestor.
		local ancestor_mos = tamnams.find_ancestor(input_mos)
		local tamnams_name = tamnams.lookup_name(ancestor_mos)
		
		if tamnams_name ~= nil then
			categories = categories .. string.format(" [[Category:%s]]", tamnams_name)
		end
	else
		categories = categories .. " [[Category:Nonoctave]]"
	end
	
	return categories
end

-- Helper function
-- Creates adjacent links for mos, found by +/-1 large or +/- small steps
function p.adjacent_links(input_mos)
	local input_mos = input_mos or mos.new(1, 1)
	
	local adjacent_mosses = {
		mos.new(input_mos.nL - 1, input_mos.ns - 1, input_mos.equave),		-- UL
		mos.new(input_mos.nL    , input_mos.ns - 1, input_mos.equave),		-- U
		mos.new(input_mos.nL + 1, input_mos.ns - 1, input_mos.equave),		-- UR
		mos.new(input_mos.nL - 1, input_mos.ns    , input_mos.equave),		-- L
		mos.new(input_mos.nL + 1, input_mos.ns    , input_mos.equave),		-- R
		mos.new(input_mos.nL - 1, input_mos.ns + 1, input_mos.equave),		-- DL
		mos.new(input_mos.nL    , input_mos.ns + 1, input_mos.equave),		-- D
		mos.new(input_mos.nL + 1, input_mos.ns + 1, input_mos.equave),		-- DR
	}
	
	local adjacent_links = {
		mos.is_valid(adjacent_mosses[1]) and string.format("[[%s|&#x2196;&nbsp;%s]]", mos.as_long_string(adjacent_mosses[1], false), mos.as_string(adjacent_mosses[1]), true) or "",
		mos.is_valid(adjacent_mosses[2]) and string.format("[[%s|&#x2191;&nbsp;%s]]", mos.as_long_string(adjacent_mosses[2], false), mos.as_string(adjacent_mosses[2]), true) or "",
		mos.is_valid(adjacent_mosses[3]) and string.format("[[%s|%s&nbsp;&#x2197;]]", mos.as_long_string(adjacent_mosses[3], false), mos.as_string(adjacent_mosses[3]), true) or "",
		mos.is_valid(adjacent_mosses[4]) and string.format("[[%s|&#x2190;&nbsp;%s]]", mos.as_long_string(adjacent_mosses[4], false), mos.as_string(adjacent_mosses[4]), true) or "",
		mos.is_valid(adjacent_mosses[5]) and string.format("[[%s|%s&nbsp;&#x2192;]]", mos.as_long_string(adjacent_mosses[5], false), mos.as_string(adjacent_mosses[5]), true) or "",
		mos.is_valid(adjacent_mosses[6]) and string.format("[[%s|&#x2199;&nbsp;%s]]", mos.as_long_string(adjacent_mosses[6], false), mos.as_string(adjacent_mosses[6]), true) or "",
		mos.is_valid(adjacent_mosses[7]) and string.format("[[%s|&#x2193;&nbsp;%s]]", mos.as_long_string(adjacent_mosses[7], false), mos.as_string(adjacent_mosses[7]), true) or "",
		mos.is_valid(adjacent_mosses[8]) and string.format("[[%s|%s&nbsp;&#x2198;]]", mos.as_long_string(adjacent_mosses[8], false), mos.as_string(adjacent_mosses[8]), true) or ""
	}
	
	return adjacent_links
end

-- TODO: Cleanup and adopt mos functions
-- Helper function
-- Produces section entries for scale sturcture
-- Section is returned as a jagged array and return value must be merged into
-- a larger array.
function p.scale_structure(input_mos)
	local input_mos = input_mos or mos.new(5, 2)
	
	local equave_as_string = mos.equave_as_string(input_mos)
	local equave_in_cents  = mos.equave_to_cents(input_mos)
	
	local number_of_periods = mos.period_count(input_mos)
	local period_as_string = ""
	if number_of_periods == 1 then
		period_as_string = equave_as_string
	else
		period_as_string = mos.reduced_period_to_et_string(input_mos, "")
	end
	local period_in_cents = equave_in_cents / number_of_periods
	
	local step_pattern = string.format("...%d steps...", input_mos.nL + input_mos.ns)
	if input_mos.nL + input_mos.ns <= 40 then
		local brightest_mode = mos.brightest_mode(input_mos)
		step_pattern = string.format("<abbr title=\"Brightest mode\">%s</abbr><br /><abbr title=\"Darkest mode\">%s</abbr>", brightest_mode, string.reverse(brightest_mode))
	end
	
	local section_header = "Scale structure"
	local section_entries = {
		{ ["Header"] = string.format("<div style=\"margin-top: 0.6em;\"><b>%s</b></div>", section_header)},
		{ ["Header"] = "[[Step pattern]]", ["Data"] = step_pattern},
		{ ["Header"] = "[[Equave]]"      , ["Data"] = string.format("%s (%.1f{{c}})", equave_as_string, equave_in_cents) },
		{ ["Header"] = "[[Period]]"      , ["Data"] = string.format("%s (%.1f{{c}})", period_as_string, period_in_cents) }
	}

	return section_entries
end

-- Helper function
-- Produces generator ranges for scale
-- Section is returned as a jagged array and return value must be merged into
-- a larger array.
function p.generator_sizes(input_mos)
	local input_mos = input_mos or mos.new(5, 2,3)
	
	local bright_min_in_steps = mos.bright_gen_to_et_string(input_mos, {1, 1}, "")
	local bright_max_in_steps = mos.bright_gen_to_et_string(input_mos, {1, 0}, "")
	local dark_min_in_steps   = mos.dark_gen_to_et_string  (input_mos, {1, 0}, "")
	local dark_max_in_steps   = mos.dark_gen_to_et_string  (input_mos, {1, 1}, "")
	
	local bright_min_in_cents = mos.bright_gen_to_cents(input_mos, {1, 1})
	local bright_max_in_cents = mos.bright_gen_to_cents(input_mos, {1, 0})
	local dark_min_in_cents = mos.dark_gen_to_cents(input_mos, {1, 0})
	local dark_max_in_cents = mos.dark_gen_to_cents(input_mos, {1, 1})
	
	local section_header = p.annotate_section_header(input_mos, "Generator size")
	local section_entries = {
		{ ["Header"] = section_header},
		{ ["Header"] = "[[Bright]]", ["Data"] = string.format("%s to %s (%.1f{{c}} to %.1f{{c}})", bright_min_in_steps, bright_max_in_steps, bright_min_in_cents, bright_max_in_cents) },
		{ ["Header"] = "[[Dark]]"  , ["Data"] = string.format("%s to %s (%.1f{{c}} to %.1f{{c}})", dark_min_in_steps, dark_max_in_steps, dark_min_in_cents, dark_max_in_cents) },
	}
	
	return section_entries
end

-- Helper function
-- Produces section entries for tamnams info
-- Conditions for tamnams info inclusion:
-- - Scale is octave-equivalent.
-- - Scales within the "named range" (6-10 notes, or is 1L 1s or 2L 2s) have
--   a tamnams name.
-- - Scales with a notecount greater than 10 and no more than 5 periods have
--   a tamnams-named ancestor.
-- - Scales with a notecount greater than 10 and more than 5 periods don't have
--   a tamnams-named ancestor, but will report what nL ns mos they descend from.
-- Section is returned as a jagged array and return value must be merged into
-- a larger array.
function p.tamnams_information(input_mos)
	local input_mos = input_mos or mos.new(6,6,3)
	
	-- If a mos is octave-equivalent and has 10 or fewer steps, then it has a
	-- tamnams name/prefix/abbrev.
	-- If a mos is octave-equivalent, has more than 10 steps, and is not a root
	-- mos nL ns, then:
	-- - If it has 5 periods or less, then its closest ancestor has a tamnams
	--   name.
	-- - If it has more than 5 periods, then it relates to a root mos nL ns that
	--   has more than 10 steps (ancestor therefore has no tamnams name).
	-- If a mos is not octave-equivalent, then it may have a tamnams name (if
	-- step count is 5 or less; currently unsupported) or not.
	local is_octave_equivalent   = mos.is_octave_equivalent(input_mos)
	local has_tamnams_name       = tamnams.lookup_name(input_mos) ~= nil
	local is_within_named_range  = mos.step_count(input_mos) <= 10
	local is_within_period_count = mos.period_count(input_mos) <= 5
	local is_root_mos = input_mos.nL == input_mos.ns
	
	local section_header = "TAMNAMS information"
	local section_entries = nil
	if is_octave_equivalent and has_tamnams_name then
		section_entries = {
			{ ["Header"] = string.format("<div style=\"margin-top: 0.6em;\"><b>%s</b></div>", section_header) },
			{ ["Header"] = "[[TAMNAMS#Mos_pattern_names | Name]]"    , ["Data"] = tamnams.lookup_name  (input_mos) },
			{ ["Header"] = "[[TAMNAMS#Mos_pattern_names | Prefix]]"  , ["Data"] = tamnams.lookup_prefix(input_mos) .. "-" },
			{ ["Header"] = "[[TAMNAMS#Mos_pattern_names | Abbrev.]]" , ["Data"] = tamnams.lookup_abbrev(input_mos) }
		}
	elseif is_octave_equivalent and not has_tamnams_name and not is_root_mos and not is_within_named_range then
		-- Lookup closest named ancestor mos
		local ancestor_mos, ratio_1, ratio_2, generations = tamnams.find_ancestor_info(input_mos)
		
		-- Link to ancestor mos
		local ancestor_link = mos.as_link(ancestor_mos)
		
		-- Lookup step ratio range
		local step_ratio_range = string.format("%s:%s to %s:%s", ratio_1[1], ratio_1[2], ratio_2[1], ratio_2[2])
		local range_name = tamnams.lookup_step_ratio_range(ratio_1, ratio_2)
		local step_ratio_range_entry = range_name == nil and step_ratio_range or string.format("%s (%s)", step_ratio_range, range_name)
		
		local ancestor_name = tamnams.lookup_name(ancestor_mos)
		local ancestor_entry = ancestor_name == nil and ancestor_link or string.format("%s (%s)", ancestor_link, ancestor_name)
		
		section_entries = {
			{ ["Header"] = string.format("<div style=\"margin-top: 0.6em;\"><b>%s</b></div>", section_header) },
			{ ["Header"] = "Related to"  , ["Data"] = ancestor_entry },
			{ ["Header"] = "With tunings", ["Data"] = step_ratio_range_entry }
		}
	end
	
	return section_entries
end

-- Helper function
-- Adds a section for scale names
function p.other_names(other_names)
	local other_names = other_names or {"p-chromatic", "hard diatonic"}
	
	local section_header = "Other names"
	
	if #other_names == 0 then
		return nil
	else
		local scale_names = ""
		for i=1, #other_names do
			scale_names = scale_names .. other_names[i]
			if i ~= #other_names then
				scale_names = scale_names .. "<br />"
			end
		end
		
		local section_entries = {
			{ ["Header"] = string.format("<div style=\"margin-top: 0.6em;\"><b>%s</b></div>", section_header)},
			{ ["Header"] = "Name(s)", ["Data"] = scale_names}
		} 
		return section_entries
	end
end

-- Helper function
-- Produces section for related scales
-- Section is returned as a jagged array and return value must be merged into
-- a larger array.
function p.related_scales(input_mos)
	local input_mos = input_mos or mos.new(5, 2)
	
	-- Produce the mos's relatives
	local parent_mos, sister_mos, soft_child_mos, hard_child_mos, neutral_mos, soft_flought_mos, hard_flought_mos
	parent_mos = mos.parent(input_mos)
	sister_mos = mos.sister(input_mos)
	soft_child_mos, hard_child_mos = mos.children(input_mos)
	neutral_mos = mos.neutralized(input_mos)
	soft_flought_mos, hard_flought_mos = mos.interleaved(input_mos)
	
	-- Produce links to those relatives; parent and sister links have extra
	-- checks to make sure they're valid mosses:
	-- - If the input mos is a root mos (nL ns), then it has no parent
	-- - If the input mos is a root mos, then it's its own sister
	local is_nL_ns = input_mos.nL == input_mos.ns
	local parent_scalesig       = is_nL_ns and "none" or mos.as_link(parent_mos)
	local sister_scalesig       = is_nL_ns and (mos.as_string(input_mos) .. " (self)") or mos.as_link(sister_mos)
	local soft_scalesig         = mos.as_link(soft_child_mos)
	local hard_scalesig         = mos.as_link(hard_child_mos)
	local neutral_scalesig      = mos.as_link(neutral_mos)
	local soft_flought_scalesig = mos.as_link(soft_flought_mos)
	local hard_flought_scalesig = mos.as_link(hard_flought_mos)
	
	local section_header = "Related MOS scales"
	local section_entries = {
		{ ["Header"] = string.format("<div style=\"margin-top: 0.6em;\"><b>%s</b></div>", section_header)},
		{ ["Header"] = "[[Operations_on_MOSes#Parent_MOS | Parent]]"         , ["Data"] = parent_scalesig },
		{ ["Header"] = "[[Operations_on_MOSes#Sister_MOS | Sister]]"         , ["Data"] = sister_scalesig },
		{ ["Header"] = "[[Operations_on_MOSes#Daughter_MOS | Daughters]]"    , ["Data"] = soft_scalesig .. ", " .. hard_scalesig },
		{ ["Header"] = "[[Operations_on_MOSes#Neutralization | Neutralized]]", ["Data"] = neutral_scalesig},
		{ ["Header"] = "[[Flought_scale | 2-Flought]]"                       , ["Data"] = soft_flought_scalesig .. ", " .. hard_flought_scalesig }
	}
	
	return section_entries
end

-- Helper function
-- Produces simple equal tunings
-- Includes xenpaper links
function p.equal_tunings(input_mos)
	local input_mos = input_mos or mos.new(5, 2)
	local bright_gen = mos.bright_gen(input_mos)
	local step_ratios = {
		{ 1, 1 },
		{ 4, 3 },
		{ 3, 2 },
		{ 5, 3 },
		{ 2, 1 },
		{ 5, 2 },
		{ 3, 1 },
		{ 4, 1 },
		{ 1, 0 }
	}
	
	local section_header = p.annotate_section_header(input_mos, "Equal tunings")
	local section_entries = {{ ["Header"] = section_header }}
	
	for i = 1, #step_ratios do
		local step_ratio = step_ratios[i]
		
		local ed_as_string = mos.et_string(input_mos, step_ratio)
		
		local gen_in_steps = mos.bright_gen_to_et_string(input_mos, step_ratio, "")
		local gen_in_cents = mos.bright_gen_to_cents(input_mos, step_ratio)
		
		local step_ratio_name = tamnams.lookup_step_ratio(step_ratio)
		step_ratio_name = step_ratio_name:gsub("^%l", string.upper)
		
		local xenpaper_link = xp.mosstep_pattern_to_xenpaper_link(mos.brightest_mode(input_mos), step_ratios[i], input_mos.equave)
		local caption = string.format("[[%s]] [%s (<span style=\"white-space: nowrap;\">L:s = %d:%d</span>)]", step_ratio_name, xenpaper_link, step_ratio[1], step_ratio[2])
		local text = string.format("[[%s|%s]] (%.1f{{c}})", ed_as_string, gen_in_steps, gen_in_cents)
		
		table.insert(section_entries, { ["Header"] = caption, ["Data"] = text })
	end

	return section_entries
end

-- New "main" function
function p._infobox_mos(input_mos)
	local input_mos = input_mos or mos.new(4, 5, 3)
	local other_names_unparsed = ""
	
	local other_names_parsed = tip.parse_entries(other_names_unparsed) or tip.parse_entries(other_names_unparsed, ",")
	
	local sections = {}
	
	-- Keyboard visualization
	local kb_vis = p.kb_vis(input_mos)
	p.concatenate_tables(sections, kb_vis)
	
	-- Scale structure section
	local scale_structure = p.scale_structure(input_mos)
	p.concatenate_tables(sections, scale_structure)
	
	-- Interval range section
	--local step_sizes = p.step_sizes(tuning_parsed)
	--p.concatenate_tables(sections, step_sizes)
	
	-- Generator sizes section
	local gen_sizes = p.generator_sizes(input_mos)
	p.concatenate_tables(sections, gen_sizes)
	
	-- Tamnams info section, if applicable
	local tamnams_info = p.tamnams_information(input_mos)
	if tamnams_info ~= nil then
		p.concatenate_tables(sections, tamnams_info)
	end
	
	-- Other names section, if applicable
	local other_names_section = p.other_names(other_names_parsed)
	if other_names_section ~= nil then
		p.concatenate_tables(sections, other_names_section)
	end
	
	-- Related scales section
	local related_scales = p.related_scales(input_mos)
	p.concatenate_tables(sections, related_scales)
	
	-- Equal tunings section
	local equal_tunings = p.equal_tunings(input_mos)
	p.concatenate_tables(sections, equal_tunings)

	-- Adjacent links
	local adjacent_links = p.adjacent_links(input_mos)
	
	local args = {
		["Adjacent Links"] = adjacent_links,
		["Title"] = mos.as_long_string(input_mos),
		["Rows"] = sections,
		["name"] = "Infobox MOS"
	}
	
	return ib._infobox(args)
	--return sections
end

-- Wrapper function
function p.infobox_MOS(frame)
	local args = getArgs(frame)
	
	-- "Scale Signature" is preferred; "Tuning" is supported for legacy purposes
	local unparsed = args["Tuning"] or args["Scale Signature"]
	local input_mos = mos.parse(unparsed)
	local other_names = args["othernames"] or nil
	local debug_mode = yesno(args["debug"], false)
	local wtext = yesno(frame.args["wtext"] or args["wtext"])
	
	local result = p._infobox_mos(input_mos)
	if not debug_mode then
		result = result .. p.categorize(input_mos)
	end
	
	if wtext then
		result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
	end
	
	return frame:preprocess(result)
end

return p