Module:ET

From Xenharmonic Wiki
Jump to navigation Jump to search
Module documentation[view] [edit] [history] [purge]

This module provides helper functions for equal-step tunings.

Functions

new
Returns an array consisting of the components of an equal-step tuning.
parse
Designed to convert strings in the format [number of steps]ed[equave] into an ET structure, and returns it via the new function. For example, ET.parse("12edo") returns an array containing {12, 2, "edo"}
as_string
Returns the string representation for an ET structure.
backslash_ratio
Converts steps to a proper ratio as a floating-point number.
backslash_display
Displays an ET structure in backslash form ([steps]\[number of divisions]).
cents
Converts the interval an ET structure represents to cents.
hekts
Converts the interval an ET structure represents to hekts (relative cent of 13edt).
approximate
Returns the floor, round, or ceiling of a particular ratio.
tempers_out
Determines if an ET tempers out a provided rational number.
is_highly_composite
Determines if an ET is highly composite.
is_zeta
Determines if an ET holds any zeta records.
why_zeta
Describes what specific properties an ET has if it is a zeta record ET.

local rat = require("Module:Rational")
local seq = require("Module:Sequence")
local p = {}

local common_suffix = {
	["3/2"] = "f",
	["2"] = "o",
	["2/1"] = "o",
	["3"] = "t",
	["3/1"] = "t",
}

local common_ratio = {
	["f"] = rat.new(3, 2),
	["o"] = 2,
	["t"] = 3,
	["ϕ"] = (1 + math.sqrt(5)) / 2,
	["n"] = math.exp(1),
	["π"] = math.pi
}

-- create a ET structure <size>ed<equave>
function p.new(size, equave, suffix)
	size = size or 12
	equave = equave or 2
	if suffix == nil then
		local equave_n, equave_m = rat.as_pair(equave)
		local equave_ratio = rat.as_ratio(equave)
		equave_ratio = equave_ratio:lower()
		suffix = "ed"
		if common_suffix[equave_ratio] then
			suffix = suffix .. common_suffix[equave_ratio]
		elseif equave_m == 1 then
			suffix = suffix .. equave_n
		else
			suffix = suffix .. equave_ratio
		end
	end
	return { size = size, equave = equave, suffix = suffix }
end

-- parse a ET structure
function p.parse(unparsed)
	local size, suffix, equave = unparsed:match("^(%d+%.*%d*)([Ee][Dd](.+))$")
	-- local size, suffix, equave = unparsed:match("^(%d+%.*%d*)([Cc]?[Ee][Dd]?[Tt]?(.*))$")
	if equave == nil then
		return nil
	end
	suffix = suffix:lower()
	size = tonumber(size)
	equave = common_ratio[equave] or rat.parse(equave)
	if size == nil or equave == nil then
		return nil
	end
	return p.new(size, equave, suffix)
end

-- construct a string representation for a ET structure
function p.as_string(et)
	return et.size .. et.suffix
end

-- convert steps to a proper ratio (except that it is a float approximation)
function p.backslash_ratio(et, steps)
	if et.size == 0 then
		return 1
	end
	return rat.as_float(et.equave) ^ (steps / et.size)
end

function p.backslash_display(et, steps)
	if et.size == 0 then
		return 1
	end
	return steps .. p.backslash_modifier(et)
end

function p.backslash_modifier(et)
	if not rat.eq(et.equave, 2) then
		return "\\" .. et.size .. et.suffix
	end
	return "\\" .. et.size
end

-- convert steps to cents
function p.cents(et, steps)
	if et.size == 0 then
		return 0
	end
	steps = steps or 1
	return 1200 * steps / et.size * math.log(rat.as_float(et.equave)) / math.log(2)
end

-- convert steps to Hekts
function p.hekts(et, steps)
	if et.size == 0 then
		return 0
	end
	steps = steps or 1
	return 1300 * steps / et.size * math.log(rat.as_float(et.equave)) / math.log(3)
end

-- convert ratio to steps
-- ratio is a float!
-- towards is one of: -1 (floor), 0 (nearest), 1 (ceil)
function p.approximate(et, ratio, towards)
	towards = towards or 0
	if et.size == 0 then
		return 0
	end
	local exact = math.log(ratio) / math.log(rat.as_float(et.equave)) * et.size
	if towards < 0 then
		return math.floor(exact)
	elseif towards > 0 then
		return math.ceil(exact)
	else
		return math.floor(exact + 0.5)
	end
end

-- whether this ET tempers out the provided rational number
function p.tempers_out(et, ratio)
	local t = 0
	for factor, power in pairs(ratio) do
		if type(factor) == "number" then
			t = t + power * p.approximate(et, factor)
		end
	end
	return t == 0
end

-- determine whether ET is highly composite
function p.is_highly_composite(et)
	et.highly_composite = et.highly_composite or rat.is_highly_composite(et.size)
	return et.highly_composite
end

-- determine whether ET's size could be within one of zeta function-related sequences
function p.is_zeta(et)
	return seq.contains(seq.zeta_peak, et.size)
		or seq.contains(seq.zeta_peak_integer, et.size)
		or seq.contains(seq.zeta_integral, et.size)
		or seq.contains(seq.zeta_gap, et.size)
end

-- describe why
function p.why_zeta(et)
	local zeta_peak = seq.contains(seq.zeta_peak, et.size)
	local zeta_peak_integer = seq.contains(seq.zeta_peak_integer, et.size)
	local zeta_integral = seq.contains(seq.zeta_integral, et.size)
	local zeta_gap = seq.contains(seq.zeta_gap, et.size)
	local z = "The Riemann zeta function and tuning#Zeta EDO lists"

	local markers = {}
	if zeta_peak then
		table.insert(markers, string.format("[[%s|Zeta peak]]", z))
	elseif zeta_peak == nil then
		table.insert(markers, string.format("[[%s|Zeta peak?]]", z))
	end

	if zeta_peak_integer then
		table.insert(markers, string.format("[[%s|Zeta peak integer]]", z))
	elseif zeta_peak_integer == nil then
		table.insert(markers, string.format("[[%s|Zeta peak integer?]]", z))
	end

	if zeta_integral then
		table.insert(markers, string.format("[[%s|Zeta integral]]", z))
	elseif zeta_integral == nil then
		table.insert(markers, string.format("[[%s|Zeta integral?]]", z))
	end

	if zeta_gap then
		table.insert(markers, string.format("[[%s|Zeta gap]]", z))
	elseif zeta_gap == nil then
		table.insert(markers, string.format("[[%s|Zeta gap?]]", z))
	end

	return table.concat(markers, "<br />")
end

return p