Module:TAMNAMS

From Xenharmonic Wiki
Revision as of 07:41, 19 July 2024 by Ganaram inukshuk (talk | contribs) (Added equave-agnostic names (even though their use is currently limited))
Jump to navigation Jump to search
Module documentation[view] [edit] [history] [purge]
This module primarily serves as a library for other modules and has no corresponding template.

This module is designed to handle TAMNAMS as it pertains to MOS scales. It is meant to be used with other modules, rather than something invoked directly or as part of a template.

Introspection summary for Module:TAMNAMS 
Functions provided (17)
Line Function Params
249 preprocess_step_ratio (step_ratio)
262 preprocess_scalesig (input_mos)
278 lookup_name (input_mos)
285 lookup_prefix (input_mos)
292 lookup_abbrev (input_mos)
299 lookup_step_ratio (step_ratio, use_extended)
313 lookup_step_ratio_range (step_ratio_1, step_ratio_2, use_extended)
336 find_ancestor (input_mos, target_note_count)
360 find_ancestor_info (input_mos, target_step_count)
427 interval_quality (interval, input_mos, abbrev_format, mos_prefix)
460 degree_quality (interval, input_mos, abbrev_format, mos_prefix)
501 decode_quality (interval, input_mos, abbrev_format)
673 mos_mode_udps (input_mos)
703 mos_mode_cpos (input_mos)
729 mode_rotation_udps (input_mode, input_mos)
743 mode_udp (input_mode, input_mos)
807 tester none
Lua modules required (4)
Variable Module Functions used
mos Module:MOS as_string
new
interval_step_count
normalize_interval
period_step_count
bright_gen_step_count
dark_gen_step_count
interval_chroma_count
period_count
mode_rotations
modes_to_step_matrices
mode_to_step_matrix
equave_step_count
interval_eq
modes_by_brightness
rat Module:Rational new
as_ratio
as_float
tip Module:Template input parse dependency not used
utils Module:Utils dependency not used

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


-- Module for TAMNAMS-related things as it pertains to mosses
-- This module is meant to be used with other modules, not as part of a template
-- Work in progress
local mos = require('Module:MOS')
local rat = require('Module:Rational')
local utils = require('Module:Utils')
local tip = require('Module:Template input parse')
local p = {}

--------------------------------------------------------------------------------
------------------------------- LOOKUP TABLES ----------------------------------
--------------------------------------------------------------------------------

