Module:Scale tree: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Ganaram inukshuk (talk | contribs)
m Headers
Ganaram inukshuk (talk | contribs)
Moved step ratio ranges to hardness column, added back default comment for which step ratios are proper
Line 77: Line 77:
equave_suffix = rat.as_ratio(input_mos.equave)
equave_suffix = rat.as_ratio(input_mos.equave)
end
end
-- Default comments for TAMNAMS-named step ratios
local default_comments = {}
default_comments["1/1"] = "Equalized " .. MOS.as_string(input_mos)
default_comments["4/3"] = "Supersoft " .. MOS.as_string(input_mos)
default_comments["3/2"] = "Soft " .. MOS.as_string(input_mos)
default_comments["5/3"] = "Semisoft " .. MOS.as_string(input_mos)
default_comments["2/1"] = "Basic " .. MOS.as_string(input_mos)
default_comments["5/2"] = "Semihard " .. MOS.as_string(input_mos)
default_comments["3/1"] = "Hard " .. MOS.as_string(input_mos)
default_comments["4/1"] = "Superhard " .. MOS.as_string(input_mos)
default_comments["1/0"] = "Collapsed " .. MOS.as_string(input_mos)
-- Create table
-- Create table
Line 146: Line 134:
-- Cell for step ratio
-- Cell for step ratio
result = result .. string.format("| %d:%d\n", step_ratio[1], step_ratio[2])
-- Include appropriate tamnams name for step ratio
local step_ratio_as_text = string.format("%d:%d", step_ratio[1], step_ratio[2])
if step_ratio_as_text == "1/1" then
result = result .. string.format("| %s (equalized)\n", step_ratio_as_text)
elseif step_ratio_as_text == "4/3" then
result = result .. string.format("| %s (supersoft)\n", step_ratio_as_text)
elseif step_ratio_as_text == "3/2" then
result = result .. string.format("| %s (soft)\n", step_ratio_as_text)
elseif step_ratio_as_text == "5/3" then
result = result .. string.format("| %s (semisoft)\n", step_ratio_as_text)
elseif step_ratio_as_text == "2/1" then
result = result .. string.format("| %s (basic)\n", step_ratio_as_text)
elseif step_ratio_as_text == "5/2" then
result = result .. string.format("| %s (semihard)\n", step_ratio_as_text)
elseif step_ratio_as_text == "3/1" then
result = result .. string.format("| %s (hard)\n", step_ratio_as_text)
elseif step_ratio_as_text == "4/1" then
result = result .. string.format("| %s (superhard)\n", step_ratio_as_text)
elseif step_ratio_as_text == "1/0" then
result = result .. string.format("| %s (collapsed)\n", step_ratio_as_text)
else
result = result .. string.format("| %s\n", step_ratio_as_text)
end
-- Cell for hardness, with divide-by-zero check
-- Cell for hardness, with divide-by-zero check
Line 161: Line 171:
-- Check for comments
-- Check for comments
local contains_custom_comment = comments[key] ~= nil
-- Add default comments for which generators create a proper scale
local contains_default_comment = default_comments[key] ~= nil
local comment = comments[key] or ""
local comment = ""
if step_ratio_as_text == "2/1" and s == n then
if contains_default_comment and contains_custom_comment then
comment = "Bright generators smaller than this are proper</br>" .. comment
comment = comment .. default_comments[key] .. "<br>" .. comments[key]
elseif contains_default_comment and not contains_custom_comment then
comment = comment .. default_comments[key]
elseif not contains_default_comment and contains_custom_comment then
comment = comment .. comments[key]
end
end
result = result .. string.format("| %s\n", comment)
result = result .. string.format("| %s\n", comment)

Revision as of 23:54, 18 February 2024


local p = {}
local MOS = require('Module:MOS')
local ET = require('Module:ET')
local u = require('Module:Utils')
local rat = require('Module:Rational')
local sb = require('Module:SB tree')
local utils = require('Module:Utils')

-- Helper function that parses entries from a semicolon-delimited string and returns them in an array
-- TODO: Separate this and parse_pairs into its own module of helper functions, as they're included
-- in various modules at this point
function p.parse_entries(unparsed)
	local parsed = {}
	for entry in string.gmatch(unparsed, '([^;]+)') do
		local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
		table.insert(parsed, trimmed)		-- Add to array
	end
	return parsed
end

-- Helper function that parses pairs of elements separated by a colon
-- A pair must be two elements or it will be returned as an empty array
function p.parse_pair(unparsed)
	local parsed = {}
	for entry in string.gmatch(unparsed, '([^:]+)') do
		local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
		table.insert(parsed, trimmed)		-- Add to array
	end
	if #parsed == 2 then
		return parsed
	else
		return {}
	end
end

-- Function that takes a list of semicolon-delimited pairs and returns a map
-- (or dictionary or associative array) of key-value pairs
-- Each entry is colon-delimited as key : pair
function p.parse_kv_pairs(unparsed)
	-- Tokenize the string of unparsed pairs
	local parsed = p.parse_entries(unparsed)
	-- Then tokenize the tokens into key-value pairs
	local pairs_ = {}
	for i = 1, #parsed do
		local pair = p.parse_pair(parsed[i])
		if #pair == 2 then
			pairs_[pair[1]] = pair[2]
		end
	end
	return pairs_
end

