Module:MOS mode degrees: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Ganaram inukshuk (talk | contribs)
m Resolved issue with pipe and <nowiki>
ArrowHead294 (talk | contribs)
mNo edit summary
 
(133 intermediate revisions by 3 users not shown)
Line 1: Line 1:
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 = {}
local p = {}


-- TODO?: Move some functions to the mos notation module
local mos = require("Module:MOS")
local tamnams = require("Module:TAMNAMS")
local tip = require("Module:Template input parse")
local yesno = require("Module:Yesno")


-- Helper function
-- TODO
-- Parses entries from a semicolon-delimited string and returns them in an array
-- - Split off modmos mode degrees as a separate template
-- 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
-- Global variables for cell colors
-- Extracts the prefix from the mos module, without the dash and without any text after that
-- Colors are as follows:
-- May need to revisit to clean up code since this splits text at the "-".
-- - Orange and blue for small and large sizes, respectively
function p.get_mos_prefix(scale_sig)
-- - Darker colors for altered scale degrees
local unparsed = mos.tamnams_prefix[scale_sig]
-- - No color for period intervals
p.cell_color_none = "NONE" -- For cells that don't have a color (default cell color applies)
local parsed = {}
p.cell_color_perfect_size = "NONE" -- Only applies for periods, including the root and equave
p.cell_color_lg_altered_size = "#BDD7EE"
if unparsed == nil then
p.cell_color_large_size      = "#DDEBF7"
return "mos"
p.cell_color_small_size      = "#FCE4D6"
else
p.cell_color_sm_altered_size = "#F8CBAD"
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
-- Finds the row color for a single cell
-- For an interval with two specific sizes, its large size is iL js and small size
function p.cell_color(interval, input_mos)
-- is (i-1)L (j+1)s, for a difference of a single large step swapped with a single
local interval = interval or {["L"] = 3, ["s"] = 1}
-- 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 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 period_step_count = mos.period_step_count(input_mos)
local expected_large_mosstep_vector = p.convert_mosstep_pattern_to_mosstep_vector(brightest_mode, mossteps)
local interval_step_count = mos.interval_step_count(interval)
local chroma_count = mos.interval_chroma_count(interval, input_mos)
-- Since the size difference between the large and small intervals is a single chroma,
local is_period_interval = interval_step_count % period_step_count == 0
-- 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 color = p.cell_color_none
local mos_is_nL_ns = input_mos.nL == input_mos.ns
if is_period_interval then
local mosstep_is_period = mossteps % mossteps_per_period == 0
if chroma_count > 0 then
local mosstep_is_bright_gen = mossteps % mossteps_per_period == mossteps_per_bright_gen and not mos_is_nL_ns
color = p.cell_color_lg_altered_size
local mosstep_is_dark_gen = mossteps % mossteps_per_period == mossteps_per_dark_gen and not mos_is_nL_ns
elseif chroma_count == 0 then
color = p.cell_color_none
-- Rules for encoding are shown in the comments below.
elseif chroma_count < 0 then
-- Encoding follows the rules as found in the module mos notation, where:
color = p.cell_color_sm_altered_size
-- -  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
end
else
else
-- For all other intervals:
if chroma_count > 0 then
-- If the number of chromas is 1 or more, that quality is augmented
color = p.cell_color_lg_altered_size
-- If the number of chromas is 0, that quality is major
elseif chroma_count == 0 then
-- If the number of chromas is -1, that quality is minor
color = p.cell_color_large_size
-- If the number of chromas is -2 or less, that is diminished
elseif chroma_count == -1 then
-- The encoded quality should always skip 0
color = p.cell_color_small_size
if number_of_chromas >= 1 then
elseif chroma_count < -1 then
encoded_quality = number_of_chromas + 1
color = p.cell_color_sm_altered_size
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
end
end
return color
return encoded_quality
end
end


