Module:MOS mode degrees

From Xenharmonic Wiki
Jump to navigation Jump to search

Documentation transcluded from /doc
Note: Do not invoke this module directly; use the corresponding template instead: Template:MOS mode degrees.
Icon-Todo.png Todo: Documentation

local mos = require("Module:MOS")
local tip = require("Module:Template input parse")
local tamnams = require("Module:TAMNAMS")
local yesno = require("Module:Yesno")
local p = {}

-- TODO
-- - Split off modmos mode degrees as a separate template

-- Global variables for cell colors
-- Colors are as follows:
-- - Orange and blue for small and large sizes, respectively
-- - Darker colors for altered scale degrees
-- - No color for period intervals
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 periods, including the root and equave
p.cell_color_lg_altered_size = "#BDD7EE"
p.cell_color_large_size      = "#DDEBF7"
p.cell_color_small_size      = "#FCE4D6"
p.cell_color_sm_altered_size = "#F8CBAD"

-- Finds the row color for a single cell
function p.cell_color(interval, input_mos)
	local interval = interval or {["L"] = 3, ["s"] = 1}
	local input_mos = input_mos or mos.new(5, 2)
	
	local period_step_count = mos.period_step_count(input_mos)
	local interval_step_count = mos.interval_step_count(interval)
	local chroma_count = mos.interval_chroma_count(interval, input_mos)
	
	local is_period_interval = interval_step_count % period_step_count == 0
	
	local color = p.cell_color_none
	if is_period_interval then
		if chroma_count > 0 then
			color = p.cell_color_lg_altered_size
		elseif chroma_count == 0 then
			color = p.cell_color_none
		elseif chroma_count < 0 then
			color = p.cell_color_sm_altered_size
		end
	else
		if chroma_count > 0 then
			color = p.cell_color_lg_altered_size
		elseif chroma_count == 0 then
			color = p.cell_color_large_size
		elseif chroma_count == -1 then
			color = p.cell_color_small_size
		elseif chroma_count < -1 then
			color = p.cell_color_sm_altered_size
		end
	end
	return color
end

