Module:TAMNAMS
Jump to navigation
Jump to search
This module is designed to handle TAMNAMS as it pertains to MOS scales. It is meant to be used with other modules, rather than something invoked directly or as part of a template.
This module should reflect current TAMNAMS conventions:
- Names for step ratios and ranges (soft, hard, etc), plus extended spectrum names (currently unsupported by module)
- Naming for intervals and scale degrees (M2ms and M2md)
- Naming for modes (simplified UDP)
- Naming for select scales
local p = {}
local mos = require("Module:MOS")
local rat = require("Module:Rational")
-- TODO
--Function to parse a UDP and (possibly) scale degrees.
--Separate interval/degree lookup into separate functions for for abbrevs and non-abbrev formats.
--Added arbitrary hardness lookup for a single ratio (e.g., passing in 13:8 would return "quasisoft".
--------------------------------------------------------------------------------
--------------------------- NAME LOOKUP TABLES ---------------------------------
--------------------------------------------------------------------------------
-- Lookup table for tamnams step ratios
p.step_ratios = {
["1:1"] = "equalized",
["4:3"] = "supersoft",
["3:2"] = "soft",
["5:3"] = "semisoft",
["2:1"] = "basic",
["5:2"] = "semihard",
["3:1"] = "hard",
["4:1"] = "superhard",
["1:0"] = "collapsed"
}
-- And step ratio ranges
p.step_ratio_ranges = {
["1:1 to 2:1"] = "soft-of-basic",
["1:1 to 3:2"] = "soft",
["1:1 to 4:3"] = "ultrasoft",
["4:3 to 3:2"] = "parasoft",
["3:2 to 2:1"] = "hyposoft",
["3:2 to 5:3"] = "quasisoft",
["5:3 to 2:1"] = "minisoft",
["2:1 to 5:2"] = "minihard",
["5:2 to 3:1"] = "quasihard",
["2:1 to 3:1"] = "hypohard",
["3:1 to 4:1"] = "parahard",
["4:1 to 1:0"] = "ultrahard",
["3:1 to 1:0"] = "hard",
["2:1 to 1:0"] = "hard-of-basic"
}
-- Lookup table for tamnams extended step ratios
p.step_ratios_ext = {
["1:1"] = "equalized",
["6:5"] = "semiequalized",
["4:3"] = "supersoft",
["3:2"] = "soft",
["5:3"] = "semisoft",
["2:1"] = "basic",
["5:2"] = "semihard",
["3:1"] = "hard",
["4:1"] = "superhard",
["6:1"] = "extrahard",
["10:1"] = "semicollapsed",
["1:0"] = "collapsed"
}
-- And extended step ratio ranges
p.step_ratio_ranges_ext = {
["1:1 to 2:1"] = "soft-of-basic",
["1:1 to 3:2"] = "soft",
["1:1 to 6:5"] = "pseudoequalized",
["6:5 to 4:3"] = "ultrasoft",
["4:3 to 3:2"] = "parasoft",
["3:2 to 2:1"] = "hyposoft",
["3:2 to 5:3"] = "quasisoft",
["5:3 to 2:1"] = "minisoft",
["2:1 to 5:2"] = "minihard",
["5:2 to 3:1"] = "quasihard",
["2:1 to 3:1"] = "hypohard",
["3:1 to 4:1"] = "parahard",
["4:1 to 6:1"] = "hyperhard",
["6:1 to 10:1"] = "clustered",
["4:1 to 10:1"] = "ultrahard",
["10:1 to 1:0"] = "pseudocollapsed",
["3:1 to 1:0"] = "hard",
["2:1 to 1:0"] = "hard-of-basic"
}
-- Lookup table for tamnams names within the range of 6-10 steps
p.mos_names = {
["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"
}
-- And prefixes
p.mos_prefixes = {
["1L 1s"] = "monwd",
["2L 2s"] = "biwd",
["2L 3s"] = "pent",
["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"
}
-- And abbrevs
p.mos_abbrevs = {
["1L 1s"] = "w",
["2L 2s"] = "bw",
["1L 5s"] = "amk",
["2L 4s"] = "mal",
["3L 3s"] = "tw",
["4L 2s"] = "cit",
["5L 1s"] = "mk",
["1L 6s"] = "on",
["2L 5s"] = "pel",
["3L 4s"] = "mosh",
["4L 3s"] = "smi",
["5L 2s"] = "dia",
["6L 1s"] = "arc",
["1L 7s"] = "ap",
["2L 6s"] = "sb",
["3L 5s"] = "chk",
["4L 4s"] = "ttw",
["5L 3s"] = "onei",
["6L 2s"] = "ek",
["7L 1s"] = "p",
["1L 8s"] = "ablu",
["2L 7s"] = "bz",
["3L 6s"] = "ch",
["4L 5s"] = "gm",
["5L 4s"] = "ct",
["6L 3s"] = "hy",
["7L 2s"] = "arm",
["8L 1s"] = "blu",
["1L 9s"] = "asi",
["2L 8s"] = "ja",
["3L 7s"] = "sp",
["4L 6s"] = "lm",
["5L 5s"] = "pw",
["6L 4s"] = "le",
["7L 3s"] = "di",
["8L 2s"] = "ta",
["9L 1s"] = "si"
}
-- TAMNAMS equave-agnostic names
p.equave_agnostic_names = {
["1L 1s"] = "trivial",
["1L 2s"] = "antrial",
["2L 1s"] = "trial",
["1L 3s"] = "antetric",
["3L 1s"] = "tetric",
["1L 4s"] = "pedal",
["2L 3s"] = "pentic",
["3L 2s"] = "anpentic",
["4L 1s"] = "manual"
}
-- And prefixes
p.equave_agnostic_prefixes = {
["1L 1s"] = "trv",
["1L 2s"] = "at",
["2L 1s"] = "t",
["1L 3s"] = "att",
["3L 1s"] = "tt",
["1L 4s"] = "pd",
["2L 3s"] = "pt",
["3L 2s"] = "apt",
["4L 1s"] = "mnu"
}
-- And abbrevs
p.equave_agnostic_abbrevs = {
["1L 1s"] = "trv",
["1L 2s"] = "at",
["2L 1s"] = "t",
["1L 3s"] = "att",
["3L 1s"] = "tt",
["1L 4s"] = "ped",
["2L 3s"] = "pt",
["3L 2s"] = "apt",
["4L 1s"] = "mnu"
}
--------------------------------------------------------------------------------
------------------------------ HELPER FUNCTIONS --------------------------------
--------------------------------------------------------------------------------
-- Step ratios are entered as an array of two numeric values, or alternatively,
-- as a ratio as defined by the rational module. If of the former, this helper
-- function converts it to the latter. This preprocess step is for simplifying
-- ratios.
function p.preprocess_step_ratio(step_ratio)
if type(step_ratio) == "string" then
return step_ratio
elseif (type(step_ratio) == "table" and type(step_ratio[1]) == "number" and type(step_ratio[2]) == "number") then
return rat.new(step_ratio[1], step_ratio[2])
else
return nil
end
end
-- Mosses for name lookup are entered either as a scalesig or as a mos as
-- defined in the mos module. If of the latter, it's converted into a textual
-- scalesig. Scalesigs should have a normal space, not a nonbreaking space, as
-- the lookup tables use a normal space.
function p.preprocess_scalesig(input_mos)
if type(input_mos) == "string" then
local parsed_mos = mos.parse(input_mos)
return mos.as_string(parsed_mos, false)
elseif type(input_mos) == "table" then
return mos.as_string(input_mos, false)
else
return nil
end
end
--------------------------------------------------------------------------------
------------------------ TEMPLATE HELPER FUNCTIONS -----------------------------
--------------------------------------------------------------------------------
-- Verifier function that checks for a prefix already provided by TAMNAMS.
-- If there is one provided, use that. If there is none, use the one passed in.
-- If no prefix is provided, defualt to "mos". If "NONE" is entered, return an
-- empty string.
function p.verify_prefix(input_mos, mos_prefix)
local mos_prefix = p.lookup_prefix(input_mos) or mos_prefix or "mos"
if mos_prefix == "NONE" or mos_prefix == "none" then
mos_prefix = ""
elseif mos_prefix == "" then
mos_prefix = "mos"
end
return mos_prefix
end
-- Verifier function that checks for an abbrev already provided by TAMNAMS.
-- If there is one provided, use that. If there is none, use the one passed in.
-- If no abbrev is provided, defualt to "m". If "NONE" is entered, return an
-- empty string.
function p.verify_abbrev(input_mos, mos_abbrev)
local mos_abbrev = p.lookup_abbrev(input_mos) or mos_abbrev or "m"
if mos_abbrev == "NONE" or mos_abbrev == "none" then
mos_abbrev = ""
elseif mos_abbrev == "" then
mos_abbrev = "m"
end
return mos_abbrev
end
--------------------------------------------------------------------------------
------------------------- NAME LOOKUP FUNCTIONS --------------------------------
--------------------------------------------------------------------------------
-- Function for looking up a mos's name (octave-equivalent mosses only).
-- Can accept either a mos (defined by mos module) or its scalesig.
function p.lookup_name(input_mos)
local scalesig = p.preprocess_scalesig(input_mos)
return p.mos_names[scalesig]
end
-- Function for looking up a mos's prefix (octave-equivalent mosses only).
-- Can accept either a mos (defined by mos module) or its scalesig.
function p.lookup_prefix(input_mos)
local scalesig = p.preprocess_scalesig(input_mos)
return p.mos_prefixes[scalesig]
end
-- Function for looking up a mos's abbrev (octave-equivalent mosses only).
-- Can accept either a mos (defined by mos module) or its scalesig.
function p.lookup_abbrev(input_mos)
local scalesig = p.preprocess_scalesig(input_mos)
return p.mos_abbrevs[scalesig]
end
-- Function for looking up a step ratio range
-- Module:Rational is used to help simplify ratios
function p.lookup_step_ratio(step_ratio, use_extended)
local step_ratio = p.preprocess_step_ratio(step_ratio)
local use_extended = use_extended == true
-- Produce the key needed to lookup the step ratio name
-- use_extended is used to toggle between central range and extended range
local key = rat.as_ratio(step_ratio, ":")
local named_ratio = use_extended and p.step_ratios_ext[key] or p.step_ratios[key]
return named_ratio
end
-- Function for looking up a step ratio range
-- Module:Rational is used to help simplify ratios
function p.lookup_step_ratio_range(step_ratio_1, step_ratio_2, use_extended)
local step_ratio_1 = p.preprocess_step_ratio(step_ratio_1)
local step_ratio_2 = p.preprocess_step_ratio(step_ratio_2)
local use_extended = use_extended == true
-- Produce the key needed for the lookup table as a/b to c/d
-- Swap ratios if ratio 1 has a higher hardness than ratio 2
local key = ""
local float_1 = rat.as_float(step_ratio_1)
local float_2 = rat.as_float(step_ratio_2)
if (float_1 > float_2) then
key = string.format("%s to %s", rat.as_ratio(step_ratio_2, ":"), rat.as_ratio(step_ratio_1, ":"))
else
key = string.format("%s to %s", rat.as_ratio(step_ratio_1, ":"), rat.as_ratio(step_ratio_2, ":"))
end
-- use_extended is used to toggle between central range and extended range
local named_ratio_range = use_extended and p.step_ratio_ranges_ext[key] or p.step_ratio_ranges[key]
return named_ratio_range
end
--------------------------------------------------------------------------------
------------------------- NAME FINDER FUNCTIONS --------------------------------
--------------------------------------------------------------------------------
-- Helper function
-- "Rounds" step ratios up to the nearest named ratio
function p.step_ratio_ceil(step_ratio)
local hardness = step_ratio[1] / step_ratio[2]
local rounded_step_ratio = nil
if hardness > 1/1 and hardness <= 4/3 then
rounded_step_ratio = {4,3}
elseif hardness > 4/3 and hardness <= 3/2 then
rounded_step_ratio = {3,2}
elseif hardness > 3/2 and hardness <= 5/3 then
rounded_step_ratio = {5,3}
elseif hardness > 5/3 and hardness <= 2/1 then
rounded_step_ratio = {2,1}
elseif hardness > 2/1 and hardness <= 5/2 then
rounded_step_ratio = {5,2}
elseif hardness > 5/2 and hardness <= 3/1 then
rounded_step_ratio = {3,1}
elseif hardness > 3/1 and hardness <= 4/1 then
rounded_step_ratio = {4,1}
elseif hardness > 4/1 and hardness <= 1/0 then
rounded_step_ratio = {1,0}
end
return rounded_step_ratio
end
-- Helper function
-- "Rounds" step ratios down to the nearest named ratio
function p.step_ratio_floor(step_ratio)
local hardness = step_ratio[1] / step_ratio[2]
local rounded_step_ratio = nil
if hardness >= 1/1 and hardness < 4/3 then
rounded_step_ratio = {1,1}
elseif hardness >= 4/3 and hardness < 3/2 then
rounded_step_ratio = {4,3}
elseif hardness >= 3/2 and hardness < 5/3 then
rounded_step_ratio = {3,2}
elseif hardness >= 5/3 and hardness < 2/1 then
rounded_step_ratio = {5,3}
elseif hardness >= 2/1 and hardness < 5/2 then
rounded_step_ratio = {2,1}
elseif hardness >= 5/2 and hardness < 3/1 then
rounded_step_ratio = {5,2}
elseif hardness >= 3/1 and hardness < 4/1 then
rounded_step_ratio = {3,1}
elseif hardness >= 4/1 and hardness < 1/0 then
rounded_step_ratio = {4,1}
end
return rounded_step_ratio
end
-- Function for finding the smallest step ratio range that encompasses the two
-- ratios passed in.
function p.find_step_ratio_range_for_ratio_pair(step_ratio_1, step_ratio_2, use_extended)
local use_extended = use_extended == true -- Does nothing for now
-- Swap ratios so they're in the right order
local hardness_1 = step_ratio_1[1] / step_ratio_1[2]
local hardness_2 = step_ratio_2[1] / step_ratio_2[2]
local lower_ratio = nil
local upper_ratio = nil
local named_ratio_range = ""
if hardness_1 <= hardness_2 then
lower_ratio = p.step_ratio_floor(step_ratio_1)
upper_ratio = p.step_ratio_ceil (step_ratio_2)
else
lower_ratio = p.step_ratio_floor(step_ratio_2)
upper_ratio = p.step_ratio_ceil (step_ratio_1)
end
-- If one ratio corresponds to the endpoint of a named hardness range
-- but the other ratio exceeds that of a smaller range, default to the
-- largest range that would accommodate it.
-- 2:1 to (L:s > 3:1) = hard-of-basic
-- 4:1 and up = ultrahard
-- (L:s < 3:2) to 2:1 = soft-of-basic
-- 4:3 and lower = ultrasoft
if (lower_ratio[1]/lower_ratio[2] == 2/1 and upper_ratio[1]/upper_ratio[2] > 3/1) or lower_ratio[1]/lower_ratio[2] == 4/1 then
upper_ratio = {1,0}
elseif (upper_ratio[1]/upper_ratio[2] == 2/1 and lower_ratio[1]/lower_ratio[2] < 3/2) or upper_ratio[1]/upper_ratio[2] == 4/3 then
lower_ratio = {1,1}
end
local named_ratio_range = p.lookup_step_ratio_range(lower_ratio, upper_ratio, use_extended)
return named_ratio_range
end
-- Given a mos, find the ancestor mos within the target note count.
function p.find_ancestor(input_mos, target_note_count)
local target_note_count = target_note_count or 10
local z = input_mos.nL
local w = input_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 mos.new(z, w, input_mos.equave)
end
-- Given a mos, find the ancestor mos within the target note count, while also
-- returning the step ancestor's step ratio range (as two ratios) and the number
-- of generations between the two mosses. (A more in-depth version of the prev.)
function p.find_ancestor_info(input_mos, target_step_count)
local target_step_count = target_step_count or 10
-- For an ancestor mos zU wv and descendant xL ys, how many steps of size
-- L and s can fit inside U and v? (basically the chunking operation)
local z = input_mos.nL
local w = input_mos.ns
local lg_chunk = { nL = 1, ns = 0 }
local sm_chunk = { nL = 0, ns = 1 }
local generations = 0
while (z ~= w) and (z + w > target_step_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
-- Count how many generations
generations = generations + 1
-- Update step ratios
z = m2
w = m1 - m2
-- Update large chunk
local prev_lg_chunk = { nL = lg_chunk.nL, ns = lg_chunk.ns }
lg_chunk.nL = lg_chunk.nL + sm_chunk.nL
lg_chunk.ns = lg_chunk.ns + sm_chunk.ns
-- Update small chunk
if z ~= z_prev then
sm_chunk = prev_lg_chunk
end
end
-- Translate chunks into step ratios
local num1 = lg_chunk.nL + lg_chunk.ns
local den1 = sm_chunk.nL + sm_chunk.ns
local num2 = lg_chunk.nL
local den2 = sm_chunk.nL
local ratio_1, ratio_2
if num1/den1 < num2/den2 then
ratio_1 = { num1, den1 }
ratio_2 = { num2, den2 }
else
ratio_2 = { num1, den1 }
ratio_1 = { num2, den2 }
end
return mos.new(z, w, input_mos.equave), ratio_1, ratio_2, generations
end
--------------------------------------------------------------------------------
--------------------- MOSSTEP/MOSDEGREE QUALITY FUNCTIONS ----------------------
--------------------------------------------------------------------------------
-- Given an interval vector for a mos, produce the name for that interval.
-- Prefix lookup is done automatically if no prefix is provided; defaults to
-- "mos" if no prefix is found. Prefixes are used for the full name for an
-- interval, whereas abbrevs are used for the abbreviated form. (This is
-- because some abbrevs are shorter than the corresponding prefix.)
-- Formats are as follows:
-- - NONE: full name, eg "perfect 4-diastep"
-- - SENTENCE-CASE: same as NONE, but with capitalized first letter
-- - SHORTENED: shortened form, eg "Perf. 4-diastep"
-- - ABBREV: abbreviated form, eg "P4dias"
function p.interval_quality(interval, input_mos, abbrev_format, mos_prefix)
local abbrev_format = abbrev_format or "none"
local mos_prefix = mos_prefix
or (abbrev_format == "abbrev" and p.lookup_abbrev(input_mos) or p.lookup_prefix(input_mos))
or (abbrev_format == "abbrev" and "m" or "mos")
-- Get the step count of the interval. The sum of L's and s's will always
-- determine what k-mosstep the interval is.
local step_count = mos.interval_step_count(interval)
-- Decode the quality
local quality = p.decode_quality(interval, input_mos, abbrev_format)
if abbrev_format == "abbrev" or abbrev_format == "ABBREV" then
return string.format("%s%d%ss", quality, step_count, mos_prefix)
elseif abbrev_format == "shortened" or abbrev_format == "SHORTENED" then
return string.format("%s %d-%ss.", quality, step_count, mos_prefix)
elseif abbrev_format == "sentence-case" or abbrev_format == "SENTENCE-CASE" then
return string.format("%s %d-%sstep", quality, step_count, mos_prefix)
else
return string.format("%s %d-%sstep", quality, step_count, mos_prefix)
end
end
-- Given an interval vector for a mos, produce the name for the scale degree
-- reached by going up that interval, from the root. (This is identical to the
-- previous function, except it uses "degree" instead of "step".)
-- Prefix lookup is done automatically, as with interval_quality().
-- Formats are as follows:
-- - NONE: full name, eg "perfect 4-diadegree"
-- - SENTENCE-CASE: same as NONE, but with capitalized first letter
-- - SHORTENED: shortened form, eg "Perf. 4-diadegree"
-- - ABBREV: abbreviated form, eg "P4diad"
function p.degree_quality(interval, input_mos, abbrev_format, mos_prefix)
local abbrev_format = abbrev_format or "none"
local mos_prefix = mos_prefix
or (abbrev_format == "abbrev" and p.lookup_abbrev(input_mos) or p.lookup_prefix(input_mos))
or (abbrev_format == "abbrev" and "m" or "mos")
-- Get the step count of the interval. The sum of L's and s's will always
-- determine what k-mosstep the interval is.
local step_count = mos.interval_step_count(interval)
-- Decode the quality
local quality = p.decode_quality(interval, input_mos, abbrev_format)
if abbrev_format == "abbrev" or abbrev_format == "ABBREV" then
return string.format("%s%d%sd", quality, step_count, mos_prefix)
elseif abbrev_format == "shortened" or abbrev_format == "SHORTENED" then
return string.format("%s %d-%sd.", quality, step_count, mos_prefix)
elseif abbrev_format == "sentence-case" or abbrev_format == "SENTENCE-CASE" then
return string.format("%s %d-%sdegree", quality, step_count, mos_prefix)
else
return string.format("%s %d-%sdegree", quality, step_count, mos_prefix)
end
end
-- Decodes the quality of a mosstep. Helper function to interval_quality() and
-- degree_quality(), but can be used standalone if only the keyword (maj, min,
-- aug, perf, dim) is needed. The chroma amounts are as follows:
-- AMT| PERFECTABLE | NONPERFECTABLE | DARK GEN ONLY
-- ---+-----------------+-------------------+------------------
-- ...| . . . | . . . | . . .
-- 4 | 4x augmented | 4x augmented | 5x augmented
-- 3 | 3x augmented | 3x augmented | 4x augmented
-- 2 | 2x augmented | 2x augmented | 3x augmented
-- 1 | augmented | augmented | 4x augmented
-- 0 | perfect | major | augmented
-- -1 | diminished | minor | perfect
-- -2 | 2x diminished | diminished | diminished
-- -3 | 3x diminished | 2x diminished | 2x diminished
-- -4 | 4x diminished | 3x diminished | 3x diminished
-- -5 | 5x diminished | 4x diminished | 4x diminished
-- ...| . . . | . . . | . . .
function p.decode_quality(interval, input_mos, abbrev_format)
local abbrev_format = abbrev_format or "none" -- Default is no abbreviation
-- Normalize the interval so negative values aren't being used.
local interval = mos.normalize_interval(interval)
-- Get the step count of the interval. The sum of L's and s's will always
-- determine what k-mosstep the interval is.
local step_count = mos.interval_step_count(interval)
-- Determine what "special" type the interval is so that the designations
-- of augmented/perfect/diminished (APd) apply, skipping major/minor (Mm).
-- If it's the period or equave, then it's a multiple of the period.
-- If it's any one of the gens, then reducing it should produce that gen.
local is_period = step_count % mos.period_step_count(input_mos) == 0
local is_bright_gen = step_count % mos.period_step_count(input_mos) == mos.bright_gen_step_count(input_mos)
local is_dark_gen = step_count % mos.period_step_count(input_mos) == mos.dark_gen_step_count(input_mos)
-- Special case: APd does not apply to a root mos's (nL ns) generators;
-- instead, it's Mm.
local is_root_mos = input_mos.nL == input_mos.ns
-- Is perfectable? This is for intervals for which maj/min does not apply.
local is_perfectable = is_period or (is_bright_gen and not is_root_mos) or (is_dark_gen and not is_root_mos)
-- Get chroma count and adjust as needed
local chroma_count = 0
if is_period then
-- Chroma count 0 is the perfect size. This interval does not appear
-- as any other size across all mos modes.
chroma_count = mos.interval_chroma_count(interval, input_mos)
elseif is_bright_gen and not is_root_mos then
-- Chroma count 0 is the large size, and -1 the small size; these
-- are perfect and diminished respectively.
chroma_count = mos.interval_chroma_count(interval, input_mos)
elseif is_dark_gen and not is_root_mos then
-- Chroma count 0 is the large size, and -1 the small size; these
-- are augmented and perfect respectively. Since the perfect size
-- corresponds to a chroma count of -1, pass in -1 as the 3rd arg.
chroma_count = mos.interval_chroma_count(interval, input_mos, -1)
else
-- Chroma count 0 is the large size, and -1 the small size; these are
-- major and minor respectively.
chroma_count = mos.interval_chroma_count(interval, input_mos)
end
-- Get absolute value of chroma count
local chroma_abs = math.abs(chroma_count)
local quality = ""
if is_perfectable then
-- Get the quality for perfectable intervals
if abbrev_format == "none" or abbrev_format == "NONE" then
if chroma_count < 0 then
quality = "diminished"
elseif chroma_count > 0 then
quality = "augmented"
else
quality = "perfect"
end
if chroma_abs > 1 then
quality = string.format("%d× %s", chroma_abs, quality)
end
elseif abbrev_format == "sentence-case" or abbrev_format == "SENTENCE-CASE" then
if chroma_count < 0 then
quality = "Diminished"
elseif chroma_count > 0 then
quality = "Augmented"
else
quality = "Perfect"
end
if chroma_abs > 1 then
quality = string.format("%d× %s", chroma_abs, quality)
end
elseif abbrev_format == "shortened" or abbrev_format == "SHORTENED" then
if chroma_count < 0 then
quality = "Dim."
elseif chroma_count > 0 then
quality = "Aug."
else
quality = "Perf."
end
if chroma_abs > 1 then
quality = string.format("%d× %s", chroma_abs, quality)
end
elseif abbrev_format == "abbrev" or abbrev_format == "ABBREV" then
if chroma_count < 0 then
quality = "d"
elseif chroma_count > 0 then
quality = "A"
else
quality = "P"
end
if chroma_abs > 3 then
quality = string.format("%s<sup>%d</sup>", quality, chroma_abs)
elseif chroma_abs > 1 and chroma_abs <= 3 then
quality = string.rep(quality, chroma_abs)
end
end
else
-- Get the quality for nonperfectable intervals
-- Is the interval major? If not, decrement chroma_abs by 1
local is_positive = chroma_count >= 0
chroma_abs = is_positive and chroma_abs or chroma_abs - 1
if abbrev_format == "none" or abbrev_format == "NONE" then
if chroma_abs > 0 and is_positive then
quality = "augmented"
elseif chroma_abs > 0 and not is_positive then
quality = "diminished"
else
quality = is_positive and "major" or "minor"
end
if chroma_abs > 1 then
quality = string.format("%d× %s", chroma_abs, quality)
end
elseif abbrev_format == "sentence-case" or abbrev_format == "SENTENCE-CASE" then
if chroma_abs > 0 and is_positive then
quality = "Augmented"
elseif chroma_abs > 0 and not is_positive then
quality = "Diminished"
else
quality = is_positive and "Major" or "Minor"
end
if chroma_abs > 1 then
quality = string.format("%d× %s", chroma_abs, quality)
end
elseif abbrev_format == "shortened" or abbrev_format == "SHORTENED" then
if chroma_abs > 0 and is_positive then
quality = "Aug."
elseif chroma_abs > 0 and not is_positive then
quality = "Dim."
else
quality = is_positive and "Maj." or "Min."
end
if chroma_abs > 1 then
quality = string.format("%d× %s", chroma_abs, quality)
end
elseif abbrev_format == "abbrev" or abbrev_format == "ABBREV" then
if chroma_abs > 0 and is_positive then
quality = "A"
elseif chroma_abs > 0 and not is_positive then
quality = "d"
else
quality = is_positive and "M" or "m"
end
if chroma_abs > 3 then
quality = string.format("%s<sup>%d</sup>", quality, chroma_abs)
elseif chroma_abs > 1 and chroma_abs <= 3 then
quality = string.rep(quality, chroma_abs)
end
end
end
return quality
end
--------------------------------------------------------------------------------
-------------------- MODE NOTATION/COMPARISON FUNCTIONS ------------------------
-------------------------- BASED ON UDP NOTATION -------------------------------
--------------------------------------------------------------------------------
-- Given the number of gens up and down per period and a period count (u, d, and
-- p respectively), construct the UDP as a string, as up|dp(p) or u|d, for
-- multi-period and single-period respectively.
-- If only u is known, then d = p - u - 1.
-- If only d is known, then u = p - d - 1. (Basically, u + d + 1 = steps/p.)
-- A table of alterations can also be passed in, assuming they are already
-- written out (EG, d3md or, if notation is established, b4).
function p.udp_as_string(gens_up_per_period, gens_down_per_period, periods, alterations)
local periods = periods or 1
local alterations_as_string = ""
if alterations ~= nil then
for i = 1, #alterations do
alterations_as_string = alterations_as_string .. " " .. alterations[i]
end
end
if periods == 1 then
return string.format("%s{{pipe}}%s", gens_up_per_period, gens_down_per_period) .. alterations_as_string
else
return string.format("%s{{pipe}}%s(%s)", gens_up_per_period * periods, gens_down_per_period * periods, periods) .. alterations_as_string
end
end
-- Given an input mos, list the udps for each of its modes, listed in order of
-- decreasing brightness.
function p.mos_mode_udps(input_mos)
local steps_per_period = mos.period_step_count(input_mos)
local period_count = mos.period_count(input_mos)
local udps = {}
for i = 1, steps_per_period do
local gens_up = steps_per_period - i
local gens_down = steps_per_period - gens_up - 1
local udp = p.udp_as_string(gens_up, gens_down, period_count)
table.insert(udps, udp)
end
return udps
end
-- Given an input mos, list the cpos for each of its modes. The circular
-- permutation orderings are listed starting from the brightest mode.
-- Example with 5L 2s modes
-- MODE NAME | UDP | CPO
-- ------------+-----+-----
-- Lydian | 6|0 | 1
-- Ionian | 5|1 | 5
-- Mixolydian | 4|2 | 2
-- Dorian | 3|3 | 6
-- Aeolian | 2|4 | 3
-- Phrygian | 1|5 | 7
-- Lociran | 0|6 | 4
function p.mos_mode_cpos(input_mos)
local steps_per_period = mos.period_step_count(input_mos)
local period_count = mos.period_count(input_mos)
local steps_per_bright_gen = mos.bright_gen_step_count(input_mos)
local cpos = {}
for i = 1, steps_per_period do
local cpo = ((i - 1) * steps_per_bright_gen) % steps_per_period + 1
table.insert(cpos, cpo)
end
return cpos
end
-- Given a string that represents a mode, return the udps for each of its
-- rotations. If the mode is a modmos, the closest mode and its alterations
-- are returned as a string.
-- NOTES:
-- - A period of repetition will always have as many modes as there are steps
-- in that period. If it were any less, then the true period of repetition
-- is a substring of that.
function p.mode_rotation_udps(input_mode, input_mos, mos_abbrev, use_brightest_mode_search)
local use_brightest_mode_search = use_brightest_mode_search == nil or use_brightest_mode_search
local modes = mos.mode_rotations(input_mode)
local udps = {}
for i = 1, #modes do
table.insert(udps, p.mode_udp(modes[i], input_mos, mos_abbrev, use_brightest_mode_search))
end
return udps
end
-- Given a string that represents a mode, return its udp.
-- Helper function for mode_rotation_udps().
-- If a mode is for a modmos, it will return the closest brightest mode followed
-- by its altered scale degrees. Alterations require a mos abbrev, which is
-- automatically looked up, defaulting to "m" if no abbrev can be found.
-- NOTES:
-- - This is inefficient when used on true-mos modes since it's a brute-force
-- search for what mode it's closest to. It is, however, effective on
-- modmosses, which is the primary use for this function.
function p.mode_udp(input_mode, input_mos, mos_abbrev, use_brightest_mode_search)
local use_brightest_mode_search = use_brightest_mode_search == nil or use_brightest_mode_search
local mos_abbrev = mos_abbrev or p.lookup_abbrev(input_mos) or "m"
local true_modes = mos.modes_to_step_matrices(input_mos)
local input_mode_as_step_matrix = mos.mode_to_step_matrix(input_mode)
-- For each mode, count the number of differences between each true mode
-- and the entered mode and keep track of which mode has the fewest diffs.
-- That mode is considered the closest mode, whose UDP will be used for the
-- mode name, followed by which scale degrees are changed.
-- If the number of diffs is ever zero, then the entered mode was a true-mos
-- mode and has zero alterations.
local lowest_differences = #input_mode_as_step_matrix
local bright_gens_down_per_period = 0
local differences = {}
for i = 1, #true_modes do
local current_true_mode = true_modes[i]
local current_differences = p.differences_between_modes(current_true_mode, input_mode_as_step_matrix)
-- It's possible for more than one mode to be closest. The tiebreaker is
-- whichever mode is brightest (or darkest, which can be toggled).
if use_brightest_mode_search then
if #current_differences < lowest_differences then
-- Brightest-closest match
bright_gens_down_per_period = i - 1
lowest_differences = #current_differences
differences = current_differences
end
else
if #current_differences <= lowest_differences then
-- Darkest-closest match
bright_gens_down_per_period = i - 1
lowest_differences = #current_differences
differences = current_differences
end
end
end
-- Parse the differences as scale degrees.
-- The differences between the true mode and the input mode are denoted as
-- interval vectors. These should be parsed into a list of altered scale
-- degrees before being passed into udp_as_string. Coding it this way allows
-- for the possibility of adding custom mos notation (EG, diamond-mos).
local alterations = {}
for i = 1, #differences do
table.insert(alterations, p.degree_quality(differences[i], input_mos, "ABBREV", mos_abbrev))
end
-- Produce the UDP (as text) for the mode, formatted as up|dp(p) for multi-
-- period mosses, or u|d for single-period mosses.
local period_count = mos.period_count(input_mos)
local bright_gens_up_per_period = mos.period_step_count(input_mos) - 1 - bright_gens_down_per_period
udp = p.udp_as_string(bright_gens_up_per_period, bright_gens_down_per_period, period_count, alterations)
return udp
end
-- Helper function for mode_udp, but can be used standalone.
-- Given two modes as step matrices, produce a list of differences between the
-- base mode and the altered mode. Diffs are listed as an array of mosstep
-- vectors (a table containing the number of L's and s's for that interval).
function p.differences_between_modes(base_step_matrix, altered_step_matrix)
local differences = {}
for i = 1, #altered_step_matrix do
local altered_interval = altered_step_matrix[i]
-- If i is greater than the number of intervals in base_step_matrix,
-- then the interval being accessed is an extra-equave interval. Instead
-- of accessing the ith interval (which would lead to a nil error),
-- access the corresponding equave-reduced interval, then raise it by
-- the necessary number of equaves.
local base_interval = {}
if i > #base_step_matrix then
local current_mossteps = i - 1
local equave_step_count = #base_step_matrix - 1
local equave_interval = base_step_matrix[#base_step_matrix]
local equave_reduced_interval = base_step_matrix[current_mossteps % equave_step_count + 1]
local equave_count = math.floor(current_mossteps / equave_step_count)
local equaves = mos.interval_mul(equave_interval, equave_count)
base_interval = mos.interval_add(equave_reduced_interval, equaves)
else
base_interval = base_step_matrix[i]
end
if not mos.interval_eq(base_interval, altered_interval) then
table.insert(differences, altered_interval)
end
end
return differences
end
--------------------------------------------------------------------------------
----------------------------- TESTER FUNCTION ----------------------------------
--------------------------------------------------------------------------------
function p.tester()
local input_mos = mos.new(5,2)
return p.preprocess_scalesig(input_mos)
end
return p