aboutsummaryrefslogtreecommitdiffhomepage
path: root/midi.lua
diff options
context:
space:
mode:
authorRobin Haberkorn <robin.haberkorn@googlemail.com>2023-09-13 17:26:53 +0300
committerRobin Haberkorn <robin.haberkorn@googlemail.com>2023-09-13 17:26:53 +0300
commit7fc0f17fb37326c86a83627b856a5b75f522c090 (patch)
treebc5de8b30c705afafc9b30e6fd3f5bc2b12f19d6 /midi.lua
parent1cfaa431c1f3c4f417811dcff342c9f28600f13f (diff)
downloadapplause2-7fc0f17fb37326c86a83627b856a5b75f522c090.tar.gz
added LDoc documentation
* gives a useful overview of everything supported right now * especially the type documentation is useful, as these things are not self-evident in Lua (because of dynamic typing). * The LDoc page can later be published as the Github pages of the project. This can even be done automatically by a Github action. However, we should first make sure that it's okay to publish the project before defending the thesis since Github pages will always be public even for private repositories. * Documentation of command-line parameters is lacking (TODO). * It may be possible to use types like "Stream(number)" to describe streams of numbers. The LDoc documentation mentions boxed types. Perhaps there can even be Streamable(number)? * We are also lacking good example programs and/or introductory material.
Diffstat (limited to 'midi.lua')
-rw-r--r--midi.lua168
1 files changed, 145 insertions, 23 deletions
diff --git a/midi.lua b/midi.lua
index 4025fe5..bf24489 100644
--- a/midi.lua
+++ b/midi.lua
@@ -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