Module:MOS gamut: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Ganaram inukshuk (talk | contribs)
No edit summary
ArrowHead294 (talk | contribs)
mNo edit summary
 
(24 intermediate revisions by 3 users not shown)
Line 1: Line 1:
local mos = require('Module:MOS')
local rat = require('Module:Rational')
local mosm = require('Module:MOS modes')
local et = require('Module:ET')
local p = {}
local p = {}


-- Helper function for creating a genchain, a sequence of named pitches where consecutive
local et = require("Module:ET")
-- pitches are a generator apart. This can only work in one direction at a time, so it's
local mos = require("Module:MOS")
-- necessary to call this twice if both an ascending and descending chain are needed. For
local mosm = require("Module:MOS modes")
-- a multi-period mos, multiple genchains are returned as an array of arrays, where each
local mosnot = require("Module:MOS notation")
-- array has indices denote the number of generators going up (or down) and the element
local rat = require("Module:Rational")
-- denote the named pitch. For the single-period case, it's a size-1 array whose element
local utils = require("Module:Utils")
-- is a single genchain.
local yesno = require("Module:Yesno")
-- This genchain is agnostic of notation, so in standard notation, instead of
-- C, C#/Db, D etc, it's denoted  as N(0), N(0)+c/N(1)-c, N(1)
-- What is returned is the number in the parentheses (k, accessed as index) and
-- how many chromas to add (an integer value)
-- Parameters:
-- - input_mos - the mos itself represented as a data structure from Module:MOS
-- - genchain_init - how many named pitches per period are there without accidentals added?
--  This is either the value u or d for the UDP of up|dp.
-- - genchain_length - how many generators should the genchain extend after the root?
-- - going_up - bool; whether the genchain is going up or down; true for up, false for down
function p.mos_genchain(input_mos, genchain_init, genchain_length, going_up)
-- Default parameters for testing
--[[
local input_mos = input_mos or mos.new(5, 2, 2)
local genchain_init = genchain_init or 5
local genchain_length = genchain_length or 10
local note_symbols = note_symbols or "CDEFGAB"
local chroma_symbol = chroma_symbol or "#"
local going_up = going_up or true
]]--
-- Get the number of mossteps per period and equave
local mossteps_per_equave = input_mos.nL + input_mos.ns
local periods = rat.gcd(input_mos.nL, input_mos.ns)
local mossteps_per_period = mossteps_per_equave / periods
--[[
-- Split the note symbols string into subsets
-- This is only necessary if the mos is multi-period
local note_subsets = {}
for i = 1, periods do
local start_index = (i - 1) * mossteps_per_period + 1
local stop_index = i * mossteps_per_period
local substr = string.sub(note_symbols, start_index, stop_index)
table.insert(note_subsets, substr)
end
]]--
-- Create the genchain for each period
local genchains = {}
for i = 1, periods do
--local note_names = note_subsets[i]
-- Get the size of the generator in mossteps
local gen = mos.bright_gen(input_mos)
local gen_in_mossteps = gen['L'] + gen['s']
-- If the genchain is descending (ie, going_up is false), switch to
-- using the dark gen in mossteps, which is the period complement
-- of the bright gen; going up by the dark gen is the same as going
-- down by the bright gen
if not going_up then
gen_in_mossteps = mossteps_per_period - gen_in_mossteps
end
-- Use this value, with modular arithmteic, as an index to get the note name
local accumulator = 0
-- Create a genchain that initially starts at the root
--local root = string.sub(note_names, 1, 1)
--local genchain = { root }
local root = { ['Nk'] = 0, ['nc'] = 0 }
local genchain = { root }
-- Create the rest of the genchain
for j = 1, genchain_length do
-- Increment the index by the generator
accumulator = accumulator + gen_in_mossteps
-- Convert the accumulator into an index
local index = accumulator % mossteps_per_period
-- Add accidentals
-- This is negative if the genchain is descending
local accidentals_to_add = 0
if j > genchain_init then
accidentals_to_add = math.ceil((j - genchain_init) / mossteps_per_period)
end
if not going_up then
accidentals_to_add = accidentals_to_add * -1
end
-- Get the final note name N(k)+nc
local note_name = {}
note_name['Nk'] = index -- The N(k) in N(k) notation
note_name['nc'] = accidentals_to_add -- How many chromas to add or subtract
-- Add the note name
table.insert(genchain, note_name)
end
-- Add the genchain
table.insert(genchains, genchain)
end
return genchains
end


