Module:MOS degrees: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Ganaram inukshuk (talk | contribs)
No edit summary
Ganaram inukshuk (talk | contribs)
Was editing incorrect module; restored text
Line 1: Line 1:
local mos = require('Module:MOS')
local mos = require('Module:MOS')
local et = require('Module:ET')
local rat = require('Module:Rational')
local rat = require('Module:Rational')
local utils = require('Module:Utils')
local utils = require('Module:Utils')
local mosnot = require('Module:MOS notation')
local mosnot = require('Module:MOS notation') -- Contains the important functions
local et = require('Module:ET')
local p = {}
local p = {}


-- Version 2 of the mos degrees moudle (by ganaraminukshuk)
-- TODO: For preprocessing, add bools for whether a note is a nominal, the
-- This is based more off of the mos intervals module and older, non-tempalte tables that feature JI ratios,
-- unison, the period, or the equave, so the main function doesn't have to
-- and doesn't depend on genchains (except for note names) to calculate cent values or degree names.
-- calculate them
-- Current TODO list:
-- - Optional note names column
 
-- Helper function
-- Parses entries from a semicolon-delimited string and returns them in an array
-- TODO: Separate this and related functions (parse_pair and parse_kv_pairs) into its own module, as they're included
-- in various modules at this point, such as: scale tree, mos mdoes
function p.parse_entries(unparsed)
local parsed = {}
for entry in string.gmatch(unparsed, '([^;]+)') do
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
table.insert(parsed, trimmed) -- Add to array
end
return parsed
end
 
-- Helper function
-- Parses pairs of elements separated by a colon
-- A pair must be two elements or it will be returned as an empty array
function p.parse_pair(unparsed)
local parsed = {}
for entry in string.gmatch(unparsed, '([^:]+)') do
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
table.insert(parsed, trimmed) -- Add to array
end
if #parsed == 2 then
return parsed
else
return {}
end
end
 
-- Helper function
-- Takes a list of semicolon-delimited pairs and returns a map
-- (or dictionary or associative array) of key-value pairs
-- Each entry is colon-delimited as key : pair
function p.parse_kv_pairs(unparsed)
-- Tokenize the string of unparsed pairs
local parsed = p.parse_entries(unparsed)
-- Then tokenize the tokens into key-value pairs
local pairs_ = {}
for i = 1, #parsed do
local pair = p.parse_pair(parsed[i])
if #pair == 2 then
pairs_[pair[1]] = pair[2]
end
end
return pairs_
end


-- Helper function
-- Helper function
Line 63: Line 14:
-- where each step ratio is separated with a slash
-- where each step ratio is separated with a slash
-- EG, "2/1; 3/1; 3/2" becomes {{2, 1}, {3, 1}, {3, 2}}
-- EG, "2/1; 3/1; 3/2" becomes {{2, 1}, {3, 1}, {3, 2}}
-- NOTE: module relies on mosnot (mos notation) to parse step ratios
function p.parse_step_ratios(unparsed)
function p.parse_step_ratio(unparsed)
local parsed = {}
local parsed = {}
for entry in string.gmatch(unparsed, '([^;]+)') do
for entry in string.gmatch(unparsed, '([^;]+)') do
Line 88: Line 38:


-- Helper function
-- Helper function
-- Takes in a step pattern and a quantity of mossteps and calculates the number
-- Parses genchain extend values, where the first value is for the ascending
-- of large and small steps in that interval (or substring), returned as an
-- chain and the second value is for the descending chain
-- associative array containing the large and small step counts.
function p.parse_genchain_extend(unparsed)
-- It's an associative array b/c that's how the brightgen function in the mos
local parsed = {}
-- module works.
for entry in string.gmatch(unparsed, '([^,]+)') do
function p.mosstep_pattern_to_vector(mosstep_pattern, mossteps)
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
local large_step_count = 0
table.insert(parsed, trimmed) -- Add to array
local small_step_count = 0
for i = 1, mossteps do
local step = string.sub(mosstep_pattern, i, i)
if step == "L" then
large_step_count = large_step_count + 1
elseif step == "s" then
small_step_count = small_step_count + 1
end
end
end
local mosstep_vector = { ['L'] = large_step_count, ['s'] = small_step_count }
return mosstep_vector
end
-- Helper function
-- Takes in a mosstep (as an assoc. array containing the number of L's and s's),
-- and a step ratio (as 2-element array containing the sizes of L and s) and
-- calculates number of et-steps.
function p.interval_to_etsteps(mosstep_vector, step_ratios)
return mosstep_vector['L'] * step_ratios[1] + mosstep_vector['s'] * step_ratios[2]
end
-- Helper function
-- Extracts the prefix from the mos module, without the dash and without any text after that
-- May need to revisit to clean up code since this splits text at the "-".
function p.get_mos_prefix(scale_sig)
local unparsed = mos.tamnams_prefix[scale_sig]
local parsed = {}
if unparsed == nil then
if #parsed == 1 then
return "mos"
return { tonumber(parsed[1]), tonumber(parsed[1]) }
else
else
for entry in string.gmatch(unparsed, '([^-]+)') do
return { tonumber(parsed[1]), tonumber(parsed[2]) }
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
table.insert(parsed, trimmed) -- Add to array
end
return parsed[1]
end
end
end
end


-- Helper function
-- Helper function; creates the column for the note name
-- Calculates note names and stores it in an associative array
-- Decoupled degree and note name functions allow note names being omitted.
-- Default notation is diamond-mos, unless it's 5L 2s, then it's standard notation
function p.preprocess_note_names(input_mos, udp, note_symbols, sharp_symbol, flat_symbol, asc_chain_length, des_chain_length)
function p.get_note_names(input_mos, udp, note_symbols, chroma_plus_symbol, chroma_minus_symbol, number_of_alterations)
-- Test parameters
-- 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)
local udp = {5,1} or udp
local udp = udp or { 5, 1 }
local note_symbols = note_symbols or "CDEFGAB"
local note_symbols = note_symbols or "CDEFGAB"
local chroma_plus_symbol = chroma_plus_symbol or "#"
local sharp_symbol = sharp_symbol or "#"
local chroma_minus_symbol = chroma_minus_symbol or "b"
local flat_symbol = flat_symbol or "b"
local number_of_alterations = number_of_alterations or 1
local asc_chain_length = input_mos.nL * 2 + input_mos.ns
local des_chain_length = input_mos.nL * 2 + input_mos.ns
]]--
-- Get the number of mossteps per period and equave
-- Get the number of mossteps per period and equave
Line 154: Line 73:
local mossteps_per_period = mossteps_per_equave / periods_per_equave
local mossteps_per_period = mossteps_per_equave / periods_per_equave
-- Get the number of generators going up and down from the UDP
-- How long are the initial genchain lengths? (These correspond to the UDP)
local generators_up = udp[1]
local gens_up_per_period = udp[1] / periods_per_equave
local generators_down = udp[2]
local gens_dn_per_period = udp[2] / periods_per_equave
-- 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
-- How long should the genchain extend after the initial genchain?
-- The initial genchain lengths are determined by the U and D in the UDP
-- The final genchain length is the following: (x + y) * (alterations + 1)
local ascending_genchain_length = (mossteps_per_period) * (number_of_alterations + 1)
local descending_genchain_length = (mossteps_per_period) * (number_of_alterations + 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)
-- Also get the ascending and descending degreechains
-- These chains are encoded in a numeric form and must be converted into actual names
local ascending_degchain = mosnot.mos_degree_chain(input_mos, ascending_genchain_length, true)
local descending_degchain = mosnot.mos_degree_chain(input_mos, descending_genchain_length, false)
-- Create an empty asoociative array
-- Get the genchains
local note_names = {}
local asc_genchain = mosnot.mos_nomacc_chain(input_mos, gens_up_per_period, asc_chain_length, true)
local des_genchain = mosnot.mos_nomacc_chain(input_mos, gens_dn_per_period, des_chain_length, false)
-- Add the notes to the array
-- Calculate the entries for each cell
for j = 1, periods_per_equave do
local column = {}
for i = 1, #ascending_genchain[j] do
for i = 1, periods_per_equave do
-- Convert the notationally agnostic form into a form that uses given notation
-- Add degrees from ascending chain
local note = ascending_genchain[j][i]
for j = 1, asc_chain_length do
local note_symbol = string.sub(note_symbols, note['mossteps'] + 1, note['mossteps'] + 1)
local mossteps = asc_genchain[i][j]['mossteps']
local chroma_count = note['chromas']
local chromas  = asc_genchain[i][j]['chromas']
local note_name = note_symbol .. string.rep(chroma_plus_symbol, chroma_count)
-- Convert the encoded degree into text
-- Find the note name
local degree_encoded = ascending_degchain[j][i]
local note_symbol = string.sub(note_symbols, mossteps + 1, mossteps + 1)
local degree_decoded = mosnot.mosstep_and_quality_to_degree(degree_encoded['mossteps'], degree_encoded['quality'], "m", "mosdegree", "abbreviated")
local note_name = mosnot.mosstep_and_chroma_to_note_name(mossteps, chromas, note_symbol, sharp_symbol)
 
