aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--applause.c263
-rw-r--r--applause.lua418
-rw-r--r--sndfile.lua17
3 files changed, 503 insertions, 195 deletions
diff --git a/applause.c b/applause.c
index 8f3323a..7ec50b8 100644
--- a/applause.c
+++ b/applause.c
@@ -39,18 +39,27 @@
#define MAX(X,Y) ((X) > (Y) ? (X) : (Y))
-static jack_port_t *output_port;
static jack_client_t *client = NULL;
-#define DEFAULT_BUFFER_SIZE 100 /* milliseconds */
-static jack_ringbuffer_t *buffer = NULL;
-static int buffer_sem;
-/**
- * True if a buffer underrun occurs.
- * FIXME: sig_atomic_t is probably the wrong type.
- * Perhaps use the Mintomic types.
- */
-static sig_atomic_t buffer_xrun = 0;
+#define DEFAULT_NUMBER_OF_OUTPUT_PORTS 1
+#define DEFAULT_BUFFER_SIZE 100 /* milliseconds */
+
+typedef struct applause_output_port {
+ jack_port_t *jack_port;
+ jack_ringbuffer_t *buffer;
+ int buffer_sem;
+ /**
+ * True if a buffer underrun occurs.
+ * FIXME: sig_atomic_t is probably the wrong type.
+ * Perhaps use the Mintomic types.
+ */
+ sig_atomic_t buffer_xrun;
+} applause_output_port;
+
+/** List of Applause output ports */
+static applause_output_port *output_ports = NULL;
+/** Number of Applause output ports */
+static int output_ports_count = DEFAULT_NUMBER_OF_OUTPUT_PORTS;
static sig_atomic_t interrupted = 0;
@@ -167,37 +176,38 @@ signal_handler(int signum)
static int
jack_process(jack_nframes_t nframes, void *arg)
{
- jack_default_audio_sample_t *out;
- size_t len = sizeof(*out)*nframes;
- size_t r;
+ size_t len = sizeof(jack_default_audio_sample_t)*nframes;
void *midi_in;
jack_nframes_t midi_events;
- out = (jack_default_audio_sample_t *)
- jack_port_get_buffer(output_port, nframes);
+ for (int i = 0; i < output_ports_count; i++) {
+ applause_output_port *port = output_ports + i;
+ jack_default_audio_sample_t *out;
+ size_t r;
- /**
- * @bug Do we have to care about more than one
- * channel per frame?
- */
- r = jack_ringbuffer_read(buffer, (char *)out, len);
+ out = (jack_default_audio_sample_t *)
+ jack_port_get_buffer(port->jack_port, nframes);
- /*
- * The semaphor value corresponds with the number of
- * writable bytes in buffer, i.e. the available space.
- * This operation should never block and is supposed to
- * be real-time safe :-)
- */
- if (r > 0)
- svsem_op(buffer_sem, r);
+ r = jack_ringbuffer_read(port->buffer, (char *)out, len);
- /*
- * Here we're assuming that memset() is realtime-capable.
- * It might not be on every UNIX!?
- */
- memset((char *)out + r, 0, len - r);
- buffer_xrun |= len - r > 0;
+ /*
+ * The semaphor value corresponds with the number of
+ * writable bytes in buffer, i.e. the available space.
+ * This operation should never block and is supposed to
+ * be real-time safe :-)
+ */
+ if (r > 0)
+ svsem_op(port->buffer_sem, r);
+
+ /*
+ * Add silence for missing output samples.
+ * Here we're assuming that memset() is realtime-capable.
+ * It might not be on every UNIX!?
+ */
+ memset((char *)out + r, 0, len - r);
+ port->buffer_xrun |= len - r > 0;
+ }
/*
* MIDI processing.
@@ -284,28 +294,76 @@ init_audio(int buffer_size)
fprintf(stderr, "unique name `%s' assigned\n", client_name);
}
- /* tell the JACK server to call `process()' whenever
- there is work to be done.
- */
-
- jack_set_process_callback(client, jack_process, 0);
+ /*
+ * Tell the JACK server to call `jack_process()' whenever
+ * there is work to be done.
+ * This will fill all output buffers and handle MIDI events.
+ */
+ jack_set_process_callback(client, jack_process, NULL);
- /* tell the JACK server to call `jack_shutdown()' if
- it ever shuts down, either entirely, or if it
- just decides to stop calling us.
- */
+ /*
+ * Tell the JACK server to call `jack_shutdown()' if
+ * it ever shuts down, either entirely, or if it
+ * just decides to stop calling us.
+ */
+ jack_on_shutdown(client, jack_shutdown, NULL);
- jack_on_shutdown (client, jack_shutdown, 0);
+ /*
+ * Calculate the buffer size in bytes given the `buffer_size`
+ * in milliseconds.
+ * Make sure it is at least the Jack server's buffer size,
+ * else jack_process() is very likely not able to provide
+ * enough bytes.
+ * FIXME: The Jack server's sample rate and buffer size can
+ * theoretically change at runtime but currently,
+ * the buffer size is not adapted
+ * which means it could be too small after the change.
+ */
+ buffer_bytes = sizeof(jack_default_audio_sample_t)*
+ MAX(jack_get_sample_rate(client)*buffer_size/1000,
+ jack_get_buffer_size(client));
/*
* Create output ports
*/
- output_port = jack_port_register (client, "output",
- JACK_DEFAULT_AUDIO_TYPE,
- JackPortIsOutput, 0);
- if (output_port == NULL) {
- fprintf(stderr, "no more JACK ports available\n");
- return 1;
+ free(output_ports);
+ output_ports = calloc(output_ports_count, sizeof(*output_ports));
+
+ for (int i = 0; i < output_ports_count; i++) {
+ applause_output_port *port = output_ports + i;
+ char name[256];
+
+ /*
+ * NOTE: Port names must be unique.
+ * FIXME: Make the names configurable.
+ */
+ snprintf(name, sizeof(name), "output_%d", i+1);
+
+ port->jack_port = jack_port_register(client, name,
+ JACK_DEFAULT_AUDIO_TYPE,
+ JackPortIsOutput, 0);
+ if (!port->jack_port) {
+ fprintf(stderr, "No more JACK ports available\n");
+ return 1;
+ }
+
+ /*
+ * Initialize ring buffer of samples.
+ * The semaphore is initialized with the same size
+ * since it represents the available bytes in the ring
+ * buffer.
+ */
+ port->buffer = jack_ringbuffer_create(buffer_bytes);
+ if (!port->buffer) {
+ fprintf(stderr, "cannot create ringbuffer\n");
+ return 1;
+ }
+
+ port->buffer_sem = svsem_init(buffer_bytes);
+ if (port->buffer_sem < 0) {
+ fprintf(stderr, "error initializing semaphore\n");
+ return 1;
+ }
}
/*
@@ -331,39 +389,6 @@ init_audio(int buffer_size)
pthread_mutex_init(&midi_mutex, &prioinherit);
- /*
- * Calculate the buffer size in bytes given the `buffer_size`
- * in milliseconds.
- * Make sure it is at least the Jack server's buffer size,
- * else jack_process() is very likely not able to provide
- * enough bytes.
- * FIXME: The Jack server's sample rate and buffer size can
- * theoretically change at runtime but currently,
- * the buffer size is not adapted
- * which means it could be too small after the change.
- */
- buffer_bytes = sizeof(jack_default_audio_sample_t)*
- MAX(jack_get_sample_rate(client)*buffer_size/1000,
- jack_get_buffer_size(client));
-
- /*
- * Initialize ring buffer of samples.
- * The semaphore is initialized with the same size
- * since it represents the available bytes in the ring
- * buffer.
- */
- buffer = jack_ringbuffer_create(buffer_bytes);
- if (!buffer) {
- fprintf(stderr, "cannot create ringbuffer\n");
- return 1;
- }
-
- buffer_sem = svsem_init(buffer_bytes);
- if (buffer_sem < 0) {
- fprintf(stderr, "error initializing semaphore\n");
- return 1;
- }
-
/* Tell the JACK server that we are ready to roll. Our
* process() callback will start running now. */
@@ -379,19 +404,24 @@ init_audio(int buffer_size)
* "input" to the backend, and capture ports are "output" from
* it.
*/
-
- ports = jack_get_ports (client, NULL, NULL,
- JackPortIsPhysical|JackPortIsInput);
+ ports = jack_get_ports(client, NULL, NULL,
+ JackPortIsPhysical | JackPortIsInput);
if (ports == NULL) {
fprintf(stderr, "no physical playback ports\n");
- return 1;
+ return 0;
}
- if (jack_connect (client, jack_port_name (output_port), ports[0])) {
- fprintf (stderr, "cannot connect output ports\n");
+ for (int i = 0; i < output_ports_count && ports[i]; i++) {
+ if (jack_connect(client, jack_port_name(output_ports[i].jack_port),
+ ports[i])) {
+ fprintf(stderr, "Cannot connect port %s to %s\n",
+ jack_port_name(output_ports[i].jack_port),
+ ports[i]);
+ }
}
- free (ports);
+ jack_free(ports);
+
return 0;
}
@@ -512,7 +542,8 @@ applause_midi_cc_getvalue(int control, int channel)
enum applause_audio_state {
APPLAUSE_AUDIO_OK = 0,
APPLAUSE_AUDIO_INTERRUPTED,
- APPLAUSE_AUDIO_XRUN
+ APPLAUSE_AUDIO_XRUN,
+ APPLAUSE_AUDIO_INVALID_PORT
};
/**
@@ -521,8 +552,10 @@ enum applause_audio_state {
* This function should be called from the Stream:play()
* implementation, which is faster than calling Lua functions
* from a C implementation of Stream:play() using the Lua C API
- * (supposedly, I could not reproduce this).
+ * (supposedly - I could not reproduce this).
*
+ * @param output_port_id The number of the output port.
+ * The first one being 1.
* @param sample_double The audio sample as a double (compatible
* with lua_Number).
* This is speed relevant since otherwise
@@ -530,11 +563,26 @@ enum applause_audio_state {
* float type.
*/
enum applause_audio_state
-applause_push_sample(double sample_double)
+applause_push_sample(int output_port_id, double sample_double)
{
+ applause_output_port *port;
jack_default_audio_sample_t sample =
(jack_default_audio_sample_t)sample_double;
+ if (interrupted) {
+ interrupted = 0;
+ return APPLAUSE_AUDIO_INTERRUPTED;
+ }
+
+ /*
+ * NOTE: The alternative to reporting invalid port Ids here
+ * would be exporting output_ports_count, so the Lua code can
+ * check it and assert()ing here instead.
+ */
+ if (output_port_id < 1 || output_port_id > output_ports_count)
+ return APPLAUSE_AUDIO_INVALID_PORT;
+ port = output_ports + output_port_id - 1;
+
/*
* We are about to "consume" one free sample in the buffer.
* This can block when the buffer is full.
@@ -542,17 +590,13 @@ applause_push_sample(double sample_double)
* in buffer since jack_process() will only read from the
* buffer.
*/
- svsem_op(buffer_sem, -(int)sizeof(sample));
+ svsem_op(port->buffer_sem, -(int)sizeof(sample));
- jack_ringbuffer_write(buffer, (const char *)&sample,
+ jack_ringbuffer_write(port->buffer, (const char *)&sample,
sizeof(sample));
- if (interrupted) {
- interrupted = 0;
- return APPLAUSE_AUDIO_INTERRUPTED;
- }
- if (buffer_xrun) {
- buffer_xrun = 0;
+ if (port->buffer_xrun) {
+ port->buffer_xrun = 0;
return APPLAUSE_AUDIO_XRUN;
}
@@ -588,6 +632,7 @@ l_Stream_fork(lua_State *L)
if (child_pid != 0) {
/* in the parent process */
//jack_activate(client);
+ /* FIXME: Pass on the real buffer size */
init_audio(DEFAULT_BUFFER_SIZE);
/* call Client:new(child_pid) */
@@ -603,6 +648,7 @@ l_Stream_fork(lua_State *L)
//jack_client_close(client);
+ /* FIXME: Pass on the real buffer size */
init_audio(DEFAULT_BUFFER_SIZE);
for (;;) {
@@ -861,7 +907,9 @@ main(int argc, char **argv)
* FIXME: Support --help
*/
if (argc > 1)
- buffer_size = atoi(argv[1]);
+ output_ports_count = atoi(argv[1]);
+ if (argc > 2)
+ buffer_size = atoi(argv[2]);
/*
* Register sigint_handler() as the SIGINT handler.
@@ -904,7 +952,9 @@ main(int argc, char **argv)
/* remove traceback function */
lua_remove(L, -1);
- init_audio(buffer_size);
+ if (init_audio(buffer_size))
+ /* error has already been printed */
+ return EXIT_FAILURE;
/*
* Register native C functions.
@@ -962,9 +1012,12 @@ main(int argc, char **argv)
/*
* FIXME: Shut down connection server.
+ * FIXME: Clean up properly.
*/
-
- svsem_free(buffer_sem);
+ for (int i = 0; i < output_ports_count; i++) {
+ svsem_free(output_ports[i].buffer_sem);
+ }
+ free(output_ports);
lua_close(L);
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
diff --git a/sndfile.lua b/sndfile.lua
index 47ea1ff..e91fb70 100644
--- a/sndfile.lua
+++ b/sndfile.lua
@@ -119,19 +119,22 @@ const char* sf_strerror(SNDFILE *sndfile);
int sf_command(SNDFILE *sndfile, int command, void *data, int datasize);
-sf_count_t sf_read_double(SNDFILE *sndfile, double *ptr, sf_count_t items) ;
+sf_count_t sf_read_double(SNDFILE *sndfile, double *ptr, sf_count_t items);
+sf_count_t sf_readf_double(SNDFILE *sndfile, double *ptr, sf_count_t frames);
+
sf_count_t sf_write_double(SNDFILE *sndfile, const double *ptr, sf_count_t items);
+sf_count_t sf_writef_double(SNDFILE *sndfile, const double *ptr, sf_count_t frames);
int sf_close(SNDFILE *sndfile);
]]
local lib = ffi.load("sndfile")
-local double_type = ffi.typeof("double[?]")
+sndfile.frame_type = ffi.typeof("double[?]")
-- This can be reused in sndfile:read() and sndfile:write()
-- to avoid allocations.
-local double_buffer = double_type(1)
+local double_buffer = sndfile.frame_type(1)
-- NOTE: Constants are also in ffi.C
sndfile.SF_FORMAT = ffi.typeof("SF_FORMAT")
@@ -196,12 +199,20 @@ function sndfile:read()
tonumber(double_buffer[0]) or nil
end
+function sndfile:readf(frame)
+ return lib.sf_readf_double(self.handle, frame, 1) == 1
+end
+
-- TODO: Maybe support writing multiple samples at once
function sndfile:write(sample)
double_buffer[0] = sample
return lib.sf_write_double(self.handle, double_buffer, 1)
end
+function sndfile:writef(frame)
+ return lib.sf_writef_double(self.handle, frame, 1)
+end
+
function sndfile:close()
if self.handle then
lib.sf_close(self.handle)