aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--TODO2
-rw-r--r--backends/lua.c302
-rw-r--r--backends/lua.h10
-rw-r--r--backends/lua.md32
-rw-r--r--configs/demo.lua45
-rw-r--r--configs/lua.cfg10
6 files changed, 359 insertions, 42 deletions
diff --git a/TODO b/TODO
index cfdd409..f39dae0 100644
--- a/TODO
+++ b/TODO
@@ -5,4 +5,4 @@ Printing backend
document example configs
lua timer
-evdev relaxes size
+evdev relative axis size
diff --git a/backends/lua.c b/backends/lua.c
index ae2d460..08d8d3c 100644
--- a/backends/lua.c
+++ b/backends/lua.c
@@ -1,10 +1,18 @@
#include <string.h>
+#include <sys/timerfd.h>
+#include <unistd.h>
+#include <errno.h>
#include "lua.h"
#define BACKEND_NAME "lua"
#define LUA_REGISTRY_KEY "_midimonster_lua_instance"
-//TODO instance identification for callvacks
+static size_t timers = 0;
+static lua_timer* timer = NULL;
+static struct itimerspec timer_config = {
+ 0
+};
+static int timer_fd = -1;
int init(){
backend lua = {
@@ -24,31 +32,83 @@ int init(){
fprintf(stderr, "Failed to register lua backend\n");
return 1;
}
+
+ //create the timer to expire intervals
+ timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
+ if(timer_fd < 0){
+ fprintf(stderr, "Failed to create timer for Lua backend\n");
+ return 1;
+ }
+ return 0;
+}
+
+static int lua_update_timerfd(){
+ uint64_t interval = 0, gcd, residual;
+ size_t n = 0;
+
+ //find the minimum for the lower interval bounds
+ for(n = 0; n < timers; n++){
+ if(timer[n].interval && (!interval || timer[n].interval < interval)){
+ interval = timer[n].interval;
+ }
+ }
+
+ //stop the timer
+ if(!interval){
+ memset(&timer_config, 0, sizeof(struct itimerspec));
+ }
+ //calculate gcd of all timers
+ else{
+ for(n = 0; n < timers; n++){
+ if(timer[n].interval){
+ //calculate gcd of current interval and this timers interval
+ gcd = timer[n].interval;
+ while(gcd){
+ residual = interval % gcd;
+ interval = gcd;
+ gcd = residual;
+ }
+ //since we round everything, 10 is the lowest interval we get
+ if(interval == 10){
+ break;
+ }
+ }
+ }
+
+ timer_config.it_interval.tv_sec = interval / 1000;
+ timer_config.it_interval.tv_nsec = (interval % 1000) * 1e6;
+ timer_config.it_value.tv_nsec = 1;
+ }
+
+ //configure the new interval
+ timerfd_settime(timer_fd, 0, &timer_config, NULL);
return 0;
}
static int lua_callback_output(lua_State* interpreter){
- int arguments = lua_gettop(interpreter);
- size_t n;
+ size_t n = 0;
channel_value val;
const char* channel_name = NULL;
channel* channel = NULL;
instance* inst = NULL;
lua_instance_data* data = NULL;
- if(arguments != 2){
- fprintf(stderr, "Lua output function called with %d arguments, expected 2\n", arguments);
+ if(lua_gettop(interpreter) != 2){
+ fprintf(stderr, "Lua output function called with %d arguments, expected 2 (string, number)\n", lua_gettop(interpreter));
return 0;
}
- channel_name = lua_tostring(interpreter, 1);
- val.normalised = clamp(lua_tonumber(interpreter, 2), 1.0, 0.0);
-
+ //get instance pointer from registry
lua_pushstring(interpreter, LUA_REGISTRY_KEY);
lua_gettable(interpreter, LUA_REGISTRYINDEX);
- inst = (instance *) lua_touserdata(interpreter, -1);
+ inst = (instance*) lua_touserdata(interpreter, -1);
data = (lua_instance_data*) inst->impl;
+ //fetch function parameters
+ channel_name = lua_tostring(interpreter, 1);
+ val.normalised = clamp(luaL_checknumber(interpreter, 2), 1.0, 0.0);
+
+ //find correct channel & output value
for(n = 0; n < data->channels; n++){
if(!strcmp(channel_name, data->channel_name[n])){
channel = mm_channel(inst, n, 0);
@@ -56,6 +116,7 @@ static int lua_callback_output(lua_State* interpreter){
return 0;
}
mm_channel_event(channel, val);
+ data->output[n] = val.normalised;
return 0;
}
}
@@ -64,6 +125,122 @@ static int lua_callback_output(lua_State* interpreter){
return 0;
}
+static int lua_callback_interval(lua_State* interpreter){
+ size_t n = 0;
+ instance* inst = NULL;
+ lua_instance_data* data = NULL;
+ uint64_t interval = 0;
+ int reference = LUA_NOREF;
+
+ if(lua_gettop(interpreter) != 2){
+ fprintf(stderr, "Lua output function called with %d arguments, expected 2 (string, number)\n", lua_gettop(interpreter));
+ return 0;
+ }
+
+ //get instance pointer from registry
+ lua_pushstring(interpreter, LUA_REGISTRY_KEY);
+ lua_gettable(interpreter, LUA_REGISTRYINDEX);
+ inst = (instance*) lua_touserdata(interpreter, -1);
+ data = (lua_instance_data*) inst->impl;
+
+ //fetch and round the interval
+ interval = luaL_checkinteger(interpreter, 2);
+ if(interval % 10 < 5){
+ interval -= interval % 10;
+ }
+ else{
+ interval += (10 - (interval % 10));
+ }
+
+ //push the function again
+ lua_pushvalue(interpreter, 1);
+ if(lua_gettable(interpreter, LUA_REGISTRYINDEX) == LUA_TNUMBER){
+ //already interval'd
+ reference = luaL_checkinteger(interpreter, 4);
+ }
+ else if(interval){
+ //get a reference to the function
+ lua_pushvalue(interpreter, 1);
+ reference = luaL_ref(interpreter, LUA_REGISTRYINDEX);
+
+ //the function indexes the reference
+ lua_pushvalue(interpreter, 1);
+ lua_pushinteger(interpreter, reference);
+ lua_settable(interpreter, LUA_REGISTRYINDEX);
+ }
+
+ //find matching timer
+ for(n = 0; n < timers; n++){
+ if(timer[n].reference == reference && timer[n].interpreter == interpreter){
+ break;
+ }
+ }
+
+ if(n < timers){
+ //set new interval
+ timer[n].interval = interval;
+ timer[n].delta = 0;
+ }
+ else if(interval){
+ //append new timer
+ timer = realloc(timer, (timers + 1) * sizeof(lua_timer));
+ if(!timer){
+ fprintf(stderr, "Failed to allocate memory\n");
+ timers = 0;
+ return 0;
+ }
+ timer[timers].interval = interval;
+ timer[timers].delta = 0;
+ timer[timers].interpreter = interpreter;
+ timer[timers].reference = reference;
+ timers++;
+ }
+
+ //recalculate timerspec
+ lua_update_timerfd();
+ return 0;
+}
+
+static int lua_callback_value(lua_State* interpreter, uint8_t input){
+ size_t n = 0;
+ instance* inst = NULL;
+ lua_instance_data* data = NULL;
+ const char* channel_name = NULL;
+
+ if(lua_gettop(interpreter) != 1){
+ fprintf(stderr, "Lua get_value function called with %d arguments, expected 1 (string)\n", lua_gettop(interpreter));
+ return 0;
+ }
+
+ //get instance pointer from registry
+ lua_pushstring(interpreter, LUA_REGISTRY_KEY);
+ lua_gettable(interpreter, LUA_REGISTRYINDEX);
+ inst = (instance*) lua_touserdata(interpreter, -1);
+ data = (lua_instance_data*) inst->impl;
+
+ //fetch argument
+ channel_name = lua_tostring(interpreter, 1);
+
+ //find correct channel & return value
+ for(n = 0; n < data->channels; n++){
+ if(!strcmp(channel_name, data->channel_name[n])){
+ lua_pushnumber(data->interpreter, (input) ? data->input[n] : data->output[n]);
+ return 1;
+ }
+ }
+
+ fprintf(stderr, "Tried to get unknown channel %s.%s\n", inst->name, channel_name);
+ return 0;
+}
+
+static int lua_callback_input_value(lua_State* interpreter){
+ return lua_callback_value(interpreter, 1);
+}
+
+static int lua_callback_output_value(lua_State* interpreter){
+ return lua_callback_value(interpreter, 0);
+}
+
static int lua_configure(char* option, char* value){
fprintf(stderr, "The lua backend does not take any global configuration\n");
return 1;
@@ -72,6 +249,7 @@ static int lua_configure(char* option, char* value){
static int lua_configure_instance(instance* inst, char* option, char* value){
lua_instance_data* data = (lua_instance_data*) inst->impl;
+ //load a lua file into the interpreter
if(!strcmp(option, "script")){
if(luaL_dofile(data->interpreter, value)){
fprintf(stderr, "Failed to load lua source file %s for instance %s: %s\n", value, inst->name, lua_tostring(data->interpreter, -1));
@@ -105,13 +283,16 @@ static instance* lua_instance(){
}
luaL_openlibs(data->interpreter);
- //register lua api functions
+ //register lua interface functions
lua_register(data->interpreter, "output", lua_callback_output);
+ lua_register(data->interpreter, "interval", lua_callback_interval);
+ lua_register(data->interpreter, "input_value", lua_callback_input_value);
+ lua_register(data->interpreter, "output_value", lua_callback_output_value);
//store instance pointer to the lua state
lua_pushstring(data->interpreter, LUA_REGISTRY_KEY);
lua_pushlightuserdata(data->interpreter, (void *) inst);
- lua_settable(data->interpreter, LUA_REGISTRYINDEX);
+ lua_settable(data->interpreter, LUA_REGISTRYINDEX);
inst->impl = data;
return inst;
@@ -131,11 +312,16 @@ static channel* lua_channel(instance* inst, char* spec){
//allocate new channel
if(u == data->channels){
data->channel_name = realloc(data->channel_name, (u + 1) * sizeof(char*));
- if(!data->channel_name){
+ data->reference = realloc(data->reference, (u + 1) * sizeof(int));
+ data->input = realloc(data->input, (u + 1) * sizeof(double));
+ data->output = realloc(data->output, (u + 1) * sizeof(double));
+ if(!data->channel_name || !data->reference || !data->input || !data->output){
fprintf(stderr, "Failed to allocate memory\n");
return NULL;
}
+ data->reference[u] = LUA_NOREF;
+ data->input[u] = data->output[u] = 0.0;
data->channel_name[u] = strdup(spec);
if(!data->channel_name[u]){
fprintf(stderr, "Failed to allocate memory\n");
@@ -151,25 +337,91 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){
size_t n = 0;
lua_instance_data* data = (lua_instance_data*) inst->impl;
+ //handle all incoming events
for(n = 0; n < num; n++){
- //call lua channel handlers
- lua_getglobal(data->interpreter, data->channel_name[c[n]->ident]);
- lua_pushnumber(data->interpreter, v[n].normalised);
- if(lua_pcall(data->interpreter, 1, 0, 0) != LUA_OK){
- fprintf(stderr, "Failed to call handler for %s.%s: %s\n", inst->name, data->channel_name[c[n]->ident], lua_tostring(data->interpreter, -1));
- lua_pop(data->interpreter, 1);
+ data->input[c[n]->ident] = v[n].normalised;
+ //call lua channel handlers if present
+ if(data->reference[n] != LUA_NOREF){
+ lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->reference[c[n]->ident]);
+ lua_pushnumber(data->interpreter, v[n].normalised);
+ if(lua_pcall(data->interpreter, 1, 0, 0) != LUA_OK){
+ fprintf(stderr, "Failed to call handler for %s.%s: %s\n", inst->name, data->channel_name[c[n]->ident], lua_tostring(data->interpreter, -1));
+ lua_pop(data->interpreter, 1);
+ }
}
}
return 0;
}
static int lua_handle(size_t num, managed_fd* fds){
- //TODO call timer callbacks
+ uint8_t read_buffer[100];
+ uint64_t delta = timer_config.it_interval.tv_sec * 1000 + timer_config.it_interval.tv_nsec / 1e6;
+ size_t n;
+
+ if(!num){
+ return 0;
+ }
+
+ //read the timer iteration to acknowledge the fd
+ if(read(timer_fd, read_buffer, sizeof(read_buffer)) < 0){
+ fprintf(stderr, "Failed to read from Lua timer: %s\n", strerror(errno));
+ return 1;
+ }
+
+ //add delta to all active timers
+ for(n = 0; n < timers; n++){
+ if(timer[n].interval){
+ timer[n].delta += delta;
+ //call lua function if timer expired
+ if(timer[n].delta >= timer[n].interval){
+ timer[n].delta %= timer[n].interval;
+ lua_rawgeti(timer[n].interpreter, LUA_REGISTRYINDEX, timer[n].reference);
+ lua_pcall(timer[n].interpreter, 0, 0, 0);
+ }
+ }
+ }
return 0;
}
static int lua_start(){
- //TODO start timers / register fds
+ size_t n, u, p;
+ instance** inst = NULL;
+ lua_instance_data* data = NULL;
+
+ //fetch all defined instances
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ return 1;
+ }
+
+ //resolve channels to their handler functions
+ for(u = 0; u < n; u++){
+ data = (lua_instance_data*) inst[u]->impl;
+ for(p = 0; p < data->channels; p++){
+ //exclude reserved names
+ if(strcmp(data->channel_name[p], "output")
+ && strcmp(data->channel_name[p], "input_value")
+ && strcmp(data->channel_name[p], "output_value")
+ && strcmp(data->channel_name[p], "interval")){
+ lua_getglobal(data->interpreter, data->channel_name[p]);
+ data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX);
+ if(data->reference[p] == LUA_REFNIL){
+ data->reference[p] = LUA_NOREF;
+ }
+ }
+ }
+ }
+
+ free(inst);
+ if(!n){
+ return 0;
+ }
+
+ //register the timer with the core
+ fprintf(stderr, "Lua backend registering 1 descriptor to core\n");
+ if(mm_manage_fd(timer_fd, BACKEND_NAME, 1, NULL)){
+ return 1;
+ }
return 0;
}
@@ -178,6 +430,7 @@ static int lua_shutdown(){
instance** inst = NULL;
lua_instance_data* data = NULL;
+ //fetch all instances
if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
fprintf(stderr, "Failed to fetch instance list\n");
return 1;
@@ -192,10 +445,19 @@ static int lua_shutdown(){
free(data->channel_name[p]);
}
free(data->channel_name);
+ free(data->reference);
+ free(data->input);
+ free(data->output);
free(inst[u]->impl);
}
free(inst);
+ //free module-global data
+ free(timer);
+ timer = NULL;
+ timers = 0;
+ close(timer_fd);
+ timer_fd = -1;
fprintf(stderr, "Lua backend shut down\n");
return 0;
diff --git a/backends/lua.h b/backends/lua.h
index 27e1afd..ccffef7 100644
--- a/backends/lua.h
+++ b/backends/lua.h
@@ -17,5 +17,15 @@ static int lua_shutdown();
typedef struct /*_lua_instance_data*/ {
size_t channels;
char** channel_name;
+ int* reference;
+ double* input;
+ double* output;
lua_State* interpreter;
} lua_instance_data;
+
+typedef struct /*_lua_interval_callback*/ {
+ uint64_t interval;
+ uint64_t delta;
+ lua_State* interpreter;
+ int reference;
+} lua_timer;
diff --git a/backends/lua.md b/backends/lua.md
index 91e8fe2..970e8e2 100644
--- a/backends/lua.md
+++ b/backends/lua.md
@@ -5,15 +5,32 @@ events using the Lua programming language.
Every instance has it's own interpreter state which can be loaded with custom handler scripts.
-To process incoming channel events, the MIDIMonster calls corresponding Lua functions with
-the value (as a Lua `number` type) as parameter. To send output on a channel, the Lua environment
-provides the function `output(channel-name, value)`.
+To process incoming channel events, the MIDIMonster calls corresponding Lua functions (if they exist)
+with the value (as a Lua `number` type) as parameter.
+
+The following functions are provided within the Lua interpreter for interaction with the MIDIMonster
+
+| Function | Usage example | Description |
+|-------------------------------|-------------------------------|---------------------------------------|
+| `output(string, number)` | `output("foo", 0.75)` | Output a value event to a channel |
+| `interval(function, number)` | `interval(update, 100)` | Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms) |
+| `input_value(string)` | `input_value("foo")` | Get the last input value on a channel |
+| `output_value(string)` | `output_value("bar") | Get the last output value on a channel |
+
Example script:
```
function bar(value)
output("foo", value / 2)
end
+
+step = 0
+function toggle()
+ output("bar", step * 1.0)
+ step = (step + 1) % 2;
+end
+
+interval(toggle, 1000)
```
Input values range between 0.0 and 1.0, output values are clamped to the same range.
@@ -41,9 +58,12 @@ lua1.foo > lua2.bar
#### Known bugs / problems
-Using `output` as an input channel name to a Lua instance does not work, as the interpreter has
-`output` globally assigned to the event output function. Using `output` as an output channel name
-via `output("output", value)` works as intended.
+Using any of the interface functions (`output`, `interval`, `input\_value`, `output\_value`) as an
+input channel name to a Lua instance will not call any handler functions.
+Using these names as arguments to the output and value interface functions works as intended.
+
+Output values will not trigger corresponding input event handlers unless the channel is mapped
+back in the MIDIMonster configuration.
The path to the Lua source files is relative to the current working directory. This may lead
to problems when copying configuration between installations.
diff --git a/configs/demo.lua b/configs/demo.lua
index e816ac4..24a8396 100644
--- a/configs/demo.lua
+++ b/configs/demo.lua
@@ -1,21 +1,42 @@
--- This example MIDIMonstaer Lua script spreads one input channel onto multiple output
+-- This example MIDIMonster Lua script spreads one input channel onto multiple output
-- channels using a polynomial function evaluated at multiple points. This effect can
-- be visualized e.g. with martrix (https://github.com/cbdevnet/martrix).
--- This is just a demonstration of global variables
-foo = 0
-
-- The polynomial to evaluate
-function polynomial(offset, x)
- return math.exp(-20 * (x - offset) ^ 2)
+function polynomial(x)
+ return math.exp(-40 * input_value("width") * (x - input_value("offset")) ^ 2)
end
--- Handler function for the input channel
-function input(value)
- foo = foo + 1
- print("input at ", value, foo)
-
+-- Evaluate and set output channels
+function evaluate()
for chan=0,10 do
- output("out" .. chan, polynomial(value, (1 / 10) * chan))
+ output("out" .. chan, polynomial((1 / 10) * chan))
+ end
+end
+
+-- Handler functions for the input channels
+function offset(value)
+ evaluate()
+end
+
+function width(value)
+ evaluate()
+end
+
+-- This is an example showing a simple chase running on its own without the need
+-- (but the possibility) for external input
+
+-- Global value for the current step
+current_step = 0
+
+function step()
+ if(current_step > 0) then
+ output("dim", 0.0)
+ else
+ output("dim", 1.0)
end
+
+ current_step = (current_step + 1) % 2
end
+
+interval(step, 1000)
diff --git a/configs/lua.cfg b/configs/lua.cfg
index 9182122..42fd27d 100644
--- a/configs/lua.cfg
+++ b/configs/lua.cfg
@@ -8,8 +8,9 @@ bind = 0.0.0.0
device = /dev/input/by-path/platform-i8042-serio-2-event-mouse
[evdev xbox]
-input = Xbox Wireless
+device = /dev/input/event17
axis.ABS_X = 34300 0 65535 255 4095
+axis.ABS_Y = 34300 0 65535 255 4095
[lua lua]
script = configs/demo.lua
@@ -19,8 +20,9 @@ universe = 0
destination = 255.255.255.255
[map]
-mouse.EV_KEY.BTN_LEFT > lua.input
-xbox.EV_ABS.ABS_X > lua.input
+mouse.EV_KEY.BTN_LEFT > lua.click
+xbox.EV_ABS.ABS_X > lua.offset
+xbox.EV_ABS.ABS_Y > lua.width
art.1 < lua.out0
art.2 < lua.out1
@@ -33,3 +35,5 @@ art.8 < lua.out7
art.9 < lua.out8
art.10 < lua.out9
art.11 < lua.out10
+
+art.12 < lua.dim