Module:MOS mode degrees

From Xenharmonic Wiki
Jump to navigation Jump to search

Documentation for this module may be created at Module:MOS mode degrees/doc

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

-- Global variables for cell colors
-- Colors are as follows:
-- Large intervals are yellow, small intervals are blue, augmented intervals are dark yellow, and diminished intervals are dark blue
p.cell_color_none = "NONE"				-- For cells that don't have a color (default cell color applies)
p.cell_color_perfect_size = "NONE"		-- Only applies for periods, including the root and equave
p.cell_color_lg_altered_size = "BDD7EE"
p.cell_color_large_size      = "DDEBF7"
p.cell_color_small_size      = "FCE4D6"
p.cell_color_sm_altered_size = "F8CBAD"

-- TODO?: Move some functions to the mos notation module

-- 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
-- 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
		return "mos"
	else
		for entry in string.gmatch(unparsed, '([^-]+)') do
			local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
			table.insert(parsed, trimmed)		-- Add to array
		end
		return parsed[1]
	end
end

-- Helper function
-- Determines whether an item is in an array
function p.find_item_in_table(table, item)

	local item_found = false
	for i = 1, #table do
		if table[i] == item then
			item_found = true
			break
		end
	end
	return item_found
end

-- Calculate the mosstep vector from the mosstep pattern
-- Given a mos's step pattern and a quantity of mossteps, this takes an interval
-- (as a substring of the mosstep pattern starting from the root) and counts the number
-- of L's and s's in that substring. Allowed steps are:
-- - L - a single large step
-- - s - a single small step (can be capital S or lowercase s)
-- - c - a single chroma, defined as L-s; counting this adds one L and subtracts one s
--       Note that L-c=s and s+c=L.
-- - A - an augmented step, defined as L+c; counting this adds two L's and subtracts one s
-- - d - a diminished step, defined as s-c; counting this subtracts one L and adds two s's
-- Note that adding a chroma to an s makes it an L, and removing a chroma from an L
-- makes it an s.
-- Above-equave mossteps are supported. 
function p.convert_mosstep_pattern_to_mosstep_vector(mosstep_pattern, mossteps)
	local mossteps = mossteps or 7
	local mosstep_pattern = mosstep_pattern or "LLLsLLs"
	
	local large_step_count = 0
	local small_step_count = 0
	local step_count = #mosstep_pattern

	-- If the number of mossteps exceeds the mosstep pattern, divide that quantity
	-- by the number of steps and round up, then duplicate the pattern by that much.
	local number_of_repetitions = math.ceil(mossteps / step_count)
	local mosstep_pattern_duplicated = string.rep(mosstep_pattern, number_of_repetitions)
	
	-- Count the number of L's and s's in the string
	-- C's, A's, and d's are worth some number of L's and s's
	for i = 1, mossteps do
		local step = string.sub(mosstep_pattern_duplicated, i, i)
		if step == "L" then
			large_step_count = large_step_count + 1
		elseif step == "s" or step == "S" then
			small_step_count = small_step_count + 1
		elseif step == "c" then
			large_step_count = large_step_count + 1
			small_step_count = small_step_count - 1
		elseif step == "A" then
			large_step_count = large_step_count + 2
			small_step_count = small_step_count - 1
		elseif step == "d" then
			large_step_count = large_step_count - 1
			small_step_count = small_step_count + 2
		end
	end
	
	local mosstep_vector = { ['L'] = large_step_count, ['s'] = small_step_count }
	return mosstep_vector
end

