aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Makefile4
-rw-r--r--applause.c1
-rw-r--r--applause.lua56
-rw-r--r--evdev.c133
-rw-r--r--evdev.h14
-rw-r--r--evdev.lua121
-rw-r--r--examples/evdev.lua12
7 files changed, 333 insertions, 8 deletions
diff --git a/Makefile b/Makefile
index 97ff2f7..d91b265 100644
--- a/Makefile
+++ b/Makefile
@@ -21,8 +21,8 @@ LDFLAGS += -rdynamic
all : applause
-applause : applause.o
- $(CC) -o $@ $< $(LDFLAGS)
+applause : applause.o evdev.o
+ $(CC) -o $@ $^ $(LDFLAGS)
clean:
$(RM) *.o applause
diff --git a/applause.c b/applause.c
index b50b3aa..d859323 100644
--- a/applause.c
+++ b/applause.c
@@ -5,6 +5,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
+#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <poll.h>
diff --git a/applause.lua b/applause.lua
index b8b7631..8a4b07f 100644
--- a/applause.lua
+++ b/applause.lua
@@ -19,6 +19,12 @@ function cdef_safe(def)
end
end
+function cdef_include(file)
+ local hnd = assert(io.open(file))
+ cdef_safe(hnd:read('*a'))
+ hnd:close()
+end
+
--
-- Define C functions for benchmarking (POSIX libc)
--
@@ -71,6 +77,7 @@ end
--
-- Define the Lua FFI part of Applause's C core.
-- These functions and types are defined in applause.c
+-- FIXME: Could be read from a common file.
--
cdef_safe[[
enum applause_audio_state {
@@ -83,9 +90,13 @@ enum applause_audio_state {
enum applause_audio_state applause_push_sample(int output_port_id,
double sample_double);
+// FIXME: Perhaps a struct would be easier to handle?
typedef uint32_t applause_midi_sample;
applause_midi_sample applause_pull_midi_sample(void);
+
+// Useful in various situations
+void free(void *ptr);
]]
-- Sample rate
@@ -287,6 +298,8 @@ 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
+-- for Stream:ccscale().
function Stream:ccscale(v1, v2)
local lower = v2 and v1 or 0
local upper = v2 or v1
@@ -527,9 +540,8 @@ function Stream:foreach(fnc)
local tick = self:gtick()
while true do
- clear(sampleCache)
-
frame[1] = tick()
+ clear(sampleCache)
if not frame[1] or fnc(frame) then break end
end
end
@@ -544,6 +556,35 @@ function Stream:benchmark()
end)
end
+-- Dump bytecode of tick function.
+function Stream:jbc(out, all)
+ -- Load the utility library on-demand.
+ -- Its API is not stable according to luajit docs.
+ require("jit.bc").dump(self:gtick(), out, all)
+end
+
+function Stream:jdump(opt, outfile)
+ local dump = require("jit.dump")
+ local tick = self:gtick()
+
+ -- Make sure we discard any existing traces to
+ -- arrive at more or less reproducible results
+ jit.flush(tick, true)
+ jit.on(tick, true)
+
+ dump.on(opt, outfile)
+ -- FIXME: A single tick() call will not get jit-compiled
+ -- and there appears to be no way to force compilation of a function.
+ -- Getting any output at all would require saving the stream and
+ -- force some bulk calculations, so instead we always generate
+ -- up to 1s of samples here.
+ local _, err = pcall(function()
+ for _ = 1, samplerate do tick() end
+ end)
+ dump.off()
+ if err then error(err) end
+end
+
-- TODO: Use a buffer to improve perfomance (e.g. 1024 samples)
function Stream:save(filename, format)
if self:len() == math.huge then
@@ -1710,7 +1751,7 @@ function Stream:CC(control, channel)
return self:map(function(sample)
value = band(sample, 0xFFFF) == filter and
- rshift(sample, 16) or value
+ tonumber(rshift(sample, 16)) or value
return value
end)
end
@@ -1897,7 +1938,9 @@ function Stream:instrument(on_stream, off_stream)
return InstrumentStream:new(self, on_stream, off_stream)
end
--- primitives
+--
+-- Primitives
+--
function tostream(v)
if type(v) == "table" then
@@ -2328,6 +2371,7 @@ Client.__gc = Client.kill
-- so they react to reload()
--
dofile "dssi.lua"
+dofile "evdev.lua"
--
-- See above, MIDIStream depends on tostream() and other
@@ -2343,7 +2387,7 @@ do
-- FIXME: Since a sample must only be pulled once
-- per tick, so MIDIStream can be reused, it must
-- always be cached.
- -- Perhaps it's easier to peek into the ring buffer
- -- advance the read pointer per tick.
+ -- FIXME: This could be done directly in gtick(),
+ -- so this definition can be moved upwards to the rest of the MIDI stuff.
MIDIStream = class:cache()
end
diff --git a/evdev.c b/evdev.c
new file mode 100644
index 0000000..2d179d2
--- /dev/null
+++ b/evdev.c
@@ -0,0 +1,133 @@
+#define _GNU_SOURCE
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <fcntl.h>
+
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <linux/input.h>
+
+#include <pthread.h>
+
+#include <jack/ringbuffer.h>
+
+#include "evdev.h"
+
+typedef struct applause_evdev {
+ int fd;
+ jack_ringbuffer_t *buffer;
+ pthread_t thread;
+} applause_evdev;
+
+char *
+applause_evdev_getname(const char *node)
+{
+ int rc, fd;
+
+ fd = open(node, O_RDONLY);
+ if (fd < 0)
+ return NULL;
+
+ char name[256];
+ rc = ioctl(fd, EVIOCGNAME(sizeof(name)), name);
+ close(fd);
+
+ return rc < 0 ? NULL : strdup(name);
+}
+
+static void *
+applause_evdev_thread_cb(void *userdata)
+{
+ applause_evdev *evdev = userdata;
+
+ for (;;) {
+ struct input_event event;
+ applause_evdev_sample sample;
+
+ /*
+ * FIXME: How to detect thread cancellation reliably?
+ */
+ if (read(evdev->fd, &event, sizeof(event)) != sizeof(event))
+ break;
+
+ /*
+ * FIXME: Why do we get one "empty" event after each successful read?
+ */
+ if (event.type == 0)
+ continue;
+
+ sample.type = event.type;
+ sample.code = event.code;
+ sample.value = event.value;
+ jack_ringbuffer_write(evdev->buffer, (const char *)&sample, sizeof(sample));
+ }
+
+ /* never reached */
+ return NULL;
+}
+
+applause_evdev *
+applause_evdev_new(const char *node, bool grab)
+{
+ applause_evdev *evdev = calloc(1, sizeof(applause_evdev));
+ if (!evdev)
+ return NULL;
+
+ evdev->fd = open(node, O_RDONLY);
+ if (evdev->fd < 0)
+ goto error;
+
+ /*
+ * This will fail if the device is already grabbed
+ * which makes sense since you cannot receive events for such a device.
+ */
+ if (grab && ioctl(evdev->fd, EVIOCGRAB, true))
+ goto error;
+
+ evdev->buffer = jack_ringbuffer_create(sizeof(applause_evdev_sample)*1024);
+ if (!evdev->buffer)
+ goto error;
+
+ if (pthread_create(&evdev->thread, NULL, applause_evdev_thread_cb, evdev))
+ goto error;
+
+ return evdev;
+
+error:
+ if (evdev->buffer)
+ jack_ringbuffer_free(evdev->buffer);
+ if (evdev->fd > 0)
+ close(evdev->fd);
+ free(evdev);
+ return NULL;
+}
+
+void
+applause_evdev_pull(applause_evdev *evdev, applause_evdev_sample *sample)
+{
+ memset(sample, 0, sizeof(*sample));
+ jack_ringbuffer_read(evdev->buffer, (char *)sample, sizeof(*sample));
+}
+
+void
+applause_evdev_free(applause_evdev *evdev)
+{
+ /*
+ * NOTE: It's important to support evdev == NULL so that applause_evdev_free()
+ * can be passed safely to ffi.gc().
+ */
+ if (!evdev)
+ return;
+
+ pthread_cancel(evdev->thread);
+ pthread_join(evdev->thread, NULL);
+ jack_ringbuffer_free(evdev->buffer);
+ close(evdev->fd);
+ free(evdev);
+}
diff --git a/evdev.h b/evdev.h
new file mode 100644
index 0000000..764b11b
--- /dev/null
+++ b/evdev.h
@@ -0,0 +1,14 @@
+/* This header is included from C and LuaJIT. */
+
+typedef struct applause_evdev_sample {
+ uint16_t type;
+ uint16_t code;
+ int32_t value;
+} applause_evdev_sample;
+
+typedef struct applause_evdev applause_evdev;
+
+char *applause_evdev_getname(const char *node);
+applause_evdev *applause_evdev_new(const char *node, bool grab);
+void applause_evdev_pull(applause_evdev *evdev, applause_evdev_sample *sample);
+void applause_evdev_free(applause_evdev *evdev);
diff --git a/evdev.lua b/evdev.lua
new file mode 100644
index 0000000..dcf5a57
--- /dev/null
+++ b/evdev.lua
@@ -0,0 +1,121 @@
+local ffi = require "ffi"
+local C = ffi.C
+
+cdef_include "evdev.h"
+cdef_safe[[
+/*
+ * These definitions are copied from input-event-codes.h
+ */
+enum applause_evdev_type {
+ EV_KEY = 0x01,
+ EV_REL = 0x02,
+ EV_ABS = 0x03
+};
+
+enum applause_evdev_rel {
+ REL_X = 0x00,
+ REL_Y = 0x01,
+ REL_Z = 0x02,
+ REL_RX = 0x03,
+ REL_RY = 0x04,
+ REL_RZ = 0x05,
+ REL_HWHEEL = 0x06,
+ REL_DIAL = 0x07,
+ REL_WHEEL = 0x08,
+ REL_MISC = 0x09,
+ REL_WHEEL_HI_RES = 0x0b,
+ REL_HWHEEL_HI_RES = 0x0c
+};
+
+enum applause_evdev_abs {
+ ABS_X = 0x00,
+ ABS_Y = 0x01,
+ ABS_Z = 0x02
+};
+]]
+
+EvdevStream = DeriveClass(Stream)
+
+function EvdevStream:ctor(id, grab)
+ local grab = grab == nil or grab
+
+ local node
+ if type(id) == "number" then
+ node = "/dev/input/event"..id
+ else
+ assert(type(id) == "string")
+ -- id is assumed to be a Lua pattern to match against the device name
+ local name
+ local i = 0
+ repeat
+ node = "/dev/input/event"..i
+ local buffer = ffi.gc(C.applause_evdev_getname(node), C.free)
+ if buffer == nil then error("Evdev device not found!") end
+ name = ffi.string(buffer)
+ i = i + 1
+ until name:match(id)
+ end
+
+ -- Creating only one object has the advantage that the device can be
+ -- grabbed.
+ -- NOTE: To reliably ungrab the device, the entire object needs to be niled
+ -- and you have to drive the garbage collector manually.
+ self.evdev = ffi.gc(C.applause_evdev_new(node, grab), C.applause_evdev_free)
+ if self.evdev == nil then error("Evdev device not found!") end
+end
+
+function EvdevStream:gtick()
+ local evdev = self.evdev
+ local sample = ffi.new("applause_evdev_sample[1]")
+
+ return function()
+ -- EvdevStreams only have a single event queue no matter how often they are
+ -- gticked. That's why it must always be cached.
+ local cached_sample = sampleCache[self]
+ if not cached_sample then
+ C.applause_evdev_pull(evdev, sample)
+ sampleCache[self] = sample[0]
+ cached_sample = sample[0]
+ end
+ return cached_sample
+ end
+end
+
+-- Relative devices like some mouses and the trackpoint.
+-- The coordinate is returned in `resolution` steps between [-1, 1]
+-- NOTE: `code` is optional (default: REL_X) and you can specify a number (0 or 1)
+-- or string ('REL_X', 'REL_Y') as well.
+function Stream:evrel(code, resolution)
+ code = ffi.cast("enum applause_evdev_rel", code)
+ resolution = resolution or 1000
+ local min, max = math.min, math.max
+
+ return self:scan(function(last, sample)
+ last = last or 0
+ return sample.type == C.EV_REL and sample.code == code and
+ min(max(last+sample.value, 0), resolution) or last
+ end) / (resolution/2) - 1
+end
+
+-- FIXME: min and max can be read from the properties of the corresponding code.
+-- This would however require us to do all postprocessing already in EvdevStream:gtick()
+-- or even in applause_evdev_new().
+function Stream:evabs(code, min, max)
+ code = ffi.cast("enum applause_evdev_abs", code)
+
+ return self:scan(function(last, sample)
+ last = last or 0
+ return sample.type == C.EV_ABS and sample.code == code and
+ (sample.value - min)/((max-min)/2) - 1 or last
+ end)
+end
+
+-- FIXME: We haven't got constants for all KEY_X constants,#
+-- so you will have to look up the key code in input-event-codes.h.
+function Stream:evkey(key)
+ return self:scan(function(last, sample)
+ last = last or 0
+ return sample.type == C.EV_KEY and sample.code == key and
+ sample.value or last
+ end)
+end
diff --git a/examples/evdev.lua b/examples/evdev.lua
new file mode 100644
index 0000000..44ee3a3
--- /dev/null
+++ b/examples/evdev.lua
@@ -0,0 +1,12 @@
+trackpoint = EvdevStream("TrackPoint")
+trackpoint:evrel('REL_X'):scale(440,880):SinOsc():gain(trackpoint:evrel('REL_Y'):scale(1)):play()
+
+-- This is for the trackball and makes nice noises probably due to overflowing the Quality value
+trackball = EvdevStream("TrackBall Mouse")
+NoiseStream:BPF(trackball:evrel('REL_X'):scale(100,5000), trackball:evrel('REL_Y')):gain(trackball:evrel('REL_WHEEL'):scale(1)):play()
+
+touchpad = EvdevStream("TouchPad")
+touchpad:evabs('ABS_X', 1232, 5712):scale(440,880):SinOsc():gain(touchpad:evabs('ABS_Y', 1074, 4780):scale(1)):play()
+
+# FIXME: Make a small polyphonic keyboard
+EvdevStream(10):evkey(16):instrument(Stream.SinOsc(440)):play()