Module:MOS degrees: Difference between revisions
m Forgot to pass notation and prefix for the descending chain's function call |
ArrowHead294 (talk | contribs) mNo edit summary |
||
| (45 intermediate revisions by 3 users not shown) | |||
| Line 1: | Line 1: | ||
local p = {} | local p = {} | ||
local et = require("Module:ET") | |||
--local jiraf = require("Module:JI ratio finder") | |||
local mos = require("Module:MOS") | |||
local mosnot = require("Module:MOS notation") | |||
local rat = require("Module:Rational") | |||
local tamnams = require("Module:TAMNAMS") | |||
local utils = require("Module:Utils") | |||
local yesno = require("Module:Yesno") | |||
-- TODO: | |||
-- Rewrite "main function" into a underscore-prefixed function to be called by Lua code and a wrapper to be called by templates. (HIGH PRIORITY!!!) | |||
-- Adopt MOS arithmetic function (MEDIUM-PRIORITY!!!) | |||
-- Add support for double accidentals (low-priority) | |||
-- Move certain helper functions to helper modules (low-priority) | |||
-- Helper function | |||
-- Parses entries from a semicolon-delimited string and returns them in an array | |||
-- TODO: Separate this and related functions (parse_pair and parse_kv_pairs) into its own module, as they're included | |||
-- in various modules at this point, such as: scale tree, MOS modes | |||
function p.parse_entries(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 | |||
return parsed | |||
end | |||
-- Helper function | |||
-- Parses pairs of elements separated by a colon | |||
-- A pair must be two elements or it will be returned as an empty array | |||
function p.parse_pair(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 == 2 then | |||
return parsed | |||
else | |||
return {} | |||
end | |||
end | |||
-- Helper function | |||
-- Takes a list of semicolon-delimited pairs and returns a map | |||
-- (or dictionary or associative array) of key-value pairs | |||
-- Each entry is colon-delimited as key : pair | |||
function p.parse_kv_pairs(unparsed) | |||
-- Tokenize the string of unparsed pairs | |||
local parsed = p.parse_entries(unparsed) | |||
-- Then tokenize the tokens into key-value pairs | |||
local pairs_ = {} | |||
for i = 1, #parsed do | |||
local pair = p.parse_pair(parsed[i]) | |||
if #pair == 2 then | |||
pairs_[pair[1]] = pair[2] | |||
end | |||
end | |||
return pairs_ | |||
end | |||
-- Helper function | -- Helper function | ||
| Line 10: | Line 67: | ||
-- 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. | -- NOTE: module relies on mosnot (mos notation) to parse step ratios | ||
function p.parse_step_ratio(unparsed) | |||
local parsed = {} | local parsed = {} | ||
for entry in string.gmatch(unparsed, | 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 25: | Line 83: | ||
table.insert(step_ratios, ratio) | table.insert(step_ratios, ratio) | ||
end | end | ||
return step_ratios | -- 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 | end | ||
-- Helper function | -- Helper function | ||
-- | -- Takes in a step pattern and a quantity of mossteps and calculates the number | ||
function p. | -- of large and small steps in that interval (or substring), returned as an | ||
-- associative array containing the large and small step counts. | |||
-- It's an associative array b/c that's how the brightgen function in the mos | |||
local | -- module works. | ||
function p.mosstep_pattern_to_vector(mosstep_pattern, mossteps) | |||
local large_step_count = 0 | |||
local | local small_step_count = 0 | ||
for i = 1, mossteps do | |||
local | local step = string.sub(mosstep_pattern, i, i) | ||
if step == "L" then | |||
large_step_count = large_step_count + 1 | |||
elseif step == "s" then | |||
small_step_count = small_step_count + 1 | |||
end | |||
end | |||
local mosstep_vector = { ["L"] = large_step_count, ["s"] = small_step_count } | |||
return mosstep_vector | |||
end | |||
-- Helper function | |||
-- Takes in a mosstep (as an assoc. array containing the number of L's and s's), | |||
-- and a step ratio (as 2-element array containing the sizes of L and s) and | |||
-- calculates number of et-steps. | |||
function p.interval_to_etsteps(mosstep_vector, step_ratios) | |||
return mosstep_vector["L"] * step_ratios[1] + mosstep_vector["s"] * step_ratios[2] | |||
end | |||
-- Helper function | |||
-- For producing row highlighting for the table | |||
-- Alterations are highlighted, except for singy augmented/diminished intervals for generators | |||
function p.calculate_row_colors(input_mos, number_of_alterations) | |||
-- Default parameters for input mos and step ratio (5L 2s and 2:1 step ratio) | |||
local input_mos = input_mos or mos.new(4, 4, 2) | |||
local number_of_alterations = number_of_alterations or 1 | |||
-- 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 = | 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 | ||
local row_colors = {} | |||
for i = 1, mossteps_per_equave + 1 do | |||
local mosstep = i - 1 | |||
local is_period = mosstep % mossteps_per_period == 0 | |||
local is_root = mosstep == 0 | |||
local | local is_equave = mosstep == mossteps_per_equave | ||
-- Row colors for pre-alterations | |||
-- If this is the root, don't add rows before it | |||
if not is_root then | |||
for i = 1, number_of_alterations do | |||
table.insert(row_colors, "#eaeaff") | |||
for i = 1, | |||
- | |||
end | end | ||
end | end | ||
-- | -- Row colors for main mossetps (default row color) | ||
if is_period then | |||
table.insert(row_colors, "none") | |||
else | |||
table.insert(row_colors, "none") | |||
table.insert(row_colors, "none") | |||
end | end | ||
-- | -- Row colors for post-alterations | ||
-- If this is the equave, don't add rows after it | |||
-- | if not is_equave then | ||
for i = 1, number_of_alterations do | |||
table.insert(row_colors, "#eaeaff") | |||
end | end | ||
end | end | ||
end | end | ||
return | return row_colors | ||
end | end | ||
-- Helper function | -- Helper function | ||
-- | -- Calculates note names and stores it in an associative array | ||
-- | -- Default notation is diamond-mos, unless it's 5L 2s, then it's standard notation | ||
function p.calculate_note_names(input_mos, udp, note_symbols, chroma_plus_symbol, chroma_minus_symbol, number_of_alterations) | |||
function p. | -- Default parameters for input mos and step ratio (5L 2s and 2:1 step ratio) | ||
-- | local input_mos = input_mos or mos.new(5, 2) | ||
local udp = udp or {5,2} | |||
local input_mos = input_mos or mos.new(5 | local note_symbols = note_symbols or "CDEFGAB" | ||
local | local chroma_plus_symbol = chroma_plus_symbol or "#" | ||
local | local chroma_minus_symbol = chroma_minus_symbol or "b" | ||
local | local number_of_alterations = number_of_alterations or 0 | ||
-- 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 = | 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 generators going up and down from the UDP | ||
local | local generators_up = udp[1] | ||
local | local generators_down = udp[2] | ||
-- How long is the inital genchain for notes without accidentals? | |||
local gens_up_per_period = generators_up / periods_per_equave | |||
local gens_down_per_period = generators_down / periods_per_equave | |||
-- How long should the genchain extend after the initial genchain? | |||
-- The initial genchain lengths are determined by the U and D in the UDP | |||
-- The final genchain length is the following: (x + y) * (alterations + 1) | |||
local ascending_genchain_length = (mossteps_per_period) * (number_of_alterations + 1) | |||
local descending_genchain_length = (mossteps_per_period) * (number_of_alterations + 1) | |||
-- | -- Get the ascending and descending genchains | ||
local | -- The genchains are notationally agnostic so notation needs to be applied to them | ||
local | local ascending_genchain = mosnot.mos_nomacc_chain(input_mos, gens_up_per_period, ascending_genchain_length, true) | ||
local | local descending_genchain = mosnot.mos_nomacc_chain(input_mos, gens_down_per_period, descending_genchain_length, false) | ||
local | |||
-- Also get the ascending and descending degreechains | |||
-- These chains are encoded in a numeric form and must be converted into actual names | |||
local ascending_degchain = mosnot.mos_degree_chain(input_mos, ascending_genchain_length, true) | |||
local descending_degchain = mosnot.mos_degree_chain(input_mos, descending_genchain_length, false) | |||
-- | -- Create an empty asoociative array | ||
local | local note_names = {} | ||
-- | -- Add the notes to the array | ||
for j = 1, periods_per_equave do | |||
for i = 1, #ascending_genchain[j] do | |||
-- Convert the notationally agnostic form into a form that uses given notation | |||
local note = ascending_genchain[j][i] | |||
local note_symbol = string.sub(note_symbols, note["Mossteps"] + 1, note["Mossteps"] + 1) | |||
local chroma_count = note["Chromas"] | |||
local note_name = note_symbol .. string.rep(chroma_plus_symbol, chroma_count) | |||
-- | -- Convert the encoded degree into text | ||
local | local degree_encoded = ascending_degchain[j][i] | ||
local degree_decoded = mosnot.decode_mosstep_quality(degree_encoded, "m", "mosdegree", "abbreviated") | |||
-- Add to note names | |||
note_names[degree_decoded] = note_name | |||
-- Add | |||
end | end | ||
for i = 1, #descending_genchain[j] do | |||
-- Convert the notationally agnostic form into a form that uses given notation | |||
local note = descending_genchain[j][i] | |||
local note_symbol = string.sub(note_symbols, note["Mossteps"] + 1, note["Mossteps"] + 1) | |||
local chroma_count = note["Chromas"] * -1 | |||
local note_name = note_symbol .. string.rep(chroma_minus_symbol, chroma_count) | |||
-- | -- Convert the encoded degree into text | ||
local | local degree_encoded = descending_degchain[j][i] | ||
-- | -- For the descending chain, any mossteps that correspond to the root of | ||
-- a period should correspond instead to the root one period up (EG, if | |||
-- the root refers to the unison for a single-period mos, it should be | |||
-- the degree one octave up) | |||
if degree_encoded["Mossteps"] % mossteps_per_period == 0 then | |||
-- Transpose the mosstep by one period | |||
degree_encoded["Mossteps"] = degree_encoded["Mossteps"] + mossteps_per_period | |||
end | |||
-- | -- Correct the note name based on whether it should be a note that is | ||
-- | -- one period up. If the mos is single-period, then do not transpose. | ||
if | if degree_encoded["Mossteps"] % mossteps_per_period == 0 and degree_encoded["Mossteps"] == 0 then | ||
-- Correct the note name | |||
note_symbol = string.sub(note_symbols, 1, 1) | |||
note_name = note_symbol .. string.rep(chroma_minus_symbol, chroma_count) | |||
elseif degree_encoded["Mossteps"] % mossteps_per_period == 0 and degree_encoded["Mossteps"] == 0 then | |||
-- Correct the note name | |||
note_symbol = string.sub(note_symbols, degree_encoded["Mossteps"] + 1, degree_encoded["Mossteps"] + 1) | |||
note_name = note_symbol .. string.rep(chroma_minus_symbol, chroma_count) | |||
end | end | ||
-- | -- Pass the encoded degree, along with the other args | ||
local | local degree_decoded = mosnot.decode_mosstep_quality(degree_encoded, "m", "mosdegree", "abbreviated") | ||
-- Add to note names | |||
note_names[degree_decoded] = note_name | |||
end | end | ||
end | end | ||
return | return note_names | ||
end | end | ||
-- | -- Helper function; generate the step vectors for every interval required for the table | ||
function p.calculate_mosstep_vectors(input_mos, number_of_alterations) | |||
-- Default params | |||
local input_mos = input_mos or mos.new(5, 2) | |||
function p. | local number_of_alterations = number_of_alterations or 0 | ||
-- Default | |||
local input_mos = | |||
-- | -- Get the brightest mode | ||
local brightest_mode = mos.brightest_mode(input_mos) | |||
local | |||
-- 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 | local mossteps_per_period = mossteps_per_equave / utils._gcd(input_mos.nL, input_mos.ns) | ||
-- | -- Add intervals and their alterations, using the large interval size as the zero point for alterations | ||
local mosstep_vectors = {} | |||
for i = 1, mossteps_per_equave + 1 do | |||
local mossteps = i - 1 | |||
-- Consecutive alterations are always one chroma apart | |||
-- With a perfect non-generator interval, alterations are added by going down and up the same amount | |||
-- With all other intervals, since there are two sizes and the large interval size is treated as the zero point, | |||
-- alterations are instead by going down n+1 chromas, then going up n chromas | |||
-- With the unison, don't go down (only up), and with the equave, don't go up (only down). | |||
local min_alterations = 0 | |||
local max_alterations = 0 | |||
if mossteps == 0 then | |||
-- Unison; the min number of alterations is 0 | |||
min_alterations = 0 | |||
max_alterations = number_of_alterations | |||
elseif mossteps == mossteps_per_equave then | |||
-- Equave; the max number of alterations is 0 | |||
min_alterations = -number_of_alterations | |||
max_alterations = 0 | |||
elseif mossteps % mossteps_per_period == 0 then | |||
-- Non-unison non-equave periods; the max and min have the "distance" from the zero point | |||
min_alterations = -number_of_alterations | |||
max_alterations = number_of_alterations | |||
else | |||
-- All other intervals; the min's distance is one more than the max's distance | |||
min_alterations = -number_of_alterations - 1 | |||
max_alterations = number_of_alterations | |||
end | |||
-- Get the current mosstep vector based on the brightest mode | |||
local current_mosstep_vector = p.mosstep_pattern_to_vector(brightest_mode, mossteps) | |||
for j = min_alterations, max_alterations do | |||
-- j is the number of chromas to add or subtract from the base vector | |||
-- Since a chroma is defined as (L-s), add j large steps and subtract j small steps from the current mosstep vector | |||
local L_count = current_mosstep_vector["L"] + j | |||
local s_count = current_mosstep_vector["s"] - j | |||
local current_mosstep_vector = { ["L"] = L_count, ["s"] = s_count } | |||
table.insert(mosstep_vectors, current_mosstep_vector) | |||
end | |||
end | end | ||
-- | return mosstep_vectors | ||
-- | end | ||
-- | |||
-- | -- Helper function; generate the mosdegree names and their abbreviations for the mos | ||
local | function p.calculate_mosdegree_names_and_abbrevs(input_mos, mos_prefix, number_of_alterations) | ||
-- Default params | |||
local input_mos = input_mos or mos.new(5, 2) | |||
local number_of_alterations = number_of_alterations or 0 | |||
local mos_prefix = mos_prefix or "mos" | |||
-- Get the number of mossteps per period and equave | |||
local mossteps_per_equave = (input_mos.nL + input_mos.ns) | |||
local mossteps_per_period = mossteps_per_equave / utils._gcd(input_mos.nL, input_mos.ns) | |||
-- Get the step counts for the bright and dark 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 | |||
-- Main loop | |||
-- Interval qualities depend on whether the intervals are generators or if there is only one size. | |||
-- Cases for which there are alterations either below or above the main interval sizes, but not both: | |||
-- - If the interval class is the unison, there are no extensions before it and there is only one size (perfect). | |||
-- - If the interval class is the equave, there are no extensions after it and there is only one size (perfect). | |||
-- Cases for which there are alterations above and below the main interval sizes: | |||
-- - If the interval class is a non-unison non-equave period, there are extensions before and after and there is only one size (perfect). | |||
-- - If the interval class is a non-generator interval, or is the generator for an nL ns mos, there are extensions before and after and the sizes are major and minor. | |||
-- - If the interval class is the bright generator, there are extensions before and after and the sizes are perfect (large) and diminished (small). | |||
-- - If the interval class is the dark generator, there are extensions before and after and the sizes are augmented (large) and perfect (small). | |||
local mosdegree_names = {} | |||
local mosdegree_abbrevs = {} | |||
for i = 1, mossteps_per_equave + 1 do | |||
-- For calculating mossteps | |||
local mossteps = i - 1 | |||
-- For bright and dark gens | |||
local is_nL_ns = input_mos.nL == input_mos.ns | |||
local is_bright_gen = mossteps % mossteps_per_period == mossteps_per_bright_gen and not is_nL_ns | |||
local is_dark_gen = mossteps % mossteps_per_period == mossteps_per_dark_gen and not is_nL_ns | |||
if mossteps % mossteps_per_period == 0 then | |||
-- For perfect intervals | |||
-- Operation for pre-alterations (diminshed degrees) | |||
if number_of_alterations > 0 and mossteps ~= 0 then | |||
for j = number_of_alterations, 1, -1 do | |||
-- Diminished degree is formatted as "Diminished degree"; more than 1 augmentation is "2× Diminished", "3× Diminished", and so on | |||
local dim_degree = "" | |||
if j == 1 then dim_degree = string.format("Diminished %d-%sdegree", mossteps, mos_prefix) | |||
else dim_degree = string.format("%d× Diminished %d-%sdegree", j, mossteps, mos_prefix) | |||
end | |||
-- Format abbreviation as repetitions of the letter "d", followed by the mosdegree | |||
local dim_abbrev = string.rep("d", j) .. string.format("%dmd", mossteps) | |||
-- Insert | |||
table.insert(mosdegree_names, dim_degree) | |||
table.insert(mosdegree_abbrevs, dim_abbrev) | |||
end | |||
end | |||
-- Calculate the main degree name and abbreviation | |||
local degree_name = string.format("Perfect %d-%sdegree", mossteps, mos_prefix) | |||
local abbrev_name = string.format("P%dmd", mossteps) | |||
-- Main operation | |||
table.insert(mosdegree_names, degree_name) | |||
table.insert(mosdegree_abbrevs, abbrev_name) | |||
-- Operation for post-alterations (augmented degrees) | |||
if number_of_alterations > 0 and mossteps ~= mossteps_per_equave then | |||
for j = 1, number_of_alterations do | |||
-- Augmented degree is formatted as "Augmented degree"; more than 1 augmentation is "2× Augmented", "3× Augmented", and so on | |||
local aug_degree = "" | |||
if j == 1 then aug_degree = string.format("Augmented %d-%sdegree", mossteps, mos_prefix) | |||
else aug_degree = string.format("%d× Augmented %d-%sdegree", j, mossteps, mos_prefix) | |||
end | |||
-- Format abbreviation as repetitions of the letter "A", followed by the mosdegree | |||
local aug_abbrev = string.rep("A", j) .. string.format("%dmd", mossteps) | |||
-- Insert | |||
table.insert(mosdegree_names, aug_degree) | |||
table.insert(mosdegree_abbrevs, aug_abbrev) | |||
end | |||
end | |||
else | |||
-- For intervals with two sizes | |||
-- Operation for pre-alterations (diminshed degrees) | |||
if number_of_alterations > 0 and mossteps ~= 0 then | |||
for j = number_of_alterations, 1, -1 do | |||
-- The number of diminishings depends on whether the interval class is the bright gen; if so, | |||
-- then one interval will already be diminished so intervals below that already start at 2xdim. | |||
local dim_amount = 0 | |||
if is_bright_gen then dim_amount = 1 + j | |||
else dim_amount = j | |||
end | |||
-- Diminished degree is formatted as "Diminished degree"; more than 1 augmentation is "2× Diminished", "3× Diminished", and so on | |||
local dim_degree = "" | |||
if dim_amount == 1 then dim_degree = string.format("Diminished %d-%sdegree", mossteps, mos_prefix) | |||
else dim_degree = string.format("%d× Diminished %d-%sdegree", dim_amount, mossteps, mos_prefix) | |||
end | |||
-- Format abbreviation as repetitions of the letter "d", followed by the mosdegree | |||
local dim_abbrev = string.rep("d", dim_amount) .. string.format("%dmd", mossteps) | |||
-- Insert | |||
table.insert(mosdegree_names, dim_degree) | |||
table.insert(mosdegree_abbrevs, dim_abbrev) | |||
end | |||
end | |||
-- Calculate the small and large names and abbreviations | |||
-- Non-generator intervals for non-nL-ns mosses are minor (small) and major (large) | |||
local small_degree_label = "Minor" | |||
local large_degree_label = "Major" | |||
local small_degree_abbrev = "m" | |||
local large_degree_abbrev = "M" | |||
if is_bright_gen then | |||
-- Bright gen: diminished (small) and perfect (large) | |||
small_degree_label = "Diminished" | |||
large_degree_label = "Perfect" | |||
small_degree_abbrev = "d" | |||
large_degree_abbrev = "P" | |||
elseif is_dark_gen then | |||
-- Dark gen: perfect (small) and augmentd (large) | |||
small_degree_label = "Perfect" | |||
large_degree_label = "Augmented" | |||
small_degree_abbrev = "P" | |||
large_degree_abbrev = "A" | |||
end | |||
-- Main operation | |||
local small_degree_name = string.format("%s %d-%sdegree", small_degree_label, mossteps, mos_prefix) | |||
local large_degree_name = string.format("%s %d-%sdegree", large_degree_label, mossteps, mos_prefix) | |||
local small_abbrev_name = string.format("%s%dmd", small_degree_abbrev, mossteps) | |||
local large_abbrev_name = string.format("%s%dmd", large_degree_abbrev, mossteps) | |||
table.insert(mosdegree_names, small_degree_name) | |||
table.insert(mosdegree_names, large_degree_name) | |||
table.insert(mosdegree_abbrevs, small_abbrev_name) | |||
table.insert(mosdegree_abbrevs, large_abbrev_name) | |||
-- Operation for post-alterations (augmented degrees) | |||
if number_of_alterations > 0 and mossteps ~= mossteps_per_equave then | |||
for j = 1, number_of_alterations do | |||
-- The number of augmentings depends on whether the interval class is the dark gen; if so, | |||
-- then one interval will already be augmented so intervals above that already start at 2xaug. | |||
local aug_amount = 0 | |||
if is_dark_gen then aug_amount = 1 + j | |||
else aug_amount = j | |||
end | |||
-- Augmented degree is formatted as "Augmented degree"; more than 1 augmentation is "2× Augmented", "3× Augmented", and so on | |||
local aug_degree = "" | |||
if aug_amount == 1 then aug_degree = string.format("Augmented %d-%sdegree", mossteps, mos_prefix) | |||
else aug_degree = string.format("%d× Augmented %d-%sdegree", aug_amount, mossteps, mos_prefix) | |||
end | |||
-- Format abbreviation as repetitions of the letter "A", followed by the mosdegree | |||
local aug_abbrev = string.rep("A", aug_amount) .. string.format("%dmd", mossteps) | |||
-- Insert | |||
table.insert(mosdegree_names, aug_degree) | |||
table.insert(mosdegree_abbrevs, aug_abbrev) | |||
end | |||
end | |||
end | |||
end | end | ||
return mosdegree_names, mosdegree_abbrevs | |||
end | |||
-- | |||
local | -- Separate function for testing; the main "frame" function will call this | ||
local | function p.mos_degrees(input_mos, step_ratios, mos_prefix, show_abbreviations, number_of_alterations, ji_ratios, udp, notation, show_notation) | ||
-- Default params; all parameters are already parsed | |||
local input_mos = input_mos or mos.new(5, 2) | |||
local step_ratios = step_ratios or {{2,1}, {3,1}, {3,2}} | |||
local mos_prefix = mos_prefix or "mos" | |||
local show_abbrevs = show_abbreviations == 1 | |||
local number_of_alterations = number_of_alterations or 1 | |||
local ji_ratios = ji_ratios or {["P0md"]="1/1"} | |||
local udp = udp or {5,1} | |||
local notation = notation or mosnot.parse_notation("CDEFGAB; #; b") | |||
local show_notation = show_notation == 1 | |||
-- Get | -- Get the scale sig | ||
local scale_sig = mos.as_string(input_mos) | |||
local | |||
-- Get | -- Get the brightest and darkest modes for the mos | ||
local brightest_mode = mos.brightest_mode(input_mos) | |||
local darkest_mode = string.reverse(brightest_mode) | |||
local | |||
-- | -- Get the number of mossteps per period and equave, and periods per 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_equave = input_mos.nL + input_mos.ns | |||
local periods_per_equave = | |||
local mossteps_per_period = mossteps_per_equave / periods_per_equave | local mossteps_per_period = mossteps_per_equave / periods_per_equave | ||
local | |||
]]-- | -- Get the step counts for the bright and dark generators | ||
local bright_gen = mos.bright_gen(input_mos) | |||
local steps_per_bright_gen = bright_gen["L"] + bright_gen["s"] | |||
local steps_per_dark_gen = mossteps_per_period - steps_per_bright_gen | |||
-- Get the step counts as a vector (or associative array, rather) | |||
local input_mos_step_vector = {["L"] = input_mos.nL, ["s"] = input_mos.ns} | |||
-- What's the equave in cents? | |||
local equave_in_cents = rat.cents(input_mos.equave) | |||
-- | -- How many decimal places to round to? (hardcoded) | ||
local | local round = 1 | ||
local | |||
-- Precalculate row colors | |||
local row_colors = p.calculate_row_colors(input_mos, number_of_alterations) | |||
-- | -- Precalculate the ets for each step ratio | ||
local | -- Each et is used to calculate a scale degree's cent value | ||
local ets_for_mos = {} | |||
for i = 1, #step_ratios do | |||
local etsteps = p.interval_to_etsteps(input_mos_step_vector, step_ratios[i]) | |||
local et_for_mos = et.new(etsteps, input_mos.equave) | |||
table.insert(ets_for_mos, et_for_mos) | |||
end | |||
-- Precalculate degree names, degree abbreviations, and mosstep vectors | |||
local degree_names, degree_abbrevs = p.calculate_mosdegree_names_and_abbrevs(input_mos, mos_prefix, number_of_alterations) | |||
local mosstep_vectors = p.calculate_mosstep_vectors(input_mos, number_of_alterations) | |||
-- Precalculate default comments for JI ratios; there's only two entries here | |||
local default_ji_comments = {} | |||
default_ji_comments["P0md"] = "1/1 (exact)" | |||
default_ji_comments[string.format("P%dmd", mossteps_per_equave)] = string.format("%s (exact)", rat.as_ratio(input_mos.equave)) | |||
-- | -- Then, using the UDP, get the notation | ||
-- | -- The default notation is either standard notation (for 5L 2s) or diamond-mos (most other mosses) | ||
local | -- If notation is passed in, use that instead | ||
-- If no notation is passed in, notation will not be displayed | |||
local note_names = {} | |||
local root_note = "" | |||
if show_notation then | |||
note_names = p.calculate_note_names(input_mos, udp, notation["Naturals"], notation["Sharp"], notation["Flat"], number_of_alterations) | |||
root_note = string.sub(notation["Naturals"], 1, 1) | |||
end | end | ||
-- | -- Create the table, starting with the headers | ||
local result = | local result = "{| class=\"wikitable sortable mw-collapsible mw-collapsed\"\n" | ||
-- First row | |||
result = result | |||
.. "|+ style=\"font-size: 105%%; white-space: nowrap;\" | " .. string.format("Scale degree of %s\n", scale_sig) | |||
.. "|-\n" | |||
.. "! rowspan=\"2\" class=\"unsortable\" | Scale degree\n" | |||
-- Add column for abbreviations | |||
-- Abbreviations do not use a mos-prefix or mos-name | |||
if show_abbrevs then | |||
result = result .. "! rowspan=\"2\" class=\"unsortable\" | Abbrev.\n" | |||
end | |||
-- | -- Add column for note names | ||
if show_notation then | |||
result = result .. string.format("! rowspan=\"2\" class=\"unsortable\" | On %s\n", root_note) | |||
end | end | ||
-- Add | -- Add column headers for up to 5 different step ratios | ||
for i = 1, #step_ratios do | for i = 1, #step_ratios do | ||
result = result .. | -- Step ratio names, for reference | ||
result = result .. "( | local tamnams_step_ratios = { | ||
["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", | |||
} | |||
-- Get name for step ratio | |||
local step_ratio_simplified = mosnot.simplify_step_ratio(step_ratios[i]) | |||
local step_ratio_key = step_ratio_simplified[1] .. ":" .. step_ratio_simplified[2] | |||
local step_ratio_name = tamnams_step_ratios[step_ratio_key] | |||
-- Calculate the et for the mos with a given step ratio; this is needed to produce | |||
-- the name for the et/edo | |||
local et_for_mos = et.new(ets_for_mos[i].size, input_mos.equave) | |||
local et_as_string = et.as_string(et_for_mos) | |||
-- Add the step ratio name if there is one | |||
if step_ratio_name == nil then | |||
result = result .. "! colspan=\"2\" | " .. et_as_string .. " (L:s = " .. step_ratio_key .. ")\n" | |||
else | |||
result = result .. "! colspan=\"2\" | " .. et_as_string .. " (" .. step_ratio_name .. ", L:s = " .. step_ratio_key .. ")\n" | |||
end | |||
end | end | ||
-- | -- Add JI ratio column header | ||
result = result .. "! rowspan=\"2\" class=\"unsortable\" | Approx. JI Ratios\n" | |||
-- Second row | |||
result = result .. "|-\n" | result = result .. "|-\n" | ||
-- Add headers for the steps and cents up to 5 times | |||
for i = 1, #step_ratios do | |||
result = result .. "! Steps\n" | |||
result = result .. "! Cents\n" | |||
end | |||
-- Add | -- Add in successive rows, containing the degree name, abbreviation (if applicable), | ||
result = result .. string. | -- note names (if applicable), step size (in steps and cents), and JI ratio | ||
for i = 1, #degree_names do | |||
-- Start new row | |||
-- Add row highlighting if provided | |||
local row_color = row_colors[i] | |||
if row_color == "" then | |||
result = result .. "|-\n" | |||
else | |||
result = result .. string.format("|- style=\"background: %s\"\n", row_color) | |||
end | |||
-- | -- Add degree name | ||
-- | -- Make the text bold if the interval is a perfect interval | ||
local degree_name = degree_names[i] | |||
if string.find(degree_name, "Perfect") then | |||
if i == 1 then | |||
result = result .. string.format("| '''%s (unison)'''\n", degree_names[i]) | |||
elseif i == #degree_names and equave_in_cents == 1200 then | |||
result = result .. string.format("| '''%s (octave)'''\n", degree_names[i]) | |||
elseif i == #degree_names and equave_in_cents ~= 1200 then | |||
result = result .. string.format("| '''%s (equave)'''\n", degree_names[i]) | |||
else | |||
result = result .. string.format("| '''%s'''\n", degree_names[i]) | |||
end | |||
else | else | ||
result = result .. "| | result = result .. string.format("| %s\n", degree_names[i]) | ||
end | end | ||
-- | -- Add abbrev if allowed | ||
local degree_abbrev = degree_abbrevs[i] | |||
if show_abbrevs then | |||
local | result = result .. string.format("| %s\n", degree_abbrev) | ||
end | |||
-- Add | -- Add note names if allowed | ||
-- | -- Use the degree_abbrev as the key when accessing key-value pairs | ||
if | if show_notation then | ||
result = result .. "| | result = result .. string.format("| %s\n", note_names[degree_abbrev]) | ||
end | end | ||
-- Add | -- Add mossteps and cent values | ||
for j = 1, #step_ratios | -- Rounding is hardcoded to one decimal place | ||
result = result .. "| " .. | -- Also record the cent value for JI ratio search | ||
result = result .. "| " .. | local round = 1 | ||
local average_cents = 0 | |||
for j = 1, #ets_for_mos do | |||
local etsteps = mosstep_vectors[i]["L"] * step_ratios[j][1] + mosstep_vectors[i]["s"] * step_ratios[j][2] | |||
local cents = utils._round_dec(et.cents(ets_for_mos[j], etsteps), round) | |||
result = result .. string.format("| %s\n", etsteps) | |||
result = result .. string.format("| %s\n", cents) | |||
average_cents = average_cents + cents / #ets_for_mos | |||
end | |||
-- Calculate JI ratio approximations using jiraf module | |||
-- For now: | |||
-- - Cent value is the average of the sizes given the step ratios | |||
-- - Tolerance is hardcoded to +/-15 cents | |||
-- - Prime limit is hardocoded to 19 | |||
-- - Odd limit hardcoded to 49 | |||
--local approx_ratios = jiraf.find_ratios_for_cents(average_cents, 15, 19, 39) | |||
--local ratios_as_text = jiraf.ratios_to_text(approx_ratios); | |||
-- Add JI ratios if any | |||
local ji_comment_entry = "" | |||
local default_ji_comment = default_ji_comments[degree_abbrev] | |||
-- Add ratios found using jiraf | |||
local entered_ji_comment = ji_ratios[degree_abbrev] | |||
--local default_ji_comment = nil | |||
--local entered_ji_comment = ratios_as_text | |||
if default_ji_comment == nil and entered_ji_comment == nil then | |||
-- No comments | |||
result = result .. "|\n" | |||
elseif default_ji_comment ~= nil and entered_ji_comment == nil then | |||
-- Default comments but no entered comments | |||
result = result .. string.format("| %s\n", default_ji_comment) | |||
elseif default_ji_comment == nil and entered_ji_comment ~= nil then | |||
-- Entered comments but no default comments | |||
result = result .. string.format("| %s\n", entered_ji_comment) | |||
else | |||
-- Both comments present; default comments take precedence | |||
result = result .. string.format("| %s, %s\n", default_ji_comment, entered_ji_comment) | |||
end | end | ||
end | end | ||
-- End of table | |||
result = result .. "|}" | result = result .. "|}" | ||
return result | return result | ||
end | end | ||
-- This function is to be called by a template, with parameters | |||
function p.mos_degrees_frame(frame) | |||
-- Default param for input mos is 5L 2s | |||
local input_mos = mos.parse(frame.args["Scale Signature"]) or mos.new(2, 5, 2) | |||
-- Get the scale sig; for calculating the mos prefix | |||
local scale_sig = mos.as_string(input_mos) | |||
-- Get the step ratio | |||
local step_ratios = p.parse_step_ratio(frame.args["Step Ratio"]) or p.parse_step_ratio("2/1") | |||
-- Default param for mos prefix | |||
-- If "NONE" is given, no prefix will be used | |||
-- If left blank, try to find the appropriate mos prefix, or else defualt to "mos" | |||
-- If not left blank, use the prefix passed in instead | |||
local mos_prefix = "mos" | |||
if frame.args["MOS Prefix"] == "NONE" then | |||
mos_prefix = "" | |||
elseif string.len(frame.args["MOS Prefix"]) == 0 then | |||
mos_prefix_lookup = tamnams.lookup_prefix(input_mos) or "" | |||
if string.len(mos_prefix_lookup) ~= 0 then | |||
mos_prefix = mos_prefix_lookup | |||
end | |||
else | |||
mos_prefix = frame.args["MOS Prefix"] | |||
end | |||
-- Get whether to display abbreviations | |||
local show_abbreviations = 0 | |||
if frame.args["Show Abbreviations"] == "1" or frame.args["Show Abbreviations"] == 1 then | |||
show_abbreviations = 1 | |||
end | |||
-- Get the number of alterations | |||
local number_of_alterations = 0 | |||
if string.len(frame.args["Number of Alterations"]) ~= 0 then | |||
number_of_alterations = tonumber(frame.args["Number of Alterations"]) | |||
end | |||
-- Get JI ratios | |||
local ji_ratios_parsed = {} | |||
if #frame.args["JI Ratios"] > 0 then | |||
-- If the comments can't be parsed, default to an empty table | |||
ji_ratios_parsed = p.parse_kv_pairs(frame.args["JI Ratios"]) or {} | |||
end | |||
-- Get the number of mossteps per period and equave, and periods per equave | |||
-- Needed for calculating default UDP and notation | |||
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 | |||
-- Get UDP | |||
-- If no UDP is found, a default will be calculated as the middle mode, or the | |||
-- brighter of two middle modes (as with an even number of modes in a mos) | |||
local udp_parsed = { 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_parsed = { 5, 1 } | |||
end | |||
if #frame.args["UDP"] > 0 then | |||
udp_parsed = mosnot.parse_udp(frame.args["UDP"]) | |||
end | |||
-- Get notation | |||
-- This also determines whether to show notation | |||
-- Typing in "Default" is a shortcut to default notation, wherein standard notation (for 5L 2s) or diamond-mos (for other mosses) is used | |||
local notation_parsed = {} | |||
local show_notation = 0 | |||
if #frame.args["Notation"] > 0 then | |||
if frame.args["Notation"] == "Default" and scale_sig == "5L 2s" then | |||
notation_parsed = { ["Naturals"] = "CDEFGAB", ["Sharp"] = "#", ["Flat"] = "b" } | |||
show_notation = 1 | |||
elseif frame.args["Notation"] == "Default" and scale_sig ~= "5L 2s" then | |||
local default_nominals = "JKLMNOPQRSTUVWXYZ" | |||
notation_parsed = { ["Naturals"] = string.sub(default_nominals, 1, mossteps_per_equave), ["Sharp"] = "&", ["Flat"] = "@" } | |||
show_notation = 1 | |||
else | |||
notation_parsed = mosnot.parse_notation(frame.args["Notation"]) | |||
if notation_parsed ~= nil then | |||
show_notation = 1 | |||
end | |||
end | |||
end | |||
result = p.mos_degrees(input_mos, step_ratios, mos_prefix, show_abbreviations, number_of_alterations, ji_ratios_parsed, udp_parsed, notation_parsed, show_notation) | |||
-- Debugger | |||
local debugg = yesno(frame.args["debug"]) | |||
if debugg == true then | |||
result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>" | |||
end | |||
return frame:preprocess(result) | |||
end | |||
return p | return p | ||