-- Produce an encoded mosdegree from a mosstep vector
-- For an interval with two specific sizes, its large size is iL js and small size
-- is (i-1)L (j+1)s, for a difference of a single large step swapped with a single
-- small step. Alterations are denoted by adding or subtracting chromas, c, where a
-- chroma is L-s. An augmented step is L+c, denoted with a single A, so A=L+L-s.
-- Summing the number of L's and s's cancels out any negative step quantites for the
-- single A, so for any mosstep represented as a string of L's, s's, and A's, the sum of
-- the number of L's and s's produces the original interval (in mossteps). Similar
-- inductive reasoning applies with c's and d's, should a scale contain such steps.
-- To find how many alterations a mosstep vector had been applied to it:
-- - First, add or subtract chromas as needed until neither the L-count nor s-count
--   is negative, and record that number of chromas as k1.
-- - Then compare the step vector of that mosstep with the expected large or small size.
--   However many chromas are needed to add to reach the small size, or how many
--   chromas are need to remove to reach the small size, is the additional number
--   of chromas, k2, needed to reach the mos's large or small size. (Note that if
--   adding chromas, k2 is positive, but if removing chromas, k2 is negative.)
-- - Add k1 and k2. This is the number of alrerations the mosstep was from its large
--   or small size.
-- - Note that for mossteps with two specific sizes, there are effectively two "zero
--   points", one each for the large and small size, and which one to use depends
--   on whichever is closer. For mossteps with only one size, there is only one
--   zero point.
function p.calculate_mosstep_quality(input_mos, mosstep_vector)
	local input_mos = input_mos or mos.new(5, 2)
	local mosstep_vector = mosstep_vector or { ['L'] = 5, ['s'] = 2 }
	
	-- 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 mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- Get the number of mossteps in the bright gen and dark gen
	local bright_gen = mos.bright_gen(input_mos)
	local mossteps_per_bright_gen = bright_gen['L'] + bright_gen['s']
	local mossteps_per_dark_gen = mossteps_per_period - mossteps_per_bright_gen
	
	-- Get the number of mossteps as the sum of the number of L's and s's
	local mossteps = mosstep_vector['L'] + mosstep_vector['s']
	
	-- Get the brightest and darkest modes for the input mos
	local brightest_mode = mos.brightest_mode(input_mos)
	local darkest_mode = string.reverse(brightest_mode)
	
	-- Get the expected vector of the large mosstep
	local expected_large_mosstep_vector = p.convert_mosstep_pattern_to_mosstep_vector(brightest_mode, mossteps)
	
	-- Since the size difference between the large and small intervals is a single chroma,
	-- which is L-s, simply count the large step difference between the given mosstep vector
	-- and the expected large and small ones.
	local large_step_count = mosstep_vector['L']
	local number_of_chromas = large_step_count - expected_large_mosstep_vector['L']
	
	-- Determine what mosstep was passed in; is it a generator or period?
	local mos_is_nL_ns = input_mos.nL == input_mos.ns
	local mosstep_is_period = mossteps % mossteps_per_period == 0
	local mosstep_is_bright_gen = mossteps % mossteps_per_period == mossteps_per_bright_gen and not mos_is_nL_ns
	local mosstep_is_dark_gen = mossteps % mossteps_per_period == mossteps_per_dark_gen and not mos_is_nL_ns
	
	-- Rules for encoding are shown in the comments below.
	-- Encoding follows the rules as found in the module mos notation, where:
	-- -  3 = 2x augmented
	-- -  2 = 1x augmented
	-- -  1 = major
	-- -  0 = perfect (used for generators, roots, and periods)
	-- - -1 = minor
	-- - -2 = 1x diminished
	-- - -3 = 2x diminished
	-- The number_of_chromas found previously must be translated to this encoding.
	local encoded_quality = 0
	if mosstep_is_period then
		-- For periods:
		-- - If the number of chromas is 1 or more, that quality is augmented
		-- - If the nubmer of chromas is 0, that quality is perfect
		-- - If the number of chromas is -1 or less, that quality is diminished
		-- The encoded quality should always skip 1 and -1
		if number_of_chromas >= 1 then
			encoded_quality = number_of_chromas + 1
		elseif number_of_chromas == 0 then
			encoded_quality = number_of_chromas
		elseif number_of_chromas <= -1 then
			encoded_quality = number_of_chromas - 1
		end
	elseif mosstep_is_bright_gen then
		-- For bright gens:
		-- If the number of chromas is 1 or more, that quality is augmented
		-- If the number of chromas is 0, that quality is perfect
		-- If the number of chromas is -1 or less, that is diminished
		-- The encoded quality should always skip 1 and -1
		if number_of_chromas >= 1 then
			encoded_quality = number_of_chromas + 1
		elseif number_of_chromas == 0 then
			encoded_quality = 0
		elseif number_of_chromas <= -1 then
			encoded_quality = number_of_chromas - 1
		end
	elseif mosstep_is_dark_gen then
		-- For bright gens:
		-- If the number of chromas is 0 or more, that quality is augmented
		-- If the number of chromas is -1, that quality is perfect
		-- If the number of chromas is -2 or less, that is diminished
		-- The encoded quality should always skip 1 and -1
		if number_of_chromas >= 0 then
			encoded_quality = number_of_chromas + 2
		elseif number_of_chromas == -1 then
			encoded_quality = 0
		elseif number_of_chromas <= -2 then
			encoded_quality = number_of_chromas
		end
	else
		-- For all other intervals:
		-- If the number of chromas is 1 or more, that quality is augmented
		-- If the number of chromas is 0, that quality is major
		-- If the number of chromas is -1, that quality is minor
		-- If the number of chromas is -2 or less, that is diminished
		-- The encoded quality should always skip 0
		if number_of_chromas >= 1 then
			encoded_quality = number_of_chromas + 1
		elseif number_of_chromas == 0 then
			encoded_quality = 1
		elseif number_of_chromas == -1 then
			encoded_quality = -1
		elseif number_of_chromas <= -2 then
			encoded_quality = number_of_chromas
		end
	end
	
	return encoded_quality