-- Add to note names
table.insert(column, note_name)
note_names[degree_decoded] = note_name
end
-- Calculate the stop value for the for loop as being 1 or 2, depending
-- on whether this is the last period or not
local stop_value = 1
if i ~= periods_per_equave then
stop_value = stop_value + 1
end
end
for i = 1, #descending_genchain[j] do
-- Add degrees from descending chain
-- Convert the notationally agnostic form into a form that uses given notation
-- The descending chain differs from the ascending chain:
local note = descending_genchain[j][i]
-- - The descending chain should follow after the ascending chain.
local note_symbol = string.sub(note_symbols, note['mossteps'] + 1, note['mossteps'] + 1)
-- - The descending chain's entries should be added backwards and skip
local chroma_count = note['chromas'] * -1
--  the root.
local note_name = note_symbol .. string.rep(chroma_minus_symbol, chroma_count)
-- - This way, if the mos is multi-period, the root of the next period's
--  ascending chain (which is the same as the current period's descend-
--  ing chain) won't be added twice.
-- - If the period is the last period, add the root as the equave.
for j = des_chain_length, stop_value, -1 do
local mossteps = des_genchain[i][j]['mossteps']
local chromas  = des_genchain[i][j]['chromas']
-- Convert the encoded degree into text
-- Find the note name
local degree_encoded = descending_degchain[j][i]
-- If the mosstep is the root of the period, add a period to it
local note_symbol = string.sub(note_symbols, mossteps + 1, mossteps + 1)
-- For the descending chain, any mossteps that correspond to the root of
if mossteps % mossteps_per_period == 0 then
-- a period should correspond instead to the root one period up (EG, if
mossteps = mossteps + mossteps_per_period
-- the root refers to the unison, it should be the degree one octave up)
local mossteps_transposed = degree_encoded['mossteps']
if mossteps_transposed % mossteps_per_period == 0 then
mossteps_transposed = mossteps_transposed + mossteps_per_period
end
end
local note_name = mosnot.mosstep_and_chroma_to_note_name(mossteps, chromas, note_symbol, flat_symbol)
local degree_decoded = mosnot.mosstep_and_quality_to_degree(mossteps_transposed, degree_encoded['quality'], "m", "mosdegree", "abbreviated")
table.insert(column, note_name)
-- Add to note names
note_names[degree_decoded] = note_name
end
end
end
end
-- Last note in the gamut is the root up one equave
return column
--local degree_for_equave = string.format("P%dmd", mossteps_per_equave)
--local degree_for_root = string.format("P0md", mossteps_per_equave)
--note_names[degree_for_equave] = note_names[degree_for_root]
return note_names
end
end


-- Helper function; generate the step vectors for every interval required for the table
-- Helper function; creates the column for the degree name
function p.get_mosstep_vectors(input_mos, number_of_alterations)
-- Decoupled degree and note name functions allow note names being omitted.
-- Default params
function p.preprocess_degrees(input_mos, asc_chain_length, des_chain_length, prefix, notation)
local input_mos = input_mos or mos.new(5, 2)
-- Test parameters
local number_of_alterations = number_of_alterations or 0
--[[
local input_mos = input_mos or mos.new(5, 2, 2)
local asc_chain_length = input_mos.nL * 2 + input_mos.ns
local des_chain_length = input_mos.nL * 2 + input_mos.ns
local prefix = "mos"
local notation = "mosstep"
]]--
-- Get the brightest mode
-- Get the number of mossteps per period and equave
local brightest_mode = mos.brightest_mode(input_mos)
local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
-- Get the number of mossteps per period and equave
-- Get the degrees
local mossteps_per_equave = (input_mos.nL + input_mos.ns)
local asc_degrees = mosnot.mos_degree_chain(input_mos, asc_chain_length, true)
local mossteps_per_period = mossteps_per_equave / utils._gcd(input_mos.nL, input_mos.ns)
local des_degrees = mosnot.mos_degree_chain(input_mos, des_chain_length, false)
-- Add intervals and their alterations, using the large interval size as the zero point for alterations
-- Calculate the entries for each cell
local mosstep_vectors = {}
local column = {}
for i = 1, mossteps_per_equave + 1 do
for i = 1, periods_per_equave do
local mossteps = i - 1
-- Add degrees from ascending chain
for j = 1, asc_chain_length do
local mossteps = asc_degrees[i][j]['mossteps']
local quality  = asc_degrees[i][j]['quality']
-- Find the degree name
-- If the degree is the perfect 0-mosdegree, append "unison"
local degree_name = mosnot.mosstep_and_quality_to_degree(mossteps, quality, prefix, notation)
if mossteps == 0 and quality == 0 then
degree_name = degree_name .. " (unison)"
end
table.insert(column, degree_name)
end
-- Consecutive alterations are always one chroma apart
-- Calculate the stop value for the for loop as being 1 or 2, depending
-- With a perfect non-generator interval, alterations are added by going down and up the same amount
-- on whether this is the last period or not
-- With all other intervals, since there are two sizes and the large interval size is treated as the zero point,
local stop_value = 1
-- alterations are instead by going down n+1 chromas, then going up n chromas
if i ~= periods_per_equave then
-- With the unison, don't go down (only up), and with the equave, don't go up (only down).
stop_value = stop_value + 1
local min_alterations = 0
local max_alterations = 0
if mossteps == 0 then
-- Unison; the min number of alterations is 0
min_alterations = 0
max_alterations = number_of_alterations
elseif mossteps == mossteps_per_equave then
-- Equave; the max number of alterations is 0
min_alterations = -number_of_alterations
max_alterations = 0
elseif mossteps % mossteps_per_period == 0 then
-- Non-unison non-equave periods; the max and min have the "distance" from the zero point
min_alterations = -number_of_alterations
max_alterations = number_of_alterations
else
-- All other intervals; the min's distance is one more than the max's distance
min_alterations = -number_of_alterations - 1
max_alterations = number_of_alterations
end
end
-- Get the current mosstep vector based on the brightest mode
-- Add degrees from descending chain
local current_mosstep_vector = p.mosstep_pattern_to_vector(brightest_mode, mossteps)
-- The descending chain differs from the ascending chain:
for j = min_alterations, max_alterations do
-- - The descending chain should follow after the ascending chain.
-- j is the number of chromas to add or subtract from the base vector
-- - The descending chain's entries should be added backwards and skip
-- Since a chroma is defined as (L-s), add j large steps and subtract j small steps from the current mosstep vector
--  the root.
local L_count = current_mosstep_vector['L'] + j
-- - This way, if the mos is multi-period, the root of the next period's
local s_count = current_mosstep_vector['s'] - j
--   ascending chain (which is the same as the current period's descend-
--   ing chain) won't be added twice.
-- - If the period is the last period, add the root as the equave.
for j = des_chain_length, stop_value, -1 do
local mossteps = des_degrees[i][j]['mossteps']
local quality  = des_degrees[i][j]['quality']
local current_mosstep_vector = { ['L'] = L_count, ['s'] = s_count }
-- Find the degree name
table.insert(mosstep_vectors, current_mosstep_vector)
-- If the degree corresponds to the equave, say it's the equave
local degree_name = mosnot.mosstep_and_quality_to_degree(mossteps, quality, prefix, notation)
-- If j is ever 1, then the mosdegree is the equave
-- This only happens if the current period is the last period
-- If the equave is 2/1, that's the octave
if j == 1 then
if rat.eq(input_mos.equave, 2) then
degree_name = degree_name .. " (octave)"
else
degree_name = degree_name .. " (equave)"
end
end
table.insert(column, degree_name)
end
end
end
end
return mosstep_vectors
return column
end
end


