diff options
Diffstat (limited to 'applause.lua')
-rw-r--r-- | applause.lua | 418 |
1 files changed, 331 insertions, 87 deletions
diff --git a/applause.lua b/applause.lua index a822cc0..d72f4dc 100644 --- a/applause.lua +++ b/applause.lua @@ -64,10 +64,12 @@ cdef_safe[[ enum applause_audio_state { APPLAUSE_AUDIO_OK = 0, APPLAUSE_AUDIO_INTERRUPTED, - APPLAUSE_AUDIO_XRUN + APPLAUSE_AUDIO_XRUN, + APPLAUSE_AUDIO_INVALID_PORT }; -enum applause_audio_state applause_push_sample(double sample_double); +enum applause_audio_state applause_push_sample(int output_port_id, + double sample_double); int applause_midi_velocity_getvalue(int note, int channel); int applause_midi_note_getvalue(int channel); @@ -183,6 +185,9 @@ end -- without knowing that the table at hand is an object Stream.is_a_stream = true +-- All streams except the special MuxStream are mono +Stream.channels = 1 + -- A stream, produces an infinite number of the same value by default -- (eternal quietness by default) function Stream:tick() @@ -200,6 +205,7 @@ end -- Explicitly clock-sync a stream. -- FIXME: This should be done automatically by an optimizer stage. +-- FIXME: That is counter-productive for simple number streams function Stream:sync() return SyncedStream:new(self) end @@ -405,16 +411,14 @@ function Stream:len() return math.huge -- infinity end -function Stream:play() - self:reset() - - local tick = self:tick() +function Stream:play(first_port) + first_port = first_port or 1 + first_port = first_port - 1 -- Make sure JIT compilation is turned on for the generator function -- and all subfunctions. -- This should not be necessary theoretically. jit.on(true, true) - jit.on(tick, true) -- Perform garbage collection cycle and tweak it -- to be more realtime friendly. @@ -427,26 +431,30 @@ function Stream:play() local old_pause = collectgarbage("setpause", 100) local old_stepmul = collectgarbage("setstepmul", 100) + local channels = self.channels local state - repeat - -- Advance clock - clock_signal = not clock_signal + self:foreach(function(frame) + -- Loop should get unrolled automatically + for i = 1, channels do + local sample = tonumber(frame[i]) + assert(sample ~= nil) + + -- NOTE: Invalid port Ids are currently silently + -- ignored. Perhaps it's better to check state or + -- to access output_ports_count from applause.c. + state = C.applause_push_sample(first_port+i, sample) + + -- React to buffer underruns. + -- This is done here instead of in the realtime thread + -- even though it is already overloaded, so as not to + -- affect other applications in the Jack graph. + if state == C.APPLAUSE_AUDIO_XRUN then + io.stderr:write("WARNING: Buffer underrun detected\n") + end - local sample = tick() - if not sample then break end - - -- FIXME: What if the sample is not a number, - -- perhaps we should check that here - state = C.applause_push_sample(sample) - - -- React to buffer underruns. - -- This is done here instead of in the realtime thread - -- even though it is already overloaded, so as not to - -- affect other applications in the Jack graph. - if state == C.APPLAUSE_AUDIO_XRUN then - io.stderr:write("WARNING: Buffer underrun detected\n") + if state == C.APPLAUSE_AUDIO_INTERRUPTED then return true end end - until state == C.APPLAUSE_AUDIO_INTERRUPTED + end) collectgarbage("setpause", old_pause) collectgarbage("setstepmul", old_stepmul) @@ -461,18 +469,19 @@ function Stream:fork() error("C function not registered!") end +-- NOTE: This implementation is for single-channel streams +-- only. See also MuxStream:foreach() function Stream:foreach(fnc) self:reset() + local frame = table.new(1, 0) local tick = self:tick() while true do clock_signal = not clock_signal - local sample = tick() - if not sample then break end - - fnc(sample) + frame[1] = tick() + if not frame[1] or fnc(frame) then break end end end @@ -482,13 +491,28 @@ function Stream:save(filename, format) error("Cannot save infinite stream") end + local channels = self.channels local hnd = sndfile:new(filename, "SFM_WRITE", - samplerate, 1, format) + samplerate, channels, format) + + local frame_buffer = sndfile.frame_type(channels) + + self:foreach(function(frame) + -- NOTE: This should be (hopefully) automatically + -- unrolled for single-channel streams + -- Otherwise each loop copies an entire frame. + -- This should be faster than letting LuaJIT translate + -- the frame directly. + for i = 1, channels do + local sample = tonumber(frame[i]) + assert(sample ~= nil) + frame_buffer[i-1] = sample + end - self:foreach(function(sample) - -- FIXME: What if the sample is not a number, - -- perhaps we should check that here - hnd:write(sample) + -- NOTE: Apparently we cannot use hnd:write() if a frame is larger than one sample + -- (i.e. multichannel streams) + -- FIXME: Check return value + hnd:writef(frame_buffer) end) hnd:close() @@ -499,21 +523,31 @@ function Stream:totable() error("Cannot serialize infinite stream") end - local vector = table.new(self:len(), 0) + local channels = self.channels + local channel_vectors = table.new(channels, 0) + + for i = 1, channels do + channel_vectors[i] = table.new(self:len(), 0) + end - self:foreach(function(sample) - vector[#vector + 1] = sample + self:foreach(function(frame) + -- Loop should be unrolled automatically + for i = 1, channels do + channel_vectors[i][#channel_vectors[i] + 1] = frame[i] + end end) - return vector + -- Return a list of vectors, one per channel + return unpack(channel_vectors) end -- Effectively eager-evaluates the stream returning -- an array-backed stream. function Stream:eval() - return VectorStream:new(self:totable()) + return MuxStream:new(self:totable()) end +-- NOTE: This will only plot the stream's first channel function Stream:toplot(rows, cols) rows = rows or 25 cols = cols or 80 @@ -558,8 +592,9 @@ function Stream:pipe(prog, vbufmode, vbufsize) local hnd = io.popen(prog, "w") hnd:setvbuf(vbufmode or "full", vbufsize) - self:foreach(function(sample) - hnd:write(sample, "\n") + self:foreach(function(frame) + hnd:write(unpack(frame)) + hnd:write("\n") end) hnd:close() @@ -579,20 +614,34 @@ function Stream:gnuplot() local second = sec() local i = 1 - self:foreach(function(sample) - hnd:write(i/second, " ", sample, "\n") + self:foreach(function(frame) + hnd:write(i/second, " ", unpack(frame)) + hnd:write("\n") i = i + 1 end) hnd:close() end +function Stream:mux(...) + return MuxStream:new(self, ...) +end + +-- For single-channel streams only, see also MuxStream:demux() +function Stream:demux(i, j) + j = j or i + assert(i == 1 and j == 1, + "Invalid channel range specified (mono-channel stream)") + return self +end + -- Stream metamethods -- NOTE: Currently non-functional since Lua 5.1 does not -- consider metamethods when evaluating the length operator. function Stream:__len() return self:len() end +-- NOTE: Will only convert the first channel function Stream:__tostring() local t @@ -662,9 +711,151 @@ function Stream.__le(op1, op2) return op1:len() <= op2:len() end -SyncedStream = DeriveClass(Stream) +MuxStream = DeriveClass(Stream) + +function MuxStream:ctor(...) + self.streams = {} + for k, stream in ipairs{...} do + stream = tostream(stream) + if stream.channels == 1 then + table.insert(self.streams, stream) + else + for _, v in ipairs(stream.streams) do + table.insert(self.streams, v) + end + end + if stream:len() ~= self.streams[1]:len() then + error("Incompatible length of stream "..k) + end + end + self.channels = #self.streams + + -- Single-channel streams must not be MuxStream! + -- This means that MuxStream:new() can be used as a + -- kind of multi-channel aware tostream() and is also + -- the inverse of totable() + if self.channels == 1 then return self.streams[1] end +end + +function MuxStream:tick() + error("MuxStreams cannot be ticked") +end + +function MuxStream:len() + -- All channel streams have the same length + return self.streams[1]:len() +end + +-- Overrides Stream:demux() +function MuxStream:demux(i, j) + j = j or i + assert(1 <= i and i <= self.channels and + 1 <= j and j <= self.channels and i <= j, + "Invalid channel range specified") + + -- NOTE: We cannot create single-channel MuxStreams + return i == j and self.streams[i] + or MuxStream:new(unpack(self.streams, i, j)) +end + +-- Overrides Stream:foreach() +-- NOTE: This could easily be integrated into Stream:foreach(), +-- however this results in the loop to be unrolled explicitly +-- for single-channel streams. +function MuxStream:foreach(fnc) + self:reset() + + local ticks = {} + for i = 1, #self.streams do + ticks[i] = self.streams[i]:tick() + end + + local channels = self.channels + local frame = table.new(channels, 0) + + while true do + clock_signal = not clock_signal + + for i = 1, channels do + frame[i] = ticks[i]() + -- Since all streams must have the same + -- length, if one ends all end + if not frame[i] then return end + end + + if fnc(frame) then break end + end +end + +-- Base class for all streams that operate on arbitrary numbers +-- of other streams. Handles muxing opaquely. +MuxableStream = DeriveClass(Stream) + +-- Describes the part of the muxableCtor's signature +-- containing muxable streams. +-- By default all arguments are muxable streams. +MuxableStream.sig_first_stream = 1 +MuxableStream.sig_last_stream = -1 + +function MuxableStream:ctor(...) + local args = {...} + + -- automatic base constructor call, ignore + if #args == 0 then return end + + local first_stream = self.sig_first_stream + local last_stream = self.sig_last_stream > 0 and + self.sig_last_stream or #args + local channels + + for i = first_stream, last_stream do + -- Streamify all stream arguments + args[i] = tostream(args[i]) + -- The first non-mono stream determines the number of + -- channels to check for + channels = channels or (args[i].channels > 1 and args[i].channels) + end + + if not channels then + -- all mono-streams + return self:muxableCtor(unpack(args)) + end + + for i = first_stream, last_stream do + -- Single-channel (non-MuxStream) streams are blown up + -- to the final number of channels + if args[i].channels == 1 then + local synced = args[i]:sync() + -- FIXME: May need a list creation function + local duped_channels = {} + for j = 1, channels do + duped_channels[j] = synced + end + args[i] = MuxStream:new(unpack(duped_channels)) + end -function SyncedStream:ctor(stream) + -- Otherwise all stream arguments must have the same number of channels + assert(args[i].channels == channels, + "Incompatible number of channels") + end + + local channel_streams = {} + local mono_args = {...} + + for channel = 1, args[first_stream].channels do + for i = first_stream, last_stream do + mono_args[i] = args[i].streams[channel] + end + + channel_streams[channel] = self.base:new(unpack(mono_args)) + end + + return MuxStream:new(unpack(channel_streams)) +end + +SyncedStream = DeriveClass(MuxableStream) + +function SyncedStream:muxableCtor(stream) self.streams = {stream} end @@ -680,7 +871,7 @@ function SyncedStream:tick() local tick = self.streams[1]:tick() - self.syncedTick = function() + function self.syncedTick() if clock_signal ~= last_clock then last_clock = clock_signal last_sample = tick() @@ -692,8 +883,14 @@ function SyncedStream:tick() return self.syncedTick end +function SyncedStream:len() + return self.streams[1]:len() +end + VectorStream = DeriveClass(Stream) +-- NOTE: This is mono-streams only, the inverse of Stream:totable() +-- is MuxStream:new() which will also work for single streams function VectorStream:ctor(vector) self.vector = vector end @@ -715,7 +912,8 @@ end -- NOTE: A SndfileStream itself cannot currently be reused within -- one high-level stream (i.e. UGen graph). -- SndfileStream:sync() must be called to wrap it in a --- synced stream manually. +-- synced stream manually. This is done automatically and necessarily +-- for multi-channel streams already. -- FIXME: This will no longer be necessary when syncing -- streams automatically in an optimization phase. SndfileStream = DeriveClass(Stream) @@ -724,6 +922,17 @@ function SndfileStream:ctor(filename) -- FIXME: This fails if the file is not at the -- correct sample rate. Need to resample... self.handle = sndfile:new(filename, "SFM_READ") + + if self.handle.info.channels > 1 then + local synced = self:sync() + local streams = {} + for i = 0, self.handle.info.channels-1 do + streams[i+1] = synced:map(function(frame) + return tonumber(frame[i]) + end) + end + return MuxStream:new(unpack(streams)) + end end function SndfileStream:reset() @@ -734,8 +943,20 @@ end function SndfileStream:tick() local handle = self.handle - return function() - return handle:read() + if handle.info.channels == 1 then + return function() + return handle:read() + end + else + -- For multi-channel audio files, we generate a stream + -- of frame buffers. + -- However, the user never sees these since they are translated + -- to a MuxStream automatically (see ctor()) + local frame = sndfile.frame_type(handle.info.channels) + + return function() + return handle:readf(frame) and frame or nil + end end end @@ -749,12 +970,11 @@ function SndfileStream:close() self.handle:close() end -ConcatStream = DeriveClass(Stream) +ConcatStream = DeriveClass(MuxableStream) -function ConcatStream:ctor(...) +function ConcatStream:muxableCtor(...) self.streams = {} for _, v in ipairs{...} do - v = tostream(v) if v:instanceof(ConcatStream) then -- Optimization: Avoid redundant -- ConcatStream objects @@ -807,10 +1027,13 @@ function ConcatStream:len() return len end -RepeatStream = DeriveClass(Stream) +RepeatStream = DeriveClass(MuxableStream) + +-- we have a trailing non-stream argument +RepeatStream.sig_last_stream = 1 -function RepeatStream:ctor(stream, repeats) - self.streams = {tostream(stream)} +function RepeatStream:muxableCtor(stream, repeats) + self.streams = {stream} self.repeats = repeats or math.huge end @@ -842,10 +1065,10 @@ end -- This removes one level of nesting from nested streams -- (e.g. streams of streams), and is semantically similar -- to folding the stream with the Concat operation. -RavelStream = DeriveClass(Stream) +RavelStream = DeriveClass(MuxableStream) -function RavelStream:ctor(stream) - self.streams = {tostream(stream)} +function RavelStream:muxableCtor(stream) + self.streams = {stream} end function RavelStream:tick() @@ -925,10 +1148,13 @@ function IotaStream:len() end -- i and j have the same semantics as in string.sub() -SubStream = DeriveClass(Stream) +SubStream = DeriveClass(MuxableStream) + +-- We have trailing non-stream arguments +SubStream.sig_last_stream = 1 -function SubStream:ctor(stream, i, j) - self.streams = {tostream(stream)} +function SubStream:muxableCtor(stream, i, j) + self.streams = {stream} self.i = i self.j = j or -1 @@ -967,12 +1193,12 @@ end -- FIXME: Will not work for non-samlpe streams -- This should be split into a generic (index) and -- sample-only (interpolate) operation -IndexStream = DeriveClass(Stream) +IndexStream = DeriveClass(MuxableStream) -function IndexStream:ctor(stream, index_stream) +function IndexStream:muxableCtor(stream, index_stream) -- NOTE: For stream resetting to work and to simplify -- future optimization passes, all streams are in the streams array - self.streams = {tostream(stream), tostream(index_stream)} + self.streams = {stream, index_stream} end function IndexStream:tick() @@ -1019,10 +1245,13 @@ function IndexStream:len() return self.streams[2]:len() end -MapStream = DeriveClass(Stream) +MapStream = DeriveClass(MuxableStream) -function MapStream:ctor(stream, fnc) - self.streams = {tostream(stream)} +-- We have trailing non-stream arguments +MapStream.sig_last_stream = 1 + +function MapStream:muxableCtor(stream, fnc) + self.streams = {stream} self.fnc = fnc end @@ -1039,10 +1268,13 @@ function MapStream:len() return self.streams[1]:len() end -ScanStream = DeriveClass(Stream) +ScanStream = DeriveClass(MuxableStream) + +-- We have trailing non-stream arguments +ScanStream.sig_last_stream = 1 -function ScanStream:ctor(stream, fnc) - self.streams = {tostream(stream)} +function ScanStream:muxableCtor(stream, fnc) + self.streams = {stream} self.fnc = fnc end @@ -1063,10 +1295,13 @@ function ScanStream:len() return self.streams[1]:len() end -FoldStream = DeriveClass(Stream) +FoldStream = DeriveClass(MuxableStream) + +-- We have trailing non-stream arguments +FoldStream.sig_last_stream = 1 -function FoldStream:ctor(stream, fnc) - self.streams = {tostream(stream)} +function FoldStream:muxableCtor(stream, fnc) + self.streams = {stream} self.fnc = fnc end @@ -1094,14 +1329,16 @@ end -- ZipStream combines any number of streams into a single -- stream using a function. This is the basis of the "+" -- and "*" operations. -ZipStream = DeriveClass(Stream) +ZipStream = DeriveClass(MuxableStream) -function ZipStream:ctor(fnc, ...) +-- We have a leading non-stream argument +ZipStream.sig_first_stream = 2 + +function ZipStream:muxableCtor(fnc, ...) self.fnc = fnc self.streams = {} for _, v in ipairs{...} do - v = tostream(v) if v:instanceof(ZipStream) and v.fnc == fnc then -- Optimization: Avoid redundant -- ZipStream objects @@ -1173,6 +1410,7 @@ function ZipStream:len() return max end +-- FIXME: Different kinds of Noise, e.g. pink or brown noise NoiseStream = DeriveClass(Stream) function NoiseStream:tick() @@ -1425,12 +1663,12 @@ local function ddn(f) (f < -1e-15 and f > -1e15 and f or 0) end -LPFStream = DeriveClass(Stream) +LPFStream = DeriveClass(MuxableStream) -function LPFStream:ctor(stream, freq) +function LPFStream:muxableCtor(stream, freq) -- NOTE: For stream resetting to work and to simplify -- future optimization passes, all streams are in the streams array - self.streams = {tostream(stream), tostream(freq)} + self.streams = {stream, freq} end function LPFStream:tick() @@ -1485,12 +1723,12 @@ function LPFStream:len() return self.streams[1]:len() end -HPFStream = DeriveClass(Stream) +HPFStream = DeriveClass(MuxableStream) -function HPFStream:ctor(stream, freq) +function HPFStream:muxableCtor(stream, freq) -- NOTE: For stream resetting to work and to simplify -- future optimization passes, all streams are in the streams array - self.streams = {tostream(stream), tostream(freq)} + self.streams = {stream, freq} end function HPFStream:tick() @@ -1552,12 +1790,15 @@ end -- NOTE: The quality factor, indirectly proportional -- to the passband width -BPFStream = DeriveClass(Stream) +BPFStream = DeriveClass(MuxableStream) + +-- Trailing non-stream arguments +BPFStream.sig_last_stream = 2 -function BPFStream:ctor(stream, freq, quality) +function BPFStream:muxableCtor(stream, freq, quality) -- NOTE: For stream resetting to work and to simplify -- future optimization passes, all streams are in the streams array - self.streams = {tostream(stream), tostream(freq)} + self.streams = {stream, freq} -- FIXME: Does this make sense to be a stream? self.quality = quality end @@ -1619,10 +1860,13 @@ end -- NOTE: The quality factor, indirectly proportional -- to the passband width -BRFStream = DeriveClass(Stream) +BRFStream = DeriveClass(MuxableStream) + +-- Trailing non-stream arguments +BRFStream.sig_last_stream = 2 -function BRFStream:ctor(stream, freq, quality) - self.streams = {tostream(stream), tostream(freq)} +function BRFStream:muxableCtor(stream, freq, quality) + self.streams = {stream, freq} -- FIXME: Does this make sense to be a stream? self.quality = quality end |