end

-- Helper function
-- Given a quality (and only a quality), decode it from a numeric value to text.
-- Encoding follows the rules as found in the module mos notation, where:
-- -  3 = 2x augmented
-- -  2 = 1x augmented
-- -  1 = major
-- -  0 = perfect (used for generators, roots, and periods)
-- - -1 = minor
-- - -2 = 1x diminished
-- - -3 = 2x diminished
-- That encoded value should be converted back to text
function p.decode_quality(encoded_quality)

	local quality_as_text = ""
	if encoded_quality == 0 then
		quality_as_text = "Perf."
	elseif encoded_quality == 1 then
		quality_as_text = "Maj."
	elseif encoded_quality == 2 then
		quality_as_text = "Aug."
	elseif encoded_quality > 2 then
		quality_as_text = (encoded_quality - 1) .. "× Aug."
	elseif encoded_quality == -1 then
		quality_as_text = "Min."
	elseif encoded_quality == -2 then
		quality_as_text = "Dim."
	elseif encoded_quality < -2 then
		quality_as_text = (math.abs(encoded_quality) - 1) .. "× Dim."
	end

	return quality_as_text
end

-- Helper function
-- Calcualtes the qualities of each scale degree given a mosstep pattern and input mos
-- Input mos is necessary for comparing step patterns with the true mos pattern, esp. with modmosses.
function p.calculate_mode_degrees(input_mos, mosstep_pattern)
	local input_mos = input_mos or mos.new(5, 2)
	local mosstep_pattern = mosstep_pattern or "LLsLLLs"
	
	-- Get the number of mossteps per period
	local mossteps_per_equave = input_mos.nL + input_mos.ns
	
	local mode_degrees = {}
	for i = 1, mossteps_per_equave + 1 do
		local mossteps = i - 1
		
		local mosdegree_vector = p.convert_mosstep_pattern_to_mosstep_vector(mosstep_pattern, mossteps)
		local encoded_mosdegree = p.calculate_mosstep_quality(input_mos, mosdegree_vector)
		table.insert(mode_degrees, encoded_mosdegree)
	end
	
	return mode_degrees
