Module:MOS: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
ArrowHead294 (talk | contribs)
m Parenthesise to make this easier to read
Ganaram inukshuk (talk | contribs)
m dont use block comments on comments; it's harder to read
Line 1: Line 1:
--[[
-- Module for working with mosses in lua code; this serves as a "library" for
  Module for working with mosses in lua code; this serves as a "library" for
-- mos-related modules and thus does not have a corresponding template.
  mos-related modules and thus does not have a corresponding template.
    
    
  Functionality includes:
-- Functionality includes:
  - Creating/parsing mosses
-- - Creating/parsing mosses
  - Creating scalesigs (string representations) of mosses
-- - Creating scalesigs (string representations) of mosses
  - Finding certain modes of a mos
-- - Finding certain modes of a mos
  - Finding generators for a mos
-- - Finding generators for a mos
  - Producing vectors for simple mos intervals
-- - Producing vectors for simple mos intervals
  - Interval arithmetic, in the form of adding vectors of L's and s's, and
-- - Interval arithmetic, in the form of adding vectors of L's and s's, and
    period/equave-reducing intervals
--  period/equave-reducing intervals
  - Finding equal tunings for mosses
-- - Finding equal tunings for mosses
]]--


local rat = require("Module:Rational")
local rat = require("Module:Rational")
Line 20: Line 18:
local p = {}
local p = {}


--[[
-- Naming scheme for function names:
  Naming scheme for function names:
--  - Functions related to mosses don't have any special 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 modes generally end with "mode".
--  - Functions related to a mos's generators, equave, or period contain the
  - Functions related to a mos's generators, equave, or period contain the
--    corresponding interval as part of its name.
    corresponding interval as part of its name.
--  - Functions related to intervals generally begin with "interval".
  - Functions related to intervals generally begin with "interval".
--  - Interval complement/reduce functions end with "complement" and "reduce".
  - Interval complement/reduce functions end with "complement" and "reduce".
--  - Functions that produce strings generally have the phrase "as string".
  - Functions that produce strings generally have the phrase "as string".
--  - Functions that "count" something generally end with "count".
  - Functions that "count" something generally end with "count".
--  - If a function requires an interval and mos as input, the interval(s) come
  - If a function requires an interval and mos as input, the interval(s) come
--    after the mos.
    after the mos.
--  - Functions that have to do with equal tunings will have "et" in its name.
  - Functions that have to do with equal tunings will have "et" in its name.
]]--


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Line 82: Line 78:
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


--[[
-- Construct a string representation (scalesig) for a MOS structure.
  Construct a string representation (scalesig) for a MOS structure.
-- Scalesig is "xL ys", or "xL ys<p/q>" for nonoctave scales.
  Scalesig is "xL ys", or "xL ys<p/q>" for nonoctave scales.
-- Option to use nbsp is provided using the second param; default is no nbsp
  Option to use nbsp is provided using the second param; default is no 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)
local use_nbsp = (use_nbsp == nil and true or use_nbsp)
Line 96: Line 90:
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.
 
-- Option to use nbsp is provided using the second param; default is no nbsp
  Scalesig is "xL ys", or "xL ys (p/q-equivalent)" for nonoctave scales.
 
  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)
local use_nbsp = (use_nbsp == nil and true or use_nbsp)
Line 113: Line 103:
end
end


--[[
-- Given an interval as a vector of L's and s's, produce a string "iL + js",
  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.
  where i and j are the quantities for L and s.
]]--
function p.interval_as_string(interval)
function p.interval_as_string(interval)
Line 177: Line 165:
------------------------------- 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 190:
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 215:
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 going 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:
  0 is the brightest mode, 1 is 2nd brightest, etc...
]]--
function p.mode_from_mos(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))
Line 259: Line 241:
end
end


