local bit = require "bit" local ffi = require "ffi" local C = ffi.C cdef_include "midi.h" MIDIStream = DeriveClass(Stream) 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 -- Last value of a specific control channel 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 value = 0 local band, rshift = bit.band, bit.rshift return self:map(function(sample) value = band(sample, 0xFFFF) == filter and tonumber(rshift(sample, 16)) or value return value end) end -- same as Stream:scale() but for values between [0, 127] -- (ie. MIDI CC values) -- FIXME: If Stream:CC() would output between [-1, 1], there would be no need -- for Stream:ccscale(). function Stream:ccscale(v1, v2) local lower = v2 and v1 or 0 local upper = v2 or v1 if type(lower) == "number" and type(upper) == "number" then return self:map(function(x) return (x/127)*(upper - lower) + lower end) else return self*((upper - lower)/127) + lower end end -- Velocity of NOTE ON for a specific note on a channel function Stream:mvelocity(note, channel) -- `note` may be a note name like "A4" 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 on_filter = bit.bor(0x90, channel, bit.lshift(note, 8)) local off_filter = bit.bor(0x80, channel, bit.lshift(note, 8)) local value = 0 local band, rshift = bit.band, bit.rshift return self:map(function(sample) value = band(sample, 0xFFFF) == on_filter and rshift(sample, 16) or band(sample, 0xFFFF) == off_filter and 0 or value return value end) 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" } -- MIDI note number to name -- NOTE: mton() can handle the words as generated by MIDINoteStream function mton(note) note = band(note, 0xFF) local octave = floor(note / 12)-1 return note_names[(note % 12)+1]..octave end 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 -- Note name to MIDI note number function ntom(name) local octave = name:byte(-1) - 48 + 1 return octave*12 + ntom_offsets[name:sub(1, -2)] end 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*math.pow(2, (note - 69)/12) end -- Convert from MIDI note to frequency -- NOTE: mtof() can handle the words as generated by MIDINoteStream function mtof(note) return mtof_cache[band(note, 0xFF)] end function Stream:mtof() return self:map(mtof) end -- Convert from frequency to closest MIDI note function ftom(freq) -- NOTE: math.log/2 is a LuaJIT extension return floor(12*log(freq/440, 2) + 0.5)+69 end function Stream:ftom() return self:map(ftom) end end -- Convert from MIDI name to frequency function ntof(name) return mtof(ntom(name)) end function Stream:ntof() return self:map(ntof) end -- Convert from frequency to closest MIDI note name function fton(freq) return mton(ftom(freq)) end function Stream:fton() return self:map(fton) end -- Tick an instrument only when an inputstream (note_stream), -- gets ~= 0. When it changes back to 0 again, an "off"-stream -- is triggered. This allows the construction of instruments with -- Attack-Sustain and Decay phases based on real-time control signals. -- The note values can be passed into the constructor by using functions -- for the "on" and "off" streams. -- Usually, the note stream will be a MIDIStream: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). 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 function Stream:instrument(on_stream, off_stream) return InstrumentStream:new(self, on_stream, off_stream) end