-- Rewrite of scale tree function (has bugfixes and new formatting)
function p._scale_tree(input_mos, depth, comments)
	local input_mos = input_mos or MOS.new(5, 2)
	local depth = depth or 5
	local comments = comments or {}
	
	local equave = input_mos.equave
	local L = input_mos.nL		-- Large steps in mos
	local s = input_mos.ns		-- Small steps in mos
	local n = utils._gcd(L, s)		-- Number of periods
	local abstract_bright_gen = MOS.bright_gen(input_mos)
	
	local step_ratios = sb.sb_tree_ratios(depth)
	local depths = sb.sb_tree_depths(depth)
	
	-- What is the equave suffix (edo, edt, edf, ed-p/q)
	local equave_suffix = ""
	if rat.eq(input_mos.equave, rat.new(2)) then
		equave_suffix = "o"
	elseif rat.eq(input_mos.equave, rat.new(3)) then
		equave_suffix = "t"
	elseif rat.eq(input_mos.equave, rat.new(3,2)) then
		equave_suffix = "f"
	else
		equave_suffix = rat.as_ratio(input_mos.equave)
	end
	
	-- Create table
	local result = ""
	
	-- Table headers
	-- There are 6 columns:
	-- - Steps of ED
	-- - Bright and dark gens in cents
	-- - Step ratio and hardness
	-- - Comments
	result = '{| class="wikitable center-all"\n'
	result = result .. '|+\n'
	result = result .. string.format('! rowspan="2" colspan="%d" | Generator (in steps of [[Ed%s|ed%s]])\n', depth+1, equave_suffix, equave_suffix)
	result = result .. '! colspan="2" | Cents\n'
	result = result .. '! colspan="2" | Step ratio\n'
	result = result .. '! rowspan="2" | Comments\n'
	result = result .. '|-\n'
	result = result .. '! Bright\n'
	result = result .. '! Dark\n'
	result = result .. '! L:s\n'
	result = result .. '! Hardness\n'

	-- Rounding is done using string.format, to 3 decimal places (%.3f)
	
	-- Create each row of the table
	for i = 1, #step_ratios do
		local step_ratio = step_ratios[i]
		local steps_per_equave = step_ratio[1] * L + step_ratio[2] * s
		local steps_per_period = steps_per_equave / n
		local et = ET.new(steps_per_equave, equave)
		
		-- Calculate the bright gen and cent value
		local bright_generator_steps = step_ratio[1] * abstract_bright_gen['L'] + step_ratio[2] * abstract_bright_gen['s']
		local bright_generator_cents = ET.cents(et, bright_generator_steps)
		
		-- Calculate dark generator step count and cent value
		local dark_generator_steps = steps_per_period - bright_generator_steps
		local dark_generator_cents = ET.cents(et, dark_generator_steps)
		
		-- New row
		result = result .. "|-\n"	
		
		-- Cells for bright generator, as steps in et
		local current_depth = depths[i]
		for i = 1, depth+1 do
			if i == current_depth then
				result = result .. string.format("| [[%s|%d\\%s]]\n", ET.as_string(et), bright_generator_steps, et.size)
			else
				result = result .. "|\n"
			end
		end
		
		-- Cells for generators in cents
		result = result .. string.format("| %.3f\n", bright_generator_cents)
		result = result .. string.format("| %.3f\n", dark_generator_cents)
		
		-- Cell for step ratio
		-- Include appropriate tamnams name for step ratio
		local step_ratio_as_text = string.format("%d:%d", step_ratio[1], step_ratio[2])
		if step_ratio_as_text == "1/1" then
			result = result .. string.format("| %s&nbsp;(equalized)\n", step_ratio_as_text)
		elseif step_ratio_as_text == "4/3" then
			result = result .. string.format("| %s&nbsp;(supersoft)\n", step_ratio_as_text)
		elseif step_ratio_as_text == "3/2" then
			result = result .. string.format("| %s&nbsp;(soft)\n", step_ratio_as_text)
		elseif step_ratio_as_text == "5/3" then
			result = result .. string.format("| %s&nbsp;(semisoft)\n", step_ratio_as_text)
		elseif step_ratio_as_text == "2/1" then
			result = result .. string.format("| %s&nbsp;(basic)\n", step_ratio_as_text)
		elseif step_ratio_as_text == "5/2" then
			result = result .. string.format("| %s&nbsp;(semihard)\n", step_ratio_as_text)
		elseif step_ratio_as_text == "3/1" then
			result = result .. string.format("| %s&nbsp;(hard)\n", step_ratio_as_text)
		elseif step_ratio_as_text == "4/1" then
			result = result .. string.format("| %s&nbsp;(superhard)\n", step_ratio_as_text)
		elseif step_ratio_as_text == "1/0" then
			result = result .. string.format("| %s&nbsp;(collapsed)\n", step_ratio_as_text)
		else
			result = result .. string.format("| %s\n", step_ratio_as_text)
		end
		
		-- Cell for hardness, with divide-by-zero check
		local hardness = ""
		if step_ratio[2] == 0 then
			hardness = "→ ∞"
		else
			hardness = string.format("%.3f", step_ratio[1] / step_ratio[2])
		end
		result = result .. string.format("| %s\n", hardness)
		
		-- Cell for comment
		local key = step_ratios[i][1] .. "/" .. step_ratios[i][2]		-- The step ratio is (literally and figuratively) the key to add comments!
		
		-- Check for comments
		-- Add default comments for which generators create a proper scale
		local comment = comments[key] or ""
		if step_ratio_as_text == "2/1" and s == n then
			comment = "Bright generators smaller than this are proper</br>" .. comment
		end
		result = result .. string.format("| %s\n", comment)
		
	end
	
	result = result .. "|}"
	return result

end

function p.scale_tree(frame)
	local mos = MOS.parse(frame.args["tuning"])
	local depth = frame.args["depth"] or 5
	local comments_unparsed = frame.args["Comments"] or ""
	local comments = p.parse_kv_pairs(comments_unparsed) or {}
	
	local result = p._scale_tree(mos, depth, comments)
	return result
end

return p