Module:MOS notation: Difference between revisions

Ganaram inukshuk (talk | contribs)
Module now uses ordinal module
ArrowHead294 (talk | contribs)
m Sort dependencies
 
(11 intermediate revisions by 3 users not shown)
Line 1: Line 1:
-- This is a helper module that contains commonly used functions for:
-- - MOS degrees
-- - MOS gamut
local mos = require('Module:MOS')
local rat = require('Module:Rational')
local ord = require('Module:Ordinal')
local p = {}
local p = {}


-- Helper function for parsing notation entered as a string; for example,
local mos = require("Module:MOS")
local ord = require("Module:Ordinal")
local rat = require("Module:Rational")
local utils = require("Module:Utils")
 
-- ----------------------------------------------------------------------------
-- ------------------------ PARSER FUNCTIONS ----------------------------------
-- ----------------------------------------------------------------------------
 
-- Parser function
-- Parses notation entered as a string; for example,
-- "CDEFGAB; #; b" becomes an associative array, where:
-- "CDEFGAB; #; b" becomes an associative array, where:
-- - the key ['Naturals'] has the value "CDEFGAB"; also called "nominals"
-- - the key ['Naturals'] has the value "CDEFGAB"; also called "nominals"
Line 18: Line 22:
local parsed = {}
local parsed = {}
for entry in string.gmatch(unparsed, '([^;]+)') do
for entry in string.gmatch(unparsed, "([^;]+)") do
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
table.insert(parsed, trimmed) -- Add to array
table.insert(parsed, trimmed) -- Add to array
end
end
local notation = { ['Naturals'] = parsed[1], ['Sharp'] = parsed[2], ['Flat'] = parsed[3] }
local notation = { ["Naturals"] = parsed[1], ["Sharp"] = parsed[2], ["Flat"] = parsed[3] }
if #parsed == 3 then
if #parsed == 3 then
return notation
return notation
Line 31: Line 35:
end
end


-- Helper function for parsing a step ratio entered as a string "p/q"
-- Parser function
-- Parses a step ratio entered as a string "p/q"
function p.parse_step_ratio(unparsed)
function p.parse_step_ratio(unparsed)
local parsed = {}
local parsed = {}
for entry in string.gmatch(unparsed, '([^/]+)') do
for entry in string.gmatch(unparsed, "([^/]+)") do
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
table.insert(parsed, trimmed) -- Add to array
table.insert(parsed, trimmed) -- Add to array
Line 47: Line 52:
end
end


-- Helper function to simplify step ratio
-- Helper function
-- Simplifies step ratio
function p.simplify_step_ratio(step_ratio_unsimplified)
function p.simplify_step_ratio(step_ratio_unsimplified)
local kp = step_ratio_unsimplified[1]
local kp = step_ratio_unsimplified[1]
local kq = step_ratio_unsimplified[2]
local kq = step_ratio_unsimplified[2]
local k = rat.gcd(kp, kq)
local k = utils._gcd(kp, kq)
local num = kp / k
local num = kp / k
local den = kq / k
local den = kq / k
Line 59: Line 65:
end
end


-- Helper function for parsing a UDP entered as a string "up,dp"
-- Parser function
-- Parses a UDP entered as a string "up,dp"
-- To avoid potential issues, the "," character is used instead of "|"
-- To avoid potential issues, the "," character is used instead of "|"
function p.parse_udp(step_ratio_unparsed)
function p.parse_udp(step_ratio_unparsed)
local parsed = {}
local parsed = {}
for entry in string.gmatch(step_ratio_unparsed, '([^,]+)') do
for entry in string.gmatch(step_ratio_unparsed, "([^,]+)") do
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
table.insert(parsed, trimmed) -- Add to array
table.insert(parsed, trimmed) -- Add to array
Line 76: Line 83:
end
end


-- Helper function that converts a note name given as a quantity of mossteps
-- ----------------------------------------------------------------------------
-- ------------------------ DECODER FUNCTIONS ---------------------------------
-- ----------------------------------------------------------------------------
 
-- Decoder function
-- Decodes a note name given as a quantity of mossteps
-- and chromas (see gamut function) into a name, such as "C#"
-- and chromas (see gamut function) into a name, such as "C#"
-- To be used in conjunction with the genchain function
-- To be used in conjunction with the genchain function
function p.mosstep_and_chroma_to_note_name(mossteps, chromas, note_symbol, chroma_symbol)
-- Function is currently not used in any other modules as of time 2023-10-21
function p.decode_note_name(mossteps, chromas, note_symbol, chroma_symbol)
local note_name = note_symbol .. string.rep(chroma_symbol, math.abs(chromas))
local note_name = note_symbol .. string.rep(chroma_symbol, math.abs(chromas))
Line 85: Line 98:
end
end


