Module:MOS

Revision as of 23:06, 11 October 2025 by Ganaram inukshuk (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Module documentation[view] [edit] [history] [purge]
This module primarily serves as a library for other modules and has no corresponding template.

This module provides functions for working with MOS scales in Lua code.


Introspection summary for Module:MOS 
Functions provided (79)
Line Function Params
14 new (nL, ns, equave)
24 parse (unparsed)
42 is_valid (mos)
47 is_octave_equivalent (mos)
53 is_root_mos (mos)
66 as_string (mos, use_nbsp)
84 as_long_string (mos, use_nbsp)
100 as_link (mos)
112 as_long_link (mos)
119 interval_as_string (interval)
153 equave_as_string (mos)
158 equave_as_enclosed_string (mos)
168 parent (mos)
173 root (mos)
179 children (mos)
184 sister (mos)
190 neutralized (mos)
199 interleaved (mos)
210 brightest_mode (mos)
233 darkest_mode (mos)
261 mode_by_brightness (mos, bright_gens_down)
266 modes_by_brightness (mos)
288 mode_rotations (mode_string)
302 rotate_mode (mode_string, shift_amt)
315 mode_to_step_matrix (mode_string)
333 modes_to_step_matrices (mos)
345 mode_rotations_to_step_matrices (mode_string)
357 modal_union (input_mos)
388 bright_gen (mos)
421 dark_gen (mos)
428 period (mos)
439 equave (mos)
454 unison none
460 chroma none
465 augmented_step none
470 large_step none
475 small_step none
480 diminished_step none
489 interval_from_step_counts (i, j)
499 interval_from_mos (mos, step_count, size_offset)
520 interval_from_step_sequence (step_sequence)
547 step_count (mos)
552 bright_gen_step_count (mos)
558 dark_gen_step_count (mos)
563 period_step_count (mos)
569 equave_step_count (mos)
574 period_count (mos)
581 interval_step_count (interval)
593 interval_chroma_count (interval, mos, size_offset)
606 interval_add (interval_1, interval_2)
614 interval_sub (interval_1, interval_2)
622 interval_mul (interval, amt)
630 interval_eq (interval_1, interval_2)
639 period_complement (interval, mos)
647 equave_complement (interval, mos)
657 period_reduce (interval, mos)
667 equave_reduce (interval, mos)
676 invert_interval (interval)
682 normalize_interval (interval)
694 as_et (mos, step_ratio, suffix)
702 bright_gen_to_et_steps (mos, step_ratio)
707 dark_gen_to_et_steps (mos, step_ratio)
712 period_to_et_steps (mos, step_ratio)
717 equave_to_et_steps (mos, step_ratio)
722 interval_to_et_steps (interval, step_ratio)
731 et_suffix (mos)
744 et_string (mos, step_ratio, suffix)
752 bright_gen_to_et_string (mos, step_ratio, suffix)
758 dark_gen_to_et_string (mos, step_ratio, suffix)
764 period_to_et_string (mos, step_ratio, suffix)
772 reduced_period_to_et_string (mos, suffix)
778 equave_to_et_string (mos, step_ratio, suffix)
785 interval_to_et_string (interval, mos, step_ratio, suffix)
796 bright_gen_to_cents (mos, step_ratio)
803 dark_gen_to_cents (mos, step_ratio)
812 period_to_cents (mos)
820 equave_to_cents (mos)
825 interval_to_cents (interval, mos, step_ratio)
836 tester none
Lua modules required (3)
Variable Module Functions used
et Module:ET new
as_string
backslash_display
rat Module:Rational parse
eq
new
as_ratio
cents
utils Module:Utils _gcd
_round_dec
table_contains

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


-- This module follows [[User:Ganaram inukshuk/Provisional style guide for Lua]]
local et    = require("Module:ET")
local rat   = require("Module:Rational")
local utils = require("Module:Utils")

local p = {}

--------------------------------------------------------------------------------
----------------------------- MOS-CREATING FUNCTIONS ---------------------------
--------------------------------------------------------------------------------

-- Create a new mos as a table containing the counts for large and small steps,
-- plus the equave.
function p.new(nL, ns, equave)
	local nL = nL or 5
	local ns = ns or 2
	local equave = equave or 2
	
	return { nL = nL, ns = ns, equave = equave }
end

-- Parse a mos from its scalesig "xL ys<p/q>" or "xL ys (p/q-equivalent)".
-- If no equave "p/q" is provided, it's assumed to be 2/1-equivalent.
function p.parse(unparsed)
	local nL, ns, equave = unparsed:match("^(%d+)[Ll].-(%d+)[Ss]%s*(.*)$")
	nL = tonumber(nL)
	ns = tonumber(ns)
	equave = equave:match("^%((.*)-equivalent%)$") or equave:match("^⟨(.*)⟩$") or equave:match("^<(.*)>$") or "2/1" -- Assumes this is a rational ratio written a/b
	equave = rat.parse(equave)
	if nL == nil or ns == nil or equave == nil then
		return nil
	end
	
	return p.new(nL, ns, equave)
end

--------------------------------------------------------------------------------
---------------------- VALIDATION AND CHECKING FUNCTIONS -----------------------
--------------------------------------------------------------------------------

-- Is the mos xL ys valid (x and y are greater than 0)?
function p.is_valid(mos)
	return mos.nL > 0 and mos.ns > 0
end

-- Is the mos xL ys octave-equivalent?
function p.is_octave_equivalent(mos)
	return rat.eq(mos.equave, rat.new(2))
end

-- Is the mos nL ns? (Root mos, with root in the sense of being the root of
-- the scale tree.)
function p.is_root_mos(mos)
	return mos.nL == mos.ns
end

--------------------------------------------------------------------------------
---------------------------- STRING/LINK FUNCTIONS -----------------------------
--------------------------------------------------------------------------------

-- Construct a string representation (scalesig) for a MOS structure.
-- Scalesig is "xL ys <p/q>" for valid mosses, omitting <p/q> for 2/1 scales.
-- Degenerate mosses (nL 0s or 0L ns) produce a string for its corresponding
-- et (n-ed-p/q).
-- Option to use nbsp is provided using the second param; default is nbsp.
function p.as_string(mos, use_nbsp)
	if p.is_valid(mos) then
		local use_nbsp = (use_nbsp == nil and true or use_nbsp)
		local suffix = ""
		if not rat.eq(mos.equave, 2) then
			suffix = "⟨" .. rat.as_ratio(mos.equave):lower() .. "⟩"
		end
		return mos.nL .. "L" .. (use_nbsp and "&nbsp;" or " ") .. mos.ns .. "s" .. suffix
	else
		return math.max(mos.nL, mos.ns) .. p.et_suffix(mos)
	end
end

-- Construct a longer string representation for a MOS structure.
-- Scalesig is "xL ys", or "xL ys (p/q-equivalent)" for nonoctave scales.
-- Degenerate mosses (nL 0s or 0L ns) produce a string for its corresponding
-- et (n-ed-p/q).
-- Option to use nbsp is provided using the second param; default is nbsp.
function p.as_long_string(mos, use_nbsp)
	if p.is_valid(mos) then
		local use_nbsp = (use_nbsp ~= nil and use_nbsp or true)
		local suffix = ""
		if not rat.eq(mos.equave, 2) then
			suffix = (use_nbsp and "&nbsp;" or " ") .. string.format("(%s-equivalent)", rat.as_ratio(mos.equave):lower())
		end
		return mos.nL .. "L" .. (use_nbsp and "&nbsp;" or " ") .. mos.ns .. "s" .. suffix
	else
		return math.max(mos.nL, mos.ns) .. p.et_suffix(mos)
	end
end

-- Construct the link to a mos. If the mos is a degenerate (nL 0s) mos, then it
-- will link to the corresponding equal-division page n-ed-p/q and display the
-- link text as an ed, rather than a mos.
function p.as_link(mos)
	local link = p.as_long_string(mos)
	local text = p.as_string(mos)
	if link == text then
		return string.format("[[%s]]", link)
	else
		return string.format("[[%s|%s]]", link, text)
	end
end

-- Construct the link to a mos, where the displayed text is the long string
-- instead. Degenerate mosses link to the corresponding equal-division page.
function p.as_long_link(mos)
	local link = p.as_long_string(mos)
	return string.format("[[%s]]", link)
end

-- Given an interval as a vector of L's and s's, produce a string "iL + js",
-- where i and j are the quantities for L and s.
function p.interval_as_string(interval)
	
	-- Quantity of L's as a string
	local L_string = ""
	if interval["L"] == 0 then
		L_string = ""
	elseif interval["L"] == 1 then
		L_string = "L"
	else
		L_string = string.format("%dL", interval["L"])
	end
	
	-- Quantity of s's as a string
	local s_string = ""
	if math.abs(interval["s"]) == 0 then
		s_string = ""
	elseif math.abs(interval["s"]) == 1 then
		s_string = "s"
	else
		s_string = string.format("%ds", math.abs(interval["s"]))
	end
	
	if interval["L"] == 0 and interval["s"] == 0 then
		return "0"
	elseif interval["L"] == 0 and interval["s"] ~= 0 then 
		return s_string
	elseif interval["L"] ~= 0 and interval["s"] == 0 then 
		return L_string
	else
		return L_string .. (interval["s"] > 0 and " + " or " - ") .. s_string
	end
end

-- Return the equave by itself as a string.
function p.equave_as_string(mos)
	return rat.as_ratio(mos.equave)
end

-- Return the equave enclosed in brackets.
function p.equave_as_enclosed_string(mos)
	return "⟨" .. rat.as_ratio(mos.equave) .. "⟩"
end

--------------------------------------------------------------------------------
----------------------- MOS RELATIVE/OPERATION FUNCTIONS -----------------------
--------------------------------------------------------------------------------

-- Find the parent mos of a mos. May return invalid mosses (nL 0s), meant to
-- represent equal divisions of the octave (or arbitrary equave).
function p.parent(mos)
	return p.new(math.min(mos.nL, mos.ns), math.abs(mos.nL-mos.ns), mos.equave)
end

-- Find the root of a mos nxL nys as nL ns.
function p.root(mos)
	local num_periods = p.period_count(mos)
	return p.new(num_periods, num_periods, mos.equave)
end

-- Find the two child mosses of a mos xL ys as (x+y)L xs and xL x+ys.
function p.children(mos)
	return p.new(mos.nL+mos.ns, mos.nL, mos.equave), p.new(mos.nL, mos.nL+mos.ns, mos.equave)
end

-- Find the sister of a mos xL ys as yL xs.
function p.sister(mos)
	return p.new(mos.ns, mos.nL, mos.equave)
end

-- Find the neutralized form of a mos. May return invalid mosses (nL 0s), meant
-- to represent equal divisions of the octave (or arbitrary equave).
function p.neutralized(mos)
	if mos.nL > mos.ns then
		return p.new(mos.nL-mos.ns, 2*mos.ns, mos.equave)
	else
		return p.new(2*mos.nL, mos.ns-mos.nL, mos.equave)
	end
end

-- Find the two interleaved mosses of a mos xL ys as (2x+y)L ys and xL (x+2y)s.
function p.interleaved(mos)
	return p.new(mos.nL*2+mos.ns, mos.ns, mos.equave), p.new(mos.nL, mos.ns*2+mos.nL, mos.equave)
end

--------------------------------------------------------------------------------
------------------------------- MODE FUNCTIONS ---------------------------------
--------------------------------------------------------------------------------

-- Find the brightest (true-mos) mode of a mos, as a string of L's and s's.
-- Calculation is based on the definition of a Christoffel word, as the closest
-- integer approximation to line y = #s/#L*x.
function p.brightest_mode(mos)
	local nL = mos.nL
	local ns = mos.ns
	local d = utils._gcd(nL, ns)
	if d > 1 then -- use single period mos, with period as new equave
		nL = utils._round_dec(nL / d)
		ns = utils._round_dec(ns / d)
	end
	local current_L, current_s = 0, 0
	local result = ""
	while current_L < nL or current_s < ns do
		if (current_s + 1) * nL <= ns * (current_L) then
            current_s = current_s + 1
            result = result .. "s"
        else
            current_L = current_L + 1
            result = result .. "L"
        end
	end
	return string.rep(result, d)
end

-- Find the darkest true-mos mode of a mos. It's the reverse of the brightest mode.
function p.darkest_mode(mos)
	local nL = mos.nL
	local ns = mos.ns
	local d = utils._gcd(nL, ns)
	if d > 1 then -- use single period mos, with period as new equave
		nL = utils._round_dec(nL / d)
		ns = utils._round_dec(ns / d)
	end
	local current_L, current_s = 0, 0
	local result = ""
	while current_L < nL or current_s < ns do
		if (current_s + 1) * nL <= ns * (current_L) then
            current_s = current_s + 1
            result = "s" .. result		-- !esreveR
        else
            current_L = current_L + 1
            result = "L" .. result		-- !esreveR
        end
	end
	
	return string.rep(result, d)
end

-- Given a mos, return a mode based on how it's ranked by modal brightness.
-- Ordering here is based on the number of BRIGHT GENS DOWN PER PERIOD:
-- 0 is the brightest mode, 1 is 2nd brightest, etc...
-- To go by darkness, pass in p-d-1 for the 2nd arg, where p is the period count
-- and d is the number of DARK GENS UP PER PERIOD.
function p.mode_by_brightness(mos, bright_gens_down)
	return p.rotate_mode(p.brightest_mode(mos), bright_gens_down * p.bright_gen_step_count(mos))
end

-- Given a mos, list all modes in descending order of brightness.
function p.modes_by_brightness(mos)
	local bright_gen_step_count = p.bright_gen_step_count(mos)
	local period_step_count = p.period_step_count(mos)
	
	local modes = {}
	local current_mode = p.brightest_mode(mos)
	for i = 1, period_step_count do
		table.insert(modes, current_mode)
		current_mode = p.rotate_mode(current_mode, bright_gen_step_count)
	end
	
	return modes
end

-- List all unique rotations for a mode, by order of leftward shifts. Order by
-- rotation will usually give a different order compared to order by brightness,
-- but this is expected if the order isn't by brightness (EG, modmosses).
-- Note: there will always be s/p modes, where s is the number of steps in the
-- entered mode, and p is the period of repetition. At most, there will be s
-- modes, but if there is a substring of length p that repeats within the mode
-- (where s mod p = 0), then there will be p modes. If the mode has one step
-- type, then there is only one mode.
function p.mode_rotations(mode_string)
	local rotations = {}
	local current_mode = mode_string
	for i = 1, #mode_string do
		if not utils.table_contains(rotations, current_mode) then
			table.insert(rotations, current_mode)
		end
		current_mode = p.rotate_mode(current_mode)
	end
	return rotations
end

-- Rotate a mode by shifting the step sequence to the left. Negative values
-- shift it to the right. Helper function for mode_by_brightness().
function p.rotate_mode(mode_string, shift_amt)
	local shift_amt = shift_amt == nil and 1 or shift_amt % #mode_string		-- Default is 1
	local first = string.sub(mode_string, 1, shift_amt)
	local second = string.sub(mode_string, shift_amt + 1, #mode_string)
	return second .. first
end

--------------------------------------------------------------------------------
---------------------------- STEP MATRIX FUNCTIONS -----------------------------
--------------------------------------------------------------------------------

-- Convert a single mode (as a string) into a step matrix. This is a listing of
-- every interval's step vector in the mode.
function p.mode_to_step_matrix(mode_string)
	local matrix = {}
	for i = 0, #mode_string do
		local interval = p.interval_from_step_sequence(string.sub(mode_string, 0, i))
		table.insert(matrix, interval)
	end
	return matrix
end


-- TODO?: replaces mode_to_step_matrices/mode_rotations_to_step_matrices with
-- one function called modes_to_step_matrices? Encompasses functionality of both
-- functions, but step patterns for either are generated into the same function,
-- where the modes as strings are passed in.


-- Given a mos, produce every step matrix for every mode. Modes are listed in
-- order of brightness.
function p.modes_to_step_matrices(mos)
	local modes = p.modes_by_brightness(mos)
	local matrices = {}
	for i = 1, #modes do
		table.insert(matrices, p.mode_to_step_matrix(modes[i]))
	end
	
	return matrices
end

-- Given a single mode (as a string), produce the step matrices for each 
-- rotation of that mode. Modes are listed in order of rotation.
function p.mode_rotations_to_step_matrices(mode_string)
	local modes = p.mode_rotations(mode_string)
	local matrices = {}
	for i = 1, #modes do 
		table.insert(matrices, p.mode_to_step_matrix(modes[i]))
	end
	
	return matrices
end

-- Given an input mos, produce its modal union.
-- This is a listing of every interval's large and small sizes.
function p.modal_union(input_mos)
	local brightest_mode = p.brightest_mode(input_mos)
	local darkest_mode   = p.darkest_mode  (input_mos)
	local interval_count = p.equave_step_count(input_mos) + 1
	
	local modal_union = {}
	for i = 1, interval_count do
		local bright_step_seq = string.sub(brightest_mode, 1, i-1)
		local dark_step_seq   = string.sub(darkest_mode  , 1, i-1)
		
		local bright_interval = p.interval_from_step_sequence(bright_step_seq)
		local dark_interval   = p.interval_from_step_sequence(dark_step_seq  )
		
		if p.interval_eq(bright_interval, dark_interval) then
			table.insert(modal_union, bright_interval)
		else
			table.insert(modal_union, dark_interval  )
			table.insert(modal_union, bright_interval)
		end
	end
	
	return modal_union
end

--------------------------------------------------------------------------------
--------------- FUNCTIONS FOR GENERATOR AND PERIOD INTERVALS -------------------
--------------------------------------------------------------------------------

-- Compute the bright gen as a vector of L's and s's. Since all mosstep
-- intervals (excluding the root and period) have two sizes, this returns the
-- large/perfect size.
function p.bright_gen(mos)
	local nL = mos.nL
	local ns = mos.ns
	local d = utils._gcd(nL, ns)
	if d > 1 then -- use single period mos, with period as new equave
		nL = utils._round_dec(nL / d)
		ns = utils._round_dec(ns / d)
	end
	local min_dist = 2; -- the distance we get will always be <= sqrt(2)
	local current_L, current_s = 0, 0
	local result = {["L"] = 0, ["s"] = 0} 
	while current_L < nL or current_s < ns do
		if (current_s + 1) * nL <= ns * (current_L) then
            current_s = current_s + 1
        else
            current_L = current_L + 1
		end
    	if current_L < nL or current_s < ns then -- check to exclude (current_L, current_s) = (nL, ns)
    		local distance_here = math.abs(nL * current_s - ns * current_L) / math.sqrt(nL^2 + ns^2)
    		if distance_here < min_dist then
    			min_dist = distance_here
    			result["L"] = current_L
    			result["s"] = current_s
    		end
    	end
	end
	
	return result
end

-- Compute the dark gen as a vector of L's and s's. Since all mosstep
-- intervals (excluding the root and period) have two sizes, this returns the
-- small/perfect size.
function p.dark_gen(mos)
	local bright_gen = p.bright_gen(mos)
	return p.period_complement(bright_gen, mos)
end

-- Compute the period as a vector of L's and s's.
-- Period intervals as mossteps only appear as one size.
function p.period(mos) 
	local gcd = utils._gcd(mos.nL, mos.ns)
	return {
		["L"] = mos.nL / gcd,
		["s"] = mos.ns / gcd
	}
end

-- Compute the equave as a vector of L's and s's.
-- Equaves as mossteps only appear as one size. For a single-period mos, this
-- is the same as p.period().
function p.equave(mos) 
	return {
		["L"] = mos.nL,
		["s"] = mos.ns
	}
end

--------------------------------------------------------------------------------
------------------- FUNCTIONS FOR SINGLE-STEP INTERVALS ------------------------
--------------------------------------------------------------------------------

-- Return the unison as a vector of L's and s's.
-- The unison is denoted by moving up from the root by zero steps, and thus does
-- not need a mos as input. It's basically a zero vector.
-- The unison only has one size: perfect.
function p.unison()
	return { ["L"] = 0, ["s"] = 0 }
end

-- Return the vector for a single chroma. It's a large step minus a small step.
-- Adding or subtracting any interval by this interval changes its "size".
function p.chroma()
	return { ["L"] = 1, ["s"] = -1 }
end

-- Return the vector for an augmented step. It's a large step plus a chroma.
function p.augmented_step()
	return { ["L"] = 2, ["s"] = -1 }
end

-- Return the vector for a single large step.
function p.large_step()
	return { ["L"] = 1, ["s"] = 0 }
end

-- Return the vector for a single small step.
function p.small_step()
	return { ["L"] = 0, ["s"] = 1 }
end

-- Return the vector for a diminished step. It's a small step minus a chroma.
function p.diminished_step()
	return { ["L"] = -1, ["s"] = 2 }
end

--------------------------------------------------------------------------------
---------------- INTERVAL FUNCTIONS FOR ARBITRARY INTERVALS --------------------
--------------------------------------------------------------------------------

-- Create a new interval using step counts (the quantities of L's and s's).
function p.interval_from_step_counts(i, j)
	return { ["L"] = i, ["s"] = j }
end

-- Compute an arbitrary mos interval as a vector of L's and s's. Params:
-- - step_count: the number of steps subtended by the mosstep.
-- - size_offset: denotes whether to return the large size (0) or the small
--   size (-1) (or if this is a period interval, the diminished size). Values
--   other than 0 or 1 represent alterations by multiple chromas, such as
--   augmented (1) or diminished (-2).
function p.interval_from_mos(mos, step_count, size_offset)
	local size_offset = size_offset or 0		-- Optional param; defaults to large size
	local step_sequence = p.brightest_mode(mos)
	step_sequence = string.rep(step_sequence, math.ceil(step_count/(mos.nL + mos.ns)))
	step_sequence = string.sub(step_sequence, 1, step_count)
	
	local interval_vector = p.interval_from_step_sequence(step_sequence)
	local chromas = p.interval_mul(p.chroma(), size_offset)
	interval_vector = p.interval_add(interval_vector, chromas)
	
	return interval_vector
end

-- Compute an arbitrary mos interval (as a string of steps) as a vector of L's
-- and s's. This also serves as a helper function for p.interval_from_mos().
-- Sequences of steps can be entered, where each step is one of five sizes:
--  - L: large step.
--  - s: small step.
--  - c: a chroma; the difference between a large and small step.
--  - A: an augmented step; a large step plus a chroma.
--  - d: a diminished step, or diesis; a small step minus a chroma.
function p.interval_from_step_sequence(step_sequence)
	local mossteps = #step_sequence
	local interval_vector = p.unison()
	
	for i = 1, mossteps do
		local step = string.sub(step_sequence, i, i)
		if step == "L" then
			interval_vector = p.interval_add(interval_vector, p.large_step())
		elseif step == "s" or step == "S" then
			interval_vector = p.interval_add(interval_vector, p.small_step())
		elseif step == "c" then
			interval_vector = p.interval_add(interval_vector, p.chroma())
		elseif step == "A" then
			interval_vector = p.interval_add(interval_vector, p.augmented_step())
		elseif step == "d" then
			interval_vector = p.interval_add(interval_vector, p.diminished_step())
		end
	end
	
	return interval_vector
end

--------------------------------------------------------------------------------
------------------------------- COUNT FUNCTIONS --------------------------------
--------------------------------------------------------------------------------

-- Given a mos, return the number of steps.
function p.step_count(mos)
	return mos.nL + mos.ns
end

-- Given a mos, compute the number of steps in its bright gen (L's plus s's).
function p.bright_gen_step_count(mos)
	local interval = p.bright_gen(mos)
	return interval["L"] + interval["s"]
end

-- Given a mos, compute the number of steps in its dark gen (L's plus s's).
function p.dark_gen_step_count(mos)
	return p.period_step_count(mos) - p.bright_gen_step_count(mos)
end

-- Given a mos, compute the number of steps in its period (L's plus s's).
function p.period_step_count(mos)
	return (mos.nL + mos.ns) / utils._gcd(mos.nL, mos.ns)
end

-- TODO: deprecate this since "equave_step_count" is redundant and longer than
-- "step count".
function p.equave_step_count(mos)
	return mos.nL + mos.ns
end

-- Given a mos, compute the number of periods it has.
function p.period_count(mos)
	return utils._gcd(mos.nL, mos.ns)
end

-- Given a vector representing an interval, compute the number of mossteps it
-- corresponds to. Knowledge of the corresponding mos is not needed. Intervals
-- can be negative, resulting in a negative output.
function p.interval_step_count(interval)
	return interval["L"] + interval["s"]
end

-- Given a vector representing an interval, compute the number of chromas it was
-- raised or lowered by from its large size (for non-period intervals) or its
-- perfect size (for period/root/equave intervals). This requires the mos as
-- input.
-- size_offset denotes whether to count chromas from the large size; changing
-- this to -1 counts chromas from the small size. Like size_offset for
-- interval_from_mos, this can be used to denote altered mossteps (augmented,
-- diminished, etc).
function p.interval_chroma_count(interval, mos, size_offset)
	local size_offset = size_offset or 0		-- Default of 0.
	local step_count = p.interval_step_count(interval)
	local base_interval = p.interval_from_mos(mos, step_count, 0)
	
	return interval["L"] - base_interval["L"] - size_offset
end

--------------------------------------------------------------------------------
--------------- INTERVAL ARITHMETIC AND MANIPULATION FUNCTIONS -----------------
--------------------------------------------------------------------------------

-- Add two intervals together by adding their respective vectors.
function p.interval_add(interval_1, interval_2)
	return { 
		["L"] = interval_1["L"] + interval_2["L"],
		["s"] = interval_1["s"] + interval_2["s"]
	}
end
	
-- Subtract two intervals by subtracting their respective vectors.
function p.interval_sub(interval_1, interval_2)
	return { 
		["L"] = interval_1["L"] - interval_2["L"],
		["s"] = interval_1["s"] - interval_2["s"]
	}
end

-- Stack an interval, or repeatedly add the same interval to itself.
function p.interval_mul(interval, amt)
	return { 
		["L"] = interval["L"] * amt,
		["s"] = interval["s"] * amt
	}
end

-- Check whether two intervals are equal to one another.
function p.interval_eq(interval_1, interval_2)
	return 
		interval_1["L"] == interval_2["L"] and
		interval_1["s"] == interval_2["s"]
end

-- Given an interval vector and a mos, find its period complement. This is the
-- interval to add to produce the period. For single-period mosses, the period
-- complement is the same as the equave complement.
function p.period_complement(interval, mos)
	local sign = p.interval_step_count(interval) < 0 and -1 or 1
	local period_vector = p.period(mos)
	return p.interval_sub(p.interval_mul(period_vector, sign), interval)
end

-- Given an interval vector and a mos, find its equave complement. This is the
-- interval to add to produce the equave.
function p.equave_complement(interval, mos)
	local sign = p.interval_step_count(interval) < 0 and -1 or 1
	local equave_vector = p.equave(mos, interval)
	return p.interval_sub(p.interval_mul(equave_vector, sign), interval)
end

-- Given an interval vector and a mos, period-reduce it. This works like
-- modular arithmetic, so passing a negative interval returns a positive one.
-- For single-period mosses, period-reducing is the same as octave-reducing, or
-- equave-reducing (for nonoctave scales).
function p.period_reduce(interval, mos)
	local step_count = p.interval_step_count(interval)
	local reduce_amt = math.floor(step_count / p.period_step_count(mos))
	local periods = p.interval_mul(p.period(mos), reduce_amt)
	
	return p.interval_sub(interval, periods)
end

-- Given an interval vector and a mos, equave-reduce it. This works like
-- modular arithmetic, so passing a negative interval returns a positive one.
function p.equave_reduce(interval, mos)
	local step_count = p.interval_step_count(interval)
	local reduce_amt = math.floor(step_count / p.equave_step_count(mos))
	local equaves = p.interval_mul(p.equave(mos), reduce_amt)
	
	return p.interval_sub(interval, equaves)
end

-- Invert an interval. This makes an interval negative.
function p.invert_interval(interval)
	return p.interval_mul(interval, -1)
end

-- Intervals usually denote distances between two scale degrees and should be
-- positive values. Normalizing makes a negative interval positive again.
function p.normalize_interval(interval)
	return p.interval_step_count(interval) < 0 and p.interval_mul(interval, -1) or interval
end

--------------------------------------------------------------------------------
---------------------------- EQUAL-TUNING FUNCTIONS ----------------------------
--------------------------------------------------------------------------------

-- Given a mos and a step ratio, return an equal tuning (or equal division).
-- The step ratio is entered as a 2-element array to allow non-simplified
-- ratios to be entered. (The rational module isn't suitable since it simplifies
-- ratios.)
function p.as_et(mos, step_ratio, suffix)
	local suffix = suffix or nil
	local et_size = mos.nL * step_ratio[1] + mos.ns * step_ratio[2]
	return et.new(et_size, mos.equave, suffix)
end

-- Given a mos and a step ratio, return the number of et-steps for its bright
-- generator.
function p.bright_gen_to_et_steps(mos, step_ratio)
	return p.interval_to_et_steps(p.bright_gen(mos), step_ratio)
end

-- Given a mos and a step ratio, return the number of et-steps for its dark generator.
function p.dark_gen_to_et_steps(mos, step_ratio)
	return p.interval_to_et_steps(p.dark_gen(mos), step_ratio)
end

-- Given a mos and a step ratio, return the number of et-steps for its period.
function p.period_to_et_steps(mos, step_ratio)
	return p.interval_to_et_steps(p.period(mos), step_ratio)
end

-- Given a mos and a step ratio, return the number of et-steps for its equave.
function p.equave_to_et_steps(mos, step_ratio)
	return p.interval_to_et_steps(p.equave(mos), step_ratio)
end

-- Given an interval vector and step ratio, compute the number of et-steps it corresponds to.
function p.interval_to_et_steps(interval, step_ratio)
	return interval["L"] * step_ratio[1] + interval["s"] * step_ratio[2]
end

--------------------------------------------------------------------------------
------------------------ EQUAL-TUNING STRING FUNCTIONS -------------------------
--------------------------------------------------------------------------------

-- Given a mos, return its equal temperament suffix as a string (edo, edt, edf, or ed-p/q).
function p.et_suffix(mos)
	if rat.eq(mos.equave, rat.new(2)) then
		return "edo"
	elseif rat.eq(mos.equave, rat.new(3)) then
		return "edt"
	elseif rat.eq(mos.equave, rat.new(3, 2)) then
		return "edf"
	else
		return "ed" .. rat.as_ratio(mos.equave)
	end
end

-- Given a mos and step ratio, return its equal temperament as a string "{steps}\{division}{suffix}".
function p.et_string(mos, step_ratio, suffix)
	local suffix = suffix or nil
	local et_mos = p.as_et(mos, step_ratio, suffix)
	return et.as_string(et_mos)
end

-- Given a mos and step ratio, compute the number of et-steps for its bright gen
-- as a string "{steps}\{division}{suffix}".
function p.bright_gen_to_et_string(mos, step_ratio, suffix)
	return p.interval_to_et_string(p.bright_gen(mos), mos, step_ratio, suffix)
end

-- Given a mos and step ratio, compute the number of et-steps for its dark gen,
-- as a string "{steps}\{division}{suffix}".
function p.dark_gen_to_et_string(mos, step_ratio, suffix)
	return p.interval_to_et_string(p.dark_gen(mos), mos, step_ratio, suffix)
end

-- Given a mos and step ratio, compute the number of et-steps for its period,
-- as a string "{steps}\{division}{suffix}".
function p.period_to_et_string(mos, step_ratio, suffix)
	return p.interval_to_et_string(p.period(mos), mos, step_ratio, suffix)
end

-- Given a mos, compute the number of et-steps for its period, reduced,
-- as a string "{steps}\{division}{suffix}". Does not reuqire a step ratio.
-- NOTE: no such function for returning only the number of steps is needed since
-- that's the same as period_count().
function p.reduced_period_to_et_string(mos, suffix)
	return p.interval_to_et_string({["L"] = 1, ["s"] = 1}, p.root(mos), {1,0}, suffix)
end

-- Given a mos and step ratio, compute the number of et-steps for its equave,
-- as a string "{steps}\{division}{suffix}".
function p.equave_to_et_string(mos, step_ratio, suffix)
	return p.interval_to_et_string(p.equave(mos), mos, step_ratio, suffix)
end

-- Given an interval vector and step ratio, compute the number of et-steps it
-- corresponds to, as a string "{steps}\{division}{suffix}". Requires info
-- about the mos itself.
function p.interval_to_et_string(interval, mos, step_ratio, suffix)
	local suffix = suffix or nil
	local mos_et = p.as_et(mos, step_ratio, suffix)
	return et.backslash_display(mos_et, p.interval_to_et_steps(interval, step_ratio))
end

--------------------------------------------------------------------------------
------------------------------- CENT FUNCTIONS ---------------------------------
--------------------------------------------------------------------------------

-- Given a mos and a step ratio, return the number of cents for its bright gen.
function p.bright_gen_to_cents(mos, step_ratio)
	local interval_steps = p.interval_to_et_steps(p.bright_gen(mos), step_ratio)
	local equave_steps = p.equave_to_et_steps(mos, step_ratio)
	return interval_steps * rat.cents(mos.equave) / equave_steps
end

-- Given a mos and a step ratio, return the number of cents for its dark gen.
function p.dark_gen_to_cents(mos, step_ratio)
	local interval_steps = p.interval_to_et_steps(p.dark_gen(mos), step_ratio)
	local equave_steps = p.equave_to_et_steps(mos, step_ratio)
	return interval_steps * rat.cents(mos.equave) / equave_steps
end

-- Given a mos and a step ratio, return the number of cents for its period.
-- The period is the interval at which the step pattern repeats, so no step
-- ratio is needed.
function p.period_to_cents(mos)
	return rat.cents(mos.equave) / p.period_count(mos)
end

-- Given a mos and a step ratio, return the number of cents for its equave.
-- The period is the interval at which the step pattern repeats, and the equave
-- is a multiple of that (at least for multi-period mosses), so no step ratio is
-- needed.
function p.equave_to_cents(mos)
	return rat.cents(mos.equave)
end

-- Given an interval vector and step ratio, convert it to cents. This requires info about the mos itself.
function p.interval_to_cents(interval, mos, step_ratio)
	local interval_steps = p.interval_to_et_steps(interval, step_ratio)
	local equave_steps = p.equave_to_et_steps(mos, step_ratio)
	return interval_steps * rat.cents(mos.equave) / equave_steps
end

--------------------------------------------------------------------------------
----------------------------------- TESTER -------------------------------------
--------------------------------------------------------------------------------

-- Tester function
function p.tester()
	local input_mos = p.new(4,1,3)
	local step_ratio = {2,1}
	local interval_vector = {["L"] = 3, ["s"] = 1}
	--return p.as_string(input_mos, false)

	--return p.as_et(p.new(5,2), {2,1})
	
	--[[
	return 
		p.mode_by_brightness(p.new(5,2), 0) .. " " .. p.mode_by_brightness(p.new(5,2), 6-6) .. "\n" ..
		p.mode_by_brightness(p.new(5,2), 1) .. " " .. p.mode_by_brightness(p.new(5,2), 6-5) .. "\n" ..
		p.mode_by_brightness(p.new(5,2), 2) .. " " .. p.mode_by_brightness(p.new(5,2), 6-4) .. "\n" ..
		p.mode_by_brightness(p.new(5,2), 3) .. " " .. p.mode_by_brightness(p.new(5,2), 6-3) .. "\n" ..
		p.mode_by_brightness(p.new(5,2), 4) .. " " .. p.mode_by_brightness(p.new(5,2), 6-2) .. "\n" ..
		p.mode_by_brightness(p.new(5,2), 5) .. " " .. p.mode_by_brightness(p.new(5,2), 6-1) .. "\n" ..
		p.mode_by_brightness(p.new(5,2), 6) .. " " .. p.mode_by_brightness(p.new(5,2), 6-0)
		]]--
	
	return
		p.as_string(p.new(5,2))        .. "\n" ..
		p.as_string(p.new(4,5,3))      .. "\n" ..
		p.as_long_string(p.new(5,2))   .. "\n" ..
		p.as_long_string(p.new(4,5,3)) .. "\n" ..
		p.as_link(p.new(5,2))          .. "\n" ..
		p.as_link(p.new(4,5,3))        .. "\n" ..
		p.as_long_link(p.new(5,2))     .. "\n" ..
		p.as_long_link(p.new(4,5,3))   .. "\n" ..
		p.as_string(p.new(5,0))        .. "\n" ..
		p.as_string(p.new(4,0,3))      .. "\n" ..
		p.as_long_string(p.new(5,0))   .. "\n" ..
		p.as_long_string(p.new(4,0,3)) .. "\n" ..
		p.as_link(p.new(5,0))          .. "\n" ..
		p.as_link(p.new(4,0,3))        .. "\n" ..
		p.as_long_link(p.new(5,0))     .. "\n" ..
		p.as_long_link(p.new(4,0,3))   .. "\n" ..
		p.as_string(p.new(0,2))        .. "\n" ..
		p.as_string(p.new(0,5,3))      .. "\n" ..
		p.as_long_string(p.new(0,2))   .. "\n" ..
		p.as_long_string(p.new(0,5,3)) .. "\n" ..
		p.as_link(p.new(0,2))          .. "\n" ..
		p.as_link(p.new(0,5,3))        .. "\n" ..
		p.as_long_link(p.new(0,2))     .. "\n" ..
		p.as_long_link(p.new(0,5,3))
end

return p