--- --- @module applause --- local bit = require "bit" local ffi = require "ffi" local C = ffi.C cdef_include "midi.h" --- MIDI event stream. -- Since it does not have parameters, you don't necessarily have to instantiate it. -- The class table itself is a valid stream object. -- -- MIDI events are 24-bit words with the -- [following structure](https://www.codecademy.com/resources/docs/markdown/tables): -- -- --
Bit 23-1615-87-43-0 --
Controller value / velocityController number / keyCommand codeChannel number --
-- -- MIDI streams can and are usually ticked at the sample rate but will generate 0 words -- in absence of events. -- -- @type MIDIStream -- @usage Stream.SinOsc(440):gain(MIDIStream:CC(0):scale()):play() -- @fixme Theoretically, we could pass on a C structure as well. -- On the other hand, bit manipulations are necessary anyway to parse the message. MIDIStream = DeriveClass(Stream) --- Create new MIDIStream -- @function new -- @treturn MIDIStream -- @see Stream:CC -- @see Stream:mvelocity function MIDIStream:gtick() return function() -- This is always cached since there is only one MIDI event queue -- and it must not be pulled more than once per tick. local sample = sampleCache[MIDIStream] if not sample then sample = C.applause_pull_midi_sample() sampleCache[MIDIStream] = sample end return sample end end --- Filter out last value of a specific MIDI control channel, scaled to [-1,+1]. -- This remembers the last value and is therefore a stream, representing the controller state. -- It is usually applied on @{MIDIStream}. -- @within Class Stream -- @int control Controller number between [0,127]. -- @int[opt=0] channel MIDI channel between [0,15]. -- @treturn Stream Stream of numbers between [-1,+1]. -- @see MIDIStream -- @usage Stream.SinOsc(440):gain(MIDIStream:CC(0):scale()):play() -- @todo MIDI defines standart controller names. -- Perhaps we want to support these by passing strings. -- @fixme Most MIDI software appears to use origin 1 channels. -- @fixme Is there actually any reason to keep Stream:CC instead of Stream:CC14? -- Stream:CC14 will be slightly slower in case of MIDI events, but those are rare. -- Or are there any controllers that will use controller ids where bit 0x20 is set for other purposes? -- Unfortunately, we cannot be sure of it. function Stream:CC(control, channel) channel = channel or 0 assert(0 <= control and control <= 127, "MIDI control number out of range (0 <= x <= 127)") assert(0 <= channel and channel <= 15, "MIDI channel out of range (0 <= x <= 15)") local filter = bit.bor(0xB0, channel, bit.lshift(control, 8)) local band, rshift = bit.band, bit.rshift return self:scan(function(last, sample) last = last or 0 local sample_masked = band(sample, 0xFFFF) if sample_masked == filter then return tonumber(rshift(sample, 16)*2)/0x7F - 1 end return last end) end --- Filter out last value of a specific relative MIDI control channel. -- This remembers the last value and is therefore a stream, representing the controller state. -- It will currently only work if the CC values are two's complement signed values. -- It is usually applied on @{MIDIStream}. -- @within Class Stream -- @int control Controller number between [0,127]. -- @int[opt=0] channel MIDI channel between [0,15]. -- @number[opt=1000] resolution -- The device resolution, ie. the number of steps between [-1,+1]. -- The larger this value, the longer it takes to move from the minimum to the maximum position. -- @treturn Stream Stream of numbers between [-1,+1]. -- @see MIDIStream -- @see Stream:evrel -- @usage Stream.SinOsc(440):gain(MIDIStream:CCrel(0):scale()):play() -- @todo MIDI defines standart controller names. -- Perhaps we want to support these by passing strings. -- @fixme Most MIDI software appears to use origin 1 channels. function Stream:CCrel(control, channel, resolution) channel = channel or 0 resolution = (resolution or 1000)/2 assert(0 <= control and control <= 127, "MIDI control number out of range (0 <= x <= 127)") assert(0 <= channel and channel <= 15, "MIDI channel out of range (0 <= x <= 15)") local filter = bit.bor(0xB0, channel, bit.lshift(control, 8)) local band, rshift = bit.band, bit.rshift local min, max = math.min, math.max local int8_ct = ffi.typeof('int8_t') return self:scan(function(last, sample) last = last or -1 local sample_masked = band(sample, 0xFFFF) if sample_masked == filter then local rel = tonumber(int8_ct(band(rshift(sample, 16-1), 0xFE)) / 2) return min(max(last+rel/resolution, -1), 1) end return last end) end --- Filter out last value of a specific **14-bit** MIDI control channel, scaled to [-1,+1]. -- This remembers the last value and is therefore a stream, representing the controller state. -- It is usually applied on @{MIDIStream}. -- In contrast to @{Stream:CC}, this supports 14-bit controllers where the -- least-significant byte is sent on controller with offset 0x20. -- @within Class Stream -- @int control Controller number between [0,127]. -- @int[opt=0] channel MIDI channel between [0,15]. -- @treturn Stream Stream of numbers between [-1,+1]. -- @see MIDIStream -- @see Stream:CC -- @usage Stream.SinOsc(440):gain(MIDIStream:CC14(0):scale()):play() -- @todo MIDI defines standart controller names. -- Perhaps we want to support these by passing strings. -- @fixme Most MIDI software appears to use origin 1 channels. function Stream:CC14(control, channel) channel = channel or 0 assert(0 <= control and control <= 127, "MIDI control number out of range (0 <= x <= 127)") assert(0 <= channel and channel <= 15, "MIDI channel out of range (0 <= x <= 15)") local filter_msb = bit.bor(0xB0, channel, bit.lshift(control, 8)) local value_msb = 0 local filter_lsb = bit.bor(0xB0, channel, bit.lshift(bit.bor(0x20, control), 8)) local value_lsb = 0 local band, bor, rshift = bit.band, bit.bor, bit.rshift return self:scan(function(last, sample) last = last or 0 local sample_masked = band(sample, 0xFFFF) if sample_masked == filter_msb then value_msb = rshift(band(sample, 0xFF0000), 8+1) -- There is some redundancy, but it's important to scale here -- in order to optimize the common case (unchanged CC). return tonumber(bor(value_msb, value_lsb)*2)/0x3FFF - 1 elseif sample_masked == filter_lsb then value_lsb = rshift(sample, 16) return tonumber(bor(value_msb, value_lsb)*2)/0x3FFF - 1 end return last end) end --- Filter out last value of a MIDI note velocity. -- This will be a stream of velocity values as long as the given note is on and otherwise 0. -- It is usually applied on @{MIDIStream}. -- @within Class Stream -- @tparam string|int note A MIDI note name (eg. "A4") or number between [0,127]. -- @int[opt=0] channel MIDI channel between [0,15]. -- @treturn Stream -- Stream of velocities between [0,127]. -- The note is on as long as the value is not 0. -- @see MIDIStream -- @see ntom -- @usage Stream.SinOsc(ntof("C4")):gain(MIDIStream:mvelocity("C4") / 127):play() -- @fixme Perhaps it also makes sense to scale to [-1,+1]? -- A velocity will very seldom be used in situations where such a signal is useful, though. function Stream:mvelocity(note, channel) note = type(note) == "string" and ntom(note) or note channel = channel or 0 assert(0 <= note and note <= 127, "MIDI note out of range (0 <= x <= 127)") assert(0 <= channel and channel <= 15, "MIDI channel out of range (0 <= x <= 15)") local filter_on = bit.bor(0x90, channel, bit.lshift(note, 8)) local filter_off = bit.bor(0x80, channel, bit.lshift(note, 8)) local band, rshift = bit.band, bit.rshift return self:scan(function(last, sample) last = last or 0 local sample_masked = band(sample, 0xFFFF) return sample_masked == filter_on and rshift(sample, 16) or sample_masked == filter_off and 0 or last end) end --- --- @section end --- -- -- MIDI primitives -- do local band = bit.band local floor, log = math.floor, math.log local note_names = { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" } --- Convert MIDI note number to name. -- See also the [conversion table](https://newt.phys.unsw.edu.au/jw/notes.html). -- @int note MIDI note number. -- @treturn string MIDI note name in all upper case. -- @see Stream:mton -- @see ntom function mton(note) note = band(note, 0xFF) local octave = floor(note / 12)-1 return note_names[(note % 12)+1]..octave end --- Convert stream of MIDI note numbers to names. -- @within Class Stream -- @treturn Stream -- @see mton function Stream:mton() return self:map(mton) end local ntom_offsets = {} for i, name in ipairs(note_names) do ntom_offsets[name] = i-1 -- Saving the offsets for the lower-cased note names -- avoids a string.upper() call in ntom() ntom_offsets[name:lower()] = i-1 end --- Convert MIDI note name to number. -- See also the [conversion table](https://newt.phys.unsw.edu.au/jw/notes.html). -- @string name MIDI note name (case insensitive). -- @treturn int MIDI note number between [0,127]. -- @see Stream:ntom -- @see mton function ntom(name) local octave = name:byte(-1) - 48 + 1 return octave*12 + ntom_offsets[name:sub(1, -2)] end --- Convert stream of MIDI note names to numbers. -- @within Class Stream -- @treturn Stream -- @see ntom function Stream:ntom() return self:map(ntom) end -- There are only 128 possible MIDI notes, -- so their frequencies can and should be cached. -- We do this once instead of on-demand, so the lookup -- table consists of consecutive numbers. local mtof_cache = table.new(128, 0) for note = 0, 127 do -- MIDI NOTE 69 corresponds to 440 Hz mtof_cache[note] = 440 * 2^((note - 69)/12) end --- Convert from MIDI note to frequency. -- See also the [conversion table](https://newt.phys.unsw.edu.au/jw/notes.html). -- @int note MIDI note number between [0,127]. -- @treturn number Frequency -- @see Stream:mtof -- @see ftom function mtof(note) return mtof_cache[band(note, 0xFF)] end --- Convert stream of MIDI note numbers to frequencies. -- @within Class Stream -- @treturn Stream -- @see mtof function Stream:mtof() return self:map(mtof) end --- Convert from frequency to closest MIDI note. -- @number freq Arbitrary frequency. -- @treturn int Closest MIDI note number. -- @see Stream:ftom -- @see mtof function ftom(freq) -- NOTE: math.log/2 is a LuaJIT extension return floor(12*log(freq/440, 2) + 0.5)+69 end --- Convert stream of frequencies to closest MIDI note numbers. -- @within Class Stream -- @treturn Stream -- @see ftom function Stream:ftom() return self:map(ftom) end end --- Convert from MIDI name to frequency. -- @string name MIDI name (case-insensitive). -- @treturn number Frequency -- @see Stream:ntof -- @see ntom -- @see mtof function ntof(name) return mtof(ntom(name)) end --- Convert stream of MIDI names to frequencies. -- @within Class Stream -- @treturn Stream -- @see ntof -- @usage =tostream{"A4", "B4", "C4"}:ntof() function Stream:ntof() return self:map(ntof) end --- Convert from frequency to closest MIDI note name. -- @number freq Frequency -- @treturn string The all-upper-case MIDI note name. -- @see Stream:fton -- @see ftom -- @see mton function fton(freq) return mton(ftom(freq)) end --- Convert stream of frequencies to closest MIDI note names. -- @within Class Stream -- @treturn Stream -- @see fton function Stream:fton() return self:map(fton) end InstrumentStream = DeriveClass(MuxableStream) InstrumentStream.sig_last_stream = 1 function InstrumentStream:muxableCtor(note_stream, on_stream, off_stream) note_stream = tostream(note_stream) local note_stream_cached if type(on_stream) == "function" then note_stream_cached = note_stream:cache() self.on_stream = on_stream(note_stream_cached) else self.on_stream = tostream(on_stream) end if type(off_stream) == "function" then note_stream_cached = note_stream_cached or note_stream:cache() self.off_stream = off_stream(note_stream_cached) else -- The "off" stream is optional self.off_stream = off_stream and tostream(off_stream) end -- `note_stream` is cached only when required self.note_stream = note_stream_cached or note_stream end function InstrumentStream:gtick() local note_tick = self.note_stream:gtick() local on_stream = self.on_stream local on_stream_inf = on_stream:len() == math.huge local off_stream = self.off_stream local on_tick local function off_tick() return 0 end return function() local note = note_tick() if not note then return end if on_tick == nil then -- no note if note == 0 then return off_tick() or 0 end -- FIXME: This is not strictly real-time safe on_tick = on_stream:gtick() return on_tick() or 0 else -- note on if note ~= 0 then local sample = on_tick() if sample then return sample end -- on_stream must be finite, retrigger on_tick = on_stream:gtick() return on_tick() or 0 elseif not on_stream_inf then -- don't cut off finite on_streams local sample = on_tick() if sample then return sample end end -- FIXME: This is not strictly real-time safe on_tick = nil if off_stream then off_tick = off_stream:gtick() end return off_tick() or 0 end end end function InstrumentStream:len() return self.note_stream:len() end --- Tick instrument streams based on an input stream. -- Once the input stream is unequal to 0, the "on"-stream is activated. -- When it changes back to 0 again, the "off"-stream gets triggered. -- This allows the construction of instruments with -- Attack-Sustain and Decay phases based on real-time control signals. -- Usually, the instrument stream will be applied on @{Stream:mvelocity}, so the two -- instrument streams can be based on the MIDI velocity (but don't have -- to be if the velocity is not important). -- @within Class Stream -- @tparam StreamableNumber|func on_stream -- Stream to trigger when the input value becomes unequal to 0. -- If this is a function, it will be called with the input value (usually a MIDI velocity) -- to generate the "on"-stream. -- @tparam[opt] StreamableNumber|func off_stream -- Stream to trigger when the input value becomes 0. -- If this is a function, it will be called with the previous input value (usually a MIDI velocity) -- to generate the "off"-stream. -- @treturn Stream -- @see Stream:mvelocity -- @usage MIDIStream:mvelocity("C4"):instrument(Stream.SinOsc(ntof("C4"))):play() function Stream:instrument(on_stream, off_stream) return InstrumentStream:new(self, on_stream, off_stream) end