Module:Infobox MOS

From Xenharmonic Wiki
Revision as of 09:26, 7 March 2025 by Ganaram inukshuk (talk | contribs) (bugfix incorrectly passing the long string as the input mos)
Jump to navigation Jump to search
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 (12)
Line Function Params
16 concatenate_tables (t1, t2)
24 kb_vis (input_mos)
39 categorize (input_mos)
74 adjacent_links (input_mos)
106 scale_structure (input_mos)
142 generator_sizes (input_mos)
195 tamnams_information (input_mos)
252 other_names (other_names)
280 related_scales (input_mos)
337 equal_tunings (input_mos)
390 _infobox_mos (main) (input_mos, other_names_unparsed)
448 infobox_MOS (invokable) (frame)
Lua modules required (9)
Variable Module Functions used
getArgs Module:Arguments getArgs
ib Module:Infobox _infobox
kbvis Module:Keyboard vis vis_small
mos Module:MOS new
brightest_mode
is_valid_mos
as_long_string
as_string
period_count
reduced_period_to_et_steps_as_string
period_step_count
bright_gen
dark_gen
mos_to_et_as_string
bright_gen_to_et_steps_as_string
dark_gen_to_et_steps_as_string
bright_gen_to_cents
dark_gen_to_cents
parse
rat Module:Rational eq
new
as_ratio
cents
is_harmonic
as_pair
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 rat = require("Module:Rational")
local mos = require("Module:MOS")
local xp = require("Module:Xenpaper")
local ib = require("Module:Infobox")
local kbvis = require("Module:Keyboard vis")
local tip = require("Module:Template input parse")
local tamnams = require("Module:TAMNAMS")
local yesno = require("Module:Yesno")
local getArgs = require("Module:Arguments").getArgs

