From 871bc1568f94ee4026fd64df062572b91d45d462 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 4 Jan 2020 18:39:04 +0100 Subject: Add input_channel call to Lua backend --- backends/lua.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'backends/lua.md') diff --git a/backends/lua.md b/backends/lua.md index f38e189..650fdb9 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -16,7 +16,7 @@ The following functions are provided within the Lua interpreter for interaction | `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 | - +| `input_channel()` | `print(input_channel())` | Returns the name of the input channel whose handler function is currently running or `nil` if in an `interval`'ed function (or the initial parse step) | Example script: ``` @@ -58,8 +58,8 @@ lua1.foo > lua2.bar #### Known bugs / problems -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 any of the interface functions (`output`, `interval`, `input_value`, `output_value`, `input_channel`) +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 -- cgit v1.2.3 From 8ebc4311185e3329622ad883dde54409fa1c8350 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 9 Feb 2020 11:50:41 +0100 Subject: Mention release page in installer section, move images around --- README.md | 7 +++++-- TODO | 1 + backends/lua.md | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) (limited to 'backends/lua.md') diff --git a/README.md b/README.md index 5ed19c7..4ab20a3 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ MIDIMonster Logo +[![Build Status](https://travis-ci.com/cbdevnet/midimonster.svg?branch=master)](https://travis-ci.com/cbdevnet/midimonster) [![Coverity Scan Build Status](https://scan.coverity.com/projects/15168/badge.svg)](https://scan.coverity.com/projects/15168) + Named for its scary math, the MIDIMonster is a universal control and translation tool for multi-channel absolute-value-based control and/or bus protocols. @@ -33,8 +35,6 @@ on any other (or the same) supported protocol, for example to: * Control lighting fixtures or DAWs using gamepad controllers, trackballs, etc ([Example configuration](configs/evdev.cfg)) * Play games, type, or control your mouse using MIDI controllers ([Example configuration](configs/midi-mouse.cfg)) -[![Build Status](https://travis-ci.com/cbdevnet/midimonster.svg?branch=master)](https://travis-ci.com/cbdevnet/midimonster) [![Coverity Scan Build Status](https://scan.coverity.com/projects/15168/badge.svg)](https://scan.coverity.com/projects/15168) - If you encounter a bug or suspect a problem with a a protocol implementation, please [open an Issue](https://github.com/cbdevnet/midimonster/issues) or get in touch with us via IRC on [Hackint in `#midimonster`](https://webirc.hackint.org/#irc://irc.hackint.org/#midimonster). @@ -151,6 +151,9 @@ This section will explain how to build and install the MIDIMonster. Development is mainly done on Linux, but builds for OSX and Windows are possible. +Binary builds for all supported systems are available for download on the +[Release page](https://github.com/cbdevnet/midimonster/releases). + ### Using the installer The easiest way to install MIDIMonster and its dependencies on a Linux system diff --git a/TODO b/TODO index ee58777..3a78b9b 100644 --- a/TODO +++ b/TODO @@ -5,3 +5,4 @@ udp backends may ignore MTU mm_managed_fd.impl is not freed currently (and is heaped most of the time anyway) -> documentation make event collectors threadsafe to stop marshalling data... collect & check backend API version +windows strerror diff --git a/backends/lua.md b/backends/lua.md index 650fdb9..d01a8c6 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -3,7 +3,7 @@ The `lua` backend provides a flexible programming environment, allowing users to route and manipulate events using the Lua programming language. -Every instance has it's own interpreter state which can be loaded with custom handler scripts. +Every instance has its own interpreter state which can be loaded with custom handler scripts. To process incoming channel events, the MIDIMonster calls corresponding Lua functions (if they exist) with the value (as a Lua `number` type) as parameter. -- cgit v1.2.3 From f49c46c184ecdd5209f1fd9df47daed0acfae728 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 28 Feb 2020 18:53:45 +0100 Subject: Update README to reflect all backends, add notes for building Lua on Windows --- README.md | 19 ++++++++++--------- backends/lua.md | 10 ++++++++++ 2 files changed, 20 insertions(+), 9 deletions(-) (limited to 'backends/lua.md') diff --git a/README.md b/README.md index 4d0b052..2af26cf 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ # The MIDIMonster - MIDIMonster Logo [![Build Status](https://travis-ci.com/cbdevnet/midimonster.svg?branch=master)](https://travis-ci.com/cbdevnet/midimonster) @@ -11,17 +10,19 @@ tool for multi-channel absolute-value-based control and/or bus protocols. Currently, the MIDIMonster supports the following protocols: -| Protocol | Operating Systems | Notes | Backends | -|-------------------------------|-----------------------|-------------------------------|-------------------------------| +| Protocol / Interface | Operating Systems | Notes | Backends | +|-------------------------------|-----------------------|-------------------------------|---------------------------------------| | MIDI | Linux, Windows, OSX | Linux: via ALSA/JACK, OSX: via JACK | [`midi`](backends/midi.md), [`winmidi`](backends/winmidi.md), [`jack`](backends/jack.md) | -| ArtNet | Linux, Windows, OSX | Version 4 | [`artnet`](backends/artnet.md)| -| Streaming ACN (sACN / E1.31) | Linux, Windows, OSX | | [`sacn`](backends/sacn.md) | -| OpenSoundControl (OSC) | Linux, Windows, OSX | | [`osc`](backends/osc.md) | +| ArtNet | Linux, Windows, OSX | Version 4 | [`artnet`](backends/artnet.md) | +| Streaming ACN (sACN / E1.31) | Linux, Windows, OSX | | [`sacn`](backends/sacn.md) | +| OpenSoundControl (OSC) | Linux, Windows, OSX | | [`osc`](backends/osc.md) | | OpenPixelControl | Linux, Windows, OSX | 8 Bit & 16 Bit modes | [`openpixelcontrol`](backends/openpixelcontrol.md) | -| evdev input devices | Linux | Virtual output supported | [`evdev`](backends/evdev.md) | -| Open Lighting Architecture | Linux, OSX | | [`ola`](backends/ola.md) | +| evdev input devices | Linux | Virtual output supported | [`evdev`](backends/evdev.md) | +| Open Lighting Architecture | Linux, OSX | | [`ola`](backends/ola.md) | | MA Lighting Web Remote | Linux, Windows, OSX | GrandMA2 and dot2 (incl. OnPC) | [`maweb`](backends/maweb.md) | -| JACK/LV2 Control Voltage (CV) | Linux, OSX | | [`jack`](backends/jack.md) | +| JACK/LV2 Control Voltage (CV) | Linux, OSX | | [`jack`](backends/jack.md) | +| Lua Scripting | Linux, Windows, OSX | | [`lua`](backends/lua.md) | +| Loopback | Linux, Windosw, OSX | | [`loopback`](backends/loopback.md) | with additional flexibility provided by a [Lua scripting environment](backends/lua.md). diff --git a/backends/lua.md b/backends/lua.md index d01a8c6..b936a99 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -64,3 +64,13 @@ Using these names as arguments to the output and value interface functions works Output values will not trigger corresponding input event handlers unless the channel is mapped back in the MIDIMonster configuration. + +To build (and run) the `lua` backend on Windows, a compiled version of the Lua library is required. +For various reasons (legal, separations of concern, not wanting to ship binary data in the repository), +you will need to acquire a copy of `lua53.dll` (for example by downloading it from the [luabinaries +project](http://luabinaries.sourceforge.net/download.html). + +To build the `lua` backend for Windows, place `lua53.dll` in a subdirectory `libs/` in the project root +and run `make lua.dll` inside the `backends/` directory. + +At runtime, Windows searches for the file in the same directory as `midimonster.exe`. -- cgit v1.2.3 From d574abf032da350f1208d9480d24c51187bc7c63 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 28 Feb 2020 19:00:58 +0100 Subject: Minor documentation fix --- README.md | 6 ++---- backends/lua.md | 5 +++-- 2 files changed, 5 insertions(+), 6 deletions(-) (limited to 'backends/lua.md') diff --git a/README.md b/README.md index 2af26cf..7313415 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,8 @@ Currently, the MIDIMonster supports the following protocols: | Lua Scripting | Linux, Windows, OSX | | [`lua`](backends/lua.md) | | Loopback | Linux, Windosw, OSX | | [`loopback`](backends/loopback.md) | -with additional flexibility provided by a [Lua scripting environment](backends/lua.md). - -With these features, the MIDIMonster allows the user to translate any channel on one protocol into channel(s) -on any other (or the same) supported protocol, for example to: +With these features, the MIDIMonster allows users to control any channel on any of these protocols, and translate any channel on +one protocol into channel(s) on any other (or the same) supported protocol, for example to: * Translate MIDI Control Changes into MIDI Notes ([Example configuration](configs/unifest-17.cfg)) * Translate MIDI Notes into ArtNet or sACN ([Example configuration](configs/launchctl-sacn.cfg)) diff --git a/backends/lua.md b/backends/lua.md index b936a99..d2552e0 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -65,9 +65,10 @@ Using these names as arguments to the output and value interface functions works Output values will not trigger corresponding input event handlers unless the channel is mapped back in the MIDIMonster configuration. -To build (and run) the `lua` backend on Windows, a compiled version of the Lua library is required. +To build (and run) the `lua` backend on Windows, a compiled version of the Lua 5.3 library is required. For various reasons (legal, separations of concern, not wanting to ship binary data in the repository), -you will need to acquire a copy of `lua53.dll` (for example by downloading it from the [luabinaries +the MIDIMonster project can not provide this file within this repository. +You will need to acquire a copy of `lua53.dll`, for example by downloading it from the [luabinaries project](http://luabinaries.sourceforge.net/download.html). To build the `lua` backend for Windows, place `lua53.dll` in a subdirectory `libs/` in the project root -- cgit v1.2.3 From 1618f49ff5dc317968e385e5cbc4aae32e8fb67b Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 1 Mar 2020 15:02:33 +0100 Subject: Fix some errors in the Lua documentation --- backends/lua.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'backends/lua.md') diff --git a/backends/lua.md b/backends/lua.md index d2552e0..ce82a4d 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -1,7 +1,7 @@ ### The `lua` backend -The `lua` backend provides a flexible programming environment, allowing users to route and manipulate -events using the Lua programming language. +The `lua` backend provides a flexible programming environment, allowing users to route, generate +and manipulate events using the Lua scripting language. Every instance has its own interpreter state which can be loaded with custom handler scripts. @@ -45,7 +45,7 @@ The `lua` backend does not take any global configuration. |---------------|-----------------------|-----------------------|-----------------------| | `script` | `script.lua` | none | Lua source file (relative to configuration file)| -A single instance may have multiple `source` options specified, which will all be read cumulatively. +A single instance may have multiple `script` options specified, which will all be read cumulatively. #### Channel specification @@ -63,7 +63,7 @@ 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. +back in the MIDIMonster configuration. This is intentional. To build (and run) the `lua` backend on Windows, a compiled version of the Lua 5.3 library is required. For various reasons (legal, separations of concern, not wanting to ship binary data in the repository), -- cgit v1.2.3 From dde2cca435b2c314a08024825b1daffa6437b947 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 7 Mar 2020 14:11:26 +0100 Subject: Update interval() documentation --- backends/lua.md | 2 +- backends/openpixelcontrol.c | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) (limited to 'backends/lua.md') diff --git a/backends/lua.md b/backends/lua.md index ce82a4d..0c592b7 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -13,7 +13,7 @@ The following functions are provided within the Lua interpreter for interaction | 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) | +| `interval(function, number)` | `interval(update, 100)` | Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms). Calling `interval` on a Lua function multiple times updates the interval. Specifying `0` as interval stops periodic calls to the function | | `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 | | `input_channel()` | `print(input_channel())` | Returns the name of the input channel whose handler function is currently running or `nil` if in an `interval`'ed function (or the initial parse step) | diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c index b5885b0..168e077 100644 --- a/backends/openpixelcontrol.c +++ b/backends/openpixelcontrol.c @@ -1,5 +1,4 @@ #define BACKEND_NAME "openpixelcontrol" -#define DEBUG #include -- cgit v1.2.3 From f9829ae90d4017940047b561e412c6eb7f431adb Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 8 Mar 2020 12:13:38 +0100 Subject: Implement timestamp() callback for Lua --- backends/lua.c | 16 +++++++++++----- backends/lua.md | 5 +++-- 2 files changed, 14 insertions(+), 7 deletions(-) (limited to 'backends/lua.md') diff --git a/backends/lua.c b/backends/lua.c index 955341a..9a8091e 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -75,7 +75,7 @@ static int lua_update_timerfd(){ size_t n = 0; #ifdef MMBACKEND_LUA_TIMERFD struct itimerspec timer_config = { - 0 + {0} }; #endif @@ -260,7 +260,7 @@ static int lua_callback_value(lua_State* interpreter, uint8_t input){ //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]); + lua_pushnumber(interpreter, (input) ? data->input[n] : data->output[n]); return 1; } } @@ -283,6 +283,11 @@ static int lua_callback_input_channel(lua_State* interpreter){ return 1; } +static int lua_callback_timestamp(lua_State* interpreter){ + lua_pushnumber(interpreter, mm_timestamp()); + return 1; +} + static int lua_configure(char* option, char* value){ LOG("No backend configuration possible"); return 1; @@ -326,6 +331,7 @@ static int lua_instance(instance* inst){ lua_register(data->interpreter, "input_value", lua_callback_input_value); lua_register(data->interpreter, "output_value", lua_callback_output_value); lua_register(data->interpreter, "input_channel", lua_callback_input_channel); + lua_register(data->interpreter, "timestamp", lua_callback_timestamp); //store instance pointer to the lua state lua_pushstring(data->interpreter, LUA_REGISTRY_KEY); @@ -417,9 +423,6 @@ static int lua_handle(size_t num, managed_fd* fds){ return 1; } #else - if(!last_timestamp){ - last_timestamp = mm_timestamp(); - } delta = mm_timestamp() - last_timestamp; last_timestamp = mm_timestamp(); #endif @@ -458,6 +461,7 @@ static int lua_start(size_t n, instance** inst){ && strcmp(data->channel_name[p], "input_value") && strcmp(data->channel_name[p], "output_value") && strcmp(data->channel_name[p], "input_channel") + && strcmp(data->channel_name[p], "timestamp") && strcmp(data->channel_name[p], "interval")){ lua_getglobal(data->interpreter, data->channel_name[p]); data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX); @@ -474,6 +478,8 @@ static int lua_start(size_t n, instance** inst){ if(mm_manage_fd(timer_fd, BACKEND_NAME, 1, NULL)){ return 1; } + #else + last_timestamp = mm_timestamp(); #endif return 0; } diff --git a/backends/lua.md b/backends/lua.md index 0c592b7..db4cf39 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -17,6 +17,7 @@ The following functions are provided within the Lua interpreter for interaction | `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 | | `input_channel()` | `print(input_channel())` | Returns the name of the input channel whose handler function is currently running or `nil` if in an `interval`'ed function (or the initial parse step) | +| `timestamp()` | `print(timestamp())` | Returns the core timestamp for this iteration with millisecond resolution. This is not a performance timer, but intended for timeouting, etc | Example script: ``` @@ -58,8 +59,8 @@ lua1.foo > lua2.bar #### Known bugs / problems -Using any of the interface functions (`output`, `interval`, `input_value`, `output_value`, `input_channel`) -as an input channel name to a Lua instance will not call any handler functions. +Using any of the interface functions (`output`, `interval`, `input_value`, `output_value`, `input_channel`, +`timestamp`) 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 -- cgit v1.2.3 From 9718e10c7f4151cea895f515c785c14e0021d967 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 18 Mar 2020 22:36:35 +0100 Subject: Implement default channel handlers for Lua/Python --- backends/lua.c | 17 +++++++++++++- backends/lua.h | 2 ++ backends/lua.md | 10 +++++---- backends/python.c | 65 +++++++++++++++++++++++++++++++++++++----------------- backends/python.h | 3 +++ backends/python.md | 8 ++++--- 6 files changed, 77 insertions(+), 28 deletions(-) (limited to 'backends/lua.md') diff --git a/backends/lua.c b/backends/lua.c index e7ba9f9..498a037 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -308,6 +308,11 @@ static int lua_configure_instance(instance* inst, char* option, char* value){ } return 0; } + else if(!strcmp(option, "default-handler")){ + free(data->default_handler); + data->default_handler = strdup(value); + return 0; + } LOGPF("Unknown instance configuration parameter %s for instance %s", option, inst->name); return 1; @@ -461,7 +466,8 @@ static int lua_start(size_t n, instance** inst){ data = (lua_instance_data*) inst[u]->impl; for(p = 0; p < data->channels; p++){ //exclude reserved names - if(strcmp(data->channel_name[p], "output") + if(!data->default_handler + && 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], "input_channel") @@ -473,6 +479,14 @@ static int lua_start(size_t n, instance** inst){ data->reference[p] = LUA_NOREF; } } + else if(data->default_handler){ + lua_getglobal(data->interpreter, data->default_handler); + data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX); + if(data->reference[p] == LUA_REFNIL){ + data->reference[p] = LUA_NOREF; + LOGPF("Failed to resolve default handler function %s on instance %s", data->default_handler, inst[u]->name); + } + } } } @@ -504,6 +518,7 @@ static int lua_shutdown(size_t n, instance** inst){ free(data->reference); free(data->input); free(data->output); + free(data->default_handler); free(inst[u]->impl); } diff --git a/backends/lua.h b/backends/lua.h index ebe2046..743f978 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -29,6 +29,8 @@ typedef struct /*_lua_instance_data*/ { double* input; double* output; lua_State* interpreter; + + char* default_handler; } lua_instance_data; typedef struct /*_lua_interval_callback*/ { diff --git a/backends/lua.md b/backends/lua.md index db4cf39..96e53c8 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -6,7 +6,8 @@ and manipulate events using the Lua scripting language. Every instance has its own interpreter state which can be loaded with custom handler scripts. To process incoming channel events, the MIDIMonster calls corresponding Lua functions (if they exist) -with the value (as a Lua `number` type) as parameter. +with the value (as a Lua `number` type) as parameter. Alternatively, a designated default channel handler +may be supplied in the configuration. The following functions are provided within the Lua interpreter for interaction with the MIDIMonster @@ -42,9 +43,10 @@ The `lua` backend does not take any global configuration. #### Instance configuration -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------| -| `script` | `script.lua` | none | Lua source file (relative to configuration file)| +| Option | Example value | Default value | Description | +|-----------------------|-----------------------|-----------------------|-----------------------| +| `script` | `script.lua` | none | Lua source file (relative to configuration file) | +| `default-handler` | `handler` | none | Name of a function to be called as handler for all incoming channels (instead of the per-channel handlers) | A single instance may have multiple `script` options specified, which will all be read cumulatively. diff --git a/backends/python.c b/backends/python.c index 70c2548..735e838 100644 --- a/backends/python.c +++ b/backends/python.c @@ -378,6 +378,11 @@ static int python_configure_instance(instance* inst, char* option, char* value){ PyEval_ReleaseThread(data->interpreter); return 0; } + else if(!strcmp(option, "default-handler")){ + free(data->default_handler); + data->default_handler = strdup(value); + return 0; + } LOGPF("Unknown instance parameter %s for instance %s", option, inst->name); return 1; @@ -600,38 +605,56 @@ static int python_handle(size_t num, managed_fd* fds){ return 0; } +static PyObject* python_resolve_symbol(char* spec_raw){ + char* module_name = NULL, *object_name = NULL, *spec = strdup(spec_raw); + PyObject* module = NULL, *result = NULL; + + module = PyImport_AddModule("__main__"); + object_name = spec; + module_name = strchr(object_name, '.'); + if(module_name){ + *module_name = 0; + //returns borrowed reference + module = PyImport_AddModule(object_name); + + if(!module){ + LOGPF("Module %s for symbol %s.%s is not loaded", object_name, object_name, module_name + 1); + return NULL; + } + + object_name = module_name + 1; + + //returns new reference + result = PyObject_GetAttrString(module, object_name); + } + + free(spec); + return result; +} + static int python_start(size_t n, instance** inst){ python_instance_data* data = NULL; - PyObject* module = NULL; size_t u, p; - char* module_name = NULL, *channel_name = NULL; //resolve channel references to handler functions for(u = 0; u < n; u++){ data = (python_instance_data*) inst[u]->impl; + DBGPF("Starting up instance %s", inst[u]->name); //switch to interpreter PyEval_RestoreThread(data->interpreter); - for(p = 0; p < data->channels; p++){ - module = PyImport_AddModule("__main__"); - channel_name = data->channel[p].name; - module_name = strchr(channel_name, '.'); - if(module_name){ - *module_name = 0; - //returns borrowed reference - module = PyImport_AddModule(channel_name); - - if(!module){ - LOGPF("Module %s for qualified channel %s.%s is not loaded on instance %s", channel_name, channel_name, module_name + 1, inst[u]->name); - return 1; - } - *module_name = '.'; - channel_name = module_name + 1; - } + if(data->default_handler){ + data->handler = python_resolve_symbol(data->default_handler); + } - //returns new reference - data->channel[p].handler = PyObject_GetAttrString(module, channel_name); + for(p = 0; p < data->channels; p++){ + if(!strchr(data->channel[p].name, '.') && data->handler){ + data->channel[p].handler = data->handler; + } + else{ + data->channel[p].handler = python_resolve_symbol(data->channel[p].name); + } } //release interpreter @@ -654,6 +677,7 @@ static int python_shutdown(size_t n, instance** inst){ Py_XDECREF(data->channel[p].handler); } free(data->channel); + free(data->default_handler); //do not free data here, needed for shutting down interpreters } @@ -675,6 +699,7 @@ static int python_shutdown(size_t n, instance** inst){ for(p = 0; p handler); DBGPF("Shutting down interpreter for instance %s", inst[u]->name); //swap to interpreter and end it, GIL is held after this but state is NULL diff --git a/backends/python.h b/backends/python.h index 8ca12f9..a40098b 100644 --- a/backends/python.h +++ b/backends/python.h @@ -41,4 +41,7 @@ typedef struct /*_python_instance_data*/ { size_t channels; mmpython_channel* channel; mmpython_channel* current_channel; + + char* default_handler; + PyObject* handler; } python_instance_data; diff --git a/backends/python.md b/backends/python.md index f06e504..6852a79 100644 --- a/backends/python.md +++ b/backends/python.md @@ -6,6 +6,7 @@ to route, generate and manipulate channel events using the Python 3 scripting la Every instance has its own interpreter, which can be loaded with multiple Python modules. These modules may contain member functions accepting a single `float` parameter, which can then be used as target channels. For each incoming event, the handler function is called. +Channels in the global scope may be assigned a default handler function. Python modules may also register `socket` objects (and an associated callback function) with the MIDIMonster core, which will then alert the module when there is data ready to be read. @@ -67,9 +68,10 @@ The `python` backend does not take any global configuration. #### Instance configuration -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------------------------------| -| `module` | `my_handlers.py` | none | (Path to) Python module source file, relative to configuration file location | +| Option | Example value | Default value | Description | +|-----------------------|-----------------------|-----------------------|-----------------------------------------------| +| `module` | `my_handlers.py` | none | (Path to) Python module source file, relative to configuration file location | +| `default-handler` | `mu_handlers.default` | none | Function to be called as handler for all top-level channels (not belonging to a module) | A single instance may have multiple `module` options specified. This will make all handlers available within their module namespaces (see the section on channel specification). -- cgit v1.2.3 From 2d66d5cee9bf3ed5779f65d8a99b40ee5181bf30 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 20 Mar 2020 21:50:33 +0100 Subject: Implement Lua threading --- backends/lua.c | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- backends/lua.h | 7 ++++ backends/lua.md | 30 ++++++++++++--- 3 files changed, 144 insertions(+), 7 deletions(-) (limited to 'backends/lua.md') diff --git a/backends/lua.c b/backends/lua.c index 498a037..b66a27a 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -10,6 +10,7 @@ #define LUA_REGISTRY_KEY "_midimonster_lua_instance" #define LUA_REGISTRY_CURRENT_CHANNEL "_midimonster_lua_channel" +#define LUA_REGISTRY_CURRENT_THREAD "_midimonster_lua_thread" static size_t timers = 0; static lua_timer* timer = NULL; @@ -20,6 +21,9 @@ static int timer_fd = -1; static uint64_t last_timestamp; #endif +static size_t threads = 0; +static lua_thread* thread = NULL; + MM_PLUGIN_API int init(){ backend lua = { #ifndef MMBACKEND_LUA_TIMERFD @@ -86,6 +90,12 @@ static int lua_update_timerfd(){ interval = timer[n].interval; } } + + for(n = 0; n < threads; n++){ + if(thread[n].timeout && (!interval || thread[n].timeout < interval)){ + interval = thread[n].timeout; + } + } DBGPF("Recalculating timers, minimum is %" PRIu64, interval); //calculate gcd of all timers if any are active @@ -100,7 +110,8 @@ static int lua_update_timerfd(){ gcd = residual; } //since we round everything, 10 is the lowest interval we get - if(interval == 10){ + if(interval <= 10){ + interval = 10; break; } } @@ -126,6 +137,89 @@ static int lua_update_timerfd(){ return 0; } +static void lua_thread_resume(size_t current_thread){ + //push coroutine reference + lua_pushstring(thread[current_thread].thread, LUA_REGISTRY_CURRENT_THREAD); + lua_pushnumber(thread[current_thread].thread, current_thread); + lua_settable(thread[current_thread].thread, LUA_REGISTRYINDEX); + + //call thread main + DBGPF("Resuming thread %" PRIsize_t " on %s", current_thread, thread[current_thread].instance->name); + if(lua_resume(thread[current_thread].thread, NULL, 0) != LUA_YIELD){ + DBGPF("Thread %" PRIsize_t " on %s terminated", current_thread, thread[current_thread].instance->name); + thread[current_thread].timeout = 0; + } + + //remove coroutine reference + lua_pushstring(thread[current_thread].thread, LUA_REGISTRY_CURRENT_THREAD); + lua_pushnil(thread[current_thread].thread); + lua_settable(thread[current_thread].thread, LUA_REGISTRYINDEX); +} + +static int lua_callback_thread(lua_State* interpreter){ + instance* inst = NULL; + size_t u = threads; + if(lua_gettop(interpreter) != 1){ + LOGPF("Thread function called with %d arguments, expected function", lua_gettop(interpreter)); + return 0; + } + + luaL_checktype(interpreter, 1, LUA_TFUNCTION); + + //get instance pointer from registry + lua_pushstring(interpreter, LUA_REGISTRY_KEY); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + inst = (instance*) lua_touserdata(interpreter, -1); + + //make space for a new thread + thread = realloc(thread, (threads + 1) * sizeof(lua_thread)); + if(!thread){ + threads = 0; + LOG("Failed to allocate memory"); + return 0; + } + threads++; + + thread[u].thread = lua_newthread(interpreter); + thread[u].instance = inst; + thread[u].timeout = 0; + thread[u].reference = luaL_ref(interpreter, LUA_REGISTRYINDEX); + + DBGPF("Registered thread %" PRIsize_t " on %s", threads, inst->name); + + //push thread main + luaL_checktype(interpreter, 1, LUA_TFUNCTION); + lua_pushvalue(interpreter, 1); + lua_xmove(interpreter, thread[u].thread, 1); + + lua_thread_resume(u); + lua_update_timerfd(); + return 0; +} + +static int lua_callback_sleep(lua_State* interpreter){ + uint64_t timeout = 0; + size_t current_thread = threads; + if(lua_gettop(interpreter) != 1){ + LOGPF("Sleep function called with %d arguments, expected number", lua_gettop(interpreter)); + return 0; + } + + timeout = luaL_checkinteger(interpreter, 1); + + lua_pushstring(interpreter, LUA_REGISTRY_CURRENT_THREAD); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + + current_thread = luaL_checkinteger(interpreter, -1); + + if(current_thread < threads){ + DBGPF("Yielding for %" PRIu64 "msec on thread %" PRIsize_t, timeout, current_thread); + thread[current_thread].timeout = timeout; + lua_yield(interpreter, 0); + } + return 0; +} + static int lua_callback_output(lua_State* interpreter){ size_t n = 0; channel_value val; @@ -341,6 +435,8 @@ static int lua_instance(instance* inst){ lua_register(data->interpreter, "output_value", lua_callback_output_value); lua_register(data->interpreter, "input_channel", lua_callback_input_channel); lua_register(data->interpreter, "timestamp", lua_callback_timestamp); + lua_register(data->interpreter, "thread", lua_callback_thread); + lua_register(data->interpreter, "sleep", lua_callback_sleep); //store instance pointer to the lua state lua_pushstring(data->interpreter, LUA_REGISTRY_KEY); @@ -454,6 +550,17 @@ static int lua_handle(size_t num, managed_fd* fds){ } } } + + //check for threads to wake up + for(n = 0; n < threads; n++){ + if(thread[n].timeout && delta >= thread[n].timeout){ + lua_thread_resume(n); + lua_update_timerfd(); + } + else if(thread[n].timeout){ + thread[n].timeout -= delta; + } + } return 0; } @@ -468,6 +575,8 @@ static int lua_start(size_t n, instance** inst){ //exclude reserved names if(!data->default_handler && strcmp(data->channel_name[p], "output") + && strcmp(data->channel_name[p], "thread") + && strcmp(data->channel_name[p], "sleep") && strcmp(data->channel_name[p], "input_value") && strcmp(data->channel_name[p], "output_value") && strcmp(data->channel_name[p], "input_channel") @@ -526,6 +635,9 @@ static int lua_shutdown(size_t n, instance** inst){ free(timer); timer = NULL; timers = 0; + free(thread); + thread = NULL; + threads = 0; #ifdef MMBACKEND_LUA_TIMERFD close(timer_fd); timer_fd = -1; diff --git a/backends/lua.h b/backends/lua.h index 743f978..d8e720a 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -39,3 +39,10 @@ typedef struct /*_lua_interval_callback*/ { lua_State* interpreter; int reference; } lua_timer; + +typedef struct /*_lua_coroutine*/ { + instance* instance; + lua_State* thread; + int reference; + uint64_t timeout; +} lua_thread; diff --git a/backends/lua.md b/backends/lua.md index 96e53c8..05509b6 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -19,24 +19,41 @@ The following functions are provided within the Lua interpreter for interaction | `output_value(string)` | `output_value("bar")` | Get the last output value on a channel | | `input_channel()` | `print(input_channel())` | Returns the name of the input channel whose handler function is currently running or `nil` if in an `interval`'ed function (or the initial parse step) | | `timestamp()` | `print(timestamp())` | Returns the core timestamp for this iteration with millisecond resolution. This is not a performance timer, but intended for timeouting, etc | +| `thread(function)` | `thread(run_show)` | Run a function as a Lua thread (see below) | +| `sleep(number)` | `sleep(100)` | Suspend current thread for time specified in milliseconds | Example script: -``` +```lua function bar(value) - output("foo", value / 2) + output("foo", value / 2); end step = 0 function toggle() - output("bar", step * 1.0) + output("bar", step * 1.0); step = (step + 1) % 2; end +function run_show() + while(true) do + sleep(1000); + output("narf", 0); + sleep(1000); + output("narf", 1.0); + end +end + interval(toggle, 1000) +thread(run_show) ``` Input values range between 0.0 and 1.0, output values are clamped to the same range. +Threads are implemented as Lua coroutines, not operating system threads. This means that +cooperative multithreading is required, which can be achieved by calling the `sleep(number)` +function from within a running thread. Calling that function from any other context is +not supported. + #### Global configuration The `lua` backend does not take any global configuration. @@ -61,9 +78,10 @@ lua1.foo > lua2.bar #### Known bugs / problems -Using any of the interface functions (`output`, `interval`, `input_value`, `output_value`, `input_channel`, -`timestamp`) 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. +Using any of the interface functions (`output`, `interval`, etc.) 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. When using a default handler, the default handler will +be called. Output values will not trigger corresponding input event handlers unless the channel is mapped back in the MIDIMonster configuration. This is intentional. -- cgit v1.2.3