diff options
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | applause.c | 1 | ||||
-rw-r--r-- | applause.lua | 56 | ||||
-rw-r--r-- | evdev.c | 133 | ||||
-rw-r--r-- | evdev.h | 14 | ||||
-rw-r--r-- | evdev.lua | 121 | ||||
-rw-r--r-- | examples/evdev.lua | 12 |
7 files changed, 333 insertions, 8 deletions
@@ -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 @@ -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 @@ -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); +} @@ -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() |