Module:MOS
Jump to navigation
Jump to search
Documentation for this module may be created at Module:MOS/doc
local rat = require('Module:Rational')
local seq = require('Module:Sequence')
local utils = require('Module:Utils')
local et = require('Module:ET')
local p = {}
-- Table of official tamnams names (2/1-equave only)
p.tamnams_name = { -- Only mosses with 2/1-equave names in TAMNAMS
['1L 1s'] = 'monowood',
['2L 2s'] = 'biwood',
['1L 5s'] = 'antimachinoid',
['2L 4s'] = 'malic',
['3L 3s'] = 'triwood',
['4L 2s'] = 'citric',
['5L 1s'] = 'machinoid',
['1L 6s'] = 'onyx',
['2L 5s'] = 'antidiatonic',
['3L 4s'] = 'mosh',
['4L 3s'] = 'smitonic',
['5L 2s'] = 'diatonic',
['6L 1s'] = 'archaeotonic',
['1L 7s'] = 'antipine',
['2L 6s'] = 'subaric',
['3L 5s'] = 'checkertonic',
['4L 4s'] = 'tetrawood',
['5L 3s'] = 'oneirotonic',
['6L 2s'] = 'ekic',
['7L 1s'] = 'pine',
['1L 8s'] = 'antisubneutralic',
['2L 7s'] = 'balzano',
['3L 6s'] = 'tcherepnin',
['4L 5s'] = 'gramitonic',
['5L 4s'] = 'semiquartal',
['6L 3s'] = 'hyrulic',
['7L 2s'] = 'armotonic',
['8L 1s'] = 'subneutralic',
['1L 9s'] = 'antisinatonic',
['2L 8s'] = 'jaric',
['3L 7s'] = 'sephiroid',
['4L 6s'] = 'lime',
['5L 5s'] = 'pentawood',
['6L 4s'] = 'lemon',
['7L 3s'] = 'dicoid',
['8L 2s'] = 'taric',
['9L 1s'] = 'sinatonic'
}
-- Prefixes
p.tamnams_prefix = { -- Only mosses with 2/1-equave names in TAMNAMS
['1L 1s'] = 'monwd-',
['2L 2s'] = 'biwd-',
['1L 5s'] = 'amech-',
['2L 4s'] = 'mal-',
['3L 3s'] = 'triwd-',
['4L 2s'] = 'citro-',
['5L 1s'] = 'mech-',
['1L 6s'] = 'on-',
['2L 5s'] = 'pel-',
['3L 4s'] = 'mosh-',
['4L 3s'] = 'smi-',
['5L 2s'] = 'dia-',
['6L 1s'] = 'arch-',
['1L 7s'] = 'apine-',
['2L 6s'] = 'subar-',
['3L 5s'] = 'check-',
['4L 4s'] = 'tetrawd-',
['5L 3s'] = 'oneiro-',
['6L 2s'] = 'ek-',
['7L 1s'] = 'pine-',
['1L 8s'] = 'ablu-',
['2L 7s'] = 'bal-',
['3L 6s'] = 'cher-',
['4L 5s'] = 'gram-',
['5L 4s'] = 'cthon-',
['6L 3s'] = 'hyru-',
['7L 2s'] = 'arm-',
['8L 1s'] = 'blu-',
['1L 9s'] = 'asina-',
['2L 8s'] = 'jara-',
['3L 7s'] = 'seph-',
['4L 6s'] = 'lime-',
['5L 5s'] = 'pentawd-',
['6L 4s'] = 'lem-',
['7L 3s'] = 'dico-',
['8L 2s'] = 'tara-',
['9L 1s'] = 'sina-'
}
-- Abbreviations (most abbrevs are the same as the prefixes but there are exceptions)
p.tamnams_abbrev = { -- Only mosses with 2/1-equave names in TAMNAMS
['1L 1s'] = 'wood',
['2L 2s'] = 'bw',
['1L 5s'] = 'amech',
['2L 4s'] = 'mal',
['3L 3s'] = 'trw',
['4L 2s'] = 'cit',
['5L 1s'] = 'mech',
['1L 6s'] = 'on',
['2L 5s'] = 'pel',
['3L 4s'] = 'mosh',
['4L 3s'] = 'smi',
['5L 2s'] = 'dia',
['6L 1s'] = 'arch',
['1L 7s'] = 'apine',
['2L 6s'] = 'subar',
['3L 5s'] = 'chk',
['4L 4s'] = 'ttw',
['5L 3s'] = 'onei',
['6L 2s'] = 'ek',
['7L 1s'] = 'pine',
['1L 8s'] = 'ablu',
['2L 7s'] = 'bal',
['3L 6s'] = 'ch',
['4L 5s'] = 'gram',
['5L 4s'] = 'cth',
['6L 3s'] = 'hyru',
['7L 2s'] = 'arm',
['8L 1s'] = 'blu',
['1L 9s'] = 'asi',
['2L 8s'] = 'jar',
['3L 7s'] = 'seph',
['4L 6s'] = 'lime',
['5L 5s'] = 'pw',
['6L 4s'] = 'lem',
['7L 3s'] = 'dico',
['8L 2s'] = 'tar',
['9L 1s'] = 'si'
}
function table_invert(t)
local s={}
for k,v in pairs(t) do
s[v]=k
end
return s
end
-- Create a table that parses a mos string from the TAMNAMS name.
p.parse_name = table_invert(p.tamnams_name)
-- create a MOS structure (nL)L (ns)s <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
function round(num, numDecimalPlaces)
local mult = 10^(numDecimalPlaces or 0)
return math.floor(num * mult + 0.5) / mult
end
-- parse a MOS structure
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
-- construct a string representation for a MOS structure
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 long string representation for a MOS structure
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
-- Find the brightest mode of a mos (the Christoffel word)
-- using its definition as a 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
function p.bright_gen(mos) -- Compute the abstract, equave-agnostic bright generator as a "vector" of L and s steps.
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
-- Given mos a MOS structure, hardness = L/s a rational number,
-- return the et and the bright MOS generator corresponding to the hardness.
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
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
return p