Module:MOS degrees
Jump to navigation
Jump to search
Documentation for this module may be created at Module:MOS degrees/doc
local mos = require('Module:MOS')
local et = require('Module:ET')
local rat = require('Module:Rational')
local utils = require('Module:Utils')
local mosnot = require('Module:MOS notation') -- Contains the important functions
local p = {}
-- This module has been replaced with a new version called Module:MOS degrees v2
-- Helper function
-- Parses up to 5 step ratios entered as text in a semicolon-delimited string,
-- where each step ratio is separated with a slash
-- EG, "2/1; 3/1; 3/2" becomes {{2, 1}, {3, 1}, {3, 2}}
function p.parse_step_ratios(unparsed)
local parsed = {}
for entry in string.gmatch(unparsed, '([^;]+)') do
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
table.insert(parsed, trimmed) -- Add to array
end
-- Parse up to 5 step ratios (hardcoded)
local max_ratios = 5
local loop_limit = math.min(max_ratios, #parsed)
local step_ratios = {}
for i = 1, loop_limit do
local ratio = mosnot.parse_step_ratio(parsed[i])
table.insert(step_ratios, ratio)
end
-- Return nil if the size is zero (meaning nothing was entered or parsable)
if loop_limit == 0 then
return nil
else
return step_ratios
end
end
-- Helper function
-- Parses genchain extend values, where the first value is for the ascending
-- chain and the second value is for the descending chain
function p.parse_genchain_extend(unparsed)
local parsed = {}
for entry in string.gmatch(unparsed, '([^,]+)') do
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
table.insert(parsed, trimmed) -- Add to array
end
if #parsed == 1 then
return { tonumber(parsed[1]), tonumber(parsed[1]) }
else
return { tonumber(parsed[1]), tonumber(parsed[2]) }
end
end
-- Helper function; creates the column for the note name
-- Decoupled degree and note name functions allow note names being omitted.
function p.preprocess_note_names(input_mos, udp, note_symbols, sharp_symbol, flat_symbol, asc_chain_length, des_chain_length)
-- Test parameters
--[[
local input_mos = input_mos or mos.new(5, 2, 2)
local udp = udp or { 5, 1 }
local note_symbols = note_symbols or "CDEFGAB"
local sharp_symbol = sharp_symbol or "#"
local flat_symbol = flat_symbol or "b"
local asc_chain_length = input_mos.nL * 2 + input_mos.ns
local des_chain_length = input_mos.nL * 2 + input_mos.ns
]]--
-- Get the number of mossteps per period and equave
local mossteps_per_equave = 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
-- 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
-- Get the genchains
local asc_genchain = mosnot.mos_nomacc_chain(input_mos, gens_up_per_period, asc_chain_length, true)
local des_genchain = mosnot.mos_nomacc_chain(input_mos, gens_dn_per_period, des_chain_length, false)
-- Calculate the entries for each cell
local column = {}
for i = 1, periods_per_equave do
-- Add degrees from ascending chain
for j = 1, asc_chain_length do
local mossteps = asc_genchain[i][j]['mossteps']
local chromas = asc_genchain[i][j]['chromas']
-- Find the note name
local note_symbol = string.sub(note_symbols, mossteps + 1, mossteps + 1)
local note_name = mosnot.mosstep_and_chroma_to_note_name(mossteps, chromas, note_symbol, sharp_symbol)
table.insert(column, note_name)
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
-- The descending chain differs from the ascending chain:
-- - The descending chain should follow after the ascending chain.
-- - The descending chain's entries should be added backwards and skip
-- the root.
-- - This way, if the mos is multi-period, the root of the next period's
-- ascending chain (which is the same as the current period's descend-
-- ing chain) won't be added twice.
-- - If the period is the last period, add the root as the equave.
for j = des_chain_length, stop_value, -1 do
local mossteps = des_genchain[i][j]['mossteps']
local chromas = des_genchain[i][j]['chromas']
-- 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 = mosnot.mosstep_and_chroma_to_note_name(mossteps, chromas, note_symbol, flat_symbol)
table.insert(column, note_name)
end
end
return column
end
-- Helper function; creates the column for the degree name
-- Decoupled degree and note name functions allow note names being omitted.
function p.preprocess_degrees(input_mos, asc_chain_length, des_chain_length, prefix, notation)
-- Test parameters
--[[
local input_mos = input_mos or mos.new(5, 2, 2)
local asc_chain_length = input_mos.nL * 2 + input_mos.ns
local des_chain_length = input_mos.nL * 2 + input_mos.ns
local prefix = "mos"
local notation = "mosstep"
]]--
-- Get the number of mossteps per period and equave
local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
-- Get the degrees
local asc_degrees = mosnot.mos_degree_chain(input_mos, asc_chain_length, true)
local des_degrees = mosnot.mos_degree_chain(input_mos, des_chain_length, false)
-- Calculate the entries for each cell
local column = {}
for i = 1, periods_per_equave do
-- Add degrees from ascending chain
for j = 1, asc_chain_length do
local mossteps = asc_degrees[i][j]['mossteps']
local quality = asc_degrees[i][j]['quality']
-- Find the degree name
-- If the degree is the perfect 0-mosdegree, append "unison"
local degree_name = mosnot.mosstep_and_quality_to_degree(mossteps, quality, prefix, notation)
if mossteps == 0 and quality == 0 then
degree_name = degree_name .. " (unison)"
end
table.insert(column, degree_name)
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
-- The descending chain differs from the ascending chain:
-- - The descending chain should follow after the ascending chain.
-- - The descending chain's entries should be added backwards and skip
-- the root.
-- - This way, if the mos is multi-period, the root of the next period's
-- ascending chain (which is the same as the current period's descend-
-- ing chain) won't be added twice.
-- - If the period is the last period, add the root as the equave.
for j = des_chain_length, stop_value, -1 do
local mossteps = des_degrees[i][j]['mossteps']
local quality = des_degrees[i][j]['quality']
-- Find the degree name
-- If the degree corresponds to the equave, say it's the equave
local degree_name = mosnot.mosstep_and_quality_to_degree(mossteps, quality, prefix, notation)
-- If j is ever 1, then the mosdegree is the equave
-- This only happens if the current period is the last period
-- If the equave is 2/1, that's the octave
if j == 1 then
if rat.eq(input_mos.equave, 2) then
degree_name = degree_name .. " (octave)"
else
degree_name = degree_name .. " (equave)"
end
end
table.insert(column, degree_name)
end
end
return column
end
-- Helper function
-- Creates the columns for the step and cent sizes
-- Separating this into its own function makes it easy to add colums for
-- different step ratios in the same table.
function p.preprocess_steps_and_cents(input_mos, step_ratio, asc_chain_length, des_chain_length)
-- Test parameters
--[[
local input_mos = input_mos or mos.new(5, 2, 2)
local step_ratio = { 2, 1 }
local asc_chain_length = input_mos.nL * 2 + input_mos.ns
local des_chain_length = input_mos.nL * 2 + input_mos.ns
]]--
-- Get the number of mossteps per period and equave
local mossteps_per_equave = 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
-- What et is produced given the step ratio and equave?
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)
-- 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
-- How many decimal places to round to?
local round = 1
-- Calculate the entries for each row
local rows = {}
for i = 1, periods_per_equave do
-- Add step/cent values for ascending chain
for j = 1, asc_chain_length do
-- 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 = utils._round_dec(et.cents(et_for_mos, estep_count), round)
-- Add the row
local row = { estep_count, cent_value }
table.insert(rows, row)
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 step/cent values for ascending chain
-- The descending chain differs from the ascending chain:
-- - The descending chain should follow after the ascending chain.
-- - The descending chain's entries should be added backwards and skip
-- the root.
-- - This way, if the mos is multi-period, the root of the next period's
-- ascending chain (which is the same as the current period's descend-
-- ing chain) won't be added twice.
-- - If the period is the last period, add the root as the equave.
for j = des_chain_length, stop_value, -1 do
-- 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 = utils._round_dec(et.cents(et_for_mos, estep_count), round)
-- If j is ever 1, then the cent and step values are for the equave
-- This only happens if the current period is the last period
if j == 1 then
cent_value = utils._round_dec(rat.cents(input_mos.equave), round)
estep_count = steps_in_et
end
-- Add the row
local row = { estep_count, cent_value }
table.insert(rows, row)
end
end
return rows
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 = mos.parse(frame.args['Scale Signature']) or mos.new(2, 5, 2)
-- Step ratios
-- Up to three step ratios can be entered; the default is only 2/1
-- Had to use parse function to make sure the default works
local step_ratios = p.parse_step_ratios(frame.args['Step Ratio']) or p.parse_step_ratios("2/1")
-- Get the number of mossteps per period and equave
local mossteps_per_equave = 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
-- 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_default = { 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_default = { 5, 1 }
end
local udp = mosnot.parse_udp(frame.args['UDP']) or udp_default
-- Get genchain extend value
-- There are two genchain extend values for ascending and descending chains
-- The default value for both is the number of large steps per period, so
-- this value is per genchain per period.
local genchain_extend_default = input_mos.nL / periods_per_equave
local genchain_extend = p.parse_genchain_extend(frame.args['Genchain Extend'])
local genchain_extend_up = genchain_extend[1] or genchain_extend_default
local genchain_extend_dn = genchain_extend[2] or genchain_extend_default
-- Should a note names column be added?
local add_note_names = frame.args['Notation'] ~= "NONE"
-- Get notation: naturals (or nominals), sharp symbol, and flat symbol
local notation_default = { ['Naturals'] = string.sub("JKLMNOPQRSTUVWXYZ", 1, mossteps_per_equave), ['Sharp'] = "&", ['Flat'] = "@" }
if scale_sig == "5L 2s" then
notation_default['Naturals'] = "CDEFGAB"
notation_default['Sharp'] = "#"
notation_default['Flat'] = "b"
end
local notation = mosnot.parse_notation(frame.args['Notation']) or notation_default
local note_symbols = notation['Naturals']
local sharp_symbol = notation['Sharp']
local flat_symbol = notation['Flat']
-- Get notational options
local mos_prefix = "mos" -- TODO: add prefix lookup
if frame.args['MOS Prefix'] == "NONE" then
mos_prefix = ""
elseif string.len(frame.args['MOS Prefix']) > 0 then
mos_prefix = frame.args['MOS Prefix']
end
-- Override values for testing
--[[
local input_mos = mos.new(5, 2, 2)
local step_ratios = {{ 2, 1 }, {3, 1}, {3, 2}}
local udp = { 5, 1 }
local note_symbols = "CDEFGAB"
local sharp_symbol = "#"
local flat_symbol = "b"
local mossteps_per_equave = 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 scale_sig = mos.as_string(inpnut_mos)
]]--
-- Calculate the chain lengths
local asc_chain_length = udp[1] / periods_per_equave + 1 + genchain_extend_up
local des_chain_length = udp[2] / periods_per_equave + 1 + genchain_extend_dn
-- Get the degrees and note names
local degrees = p.preprocess_degrees (input_mos, asc_chain_length, des_chain_length, mos_prefix)
local note_names = p.preprocess_note_names(input_mos, udp, note_symbols, sharp_symbol, flat_symbol, asc_chain_length, des_chain_length)
-- Get the step and cent values
-- Do this for each step ratio
local steps_and_cents = {}
for i = 1, #step_ratios do
local cells = p.preprocess_steps_and_cents(input_mos, step_ratios[i], asc_chain_length, des_chain_length)
table.insert(steps_and_cents, cells)
end
-- Pre-calculate the ets and step counts for each step ratio
local steps_in_ets = {}
local ets_for_mos = {}
for i = 1, #step_ratios do
local steps_in_ets_i = input_mos.nL * step_ratios[i][1] + input_mos.ns * step_ratios[i][2]
local ets_for_mos_i = et.new(steps_in_ets_i, input_mos.equave)
table.insert(steps_in_ets, steps_in_ets_i)
table.insert(ets_for_mos, ets_for_mos_i)
end
-- Format the output as a table, starting with the header row
local result = '{| class="wikitable sortable"\n'
result = result .. '! rowspan="2" |Scale degree\n'
if add_note_names then
result = result .. '! rowspan="2" |On ' .. string.sub(note_symbols, 1, 1) .. "\n"
end
-- Add this once for every step ratio to be represented
for i = 1, #step_ratios do
-- Add names for specific step ratios
local step_ratio_names = {
['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",
}
local step_ratio_simplified = mosnot.simplify_step_ratio(step_ratios[i])
local step_ratio_string = step_ratio_simplified[1] .. ":" .. step_ratio_simplified[2]
local step_ratio_name = step_ratio_names[step_ratio_string]
-- Add column header text
if step_ratio_name == nil then
result = result .. '! colspan="2" |' .. et.as_string(ets_for_mos[i]) .. " (L:s = " .. step_ratios[i][1] .. ":" .. step_ratios[i][2] .. ")\n"
else
result = result .. '! colspan="2" |' .. step_ratio_name .. " " .. scale_sig .. "\n"
result = result .. '(' .. et.as_string(ets_for_mos[i]) .. ", L:s = " .. step_ratios[i][1] .. ":" .. step_ratios[i][2] .. ")\n"
end
end
-- Next row
result = result .. "|-\n"
-- Add this once for every step ratio to be represented
result = result .. string.rep("! Steps\n! Cents\n", #step_ratios)
-- Add each row, containing a degree, note name, step count, and cent value
for i = 1, #degrees do
-- Is the row for a nominal? (Nominals have no accidentals and are
-- therefore strings of size 1)
-- Nominals don't depend on step ratio
local is_nominal = string.len(note_names[i]) == 1
-- Add new row, with coloring
if not is_nominal then
result = result .. '|- bgcolor="#eaeaff"\n'
else
result = result .. "|-\n"
end
-- Is the row for a period?
-- Check whether it's a period only for the using the cent value for the
-- first step ratio, since it'll be a period for the other ratios
local cent_value = steps_and_cents[1][i][1] -- First set of cells, current row, first column is current cent value
local is_period = cent_value % (steps_in_ets[1] / periods_per_equave) == 0
-- Add cell for degree
-- Make any nominals that correspond to the period bold
if is_period and is_nominal then
result = result .. "| '''" .. degrees[i] .. "'''\n"
else
result = result .. "| " .. degrees[i] .. "\n"
end
-- Add cell for note name, if allowed
if add_note_names then
result = result .. "| " .. note_names[i] .. "\n"
end
-- Add cells for step size
for j = 1, #step_ratios do
result = result .. "| " .. steps_and_cents[j][i][1] .. "\n" -- Steps
result = result .. "| " .. steps_and_cents[j][i][2] .. "¢\n" -- Cents
end
end
result = result .. "|}"
return result
end
return p