end

-- Helper function
-- Calculate the UDP for each mode, given the modes are for the true
-- mos pattern and start at the brightest mode
function p.calculate_mos_mode_brightness_order(input_mos)
	local input_mos = input_mos or mos.new(5, 2)

	-- 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 mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- Get the number of mossteps in the bright gen and dark gen
	local bright_gen = mos.bright_gen(input_mos)
	local mossteps_per_bright_gen = bright_gen['L'] + bright_gen['s']

	-- For each scale degree within a single period of a step pattern,
	-- there is a unique mode.
	-- If the mos is single-period, then there are x+y unique modes.
	-- If the mos is multi-period nxL nys, then there are x+y modes instead
	-- of nx+ny modes due to repetition.
	local number_of_modes = mossteps_per_period

	local number_of_gens_down = 0
	local number_of_gens_up = mossteps_per_equave - periods_per_equave
	local brightness_order = {}
	for i = 1, mossteps_per_period do
		local current_udp = number_of_gens_up .. '&#124;' .. number_of_gens_down
		if periods_per_equave ~= 1 then
			current_udp = current_udp .. string.format("(%d)", periods_per_equave)
		end
		
		table.insert(brightness_order, current_udp)
		number_of_gens_up = number_of_gens_up - periods_per_equave
		number_of_gens_down = number_of_gens_down + periods_per_equave
	end

	return brightness_order
end

-- Helper function
-- Calculate the rotational order for each mode, given the modes are
-- for the true mos pattern and start at the brightest mode
function p.calculate_mos_mode_rotational_order(input_mos)
	local input_mos = input_mos or mos.new(5, 2)

	-- 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 mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- Get the number of mossteps in the bright gen and dark gen
	local bright_gen = mos.bright_gen(input_mos)
	local mossteps_per_bright_gen = bright_gen['L'] + bright_gen['s']

	-- For each scale degree within a single period of a step pattern,
	-- there is a unique mode.
	-- If the mos is single-period, then there are x+y unique modes.
	-- If the mos is multi-period nxL nys, then there are x+y modes instead
	-- of nx+ny modes due to repetition.
	local number_of_modes = mossteps_per_period

	local bright_gens_up = 0
	local rotational_order = {}
	for i = 1, mossteps_per_period do
		local current_mode_order = bright_gens_up % mossteps_per_period + 1
		bright_gens_up = bright_gens_up + mossteps_per_bright_gen
		table.insert(rotational_order, current_mode_order)
	end

	return rotational_order
end

-- Calculate the rotations of a step pattern
-- Modes can either be sorted by decreasing brightness or by leftward shifts (rotation)
-- This is meant to be used as a helper function for the following functions:
-- - calculate_mos_mode_degrees
-- - calculate_modmos_mode_degrees
function p.calculate_step_pattern_rotations(input_mos, mosstep_pattern, rotate_by_generator)
	local input_mos = input_mos or mos.new(6, 4)
	local mosstep_pattern = mosstep_pattern or "LLsLsLLsLs"
	local rotate_by_generator = rotate_by_generator == true
	
	-- 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 mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- Get the amount to shift by
	-- Shifting by the number of mossteps in the generator produces modes by
	-- descending brightness; shifting by 1 produces them in rotational order
	local shift_amount = 1
	if rotate_by_generator then
		local bright_gen = mos.bright_gen(input_mos)
		local mossteps_per_bright_gen = bright_gen['L'] + bright_gen['s']
		shift_amount = mossteps_per_bright_gen
	end
	
	local current_mode = mosstep_pattern
	local rotations = {}
	for i = 1, mossteps_per_equave do
		if not p.find_item_in_table(rotations, current_mode) then
			table.insert(rotations, current_mode)
		end
		
		-- Rotate current mode
		local first_substr = string.sub(current_mode, 1, shift_amount)
		local second_substr = string.sub(current_mode, shift_amount + 1, mossteps_per_equave)
		current_mode = second_substr .. first_substr
	end
	
	return rotations
