Module:MOS in EDO

Revision as of 20:17, 5 December 2023 by Ganaram inukshuk (talk | contribs) (Undo revision 128495 by Ganaram inukshuk (talk); adjusted colors were too dark)
Module documentation[view] [edit] [history] [purge]
This module should not be invoked directly; use its corresponding template instead: Template:MOS in EDO.

This module displays all MOS scales in a given EDO with a particular generator and number of periods per octave.

Introspection summary for Module:MOS in EDO 
Functions provided (5)
Line Function Params
17 calculate_step_sizes (step_array)
41 mos_step_pattern_to_scale_sig (step_array)
69 calculate_next_mos_step_pattern (step_array)
156 mos_in_edo (edo, gen_in_edosteps, number_of_periods, temperament)
288 mos_in_edo_frame (invokable) (frame)
Lua modules required (2)
Variable Module Functions used
mos Module:MOS dependency not used
utils Module:Utils _gcd

No function descriptions were provided. The Lua code may have further information.


local mos = require('Module:MOS')
local utils = require('Module:Utils')
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 = "NONE"				-- For cells that don't have a color (default cell color applies)
p.cell_color_perfect_size = "NONE"		-- 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

-- 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 = string.format('{| class="wikitable"\n')
	result = result .. string.format('|+ Generators %i\\%i and %i\\%i\n', gen_in_edosteps, edo, comp_in_edosteps, edo)
	result = result .. string.format('|-\n')
	result = result .. string.format('! colspan="%i" |Steps\n', edo)
	result = result .. string.format('!MOS (name)\n')
	result = result .. string.format('!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 .. string.format('|-\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 .. string.format('| 1\n')
				else
					result = result .. string.format('| bgcolor="%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('| bgcolor="%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 = mos.tamnams_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 .. string.format('|}\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["Number of Periods"])
	
	local result = p.mos_in_edo(edo, gen_in_edosteps, number_of_periods, temperament)
	
	return result
	
end

return p