-- Helper function; generate the mosdegree names and their abbreviations for the mos
-- Helper function
function p.get_mosdegree_names_and_abbrevs(input_mos, mos_prefix, number_of_alterations)
-- Creates the columns for the step and cent sizes
-- Default params
-- Separating this into its own function makes it easy to add colums for
local input_mos = input_mos or mos.new(5, 2)
-- different step ratios in the same table.
local number_of_alterations = number_of_alterations or 0
function p.preprocess_steps_and_cents(input_mos, step_ratio, asc_chain_length, des_chain_length)
local mos_prefix = mos_prefix or "mos"
-- Test parameters
--[[
local input_mos = input_mos or mos.new(5, 2, 2)
local step_ratio = { 2, 1 }
local asc_chain_length = input_mos.nL * 2 + input_mos.ns
local des_chain_length = input_mos.nL * 2 + input_mos.ns
]]--
-- 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 mossteps_per_period = mossteps_per_equave / utils._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_per_equave
-- What et is produced given the step ratio and equave?
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)
-- Get the step counts for the bright and dark generators
-- How many esteps per period? Per bright/dark gen?
local esteps_per_period = steps_in_et / periods_per_equave
local bright_gen = mos.bright_gen(input_mos)
local bright_gen = mos.bright_gen(input_mos)
local mossteps_per_bright_gen = bright_gen['L'] + bright_gen['s']
local esteps_per_bright_gen = bright_gen['L'] * step_ratio[1] + bright_gen['s'] * step_ratio[2]
local mossteps_per_dark_gen = mossteps_per_period - mossteps_per_bright_gen
local esteps_per_dark_gen = esteps_per_period - esteps_per_bright_gen
-- How many decimal places to round to?
local round = 1
-- Main loop
-- Calculate the entries for each row
-- Interval qualities depend on whether the intervals are generators or if there is only one size.
local rows = {}
-- Cases for which there are alterations either below or above the main interval sizes, but not both:
for i = 1, periods_per_equave do
-- - If the interval class is the unison, there are no extensions before it and there is only one size (perfect).
-- Add step/cent values for ascending chain
-- - If the interval class is the equave, there are no extensions after it and there is only one size (perfect).
for j = 1, asc_chain_length do
-- Cases for which there are alterations above and below the main interval sizes:
-- - If the interval class is a non-unison non-equave period, there are extensions before and after and there is only one size (perfect).
-- Find the estep count
-- - If the interval class is a non-generator interval, or is the generator for an nL ns mos, there are extensions before and after and the sizes are major and minor.
local estep_count = ((j - 1) * esteps_per_bright_gen) % esteps_per_period + (i - 1) * esteps_per_period
-- - If the interval class is the bright generator, there are extensions before and after and the sizes are perfect (large) and diminished (small).
-- - If the interval class is the dark generator, there are extensions before and after and the sizes are augmented (large) and perfect (small).
-- Find the cent value, rounded
local mosdegree_names = {}
local cent_value = utils._round_dec(et.cents(et_for_mos, estep_count), round)
local mosdegree_abbrevs = {}
for i = 1, mossteps_per_equave + 1 do
-- Add the row
-- For calculating mossteps
local row = { estep_count, cent_value }
local mossteps = i - 1
table.insert(rows, row)
end
-- For bright and dark gens
-- Calculate the stop value for the for loop as being 1 or 2, depending
local is_nL_ns = input_mos.nL == input_mos.ns
-- on whether this is the last period or not
local is_bright_gen = mossteps % mossteps_per_period == mossteps_per_bright_gen and not is_nL_ns
local stop_value = 1
local is_dark_gen = mossteps % mossteps_per_period == mossteps_per_dark_gen and not is_nL_ns
if i ~= periods_per_equave then
stop_value = stop_value + 1
end
if mossteps % mossteps_per_period == 0 then
-- Add step/cent values for ascending chain
-- For perfect intervals
-- The descending chain differs from the ascending chain:
-- Operation for pre-alterations (diminshed degrees)
-- - The descending chain should follow after the ascending chain.
if number_of_alterations > 0 and mossteps ~= 0 then
-- - The descending chain's entries should be added backwards and skip
for j = number_of_alterations, 1, -1 do
--   the root.
-- Diminished degree is formatted as "Diminished degree"; more than 1 augmentation is "2× Diminished", "3× Diminished", and so on
-- - This way, if the mos is multi-period, the root of the next period's
local dim_degree = ""
--  ascending chain (which is the same as the current period's descend-
if j == 1 then dim_degree = string.format("Diminished %d-%sstep", mossteps, mos_prefix)
--   ing chain) won't be added twice.
else dim_degree = string.format("%d× Diminished %d-%sstep", j, mossteps, mos_prefix)
-- - If the period is the last period, add the root as the equave.
end
for j = des_chain_length, stop_value, -1 do
-- Format abbreviation as repetitions of the letter "d", followed by the mosdegree
local dim_abbrev = string.rep("d", j) .. string.format("%dmd", mossteps)
-- Insert
table.insert(mosdegree_names, dim_degree)
table.insert(mosdegree_abbrevs, dim_abbrev)
end
end
-- Calculate the main degree name and abbreviation
-- Find the estep count
local degree_name = string.format("Perfect %d-%sstep", mossteps, mos_prefix)
local estep_count = ((j - 1) * esteps_per_dark_gen) % esteps_per_period + (i - 1) * esteps_per_period
local abbrev_name = string.format("P%dmd", mossteps)
-- Main operation
-- Find the cent value
table.insert(mosdegree_names, degree_name)
local cent_value = utils._round_dec(et.cents(et_for_mos, estep_count), round)
table.insert(mosdegree_abbrevs, abbrev_name)
-- Operation for post-alterations (augmented degrees)
-- If j is ever 1, then the cent and step values are for the equave
if number_of_alterations > 0 and mossteps ~= mossteps_per_equave then
-- This only happens if the current period is the last period
for j = 1, number_of_alterations do
if j == 1 then
-- Augmented degree is formatted as "Augmented degree"; more than 1 augmentation is "2× Augmented", "3× Augmented", and so on
cent_value = utils._round_dec(rat.cents(input_mos.equave), round)
local aug_degree = ""
estep_count = steps_in_et
if j == 1 then aug_degree = string.format("Augmented %d-%sstep", mossteps, mos_prefix)
else aug_degree = string.format("%d× Augmented %d-%sstep", j, mossteps, mos_prefix)
end
-- Format abbreviation as repetitions of the letter "A", followed by the mosdegree
local aug_abbrev = string.rep("A", j) .. string.format("%dmd", mossteps)
-- Insert
table.insert(mosdegree_names, aug_degree)
table.insert(mosdegree_abbrevs, aug_abbrev)
end
end
else
-- For intervals with two sizes
-- Operation for pre-alterations (diminshed degrees)
if number_of_alterations > 0 and mossteps ~= 0 then
for j = number_of_alterations, 1, -1 do
-- The number of diminishings depends on whether the interval class is the bright gen; if so,
-- then one interval will already be diminished so intervals below that already start at 2xdim.
local dim_amount = 0
if is_bright_gen then dim_amount = 1 + j
else dim_amount = j
end
-- Diminished degree is formatted as "Diminished degree"; more than 1 augmentation is "2× Diminished", "3× Diminished", and so on
local dim_degree = ""
if dim_amount == 1 then dim_degree = string.format("Diminished %d-%sstep", mossteps, mos_prefix)
else dim_degree = string.format("%d× Diminished %d-%sstep", dim_amount, mossteps, mos_prefix)
end
-- Format abbreviation as repetitions of the letter "d", followed by the mosdegree
local dim_abbrev = string.rep("d", dim_amount) .. string.format("%dmd", mossteps)
-- Insert
table.insert(mosdegree_names, dim_degree)
table.insert(mosdegree_abbrevs, dim_abbrev)
end
end
-- Calculate the small and large names and abbreviations
local small_degree_label = "Minor"
local large_degree_label = "Major"
local small_degree_abbrev = "m"
local large_degree_abbrev = "M"
if is_bright_gen then
-- Bright gen: diminished (small) and perfect (large)
small_degree_label = "Diminished"
large_degree_label = "Perfect"
small_degree_abbrev = "d"
large_degree_abbrev = "P"
elseif is_dark_gen then
-- Dark gen: perfect (small) and augmentd (large)
small_degree_label = "Perfect"
large_degree_label = "Augmented"
small_degree_abbrev = "P"
large_degree_abbrev = "A"
end
end
-- Main operation
-- Add the row
local small_degree_name = string.format("%s %d-%sstep", small_degree_label, mossteps, mos_prefix)
local row = { estep_count, cent_value }
local large_degree_name = string.format("%s %d-%sstep", large_degree_label, mossteps, mos_prefix)
table.insert(rows, row)
local small_abbrev_name = string.format("%s%dmd", small_degree_abbrev, mossteps)
local large_abbrev_name = string.format("%s%dmd", large_degree_abbrev, mossteps)
table.insert(mosdegree_names, small_degree_name)
table.insert(mosdegree_names, large_degree_name)
table.insert(mosdegree_abbrevs, small_abbrev_name)
table.insert(mosdegree_abbrevs, large_abbrev_name)
-- Operation for post-alterations (augmented degrees)
if number_of_alterations > 0 and mossteps ~= mossteps_per_equave then
for j = 1, number_of_alterations do
-- The number of augmentings depends on whether the interval class is the dark gen; if so,
-- then one interval will already be augmented so intervals above that already start at 2xaug.
local aug_amount = 0
if is_dark_gen then aug_amount = 1 + j
else aug_amount = j
end
-- Augmented degree is formatted as "Augmented degree"; more than 1 augmentation is "2× Augmented", "3× Augmented", and so on
local aug_degree = ""
if aug_amount == 1 then aug_degree = string.format("Augmented %d-%sstep", mossteps, mos_prefix)
else aug_degree = string.format("%d× Augmented %d-%sstep", aug_amount, mossteps, mos_prefix)
end
-- Format abbreviation as repetitions of the letter "A", followed by the mosdegree
local aug_abbrev = string.rep("A", aug_amount) .. string.format("%dmd", mossteps)
-- Insert
table.insert(mosdegree_names, aug_degree)
table.insert(mosdegree_abbrevs, aug_abbrev)
end
end
end
end
end
end
return mosdegree_names, mosdegree_abbrevs
return rows
end
end