-- Helper function
-- Create a table of a mos's degrees
-- Given a quality (and only a quality), decode it from a numeric value to text.
-- If a step pattern is provided, it's assumed to be that of a modmos
-- Encoding follows the rules as found in the module mos notation, where:
function p._mos_mode_degrees(input_mos, mos_prefix, is_collapsed, step_pattern)
-- -  3 = 2x augmented
local is_true_mos = step_pattern == nil
-- -  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 input_mos = input_mos or mos.new(5, 2)
local mosstep_pattern = mosstep_pattern or "LLsLLLs"
local mos_prefix = mos_prefix or "mos"
local is_collapsed = is_collapsed == true
-- Get the number of mossteps per period
-- Get the modes as strings and step vectors
local mossteps_per_equave = input_mos.nL + input_mos.ns
local step_patterns = {}
local step_matrices = {}
local mode_degrees = {}
if is_true_mos then
for i = 1, mossteps_per_equave + 1 do
step_patterns = mos.modes_by_brightness(input_mos)
local mossteps = i - 1
step_matrices = mos.modes_to_step_matrices(input_mos)
else
local mosdegree_vector = p.convert_mosstep_pattern_to_mosstep_vector(mosstep_pattern, mossteps)
step_patterns = mos.mode_rotations(step_pattern)
local encoded_mosdegree = p.calculate_mosstep_quality(input_mos, mosdegree_vector)
step_matrices = mos.mode_rotations_to_step_matrices(step_pattern)
table.insert(mode_degrees, encoded_mosdegree)
end
end
return mode_degrees
-- Get the scale sig
end
local scale_sig = mos.as_string(input_mos)
 
-- 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
-- Equave step count; needed for degree column count
local mossteps_per_equave = (input_mos.nL + input_mos.ns)
local equave_step_count = mos.equave_step_count(input_mos)
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
-- Get the brightness (UDP) and rotational orderings (CPOs).
local bright_gen = mos.bright_gen(input_mos)
-- Also produce default mode names if set to do so.
local mossteps_per_bright_gen = bright_gen['L'] + bright_gen['s']
local udps = {}
 
local cpos = {}
-- For each scale degree within a single period of a step pattern,
if is_true_mos then
-- there is a unique mode.
-- Get UDPs and CPOs
-- If the mos is single-period, then there are x+y unique modes.
udps = tamnams.mos_mode_udps(input_mos)
-- If the mos is multi-period nxL nys, then there are x+y modes instead
cpos = tamnams.mos_mode_cpos(input_mos)
-- of nx+ny modes due to repetition.
else
local number_of_modes = mossteps_per_period
-- Modmos udps require a mosabbrev; this is forced to be "m" since some
 
-- abbrevs are tooo long. Get both the names for the closest-bright and
local number_of_gens_down = 0
-- closest-dark mode. If they're the same, only one name will be used;
local number_of_gens_up = mossteps_per_equave - periods_per_equave
-- if not, both are used.
local brightness_order = {}
-- The CPOs of a modmos are simply 1 to n (n is the mode count).
for i = 1, mossteps_per_period do
local udps_closest_bright = tamnams.mode_rotation_udps(step_pattern, input_mos, "m", true)
local current_mode_brightness = string.format("%d<nowiki>|</nowiki>%d", number_of_gens_up, number_of_gens_down)
local udps_closest_dark = tamnams.mode_rotation_udps(step_pattern, input_mos, "m", false)
table.insert(brightness_order, current_mode_brightness)
for i = 1, #udps_closest_bright do
number_of_gens_up = number_of_gens_up - periods_per_equave
if udps_closest_bright[i] == udps_closest_dark[i] then
number_of_gens_down = number_of_gens_down + periods_per_equave
table.insert(udps, udps_closest_bright[i])
end
else
 
table.insert(udps, string.format("%s<br />%s", udps_closest_bright[i], udps_closest_dark[i]))
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
function p.compare_modmos_with_true_mos_modes(input_mos, mos_modes, modmos_step_pattern)
local input_mos = input_mos or mos.new(5, 2)
local mos_modes = 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, #mos_modes do
local number_of_alterations = 0
-- Get the current mode's degree vector
local mode_vector = p.calculate_mode_degrees(input_mos, mos_modes[i])
-- Compare the vectors
for j = 1, #mos_modes[i] do
if mode_vector[j] ~= modmos_vector[j] then
number_of_alterations = number_of_alterations + 1
end
end
end
table.insert(cpos, i)
-- 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
end
end
end
-- Calculate the UDP of the closest mode
-- Create table
local gens_down = (index_of_closest_mode - 1) * periods_per_equave
local result = "{| class=\"wikitable sortable mw-collapsible center-2 center-3"
local gens_up = (mossteps_per_period - index_of_closest_mode) * periods_per_equave
.. (is_collapsed and " mw-collapsed\"" or "\"") .. "\n"
local udp_of_closest_mode = string.format("%d|%d", gens_up, gens_down)
-- Calculate alterations by comparing the modmos and the closest mode's degrees
local mode_vector = p.calculate_mode_degrees(input_mos, 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 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, mos_modes, modmos_modes[i])
table.insert(alterations, alteration)
end
return alterations
end
 