end

-- Helper function
-- Calculate the scale degrees given an input mos and its modes
-- Modes can also be modmos modes
function p.calculate_mos_mode_degrees(input_mos, modes)
	local input_mos = input_mos or mos.new(5, 2)
	local modes = modes or p.calculate_step_pattern_rotations(input_mos, "LLLsLLs", true)
	
	-- 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
	
	-- Get the number of mossteps per bright gen
	local bright_gen = mos.bright_gen(input_mos)
	local mossteps_per_bright_gen = bright_gen['L'] + bright_gen['s']
	
	local mode_degrees = {}
	for i = 1, #modes do
		local current_mode_degrees = p.calculate_mode_degrees(input_mos, modes[i])
		table.insert(mode_degrees, current_mode_degrees)
	end
	
	return mode_degrees
end

-- Helper function
-- For a given modmos step pattern, find the closest true-mos mode and alterations
-- The mos and its modes in rotational order should also be passed in
-- This finds the closest mode for only the modmos's step pattern, not all rotations
-- Alterations are denoted as a UDP followed by which scale degrees are altered from the original mode
-- If multiple true-mos modes are tied with being closest, use the darkest mode instead
function p.compare_modmos_with_true_mos_modes(input_mos, true_mos_modes, modmos_step_pattern)
	local input_mos = input_mos or mos.new(5, 2)
	local true_mos_modes = true_mos_modes or p.calculate_step_pattern_rotations(mos.new(5, 2), "LLLsLLs", true)
	local modmos_step_pattern = modmos_step_pattern or "sLsLLsA"
	
	-- 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 mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- Get the modmos's mosstep vectors to compare
	local modmos_vector = p.calculate_mode_degrees(input_mos, modmos_step_pattern)

	-- Compare each mode in the array of mos modes.
	-- For each mode compared, count how many alterations there are. The mode
	-- with the fewest alterations is the closest true mos mode.
	local index_of_closest_mode = 1
	local lowest_number_of_alterations = mossteps_per_equave
	for i = 1, #true_mos_modes do
		local number_of_alterations = 0
		
		-- Get the current mode's degree vector
		local mode_vector = p.calculate_mode_degrees(input_mos, true_mos_modes[i])
		
		-- Compare the vectors
		for j = 1, #true_mos_modes[i] do
			if mode_vector[j] ~= modmos_vector[j] then
				number_of_alterations = number_of_alterations + 1
			end
		end
		
		-- If the current mode had fewer alterations, update
		if number_of_alterations < lowest_number_of_alterations then
			index_of_closest_mode = i
			lowest_number_of_alterations = number_of_alterations
		end
		
		-- If the current mode had the same number of alterations but is of a darker mode, update
		--if number_of_alterations == lowest_number_of_alterations then
		--	index_of_closest_mode = i
		--end
	end
	
	-- Calculate the UDP of the closest mode
	local gens_down = (index_of_closest_mode - 1) * periods_per_equave
	local gens_up = (mossteps_per_period - index_of_closest_mode) * periods_per_equave
	local udp_of_closest_mode = gens_up .. '&#124;' .. gens_down
	if periods_per_equave ~= 1 then
		udp_of_closest_mode = udp_of_closest_mode .. string.format("(%d)", periods_per_equave)
	end
	
	-- Calculate alterations by comparing the modmos and the closest mode's degrees
	local mode_vector = p.calculate_mode_degrees(input_mos, true_mos_modes[index_of_closest_mode])
	local alterations = ""
	for i = 1, #mode_vector do
		if mode_vector[i] ~= modmos_vector[i] then
			local encoded_degree = { ['Mossteps'] = i - 1, ['Quality'] = modmos_vector[i] }
			local decoded_degree = mosnot.decode_mosstep_quality(encoded_degree, "m", "mosdegree", "abbreviated")
			alterations = string.format("%s %s", alterations, decoded_degree)
		end
	end
	
	return udp_of_closest_mode .. alterations 
