Module:MOS intervals

From Xenharmonic Wiki
Revision as of 23:05, 1 August 2024 by Ganaram inukshuk (talk | contribs) (Removed unused modules)
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 intervals.

This module generates a table showing interval varieties and qualities for a given MOS scale.

Introspection summary for Module:MOS intervals 
Functions provided (3)
Line Function Params
13 _mos_intervals (main) (input_mos, mos_prefix, mos_abbrev, is_collapsed)
140 mos_intervals_description (input_mos, mos_prefix)
187 mos_intervals (invokable) (frame)
Lua modules required (6)
Variable Module Functions used
et Module:ET dependency not used
interval_extension Module:Interval_extension harmonic_entropy_with_lookup_table
mos Module:MOS new
as_string
mode_to_step_matrix
brightest_mode
darkest_mode
equave_step_count
period_step_count
bright_gen_step_count
dark_gen_step_count
interval_eq
interval_to_cents
interval_as_string
period_count
period
interval_mul
parse
rat Module:Rational eq
tamnams Module:TAMNAMS interval_quality
verify_prefix
verify_abbrev
yesno Module:Yesno yesno

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


local mos = require("Module:MOS")
local rat = require("Module:Rational")
local et = require("Module:ET")
local tamnams = require("Module:TAMNAMS")
local interval_extension = require("Module:Interval_extension")
local yesno = require("Module:Yesno")
local p = {}

-- TODO:
-- - Move harmonic entropy to separate module/template pair????

-- Main function; to be called by wrapper
function p._mos_intervals(input_mos, mos_prefix, mos_abbrev, is_collapsed)
	-- Default param for input mos is 5L 2s
	local input_mos = input_mos or mos.new(5, 2, 2)
	local mos_prefix = mos_prefix or "mos"
	local mos_abbrev = mos_abbrev or "m"
	local is_collapsed = is_collapsed == true
	
	-- Get the scale sig
	local scale_sig = mos.as_string(input_mos)
	
	-- Get the brightest and darkest modes as step matrices
	local bright_step_matrix = mos.mode_to_step_matrix(mos.brightest_mode(input_mos))
	local dark_step_matrix = mos.mode_to_step_matrix(mos.darkest_mode(input_mos))
	
	-- Get the number of steps per period and equave
	local equave_step_count = mos.equave_step_count(input_mos)
	local period_step_count = mos.period_step_count(input_mos)
	
	-- Get the step counts for the bright and dark generators
	local bright_gen_step_count = mos.bright_gen_step_count(input_mos)
	local dark_gen_step_count = mos.dark_gen_step_count(input_mos)
	
	-- Setup for harmonic entropy
	local step_ratios = {
		{ 4, 3 },
		{ 3, 2 },
		{ 5, 3 },
		{ 2, 1 },
		{ 5, 2 },
		{ 3, 1 },
		{ 4, 1 },
	}
	
	-- Create the table
	local result = "{| class=\"wikitable mw-collapsible" .. (is_collapsed and " mw-collapsed\"\n" or "\"\n")
	
	-- Create table caption
	result = result
		.. "|+ style=\"font-size: 105%;\" | Intervals of " .. scale_sig .. "\n"
		
	-- Create table headers
	result = result
		.. "! colspan=\"3\" | Intervals"
		.. "!! rowspan=\"2\" | Steps subtended"
		.. "!! rowspan=\"2\" | Range in cents"
		.. "!! rowspan=\"2\" | Average of [[HE]]<br/>(from [http://www.mikebattagliamusic.com/HE-JS/HE.html HE Calc])\n"
		.. "!! rowspan=\"2\" | Min of [[HE]]\n"
		.. "|-\n"	-- Start of second row of header cells
		.. "! Generic"
		.. "!! Specific"
		.. "!! Abbrev.\n"
	
	-- Write each row
	for i = 1, #bright_step_matrix do
		-- Compare the bright and dark intervals. If they're the same, then the
		-- current interval class is a period interval.
		local current_bright_interval = bright_step_matrix[i]
		local current_dark_interval = dark_step_matrix[i]
		local is_period = mos.interval_eq(current_bright_interval, current_dark_interval)
		
		-- If it's a period interval, then there is only one row to write.
		-- Otherwise, there are two rows to write, one for each size.
		if is_period then
			local cents = mos.interval_to_cents(current_bright_interval, input_mos, {1, 1})
			
			result = result .. "|-\n"
				.. string.format("| '''%s-%sstep''' ", i - 1, mos_prefix)
				.. string.format("|| %s " , tamnams.interval_quality(current_bright_interval, input_mos, "sentence-case", mos_prefix))
				.. string.format("|| %s " , tamnams.interval_quality(current_bright_interval, input_mos, "abbrev"       , mos_abbrev))
				.. string.format("|| %s " , mos.interval_as_string(current_bright_interval))
				.. string.format("|| %.1f¢ ", cents)
				.. string.format("|| ~%.4f nats ", interval_extension.harmonic_entropy_with_lookup_table(cents))
				.. string.format("|| ~%.4f nats", interval_extension.harmonic_entropy_with_lookup_table(cents))
		else
			-- Calculate the best and average harmonic entropies
			local he_dark_average = 0
			local he_bright_average = 0
			local he_dark_best = 1000
			local he_bright_best = 1000
			for i = 1, #step_ratios do
				local step_ratio = step_ratios[i]
				local he_dark = interval_extension.harmonic_entropy_with_lookup_table(mos.interval_to_cents(current_dark_interval, input_mos, step_ratio))
				local he_bright = interval_extension.harmonic_entropy_with_lookup_table(mos.interval_to_cents(current_bright_interval, input_mos, step_ratio))
				he_dark_average = he_dark_average + he_dark / #step_ratios
				he_bright_average = he_bright_average + he_bright / #step_ratios
				if he_dark_best > he_dark then
					he_dark_best = he_dark 
				end
				if he_bright_best > he_bright then
					he_bright_best = he_bright 
				end
			end
			
			-- Calculate the cent values min and max for the current intervals
			local sm_min_cents = mos.interval_to_cents(current_dark_interval, input_mos, {1,1})
			local sm_max_cents = mos.interval_to_cents(current_dark_interval, input_mos, {1,0})
			local lg_min_cents = mos.interval_to_cents(current_bright_interval, input_mos, {1,1})
			local lg_max_cents = mos.interval_to_cents(current_bright_interval, input_mos, {1,0})
			
			-- Then sort, as the min and max may be swapped 
			-- This happens if the dark interval has more small steps than large steps
			local dark_interval_range   = string.format("%.1f¢ to %.1f¢", math.min(sm_min_cents, sm_max_cents), math.max(sm_min_cents, sm_max_cents))
			local bright_interval_range = string.format("%.1f¢ to %.1f¢", math.min(lg_min_cents, lg_max_cents), math.max(lg_min_cents, lg_max_cents))
			
			result = result .. "|-\n"
				.. string.format("| rowspan=\"2\" | %s-%sstep ", i - 1, mos_prefix)
				.. string.format("|| %s " , tamnams.interval_quality(current_dark_interval, input_mos, "sentence-case", mos_prefix))
				.. string.format("|| %s " , tamnams.interval_quality(current_dark_interval, input_mos, "abbrev"       , mos_abbrev))
				.. string.format("|| %s " , mos.interval_as_string(current_dark_interval))
				.. string.format("|| %s " , dark_interval_range)
				.. string.format("|| ~%.4f nats " , he_dark_average)
				.. string.format("|| ~%.4f nats\n" , he_dark_best)
				.. "|-\n"
				.. string.format("| %s "  , tamnams.interval_quality(current_bright_interval, input_mos, "sentence-case", mos_prefix))
				.. string.format("|| %s " , tamnams.interval_quality(current_bright_interval, input_mos, "abbrev"       , mos_abbrev))
				.. string.format("|| %s " , mos.interval_as_string(current_bright_interval))
				.. string.format("|| %s " , bright_interval_range)
				.. string.format("|| ~%.4f nats " , he_bright_average)
				.. string.format("|| ~%.4f nats" , he_bright_best)
		end
		result = result .. "\n"
	end
	result = result .. "|}"

	return result
