Module:MOS tunings

From Xenharmonic Wiki
Revision as of 07:08, 28 August 2024 by Ganaram inukshuk (talk | contribs)
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 tunings.

This module displays the cent values of a MOS scale's degrees under various tunings, or step ratios, as well as the nearest JI ratios represented by those scale degrees.

Introspection summary for Module:MOS tunings 
Functions provided (8)
Line Function Params
19 capitalize_first (text)
25 sort_step_ratios (step_ratios)
52 ji_ratio_search (integer_limit)
85 preprocess_step_ratios (step_ratios)
129 preprocess_ji_ratios (input_mos, modal_union, step_ratios, ji_ratios)
174 _mos_tunings (main) (input_mos, mos_prefix, mos_abbrev, step_ratios, ji_ratios)
271 mos_tunings (invokable) (frame)
290 tester none
Lua modules required (6)
Variable Module Functions used
et Module:ET as_string
mos Module:MOS interval_to_cents
new
modal_union
as_string
mos_to_et
interval_to_et_steps
parse
rat Module:Rational cents
tamnams Module:TAMNAMS find_step_ratio_range_for_ratio_pair
lookup_step_ratio
degree_quality
verify_prefix
verify_abbrev
tip Module:Template input parse parse_numeric_pairs
yesno Module:Yesno yesno

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


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

-- Rewritten/simplified module replacement for Module:MOS degrees
-- A new template is chosen because it's a better name than the old one and is
-- far easier to maintain than the old one.

-- TODO:
-- - JI ratio search and input
-- - Diatonic interval category lookup?!!

-- Helper function
-- Capitalizes the first character of a string
function p.capitalize_first(text)
	return string.upper(string.sub(text, 1, 1)) .. string.sub(text, 2, -1)
end

-- Helper function
-- Sorts step ratios L:s by their hardnesses
function p.sort_step_ratios(step_ratios)
	if #step_ratios < 2 then
		return step_ratios
	end
	
	-- Sort using selection sort, which is ok for smol datasets.
	for i = 1, #step_ratios - 1 do
		local index_of_smallest = i
		local current_val = step_ratios[i][1] / step_ratios[i][2]
		
		-- Find the ratio with the smallest hardness
		for j = i + 1, #step_ratios do
			if (step_ratios[j][1] / step_ratios[j][2] < current_val) then
				index_of_smallest = j
			end
		end
		
		if index_of_smallest ~= i then
			local temp = step_ratios[index_of_smallest]
			step_ratios[index_of_smallest] = step_ratios[i]
			step_ratios[i] = temp
		end
	end
	return step_ratios
end

