diff options
Diffstat (limited to 'backends')
-rw-r--r-- | backends/artnet.c | 43 | ||||
-rw-r--r-- | backends/evdev.c | 8 | ||||
-rw-r--r-- | backends/jack.c | 52 | ||||
-rw-r--r-- | backends/lua.c | 97 | ||||
-rw-r--r-- | backends/lua.h | 1 | ||||
-rw-r--r-- | backends/lua.md | 15 | ||||
-rw-r--r-- | backends/osc.c | 119 | ||||
-rw-r--r-- | backends/python.c | 103 | ||||
-rw-r--r-- | backends/python.h | 1 | ||||
-rw-r--r-- | backends/python.md | 16 | ||||
-rw-r--r-- | backends/sacn.c | 73 | ||||
-rw-r--r-- | backends/winmidi.c | 5 |
12 files changed, 337 insertions, 196 deletions
diff --git a/backends/artnet.c b/backends/artnet.c index caab6e0..585895b 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -206,10 +206,10 @@ static channel* artnet_channel(instance* inst, char* spec, uint8_t flags){ return data->data.channel + chan_a; } -static int artnet_transmit(instance* inst){ - size_t u; +static int artnet_transmit(instance* inst, artnet_output_universe* output){ artnet_instance_data* data = (artnet_instance_data*) inst->impl; - //output frame + + //build output frame artnet_pkt frame = { .magic = {'A', 'r', 't', '-', 'N', 'e', 't', 0x00}, .opcode = htobe16(OpDmx), @@ -224,16 +224,30 @@ static int artnet_transmit(instance* inst){ memcpy(frame.data, data->data.out, 512); if(sendto(artnet_fd[data->fd_index].fd, (uint8_t*) &frame, sizeof(frame), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ - LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); + #ifndef _WIN32 + if(errno != EAGAIN){ + LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); + #else + if(WSAGetLastError() != WSAEWOULDBLOCK){ + char* error = NULL; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); + LOGPF("Failed to output frame for instance %s: %s", inst->name, error); + LocalFree(error); + #endif + return 1; + } + //reschedule frame output + output->mark = 1; + if(!next_frame || next_frame > ARTNET_SYNTHESIZE_MARGIN){ + next_frame = ARTNET_SYNTHESIZE_MARGIN; + } + return 0; } //update last frame timestamp - for(u = 0; u < artnet_fd[data->fd_index].output_instances; u++){ - if(artnet_fd[data->fd_index].output_instance[u].label == inst->ident){ - artnet_fd[data->fd_index].output_instance[u].last_frame = mm_timestamp(); - artnet_fd[data->fd_index].output_instance[u].mark = 0; - } - } + output->last_frame = mm_timestamp(); + output->mark = 0; return 0; } @@ -285,7 +299,7 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v) } return 0; } - return artnet_transmit(inst); + return artnet_transmit(inst, artnet_fd[data->fd_index].output_instance + u); } return 0; @@ -366,7 +380,7 @@ static int artnet_handle(size_t num, managed_fd* fds){ || synthesize_delta >= ARTNET_KEEPALIVE_INTERVAL){ //keepalive timeout inst = mm_instance_find(BACKEND_NAME, artnet_fd[u].output_instance[c].label); if(inst){ - artnet_transmit(inst); + artnet_transmit(inst, artnet_fd[u].output_instance + c); } } @@ -378,11 +392,6 @@ static int artnet_handle(size_t num, managed_fd* fds){ } } - if(!num){ - //early exit - return 0; - } - for(u = 0; u < num; u++){ do{ bytes_read = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); diff --git a/backends/evdev.c b/backends/evdev.c index af5ec74..8a14200 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -357,10 +357,6 @@ static int evdev_handle(size_t num, managed_fd* fds){ int read_status; struct input_event ev; - if(!num){ - return 0; - } - for(fd = 0; fd < num; fd++){ inst = (instance*) fds[fd].impl; if(!inst){ @@ -437,10 +433,6 @@ static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v) int32_t value = 0; uint64_t range = 0; - if(!num){ - return 0; - } - if(!data->output_enabled){ LOGPF("Instance %s not enabled for output (%" PRIsize_t " channel events)", inst->name, num); return 0; diff --git a/backends/jack.c b/backends/jack.c index c862096..d430c60 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -549,38 +549,36 @@ static int mmjack_handle(size_t num, managed_fd* fds){ ssize_t bytes; uint8_t recv_buf[1024]; - if(num){ - for(u = 0; u < num; u++){ - inst = (instance*) fds[u].impl; - data = (mmjack_instance_data*) inst->impl; - bytes = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); - if(bytes < 0){ - LOGPF("Failed to receive on feedback socket for instance %s", inst->name); - return 1; - } + for(u = 0; u < num; u++){ + inst = (instance*) fds[u].impl; + data = (mmjack_instance_data*) inst->impl; + bytes = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); + if(bytes < 0){ + LOGPF("Failed to receive on feedback socket for instance %s", inst->name); + return 1; + } - for(p = 0; p < data->ports; p++){ - if(data->port[p].input && data->port[p].mark){ - pthread_mutex_lock(&data->port[p].lock); - switch(data->port[p].type){ - case port_cv: - mmjack_handle_cv(inst, p, data->port + p); - break; - case port_midi: - mmjack_handle_midi(inst, p, data->port + p); - break; - default: - LOGPF("Output handler not implemented for unknown channel type on %s.%s", inst->name, data->port[p].name); - break; - } - - data->port[p].mark = 0; - pthread_mutex_unlock(&data->port[p].lock); + for(p = 0; p < data->ports; p++){ + if(data->port[p].input && data->port[p].mark){ + pthread_mutex_lock(&data->port[p].lock); + switch(data->port[p].type){ + case port_cv: + mmjack_handle_cv(inst, p, data->port + p); + break; + case port_midi: + mmjack_handle_midi(inst, p, data->port + p); + break; + default: + LOGPF("Output handler not implemented for unknown channel type on %s.%s", inst->name, data->port[p].name); + break; } + + data->port[p].mark = 0; + pthread_mutex_unlock(&data->port[p].lock); } } } - + if(config.jack_shutdown){ LOG("Server disconnected"); return 1; diff --git a/backends/lua.c b/backends/lua.c index 7f80cc7..d7f2643 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -155,8 +155,19 @@ static void lua_thread_resume(size_t current_thread){ lua_settable(thread[current_thread].thread, LUA_REGISTRYINDEX); } -static int lua_callback_thread(lua_State* interpreter){ +static instance* lua_fetch_instance(lua_State* interpreter){ instance* inst = NULL; + + //get instance pointer from registry + lua_pushstring(interpreter, LUA_REGISTRY_KEY); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + inst = (instance*) lua_touserdata(interpreter, -1); + lua_pop(interpreter, 1); + return inst; +} + +static int lua_callback_thread(lua_State* interpreter){ + instance* inst = lua_fetch_instance(interpreter); size_t u = threads; if(lua_gettop(interpreter) != 1){ LOGPF("Thread function called with %d arguments, expected function", lua_gettop(interpreter)); @@ -165,11 +176,6 @@ static int lua_callback_thread(lua_State* interpreter){ 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){ @@ -223,26 +229,25 @@ static int lua_callback_output(lua_State* interpreter){ size_t n = 0; channel_value val; const char* channel_name = NULL; - instance* inst = NULL; - lua_instance_data* data = NULL; + instance* inst = lua_fetch_instance(interpreter); + lua_instance_data* data = (lua_instance_data*) inst->impl; if(lua_gettop(interpreter) != 2){ LOGPF("Output function called with %d arguments, expected 2 (string, number)", 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 function parameters channel_name = lua_tostring(interpreter, 1); + if(!channel_name){ + LOG("Output function called with invalid channel specification"); + return 0; + } + val.normalised = clamp(luaL_checknumber(interpreter, 2), 1.0, 0.0); //if not started yet, create any requested channels so scripts may set them at load time - if(!last_timestamp && channel_name){ + if(!last_timestamp){ lua_channel(inst, (char*) channel_name, mmchannel_output); } @@ -264,6 +269,31 @@ static int lua_callback_output(lua_State* interpreter){ return 0; } +static int lua_callback_cleanup_handler(lua_State* interpreter){ + instance* inst = lua_fetch_instance(interpreter); + lua_instance_data* data = (lua_instance_data*) inst->impl; + int current_handler = data->cleanup_handler; + + if(lua_gettop(interpreter) != 1){ + LOGPF("Cleanup handler function called with %d arguments, expected 1 (function)", lua_gettop(interpreter)); + return 0; + } + + if(lua_type(interpreter, 1) != LUA_TFUNCTION && lua_type(interpreter, 1) != LUA_TNIL){ + LOG("Cleanup handler function parameter was neither nil nor a function"); + return 0; + } + + data->cleanup_handler = luaL_ref(interpreter, LUA_REGISTRYINDEX); + if(current_handler == LUA_NOREF || current_handler == LUA_REFNIL){ + lua_pushnil(interpreter); + return 1; + } + lua_rawgeti(interpreter, LUA_REGISTRYINDEX, current_handler); + luaL_unref(interpreter, LUA_REGISTRYINDEX, current_handler); + return 1; +} + static int lua_callback_interval(lua_State* interpreter){ size_t n = 0; uint64_t interval = 0; @@ -274,10 +304,6 @@ static int lua_callback_interval(lua_State* interpreter){ return 0; } - //get instance pointer from registry - lua_pushstring(interpreter, LUA_REGISTRY_KEY); - lua_gettable(interpreter, LUA_REGISTRYINDEX); - //fetch and round the interval interval = luaL_checkinteger(interpreter, 2); if(interval % 10 < 5){ @@ -341,23 +367,21 @@ static int lua_callback_interval(lua_State* interpreter){ 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; + instance* inst = lua_fetch_instance(interpreter); + lua_instance_data* data = (lua_instance_data*) inst->impl; if(lua_gettop(interpreter) != 1){ LOGPF("get_value function called with %d arguments, expected 1 (string)", 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); + if(!channel_name){ + LOG("get_value function called with invalid channel specification"); + return 0; + } //find correct channel & return value for(n = 0; n < data->channels; n++){ @@ -425,6 +449,7 @@ static int lua_instance(instance* inst){ //load the interpreter data->interpreter = luaL_newstate(); + data->cleanup_handler = LUA_NOREF; if(!data->interpreter){ LOG("Failed to initialize interpreter"); free(data); @@ -441,6 +466,7 @@ static int lua_instance(instance* inst){ lua_register(data->interpreter, "timestamp", lua_callback_timestamp); lua_register(data->interpreter, "thread", lua_callback_thread); lua_register(data->interpreter, "sleep", lua_callback_sleep); + lua_register(data->interpreter, "cleanup_handler", lua_callback_cleanup_handler); //store instance pointer to the lua state lua_pushstring(data->interpreter, LUA_REGISTRY_KEY); @@ -471,7 +497,8 @@ static channel* lua_channel(instance* inst, char* spec, uint8_t flags){ return NULL; } - data->channel[u].in = data->channel[u].out = 0.0; + //initialize new channel + memset(data->channel + u, 0, sizeof(lua_channel_data)); data->channel[u].name = strdup(spec); if(!data->channel[u].name){ LOG("Failed to allocate memory"); @@ -515,7 +542,8 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ } static int lua_handle(size_t num, managed_fd* fds){ - uint64_t delta = timer_interval; + uint64_t delta = mm_timestamp() - last_timestamp; + last_timestamp = mm_timestamp(); size_t n; #ifdef MMBACKEND_LUA_TIMERFD @@ -529,9 +557,6 @@ static int lua_handle(size_t num, managed_fd* fds){ LOGPF("Failed to read timer: %s", strerror(errno)); return 1; } - #else - delta = mm_timestamp() - last_timestamp; - last_timestamp = mm_timestamp(); #endif //no timers active @@ -577,6 +602,7 @@ static int lua_resolve_symbol(lua_State* interpreter, char* symbol){ || !strcmp(symbol, "output_value") || !strcmp(symbol, "input_channel") || !strcmp(symbol, "timestamp") + || !strcmp(symbol, "cleanup_handler") || !strcmp(symbol, "interval")){ return LUA_NOREF; } @@ -638,6 +664,13 @@ static int lua_shutdown(size_t n, instance** inst){ for(u = 0; u < n; u++){ data = (lua_instance_data*) inst[u]->impl; + + //call cleanup function if one is registered + if(data->cleanup_handler != LUA_NOREF && data->cleanup_handler != LUA_REFNIL){ + lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->cleanup_handler); + lua_pcall(data->interpreter, 0, 0, 0); + } + //stop the interpreter lua_close(data->interpreter); //cleanup channel data diff --git a/backends/lua.h b/backends/lua.h index 4583dfe..5587bf9 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -35,6 +35,7 @@ typedef struct /*_lua_instance_data*/ { lua_channel_data* channel; lua_State* interpreter; + int cleanup_handler; char* default_handler; } lua_instance_data; diff --git a/backends/lua.md b/backends/lua.md index 05509b6..e59e513 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -13,10 +13,11 @@ 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 | +| `output(string, number)` | `output("foo", 0.75)` | Output a value event to a channel on this instance | | `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 | +| `cleanup_handler(function)` | `cleanup_handler(shutdown)` | Register a function to be called when the instance is destroyed (on MIDIMonster shutdown). One cleanup handler can be registered per instance. Calling this function when the instance already has a cleanup handler registered replaces the handler, returning the old one. | +| `input_value(string)` | `input_value("foo")` | Get the last input value on a channel on this instance | +| `output_value(string)` | `output_value("bar")` | Get the last output value on a channel on this instance | | `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) | @@ -43,8 +44,13 @@ function run_show() end end +function save_values() + -- Store state to a file, for example +end + interval(toggle, 1000) thread(run_show) +cleanup_handler(save_values) ``` Input values range between 0.0 and 1.0, output values are clamped to the same range. @@ -86,6 +92,9 @@ be called. Output values will not trigger corresponding input event handlers unless the channel is mapped back in the MIDIMonster configuration. This is intentional. +Output events generated from cleanup handlers called during shutdown will not be routed, as the core +routing facility has already shut down at this point. There are no plans to change this behaviour. + 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), the MIDIMonster project can not provide this file within this repository. diff --git a/backends/osc.c b/backends/osc.c index 754c290..72a7b50 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -10,6 +10,7 @@ /* * TODO * ping method + * bundle output */ #define osc_align(a) ((((a) / 4) + (((a) % 4) ? 1 : 0)) * 4) @@ -231,7 +232,7 @@ static int osc_path_validate(char* path, uint8_t allow_patterns){ char pattern_chars[] = "?[]{}*"; size_t u, c; uint8_t square_open = 0, curly_open = 0; - + if(path[0] != '/'){ LOGPF("%s is not a valid OSC path: Missing root /", path); return 1; @@ -331,7 +332,7 @@ static int osc_path_match(char* pattern, char* path){ } if(pattern[match_end + 1] == '-' && pattern[match_end + 2] != ']'){ - if((pattern[match_end] > pattern[match_end + 2] + if((pattern[match_end] > pattern[match_end + 2] && path[u] >= pattern[match_end + 2] && path[u] <= pattern[match_end]) || (pattern[match_end] <= pattern[match_end + 2] @@ -666,7 +667,7 @@ static int osc_output_channel(instance* inst, size_t channel){ memcpy(xmit_buf, data->root, strlen(data->root)); offset += strlen(data->root); } - + memcpy(xmit_buf + offset, data->channel[channel].path, strlen(data->channel[channel].path)); offset += strlen(data->channel[channel].path) + 1; offset = osc_align(offset); @@ -703,16 +704,12 @@ static int osc_output_channel(instance* inst, size_t channel){ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ size_t evt = 0, mark = 0; int rv = 0; + osc_instance_data* data = (osc_instance_data*) inst->impl; osc_channel_ident ident = { .label = 0 }; osc_parameter_value current; - if(!num){ - return 0; - } - - osc_instance_data* data = (osc_instance_data*) inst->impl; if(!data->dest_len){ LOGPF("Instance %s does not have a destination, output is disabled (%" PRIsize_t " channels)", inst->name, num); return 0; @@ -724,7 +721,7 @@ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ //sanity check if(ident.fields.channel >= data->channels || ident.fields.parameter >= data->channel[ident.fields.channel].params){ - LOG("Channel identifier out of range"); + LOG("Channel identifier out of range, possibly an output channel was not pre-configured"); return 1; } @@ -747,7 +744,7 @@ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ mark = 1; } } - + if(mark){ //output all marked channels for(evt = 0; !rv && evt < num; evt++){ @@ -761,7 +758,7 @@ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ return rv; } -static int osc_process_packet(instance* inst, char* local_path, char* format, uint8_t* payload, size_t payload_len){ +static int osc_process_message(instance* inst, char* local_path, char* format, uint8_t* payload, size_t payload_len){ osc_instance_data* data = (osc_instance_data*) inst->impl; size_t c, p, offset = 0; osc_parameter_value min, max, cur; @@ -813,15 +810,82 @@ static int osc_process_packet(instance* inst, char* local_path, char* format, ui return 0; } +static int osc_process_packet(instance* inst, uint8_t* buffer, size_t len){ + osc_instance_data* data = (osc_instance_data*) inst->impl; + size_t offset = 0, message_length = len; + char* osc_local = NULL, *osc_fmt = NULL; + uint8_t* osc_data = NULL; + uint32_t* bundle_size = NULL; + uint8_t decode_bundle = 0; + + //bundles need at least a header and timestamp + if(len >= 16 && !memcmp(buffer, "#bundle\0", 8)){ + decode_bundle = 1; + offset = 16; + } + + do{ + if(decode_bundle){ + if(len - offset < 4){ + LOGPF("Failed to decode bundle size: %" PRIsize_t " bytes left at %" PRIsize_t " of %" PRIsize_t, len - offset, offset, len); + break; + } + bundle_size = (uint32_t*) (buffer + offset); + message_length = be32toh(*bundle_size); + DBGPF("Next bundle entry has %" PRIsize_t " bytes", message_length); + offset += 4; + + if(len - offset < message_length){ + LOGPF("Bundle member size out of bounds: %" PRIsize_t " bytes left", len - offset); + break; + } + } + + //check for recursive bundles + if(message_length >= 16 && !memcmp(buffer + offset, "#bundle\0", 8)){ + DBGPF("Recursing into sub-bundle of size %" PRIsize_t " on %s", message_length, inst->name); + osc_process_packet(inst, buffer + offset, message_length); + } + //ignore messages if root filter active + else if(data->root && strncmp((char*) (buffer + offset), data->root, min(message_length, strlen(data->root)))){ + DBGPF("Ignoring message due to active root filter %s: data is for %s", data->root, buffer + offset); + } + else{ + //FIXME all these accesses should be checked against message_length + osc_local = (char*) (buffer + offset + (data->root ? strlen(data->root) : 0)); + osc_fmt = (char*) (buffer + offset + osc_align(strlen((char*) (buffer + offset)) + 1)); + + if(*osc_fmt != ','){ + //invalid format string + LOGPF("Invalid format string in packet for instance %s: %s", inst->name, osc_fmt); + } + else{ + osc_fmt++; + + if(osc_global_config.detect){ + LOGPF("Incoming data: Path %s.%s Format %s", inst->name, osc_local, osc_fmt); + } + + osc_data = (uint8_t*) osc_fmt + (osc_align(strlen(osc_fmt) + 2) - 1); + if(osc_process_message(inst, osc_local, osc_fmt, osc_data, message_length - (osc_data - (uint8_t*) buffer))){ + LOGPF("Failed to process OSC message on %s", inst->name); + } + } + } + + offset += message_length; + } + while(offset < len); + + return 0; +} + static int osc_handle(size_t num, managed_fd* fds){ size_t fd; - char recv_buf[OSC_RECV_BUF]; + uint8_t recv_buf[OSC_RECV_BUF]; instance* inst = NULL; osc_instance_data* data = NULL; ssize_t bytes_read = 0; - char* osc_fmt = NULL; - char* osc_local = NULL; - uint8_t* osc_data = NULL; for(fd = 0; fd < num; fd++){ inst = (instance*) fds[fd].impl; @@ -845,30 +909,7 @@ static int osc_handle(size_t num, managed_fd* fds){ break; } - if(data->root && strncmp(recv_buf, data->root, min(bytes_read, strlen(data->root)))){ - //ignore packet for different root - continue; - } - osc_local = recv_buf + (data->root ? strlen(data->root) : 0); - - osc_fmt = recv_buf + osc_align(strlen(recv_buf) + 1); - if(*osc_fmt != ','){ - //invalid format string - LOGPF("Invalid format string in packet for instance %s", inst->name); - continue; - } - osc_fmt++; - - if(osc_global_config.detect){ - LOGPF("Incoming data: Path %s.%s Format %s", inst->name, osc_local, osc_fmt); - } - - //FIXME check supplied data length - osc_data = (uint8_t*) osc_fmt + (osc_align(strlen(osc_fmt) + 2) - 1); - - if(osc_process_packet(inst, osc_local, osc_fmt, osc_data, bytes_read - (osc_data - (uint8_t*) recv_buf))){ - return 1; - } + osc_process_packet(inst, recv_buf, bytes_read); } while(bytes_read > 0); #ifdef _WIN32 diff --git a/backends/python.c b/backends/python.c index 9f1d642..28b95a9 100644 --- a/backends/python.c +++ b/backends/python.c @@ -257,6 +257,35 @@ static PyObject* mmpy_interval(PyObject* self, PyObject* args){ return Py_None; } +static PyObject* mmpy_cleanup_handler(PyObject* self, PyObject* args){ + instance* inst = *((instance**) PyModule_GetState(self)); + python_instance_data* data = (python_instance_data*) inst->impl; + PyObject* current_handler = data->cleanup_handler; + + if(!PyArg_ParseTuple(args, "O", &(data->cleanup_handler)) + || (data->cleanup_handler != Py_None && !PyCallable_Check(data->cleanup_handler))){ + data->cleanup_handler = current_handler; + return NULL; + } + + if(data->cleanup_handler == Py_None){ + DBGPF("Cleanup handler removed on %s (previously %s)", inst->name, current_handler ? "active" : "inactive"); + data->cleanup_handler = NULL; + } + else{ + DBGPF("Cleanup handler installed on %s (previously %s)", inst->name, current_handler ? "active" : "inactive"); + Py_INCREF(data->cleanup_handler); + } + + if(!current_handler){ + Py_INCREF(Py_None); + return Py_None; + } + + //do not decrease refcount on current_handler here as the reference may be used by python code again + return current_handler; +} + static PyObject* mmpy_manage_fd(PyObject* self, PyObject* args){ instance* inst = *((instance**) PyModule_GetState(self)); python_instance_data* data = (python_instance_data*) inst->impl; @@ -264,12 +293,10 @@ static PyObject* mmpy_manage_fd(PyObject* self, PyObject* args){ 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"); + if(!PyArg_ParseTuple(args, "OO", &handler, &sock) + || sock == Py_None + || (handler != Py_None && !PyCallable_Check(handler))){ + PyErr_SetString(PyExc_TypeError, "manage() requires either None or a callable and a socket-like object"); return NULL; } @@ -398,13 +425,14 @@ static PyObject* mmpy_init(){ }; static PyMethodDef mmpy_methods[] = { - {"output", mmpy_output, METH_VARARGS, "Output a channel event"}, - {"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"}, + {"output", mmpy_output, METH_VARARGS, "Output a channel event on the instance"}, + {"inputvalue", mmpy_input_value, METH_VARARGS, "Get last input value for a channel on the instance"}, + {"outputvalue", mmpy_output_value, METH_VARARGS, "Get the last output value for a channel on the instance"}, {"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"}, + {"cleanup_handler", mmpy_cleanup_handler, METH_VARARGS, "Register or update the instances cleanup handler"}, {0} }; @@ -569,6 +597,7 @@ static int python_handle(size_t num, managed_fd* fds){ //if timer expired, call handler if(interval[u].delta >= interval[u].interval){ interval[u].delta %= interval[u].interval; + DBGPF("Calling interval handler %" PRIsize_t ", last delta %" PRIu64, u, delta); //swap to interpreter PyEval_RestoreThread(interval[u].interpreter); @@ -577,7 +606,6 @@ static int python_handle(size_t num, managed_fd* fds){ Py_XDECREF(result); //release interpreter PyEval_ReleaseThread(interval[u].interpreter); - DBGPF("Calling interval handler %" PRIsize_t, u); } } } @@ -674,29 +702,44 @@ static int python_start(size_t n, instance** inst){ static int python_shutdown(size_t n, instance** inst){ size_t u, p; + PyObject* result = NULL; python_instance_data* data = NULL; - //clean up channels - //this needs to be done before stopping the interpreters, - //because the handler references are refcounted - for(u = 0; u < n; u++){ - data = (python_instance_data*) inst[u]->impl; - for(p = 0; p < data->channels; p++){ - free(data->channel[p].name); - Py_XDECREF(data->channel[p].handler); + //if there are no instances, the python interpreter is not started, so cleanup can be skipped + if(python_main){ + //release interval references + for(p = 0; p < intervals; p++){ + //swap to interpreter + PyEval_RestoreThread(interval[p].interpreter); + Py_XDECREF(interval[p].reference); + PyEval_ReleaseThread(interval[p].interpreter); } - free(data->channel); - free(data->default_handler); - //do not free data here, needed for shutting down interpreters - } - if(python_main){ - //just used to lock the GIL + //lock the GIL for later interpreter release PyEval_RestoreThread(python_main); for(u = 0; u < n; u++){ data = (python_instance_data*) inst[u]->impl; + //swap to interpreter to be safe for releasing the references + PyThreadState_Swap(data->interpreter); + + //run cleanup handler before cleaning up channel data to allow reading channel data + if(data->cleanup_handler){ + result = PyObject_CallFunction(data->cleanup_handler, NULL); + Py_XDECREF(result); + Py_XDECREF(data->cleanup_handler); + } + + //clean up channels + for(p = 0; p < data->channels; p++){ + free(data->channel[p].name); + Py_XDECREF(data->channel[p].handler); + } + free(data->channel); + free(data->default_handler); + Py_XDECREF(data->handler); + //close sockets for(p = 0; p < data->sockets; p++){ close(data->socket[p].fd); //FIXME does python do this on its own? @@ -704,26 +747,18 @@ static int python_shutdown(size_t n, instance** inst){ Py_XDECREF(data->socket[p].handler); } - //release interval references - for(p = 0; p <intervals; p++){ - Py_XDECREF(interval[p].reference); - } - Py_XDECREF(data->handler); - + //shut down interpreter, GIL is held after this but state is NULL 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); PyErr_Clear(); //PyThreadState_Clear(data->interpreter); Py_EndInterpreter(data->interpreter); - free(data); } //shut down main interpreter PyThreadState_Swap(python_main); if(Py_FinalizeEx()){ - LOG("Failed to destroy python interpreters"); + LOG("Failed to shut down python library"); } PyMem_RawFree(program_name); } diff --git a/backends/python.h b/backends/python.h index 020aeac..539389b 100644 --- a/backends/python.h +++ b/backends/python.h @@ -45,4 +45,5 @@ typedef struct /*_python_instance_data*/ { char* default_handler; PyObject* handler; + PyObject* cleanup_handler; } python_instance_data; diff --git a/backends/python.md b/backends/python.md index 6852a79..ab0fb38 100644 --- a/backends/python.md +++ b/backends/python.md @@ -17,13 +17,14 @@ The `midimonster` module provides the following functions: | Function | Usage example | Description | |-------------------------------|---------------------------------------|-----------------------------------------------| -| `output(string, float)` | `midimonster.output("foo", 0.75)` | Output a value event to a channel | -| `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 | +| `output(string, float)` | `midimonster.output("foo", 0.75)` | Output a value event to a channel on this instance | +| `inputvalue(string)` | `midimonster.inputvalue("foo")` | Get the last input value on a channel of this instance | +| `outputvalue(string)` | `midimonster.outputvalue("bar")` | Get the last output value on a channel of this instance | | `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 | +| `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 | +| `cleanup_handler(function)` | `midimonster.cleanup_handler(save_all)`| Register a function to be called when the instance is destroyed (on MIDIMonster shutdown). One cleanup handler can be registered per instance. Calling this function when the instance already has a cleanup handler registered replaces the handler, returning the old one. | Example Python module: ```python @@ -48,12 +49,16 @@ def socket_handler(sock): def ping(): print(midimonster.timestamp()) +def save_positions(): + # Store some data to disk + # 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(socket_handler, s) +midimonster.cleanup_handler(save_positions) ``` Input values range between 0.0 and 1.0, output values are clamped to the same range. @@ -92,6 +97,9 @@ 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. +Output events generated from cleanup handlers called during shutdown will not be routed, as the core +routing facility has already shut down at this point. There are no plans to change this behaviour. + 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` diff --git a/backends/sacn.c b/backends/sacn.c index bd5c75a..b3376d7 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -275,9 +275,10 @@ static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){ return data->data.channel + chan_a; } -static int sacn_transmit(instance* inst){ - size_t u; +static int sacn_transmit(instance* inst, sacn_output_universe* output){ sacn_instance_data* data = (sacn_instance_data*) inst->impl; + + //build sacn frame sacn_data_pdu pdu = { .root = { .preamble_size = htobe16(0x10), @@ -312,16 +313,31 @@ static int sacn_transmit(instance* inst){ memcpy((((uint8_t*)pdu.data.data) + 1), data->data.out, 512); if(sendto(global_cfg.fd[data->fd_index].fd, (uint8_t*) &pdu, sizeof(pdu), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ - LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); - } + #ifndef _WIN32 + if(errno != EAGAIN){ + LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); + #else + if(WSAGetLastError() != WSAEWOULDBLOCK){ + char* error = NULL; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); + LOGPF("Failed to output frame for instance %s: %s", inst->name, error); + LocalFree(error); + #endif + return 1; + } - //update last transmit timestamp, unmark instance - for(u = 0; u < global_cfg.fd[data->fd_index].universes; u++){ - if(global_cfg.fd[data->fd_index].universe[u].universe == data->uni){ - global_cfg.fd[data->fd_index].universe[u].last_frame = mm_timestamp(); - global_cfg.fd[data->fd_index].universe[u].mark = 0; + //reschedule output + output->mark = 1; + if(!global_cfg.next_frame || global_cfg.next_frame > SACN_SYNTHESIZE_MARGIN){ + global_cfg.next_frame = SACN_SYNTHESIZE_MARGIN; } + return 0; } + + //update last transmit timestamp, unmark instance + output->last_frame = mm_timestamp(); + output->mark = 0; return 0; } @@ -330,10 +346,6 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ uint32_t frame_delta = 0; sacn_instance_data* data = (sacn_instance_data*) inst->impl; - if(!num){ - return 0; - } - if(!data->xmit_prio){ LOGPF("Instance %s not enabled for output (%" PRIsize_t " channel events)", inst->name, num); return 0; @@ -361,14 +373,14 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ //send packet if required if(mark){ - if(!data->realtime){ - //find output instance data - for(u = 0; u < global_cfg.fd[data->fd_index].universes; u++){ - if(global_cfg.fd[data->fd_index].universe[u].universe == data->uni){ - break; - } + //find output instance data + for(u = 0; u < global_cfg.fd[data->fd_index].universes; u++){ + if(global_cfg.fd[data->fd_index].universe[u].universe == data->uni){ + break; } + } + if(!data->realtime){ frame_delta = mm_timestamp() - global_cfg.fd[data->fd_index].universe[u].last_frame; //check if ratelimiting engaged @@ -380,7 +392,7 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ return 0; } } - sacn_transmit(inst); + sacn_transmit(inst, global_cfg.fd[data->fd_index].universe + u); } return 0; @@ -500,7 +512,19 @@ static void sacn_discovery(size_t fd){ memcpy(pdu.data.data, global_cfg.fd[fd].universe + page * 512, universes * sizeof(uint16_t)); if(sendto(global_cfg.fd[fd].fd, (uint8_t*) &pdu, sizeof(pdu) - (512 - universes) * sizeof(uint16_t), 0, (struct sockaddr*) &discovery_dest, sizeof(discovery_dest)) < 0){ - LOGPF("Failed to output universe discovery frame for interface %" PRIsize_t ": %s", fd, strerror(errno)); + #ifndef _WIN32 + if(errno != EAGAIN){ + LOGPF("Failed to output universe discovery frame for interface %" PRIsize_t ": %s", fd, strerror(errno)); + } + #else + if(WSAGetLastError() != WSAEWOULDBLOCK){ + char* error = NULL; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); + LOGPF("Failed to output universe discovery frame for interface %" PRIsize_t ": %s", fd, error); + LocalFree(error); + } + #endif } } } @@ -541,7 +565,7 @@ static int sacn_handle(size_t num, managed_fd* fds){ instance_id.fields.uni = global_cfg.fd[u].universe[c].universe; inst = mm_instance_find(BACKEND_NAME, instance_id.label); if(inst){ - sacn_transmit(inst); + sacn_transmit(inst, global_cfg.fd[u].universe + c); } } @@ -554,11 +578,6 @@ static int sacn_handle(size_t num, managed_fd* fds){ } } - //early exit - if(!num){ - return 0; - } - for(u = 0; u < num; u++){ do{ bytes_read = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); diff --git a/backends/winmidi.c b/backends/winmidi.c index d9b3047..753f25c 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -185,11 +185,6 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v }; size_t u; - //early exit - if(!num){ - return 0; - } - if(!data->device_out){ LOGPF("Instance %s has no output device", inst->name); return 0; |