Module:MOS degrees: Difference between revisions
yet more gcd fixes |
Added support for notation; notation is optional now |
||
| Line 1: | Line 1: | ||
local mos = require('Module:MOS') | local mos = require('Module:MOS') | ||
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') | ||
local et = require('Module:ET') | |||
local p = {} | local p = {} | ||
-- TODO: | -- Version 2 of the mos degrees moudle (by ganaraminukshuk) | ||
-- | -- This is based more off of the mos intervals module and older, non-tempalte tables that feature JI ratios, | ||
-- | -- and doesn't depend on genchains (except for note names) to calculate cent values or degree names. | ||
-- Current TODO list: | |||
-- - Optional note names column | |||
-- 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 mdoes | |||
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 14: | Line 63: | ||
-- 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, '([^;]+)') do | for entry in string.gmatch(unparsed, '([^;]+)') do | ||
| Line 38: | Line 88: | ||
-- Helper function | -- Helper function | ||
-- | -- Takes in a step pattern and a quantity of mossteps and calculates the number | ||
-- | -- of large and small steps in that interval (or substring), returned as an | ||
function p. | -- 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 | |||
-- module works. | |||
function p.mosstep_pattern_to_vector(mosstep_pattern, mossteps) | |||
local large_step_count = 0 | |||
local small_step_count = 0 | |||
for i = 1, mossteps do | |||
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 | |||
-- Extracts the prefix from the mos module, without the dash and without any text after that | |||
-- May need to revisit to clean up code since this splits text at the "-". | |||
function p.get_mos_prefix(scale_sig) | |||
local unparsed = mos.tamnams_prefix[scale_sig] | |||
local parsed = {} | local parsed = {} | ||
if | if unparsed == nil then | ||
return | return "mos" | ||
else | else | ||
for entry in string.gmatch(unparsed, '([^-]+)') do | |||
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1") | |||
table.insert(parsed, trimmed) -- Add to array | |||
end | |||
return parsed[1] | |||
end | end | ||
end | end | ||
-- Helper function | -- Helper function | ||
-- | -- Calculates note names and stores it in an associative array | ||
function p. | -- Default notation is diamond-mos, unless it's 5L 2s, then it's standard notation | ||
-- | function p.get_note_names(input_mos, udp, note_symbols, chroma_plus_symbol, chroma_minus_symbol, 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(5, 2, 2) | local input_mos = input_mos or mos.new(5, 2, 2) | ||
local udp = | local udp = {5,1} or udp | ||
local note_symbols = note_symbols or "CDEFGAB" | 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 1 | ||
-- Get the number of mossteps per period and equave | -- Get the number of mossteps per period and equave | ||
| Line 73: | Line 154: | ||
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 | |||
-- The genchains are notationally agnostic so notation needs to be applied to them | |||
local ascending_genchain = mosnot.mos_nomacc_chain(input_mos, gens_up_per_period, ascending_genchain_length, true) | |||
local descending_genchain = mosnot.mos_nomacc_chain(input_mos, gens_down_per_period, descending_genchain_length, false) | |||
-- 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 | for i = 1, #ascending_genchain[j] do | ||
-- Convert the notationally agnostic form into a form that uses given notation | |||
for | local note = ascending_genchain[j][i] | ||
local | local note_symbol = string.sub(note_symbols, note['mossteps'] + 1, note['mossteps'] + 1) | ||
local | 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 | local degree_decoded = mosnot.mosstep_and_quality_to_degree(degree_encoded['mossteps'], degree_encoded['quality'], "m", "mosdegree", "abbreviated") | ||
-- Add to note names | |||
note_names[degree_decoded] = note_name | |||
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 degree_encoded = descending_degchain[j][i] | |||
local | |||
-- | -- 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 | ||
local | -- the root refers to the unison, it should be the degree one octave up) | ||
if | local mossteps_transposed = degree_encoded['mossteps'] | ||
if mossteps_transposed % mossteps_per_period == 0 then | |||
mossteps_transposed = mossteps_transposed + mossteps_per_period | |||
end | end | ||
local degree_decoded = mosnot.mosstep_and_quality_to_degree(mossteps_transposed, degree_encoded['quality'], "m", "mosdegree", "abbreviated") | |||
-- Add to note names | |||
note_names[degree_decoded] = note_name | |||
end | end | ||
end | end | ||
return | -- Last note in the gamut is the root up one equave | ||
--local degree_for_equave = string.format("P%dmd", mossteps_per_equave) | |||
--local degree_for_root = string.format("P0md", mossteps_per_equave) | |||
--note_names[degree_for_equave] = note_names[degree_for_root] | |||
return note_names | |||
end | end | ||
-- Helper function; | -- Helper function; generate the step vectors for every interval required for the table | ||
function p.get_mosstep_vectors(input_mos, number_of_alterations) | |||
function p. | -- Default params | ||
-- | local input_mos = input_mos or mos.new(5, 2) | ||
local number_of_alterations = number_of_alterations or 0 | |||
local input_mos = input_mos or mos.new(5 | |||
local | -- 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 | 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) | |||
-- | -- Add intervals and their alterations, using the large interval size as the zero point for alterations | ||
local | 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 | end | ||
-- | -- Get the current mosstep vector based on the brightest mode | ||
local current_mosstep_vector = p.mosstep_pattern_to_vector(brightest_mode, mossteps) | |||
local | 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 | |||
local | |||
local current_mosstep_vector = { ['L'] = L_count, ['s'] = s_count } | |||
table.insert(mosstep_vectors, current_mosstep_vector) | |||
table.insert( | |||
end | end | ||
end | end | ||
return | return mosstep_vectors | ||
end | end | ||
-- Helper function | -- Helper function; generate the mosdegree names and their abbreviations for the mos | ||
function p.get_mosdegree_names_and_abbrevs(input_mos, mos_prefix, 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 | ||
-- | local mos_prefix = mos_prefix or "mos" | ||
local input_mos = input_mos or mos.new(5 | |||
local | |||
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) | ||
-- | -- Get the step counts for the bright and dark generators | ||
local bright_gen = mos.bright_gen(input_mos) | local bright_gen = mos.bright_gen(input_mos) | ||
local | local mossteps_per_bright_gen = bright_gen['L'] + bright_gen['s'] | ||
local | 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). | ||
local | -- - If the interval class is the equave, there are no extensions after it and there is only one size (perfect). | ||
for i = 1, | -- 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-%sstep", mossteps, mos_prefix) | |||
else dim_degree = string.format("%d× Diminished %d-%sstep", 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 | local degree_name = string.format("Perfect %d-%sstep", 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) | ||
local | 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-%sstep", mossteps, mos_prefix) | |||
else aug_degree = string.format("%d× Augmented %d-%sstep", 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-%sstep", mossteps, mos_prefix) | |||
else dim_degree = string.format("%d× Diminished %d-%sstep", 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 | ||
local | 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 | local small_degree_name = string.format("%s %d-%sstep", small_degree_label, mossteps, mos_prefix) | ||
local large_degree_name = string.format("%s %d-%sstep", 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-%sstep", mossteps, mos_prefix) | |||
else aug_degree = string.format("%d× Augmented %d-%sstep", 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 | ||
end | end | ||
return | return mosdegree_names, mosdegree_abbrevs | ||
end | end | ||
-- | -- Separate function for testing; the main "frame" function will call this | ||
function p.mos_degrees(input_mos, step_ratios, mos_prefix, show_abbreviations, number_of_alterations, ji_ratios, udp, notation) | |||
-- Default params | |||
local input_mos = input_mos or mos.new(5, 2) | |||
function p. | local step_ratios = step_ratios or {{2,1}, {3,1}, {3,2}} | ||
-- Default | local mos_prefix = mos_prefix or "mos" | ||
local | 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} -- This is calculated later in the code | |||
local notation = "Default" | |||
-- Get the scale sig | |||
local scale_sig = mos.as_string(input_mos) | |||
-- | -- 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 | -- Get the number of mossteps per period and equave, and periods per 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 = 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 | ||
-- | -- Get the step counts for the bright and dark generators | ||
-- | local bright_gen = mos.bright_gen(input_mos) | ||
local | 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 round = 1 | |||
-- Precalculate the ets for each step ratio | |||
-- 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.get_mosdegree_names_and_abbrevs(input_mos, mos_prefix, number_of_alterations) | |||
local mosstep_vectors = p.get_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)) | |||
-- Precalculate notation in two steps, starting with the UDP | |||
-- The default UDP corresponds to the middle mode. For mosses with an even | -- 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 | -- number of modes, there are two middle modes, so use the brighter of the | ||
| Line 328: | Line 524: | ||
udp_default = { 5, 1 } | udp_default = { 5, 1 } | ||
end | end | ||
local udp = | local udp = udp or udp_default | ||
-- | -- Then, using the UDP, get the notation | ||
-- | -- The default notation is either standard notation (for 5L 2s) or diamond-mos (most other mosses) | ||
-- | -- If notation is passed in, use that instead | ||
-- | -- If no notation is passed in, notation will not be displayed | ||
local | local note_names = {} | ||
local | local show_note_names = false | ||
local | local root_note = "" | ||
if notation == "Default" then | |||
if scale_sig == "5L 2s" then | |||
note_names = p.get_note_names(input_mos, udp, "CDEFGAB", "#", "b", number_of_alterations) | |||
root_note = "C" | |||
show_note_names = true | |||
else | |||
local default_nominals = string.sub("JKLMNOPQRSTUVWXYZ", 1, mossteps_per_equave) | |||
note_names = p.get_note_names(input_mos, udp, default_nominals, "&", "@", number_of_alterations) | |||
root_note = "J" | |||
show_note_names = true | |||
end | |||
elseif string.len(notation) > 0 then | |||
local notation_parsed = mosnot.parse_notation(notation) | |||
note_names = p.get_note_names(input_mos, udp, notation_parsed['Naturals'], notation_parsed["Sharp"], notation_parsed["Flat"], number_of_alterations) | |||
root_note = string.sub(notation_parsed['Naturals'], 1, 1) | |||
show_note_names = true | |||
end | end | ||
-- | -- Create the table, starting with the headers | ||
local | local result = '{| class="wikitable sortable"\n' | ||
-- | -- First row | ||
result = result .. string.format("|+ Scale degree of %s\n", scale_sig) | |||
result = result .. '! 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 | end | ||
if | -- Add column for note names | ||
result = result .. '! rowspan="2" |On ' | if show_note_names 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 | ||
-- | -- Step ratio names, for reference | ||
local | local tamnams_step_ratios = { | ||
['1:1'] = "Equalized", | ['1:1'] = "Equalized", | ||
['4:3'] = "Supersoft", | ['4:3'] = "Supersoft", | ||
| Line 424: | Line 583: | ||
['1:0'] = "Collapsed", | ['1:0'] = "Collapsed", | ||
} | } | ||
-- Get name for step ratio | |||
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_key = step_ratio_simplified[1] .. ":" .. step_ratio_simplified[2] | ||
local step_ratio_name = | local step_ratio_name = tamnams_step_ratios[step_ratio_key] | ||
-- Add | -- 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 | if step_ratio_name == nil then | ||
result = result .. '! colspan="2" |' .. | result = result .. '! colspan="2" |' .. et_as_string .. " (L:s = " .. step_ratio_key .. ")\n" | ||
else | else | ||
result = result .. '! colspan="2" |' .. | result = result .. '! colspan="2" |' .. et_as_string .. " (" .. step_ratio_name .. ", L:s = " .. step_ratio_key .. ")\n" | ||
end | 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), | ||
-- note names (if applicable), step size (in steps and cents), and JI ratio | |||
for i = 1, #degree_names do | |||
-- | -- Start new row | ||
for i = 1, # | result = result .. "|-\n" | ||
-- | -- Add degree name | ||
-- | -- Make the text bold if the interval is a perfect interval | ||
local degree_name = degree_names[i] | |||
local | if string.find(degree_name, "Perfect") then | ||
result = result .. string.format("| '''%s'''\n", degree_names[i]) | |||
result = result .. ' | |||
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_note_names then | ||
result = result .. "| | result = result .. string.format("| %s\n", note_names[degree_abbrev]) | ||
end | end | ||
-- Add | -- Add mossteps and cent values | ||
-- Rounding is hardcoded to one decimal place | |||
result = result .. "| " .. | local round = 1 | ||
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) | |||
end | end | ||
-- Add | -- Add JI ratios if any | ||
local ji_comment_entry = "" | |||
result = result .. "| " .. | local default_ji_comment = default_ji_comments[degree_abbrev] | ||
result = result .. "| " .. | local entered_ji_comment = ji_ratios[degree_abbrev] | ||
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 = p.get_mos_prefix(scale_sig) | |||
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 | |||
result = p.mos_degrees(input_mos, step_ratios, mos_prefix, show_abbreviations, number_of_alterations, ji_ratios_parsed) | |||
return result | |||
end | |||
return p | return p | ||