Module:Keyboard

Revision as of 13:19, 17 October 2022 by Plumtree (talk | contribs) (Automatic keyboard layout)
Module documentation[view] [edit] [history] [purge]
Introspection summary for Module:Keyboard 
Functions provided (5)
Line Function Params
11 auto_layout (n, m, convergents)
37 ET (invokable) (frame)
75 build (invokable) (frame)
103 build_ET_keyboard (et, from, to, base_freq, layout, key_width, key_height, dur, gain, instrument, harmonic_spectrum)
125 build_keyboard (freqs, colours, seps, key_width, key_height, dur, gain, instrument, harmonic_spectrum)
Lua modules required (2)
Variable Module Functions used
ET Module:ET parse
approximate
as_string
cents
rat Module:Rational as_pair
as_ratio
eq
convergents

No function descriptions were provided. The Lua code may have further information.


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_ratio(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
	return r_layout .. p.auto_layout(n - r_n, m - r_m, convergents)
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)
	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 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
		s = s .. ' style="width: ' .. key_width .. 'px; height: ' .. key_height .. 'px; border: 1px solid #7f7f7f;'
		if seps[i] then
			s = s .. ' border-left: 1px solid #ff0000;'
		end
		if seps[i + 1] then
			s = s .. ' border-right: 1px solid #ff0000;'
		end
		s = s .. '"></div>'
	end
	s = s .. '</div>'	
	return s
end

return p