Module:ED intro

From Xenharmonic Wiki
Jump to navigation Jump to search

Documentation for this module may be created at Module:ED intro/doc

local ord = require('Module:Ordinal')
local utils = require('Module:Utils')
local rat = require('Module:Rational')
local p = {}

-- TODO: Add support for the following
-- - Make it clear what text is being swapped in string.gsub

-- Notes:
-- - Edo and edt are technically equal divisions of a harmonic, and edf an equal
--   division of a ratio; these are their own types because their intros have
--   specific wording.
-- - An equal division of a harmonic is a special case of an equal division of a
--   ratio, where the ratio is h/1.
-- - An equal division of a ratio and an equal division of a non-integer 
--   constant or cent value are also called AS (ambitonal sequence) and APS 
--   (arithmetic pitch sequence), respectively. 
-- - All of these are equal-step tunings. We've adopted the 1ed-p notation where
--   possible. 
-- - Equal divisions of irrational constants (such as pi and e) are not very
--   common, but count as equal divisions of arbitrary cent values.

local ED_TYPE_EDO = "EDO"
local ED_TYPE_EDT = "EDT"
local ED_TYPE_EDF = "EDF"
local ED_TYPE_EDH = "EDH" -- "equal division of a harmonic"
local ED_TYPE_EDR = "EDR" -- "equal division of a ratio"
local ED_TYPE_EDC = "EDC" -- "equal division of a cent value"
local ED_TYPE_DEFAULT = "UNKNOWN_TYPE"

-- Separate function for edo intro
function p.edo_intro(ed)
	local ed = ed or 12
	
	-- Exactly or about? Check up to three significant figures
	local edstep_size = 1200 / ed
	local edstep_size_rounded = utils._round(edstep_size, 3)
	local is_exact = edstep_size - edstep_size_rounded == 0
	
	local ordinal = ord._ordinal(ed)
	
	local intro_text = ""
	if ed == 1 then
		intro_text = "'''1 equal division of the octave''' (abbreviated '''1edo''' or '''1ed2'''), also called '''1-tone equal temperament''' ('''1tet''') or '''1 equal temperament''' ('''1et''') when viewed under a [[regular temperament]] perspective, is the [[tuning system]] that uses [[equal]] steps of 2/1 (one [[octave]]), or exactly/about ''s'' [[¢]]."	
	else
		intro_text = "'''''k'' equal divisions of the octave''' (abbreviated '''''k''edo''' or '''''k''ed2'''), also called '''''k''-tone equal temperament''' ('''''k''tet''') or '''''k'' equal temperament''' ('''''k''et''') when viewed under a [[regular temperament]] perspective, is the [[tuning system]] that divides the [[octave]] into ''k'' [[equal]] parts of exactly/about ''s'' [[¢]] each. Each step represents a [[frequency ratio]] of 2<sup>1/''k''</sup>, or the ''kth'' root of 2."	
	end

	-- Replace certain strings with the intended final versions
	intro_text = string.gsub(intro_text, "''k''", ed)
	intro_text = string.gsub(intro_text, "exactly/about", (is_exact and "exactly" or "about"))
	intro_text = string.gsub(intro_text, "''s''", utils._round (edstep_size, 3))
	intro_text = string.gsub(intro_text, "''kth''", ord._ordinal(ed))
	
	return intro_text
end

-- Separate function for edt intro
function p.edt_intro(ed)
	local ed = ed or 13
	
	-- Exactly or about? Check up to three significant figures
	local equave_in_cents = math.log(3) * 1200
	local edstep_size = equave_in_cents / math.log(2) / ed
	local edstep_size_rounded = utils._round(edstep_size, 3)
	local is_exact = edstep_size - edstep_size_rounded == 0
	
	local ordinal = ord._ordinal(ed)
	
	local intro_text = ""
	if ed == 1 then
		intro_text = "'''1 equal division of the tritave''', '''perfect twelfth''', or '''3rd harmonic''' (abbreviated '''1edt''' or '''1ed3'''), is a [[nonoctave]] [[tuning system]] that uses [[equal]] steps of [[3/1]] (one tritave), or exactly/about ''s'' [[¢]]."	
	else
		intro_text = "'''''k'' equal divisions of the tritave''', '''perfect twelfth''', or '''3rd harmonic''' (abbreviated '''''k''edt''' or '''''k''ed3'''), is a [[nonoctave]] [[tuning system]] that divides the interval of [[3/1]] into ''k'' [[equal]] parts of exactly/about ''s'' [[¢]] each. Each step represents a [[frequency ratio]] of 3<sup>1/''k''</sup>, or the ''kth'' root of 3."	
	end

	-- Replace certain strings with the intended final versions
	intro_text = string.gsub(intro_text, "''k''", ed)
	intro_text = string.gsub(intro_text, "exactly/about", (is_exact and "exactly" or "about"))
	intro_text = string.gsub(intro_text, "''s''", utils._round (edstep_size, 3))
	intro_text = string.gsub(intro_text, "''kth''", ord._ordinal(ed))
	
	return intro_text
