Module:MOS degrees

From Xenharmonic Wiki
Revision as of 21:03, 16 June 2023 by Ganaram inukshuk (talk | contribs) (Created page with "local mos = require('Module:MOS') local mosg = require('Module:MOS gamut') local et = require('Module:ET') local rat = require('Module:Rational') --local mosnot = require('Mod...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search
Module documentation[view] [edit] [history] [purge]
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 
Functions provided (4)
Line Function Params
10 parse_step_ratio (step_ratio_unparsed)
24 simplify_step_ratio (step_ratio_unsimplified)
38 mos_degrees (input_mos, step_ratio)
217 mos_degrees_frame (invokable) (frame)
Lua modules required (4)
Variable Module Functions used
et Module:ET new
as_string
mos Module:MOS new
bright_gen
parse
as_string
mosg Module:MOS gamut mos_genchain
mos_gamut
rat Module:Rational gcd
cents

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 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 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 list of mosdegrees for a mos with a step ratio
-- TODO: separate this into a helper module called "MOS notation"
function p.mos_degrees(input_mos, step_ratio)
	local input_mos = input_mos or mos.new(5, 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
	
	-- 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 = mossteps_per_period - 2
	local gens_dn_per_period = 1
	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, gens_up_per_period, asc_length, true)
	local des_genchain = mosg.mos_genchain(input_mos, gens_up_per_period, 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
	for j = 1, periods_per_equave do
		local accumulator = 0
		for i = 1, #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 there's already something there, separate with a comma, but
			-- don't add duplicates, which may happen with multi-period mosses
			if degrees[index] == "" then
				degrees[index] = degrees[index] .. degree_name
			elseif degrees[index] ~= degree_name then
				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 generators_up value corresponds to the brightest mode,
	-- unless the mos is 5L 2s, then it's the 2nd-brightest mode
	local generators_up = mossteps_per_equave - periods_per_equave
	if scale_sig == "5L 2s" then
		generators_up = 5
	end
	-- If a value was entered, override the default value
	if string.len(frame.args['Bright Gens Up']) > 0 then
		generators_up = tonumber(frame.args['Bright Gens Up'])
	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, generators_up, step_ratio, note_symbols, chroma_plus_symbol, chroma_minus_symbol)

	-- Get the scale degrees
	local degrees = p.mos_degrees(input_mos, 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")
		
		-- Add row
		result = result .. "|-\n"
		result = result .. "| " .. (i - 1) * step_ratio_gcd .. "\n"
		result = result .. "| " .. cents_per_equave * (i - 1) / (#gamut - 1)
		result = result .. "| " .. degree .. "\n"
		result = result .. "| " .. note_name .. "\n"
	end
	
	result = result .. "|}"
	
end
	
return p