Module:MOS: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Ganaram inukshuk (talk | contribs)
fix logic for adding npsp for scalesigs
Ganaram inukshuk (talk | contribs)
mNo edit summary
 
(31 intermediate revisions by 2 users not shown)
Line 1: Line 1:
--[[
-- This module follows [[User:Ganaram inukshuk/Provisional style guide for Lua]]
  Module for working with mosses in lua code; this serves as a "library" for
local et    = require("Module:ET")
   mos-related modules and thus does not have a corresponding template.
local rat   = require("Module:Rational")
 
local utils = require("Module:Utils")
  Functionality includes:
  - Creating/parsing mosses
  - Creating scalesigs (string representations) of mosses
  - Finding certain modes of a mos
  - Finding generators for a mos
  - Producing vectors for simple mos intervals
  - Interval arithmetic, in the form of adding vectors of L's and s's, and
    period/equave-reducing intervals
  - Finding equal tunings for mosses
]]--


local rat = require("Module:Rational")
local utils = require("Module:Utils")
local et = require("Module:ET")
local tip = require("Module:Template input parse")
local p = {}
local p = {}
--[[
  Naming scheme for function names:
  - Functions related to mosses don't have any special names.
  - Functions related to a mos's modes generally end with "mode".
  - Functions related to a mos's generators, equave, or period contain the
    corresponding interval as part of its name.
  - Functions related to intervals generally begin with "interval".
  - Interval complement/reduce functions end with "complement" and "reduce".
  - Functions that produce strings generally have the phrase "as string".
  - Functions that "count" something generally end with "count".
  - If a function requires an interval and mos as input, the interval(s) come
    after the mos.
  - Functions that have to do with equal tunings will have "et" in its name.
]]--


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


function p.find_item_in_table(table, item)
-- Create a new mos as a table containing the counts for large and small steps,
local item_found = false
-- plus the equave.
for i = 1, #table do
if table[i] == item then
item_found = true
break
end
end
return item_found
end
 
--------------------------------------------------------------------------------
-------------------------------- BASE FUNCTIONS --------------------------------
--------------------------------------------------------------------------------
 
-- Create a new mos. (Contains the number of large and small steps, and equave.)
function p.new(nL, ns, equave)
function p.new(nL, ns, equave)
local nL = nL or 5
local nL = nL or 5
Line 64: Line 20:
end
end


-- Parse a mos from its scalesig.
-- 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)
function p.parse(unparsed)
local nL, ns, equave = unparsed:match("^(%d+)[Ll]%s*(%d+)[Ss]%s*(.*)$")
local nL, ns, equave = unparsed:match("^(%d+)[Ll].-(%d+)[Ss]%s*(.*)$")
nL = tonumber(nL)
nL = tonumber(nL)
ns = tonumber(ns)
ns = tonumber(ns)
Line 79: Line 36:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
------------------------------- STRING FUNCTIONS -------------------------------
---------------------- 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.
  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.
  Scalesig is "xL ys", or "xL ys<p/q>" for nonoctave scales.
-- Degenerate mosses (nL 0s or 0L ns) produce a string for its corresponding
  Option to use nbsp is provided using the second param; default is no nbsp
-- 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)
function p.as_string(mos, use_nbsp)
local use_nbsp = use_nbsp == nil and true or use_nbsp
if p.is_valid(mos) then
local suffix = ""
local use_nbsp = (use_nbsp == nil and true or use_nbsp)
if not rat.eq(mos.equave, 2) then
local suffix = ""
suffix = "⟨" .. rat.as_ratio(mos.equave):lower() .. "⟩"
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
return "" .. mos.nL .. "L" .. (use_nbsp and "&nbsp;" or " ") .. mos.ns .. "s" .. suffix
end
end