end

function p.mos_intervals_description(input_mos, mos_prefix)
	local input_mos = input_mos or mos.new(5,2)
	local mos_prefix = mos_prefix or "dia"
	local scalesig = mos.as_string(input_mos)
	
	-- How many periods?
	-- What are the period intervals?
	local period_count = mos.period_count(input_mos)
	local period_interval = mos.period(input_mos)
	local period_intervals = {}
	for i = 1, period_count + 1 do
		local interval = mos.interval_mul(period_interval, i-1)
		table.insert(period_intervals, interval)
	end
	
	-- As a sentence piece
	local period_intervals_as_text = ""
	if #period_intervals == 2 then
		period_intervals_as_text = string.format("%s and %s", tamnams.interval_quality(period_intervals[1], input_mos, "none", mos_prefix), tamnams.interval_quality(period_intervals[2], input_mos, "none", mos_prefix))
	else
		for i = 1, period_count do
			period_intervals_as_text = period_intervals_as_text
				.. string.format("%s, ", tamnams.interval_quality(period_intervals[i], input_mos, "none", mos_prefix))
		end
		period_intervals_as_text = period_intervals_as_text
			.. string.format("and %s", tamnams.interval_quality(period_intervals[period_count + 1], input_mos, "none", mos_prefix))
	end
	
	local result = string.format("The intervals of %s are named after the number of mossteps (L and s) they subtend.", scalesig)
	
	local period_interval_desc = ""
	if period_count == 1 then
		period_interval_desc = "the root and " .. (rat.eq(input_mos.equave, 2) and "octave" or "equave")
	else
		period_interval_desc = "the period intervals"
	end
	result = result
		.. string.format(" Each interval, apart from %s (%s), has two varieties, or sizes, each.", period_interval_desc, period_intervals_as_text)
	
	result = result
		.. " Interval varieties are named major and minor for the large and small sizes, respectively"
		.. (input_mos.nL == input_mos.ns and "." or ", and augmented, perfect, and diminished for the scale's generators.")
	
	return result
end

-- Wrapper function; to be called by template
function p.mos_intervals(frame)
	-- Get params
	local scalesig   = frame.args["Scale Signature"]
	local mos_prefix = frame.args["MOS Prefix"]
	local mos_abbrev = frame.args["MOS Abbrev"]
	local collapsed  = yesno(frame.args["Collapsed"], false)
	
	-- Parse scalesig
	local input_mos = mos.parse(scalesig)
	
	-- Verify name/prefix/abbrev
	mos_prefix = tamnams.verify_prefix(input_mos, mos_prefix)
	mos_abbrev = tamnams.verify_abbrev(input_mos, mos_abbrev)

	return p.mos_intervals_description(input_mos, mos_prefix) .. "\n" .. p._mos_intervals(input_mos, mos_prefix, mos_abbrev, is_collapsed)
end

return p