Module:MOS
Jump to navigation
Jump to search
This module provides functions for working with MOS scales in Lua code. It serves as a "library" for mos-related modules and thus does not have a corresponding template.
It allows working with MOS scales with no fixed step sizes, as well as working with its modes and intervals. Also included are functions related to equal tunings to obviate the need to include Module:ET, in the form of functions that produce an ET, denote mossteps as steps of an ET, and produce the cent values of mossteps.-- This module follows User:Ganaram inukshuk/Provisional style guide for Lua#Lua_style
local p = {}
local et = require("Module:ET")
local rat = require("Module:Rational")
local utils = require("Module:Utils")
--------------------------------------------------------------------------------
------------------------------- HELPER FUNCTIONS -------------------------------
--------------------------------------------------------------------------------
-- Helper function
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
--------------------------------------------------------------------------------
----------------------------- 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 " " 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 " " or " ") .. string.format("(%s-equivalent)", rat.as_ratio(mos.equave):lower())
end
return mos.nL .. "L" .. (use_nbsp and " " 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 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_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