diff options
-rw-r--r-- | TODO | 9 | ||||
-rw-r--r-- | midi.lua | 102 |
2 files changed, 64 insertions, 47 deletions
@@ -6,6 +6,7 @@ # Features +* Real-time input. See inputstream branch. * Line Continuations on the CLI (like Lua's CLI) * CLI auto completions via libreadline. This could directly introspect the Stream object for instance. @@ -48,14 +49,6 @@ Unfortunately, it will not resolve all non-realtime-safe gtick-invocations. It's also questionable what happens when the timestamp wraps. Whether a wrap is safe or not, will depend on the generator. -* Inconsistent signal normalization. - Some signals like Stream:CC() are not normalized to [-1,1], so you need - special scaling methods like Stream:ccscale(). - The supposed advantage is that often a signal between [0,1] is needed, so you only - need a single division. E.g. Stream:mul(Stream:CC(...) / 127). - On the other hand, with normalized outputs, you could also write Stram:mul(Stream:CC(...):scale(1)). - Or there could even be a Stream:vol() method that takes signals between [-1,1]. - The question is whether the JIT compiler is smart enough to optimize this code. * The JIT compiler currently does not emit SIMD instructions (see simd-test.lua). See also https://github.com/LuaJIT/LuaJIT/issues/40 * Perhaps always enable Fused multiply-add (-O+fma). @@ -22,7 +22,7 @@ cdef_include "midi.h" -- in absence of events. -- -- @type MIDIStream --- @usage Stream.SinOsc(440):gain(MIDIStream:CC(0):ccscale()):play() +-- @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) @@ -46,16 +46,19 @@ function MIDIStream:gtick() end end ---- Filter out last value of a specific MIDI control channel. +--- 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 [0,127]. +-- @treturn Stream Stream of numbers between [-1,+1]. -- @see MIDIStream --- @see Stream:ccscale --- @usage Stream.SinOsc(440):gain(MIDIStream:CC(0):ccscale()):play() +-- @usage Stream.SinOsc(440):gain(MIDIStream:CC(0):scale()):play() +-- @fixme Most MIDI software appears to use origin 1 channels and control ids. +-- @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 >= 0x20 for other purposes? function Stream:CC(control, channel) channel = channel or 0 @@ -65,38 +68,59 @@ function Stream:CC(control, channel) "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 + 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 ---- Scale MIDI controller value. --- This is very similar to @{Stream:scale} but works for input values between [0, 127] --- (ie. MIDI CC values). +--- 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 --- @StreamableNumber[opt=0] v1 Delivers the lower value. --- @StreamableNumber v2 Delivers the upper value --- @treturn 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 --- @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 - 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 +-- @usage Stream.SinOsc(440):gain(MIDIStream:CC14(0):scale()):play() +-- @fixme Most MIDI software appears to use origin 1 channels and control ids. +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. @@ -111,6 +135,8 @@ end -- @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 @@ -120,17 +146,15 @@ function Stream:mvelocity(note, channel) 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 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: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 + 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 |