end

-- Separate function for edf intro
function p.edf_intro(ed)
	local ed = ed or 7
	
	-- Exactly or about? Check up to three significant figures
	local equave_in_cents = math.log(3/2) * 1200
	local edstep_size = equave_in_cents / math.log(2) / ed
	local edstep_size_rounded = utils._round(edstep_size, 3)
	local is_exact = edstep_size - edstep_size_rounded == 0
	
	local ordinal = ord._ordinal(ed)
	
	local intro_text = ""
	if ed == 1 then
		intro_text = "'''1 equal division of the perfect fifth''' (abbreviated '''1edf''' or '''1ed3/2''') is a [[nonoctave]] [[tuning system]] that uses [[equal]] steps of [[3/2]] (one perfect fifth), or exactly/about ''s'' [[¢]]."	
	else
		intro_text = "'''''k'' equal divisions of the perfect fifth''' (abbreviated '''''k''edf''' or '''''k''ed3/2''') is a [[nonoctave]] [[tuning system]] that divides the interval of [[3/2]] into ''k'' [[equal]] parts of exactly/about ''s'' [[¢]] each. Each step represents a [[frequency ratio]] of (3/2)<sup>1/''k''</sup>, or the ''kth'' root of 3/2."	
	end

	-- Replace certain strings with the intended final versions
	intro_text = string.gsub(intro_text, "''k''", ed)
	intro_text = string.gsub(intro_text, "exactly/about", (is_exact and "exactly" or "about"))
	intro_text = string.gsub(intro_text, "''s''", utils._round (edstep_size, 3))
	intro_text = string.gsub(intro_text, "''kth''", ord._ordinal(ed))
	
	return intro_text
end

-- Separate function for edh intro (arbitrary harmonic)
function p.edh_intro(ed, harmonic)
	local ed = ed or 12
	local harmonic = harmonic or 4
	
	-- Exactly or about? Check up to three significant figures
	local equave_in_cents = math.log(harmonic) * 1200 
	local edstep_size = equave_in_cents / math.log(2) / ed
	local edstep_size_rounded = utils._round(edstep_size, 3)
	local is_exact = edstep_size - edstep_size_rounded == 0
	
	local ordinal = ord._ordinal(ed)
	
	local intro_text = ""
	if ed == 1 then
		intro_text = "'''1 equal division of the hth harmonic''' (abbreviated '''1ed''h''''') is a [[nonoctave]] [[tuning system]] that uses [[equal]] steps of [[''h''/1]], or exactly/about ''s'' [[¢]]."	
	else
		intro_text = "'''''k'' equal divisions of the hth harmonic''' (abbreviated '''''k''ed''h''''') is a [[nonoctave]] [[tuning system]] that divides the interval of [[''h''/1]] into ''k'' [[equal]] parts of exactly/about ''s'' [[¢]] each. Each step represents a [[frequency ratio]] of ''h''<sup>1/''k''</sup>, or the ''kth'' root of ''h''."	
	end

	-- Replace certain strings with the intended final versions
	intro_text = string.gsub(intro_text, "''k''", ed)
	intro_text = string.gsub(intro_text, "exactly/about", (is_exact and "exactly" or "about"))
	intro_text = string.gsub(intro_text, "''s''", utils._round (edstep_size, 3))
	intro_text = string.gsub(intro_text, "''kth''", ord._ordinal(ed))
	intro_text = string.gsub(intro_text, "hth", ord._ordinal(harmonic))
	intro_text = string.gsub(intro_text, "''h''", harmonic)
	
	return intro_text
end