-- Create a table of a mos's degrees
function p.mos_mode_degrees(input_mos, mos_prefix, 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
-- Get the modes
local input_mode = mos.brightest_mode(input_mos)
local modes = p.calculate_step_pattern_rotations(input_mos, input_mode, true)
-- Default names for 5L 2s is the seven names of Lydian ... Locrian
if input_mos.nL == 5 and input_mos.ns == 2 and input_mos.equave == 2 then
mode_names = { "Lydian", "Ionian (major)", "Mixolydian", "Dorian", "Aeolian (minor)", "Phrygian", "Locrian" }
end
-- 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
-- Table's title
local brightness_order = p.calculate_mos_mode_brightness_order(input_mos)
-- If it's for a modmos, add the step pattern
local rotational_order = p.calculate_mos_mode_rotational_order(input_mos)
result = result .. "|+ style=\"font-size: 105%; white-space: nowrap;\" | " .. string.format("Scale degrees of the modes of %s", scale_sig)
.. (is_true_mos and "\n" or string.format(" (%s)\n", step_pattern))
-- Get mosstep vectors for all modes
.. "|-\n"
local mosstep_vectors = p.calculate_mos_mode_degrees(input_mos, modes)
-- Create table
local result = '{| class="wikitable sortable"\n'
local result = result .. string.format("|+ Scale degrees of %s modes\n", scale_sig)
-- Add table headers for first row
-- Add table headers for first row
result = result .. '! rowspan="2" | UDP\n'
result = result
result = result .. '! rowspan="2" | Rotational Order\n'
.. "! rowspan=\"2\" | UDP" .. (is_true_mos and "\n" or " and<br />alterations\n") -- If modmos, add "and alterations" string
result = result .. '! rowspan="2" | Step pattern\n'
.. "! rowspan=\"2\" | Cyclic<br />order\n"
.. "! rowspan=\"2\" | Step<br />pattern\n"
-- Add mode names if present
local mode_names_given = mode_names ~= nil and #mode_names == #modes
if mode_names_given then
result = result .. '! rowspan="2" class="unsortable" | Mode names\n'
end
-- Add header for scale degrees
-- Add header for scale degrees
result = result .. string.format('! colspan="%d" class="unsortable" | Scale degree (%sdegree)\n', mossteps_per_equave + 1, mos_prefix)
result = result .. string.format("! colspan=\"%d\" class=\"unsortable\" | Scale degree (%sdegree)\n", #step_matrices[1], mos_prefix)
-- Add second row of headers
-- Add second row of headers
result = result .. "|-\n"
result = result .. "|- class=\"unsortable\"\n"
for i = 1, mossteps_per_equave + 1 do
.. "! 0"
result = result .. string.format('! class="unsortable" |%d\n', i-1)
for i = 1, #step_patterns[1] do
result = result .. string.format("\n! %d", i)
end
end
result = result .. "\n"
-- Add table contents
-- Add table contents
for i = 1, #modes do
for i = 1, #step_patterns do
result = result .. "|-\n"
result = result .. "|-\n"
-- Add brightness order (as UDP), rotational order, and step pattern
-- 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])
.. string.format("| %s\n| %s\n| %s\n", udps[i], cpos[i], step_patterns[i])
 
-- Add mode name if given
-- Add scale degrees with cell coloring
if mode_names_given then
for j = 1, #step_matrices[i] do
result = result .. string.format('| %s\n', mode_names[i])
local current_interval = step_matrices[i][j]
end
local degree_quality = tamnams.decode_quality(current_interval, input_mos, "shortened")
-- Add scale degrees
local cell_color = p.cell_color(current_interval, input_mos)
for j = 1, #mosstep_vectors[i] do
local style_code = (cell_color == p.cell_color_none and "" or string.format("style=\"background: %s;\" | ", cell_color))
result = result .. string.format('| %s\n', p.decode_quality(mosstep_vectors[i][j]))
result = result .. string.format("| %s%s\n", style_code, degree_quality)
end
end
end
end
Line 583: Line 159:
end
end