-- Separate function for testing; the main "frame" function will call this
-- Algorithm:
function p.mos_degrees(input_mos, step_ratios, mos_prefix, show_abbreviations, number_of_alterations, ji_ratios, udp, notation, show_notation)
-- Use the input mos, udp, and step ratio to find the genchains
-- Default params; all parameters are already parsed
-- Using the genchains and UDP, find the mos's intervals/degrees
local input_mos = input_mos or mos.new(5, 2)
-- Format the result as a table
local step_ratios = step_ratios or {{2,1}, {3,1}, {3,2}}
function p.mos_degrees_frame(frame)
local mos_prefix = mos_prefix or "mos"
-- Default parameters for input mos and step ratio (5L 2s and 2:1 step ratio)
local show_abbrevs = show_abbreviations == 1
local input_mos = mos.parse(frame.args['Scale Signature']) or mos.new(2, 5, 2)
local number_of_alterations = number_of_alterations or 1
local ji_ratios = ji_ratios or {["P0md"]="1/1"}
local udp = udp or {5,1}
local notation = notation or mosnot.parse_notation("CDEFGAB; #; b")
local show_notation = show_notation == 1
-- Get the scale sig
-- Step ratios
local scale_sig = mos.as_string(input_mos)
-- Up to three step ratios can be entered; the default is only 2/1
-- Had to use parse function to make sure the default works
local step_ratios = p.parse_step_ratios(frame.args['Step Ratio']) or p.parse_step_ratios("2/1")
-- Get the brightest and darkest modes for the mos
-- Get the number of mossteps per period and equave
local brightest_mode = mos.brightest_mode(input_mos)
local mossteps_per_equave = input_mos.nL + input_mos.ns
local darkest_mode = string.reverse(brightest_mode)
-- Get the number of mossteps per period and equave, and periods per equave
local mossteps_per_equave = (input_mos.nL + input_mos.ns)
local periods_per_equave = utils._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_per_equave
local mossteps_per_period = mossteps_per_equave / periods_per_equave
-- Get the step counts for the bright and dark generators
-- If certain params were left blank and the scalesig is 5L 2s, the default
local bright_gen = mos.bright_gen(input_mos)
-- params will be for standard notation
local steps_per_bright_gen = bright_gen['L'] + bright_gen['s']
local scale_sig = mos.as_string(input_mos)
local steps_per_dark_gen = mossteps_per_period - steps_per_bright_gen
-- Get the step counts as a vector (or associative array, rather)
-- The default UDP corresponds to the middle mode. For mosses with an even
local input_mos_step_vector = {['L'] = input_mos.nL, ['s'] = input_mos.ns}
-- 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 = mosnot.parse_udp(frame.args['UDP']) or udp_default
-- What's the equave in cents?
-- Get genchain extend value
local equave_in_cents = rat.cents(input_mos.equave)
-- There are two genchain extend values for ascending and descending chains
 
-- The default value for both is the number of large steps per period, so
-- How many decimal places to round to? (hardcoded)
-- this value is per genchain per period.
local round = 1
local genchain_extend_default = input_mos.nL / periods_per_equave
 
local genchain_extend = p.parse_genchain_extend(frame.args['Genchain Extend'])
-- Precalculate the ets for each step ratio
local genchain_extend_up = genchain_extend[1] or genchain_extend_default
-- Each et is used to calculate a scale degree's cent value
local genchain_extend_dn = genchain_extend[2] or genchain_extend_default
local ets_for_mos = {}
for i = 1, #step_ratios do
local etsteps = p.interval_to_etsteps(input_mos_step_vector, step_ratios[i])
local et_for_mos = et.new(etsteps, input_mos.equave)
table.insert(ets_for_mos, et_for_mos)
end
-- Precalculate degree names, degree abbreviations, and mosstep vectors
-- Should a note names column be added?
local degree_names, degree_abbrevs = p.get_mosdegree_names_and_abbrevs(input_mos, mos_prefix, number_of_alterations)
local add_note_names = frame.args['Notation'] ~= "NONE"
local mosstep_vectors = p.get_mosstep_vectors(input_mos, number_of_alterations)
-- Precalculate default comments for JI ratios; there's only two entries here
-- Get notation: naturals (or nominals), sharp symbol, and flat symbol
local default_ji_comments = {}
local notation_default = { ['Naturals'] = string.sub("JKLMNOPQRSTUVWXYZ", 1, mossteps_per_equave), ['Sharp'] = "&", ['Flat'] = "@" }
default_ji_comments["P0md"] = "1/1 (exact)"
if scale_sig == "5L 2s" then
default_ji_comments[string.format("P%dmd", mossteps_per_equave)] = string.format("%s (exact)", rat.as_ratio(input_mos.equave))
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 sharp_symbol = notation['Sharp']
local flat_symbol = notation['Flat']
-- Then, using the UDP, get the notation
-- Get notational options
-- The default notation is either standard notation (for 5L 2s) or diamond-mos (most other mosses)
local mos_prefix = "mos" -- TODO: add prefix lookup
-- If notation is passed in, use that instead
if frame.args['MOS Prefix'] == "NONE" then
-- If no notation is passed in, notation will not be displayed
mos_prefix = ""
local note_names = {}
elseif string.len(frame.args['MOS Prefix']) > 0 then
local root_note = ""
mos_prefix = frame.args['MOS Prefix']
if show_notation then
note_names = p.get_note_names(input_mos, udp, notation['Naturals'], notation["Sharp"], notation["Flat"], number_of_alterations)
--root_note = string.sub(notation['Naturals'], 1, 1)
end
end
-- Create the table, starting with the headers
-- Override values for testing
local result = '{| class="wikitable sortable"\n'
--[[
local input_mos = mos.new(5, 2, 2)
local step_ratios = {{ 2, 1 }, {3, 1}, {3, 2}}
local udp = { 5, 1 }
local note_symbols = "CDEFGAB"
local sharp_symbol = "#"
local flat_symbol = "b"
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
local scale_sig = mos.as_string(inpnut_mos)
]]--
 
