Module:Infobox Chord

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:Infobox Chord.
Icon-Todo.png Todo: Documentation

local p = {}

local rat = require("Module:Rational")
local utils = require("Module:Utils")
local infobox = require("Module:Infobox")

function p.infobox_chord(frame)
	local debug_mode = utils.value_provided(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
		
		-- 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})
	end
	
	if debug_data ~= "" then
		table.insert(infobox_data, {
			"Debug",
			debug_data,
		})
	end

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

return p