-- Create a table of a modmos's degrees
-- Function to be called as part of a template
function p.modmos_mode_degrees(input_mos, mos_prefix, step_pattern, mode_names)
function p.mos_mode_degrees(frame)
local input_mos = input_mos or mos.new(5, 2)
-- Get args
local mos_prefix = mos_prefix or "mos"
local input_mos   = mos.parse(frame.args["Scale Signature"])
local step_pattern = step_pattern or "LsLLsAs"
local mos_prefix   = frame.args["MOS Prefix"]
local mode_names = mode_names or nil
local step_pattern = frame.args["MODMOS Step Pattern"]
local mode_names_unparsed = frame.args["Mode Names"]
-- Get the modes
-- Parse debugging option
local modes = p.calculate_step_pattern_rotations(input_mos, step_pattern, false)
local debugg = yesno(frame.args["debug"])
-- Default names for 5L 2s modmosses is the seven names of Lydian ... Locrian, modified accordingly
-- Get the scale sig; for calculating the mos prefix
if input_mos.nL == 5 and input_mos.ns == 2 and input_mos.equave == 2 and step_pattern == "LsLLsAs" then
mode_names = { "Harmonic minor", "Locrian #6", "Ionian augmented", "Dorian #4", "Phrygian dominant", "Lydian #2", "Locrian b4 bb7" }
end
-- 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)
local scale_sig = mos.as_string(input_mos)
-- Get rotational orderings; modmossses shouldn't be ordered by brightness
-- Verify mosprefix
local rotational_order = p.calculate_mos_mode_rotational_order(input_mos)
mos_prefix = tamnams.verify_prefix(input_mos, mos_prefix)
-- Get closest mode and alterations
-- Get the mode names
local alterations = p.calculate_modmos_mode_alterations(input_mos, step_pattern)
local mode_names = nil
-- Default names for 5L 2s modes and select modmosses.
-- Get mosstep vectors for all modes
-- Names are based on whichever mode is returnd by UDP closest-mode search,
local mosstep_vectors = p.calculate_mos_mode_degrees(input_mos, modes)
-- with common names added wherever applicable. Sources include:
-- - https://www.jazz-guitar-licks.com/ and likely others
-- Create table
-- - Whatever Wikipedia has cited for the Neapolitan scales
local result = '{| class="wikitable sortable"\n'
-- NOTE: these names can be overridden if they don't suffice.
local result = result .. string.format("|+ Scale degrees of %s modes (step pattern of %s)\n", scale_sig, step_pattern)
if scale_sig == "5L 2s" then
if step_pattern == "LsLLsAs" then
-- Add table headers for first row
-- Modes of harmonic minor
result = result .. '! rowspan="2" | UDP and alterations\n'
-- Closest-mode search always returns one name
result = result .. '! rowspan="2" | Rotational Order\n'
mode_names = {
result = result .. '! rowspan="2" | Step pattern\n'
"Harmonic minor<br />(Aeolian ♮7)",
"Locrian ♮6",
-- Add mode names if present
"Ionian augmented<br />(Ionian ♯5)",
local mode_names_given = mode_names ~= nil and #mode_names == #modes
"Dorian ♯4",
if mode_names_given then
"Phrygian dominant<br />(Phrygian ♮3)",
result = result .. '! rowspan="2" class="unsortable" | Mode names\n'
"Lydian ♯2",
end
"Altered diminished<br />(Locrian ♭4 𝄫7)",
}
-- Add header for scale degrees
elseif step_pattern == "LLsLsAs" then
result = result .. string.format('! colspan="%d" class="unsortable" | Scale degree (%sdegree)\n', mossteps_per_equave + 1, mos_prefix)
-- Modes of harmonic major
-- Closest-mode search always returns one name
-- Add second row of headers
mode_names = {
result = result .. "|-\n"
"Harmonic major<br />(Ionian ♭6)",
for i = 1, mossteps_per_equave + 1 do
"Dorian ♭5",
result = result .. string.format('! class="unsortable" |%d\n', i-1)
"Phrygian ♭4",
end
"Lydian ♭3",
"Mixolydian ♭2",
-- Add table contents
"Lydian augmented ♯2<br />(Lydian ♯2 ♯5)",
for i = 1, #modes do
"Locrian 𝄫7",
result = result .. "|-\n"
}
elseif step_pattern == "LsLLLLs" then
-- Add brightness order (as UDP), rotational order, and step pattern
-- Modes of melodic minor
result = result .. string.format('| %s\n| %s\n| %s\n', alterations[i], i, modes[i])
-- Closest-mode search sometimes returns two names
mode_names = {
-- Add mode name if given
"Melodic minor<br />(Ionian ♭3, Dorian ♮7)",
if mode_names_given then
"Dorian ♭2, Phrygian ♮6",
result = result .. string.format('| %s\n', mode_names[i])
"Lydian augmented<br />(Lydian ♯5)",
end
"Lydian dominant<br />(Lydian ♭7, Mixolydian ♯4)",
"Mixolydian ♭6, Aeolian ♮3",
-- Add scale degrees
"Half-diminished<br />(Aeolian ♭5, Locrian ♮2)",
for j = 1, #mosstep_vectors[i] do
"Altered, Altered dominant<br />(Locrian ♭4)",
result = result .. string.format('| %s\n', p.decode_quality(mosstep_vectors[i][j]))
}
elseif step_pattern == "sLLLLLs" then
-- Modes of Neapolitan major
-- Closest-mode search sometimes returns two names
mode_names = {
"Neapolitan major<br />(Ionian ♭2 ♭3, Phrigian ♮6 ♮7)",
"Lydian augmented ♯6<br />(Lydian ♯5 ♯6)",
"Lydian augmented dominant<br />(Lydian ♯5 ♭7, Mixolydian ♯4 ♯5)",
"Lydian minor<br />(Lydian ♭6 ♭7, Aeolian ♮3 ♯4)",
"Major locrian<br />(Mixolydian ♭5 ♭6, Locrian ♮2 ♮3)",
"Altered dominant ♮2<br />(Aeolian ♭4 ♭5, Locrian ♮2, ♭4)",
"Altered dominant 𝄫3<br />(Locrian 𝄫3 ♭4)",
}
elseif step_pattern == "sLLLsAs" then
-- Modes of Neapolitan minor
-- Closest-mode search always returns one name
mode_names = {
"Neapolitan minor<br />(Phrygian ♮7)",
"Lydian ♯6",
"Mixolydian augmented<br />(Mixolydian ♯5)",
"Aeolian ♯4",
"Locrian dominant<br />(Locrian ♮3)",
"Ionian ♯2",
"Altered diminished 𝄫3<br />(Locrian 𝄫3 ♭4 𝄫7)",
}
elseif step_pattern == "sAsLsAs" then
-- Modes of double harmonic
-- Closest-mode search sometimes returns two names
mode_names = {
"Double harmonic<br />(Ionian ♭2 ♭6, Phrygian ♮3 ♮7)",
"Lydian ♯2 ♯6",
"Altered ♮5 𝄫6<br />(Phrygian ♭4 𝄫7)",
"Double harmonic minor<br />(Lydian ♭3 ♭6, Aeolian ♯4 ♮7)",
"Mixolydian ♭2 ♭5, Locrian ♮3 ♮6",
"Ionian augmented ♯2<br />(Ionian ♯2 ♯5)",
"Locrian 𝄫3 𝄫7",
}
elseif #step_pattern == 0 then
-- True-mos modes
mode_names = {
"Lydian",
"Ionian (major)",
"Mixolydian",
"Dorian",
"Aeolian (minor)",
"Phrygian",
"Locrian"
}
end
end
end
end
-- End of table
-- If mode names are given, use those instead
result = result .. "|}"
-- If using default mode names (scalesig+udp), those names are auto-added by the relevant function
local use_default_names = false
return result
if #mode_names_unparsed ~= 0 then
 
