Module:MOS: Difference between revisions
Jump to navigation
Jump to search
Comments |
Minor renaming to some params; added unison(), which returns a zero vector |
||
| Line 227: | Line 227: | ||
['L'] = mos.nL, | ['L'] = mos.nL, | ||
['s'] = mos.ns | ['s'] = mos.ns | ||
} | |||
return result | |||
end | |||
-- 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() | |||
local result = { | |||
['L'] = 0, | |||
['s'] = 0 | |||
} | } | ||
return result | return result | ||
| Line 345: | Line 357: | ||
-- Add two intervals together by adding their respective vectors. | -- Add two intervals together by adding their respective vectors. | ||
function p.interval_add( | function p.interval_add(interval_1, interval_2) | ||
local interval_vector = { | local interval_vector = { | ||
['L'] = | ['L'] = interval_1['L'] + interval_2['L'], | ||
['s'] = | ['s'] = interval_1['s'] + interval_2['s'] | ||
} | } | ||
return interval_vector | return interval_vector | ||
| Line 354: | Line 366: | ||
-- Subtract two intervals by subtracting their respective vectors. | -- Subtract two intervals by subtracting their respective vectors. | ||
function p.interval_sub( | function p.interval_sub(interval_1, interval_2) | ||
local interval_vector = { | local interval_vector = { | ||
['L'] = | ['L'] = interval_1['L'] - interval_2['L'], | ||
['s'] = | ['s'] = interval_1['s'] - interval_2['s'] | ||
} | } | ||
return interval_vector | return interval_vector | ||
| Line 363: | Line 375: | ||
-- Repeatedly add the same interval to itself. | -- Repeatedly add the same interval to itself. | ||
function p.interval_mul( | function p.interval_mul(interval, amt) | ||
local interval_vector = { | local interval_vector = { | ||
['L'] = | ['L'] = interval['L'] * amt, | ||
['s'] = | ['s'] = interval['s'] * amt | ||
} | } | ||
return interval_vector | return interval_vector | ||
| Line 376: | Line 388: | ||
-- Given an interval vector and a mos, find its period complement. | -- Given an interval vector and a mos, find its period complement. | ||
function p.period_complement( | function p.period_complement(interval, mos) | ||
local period_vector = p.period(mos) | local period_vector = p.period(mos) | ||
return p.interval_sub(period_vector, | return p.interval_sub(period_vector, interval) | ||
end | end | ||
-- Given an interval vector and a mos, find its equave complement. | -- Given an interval vector and a mos, find its equave complement. | ||
function p.equave_complement( | function p.equave_complement(interval) | ||
local equave_vector = p.equave(mos, | local equave_vector = p.equave(mos, interval) | ||
return p.interval_sub(equave_vector, | return p.interval_sub(equave_vector, interval) | ||
end | end | ||
-- Given an interval vector and a mos, period-reduce it. | -- Given an interval vector and a mos, period-reduce it. | ||
function p.period_reduce( | function p.period_reduce(interval, mos) | ||
local step_count = p.interval_step_count( | local step_count = p.interval_step_count(interval) | ||
local reduce_amt = math.floor(step_count / p.period_step_count(mos)) | local reduce_amt = math.floor(step_count / p.period_step_count(mos)) | ||
local periods = p.interval_mul(p.period(mos), reduce_amt) | local periods = p.interval_mul(p.period(mos), reduce_amt) | ||
return p.interval_sub( | return p.interval_sub(interval) | ||
end | end | ||
-- Given an interval vector and a mos, equave-reduce it. | -- Given an interval vector and a mos, equave-reduce it. | ||
function p.equave_reduce( | function p.equave_reduce(interval) | ||
local step_count = p.interval_step_count( | local step_count = p.interval_step_count(interval) | ||
local reduce_amt = math.floor(step_count / p.equave_step_count(mos)) | local reduce_amt = math.floor(step_count / p.equave_step_count(mos)) | ||
local equaves = p.interval_mul(p.equave(mos), reduce_amt) | local equaves = p.interval_mul(p.equave(mos), reduce_amt) | ||
return p.interval_sub( | return p.interval_sub(interval, equaves) | ||
end | end | ||
Revision as of 10:07, 31 May 2024
- 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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
-- - Interval arithmetic, in the form of adding vectors of L's and s's, and
-- period/equave-reducing intervals
local rat = require('Module:Rational')
local utils = require('Module:Utils')
local et = require('Module:ET') -- Used for unused function
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.
--------------------------------------------------------------------------------
------------------------------- HELPER FUNCTIONS -------------------------------
--------------------------------------------------------------------------------
function round(num, numDecimalPlaces)
local mult = 10^(numDecimalPlaces or 0)
return math.floor(num * mult + 0.5) / mult
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
-- Pasre 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.
function p.as_string(mos)
local suffix = ''
if not rat.eq(mos.equave, 2) then
suffix = '⟨' .. rat.as_ratio(mos.equave):lower() .. '⟩'
end
return '' .. mos.nL .. 'L ' .. 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.
function p.as_long_string(mos)
local suffix = ''
if not rat.eq(mos.equave, 2) then
suffix = string.format(" (%s-equivalent)", rat.as_ratio(mos.equave):lower())
end
return '' .. mos.nL .. 'L ' .. 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)
local L_string = ""
local s_string = ""
if interval['L'] == 1 then
L_string = "L"
elseif interval['L'] ~= 0 and interval['L'] ~= 1 then
L_string = string.format("%dL", interval['L'])
end
if interval['s'] == 1 then
s_string = "s"
elseif interval['s'] ~= 0 and interval['s'] ~= 1 then
s_string = string.format("%ds", interval['s'])
end
if L_string == "" and s_string == "" then
return "0"
elseif L_string ~= "" and s_string == "" then
return L_string
elseif L_string == "" and s_string ~= "" then
return s_string
else
return L_string .. " + " .. s_string
end
end
--------------------------------------------------------------------------------
------------------------------- MODE FUNCTIONS ---------------------------------
--------------------------------------------------------------------------------
-- Find the brightest (true-mos) mode of a mos.
-- 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 = round(nL/d)
ns = round(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 = round(nL/d)
ns = round(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
--------------------------------------------------------------------------------
--------------- 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 = round(nL/d)
ns = round(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. It's 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)
local result = {
['L'] = mos.nL / gcd,
['s'] = mos.ns / gcd
}
return result
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)
local result = {
['L'] = mos.nL,
['s'] = mos.ns
}
return result
end
-- 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()
local result = {
['L'] = 0,
['s'] = 0
}
return result
end
--------------------------------------------------------------------------------
---------------- INTERVAL FUNCTIONS FOR ARBITRARY INTERVALS --------------------
--------------------------------------------------------------------------------
-- 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 param is a value that 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.
function p.interval(mos, step_count, size)
local size = size 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)
interval_vector['L'] = interval_vector['L'] + size
interval_vector['s'] = interval_vector['s'] - size
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().
-- 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 = {
['L'] = 0,
['s'] = 0
}
for i = 1, mossteps do
local step = string.sub(step_sequence, i, i)
if step == "L" then
interval_vector['L'] = interval_vector['L'] + 1
elseif step == "s" or step == "S" then
interval_vector['s'] = interval_vector['s'] + 1
elseif step == "c" then
interval_vector['L'] = interval_vector['L'] + 1
interval_vector['s'] = interval_vector['s'] - 1
elseif step == "A" then
interval_vector['L'] = interval_vector['L'] + 2
interval_vector['s'] = interval_vector['s'] - 1
elseif step == "d" then
interval_vector['L'] = interval_vector['L'] - 1
interval_vector['s'] = interval_vector['s'] + 2
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.
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.
function p.interval_chroma_count(interval, mos)
local step_count = p.interval_step_count(interval)
local base_interval = p.interval(mos, step_count, 0)
return interval['L'] - base_interval['L']
end
--------------------------------------------------------------------------------
----------------------- INTERVAL ARITHMETIC FUNCTIONS --------------------------
--------------------------------------------------------------------------------
-- Add two intervals together by adding their respective vectors.
function p.interval_add(interval_1, interval_2)
local interval_vector = {
['L'] = interval_1['L'] + interval_2['L'],
['s'] = interval_1['s'] + interval_2['s']
}
return interval_vector
end
-- Subtract two intervals by subtracting their respective vectors.
function p.interval_sub(interval_1, interval_2)
local interval_vector = {
['L'] = interval_1['L'] - interval_2['L'],
['s'] = interval_1['s'] - interval_2['s']
}
return interval_vector
end
-- Repeatedly add the same interval to itself.
function p.interval_mul(interval, amt)
local interval_vector = {
['L'] = interval['L'] * amt,
['s'] = interval['s'] * amt
}
return interval_vector
end
--------------------------------------------------------------------------------
---------------------- COMPLEMENT AND REDUCE FUNCTIONS -------------------------
--------------------------------------------------------------------------------
-- Given an interval vector and a mos, find its period complement.
function p.period_complement(interval, mos)
local period_vector = p.period(mos)
return p.interval_sub(period_vector, interval)
end
-- Given an interval vector and a mos, find its equave complement.
function p.equave_complement(interval)
local equave_vector = p.equave(mos, interval)
return p.interval_sub(equave_vector, interval)
end
-- Given an interval vector and a mos, period-reduce it.
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)
end
-- Given an interval vector and a mos, equave-reduce it.
function p.equave_reduce(interval)
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
--------------------------------------------------------------------------------
------------ UNUSED FUNCTIONS OR FUNCTIONS TO MOVE TO OTHER MODULES ------------
--------------------------------------------------------------------------------
-- Given mos a MOS structure, hardness = L/s a rational number,
-- return the et and the bright MOS generator corresponding to the hardness.
-- Currently unused
--[[
function p.et_tuning_by_hardness(mos, hardness)
local nL, ns, equave = mos.nL, mos.ns, mos.equave
hardness = rat.parse(hardness) or rat.new(hardness) or hardness
if nL == nil or ns == nil or equave == nil or hardness == nil then
return nil
end
L_in_et_steps, s_in_et_steps = rat.as_pair(hardness)
local et = et.new(nL*L_in_et_steps + ns*s_in_et_steps, equave)
local gen = p.bright_gen(mos)
local gen_steps = gen['L']*L_in_et_steps + gen['s']*s_in_et_steps
return et, gen_steps
end
]]--
-- Given a mos, find the ancestor mos with a target note count (default 10)
-- or less; to be moved to tamnams module
function p.find_ancestor(mos, target_note_count)
local mos = mos or p.new(5, 2)
local target_note_count = target_note_count or 10
local z = mos.nL
local w = mos.ns
while (z ~= w) and (z + w > target_note_count) do
local m1 = math.max(z, w)
local m2 = math.min(z, w)
-- For use with updating ancestor mos chunks
local z_prev = z
-- Update step ratios
z = m2
w = m1 - m2
end
return p.new(z, w, mos.equave)
end
--------------------------------------------------------------------------------
----------------------------------- TESTER -------------------------------------
--------------------------------------------------------------------------------
-- Tester function
function p.tester()
--return p.add_intervals({ ['L'] = 3, ['s'] = 1}, { ['L'] = 3, ['s'] = 1})
--return p.interval(p.new(5, 2), 11, 0)
--return p.dark_gen(p.new(5, 2))
--local interval = p.period_reduce({ ['L'] = 5, ['s'] = 11}, p.new(3,6))
--return p.dark_gen_step_count(p.new(5,2))
return p.interval_chroma_count({ ['L'] = 3, ['s'] = 1}, p.new(5,2))
end
return p