-- 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
-- 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 {{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 rat.eq(input_mos.equave, rat.new(2)) 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_mos(adjacent_mosses[1]) and string.format("[[%s|&#x2196;&nbsp;%s]]", mos.as_long_string(adjacent_mosses[1]), mos.as_string(adjacent_mosses[1]), true) or "",
		mos.is_valid_mos(adjacent_mosses[2]) and string.format("[[%s|&#x2191;&nbsp;%s]]", mos.as_long_string(adjacent_mosses[2]), mos.as_string(adjacent_mosses[2]), true) or "",
		mos.is_valid_mos(adjacent_mosses[3]) and string.format("[[%s|%s&nbsp;&#x2197;]]", mos.as_long_string(adjacent_mosses[3]), mos.as_string(adjacent_mosses[3]), true) or "",
		mos.is_valid_mos(adjacent_mosses[4]) and string.format("[[%s|&#x2190;&nbsp;%s]]", mos.as_long_string(adjacent_mosses[4]), mos.as_string(adjacent_mosses[4]), true) or "",
		mos.is_valid_mos(adjacent_mosses[5]) and string.format("[[%s|%s&nbsp;&#x2192;]]", mos.as_long_string(adjacent_mosses[5]), mos.as_string(adjacent_mosses[5]), true) or "",
		mos.is_valid_mos(adjacent_mosses[6]) and string.format("[[%s|&#x2199;&nbsp;%s]]", mos.as_long_string(adjacent_mosses[6]), mos.as_string(adjacent_mosses[6]), true) or "",
		mos.is_valid_mos(adjacent_mosses[7]) and string.format("[[%s|&#x2193;&nbsp;%s]]", mos.as_long_string(adjacent_mosses[7]), mos.as_string(adjacent_mosses[7]), true) or "",
		mos.is_valid_mos(adjacent_mosses[8]) and string.format("[[%s|%s&nbsp;&#x2198;]]", mos.as_long_string(adjacent_mosses[8]), mos.as_string(adjacent_mosses[8]), true) or ""
	}
	
	return adjacent_links
end

-- 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 = rat.as_ratio(input_mos.equave)
	local equave_in_cents = rat.cents(input_mos.equave)
	
	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_steps_as_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 = {
		{string.format("<div style=\"margin-top: 0.6em;\"><b>%s</b></div>", section_header)},
		{"[[Step pattern]]", step_pattern},
		{"[[Equave]]", string.format("%s (%.1f{{c}})", equave_as_string, equave_in_cents)},
		{"[[Period]]", 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)
	
	local number_of_periods = mos.period_step_count(input_mos)
	
	local bright_gen = mos.bright_gen(input_mos)
	local dark_gen   = mos.dark_gen  (input_mos)
	
	local equalized_ed = mos.mos_to_et_as_string(input_mos, {1, 1}, "")
	local collapsed_ed = mos.mos_to_et_as_string(input_mos, {1, 0}, "")
	
	local bright_min_in_steps = mos.bright_gen_to_et_steps_as_string(input_mos, {1, 1}, "")
	local bright_max_in_steps = mos.bright_gen_to_et_steps_as_string(input_mos, {1, 0}, "")
	local dark_min_in_steps   = mos.dark_gen_to_et_steps_as_string  (input_mos, {1, 0}, "")
	local dark_max_in_steps   = mos.dark_gen_to_et_steps_as_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 = "Generator size"
	local equave_annotation = ""
	if rat.eq(input_mos.equave, 3) then
		equave_annotation = "<sup><abbr title=\"In steps of edt\">(edt)</sup>"
	elseif rat.eq(input_mos.equave, rat.new(3,2)) then
		equave_annotation = "<sup><abbr title=\"In steps of edf\">(edf)</sup>"
	elseif not rat.eq(input_mos.equave, 2) then
		local equave_as_ratio = rat.as_ratio(input_mos.equave)
		equave_annotation = string.format("<sup><abbr title=\"In steps of ed%s\">(ed%s)</sup>", equave_as_ratio, equave_as_ratio)
	end
	
	local section_entries = {
		{string.format("<div style=\"margin-top: 0.6em;\"><b>%s</b>%s</div>", section_header, equave_annotation)},
		{"[[Bright]]", 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)},
		{"[[Dark]]", 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(2, 5)
	local scalesig = string.format("%dL %ds", input_mos.nL, input_mos.ns)
	
	local notecount = input_mos.nL + input_mos.ns
	local number_of_periods = mos.period_step_count(input_mos)
	
	local tamnams_name   = tamnams.lookup_name  (input_mos)
	local tamnams_prefix = tamnams.lookup_prefix(input_mos)
	local tamnams_abbrev = tamnams.lookup_abbrev(input_mos)
	
	local is_octave_equivalent = rat.eq(input_mos.equave, 2)
	local is_within_named_range = tamnams_name ~= nil and notecount <= 10
	local is_root_mos = input_mos.nL == input_mos.ns
	
	local section_header = "TAMNAMS information"
	local section_entries = nil
	if is_octave_equivalent then
		if is_within_named_range then
			-- Mos has a tamnams name
			section_entries = {
				{string.format("<div style=\"margin-top: 0.6em;\"><b>%s</b></div>", section_header)},
				{"[[TAMNAMS#Mos_pattern_names | Name]]", tamnams_name},
				{"[[TAMNAMS#Mos_pattern_names | Prefix]]", tamnams_prefix .. "-"},
				{"[[TAMNAMS#Mos_pattern_names | Abbrev.]]", tamnams_abbrev}
			}
		elseif not is_within_named_range and notecount > 10 and not is_root_mos then
			-- Mos is a non-root mos and has a tamnams-named ancestor
			local ancestor_mos, ratio_1, ratio_2, generations = tamnams.find_ancestor_info(input_mos)
			local ancestor_scalesig = mos.as_string(ancestor_mos)
			local ancestor_long_scalesig = mos.as_long_string(ancestor_mos)
			local ancestor_name = tamnams.lookup_name(ancestor_mos)
			
			-- Step ratio range as text
			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_entry = string.format("[[%s | %s]]", ancestor_long_scalesig, ancestor_scalesig)
			if ancestor_name ~= nil then
				ancestor_entry = ancestor_entry .. string.format(" (%s)", ancestor_name)
			end
			
			section_entries = {
				{string.format("<div style=\"margin-top: 0.6em;\"><b>%s</b></div>", section_header)},
				{"Descends from", ancestor_entry};
				{"Ancestor's step ratio range", string.format("%s", step_ratio_range_entry)}
			}
		end
	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 = {
			{string.format("<div style=\"margin-top: 0.6em;\"><b>%s</b></div>", section_header)},
			{"Name(s)", 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, 5)
	
	local parent_mos = mos.new(math.min(input_mos.nL, input_mos.ns), math.abs(input_mos.nL-input_mos.ns), input_mos.equave)
	local sister_mos = mos.new(input_mos.ns, input_mos.nL, input_mos.equave)
	local soft_child_mos = mos.new(input_mos.nL+input_mos.ns, input_mos.nL, input_mos.equave)
	local hard_child_mos = mos.new(input_mos.nL, input_mos.nL+input_mos.ns, input_mos.equave)
	local neutral_mos = input_mos.nL>input_mos.ns and mos.new(input_mos.nL-input_mos.ns, input_mos.ns*2, input_mos.equave) or mos.new(input_mos.nL*2, input_mos.ns-input_mos.nL, input_mos.equave)
	local soft_floght_mos = mos.new(input_mos.nL*2+input_mos.ns, input_mos.ns, input_mos.equave)
	local hard_floght_mos = mos.new(input_mos.nL, input_mos.ns*2+input_mos.nL, input_mos.equave)
	
	local parent_scalesig = string.format("[[%s|%s]]", mos.as_long_string(parent_mos), mos.as_string(parent_mos))
	local sister_scalesig = string.format("[[%s|%s]]", mos.as_long_string(sister_mos), mos.as_string(sister_mos))
	local soft_scalesig = string.format("[[%s|%s]]", mos.as_long_string(soft_child_mos), mos.as_string(soft_child_mos))
	local hard_scalesig = string.format("[[%s|%s]]", mos.as_long_string(hard_child_mos), mos.as_string(hard_child_mos))
	local neutral_scalesig = string.format("[[%s|%s]]", mos.as_long_string(neutral_mos), mos.as_string(neutral_mos))
	local soft_floght_scalesig = string.format("[[%s|%s]]", mos.as_long_string(soft_floght_mos), mos.as_string(soft_floght_mos))
	local hard_floght_scalesig = string.format("[[%s|%s]]", mos.as_long_string(hard_floght_mos), mos.as_string(hard_floght_mos))
	
	local number_of_periods = mos.period_count(input_mos)
	local is_nL_ns = input_mos.nL == number_of_periods and input_mos.ns == number_of_periods
	if is_nL_ns then
		parent_scalesig = "none"
		sister_scalesig = sister_scalesig .. " (self)"
		
		equave_suffix = ""
		if rat.eq(input_mos.equave, 2) then
			equave_suffix = "o"
		elseif rat.eq(input_mos.equave, 3) then
			equave_suffix = "t"
		elseif rat.eq(input_mos.equave, rat.new(3, 2)) then
			equave_suffix = "f"
		elseif rat.is_harmonic(input_mos.equave) then
			local a, b = rat.as_pair(input_mos.equave)
			equave_suffix = a
		else
			equave_suffix = rat.as_ratio(input_mos.equave)
		end
		neutral_scalesig = string.format("[[%ded%s]]", input_mos.nL*2, equave_suffix)
	end
	
	local section_header = "Related MOS scales"
	local section_entries = {
		{string.format("<div style=\"margin-top: 0.6em;\"><b>%s</b></div>", section_header)},
		{"[[Operations_on_MOSes#Parent_MOS | Parent]]", parent_scalesig},
		{"[[Operations_on_MOSes#Sister_MOS | Sister]]", sister_scalesig},
		{"[[Operations_on_MOSes#Daughter_MOS | Daughters]]", soft_scalesig .. ", " .. hard_scalesig},
		{"[[Operations_on_MOSes#Neutralization | Neutralized]]", neutral_scalesig},
		{"[[Flought_scale | 2-Flought]]", soft_floght_scalesig .. ", " .. hard_floght_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, rat.new(5,3))
	
	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 = "Equal tunings"
	local equave_annotation = ""
	if rat.eq(input_mos.equave, 3) then
		equave_annotation = "<sup><abbr title=\"In steps of edt\">(edt)</sup>"
	elseif rat.eq(input_mos.equave, rat.new(3,2)) then
		equave_annotation = "<sup><abbr title=\"In steps of edf\">(edf)</sup>"
	elseif not rat.eq(input_mos.equave, 2) then
		local equave_as_ratio = rat.as_ratio(input_mos.equave)
		equave_annotation = string.format("<sup><abbr title=\"In steps of ed%s\">(ed%s)</sup>", equave_as_ratio, equave_as_ratio)
	end
	
	local section_entries = {
		{string.format("<div style=\"margin-top: 0.6em;\"><b>%s</b>%s</div>", section_header, equave_annotation)},
	}
	for i = 1, #step_ratios do
		local step_ratio = step_ratios[i]
		
		local ed_as_string = mos.mos_to_et_as_string(input_mos, step_ratio)
		
		local gen_in_steps = mos.bright_gen_to_et_steps_as_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, { caption, text })
	end

	return section_entries
end

-- New "main" function
function p._infobox_mos(input_mos, other_names_unparsed)
	local input_mos = input_mos or mos.new(4,5,3)
	local other_names_unparsed = other_names_unparsed or ""
	
	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,
	}
	
	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 result = p._infobox_mos(input_mos, other_names)
	if not debug_mode then
		result = result .. p.categorize(input_mos)
	end
	
	return frame:preprocess(result)
end

return p