Module:MOS

From Xenharmonic Wiki
Revision as of 21:53, 30 May 2024 by Ganaram inukshuk (talk | contribs) (Fleshed out module; basically redefining module:mos as a module for doing mos arithmetic, with tamnams-related things being moved to module:tamnams)
Jump to navigation Jump to search
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 (22)
Line Function Params
21 new (nL, ns, equave)
29 parse (unparsed)
47 as_string (mos)
57 as_long_string (mos)
72 brightest_mode (mos)
96 darkest_mode (mos)
119 period (mos)
129 period (mos)
138 bright_gen (mos)
168 dark_gen (mos)
192 interval (mos, mossteps, size)
213 interval_from_step_sequence (mos, step_sequence)
246 add_intervals (v1, v2)
255 subtract_intervals (v1, v2)
264 multiply_interval (v1, amt)
277 period_step_count (mos)
283 equave_step_count (mos)
289 bright_gen_step_count (mos)
295 dark_gen_step_count (mos)
307 et_tuning_by_hardness (mos, hardness)
322 find_ancestor (mos, target_note_count)
483 tester none
Lua modules required (3)
Variable Module Functions used
et Module:ET new
rat Module:Rational parse
eq
as_ratio
new
as_pair
utils Module:Utils _gcd

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


-- Module for working with mosses in code, plus some basic arithmetic functions
local rat = require('Module:Rational')
local utils = require('Module:Utils')
local et = require('Module:ET')
local p = {}

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

-- parse a MOS structure 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

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

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

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

-- Find the darkest 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

-- Compute the period as a vector of L's and s's.
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.
function p.period(mos) 
	local result = {
		['L'] = mos.nL,
		['s'] = mos.ns
	}
	return result
end

-- Compute the bright gen as a vector of L's and s's.
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.
function p.dark_gen(mos)
	local bright_gen = p.bright_gen(mos)
	local period = p.period(mos)
	local dark_gen = {
		['L'] = period['L'] - bright_gen['L'],
		['s'] = period['s'] - bright_gen['s']
	}
	return dark_gen
end

--------------------------------------------------------------------------------
---------------- INTERVAL FUNCTIONS FOR ARBITRARY INTERVALS --------------------
--------------------------------------------------------------------------------

-- Compute an arbitrary mos interval as a vector of L's and s's.
-- The mossteps 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 (analogous to the minor 9th in standard
-- notation) 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, mossteps, size)
	local step_sequence = p.brightest_mode(mos)
	step_sequence = string.rep(step_sequence, math.ceil(mossteps/(mos.nL + mos.ns)))
	step_sequence = string.sub(step_sequence, 1, mossteps)
	
	local interval_vector = p.interval_from_step_sequence(mos, 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().
-- This has the same output as the above function, except arbitrary strings can
-- be entered, such as with step sequences for certain modmosses. The step types
-- are as follows:
-- - 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(mos, 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

--------------------------------------------------------------------------------
---------------------- INTERVAL ARITHMETIC FUNCTIONS ---------------------------
--------------------------------------------------------------------------------

-- Add two intervals together by adding their respective vectors
function p.add_intervals(v1, v2)
	local interval_vector = { 
		['L'] = v1['L'] + v2['L'],
		['s'] = v1['s'] + v2['s']
	}
	return interval_vector
end
	
-- Subtract two intervals together by subtracting their respective vectors
function p.subtract_intervals(v1, v2)
	local interval_vector = { 
		['L'] = v1['L'] - v2['L'],
		['s'] = v1['s'] - v2['s']
	}
	return interval_vector
end

-- Repeatedly add the same interval to itself
function p.multiply_interval(v1, amt)
	local interval_vector = { 
		['L'] = v1['L'] * amt,
		['s'] = v1['s'] * amt
	}
	return interval_vector
end
	
--------------------------------------------------------------------------------
--------- INTERVAL STEP COUNT FUNCTIONS FOR PERFECTABLE INTERVALS --------------
--------------------------------------------------------------------------------

-- Compute the size of the period in mossteps (L's plus s's)
function p.period_step_count(mos)
	local interval = p.period(mos)
	return interval['L'] + interval['s']
end

-- Compute the size of the equave in mossteps (L's plus s's)
function p.equave_step_count(mos)
	local interval = p.equave(mos)
	return interval['L'] + interval['s']
end

-- Compute the size of the bright gen in mossteps (L's plus s's)
function p.bright_gen_step_count(mos)
	local interval = p.bright_gen(mos)
	return interval['L'] + interval['s']
end

-- Compute the size of the dark gen in mossteps (L's plus s's)
function p.dark_gen_step_count(mos)
	local interval = p.dark_gen(mos)
	return interval['L'] + interval['s']
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

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

--------------------------------------------------------------------------------
----------------------------------- TESTER -------------------------------------
--------------------------------------------------------------------------------

-- Tester function
function p.tester()
	return p.add_intervals({ ['L'] = 3, ['s'] = 1}, { ['L'] = 3, ['s'] = 1})
end

return p