if mode_names_unparsed == "Default" then
end
use_default_names = true
 
else
-- Function to be called as part of a template
mode_names = tip.parse_entries(mode_names_unparsed)
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
end
else
mos_prefix = frame.args['MOS Prefix']
end
end
-- Get the mode names
-- Check if the table should start collapsed
local mode_names = p.parse_entries(frame.args['Mode Names'])
local is_collapsed = yesno(frame.args["Collapsed"], false)
-- Get the step pattern
local step_pattern = frame.args['MODMOS Step Pattern']
-- If a modmos step pattern was never provided, call the function mos_mode_degrees
-- If a modmos step pattern was never provided, call the function mos_mode_degrees
-- Otherwise, call the function modmos_mode_degrees
-- Otherwise, call the function modmos_mode_degrees
local result = ""
local result = ""
if #step_pattern ~= input_mos.nL + input_mos.ns then
if step_pattern == "" then
result = p.mos_mode_degrees(input_mos, mos_prefix, mode_names)
result = p._mos_mode_degrees(input_mos, mos_prefix, is_collapsed)
--elseif #step_pattern == input_mos.nL + input_mos.ns then
else
else
result = p.modmos_mode_degrees(input_mos, mos_prefix, step_pattern, mode_names)
result = p._mos_mode_degrees(input_mos, mos_prefix, is_collapsed, step_pattern)
end
end
return result
-- Debugger option
if debugg == true then
result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
end
return frame:preprocess(result)
end
end