--[[
-- List all unique rotations for a mode. Order of modes is by rotation.
  List all unique rotations for a mode. Order of modes is by rotation.
-- 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
  Note: there will always be s/p modes, where s is the number of steps in the
-- modes, but if there is a substring of length p that repeats within the mode
  entered mode, and p is the period of repetition. At most, there will be s
-- (where p divides s with remainder = 0), then there will be p modes. It's also
  modes, but if there is a substring of length p that repeats within the mode
-- possible to have only one mode, but this can only happen if there is only one
  (where p divides s with remainder = 0), then there will be p modes. It's also
-- step size, meaning it's a unary scale (only one step size).
  possible to have only one mode, but this can only happen if there is only one
  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 = {}
Line 281: Line 260:
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_from_mos().
  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 272:
---------------------------- 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 284:
end
end


--[[
-- Given a mos, produce every step matrix for every mode. Modes are listed in
  Given a mos, produce every step matrix for every mode. Modes are listed in
-- order of brightness.
  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 296:
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 308:
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 369: Line 338:
------------------ (IE, GENERATORS 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.
 
-- Bright gen has two sizes: perfect (large) and diminished (small). The size
  Bright gen has two sizes: perfect (large) and diminished (small). The size
-- given by this function is the large 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 372:
end
end


--[[
-- Compute the dark gen as a vector of L's and s's.
  Compute the dark gen as a vector of L's and s's.
-- 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
  Dark gen has two sizes: augmented (large) and perfect (small). The size given
-- bright gen.
  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 426: Line 390:
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.
-- Equave intervals only have one size: perfect. Equave and period intervals are
 
-- the same for single-period mosses.
  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 442: Line 403:
------------------ INTERVAL FUNCTIONS FOR SIMPLE INTERVALS ---------------------
------------------ INTERVAL FUNCTIONS FOR SIMPLE INTERVALS ---------------------
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--[[
 
  Compute the unison as a vector of L's and s's.
-- Compute 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


--[[
-- Compute 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 }
Line 491: Line 447:
end
end


--[[
-- Compute an arbitrary mos interval as a vector of L's and s's.
  Compute an arbitrary mos interval as a vector of L's and s's.
-- The step_count param is the number of mossteps in the interval. EG, in 5L 2s,
 
-- the large 2-mosstep is "LL", so the corresponding vector has L=2, s=0.
  The step_count param is the number of mossteps in the interval. EG, in 5L 2s,
-- Mossteps larger than the equave (eg, the minor 9th in non-xen music theory)
  the large 2-mosstep is "LL", so the corresponding vector has L=2, s=0.
-- are allowed.
 
-- The size_offset denotes whether the interval is the large size (0) or the
  Mossteps larger than the equave (eg, the minor 9th in non-xen music theory)
-- small size (-1). This can exceed the range of [-1, 0] to represent intervals
  are allowed.
-- raised/lowered by multiple chromas (augmented, diminished, etc).
 
-- Note that for period intervals (eg, the root and equave), there is only one
  The size_offset denotes whether the interval is the large size (0) or the
-- size (0 = perfect), so -1 is diminished and 1 is augmented.
  small size (-1). This can exceed the range of [-1, 0] to represent intervals
-- E.G., a perfect 4-diastep (perf. 5th) is 4 steps. Since it's the large size,
  raised/lowered by multiple chromas (augmented, diminished, etc).
-- the offset is 0, but to get the diminished 5th, the offset should be -1.
 
  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 472:
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 586: Line 532:
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.
-- If the number of chromas from a small (EG minor) interval is desired, then
 
-- using the param size_offset can be used: 0 for chromas from large size, -1
  If the number of chromas from a small (EG minor) interval is desired, then
-- for chromas from small size. This can exceed the range [-1, 0] if needed.
  using the param size_offset can be used: 0 for chromas from large size, -1
-- E.G., a diminished 2-diastep (dim. 3rd) has the vector {0,2}. It's reached by
  for chromas from small size. This can exceed the range [-1, 0] if needed.
-- either lowering the major 2-step by 2 chromas, or lowering the minor 2-step
 
-- by 1 chroma.
  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 654: Line 594:
---------------------- INTERVAL MANIPULATION FUNCTIONS -------------------------
---------------------- INTERVAL MANIPULATION FUNCTIONS -------------------------
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--[[
 
  Given an interval vector and a mos, find its period complement. This is the
-- Given an interval vector and a mos, find its period complement. This is the
  interval to add to produce the period.
-- 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 603:
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 611:
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.
]]--
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 621:
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 636:
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 645:
---------------------------- 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.mos_to_et(mos, step_ratio, suffix)
function p.mos_to_et(mos, step_ratio, suffix)
local suffix = suffix or nil
local suffix = suffix or nil
Line 726: Line 655:
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 839: Line 766:
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)

Revision as of 06:26, 3 March 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 (69)
Line Function Params
37 find_item_in_table (table, item)
54 new (nL, ns, equave)
63 parse (unparsed)
83 as_string (mos, use_nbsp)
95 as_long_string (mos, use_nbsp)
107 interval_as_string (interval)
145 parent_mos (mos)
150 child_mosses (mos)
155 sister_mos (mos)
160 is_valid_mos (mos)
171 brightest_mode (mos)
194 darkest_mode (mos)
220 mode_from_mos (mos, bright_gens_down)
229 modes_by_brightness (mos)
250 mode_rotations (mode_string)
264 rotate_mode (mode_string, shift_amt)
277 mode_to_step_matrix (mode_string)
288 modes_to_step_matrices (mos)
300 mode_rotations_to_step_matrices (mode_string)
312 modal_union (input_mos)
344 bright_gen (mos)
378 dark_gen (mos)
384 period (mos)
395 equave (mos)
410 unison none
416 chroma none
421 augmented_step none
426 large_step none
431 small_step none
436 diminished_step none
445 interval_from_step_counts (i, j)
461 interval_from_mos (mos, step_count, size_offset)
482 interval_from_step_sequence (step_sequence)
509 bright_gen_step_count (mos)
515 dark_gen_step_count (mos)
520 period_step_count (mos)
525 equave_step_count (mos)
530 period_count (mos)
537 interval_step_count (interval)
551 interval_chroma_count (interval, mos, size_offset)
564 interval_add (interval_1, interval_2)
572 interval_sub (interval_1, interval_2)
580 interval_mul (interval, amt)
588 interval_eq (interval_1, interval_2)
599 period_complement (interval, mos)
607 equave_complement (interval, mos)
615 period_reduce (interval, mos)
625 equave_reduce (interval, mos)
634 invert_interval (interval)
640 normalize_interval (interval)
651 mos_to_et (mos, step_ratio, suffix)
659 bright_gen_to_et_steps (mos, step_ratio)
664 dark_gen_to_et_steps (mos, step_ratio)
669 period_to_et_steps (mos, step_ratio)
674 equave_to_et_steps (mos, step_ratio)
679 interval_to_et_steps (interval, step_ratio)
688 mos_to_et_suffix (mos)
701 mos_to_et_as_string (mos, step_ratio, suffix)
709 bright_gen_to_et_steps_as_string (mos, step_ratio, suffix)
715 dark_gen_to_et_steps_as_string (mos, step_ratio, suffix)
721 period_to_et_steps_as_string (mos, step_ratio, suffix)
727 equave_to_et_steps_as_string (mos, step_ratio, suffix)
734 interval_to_et_steps_as_string (interval, mos, step_ratio, suffix)
745 bright_gen_to_cents (mos, step_ratio)
752 dark_gen_to_cents (mos, step_ratio)
764 period_to_cents (mos)
772 equave_to_cents (mos)
777 interval_to_cents (interval, mos, step_ratio)
788 tester none
Lua modules required (4)
Variable Module Functions used
et Module:ET new
as_string
backslash_display
rat Module:Rational parse
eq
as_ratio
new
cents
tip Module:Template input parse dependency not used
utils Module:Utils _gcd
_round_dec

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


-- Module for working with mosses in lua code; this serves as a "library" for
-- mos-related modules and thus does not have a corresponding template.
  
-- 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 = {}

-- 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 -------------------------------
--------------------------------------------------------------------------------

function p.find_item_in_table(table, item)
	local item_found = false
	for i = 1, #table do
		if table[i] == item then
			item_found = true
			break
		end
	end
	
	return item_found
end

--------------------------------------------------------------------------------
-------------------------------- BASE FUNCTIONS --------------------------------
--------------------------------------------------------------------------------

-- Create a new mos. (Contains the number of large and small steps, and 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.
function p.parse(unparsed)
	local nL, ns, equave = unparsed:match("^(%d+)[Ll]%s*(%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

--------------------------------------------------------------------------------
------------------------------- STRING FUNCTIONS -------------------------------
--------------------------------------------------------------------------------

-- Construct a string representation (scalesig) for a MOS structure.
-- Scalesig is "xL ys", or "xL ys<p/q>" for nonoctave scales.
-- Option to use nbsp is provided using the second param; default is no nbsp
function p.as_string(mos, use_nbsp)
	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
end

-- Construct a longer string representation for a MOS structure.
-- Scalesig is "xL ys", or "xL ys (p/q-equivalent)" for nonoctave scales.
-- Option to use nbsp is provided using the second param; default is no nbsp
function p.as_long_string(mos, use_nbsp)
	local use_nbsp = (use_nbsp == nil and true or use_nbsp)
	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
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

--------------------------------------------------------------------------------
--------------------------- MOS RELATIVES FUNCTIONS ----------------------------
--------------------------------------------------------------------------------

-- Find the parent mos of a mos
function p.parent_mos(mos)
	return p.new(math.min(mos.nL, mos.ns), math.abs(mos.nL-mos.ns), mos.equave)
end

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

-- Is the mos valid? It should have positive-integer values for nL and ns.
function p.is_valid_mos(mos)
	return mos.nL > 0 and mos.ns > 0
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 going DOWN PER PERIOD:
-- 0 is the brightest mode, 1 is 2nd brightest, etc...
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))
end

--------------------------------------------------------------------------------
--------------------------- MODE ROTATION FUNCTIONS ----------------------------
--------------------------------------------------------------------------------

-- 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. Order of modes is by rotation.
-- 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 p divides s with remainder = 0), then there will be p modes. It's also
-- possible to have only one mode, but this can only happen if there is only one
-- step size, meaning it's a unary scale (only one step size).
function p.mode_rotations(mode_string)
	local rotations = {}
	local current_mode = mode_string
	for i = 1, #mode_string do
		if not p.find_item_in_table(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_from_mos().
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

-- 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

--------------------------------------------------------------------------------
--------------- INTERVAL FUNCTIONS FOR PERFECTABLE INTERVALS -------------------
------------------ (IE, GENERATORS AND PERIOD INTERVALS) -----------------------
--------------------------------------------------------------------------------

-- Compute the bright gen as a vector of L's and s's.
-- Bright gen has two sizes: perfect (large) and diminished (small). The size
-- given by this function is the large 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.
-- 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)
	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 only have one size: perfect.
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.
-- Equave intervals only have one size: perfect. Equave and period intervals are
-- the same for single-period mosses.
function p.equave(mos) 
	return {
		["L"] = mos.nL,
		["s"] = mos.ns
	}
end

--------------------------------------------------------------------------------
------------------ INTERVAL FUNCTIONS FOR SIMPLE INTERVALS ---------------------
--------------------------------------------------------------------------------

-- Compute 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

-- 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".
function p.chroma()
	return { ["L"] = 1, ["s"] = -1 }
end

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

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

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

-- Compute 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.
-- The step_count param is the number of mossteps in the interval. EG, in 5L 2s,
-- the large 2-mosstep is "LL", so the corresponding vector has L=2, s=0.
-- 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)
	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, 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

-- Given a mos, compute the number of steps in its equave (L's plus s's).
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.
-- If the number of chromas from a small (EG minor) interval is desired, then
-- using the param size_offset can be used: 0 for chromas from large size, -1
-- 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)
	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 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

-- 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
--------------------------------------------------------------------------------
---------------------- INTERVAL MANIPULATION FUNCTIONS -------------------------
--------------------------------------------------------------------------------

-- 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)
	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.
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.mos_to_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.mos_to_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.mos_to_et_as_string(mos, step_ratio, suffix)
	local suffix = suffix or nil
	local et_mos = p.mos_to_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_steps_as_string(mos, step_ratio, suffix)
	return p.interval_to_et_steps_as_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_steps_as_string(mos, step_ratio, suffix)
	return p.interval_to_et_steps_as_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_steps_as_string(mos, step_ratio, suffix)
	return p.interval_to_et_steps_as_string(p.period(mos), mos, step_ratio, 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_steps_as_string(mos, step_ratio, suffix)
	return p.interval_to_et_steps_as_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_steps_as_string(interval, mos, step_ratio, suffix)
	local suffix = suffix or nil
	local mos_et = p.mos_to_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(5,2)
	local step_ratio = {2,1}
	local interval_vector = {["L"] = 3, ["s"] = 1}
	return p.as_string(input_mos, false)

	--return p.mos_to_et(p.new(5,2), {2,1})
end

return p