Module:Infobox MOS
Jump to navigation
Jump to search
Documentation transcluded from /doc
Note: Do not invoke this module directly; use the corresponding template instead: Template:Infobox MOS.
Documentation transcluded from /doc
Note: Do not invoke this module directly; use the corresponding template instead: Template:Infobox MOS.
Generates an infobox providing information about a given moment of symmetry (MOS) scale.
local p = {}
local rat = require("Module:Rational")
local mos = require("Module:MOS")
local et = require("Module:ET")
local xp = require("Module:Xenpaper")
local infobox = 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")
-- 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(tuning)
local tuning = tuning or "5L 2s"
local input_mos = mos.parse(tuning)
-- 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|↖ %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|↑ %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 ↗]]", 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|← %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 →]]", 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|↙ %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|↓ %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 ↘]]", 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
local ed = et.new(number_of_periods, input_mos.equave)
period_as_string = et.backslash_display(ed, 1)
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("<b>%s</b>", section_header)},
{"[[Step pattern]]", step_pattern},
{"[[Equave]]", string.format("%s (%.1f¢)", equave_as_string, equave_in_cents)},
{"[[Period]]", string.format("%s (%.1f¢)", 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 = et.new(input_mos.nL + input_mos.ns, input_mos.equave, "")
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 = 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("<b>%s</b>%s", section_header, equave_annotation)},
{"[[Bright]]", string.format("%s to %s (%.1f¢ to %.1f¢)", bright_min_in_steps, bright_max_in_steps, bright_min_in_cents, bright_max_in_cents)},
{"[[Dark]]", string.format("%s to %s (%.1f¢ to %.1f¢)", 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(12, 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("<b>%s</b>", 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("<b>%s</b>", 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("<b>%s</b>", 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, 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)
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_step_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("<b>%s</b>", 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("<b>%s</b>%s", section_header, equave_annotation)},
}
for i = 1, #step_ratios do
local step_ratio = step_ratios[i]
local ed = mos.mos_to_et(input_mos, step_ratio)
local ed_as_string = et.as_string(ed)
local gen_in_steps = mos.bright_gen_to_et_steps(input_mos, step_ratio) .. "\\" .. ed.size
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 (L:s = %d:%d)]", step_ratio_name, xenpaper_link, step_ratio[1], step_ratio[2])
local text = string.format("[[%s | %s]] (%.1f¢)", 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(tuning, other_names_unparsed)
local tuning = tuning or "5L 2s"
local other_names_unparsed = other_names_unparsed or ""
local tuning_parsed = mos.parse(tuning)
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(tuning_parsed)
p.concatenate_tables(sections, kb_vis)
-- Scale structure section
local scale_structure = p.scale_structure(tuning_parsed)
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(tuning_parsed)
p.concatenate_tables(sections, gen_sizes)
-- Tamnams info section, if applicable
local tamnams_info = p.tamnams_information(tuning_parsed)
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(tuning_parsed)
p.concatenate_tables(sections, related_scales)
-- Equal tunings section
local equal_tunings = p.equal_tunings(tuning_parsed)
p.concatenate_tables(sections, equal_tunings)
-- Adjacent links
local adjacent_links = p.adjacent_links(tuning_parsed)
return infobox.build_multilink(tuning, sections, adjacent_links)
--return sections
end
-- Wrapper function
function p.infobox_MOS(frame)
local tuning = frame.args["Tuning"]
local other_names = frame.args["othernames"] or nil
local debug_mode = yesno(tonumber(frame.args["debug"]), false)
local result = p._infobox_mos(tuning, other_names)
if not debug_mode then
result = result .. p.categorize(tuning)
end
return result
end
return p