Module:Chord consistency: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Dummy index (talk | contribs)
+maxlen
Dummy index (talk | contribs)
improve ellipsis
Line 128: Line 128:
end
end
if up then
if up then
table.insert(vals, "[[" .. i .. ed .. "]]" .. string.rep("*", llevel))
if #vals >= maxlen then
if #vals >= maxlen then
table.insert(vals, "...")
table.insert(vals, "…")
break
break
end
end
table.insert(vals, "[[" .. i .. ed .. "]]" .. string.rep("*", llevel))
end
end
end
end

Revision as of 05:56, 15 December 2024

Module documentation[view] [edit] [history] [purge]
This module primarily serves as a library for other modules and has no corresponding template.

This module provides some functions which enumerate consistent equal divisions relative to some chord.


Introspection summary for Module:Chord consistency 
Functions provided (4)
Line Function Params Description
10 additively_consistent_int (et, ratios, distinct, previous) A variant of additively_consistent from Module:Limits that must be supplied with octave-reduced intervals.
88 max_error (et, ratios)
100 consistent_edos (harmonics, distance, ed, maxlen) Output list of consistent edos relative to given chord (Lua table of harmonics) up to 72edo. Different equaves, minimum consistency distance, maximum length of list are specifiable.
142 noinfobox_chord (invokable) (frame) Obsolete. Formerly used for Module:Infobox chord.
Lua modules required (3)
Variable Module Functions used
ET Module:ET approximate
cents
parse
rat Module:Rational as_float
eq
div
mul
as_ratio
cents
new
utils Module:Utils _gcd

Format of edos list

Output of consistent_edos are links to individual edos with each trailing several asterisks. These indicate consistency distace d briefly, none as 1 ≤ d < 2; * as 2 ≤ d < 4; ** as 4 ≤ d < 8; …


local rat = require('Module:Rational')
local utils = require("Module:Utils")
local ET = require('Module:ET')
local p = {}

-- check additive consistency for a set of ratios (equave-free version):
--   approx(a*b) = approx(a) + approx(b) forall a, b: a, b, ab in ratios
-- `distinct`: whether distinct ratios are required to be mapped to distinct approximations
-- `previous`: already computed ratios for the previous iteraton
function p.additively_consistent_int(et, ratios, distinct, previous)
	distinct = distinct or false
	previous = previous or {}
	if distinct then
		local approx_set = {}
		for a_key, a in pairs(previous) do
			local a_approx = ET.approximate(et, rat.as_float(a)) % et.size
			if approx_set[a_approx] then
				if not rat.eq(rat.div(a, approx_set[a_approx]), 1) then
					mw.log(a_key .. ' -> ' .. a_approx .. ': conflict!')
					return false
				end
			end
			approx_set[a_approx] = a
			mw.log(a_key .. ' -> ' .. a_approx)
		end
		for a_key, a in pairs(ratios) do
			local a_approx = ET.approximate(et, rat.as_float(a)) % et.size
			if approx_set[a_approx] then
				if not rat.eq(rat.div(a, approx_set[a_approx]), 1) then
					mw.log(a_key .. ' -> ' .. a_approx .. ': conflict!')
					return false
				end
			end
			approx_set[a_approx] = a
			mw.log(a_key .. ' -> ' .. a_approx)
		end
	end
	if type(distinct) == 'number' then
		return true
	end
	local previous_ordered = {}
	for a_key, a in pairs(previous) do
		table.insert(previous_ordered, a)
	end
	local ratios_ordered = {}
	for a_key, a in pairs(ratios) do
		table.insert(ratios_ordered, a)
	end
	for i, a in ipairs(ratios_ordered) do
		local a_approx = ET.approximate(et, rat.as_float(a))
		for j, b in ipairs(previous_ordered) do
			local b_approx = ET.approximate(et, rat.as_float(b))
			
			local c = rat.mul(a, b)
			local c_approx = ET.approximate(et, rat.as_float(c))
			
			local c_key = rat.as_ratio(c)
			if previous[c_key] or ratios[c_key] then
				if c_approx ~= a_approx + b_approx then
					mw.log('a = ' .. rat.as_ratio(a) .. '; b = ' .. rat.as_ratio(b) .. '; ab = ' .. c_key)
					mw.log(a_approx .. ' + ' .. b_approx .. ' != ' .. c_approx)
					return false
				end
			end
		end
		for j, b in ipairs(ratios_ordered) do
			if i <= j then
				local b_approx = ET.approximate(et, rat.as_float(b))
				
				local c = rat.mul(a, b)
				local c_approx = ET.approximate(et, rat.as_float(c))
				
				local c_key = rat.as_ratio(c)
				if previous[c_key] or ratios[c_key] then
					if c_approx ~= a_approx + b_approx then
						mw.log('a = ' .. rat.as_ratio(a) .. '; b = ' .. rat.as_ratio(b) .. '; ab = ' .. c_key)
						mw.log(a_approx .. ' + ' .. b_approx .. ' != ' .. c_approx)
						return false
					end
				end
			end
		end
	end
	return true
end

-- determine maximum error
function p.max_error(et, ratios)
	local maxe = 0.0
	for a_key, a in pairs(ratios) do
		local a_approx = ET.approximate(et, rat.as_float(a))
		local e = math.abs((ET.cents(et, a_approx) - rat.cents(a)) / ET.cents(et, 1))
		if (e > maxe) then
			maxe = e
		end
	end
	return maxe
end

function p.consistent_edos(harmonics, distance, ed, maxlen)
	distance = distance or 1.0
	ed = ed or 'edo'
	maxlen = maxlen or 72
	local all_interval = {}

	for i, h in ipairs(harmonics) do
		-- compute all ratio
		for j, g in ipairs(harmonics) do
			if j > i then
				local a = rat.new(g, h)
				all_interval[rat.as_ratio(a)] = a
			end
		end
	end

	local vals = {}
	for i = 1, 72 do
		local et = ET.parse('' .. i .. ed)
		local consistent = p.additively_consistent_int(et, all_interval, false, previous)
		if consistent then
			local maxe = p.max_error(et, all_interval)
			local dist = 0.5/maxe
			local up = (dist >= distance)
			local llevel = 0
			while (dist >= 2) do
				llevel = llevel + 1
				dist = dist / 2
			end
			if up then
				if #vals >= maxlen then
					table.insert(vals, "&hellip;")
					break
				end
				table.insert(vals, "[[" .. i .. ed .. "]]" .. string.rep("*", llevel))
			end
		end
	end

	return table.concat(vals, ", ")
end

function p.noinfobox_chord(frame)
	local distance = tonumber(frame.args["Distance"])
	local debug_data = ""
	local infobox_data = {}
	local cats = ""

	--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

		if distance == nil then
			if #harmonics >= 5 then
				distance = 1.5
			elseif #harmonics >= 3 then
				distance = 2.0
			else
				distance = 3.0
			end
		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]

		local root_interval_links = {}
		local step_interval_links = {}
		for i, h in ipairs(harmonics) do
			-- 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 .. "]]")

			-- 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 .. "]]")
			end
		end
		
		cat = "(d >= " .. distance .. ") " .. p.consistent_edos(harmonics, distance, 'edo', 4)
	--end

	return cat
end

return p