-- Helper function for creating a genchain, or specifically, a nominal-accidental chain.
-- Decoder function
-- Decodes a scale degree given as a quantity of mossteps
-- and a numeric quality (0=perf, 1=maj, -1=min, 2=aug, -2=dim, etc) into a
-- scale degree
-- To be used in conjunction with the degrees function
-- For notation: options include mosstep, mosdegree, and ordinal (not recommended except for maybe 5L 2s)
-- For wording: options include abbreviated or not abbreviated (type in nothing for this option)
-- This function is formerly:
-- function p.mosstep_and_quality_to_degree(mossteps, quality, prefix, notation, wording)
-- Changes:
-- - Encoded mosstep can now be passed directly without passing the mosstep and quality individually.
function p.decode_mosstep_quality(encoded_mosstep, prefix, notation, wording)
-- Get the mossteps and quality from the encoded mosstep
local mossteps = encoded_mosstep["Mossteps"]
local quality = encoded_mosstep["Quality"]
-- Notation options currently include:
-- - mosstep, for intervals
-- - mosdegree, for scale degrees
-- - ordinal, for diatonic-like numbering; can be used for either intervals
--  or scale degrees
local prefix = prefix or "mos" -- Default prefix is mos
local notation = notation or "mosdegree" -- Default notation is mosdegree
local wording = wording or "" -- Default wording is no abbreviations
 
local degree_name = ""
if wording ~= "abbreviated" then
if notation == "mosstep" then
degree_name = mossteps .. "-" .. prefix .. "step"
elseif notation == "mosdegree" then
degree_name = mossteps .. "-" .. prefix .. "degree"
elseif notation == "ordinal" then
-- Add a dash between the prefix and ordinal, if a prefix is given
if prefix == "" then
degree_name = ord._ordinal(mossteps + 1)
else
degree_name = prefix .. "-" .. ord._ordinal(mossteps + 1)
end
end
if quality == 0 then
degree_name = "Perfect " .. degree_name
elseif quality == 1 then
degree_name = "Major " .. degree_name
elseif quality == 2 then
degree_name = "Augmented " .. degree_name
elseif quality > 2 then
degree_name = (quality - 1) .. "× augmented " .. degree_name
elseif quality == -1 then
degree_name = "Minor " .. degree_name
elseif quality == -2 then
degree_name = "Diminished " .. degree_name
elseif quality < -2 then
degree_name = (math.abs(quality) - 1) .. "× diminished " .. degree_name
end
else
if notation == "mosstep" then
degree_name = mossteps .. prefix .. "s"
elseif notation == "mosdegree" then
degree_name = mossteps .. prefix .. "d"
elseif notation == "ordinal" then
-- Add a dash between the prefix and ordinal, if a prefix is given
if prefix == "" then
degree_name = ord._ordinal(mossteps + 1)
else
degree_name = prefix .. "-" .. ord._ordinal(mossteps + 1)
end
end
if quality == 0 then
degree_name = "P" .. degree_name
elseif quality == 1 then
degree_name = "M" .. degree_name
elseif quality == 2 then
degree_name = "A" .. degree_name
elseif quality > 2 then
degree_name = string.rep("A", quality - 1) .. degree_name
elseif quality == -1 then
degree_name = "m" .. degree_name
elseif quality == -2 then
degree_name = "d" .. degree_name
elseif quality < -2 then
degree_name = string.rep("d", math.abs(quality) - 1) .. degree_name
end
end
return degree_name
end
 
-- ----------------------------------------------------------------------------
-- ------------------------ HELPER FUNCTIONS ----------------------------------
-- ----------------------------------------------------------------------------
 