--[[
-- Construct a longer string representation for a MOS structure.
  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
  Scalesig is "xL ys", or "xL ys (p/q-equivalent)" for nonoctave scales.
-- et (n-ed-p/q).
 
-- Option to use nbsp is provided using the second param; default is nbsp.
  Option to use nbsp is provided using the second param; default is no nbsp
]]--
function p.as_long_string(mos, use_nbsp)
function p.as_long_string(mos, use_nbsp)
local use_nbsp = use_nbsp == nil and true or use_nbsp
if p.is_valid(mos) then
local suffix = ""
local use_nbsp = (use_nbsp ~= nil and use_nbsp or true)
if not rat.eq(mos.equave, 2) then
local suffix = ""
suffix = (use_nbsp and "&nbsp;" or " ") .. string.format("(%s-equivalent)", rat.as_ratio(mos.equave):lower())
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
return "" .. mos.nL .. "L" .. (use_nbsp and "&nbsp;" or " ") .. mos.ns .. "s" .. suffix
end
end


--[[
-- Construct the link to a mos. If the mos is a degenerate (nL 0s) mos, then it
  Given an interval as a vector of L's and s's, produce a string "iL + js",
-- will link to the corresponding equal-division page n-ed-p/q and display the
  where i and j are the quantities for L and s.
-- 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)
function p.interval_as_string(interval)
Line 148: Line 148:
return L_string .. (interval["s"] > 0 and " + " or " - ") .. s_string
return L_string .. (interval["s"] > 0 and " + " or " - ") .. s_string
end
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
end


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


-- Find the parent mos of a mos
-- Find the parent mos of a mos. May return invalid mosses (nL 0s), meant to
function p.parent_mos(mos)
-- 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)
return p.new(math.min(mos.nL, mos.ns), math.abs(mos.nL-mos.ns), mos.equave)
end
end


-- Find the two child mosses of a mos; ordered as soft and hard
-- Find the root of a mos nxL nys as nL ns.
function p.child_mosses(mos)
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)
return p.new(mos.nL+mos.ns, mos.nL, mos.equave), p.new(mos.nL, mos.nL+mos.ns, mos.equave)
end
end


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


-- Is the mos valid? It should have positive-integer values for nL and ns.
-- Find the neutralized form of a mos. May return invalid mosses (nL 0s), meant
function p.is_valid_mos(mos)
-- to represent equal divisions of the octave (or arbitrary equave).
return mos.nL > 0 and mos.ns > 0
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
end


Line 177: Line 204:
------------------------------- MODE FUNCTIONS ---------------------------------
------------------------------- MODE FUNCTIONS ---------------------------------
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--[[
 
  Find the brightest (true-mos) mode of a mos, as a string of L's and s's.
-- 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
  Calculation is based on the definition of a Christoffel word, as the closest
-- integer approximation to line y = #s/#L*x.
  integer approximation to line y = #s/#L*x.
]]--
function p.brightest_mode(mos)
function p.brightest_mode(mos)
local nL = mos.nL
local nL = mos.nL
Line 204: Line 229:
return string.rep(result, d)
return string.rep(result, d)
end
end


-- Find the darkest true-mos mode of a mos. It's the reverse of the brightest mode.
-- Find the darkest true-mos mode of a mos. It's the reverse of the brightest mode.
Line 230: Line 254:
end
end


--[[
-- Given a mos, return a mode based on how it's ranked by modal brightness.
  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...
  Ordering here is based on the number of bright gens going DOWN PER PERIOD:
-- To go by darkness, pass in p-d-1 for the 2nd arg, where p is the period count
  0 is the brightest mode, 1 is 2nd brightest, etc...
-- and d is the number of DARK GENS UP PER PERIOD.
]]--
function p.mode_by_brightness(mos, bright_gens_down)
function p.mode_from_mos(mos, bright_gens_down)
return p.rotate_mode(p.brightest_mode(mos), bright_gens_down * p.bright_gen_step_count(mos))
return p.rotate_mode(p.brightest_mode(mos), bright_gens_down * p.bright_gen_step_count(mos))
end
end
--------------------------------------------------------------------------------
--------------------------- MODE ROTATION FUNCTIONS ----------------------------
--------------------------------------------------------------------------------


-- Given a mos, list all modes in descending order of brightness.
-- Given a mos, list all modes in descending order of brightness.
Line 259: Line 278:
end
end