-- Function that produces a gamut, a sequence of note names with accidentals, for an edo
-- Helper function for the function that has "frame" as a parameter
function p.mos_gamut(input_mos, generators_up, step_ratio, note_symbols, chroma_plus_symbol, chroma_minus_symbol)
function p.mos_gamut(input_mos, udp, step_ratio, note_symbols, chroma_plus_symbol, chroma_minus_symbol)
-- Default parameters for input mos and step ratio (5L 2s and 2:1 step ratio)
-- Default parameters for input mos and step ratio (5L 2s and 2:1 step ratio)
local input_mos = input_mos or mos.new(5, 2, 2)
local input_mos = input_mos or mos.new(5, 2, 2)
Line 118: Line 17:
-- Get the number of mossteps per period and equave
-- Get the number of mossteps per period and equave
local mossteps_per_equave = input_mos.nL + input_mos.ns
local mossteps_per_equave = input_mos.nL + input_mos.ns
local periods = rat.gcd(input_mos.nL, input_mos.ns)
local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
local mossteps_per_period = mossteps_per_equave / periods
local mossteps_per_period = mossteps_per_equave / periods_per_equave
-- Some default params will be different if the scalesig is 5L 2s
-- Some default params will be different if the scalesig is 5L 2s
local scale_sig = mos.as_string(input_mos)
local scale_sig = mos.as_string(input_mos)
-- The default generators_up value corresponds to the brightest mode,
-- The default UDP corresponds to the middle mode. For mosses with an even
-- unless the mos is 5L 2s, then it's the 2nd-brightest mode
-- number of modes, there are two middle modes, so use the brighter of the
local generators_up_default = mossteps_per_equave - periods
-- two instead.
-- If it's 5L 2s, default to the second-brightest mode.
local udp_default = { periods_per_equave * math.ceil((mossteps_per_period - 1)/ 2), periods_per_equave * math.floor((mossteps_per_period - 1) / 2) }
if scale_sig == "5L 2s" then
if scale_sig == "5L 2s" then
generators_up_default = 5
udp_default = { 5, 1 }
end
end
local generators_up = generators_up or generators_up_default
local udp = udp or udp_default
local generators_up = udp[1]
local generators_down = udp[2]


