local mos = require('Module:MOS')
local mosg = require('Module:MOS gamut')
local et = require('Module:ET')
local rat = require('Module:Rational')
local utils = require('Module:Utils')
--local mosnot = require('Module:MOS notation')
local p = {}
-- Helper function for parsing a step ratio entered as a string "p/q"
-- TODO: separate this into a helper module called "MOS notation"
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 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 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 ability to change naming from k-mosstep to mos-(k+1)th, since
-- there are cases where that's favored instead of tamnams
-- Names are abbreviated into 3- or 4-letter names
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
-- Helper function to simplify step ratio
-- TODO: separate this into a helper module called "MOS notation"
function p.simplify_step_ratio(step_ratio_unsimplified)
-- Get and simplify the step ratio
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
-- 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
-- TODO: part of a rewrite for the mos degrees function
function p.mos_degrees(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
-- Algorithm:
-- Use the input mos, udp, and step ratio to find the genchains
-- Using the genchains and UDP, find the mos's intervals/degrees
-- Format the result as a table
function p.mos_degrees_frame(frame)
-- Default parameters for input mos and step ratio (5L 2s and 2:1 step ratio)
local input_mos_unparsed = frame.args['Scale Signature']
local input_mos = mos.parse(input_mos_unparsed) or mos.new(2, 5, 2)
-- Step ratio
local step_ratio = { 2, 1 }
if string.len(frame.args['Step Ratio']) > 0 then
step_ratio = p.parse_step_ratio(frame.args['Step Ratio'])
end
-- 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
-- If certain params were left blank and the scalesig is 5L 2s, the default
-- params will be for standard notation
local scale_sig = mos.as_string(input_mos)
-- The default UDP corresponds to the middle mode. For mosses with an even
-- number of modes, there are two middle modes, so use the brighter of the
-- two instead.
-- If it's 5L 2s, default to the second-brightest mode.
local udp = { periods_per_equave * math.ceil((mossteps_per_period - 1)/ 2), periods_per_equave * math.floor((mossteps_per_period - 1) / 2) }
if scale_sig == "5L 2s" then
udp = { 5, 1 }
end
if string.len(frame.args['UDP']) > 0 then
udp = p.parse_udp(frame.args['UDP'])
end
local generators_up = udp[1]
local generators_down = udp[2]
-- Get note symbols
-- If this param was blank, default to diamond-mos; limited to 17 note names
-- But if it's blank and the scalesig is 5L 2s, default to standard notation
-- This order of operations allows for overriding standard notation for 5L 2s
local note_symbols_main = "JKLMNOPQRSTUVWXYZ"
local note_symbols = string.sub(note_symbols_main, 1, mossteps_per_equave)
if scale_sig == "5L 2s" then
note_symbols = "CDEFGAB"
end
-- If a value was entered, override the default value
if string.len(frame.args['Note Symbols']) > 0 then
note_symbols = frame.args['Note Symbols']
end
-- Get accidental symbols
-- If this param was blank, default to diamond-mos symbols & and @
-- unless the mos is 5L 2s, then it's sharp and flat # and b
-- This order of operations allows for overriding standard notation for 5L 2s
local chroma_plus_symbol = "&"
local chroma_minus_symbol = "@"
if scale_sig == "5L 2s" then
chroma_plus_symbol = "#"
chroma_minus_symbol = "b"
end
-- If value(s) were entered, override the default values
if string.len(frame.args['Sharp Symbol']) > 0 then
chroma_plus_symbol = frame.args['Sharp Symbol']
end
if string.len(frame.args['Flat Symbol']) > 0 then
chroma_minus_symbol = frame.args['Flat Symbol']
end
-- Override values for testing
--[[
local input_mos = mos.new(5, 2, 2)
local step_ratio = { 2, 1 }
local udp = { 5, 1 }
local note_symbols = "CDEFGAB"
local chroma_plus_symbol = "#"
local chroma_minus_symbol = "b"
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
local scale_sig = mos.as_string(input_mos)
]]--
-- How long are the initial genchain lengths? (These correspond to the UDP)
local gens_up_per_period = udp[1] / periods_per_equave
local gens_dn_per_period = udp[2] / periods_per_equave
-- How long should the genchains be?
-- The length should be such that:
-- - Every non-root interval is shown in its small, large, augmented, and
-- diminished size.
-- - The root and equave are shown in their perfect sizes, followed by their
-- augmented and diminished sizes respectively.
-- - Any non-root non-equave periods are shown in their perfect, augmented,
-- and diminished sizes.
-- To do this requires going up 2x+2y generators, and down the same amount.
-- Going up x+y gens from the root reaches every scale degree's large size,
-- plus the augmented root, then going up x+y-1 more gens reaches each
-- augmented degree. Same is true for going down to get minor/dim degrees.
local asc_chain_length = input_mos.nL * 2 + input_mos.ns
local des_chain_length = input_mos.nL * 2 + input_mos.ns
-- Get the genchains
local asc_genchain = mosg.mos_genchain(input_mos, gens_up_per_period, asc_chain_length, true)
local des_genchain = mosg.mos_genchain(input_mos, gens_dn_per_period, des_chain_length, false)
-- Get the degrees
local asc_degrees = p.mos_degrees(input_mos, asc_chain_length, true)
local des_degrees = p.mos_degrees(input_mos, des_chain_length, false)
-- Format the output as a table, starting with the header row
local result = '{| class="wikitable sortable"\n'
-- Produce the headers
local steps_in_et = input_mos.nL * step_ratio[1] + input_mos.ns * step_ratio[2]
local et_for_mos = et.new(steps_in_et, input_mos.equave)
result = result .. "! Scale degree !! Steps !! Cents !! Note name on ".. string.sub(note_symbols, 1, 1) .. "\n"
-- How many esteps per period? Per bright/dark gen?
local esteps_per_period = steps_in_et / periods_per_equave
local bright_gen = mos.bright_gen(input_mos)
local esteps_per_bright_gen = bright_gen['L'] * step_ratio[1] + bright_gen['s'] * step_ratio[2]
local esteps_per_dark_gen = esteps_per_period - esteps_per_bright_gen
-- Add the rows
local step_ratio_gcd = rat.gcd(step_ratio[1], step_ratio[2]) -- GCD of the sizes of L and s, in case L:s isn't simplified
local cents_per_equave = rat.cents(input_mos.equave) -- Equave in cents
-- For each period, add a row containing a scale degree, step count, cent
-- value, and note name from the ascending genchain, then do the same with
-- the descending genchain, in reverse and skipping the perfect root and
-- raising any other root by one period. Repeat for all other periods.
-- For the last period, add the perfect root as the perfect equave.
-- TODO: Formatting (rounding cent values, row coloring, etc)
for i = 1, periods_per_equave do
-- Add degrees from ascending chain
for j = 1, asc_chain_length do
local note = asc_genchain[i][j]
local mossteps = note['mossteps']
local chromas = note['chromas']
local quality = asc_degrees[i][j]['quality']
-- Find the note name
local note_symbol = string.sub(note_symbols, mossteps + 1, mossteps + 1)
local note_name = p.mosstep_and_chroma_to_note_name(mossteps, chromas, note_symbol, chroma_plus_symbol)
-- Find the degree name
-- If the degree is the 0-mosdegree, say it's the unison instead
local degree_name = p.mosstep_and_quality_to_degree(mossteps, quality)
if mossteps == 0 then
degree_name = degree_name:gsub("0-mosstep", "unison")
end
-- Find the estep count
local estep_count = ((j - 1) * esteps_per_bright_gen) % esteps_per_period + (i - 1) * esteps_per_period
-- Find the cent value, rounded
local cent_value = et.cents(et_for_mos, estep_count)
cent_value = utils._round_dec(cent_value, 1)
-- Add the row
result = result .. "|-\n"
result = result .. "| " .. degree_name .. "\n"
result = result .. "| " .. estep_count .. "\n"
result = result .. "| " .. cent_value .. "¢\n"
result = result .. "| " .. note_name .. "\n"
end
-- Calculate the stop value for the for loop as being 1 or 2, depending
-- on whether this is the last period or not
local stop_value = 1
if i ~= periods_per_equave then
stop_value = stop_value + 1
end
-- Add degrees from descending chain
for j = des_chain_length, stop_value, -1 do
local note = des_genchain[i][j]
local mossteps = note['mossteps']
local chromas = note['chromas']
local quality = des_degrees[i][j]['quality']
-- Find the note name
-- If the mosstep is the root of the period, add a period to it
local note_symbol = string.sub(note_symbols, mossteps + 1, mossteps + 1)
if mossteps % mossteps_per_period == 0 then
mossteps = mossteps + mossteps_per_period
end
local note_name = p.mosstep_and_chroma_to_note_name(mossteps, chromas, note_symbol, chroma_minus_symbol)
-- Find the degree name
-- If the degree corresponds to the equave, say it's the equave
local degree_name = p.mosstep_and_quality_to_degree(mossteps, quality)
-- Find the estep count
local estep_count = ((j - 1) * esteps_per_dark_gen) % esteps_per_period + (i - 1) * esteps_per_period
-- Find the cent value
local cent_value = et.cents(et_for_mos, estep_count)
cent_value = utils._round_dec(cent_value, 1)
-- If the note corresponds to the root, say it's the equave instead
if cent_value == 0 then
cent_value = cents_per_equave
estep_count = steps_in_et
note_name = string.sub(note_symbols, 1, 1) .. " (one equave up)"
degree_name = "Perfect equave"
end
-- Add the row
result = result .. "|-\n"
result = result .. "| " .. degree_name .. "\n"
result = result .. "| " .. estep_count .. "\n"
result = result .. "| " .. cent_value .. "¢\n"
result = result .. "| " .. note_name .. "\n"
end
end
result = result .. "|}"
return result
end
return p