-- Separate function for edr intro (arbitrary ratio)
function p.edr_intro(ed, ratio)
	local ed = ed or 12
	local ratio = ratio or rat.new(9,4)
	
	-- Exactly or about? Check up to three significant figures
	local equave_in_cents = rat.cents(ratio)
	local edstep_size = equave_in_cents / ed
	local edstep_size_rounded = utils._round(edstep_size, 3)
	local is_exact = edstep_size - edstep_size_rounded == 0
	
	local ordinal = ord._ordinal(ed)
	
	local intro_text = ""
	if ed == 1 then
		intro_text = "'''1 equal division of ''p/q''''' (abbreviated '''1ed''p/q''''') is a [[nonoctave]] [[tuning system]] that uses [[equal]] steps of [[''p/q'']], or exactly/about ''s'' [[¢]]."	
	else
		intro_text = "'''''k'' equal divisions of ''p/q''''' (abbreviated '''''k''ed''p/q''''') is a [[nonoctave]] [[tuning system]] that divides the interval of [[''p/q'']] into ''k'' [[equal]] parts of exactly/about ''s'' [[¢]] each. Each step represents a [[frequency ratio]] of (''p/q'')<sup>1/''k''</sup>, or the ''kth'' root of ''p/q''."	
	end

	-- Replace certain strings with the intended final versions
	intro_text = string.gsub(intro_text, "''k''", ed)
	intro_text = string.gsub(intro_text, "exactly/about", (is_exact and "exactly" or "about"))
	intro_text = string.gsub(intro_text, "''s''", utils._round (edstep_size, 3))
	intro_text = string.gsub(intro_text, "''kth''", ord._ordinal(ed))
	intro_text = string.gsub(intro_text, "''p/q''", rat.as_ratio(ratio))
	
	return intro_text
end

-- Separate function for edc (arbitrary cent value)
function p.edc_intro(ed, cents)
	local ed = ed or 1
	local cents = cents or 97.5
	
	-- Exactly or about? Check up to three significant figures
	local edstep_size = cents / ed
	local edstep_size_rounded = utils._round(edstep_size, 3)
	local is_exact = edstep_size - edstep_size_rounded == 0
	
	local ordinal = ord._ordinal(ed)
	
	local intro_text = ""
	if ed == 1 then
		intro_text = "'''1 equal division of ''c''¢''' (abbreviated '''1ed''c''¢''') is a [[nonoctave]] [[tuning system]] that uses [[equal]] steps of ''c'' [[¢]]."	
	else
		intro_text = "'''''k'' equal divisions of ''c''¢''' (abbreviated '''''k''ed''c''¢''') is a [[nonoctave]] [[tuning system]] that divides the interval of ''c''¢ into ''k'' [[equal]] parts of exactly/about ''s'' [[¢]] each."	
	end

	-- Replace certain strings with the intended final versions
	intro_text = string.gsub(intro_text, "''k''", ed)
	intro_text = string.gsub(intro_text, "exactly/about", (is_exact and "exactly" or "about"))
	intro_text = string.gsub(intro_text, "''s''", utils._round (edstep_size, 3))
	intro_text = string.gsub(intro_text, "''c''", cents)
	
	return intro_text
end

-- Parse function
function p.parse(unparsed)
	local unparsed = unparsed or "12"
	
	-- If the unparsed ed is only a numeric value, default to edo
	if tonumber(unparsed) ~= nil then
		unparsed = unparsed .. "edo"
	end
	
	-- Parse the step count, the suffix, and the equave
	local steps, equave = unparsed:match('^(%d+)[Ee][Dd](.+)$')
	
	-- Determine if the ed is for a cent value
	local is_cents = string.match(equave, '%d*%.?%d+[Cc¢]$') ~= nil
	
	-- Parse equave
	local parsed_equave
	local ed_type = ED_TYPE_DEFAULT
	if equave == "o" or equave == "O" or equave == "2" or equave == "2/1" then
		-- Equave is octave
		parsed_equave = 2
		ed_type = ED_TYPE_EDO
	elseif equave == "t" or equave == "T" or equave == "3" or equave == "3/1" then
		-- Equave is tritave/twelfth
		parsed_equave = 3
		ed_type = ED_TYPE_EDT
	elseif equave == "f" or equave == "F" or equave == "3/2" then
		-- Equave is fifth
		parsed_equave = rat.new(3,2)
		ed_type = ED_TYPE_EDF
	elseif string.match(equave, '^%d+$') ~= nil then
		-- Equave is arbitrary harmonic (not 2/1 or 3/1)
		parsed_equave = tonumber(equave)
		ed_type = ED_TYPE_EDH
	elseif string.match(equave, '^%d+/%d+$') ~= nil then
		-- Equave is arbitrary ratio (not 3/2)
		local num, den = equave:match('^(%d+)/(%d+)$')
		parsed_equave = rat.new(tonumber(num), tonumber(den))
		ed_type = ED_TYPE_EDR
	elseif is_cents then
		-- Equave is arbitrary cent value
		parsed_equave = tonumber(equave:match('^(%d*%.?%d+)[Cc¢]$'))
		ed_type = ED_TYPE_EDC
	else
		-- Equave is unsupported
		parsed_equave = 0
	end
	
	return tonumber(steps), parsed_equave, ed_type
