Module:Keyboard

From Xenharmonic Wiki
Jump to navigation Jump to search

Documentation transcluded from /doc
Icon-Todo.png Todo: Documentation

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

local layout_cache = {
	["0/1"] = "b",
	["1/1"] = "w"
}
-- `n`: white keys needed
-- `m`: total keys needed
function p.auto_layout(n, m, convergents)
	if m == 0 then
		return ""
	elseif layout_cache[n .. "/" .. m] then
		return layout_cache[n .. "/" .. m]
	end
	local s = ""
	local max_i = -1 / 0
	for i, ratio in ipairs(convergents) do
		local r_n, r_m = rat.as_pair(ratio)
		if r_m >= m or r_n > n then
			break
		elseif n - r_n > m - r_m then
			break
		else
			max_i = i
		end
	end
	local r_n, r_m = rat.as_pair(convergents[max_i])
	local r_layout = p.auto_layout(r_n, r_m, convergents)
	if not layout_cache[r_n .. "/" .. r_m] then
		layout_cache[r_n .. "/" .. r_m] = r_layout
	end
	if n - r_n ~= 1 or m - r_m ~= 2 then
		return r_layout .. p.auto_layout(n - r_n, m - r_m, convergents)
	else
		return r_layout .. "bw"
	end
end

function p.auto_position(layout, compact)
	local data = {}
	local pos = -1
	for i = 1, #layout do
		if compact then
			if layout:sub(i, i) == "w" then
				pos = pos + 1
				table.insert(data, {key = "w", pos = pos, kind = "full"})
			elseif layout:sub(i - 1, i - 1) == "w" and layout:sub(i + 1, i + 1) == "w" then
				table.insert(data, { key = "b", pos = pos, kind = "mid"})
			elseif layout:sub(i - 1, i - 1) == "w" then
				table.insert(data, { key = "b", pos = pos, kind = "right"})
			elseif layout:sub(i + 1, i + 1) == "w" then
				table.insert(data, { key = "b", pos = pos + 1, kind = "left"})
			else
				pos = pos + 1
				table.insert(data, { key = "b", pos = pos, kind = "full"})
			end
		else
			pos = pos + 1
			table.insert(data, { key = layout:sub(i, i), pos = pos, kind = "full" })
		end
	end
	return data
end

function p.ET(frame)
	local et = ET.parse(frame.args[1])
	local from = tonumber(frame.args["From"]) or 0
	local to = tonumber(frame.args["To"]) or et.size - 1
	local base_freq = tonumber(frame.args["Base frequency"]) or 440
	
	local layout = frame.args[2]
	if not layout and rat.eq(et.equave, 2) and et.size > 0 then
		local fifth = ET.approximate(et, 3/2)
		local convergents = rat.convergents(
			math.log(3/2) / math.log(2),
			function(ratio)
				local r_n, r_m = rat.as_pair(ratio)
				return r_m > et.size
			end
		)
		layout = p.auto_layout(fifth, et.size, convergents)
	end
	
	local key_width = tonumber(frame.args["Key width"]) or 30
	local key_height = tonumber(frame.args["Key height"]) or 60
	
	local dur = tonumber(frame.args["Duration"]) or 500
	local gain = tonumber(frame.args["Gain"]) or 0.1
	local instrument = frame.args["Instrument"] or "sine"
	
	local spectrum_arg = frame.args["Harmonic spectrum"]
	local harmonic_spectrum = nil
	if spectrum_arg then
		harmonic_spectrum = {}
		for val in spectrum_arg:gmatch("%S+") do
			table.insert(harmonic_spectrum, tonumber(val))
		end
	end
	
	return p.build_ET_keyboard(et, from, to, base_freq, layout, key_width, key_height, dur, gain, instrument, harmonic_spectrum)
end

