diff options
Diffstat (limited to 'midi.lua')
-rw-r--r-- | midi.lua | 168 |
1 files changed, 145 insertions, 23 deletions
@@ -1,11 +1,38 @@ +--- +--- @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-16 | 15-8 | 7-4 | 3-0 | +-- | --------- | ---- | --- | --- | +-- | Controller value / velocity | Controller number / key | Command code | Channel 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):ccscale()):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 @@ -19,7 +46,16 @@ function MIDIStream:gtick() end end --- Last value of a specific control channel +--- Filter out last value of a specific MIDI control channel. +-- 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 [0,127]. +-- @see MIDIStream +-- @see Stream:ccscale +-- @usage Stream.SinOsc(440):gain(MIDIStream:CC(0):ccscale()):play() function Stream:CC(control, channel) channel = channel or 0 @@ -39,9 +75,16 @@ function Stream:CC(control, channel) 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 +--- Scale MIDI controller value. +-- This is very similar to @{Stream:scale} but works for input values between [0, 127] +-- (ie. MIDI CC values). +-- @within Class Stream +-- @StreamableNumber[opt=0] v1 Delivers the lower value. +-- @StreamableNumber v2 Delivers the upper value +-- @treturn Stream +-- @see Stream:CC +-- @see Stream:scale +-- @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 @@ -56,9 +99,19 @@ function Stream:ccscale(v1, v2) end end --- Velocity of NOTE ON for a specific note on a channel +--- 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() 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 @@ -81,6 +134,10 @@ function Stream:mvelocity(note, channel) end) end +--- +--- @section end +--- + -- -- MIDI primitives -- @@ -94,14 +151,22 @@ do "F#", "G", "G#", "A", "A#", "B" } - -- MIDI note number to name - -- NOTE: mton() can handle the words as generated by MIDINoteStream + --- 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 = {} @@ -112,12 +177,21 @@ do ntom_offsets[name:lower()] = i-1 end - -- Note name to MIDI note number + --- 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, @@ -130,40 +204,68 @@ do mtof_cache[note] = 440 * 2^((note - 69)/12) end - -- Convert from MIDI note to frequency - -- NOTE: mtof() can handle the words as generated by MIDINoteStream + --- 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 + --- 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 +--- 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 +--- 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 --- 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 @@ -234,6 +336,26 @@ 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 |