-- Lookup table for tamnams step ratios
p.tamnams_ratios = {
	['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'
}

-- And step ratio ranges
p.tamnams_ranges = {
	['1:1 to 2:1'] = 'soft-of-basic',
	['1:1 to 4:3'] = 'ultrasoft',
	['4:3 to 3:2'] = 'parasoft',
	['3:2 to 2:1'] = 'hyposoft',
	['3:2 to 5:3'] = 'quasisoft',
	['5:3 to 2:1'] = 'minisoft',
	['2:1 to 5:2'] = 'minihard',
	['5:2 to 3:1'] = 'quasihard',
	['2:1 to 3:1'] = 'hypohard',
	['3:1 to 4:1'] = 'parahard',
	['4:1 to 1:0'] = 'ultrahard',
	['2:1 to 1:0'] = 'hard-of-basic'
}

-- Lookup table for tamnams extended step ratios
p.tamnams_ratios_ext = {
	['1:1'] = 'equalized',
	['6:5'] = 'semiequalized',
	['4:3'] = 'supersoft',
	['3:2'] = 'soft',
	['5:3'] = 'semisoft',
	['2:1'] = 'basic',
	['5:2'] = 'semihard',
	['3:1'] = 'hard',
	['4:1'] = 'superhard',
	['6:1'] = 'extrahard',
	['10:1'] = 'semicollapsed',
	['1:0'] = 'collapsed'
}

-- And extended step ratio ranges
p.tamnams_ranges_ext = {
	['1:1 to 2:1'] = 'soft-of-basic',
	['1:1 to 6:5'] = 'pseudoequalized',
	['6:5 to 4:3'] = 'ultrasoft',
	['4:3 to 3:2'] = 'parasoft',
	['3:2 to 2:1'] = 'hyposoft',
	['3:2 to 5:3'] = 'quasisoft',
	['5:3 to 2:1'] = 'minisoft',
	['2:1 to 5:2'] = 'minihard',
	['5:2 to 3:1'] = 'quasihard',
	['2:1 to 3:1'] = 'hypohard',
	['3:1 to 4:1'] = 'parahard',
	['4:1 to 6:1'] = 'hyperhard',
	['6:1 to 10:1'] = 'clustered',
	['4:1 to 10:1'] = 'ultrahard',
	['10:1 to 1:0'] = 'pseudocollapsed',
	['2:1 to 1:0'] = 'hard-of-basic'
}

-- Lookup table for tamnams names within the range of 6-10 steps
p.tamnams_name = {
	['1L 1s'] = 'monowood',
	['2L 2s'] = 'biwood',
	['1L 5s'] = 'antimachinoid',
	['2L 4s'] = 'malic',
	['3L 3s'] = 'triwood',
	['4L 2s'] = 'citric',
	['5L 1s'] = 'machinoid',
	['1L 6s'] = 'onyx',
	['2L 5s'] = 'antidiatonic',
	['3L 4s'] = 'mosh',
	['4L 3s'] = 'smitonic',
	['5L 2s'] = 'diatonic',
	['6L 1s'] = 'archaeotonic',
	['1L 7s'] = 'antipine',
	['2L 6s'] = 'subaric',
	['3L 5s'] = 'checkertonic',
	['4L 4s'] = 'tetrawood',
	['5L 3s'] = 'oneirotonic',
	['6L 2s'] = 'ekic',
	['7L 1s'] = 'pine',
	['1L 8s'] = 'antisubneutralic',
	['2L 7s'] = 'balzano',
	['3L 6s'] = 'tcherepnin',
	['4L 5s'] = 'gramitonic',
	['5L 4s'] = 'semiquartal',
	['6L 3s'] = 'hyrulic',
	['7L 2s'] = 'armotonic',
	['8L 1s'] = 'subneutralic',
	['1L 9s'] = 'antisinatonic',
	['2L 8s'] = 'jaric',
	['3L 7s'] = 'sephiroid',
	['4L 6s'] = 'lime',
	['5L 5s'] = 'pentawood',
	['6L 4s'] = 'lemon',
	['7L 3s'] = 'dicoid',
	['8L 2s'] = 'taric',
	['9L 1s'] = 'sinatonic'
}

-- And prefixes
p.tamnams_prefix = {
	['1L 1s'] = 'monwd',
	['2L 2s'] = 'biwd',
	['1L 5s'] = 'amech',
	['2L 4s'] = 'mal',
	['3L 3s'] = 'triwd',
	['4L 2s'] = 'citro',
	['5L 1s'] = 'mech',
	['1L 6s'] = 'on',
	['2L 5s'] = 'pel',
	['3L 4s'] = 'mosh',
	['4L 3s'] = 'smi',
	['5L 2s'] = 'dia',
	['6L 1s'] = 'arch',
	['1L 7s'] = 'apine',
	['2L 6s'] = 'subar',
	['3L 5s'] = 'check',
	['4L 4s'] = 'tetrawd',
	['5L 3s'] = 'oneiro',
	['6L 2s'] = 'ek',
	['7L 1s'] = 'pine',
	['1L 8s'] = 'ablu',
	['2L 7s'] = 'bal',
	['3L 6s'] = 'cher',
	['4L 5s'] = 'gram',
	['5L 4s'] = 'cthon',
	['6L 3s'] = 'hyru',
	['7L 2s'] = 'arm',
	['8L 1s'] = 'blu',
	['1L 9s'] = 'asina',
	['2L 8s'] = 'jara',
	['3L 7s'] = 'seph',
	['4L 6s'] = 'lime',
	['5L 5s'] = 'pentawd',
	['6L 4s'] = 'lem',
	['7L 3s'] = 'dico',
	['8L 2s'] = 'tara',
	['9L 1s'] = 'sina'
}

-- And abbrevs
p.tamnams_abbrev = {
	['1L 1s'] = 'wood',
	['2L 2s'] = 'bw',
	['1L 5s'] = 'amech',
	['2L 4s'] = 'mal',
	['3L 3s'] = 'trw',
	['4L 2s'] = 'cit',
	['5L 1s'] = 'mech',
	['1L 6s'] = 'on',
	['2L 5s'] = 'pel',
	['3L 4s'] = 'mosh',
	['4L 3s'] = 'smi',
	['5L 2s'] = 'dia',
	['6L 1s'] = 'arch',
	['1L 7s'] = 'apine',
	['2L 6s'] = 'subar',
	['3L 5s'] = 'chk',
	['4L 4s'] = 'ttw',
	['5L 3s'] = 'onei',
	['6L 2s'] = 'ek',
	['7L 1s'] = 'pine',
	['1L 8s'] = 'ablu',
	['2L 7s'] = 'bal',
	['3L 6s'] = 'ch',
	['4L 5s'] = 'gram',
	['5L 4s'] = 'cth',
	['6L 3s'] = 'hyru',
	['7L 2s'] = 'arm',
	['8L 1s'] = 'blu',
	['1L 9s'] = 'asi',
	['2L 8s'] = 'jar',
	['3L 7s'] = 'seph',
	['4L 6s'] = 'lime',
	['5L 5s'] = 'pw',
	['6L 4s'] = 'lem',
	['7L 3s'] = 'dico',
	['8L 2s'] = 'tar',
	['9L 1s'] = 'si'
}

-- TAMNAMS equave-agnostic names
p.tamnams_equave_agnostic_name = {
	['1L 1s'] = 'trivial',
	['1L 2s'] = 'antrial',
	['2L 1s'] = 'trial',
	['1L 3s'] = 'antetric',
	['3L 1s'] = 'tetric',
	['1L 4s'] = 'pedal',
	['2L 3s'] = 'pentic',
	['3L 2s'] = 'anpentic',
	['4L 1s'] = 'manual'
}

-- And prefixes
p.tamnams_equave_agnostic_prefix = {
	['1L 1s'] = 'triv',
	['1L 2s'] = 'atri',
	['2L 1s'] = 'tri',
	['1L 3s'] = 'atetra',
	['3L 1s'] = 'tetra',
	['1L 4s'] = 'ped',
	['2L 3s'] = 'pent',
	['3L 2s'] = 'apent',
	['4L 1s'] = 'manu'
}

-- And abbrevs
p.tamnams_equave_agnostic_abbrev = {
	['1L 1s'] = 'trv',
	['1L 2s'] = 'atri',
	['2L 1s'] = 'tri',
	['1L 3s'] = 'att',
	['3L 1s'] = 'tt',
	['1L 4s'] = 'ped',
	['2L 3s'] = 'pt',
	['3L 2s'] = 'apt',
	['4L 1s'] = 'manu'
}

--------------------------------------------------------------------------------
------------------------------ HELPER FUNCTIONS --------------------------------
--------------------------------------------------------------------------------

-- Step ratios are entered as an array of two numeric values, or alternatively,
-- as a ratio as defined by the rational module. If of the former, this helper
-- function converts it to the latter. This preprocess step is for simplifying
-- ratios.
function p.preprocess_step_ratio(step_ratio)
	if type(step_ratio) == "string" then
		return step_ratio
	elseif (type(step_ratio) == "table" and type(step_ratio[1]) == 'number' and type(step_ratio[2]) == 'number') then
		return rat.new(step_ratio[1], step_ratio[2])
	else
		return nil
	end
end

-- Mosses for name lookup are entered either as a scalesig or as a mos as
-- defined in the mos module. If of the latter, it's converted into a textual
-- scalesig.
function p.preprocess_scalesig(input_mos)
	if type(input_mos) == "string" then
		return input_mos
	elseif type(input_mos) == "table" then
		return mos.as_string(input_mos)
	else
		return nil
	end
end

--------------------------------------------------------------------------------
----------------------------- LOOKUP FUNCTIONS ---------------------------------
--------------------------------------------------------------------------------

-- Function for looking up a mos's name (octave-equivalent mosses only).
-- Can accept either a mos (defined by mos module) or its scalesig.
function p.lookup_name(input_mos) 
	local scalesig = p.preprocess_scalesig(input_mos)
	return p.tamnams_name[scalesig]
end

-- Function for looking up a mos's prefix (octave-equivalent mosses only).
-- Can accept either a mos (defined by mos module) or its scalesig.
function p.lookup_prefix(input_mos)
	local scalesig = p.preprocess_scalesig(input_mos)
	return p.tamnams_prefix[scalesig]
end

-- Function for looking up a mos's abbrev (octave-equivalent mosses only).
-- Can accept either a mos (defined by mos module) or its scalesig.
function p.lookup_abbrev(input_mos) 
	local scalesig = p.preprocess_scalesig(input_mos)
	return p.tamnams_abbrev[scalesig]
end

-- Function for looking up a step ratio range
-- Module:Rational is used to help simplify ratios
function p.lookup_step_ratio(step_ratio, use_extended)
	local step_ratio = p.preprocess_step_ratio(step_ratio)
	local use_extended = use_extended == true
	
	-- Produce the key needed to lookup the step ratio name
	-- use_extended is used to toggle between central range and extended range
	local key = rat.as_ratio(step_ratio, ':')
	local named_ratio = use_extended and p.tamnams_ratios_ext[key] or p.tamnams_ratios[key]
	
	return named_ratio ~= nil and named_ratio or key
end

-- Function for looking up a step ratio range
-- Module:Rational is used to help simplify ratios
function p.lookup_step_ratio_range(step_ratio_1, step_ratio_2, use_extended)
	local step_ratio_1 = p.preprocess_step_ratio(step_ratio_1)
	local step_ratio_2 = p.preprocess_step_ratio(step_ratio_2)
	local use_extended = use_extended == true
	
	-- Produce the key needed for the lookup table as a/b to c/d
	-- Swap ratios if ratio 1 has a higher hardness than ratio 2
	local key = ""
	local float_1 = rat.as_float(step_ratio_1)
	local float_2 = rat.as_float(step_ratio_2)
	if (float_1 > float_2) then
		key = string.format('%s to %s', rat.as_ratio(step_ratio_2, ':'), rat.as_ratio(step_ratio_1, ':'))
	else
		key = string.format('%s to %s', rat.as_ratio(step_ratio_1, ':'), rat.as_ratio(step_ratio_2, ':'))
	end
	
	-- use_extended is used to toggle between central range and extended range
	local named_ratio_range = use_extended and p.tamnams_ranges_ext[key] or p.tamnams_ranges[key]
	
	return named_ratio_range ~= nil and named_ratio_range or key
end

-- Given a mos, find the ancestor mos within the target note count.
function p.find_ancestor(input_mos, target_note_count)
	local target_note_count = target_note_count or 10
	
	local z = input_mos.nL
	local w = input_mos.ns
	
	while (z ~= w) and (z + w > target_note_count) do
		local m1 = math.max(z, w)
		local m2 = math.min(z, w)
		
		-- For use with updating ancestor mos chunks
		local z_prev = z
		
		-- Update step ratios
		z = m2
		w = m1 - m2
	end
	
	return p.new(z, w, mos.equave)
end

-- Given a mos, find the ancestor mos within the target note count, while also
-- returning the step ancestor's step ratio range (as two ratios) and the number
-- of generations between the two mosses. (A more in-depth version of the prev.)
function p.find_ancestor_info(input_mos, target_step_count)
	local target_step_count = target_step_count or 10
	
	-- For an ancestor mos zU wv and descendant xL ys, how many steps of size
	-- L and s can fit inside U and v? (basically the chunking operation)
	local z = input_mos.nL
	local w = input_mos.ns
	local lg_chunk = { nL = 1, ns = 0 }
	local sm_chunk = { nL = 0, ns = 1 }
	local generations = 0

	while (z ~= w) and (z + w > target_step_count) do
		local m1 = math.max(z, w)
		local m2 = math.min(z, w)
		
		-- For use with updating ancestor mos chunks
		local z_prev = z
		
		-- Count how many generations
		generations = generations + 1
		
		-- Update step ratios
		z = m2
		w = m1 - m2
		
		-- Update large chunk
		local prev_lg_chunk = { nL = lg_chunk.nL, ns = lg_chunk.ns }
		lg_chunk.nL = lg_chunk.nL + sm_chunk.nL
		lg_chunk.ns = lg_chunk.ns + sm_chunk.ns
		
		-- Update small chunk
		if z ~= z_prev then
			sm_chunk = prev_lg_chunk
		end
	end
	
	-- Translate chunks into step ratios
	local num1 = lg_chunk.nL + lg_chunk.ns
	local den1 = sm_chunk.nL + sm_chunk.ns
	local num2 = lg_chunk.nL
	local den2 = sm_chunk.nL
	local ratio_1, ratio_2
	if num1/den1 < num2/den2 then
		ratio_1 = { num1, den1 }
		ratio_2 = { num2, den2 }
	else
		ratio_2 = { num1, den1 }
		ratio_1 = { num2, den2 }
	end
	
	return mos.new(z, w, input_mos.equave), ratio_1, ratio_2, generations
end

--------------------------------------------------------------------------------
--------------------- MOSSTEP/MOSDEGREE QUALITY FUNCTIONS ----------------------
--------------------------------------------------------------------------------

-- Given an interval vector for a mos, produce the name for that interval.
-- Prefix lookup is done automatically if no prefix is provided; defaults to
-- "mos" if no prefix is found. Prefixes are used for the full name for an
-- interval, whereas abbrevs are used for the abbreviated form. (This is
-- because some abbrevs are shorter than the corresponding prefix.)
-- Formats are as follows:
-- - NONE: full name, eg "perfect 4-diastep"
-- - SENTENCE-CASE: same as NONE, but with capitalized first letter
-- - SHORTENED: shortened form, eg "Perf. 4-diastep"
-- - ABBREV: abbreviated form, eg "P4dias"
function p.interval_quality(interval, input_mos, abbrev_format, mos_prefix)
	local abbrev_format = abbrev_format or "none"
	local mos_prefix = mos_prefix 
		or (abbrev_format == "abbrev" and p.lookup_abbrev(input_mos) or p.lookup_prefix(input_mos))
		or (abbrev_format == "abbrev" and "m" or "mos")
	
	-- Get the step count of the interval. The sum of L's and s's will always
	-- determine what k-mosstep the interval is.
	local step_count = mos.interval_step_count(interval)
	
	-- Decode the quality
	local quality = p.decode_quality(interval, input_mos, abbrev_format)
	
	if abbrev_format == "abbrev" or abbrev_format == "ABBREV" then
		return string.format("%s%d%ss", quality, step_count, mos_prefix)
	elseif abbrev_format == "shortened" or abbrev_format == "SHORTENED" then
		return string.format("%s %d-%ss.", quality, step_count, mos_prefix)
	elseif abbrev_format == "sentence-case" or abbrev_format == "SENTENCE-CASE" then
		return string.format("%s %d-%sstep", quality, step_count, mos_prefix)
	else
		return string.format("%s %d-%sstep", quality, step_count, mos_prefix)
	end
end

-- Given an interval vector for a mos, produce the name for the scale degree
-- reached by going up that interval, from the root. (This is identical to the
-- previous function, except it uses "degree" instead of "step".)
-- Prefix lookup is done automatically, as with interval_quality().
-- Formats are as follows:
-- - NONE: full name, eg "perfect 4-diadegree"
-- - SENTENCE-CASE: same as NONE, but with capitalized first letter
-- - SHORTENED: shortened form, eg "Perf. 4-diadegree"
-- - ABBREV: abbreviated form, eg "P4diad"
function p.degree_quality(interval, input_mos, abbrev_format, mos_prefix)
	local abbrev_format = abbrev_format or "none"
	local mos_prefix = mos_prefix 
		or (abbrev_format == "abbrev" and p.lookup_abbrev(input_mos) or p.lookup_prefix(input_mos))
		or (abbrev_format == "abbrev" and "m" or "mos")
	
	-- Get the step count of the interval. The sum of L's and s's will always
	-- determine what k-mosstep the interval is.
	local step_count = mos.interval_step_count(interval)
	
	-- Decode the quality
	local quality = p.decode_quality(interval, input_mos, abbrev_format)
	
	if abbrev_format == "abbrev" or abbrev_format == "ABBREV" then
		return string.format("%s%d%sd", quality, step_count, mos_prefix)
	elseif abbrev_format == "shortened" or abbrev_format == "SHORTENED" then
		return string.format("%s %d-%sd.", quality, step_count, mos_prefix)
	elseif abbrev_format == "sentence-case" or abbrev_format == "SENTENCE-CASE" then
		return string.format("%s %d-%sdegree", quality, step_count, mos_prefix)
	else
		return string.format("%s %d-%sdegree", quality, step_count, mos_prefix)
	end
end

-- Decodes the quality of a mosstep. Helper function to interval_quality() and
-- degree_quality(), but can be used standalone if only the keyword (maj, min,
-- aug, perf, dim) is needed. The chroma amounts are as follows:
-- AMT| PERFECTABLE		| NONPERFECTABLE	| DARK GEN ONLY
-- ---+-----------------+-------------------+------------------
-- ...|    . . .        |    . . .          |    . . .
--  4 | 4x augmented	| 4x augmented		| 5x augmented
--  3 | 3x augmented	| 3x augmented		| 4x augmented
--  2 | 2x augmented	| 2x augmented		| 3x augmented
--  1 | augmented		| augmented			| 4x augmented
--  0 | perfect			| major				| augmented
-- -1 | diminished		| minor				| perfect
-- -2 | 2x diminished	| diminished		| diminished
-- -3 | 3x diminished	| 2x diminished		| 2x diminished
-- -4 | 4x diminished	| 3x diminished		| 3x diminished
-- -5 | 5x diminished	| 4x diminished		| 4x diminished
-- ...|    . . .        |    . . .          |    . . .
function p.decode_quality(interval, input_mos, abbrev_format)
	local abbrev_format = abbrev_format or "none"		-- Default is no abbreviation

	-- Normalize the interval so negative values aren't being used.
	local interval = mos.normalize_interval(interval)
	
	-- Get the step count of the interval. The sum of L's and s's will always
	-- determine what k-mosstep the interval is.
	local step_count = mos.interval_step_count(interval)
	
	-- Determine what "special" type the interval is so that the designations
	-- of augmented/perfect/diminished (APd) apply, skipping major/minor (Mm).
	-- If it's the period or equave, then it's a multiple of the period. 
	-- If it's any one of the gens, then reducing it should produce that gen.
	local is_period     = step_count % mos.period_step_count(input_mos) == 0
	local is_bright_gen = step_count % mos.period_step_count(input_mos) == mos.bright_gen_step_count(input_mos)
	local is_dark_gen   = step_count % mos.period_step_count(input_mos) == mos.dark_gen_step_count(input_mos)
	
	-- Special case: APd does not apply to a root mos's (nL ns) generators;
	-- instead, it's Mm.
	local is_root_mos = input_mos.nL == input_mos.ns
	
	-- Is perfectable? This is for intervals for which maj/min does not apply.
	local is_perfectable = is_period or (is_bright_gen and not is_root_mos) or (is_dark_gen and not is_root_mos)
	
	-- Get chroma count and adjust as needed
	local chroma_count = 0
	if is_period then
		-- Chroma count 0 is the perfect size. This interval does not appear
		-- as any other size across all mos modes.
		chroma_count = mos.interval_chroma_count(interval, input_mos)
	elseif is_bright_gen and not is_root_mos then
		-- Chroma count 0 is the large size, and -1 the small size; these
		-- are perfect and diminished respectively.
		chroma_count = mos.interval_chroma_count(interval, input_mos)
	elseif is_dark_gen and not is_root_mos then
		-- Chroma count 0 is the large size, and -1 the small size; these
		-- are augmented and perfect respectively. Since the perfect size
		-- corresponds to a chroma count of -1, pass in -1 as the 3rd arg.
		chroma_count = mos.interval_chroma_count(interval, input_mos, -1)
	else
		-- Chroma count 0 is the large size, and -1 the small size; these are
		-- major and minor respectively.
		chroma_count = mos.interval_chroma_count(interval, input_mos)
	end
	
	-- Get absolute value of chroma count
	local chroma_abs = math.abs(chroma_count)
	
	local quality = ""
	if is_perfectable then
		-- Get the quality for perfectable intervals
		
		if abbrev_format == "none" or abbrev_format == "NONE" then
			if chroma_count < 0 then
				quality = "diminished"
			elseif chroma_count > 0 then
				quality = "augmented"
			else
				quality = "perfect"
			end
			
			if chroma_abs > 1 then
				quality = string.format("%d× %s", chroma_abs, quality)
			end
		elseif abbrev_format == "sentence-case" or abbrev_format == "SENTENCE-CASE" then
			if chroma_count < 0 then
				quality = "Diminished"
			elseif chroma_count > 0 then
				quality = "Augmented"
			else
				quality = "Perfect"
			end
			
			if chroma_abs > 1 then
				quality = string.format("%d× %s", chroma_abs, quality)
			end
		elseif abbrev_format == "shortened" or abbrev_format == "SHORTENED" then
			if chroma_count < 0 then
				quality = "Dim."
			elseif chroma_count > 0 then
				quality = "Aug."
			else
				quality = "Perf."
			end
			
			if chroma_abs > 1 then
				quality = string.format("%d× %s", chroma_abs, quality)
			end
		elseif abbrev_format == "abbrev" or abbrev_format == "ABBREV" then
			if chroma_count < 0 then
				quality = "d"
			elseif chroma_count > 0 then
				quality = "A"
			else
				quality = "P"
			end
			
			if chroma_abs > 3 then
				quality = string.format("%s<sup>%d</sup>", quality, chroma_abs)
			elseif chroma_abs > 1 and chroma_abs <= 3 then
				quality = string.rep(quality, chroma_abs)
			end
		end
	else
		-- Get the quality for nonperfectable intervals
		
		-- Is the interval major? If not, decrement chroma_abs by 1
		local is_positive = chroma_count >= 0
		chroma_abs = is_positive and chroma_abs or chroma_abs - 1
		
		if abbrev_format == "none" or abbrev_format == "NONE" then
			if chroma_abs > 0 and is_positive then
				quality = "augmented"
			elseif chroma_abs > 0 and not is_positive then
				quality = "diminished"
			else
				quality = is_positive and "major" or "minor"
			end
			
			if chroma_abs > 1 then
				quality = string.format("%d× %s", chroma_abs, quality)
			end
		elseif abbrev_format == "sentence-case" or abbrev_format == "SENTENCE-CASE" then
			if chroma_abs > 0 and is_positive then
				quality = "Augmented"
			elseif chroma_abs > 0 and not is_positive then
				quality = "Diminished"
			else
				quality = is_positive and "Major" or "Minor"
			end
			
			if chroma_abs > 1 then
				quality = string.format("%d× %s", chroma_abs, quality)
			end
		elseif abbrev_format == "shortened" or abbrev_format == "SHORTENED" then
			if chroma_abs > 0 and is_positive then
				quality = "Aug."
			elseif chroma_abs > 0 and not is_positive then
				quality = "Dim."
			else
				quality = is_positive and "Maj." or "Min."
			end
			
			if chroma_abs > 1 then
				quality = string.format("%d× %s", chroma_abs, quality)
			end
		elseif abbrev_format == "abbrev" or abbrev_format == "ABBREV" then
			if chroma_abs > 0 and is_positive then
				quality = "A"
			elseif chroma_abs > 0 and not is_positive then
				quality = "d"
			else
				quality = is_positive and "M" or "m"
			end
			
			if chroma_abs > 3 then
				quality = string.format("%s<sup>%d</sup>", quality, chroma_abs)
			elseif chroma_abs > 1 and chroma_abs <= 3 then
				quality = string.rep(quality, chroma_abs)
			end
		end
	end
	
	return quality
end

--------------------------------------------------------------------------------
------------------------- MODE NOTATION FUNCTIONS ------------------------------
--------------------------------------------------------------------------------

-- Given an input mos, list the udps for each of its modes.
function p.mos_mode_udps(input_mos)
	local steps_per_period = mos.period_step_count(input_mos)
	local period_count = mos.period_count(input_mos)
	
	local udps = {}
	for i = 1, steps_per_period do
		local gens_up = steps_per_period - i
		local gens_down = steps_per_period - gens_up - 1
		local udp = string.format("%s&#124;%s", gens_up, gens_down)
		
		if period_count > 1 then
			udp = udp .. string.format("(%s)", period_count)
		end
		table.insert(udps, udp)
	end
	return udps
end

-- Given an input mos, list the cpos for each of its modes. The circular
-- permutation orderings are listed starting from the brightest mode.
-- Example with 5L 2s modes
--  MODE NAME  | UDP | CPO 
-- ------------+-----+-----
--  Lydian     | 6|0 | 1
--  Ionian     | 5|1 | 5
--  Mixolydian | 4|2 | 2
--  Dorian     | 3|3 | 6
--  Aeolian    | 2|4 | 3
--  Phrygian   | 1|5 | 7
--  Lociran    | 0|6 | 4
function p.mos_mode_cpos(input_mos)
	local steps_per_period = mos.period_step_count(input_mos)
	local period_count = mos.period_count(input_mos)
	local steps_per_bright_gen = mos.bright_gen_step_count(input_mos)
	
	local cpos = {}
	for i = 1, steps_per_period do
		local cpo = ((i - 1) * steps_per_bright_gen) % steps_per_period + 1
		table.insert(cpos, cpo)
	end
	return cpos
end

-- Given a string that represents a mode, return the udps for each of its
-- rotations. If the mode is a modmos, the closest mode and its alterations
-- are returned as a string.
-- NOTES:
-- - It's advised to use this for modmosses as this is less efficient if
--   used on true-mos modes.
-- - There will always be as many modes as there are steps in a period of
--   repetition, even if a modmos's period of repetition spans more than one
--   period of the original mos. EG, LLsLsLss and LLssLLss are modmosses of
--   4L 4s (LsLsLsLs), and their periods are 8, 4, and 2 steps respectively,
--   meaning they have 8, 4, and 2 unique modes respectively. Also, only the 1st
--   period of repetition is needed, as other modes reached by rotation after
--   that are literally redendant.
function p.mode_rotation_udps(input_mode, input_mos)
	local modes = mos.mode_rotations(input_mode)
	
	local udps = {}
	for i = 1, #modes do
		table.insert(udps, p.mode_udp(modes[i], input_mos))
	end
	return udps
end

-- Given a string that represents a mode, return its udp.
-- Helper function for mode_rotation_udps().
-- If a mode is for a modmos, it will return the closest brightest mode followed
-- by its altered scale degrees.
function p.mode_udp(input_mode, input_mos)
	local true_modes = mos.modes_to_step_matrices(input_mos)
	local input_mode_as_step_matrix = mos.mode_to_step_matrix(input_mode)
	
	-- For each mode, count the number of differences between each true mode
	-- and the entered mode and keep track of which mode has the fewest diffs.
	-- If the number of diffs is ever zero, then the entered mode was a true-mos
	-- mode and has zero alterations.
	local lowest_differences = mos.equave_step_count(input_mos)
	local bright_gens_down_per_period = 0
	local closest_mode_as_step_matrix = {}
	for i = 1, #true_modes do
		local differences = 0
		local current_true_mode = true_modes[i]
		
		for j = 1, #input_mode_as_step_matrix do
			local mode_interval = input_mode_as_step_matrix[j]
			local true_interval = current_true_mode[j] 
			
			if not mos.interval_eq(mode_interval, true_interval) then
				differences = differences + 1
			end
		end
		
		if differences < lowest_differences then
			bright_gens_down_per_period = i - 1
			lowest_differences = differences
			closest_mode_as_step_matrix = current_true_mode
		end
	end
	
	-- Produce the UDP (as text) for the mode, formatted as up|dp(p) for multi-
	-- period mosses, or u|d for single-period mosses.
	local period_count = mos.period_count(input_mos)
	local dark_gens_down_per_period = mos.period_step_count(input_mos) - 1 - bright_gens_down_per_period
	udp = ""
	if period_count == 1 then
		udp = string.format("%s&#124;%s", dark_gens_down_per_period, bright_gens_down_per_period)
	else
		udp = string.format("%s&#124;%s(%s)", dark_gens_down_per_period * period_count, bright_gens_down_per_period * period_count, period_count)
	end
	
	-- Produce the list of alterations, if the mode is for a modmos.
	local alterations = ""
	local closest_true_mos_mode = true_modes[bright_gens_down_per_period + 1]
	if lowest_differences > 0 then
		for i = 1, #input_mode_as_step_matrix do
			mode_interval = input_mode_as_step_matrix[i]
			true_interval = closest_mode_as_step_matrix[i]
			
			if not mos.interval_eq(mode_interval, true_interval) then
				altered_degree = p.degree_quality(mode_interval, input_mos, "ABBREV")
				alterations = alterations .. " " .. altered_degree
			end
		end
	end
	
	return udp .. alterations
end

--------------------------------------------------------------------------------
----------------------------- TESTER FUNCTION ----------------------------------
--------------------------------------------------------------------------------

function p.tester()
	local mos_modes = mos.modes_by_brightness(mos.new(5,2))
	local output_ = ""
	for i = 1, #mos_modes do
		output_ = output_ .. mos_modes[i] .. " " .. p.mode_udp(mos_modes[i], mos.new(5,2)) .. "\n"
	end
	
	output_ = output_ .. "LLsLsAs" .. " " .. p.mode_udp("LLsLsAs", mos.new(5,2))
	
	return p.mode_rotation_udps("LLsLsAs", mos.new(5,2))
	
	
end

return p