end

-- Helper function
-- For a given modmos step pattern, find the closest true-mos mode and alterations for each modmos mode
function p.calculate_modmos_mode_alterations(input_mos, modmos_step_pattern)
	local input_mos = input_mos or mos.new(5, 2)
	local modmos_step_pattern = modmos_step_pattern or "LLLsLLs"
	
	-- Calculate the modes for the truemos and modmos
	local true_mos_modes = p.calculate_step_pattern_rotations(input_mos, mos.brightest_mode(input_mos), true)
	local modmos_modes = p.calculate_step_pattern_rotations(input_mos, modmos_step_pattern, false)
	
	-- Get each mode's alterations
	local alterations = {}
	for i = 1, #modmos_modes do
		local alteration = p.compare_modmos_with_true_mos_modes(input_mos, true_mos_modes, modmos_modes[i])
		table.insert(alterations, alteration)
	end
	
	return alterations
end

-- Helper function
-- Calculates row colors given precalculated degree vectors for each mode
function p.calculate_row_colors(input_mos, input_mode_vectors)
	-- Default input mos and brightest/darkest true mos modes
	local input_mos = input_mos or mos.new(5, 2)
	local brightest_true_mode = mos.brightest_mode(input_mos)
	local darkest_true_mode = string.reverse(brightest_true_mode)
	
	-- Default input mode vectors
	local input_mode_vectors = input_mode_vectors or p.calculate_mos_mode_degrees(input_mos, p.calculate_step_pattern_rotations(input_mos, brightest_true_mode, true))
	
	-- Brightest and darkest vectors
	local brightest_vector = p.calculate_mode_degrees(input_mos, brightest_true_mode)
	local darkest_vector = p.calculate_mode_degrees(input_mos, darkest_true_mode)
	
	local row_colors = {}
	for i = 1, #input_mode_vectors do
		local cell_colors = {}
		for j = 1, #input_mode_vectors[i] do
			if input_mode_vectors[i][j] == brightest_vector[j] and input_mode_vectors[i][j] == darkest_vector[j] then
				table.insert(cell_colors, p.cell_color_perfect_size)
			elseif input_mode_vectors[i][j] == brightest_vector[j] then
				table.insert(cell_colors, p.cell_color_large_size)
			elseif input_mode_vectors[i][j] == darkest_vector[j] then
				table.insert(cell_colors, p.cell_color_small_size)
			elseif input_mode_vectors[i][j] > brightest_vector[j] then
				table.insert(cell_colors, p.cell_color_lg_altered_size)
			elseif input_mode_vectors[i][j] < darkest_vector[j] then
				table.insert(cell_colors, p.cell_color_sm_altered_size)
			end
		end
		table.insert(row_colors, cell_colors)
	end
	
	return row_colors
end