end

-- ED-EDO comparison
-- Meant for finding what edo a nonoctave ed corresponds to
function p.edo_compare(ed)
	local ed = ed or "12edf"
	
	local parsed_ed, parsed_equave, ed_type = p.parse(ed)
	local edstep_size = 0;
	if ed_type == ED_TYPE_EDC then
		edstep_size = parsed_equave / parsed_ed
	else
		edstep_size = rat.cents(parsed_equave) / parsed_ed
	end
	local ed_compare = 1200 / edstep_size
	
	local ed_compare_text = "''ed1'' roughly corresponds to ''ed2''edo, "
	
	-- Replace certain strings with the intended final versions
	ed_compare_text = string.gsub(ed_compare_text, "''ed1''", ed)
	ed_compare_text = string.gsub(ed_compare_text, "''ed2''", string.format("%.3f", ed_compare))
	
	local ed_floor = math.floor(ed_compare)
	
	local diff = ed_compare - ed_floor
	if diff > 0.5 then
		ed_compare_text = ed_compare_text .. string.format("and can be seen as %dedo with stretched octaves", ed_floor+1)
	else
		ed_compare_text = ed_compare_text .. string.format("and can be seen as %dedo with compressed octaves", ed_floor)
	end
	
	if diff > 0.4 and diff < 0.6 then
		ed_compare_text = ed_compare_text .. string.format(", or roughly every second step of %dedo.", 2*ed_floor+1)
	else
		ed_compare_text = ed_compare_text .. "."
	end
	
	return ed_compare_text
	
end

-- Primary function
function p._ed_intro(ed)
	local ed = ed or "12"
	
	local parsed_ed, parsed_equave, ed_type = p.parse(ed)
	
	if ed_type == ED_TYPE_DEFAULT then
		return "Equave type not supported or entered incorrectly."
	elseif ed_type == ED_TYPE_EDO then
		return p.edo_intro(parsed_ed)
	elseif ed_type == ED_TYPE_EDT then
		return p.edt_intro(parsed_ed)
	elseif ed_type == ED_TYPE_EDF then
		return p.edf_intro(parsed_ed)
	elseif ed_type == ED_TYPE_EDH then
		return p.edh_intro(parsed_ed, parsed_equave)
	elseif ed_type == ED_TYPE_EDR then
		return p.edr_intro(parsed_ed, parsed_equave)
	elseif ed_type == ED_TYPE_EDC then
		return p.edc_intro(parsed_ed, parsed_equave)
	end
end

-- Tester function
function p.ed_intro_tester()
	return
		p._ed_intro("12") .. "\n" ..
		p._ed_intro("12edt") .. "\n" ..
		p._ed_intro("12edf") .. "\n" ..
		p._ed_intro("12ed4") .. "\n" ..
		p._ed_intro("12ed9/8") .. "\n" ..
		p._ed_intro("12ed666c") .. "\n" .. "\n" ..
		p._ed_intro("1") .. "\n" ..
		p._ed_intro("1ed3") .. "\n" ..
		p._ed_intro("1ed3/2") .. "\n" ..
		p._ed_intro("1ed4") .. "\n" ..
		p._ed_intro("1ed9/8") .. "\n" ..
		p._ed_intro("1ed666c") .. "\n"
end

-- Wrapper function; for use with a template
function p.ed_intro(frame)
	local ed = frame.args['ED']
	local edo = frame.args['EDO']		-- For backwards compatibility with edo intro

	if edo ~= nil then
		return p._ed_intro(edo)
	else
		return p._ed_intro(ed)
	end
end

return p