-- The natural note symbols are those that correspond to diamond-mos
-- The natural note symbols are those that correspond to diamond-mos
Line 152: Line 55:
local chroma_plus_symbol = chroma_plus_symbol or chroma_plus_default
local chroma_plus_symbol = chroma_plus_symbol or chroma_plus_default
local chroma_minus_symbol = chroma_minus_symbol or chroma_minus_default
local chroma_minus_symbol = chroma_minus_symbol or chroma_minus_default
-- Reconstruct the UDP up|dp (u times p pipe d times p)
-- The generators_up corresponds to up and is given to us, so generators_down should
-- be reconstructed to correspond to dp; dividing either generators_up or generators_down
-- by the number of periods will give the number of generators per period (u and d by
-- themselves)
local generators_down = mossteps_per_equave - generators_up - periods
-- How long is the inital genchain for notes without accidentals?
-- How long is the inital genchain for notes without accidentals?
local gens_up_per_period = generators_up / periods
local gens_up_per_period = generators_up / periods_per_equave
local gens_down_per_period = generators_down / periods
local gens_down_per_period = generators_down / periods_per_equave
-- Get and simplify the step ratio
-- Get and simplify the step ratio
local kp = step_ratio[1]
local kp = step_ratio[1]
local kq = step_ratio[2]
local kq = step_ratio[2]
local k = rat.gcd(kp, kq)
local k = utils._gcd(kp, kq)
local num = kp / k
local num = kp / k
local den = kq / k
local den = kq / k
-- How many large and small steps per period?
-- How many large and small steps per period?
local x = input_mos.nL / periods -- Large step count
local x = input_mos.nL / periods_per_equave -- Large step count
local y = input_mos.ns / periods -- Small step count
local y = input_mos.ns / periods_per_equave -- Small step count
-- How many esteps are in the equave? Gamut does not include any notes reached by
-- How many esteps are in the equave? Gamut does not include any notes reached by
-- increments smaller than a chroma, so if the step ratio is not simplified, the
-- increments smaller than a chroma, so if the step ratio is not simplified, the
-- gamut returned will be for a simplified step ratio
-- gamut returned will be for a simplified step ratio
local estedps_per_equave = input_mos.nL * num + input_mos.ns * den
local esteps_per_equave = input_mos.nL * num + input_mos.ns * den
-- Similarly, how many esteps per period?
-- Similarly, how many esteps per period?
local estedps_per_period = x * num + y * den
local esteps_per_period = x * num + y * den
-- How long should the genchain extend after the initial genchain?
-- How long should the genchain extend after the initial genchain?
Line 201: Line 97:
end
end
-- How long are the genchains?
-- How long are the genchains? Length is per period
local ascending_genchain_length = gens_up_per_period + genchain_extend
-- Genchain length counts the root, hence the +1
local descending_genchain_length = gens_down_per_period + genchain_extend
local ascending_genchain_length = gens_up_per_period + genchain_extend + 1
local descending_genchain_length = gens_down_per_period + genchain_extend + 1
-- Get the ascending and descending genchains
-- Get the ascending and descending genchains
-- The genchains are notationally agnostic so notation needs to be applied to them
-- The genchains are notationally agnostic so notation needs to be applied to them
local ascending_genchain = p.mos_genchain(input_mos, gens_up_per_period, ascending_genchain_length, true)
local ascending_genchain = mosnot.mos_nomacc_chain(input_mos, gens_up_per_period, ascending_genchain_length, true)
local descending_genchain = p.mos_genchain(input_mos, gens_down_per_period, descending_genchain_length, false)
local descending_genchain = mosnot.mos_nomacc_chain(input_mos, gens_down_per_period, descending_genchain_length, false)
-- Create an empty gamut
-- Create an empty gamut
local gamut = {}
local gamut = {}
for i = 1, estedps_per_equave + 1 do
for i = 1, esteps_per_equave + 1 do
table.insert(gamut, "")
table.insert(gamut, "")
end
end
Line 218: Line 115:
-- How many esteps are the bright and dark generators?
-- How many esteps are the bright and dark generators?
local bright_gen = mos.bright_gen(input_mos)
local bright_gen = mos.bright_gen(input_mos)
local esteps_per_bright_gen = bright_gen['L'] * num + bright_gen['s'] * den
local esteps_per_bright_gen = bright_gen["L"] * num + bright_gen["s"] * den
local esteps_per_dark_gen = estedps_per_period - esteps_per_bright_gen
local esteps_per_dark_gen = esteps_per_period - esteps_per_bright_gen
-- Add the notes to the gamut
-- Add the notes to the gamut
for j = 1, periods do
for j = 1, periods_per_equave do
local bright_accumulator = 0
local bright_accumulator = 0
for i = 1, #ascending_genchain[j] do
for i = 1, #ascending_genchain[j] do
local index = (bright_accumulator % estedps_per_period) + (j - 1) * estedps_per_period + 1
local index = (bright_accumulator % esteps_per_period) + (j - 1) * esteps_per_period + 1
-- Convert the notationally agnostic form into a form that uses given notation
-- Convert the notationally agnostic form into a form that uses given notation
local note = ascending_genchain[j][i]
local note = ascending_genchain[j][i]
local note_symbol = string.sub(note_symbols, note['Nk'] + 1, note['Nk'] + 1)
local note_symbol = string.sub(note_symbols, note["Mossteps"] + 1, note["Mossteps"] + 1)
local chroma_count = note['nc']
local chroma_count = note["Chromas"]
local note_name = note_symbol .. string.rep(chroma_plus_symbol, chroma_count)
local note_name = note_symbol .. string.rep(chroma_plus_symbol, chroma_count)
Line 238: Line 135:
local dark_accumulator = esteps_per_dark_gen
local dark_accumulator = esteps_per_dark_gen
for i = 2, #descending_genchain[j] do
for i = 2, #descending_genchain[j] do
local index = (dark_accumulator % estedps_per_period) + (j - 1) * estedps_per_period + 1
local index = (dark_accumulator % esteps_per_period) + (j - 1) * esteps_per_period + 1
-- Convert the notationally agnostic form into a form that uses given notation
-- Convert the notationally agnostic form into a form that uses given notation
local note = descending_genchain[j][i]
local note = descending_genchain[j][i]
local note_symbol = string.sub(note_symbols, note['Nk'] + 1, note['Nk'] + 1)
local note_symbol = string.sub(note_symbols, note["Mossteps"] + 1, note["Mossteps"] + 1)
local chroma_count = note['nc'] * -1
local chroma_count = note["Chromas"] * -1
local note_name = note_symbol .. string.rep(chroma_minus_symbol, chroma_count)
local note_name = note_symbol .. string.rep(chroma_minus_symbol, chroma_count)
Line 261: Line 158:
return gamut
return gamut
end
-- Helper function for parsing a step ratio entered as a string "p/q"
function p.parse_step_ratio(step_ratio_unparsed)
local parsed = {}
for entry in string.gmatch(step_ratio_unparsed, '([^/]+)') do
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
table.insert(parsed, trimmed) -- Add to array
end
local ratio = { tonumber(parsed[1]), tonumber(parsed[2]) }
return ratio
end
end