return p
return p

Latest revision as of 12:43, 1 June 2025

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

This module creates a table of the scale degrees for each mode of a MOS or MODMOS scale.

Introspection summary for Module:MOS mode degrees 
Functions provided (3)
Line Function Params
24 cell_color (interval, input_mos)
59 _mos_mode_degrees (main) (input_mos, mos_prefix, is_collapsed, step_pattern)
162 mos_mode_degrees (invokable) (frame)
Lua modules required (4)
Variable Module Functions used
mos Module:MOS new
period_step_count
interval_step_count
interval_chroma_count
modes_by_brightness
modes_to_step_matrices
mode_rotations
mode_rotations_to_step_matrices
as_string
equave_step_count
parse
tamnams Module:TAMNAMS mos_mode_udps
mos_mode_cpos
mode_rotation_udps
decode_quality
verify_prefix
tip Module:Template input parse parse_entries
yesno Module:Yesno yesno

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


local p = {}

local mos = require("Module:MOS")
local tamnams = require("Module:TAMNAMS")
local tip = require("Module:Template input parse")
local yesno = require("Module:Yesno")

-- TODO
-- - Split off modmos mode degrees as a separate template

-- Global variables for cell colors
-- Colors are as follows:
-- - Orange and blue for small and large sizes, respectively
-- - Darker colors for altered scale degrees
-- - No color for period intervals
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"

-- Finds the row color for a single cell
function p.cell_color(interval, input_mos)
	local interval = interval or {["L"] = 3, ["s"] = 1}
	local input_mos = input_mos or mos.new(5, 2)
	
	local period_step_count = mos.period_step_count(input_mos)
	local interval_step_count = mos.interval_step_count(interval)
	local chroma_count = mos.interval_chroma_count(interval, input_mos)
	
	local is_period_interval = interval_step_count % period_step_count == 0
	
	local color = p.cell_color_none
	if is_period_interval then
		if chroma_count > 0 then
			color = p.cell_color_lg_altered_size
		elseif chroma_count == 0 then
			color = p.cell_color_none
		elseif chroma_count < 0 then
			color = p.cell_color_sm_altered_size
		end
	else
		if chroma_count > 0 then
			color = p.cell_color_lg_altered_size
		elseif chroma_count == 0 then
			color = p.cell_color_large_size
		elseif chroma_count == -1 then
			color = p.cell_color_small_size
		elseif chroma_count < -1 then
			color = p.cell_color_sm_altered_size
		end
	end
	return color
end