-- Find JI ratios up to an integer limit via mediants
function p.ji_ratio_search(integer_limit)
	local SEARCH_MAX = 120
	local integer_limit = integer_limit or SEARCH_MAX
	
	integer_limit = math.max(0, math.min(SEARCH_MAX, integer_limit))
	
	local ratios = {{1,1}, {2,1}}
	
	local new_ratios_added = true
	while new_ratios_added do
		new_ratios_added = false
		local new_ratios = {}
		for i = 1, #ratios-1 do
			local ratio_1 = ratios[i]
			local ratio_2 = ratios[i+1]
			local mediant = {ratio_1[1]+ratio_2[1], ratio_1[2]+ratio_2[2]}
			local int_max = math.max(mediant[1], mediant[2])
			
			table.insert(new_ratios, ratios[i])
			if int_max <= integer_limit then
				new_ratios_added = true
				table.insert(new_ratios, mediant)
			end
		end
		table.insert(new_ratios, ratios[#ratios])
		ratios = new_ratios
	end
	
	return ratios
end

-- Helper function
-- Finds the step ratio range and sorts step ratios
function p.preprocess_step_ratios(step_ratios)
	local step_ratios = p.sort_step_ratios(step_ratios)
	local step_ratio_range = ""
	
	-- If the step ratios are 3/2, 2/1, and 3/1 in that order, then they are
	-- the simple step ratios: basic, hard, and soft.
	-- These should not be sorted, since the basic-hard-soft sorting is a little
	-- more intuitive than sorting by hardness.
	if #step_ratios == 3 then
		if step_ratios[1][1] == 3 and step_ratios[1][2] == 2
			and step_ratios[2][1] == 2 and step_ratios[2][2] == 1
			and step_ratios[3][1] == 3 and step_ratios[3][2] == 1 then
			return "Simple Tunings", {{2,1}, {3,1}, {3,2}}
		end
	end
	
	-- If there are multiple step ratios, find the step ratio range it
	-- corresponds to. If there is one step ratio, find the name of that
	-- hardness. If there are zero step ratios, then return "Tunings"
	if #step_ratios > 1 then
		local lower_ratio = step_ratios[1]
		local upper_ratio = step_ratios[#step_ratios]
		
		step_ratio_range = tamnams.find_step_ratio_range_for_ratio_pair(lower_ratio, upper_ratio)
		if step_ratio_range ~= nil then
			step_ratio_range = p.capitalize_first(step_ratio_range) .. " Tunings"
		else
			step_ratio_range = "Tunings"
		end
	elseif #step_ratios == 1 then
		step_ratio_range = tamnams.lookup_step_ratio(step_ratios[1])
		if step_ratio_range ~= nil then
			step_ratio_range = p.capitalize_first(step_ratio_range) .. " Tuning"
		else
			step_ratio_range = string.format("%s/%s", step_ratios[1][1], step_ratios[1][2]) .. " Tuning"
		end
	else
		step_ratio_range = "Tunings"
	end
	
	return step_ratio_range, step_ratios
end

-- Preprocess step ratios
function p.preprocess_ji_ratios(input_mos, modal_union, step_ratios, ji_ratios)

	local avg_step_ratio = {0, 0}
	for i = 1, #step_ratios do
		avg_step_ratio[1] = avg_step_ratio[1] + step_ratios[i][1]
		avg_step_ratio[2] = avg_step_ratio[2] + step_ratios[i][2]
	end
	avg_step_ratio[1] = avg_step_ratio[1] / #step_ratios
	avg_step_ratio[2] = avg_step_ratio[2] / #step_ratios
	avg_step_ratio[1] = avg_step_ratio[1] / avg_step_ratio[2]
	avg_step_ratio[2] = avg_step_ratio[2] / avg_step_ratio[2]
	
	local cent_values = {}
	for i = 1, #modal_union do
		table.insert(cent_values, mos.interval_to_cents(modal_union[i], input_mos, avg_step_ratio))
	end
	
	local tolerance = (rat.cents(input_mos.equave) / (input_mos.nL * avg_step_ratio[1] + input_mos.ns * avg_step_ratio[2])) * 0.25
	
	local sorted_ratios = {}
	local curr_index = 1		-- Index of current_ratio
	for i = 1, #cent_values do
		local lower_bound = cent_values[i] - tolerance
		local upper_bound = cent_values[i] + tolerance
		local cents_within_range = true
		local curr_ratios = {}
		
		for j = curr_index, #ji_ratios do
			local curr_ratio = ji_ratios[j]
			local curr_cents = math.log(curr_ratio[1]/curr_ratio[2])/math.log(2) * 1200
			
			if lower_bound < curr_cents and curr_cents < upper_bound then
				table.insert(curr_ratios, curr_ratio)
			--elseif curr_cents > upper_bound then
			--	curr_index = j
			--	break
			end
		end
		
		table.insert(sorted_ratios, curr_ratios)
	end
	return sorted_ratios
end

-- Main function
function p._mos_tunings(input_mos, mos_prefix, mos_abbrev, step_ratios, ji_ratios)
	local input_mos = input_mos or mos.new(5,2)
	local mos_prefix = mos_prefix or "mos"
	local mos_abbrev = mos_abbrev or "m"
	local step_ratios = step_ratios or { {2,1}, {3,1}, {3,2} }
	local ji_ratios = ji_ratios or p.ji_ratio_search(40)
	
	local modal_union = mos.modal_union(input_mos)
	local scale_sig = mos.as_string(input_mos)
	
	-- Sort/preprocess step ratios
	local step_ratio_range = ""
	step_ratio_range, step_ratios = p.preprocess_step_ratios(step_ratios)
	
	-- Preprocess JI ratios
	local sorted_ji_ratios = p.preprocess_ji_ratios(input_mos, modal_union, step_ratios, ji_ratios)
	
	-- Create table
	local result = "{| class=\"wikitable sortable right-all left-1 left-2\"\n"
	
	-- Table caption
	result = result	.. string.format("|+ style=\"font-size: 105%%; white-space: nowrap;\" | %s of %s\n", step_ratio_range, scale_sig)
	
	-- First row of headers
	-- First two headers span two rows
	result = result .. "! rowspan=\"2\" class=\"unsortable\" | Scale degree\n"
	result = result .. "! rowspan=\"2\" class=\"unsortable\" | Abbrev.\n"
	
	-- Headers for tunings; these span two cols
	for i = 1, #step_ratios do
		local step_ratio_as_text = tamnams.lookup_step_ratio(step_ratios[i])
		if step_ratio_as_text == nil then
			step_ratio_as_text = string.format("%s:%s", step_ratios[i][1], step_ratios[i][2])
		else
			step_ratio_as_text = p.capitalize_first(step_ratio_as_text) .. string.format(" (%s:%s)", step_ratios[i][1], step_ratios[i][2])
		end
		
		local et_as_string = et.as_string(mos.mos_to_et(input_mos, step_ratios[i]))
		local header_text = string.format("%s<br>[[%s]]", step_ratio_as_text, et_as_string)
		result = result .. string.format("! colspan=\"2\" | %s\n", header_text)
	end
	
	-- Headers for JI ratios; this spans two rows
	result = result .. "! rowspan=\"2\" class=\"unsortable\" | Approx.<br>Ratios\n"
	result = result .. "|-\n"
	
	-- Second row of headers
	for i = 1, #step_ratios do
		result = result .. "! Steps\n"
		result = result .. "! Cents\n"
	end
	
	-- Add a row for each scale degree
	for i = 1, #modal_union do
		local interval = modal_union[i]
		
		-- Add cells for the degree names
		local degree_name   = tamnams.degree_quality(interval, input_mos, "sentence-case", mos_prefix)
		local degree_abbrev = tamnams.degree_quality(interval, input_mos, "abbrev"       , mos_abbrev)
		
		result = result
			.. "|-\n"
			.. string.format("| %s || %s", degree_name, degree_abbrev)
	
		-- Add cells for each interval's tunings
		for j = 1, #step_ratios do
			local step_ratio = step_ratios[j]
			local step_count = mos.interval_to_et_steps(interval, step_ratio)
			local cents = mos.interval_to_cents(interval, input_mos, step_ratio)
			
			result = result
				.. " "
				.. string.format("|| %s\\%s || %.1f", step_count, input_mos.nL * step_ratio[1] + input_mos.ns * step_ratio[2], cents)
		end
		
		-- Add cells for JI ratios
		-- TESTING: print only the unison and equave
		local ratios = ""
		for j = 1, #sorted_ji_ratios[i] do
			if j ~= #sorted_ji_ratios[i] then
				ratios = ratios .. string.format("%s/%s, ", sorted_ji_ratios[i][j][1], sorted_ji_ratios[i][j][2])
			else
				ratios = ratios .. string.format("%s/%s", sorted_ji_ratios[i][j][1], sorted_ji_ratios[i][j][2])
			end
		end
		result = result .. string.format("|| %s", ratios)
		
		result = result .. "\n"
	end
	
	-- End of table
	result = result .. "|}"
	
	return result
end

-- Wrapper function; to be called by template
function p.mos_tunings(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)		-- Currently does nothing
	local step_ratios = tip.parse_numeric_pairs(frame.args["Step Ratios"]) or (frame.args["Step Ratios"] == "Central Spectrum" and {{4,3}, {3,2}, {5,3}, {2,1}, {5,2}, {3,1}, {4,1}} or {{2,1}, {3,1}, {3,2}})
	local ji_ratios   = tip.parse_numeric_pairs(frame.args["JI Ratios"]  ) or {{1,1}, {3,2}, {2,1}}
	
	-- 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_tunings(input_mos, mos_prefix, mos_abbrev, step_ratios)
end

function p.tester()
	local range, ratios = p.preprocess_step_ratios({ {7,1}, {3,1}, {2,1} })
	local ji_ratios = p.ji_ratio_search(50)
	
	return p.preprocess_ji_ratios(mos.new(5,2), mos.modal_union(mos.new(5,2)), {{3,2}}, ji_ratios)
end

return p