function p.mos_gamut_frame(frame)
function p.mos_gamut_frame(frame)
-- Default parameters for input mos and step ratio (5L 2s and 2:1 step ratio)
-- Default parameters for input mos and step ratio (5L 2s and 2:1 step ratio)
local input_mos_unparsed = frame.args['Scale Signature']
local input_mos_unparsed = frame.args["Scale Signature"]
local input_mos = mos.parse(input_mos_unparsed) or mos.new(2, 5, 2)
local input_mos = mos.parse(input_mos_unparsed) or mos.new(2, 5, 2)
-- Step ratio
-- Step ratio
local step_ratio = { 2, 1 }
local step_ratio = { 2, 1 }
if string.len(frame.args['Step Ratio']) > 0 then
if string.len(frame.args["Step Ratio"]) > 0 then
step_ratio = p.parse_step_ratio(frame.args['Step Ratio'])
step_ratio = mosnot.parse_step_ratio(frame.args["Step Ratio"])
end
end
-- Get the number of mossteps per period and equave
-- Get the number of mossteps per period and equave
local mossteps_per_equave = input_mos.nL + input_mos.ns
local mossteps_per_equave = input_mos.nL + input_mos.ns
local periods = rat.gcd(input_mos.nL, input_mos.ns)
local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
local mossteps_per_period = mossteps_per_equave / periods
local mossteps_per_period = mossteps_per_equave / periods_per_equave
-- If certain params were left blank and the scalesig is 5L 2s, the default
-- If certain params were left blank and the scalesig is 5L 2s, the default
Line 296: Line 180:
local scale_sig = mos.as_string(input_mos)
local scale_sig = mos.as_string(input_mos)
-- The default generators_up value corresponds to the brightest mode,
-- The default UDP corresponds to the middle mode. For mosses with an even
-- unless the mos is 5L 2s, then it's the 2nd-brightest mode
-- number of modes, there are two middle modes, so use the brighter of the
local generators_up = mossteps_per_equave - periods
-- two instead.
-- If it's 5L 2s, default to the second-brightest mode.
local udp = { periods_per_equave * math.ceil((mossteps_per_period - 1)/ 2), periods_per_equave * math.floor((mossteps_per_period - 1) / 2) }
if scale_sig == "5L 2s" then
if scale_sig == "5L 2s" then
generators_up = 5
udp = { 5, 1 }
end
end
-- If a value was entered, override the default value
if string.len(frame.args["UDP"]) > 0 then
if string.len(frame.args['Bright Gens Up']) > 0 then
udp = mosnot.parse_udp(frame.args["UDP"])
generators_up = tonumber(frame.args['Bright Gens Up'])
end
end
local generators_up = udp[1]
local generators_down = udp[2]
-- Get note symbols
-- Get notation: naturals (or nominals), sharp symbol, and flat symbol
-- If this param was blank, default to diamond-mos; limited to 17 note names
local notation_default = { ["Naturals"] = string.sub("JKLMNOPQRSTUVWXYZ", 1, mossteps_per_equave), ["Sharp"] = "&", ["Flat"] = "@" }
-- But if it's blank and the scalesig is 5L 2s, default to standard notation
-- This order of operations allows for overriding standard notation for 5L 2s
local note_symbols_main = "JKLMNOPQRSTUVWXYZ"
local note_symbols = string.sub(note_symbols_main, 1, mossteps_per_equave)
if scale_sig == "5L 2s" then
if scale_sig == "5L 2s" then
note_symbols = "CDEFGAB"
notation_default["Naturals"] = "CDEFGAB"
end
notation_default["Sharp"] = "#"
-- If a value was entered, override the default value
notation_default["Flat"] = "b"
if string.len(frame.args['Note Symbols']) > 0 then
note_symbols = frame.args['Note Symbols']
end
-- Get accidental symbols
-- If this param was blank, default to diamond-mos symbols & and @
-- unless the mos is 5L 2s, then it's sharp and flat # and b
-- This order of operations allows for overriding standard notation for 5L 2s
local chroma_plus_symbol = "&"
local chroma_minus_symbol = "@"
if scale_sig == "5L 2s" then
chroma_plus_symbol = "#"
chroma_minus_symbol = "b"
end
-- If value(s) were entered, override the default values
if string.len(frame.args['Sharp Symbol']) > 0 then
chroma_plus_symbol = frame.args['Sharp Symbol']
end
if string.len(frame.args['Flat Symbol']) > 0 then
chroma_minus_symbol = frame.args['Flat Symbol']
end
end
local notation = mosnot.parse_notation(frame.args["Notation"]) or notation_default
local note_symbols = notation["Naturals"]
local chroma_plus_symbol = notation["Sharp"]
local chroma_minus_symbol = notation["Flat"]
-- Get the gamut
-- Get the gamut
local gamut = p.mos_gamut(input_mos, generators_up, step_ratio, note_symbols, chroma_plus_symbol, chroma_minus_symbol)
local gamut = p.mos_gamut(input_mos, udp, step_ratio, note_symbols, chroma_plus_symbol, chroma_minus_symbol)
-- Old code for a horizontal table; the default is now a vertical table
--[[
-- Format the gamut as a table
local result = '{| class="wikitable"\n'
-- Create the first row; this needs an edo for the header, followed by the
-- steps
local steps_in_et = input_mos.nL * step_ratio[1] + input_mos.ns * step_ratio[2]
local et_for_mos = et.new(steps_in_et, input_mos.equave)
local result = result .. "! Steps of " .. et.as_string(et_for_mos) .. "\n"
local step_ratio_gcd = rat.gcd(step_ratio[1], step_ratio[2]) -- GCD of the sizes of L and s, in case L:s isn't simplified
for i = 1, #gamut do
result = result .. "!" .. (i - 1) * step_ratio_gcd .. "\n"
end
-- The second row contains the note names
local result = result .. "|-\n"
local result = result .. "! Note names on " .. string.sub(note_symbols, 1, 1) .. "\n"
for i = 1, #gamut do
-- Get the note name
local note_name = gamut[i]
-- If the note name has a slash, replace it with a newline
note_name = note_name:gsub("/", "\n")
-- If note name string is one character, it's a natural so the cell is white
-- For anything else, the cell is black (actually gray) to mimic a piano
if string.len(note_name) == 1 then
result = result .. '|bgcolor="white"|'.. note_name .. " \n\n"
else
result = result .. '|bgcolor="gray"|'.. note_name .. "\n"
end
end
result = result .. "|}"
]]--
 