--[[
-- List all unique rotations for a mode, by order of leftward shifts. Order by
  List all unique rotations for a mode. Order of modes is by rotation.
-- 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
-- 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
-- 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
-- modes, but if there is a substring of length p that repeats within the mode
  (where p divides s with remainder = 0), then there will be p modes. It's also
-- (where s mod p = 0), then there will be p modes. If the mode has one step
  possible to have only one mode, but this can only happen if there is only one
-- type, then there is only one mode.
  step size, meaning it's a unary scale (only one step size).
]]--
function p.mode_rotations(mode_string)
function p.mode_rotations(mode_string)
local rotations = {}
local rotations = {}
local current_mode = mode_string
local current_mode = mode_string
for i = 1, #mode_string do
for i = 1, #mode_string do
if not p.find_item_in_table(rotations, current_mode) then
if not utils.table_contains(rotations, current_mode) then
table.insert(rotations, current_mode)
table.insert(rotations, current_mode)
end
end
Line 281: Line 298:
end
end


--[[
-- Rotate a mode by shifting the step sequence to the left. Negative values
  Rotate a mode by shifting the step sequence to the left. Negative values
-- shift it to the right. Helper function for mode_by_brightness().
  shift it to the right. Helper function for mode_from_mos().
]]--
function p.rotate_mode(mode_string, shift_amt)
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 shift_amt = shift_amt == nil and 1 or shift_amt % #mode_string -- Default is 1
Line 295: Line 310:
---------------------------- STEP MATRIX FUNCTIONS -----------------------------
---------------------------- STEP MATRIX FUNCTIONS -----------------------------
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--[[
 
  Convert a single mode (as a string) into a step matrix. This is a listing of
-- Convert a single mode (as a string) into a step matrix. This is a listing of
  every interval's step vector in the mode.
-- every interval's step vector in the mode.
]]--
function p.mode_to_step_matrix(mode_string)
function p.mode_to_step_matrix(mode_string)
local matrix = {}
local matrix = {}
Line 308: Line 322:
end
end


--[[
 
  Given a mos, produce every step matrix for every mode. Modes are listed in
-- TODO?: replaces mode_to_step_matrices/mode_rotations_to_step_matrices with
  order of brightness.
-- 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)
function p.modes_to_step_matrices(mos)
local modes = p.modes_by_brightness(mos)
local modes = p.modes_by_brightness(mos)
Line 322: Line 341:
end
end


--[[
-- Given a single mode (as a string), produce the step matrices for each  
  Given a single mode (as a string), produce the step matrices for each  
-- rotation of that mode. Modes are listed in order of rotation.
  rotation of that mode. Modes are listed in order of rotation.
]]--
function p.mode_rotations_to_step_matrices(mode_string)
function p.mode_rotations_to_step_matrices(mode_string)
local modes = p.mode_rotations(mode_string)
local modes = p.mode_rotations(mode_string)
Line 336: Line 353:
end
end


--[[
-- Given an input mos, produce its modal union.
  Given an input mos, produce its modal union.
-- This is a listing of every interval's large and small sizes.
 
  This is a listing of every interval's large and small sizes.
]]--
function p.modal_union(input_mos)
function p.modal_union(input_mos)
local brightest_mode = p.brightest_mode(input_mos)
local brightest_mode = p.brightest_mode(input_mos)
Line 366: Line 380:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--------------- INTERVAL FUNCTIONS FOR PERFECTABLE INTERVALS -------------------
--------------- FUNCTIONS FOR GENERATOR AND PERIOD INTERVALS -------------------
------------------ (IE, GENERATORS AND PERIOD INTERVALS) -----------------------
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--[[
 
  Compute the bright gen as a vector of L's and s's.
-- 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
  Bright gen has two sizes: perfect (large) and diminished (small). The size
-- large/perfect size.
  given by this function is the large size.
]]--
function p.bright_gen(mos)
function p.bright_gen(mos)
local nL = mos.nL
local nL = mos.nL
Line 405: Line 416:
end
end


--[[
-- Compute the dark gen as a vector of L's and s's. Since all mosstep
  Compute the dark gen as a vector of L's and s's.
-- intervals (excluding the root and period) have two sizes, this returns the
 
-- small/perfect size.
  Dark gen has two sizes: augmented (large) and perfect (small). The size given
  by this function is the small size and is equal to the period complement of the
  bright gen.
]]--
function p.dark_gen(mos)
function p.dark_gen(mos)
local bright_gen = p.bright_gen(mos)
local bright_gen = p.bright_gen(mos)
Line 417: Line 424:
end
end


-- Compute the period as a vector of L's and s's. Period intervals only have one size: perfect.
-- 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)  
function p.period(mos)  
local gcd = utils._gcd(mos.nL, mos.ns)
local gcd = utils._gcd(mos.nL, mos.ns)
Line 426: Line 434:
end
end


