From 132dd6354c0d264b4fa2000aac02c9bffc88278a Mon Sep 17 00:00:00 2001 From: Robin Haberkorn Date: Tue, 26 Sep 2023 18:44:43 +0300 Subject: Stream:CC14() added, Stream:CC() outputs normalized values, got rid of Stream:ccscale() * Stream:CC14() allows reading 14-bit CCs (split over two 7-bit MIDI messages). It's actually capable to handle regular 7-bit controllers just fine. The question is whether we still need a distinction between Stream:CC() and Stream:CC14(). Stream:CC14() has a slight overhead, but only when actually receiving MIDI messages. * Stream:CC14() and Stream:CC() output values normalized to [-1,+1] now. We actually never made use of raw CC values and the scaling can be done only once whenever a matching MIDI message is received, so the runtime overhead is irrelevant. * This means we could also get rid of Stream:ccscale() now. Just use Stream:scale() from nowon. * Use Stream:scan() now whenever relying on previous values. That way, you can often avoid having to keep an accumulator variable in the closure. --- TODO | 9 +----- midi.lua | 102 +++++++++++++++++++++++++++++++++++++++------------------------ 2 files changed, 64 insertions(+), 47 deletions(-) diff --git a/TODO b/TODO index d49a831..62cc4cb 100644 --- a/TODO +++ b/TODO @@ -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). diff --git a/midi.lua b/midi.lua index bf24489..252bbfa 100644 --- a/midi.lua +++ b/midi.lua @@ -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 -- cgit v1.2.3