Module:MOS degrees: Difference between revisions
Jump to navigation
Jump to search
m Commented out test values |
Added naming rules for nL ns mosses to newly added function; "unison" is now used instead of "0-mosstep" |
||
| Line 56: | Line 56: | ||
-- Default parameters for testing | -- Default parameters for testing | ||
--[[ | --[[ | ||
local input_mos = input_mos or mos.new( | local input_mos = input_mos or mos.new(5, 5, 2) | ||
local genchain_length_per_period = genchain_length_per_period or 10 | local genchain_length_per_period = genchain_length_per_period or 10 | ||
local going_up = | local going_up = false | ||
]]-- | ]]-- | ||
| Line 65: | Line 65: | ||
local periods_per_equave = rat.gcd(input_mos.nL, input_mos.ns) | local periods_per_equave = rat.gcd(input_mos.nL, input_mos.ns) | ||
local mossteps_per_period = mossteps_per_equave / periods_per_equave | local mossteps_per_period = mossteps_per_equave / periods_per_equave | ||
-- Get the number of mossteps for the generators | |||
local bright_gen = mos.bright_gen(input_mos) | |||
local mossteps_per_bright_gen = bright_gen['L'] + bright_gen['s'] | |||
-- The easiest way to get scale degrees is through the following method: | -- The easiest way to get scale degrees is through the following method: | ||
| Line 76: | Line 80: | ||
-- For each period, the first two elements represent the root (perfect) | -- For each period, the first two elements represent the root (perfect) | ||
-- and dark generator (small size is perfect). | -- and dark generator (small size is perfect). | ||
-- - Since the root and generators start as perfect, | -- - Since the root and generators start as perfect, any chromas added to | ||
-- | -- that represent augmented/diminished degrees. | ||
-- Note that this is agnostic of notation, so only the genchain length is | -- Note that this is agnostic of notation, so only the genchain length is | ||
-- needed. The UDP for the second-brightest mode is auto-calculated and is | -- needed. The UDP for the second-brightest mode is auto-calculated and is | ||
| Line 83: | Line 87: | ||
local genchain = mosg.mos_genchain(input_mos, mossteps_per_period - 2, genchain_length_per_period, true) | local genchain = mosg.mos_genchain(input_mos, mossteps_per_period - 2, genchain_length_per_period, true) | ||
-- Interpret as scale degrees | -- Interpret as scale degrees. | ||
-- The first two elements are always perfect. | -- The first two elements are always perfect. | ||
-- 0 chromas | -- 0 chromas is major (or minor if descending). | ||
-- 1 chroma is augmented | -- 1 chroma is augmented (diminished if descending). | ||
-- n chromas is n-times augmented, | -- n chromas is n-times augmented (n-times diminished if descending). | ||
-- Notes and caveats: | |||
-- - The 0-mosdegree is also called the unison. | |||
-- - Generators for nL ns mosses aren't called perf/aug/dim, but instead | |||
-- called major and minor. This requires offsetting the note's chroma | |||
-- value by -1, or else the singly-aug/dim generator will be skipped. | |||
local degreechain = {} | local degreechain = {} | ||
for j = 1, periods_per_equave do | for j = 1, periods_per_equave do | ||
| Line 96: | Line 105: | ||
local mossteps = note['mossteps'] | local mossteps = note['mossteps'] | ||
local chromas = note['chromas'] | local chromas = note['chromas'] | ||
-- Mosses of the form nL ns have slightly different rules. Is the | |||
-- input mos of that form? | |||
local is_nL_ns = input_mos.nL == input_mos.ns | |||
if is_nL_ns and mossteps == mossteps_per_bright_gen + (j-1) * mossteps_per_period then | |||
chromas = chromas - 1 | |||
end | |||
-- If not going up, use the period complements instead | -- If not going up, use the period complements instead | ||
| Line 109: | Line 125: | ||
end | end | ||
-- What is the quality of the note? (major, minor, etc) | |||
local quality = "" | local quality = "" | ||
if i == 1 then | if i == 1 then | ||
quality = "perfect" | quality = "perfect" | ||
elseif i == 2 then | elseif i == 2 then | ||
quality = "perfect" | if is_nL_ns then | ||
quality = maj_or_min | |||
else | |||
quality = "perfect" | |||
end | |||
else | else | ||
if chromas == 0 then | if chromas == 0 then | ||
| Line 124: | Line 145: | ||
end | end | ||
local degree = quality .. " " .. mossteps .. "-mosstep" | -- Put together the name | ||
-- TODO?: Prefix lookup | |||
-- If it's the 0-mosstep, simply say it's the unison instead. | |||
local degree = "" | |||
if mossteps == 0 then | |||
degree = quality .. " unison" | |||
else | |||
degree = quality .. " " .. mossteps .. "-mosstep" | |||
end | |||
table.insert(chain_for_period, degree) | table.insert(chain_for_period, degree) | ||
Revision as of 05:15, 21 June 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 mosg = require('Module:MOS gamut')
local et = require('Module:ET')
local rat = require('Module:Rational')
local utils = require('Module:Utils')
--local mosnot = require('Module:MOS notation')
local p = {}
-- Helper function for parsing a step ratio entered as a string "p/q"
-- TODO: separate this into a helper module called "MOS notation"
function p.parse_step_ratio(step_ratio_unparsed)
local parsed = {}
for entry in string.gmatch(step_ratio_unparsed, '([^/]+)') do
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
table.insert(parsed, trimmed) -- Add to array
end
local ratio = { tonumber(parsed[1]), tonumber(parsed[2]) }
return ratio
end
-- Helper function for parsing a UDP entered as a string "up,dp"
-- To avoid potential issues, the "," character is used instead of "|"
function p.parse_udp(step_ratio_unparsed)
local parsed = {}
for entry in string.gmatch(step_ratio_unparsed, '([^,]+)') do
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
table.insert(parsed, trimmed) -- Add to array
end
local udp = { tonumber(parsed[1]), tonumber(parsed[2]) }
return udp
end
-- Helper function to simplify step ratio
-- TODO: separate this into a helper module called "MOS notation"
function p.simplify_step_ratio(step_ratio_unsimplified)
-- Get and simplify the step ratio
local kp = step_ratio_unsimplified[1]
local kq = step_ratio_unsimplified[2]
local k = rat.gcd(kp, kq)
local num = kp / k
local den = kq / k
return { num, den }
end
-- Function that produces a chain of scale degrees. If the genchain function
-- from the module mos gamut were read as scale degrees, what would they be?
-- (EG, major 2nd, augmented 2nd, etc)
-- TODO: part of a rewrite for the mos degrees function
function p.mos_degreechain(input_mos, genchain_length_per_period, going_up)
-- Default parameters for testing
--[[
local input_mos = input_mos or mos.new(5, 5, 2)
local genchain_length_per_period = genchain_length_per_period or 10
local going_up = false
]]--
-- Get the number of mossteps per period and equave
local mossteps_per_equave = input_mos.nL + input_mos.ns
local periods_per_equave = rat.gcd(input_mos.nL, input_mos.ns)
local mossteps_per_period = mossteps_per_equave / periods_per_equave
-- Get the number of mossteps for the generators
local bright_gen = mos.bright_gen(input_mos)
local mossteps_per_bright_gen = bright_gen['L'] + bright_gen['s']
-- The easiest way to get scale degrees is through the following method:
-- - Get the genchain corresponding to the second-brightest mode and
-- interpret the result as scale degrees.
-- - If this is the ascending chain, then this represents every interval
-- class' large size. For each period, the first two elements represent
-- the root (perfect) and bright generator (large size is perfect).
-- - If this is the descending chain, then find the period complement of
-- each element, which represents every interval class's small size.
-- For each period, the first two elements represent the root (perfect)
-- and dark generator (small size is perfect).
-- - Since the root and generators start as perfect, any chromas added to
-- that represent augmented/diminished degrees.
-- Note that this is agnostic of notation, so only the genchain length is
-- needed. The UDP for the second-brightest mode is auto-calculated and is
-- used regardless of whether the chain is going up or down.
local genchain = mosg.mos_genchain(input_mos, mossteps_per_period - 2, genchain_length_per_period, true)
-- Interpret as scale degrees.
-- The first two elements are always perfect.
-- 0 chromas is major (or minor if descending).
-- 1 chroma is augmented (diminished if descending).
-- n chromas is n-times augmented (n-times diminished if descending).
-- Notes and caveats:
-- - The 0-mosdegree is also called the unison.
-- - Generators for nL ns mosses aren't called perf/aug/dim, but instead
-- called major and minor. This requires offsetting the note's chroma
-- value by -1, or else the singly-aug/dim generator will be skipped.
local degreechain = {}
for j = 1, periods_per_equave do
local chain_for_period = {}
for i = 1, #genchain[j] do
local note = genchain[j][i]
local mossteps = note['mossteps']
local chromas = note['chromas']
-- Mosses of the form nL ns have slightly different rules. Is the
-- input mos of that form?
local is_nL_ns = input_mos.nL == input_mos.ns
if is_nL_ns and mossteps == mossteps_per_bright_gen + (j-1) * mossteps_per_period then
chromas = chromas - 1
end
-- If not going up, use the period complements instead
-- Period complements of the root will end up being the note one
-- period up, so use modular arithmetic to keep the starting point
-- constrained to the root
local aug_or_dim = "augmented"
local maj_or_min = "major"
if not going_up then
mossteps = (j - 1) * mossteps_per_period + (mossteps_per_period - mossteps) % mossteps_per_period
aug_or_dim = "diminished"
maj_or_min = "minor"
end
-- What is the quality of the note? (major, minor, etc)
local quality = ""
if i == 1 then
quality = "perfect"
elseif i == 2 then
if is_nL_ns then
quality = maj_or_min
else
quality = "perfect"
end
else
if chromas == 0 then
quality = maj_or_min
elseif chromas == 1 then
quality = aug_or_dim
else
quality = chromas .. "×" .. aug_or_dim
end
end
-- Put together the name
-- TODO?: Prefix lookup
-- If it's the 0-mosstep, simply say it's the unison instead.
local degree = ""
if mossteps == 0 then
degree = quality .. " unison"
else
degree = quality .. " " .. mossteps .. "-mosstep"
end
table.insert(chain_for_period, degree)
end
table.insert(degreechain, chain_for_period)
end
return degreechain
end
-- Function that produces a list of mosdegrees for a mos with a step ratio
-- How far these extend is dependent on UDP
-- TODO: separate this into a helper module called "MOS notation"
function p.mos_degrees(input_mos, udp, step_ratio)
local input_mos = input_mos or mos.new(6, 2, 2)
local step_ratio = step_ratio or { 2, 1 }
-- Number of mossteps per period and equave, and number of periods
local mossteps_per_equave = input_mos.nL + input_mos.ns
local periods_per_equave = rat.gcd(input_mos.nL, input_mos.ns)
local mossteps_per_period = mossteps_per_equave / periods_per_equave
-- The default UDP corresponds to the brightest mode
-- If it's 5L 2s, default to the second-brightest mode
local udp_default = { mossteps_per_equave - periods_per_equave, 0 }
if scale_sig == "5L 2s" then
udp_default = { 5, 1 }
end
local udp_parsed = udp or udp_default
local generators_up = udp_parsed[1]
local generators_down = udp_parsed[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
-- Get and simplify the step ratio
local step_ratio_simplified = p.simplify_step_ratio(step_ratio)
local num = step_ratio_simplified[1]
local den = step_ratio_simplified[2]
-- Calculate genchain extend
local x = input_mos.nL / periods_per_equave
local y = input_mos.ns / periods_per_equave
local genchain_extend = 0
if num / den == 2 then
genchain_extend = x
elseif num == den or den == 0 then
genchain_extend = 0
else
genchain_extend = x * math.floor(num/2) + y * math.floor(den/2)
end
-- Arguments for the genchain function
local gens_up_per_period = generators_up / periods_per_equave
local gens_dn_per_period = generators_down / periods_per_equave
local asc_length = gens_up_per_period + genchain_extend
local des_length = gens_dn_per_period + genchain_extend
-- For ease of programming, the genchain function is called.
-- The ascending genchain represents intervals in their large size, followed by
-- augmented intervals, and the descending genchain represents intervals in their
-- small size, followed by diminished intervals.
-- Note: the descending genchain is actually a shorter ascending genchain, but the
-- equave complements are used instead.
local asc_genchain = mosg.mos_genchain(input_mos, mossteps_per_period - 2, asc_length, true)
local des_genchain = mosg.mos_genchain(input_mos, mossteps_per_period - 2, des_length, true)
-- How many esteps are in the equave? Per period? Per generator
local esteps_per_equave = input_mos.nL * num + input_mos.ns * den
local esteps_per_period = esteps_per_equave / periods_per_equave
-- How many esteps are the bright and dark generators?
local bright_gen = mos.bright_gen(input_mos)
local esteps_per_bright_gen = bright_gen['L'] * num + bright_gen['s'] * den
local esteps_per_dark_gen = esteps_per_period - esteps_per_bright_gen
-- Create an array of mosdegrees
local degrees = {}
for i = 1, esteps_per_equave + 1 do
table.insert(degrees, "")
end
-- Use the mos's prefix, if any; otherwise, use "mos"
local prefix = "mos"
-- Interpret the ascending genchain as mosdegrees
for j = 1, periods_per_equave do
local accumulator = 0
for i = 1, #asc_genchain[j] do
local index = (accumulator % esteps_per_period) + (j - 1) * esteps_per_period + 1
-- Convert the notationally agnostic form into a mosdegree
local note = asc_genchain[j][i]
local mosstep = note['mossteps']
local chroma_count = note['chromas']
-- What does it mean in text?
-- 0 chromas is major, 1 is augmented, 2 is doubly-augmented, etc
-- The first two elements in the genchain (the root and bright gen)
-- are both perfect (one size for root and large size for gen)
local quality = ""
if i == 1 then
-- The root is always perfect
quality = "Perf."
elseif i == 2 then
-- Use "perfect" if the mos is not nL ns; otherwise, use major
quality = "Perf."
else
if chroma_count == 0 then
quality = "Maj."
elseif chroma_count == 1 then
quality = "Aug."
else
quality = "Aug<sup>" .. chroma_count .. "</sup>"
end
end
-- Assemble the mosdegree by appending a quality to a k-mosdegree
-- If the mosstep is 0, use "unison" instead of "0-mosstep"
local degree_name = ""
if mosstep == 0 then
degree_name = quality .. " " .. "unison"
else
degree_name = quality .. " " .. mosstep .. "-" .. prefix .. "degree"
end
-- Add to degrees
degrees[index] = degrees[index] .. degree_name
accumulator = accumulator + esteps_per_bright_gen
end
end
-- Find the equave complements for the second genchain, and interpret those
-- as mosdegrees. This must come after the ascending genchains so in the case that
-- one pitch is reached in two directions (eg, C# and Db in standard tuning),
-- the one from the ascending chain (the C# in the example) comes first.
for j = 1, periods_per_equave do
local accumulator = 0
-- The start index for the following for loop starts at a note one period up
-- and goes down. However, with a multi-period mos, that same note is the root
-- for the next period, leading to a duplicate entry. To keep that from happening,
-- only start the for loop at 1 if this is the last period.
local start_index = 1
if j ~= periods_per_equave then
start_index = start_index + 1
end
for i = start_index, #des_genchain[j] do
local index = (accumulator % esteps_per_period) + (j - 1) * esteps_per_period + 1
-- Find the corresponding index for the equave complement
index = esteps_per_equave - index + 2
-- Convert the notationally agnostic form into a mosdegree
local note = des_genchain[j][i]
local mosstep = note['mossteps']
local chroma_count = note['chromas']
-- Use the equave complement of the mosstep
mosstep = mossteps_per_equave - mosstep
-- What does it mean in text? Use the rules for the ascending chain
-- but reversed in the other direction.
-- 0 chromas is minor, 1 is diminished, 2 is doubly-diminished, etc
-- The first two elements in the genchain (the period and dark gen)
-- are both perfect (one size for period and small size for gen).
local quality = ""
if i == 1 then
-- The period is always perfect
quality = "Perf."
elseif i == 2 then
-- Use "perfect" if the mos is not nL ns; otherwise, use minor
quality = "Perf."
else
if chroma_count == 0 or chroma_count == -0 then
quality = "Min."
elseif chroma_count == 1 then
quality = "Dim."
else
quality = "Dim<sup>" .. chroma_count .. "</sup>"
end
end
-- Assemble the mosdegree by appending a quality to a k-mosdegree
-- If the mosstep is the equave, use "equave" instead of "n-mosstep"
-- But if the equave is the octave, use "octave" instead
local degree_name = ""
if mosstep == mossteps_per_equave then
degree_name = quality .. " " .. "equave"
else
degree_name = quality .. " " .. mosstep .. "-" .. prefix .. "degree"
end
-- Add to degrees
if degrees[index] == "" then
degrees[index] = degrees[index] .. degree_name
else
degrees[index] = degrees[index] .. ", " .. degree_name
end
-- Increment by bright gen b/c this genchain is the ascending genchain's equave complements
accumulator = accumulator + esteps_per_bright_gen
end
end
return degrees
end
-- Algorithm:
-- Use the input mos, udp, and step ratio to find the genchains
-- Using the genchains and UDP, find the mos's intervals/degrees
-- Format the result as a table
function p.mos_degrees_frame(frame)
-- Default parameters for input mos and step ratio (5L 2s and 2:1 step ratio)
local input_mos_unparsed = frame.args['Scale Signature']
local input_mos = mos.parse(input_mos_unparsed) or mos.new(2, 5, 2)
-- Step ratio
local step_ratio = { 2, 1 }
if string.len(frame.args['Step Ratio']) > 0 then
step_ratio = p.parse_step_ratio(frame.args['Step Ratio'])
end
-- Get the number of mossteps per period and equave
local mossteps_per_equave = input_mos.nL + input_mos.ns
local periods_per_equave = rat.gcd(input_mos.nL, input_mos.ns)
local mossteps_per_period = mossteps_per_equave / periods_per_equave
-- If certain params were left blank and the scalesig is 5L 2s, the default
-- params will be for standard notation
local scale_sig = mos.as_string(input_mos)
-- The default UDP corresponds to the brightest mode
-- If it's 5L 2s, default to the second-brightest mode
local udp = { mossteps_per_equave - periods_per_equave, 0 }
if scale_sig == "5L 2s" then
udp = { 5, 1 }
end
if string.len(frame.args['UDP']) > 0 then
udp = p.parse_udp(frame.args['UDP'])
end
-- Get note symbols
-- If this param was blank, default to diamond-mos; limited to 17 note names
-- But if it's blank and the scalesig is 5L 2s, default to standard notation
-- This order of operations allows for overriding standard notation for 5L 2s
local note_symbols_main = "JKLMNOPQRSTUVWXYZ"
local note_symbols = string.sub(note_symbols_main, 1, mossteps_per_equave)
if scale_sig == "5L 2s" then
note_symbols = "CDEFGAB"
end
-- If a value was entered, override the default value
if string.len(frame.args['Note Symbols']) > 0 then
note_symbols = frame.args['Note Symbols']
end
-- Get accidental symbols
-- If this param was blank, default to diamond-mos symbols & and @
-- unless the mos is 5L 2s, then it's sharp and flat # and b
-- This order of operations allows for overriding standard notation for 5L 2s
local chroma_plus_symbol = "&"
local chroma_minus_symbol = "@"
if scale_sig == "5L 2s" then
chroma_plus_symbol = "#"
chroma_minus_symbol = "b"
end
-- If value(s) were entered, override the default values
if string.len(frame.args['Sharp Symbol']) > 0 then
chroma_plus_symbol = frame.args['Sharp Symbol']
end
if string.len(frame.args['Flat Symbol']) > 0 then
chroma_minus_symbol = frame.args['Flat Symbol']
end
-- Get the gamut
local gamut = mosg.mos_gamut(input_mos, udp, step_ratio, note_symbols, chroma_plus_symbol, chroma_minus_symbol)
-- Get the scale degrees
local degrees = p.mos_degrees(input_mos, udp, step_ratio)
-- Format the output as a table, starting with the header row
local result = '{| class="wikitable"\n'
-- Produce the headers
local steps_in_et = input_mos.nL * step_ratio[1] + input_mos.ns * step_ratio[2]
local et_for_mos = et.new(steps_in_et, input_mos.equave)
result = result .. "! Steps of " .. et.as_string(et_for_mos) .. " !! Cent value !! Scale degree !! Note name on ".. string.sub(note_symbols, 1, 1) .. "\n"
-- Add the rows
local step_ratio_gcd = rat.gcd(step_ratio[1], step_ratio[2]) -- GCD of the sizes of L and s, in case L:s isn't simplified
local cents_per_equave = rat.cents(input_mos.equave) -- Equave in cents
for i = 1, #gamut do
-- Get the note name
-- If the note name has a slash, replace it with a newline
local note_name = gamut[i]
note_name = note_name:gsub("/", "\n")
-- Get the scale degree
-- If there's a comma, replace it with a newline
local degree = degrees[i]
degree = degree:gsub(", ", "\n")
-- Does the current estep correspond to a natural of the mos?
-- In other words, are there no accidentals? If so, color
-- the row white. Otherwise, color it black. This is to mimic a piano
-- layout.
local has_accidentals = string.find(note_name, chroma_plus_symbol) or string.find(note_name, chroma_minus_symbol)
-- Add row
if has_accidentals then
result = result .. "|-\n"
result = result .. '|bgcolor="#c8ccd1"|' .. (i - 1) * step_ratio_gcd .. "\n"
result = result .. '|bgcolor="#c8ccd1"|' .. utils._round_dec(cents_per_equave * (i - 1) / (#gamut - 1), 3) .. "¢\n"
result = result .. '|bgcolor="#c8ccd1"|' .. degree .. "\n"
result = result .. '|bgcolor="#c8ccd1"|' .. note_name .. "\n"
else
result = result .. "|-\n"
result = result .. "| " .. (i - 1) * step_ratio_gcd .. "\n"
result = result .. "| " .. utils._round_dec(cents_per_equave * (i - 1) / (#gamut - 1), 3) .. "¢\n"
result = result .. "| " .. degree .. "\n"
result = result .. "| " .. note_name .. "\n"
end
end
result = result .. "|}"
return result
end
return p