diff options
Diffstat (limited to 'backends')
| -rw-r--r-- | backends/python.c | 294 | ||||
| -rw-r--r-- | backends/python.h | 19 | ||||
| -rw-r--r-- | backends/python.md | 44 | 
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. | 