-- Create a table of a mos's degrees
function p.mos_mode_degrees(input_mos, mos_prefix, mode_names, use_default_mode_names)
	local input_mos = input_mos or mos.new(5, 2)
	local mos_prefix = mos_prefix or "mos"
	local mode_names = mode_names or nil
	local use_default_mode_names = use_default_mode_names == 1
	
	-- Get the modes
	local input_mode = mos.brightest_mode(input_mos)
	local modes = p.calculate_step_pattern_rotations(input_mos, input_mode, true)
	
	-- 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 mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- Get the scale sig
	local scale_sig = mos.as_string(input_mos)
	
	-- Get the brightness and rotational orderings
	local brightness_order = p.calculate_mos_mode_brightness_order(input_mos)
	local rotational_order = p.calculate_mos_mode_rotational_order(input_mos)
	
	-- Get mosstep vectors for all modes
	local mosstep_vectors = p.calculate_mos_mode_degrees(input_mos, modes)
	
	-- Get row colors
	local row_colors = p.calculate_row_colors(input_mos, mosstep_vectors)
	
	-- Create table
	local result = '{| class="wikitable sortable"\n'
	local result = result .. string.format("|+ Scale degree qualities of %s modes\n", scale_sig)
	
	-- Add table headers for first row
	result = result .. '! rowspan="2" | UDP\n'
	result = result .. '! rowspan="2" | Rotational Order\n'
	result = result .. '! rowspan="2" | Step pattern\n'
	
	-- Add mode names if present
	local mode_names_given = (mode_names ~= nil and #mode_names == #modes) or use_default_mode_names
	if mode_names_given then
		result = result .. '! rowspan="2" class="unsortable" | Mode names\n'
	end
	
	-- Add header for scale degrees
	result = result .. string.format('! colspan="%d" class="unsortable" | Scale degree (%sdegree)\n', mossteps_per_equave + 1, mos_prefix)
	
	-- Add second row of headers
	result = result .. "|-\n"
	for i = 1, mossteps_per_equave + 1 do
		result = result .. string.format('! class="unsortable" |%d\n', i-1)
	end
	
	-- Add table contents
	for i = 1, #modes do
		result = result .. "|-\n"
		
		-- Add brightness order (as UDP), rotational order, and step pattern
		result = result .. string.format('| %s\n| %s\n| %s\n', brightness_order[i], rotational_order[i], modes[i])
		
		-- Add mode name if given
		if mode_names_given then
			if use_default_mode_names then
				result = result .. string.format('| %s %s\n', scale_sig, brightness_order[i])
			else
				result = result .. string.format('| %s\n', mode_names[i])
			end
		end
		
		-- Add scale degrees with cell coloring
		-- This includes period intervals bold and alterations italicized
		for j = 1, #mosstep_vectors[i] do
			if row_colors[i][j] == p.cell_color_none then
				result = result .. string.format('| %s\n', p.decode_quality(mosstep_vectors[i][j]))
			elseif row_colors[i][j] == p.cell_color_lg_altered_size or row_colors[i][j] == p.cell_color_sm_altered_size then
				result = result .. string.format('| style="background:#%s" | \'\'%s\'\'\n', row_colors[i][j], p.decode_quality(mosstep_vectors[i][j]))
			else
				result = result .. string.format('| style="background:#%s" | %s\n', row_colors[i][j], p.decode_quality(mosstep_vectors[i][j]))
			end
		end
	end
	
	-- End of table
	result = result .. "|}"
	
	return result
end

-- Create a table of a modmos's degrees
function p.modmos_mode_degrees(input_mos, mos_prefix, step_pattern, mode_names, use_default_mode_names)
	local input_mos = input_mos or mos.new(5, 2)
	local mos_prefix = mos_prefix or "mos"
	local step_pattern = step_pattern or "LsLLsAs"
	local mode_names = mode_names or nil
	local use_default_mode_names = use_default_mode_names == 1
	
	-- Get the modes
	local modes = p.calculate_step_pattern_rotations(input_mos, step_pattern, false)
	
	-- 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 mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- Get the scale sig
	local scale_sig = mos.as_string(input_mos)
	
	-- Get rotational orderings; modmossses shouldn't be ordered by brightness
	local rotational_order = p.calculate_mos_mode_rotational_order(input_mos)
	
	-- Get closest mode and alterations
	local alterations = p.calculate_modmos_mode_alterations(input_mos, step_pattern)
	
	-- Get mosstep vectors for all modes
	local mosstep_vectors = p.calculate_mos_mode_degrees(input_mos, modes)
	
	-- Get row colors
	local row_colors = p.calculate_row_colors(input_mos, mosstep_vectors)
	
	-- Create table
	local result = '{| class="wikitable sortable"\n'
	local result = result .. string.format("|+ Scale degree qualities of %s modes (step pattern of %s)\n", scale_sig, step_pattern)
	
	-- Add table headers for first row
	result = result .. '! rowspan="2" | UDP and alterations\n'
	result = result .. '! rowspan="2" | Rotational Order\n'
	result = result .. '! rowspan="2" | Step pattern\n'
	
	-- Add mode names if present
	local mode_names_given = (mode_names ~= nil and #mode_names == #modes) or use_default_mode_names
	if mode_names_given then
		result = result .. '! rowspan="2" class="unsortable" | Mode names\n'
	end
	
	-- Add header for scale degrees
	result = result .. string.format('! colspan="%d" class="unsortable" | Scale degree (%sdegree)\n', mossteps_per_equave + 1, mos_prefix)
	
	-- Add second row of headers
	result = result .. "|-\n"
	for i = 1, mossteps_per_equave + 1 do
		result = result .. string.format('! class="unsortable" |%d\n', i-1)
	end
	
	-- Add table contents
	for i = 1, #modes do
		result = result .. "|-\n"
		
		-- Add brightness order (as UDP), rotational order, and step pattern
		result = result .. string.format('| %s\n| %s\n| %s\n', alterations[i], i, modes[i])
		
		-- Add mode name if given
		if mode_names_given then
			if use_default_mode_names then
				result = result .. string.format('| %s %s\n', scale_sig, alterations[i])
			else
				result = result .. string.format('| %s\n', mode_names[i])
			end
		end
		
		-- Add scale degrees with cell coloring
		-- This includes period intervals bold and alterations italicized
		for j = 1, #mosstep_vectors[i] do
			if row_colors[i][j] == p.cell_color_none then
				result = result .. string.format('| %s\n', p.decode_quality(mosstep_vectors[i][j]))
			elseif row_colors[i][j] == p.cell_color_lg_altered_size or row_colors[i][j] == p.cell_color_sm_altered_size then
				result = result .. string.format('| style="background:#%s" | \'\'%s\'\'\n', row_colors[i][j], p.decode_quality(mosstep_vectors[i][j]))
			else
				result = result .. string.format('| style="background:#%s" | %s\n', row_colors[i][j], p.decode_quality(mosstep_vectors[i][j]))
			end
		end
	end
	
	-- End of table
	result = result .. "|}"
	
	return result

end

-- Function to be called as part of a template
function p.mos_mode_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)
	
	-- 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 the step pattern
	local step_pattern = frame.args['MODMOS Step Pattern']
	
	-- Get the mode names
	local mode_names = nil
	-- Default names for 5L 2s modes
	if scale_sig == "5L 2s" and step_pattern == "LsLLsAs" then
		mode_names = { "Harmonic minor", "Locrian #6", "Ionian augmented", "Dorian #4", "Phrygian dominant", "Lydian #2", "Locrian b4 bb7" }
	elseif scale_sig == "5L 2s" and #step_pattern == 0 then
		mode_names = { "Lydian", "Ionian (major)", "Mixolydian", "Dorian", "Aeolian (minor)", "Phrygian", "Locrian" }
	end
	
	-- If mode names are given, use those instead
	-- If using default mode names (scalesig+udp), those names are auto-added by the relevant function
	local use_default_names = 0
	if #frame.args['Mode Names'] ~= 0 then
		if frame.args['Mode Names'] == "Default" then
			use_default_names = 1
		else
			mode_names = p.parse_entries(frame.args['Mode Names'])
		end
	end
	
	-- If a modmos step pattern was never provided, call the function mos_mode_degrees
	-- Otherwise, call the function modmos_mode_degrees
	local result = ""
	if step_pattern == "" then
		result = p.mos_mode_degrees(input_mos, mos_prefix, mode_names, use_default_names)
	elseif #step_pattern == input_mos.nL + input_mos.ns then
		result = p.modmos_mode_degrees(input_mos, mos_prefix, step_pattern, mode_names, use_default_names)
	end
	
	return result
end

return p