-- Helper function
-- Creates a genchain, or specifically, a nominal-accidental chain.
-- This can only work in one direction at a time, so it's necessary to call this twice,
-- This can only work in one direction at a time, so it's necessary to call this twice,
-- once for each direction (going up by the bright generator, or down). One genchain
-- once for each direction (going up by the bright generator, or down). One genchain
Line 95: Line 204:
-- Parameters:
-- Parameters:
-- - input_mos - the mos itself represented as a data structure from Module:MOS
-- - input_mos - the mos itself represented as a data structure from Module:MOS
-- - genchain_init_per_period - how many named pitches per period are there without accidentals added?
-- - upd_gens_per_period - This is either the value u or d for the UDP of up|dp, and is
--  This is either the value u or d for the UDP of up|dp.
--  used to calculate the number of initial notes that don't have accidentals.
-- - genchain_length_per_period - how many generators should the genchain extend after the root?
--   Note that this is per period, so it's necessary to "simplify" the UDP like a fraction.
-- - chain_length_per_period - the number of notes in the resulting chain. NOTE: if working
--   in terms of "generators stacked after the root", add 1 to that value.
-- - going_up - bool; whether the genchain is going up or down; true for up, false for down
-- - going_up - bool; whether the genchain is going up or down; true for up, false for down
function p.mos_nomacc_chain(input_mos, genchain_init_per_period, genchain_length_per_period, going_up)
function p.mos_nomacc_chain(input_mos, upd_gens_per_period, chain_length_per_period, going_up)
-- Default parameters for testing
-- Default parameters for testing
--[[
--[[
local input_mos = input_mos or mos.new(5, 2, 2)
local input_mos = input_mos or mos.new(5, 2, 2)
local genchain_init_per_period = genchain_init_per_period or 5
local upd_gens_per_period = upd_gens_per_period or 5
local genchain_length_per_period = genchain_length_per_period or 10
local chain_length_per_period = chain_length_per_period or 14
local note_symbols = note_symbols or "CDEFGAB"
local note_symbols = note_symbols or "CDEFGAB"
local chroma_symbol = chroma_symbol or "#"
local chroma_symbol = chroma_symbol or "#"
Line 112: Line 223:
-- Get the number of mossteps per period and equave
-- Get the number of mossteps per period and equave
local mossteps_per_equave = input_mos.nL + input_mos.ns
local mossteps_per_equave = input_mos.nL + input_mos.ns
local periods_per_equave = rat.gcd(input_mos.nL, input_mos.ns)
local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
local mossteps_per_period = mossteps_per_equave / periods_per_equave
local mossteps_per_period = mossteps_per_equave / periods_per_equave
Line 134: Line 245:
-- Get the size of the generator in mossteps
-- Get the size of the generator in mossteps
local gen = mos.bright_gen(input_mos)
local gen = mos.bright_gen(input_mos)
local gen_in_mossteps = gen['L'] + gen['s']
local gen_in_mossteps = gen["L"] + gen["s"]
-- If the genchain is descending (ie, going_up is false), switch to
-- If the genchain is descending (ie, going_up is false), switch to
Line 151: Line 262:
--local genchain = { root }
--local genchain = { root }
local root_offest = (i - 1) * mossteps_per_period -- To make sure that, across all periods, every note has a unique index
local root_offest = (i - 1) * mossteps_per_period -- To make sure that, across all periods, every note has a unique index
local root = { ['mossteps'] = root_offest, ['chromas'] = 0 }
local genchain = { root }
-- Create the rest of the genchain
-- Create the genchain
for j = 1, genchain_length_per_period do
local genchain = { }
-- Increment the index by the generator
for j = 1, chain_length_per_period do
accumulator = accumulator + gen_in_mossteps
 
-- Convert the accumulator into an index
-- Convert the accumulator into an index
local index = accumulator % mossteps_per_period
local index = accumulator % mossteps_per_period
Line 164: Line 272:
-- Add accidentals
-- Add accidentals
-- This is negative if the genchain is descending
-- This is negative if the genchain is descending
-- The upd_gens_per_period refers to the value u (or d for descending chains) in
-- the UDP of up|dp. The first u notes reached by stacking up that many generators from the
-- root don't have accidentals, but the number of notes that don't have accidentals
-- is actually u+1, since the root doesn't have accidentals either.
local accidentals_to_add = 0
local accidentals_to_add = 0
if j > genchain_init_per_period then
if j > upd_gens_per_period + 1 then
accidentals_to_add = math.ceil((j - genchain_init_per_period) / mossteps_per_period)
accidentals_to_add = math.ceil((j - upd_gens_per_period - 1) / mossteps_per_period)
end
end
if not going_up then
if not going_up then
Line 174: Line 286:
-- Get the final note name
-- Get the final note name
local note_name = {}
local note_name = {}
note_name['mossteps'] = index + root_offest -- Mossteps needed to reach a note
note_name["Mossteps"] = index + root_offest -- Mossteps needed to reach a note
note_name['chromas'] = accidentals_to_add -- How many chromas
note_name["Chromas"] = accidentals_to_add -- How many chromas
-- Add the note name
-- Add the note name
table.insert(genchain, note_name)
table.insert(genchain, note_name)
-- Increment the index by the generator
accumulator = accumulator + gen_in_mossteps
end
end
Line 188: Line 303:
end
end


-- Helper function that converts a scale degree given as a quantity of mossteps
-- Helper function
-- and a numeric quality (0=perf, 1=maj, -1=min, 2=aug, -2=dim, etc) into a
-- Produces a chain of scale degrees. What scale degrees are
-- scale degree
-- To be used in conjunction with the degrees function
-- TODO: add options to change naming and enumeration scheme; options include:
-- - Abbreviations (Major/Minor vs Maj/Min vs M/m)
-- - TAMNAMS indexing vs regular indexing (added)
-- - Ability to pass in a prefix (added)
function p.mosstep_and_quality_to_degree(mossteps, quality, prefix, notation)
-- Notation options currently include:
-- - mosstep, for intervals
-- - mosdegree, for scale degrees
-- - ordinal, for diatonic-like numbering; can be used for either intervals
--  or scale degrees
local prefix = prefix or "mos" -- Default prefix is mos
local notation = notation or "mosdegree" -- Default notation is mosdegree
 
local degree_name = ""
if notation == "mosstep" then
degree_name = mossteps .. "-" .. prefix .. "step"
elseif notation == "mosdegree" then
degree_name = mossteps .. "-" .. prefix .. "degree"
elseif notation == "ordinal" then
-- Add a dash between the prefix and ordinal, if a prefix is given
if prefix == "" then
degree_name = ord._ordinal(mossteps + 1)
else
degree_name = prefix .. "-" .. ord._ordinal(mossteps + 1)
end
end
 
if quality == 0 then
degree_name = "Perfect " .. degree_name
elseif quality == 1 then
degree_name = "Major " .. degree_name
elseif quality == 2 then
degree_name = "Augmented " .. degree_name
elseif quality > 2 then
degree_name = (quality - 1) .. "× augmented " .. degree_name
elseif quality == -1 then
degree_name = "Minor " .. degree_name
elseif quality == -2 then
degree_name = "Diminished " .. degree_name
elseif quality < -2 then
degree_name = (math.abs(quality) - 1) .. "× diminished " .. degree_name
end
return degree_name
end
 
-- Function that produces a chain of scale degrees. What scale degrees are
-- reached by stacking a generator?
-- reached by stacking a generator?
-- (EG, major 2nd, augmented 2nd, etc)
-- (EG, major 2nd, augmented 2nd, etc)
Line 252: Line 317:
-- - -2 = 1x diminished
-- - -2 = 1x diminished
-- - -3 = 2x diminished
-- - -3 = 2x diminished
function p.mos_degree_chain(input_mos, genchain_length_per_period, going_up)
-- The following name rules are followed, and numeric values described above are used:
-- - If an interval is the period (including the unison and equave), then the quality is perfect.
-- - For all other intervals, there are two sizes of major (large size) and minor (small size).
-- - For bright generators of non-nL-ns mosses, the sizes are perfect and diminished instead.
-- - For dark generators of non-nL-ns mosses, the sizes are augmented and perfect instead.
-- - Generators of nL ns mosses use the terms major and minor instead.
-- - Alterations denote raising a large interval by a chroma, or lowering a small interval by a chroma.
--  Since non-nL-ns mosses have augmented dark gen and diminished bright gen already, alterations
--  for those are 2x-augmented and 2x-diminished intervals; these are encoded accoringly.
-- Params are as follows:
-- - input_mos: the input mos itself
-- - chain_length_per_period: the number of degrees in the resulting chain.
--  NOTE: if working in terms of "generators stacked after the root", add 1 to that value.
-- - going_up - whether the chain is built on stacking up or down; true for up, false for down
function p.mos_degree_chain(input_mos, chain_length_per_period, going_up)
-- Default parameters for testing
-- Default parameters for testing
--[[
--[[
local input_mos = input_mos or mos.new(5, 2, 2)
local input_mos = input_mos or mos.new(5, 2, 2)
local genchain_length_per_period = genchain_length_per_period or 10
local chain_length_per_period = chain_length_per_period or 10
local going_up = false
local going_up = false
]]--
]]--
Line 262: Line 341:
-- Get the number of mossteps per period and equave
-- Get the number of mossteps per period and equave
local mossteps_per_equave = input_mos.nL + input_mos.ns
local mossteps_per_equave = input_mos.nL + input_mos.ns
local periods_per_equave = rat.gcd(input_mos.nL, input_mos.ns)
local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
local mossteps_per_period = mossteps_per_equave / periods_per_equave
local mossteps_per_period = mossteps_per_equave / periods_per_equave
-- Get the number of mossteps for the generators
-- Get the number of mossteps for the generators
local bright_gen = mos.bright_gen(input_mos)
local bright_gen = mos.bright_gen(input_mos)
local mossteps_per_bright_gen = bright_gen['L'] + bright_gen['s']
local mossteps_per_bright_gen = bright_gen["L"] + bright_gen["s"]
local mossteps_per_dark_gen = mossteps_per_period - mossteps_per_bright_gen
local mossteps_per_dark_gen = mossteps_per_period - mossteps_per_bright_gen
Line 274: Line 353:
local chain_for_period = {}
local chain_for_period = {}


for i = 1, genchain_length_per_period do
for i = 1, chain_length_per_period do
-- Calculate mossteps
-- Calculate mossteps
Line 317: Line 396:
-- Put together the name
-- Put together the name
local degree = { ['mossteps'] = mossteps, ['quality'] = quality }
local degree = { ["Mossteps"] = mossteps, ["Quality"] = quality }
table.insert(chain_for_period, degree)
table.insert(chain_for_period, degree)
end
end