--[[
-- Compute the equave as a vector of L's and s's.
  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().
  Equave intervals only have one size: perfect. Equave and period intervals are
  the same for single-period mosses.
]]--
function p.equave(mos)  
function p.equave(mos)  
return {
return {
Line 440: Line 445:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
------------------ INTERVAL FUNCTIONS FOR SIMPLE INTERVALS ---------------------
------------------- FUNCTIONS FOR SINGLE-STEP INTERVALS ------------------------
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--[[
 
  Compute the unison as a vector of L's and s's.
-- 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
  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.
  not need a mos as input. It's basically a zero vector.
-- The unison only has one size: perfect.
 
  The unison only has one size: perfect.
]]--
function p.unison()
function p.unison()
return { ["L"] = 0, ["s"] = 0 }
return { ["L"] = 0, ["s"] = 0 }
end
end


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


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


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


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


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


--[[
-- Compute an arbitrary mos interval as a vector of L's and s's. Params:
  Compute an arbitrary mos interval as a vector of L's and s's.
-- - step_count: the number of steps subtended by the mosstep.
 
-- - size_offset: denotes whether to return the large size (0) or the small
  The step_count param is the number of mossteps in the interval. EG, in 5L 2s,
--   size (-1) (or if this is a period interval, the diminished size). Values
  the large 2-mosstep is "LL", so the corresponding vector has L=2, s=0.
--   other than 0 or 1 represent alterations by multiple chromas, such as
 
--   augmented (1) or diminished (-2).
  Mossteps larger than the equave (eg, the minor 9th in non-xen music theory)
  are allowed.
 
  The size_offset denotes whether the interval is the large size (0) or the
   small size (-1). This can exceed the range of [-1, 0] to represent intervals
  raised/lowered by multiple chromas (augmented, diminished, etc).
    
  Note that for period intervals (eg, the root and equave), there is only one
  size (0 = perfect), so -1 is diminished and 1 is augmented.
 
  E.G., a perfect 4-diastep (perf. 5th) is 4 steps. Since it's the large size,
  the offset is 0, but to get the diminished 5th, the offset should be -1.
]]--
function p.interval_from_mos(mos, step_count, size_offset)
function p.interval_from_mos(mos, step_count, size_offset)
local size_offset = size_offset or 0 -- Optional param; defaults to large size
local size_offset = size_offset or 0 -- Optional param; defaults to large size
Line 523: Line 510:
end
end


--[[
-- Compute an arbitrary mos interval (as a string of steps) as a vector of L's
  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().
  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.
  Sequences of steps can be entered, where each step is one of five sizes:
--  - s: small step.
  - L: large step.
--  - c: a chroma; the difference between a large and small step.
  - s: small step.
--  - A: an augmented step; a large step plus a chroma.
  - c: a chroma; the difference between a large and small step.
--  - d: a diminished step, or diesis; a small step minus a chroma.
  - 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)
function p.interval_from_step_sequence(step_sequence)
local mossteps = #step_sequence
local mossteps = #step_sequence
Line 559: Line 543:
------------------------------- COUNT FUNCTIONS --------------------------------
------------------------------- 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).
-- Given a mos, compute the number of steps in its bright gen (L's plus s's).
Line 576: Line 565:
end
end


-- Given a mos, compute the number of steps in its equave (L's plus s's).
-- TODO: deprecate this since "equave_step_count" is redundant and longer than
-- "step count".
function p.equave_step_count(mos)
function p.equave_step_count(mos)
return mos.nL + mos.ns
return mos.nL + mos.ns
Line 586: Line 576:
end
end


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


--[[
-- Given a vector representing an interval, compute the number of chromas it was
  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
  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
  perfect size (for period/root/equave intervals). This requires the mos as
-- input.
  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
  If the number of chromas from a small (EG minor) interval is desired, then
-- interval_from_mos, this can be used to denote altered mossteps (augmented,
  using the param size_offset can be used: 0 for chromas from large size, -1
-- diminished, etc).
  for chromas from small size. This can exceed the range [-1, 0] if needed.
 
  E.G., a diminished 2-diastep (dim. 3rd) has the vector {0,2}. It's reached by
  either lowering the major 2-step by 2 chromas, or lowering the minor 2-step
  by 1 chroma.
]]--
function p.interval_chroma_count(interval, mos, size_offset)
function p.interval_chroma_count(interval, mos, size_offset)
local size_offset = size_offset or 0 -- Default of 0.
local size_offset = size_offset or 0 -- Default of 0.
Line 618: Line 600:


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


Line 637: Line 619:
end
end


-- Repeatedly add the same interval to itself.
-- Stack an interval, or repeatedly add the same interval to itself.
function p.interval_mul(interval, amt)
function p.interval_mul(interval, amt)
return {  
return {  
Line 651: Line 633:
interval_1["s"] == interval_2["s"]
interval_1["s"] == interval_2["s"]
end
end
--------------------------------------------------------------------------------
 
---------------------- INTERVAL MANIPULATION FUNCTIONS -------------------------
-- 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.
  Given an interval vector and a mos, find its period complement. This is the
  interval to add to produce the period.
]]--
function p.period_complement(interval, mos)
function p.period_complement(interval, mos)
local sign = p.interval_step_count(interval) < 0 and -1 or 1
local sign = p.interval_step_count(interval) < 0 and -1 or 1
Line 664: Line 643:
end
end


--[[
-- Given an interval vector and a mos, find its equave complement. This is the
  Given an interval vector and a mos, find its equave complement. This is the
-- interval to add to produce the equave.
  interval to add to produce the equave.
]]--
function p.equave_complement(interval, mos)
function p.equave_complement(interval, mos)
local sign = p.interval_step_count(interval) < 0 and -1 or 1
local sign = p.interval_step_count(interval) < 0 and -1 or 1
Line 674: Line 651:
end
end


--[[
-- Given an interval vector and a mos, period-reduce it. This works like
  Given an interval vector and a mos, period-reduce it. This works like
-- modular arithmetic, so passing a negative interval returns a positive one.
  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)
function p.period_reduce(interval, mos)
local step_count = p.interval_step_count(interval)
local step_count = p.interval_step_count(interval)
Line 686: Line 663:
end
end


--[[
-- Given an interval vector and a mos, equave-reduce it. This works like
  Given an interval vector and a mos, equave-reduce it. This works like
-- modular arithmetic, so passing a negative interval returns a positive one.
  modular arithmetic, so passing a negative interval returns a positive one.
]]--
function p.equave_reduce(interval, mos)
function p.equave_reduce(interval, mos)
local step_count = p.interval_step_count(interval)
local step_count = p.interval_step_count(interval)
Line 703: Line 678:
end
end


--[[
-- Intervals usually denote distances between two scale degrees and should be
  Intervals usually denote distances between two scale degrees and should be
-- positive values. Normalizing makes a negative interval positive again.
  positive values. Normalizing makes a negative interval positive again.
]]--
function p.normalize_interval(interval)
function p.normalize_interval(interval)
return p.interval_step_count(interval) < 0 and p.interval_mul(interval, -1) or interval
return p.interval_step_count(interval) < 0 and p.interval_mul(interval, -1) or interval
Line 714: Line 687:
---------------------------- EQUAL-TUNING FUNCTIONS ----------------------------
---------------------------- EQUAL-TUNING FUNCTIONS ----------------------------
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--[[
 
  Given a mos and a step ratio, return an equal tuning (or equal division).
-- 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
-- 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 to be entered. (The rational module isn't suitable since it simplifies
  ratios.)
-- ratios.)
]]--
function p.as_et(mos, step_ratio, suffix)
function p.mos_to_et(mos, step_ratio, suffix)
local suffix = suffix or nil
local suffix = suffix or nil
local et_size = mos.nL * step_ratio[1] + mos.ns * step_ratio[2]
local et_size = mos.nL * step_ratio[1] + mos.ns * step_ratio[2]
Line 726: Line 698:
end
end


--[[
-- Given a mos and a step ratio, return the number of et-steps for its bright
  Given a mos and a step ratio, return the number of et-steps for its bright
-- generator.
  generator.
]]--
function p.bright_gen_to_et_steps(mos, step_ratio)
function p.bright_gen_to_et_steps(mos, step_ratio)
return p.interval_to_et_steps(p.bright_gen(mos), step_ratio)
return p.interval_to_et_steps(p.bright_gen(mos), step_ratio)
Line 759: Line 729:


-- Given a mos, return its equal temperament suffix as a string (edo, edt, edf, or ed-p/q).
-- Given a mos, return its equal temperament suffix as a string (edo, edt, edf, or ed-p/q).
function p.mos_to_et_suffix(mos)
function p.et_suffix(mos)
if rat.eq(mos.equave, rat.new(2)) then
if rat.eq(mos.equave, rat.new(2)) then
return "edo"
return "edo"
Line 772: Line 742:


-- Given a mos and step ratio, return its equal temperament as a string "{steps}\{division}{suffix}".
-- Given a mos and step ratio, return its equal temperament as a string "{steps}\{division}{suffix}".
function p.mos_to_et_as_string(mos, step_ratio, suffix)
function p.et_string(mos, step_ratio, suffix)
local suffix = suffix or nil
local suffix = suffix or nil
local et_mos = p.mos_to_et(mos, step_ratio, suffix)
local et_mos = p.as_et(mos, step_ratio, suffix)
return et.as_string(et_mos)
return et.as_string(et_mos)
end
end
Line 780: Line 750:
-- Given a mos and step ratio, compute the number of et-steps for its bright gen
-- Given a mos and step ratio, compute the number of et-steps for its bright gen
-- as a string "{steps}\{division}{suffix}".
-- as a string "{steps}\{division}{suffix}".
function p.bright_gen_to_et_steps_as_string(mos, step_ratio, suffix)
function p.bright_gen_to_et_string(mos, step_ratio, suffix)
return p.interval_to_et_steps_as_string(p.bright_gen(mos), mos, step_ratio, suffix)
return p.interval_to_et_string(p.bright_gen(mos), mos, step_ratio, suffix)
end
end


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


-- Given a mos and step ratio, compute the number of et-steps for its period,
-- Given a mos and step ratio, compute the number of et-steps for its period,
-- as a string "{steps}\{division}{suffix}".
-- as a string "{steps}\{division}{suffix}".
function p.period_to_et_steps_as_string(mos, step_ratio, suffix)
function p.period_to_et_string(mos, step_ratio, suffix)
return p.interval_to_et_steps_as_string(p.period(mos), 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
end


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


Line 805: Line 783:
-- corresponds to, as a string "{steps}\{division}{suffix}". Requires info
-- corresponds to, as a string "{steps}\{division}{suffix}". Requires info
-- about the mos itself.
-- about the mos itself.
function p.interval_to_et_steps_as_string(interval, mos, step_ratio, suffix)
function p.interval_to_et_string(interval, mos, step_ratio, suffix)
local suffix = suffix or nil
local suffix = suffix or nil
local mos_et = p.mos_to_et(mos, step_ratio, suffix)
local mos_et = p.as_et(mos, step_ratio, suffix)
return et.backslash_display(mos_et, p.interval_to_et_steps(interval, step_ratio))
return et.backslash_display(mos_et, p.interval_to_et_steps(interval, step_ratio))
end
end
Line 829: Line 807:
end
end


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


--[[
-- Given a mos and a step ratio, return the number of cents for its equave.
  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
  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
  is a multiple of that (at least for multi-period mosses), so no step ratio is
-- needed.
  needed.
]]--
function p.equave_to_cents(mos)
function p.equave_to_cents(mos)
return rat.cents(mos.equave)
return rat.cents(mos.equave)
Line 862: Line 835:
-- Tester function
-- Tester function
function p.tester()
function p.tester()
local input_mos = p.new(5,2)
local input_mos = p.new(4,1,3)
local step_ratio = {2,1}
local step_ratio = {2,1}
local interval_vector = {["L"] = 3, ["s"] = 1}
local interval_vector = {["L"] = 3, ["s"] = 1}
return p.as_string(input_mos, false)
--return p.as_string(input_mos, false)


--return p.mos_to_et(p.new(5,2), {2,1})
--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
end


return p
return p

Latest revision as of 23:06, 11 October 2025

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