-- Create a table of a mos's degrees
-- If a step pattern is provided, it's assumed to be that of a modmos
function p._mos_mode_degrees(input_mos, mos_prefix, is_collapsed, step_pattern)
	local is_true_mos = step_pattern == nil
	local input_mos = input_mos or mos.new(5, 2)
	local mos_prefix = mos_prefix or "mos"
	local is_collapsed = is_collapsed == true
	
	-- Get the modes as strings and step vectors
	local step_patterns = {}
	local step_matrices = {}
	if is_true_mos then
		step_patterns = mos.modes_by_brightness(input_mos)
		step_matrices = mos.modes_to_step_matrices(input_mos)
	else
		step_patterns = mos.mode_rotations(step_pattern)
		step_matrices = mos.mode_rotations_to_step_matrices(step_pattern)
	end
	
	-- Get the scale sig
	local scale_sig = mos.as_string(input_mos)

	-- Equave step count; needed for degree column count
	local equave_step_count = mos.equave_step_count(input_mos)
	
	-- Get the brightness (UDP) and rotational orderings (CPOs).
	-- Also produce default mode names if set to do so.
	local udps = {}
	local cpos = {}
	if is_true_mos then
		-- Get UDPs and CPOs
		udps = tamnams.mos_mode_udps(input_mos)
		cpos = tamnams.mos_mode_cpos(input_mos)
	else
		-- Modmos udps require a mosabbrev; this is forced to be "m" since some
		-- abbrevs are tooo long. Get both the names for the closest-bright and
		-- closest-dark mode. If they're the same, only one name will be used;
		-- if not, both are used.
		-- The CPOs of a modmos are simply 1 to n (n is the mode count).
		local udps_closest_bright = tamnams.mode_rotation_udps(step_pattern, input_mos, "m", true)
		local udps_closest_dark = tamnams.mode_rotation_udps(step_pattern, input_mos, "m", false)
		for i = 1, #udps_closest_bright do
			if udps_closest_bright[i] == udps_closest_dark[i] then
				table.insert(udps, udps_closest_bright[i])
			else
				table.insert(udps, string.format("%s<br />%s", udps_closest_bright[i], udps_closest_dark[i]))
			end
			table.insert(cpos, i)
		end
	end
	
	-- Create table
	local result = "{| class=\"wikitable sortable mw-collapsible center-2 center-3"
		.. (is_collapsed and " mw-collapsed\"" or "\"") .. "\n"
	
	-- Table's title
	-- If it's for a modmos, add the step pattern
	local title = string.format("Scale degrees of the modes of %s", scale_sig) .. (is_true_mos and "&nbsp;" or string.format(" (%s)&nbsp;", step_pattern))
	result = result .. "|+ style=\"font-size: 105%; white-space: nowrap;\" | " .. string.format("%s\n", title)
		.. "|-\n"
	
	-- Add table headers for first row
	result = result
		.. "! rowspan=\"2\" | UDP " .. (is_true_mos and "" or " and<br />alterations ")		-- If modmos, add "and alterations" string
		.. "!! rowspan=\"2\" | Cyclic<br>order "
		.. "!! rowspan=\"2\" | Step<br>pattern"
	
	-- Add header for scale degrees
	result = result .. string.format(" !! colspan=\"%d\" class=\"unsortable\" | Scale degree (%sdegree)\n", #step_matrices[1], mos_prefix)
	
	-- Add second row of headers
	result = result .. "|- class=\"unsortable\"\n"
		.. "! 0"
	for i = 1, #step_patterns[1] do
		result = result .. string.format(" !! %d", i)
	end
	
	result = result .. "\n"
	
	-- Add table contents
	for i = 1, #step_patterns do
		result = result .. "|-\n"
		
		-- Add brightness order (as UDP), rotational order, and step pattern
			.. string.format("| %s || %s || %s", udps[i], cpos[i], step_patterns[i])

		-- Add scale degrees with cell coloring
		for j = 1, #step_matrices[i] do
			local current_interval = step_matrices[i][j]
			local degree_quality = tamnams.decode_quality(current_interval, input_mos, "shortened")
			
			local cell_color = p.cell_color(current_interval, input_mos)
			local style_code = cell_color == p.cell_color_none and "" or string.format("style=\"background: %s;\" | ", cell_color)
			
			result = result .. string.format(" || %s%s", style_code, degree_quality)
		end
		result = result .. "\n"
	end
	
	-- End of table
	result = result .. "|}"
	
	return result
end

-- Function to be called as part of a template
function p.mos_mode_degrees(frame)
	-- Get args
	local input_mos    = mos.parse(frame.args["Scale Signature"])
	local mos_prefix   = frame.args["MOS Prefix"]
	local step_pattern = frame.args["MODMOS Step Pattern"]
	local mode_names_unparsed = frame.args["Mode Names"]
	
	-- Get the scale sig; for calculating the mos prefix
	local scale_sig = mos.as_string(input_mos)
	
	-- Verify mosprefix
	mos_prefix = tamnams.verify_prefix(input_mos, mos_prefix)
	
	-- Get the mode names
	local mode_names = nil
	-- Default names for 5L 2s modes and select modmosses.
	-- Names are based on whichever mode is returnd by UDP closest-mode search,
	-- with common names added wherever applicable. Sources include:
	-- - https://www.jazz-guitar-licks.com/ and likely others
	-- - Whatever Wikipedia has cited for the Neapolitan scales
	-- NOTE: these names can be overridden if they don't suffice.
	if scale_sig == "5L 2s" then
		if step_pattern == "LsLLsAs" then
			-- Modes of harmonic minor
			-- Closest-mode search always returns one name
			mode_names = {
				"Harmonic minor<br />(Aeolian ♮7)",
				"Locrian ♮6",
				"Ionian augmented<br />(Ionian ♯5)",
				"Dorian ♯4",
				"Phrygian dominant<br />(Phrygian ♮3)",
				"Lydian ♯2",
				"Altered diminished<br />(Locrian ♭4 𝄫7)",
			}
		elseif step_pattern == "LLsLsAs" then
			-- Modes of harmonic major
			-- Closest-mode search always returns one name
			mode_names = {
				"Harmonic major<br />(Ionian ♭6)",
				"Dorian ♭5",
				"Phrygian ♭4",
				"Lydian ♭3",
				"Mixolydian ♭2",
				"Lydian augmented ♯2<br />(Lydian ♯2 ♯5)",
				"Locrian 𝄫7",
			}
		elseif step_pattern == "LsLLLLs" then
			-- Modes of melodic minor
			-- Closest-mode search sometimes returns two names
			mode_names = {
				"Melodic minor<br />(Ionian ♭3, Dorian ♮7)",
				"Dorian ♭2, Phrygian ♮6",
				"Lydian augmented<br />(Lydian ♯5)",
				"Lydian dominant<br />(Lydian ♭7, Mixolydian ♯4)",
				"Mixolydian ♭6, Aeolian ♮3",
				"Half-diminished<br />(Aeolian ♭5, Locrian ♮2)",
				"Altered, Altered dominant<br />(Locrian ♭4)",
			}
		elseif step_pattern == "sLLLLLs" then
			-- Modes of Neapolitan major
			-- Closest-mode search sometimes returns two names
			mode_names = {
				"Neapolitan major<br />(Ionian ♭2 ♭3, Phrigian ♮6 ♮7)",
				"Lydian augmented ♯6<br />(Lydian ♯5 ♯6)",
				"Lydian augmented dominant<br />(Lydian ♯5 ♭7, Mixolydian ♯4 ♯5)",
				"Lydian minor<br />(Lydian ♭6 ♭7, Aeolian ♮3 ♯4)",
				"Major locrian<br />(Mixolydian ♭5 ♭6, Locrian ♮2 ♮3)",
				"Altered dominant ♮2<br />(Aeolian ♭4 ♭5, Locrian ♮2, ♭4)",
				"Altered dominant 𝄫3<br />(Locrian 𝄫3 ♭4)",
			}
		elseif step_pattern == "sLLLsAs" then
			-- Modes of Neapolitan minor
			-- Closest-mode search always returns one name
			mode_names = {
				"Neapolitan minor<br />(Phrygian ♮7)",
				"Lydian ♯6",
				"Mixolydian augmented<br />(Mixolydian ♯5)",
				"Aeolian ♯4",
				"Locrian dominant<br />(Locrian ♮3)",
				"Ionian ♯2",
				"Altered diminished 𝄫3<br />(Locrian 𝄫3 ♭4 𝄫7)",
			}
			elseif step_pattern == "sAsLsAs" then
			-- Modes of double harmonic
			-- Closest-mode search sometimes returns two names
			mode_names = {
				"Double harmonic<br />(Ionian ♭2 ♭6, Phrygian ♮3 ♮7)",
				"Lydian ♯2 ♯6",
				"Altered ♮5 𝄫6<br />(Phrygian ♭4 𝄫7)",
				"Double harmonic minor<br />(Lydian ♭3 ♭6, Aeolian ♯4 ♮7)",
				"Mixolydian ♭2 ♭5, Locrian ♮3 ♮6",
				"Ionian augmented ♯2<br />(Ionian ♯2 ♯5)",
				"Locrian 𝄫3 𝄫7",
			}
		elseif #step_pattern == 0 then
			-- True-mos modes
			mode_names = { 
				"Lydian",
				"Ionian (major)",
				"Mixolydian",
				"Dorian",
				"Aeolian (minor)",
				"Phrygian",
				"Locrian"
			}
		end
	end
	
	-- If mode names are given, use those instead
	-- If using default mode names (scalesig+udp), those names are auto-added by the relevant function
	local use_default_names = false
	if #mode_names_unparsed ~= 0 then
		if mode_names_unparsed == "Default" then
			use_default_names = true
		else
			mode_names = tip.parse_entries(mode_names_unparsed)
		end
	end
	
	-- Check if the table should start collapsed
	local is_collapsed = yesno(frame.args["Collapsed"], false)
	
	-- If a modmos step pattern was never provided, call the function mos_mode_degrees
	-- Otherwise, call the function modmos_mode_degrees
	local result = ""
	if step_pattern == "" then
		result = p._mos_mode_degrees(input_mos, mos_prefix, is_collapsed)
	--elseif #step_pattern == input_mos.nL + input_mos.ns then
	else
		result = p._mos_mode_degrees(input_mos, mos_prefix, is_collapsed, step_pattern)
	end
	
	return result
end

return p