function p.build(frame)
	local freqs_arg = frame.args[1]
	local freqs = {}
	for freq in freqs_arg:gmatch("%S+") do
		table.insert(freqs, tonumber(freq))
	end
	
	local colours = frame.args[2]
	
	local key_width = tonumber(frame.args["Key width"]) or 30
	local key_height = tonumber(frame.args["Key height"]) or 60
	
	local dur = tonumber(frame.args["Duration"]) or 500
	local gain = tonumber(frame.args["Gain"]) or 0.1
	local instrument = frame.args["Instrument"] or "sine"
	
	local spectrum_arg = frame.args["Harmonic spectrum"]
	local harmonic_spectrum = nil
	if spectrum_arg then
		harmonic_spectrum = {}
		for val in spectrum_arg:gmatch("%S+") do
			table.insert(harmonic_spectrum, tonumber(val))
		end
	end
	
	return p.build_keyboard(freqs, colours, {}, key_width, key_height, dur, gain, instrument, harmonic_spectrum)
end

function p.build_ET_keyboard(et, from, to, base_freq, layout, key_width, key_height, dur, gain, instrument, harmonic_spectrum)
	if #layout ~= et.size then
		return "<span style=\"color: red;\">Layout for " .. ET.as_string(et) .. " should have exactly " .. et.size .. " elements!</span>"
	end
	local freqs = {}
	local colours = ""
	local seps = {}
	
	for i = from, to do
		local cents = ET.cents(et, i)
		local hz = 2 ^ (math.log(base_freq) / math.log(2) + cents / 1200)
		table.insert(freqs, hz)
		local j = (i % et.size) + 1
		colours = colours .. layout:sub(j, j)
		if j == 1 and i > from then
			seps[i - from + 1] = true
		end
	end
	
	return p.build_keyboard(freqs, colours, seps, key_width, key_height, dur, gain, instrument, harmonic_spectrum)
end

function p.build_keyboard(freqs, colours, seps, key_width, key_height, dur, gain, instrument, harmonic_spectrum)
	key_width = key_width - (key_width % 6)
	key_height = key_height - (key_height % 20)
	if #freqs ~= #colours then
		return "<span style=\"color: red;\">Number of keys (" .. (#freqs) .. ") and of colours (" .. (#colours) .. ") are different!</span>"
	end
	local n = #freqs
	local positions = p.auto_position(colours, true)
	local s = "<div style=\"display: flex; width: fit-content;\">"
	for i = 1, n do
		s = s .. "<div class=\"sequence-audio sequence-audio-button"
		if colours:sub(i, i) == "w" then
			 s = s .. " white-key"
		elseif colours:sub(i, i) == "b" then
			 s = s .. " black-key"
		end
		s = s .. "\" data-sequence=\"" .. freqs[i] .. ":" .. dur .. ":" .. gain .. ":0:" .. instrument .. "\""
		if harmonic_spectrum then
			s = s .. " data-timbre-" .. instrument .. "=\"" .. table.concat(harmonic_spectrum, " ") .. "\""
		end
		local width = key_width
		local height = key_height
		if positions[i].kind == "mid" then
			width = 2 * (width / 3)
			height = 13 * (height / 20)
		elseif positions[i].kind == "left" or positions[i].kind == "right" then
			width = width / 2
			height = 13 * (height / 20)
		end
		s = s .. " style=\"width: " .. (width - 2) .. "px; height: " .. (height - 2) .. "px; border: 1px solid #7f7f7f;"
		if positions[i].kind == "mid" then
			s = s .. " position: absolute; z-index: 1; left: " .. (positions[i].pos * key_width + key_width - width / 2) .. "px;"
		elseif positions[i].kind == "left" then
			s = s .. " position: absolute; z-index: 1; left: " .. (positions[i].pos * key_width) .. "px;"
		elseif positions[i].kind == "right" then
			s = s .. " position: absolute; z-index: 1; left: " .. (positions[i].pos * key_width + key_width - width) .. "px;"
		end
		if seps[i] and (positions[i].kind == "full" or positions[i].kind == "left") then
			s = s .. " border-left: 1px solid #ff0000;"
		elseif seps[i - 1] and positions[i - 1].kind == "left" then
			s = s .. " border-left: 1px solid #ff0000;"
		end
		if seps[i + 1] and (positions[i].kind == "full" or positions[i].kind == "right") then
			s = s .. " border-right: 1px solid #ff0000;"
		elseif seps[i + 2] and positions[i + 1].kind == "mid" then
			s = s .. " border-right: 1px solid #ff0000;"
		end
		s = s .. "\"></div>"
	end
	s = s .. "</div>"	
	return s
end

return p