-- Calculate the chain lengths
local asc_chain_length = udp[1] / periods_per_equave + 1 + genchain_extend_up
local des_chain_length = udp[2] / periods_per_equave + 1 + genchain_extend_dn
 
-- Get the degrees and note names
local degrees    = p.preprocess_degrees  (input_mos, asc_chain_length, des_chain_length, mos_prefix)
local note_names = p.preprocess_note_names(input_mos, udp, note_symbols, sharp_symbol, flat_symbol, asc_chain_length, des_chain_length)
-- First row
-- Get the step and cent values
result = result .. string.format("|+ Scale degree of %s\n", scale_sig)
-- Do this for each step ratio
result = result .. '! rowspan="2" class="unsortable" | Scale degree\n'
local steps_and_cents = {}
for i = 1, #step_ratios do
local cells = p.preprocess_steps_and_cents(input_mos, step_ratios[i], asc_chain_length, des_chain_length)
table.insert(steps_and_cents, cells)
end
-- Add column for abbreviations
-- Pre-calculate the ets and step counts for each step ratio
-- Abbreviations do not use a mos-prefix or mos-name
local steps_in_ets = {}
if show_abbrevs then
local ets_for_mos = {}
result = result .. '! rowspan="2" class="unsortable" | Abbrev.\n'
for i = 1, #step_ratios do
local steps_in_ets_i = input_mos.nL * step_ratios[i][1] + input_mos.ns * step_ratios[i][2]
local ets_for_mos_i = et.new(steps_in_ets_i, input_mos.equave)
table.insert(steps_in_ets, steps_in_ets_i)
table.insert(ets_for_mos, ets_for_mos_i)
end
end
-- Format the output as a table, starting with the header row
local result = '{| class="wikitable sortable"\n'
result = result .. '! rowspan="2" |Scale degree\n'
-- Add column for note names
if add_note_names then
if show_notation then
result = result .. '! rowspan="2" |On ' .. string.sub(note_symbols, 1, 1) .. "\n"
result = result .. string.format('! rowspan="2" class="unsortable" | On %s\n', root_note)
end
end
-- Add column headers for up to 5 different step ratios
-- Add this once for every step ratio to be represented
for i = 1, #step_ratios do
for i = 1, #step_ratios do
-- Step ratio names, for reference
-- Add names for specific step ratios
local tamnams_step_ratios = {
local step_ratio_names = {
['1:1'] = "Equalized",
['1:1'] = "Equalized",
['4:3'] = "Supersoft",
['4:3'] = "Supersoft",
Line 559: Line 424:
['1:0'] = "Collapsed",
['1:0'] = "Collapsed",
}
}
-- Get name for step ratio
local step_ratio_simplified = mosnot.simplify_step_ratio(step_ratios[i])
local step_ratio_simplified = mosnot.simplify_step_ratio(step_ratios[i])
local step_ratio_key = step_ratio_simplified[1] .. ":" .. step_ratio_simplified[2]
local step_ratio_string = step_ratio_simplified[1] .. ":" .. step_ratio_simplified[2]
local step_ratio_name = tamnams_step_ratios[step_ratio_key]
local step_ratio_name = step_ratio_names[step_ratio_string]
-- Calculate the et for the mos with a given step ratio; this is needed to produce
-- Add column header text
-- the name for the et/edo
local et_for_mos = et.new(ets_for_mos[i].size, input_mos.equave)
local et_as_string = et.as_string(et_for_mos)
-- Add the step ratio name if there is one
if step_ratio_name == nil then
if step_ratio_name == nil then
result = result .. '! colspan="2" |' .. et_as_string .. " (L:s = " .. step_ratio_key .. ")\n"
result = result .. '! colspan="2" |' .. et.as_string(ets_for_mos[i]) .. " (L:s = " .. step_ratios[i][1] .. ":" .. step_ratios[i][2] .. ")\n"
else
else
result = result .. '! colspan="2" |' .. et_as_string .. " (" .. step_ratio_name .. ", L:s = " .. step_ratio_key .. ")\n"
result = result .. '! colspan="2" |' .. step_ratio_name .. " " .. scale_sig .. "\n"
result = result .. '(' .. et.as_string(ets_for_mos[i]) .. ", L:s = " .. step_ratios[i][1] .. ":" .. step_ratios[i][2] .. ")\n"
end
end
end
end
-- Add JI ratio column header
-- Next row
result = result .. '! Rowspan="2" class="unsortable" | Approx. JI Ratios\n'
result = result .. "|-\n"
-- Second row
-- Add this once for every step ratio to be represented
result = result .. "|-\n"
result = result .. string.rep("! Steps\n! Cents\n", #step_ratios)
-- Add headers for the steps and cents up to 5 times
for i = 1, #step_ratios do
result = result .. '! Steps\n'
result = result .. '! Cents\n'
end
-- Add in successive rows, containing the degree name, abbreviation (if applicable),
-- Add each row, containing a degree, note name, step count, and cent value
-- note names (if applicable), step size (in steps and cents), and JI ratio
for i = 1, #degrees do
for i = 1, #degree_names do
-- Start new row
-- Is the row for a nominal? (Nominals have no accidentals and are
result = result .. "|-\n"
-- therefore strings of size 1)
-- Nominals don't depend on step ratio
local is_nominal = string.len(note_names[i]) == 1
-- Add degree name
-- Add new row, with coloring
-- Make the text bold if the interval is a perfect interval
if not is_nominal then
local degree_name = degree_names[i]
result = result .. '|- bgcolor="#eaeaff"\n'
if string.find(degree_name, "Perfect") then
result = result .. string.format("| '''%s'''\n", degree_names[i])
else
else
result = result .. string.format("| %s\n", degree_names[i])
result = result .. "|-\n"
end
end
-- Add abbrev if allowed
-- Is the row for a period?
local degree_abbrev = degree_abbrevs[i]
-- Check whether it's a period only for the using the cent value for the
if show_abbrevs then
-- first step ratio, since it'll be a period for the other ratios
result = result .. string.format("| %s\n", degree_abbrev)
local cent_value = steps_and_cents[1][i][1] -- First set of cells, current row, first column is current cent value
end
local is_period = cent_value % (steps_in_ets[1] / periods_per_equave) == 0
-- Add note names if allowed
-- Add cell for degree
-- Use the degree_abbrev as the key when accessing key-value pairs
-- Make any nominals that correspond to the period bold
if show_notation then
if is_period and is_nominal then
result = result .. string.format("| %s\n", note_names[degree_abbrev])
result = result .. "| '''" .. degrees[i] .. "'''\n"
else
result = result .. "| " .. degrees[i] .. "\n"
end
end
-- Add mossteps and cent values
-- Add cell for note name, if allowed
-- Rounding is hardcoded to one decimal place
if add_note_names then
local round = 1
result = result .. "| " .. note_names[i] .. "\n"
for j = 1, #ets_for_mos do
local etsteps = mosstep_vectors[i]['L'] * step_ratios[j][1] + mosstep_vectors[i]['s'] * step_ratios[j][2]
local cents = utils._round_dec(et.cents(ets_for_mos[j], etsteps), round)
result = result .. string.format("| %s\n", etsteps)
result = result .. string.format("| %s\n", cents)
end
end
-- Add JI ratios if any
-- Add cells for step size
local ji_comment_entry = ""
for j = 1, #step_ratios do
local default_ji_comment = default_ji_comments[degree_abbrev]
result = result .. "| " .. steps_and_cents[j][i][1] .. "\n" -- Steps
local entered_ji_comment = ji_ratios[degree_abbrev]
result = result .. "| " .. steps_and_cents[j][i][2] .. "¢\n" -- Cents
if default_ji_comment == nil and entered_ji_comment == nil then
-- No comments
result = result .. "|\n"
elseif default_ji_comment ~= nil and entered_ji_comment == nil then
-- Default comments but no entered comments
result = result .. string.format("| %s\n", default_ji_comment)
elseif default_ji_comment == nil and entered_ji_comment ~= nil then
-- Entered comments but no default comments
result = result .. string.format("| %s\n", entered_ji_comment)
else
-- Both comments present; default comments take precedence
result = result .. string.format("| %s, %s\n", default_ji_comment, entered_ji_comment)
end
end
end
end
-- End of table
result = result .. "|}"
result = result .. "|}"
return result
return result
end
end
-- This function is to be called by a template, with parameters
function p.mos_degrees_frame(frame)
-- Default param for input mos is 5L 2s
local input_mos = mos.parse(frame.args['Scale Signature']) or mos.new(2, 5, 2)
-- Get the scale sig; for calculating the mos prefix
local scale_sig = mos.as_string(input_mos)
-- Get the step ratio
local step_ratios = p.parse_step_ratio(frame.args['Step Ratio']) or p.parse_step_ratio("2/1")
-- Default param for mos prefix
-- If "NONE" is given, no prefix will be used
-- If left blank, try to find the appropriate mos prefix, or else defualt to "mos"
-- If not left blank, use the prefix passed in instead
local mos_prefix = "mos"
if frame.args['MOS Prefix'] == "NONE" then
mos_prefix = ""
elseif string.len(frame.args['MOS Prefix']) == 0 then
mos_prefix_lookup = p.get_mos_prefix(scale_sig)
if string.len(mos_prefix_lookup) ~= 0 then
mos_prefix = mos_prefix_lookup
end
else
mos_prefix = frame.args['MOS Prefix']
end
-- Get whether to display abbreviations
local show_abbreviations = 0
if frame.args['Show Abbreviations'] == "1" or frame.args['Show Abbreviations'] == 1 then
show_abbreviations = 1
end
-- Get the number of alterations
local number_of_alterations = 0
if string.len(frame.args['Number of Alterations']) ~= 0 then
number_of_alterations = tonumber(frame.args['Number of Alterations'])
end
-- Get JI ratios
local ji_ratios_parsed = {}
if #frame.args['JI Ratios'] > 0 then
-- If the comments can't be parsed, default to an empty table
ji_ratios_parsed = p.parse_kv_pairs(frame.args['JI Ratios']) or {}
end
-- Get UDP
-- If no UDP is found, a default will be calculated as the middle mode, or the
-- brighter of two middle modes (as with an even number of modes in a mos)
local udp_parsed = { 5, 1 }
if #frame.args["UDP"] > 0 then
udp_parsed = mosnot.parse_udp(frame.args["UDP"])
else
if scale_sig ~= "5L 2s" then
udp_default = { periods_per_equave * math.ceil((mossteps_per_period - 1)/ 2), periods_per_equave * math.floor((mossteps_per_period - 1) / 2) }
end
end
-- Get notation
-- This also determines whether to show notation
-- Typing in "Default" is a shortcut to default notation, wherein standard notation (for 5L 2s) or diamond-mos (for other mosses) is used
local notation_parsed = {}
local show_notation = 0
if #frame.args["Notation"] > 0 then
if frame.args["Notation"] == "Default" and scale_sig == "5L 2s" then
notation_parsed = { ['Naturals'] = "CDEFGAB", ['Sharp'] = "#", ['Flat'] = "b" }
elseif frame.args["Notation"] == "Default" and scale_sig ~= "5L 2s" then
local default_nominals = "JKLMNOPQRSTUVWXYZ"
local note_count = input_mos.nL + input_mos.ns
notation_parsed = { ['Naturals'] = string.sub(default_nominals, 1, note_count), ['Sharp'] = "&", ['Flat'] = "@" }
else
notation_parsed = mosnot.parse_notation(frame.args["Notation"])
end
show_notation = 1
end
result = p.mos_degrees(input_mos, step_ratios, mos_prefix, show_abbreviations, number_of_alterations, ji_ratios_parsed, udp_parsed, notation_parsed, show_notation)
return result
end
return p
return p

Revision as of 08:16, 15 October 2023

Module documentation[view] [edit] [history] [purge]
This module should not be invoked directly; use its corresponding template instead: Template:MOS degrees.
Module:MOS degrees is deprecated and has been replaced by Module:MOS tunings. Further use of this module is not advised. This module is kept for historical purposes and should not be deleted.
Introspection summary for Module:MOS degrees 
Functions provided (6)
Line Function Params
16 parse_step_ratios (unparsed)
42 parse_genchain_extend (unparsed)
58 preprocess_note_names (input_mos, udp, note_symbols, sharp_symbol, flat_symbol, asc_chain_length, des_chain_length)
135 preprocess_degrees (input_mos, asc_chain_length, des_chain_length, prefix, notation)
216 preprocess_steps_and_cents (input_mos, step_ratio, asc_chain_length, des_chain_length)
304 mos_degrees_frame (invokable) (frame)
Lua modules required (5)
Variable Module Functions used
et Module:ET new
cents
as_string
mos Module:MOS bright_gen
parse
new
as_string
mosnot Module:MOS notation parse_step_ratio
mos_nomacc_chain
mosstep_and_chroma_to_note_name
mos_degree_chain
mosstep_and_quality_to_degree
parse_udp
parse_notation
simplify_step_ratio
rat Module:Rational eq
cents
utils Module:Utils _gcd
_round_dec

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


local mos = require('Module:MOS')
local et = require('Module:ET')
local rat = require('Module:Rational')
local utils = require('Module:Utils')
local mosnot = require('Module:MOS notation')		-- Contains the important functions
local p = {}

-- TODO: For preprocessing, add bools for whether a note is a nominal, the
-- unison, the period, or the equave, so the main function doesn't have to
-- calculate them

-- Helper function
-- Parses up to 5 step ratios entered as text in a semicolon-delimited string,
-- where each step ratio is separated with a slash
-- EG, "2/1; 3/1; 3/2" becomes {{2, 1}, {3, 1}, {3, 2}}
function p.parse_step_ratios(unparsed)
	local parsed = {}
	for entry in string.gmatch(unparsed, '([^;]+)') do
		local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
		table.insert(parsed, trimmed)		-- Add to array
	end
	
	-- Parse up to 5 step ratios (hardcoded)
	local max_ratios = 5
	local loop_limit = math.min(max_ratios, #parsed)
	local step_ratios = {}
	for i = 1, loop_limit do
		local ratio = mosnot.parse_step_ratio(parsed[i])
		table.insert(step_ratios, ratio)
	end
	-- Return nil if the size is zero (meaning nothing was entered or parsable)
	if loop_limit == 0 then
		return nil
	else
		return step_ratios
	end
end

-- Helper function
-- Parses genchain extend values, where the first value is for the ascending
-- chain and the second value is for the descending chain
function p.parse_genchain_extend(unparsed)
	local parsed = {}
	for entry in string.gmatch(unparsed, '([^,]+)') do
		local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
		table.insert(parsed, trimmed)		-- Add to array
	end
	
	if #parsed == 1 then
		return { tonumber(parsed[1]), tonumber(parsed[1]) }
	else
		return { tonumber(parsed[1]), tonumber(parsed[2]) }
	end
end

-- Helper function; creates the column for the note name
-- Decoupled degree and note name functions allow note names being omitted.
function p.preprocess_note_names(input_mos, udp, note_symbols, sharp_symbol, flat_symbol, asc_chain_length, des_chain_length)
	-- Test parameters
	--[[
	local input_mos = input_mos or mos.new(5, 2, 2)
	local udp = udp or { 5, 1 }
	local note_symbols = note_symbols or "CDEFGAB"
	local sharp_symbol = sharp_symbol or "#"
	local flat_symbol = flat_symbol or "b"
	local asc_chain_length = input_mos.nL * 2 + input_mos.ns
	local des_chain_length = input_mos.nL * 2 + input_mos.ns
	]]--
	
	-- 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
	
	-- How long are the initial genchain lengths? (These correspond to the UDP)
	local gens_up_per_period = udp[1] / periods_per_equave
	local gens_dn_per_period = udp[2] / periods_per_equave
	
	-- Get the genchains
	local asc_genchain = mosnot.mos_nomacc_chain(input_mos, gens_up_per_period, asc_chain_length, true)
	local des_genchain = mosnot.mos_nomacc_chain(input_mos, gens_dn_per_period, des_chain_length, false)
	
	-- Calculate the entries for each cell
	local column = {}
	for i = 1, periods_per_equave do
		-- Add degrees from ascending chain
		for j = 1, asc_chain_length do
			local mossteps = asc_genchain[i][j]['mossteps']
			local chromas  = asc_genchain[i][j]['chromas']
			
			-- Find the note name
			local note_symbol = string.sub(note_symbols, mossteps + 1, mossteps + 1)
			local note_name = mosnot.mosstep_and_chroma_to_note_name(mossteps, chromas, note_symbol, sharp_symbol)

			table.insert(column, note_name)
		end
		
		-- Calculate the stop value for the for loop as being 1 or 2, depending
		-- on whether this is the last period or not
		local stop_value = 1
		if i ~= periods_per_equave then
			stop_value = stop_value + 1
		end
		
		-- Add degrees from descending chain
		-- The descending chain differs from the ascending chain:
		-- - The descending chain should follow after the ascending chain.
		-- - The descending chain's entries should be added backwards and skip
		--   the root.
		-- - This way, if the mos is multi-period, the root of the next period's
		--   ascending chain (which is the same as the current period's descend-
		--   ing chain) won't be added twice.
		-- - If the period is the last period, add the root as the equave.
		for j = des_chain_length, stop_value, -1 do
			local mossteps = des_genchain[i][j]['mossteps']
			local chromas  = des_genchain[i][j]['chromas']
			
			-- Find the note name
			-- If the mosstep is the root of the period, add a period to it
			local note_symbol = string.sub(note_symbols, mossteps + 1, mossteps + 1)
			if mossteps % mossteps_per_period == 0 then
				mossteps = mossteps + mossteps_per_period
			end
			local note_name = mosnot.mosstep_and_chroma_to_note_name(mossteps, chromas, note_symbol, flat_symbol)
			
			table.insert(column, note_name)
		end
	end
	
	return column
end

-- Helper function; creates the column for the degree name
-- Decoupled degree and note name functions allow note names being omitted.
function p.preprocess_degrees(input_mos, asc_chain_length, des_chain_length, prefix, notation)
	-- Test parameters
	--[[
	local input_mos = input_mos or mos.new(5, 2, 2)
	local asc_chain_length = input_mos.nL * 2 + input_mos.ns
	local des_chain_length = input_mos.nL * 2 + input_mos.ns
	local prefix = "mos"
	local notation = "mosstep"
	]]--
	
	-- Get the number of mossteps per period and equave
	local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
	
	-- Get the degrees
	local asc_degrees = mosnot.mos_degree_chain(input_mos, asc_chain_length, true)
	local des_degrees = mosnot.mos_degree_chain(input_mos, des_chain_length, false)
	
	-- Calculate the entries for each cell
	local column = {}
	for i = 1, periods_per_equave do
		-- Add degrees from ascending chain
		for j = 1, asc_chain_length do
			local mossteps = asc_degrees[i][j]['mossteps']
			local quality  = asc_degrees[i][j]['quality']
			
			-- Find the degree name
			-- If the degree is the perfect 0-mosdegree, append "unison"
			local degree_name = mosnot.mosstep_and_quality_to_degree(mossteps, quality, prefix, notation)
			if mossteps == 0 and quality == 0 then
				degree_name = degree_name .. " (unison)"
			end
			
			table.insert(column, degree_name)
		end
		
		-- Calculate the stop value for the for loop as being 1 or 2, depending
		-- on whether this is the last period or not
		local stop_value = 1
		if i ~= periods_per_equave then
			stop_value = stop_value + 1
		end
		
		-- Add degrees from descending chain
		-- The descending chain differs from the ascending chain:
		-- - The descending chain should follow after the ascending chain.
		-- - The descending chain's entries should be added backwards and skip
		--   the root.
		-- - This way, if the mos is multi-period, the root of the next period's
		--   ascending chain (which is the same as the current period's descend-
		--   ing chain) won't be added twice.
		-- - If the period is the last period, add the root as the equave.
		for j = des_chain_length, stop_value, -1 do
			local mossteps = des_degrees[i][j]['mossteps']
			local quality  = des_degrees[i][j]['quality']
			
			-- Find the degree name
			-- If the degree corresponds to the equave, say it's the equave
			local degree_name = mosnot.mosstep_and_quality_to_degree(mossteps, quality, prefix, notation)
			
			-- If j is ever 1, then the mosdegree is the equave
			-- This only happens if the current period is the last period
			-- If the equave is 2/1, that's the octave
			if j == 1 then
				if rat.eq(input_mos.equave, 2) then
					degree_name = degree_name .. " (octave)"
				else
					degree_name = degree_name .. " (equave)"
				end
			end
			
			table.insert(column, degree_name)
		end
	end
	
	return column
end

-- Helper function
-- Creates the columns for the step and cent sizes
-- Separating this into its own function makes it easy to add colums for
-- different step ratios in the same table.
function p.preprocess_steps_and_cents(input_mos, step_ratio, asc_chain_length, des_chain_length)
	-- Test parameters
	--[[
	local input_mos = input_mos or mos.new(5, 2, 2)
	local step_ratio = { 2, 1 }
	local asc_chain_length = input_mos.nL * 2 + input_mos.ns
	local des_chain_length = input_mos.nL * 2 + input_mos.ns
	]]--
	
	-- 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
	
	-- What et is produced given the step ratio and equave?
	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)
	
	-- How many esteps per period? Per bright/dark gen?
	local esteps_per_period = steps_in_et / periods_per_equave
	local bright_gen = mos.bright_gen(input_mos)
	local esteps_per_bright_gen = bright_gen['L'] * step_ratio[1] + bright_gen['s'] * step_ratio[2]
	local esteps_per_dark_gen = esteps_per_period - esteps_per_bright_gen
	
	-- How many decimal places to round to?
	local round = 1
	
	-- Calculate the entries for each row
	local rows = {}
	for i = 1, periods_per_equave do
		-- Add step/cent values for ascending chain
		for j = 1, asc_chain_length do
			
			-- Find the estep count
			local estep_count = ((j - 1) * esteps_per_bright_gen) % esteps_per_period + (i - 1) * esteps_per_period
			
			-- Find the cent value, rounded
			local cent_value = utils._round_dec(et.cents(et_for_mos, estep_count), round)
			
			-- Add the row
			local row = { estep_count, cent_value }
			table.insert(rows, row)
		end
		
		-- Calculate the stop value for the for loop as being 1 or 2, depending
		-- on whether this is the last period or not
		local stop_value = 1
		if i ~= periods_per_equave then
			stop_value = stop_value + 1
		end
		
		-- Add step/cent values for ascending chain
		-- The descending chain differs from the ascending chain:
		-- - The descending chain should follow after the ascending chain.
		-- - The descending chain's entries should be added backwards and skip
		--   the root.
		-- - This way, if the mos is multi-period, the root of the next period's
		--   ascending chain (which is the same as the current period's descend-
		--   ing chain) won't be added twice.
		-- - If the period is the last period, add the root as the equave.
		for j = des_chain_length, stop_value, -1 do
			
			-- Find the estep count
			local estep_count = ((j - 1) * esteps_per_dark_gen) % esteps_per_period + (i - 1) * esteps_per_period
			
			-- Find the cent value
			local cent_value = utils._round_dec(et.cents(et_for_mos, estep_count), round)
			
			-- If j is ever 1, then the cent and step values are for the equave
			-- This only happens if the current period is the last period
			if j == 1 then
				cent_value = utils._round_dec(rat.cents(input_mos.equave), round)
				estep_count = steps_in_et
			end
			
			-- Add the row
			local row = { estep_count, cent_value }
			table.insert(rows, row)
		end
	end
	
	return rows
end

-- Algorithm:
-- Use the input mos, udp, and step ratio to find the genchains
-- Using the genchains and UDP, find the mos's intervals/degrees
-- Format the result as a table
function p.mos_degrees_frame(frame)
	-- Default parameters for input mos and step ratio (5L 2s and 2:1 step ratio)
	local input_mos = mos.parse(frame.args['Scale Signature']) or mos.new(2, 5, 2)
	
	-- Step ratios
	-- Up to three step ratios can be entered; the default is only 2/1
	-- Had to use parse function to make sure the default works
	local step_ratios = p.parse_step_ratios(frame.args['Step Ratio']) or p.parse_step_ratios("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
	
	-- 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_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 = mosnot.parse_udp(frame.args['UDP']) or udp_default
	
	-- Get genchain extend value
	-- There are two genchain extend values for ascending and descending chains
	-- The default value for both is the number of large steps per period, so
	-- this value is per genchain per period.
	local genchain_extend_default = input_mos.nL / periods_per_equave
	local genchain_extend = p.parse_genchain_extend(frame.args['Genchain Extend'])
	local genchain_extend_up = genchain_extend[1] or genchain_extend_default
	local genchain_extend_dn = genchain_extend[2] or genchain_extend_default
	
	-- Should a note names column be added?
	local add_note_names = frame.args['Notation'] ~= "NONE"
	
	-- 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 sharp_symbol = notation['Sharp']
	local flat_symbol = notation['Flat']
	
	-- Get notational options
	local mos_prefix = "mos"		-- TODO: add prefix lookup
	if frame.args['MOS Prefix'] == "NONE" then
		mos_prefix = ""
	elseif string.len(frame.args['MOS Prefix']) > 0 then
		mos_prefix = frame.args['MOS Prefix']
	end
	
	-- Override values for testing
	--[[
	local input_mos = mos.new(5, 2, 2)
	local step_ratios = {{ 2, 1 }, {3, 1}, {3, 2}}
	local udp = { 5, 1 }
	local note_symbols = "CDEFGAB"
	local sharp_symbol = "#"
	local flat_symbol = "b"
	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
	local scale_sig = mos.as_string(inpnut_mos)
	]]--

	-- Calculate the chain lengths
	local asc_chain_length = udp[1] / periods_per_equave + 1 + genchain_extend_up
	local des_chain_length = udp[2] / periods_per_equave + 1 + genchain_extend_dn

	-- Get the degrees and note names
	local degrees    = p.preprocess_degrees   (input_mos, asc_chain_length, des_chain_length, mos_prefix)
	local note_names = p.preprocess_note_names(input_mos, udp, note_symbols, sharp_symbol, flat_symbol, asc_chain_length, des_chain_length)
	
	-- Get the step and cent values
	-- Do this for each step ratio
	local steps_and_cents = {}
	for i = 1, #step_ratios do
		local cells = p.preprocess_steps_and_cents(input_mos, step_ratios[i], asc_chain_length, des_chain_length)
		table.insert(steps_and_cents, cells)
	end
	
	-- Pre-calculate the ets and step counts for each step ratio
	local steps_in_ets = {}
	local ets_for_mos = {}
	for i = 1, #step_ratios do
		local steps_in_ets_i = input_mos.nL * step_ratios[i][1] + input_mos.ns * step_ratios[i][2]
		local ets_for_mos_i = et.new(steps_in_ets_i, input_mos.equave)
		table.insert(steps_in_ets, steps_in_ets_i)
		table.insert(ets_for_mos, ets_for_mos_i)
	end

	-- Format the output as a table, starting with the header row
	local result = '{| class="wikitable sortable"\n'
	result = result .. '! rowspan="2" |Scale degree\n'
	
	if add_note_names then
		result = result .. '! rowspan="2" |On ' .. string.sub(note_symbols, 1, 1) .. "\n"
	end
	
	-- Add this once for every step ratio to be represented
	for i = 1, #step_ratios do
		-- Add names for specific step ratios
		local step_ratio_names = {
			['1:1'] = "Equalized",
			['4:3'] = "Supersoft",
			['3:2'] = "Soft",
			['5:3'] = "Semisoft",
			['2:1'] = "Basic",
			['5:2'] = "Semihard",
			['3:1'] = "Hard",
			['4:1'] = "Superhard",
			['1:0'] = "Collapsed",
		}
		local step_ratio_simplified = mosnot.simplify_step_ratio(step_ratios[i])
		local step_ratio_string = step_ratio_simplified[1] .. ":" .. step_ratio_simplified[2]
		local step_ratio_name = step_ratio_names[step_ratio_string]
		
		-- Add column header text
		if step_ratio_name == nil then
			result = result .. '! colspan="2" |' .. et.as_string(ets_for_mos[i]) .. " (L:s = " .. step_ratios[i][1] .. ":" .. step_ratios[i][2] .. ")\n"
		else
			result = result .. '! colspan="2" |' .. step_ratio_name .. " " .. scale_sig .. "\n"
			result = result .. '(' .. et.as_string(ets_for_mos[i]) .. ", L:s = " .. step_ratios[i][1] .. ":" .. step_ratios[i][2] .. ")\n"
		end
	end
	
	-- Next row
	result = result .. "|-\n"
	
	-- Add this once for every step ratio to be represented
	result = result .. string.rep("! Steps\n! Cents\n", #step_ratios)
	
	-- Add each row, containing a degree, note name, step count, and cent value
	for i = 1, #degrees do
		
		-- Is the row for a nominal? (Nominals have no accidentals and are
		-- therefore strings of size 1)
		-- Nominals don't depend on step ratio
		local is_nominal = string.len(note_names[i]) == 1
		
		-- Add new row, with coloring
		if not is_nominal then
			result = result .. '|- bgcolor="#eaeaff"\n'
		else
			result = result .. "|-\n"
		end
		
		-- Is the row for a period?
		-- Check whether it's a period only for the using the cent value for the
		-- first step ratio, since it'll be a period for the other ratios
		local cent_value = steps_and_cents[1][i][1]		-- First set of cells, current row, first column is current cent value
		local is_period = cent_value % (steps_in_ets[1] / periods_per_equave) == 0
		
		-- Add cell for degree
		-- Make any nominals that correspond to the period bold
		if is_period and is_nominal then
			result = result .. "| '''" .. degrees[i] .. "'''\n"
		else
			result = result .. "| " .. degrees[i] .. "\n"
		end
		
		-- Add cell for note name, if allowed
		if add_note_names then
			result = result .. "| " .. note_names[i] .. "\n"
		end
		
		-- Add cells for step size
		for j = 1, #step_ratios do
			result = result .. "| " .. steps_and_cents[j][i][1] .. "\n"		-- Steps
			result = result .. "| " .. steps_and_cents[j][i][2] .. \n"	-- Cents
		end
	end
	
	result = result .. "|}"
	
	return result
end
	
return p