Module:MOS in EDO
Jump to navigation
Jump to search
Documentation transcluded from /doc
Note: Do not invoke this module directly; use the corresponding template instead: Template:MOS in EDO.
Documentation transcluded from /doc
Note: Do not invoke this module directly; use the corresponding template instead: Template:MOS in EDO.
local mos = require("Module:MOS")
local utils = require("Module:Utils")
local tamnams = require("Module:TAMNAMS")
local p = {}
-- Global variables for cell colors
-- Dark blue for large steps, light blue for small steps
-- For mosses that are reversed (starts with s and ends with L), use orange instead
p.cell_color_none = "" -- For cells that don't have a color (default cell color applies)
p.cell_color_perfect_size = "" -- Only applies for steps of an edo
p.cell_color_large = "#BDD7EE"
p.cell_color_small = "#DDEBF7"
p.cell_color_lg_rev = "#F8CBAD"
p.cell_color_sm_rev = "#FCE4D6"
-- Helper function
-- Given a mos represented as a step pattern in an array, find the sizes of its large and small steps
function p.calculate_step_sizes(step_array)
local step_array = step_array or { 3, 3, 3, 2, 3, 3, 2 }
-- Initialize large/small step sizes to be some really big numbers
local large_step_size = -1000000000
local small_step_size = 1000000000
-- Then iterate through array and update the largest and smallest sizes
for i = 1, #step_array do
if step_array[i] > large_step_size then
large_step_size = step_array[i]
end
if step_array[i] < small_step_size then
small_step_size = step_array[i]
end
end
return { ["L"] = large_step_size, ["s"] = small_step_size }
end
-- Helper function
-- Given a mos represented as a step pattern in an array, produce the mos's scale sig
-- Scale sigs in this module are implied to be octave-equivalent, as this is meant for edo pages
function p.mos_step_pattern_to_scale_sig(step_array)
local step_array = step_array or { 5, 5, 5, 3, 5, 5, 3 }
-- Calculate step sizes
local step_sizes = p.calculate_step_sizes(step_array)
local large_step_size = step_sizes["L"]
local small_step_size = step_sizes["s"]
-- Initialize step counts
local large_step_count = 0
local small_step_count = 0
-- Then start counting by iterating through array
for i = 1, #step_array do
if step_array[i] == large_step_size then
large_step_count = large_step_count + 1
elseif step_array[i] == small_step_size then
small_step_count = small_step_count + 1
end
end
return string.format("%iL %is", large_step_count, small_step_count)
end
-- Helper function
-- Given a mos represented as a step pattern in an array, find the next mos
-- Transpiled from python code with aid of ChatGPT
function p.calculate_next_mos_step_pattern(step_array)
local step_array = step_array or { 5, 5, 5, 3, 5, 5, 3 }
-- Get the step sizes
local step_sizes = p.calculate_step_sizes(step_array)
local small_step_size = step_sizes["s"]
local large_step_size = step_sizes["L"]
local chroma = large_step_size - small_step_size
local first_step = step_array[1]
local last_step = step_array[#step_array]
local next_step_array = {}
-- If the small and large step sizes are the same, return nil
if small_step_size == large_step_size then
return nil
end
-- If the small step is smaller than the chroma, then
-- - If the first step is the large step and the last step is the small step,
-- then the replacement rules are L -> Ls and s -> s
-- - If the first step is the small step and the last step is the large step,
-- then the replacement rules are L -> sL and s -> s
-- If the small step is larger than the chroma, then
-- - If the first step is the large step and the last step is the small step,
-- then the replacement rules are L -> sL and s -> L
-- - If the first step is the small step and the last step is the large step,
-- then the replacement rules are L -> Ls and s -> L
if small_step_size < chroma then
local next_large_step_size = chroma
local next_small_step_size = small_step_size
if first_step == large_step_size and last_step == small_step_size then
for i = 1, #step_array do
local step = step_array[i]
if step == large_step_size then
table.insert(next_step_array, next_large_step_size)
table.insert(next_step_array, next_small_step_size)
elseif step == small_step_size then
table.insert(next_step_array, next_small_step_size)
end
end
else
for i = 1, #step_array do
local step = step_array[i]
if step == large_step_size then
table.insert(next_step_array, next_small_step_size)
table.insert(next_step_array, next_large_step_size)
elseif step == small_step_size then
table.insert(next_step_array, next_small_step_size)
end
end
end
else
local next_large_step_size = small_step_size
local next_small_step_size = chroma
if first_step == large_step_size and last_step == small_step_size then
for i = 1, #step_array do
local step = step_array[i]
if step == large_step_size then
table.insert(next_step_array, next_small_step_size)
table.insert(next_step_array, next_large_step_size)
elseif step == small_step_size then
table.insert(next_step_array, next_large_step_size)
end
end
else
for i = 1, #step_array do
local step = step_array[i]
if step == large_step_size then
table.insert(next_step_array, next_large_step_size)
table.insert(next_step_array, next_small_step_size)
elseif step == small_step_size then
table.insert(next_step_array, next_large_step_size)
end
end
end
end
return next_step_array
end
-- Helper function
-- Create a step visualization that's based on the table on the diasem page
function p.step_pattern_to_simple_visualization(step_pattern)
local step_pattern = step_pattern or { 2, 2, 2, 1, 2, 2, 1 }
local left_border = "├"
local right_border = "┤"
local no_border = "─"
local double_border = "╫"
local double_border_left = "╟"
local double_border_right = "╢"
local single_border = "┼"
local step_visualization = ""
-- For each step size of k, print a single border, followed by k-1 no-border symbols
-- If this is the first step, print the left border instead
-- If the step size is 0, print a double border only; if this happens, the next step should start with a border character
-- If this is the last step, add a right border after the entire sequence
for i = 1, #step_pattern do
local current_step_vis = ""
local current_step_size = step_pattern[i]
if i == 1 then
if current_step_size == 0 then
current_step_vis = double_border_left
else
current_step_vis = left_border .. string.rep(no_border, current_step_size - 1)
end
elseif i == #step_pattern then
if current_step_size == 0 then
current_step_vis = double_border_right
else
current_step_vis = single_border .. string.rep(no_border, current_step_size - 1) .. right_border
end
else
if current_step_size == 0 then
current_step_vis = double_border
elseif step_pattern[i-1] == 0 then
current_step_vis = string.rep(no_border, current_step_size - 1)
else
current_step_vis = single_border .. string.rep(no_border, current_step_size - 1)
end
end
step_visualization = step_visualization .. current_step_vis
end
return step_visualization
end
-- Primary function
-- Create a rectangular horogram depicting a mos for a given pair of generators
function p.mos_in_edo(edo, gen_in_edosteps, number_of_periods, temperament)
local edo = edo or 24
local gen_in_edosteps = gen_in_edosteps or 14
local number_of_periods = number_of_periods or 1
local temperament = temperament --or "meantone"
-- Check whether the number of periods divides the edo
-- If so, the starting scale will be a multiperiod mos
local period_in_edosteps = edo
local verified_number_of_periods = 1
if edo % number_of_periods == 0 then
period_in_edosteps = edo / number_of_periods
verified_number_of_periods = number_of_periods
end
-- Calculate whether to include temperament names
local show_temperament = temperament ~= "" and temperament ~= nil
-- Calculate the generator complement
local comp_in_edosteps = period_in_edosteps - gen_in_edosteps
-- Are the args for the starting mos valid?
-- The number of steps in the generator must be between 1 (inclusive) and the number of steps in the period (exclusive)
local starting_mos_valid = gen_in_edosteps >= 1 and gen_in_edosteps <= period_in_edosteps
-- Calculate the starting mos
local current_scale = {}
for i = 1, number_of_periods do
table.insert(current_scale, gen_in_edosteps)
table.insert(current_scale, comp_in_edosteps)
end
-- Create table, starting with headers
local result = "{| class=\"wikitable center-all\"\n"
.. "|+ style=\"font-size: 105%;\" | " .. string.format("Generators %i\\%i and %i\\%i\n", gen_in_edosteps, edo, comp_in_edosteps, edo)
.. "|-\n"
.. string.format("! colspan=\"%i\" | Steps\n", edo)
.. "! MOS (name)\n"
.. "! Step ratio\n"
if show_temperament then
result = result .. string.format("! Temperament\n")
end
-- Add the step pattern for successive mosses until the pattern becomes that for an edo
while current_scale ~= nil and starting_mos_valid do
-- Calculate current step ratio
-- Use this to determine which cell colors to use
local current_step_sizes = p.calculate_step_sizes(current_scale)
local step_ratio_gcd = utils._gcd(current_step_sizes["L"], current_step_sizes["s"])
local large_step_size = current_step_sizes["L"]
local small_step_size = current_step_sizes["s"]
-- Is the first step a large step?
local first_step_is_large_step = current_scale[1] == large_step_size
-- Add the step sizes
result = result .. "|-\n"
for i = 1, #current_scale do
local current_step = current_scale[i]
-- Calculate cell color
local cell_color = "NONE"
if large_step_size == small_step_size then
cell_color = p.cell_color_none
elseif first_step_is_large_step then
if current_step == large_step_size then
cell_color = p.cell_color_large
elseif current_step == small_step_size then
cell_color = p.cell_color_small
end
elseif not first_step_is_large_step then
if current_step == large_step_size then
cell_color = p.cell_color_lg_rev
elseif current_step == small_step_size then
cell_color = p.cell_color_sm_rev
end
end
if current_step == 1 then
if large_step_size == small_step_size then
result = result .. "| 1\n"
else
result = result .. string.format("| style=\"background-color: %s;\" | 1\n", cell_color)
end
else
if cell_color == p.cell_color_none then
result = result .. string.format("| colspan=\"%i\" | %i\n", current_step, current_step)
else
result = result .. string.format("| style=\"background-color: %s\" colspan=\"%i\" | %i\n", cell_color, current_step, current_step)
end
end
end
-- Add the scale sig
local scale_sig = p.mos_step_pattern_to_scale_sig(current_scale)
-- Get the tamnams name, if there is one
-- Don't show tamnams names for mosses with 5 notes or fewer (to keep the table from being cluttered)
local tamnams_name = tamnams.lookup_name(scale_sig)
local current_step_count = #current_scale
-- Use step sizes to determine whether the mos is an edo
local reduced_large_step_size = current_step_sizes["L"] / step_ratio_gcd
local reduced_small_step_size = current_step_sizes["s"] / step_ratio_gcd
if reduced_large_step_size == reduced_small_step_size then
result = result .. string.format("| [[%iedo]]\n", edo / step_ratio_gcd)
elseif tamnams_name ~= nil and current_step_count > 5 then
result = result .. string.format("| [[%s]] (%s)\n", scale_sig, tamnams_name)
else
result = result .. string.format("| [[%s]]\n", scale_sig)
end
-- Add the step ratio
result = result .. string.format("| %s:%s\n", current_step_sizes["L"] / step_ratio_gcd, current_step_sizes["s"] / step_ratio_gcd)
-- Add the temperament name, if there is one
if show_temperament then
local current_step_count = #current_scale
result = result .. string.format("| %s[%i]\n", temperament, current_step_count)
end
-- Produce the next scale in the sequence
current_scale = p.calculate_next_mos_step_pattern(current_scale)
end
result = result .. "|}\n"
return result
end
-- Alternate primary function
-- Instead of a "rectangular horogram", use the same type of visualization shown
-- on the diasem page
function p.mos_in_edo_simplified(edo, gen_in_edosteps, number_of_periods, generation_limit, temperament)
local edo = edo or 24
local gen_in_edosteps = gen_in_edosteps or 14
local number_of_periods = number_of_periods or 1
local generation_limit = generation_limit or edo - 1
local temperament = temperament --or "meantone"
-- Check whether the number of periods divides the edo
-- If so, the starting scale will be a multiperiod mos
local period_in_edosteps = edo
local verified_number_of_periods = 1
if edo % number_of_periods == 0 then
period_in_edosteps = edo / number_of_periods
verified_number_of_periods = number_of_periods
end
-- Check whether the generation limit is valid
-- If it's -1, then show all generations (period_in_edosteps-1)
if generation_limit == -1 then
generation_limit = period_in_edosteps - 1
end
-- Calculate whether to include temperament names
local show_temperament = temperament ~= "" and temperament ~= nil
-- Calculate the generator complement
local comp_in_edosteps = period_in_edosteps - gen_in_edosteps
-- Are the args for the starting mos valid?
-- The number of steps in the generator must be between 1 (inclusive) and the number of steps in the period (exclusive)
local starting_mos_valid = gen_in_edosteps >= 1 and gen_in_edosteps <= period_in_edosteps
-- Calculate the starting mos
local current_scale = {}
for i = 1, number_of_periods do
table.insert(current_scale, gen_in_edosteps)
table.insert(current_scale, comp_in_edosteps)
end
-- Create table, starting with headers
local result = "{| class=\"wikitable center-all\"\n"
.. "|+ style=\"font-size: 105%;\" | " .. string.format("Generators %i\\%i and %i\\%i\n", gen_in_edosteps, edo, comp_in_edosteps, edo)
.. "|-\n"
.. "! Step visualization\n"
.. "! MOS (name)\n" -- Scale sig (and name)
.. "! Step sizes\n" -- Step sizes
.. "! Step ratio\n" -- Step ratio
if show_temperament then
result = result .. "! Temperament\n" -- Temperament, if given
end
-- Add the step pattern for successive mosses until the pattern becomes that for an edo
local generation_count = 1
while current_scale ~= nil and starting_mos_valid and generation_count <= generation_limit do
-- Calculate current step ratio
-- Use this to determine which cell colors to use
local current_step_sizes = p.calculate_step_sizes(current_scale)
local step_ratio_gcd = utils._gcd(current_step_sizes["L"], current_step_sizes["s"])
local large_step_size = current_step_sizes["L"]
local small_step_size = current_step_sizes["s"]
-- New row
result = result .. "|-\n"
-- Add the step visualization
local step_vis = p.step_pattern_to_simple_visualization(current_scale)
result = result .. string.format("| %s\n", step_vis)
-- Add the scale sig
-- Also add tamnams name if there is one
local scale_sig = p.mos_step_pattern_to_scale_sig(current_scale)
local tamnams_name = tamnams.lookup_name(scale_sig)
local current_step_count = #current_scale
if large_step_size == small_step_size then
result = result .. string.format("| [[%iedo]]\n", edo / step_ratio_gcd)
elseif tamnams_name ~= nil and current_step_count > 5 then
result = result .. string.format("| [[%s]] (%s)\n", scale_sig, tamnams_name)
else
result = result .. string.format("| [[%s]]\n", scale_sig)
end
-- Add the step sizes
result = result .. string.format("| %i, %i\n", current_step_sizes["L"], current_step_sizes["s"])
-- Add step ratio
local reduced_large_step_size = large_step_size / step_ratio_gcd
local reduced_small_step_size = small_step_size / step_ratio_gcd
result = result .. string.format("| %s:%s\n", reduced_large_step_size, reduced_small_step_size)
-- Add the temperament name, if there is one
if show_temperament then
local current_step_count = #current_scale
result = result .. string.format("| %s[%i]\n", temperament, current_step_count)
end
-- Produce the next scale in the sequence
current_scale = p.calculate_next_mos_step_pattern(current_scale)
-- Increment the generation (row) count
generation_count = generation_count + 1
end
result = result .. "|}\n"
return result
end
-- Function to be called by a template
function p.mos_in_edo_frame(frame)
local edo = tonumber(frame.args["EDO"])
local gen_in_edosteps = tonumber(frame.args["Generator"])
local temperament = frame.args["Temperament"]
local number_of_periods = tonumber(frame.args["Periods"])
local generation_limit = tonumber(frame.args["Generation Limit"])
local result = ""
result = p.mos_in_edo_simplified(edo, gen_in_edosteps, number_of_periods, generation_limit, temperament)
return result
end
return p