Module:MOS gamut

From Xenharmonic Wiki
Revision as of 08:08, 6 June 2023 by Ganaram inukshuk (talk | contribs) (Created page with "local mos = require('Module:MOS') local rat = require('Module:Rational') local mosm = require('Module:MOS modes') local p = {} -- Helper function for creating a genchain, a s...")
(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 gamut.

This module produces a gamut (sequence of note names with accidentals) for an edo.

Introspection summary for Module:MOS gamut 
Functions provided (4)
Line Function Params
25 mos_genchain (input_mos, genchain_init, genchain_length, note_symbols, chroma_symbol, going_up)
104 mos_gamut (input_mos, generators_up, step_ratio, note_symbols, chroma_plus_symbol, chroma_minus_symbol)
241 parse_entries (unparsed)
249 mos_gamut_frame (invokable) (frame)
Lua modules required (3)
Variable Module Functions used
mos Module:MOS bright_gen
new
as_string
mosm Module:MOS modes dependency not used
rat Module:Rational gcd

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


local mos = require('Module:MOS')
local rat = require('Module:Rational')
local mosm = require('Module:MOS modes')
local p = {}

-- Helper function for creating a genchain, a sequence of named pitches where consecutive
-- pitches are a generator apart. This can only work in one direction at a time, so it's
-- necessary to call this twice if both an ascending and descending chain are needed. For
-- a multi-period mos, multiple genchains are returned as an array of arrays, where each
-- array has indices denote the number of generators going up (or down) and the element
-- denote the named pitch. For the single-period case, it's a size-1 array whose element
-- is a single genchain.
-- As an example, 12edo standard notation is this: 1 is G, 2 is D, 3 is A, 4 is E, etc;
-- for the descending chain, -1 is F, -2 is Bb, -3 is Eb, etc. This example is returned
-- as positive numbers, so it may also be interpreted as dark gens going up instead.
-- Parameters:
-- - input_mos - the mos itself represented as a data structure from Module:MOS
-- - genchain_init - how many named pitches per period are there without accidentals added?
--   This is either the value u or d for the UDP of up|dp.
-- - genchain_length - how many generators should the genchain extend after the root?
-- - note_symbols - the note names entered as a string, such as "CDEFGAB"
-- - chroma_symbol - the symbol for the chroma used, such as "#" (for ascending chain) or
--   "b" (for descending chain)
-- - going_up - bool; whether the genchain is going up or down; true for up, false for down
function p.mos_genchain(input_mos, genchain_init, genchain_length, note_symbols, chroma_symbol, going_up)
	-- Default parameters for testing
	--[[
	local input_mos = input_mos or mos.new(5, 2, 2)
	local genchain_init = genchain_init or 5
	local genchain_length = genchain_length or 10
	local note_symbols = note_symbols or "CDEFGAB"
	local chroma_symbol = chroma_symbol or "#"
	local going_up = going_up or true
	]]--
	
	-- Get the number of mossteps per period and equave
	local mossteps_per_equave = input_mos.nL + input_mos.ns
	local periods = rat.gcd(input_mos.nL, input_mos.ns)
	local mossteps_per_period = mossteps_per_equave / periods
	
	-- Split the note symbols string into subsets
	-- This is only necessary if the mos is multi-period
	local note_subsets = {}
	for i = 1, periods do
		local start_index = (i - 1) * mossteps_per_period + 1
		local stop_index = i * mossteps_per_period
		local substr = string.sub(note_symbols, start_index, stop_index)
		table.insert(note_subsets, substr)
	end
	
	-- Create the genchain for each period
	local genchains = {}
	for i = 1, periods do
		local note_names = note_subsets[i]
		
		-- Get the size of the generator in mossteps
		local gen = mos.bright_gen(input_mos)
		local gen_in_mossteps = gen['L'] + gen['s']
		
		-- If the genchain is descending (ie, going_up is false), switch to
		-- using the dark gen in mossteps, which is the period complement
		-- of the bright gen; going up by the dark gen is the same as going
		-- down by the bright gen
		if not going_up then
			gen_in_mossteps = mossteps_per_period - gen_in_mossteps
		end
		
		-- Use this value, with modular arithmteic, as an index to get the note name
		local accumulator = 0
		
		-- Create a genchain that initially starts at the root
		local root = string.sub(note_names, 1, 1)
		local genchain = { root }
		for j = 1, genchain_length do
			-- Increment the index by the generator
			accumulator = accumulator + gen_in_mossteps
			
			-- Convert the accumulator into an index, then add 1 because
			-- lua indexing
			local index = accumulator % mossteps_per_period + 1
			
			-- Get the note name
			local note_name = string.sub(note_names, index, index)
			
			-- Add accidentals
			local accidentals_to_add = 0
			if j > genchain_init then
				accidentals_to_add = math.ceil((j - genchain_init) / mossteps_per_period)
			end
			note_name = note_name .. string.rep(chroma_symbol, accidentals_to_add)
			
			-- Add the note name
			table.insert(genchain, note_name)
		end
		
		-- Add the genchain
		table.insert(genchains, genchain)
	end
	
	return genchains
end

-- Function that produces a gamut, a sequence of note names with accidentals, for an edo
function p.mos_gamut(input_mos, generators_up, step_ratio, note_symbols, chroma_plus_symbol, chroma_minus_symbol)
	-- Default parameters for input mos and step ratio (5L 2s and 2:1 step ratio)
	local input_mos = input_mos or mos.new(2, 5, 2)
	local step_ratio = step_ratio or { 2, 1 }
	
	-- Get the number of mossteps per period and equave
	local mossteps_per_equave = input_mos.nL + input_mos.ns
	local periods = rat.gcd(input_mos.nL, input_mos.ns)
	local mossteps_per_period = mossteps_per_equave / periods
	
	-- 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 = generators_up or mossteps_per_equave - periods
	local scale_sig = mos.as_string(input_mos)
	if scale_sig == "5L 2s" then
		generators_up = 5
	end
	
	-- The natural note symbols are those that correspond to diamond-mos
	-- (JKLMN...) unless the mos is 5L 2s, then it's CDEFGAB
	-- If it's diamond-mos, gamut is limited to 17 note names
	local note_symbols_main = "JKLMNOPQRSTUVWXYZ"
	local note_symbols = note_symbols or string.sub(note_symbols_main, 1, mossteps_per_equave)
	if scale_sig == "5L 2s" then
		note_symbols = "CDEFGAB"
	end
	
	-- The default accidentals are the amp and at (& and @)
	-- unless the mos is 5L 2s, then it's sharp and flat (# and b)
	local chroma_plus_symbol = chroma_plus_symbol or "&"
	local chroma_minus_symbol = chroma_minus_symbol or "@"
	if scale_sig == "5L 2s" then
		chroma_plus_symbol = "#"
		chroma_minus_symbol = "b"
	end
	
	-- For a mos nxL nys with a given step ratio p/q, the gamut is such that every accidental
	-- note (what would be the black keys) has at least one name within the edo nxp+nyq.
	-- Start with the mode defined by the udp un|dn and, starting at the root (usu. C or J),
	-- construct two generator chains that goes up u generators and d generators. For a
	-- multi-period mos with n periods, there needs to be a separate genchain pair per period
	-- for n genchain pairs.
	
	-- Reconstruct the UDP up|dp (u times p pipe d times p)
	-- The generators_up corresponds to up and is given to us, so generators_down should
	-- be reconstructed to correspond to dp; dividing either generators_up or generators_down
	-- by the number of periods will give the number of generators per period (u and d by
	-- themselves)
	local generators_down = mossteps_per_equave - generators_up - periods
	
	-- How long is the inital genchain for notes without accidentals?
	local gens_up_per_period = generators_up / periods
	local gens_down_per_period = generators_down / periods
	
	-- How long is the genchain extended for per period?
	-- For nxL nys with a step ratio 2:1, extend the genchains by x generators for each period.
	-- For any other step ratio p:q, extend by x(p-2)+y(q-1) gens per genchain per period.
	-- If the step ratio p/q is such that p and q are not coprime and share a common
	-- factor k, then the gamut produced is one of several gamuts shifted by up to k-1
	-- edosteps up or down. For simplicity, this isn't included so the gamut will be that for
	-- a step ratio p/q rather than kp/kq.
	local kp = step_ratio[1]
	local kq = step_ratio[2]
	local k = rat.gcd(kp, kq)
	local num = kp / k
	local den = kq / k
	
	-- How many large and small steps per period?
	local x = input_mos.nL / periods		-- Large step count
	local y = input_mos.ns / periods		-- Small step count
	
	-- How many esteps are in the equave? Gamut does not include any notes reached by
	-- increments smaller than a chroma, so if the step ratio is not simplified, the
	-- gamut will be for a simplified step ratio
	local estedps_per_equave = input_mos.nL * num + input_mos.ns * den
	
	-- Similarly, how many esteps per period?
	local estedps_per_period = x * num + y * den
	
	-- How long should the genchain extend after the initial genchain?
	-- For a basic step ratio 2:1, extend by x
	-- For any other ratio p:q (simplified), do this calculation:
	-- x*floor(p/2) + y*floor(q/2)
	local genchain_extend = 0
	if num / den == 2 then
		genchain_extend = x
	else
		genchain_extend = x * math.floor(num/2) + y * math.floor(den/2)
	end
	
	-- How long are the genchains?
	local ascending_genchain_length = gens_up_per_period + genchain_extend
	local descending_genchain_length = gens_down_per_period + genchain_extend
	
	-- Get the ascending and descending genchains
	local ascending_genchain = p.mos_genchain(input_mos, gens_up_per_period, ascending_genchain_length, note_symbols, chroma_plus_symbol, true)
	local descending_genchain = p.mos_genchain(input_mos, gens_down_per_period, descending_genchain_length, note_symbols, chroma_minus_symbol, false)
	
	-- Create an empty gamut
	local gamut = {}
	for i = 1, estedps_per_equave + 1 do
		table.insert(gamut, "")
	end
	
	-- 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 = estedps_per_period - esteps_per_bright_gen
	
	-- Add the notes to the gamut
	-- Notes with sharps (or diamond-mos equivalent) take precedence
	for j = 1, periods do
		local bright_accumulator = 0
		for i = 1, #ascending_genchain[j] do
			local index = (bright_accumulator % estedps_per_period) + (j - 1) * estedps_per_period + 1
			gamut[index] = gamut[index] .. ascending_genchain[j][i]
			bright_accumulator = bright_accumulator + esteps_per_bright_gen
		end
		local dark_accumulator = esteps_per_dark_gen
		for i = 2, #descending_genchain[j] do
			local index = (dark_accumulator % estedps_per_period) + (j - 1) * estedps_per_period + 1
			if gamut[index] ~= "" then
				gamut[index] = gamut[index] .. "/" .. descending_genchain[j][i]
			else
				gamut[index] = gamut[index] .. descending_genchain[j][i]
			end
			dark_accumulator = dark_accumulator + esteps_per_dark_gen
		end
	end
	
	-- Last note in the gamut is the root up one equave
	gamut[#gamut] = ascending_genchain[1][1]
	
	return gamut
end

-- Helper function that parses entries from a semicolon-delimited string and returns them in an array
function p.parse_entries(unparsed)
	local parsed = {}
	for entry in string.gmatch(unparsed, '([^/]+)') do
		table.insert(parsed, entry)		-- Add to array
	end
	return parsed
end

function p.mos_gamut_frame(frame)
	-- Default parameters for input mos and step ratio (5L 2s and 2:1 step ratio)
	local input_mos = frame.args['Scale Signature'] or mos.new(2, 5, 2)
	local step_ratio_unparsed = frame.args['Step Ratio'] 
	local step_ratio = p.parse_entries(step_ratio_unparsed) or { 2, 1 }
	
	-- Get the number of mossteps per period and equave
	local mossteps_per_equave = input_mos.nL + input_mos.ns
	local periods = rat.gcd(input_mos.nL, input_mos.ns)
	local mossteps_per_period = mossteps_per_equave / periods
	
	-- 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 = frame.args['Bright Gens Up'] or mossteps_per_equave - periods
	local scale_sig = mos.as_string(input_mos)
	if scale_sig == "5L 2s" then
		generators_up = 5
	end
	
	-- The natural note symbols are those that correspond to diamond-mos
	-- (JKLMN...) unless the mos is 5L 2s, then it's CDEFGAB
	-- If it's diamond-mos, gamut is limited to 17 note names
	local note_symbols_main = "JKLMNOPQRSTUVWXYZ"
	local note_symbols = frame.args['Note Symbols'] or string.sub(note_symbols_main, 1, mossteps_per_equave)
	if scale_sig == "5L 2s" then
		note_symbols = "CDEFGAB"
	end
	
	-- The default accidentals are the amp and at (& and @)
	-- unless the mos is 5L 2s, then it's sharp and flat (# and b)
	local chroma_plus_symbol = frame.args['Sharp Symbol'] or "&"
	local chroma_minus_symbol = frame.args['Flat Symbol'] or "@"
	if scale_sig == "5L 2s" then
		chroma_plus_symbol = "#"
		chroma_minus_symbol = "b"
	end
	
	-- Get the gamut
	local gamut = p.mos_gamut(input_mos, generators_up, step_ratio, note_symbols, chroma_plus_symbol, chroma_minus_symbol)
	
	-- Format the gamut as a table
	local result = '{| class="wikitable"\n'
	local result = result .. "|-\n"
	for i = 1, #gamut do
		local note_name = gamut[i]
		-- If note name string is one character, it's a natural so the cell is white
		-- For anything else, the cell is black (actually gray) to mimic a piano
		if string.len(note_name) == 1 then
			result = result .. '|bgcolor="white"|'.. note_name .. "\n"
		else
			result = result .. '|bgcolor="gray"|'.. note_name .. "\n"
		end
	end
	result = result .. "|}"
	
	return result
end

return p