Module:MOS degrees

From Xenharmonic Wiki
Jump to navigation Jump to search

Documentation for this module may be created at Module:MOS degrees/doc

local mos = require('Module:MOS')
local et = require('Module:ET')
local rat = require('Module:Rational')
local utils = require('Module:Utils')
local mosnot = require('Module:MOS notation')		-- Contains the important functions
local p = {}

-- This module has been replaced with a new version called Module:MOS degrees v2

-- Helper function
-- Parses up to 5 step ratios entered as text in a semicolon-delimited string,
-- where each step ratio is separated with a slash
-- EG, "2/1; 3/1; 3/2" becomes {{2, 1}, {3, 1}, {3, 2}}
function p.parse_step_ratios(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
	
	-- Parse up to 5 step ratios (hardcoded)
	local max_ratios = 5
	local loop_limit = math.min(max_ratios, #parsed)
	local step_ratios = {}
	for i = 1, loop_limit do
		local ratio = mosnot.parse_step_ratio(parsed[i])
		table.insert(step_ratios, ratio)
	end
	-- 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

-- Helper function
-- Parses genchain extend values, where the first value is for the ascending
-- chain and the second value is for the descending chain
function p.parse_genchain_extend(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 == 1 then
		return { tonumber(parsed[1]), tonumber(parsed[1]) }
	else
		return { tonumber(parsed[1]), tonumber(parsed[2]) }
	end
end

-- Helper function; creates the column for the note name
-- Decoupled degree and note name functions allow note names being omitted.
function p.preprocess_note_names(input_mos, udp, note_symbols, sharp_symbol, flat_symbol, asc_chain_length, des_chain_length)
	-- Test parameters
	--[[
	local input_mos = input_mos or mos.new(5, 2, 2)
	local udp = udp or { 5, 1 }
	local note_symbols = note_symbols or "CDEFGAB"
	local sharp_symbol = sharp_symbol or "#"
	local flat_symbol = flat_symbol or "b"
	local asc_chain_length = input_mos.nL * 2 + input_mos.ns
	local des_chain_length = input_mos.nL * 2 + input_mos.ns
	]]--
	
	-- Get the number of mossteps per period and 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_period = mossteps_per_equave / periods_per_equave
	
	-- How long are the initial genchain lengths? (These correspond to the UDP)
	local gens_up_per_period = udp[1] / periods_per_equave
	local gens_dn_per_period = udp[2] / periods_per_equave
	
	-- Get the genchains
	local asc_genchain = mosnot.mos_nomacc_chain(input_mos, gens_up_per_period, asc_chain_length, true)
	local des_genchain = mosnot.mos_nomacc_chain(input_mos, gens_dn_per_period, des_chain_length, false)
	
	-- Calculate the entries for each cell
	local column = {}
	for i = 1, periods_per_equave do
		-- Add degrees from ascending chain
		for j = 1, asc_chain_length do
			local mossteps = asc_genchain[i][j]['mossteps']
			local chromas  = asc_genchain[i][j]['chromas']
			
			-- Find the note name
			local note_symbol = string.sub(note_symbols, mossteps + 1, mossteps + 1)
			local note_name = mosnot.mosstep_and_chroma_to_note_name(mossteps, chromas, note_symbol, sharp_symbol)

			table.insert(column, note_name)
		end
		
		-- Calculate the stop value for the for loop as being 1 or 2, depending
		-- on whether this is the last period or not
		local stop_value = 1
		if i ~= periods_per_equave then
			stop_value = stop_value + 1
		end
		
		-- Add degrees from descending chain
		-- The descending chain differs from the ascending chain:
		-- - The descending chain should follow after the ascending chain.
		-- - The descending chain's entries should be added backwards and skip
		--   the root.
		-- - This way, if the mos is multi-period, the root of the next period's
		--   ascending chain (which is the same as the current period's descend-
		--   ing chain) won't be added twice.
		-- - If the period is the last period, add the root as the equave.
		for j = des_chain_length, stop_value, -1 do
			local mossteps = des_genchain[i][j]['mossteps']
			local chromas  = des_genchain[i][j]['chromas']
			
			-- Find the note name
			-- If the mosstep is the root of the period, add a period to it
			local note_symbol = string.sub(note_symbols, mossteps + 1, mossteps + 1)
			if mossteps % mossteps_per_period == 0 then
				mossteps = mossteps + mossteps_per_period
			end
			local note_name = mosnot.mosstep_and_chroma_to_note_name(mossteps, chromas, note_symbol, flat_symbol)
			
			table.insert(column, note_name)
		end
	end
	
	return column
end

-- Helper function; creates the column for the degree name
-- Decoupled degree and note name functions allow note names being omitted.
function p.preprocess_degrees(input_mos, asc_chain_length, des_chain_length, prefix, notation)
	-- Test parameters
	--[[
	local input_mos = input_mos or mos.new(5, 2, 2)
	local asc_chain_length = input_mos.nL * 2 + input_mos.ns
	local des_chain_length = input_mos.nL * 2 + input_mos.ns
	local prefix = "mos"
	local notation = "mosstep"
	]]--
	
	-- Get the number of mossteps per period and equave
	local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
	
	-- Get the degrees
	local asc_degrees = mosnot.mos_degree_chain(input_mos, asc_chain_length, true)
	local des_degrees = mosnot.mos_degree_chain(input_mos, des_chain_length, false)
	
	-- Calculate the entries for each cell
	local column = {}
	for i = 1, periods_per_equave do
		-- Add degrees from ascending chain
		for j = 1, asc_chain_length do
			local mossteps = asc_degrees[i][j]['mossteps']
			local quality  = asc_degrees[i][j]['quality']
			
			-- Find the degree name
			-- If the degree is the perfect 0-mosdegree, append "unison"
			local degree_name = mosnot.mosstep_and_quality_to_degree(mossteps, quality, prefix, notation)
			if mossteps == 0 and quality == 0 then
				degree_name = degree_name .. " (unison)"
			end
			
			table.insert(column, degree_name)
		end
		
		-- Calculate the stop value for the for loop as being 1 or 2, depending
		-- on whether this is the last period or not
		local stop_value = 1
		if i ~= periods_per_equave then
			stop_value = stop_value + 1
		end
		
		-- Add degrees from descending chain
		-- The descending chain differs from the ascending chain:
		-- - The descending chain should follow after the ascending chain.
		-- - The descending chain's entries should be added backwards and skip
		--   the root.
		-- - This way, if the mos is multi-period, the root of the next period's
		--   ascending chain (which is the same as the current period's descend-
		--   ing chain) won't be added twice.
		-- - If the period is the last period, add the root as the equave.
		for j = des_chain_length, stop_value, -1 do
			local mossteps = des_degrees[i][j]['mossteps']
			local quality  = des_degrees[i][j]['quality']
			
			-- Find the degree name
			-- If the degree corresponds to the equave, say it's the equave
			local degree_name = mosnot.mosstep_and_quality_to_degree(mossteps, quality, prefix, notation)
			
			-- If j is ever 1, then the mosdegree is the equave
			-- This only happens if the current period is the last period
			-- If the equave is 2/1, that's the octave
			if j == 1 then
				if rat.eq(input_mos.equave, 2) then
					degree_name = degree_name .. " (octave)"
				else
					degree_name = degree_name .. " (equave)"
				end
			end
			
			table.insert(column, degree_name)
		end
	end
	
	return column
end

-- Helper function
-- Creates the columns for the step and cent sizes
-- Separating this into its own function makes it easy to add colums for
-- different step ratios in the same table.
function p.preprocess_steps_and_cents(input_mos, step_ratio, asc_chain_length, des_chain_length)
	-- Test parameters
	--[[
	local input_mos = input_mos or mos.new(5, 2, 2)
	local step_ratio = { 2, 1 }
	local asc_chain_length = input_mos.nL * 2 + input_mos.ns
	local des_chain_length = input_mos.nL * 2 + input_mos.ns
	]]--
	
	-- Get the number of mossteps per period and 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_period = mossteps_per_equave / periods_per_equave
	
	-- What et is produced given the step ratio and equave?
	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)
	
	-- How many esteps per period? Per bright/dark gen?
	local esteps_per_period = steps_in_et / periods_per_equave
	local bright_gen = mos.bright_gen(input_mos)
	local esteps_per_bright_gen = bright_gen['L'] * step_ratio[1] + bright_gen['s'] * step_ratio[2]
	local esteps_per_dark_gen = esteps_per_period - esteps_per_bright_gen
	
	-- How many decimal places to round to?
	local round = 1
	
	-- Calculate the entries for each row
	local rows = {}
	for i = 1, periods_per_equave do
		-- Add step/cent values for ascending chain
		for j = 1, asc_chain_length do
			
			-- Find the estep count
			local estep_count = ((j - 1) * esteps_per_bright_gen) % esteps_per_period + (i - 1) * esteps_per_period
			
			-- Find the cent value, rounded
			local cent_value = utils._round_dec(et.cents(et_for_mos, estep_count), round)
			
			-- Add the row
			local row = { estep_count, cent_value }
			table.insert(rows, row)
		end
		
		-- Calculate the stop value for the for loop as being 1 or 2, depending
		-- on whether this is the last period or not
		local stop_value = 1
		if i ~= periods_per_equave then
			stop_value = stop_value + 1
		end
		
		-- Add step/cent values for ascending chain
		-- The descending chain differs from the ascending chain:
		-- - The descending chain should follow after the ascending chain.
		-- - The descending chain's entries should be added backwards and skip
		--   the root.
		-- - This way, if the mos is multi-period, the root of the next period's
		--   ascending chain (which is the same as the current period's descend-
		--   ing chain) won't be added twice.
		-- - If the period is the last period, add the root as the equave.
		for j = des_chain_length, stop_value, -1 do
			
			-- Find the estep count
			local estep_count = ((j - 1) * esteps_per_dark_gen) % esteps_per_period + (i - 1) * esteps_per_period
			
			-- Find the cent value
			local cent_value = utils._round_dec(et.cents(et_for_mos, estep_count), round)
			
			-- If j is ever 1, then the cent and step values are for the equave
			-- This only happens if the current period is the last period
			if j == 1 then
				cent_value = utils._round_dec(rat.cents(input_mos.equave), round)
				estep_count = steps_in_et
			end
			
			-- Add the row
			local row = { estep_count, cent_value }
			table.insert(rows, row)
		end
	end
	
	return rows
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 = mos.parse(frame.args['Scale Signature']) or mos.new(2, 5, 2)
	
	-- Step ratios
	-- Up to three step ratios can be entered; the default is only 2/1
	-- Had to use parse function to make sure the default works
	local step_ratios = p.parse_step_ratios(frame.args['Step Ratio']) or p.parse_step_ratios("2/1")
	
	-- Get the number of mossteps per period and 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_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 middle mode. For mosses with an even
	-- number of modes, there are two middle modes, so use the brighter of the
	-- two instead.
	-- If it's 5L 2s, default to the second-brightest mode.
	local udp_default = { 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_default = { 5, 1 }
	end
	local udp = mosnot.parse_udp(frame.args['UDP']) or udp_default
	
	-- Get genchain extend value
	-- There are two genchain extend values for ascending and descending chains
	-- The default value for both is the number of large steps per period, so
	-- this value is per genchain per period.
	local genchain_extend_default = input_mos.nL / periods_per_equave
	local genchain_extend = p.parse_genchain_extend(frame.args['Genchain Extend'])
	local genchain_extend_up = genchain_extend[1] or genchain_extend_default
	local genchain_extend_dn = genchain_extend[2] or genchain_extend_default
	
	-- Should a note names column be added?
	local add_note_names = frame.args['Notation'] ~= "NONE"
	
	-- Get notation: naturals (or nominals), sharp symbol, and flat symbol
	local notation_default = { ['Naturals'] = string.sub("JKLMNOPQRSTUVWXYZ", 1, mossteps_per_equave), ['Sharp'] = "&", ['Flat'] = "@" }
	if scale_sig == "5L 2s" then
		notation_default['Naturals'] = "CDEFGAB"
		notation_default['Sharp'] = "#"
		notation_default['Flat'] = "b"
	end
	local notation = mosnot.parse_notation(frame.args['Notation']) or notation_default
	local note_symbols = notation['Naturals']
	local sharp_symbol = notation['Sharp']
	local flat_symbol = notation['Flat']
	
	-- Get notational options
	local mos_prefix = "mos"		-- TODO: add prefix lookup
	if frame.args['MOS Prefix'] == "NONE" then
		mos_prefix = ""
	elseif string.len(frame.args['MOS Prefix']) > 0 then
		mos_prefix = frame.args['MOS Prefix']
	end
	
	-- Override values for testing
	--[[
	local input_mos = mos.new(5, 2, 2)
	local step_ratios = {{ 2, 1 }, {3, 1}, {3, 2}}
	local udp = { 5, 1 }
	local note_symbols = "CDEFGAB"
	local sharp_symbol = "#"
	local flat_symbol = "b"
	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
	local scale_sig = mos.as_string(inpnut_mos)
	]]--

	-- Calculate the chain lengths
	local asc_chain_length = udp[1] / periods_per_equave + 1 + genchain_extend_up
	local des_chain_length = udp[2] / periods_per_equave + 1 + genchain_extend_dn

	-- Get the degrees and note names
	local degrees    = p.preprocess_degrees   (input_mos, asc_chain_length, des_chain_length, mos_prefix)
	local note_names = p.preprocess_note_names(input_mos, udp, note_symbols, sharp_symbol, flat_symbol, asc_chain_length, des_chain_length)
	
	-- Get the step and cent values
	-- Do this for each step ratio
	local steps_and_cents = {}
	for i = 1, #step_ratios do
		local cells = p.preprocess_steps_and_cents(input_mos, step_ratios[i], asc_chain_length, des_chain_length)
		table.insert(steps_and_cents, cells)
	end
	
	-- Pre-calculate the ets and step counts for each step ratio
	local steps_in_ets = {}
	local ets_for_mos = {}
	for i = 1, #step_ratios do
		local steps_in_ets_i = input_mos.nL * step_ratios[i][1] + input_mos.ns * step_ratios[i][2]
		local ets_for_mos_i = et.new(steps_in_ets_i, input_mos.equave)
		table.insert(steps_in_ets, steps_in_ets_i)
		table.insert(ets_for_mos, ets_for_mos_i)
	end

	-- Format the output as a table, starting with the header row
	local result = '{| class="wikitable sortable"\n'
	result = result .. '! rowspan="2" |Scale degree\n'
	
	if add_note_names then
		result = result .. '! rowspan="2" |On ' .. string.sub(note_symbols, 1, 1) .. "\n"
	end
	
	-- Add this once for every step ratio to be represented
	for i = 1, #step_ratios do
		-- Add names for specific step ratios
		local step_ratio_names = {
			['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",
		}
		local step_ratio_simplified = mosnot.simplify_step_ratio(step_ratios[i])
		local step_ratio_string = step_ratio_simplified[1] .. ":" .. step_ratio_simplified[2]
		local step_ratio_name = step_ratio_names[step_ratio_string]
		
		-- Add column header text
		if step_ratio_name == nil then
			result = result .. '! colspan="2" |' .. et.as_string(ets_for_mos[i]) .. " (L:s = " .. step_ratios[i][1] .. ":" .. step_ratios[i][2] .. ")\n"
		else
			result = result .. '! colspan="2" |' .. step_ratio_name .. " " .. scale_sig .. "\n"
			result = result .. '(' .. et.as_string(ets_for_mos[i]) .. ", L:s = " .. step_ratios[i][1] .. ":" .. step_ratios[i][2] .. ")\n"
		end
	end
	
	-- Next row
	result = result .. "|-\n"
	
	-- Add this once for every step ratio to be represented
	result = result .. string.rep("! Steps\n! Cents\n", #step_ratios)
	
	-- Add each row, containing a degree, note name, step count, and cent value
	for i = 1, #degrees do
		
		-- Is the row for a nominal? (Nominals have no accidentals and are
		-- therefore strings of size 1)
		-- Nominals don't depend on step ratio
		local is_nominal = string.len(note_names[i]) == 1
		
		-- Add new row, with coloring
		if not is_nominal then
			result = result .. '|- bgcolor="#eaeaff"\n'
		else
			result = result .. "|-\n"
		end
		
		-- Is the row for a period?
		-- Check whether it's a period only for the using the cent value for the
		-- first step ratio, since it'll be a period for the other ratios
		local cent_value = steps_and_cents[1][i][1]		-- First set of cells, current row, first column is current cent value
		local is_period = cent_value % (steps_in_ets[1] / periods_per_equave) == 0
		
		-- Add cell for degree
		-- Make any nominals that correspond to the period bold
		if is_period and is_nominal then
			result = result .. "| '''" .. degrees[i] .. "'''\n"
		else
			result = result .. "| " .. degrees[i] .. "\n"
		end
		
		-- Add cell for note name, if allowed
		if add_note_names then
			result = result .. "| " .. note_names[i] .. "\n"
		end
		
		-- Add cells for step size
		for j = 1, #step_ratios do
			result = result .. "| " .. steps_and_cents[j][i][1] .. "\n"		-- Steps
			result = result .. "| " .. steps_and_cents[j][i][2] .. \n"	-- Cents
		end
	end
	
	result = result .. "|}"
	
	return result
end
	
return p