Module:MOS intro: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Ganaram inukshuk (talk | contribs)
Intro text includes dark generator range, following the bright generator
ArrowHead294 (talk | contribs)
mNo edit summary
 
(70 intermediate revisions by 4 users not shown)
Line 1: Line 1:
local mos = require('Module:MOS')
local rat = require('Module:Rational')
local utils = require('Module:Utils')
local et = require('Module:ET')
local p = {}
local p = {}


-- Helper function that parses entries from a semicolon-delimited string and returns them in an array
local et = require("Module:ET")
function p.parse_entries(unparsed)
local mos = require("Module:MOS")
local parsed = {}
local rat = require("Module:Rational")
for entry in string.gmatch(unparsed, '([^;]+)') do
local tamnams = require("Module:TAMNAMS")
table.insert(parsed, entry) -- Add to array
local tip = require("Module:Template input parse")
end
local utils = require("Module:Utils")
return parsed
local yesno = require("Module:Yesno")
end
 
-- TODO:
-- - Possible cleanup/rewording
-- - Possible official deprecation of other names; focus should be on what a mos
--  is, not what it's called.


-- Function that creates a mos intro sentence, given a mos and any other names
-- Helper function
function p.mos_intro_sentence(input_mos, other_names)
-- Lists out names, with each name being bold
local input_mos = input_mos or mos.new(5, 2)
function p.mos_intro_list_names(mos_names, conjunction)
local other_names = other_names or "name1; name2; name3"
local mos_names = mos_names or { "name1", "name2", "name3" }
local conjunction = conjunction or "and"
-- Get the step counts and number of periods
-- List the names
local nL = input_mos.nL -- Number of large steps per equave
local names_list = ""
local ns = input_mos.ns -- Number of small steps per equave
if #mos_names == 1 then
local n = rat.gcd(nL, ns) -- Number of periods
-- Only one mos name
local x = round(nL / n) -- Number of large steps per period
names_list = string.format("'''%s'''", mos_names[1])
local y = round(ns / n) -- Number of small steps per period
elseif #mos_names == 2 then
-- Two mos names (name and alternate-name)
names_list = string.format("'''%s''' %s '''%s'''", mos_names[1], conjunction, mos_names[2])
elseif #mos_names > 2 then
-- Three or more mos names
for i = 1, #mos_names - 1 do
names_list = names_list .. string.format("'''%s''', ", mos_names[i])
end
names_list = names_list .. string.format("%s '''%s'''", conjunction, mos_names[#mos_names])
else
-- No names
names_list = ""
end
-- Get the equave as a ratio and in cents
return names_list
local equave = input_mos.equave
end
local equave_in_cents = rat.cents(equave)


-- Get the period in cents
-- Helper function
local period_in_cents = equave_in_cents / n
-- Introduces the mos by its scale sig and names
-- Names must be entered as an array
function p.mos_intro_names(scale_sig, tamnams_name, other_names)
local scale_sig = scale_sig or "5L 2s"
local tamnams_name = tamnams_name or { "diatonic" }
local other_names = other_names or { "other-name" }
-- Get the scalesig
-- Get all the mos's names, starting with tamnams names if applicable
local scale_sig = mos.as_string(input_mos)
local tamnams_names_list = p.mos_intro_list_names(tamnams_name, "and")
local other_names_list = p.mos_intro_list_names(other_names, "or")
-- Get all the mos's names, starting with tamnams names if applicable
-- Construct the sentence
-- Some mosses have two tamnams names, so it's necessary to tokenize it
local sentence = string.format("'''%s'''", scale_sig)
local tamnams_name = mos.tamnams_name[scale_sig] or ""
local mos_names = p.parse_entries(tamnams_name)
-- Tokenize the other names passed in, then add them to the mos names
-- Add names
local other_names_tokenized = p.parse_entries(other_names)
if tamnams_names_list ~= "" and other_names_list ~= "" then
for i = 1, #other_names_tokenized do
-- There are both tamnams names and alternate names
mos_names[#mos_names + 1] = other_names_tokenized[i]
sentence = sentence .. string.format(", named %s in [[TAMNAMS]] (also known as %s),", tamnams_names_list, other_names_list)
elseif tamnams_names_list ~= "" and other_names_list == "" then
-- There are only tamnams names
sentence = sentence .. string.format(", named %s in [[TAMNAMS]],", tamnams_names_list)
elseif tamnams_names_list == "" and other_names_list ~= "" then
-- There are no tamnams names but there are alternate names
sentence = sentence .. string.format(", also called %s,", other_names_list)
end
end
-- Get the eds (ets) corresponding to the collapsed and equalized mosses
return sentence
local collapsed_et = et.new(nL, input_mos.equave)
end
local equalized_et = et.new(nL + ns, input_mos.equave)
 
-- Helper function
-- Determines what mos the given mos descends from
-- as well as what step ratio that produces this scale
function p.find_mos_ancestor(input_mos)
local input_mos = input_mos or mos.new(7, 7)
local z = input_mos.nL
local w = input_mos.ns
local generations = 0
-- Get the sizes of the generator for the collapsed and equalized et in steps
-- For an ancestral mos zU wv and descendant xL ys, how many steps of size
-- These are used to calculate cent values for the generators
-- L and s can fit inside U and v? (basically the chunking operation)
local generator = mos.bright_gen(input_mos)
local lg_chunk = { nL = 1, ns = 0 }
local gen_collapsed_in_steps = generator["L"]
local sm_chunk = { nL = 0, ns = 1 }
local gen_equalized_in_steps = generator["L"] + generator["s"]
-- Is the mos octave-equivalent or non-octave?
while (z ~= w) and (z + w > 10) do
local is_octave_equivalent = equave_in_cents == 1200
local m1 = math.max(z, w)
local m2 = math.min(z, w)
-- For use with updating ancestor mos chunks
local z_prev = z
-- Count how many generations
generations = generations + 1
-- Update step ratios
z = m2
w = m1 - m2
-- Update large chunk
local prev_lg_chunk = { nL = lg_chunk.nL, ns = lg_chunk.ns }
lg_chunk.nL = lg_chunk.nL + sm_chunk.nL
lg_chunk.ns = lg_chunk.ns + sm_chunk.ns
-- Update small chunk
if z ~= z_prev then
sm_chunk = prev_lg_chunk
end
end
-- How many places should cent values be rounded to?
return mos.new(z, w, input_mos.equave), lg_chunk, sm_chunk, generations
local round = 3
end
 
-- Helper function
-- What mos does the input mos descend from?
function p.mos_descends_from(input_mos)
local input_mos = input_mos or mos.new(7, 7)
-- Create the intro string starting with the scale
local ancestor_mos, lg_chunk, sm_chunk, generations = p.find_mos_ancestor(input_mos)
local intro = "'''" .. scale_sig .. "'''"
-- Add the mos names, if any
--[[
if #mos_names == 1 then
-- Calculate the range of step ratios the ancestor should have
-- Only one mos name
-- Sort ratios by hardness
intro = intro .. ", also called '''" .. mos_names[1] .. "''', is a"
local num1 = lg_chunk.nL + lg_chunk.ns
elseif #mos_names == 2 then
local den1 = sm_chunk.nL + sm_chunk.ns
-- Two mos names (name and alternate-name)
local num2 = lg_chunk.nL
intro = intro .. ", also called '''" .. mos_names[1] .. "''' or '''" .. mos_names[2] .. "''', is a"
local den2 = sm_chunk.nL
elseif #mos_names > 2 then
local first_ancestor_step_ratio = ""
-- Three or more mos names (name, alternate-name, and other-alternate-name)
local second_ancestor_step_ratio = ""
intro = intro .. ", also called "
if num1/den1 < num2/den2 then
for i = 1, #mos_names - 1 do
first_ancestor_step_ratio = string.format("%d:%d", num1, den1)
intro = intro .. "'''" .. mos_names[i] .. "''', "
second_ancestor_step_ratio = string.format("%d:%d", num2, den2)
end
intro = intro .. "or '''" .. mos_names[#mos_names] .. "''', is a"
else
else
-- No names
first_ancestor_step_ratio = string.format("%d:%d", num2, den2)
intro = intro .. " is a"
second_ancestor_step_ratio = string.format("%d:%d", num1, den1)
end
end
-- Add whether it's non-octave
-- Step ratio range as text
if is_octave_equivalent then
local step_ratio_range = string.format("%s to %s", first_ancestor_step_ratio, second_ancestor_step_ratio)
intro = intro .. " [[moment of symmetry]] scale consisting of "
else
-- Step ratio range as a named range
intro = intro .. " [[non-octave]] [[moment of symmetry]] scale consisting of "
local named_range = ""
if step_ratio_range == "1:1 to 2:1" then
named_range = "soft-of-basic"
elseif step_ratio_range == "2:1 to 1:0" then
named_range = "hard-of-basic"
elseif step_ratio_range == "1:1 to 3:2" then
named_range = "soft"
elseif step_ratio_range == "3:2 to 2:1" then
named_range = "hyposoft"
elseif step_ratio_range == "2:1 to 3:1" then
named_range = "hypohard"
elseif step_ratio_range == "3:1 to 1:0" then
named_range = "hard"
elseif step_ratio_range == "1:1 to 4:3" then
named_range = "ultrasoft"
elseif step_ratio_range == "4:3 to 3:2" then
named_range = "parasoft"
elseif step_ratio_range == "3:2 to 5:3" then
named_range = "quasisoft"
elseif step_ratio_range == "5:3 to 2:1" then
named_range = "minisoft"
elseif step_ratio_range == "2:1 to 5:2" then
named_range = "minihard"
elseif step_ratio_range == "5:2 to 3:1" then
named_range = "quasihard"
elseif step_ratio_range == "3:1 to 4:1" then
named_range = "parahard"
elseif step_ratio_range == "4:1 to 1:0" then
named_range = "ultrahard"
end
end
]]--
-- Add the step counts per period
local descendant_text = ""
-- Determine where "steps" should be plural or singular, as well
if generations == 1 then
if nL == 1 then
descendant_text = string.format("%s is a child scale of [[%s]]", mos.as_string(input_mos), mos.as_string(ancestor_mos))
intro = intro .. "1 large step and "
elseif generations == 2 then
descendant_text = string.format("%s is a grandchild scale of [[%s]]", mos.as_string(input_mos), mos.as_string(ancestor_mos))
elseif generations == 3 then
descendant_text = string.format("%s is a great-grandchild scale of [[%s]]", mos.as_string(input_mos), mos.as_string(ancestor_mos))
else
else
intro = intro .. nL .. " large steps and "
descendant_text = string.format("%s is related to [[%s]]", mos.as_string(input_mos), mos.as_string(ancestor_mos))
end
end
if ns == 1 then
intro = intro .. "1 small step,"
--if named_range == "" then
-- descendant_text = descendant_text .. string.format(", produced by such scales with a [[step ratio]] within the range of %s.", step_ratio_range)
-- descendant_text = descendant_text .. "."
--else
-- descendant_text = descendant_text .. string.format(", produced by such scales with a [[step ratio]] within the %s range (%s).", named_range, step_ratio_range)
-- descendant_text = descendant_text .. "."
--end
descendant_text = descendant_text .. string.format(", expanding it by %d tones.", input_mos.nL + input_mos.ns - ancestor_mos.nL - ancestor_mos.ns)
return descendant_text
end
 
-- Main function (updated)
function p._mos_intro(input_mos, other_names)
local input_mos = input_mos or mos.new(5, 5, 3)
local other_names = ""
-- Scale sig
local scale_sig = mos.as_string(input_mos)
-- Tamnams names, if any
local tamnams_name = tamnams.lookup_name(input_mos) or ""
-- Parsed names
local tamnams_pasred = tip.parse_entries(tamnams_name)
local other_parsed = tip.parse_entries(other_names)
-- Step counts for large steps, small steps, and number of periods
local nL = input_mos.nL
local ns = input_mos.ns
local n = utils._gcd(nL, ns)
-- Equave as ratio and cents
local equave_as_ratio = rat.as_ratio(input_mos.equave)
local equave_in_cents = rat.cents(input_mos.equave)
local period_in_cents = equave_in_cents / n
-- How many decimal places to round to?
local round = 1
-- Build up intro text, starting with the scale sig and scale names
-- This is done through the aid of a helper function
local intro = p.mos_intro_names(scale_sig, tamnams_pasred, other_parsed)
-- Add equave equivalence
if rat.eq(input_mos.equave, rat.new(2)) then
intro = intro .. " is a 2/1-equivalent ([[octave equivalence|octave-equivalent]]) [[moment of symmetry]] scale"
elseif rat.eq(input_mos.equave, rat.new(3)) then
intro = intro .. " is a 3/1-equivalent ([[tritave]]-equivalent) [[moment of symmetry]] scale"
elseif rat.eq(input_mos.equave, rat.new(3,2)) then
intro = intro .. " is a 3/2-equivalent (fifth-equivalent) [[moment of symmetry]] scale"
else
else
intro = intro .. ns .. " small steps,"
intro = intro .. string.format(" is a %s-equivalent ([[nonoctave|non-octave]]) [[moment of symmetry]] scale", equave_as_ratio)
end
end
-- Add the number of repetitions
-- Add step counts
-- If multi-period, determine whether "steps" should be plural or singular, as well
intro = intro .. string.format(" containing %d large %s", nL, (nL == 1 and "step" or "steps"))
intro = intro .. string.format(" and %d small %s", ns, (ns == 1 and "step" or "steps"))
-- Add repetition
if n == 1 then
if n == 1 then
intro = intro .. " repating every "
intro = intro .. ", repeating every " .. (equave_in_cents == 1200 and "[[octave]]." or string.format(" interval of [[%s]] (%.1f{{cent}}).", equave_as_ratio, equave_in_cents, round))
else
else
intro = intro .. " with a [[period]] of "
intro = intro .. string.format(", with a [[period]] of %d large %s", nL/n, (nL/n == 1 and "step" or "steps"))
if x == 1 then
intro = intro .. string.format(" and %d small %s", ns/n, (ns/n == 1 and "step" or "steps"))
intro = intro .. "1 large step and "
intro = intro .. string.format(" that repeats every %.1f{{cent}}", period_in_cents)
else
intro = intro .. (n == 2 and ", or twice every" or string.format(", or %d times every", n)) .. (equave_in_cents == 1200 and " octave." or string.format(" interval of [[%s]] (%.1f{{cent}}).", equave_as_ratio, equave_in_cents, round))
intro = intro .. x .. " large steps and "
end
if y == 1 then
intro = intro .. "1 small step "
else
intro = intro .. y .. " small steps "
end
if n == 2 then
intro = intro .. "that repeats twice every "
else
intro = intro .. "that repeats " .. n .. " times every "
end
end
end
-- Add the equivalence interval
-- TODO: add descendant info
if is_octave_equivalent then
if equave_in_cents == 1200 and nL + ns > 10 and nL ~= ns then
intro = intro .. "[[octave]]"
intro = intro .. " " .. p.mos_descends_from(input_mos)
else
intro = intro .. "interval of [[" .. rat.as_ratio(equave) .. "]] (" .. utils._round_dec(equave_in_cents, round) .. "¢)"
end
end
-- Add the period (this is a pun)
-- Add generator ranges
if n == 1 then
-- Get the eds (ets) corresponding to the collapsed and equalized mosses
intro = intro .. ". "
local collapsed_et = et.new(nL, input_mos.equave)
else
local equalized_et = et.new(nL + ns, input_mos.equave)
intro = intro .. ", or every " .. utils._round_dec(period_in_cents, round) .. "¢. "
end
-- Get the sizes of the bright generator for the collapsed and equalized et in steps
-- These are used to calculate cent values for the generators
-- The values for the dark generator are the period complements
local generator = mos.bright_gen(input_mos)
local gen_collapsed_in_steps = generator["L"]
local gen_equalized_in_steps = generator["L"] + generator["s"]
local bright_gen_max = et.cents(collapsed_et, gen_collapsed_in_steps)
local bright_gen_min = et.cents(equalized_et, gen_equalized_in_steps)
local dark_gen_min = equave_in_cents / n - bright_gen_max
local dark_gen_max = equave_in_cents / n - bright_gen_min
local bright_gen_min_r = tostring(utils._round_dec(bright_gen_min, round))
local bright_gen_max_r = tostring(utils._round_dec(bright_gen_max, round))
local dark_gen_min_r = tostring(utils._round_dec(dark_gen_min, round))
local dark_gen_max_r = tostring(utils._round_dec(dark_gen_max, round))
-- Add the generator range (for bright generator)
intro = intro .. string.format(" [[Generator]]s that produce this scale range from %s{{cent}} to %s{{cent}}, or from %s{{cent}} to %s{{cent}}.", bright_gen_min_r, bright_gen_max_r, dark_gen_min_r, dark_gen_max_r)
local collapsed_gen_in_cents = et.cents(collapsed_et, gen_collapsed_in_steps)
local equalized_gen_in_cents = et.cents(equalized_et, gen_equalized_in_steps)
intro = intro .. "This scale is made using a [[generator]] ranging from " .. utils._round_dec(equalized_gen_in_cents, round)
intro = intro .. "¢ to " .. utils._round_dec(collapsed_gen_in_cents, round) .. "¢, "
-- Add the generator range (for dark generator)
-- Rothenberg propriety (rothenprop) info
-- The dark generator range is the complement of the bright generator's extremes
if ns == 1 then
intro = intro .. "or from " .. utils._round_dec(equave_in_cents - collapsed_gen_in_cents, round)
intro = intro .. " Scales of this form are always [[proper]] because there is only one small step."
intro = intro .. "¢ to " .. utils._round_dec(equave_in_cents - equalized_gen_in_cents, round) .. "¢."
elseif ns / n == 1 then
intro = intro .. " Scales of the true MOS form, where every period is the same, are [[proper]] because there is only one small step per period."
end
return intro
return intro
Line 157: Line 296:


-- Function for use with a template
-- Function for use with a template
function p.mos_intro(frame)
function p.mos_intro_frame(frame)
-- Get and parse the the mos's scale signature, in the form xL ys or xL ys <p/q>
-- Get and parse the the mos's scale signature, in the form xL ys or xL ys <p/q>
local input_mos = mos.parse(frame.args['Scale Signature']) or mos.new(5, 2, 2)
local input_mos = mos.parse(frame.args["Scale Signature"]) or mos.new(5, 2, 2)
local other_names = frame.args['Other Names'] or ""
local other_names = frame.args["Other Names"] or ""
local depparams = (other_names ~= "" and " [[Category:Pages with deprecated parameters]]" or "")
    local result = p._mos_intro(input_mos, other_names) .. depparams
    local debugg = yesno(frame.args["debug"])
   
    -- Debugger option
    if debugg == true then
result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
end
return p.mos_intro_sentence(input_mos, other_names)
return frame:preprocess(result)
end
end


return p
return p

Latest revision as of 02:40, 22 January 2026

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

This module automatically fills in an introduction for MOS scales. It clarifies the equave, numbers of long and short steps, and range of generators that produce it.

Introspection summary for Module:MOS intro 
Functions provided (6)
Line Function Params
18 mos_intro_list_names (mos_names, conjunction)
47 mos_intro_names (scale_sig, tamnams_name, other_names)
77 find_mos_ancestor (input_mos)
119 mos_descends_from (input_mos)
202 _mos_intro (main) (input_mos, other_names)
298 mos_intro_frame (invokable) (frame)
Lua modules required (7)
Variable Module Functions used
et Module:ET new
cents
mos Module:MOS new
as_string
bright_gen
parse
rat Module:Rational as_ratio
cents
eq
new
tamnams Module:TAMNAMS lookup_name
tip Module:Template input parse parse_entries
utils Module:Utils _gcd
_round_dec
yesno Module:Yesno yesno

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


local p = {}

local et = require("Module:ET")
local mos = require("Module:MOS")
local rat = require("Module:Rational")
local tamnams = require("Module:TAMNAMS")
local tip = require("Module:Template input parse")
local utils = require("Module:Utils")
local yesno = require("Module:Yesno")

-- TODO:
-- - Possible cleanup/rewording
-- - Possible official deprecation of other names; focus should be on what a mos
--   is, not what it's called.

-- Helper function
-- Lists out names, with each name being bold
function p.mos_intro_list_names(mos_names, conjunction)
	local mos_names = mos_names or { "name1", "name2", "name3" }
	local conjunction = conjunction or "and"
	
	-- List the names
	local names_list = ""
	if #mos_names == 1 then
		-- Only one mos name
		names_list = string.format("'''%s'''", mos_names[1])
	elseif #mos_names == 2 then
		-- Two mos names (name and alternate-name)
		names_list = string.format("'''%s''' %s '''%s'''", mos_names[1], conjunction, mos_names[2])
	elseif #mos_names > 2 then
		-- Three or more mos names
		for i = 1, #mos_names - 1 do
			names_list = names_list .. string.format("'''%s''', ", mos_names[i])
		end
		names_list = names_list .. string.format("%s '''%s'''", conjunction, mos_names[#mos_names])
	else
		-- No names
		names_list = ""
	end
	
	return names_list
end

-- Helper function
-- Introduces the mos by its scale sig and names
-- Names must be entered as an array
function p.mos_intro_names(scale_sig, tamnams_name, other_names)
	local scale_sig = scale_sig or "5L 2s"
	local tamnams_name = tamnams_name or { "diatonic" }
	local other_names = other_names or { "other-name" }
	
	-- Get all the mos's names, starting with tamnams names if applicable
	local tamnams_names_list = p.mos_intro_list_names(tamnams_name, "and")
	local other_names_list = p.mos_intro_list_names(other_names, "or")
	
	-- Construct the sentence
	local sentence = string.format("'''%s'''", scale_sig)
	
	-- Add names
	if tamnams_names_list ~= "" and other_names_list ~= "" then
		-- There are both tamnams names and alternate names
		sentence = sentence .. string.format(", named %s in [[TAMNAMS]] (also known as %s),", tamnams_names_list, other_names_list)
	elseif tamnams_names_list ~= "" and other_names_list == "" then
		-- There are only tamnams names
		sentence = sentence .. string.format(", named %s in [[TAMNAMS]],", tamnams_names_list)
	elseif tamnams_names_list == "" and other_names_list ~= "" then
		-- There are no tamnams names but there are alternate names
		sentence = sentence .. string.format(", also called %s,", other_names_list)
	end
	
	return sentence
end

-- Helper function
-- Determines what mos the given mos descends from
-- as well as what step ratio that produces this scale
function p.find_mos_ancestor(input_mos)
	local input_mos = input_mos or mos.new(7, 7)
	
	local z = input_mos.nL
	local w = input_mos.ns
	local generations = 0
	
	-- For an ancestral mos zU wv and descendant xL ys, how many steps of size
	-- L and s can fit inside U and v? (basically the chunking operation)
	local lg_chunk = { nL = 1, ns = 0 }
	local sm_chunk = { nL = 0, ns = 1 }
	
	while (z ~= w) and (z + w > 10) do
		local m1 = math.max(z, w)
		local m2 = math.min(z, w)
		
		-- For use with updating ancestor mos chunks
		local z_prev = z
		
		-- Count how many generations
		generations = generations + 1
		
		-- Update step ratios
		z = m2
		w = m1 - m2
		
		-- Update large chunk
		local prev_lg_chunk = { nL = lg_chunk.nL, ns = lg_chunk.ns }
		lg_chunk.nL = lg_chunk.nL + sm_chunk.nL
		lg_chunk.ns = lg_chunk.ns + sm_chunk.ns
		
		-- Update small chunk
		if z ~= z_prev then
			sm_chunk = prev_lg_chunk
		end
	end
	
	return mos.new(z, w, input_mos.equave), lg_chunk, sm_chunk, generations
end

-- Helper function
-- What mos does the input mos descend from?
function p.mos_descends_from(input_mos)
	local input_mos = input_mos or mos.new(7, 7)
	
	local ancestor_mos, lg_chunk, sm_chunk, generations = p.find_mos_ancestor(input_mos)
	
	--[[
	-- Calculate the range of step ratios the ancestor should have
	-- Sort ratios by hardness
	local num1 = lg_chunk.nL + lg_chunk.ns
	local den1 = sm_chunk.nL + sm_chunk.ns
	local num2 = lg_chunk.nL
	local den2 = sm_chunk.nL
	local first_ancestor_step_ratio = ""
	local second_ancestor_step_ratio = ""
	if num1/den1 < num2/den2 then
		first_ancestor_step_ratio = string.format("%d:%d", num1, den1)
		second_ancestor_step_ratio = string.format("%d:%d", num2, den2)
	else
		first_ancestor_step_ratio = string.format("%d:%d", num2, den2)
		second_ancestor_step_ratio = string.format("%d:%d", num1, den1)
	end
	
	-- Step ratio range as text
	local step_ratio_range = string.format("%s to %s", first_ancestor_step_ratio, second_ancestor_step_ratio)
	
	-- Step ratio range as a named range
	local named_range = ""
	if step_ratio_range == "1:1 to 2:1" then
		named_range = "soft-of-basic"
	elseif step_ratio_range == "2:1 to 1:0" then
		named_range = "hard-of-basic"
	elseif step_ratio_range == "1:1 to 3:2" then
		named_range = "soft"
	elseif step_ratio_range == "3:2 to 2:1" then
		named_range = "hyposoft"
	elseif step_ratio_range == "2:1 to 3:1" then
		named_range = "hypohard"
	elseif step_ratio_range == "3:1 to 1:0" then
		named_range = "hard"
	elseif step_ratio_range == "1:1 to 4:3" then
		named_range = "ultrasoft"
	elseif step_ratio_range == "4:3 to 3:2" then
		named_range = "parasoft"
	elseif step_ratio_range == "3:2 to 5:3" then
		named_range = "quasisoft"
	elseif step_ratio_range == "5:3 to 2:1" then
		named_range = "minisoft"
	elseif step_ratio_range == "2:1 to 5:2" then
		named_range = "minihard"
	elseif step_ratio_range == "5:2 to 3:1" then
		named_range = "quasihard"
	elseif step_ratio_range == "3:1 to 4:1" then
		named_range = "parahard"
	elseif step_ratio_range == "4:1 to 1:0" then
		named_range = "ultrahard"
	end
	]]--
	
	local descendant_text = ""
	if generations == 1 then
		descendant_text = string.format("%s is a child scale of [[%s]]", mos.as_string(input_mos), mos.as_string(ancestor_mos))
	elseif generations == 2 then
		descendant_text = string.format("%s is a grandchild scale of [[%s]]", mos.as_string(input_mos), mos.as_string(ancestor_mos))
	elseif generations == 3 then
		descendant_text = string.format("%s is a great-grandchild scale of [[%s]]", mos.as_string(input_mos), mos.as_string(ancestor_mos))
	else
		descendant_text = string.format("%s is related to [[%s]]", mos.as_string(input_mos), mos.as_string(ancestor_mos))
	end
	
	--if named_range == "" then
	--	descendant_text = descendant_text .. string.format(", produced by such scales with a [[step ratio]] within the range of %s.", step_ratio_range)
	--	descendant_text = descendant_text .. "."
	--else
	--	descendant_text = descendant_text .. string.format(", produced by such scales with a [[step ratio]] within the %s range (%s).", named_range, step_ratio_range)
	--	descendant_text = descendant_text .. "."
	--end
	
	descendant_text = descendant_text .. string.format(", expanding it by %d tones.", input_mos.nL + input_mos.ns - ancestor_mos.nL - ancestor_mos.ns)
	
	return descendant_text
end

-- Main function (updated)
function p._mos_intro(input_mos, other_names)
	local input_mos = input_mos or mos.new(5, 5, 3)
	local other_names = ""
	
	-- Scale sig
	local scale_sig = mos.as_string(input_mos)
	
	-- Tamnams names, if any
	local tamnams_name = tamnams.lookup_name(input_mos) or ""
	
	-- Parsed names
	local tamnams_pasred = tip.parse_entries(tamnams_name)
	local other_parsed = tip.parse_entries(other_names)
	
	-- Step counts for large steps, small steps, and number of periods
	local nL = input_mos.nL
	local ns = input_mos.ns
	local n = utils._gcd(nL, ns)
	
	-- Equave as ratio and cents
	local equave_as_ratio = rat.as_ratio(input_mos.equave)
	local equave_in_cents = rat.cents(input_mos.equave)
	local period_in_cents = equave_in_cents / n
	
	-- How many decimal places to round to?
	local round = 1
	
	-- Build up intro text, starting with the scale sig and scale names
	-- This is done through the aid of a helper function
	local intro = p.mos_intro_names(scale_sig, tamnams_pasred, other_parsed)
	
	-- Add equave equivalence
	if rat.eq(input_mos.equave, rat.new(2)) then
		intro = intro .. " is a 2/1-equivalent ([[octave equivalence|octave-equivalent]]) [[moment of symmetry]] scale"
	elseif rat.eq(input_mos.equave, rat.new(3)) then
		intro = intro .. " is a 3/1-equivalent ([[tritave]]-equivalent) [[moment of symmetry]] scale"
	elseif rat.eq(input_mos.equave, rat.new(3,2)) then
		intro = intro .. " is a 3/2-equivalent (fifth-equivalent) [[moment of symmetry]] scale"
	else
		intro = intro .. string.format(" is a %s-equivalent ([[nonoctave|non-octave]]) [[moment of symmetry]] scale", equave_as_ratio)
	end
	
	-- Add step counts
	intro = intro .. string.format(" containing %d large %s", nL, (nL == 1 and "step" or "steps"))
	intro = intro .. string.format(" and %d small %s", ns, (ns == 1 and "step" or "steps"))
	
	-- Add repetition
	if n == 1 then
		intro = intro .. ", repeating every " .. (equave_in_cents == 1200 and "[[octave]]." or string.format(" interval of [[%s]] (%.1f{{cent}}).", equave_as_ratio, equave_in_cents, round))
	else
		intro = intro .. string.format(", with a [[period]] of %d large %s", nL/n, (nL/n == 1 and "step" or "steps"))
		intro = intro .. string.format(" and %d small %s", ns/n, (ns/n == 1 and "step" or "steps"))
		intro = intro .. string.format(" that repeats every %.1f{{cent}}", period_in_cents)
		intro = intro .. (n == 2 and ", or twice every" or string.format(", or %d times every", n)) .. (equave_in_cents == 1200 and " octave." or string.format(" interval of [[%s]] (%.1f{{cent}}).", equave_as_ratio, equave_in_cents, round))
	end
	
	-- TODO: add descendant info
	if equave_in_cents == 1200 and nL + ns > 10 and nL ~= ns then
		intro = intro .. " " .. p.mos_descends_from(input_mos)
	end
	
	-- Add generator ranges
	-- Get the eds (ets) corresponding to the collapsed and equalized mosses
	local collapsed_et = et.new(nL, input_mos.equave)
	local equalized_et = et.new(nL + ns, input_mos.equave)
	
	-- Get the sizes of the bright generator for the collapsed and equalized et in steps
	-- These are used to calculate cent values for the generators
	-- The values for the dark generator are the period complements
	local generator = mos.bright_gen(input_mos)
	local gen_collapsed_in_steps = generator["L"]
	local gen_equalized_in_steps = generator["L"] + generator["s"]
	local bright_gen_max = et.cents(collapsed_et, gen_collapsed_in_steps)
	local bright_gen_min = et.cents(equalized_et, gen_equalized_in_steps)
	
	local dark_gen_min = equave_in_cents / n - bright_gen_max
	local dark_gen_max = equave_in_cents / n - bright_gen_min
	
	local bright_gen_min_r = tostring(utils._round_dec(bright_gen_min, round))
	local bright_gen_max_r = tostring(utils._round_dec(bright_gen_max, round))
	local dark_gen_min_r = tostring(utils._round_dec(dark_gen_min, round))
	local dark_gen_max_r = tostring(utils._round_dec(dark_gen_max, round))
	
	intro = intro .. string.format(" [[Generator]]s that produce this scale range from %s{{cent}} to %s{{cent}}, or from %s{{cent}} to %s{{cent}}.", bright_gen_min_r, bright_gen_max_r, dark_gen_min_r, dark_gen_max_r)
	
	-- Rothenberg propriety (rothenprop) info
	if ns == 1 then
		intro = intro .. " Scales of this form are always [[proper]] because there is only one small step."
	elseif ns / n == 1 then
		intro = intro .. " Scales of the true MOS form, where every period is the same, are [[proper]] because there is only one small step per period."
	end
	
	return intro
end

-- Function for use with a template
function p.mos_intro_frame(frame)
	-- Get and parse the the mos's scale signature, in the form xL ys or xL ys <p/q>
	local input_mos = mos.parse(frame.args["Scale Signature"]) or mos.new(5, 2, 2)
	local other_names = frame.args["Other Names"] or ""
	
	local depparams = (other_names ~= "" and " [[Category:Pages with deprecated parameters]]" or "")
	
    local result = p._mos_intro(input_mos, other_names) .. depparams
    local debugg = yesno(frame.args["debug"])
    
    -- Debugger option
    if debugg == true then
		result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
	end
	
	return frame:preprocess(result)
end

return p