Module:Infobox chord: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Undo revision 184878 by ArrowHead294 (talk). The debug mode is used in several places on the wiki where categories should be disabled
Tag: Undo
m Fix
Line 260: Line 260:
end
end
return frame:preprocess(s)
return frame:preprocess(result)
end
end


return p
return p

Revision as of 18:53, 6 March 2025

Module documentation[view] [edit] [history] [purge]
Note: Do not invoke this module directly; use the corresponding template instead: Template:Infobox chord.

This module implements {{Infobox chord}} to generate an infobox providing information about a given chord.


local p = {}

local rat = require("Module:Rational")
local utils = require("Module:Utils")
local consistency = require("Module:Chord consistency")
local infobox = require("Module:Infobox")
local yesno = require("Module:Yesno")

function p.infobox_chord(frame)
	local debug_mode = yesno(frame.args["debug"])

	local page_name = frame:preprocess("{{PAGENAME}}")
	
	local debug_data = ""
	local infobox_data = {}
	local cats = ""

	local color_names = {}
	if utils.value_provided(frame.args["ColorName"]) then
		color_name = frame.args["ColorName"]
		-- search for ", " not "," because many chord names contain commas, e.g. Cz,y6
		for name in (color_name .. ", "):gmatch("(.-), ") do
			table.insert(color_names, name)
		end
	else
		cats = cats .. "[[Category:Todo:add color name]]"
	end

	if utils.value_provided(frame.args["Harmonics"]) then
		local harmonics = {}
		for hs in string.gmatch(frame.args["Harmonics"], "[^:]+") do
			h = tonumber(hs)  -- TODO: support rational entries?
			assert(h > 0, "invalid harmonic")
			table.insert(harmonics, h)
		end

		-- reduce harmonics to simplest terms, in case the user accidentally failed to reduce them
		local gcd = harmonics[1]
		for i, h in ipairs(harmonics) do
			gcd = utils._gcd(gcd, h)
			if gcd == 1 then break end
		end
		if gcd > 1 then
			for i, h in ipairs(harmonics) do
				harmonics[i] = harmonics[i] / gcd
			end
		end

		local root = harmonics[1]
		if utils.value_provided(frame.args["Root"]) then
			root = tonumber(frame.args["Root"])
			assert(root > 0, "invalid root")
		end
		assert(root > 0, "no harmonics given")

		local prime_limit = 1
		local lcm = 1
		local otonal_odd_limit = 1
		local root_interval_links = {}
		local step_interval_links = {}
		local root_cents_steps = {}
		local step_cents = {}
		local genus = {}
		for i, h in ipairs(harmonics) do
			-- compute LCM of all harmonics to use as the denominator in utonal form, if needed
			lcm = lcm * h / (utils._gcd(lcm, h))

			-- increase otonal odd limit for this harmonic, if needed
			local odd = h
			while odd > 0 and odd % 2 == 0 do
				odd = odd / 2
			end
			if odd > otonal_odd_limit then
				otonal_odd_limit = odd
			end

			-- increase prime limit for this harmonic, if needed,
			-- and increase genus factors if needed
			for prime, n in pairs(utils.prime_factorization_raw(h)) do
				if prime > prime_limit then
					prime_limit = prime
				end
				if prime > 2 then
					local prev = genus[prime] or 0
					if n > prev then
						genus[prime] = n
					end
				end
			end
			
			-- compute ratio of this harmonic relative to the root
			local gcd = utils._gcd(h, root)
			local numer = h / gcd
			local denom = root / gcd
			table.insert(root_interval_links, "[[" .. numer .. "/" .. denom .. "]]")

			local cents_ln2 = 1731.234

			-- compute ratio of this harmonic relative to the previous
			if i > 1 then
				local prev = harmonics[i-1]
				local step_gcd = utils._gcd(h, prev)
				local step_numer = h / step_gcd
				local step_denom = prev / step_gcd
				table.insert(step_interval_links, "[[" .. step_numer .. "/" .. step_denom .. "]]")
				table.insert(step_cents, utils._round_dec(cents_ln2 * math.log(step_numer / step_denom)) .. "¢")
			end
			
			table.insert(root_cents_steps, utils._round_dec(cents_ln2 * math.log(numer / denom)) .. "¢")
		end
		
		local utonal_odd_limit = 1
		local subharmonics = {}
		for i, h in ipairs(harmonics) do
			-- find the subharmonics from the harmonics
			local gcd = utils._gcd(lcm, h)
			local s = lcm / gcd
			table.insert(subharmonics, s)
			
			-- find the utonal odd limit
			local s_odd = s
			while s_odd > 0 and s_odd % 2 == 0 do
				s_odd = s_odd / 2
			end
			if s_odd > utonal_odd_limit then
				utonal_odd_limit = s_odd
			end
		end

		-- intervallic odd limit
		local odd_limit = 1
		for j, b in ipairs(harmonics) do
			for i, a in ipairs(harmonics) do
				local gcd = utils._gcd(a, b)
				local numer = b / gcd
				local denom = a / gcd

				while numer % 2 == 0 do
					numer = numer / 2
				end
				if numer > odd_limit then
					odd_limit = numer
				end

				while denom > 0 and denom % 2 == 0 do
					denom = denom / 2
				end
				if denom > odd_limit then
					odd_limit = denom
				end
			end
		end
		
		-- genus
		local genus_terms = {}
		local genus_product = 1
		local primes = {}
		for prime, _ in pairs(genus) do
			table.insert(primes, prime)
		end
		table.sort(primes)
		for i, prime in ipairs(primes) do
			local exponent = genus[prime]
			if exponent == 1 then
				table.insert(genus_terms, prime)
			else
				table.insert(genus_terms, prime .. "<sup>" .. exponent .. "</sup>")
			end
			genus_product = genus_product * (prime ^ exponent)
		end
		
		-- consistent edos
		local distance = tonumber(frame.args["Distance"])
		if distance == nil then
			if #harmonics >= 5 then
				distance = 1.5
			elseif #harmonics >= 3 then
				distance = 2.0
			else
				distance = 3.0
			end
		end
		local consistent_edos = consistency.consistent_edos(harmonics, distance, 'edo', 4)
		
		-- compute tag to add for category sort order: as many "#" as the number of digits in the first harmonic
		local sort_tag = ""
		local first_num = page_name:match("^(%d+):")
		if first_num then
			first_num = tonumber(first_num)
			sort_tag = "|#"
			local sort_bound = 10
			while sort_bound <= harmonics[1] do
				sort_tag = sort_tag .. "#"
				sort_bound = sort_bound * 10
			end
		end

		table.insert(infobox_data, {"Harmonics", table.concat(harmonics, ":")})
		if (not utils.value_provided(frame.args["Root"])) and (utonal_odd_limit <= otonal_odd_limit or utonal_odd_limit < 1000) then
			table.insert(infobox_data, {"Subharmonics", "1/(" .. table.concat(subharmonics, ":") .. ")"})
		end
		table.insert(infobox_data, {"Intervals from root", table.concat(root_interval_links, "&thinsp;&ndash;&thinsp;")})
		table.insert(infobox_data, {"Cents from root", table.concat(root_cents_steps, "&#x2007;")})
		table.insert(infobox_data, {"Step intervals", table.concat(step_interval_links, ", ")})
		table.insert(infobox_data, {"Step cents", table.concat(step_cents, ", ")})

		-- TODO: category goes here.

		if table.getn(color_names) > 0 then
			local label = "Color name"
			if table.getn(color_names) > 1 then
				label = "Color names"
			end
			table.insert(infobox_data, {"[[Color notation|" .. label .. "]]", table.concat(color_names, "<br />")})
		end

		if prime_limit < 96 then
			table.insert(infobox_data, {"[[Prime limit]]", "[[" .. prime_limit .. "-limit|" .. prime_limit .. "]]"})
			cats = cats .. "[[Category:" .. prime_limit .. "-limit chords" .. sort_tag .. "]]"
		else
			table.insert(infobox_data, {"[[Prime limit]]", prime_limit})
			cats = cats .. "[[Category:Just intonation chords" .. sort_tag .. "]]"
		end

		local genus_data = table.concat(genus_terms, "&#x200A;&sdot;&#x200A;")
		-- append the actual product if it's (arbitrarily) 9 digits or fewer
		if genus_product < 1000000000 then
			genus_data = genus_data .. " (" .. genus_product .. ")"
		end
		table.insert(infobox_data, {"[[Euler-Fokker genus|Genus]]", genus_data})

		if odd_limit < 32 then
			table.insert(infobox_data, {"[[Intervallic odd limit]]", "[[" .. odd_limit .. "-odd-limit|" .. odd_limit .. "]]"})
			cats = cats .. "[[Category:" .. odd_limit .. "-odd-limit chords" .. sort_tag .. "]]"
		else 
			table.insert(infobox_data, {"[[Intervallic odd limit]]", odd_limit})
		end
		table.insert(infobox_data, {"[[Otonal odd limit]]", otonal_odd_limit})
		table.insert(infobox_data, {"[[Utonal odd limit]]", utonal_odd_limit})

		if consistent_edos ~= "" then
			table.insert(infobox_data, {"[[Consistency|Consistent edos]] (''d'' &ge; " .. distance .. ")", "<span style=\"font-size: 75%;\">" .. consistent_edos .. "</span>"})
			table.insert(infobox_data, {"<div style=\"font-size: 75%; text-align: right; white-space: nowrap;\">[[Module:Chord consistency/doc|* 2 &le; ''d'' &lt; 4; ** 4 &le; ''d'' &lt; 8; *** 8 &le; ''d'' &lt; 16; &hellip;]]</div>"})
		else
			table.insert(infobox_data, {"[[Consistency|Consistent edos]] (''d'' &ge; " .. distance .. ")", "<small>not exist in the range up to 72</small>"})
		end
	end
	
	if debug_data ~= "" then
		table.insert(infobox_data, {
			"Debug",
			debug_data,
		})
	end

	local result = infobox.build("<u>Chord information</u>", infobox_data)
	
	if not debug_mode then
		result = result .. cats
	end
	
	return frame:preprocess(result)
end

return p