--[[
-- Format the gamut as a table
local result = '{| class="wikitable"\n'
 
-- Produce the headers
local steps_in_et = input_mos.nL * step_ratio[1] + input_mos.ns * step_ratio[2]
local et_for_mos = et.new(steps_in_et, input_mos.equave)
result = result .. "! Steps of " .. et.as_string(et_for_mos) .. " !! Note name\n"
-- Add the rows
local step_ratio_gcd = rat.gcd(step_ratio[1], step_ratio[2]) -- GCD of the sizes of L and s, in case L:s isn't simplified
-- If note name string is one character, it's a natural so the row is white
-- For anything else, the row is black (actually gray) to mimic a piano
for i = 1, #gamut do
-- Get the note name
local note_name = gamut[i]
-- If the note name has a slash, replace it with a comma
note_name = note_name:gsub("/", ", ")
result = result .. "|-\n"
if string.len(note_name) == 1 then
result = result .. '|' .. step_ratio_gcd * (i-1) .. "||" .. note_name .. " \n\n"
else
result = result .. '|bgcolor="#c8ccd1"|' .. step_ratio_gcd * (i-1) .. '||bgcolor="#c8ccd1"|' .. note_name .. " \n\n"
end
end
result = result .. "|}"
]]--


-- Since the gamut on a mos page is just text, so will this
-- Since the gamut on a mos page is just text, so will this
-- Formatting options may be explored at a later date
-- Formatting options may be explored at a later date
local steps_in_et = input_mos.nL * step_ratio[1] + input_mos.ns * step_ratio[2]
local result = ""
local et_for_mos = et.new(steps_in_et, input_mos.equave)
local result = mos.as_string(input_mos) " gamut for " .. et.as_string(et_for_mos) .. ": "
for i = 1, #gamut - 1 do
for i = 1, #gamut - 1 do
-- If the note name does not contain accidentals, it's a natural and should be bold
-- If the note name does not contain accidentals, it's a natural and should be bold
Line 427: Line 223:
result = result .. "'''" .. gamut[#gamut] .. "'''"
result = result .. "'''" .. gamut[#gamut] .. "'''"
return result
-- Debugger option
local debugg = yesno(frame.args["debug"])
if debugg == true then
result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
end
return frame:preprocess(result)
end
end


return p
return p

Latest revision as of 12:29, 1 June 2025

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

This module produces a gamut (sequence of note names with accidentals) for an edo.

Introspection summary for Module:MOS gamut 
Functions provided (2)
Line Function Params
12 mos_gamut (input_mos, udp, step_ratio, note_symbols, chroma_plus_symbol, chroma_minus_symbol)
162 mos_gamut_frame (invokable) (frame)
Lua modules required (7)
Variable Module Functions used
et Module:ET dependency not used
mos Module:MOS new
as_string
bright_gen
parse
mosm Module:MOS modes dependency not used
mosnot Module:MOS notation mos_nomacc_chain
parse_step_ratio
parse_udp
parse_notation
rat Module:Rational dependency not used
utils Module:Utils _gcd
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 mosm = require("Module:MOS modes")
local mosnot = require("Module:MOS notation")
local rat = require("Module:Rational")
local utils = require("Module:Utils")
local yesno = require("Module:Yesno")

-- Helper function for the function that has "frame" as a parameter
function p.mos_gamut(input_mos, udp, step_ratio, note_symbols, chroma_plus_symbol, chroma_minus_symbol)
	-- Default parameters for input mos and step ratio (5L 2s and 2:1 step ratio)
	local input_mos = input_mos or mos.new(5, 2, 2)
	local step_ratio = step_ratio or { 2, 1 }
	
	-- Get the number of mossteps per period and equave
	local mossteps_per_equave = input_mos.nL + input_mos.ns
	local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
	local mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- Some default params will be different if the scalesig is 5L 2s
	local scale_sig = mos.as_string(input_mos)
	
	-- The default UDP corresponds to the middle mode. For mosses with an even
	-- number of modes, there are two middle modes, so use the brighter of the
	-- two instead.
	-- If it's 5L 2s, default to the second-brightest mode.
	local udp_default = { periods_per_equave * math.ceil((mossteps_per_period - 1)/ 2), periods_per_equave * math.floor((mossteps_per_period - 1) / 2) }
	if scale_sig == "5L 2s" then
		udp_default = { 5, 1 }
	end
	local udp = udp or udp_default
	local generators_up = udp[1]
	local generators_down = udp[2]

	-- The natural note symbols are those that correspond to diamond-mos
	-- (JKLMN...) unless the mos is 5L 2s, then it's CDEFGAB
	-- If it's diamond-mos, gamut is limited to 17 note names
	local note_symbols_main = "JKLMNOPQRSTUVWXYZ"
	local note_symbols_default = string.sub(note_symbols_main, 1, mossteps_per_equave)
	if scale_sig == "5L 2s" then
		note_symbols_default = "CDEFGAB"
	end
	local note_symbols = note_symbols or note_symbols_default
	
	-- The default accidentals are the amp and at (& and @)
	-- unless the mos is 5L 2s, then it's sharp and flat (# and b)
	local chroma_plus_default = "&"
	local chroma_minus_default = "@"
	if scale_sig == "5L 2s" then
		chroma_plus_default = "#"
		chroma_minus_default = "b"
	end
	local chroma_plus_symbol = chroma_plus_symbol or chroma_plus_default
	local chroma_minus_symbol = chroma_minus_symbol or chroma_minus_default
	
	-- How long is the inital genchain for notes without accidentals?
	local gens_up_per_period = generators_up / periods_per_equave
	local gens_down_per_period = generators_down / periods_per_equave
	
	-- Get and simplify the step ratio
	local kp = step_ratio[1]
	local kq = step_ratio[2]
	local k = utils._gcd(kp, kq)
	local num = kp / k
	local den = kq / k
	
	-- How many large and small steps per period?
	local x = input_mos.nL / periods_per_equave		-- Large step count
	local y = input_mos.ns / periods_per_equave		-- Small step count
	
	-- How many esteps are in the equave? Gamut does not include any notes reached by
	-- increments smaller than a chroma, so if the step ratio is not simplified, the
	-- gamut returned will be for a simplified step ratio
	local esteps_per_equave = input_mos.nL * num + input_mos.ns * den
	
	-- Similarly, how many esteps per period?
	local esteps_per_period = x * num + y * den
	
	-- How long should the genchain extend after the initial genchain?
	-- For a basic step ratio 2:1, extend by x
	-- For a collapsed or equalized step ratio, don't extend at all
	-- For any other ratio p:q (simplified), do this calculation:
	-- x*floor(p/2) + y*floor(q/2)
	-- This is such that each altered note (what would be the black keys on a piano)
	-- has names that contain the fewest chromas possible, even if they have more than
	-- one name. EG, standard notation has C#/Db have two names, but both names
	-- have the fewest possible accidentals
	local genchain_extend = 0
	if num / den == 2 then
		genchain_extend = x
	elseif num == den or den == 0 then
		genchain_extend = 0
	else
		genchain_extend = x * math.floor(num/2) + y * math.floor(den/2)
	end
	
	-- How long are the genchains? Length is per period
	-- Genchain length counts the root, hence the +1
	local ascending_genchain_length = gens_up_per_period + genchain_extend + 1
	local descending_genchain_length = gens_down_per_period + genchain_extend + 1
	
	-- Get the ascending and descending genchains
	-- The genchains are notationally agnostic so notation needs to be applied to them
	local ascending_genchain = mosnot.mos_nomacc_chain(input_mos, gens_up_per_period, ascending_genchain_length, true)
	local descending_genchain = mosnot.mos_nomacc_chain(input_mos, gens_down_per_period, descending_genchain_length, false)
	
	-- Create an empty gamut
	local gamut = {}
	for i = 1, esteps_per_equave + 1 do
		table.insert(gamut, "")
	end
	
	-- How many esteps are the bright and dark generators?
	local bright_gen = mos.bright_gen(input_mos)
	local esteps_per_bright_gen = bright_gen["L"] * num + bright_gen["s"] * den
	local esteps_per_dark_gen = esteps_per_period - esteps_per_bright_gen
	
	-- Add the notes to the gamut
	for j = 1, periods_per_equave do
		local bright_accumulator = 0
		for i = 1, #ascending_genchain[j] do
			local index = (bright_accumulator % esteps_per_period) + (j - 1) * esteps_per_period + 1
			
			-- Convert the notationally agnostic form into a form that uses given notation
			local note = ascending_genchain[j][i]
			local note_symbol = string.sub(note_symbols, note["Mossteps"] + 1, note["Mossteps"] + 1)
			local chroma_count = note["Chromas"]
			local note_name = note_symbol .. string.rep(chroma_plus_symbol, chroma_count)
			
			gamut[index] = gamut[index] .. note_name
			bright_accumulator = bright_accumulator + esteps_per_bright_gen
		end
		local dark_accumulator = esteps_per_dark_gen
		for i = 2, #descending_genchain[j] do
			local index = (dark_accumulator % esteps_per_period) + (j - 1) * esteps_per_period + 1
			
			-- Convert the notationally agnostic form into a form that uses given notation
			local note = descending_genchain[j][i]
			local note_symbol = string.sub(note_symbols, note["Mossteps"] + 1, note["Mossteps"] + 1)
			local chroma_count = note["Chromas"] * -1
			local note_name = note_symbol .. string.rep(chroma_minus_symbol, chroma_count)
			
			-- Add to gamut
			-- If there is a note there already, then append and separate with a slash
			if gamut[index] ~= "" then
				gamut[index] = gamut[index] .. "/" .. note_name
			else
				gamut[index] = gamut[index] .. note_name
			end
			dark_accumulator = dark_accumulator + esteps_per_dark_gen
		end
	end
	
	-- Last note in the gamut is the root up one equave
	gamut[#gamut] = gamut[1]
	
	return gamut
end

function p.mos_gamut_frame(frame)
	-- Default parameters for input mos and step ratio (5L 2s and 2:1 step ratio)
	local input_mos_unparsed = frame.args["Scale Signature"]
	local input_mos = mos.parse(input_mos_unparsed) or mos.new(2, 5, 2)
	
	-- Step ratio
	local step_ratio = { 2, 1 }
	if string.len(frame.args["Step Ratio"]) > 0 then
		step_ratio = mosnot.parse_step_ratio(frame.args["Step Ratio"])
	end
	
	-- Get the number of mossteps per period and equave
	local mossteps_per_equave = input_mos.nL + input_mos.ns
	local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
	local mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- If certain params were left blank and the scalesig is 5L 2s, the default
	-- params will be for standard notation
	local scale_sig = mos.as_string(input_mos)
	
	-- The default UDP corresponds to the middle mode. For mosses with an even
	-- number of modes, there are two middle modes, so use the brighter of the
	-- two instead.
	-- If it's 5L 2s, default to the second-brightest mode.
	local udp = { periods_per_equave * math.ceil((mossteps_per_period - 1)/ 2), periods_per_equave * math.floor((mossteps_per_period - 1) / 2) }
	if scale_sig == "5L 2s" then
		udp = { 5, 1 }
	end
	if string.len(frame.args["UDP"]) > 0 then
		udp = mosnot.parse_udp(frame.args["UDP"])
	end
	local generators_up = udp[1]
	local generators_down = udp[2]
	
	-- Get notation: naturals (or nominals), sharp symbol, and flat symbol
	local notation_default = { ["Naturals"] = string.sub("JKLMNOPQRSTUVWXYZ", 1, mossteps_per_equave), ["Sharp"] = "&", ["Flat"] = "@" }
	if scale_sig == "5L 2s" then
		notation_default["Naturals"] = "CDEFGAB"
		notation_default["Sharp"] = "#"
		notation_default["Flat"] = "b"
	end
	local notation = mosnot.parse_notation(frame.args["Notation"]) or notation_default
	local note_symbols = notation["Naturals"]
	local chroma_plus_symbol = notation["Sharp"]
	local chroma_minus_symbol = notation["Flat"]
	
	-- Get the gamut
	local gamut = p.mos_gamut(input_mos, udp, step_ratio, note_symbols, chroma_plus_symbol, chroma_minus_symbol)

	-- Since the gamut on a mos page is just text, so will this
	-- Formatting options may be explored at a later date
	local result = ""
	for i = 1, #gamut - 1 do
		-- If the note name does not contain accidentals, it's a natural and should be bold
		local note_name = gamut[i]
		if string.match(note_name, chroma_plus_symbol) or string.match(note_name, chroma_minus_symbol) then
			result = result .. note_name .. ", "
		else
			result = result .. "'''" .. note_name .. "''', "
		end
	end
	result = result .. "'''" .. gamut[#gamut] .. "'''"
	
	-- Debugger option
	local debugg = yesno(frame.args["debug"])
	if debugg == true then
		result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
	end
	
	return frame:preprocess(result)
end

return p