Module:MOS degrees: Difference between revisions
Jump to navigation
Jump to search
No edit summary |
Was editing incorrect module; restored text |
||
| Line 1: | Line 1: | ||
local mos = require('Module:MOS') | local mos = require('Module:MOS') | ||
local et = require('Module:ET') | |||
local rat = require('Module:Rational') | local rat = require('Module:Rational') | ||
local utils = require('Module:Utils') | local utils = require('Module:Utils') | ||
local mosnot = require('Module:MOS notation') | local mosnot = require('Module:MOS notation') -- Contains the important functions | ||
local p = {} | local p = {} | ||
-- | -- TODO: For preprocessing, add bools for whether a note is a nominal, the | ||
-- unison, the period, or the equave, so the main function doesn't have to | |||
-- calculate them | |||
-- | |||
-- | |||
-- Helper function | -- Helper function | ||
| Line 63: | Line 14: | ||
-- where each step ratio is separated with a slash | -- where each step ratio is separated with a slash | ||
-- EG, "2/1; 3/1; 3/2" becomes {{2, 1}, {3, 1}, {3, 2}} | -- EG, "2/1; 3/1; 3/2" becomes {{2, 1}, {3, 1}, {3, 2}} | ||
function p.parse_step_ratios(unparsed) | |||
function p. | |||
local parsed = {} | local parsed = {} | ||
for entry in string.gmatch(unparsed, '([^;]+)') do | for entry in string.gmatch(unparsed, '([^;]+)') do | ||
| Line 88: | Line 38: | ||
-- Helper function | -- 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 | |||
function p. | local trimmed = entry:gsub("^%s*(.-)%s*$", "%1") | ||
local | table.insert(parsed, trimmed) -- Add to array | ||
for | |||
end | end | ||
if | if #parsed == 1 then | ||
return | return { tonumber(parsed[1]), tonumber(parsed[1]) } | ||
else | else | ||
return { tonumber(parsed[1]), tonumber(parsed[2]) } | |||
end | end | ||
end | end | ||
-- Helper function | -- 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) | |||
function p. | -- Test parameters | ||
-- | --[[ | ||
local input_mos = input_mos or mos.new(5, 2, 2) | local input_mos = input_mos or mos.new(5, 2, 2) | ||
local udp = {5,1} | local udp = udp or { 5, 1 } | ||
local note_symbols = note_symbols or "CDEFGAB" | local note_symbols = note_symbols or "CDEFGAB" | ||
local | local sharp_symbol = sharp_symbol or "#" | ||
local | local flat_symbol = flat_symbol or "b" | ||
local | 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 | -- Get the number of mossteps per period and equave | ||
| Line 154: | Line 73: | ||
local mossteps_per_period = mossteps_per_equave / periods_per_equave | local mossteps_per_period = mossteps_per_equave / periods_per_equave | ||
-- | -- How long are the initial genchain lengths? (These correspond to the UDP) | ||
local | local gens_up_per_period = udp[1] / periods_per_equave | ||
local | local gens_dn_per_period = udp[2] / periods_per_equave | ||
-- | -- Get the genchains | ||
local | 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 | ||
for | local column = {} | ||
for | for i = 1, periods_per_equave do | ||
-- Add degrees from ascending chain | |||
local | for j = 1, asc_chain_length do | ||
local mossteps = asc_genchain[i][j]['mossteps'] | |||
local | local chromas = asc_genchain[i][j]['chromas'] | ||
-- | -- Find the note name | ||
local | local note_symbol = string.sub(note_symbols, mossteps + 1, mossteps + 1) | ||
local | 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 | end | ||
-- Add degrees from descending chain | |||
-- The descending chain differs from the ascending chain: | |||
local | -- - The descending chain should follow after the ascending chain. | ||
-- - The descending chain's entries should be added backwards and skip | |||
local | -- 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 | |||
local | |||
if | |||
end | end | ||
local note_name = mosnot.mosstep_and_chroma_to_note_name(mossteps, chromas, note_symbol, flat_symbol) | |||
table.insert(column, note_name) | |||
end | end | ||
end | end | ||
return column | |||
return | |||
end | end | ||
-- Helper function; | -- Helper function; creates the column for the degree name | ||
function p. | -- 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) | ||
local input_mos = input_mos or mos.new(5, 2) | -- Test parameters | ||
local | --[[ | ||
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 | -- Get the number of mossteps per period and equave | ||
local | local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns) | ||
-- Get the | -- Get the degrees | ||
local | local asc_degrees = mosnot.mos_degree_chain(input_mos, asc_chain_length, true) | ||
local | local des_degrees = mosnot.mos_degree_chain(input_mos, des_chain_length, false) | ||
-- | -- Calculate the entries for each cell | ||
local | local column = {} | ||
for i = 1, | 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 | ||
local | |||
if | |||
end | 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. | |||
local | -- - This way, if the mos is multi-period, the root of the next period's | ||
local | -- 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'] | |||
local | -- Find the degree name | ||
table.insert( | -- 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 | ||
end | end | ||
return | return column | ||
end | end | ||
-- Helper function | -- Helper function | ||
function p. | -- Creates the columns for the step and cent sizes | ||
-- | -- Separating this into its own function makes it easy to add colums for | ||
local input_mos = input_mos or mos.new(5, 2) | -- different step ratios in the same table. | ||
local | function p.preprocess_steps_and_cents(input_mos, step_ratio, asc_chain_length, des_chain_length) | ||
local | -- 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 | -- Get the number of mossteps per period and equave | ||
local mossteps_per_equave = | local mossteps_per_equave = input_mos.nL + input_mos.ns | ||
local | 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 bright_gen = mos.bright_gen(input_mos) | ||
local | local esteps_per_bright_gen = bright_gen['L'] * step_ratio[1] + bright_gen['s'] * step_ratio[2] | ||
local | 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 | ||
local | -- 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 | 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 | end | ||
-- | -- Add the row | ||
local | local row = { estep_count, cent_value } | ||
table.insert(rows, row) | |||
end | end | ||
end | end | ||
return | return rows | ||
end | end | ||
-- | -- Algorithm: | ||
function p. | -- Use the input mos, udp, and step ratio to find the genchains | ||
-- Default | -- Using the genchains and UDP, find the mos's intervals/degrees | ||
local input_mos = | -- 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 | ||
local | -- 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 | |||
-- Get the number of mossteps per period and | |||
local mossteps_per_equave = | |||
local periods_per_equave = utils._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 | ||
-- | -- If certain params were left blank and the scalesig is 5L 2s, the default | ||
local | -- 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 | ||
local | |||
-- | -- Should a note names column be added? | ||
local | local add_note_names = frame.args['Notation'] ~= "NONE" | ||
-- | -- Get notation: naturals (or nominals), sharp symbol, and flat symbol | ||
local | 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 | end | ||
-- | -- Override values for testing | ||
local | --[[ | ||
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 | 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 | |||
if | result = result .. '! rowspan="2" |On ' .. string.sub(note_symbols, 1, 1) .. "\n" | ||
result = result .. | |||
end | end | ||
-- Add | -- Add this once for every step ratio to be represented | ||
for i = 1, #step_ratios do | for i = 1, #step_ratios do | ||
-- | -- Add names for specific step ratios | ||
local | local step_ratio_names = { | ||
['1:1'] = "Equalized", | ['1:1'] = "Equalized", | ||
['4:3'] = "Supersoft", | ['4:3'] = "Supersoft", | ||
| Line 559: | Line 424: | ||
['1:0'] = "Collapsed", | ['1:0'] = "Collapsed", | ||
} | } | ||
local step_ratio_simplified = mosnot.simplify_step_ratio(step_ratios[i]) | local step_ratio_simplified = mosnot.simplify_step_ratio(step_ratios[i]) | ||
local | local step_ratio_string = step_ratio_simplified[1] .. ":" .. step_ratio_simplified[2] | ||
local step_ratio_name = | local step_ratio_name = step_ratio_names[step_ratio_string] | ||
-- Add column header text | |||
-- Add | |||
if step_ratio_name == nil then | if step_ratio_name == nil then | ||
result = result .. '! colspan="2" |' .. | result = result .. '! colspan="2" |' .. et.as_string(ets_for_mos[i]) .. " (L:s = " .. step_ratios[i][1] .. ":" .. step_ratios[i][2] .. ")\n" | ||
else | else | ||
result = result .. '! colspan="2" |' .. | 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 | ||
end | end | ||
-- | -- Next row | ||
result = result .. | result = result .. "|-\n" | ||
-- Add this once for every step ratio to be represented | |||
result = result .. string.rep("! Steps\n! Cents\n", #step_ratios) | |||
-- Add | |||
-- Add | -- Add each row, containing a degree, note name, step count, and cent value | ||
for i = 1, #degrees do | |||
for i = 1, # | |||
-- | -- 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 | -- Add new row, with coloring | ||
if not is_nominal then | |||
result = result .. '|- bgcolor="#eaeaff"\n' | |||
result = result .. | |||
else | else | ||
result = result .. | result = result .. "|-\n" | ||
end | end | ||
-- | -- Is the row for a period? | ||
local | -- 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 | -- Add cell for degree | ||
-- | -- Make any nominals that correspond to the period bold | ||
if | if is_period and is_nominal then | ||
result = result .. | result = result .. "| '''" .. degrees[i] .. "'''\n" | ||
else | |||
result = result .. "| " .. degrees[i] .. "\n" | |||
end | end | ||
-- Add | -- Add cell for note name, if allowed | ||
if add_note_names then | |||
result = result .. "| " .. note_names[i] .. "\n" | |||
result = result .. | |||
end | end | ||
-- Add | -- 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 | |||
result = result .. "| | |||
result = result .. | |||
end | end | ||
end | end | ||
result = result .. "|}" | result = result .. "|}" | ||
return result | return result | ||
end | end | ||
return p | return p | ||
Revision as of 08:16, 15 October 2023
- This module should not be invoked directly; use its corresponding template instead: Template:MOS degrees.
| Module:MOS degrees is deprecated and has been replaced by Module:MOS tunings. Further use of this module is not advised. This module is kept for historical purposes and should not be deleted. |
| Introspection summary for Module:MOS degrees | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
| |||||||||||||||||||||||||||||||||||||||
No function descriptions were provided. The Lua code may have further information.
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 = {}
-- TODO: For preprocessing, add bools for whether a note is a nominal, the
-- unison, the period, or the equave, so the main function doesn't have to
-- calculate them
-- 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