-- Create a table of a mos's degrees
-- If a step pattern is provided, it's assumed to be that of a modmos
function p._mos_mode_degrees(input_mos, mos_prefix, is_collapsed, step_pattern)
	local is_true_mos = step_pattern == nil
	local input_mos = input_mos or mos.new(5, 2)
	local mos_prefix = mos_prefix or "mos"
	local is_collapsed = is_collapsed == true
	
	-- Get the modes as strings and step vectors
	local step_patterns = {}
	local step_matrices = {}
	if is_true_mos then
		step_patterns = mos.modes_by_brightness(input_mos)
		step_matrices = mos.modes_to_step_matrices(input_mos)
	else
		step_patterns = mos.mode_rotations(step_pattern)
		step_matrices = mos.mode_rotations_to_step_matrices(step_pattern)
	end
	
	-- Get the scale sig
	local scale_sig = mos.as_string(input_mos)

	-- Equave step count; needed for degree column count
	local equave_step_count = mos.equave_step_count(input_mos)
	
	-- Get the brightness (UDP) and rotational orderings (CPOs).
	-- Also produce default mode names if set to do so.
	local udps = {}
	local cpos = {}
	if is_true_mos then
		-- Get UDPs and CPOs
		udps = tamnams.mos_mode_udps(input_mos)
		cpos = tamnams.mos_mode_cpos(input_mos)
	else
		-- Modmos udps require a mosabbrev; this is forced to be "m" since some
		-- abbrevs are tooo long. Get both the names for the closest-bright and
		-- closest-dark mode. If they're the same, only one name will be used;
		-- if not, both are used.
		-- The CPOs of a modmos are simply 1 to n (n is the mode count).
		local udps_closest_bright = tamnams.mode_rotation_udps(step_pattern, input_mos, "m", true)
		local udps_closest_dark = tamnams.mode_rotation_udps(step_pattern, input_mos, "m", false)
		for i = 1, #udps_closest_bright do
			if udps_closest_bright[i] == udps_closest_dark[i] then
				table.insert(udps, udps_closest_bright[i])
			else
				table.insert(udps, string.format("%s<br />%s", udps_closest_bright[i], udps_closest_dark[i]))
			end
			table.insert(cpos, i)
		end
	end
	
	-- Create table
	local result = "{| class=\"wikitable sortable mw-collapsible center-2 center-3"
		.. (is_collapsed and " mw-collapsed\"" or "\"") .. "\n"
	
	-- Table's title
	-- If it's for a modmos, add the step pattern
	result = result .. "|+ style=\"font-size: 105%; white-space: nowrap;\" | " .. string.format("Scale degrees of the modes of %s", scale_sig)
		.. (is_true_mos and "\n" or string.format(" (%s)\n", step_pattern))
		.. "|-\n"
	
	-- Add table headers for first row
	result = result
		.. "! rowspan=\"2\" | UDP" .. (is_true_mos and "\n" or " and<br />alterations\n")		-- If modmos, add "and alterations" string
		.. "! rowspan=\"2\" | Cyclic<br />order\n"
		.. "! rowspan=\"2\" | Step<br />pattern\n"
	
	-- Add header for scale degrees
	result = result .. string.format("! colspan=\"%d\" class=\"unsortable\" | Scale degree (%sdegree)\n", #step_matrices[1], mos_prefix)
	
	-- Add second row of headers
	result = result .. "|- class=\"unsortable\"\n"
		.. "! 0"
	for i = 1, #step_patterns[1] do
		result = result .. string.format("\n! %d", i)
	end
	
	result = result .. "\n"
	
	-- Add table contents
	for i = 1, #step_patterns do
		result = result .. "|-\n"
		
		-- Add brightness order (as UDP), rotational order, and step pattern
			.. string.format("| %s\n| %s\n| %s\n", udps[i], cpos[i], step_patterns[i])

		-- Add scale degrees with cell coloring
		for j = 1, #step_matrices[i] do
			local current_interval = step_matrices[i][j]
			local degree_quality = tamnams.decode_quality(current_interval, input_mos, "shortened")
			
			local cell_color = p.cell_color(current_interval, input_mos)
			local style_code = (cell_color == p.cell_color_none and "" or string.format("style=\"background: %s;\" | ", cell_color))
			
			result = result .. string.format("| %s%s\n", style_code, degree_quality)
		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)
	-- Get args
	local input_mos    = mos.parse(frame.args["Scale Signature"])
	local mos_prefix   = frame.args["MOS Prefix"]
	local step_pattern = frame.args["MODMOS Step Pattern"]
	local mode_names_unparsed = frame.args["Mode Names"]
	
	-- Parse debugging option
	local debugg = yesno(frame.args["debug"])
	
	-- Get the scale sig; for calculating the mos prefix
	local scale_sig = mos.as_string(input_mos)
	
	-- Verify mosprefix
	mos_prefix = tamnams.verify_prefix(input_mos, mos_prefix)
	
	-- Get the mode names
	local mode_names = nil
	-- Default names for 5L 2s modes and select modmosses.
	-- Names are based on whichever mode is returnd by UDP closest-mode search,
	-- with common names added wherever applicable. Sources include:
	-- - https://www.jazz-guitar-licks.com/ and likely others
	-- - Whatever Wikipedia has cited for the Neapolitan scales
	-- NOTE: these names can be overridden if they don't suffice.
	if scale_sig == "5L 2s" then
		if step_pattern == "LsLLsAs" then
			-- Modes of harmonic minor
			-- Closest-mode search always returns one name
			mode_names = {
				"Harmonic minor<br />(Aeolian ♮7)",
				"Locrian ♮6",
				"Ionian augmented<br />(Ionian ♯5)",
				"Dorian ♯4",
				"Phrygian dominant<br />(Phrygian ♮3)",
				"Lydian ♯2",
				"Altered diminished<br />(Locrian ♭4 𝄫7)",
			}
		elseif step_pattern == "LLsLsAs" then
			-- Modes of harmonic major
			-- Closest-mode search always returns one name
			mode_names = {
				"Harmonic major<br />(Ionian ♭6)",
				"Dorian ♭5",
				"Phrygian ♭4",
				"Lydian ♭3",
				"Mixolydian ♭2",
				"Lydian augmented ♯2<br />(Lydian ♯2 ♯5)",
				"Locrian 𝄫7",
			}
		elseif step_pattern == "LsLLLLs" then
			-- Modes of melodic minor
			-- Closest-mode search sometimes returns two names
			mode_names = {
				"Melodic minor<br />(Ionian ♭3, Dorian ♮7)",
				"Dorian ♭2, Phrygian ♮6",
				"Lydian augmented<br />(Lydian ♯5)",
				"Lydian dominant<br />(Lydian ♭7, Mixolydian ♯4)",
				"Mixolydian ♭6, Aeolian ♮3",
				"Half-diminished<br />(Aeolian ♭5, Locrian ♮2)",
				"Altered, Altered dominant<br />(Locrian ♭4)",
			}
		elseif step_pattern == "sLLLLLs" then
			-- Modes of Neapolitan major
			-- Closest-mode search sometimes returns two names
			mode_names = {
				"Neapolitan major<br />(Ionian ♭2 ♭3, Phrigian ♮6 ♮7)",
				"Lydian augmented ♯6<br />(Lydian ♯5 ♯6)",
				"Lydian augmented dominant<br />(Lydian ♯5 ♭7, Mixolydian ♯4 ♯5)",
				"Lydian minor<br />(Lydian ♭6 ♭7, Aeolian ♮3 ♯4)",
				"Major locrian<br />(Mixolydian ♭5 ♭6, Locrian ♮2 ♮3)",
				"Altered dominant ♮2<br />(Aeolian ♭4 ♭5, Locrian ♮2, ♭4)",
				"Altered dominant 𝄫3<br />(Locrian 𝄫3 ♭4)",
			}
		elseif step_pattern == "sLLLsAs" then
			-- Modes of Neapolitan minor
			-- Closest-mode search always returns one name
			mode_names = {
				"Neapolitan minor<br />(Phrygian ♮7)",
				"Lydian ♯6",
				"Mixolydian augmented<br />(Mixolydian ♯5)",
				"Aeolian ♯4",
				"Locrian dominant<br />(Locrian ♮3)",
				"Ionian ♯2",
				"Altered diminished 𝄫3<br />(Locrian 𝄫3 ♭4 𝄫7)",
			}
			elseif step_pattern == "sAsLsAs" then
			-- Modes of double harmonic
			-- Closest-mode search sometimes returns two names
			mode_names = {
				"Double harmonic<br />(Ionian ♭2 ♭6, Phrygian ♮3 ♮7)",
				"Lydian ♯2 ♯6",
				"Altered ♮5 𝄫6<br />(Phrygian ♭4 𝄫7)",
				"Double harmonic minor<br />(Lydian ♭3 ♭6, Aeolian ♯4 ♮7)",
				"Mixolydian ♭2 ♭5, Locrian ♮3 ♮6",
				"Ionian augmented ♯2<br />(Ionian ♯2 ♯5)",
				"Locrian 𝄫3 𝄫7",
			}
		elseif #step_pattern == 0 then
			-- True-mos modes
			mode_names = { 
				"Lydian",
				"Ionian (major)",
				"Mixolydian",
				"Dorian",
				"Aeolian (minor)",
				"Phrygian",
				"Locrian"
			}
		end
	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 = false
	if #mode_names_unparsed ~= 0 then
		if mode_names_unparsed == "Default" then
			use_default_names = true
		else
			mode_names = tip.parse_entries(mode_names_unparsed)
		end
	end
	
	-- Check if the table should start collapsed
	local is_collapsed = yesno(frame.args["Collapsed"], false)
	
	-- 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, is_collapsed)
	--elseif #step_pattern == input_mos.nL + input_mos.ns then
	else
		result = p._mos_mode_degrees(input_mos, mos_prefix, is_collapsed, step_pattern)
	end
	
	-- Debugger option
	if debugg == true then
		result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
	end
	
	return frame:preprocess(result)
end

return p