Module:MOS notation: Difference between revisions
Jump to navigation
Jump to search
Separated functions from mos degrees module into a helper module |
No edit summary |
||
| Line 53: | Line 53: | ||
local note_name = note_symbol .. string.rep(chroma_symbol, math.abs(chromas)) | local note_name = note_symbol .. string.rep(chroma_symbol, math.abs(chromas)) | ||
return note_name | return note_name | ||
end | |||
-- Helper function for creating 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, | |||
-- once for each direction (going up by the bright generator, or down). One genchain | |||
-- is generated for each period, so this returns an array of arrays. | |||
-- This genchain is agnostic of notation, and only denotes the mossteps needed to reach | |||
-- a note, followed by the number of chromas. For example, F# is reached going up 3 | |||
-- mossteps and adding one chroma; Fb is the same except subtracting one chroma. | |||
-- Specific notation is needed to interpret this into note names. | |||
-- Parameters: | |||
-- - 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? | |||
-- This is either the value u or d for the UDP of up|dp. | |||
-- - genchain_length_per_period - how many generators should the genchain extend after the root? | |||
-- - 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) | |||
-- Default parameters for testing | |||
--[[ | |||
local input_mos = input_mos or mos.new(5, 2, 2) | |||
local genchain_init_per_period = genchain_init_per_period or 5 | |||
local genchain_length_per_period = genchain_length_per_period or 10 | |||
local note_symbols = note_symbols or "CDEFGAB" | |||
local chroma_symbol = chroma_symbol or "#" | |||
local going_up = going_up or true | |||
]]-- | |||
-- Get the number of mossteps per period and equave | |||
local mossteps_per_equave = input_mos.nL + input_mos.ns | |||
local periods_per_equave = rat.gcd(input_mos.nL, input_mos.ns) | |||
local mossteps_per_period = mossteps_per_equave / periods_per_equave | |||
--[[ | |||
-- Split the note symbols string into subsets | |||
-- This is only necessary if the mos is multi-period | |||
local note_subsets = {} | |||
for i = 1, periods_per_equave do | |||
local start_index = (i - 1) * mossteps_per_period + 1 | |||
local stop_index = i * mossteps_per_period | |||
local substr = string.sub(note_symbols, start_index, stop_index) | |||
table.insert(note_subsets, substr) | |||
end | |||
]]-- | |||
-- Create the genchain for each period | |||
local genchains = {} | |||
for i = 1, periods_per_equave do | |||
--local note_names = note_subsets[i] | |||
-- Get the size of the generator in mossteps | |||
local gen = mos.bright_gen(input_mos) | |||
local gen_in_mossteps = gen['L'] + gen['s'] | |||
-- If the genchain is descending (ie, going_up is false), switch to | |||
-- using the dark gen in mossteps, which is the period complement | |||
-- of the bright gen; going up by the dark gen is the same as going | |||
-- down by the bright gen | |||
if not going_up then | |||
gen_in_mossteps = mossteps_per_period - gen_in_mossteps | |||
end | |||
-- Use this value, with modular arithmteic, as an index to get the note name | |||
local accumulator = 0 | |||
-- Create a genchain that initially starts at the root | |||
--local root = string.sub(note_names, 1, 1) | |||
--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 = { ['mossteps'] = root_offest, ['chromas'] = 0 } | |||
local genchain = { root } | |||
-- Create the rest of the genchain | |||
for j = 1, genchain_length_per_period do | |||
-- Increment the index by the generator | |||
accumulator = accumulator + gen_in_mossteps | |||
-- Convert the accumulator into an index | |||
local index = accumulator % mossteps_per_period | |||
-- Add accidentals | |||
-- This is negative if the genchain is descending | |||
local accidentals_to_add = 0 | |||
if j > genchain_init_per_period then | |||
accidentals_to_add = math.ceil((j - genchain_init_per_period) / mossteps_per_period) | |||
end | |||
if not going_up then | |||
accidentals_to_add = accidentals_to_add * -1 | |||
end | |||
-- Get the final note name | |||
local note_name = {} | |||
note_name['mossteps'] = index + root_offest -- Mossteps needed to reach a note | |||
note_name['chromas'] = accidentals_to_add -- How many chromas | |||
-- Add the note name | |||
table.insert(genchain, note_name) | |||
end | |||
-- Add the genchain | |||
table.insert(genchains, genchain) | |||
end | |||
return genchains | |||
end | end | ||
| Line 98: | Line 201: | ||
-- - -2 = 1x diminished | -- - -2 = 1x diminished | ||
-- - -3 = 2x diminished | -- - -3 = 2x diminished | ||
function p. | function p.mos_degree_chain(input_mos, genchain_length_per_period, going_up) | ||
-- Default parameters for testing | -- Default parameters for testing | ||
--[[ | --[[ | ||
Revision as of 04:37, 23 June 2023
- This module primarily serves as a library for other modules and has no corresponding template.
| Introspection summary for Module:MOS notation | |||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
| ||||||||||||||||||||||||||||||||||||
No function descriptions were provided. The Lua code may have further information.
-- This is a helper module that contains commonly used functions for:
-- - MOS degrees
-- - MOS gamut
local mos = require('Module:MOS')
local mosg = require('Module:MOS gamut') -- TODO: move gamut function to here
local rat = require('Module:Rational')
local p = {}
-- Helper function for parsing a step ratio entered as a string "p/q"
function p.parse_step_ratio(step_ratio_unparsed)
local parsed = {}
for entry in string.gmatch(step_ratio_unparsed, '([^/]+)') do
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
table.insert(parsed, trimmed) -- Add to array
end
local ratio = { tonumber(parsed[1]), tonumber(parsed[2]) }
return ratio
end
-- Helper function to simplify step ratio
function p.simplify_step_ratio(step_ratio_unsimplified)
local kp = step_ratio_unsimplified[1]
local kq = step_ratio_unsimplified[2]
local k = rat.gcd(kp, kq)
local num = kp / k
local den = kq / k
return { num, den }
end
-- Helper function for parsing a UDP entered as a string "up,dp"
-- To avoid potential issues, the "," character is used instead of "|"
function p.parse_udp(step_ratio_unparsed)
local parsed = {}
for entry in string.gmatch(step_ratio_unparsed, '([^,]+)') do
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
table.insert(parsed, trimmed) -- Add to array
end
local udp = { tonumber(parsed[1]), tonumber(parsed[2]) }
return udp
end
-- Helper function that converts a note name given as a quantity of mossteps
-- and chromas (see gamut function) into a name, such as "C#"
-- To be used in conjunction with the genchain function
function p.mosstep_and_chroma_to_note_name(mossteps, chromas, note_symbol, chroma_symbol)
local note_name = note_symbol .. string.rep(chroma_symbol, math.abs(chromas))
return note_name
end
-- Helper function for creating 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,
-- once for each direction (going up by the bright generator, or down). One genchain
-- is generated for each period, so this returns an array of arrays.
-- This genchain is agnostic of notation, and only denotes the mossteps needed to reach
-- a note, followed by the number of chromas. For example, F# is reached going up 3
-- mossteps and adding one chroma; Fb is the same except subtracting one chroma.
-- Specific notation is needed to interpret this into note names.
-- Parameters:
-- - 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?
-- This is either the value u or d for the UDP of up|dp.
-- - genchain_length_per_period - how many generators should the genchain extend after the root?
-- - 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)
-- Default parameters for testing
--[[
local input_mos = input_mos or mos.new(5, 2, 2)
local genchain_init_per_period = genchain_init_per_period or 5
local genchain_length_per_period = genchain_length_per_period or 10
local note_symbols = note_symbols or "CDEFGAB"
local chroma_symbol = chroma_symbol or "#"
local going_up = going_up or true
]]--
-- Get the number of mossteps per period and equave
local mossteps_per_equave = input_mos.nL + input_mos.ns
local periods_per_equave = rat.gcd(input_mos.nL, input_mos.ns)
local mossteps_per_period = mossteps_per_equave / periods_per_equave
--[[
-- Split the note symbols string into subsets
-- This is only necessary if the mos is multi-period
local note_subsets = {}
for i = 1, periods_per_equave do
local start_index = (i - 1) * mossteps_per_period + 1
local stop_index = i * mossteps_per_period
local substr = string.sub(note_symbols, start_index, stop_index)
table.insert(note_subsets, substr)
end
]]--
-- Create the genchain for each period
local genchains = {}
for i = 1, periods_per_equave do
--local note_names = note_subsets[i]
-- Get the size of the generator in mossteps
local gen = mos.bright_gen(input_mos)
local gen_in_mossteps = gen['L'] + gen['s']
-- If the genchain is descending (ie, going_up is false), switch to
-- using the dark gen in mossteps, which is the period complement
-- of the bright gen; going up by the dark gen is the same as going
-- down by the bright gen
if not going_up then
gen_in_mossteps = mossteps_per_period - gen_in_mossteps
end
-- Use this value, with modular arithmteic, as an index to get the note name
local accumulator = 0
-- Create a genchain that initially starts at the root
--local root = string.sub(note_names, 1, 1)
--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 = { ['mossteps'] = root_offest, ['chromas'] = 0 }
local genchain = { root }
-- Create the rest of the genchain
for j = 1, genchain_length_per_period do
-- Increment the index by the generator
accumulator = accumulator + gen_in_mossteps
-- Convert the accumulator into an index
local index = accumulator % mossteps_per_period
-- Add accidentals
-- This is negative if the genchain is descending
local accidentals_to_add = 0
if j > genchain_init_per_period then
accidentals_to_add = math.ceil((j - genchain_init_per_period) / mossteps_per_period)
end
if not going_up then
accidentals_to_add = accidentals_to_add * -1
end
-- Get the final note name
local note_name = {}
note_name['mossteps'] = index + root_offest -- Mossteps needed to reach a note
note_name['chromas'] = accidentals_to_add -- How many chromas
-- Add the note name
table.insert(genchain, note_name)
end
-- Add the genchain
table.insert(genchains, genchain)
end
return genchains
end
-- Helper function that converts 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
-- 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 (0-mossteps vs mos-1st)
-- - Ability to pass in a prefix (default is "mos")
function p.mosstep_and_quality_to_degree(mossteps, quality)
local degree_name = mossteps .. "-mosstep"
if quality == 0 then
degree_name = "Perf. " .. degree_name
elseif quality == 1 then
degree_name = "Maj. " .. degree_name
elseif quality == 2 then
degree_name = "Aug. " .. degree_name
elseif quality > 2 then
degree_name = (quality - 1) .. "× aug. " .. degree_name
elseif quality == -1 then
degree_name = "Min. " .. degree_name
elseif quality == -2 then
degree_name = "Dim. " .. degree_name
elseif quality < -2 then
degree_name = (math.abs(quality) - 1) .. "× dim. " .. degree_name
end
return degree_name
end
-- Function that produces a chain of scale degrees. What scale degrees are
-- reached by stacking a generator?
-- (EG, major 2nd, augmented 2nd, etc)
-- This function only works one direction at a time, so it's necessary to call
-- it twice, one for each direction.
-- Quality encodes maj/min/aug/perf/dim numerically:
-- - 3 = 2x augmented
-- - 2 = 1x augmented
-- - 1 = major
-- - 0 = perfect (used for generators and root)
-- - -1 = minor
-- - -2 = 1x diminished
-- - -3 = 2x diminished
function p.mos_degree_chain(input_mos, genchain_length_per_period, going_up)
-- Default parameters for testing
--[[
local input_mos = input_mos or mos.new(5, 2, 2)
local genchain_length_per_period = genchain_length_per_period or 10
local going_up = false
]]--
-- Get the number of mossteps per period and equave
local mossteps_per_equave = input_mos.nL + input_mos.ns
local periods_per_equave = rat.gcd(input_mos.nL, input_mos.ns)
local mossteps_per_period = mossteps_per_equave / periods_per_equave
-- Get the number of mossteps for the generators
local bright_gen = mos.bright_gen(input_mos)
local mossteps_per_bright_gen = bright_gen['L'] + bright_gen['s']
local mossteps_per_dark_gen = mossteps_per_period - mossteps_per_bright_gen
local degreechain = {}
for j = 1, periods_per_equave do
local chain_for_period = {}
for i = 1, genchain_length_per_period do
-- Calculate mossteps
local mossteps = 0
if going_up then
mossteps = (i - 1) * mossteps_per_bright_gen % mossteps_per_period + (j - 1) * mossteps_per_period
else
mossteps = (i - 1) * mossteps_per_dark_gen % mossteps_per_period + (j - 1) * mossteps_per_period
end
-- Calculate quality
-- The first two elements in the chain are always perfect
-- All intervals after that are major (or minor if going down)
-- After the major intervals are augmented intervals, which starts
-- with the augmented dark generator, which comes before the
-- augmented unison. (or minor and dim bright gen if going down)
-- For nL ns mosses, generators are major and minor instead, so only
-- the root is perfect
local quality = 0
if input_mos.nL ~= input_mos.ns then
if i == 1 or i == 2 then
quality = 0
else
-- Offsetting i by +1 will make it so the dark generator
-- before the augmented unison is denoted as augmented,
-- but lua's start-from-1 indexing offsets it by 1 already.
quality = math.floor(i / mossteps_per_period) + 1
if not going_up then
quality = quality * -1
end
end
else
if i == 1 then
quality = 0
else
quality = math.floor((i + 1) / mossteps_per_period)
if not going_up then
quality = quality * -1
end
end
end
-- Put together the name
local degree = { ['mossteps'] = mossteps, ['quality'] = quality }
table.insert(chain_for_period, degree)
end
table.insert(degreechain, chain_for_period)
end
return degreechain
end
return p