aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorcbdev <cb@cbcdn.com>2020-03-08 13:24:33 +0100
committercbdev <cb@cbcdn.com>2020-03-08 13:24:33 +0100
commit38d3724b2af3c2b08c548326797c2421b054c846 (patch)
tree307d15dcbe27426b396f5e5f06b40bb67a8d2376
parentf9829ae90d4017940047b561e412c6eb7f431adb (diff)
downloadmidimonster-38d3724b2af3c2b08c548326797c2421b054c846.tar.gz
midimonster-38d3724b2af3c2b08c548326797c2421b054c846.tar.bz2
midimonster-38d3724b2af3c2b08c548326797c2421b054c846.zip
Implement python intervaling and socket management
-rw-r--r--backends/python.c294
-rw-r--r--backends/python.h19
-rw-r--r--backends/python.md44
3 files changed, 346 insertions, 11 deletions
diff --git a/backends/python.c b/backends/python.c
index 9fad0ae..70c2548 100644
--- a/backends/python.c
+++ b/backends/python.c
@@ -7,13 +7,14 @@
#define MMPY_INSTANCE_KEY "midimonster_instance"
-/*
- * TODO might want to export the full MM_API set to python at some point
- */
-
static PyThreadState* python_main = NULL;
static wchar_t* program_name = NULL;
+static uint64_t last_timestamp = 0;
+static uint32_t timer_interval = 0;
+static size_t intervals = 0;
+static mmpy_timer* interval = NULL;
+
MM_PLUGIN_API int init(){
backend python = {
.name = BACKEND_NAME,
@@ -24,6 +25,7 @@ MM_PLUGIN_API int init(){
.handle = python_set,
.process = python_handle,
.start = python_start,
+ .interval = python_interval,
.shutdown = python_shutdown
};
@@ -35,6 +37,57 @@ MM_PLUGIN_API int init(){
return 0;
}
+static uint32_t python_interval(){
+ size_t u = 0;
+ uint32_t next_timer = 1000;
+
+ if(timer_interval){
+ for(u = 0; u < intervals; u++){
+ if(interval[u].interval &&
+ interval[u].interval - interval[u].delta < next_timer){
+ next_timer = interval[u].interval - interval[u].delta;
+ }
+ }
+ DBGPF("Next timer fires in %" PRIu32, next_timer);
+ return next_timer;
+ }
+
+ return 1000;
+}
+
+static void python_timer_recalculate(){
+ uint64_t next_interval = 0, gcd, residual;
+ size_t u;
+
+ //find lower interval bounds
+ for(u = 0; u < intervals; u++){
+ if(interval[u].interval && (!next_interval || interval[u].interval < next_interval)){
+ next_interval = interval[u].interval;
+ }
+ }
+
+ if(next_interval){
+ for(u = 0; u < intervals; u++){
+ if(interval[u].interval){
+ //calculate gcd of current interval and this timers interval
+ gcd = interval[u].interval;
+ while(gcd){
+ residual = next_interval % gcd;
+ next_interval = gcd;
+ gcd = residual;
+ }
+
+ //10msec is absolute lower limit and minimum gcd due to rounding
+ if(next_interval == 10){
+ break;
+ }
+ }
+ }
+ }
+
+ timer_interval = next_interval;
+}
+
static int python_configure(char* option, char* value){
LOG("No backend configuration possible");
return 1;
@@ -64,7 +117,7 @@ static PyObject* mmpy_output(PyObject* self, PyObject* args){
const char* channel_name = NULL;
channel* chan = NULL;
channel_value val = {
- 0
+ {0}
};
size_t u;
@@ -137,6 +190,163 @@ static PyObject* mmpy_input_value(PyObject* self, PyObject* args){
return mmpy_channel_value(self, args, 1);
}
+static PyObject* mmpy_timestamp(PyObject* self, PyObject* args){
+ return PyLong_FromUnsignedLong(mm_timestamp());
+}
+
+static PyObject* mmpy_interval(PyObject* self, PyObject* args){
+ instance* inst = *((instance**) PyModule_GetState(self));
+ python_instance_data* data = (python_instance_data*) inst->impl;
+ unsigned long updated_interval = 0;
+ PyObject* reference = NULL;
+ size_t u;
+
+ if(!PyArg_ParseTuple(args, "Ok", &reference, &updated_interval)){
+ return NULL;
+ }
+
+ if(!PyCallable_Check(reference)){
+ PyErr_SetString(PyExc_TypeError, "interval() requires a callable");
+ return NULL;
+ }
+
+ //round interval
+ if(updated_interval % 10 < 5){
+ updated_interval -= updated_interval % 10;
+ }
+ else{
+ updated_interval += (10 - (updated_interval % 10));
+ }
+
+ //find reference
+ for(u = 0; u < intervals; u++){
+ if(interval[u].interpreter == data->interpreter
+ && PyObject_RichCompareBool(reference, interval[u].reference, Py_EQ) == 1){
+ DBGPF("Updating interval to %" PRIu64 " msec", updated_interval);
+ break;
+ }
+ }
+
+ //register new interval
+ if(u == intervals && updated_interval){
+ //create new interval slot
+ DBGPF("Registering interval with %" PRIu64 " msec", updated_interval);
+ interval = realloc(interval, (intervals + 1) * sizeof(mmpy_timer));
+ if(!interval){
+ intervals = 0;
+ LOG("Failed to allocate memory");
+ return NULL;
+ }
+ Py_INCREF(reference);
+ interval[intervals].delta = 0;
+ interval[intervals].reference = reference;
+ interval[intervals].interpreter = data->interpreter;
+ intervals++;
+ }
+
+ //update if existing or created
+ if(u < intervals){
+ interval[u].interval = updated_interval;
+ python_timer_recalculate();
+ }
+
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+static PyObject* mmpy_manage_fd(PyObject* self, PyObject* args){
+ instance* inst = *((instance**) PyModule_GetState(self));
+ python_instance_data* data = (python_instance_data*) inst->impl;
+ PyObject* handler = NULL, *sock = NULL, *fileno = NULL;
+ size_t u = 0, last_free = 0;
+ int fd = -1;
+
+ if(!PyArg_ParseTuple(args, "OO", &handler, &sock)){
+ return NULL;
+ }
+
+ if(handler != Py_None && !PyCallable_Check(handler)){
+ PyErr_SetString(PyExc_TypeError, "manage() requires either None or a callable");
+ return NULL;
+ }
+
+ fileno = PyObject_CallMethod(sock, "fileno", NULL);
+ if(!fileno || fileno == Py_None || !PyLong_Check(fileno)){
+ PyErr_SetString(PyExc_TypeError, "manage() requires a socket-like object");
+ return NULL;
+ }
+
+ fd = PyLong_AsLong(fileno);
+ if(fd < 0){
+ PyErr_SetString(PyExc_TypeError, "manage() requires a (connected) socket-like object");
+ return NULL;
+ }
+
+ //check if this socket instance was already registered
+ last_free = data->sockets;
+ for(u = 0; u < data->sockets; u++){
+ if(!data->socket[u].socket){
+ last_free = u;
+ }
+ else if(PyObject_RichCompareBool(sock, data->socket[u].socket, Py_EQ) == 1){
+ break;
+ }
+ }
+
+ if(u < data->sockets){
+ //modify existing socket
+ Py_XDECREF(data->socket[u].handler);
+ if(handler != Py_None){
+ DBGPF("Updating handler for fd %d on %s", fd, inst->name);
+ data->socket[u].handler = handler;
+ Py_INCREF(handler);
+ }
+ else{
+ DBGPF("Unregistering fd %d on %s", fd, inst->name);
+ mm_manage_fd(data->socket[u].fd, BACKEND_NAME, 0, NULL);
+ Py_XDECREF(data->socket[u].socket);
+ data->socket[u].handler = NULL;
+ data->socket[u].socket = NULL;
+ data->socket[u].fd = -1;
+ }
+ }
+ else if(handler != Py_None){
+ //check that the fd is not already registered with another socket instance
+ for(u = 0; u < data->sockets; u++){
+ if(data->socket[u].fd == fd){
+ //FIXME this might also raise an exception
+ LOGPF("Descriptor already registered with another socket on instance %s", inst->name);
+ Py_INCREF(Py_None);
+ return Py_None;
+ }
+ }
+
+ DBGPF("Registering new fd %d on %s", fd, inst->name);
+ if(last_free == data->sockets){
+ //allocate a new socket instance
+ data->socket = realloc(data->socket, (data->sockets + 1) * sizeof(mmpy_socket));
+ if(!data->socket){
+ data->sockets = 0;
+ LOG("Failed to allocate memory");
+ return NULL;
+ }
+ data->sockets++;
+ }
+
+ //store new reference
+ //FIXME check this for errors
+ mm_manage_fd(fd, BACKEND_NAME, 1, inst);
+ data->socket[last_free].fd = fd;
+ Py_INCREF(handler);
+ data->socket[last_free].handler = handler;
+ Py_INCREF(sock);
+ data->socket[last_free].socket = sock;
+ }
+
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
static int mmpy_exec(PyObject* module) {
instance** inst = (instance**) PyModule_GetState(module);
//FIXME actually use interpreter dict (from python 3.8) here at some point
@@ -146,7 +356,7 @@ static int mmpy_exec(PyObject* module) {
return 0;
}
- //TODO raise exception
+ PyErr_SetString(PyExc_AssertionError, "Failed to pass instance pointer for initialization");
return -1;
}
@@ -184,6 +394,9 @@ static PyObject* mmpy_init(){
{"inputvalue", mmpy_input_value, METH_VARARGS, "Get last input value for a channel"},
{"outputvalue", mmpy_output_value, METH_VARARGS, "Get the last output value for a channel"},
{"current", mmpy_current_handler, METH_VARARGS, "Get the name of the currently executing channel handler"},
+ {"timestamp", mmpy_timestamp, METH_VARARGS, "Get the core timestamp (in milliseconds)"},
+ {"manage", mmpy_manage_fd, METH_VARARGS, "(Un-)register a socket or file descriptor for notifications"},
+ {"interval", mmpy_interval, METH_VARARGS, "Register or update an interval handler"},
{0}
};
@@ -324,12 +537,66 @@ static int python_set(instance* inst, size_t num, channel** c, channel_value* v)
}
}
+ //release interpreter
PyEval_ReleaseThread(data->interpreter);
return 0;
}
static int python_handle(size_t num, managed_fd* fds){
- //TODO implement some kind of intervaling functionality before people get it in their heads to start `import threading`
+ instance* inst = NULL;
+ python_instance_data* data = NULL;
+ PyObject* result = NULL;
+ size_t u, p;
+
+ //handle intervals
+ if(timer_interval){
+ uint64_t delta = mm_timestamp() - last_timestamp;
+ last_timestamp = mm_timestamp();
+
+ //add delta to all active timers
+ for(u = 0; u < intervals; u++){
+ if(interval[u].interval){
+ interval[u].delta += delta;
+
+ //if timer expired, call handler
+ if(interval[u].delta >= interval[u].interval){
+ interval[u].delta %= interval[u].interval;
+
+ //swap to interpreter
+ PyEval_RestoreThread(interval[u].interpreter);
+ //call handler
+ result = PyObject_CallFunction(interval[u].reference, NULL);
+ Py_XDECREF(result);
+ //release interpreter
+ PyEval_ReleaseThread(interval[u].interpreter);
+ DBGPF("Calling interval handler %" PRIsize_t, u);
+ }
+ }
+ }
+ }
+
+ for(u = 0; u < num; u++){
+ inst = (instance*) fds[u].impl;
+ data = (python_instance_data*) inst->impl;
+
+ //swap to interpreter
+ PyEval_RestoreThread(data->interpreter);
+
+ //handle callbacks
+ for(p = 0; p < data->sockets; p++){
+ if(data->socket[p].socket
+ && data->socket[p].fd == fds[u].fd){
+ //FIXME maybe close/unregister the socket on handling errors
+ DBGPF("Calling descriptor handler on %s for fd %d", inst->name, data->socket[p].fd);
+ result = PyObject_CallFunction(data->socket[p].handler, "O", data->socket[p].socket);
+ Py_XDECREF(result);
+ }
+ }
+
+ //release interpreter
+ PyEval_ReleaseThread(data->interpreter);
+ }
+
return 0;
}
@@ -396,6 +663,19 @@ static int python_shutdown(size_t n, instance** inst){
for(u = 0; u < n; u++){
data = (python_instance_data*) inst[u]->impl;
+
+ //close sockets
+ for(p = 0; p < data->sockets; p++){
+ close(data->socket[p].fd); //FIXME does python do this on its own?
+ Py_XDECREF(data->socket[p].socket);
+ Py_XDECREF(data->socket[p].handler);
+ }
+
+ //release interval references
+ for(p = 0; p <intervals; p++){
+ Py_XDECREF(interval[p].reference);
+ }
+
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
PyThreadState_Swap(data->interpreter);
diff --git a/backends/python.h b/backends/python.h
index 10411ca..8ca12f9 100644
--- a/backends/python.h
+++ b/backends/python.h
@@ -1,6 +1,7 @@
#include "midimonster.h"
MM_PLUGIN_API int init();
+static uint32_t python_interval();
static int python_configure(char* option, char* value);
static int python_configure_instance(instance* inst, char* option, char* value);
static int python_instance(instance* inst);
@@ -17,8 +18,26 @@ typedef struct /*_python_channel_data*/ {
double out;
} mmpython_channel;
+typedef struct /*_mmpy_registered_socket*/ {
+ int fd;
+ PyObject* handler;
+ PyObject* socket;
+} mmpy_socket;
+
+typedef struct /*_mmpy_interval*/ {
+ uint64_t interval;
+ uint64_t delta;
+ PyObject* reference;
+ PyThreadState* interpreter;
+} mmpy_timer;
+
typedef struct /*_python_instance_data*/ {
PyThreadState* interpreter;
+ PyObject* config; //TODO
+
+ size_t sockets;
+ mmpy_socket* socket;
+
size_t channels;
mmpython_channel* channel;
mmpython_channel* current_channel;
diff --git a/backends/python.md b/backends/python.md
index fae3139..b6a1162 100644
--- a/backends/python.md
+++ b/backends/python.md
@@ -7,6 +7,9 @@ Every instance has its own interpreter, which can be loaded with multiple Python
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.
+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.
+
To interact with the MIDIMonster core, import the `midimonster` module from within your module.
The `midimonster` module provides the following functions:
@@ -17,17 +20,47 @@ The `midimonster` module provides the following functions:
| `inputvalue(string)` | `midimonster.inputvalue("foo")` | Get the last input value on a channel |
| `outputvalue(string)` | `midimonster.outputvalue("bar")` | Get the last output value on a channel |
| `current()` | `print(midimonster.current())` | Returns the name of the input channel whose handler function is currently running or `None` if the interpreter was called from another context |
+| `timestamp()` | `print(midimonster.timestamp())` | Get the internal core timestamp (in milliseconds) |
+| `interval(function, long)` | `midimonster.interval(toggle, 100)` | Register a function to be called periodically. Interval is specified in milliseconds (accurate to 10msec). Calling `interval` with the same function again updates the interval. Specifying the interval as `0` cancels the interval |
+| `manage(function, socket)` | `midimonster.manage(handler, socket)`| Register a (connected/listening) socket to the MIDIMonster core. Calls `function(socket)` when the socket is ready to read. Calling this method with `None` as the function argument unregisters the socket. A socket may only have one associated handler |
Example Python module:
```
+import socket
import midimonster
+# Simple channel ahndler
def in1(value):
midimonster.output("out1", 1 - value)
+
+# Socket data handler
+def socket_handler(sock):
+ # This should get some more error handling
+ data = sock.recv(1024)
+ print("Received %d bytes from socket: %s" % (len(data), data))
+ if(len(data) == 0):
+ # Unmanage the socket if it has been closed
+ midimonster.manage(None, sock)
+ sock.close()
+
+# Interval handler
+def ping():
+ print(midimonster.interval())
+
+# Register an interval
+midimonster.interval(ping, 1000)
+# Create and register a client socket (add error handling as you like)
+s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+s.connect(("localhost", 8990))
+midimonster.manage(reader, s)
```
Input values range between 0.0 and 1.0, output values are clamped to the same range.
+Note that registered sockets that have been closed (`socket.recv()` returned 0 bytes)
+need to be unregistered from the MIDIMonster core, otherwise the core socket multiplexing
+mechanism will report an error and shut down the MIDIMonster.
+
#### Global configuration
The `python` backend does not take any global configuration.
@@ -49,7 +82,7 @@ specify the channel as the functions qualified path (by prefixing it with the mo
Example mappings:
```
py1.my_handlers.in1 < py1.foo
-py1.out1 > py2.module.handler
+py1.out1 > py2.module.handler
```
#### Known bugs / problems
@@ -57,7 +90,10 @@ py1.out1 > py2.module.handler
Output values will not trigger corresponding input event handlers unless the channel is mapped
back in the MIDIMonster configuration. This is intentional.
-Importing a Python module named `midimonster` may cause problems and is unsupported.
+Importing a Python module named `midimonster` is probably a bad idea and thus unsupported.
+
+The MIDIMonster is, at its core, single-threaded. Do not try to use Python's `threading`
+module with the MIDIMonster.
-There is currently no functionality for cyclic execution. This may be implemented in a future
-release.
+Note that executing Python code blocks the MIDIMonster core. It is not a good idea to call functions that
+take a long time to complete (such as `time.sleep()`) within your Python modules.