Module:MOS notation

Revision as of 04:26, 23 June 2023 by Ganaram inukshuk (talk | contribs) (Separated functions from mos degrees module into a helper module)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Module documentation[view] [edit] [history] [purge]
This module primarily serves as a library for other modules and has no corresponding template.


Introspection summary for Module:MOS notation 
Functions provided (6)
Line Function Params
10 parse_step_ratio (step_ratio_unparsed)
23 simplify_step_ratio (step_ratio_unsimplified)
36 parse_udp (step_ratio_unparsed)
51 mosstep_and_chroma_to_note_name (mossteps, chromas, note_symbol, chroma_symbol)
65 mosstep_and_quality_to_degree (mossteps, quality)
100 mos_degrees (input_mos, genchain_length_per_period, going_up)
Lua modules required (3)
Variable Module Functions used
mos Module:MOS bright_gen
mosg Module:MOS gamut dependency not used
rat Module:Rational gcd

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


-- This is a helper module that contains commonly used functions for:
-- - MOS degrees
-- - MOS gamut
local mos = require('Module:MOS')
local mosg = require('Module:MOS gamut')	-- TODO: move gamut function to here
local rat = require('Module:Rational')
local p = {}

-- Helper function for parsing a step ratio entered as a string "p/q"
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
function p.simplify_step_ratio(step_ratio_unsimplified)
	
	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

-- 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 that converts a note name given as a quantity of mossteps
-- and chromas (see gamut function) into a name, such as "C#"
-- To be used in conjunction with the genchain function
function p.mosstep_and_chroma_to_note_name(mossteps, chromas, note_symbol, chroma_symbol)
	
	local note_name = note_symbol .. string.rep(chroma_symbol, math.abs(chromas))
	return note_name
end

-- Helper function that converts a scale degree given as a quantity of mossteps
-- and a numeric quality (0=perf, 1=maj, -1=min, 2=aug, -2=dim, etc) into a
-- scale degree
-- To be used in conjunction with the degrees function
-- TODO: add options to change naming and enumeration scheme; options include:
-- - Abbreviations (Major/Minor vs Maj/Min vs M/m)
-- - TAMNAMS indexing vs regular indexing (0-mossteps vs mos-1st)
-- - Ability to pass in a prefix (default is "mos")
function p.mosstep_and_quality_to_degree(mossteps, quality)
	
	local degree_name = mossteps .. "-mosstep"
	if quality == 0 then
		degree_name = "Perf. " .. degree_name
	elseif quality == 1 then
		degree_name = "Maj. " .. degree_name
	elseif quality == 2 then
		degree_name = "Aug. " .. degree_name
	elseif quality > 2 then
		degree_name = (quality - 1) .. "× aug. " .. degree_name
	elseif quality == -1 then
		degree_name = "Min. " .. degree_name
	elseif quality == -2 then
		degree_name = "Dim. " .. degree_name
	elseif quality < -2 then
		degree_name = (math.abs(quality) - 1) .. "× dim. " .. degree_name
	end
	
	return degree_name
end

-- Function that produces a chain of scale degrees. What scale degrees are
-- reached by stacking a generator?
-- (EG, major 2nd, augmented 2nd, etc)
-- This function only works one direction at a time, so it's necessary to call
-- it twice, one for each direction.
-- Quality encodes maj/min/aug/perf/dim numerically:
-- -  3 = 2x augmented
-- -  2 = 1x augmented
-- -  1 = major
-- -  0 = perfect (used for generators and root)
-- - -1 = minor
-- - -2 = 1x diminished
-- - -3 = 2x diminished
function p.mos_degrees(input_mos, genchain_length_per_period, going_up)
	-- Default parameters for testing
	--[[
	local input_mos = input_mos or mos.new(5, 2, 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']
	local mossteps_per_dark_gen = mossteps_per_period - mossteps_per_bright_gen
	
	local degreechain = {}
	for j = 1, periods_per_equave do
		local chain_for_period = {}

		for i = 1, genchain_length_per_period do
			
			-- Calculate mossteps
			local mossteps = 0
			if going_up then
				mossteps = (i - 1) * mossteps_per_bright_gen % mossteps_per_period + (j - 1) * mossteps_per_period
			else
				mossteps = (i - 1) * mossteps_per_dark_gen % mossteps_per_period + (j - 1) * mossteps_per_period
			end

			-- Calculate quality
			-- The first two elements in the chain are always perfect
			-- All intervals after that are major (or minor if going down)
			-- After the major intervals are augmented intervals, which starts
			-- with the augmented dark generator, which comes before the
			-- augmented unison. (or minor and dim bright gen if going down)
			-- For nL ns mosses, generators are major and minor instead, so only
			-- the root is perfect
			local quality = 0
			if input_mos.nL ~= input_mos.ns then
				if i == 1 or i == 2 then
					quality = 0
				else
					-- Offsetting i by +1 will make it so the dark generator
					-- before the augmented unison is denoted as augmented,
					-- but lua's start-from-1 indexing offsets it by 1 already.
					quality = math.floor(i / mossteps_per_period) + 1
					if not going_up then
						quality = quality * -1
					end
				end
			else
				if i == 1 then
					quality = 0
				else
					quality = math.floor((i + 1) / mossteps_per_period)
					if not going_up then
						quality = quality * -1
					end
				end
			end
			
			-- Put together the name
			local degree = { ['mossteps'] = mossteps, ['quality'] = quality }
			table.insert(chain_for_period, degree)
		end
		table.insert(degreechain, chain_for_period)
	end
	
	return degreechain
end

return p