aboutsummaryrefslogtreecommitdiffhomepage
path: root/backends
diff options
context:
space:
mode:
authorcbdev <cb@cbcdn.com>2020-03-23 00:15:00 +0100
committercbdev <cb@cbcdn.com>2020-03-23 00:15:00 +0100
commit37e712edf23a49be5387f945ab9ea57cc0b57f22 (patch)
tree5037b14bdbd08baa8f104f7e4946aa5643281a94 /backends
parent666aec036f9bf0de82c435bd7eace271613cee1e (diff)
parentaa02ccf3abf183207b24fcbb9460cbd904a698e2 (diff)
downloadmidimonster-37e712edf23a49be5387f945ab9ea57cc0b57f22.tar.gz
midimonster-37e712edf23a49be5387f945ab9ea57cc0b57f22.tar.bz2
midimonster-37e712edf23a49be5387f945ab9ea57cc0b57f22.zip
Merge current master
Diffstat (limited to 'backends')
-rw-r--r--backends/Makefile11
-rw-r--r--backends/artnet.c110
-rw-r--r--backends/artnet.h19
-rw-r--r--backends/artnet.md5
-rw-r--r--backends/evdev.c13
-rw-r--r--backends/evdev.h2
-rw-r--r--backends/jack.c12
-rw-r--r--backends/jack.h2
-rw-r--r--backends/libmmbackend.c4
-rw-r--r--backends/loopback.c16
-rw-r--r--backends/loopback.h2
-rw-r--r--backends/lua.c285
-rw-r--r--backends/lua.h24
-rw-r--r--backends/lua.md66
-rw-r--r--backends/maweb.c32
-rw-r--r--backends/maweb.h2
-rw-r--r--backends/maweb.md5
-rw-r--r--backends/midi.c143
-rw-r--r--backends/midi.h2
-rw-r--r--backends/midi.md5
-rw-r--r--backends/ola.cpp16
-rw-r--r--backends/ola.h2
-rw-r--r--backends/openpixelcontrol.c686
-rw-r--r--backends/openpixelcontrol.h59
-rw-r--r--backends/openpixelcontrol.md55
-rw-r--r--backends/osc.c11
-rw-r--r--backends/osc.h2
-rw-r--r--backends/python.c733
-rw-r--r--backends/python.h48
-rw-r--r--backends/python.md101
-rw-r--r--backends/sacn.c120
-rw-r--r--backends/sacn.h20
-rw-r--r--backends/sacn.md4
-rw-r--r--backends/winmidi.c80
-rw-r--r--backends/winmidi.h2
35 files changed, 2330 insertions, 369 deletions
diff --git a/backends/Makefile b/backends/Makefile
index f0d5c3e..8956a20 100644
--- a/backends/Makefile
+++ b/backends/Makefile
@@ -1,7 +1,7 @@
.PHONY: all clean full
LINUX_BACKENDS = midi.so evdev.so
-WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll rtpmidi.dll
-BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so rtpmidi.so
+WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll openpixelcontrol.dll rtpmidi.dll
+BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.so rtpmidi.dll
OPTIONAL_BACKENDS = ola.so
BACKEND_LIB = libmmbackend.o
@@ -36,6 +36,10 @@ sacn.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
sacn.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
sacn.dll: LDLIBS += -lws2_32
+openpixelcontrol.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
+openpixelcontrol.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
+openpixelcontrol.dll: LDLIBS += -lws2_32
+
maweb.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
maweb.so: LDLIBS = -lssl
maweb.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
@@ -63,6 +67,9 @@ lua.so: LDLIBS += $(shell pkg-config --libs lua53 || pkg-config --libs lua5.3 ||
lua.dll: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"")
lua.dll: LDLIBS += -L../libs -llua53
+python.so: CFLAGS += $(shell pkg-config --cflags python3 || pkg-config --cflags python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"")
+python.so: CFLAGS += $(shell pkg-config --libs python3 || pkg-config --libs python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"")
+
%.so :: %.c %.h $(BACKEND_LIB)
$(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS)
diff --git a/backends/artnet.c b/backends/artnet.c
index 0bd1a32..caab6e0 100644
--- a/backends/artnet.c
+++ b/backends/artnet.c
@@ -9,6 +9,7 @@
#define MAX_FDS 255
+static uint32_t next_frame = 0;
static uint8_t default_net = 0;
static size_t artnet_fds = 0;
static artnet_descriptor* artnet_fd = NULL;
@@ -37,7 +38,6 @@ static int artnet_listener(char* host, char* port){
artnet_fd[artnet_fds].fd = fd;
artnet_fd[artnet_fds].output_instances = 0;
artnet_fd[artnet_fds].output_instance = NULL;
- artnet_fd[artnet_fds].last_frame = NULL;
artnet_fds++;
return 0;
}
@@ -52,6 +52,7 @@ MM_PLUGIN_API int init(){
.handle = artnet_set,
.process = artnet_handle,
.start = artnet_start,
+ .interval = artnet_interval,
.shutdown = artnet_shutdown
};
@@ -68,6 +69,13 @@ MM_PLUGIN_API int init(){
return 0;
}
+static uint32_t artnet_interval(){
+ if(next_frame){
+ return next_frame;
+ }
+ return ARTNET_KEEPALIVE_INTERVAL;
+}
+
static int artnet_configure(char* option, char* value){
char* host = NULL, *port = NULL, *fd_opts = NULL;
if(!strcmp(option, "net")){
@@ -94,23 +102,23 @@ static int artnet_configure(char* option, char* value){
return 1;
}
-static instance* artnet_instance(){
- artnet_instance_data* data = NULL;
- instance* inst = mm_instance();
- if(!inst){
- return NULL;
- }
+static int artnet_instance(instance* inst){
+ artnet_instance_data* data = calloc(1, sizeof(artnet_instance_data));
+ size_t u;
- data = calloc(1, sizeof(artnet_instance_data));
if(!data){
LOG("Failed to allocate memory");
- return NULL;
+ return 1;
}
data->net = default_net;
+ for(u = 0; u < sizeof(data->data.channel) / sizeof(channel); u++){
+ data->data.channel[u].ident = u;
+ data->data.channel[u].instance = inst;
+ }
inst->impl = data;
- return inst;
+ return 0;
}
static int artnet_configure_instance(instance* inst, char* option, char* value){
@@ -162,6 +170,11 @@ static channel* artnet_channel(instance* inst, char* spec, uint8_t flags){
}
chan_a--;
+ //check output capabilities
+ if((flags & mmchannel_output) && !data->dest_len){
+ LOGPF("Channel %s.%s mapped for output, but instance is not configured for output (missing destination)", inst->name, spec);
+ }
+
//secondary channel setup
if(*spec_next == '+'){
chan_b = strtoul(spec_next + 1, NULL, 10);
@@ -190,7 +203,7 @@ static channel* artnet_channel(instance* inst, char* spec, uint8_t flags){
}
data->data.map[chan_a] = (*spec_next == '+') ? (MAP_COARSE | chan_b) : (MAP_SINGLE | chan_a);
- return mm_channel(inst, chan_a, 1);
+ return data->data.channel + chan_a;
}
static int artnet_transmit(instance* inst){
@@ -217,14 +230,16 @@ static int artnet_transmit(instance* inst){
//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].last_frame[u] = mm_timestamp();
+ artnet_fd[data->fd_index].output_instance[u].last_frame = mm_timestamp();
+ artnet_fd[data->fd_index].output_instance[u].mark = 0;
}
}
return 0;
}
static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v){
- size_t u, mark = 0;
+ uint32_t frame_delta = 0;
+ size_t u, mark = 0, channel_offset = 0;
artnet_instance_data* data = (artnet_instance_data*) inst->impl;
if(!data->dest_len){
@@ -232,28 +247,44 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v)
return 0;
}
- //FIXME maybe introduce minimum frame interval
for(u = 0; u < num; u++){
- if(IS_WIDE(data->data.map[c[u]->ident])){
+ channel_offset = c[u]->ident;
+ if(IS_WIDE(data->data.map[channel_offset])){
uint32_t val = v[u].normalised * ((double) 0xFFFF);
//the primary (coarse) channel is the one registered to the core, so we don't have to check for that
- if(data->data.out[c[u]->ident] != ((val >> 8) & 0xFF)){
+ if(data->data.out[channel_offset] != ((val >> 8) & 0xFF)){
mark = 1;
- data->data.out[c[u]->ident] = (val >> 8) & 0xFF;
+ data->data.out[channel_offset] = (val >> 8) & 0xFF;
}
- if(data->data.out[MAPPED_CHANNEL(data->data.map[c[u]->ident])] != (val & 0xFF)){
+ if(data->data.out[MAPPED_CHANNEL(data->data.map[channel_offset])] != (val & 0xFF)){
mark = 1;
- data->data.out[MAPPED_CHANNEL(data->data.map[c[u]->ident])] = val & 0xFF;
+ data->data.out[MAPPED_CHANNEL(data->data.map[channel_offset])] = val & 0xFF;
}
}
- else if(data->data.out[c[u]->ident] != (v[u].normalised * 255.0)){
+ else if(data->data.out[channel_offset] != (v[u].normalised * 255.0)){
mark = 1;
- data->data.out[c[u]->ident] = v[u].normalised * 255.0;
+ data->data.out[channel_offset] = v[u].normalised * 255.0;
}
}
if(mark){
+ //find last frame time
+ for(u = 0; u < artnet_fd[data->fd_index].output_instances; u++){
+ if(artnet_fd[data->fd_index].output_instance[u].label == inst->ident){
+ break;
+ }
+ }
+
+ frame_delta = mm_timestamp() - artnet_fd[data->fd_index].output_instance[u].last_frame;
+ //check output rate limit, request next frame
+ if(frame_delta < ARTNET_FRAME_TIMEOUT){
+ artnet_fd[data->fd_index].output_instance[u].mark = 1;
+ if(!next_frame || next_frame > (ARTNET_FRAME_TIMEOUT - frame_delta)){
+ next_frame = (ARTNET_FRAME_TIMEOUT - frame_delta);
+ }
+ return 0;
+ }
return artnet_transmit(inst);
}
@@ -285,16 +316,9 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){
for(p = 0; p <= max_mark; p++){
if(data->data.map[p] & MAP_MARK){
data->data.map[p] &= ~MAP_MARK;
+ chan = data->data.channel + p;
if(data->data.map[p] & MAP_FINE){
- chan = mm_channel(inst, MAPPED_CHANNEL(data->data.map[p]), 0);
- }
- else{
- chan = mm_channel(inst, p, 0);
- }
-
- if(!chan){
- LOGPF("Active channel %" PRIsize_t " on %s not known to core", p, inst->name);
- return 1;
+ chan = data->data.channel + MAPPED_CHANNEL(data->data.map[p]);
}
if(IS_WIDE(data->data.map[p])){
@@ -323,6 +347,7 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){
static int artnet_handle(size_t num, managed_fd* fds){
size_t u, c;
uint64_t timestamp = mm_timestamp();
+ uint32_t synthesize_delta = 0;
ssize_t bytes_read;
char recv_buf[ARTNET_RECV_BUF];
artnet_instance_id inst_id = {
@@ -331,15 +356,25 @@ static int artnet_handle(size_t num, managed_fd* fds){
instance* inst = NULL;
artnet_pkt* frame = (artnet_pkt*) recv_buf;
- //transmit keepalive frames
+ //transmit keepalive & synthesized frames
+ next_frame = 0;
for(u = 0; u < artnet_fds; u++){
for(c = 0; c < artnet_fd[u].output_instances; c++){
- if(timestamp - artnet_fd[u].last_frame[c] >= ARTNET_KEEPALIVE_INTERVAL){
+ synthesize_delta = timestamp - artnet_fd[u].output_instance[c].last_frame;
+ if((artnet_fd[u].output_instance[c].mark
+ && synthesize_delta >= ARTNET_FRAME_TIMEOUT + ARTNET_SYNTHESIZE_MARGIN) //synthesize next frame
+ || 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);
}
}
+
+ //update next_frame
+ if(artnet_fd[u].output_instance[c].mark
+ && (!next_frame || next_frame > ARTNET_FRAME_TIMEOUT + ARTNET_SYNTHESIZE_MARGIN - synthesize_delta)){
+ next_frame = ARTNET_FRAME_TIMEOUT + ARTNET_SYNTHESIZE_MARGIN - synthesize_delta;
+ }
}
}
@@ -413,15 +448,15 @@ static int artnet_start(size_t n, instance** inst){
//if enabled for output, add to keepalive tracking
if(data->dest_len){
- artnet_fd[data->fd_index].output_instance = realloc(artnet_fd[data->fd_index].output_instance, (artnet_fd[data->fd_index].output_instances + 1) * sizeof(artnet_instance_id));
- artnet_fd[data->fd_index].last_frame = realloc(artnet_fd[data->fd_index].last_frame, (artnet_fd[data->fd_index].output_instances + 1) * sizeof(uint64_t));
+ artnet_fd[data->fd_index].output_instance = realloc(artnet_fd[data->fd_index].output_instance, (artnet_fd[data->fd_index].output_instances + 1) * sizeof(artnet_output_universe));
- if(!artnet_fd[data->fd_index].output_instance || !artnet_fd[data->fd_index].last_frame){
+ if(!artnet_fd[data->fd_index].output_instance){
LOG("Failed to allocate memory");
goto bail;
}
- artnet_fd[data->fd_index].output_instance[artnet_fd[data->fd_index].output_instances] = id;
- artnet_fd[data->fd_index].last_frame[artnet_fd[data->fd_index].output_instances] = 0;
+ artnet_fd[data->fd_index].output_instance[artnet_fd[data->fd_index].output_instances].label = id.label;
+ artnet_fd[data->fd_index].output_instance[artnet_fd[data->fd_index].output_instances].last_frame = 0;
+ artnet_fd[data->fd_index].output_instance[artnet_fd[data->fd_index].output_instances].mark = 0;
artnet_fd[data->fd_index].output_instances++;
}
@@ -449,7 +484,6 @@ static int artnet_shutdown(size_t n, instance** inst){
for(p = 0; p < artnet_fds; p++){
close(artnet_fd[p].fd);
free(artnet_fd[p].output_instance);
- free(artnet_fd[p].last_frame);
}
free(artnet_fd);
diff --git a/backends/artnet.h b/backends/artnet.h
index 59bd53f..a517aa0 100644
--- a/backends/artnet.h
+++ b/backends/artnet.h
@@ -4,9 +4,10 @@
#include "midimonster.h"
MM_PLUGIN_API int init();
+static uint32_t artnet_interval();
static int artnet_configure(char* option, char* value);
static int artnet_configure_instance(instance* instance, char* option, char* value);
-static instance* artnet_instance();
+static int artnet_instance(instance* inst);
static channel* artnet_channel(instance* instance, char* spec, uint8_t flags);
static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v);
static int artnet_handle(size_t num, managed_fd* fds);
@@ -16,7 +17,11 @@ static int artnet_shutdown(size_t n, instance** inst);
#define ARTNET_PORT "6454"
#define ARTNET_VERSION 14
#define ARTNET_RECV_BUF 4096
-#define ARTNET_KEEPALIVE_INTERVAL 2000
+
+#define ARTNET_KEEPALIVE_INTERVAL 1000
+//limit transmit rate to at most 44 packets per second (1000/44 ~= 22)
+#define ARTNET_FRAME_TIMEOUT 20
+#define ARTNET_SYNTHESIZE_MARGIN 10
#define MAP_COARSE 0x0200
#define MAP_FINE 0x0400
@@ -32,6 +37,7 @@ typedef struct /*_artnet_universe_model*/ {
uint8_t in[512];
uint8_t out[512];
uint16_t map[512];
+ channel channel[512];
} artnet_universe;
typedef struct /*_artnet_instance_model*/ {
@@ -52,11 +58,16 @@ typedef union /*_artnet_instance_id*/ {
uint64_t label;
} artnet_instance_id;
+typedef struct /*_artnet_fd_universe*/ {
+ uint64_t label;
+ uint64_t last_frame;
+ uint8_t mark;
+} artnet_output_universe;
+
typedef struct /*_artnet_fd*/ {
int fd;
size_t output_instances;
- artnet_instance_id* output_instance;
- uint64_t* last_frame;
+ artnet_output_universe* output_instance;
} artnet_descriptor;
#pragma pack(push, 1)
diff --git a/backends/artnet.md b/backends/artnet.md
index 90a7697..383203d 100644
--- a/backends/artnet.md
+++ b/backends/artnet.md
@@ -3,6 +3,8 @@
The ArtNet backend provides read-write access to the UDP-based ArtNet protocol for lighting
fixture control.
+Art-Netâ„¢ Designed by and Copyright Artistic Licence Holdings Ltd.
+
#### Global configuration
| Option | Example value | Default value | Description |
@@ -36,6 +38,3 @@ net1.1+2 > net2.5+123
A normal channel that is part of a wide channel can not be mapped individually.
#### Known bugs / problems
-
-The minimum inter-frame-time is disregarded, as the packet rate is determined by the rate of incoming
-channel events. \ No newline at end of file
diff --git a/backends/evdev.c b/backends/evdev.c
index 4725ef7..af5ec74 100644
--- a/backends/evdev.c
+++ b/backends/evdev.c
@@ -63,16 +63,11 @@ static int evdev_configure(char* option, char* value) {
return 1;
}
-static instance* evdev_instance(){
- instance* inst = mm_instance();
- if(!inst){
- return NULL;
- }
-
+static int evdev_instance(instance* inst){
evdev_instance_data* data = calloc(1, sizeof(evdev_instance_data));
if(!data){
LOG("Failed to allocate memory");
- return NULL;
+ return 1;
}
data->input_fd = -1;
@@ -81,12 +76,12 @@ static instance* evdev_instance(){
if(!data->output_proto){
LOG("Failed to initialize libevdev output prototype device");
free(data);
- return NULL;
+ return 1;
}
#endif
inst->impl = data;
- return inst;
+ return 0;
}
static int evdev_attach(instance* inst, evdev_instance_data* data, char* node){
diff --git a/backends/evdev.h b/backends/evdev.h
index 0c877fc..e896d2d 100644
--- a/backends/evdev.h
+++ b/backends/evdev.h
@@ -11,7 +11,7 @@
MM_PLUGIN_API int init();
static int evdev_configure(char* option, char* value);
static int evdev_configure_instance(instance* instance, char* option, char* value);
-static instance* evdev_instance();
+static int evdev_instance(instance* inst);
static channel* evdev_channel(instance* instance, char* spec, uint8_t flags);
static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v);
static int evdev_handle(size_t num, managed_fd* fds);
diff --git a/backends/jack.c b/backends/jack.c
index d7f68c4..c862096 100644
--- a/backends/jack.c
+++ b/backends/jack.c
@@ -334,19 +334,13 @@ static int mmjack_configure_instance(instance* inst, char* option, char* value){
return 0;
}
-static instance* mmjack_instance(){
- instance* inst = mm_instance();
- if(!inst){
- return NULL;
- }
-
+static int mmjack_instance(instance* inst){
inst->impl = calloc(1, sizeof(mmjack_instance_data));
if(!inst->impl){
LOG("Failed to allocate memory");
- return NULL;
+ return 1;
}
-
- return inst;
+ return 0;
}
static int mmjack_parse_midispec(mmjack_channel_ident* ident, char* spec){
diff --git a/backends/jack.h b/backends/jack.h
index 66c66db..03ce052 100644
--- a/backends/jack.h
+++ b/backends/jack.h
@@ -5,7 +5,7 @@
MM_PLUGIN_API int init();
static int mmjack_configure(char* option, char* value);
static int mmjack_configure_instance(instance* inst, char* option, char* value);
-static instance* mmjack_instance();
+static int mmjack_instance(instance* inst);
static channel* mmjack_channel(instance* inst, char* spec, uint8_t flags);
static int mmjack_set(instance* inst, size_t num, channel** c, channel_value* v);
static int mmjack_handle(size_t num, managed_fd* fds);
diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c
index ffa403b..b9513ac 100644
--- a/backends/libmmbackend.c
+++ b/backends/libmmbackend.c
@@ -153,7 +153,11 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin
int mmbackend_send(int fd, uint8_t* data, size_t length){
ssize_t total = 0, sent;
while(total < length){
+ #ifndef LIBMMBACKEND_TCP_TORTURE
sent = send(fd, data + total, length - total, 0);
+ #else
+ sent = send(fd, data + total, 1, 0);
+ #endif
if(sent < 0){
LOGPF("Failed to send: %s", strerror(errno));
return 1;
diff --git a/backends/loopback.c b/backends/loopback.c
index 085d1df..4274832 100644
--- a/backends/loopback.c
+++ b/backends/loopback.c
@@ -34,19 +34,14 @@ static int loopback_configure_instance(instance* inst, char* option, char* value
return 0;
}
-static instance* loopback_instance(){
- instance* i = mm_instance();
- if(!i){
- return NULL;
- }
-
- i->impl = calloc(1, sizeof(loopback_instance_data));
- if(!i->impl){
+static int loopback_instance(instance* inst){
+ inst->impl = calloc(1, sizeof(loopback_instance_data));
+ if(!inst->impl){
LOG("Failed to allocate memory");
- return NULL;
+ return 1;
}
- return i;
+ return 0;
}
static channel* loopback_channel(instance* inst, char* spec, uint8_t flags){
@@ -107,6 +102,7 @@ static int loopback_shutdown(size_t n, instance** inst){
}
free(data->name);
free(inst[u]->impl);
+ inst[u]->impl = NULL;
}
LOG("Backend shut down");
diff --git a/backends/loopback.h b/backends/loopback.h
index c508d72..cfb2e19 100644
--- a/backends/loopback.h
+++ b/backends/loopback.h
@@ -3,7 +3,7 @@
MM_PLUGIN_API int init();
static int loopback_configure(char* option, char* value);
static int loopback_configure_instance(instance* inst, char* option, char* value);
-static instance* loopback_instance();
+static int loopback_instance(instance* inst);
static channel* loopback_channel(instance* inst, char* spec, uint8_t flags);
static int loopback_set(instance* inst, size_t num, channel** c, channel_value* v);
static int loopback_handle(size_t num, managed_fd* fds);
diff --git a/backends/lua.c b/backends/lua.c
index ee9e03f..7f80cc7 100644
--- a/backends/lua.c
+++ b/backends/lua.c
@@ -9,15 +9,19 @@
#endif
#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;
uint64_t timer_interval = 0;
#ifdef MMBACKEND_LUA_TIMERFD
static int timer_fd = -1;
-#else
-static uint64_t last_timestamp;
#endif
+static uint64_t last_timestamp = 0;
+
+static size_t threads = 0;
+static lua_thread* thread = NULL;
MM_PLUGIN_API int init(){
backend lua = {
@@ -63,6 +67,7 @@ static uint32_t lua_interval(){
next_timer = timer[n].interval - timer[n].delta;
}
}
+ DBGPF("Next timer fires in %" PRIu32, next_timer);
return next_timer;
}
return 1000;
@@ -74,7 +79,7 @@ static int lua_update_timerfd(){
size_t n = 0;
#ifdef MMBACKEND_LUA_TIMERFD
struct itimerspec timer_config = {
- 0
+ {0}
};
#endif
@@ -85,6 +90,13 @@ static int lua_update_timerfd(){
}
}
+ 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
if(interval){
for(n = 0; n < timers; n++){
@@ -97,7 +109,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;
}
}
@@ -110,22 +123,106 @@ static int lua_update_timerfd(){
}
if(interval == timer_interval){
+ DBGPF("Keeping interval at %" PRIu64, interval);
return 0;
}
#ifdef MMBACKEND_LUA_TIMERFD
- //configure the new interval
+ //configure the new interval, 0.0 disarms the timer
+ DBGPF("Reconfiguring timerfd to %" PRIu64 ".%" PRIu64, timer_config.it_interval.tv_sec, timer_config.it_interval.tv_nsec);
timerfd_settime(timer_fd, 0, &timer_config, NULL);
#endif
timer_interval = interval;
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;
const char* channel_name = NULL;
- channel* channel = NULL;
instance* inst = NULL;
lua_instance_data* data = NULL;
@@ -144,15 +241,21 @@ static int lua_callback_output(lua_State* interpreter){
channel_name = lua_tostring(interpreter, 1);
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){
+ lua_channel(inst, (char*) channel_name, mmchannel_output);
+ }
+
//find correct channel & output value
for(n = 0; n < data->channels; n++){
- if(!strcmp(channel_name, data->channel_name[n])){
- channel = mm_channel(inst, n, 0);
- if(!channel){
- return 0;
+ if(!strcmp(channel_name, data->channel[n].name)){
+ data->channel[n].out = val.normalised;
+ if(!last_timestamp){
+ data->channel[n].mark = 1;
+ }
+ else{
+ mm_channel_event(mm_channel(inst, n, 0), val);
}
- mm_channel_event(channel, val);
- data->output[n] = val.normalised;
return 0;
}
}
@@ -189,6 +292,7 @@ static int lua_callback_interval(lua_State* interpreter){
if(lua_gettable(interpreter, LUA_REGISTRYINDEX) == LUA_TNUMBER){
//already interval'd
reference = luaL_checkinteger(interpreter, 4);
+ DBGPF("Updating interval to %" PRIu64 " msec", interval);
}
else if(interval){
//get a reference to the function
@@ -199,6 +303,8 @@ static int lua_callback_interval(lua_State* interpreter){
lua_pushvalue(interpreter, 1);
lua_pushinteger(interpreter, reference);
lua_settable(interpreter, LUA_REGISTRYINDEX);
+
+ DBGPF("Registered interval with %" PRIu64 " msec", interval);
}
//find matching timer
@@ -255,8 +361,8 @@ 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]);
+ if(!strcmp(channel_name, data->channel[n].name)){
+ lua_pushnumber(interpreter, (input) ? data->channel[n].in : data->channel[n].out);
return 1;
}
}
@@ -273,6 +379,17 @@ static int lua_callback_output_value(lua_State* interpreter){
return lua_callback_value(interpreter, 0);
}
+static int lua_callback_input_channel(lua_State* interpreter){
+ lua_pushstring(interpreter, LUA_REGISTRY_CURRENT_CHANNEL);
+ lua_gettable(interpreter, LUA_REGISTRYINDEX);
+ 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;
@@ -289,21 +406,21 @@ 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;
}
-static instance* lua_instance(){
- instance* inst = mm_instance();
- if(!inst){
- return NULL;
- }
-
+static int lua_instance(instance* inst){
lua_instance_data* data = calloc(1, sizeof(lua_instance_data));
if(!data){
LOG("Failed to allocate memory");
- return NULL;
+ return 1;
}
//load the interpreter
@@ -311,7 +428,7 @@ static instance* lua_instance(){
if(!data->interpreter){
LOG("Failed to initialize interpreter");
free(data);
- return NULL;
+ return 1;
}
luaL_openlibs(data->interpreter);
@@ -320,6 +437,10 @@ static instance* lua_instance(){
lua_register(data->interpreter, "interval", lua_callback_interval);
lua_register(data->interpreter, "input_value", lua_callback_input_value);
lua_register(data->interpreter, "output_value", lua_callback_output_value);
+ 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);
@@ -327,7 +448,7 @@ static instance* lua_instance(){
lua_settable(data->interpreter, LUA_REGISTRYINDEX);
inst->impl = data;
- return inst;
+ return 0;
}
static channel* lua_channel(instance* inst, char* spec, uint8_t flags){
@@ -336,26 +457,23 @@ static channel* lua_channel(instance* inst, char* spec, uint8_t flags){
//find matching channel
for(u = 0; u < data->channels; u++){
- if(!strcmp(spec, data->channel_name[u])){
+ if(!strcmp(spec, data->channel[u].name)){
break;
}
}
//allocate new channel
if(u == data->channels){
- data->channel_name = realloc(data->channel_name, (u + 1) * sizeof(char*));
- data->reference = realloc(data->reference, (u + 1) * sizeof(int));
- data->input = realloc(data->input, (u + 1) * sizeof(double));
- data->output = realloc(data->output, (u + 1) * sizeof(double));
- if(!data->channel_name || !data->reference || !data->input || !data->output){
+ data->channel = realloc(data->channel, (data->channels + 1) * sizeof(lua_channel_data));
+ if(!data->channel){
LOG("Failed to allocate memory");
+ data->channels = 0;
return NULL;
}
- data->reference[u] = LUA_NOREF;
- data->input[u] = data->output[u] = 0.0;
- data->channel_name[u] = strdup(spec);
- if(!data->channel_name[u]){
+ data->channel[u].in = data->channel[u].out = 0.0;
+ data->channel[u].name = strdup(spec);
+ if(!data->channel[u].name){
LOG("Failed to allocate memory");
return NULL;
}
@@ -366,22 +484,33 @@ static channel* lua_channel(instance* inst, char* spec, uint8_t flags){
}
static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){
- size_t n = 0;
+ size_t n = 0, ident;
lua_instance_data* data = (lua_instance_data*) inst->impl;
//handle all incoming events
for(n = 0; n < num; n++){
- data->input[c[n]->ident] = v[n].normalised;
+ ident = c[n]->ident;
+ data->channel[ident].in = v[n].normalised;
//call lua channel handlers if present
- if(data->reference[c[n]->ident] != LUA_NOREF){
- lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->reference[c[n]->ident]);
+ if(data->channel[ident].reference != LUA_NOREF){
+ //push the channel name
+ lua_pushstring(data->interpreter, LUA_REGISTRY_CURRENT_CHANNEL);
+ lua_pushstring(data->interpreter, data->channel[ident].name);
+ lua_settable(data->interpreter, LUA_REGISTRYINDEX);
+
+ lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->channel[ident].reference);
lua_pushnumber(data->interpreter, v[n].normalised);
if(lua_pcall(data->interpreter, 1, 0, 0) != LUA_OK){
- LOGPF("Failed to call handler for %s.%s: %s", inst->name, data->channel_name[c[n]->ident], lua_tostring(data->interpreter, -1));
+ LOGPF("Failed to call handler for %s.%s: %s", inst->name, data->channel[ident].name, lua_tostring(data->interpreter, -1));
lua_pop(data->interpreter, 1);
}
}
}
+
+ //clear the channel name
+ lua_pushstring(data->interpreter, LUA_REGISTRY_CURRENT_CHANNEL);
+ lua_pushnil(data->interpreter);
+ lua_settable(data->interpreter, LUA_REGISTRYINDEX);
return 0;
}
@@ -401,9 +530,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
@@ -422,30 +548,75 @@ static int lua_handle(size_t num, managed_fd* fds){
timer[n].delta %= timer[n].interval;
lua_rawgeti(timer[n].interpreter, LUA_REGISTRYINDEX, timer[n].reference);
lua_pcall(timer[n].interpreter, 0, 0, 0);
+ DBGPF("Calling interval timer function %" PRIsize_t, n);
}
}
}
+
+ //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;
}
+static int lua_resolve_symbol(lua_State* interpreter, char* symbol){
+ int reference = LUA_REFNIL;
+
+ //exclude reserved names
+ if(!strcmp(symbol, "output")
+ || !strcmp(symbol, "thread")
+ || !strcmp(symbol, "sleep")
+ || !strcmp(symbol, "input_value")
+ || !strcmp(symbol, "output_value")
+ || !strcmp(symbol, "input_channel")
+ || !strcmp(symbol, "timestamp")
+ || !strcmp(symbol, "interval")){
+ return LUA_NOREF;
+ }
+
+ lua_getglobal(interpreter, symbol);
+ reference = luaL_ref(interpreter, LUA_REGISTRYINDEX);
+ if(reference == LUA_REFNIL){
+ return LUA_NOREF;
+ }
+ return reference;
+}
+
static int lua_start(size_t n, instance** inst){
size_t u, p;
lua_instance_data* data = NULL;
+ int default_handler;
+ channel_value v;
//resolve channels to their handler functions
for(u = 0; u < n; u++){
data = (lua_instance_data*) inst[u]->impl;
+ default_handler = LUA_NOREF;
+
+ //try to resolve default handler if given
+ if(data->default_handler){
+ default_handler = lua_resolve_symbol(data->interpreter, data->default_handler);
+ if(default_handler == LUA_NOREF){
+ LOGPF("Failed to resolve default handler %s on %s", data->default_handler, inst[u]->name);
+ }
+ }
+
for(p = 0; p < data->channels; p++){
- //exclude reserved names
- if(strcmp(data->channel_name[p], "output")
- && strcmp(data->channel_name[p], "input_value")
- && strcmp(data->channel_name[p], "output_value")
- && strcmp(data->channel_name[p], "interval")){
- lua_getglobal(data->interpreter, data->channel_name[p]);
- data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX);
- if(data->reference[p] == LUA_REFNIL){
- data->reference[p] = LUA_NOREF;
- }
+ data->channel[p].reference = default_handler;
+ if(!data->default_handler){
+ data->channel[p].reference = lua_resolve_symbol(data->interpreter, data->channel[p].name);
+ }
+ //push initial values
+ if(data->channel[p].mark){
+ v.normalised = data->channel[p].out;
+ mm_channel_event(mm_channel(inst[u], p, 0), v);
}
}
}
@@ -457,6 +628,7 @@ static int lua_start(size_t n, instance** inst){
return 1;
}
#endif
+ last_timestamp = mm_timestamp();
return 0;
}
@@ -470,12 +642,10 @@ static int lua_shutdown(size_t n, instance** inst){
lua_close(data->interpreter);
//cleanup channel data
for(p = 0; p < data->channels; p++){
- free(data->channel_name[p]);
+ free(data->channel[p].name);
}
- free(data->channel_name);
- free(data->reference);
- free(data->input);
- free(data->output);
+ free(data->channel);
+ free(data->default_handler);
free(inst[u]->impl);
}
@@ -483,6 +653,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 75f03c4..4583dfe 100644
--- a/backends/lua.h
+++ b/backends/lua.h
@@ -12,7 +12,7 @@
MM_PLUGIN_API int init();
static int lua_configure(char* option, char* value);
static int lua_configure_instance(instance* inst, char* option, char* value);
-static instance* lua_instance();
+static int lua_instance(instance* inst);
static channel* lua_channel(instance* inst, char* spec, uint8_t flags);
static int lua_set(instance* inst, size_t num, channel** c, channel_value* v);
static int lua_handle(size_t num, managed_fd* fds);
@@ -22,13 +22,20 @@ static int lua_shutdown(size_t n, instance** inst);
static uint32_t lua_interval();
#endif
+typedef struct /*_lua_channel*/ {
+ char* name;
+ int reference;
+ double in;
+ double out;
+ uint8_t mark;
+} lua_channel_data;
+
typedef struct /*_lua_instance_data*/ {
size_t channels;
- char** channel_name;
- int* reference;
- double* input;
- double* output;
+ lua_channel_data* channel;
+
lua_State* interpreter;
+ char* default_handler;
} lua_instance_data;
typedef struct /*_lua_interval_callback*/ {
@@ -37,3 +44,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 f38e189..05509b6 100644
--- a/backends/lua.md
+++ b/backends/lua.md
@@ -1,51 +1,71 @@
### 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 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.
+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
| 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) |
+| `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.
#### 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 `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
@@ -58,9 +78,21 @@ 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 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.
+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),
+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
+and run `make lua.dll` inside the `backends/` directory.
+
+At runtime, Windows searches for the file in the same directory as `midimonster.exe`.
diff --git a/backends/maweb.c b/backends/maweb.c
index 39a6cb2..6861d75 100644
--- a/backends/maweb.c
+++ b/backends/maweb.c
@@ -15,10 +15,15 @@
#define WS_FLAG_FIN 0x80
#define WS_FLAG_MASK 0x80
+/*
+ * TODO handle peer close/unregister/reopen and fallback connections
+ */
+
static uint64_t last_keepalive = 0;
static uint64_t update_interval = 50;
static uint64_t last_update = 0;
static uint64_t updates_inflight = 0;
+static uint64_t quiet_mode = 0;
static maweb_command_key cmdline_keys[] = {
{"PREV", 109, 0, 1}, {"SET", 108, 1, 0, 1}, {"NEXT", 110, 0, 1},
@@ -139,6 +144,10 @@ static int maweb_configure(char* option, char* value){
update_interval = strtoul(value, NULL, 10);
return 0;
}
+ else if(!strcmp(option, "quiet")){
+ quiet_mode = strtoul(value, NULL, 10);
+ return 0;
+ }
LOGPF("Unknown backend configuration option %s", option);
return 1;
@@ -205,16 +214,11 @@ static int maweb_configure_instance(instance* inst, char* option, char* value){
return 1;
}
-static instance* maweb_instance(){
- instance* inst = mm_instance();
- if(!inst){
- return NULL;
- }
-
+static int maweb_instance(instance* inst){
maweb_instance_data* data = calloc(1, sizeof(maweb_instance_data));
if(!data){
LOG("Failed to allocate memory");
- return NULL;
+ return 1;
}
data->fd = -1;
@@ -222,12 +226,12 @@ static instance* maweb_instance(){
if(!data->buffer){
LOG("Failed to allocate memory");
free(data);
- return NULL;
+ return 1;
}
data->allocated = MAWEB_RECV_CHUNK;
inst->impl = data;
- return inst;
+ return 0;
}
static channel* maweb_channel(instance* inst, char* spec, uint8_t flags){
@@ -462,7 +466,9 @@ static int maweb_request_playbacks(instance* inst){
size_t page_index = 0, view = 3, channel = 0, offsets[3], channel_offset, channels;
if(updates_inflight){
- LOGPF("Skipping update request, %" PRIu64 " updates still inflight", updates_inflight);
+ if(quiet_mode < 1){
+ LOGPF("Skipping update request, %" PRIu64 " updates still inflight - consider raising the interval time", updates_inflight);
+ }
return 0;
}
@@ -593,7 +599,9 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le
data->login = 0;
return 0;
}
- LOGPF("Session id is now %" PRId64, data->session);
+ if(quiet_mode < 2){
+ LOGPF("Session id is now %" PRId64, data->session);
+ }
}
if(json_obj_bool(payload, "forceLogin", 0)){
@@ -752,7 +760,7 @@ static ssize_t maweb_handle_ws(instance* inst, ssize_t bytes_read){
static int maweb_handle_fd(instance* inst){
maweb_instance_data* data = (maweb_instance_data*) inst->impl;
- ssize_t bytes_read, bytes_left = data->allocated - data->offset, bytes_handled;
+ ssize_t bytes_read, bytes_left = data->allocated - data->offset, bytes_handled = 0;
if(bytes_left < 3){
data->buffer = realloc(data->buffer, (data->allocated + MAWEB_RECV_CHUNK) * sizeof(uint8_t));
diff --git a/backends/maweb.h b/backends/maweb.h
index 50b777a..80835d9 100644
--- a/backends/maweb.h
+++ b/backends/maweb.h
@@ -3,7 +3,7 @@
MM_PLUGIN_API int init();
static int maweb_configure(char* option, char* value);
static int maweb_configure_instance(instance* inst, char* option, char* value);
-static instance* maweb_instance();
+static int maweb_instance(instance* inst);
static channel* maweb_channel(instance* inst, char* spec, uint8_t flags);
static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v);
static int maweb_handle(size_t num, managed_fd* fds);
diff --git a/backends/maweb.md b/backends/maweb.md
index 45dc778..eddf1a5 100644
--- a/backends/maweb.md
+++ b/backends/maweb.md
@@ -1,7 +1,7 @@
### The `maweb` backend
This backend connects directly with the integrated *MA Web Remote* of MA Lighting consoles and OnPC
-instances (GrandMA2 / GrandMA2 OnPC / GrandMA Dot2 / GrandMA Dot2 OnPC).
+instances (GrandMA2 / GrandMA2 OnPC / Dot2 / Dot2 OnPC).
It grants read-write access to the console's playback controls as well as write access to most command
line and control keys.
@@ -19,6 +19,7 @@ Web Remote. Set a web remote password using the option below the activation sett
| Option | Example value | Default value | Description |
|---------------|-----------------------|-----------------------|---------------------------------------------------------------|
| `interval` | `100` | `50` | Query interval for input data polling (in msec) |
+| `quiet` | `1` | `0` | Turn off some warning messages, for use by experts |
#### Instance configuration
@@ -32,7 +33,7 @@ Web Remote. Set a web remote password using the option below the activation sett
The per-instance command line mode may be one of `remote`, `console` or `downgrade`. The first option handles
command keys with a "virtual" commandline belonging to the Web Remote connection. Any commands entered are
not visible on the main console. The `console` mode is only available with GrandMA2 remotes and injects key events
-into the main console. This mode also supports additional hardkeys that are only available on GrandMA consoles.
+into the main console. This mode also supports additional hardkeys that are only available on GrandMA2 consoles.
When connected to a dot2 console while this mode is active, the use of commandline keys will not be possible.
With the `downgrade` mode, keys are handled on the console if possible, falling back to remote handling if not.
diff --git a/backends/midi.c b/backends/midi.c
index 11d759d..1f0f2d5 100644
--- a/backends/midi.c
+++ b/backends/midi.c
@@ -13,9 +13,7 @@ enum /*_midi_channel_type*/ {
cc,
pressure,
aftertouch,
- pitchbend,
- nrpn,
- sysmsg
+ pitchbend
};
static struct {
@@ -69,19 +67,14 @@ static int midi_configure(char* option, char* value){
return 1;
}
-static instance* midi_instance(){
- instance* inst = mm_instance();
- if(!inst){
- return NULL;
- }
-
+static int midi_instance(instance* inst){
inst->impl = calloc(1, sizeof(midi_instance_data));
if(!inst->impl){
LOG("Failed to allocate memory");
- return NULL;
+ return 1;
}
- return inst;
+ return 0;
}
static int midi_configure_instance(instance* inst, char* option, char* value){
@@ -116,39 +109,22 @@ static channel* midi_channel(instance* inst, char* spec, uint8_t flags){
.label = 0
};
- //support deprecated syntax for a transition period...
- uint8_t old_syntax = 0;
- char* channel;
-
+ char* channel = NULL;
if(!strncmp(spec, "ch", 2)){
channel = spec + 2;
if(!strncmp(spec, "channel", 7)){
channel = spec + 7;
}
}
- else if(!strncmp(spec, "cc", 2)){
- ident.fields.type = cc;
- channel = spec + 2;
- old_syntax = 1;
- }
- else if(!strncmp(spec, "note", 4)){
- ident.fields.type = note;
- channel = spec + 4;
- old_syntax = 1;
- }
- else if(!strncmp(spec, "nrpn", 4)){
- ident.fields.type = nrpn;
- channel = spec + 4;
- old_syntax = 1;
- }
- else{
- LOGPF("Unknown control type in %s", spec);
+
+ if(!channel){
+ LOGPF("Invalid channel specification %s", spec);
return NULL;
}
ident.fields.channel = strtoul(channel, &channel, 10);
if(ident.fields.channel > 15){
- LOGPF("Channel out of range in spec %s", spec);
+ LOGPF("MIDI channel out of range in spec %s", spec);
return NULL;
}
@@ -159,33 +135,27 @@ static channel* midi_channel(instance* inst, char* spec, uint8_t flags){
//skip the period
channel++;
- if(!old_syntax){
- if(!strncmp(channel, "cc", 2)){
- ident.fields.type = cc;
- channel += 2;
- }
- else if(!strncmp(channel, "note", 4)){
- ident.fields.type = note;
- channel += 4;
- }
- else if(!strncmp(channel, "nrpn", 4)){
- ident.fields.type = nrpn;
- channel += 4;
- }
- else if(!strncmp(channel, "pressure", 8)){
- ident.fields.type = pressure;
- channel += 8;
- }
- else if(!strncmp(channel, "pitch", 5)){
- ident.fields.type = pitchbend;
- }
- else if(!strncmp(channel, "aftertouch", 10)){
- ident.fields.type = aftertouch;
- }
- else{
- LOGPF("Unknown control type in %s", spec);
- return NULL;
- }
+ if(!strncmp(channel, "cc", 2)){
+ ident.fields.type = cc;
+ channel += 2;
+ }
+ else if(!strncmp(channel, "note", 4)){
+ ident.fields.type = note;
+ channel += 4;
+ }
+ else if(!strncmp(channel, "pressure", 8)){
+ ident.fields.type = pressure;
+ channel += 8;
+ }
+ else if(!strncmp(channel, "pitch", 5)){
+ ident.fields.type = pitchbend;
+ }
+ else if(!strncmp(channel, "aftertouch", 10)){
+ ident.fields.type = aftertouch;
+ }
+ else{
+ LOGPF("Unknown control type in %s", spec);
+ return NULL;
}
ident.fields.control = strtoul(channel, NULL, 10);
@@ -229,9 +199,6 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){
case aftertouch:
snd_seq_ev_set_chanpress(&ev, ident.fields.channel, v[u].normalised * 127.0);
break;
- case nrpn:
- //FIXME set to nrpn output
- break;
}
snd_seq_event_output(sequencer, &ev);
@@ -241,6 +208,24 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){
return 0;
}
+static char* midi_type_name(uint8_t type){
+ switch(type){
+ case none:
+ return "none";
+ case note:
+ return "note";
+ case cc:
+ return "cc";
+ case pressure:
+ return "pressure";
+ case aftertouch:
+ return "aftertouch";
+ case pitchbend:
+ return "pitch";
+ }
+ return "unknown";
+}
+
static int midi_handle(size_t num, managed_fd* fds){
snd_seq_event_t* ev = NULL;
instance* inst = NULL;
@@ -258,59 +243,45 @@ static int midi_handle(size_t num, managed_fd* fds){
while(snd_seq_event_input(sequencer, &ev) > 0){
event_type = NULL;
ident.label = 0;
+
+ ident.fields.channel = ev->data.note.channel;
+ ident.fields.control = ev->data.note.note;
+ val.normalised = (double) ev->data.note.velocity / 127.0;
+
switch(ev->type){
case SND_SEQ_EVENT_NOTEON:
case SND_SEQ_EVENT_NOTEOFF:
case SND_SEQ_EVENT_NOTE:
ident.fields.type = note;
- ident.fields.channel = ev->data.note.channel;
- ident.fields.control = ev->data.note.note;
- val.normalised = (double)ev->data.note.velocity / 127.0;
if(ev->type == SND_SEQ_EVENT_NOTEOFF){
val.normalised = 0;
}
- event_type = "note";
break;
case SND_SEQ_EVENT_KEYPRESS:
ident.fields.type = pressure;
- ident.fields.channel = ev->data.note.channel;
- ident.fields.control = ev->data.note.note;
- val.normalised = (double)ev->data.note.velocity / 127.0;
- event_type = "pressure";
break;
case SND_SEQ_EVENT_CHANPRESS:
ident.fields.type = aftertouch;
ident.fields.channel = ev->data.control.channel;
- val.normalised = (double)ev->data.control.value / 127.0;
- event_type = "aftertouch";
+ val.normalised = (double) ev->data.control.value / 127.0;
break;
case SND_SEQ_EVENT_PITCHBEND:
ident.fields.type = pitchbend;
ident.fields.channel = ev->data.control.channel;
- val.normalised = ((double)ev->data.control.value + 8192) / 16383.0;
- event_type = "pitch";
+ val.normalised = ((double) ev->data.control.value + 8192) / 16383.0;
break;
case SND_SEQ_EVENT_CONTROLLER:
ident.fields.type = cc;
ident.fields.channel = ev->data.control.channel;
ident.fields.control = ev->data.control.param;
- val.raw.u64 = ev->data.control.value;
- val.normalised = (double)ev->data.control.value / 127.0;
- event_type = "cc";
- break;
- case SND_SEQ_EVENT_CONTROL14:
- case SND_SEQ_EVENT_NONREGPARAM:
- case SND_SEQ_EVENT_REGPARAM:
- //FIXME value calculation
- ident.fields.type = nrpn;
- ident.fields.channel = ev->data.control.channel;
- ident.fields.control = ev->data.control.param;
+ val.normalised = (double) ev->data.control.value / 127.0;
break;
default:
LOG("Ignored event of unsupported type");
continue;
}
+ event_type = midi_type_name(ident.fields.type);
inst = mm_instance_find(BACKEND_NAME, ev->dest.port);
if(!inst){
//FIXME might want to return failure
diff --git a/backends/midi.h b/backends/midi.h
index 66a02bc..dcee010 100644
--- a/backends/midi.h
+++ b/backends/midi.h
@@ -3,7 +3,7 @@
MM_PLUGIN_API int init();
static int midi_configure(char* option, char* value);
static int midi_configure_instance(instance* instance, char* option, char* value);
-static instance* midi_instance();
+static int midi_instance(instance* inst);
static channel* midi_channel(instance* instance, char* spec, uint8_t flags);
static int midi_set(instance* inst, size_t num, channel** c, channel_value* v);
static int midi_handle(size_t num, managed_fd* fds);
diff --git a/backends/midi.md b/backends/midi.md
index 108860e..d3d6e33 100644
--- a/backends/midi.md
+++ b/backends/midi.md
@@ -30,12 +30,9 @@ The MIDI backend supports mapping different MIDI events to MIDIMonster channels.
* `pressure` - Note pressure/aftertouch messages
* `aftertouch` - Channel-wide aftertouch messages
* `pitch` - Channel pitchbend messages
-* `nrpn` - NRPNs (not yet implemented)
A MIDIMonster channel is specified using the syntax `channel<channel>.<type><index>`. The shorthand `ch` may be
used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number).
-The earlier syntax of `<type><channel>.<index>` is officially deprecated but still supported for compatibility
-reasons. This support may be removed at some future time.
The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel<channel>.<type>`.
@@ -59,7 +56,5 @@ Currently, no Note Off messages are sent (instead, Note On messages with a veloc
generated, which amount to the same thing according to the spec). This may be implemented as
a configuration option at a later time.
-NRPNs are not yet fully implemented, though rudimentary support is in the codebase.
-
To see which events your MIDI devices output, ALSA provides the `aseqdump` utility. You can
list all incoming events using `aseqdump -p <portname>`.
diff --git a/backends/ola.cpp b/backends/ola.cpp
index 09d68c9..106dbd5 100644
--- a/backends/ola.cpp
+++ b/backends/ola.cpp
@@ -40,21 +40,15 @@ static int ola_configure(char* option, char* value){
return 1;
}
-static instance* ola_instance(){
- ola_instance_data* data = NULL;
- instance* inst = mm_instance();
- if(!inst){
- return NULL;
- }
-
- data = (ola_instance_data*)calloc(1, sizeof(ola_instance_data));
+static int ola_instance(instance* inst){
+ ola_instance_data* data = (ola_instance_data*) calloc(1, sizeof(ola_instance_data));
if(!data){
LOG("Failed to allocate memory");
- return NULL;
+ return 1;
}
inst->impl = data;
- return inst;
+ return 0;
}
static int ola_configure_instance(instance* inst, char* option, char* value){
@@ -188,7 +182,7 @@ static void ola_data_receive(unsigned int universe, const ola::DmxBuffer& ola_dm
else{
chan = mm_channel(inst, p, 0);
}
-
+
if(!chan){
LOGPF("Active channel %" PRIsize_t " on %s not known to core", p, inst->name);
return;
diff --git a/backends/ola.h b/backends/ola.h
index 083e971..68244ec 100644
--- a/backends/ola.h
+++ b/backends/ola.h
@@ -7,7 +7,7 @@ extern "C" {
MM_PLUGIN_API int init();
static int ola_configure(char* option, char* value);
static int ola_configure_instance(instance* instance, char* option, char* value);
- static instance* ola_instance();
+ static int ola_instance(instance* inst);
static channel* ola_channel(instance* instance, char* spec, uint8_t flags);
static int ola_set(instance* inst, size_t num, channel** c, channel_value* v);
static int ola_handle(size_t num, managed_fd* fds);
diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c
new file mode 100644
index 0000000..2a5e01f
--- /dev/null
+++ b/backends/openpixelcontrol.c
@@ -0,0 +1,686 @@
+#define BACKEND_NAME "openpixelcontrol"
+
+#include <string.h>
+
+#include "libmmbackend.h"
+#include "openpixelcontrol.h"
+
+/*
+ * TODO handle destination close/unregister/reopen
+ */
+
+MM_PLUGIN_API int init(){
+ backend openpixel = {
+ .name = BACKEND_NAME,
+ .conf = openpixel_configure,
+ .create = openpixel_instance,
+ .conf_instance = openpixel_configure_instance,
+ .channel = openpixel_channel,
+ .handle = openpixel_set,
+ .process = openpixel_handle,
+ .start = openpixel_start,
+ .shutdown = openpixel_shutdown
+ };
+
+ //register backend
+ if(mm_backend_register(openpixel)){
+ LOG("Failed to register backend");
+ return 1;
+ }
+ return 0;
+}
+
+static int openpixel_configure(char* option, char* value){
+ //no global configuration
+ LOG("No backend configuration possible");
+ return 1;
+}
+
+static int openpixel_configure_instance(instance* inst, char* option, char* value){
+ char* host = NULL, *port = NULL;
+ openpixel_instance_data* data = (openpixel_instance_data*) inst->impl;
+
+ //FIXME this should store the destination/listen address and establish on _start
+ if(!strcmp(option, "destination")){
+ mmbackend_parse_hostspec(value, &host, &port, NULL);
+ if(!host || !port){
+ LOGPF("Invalid destination address specified for instance %s", inst->name);
+ return 1;
+ }
+
+ data->dest_fd = mmbackend_socket(host, port, SOCK_STREAM, 0, 0);
+ if(data->dest_fd >= 0){
+ return 0;
+ }
+ LOGPF("Failed to connect to server for instance %s", inst->name);
+ return 1;
+ }
+ if(!strcmp(option, "listen")){
+ mmbackend_parse_hostspec(value, &host, &port, NULL);
+ if(!host || !port){
+ LOGPF("Invalid listen address specified for instance %s", inst->name);
+ return 1;
+ }
+
+ data->listen_fd = mmbackend_socket(host, port, SOCK_STREAM, 1, 0);
+ if(data->listen_fd >= 0 && !listen(data->listen_fd, SOMAXCONN)){
+ return 0;
+ }
+ LOGPF("Failed to bind server descriptor for instance %s", inst->name);
+ return 1;
+ }
+ else if(!strcmp(option, "mode")){
+ if(!strcmp(value, "16bit")){
+ data->mode = rgb16;
+ return 0;
+ }
+ else if(!strcmp(value, "8bit")){
+ data->mode = rgb8;
+ return 0;
+ }
+ LOGPF("Unknown instance mode %s\n", value);
+ return 1;
+ }
+
+ LOGPF("Unknown instance option %s for instance %s", option, inst->name);
+ return 1;
+}
+
+static int openpixel_instance(instance* inst){
+ openpixel_instance_data* data = calloc(1, sizeof(openpixel_instance_data));
+ inst->impl = data;
+ if(!inst->impl){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+
+ data->dest_fd = -1;
+ data->listen_fd = -1;
+ return 0;
+}
+
+static ssize_t openpixel_buffer_find(openpixel_instance_data* data, uint8_t strip, uint8_t input){
+ ssize_t n = 0;
+
+ for(n = 0; n < data->buffers; n++){
+ if(data->buffer[n].strip == strip
+ && (data->buffer[n].flags & OPENPIXEL_INPUT) >= input){
+ DBGPF("Using allocated %s buffer for requested strip %d, size %d", input ? "input" : "output", strip, data->buffer[n].bytes);
+ return n;
+ }
+ }
+ DBGPF("Instance has no %s buffer for requested strip %d", input ? "input" : "output", strip);
+ return -1;
+}
+
+static int openpixel_buffer_extend(openpixel_instance_data* data, uint8_t strip, uint8_t input, uint16_t length){
+ ssize_t buffer = openpixel_buffer_find(data, strip, input);
+
+ //length is in component-channels, round it to the nearest rgb-triplet
+ //this guarantees that any allocated buffer has at least three bytes, which is important to parts of the receive handler
+ length = (length % 3) ? ((length / 3) + 1) * 3 : length;
+
+ //calculate required buffer length
+ size_t bytes_required = (data->mode == rgb8) ? length : length * 2;
+ if(buffer < 0){
+ //allocate new buffer
+ data->buffer = realloc(data->buffer, (data->buffers + 1) * sizeof(openpixel_buffer));
+ if(!data->buffer){
+ data->buffers = 0;
+ LOG("Failed to allocate memory");
+ return -1;
+ }
+
+ buffer = data->buffers;
+ data->buffers++;
+
+ data->buffer[buffer].strip = strip;
+ data->buffer[buffer].flags = input ? OPENPIXEL_INPUT : 0;
+ data->buffer[buffer].bytes = 0;
+ data->buffer[buffer].data.u8 = NULL;
+ }
+
+ if(data->buffer[buffer].bytes < bytes_required){
+ //resize buffer
+ data->buffer[buffer].data.u8 = realloc(data->buffer[buffer].data.u8, bytes_required);
+ if(!data->buffer[buffer].data.u8){
+ data->buffer[buffer].bytes = 0;
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+ //FIXME might want to memset() only newly allocated channels
+ memset(data->buffer[buffer].data.u8, 0, bytes_required);
+ data->buffer[buffer].bytes = bytes_required;
+ }
+ return 0;
+}
+
+static channel* openpixel_channel(instance* inst, char* spec, uint8_t flags){
+ uint32_t strip = 0, channel = 0;
+ char* token = spec;
+ openpixel_instance_data* data = (openpixel_instance_data*) inst->impl;
+
+ //read strip index if supplied
+ if(!strncmp(spec, "strip", 5)){
+ strip = strtoul(spec + 5, &token, 10);
+ //skip the dot
+ token++;
+ }
+
+ //read (and calculate) channel index
+ if(!strncmp(token, "channel", 7)){
+ channel = strtoul(token + 7, NULL, 10);
+ }
+ else if(!strncmp(token, "red", 3)){
+ channel = strtoul(token + 3, NULL, 10) * 3 - 2;
+ }
+ else if(!strncmp(token, "green", 5)){
+ channel = strtoul(token + 5, NULL, 10) * 3 - 1;
+ }
+ else if(!strncmp(token, "blue", 4)){
+ channel = strtoul(token + 4, NULL, 10) * 3;
+ }
+
+ if(!channel){
+ LOGPF("Invalid channel specification %s", spec);
+ return NULL;
+ }
+
+ //check channel direction
+ if(flags & mmchannel_input){
+ //strip 0 (bcast) can not be mapped as input
+ if(!strip){
+ LOGPF("Broadcast channel %s.%s can not be mapped as an input", inst->name, spec);
+ return NULL;
+ }
+ if(data->listen_fd < 0){
+ LOGPF("Channel %s mapped as input, but instance %s is not accepting input", spec, inst->name);
+ return NULL;
+ }
+
+ if(openpixel_buffer_extend(data, strip, 1, channel)){
+ return NULL;
+ }
+ }
+
+ if(flags & mmchannel_output){
+ if(data->dest_fd < 0){
+ LOGPF("Channel %s mapped as output, but instance %s is not sending output", spec, inst->name);
+ return NULL;
+ }
+
+ if(openpixel_buffer_extend(data, strip, 0, channel)){
+ return NULL;
+ }
+ }
+
+ return mm_channel(inst, ((uint64_t) strip) << 32 | channel, 1);
+}
+
+static int openpixel_output_data(openpixel_instance_data* data){
+ size_t u;
+ openpixel_header hdr;
+
+ //send updated strips
+ for(u = 0; u < data->buffers; u++){
+ if(!(data->buffer[u].flags & OPENPIXEL_INPUT) && (data->buffer[u].flags & OPENPIXEL_MARK)){
+ //remove mark
+ data->buffer[u].flags &= ~OPENPIXEL_MARK;
+
+ //prepare header
+ hdr.strip = data->buffer[u].strip;
+ hdr.mode = data->mode;
+ hdr.length = htobe16(data->buffer[u].bytes);
+
+ //output data
+ if(mmbackend_send(data->dest_fd, (uint8_t*) &hdr, sizeof(hdr))
+ || mmbackend_send(data->dest_fd, data->buffer[u].data.u8, data->buffer[u].bytes)){
+ return 1;
+ }
+ }
+ }
+
+ return 0;
+}
+
+static int openpixel_set(instance* inst, size_t num, channel** c, channel_value* v){
+ openpixel_instance_data* data = (openpixel_instance_data*) inst->impl;
+ size_t u, p;
+ ssize_t buffer;
+ uint32_t strip, channel;
+
+ for(u = 0; u < num; u++){
+ //read strip/channel
+ strip = c[u]->ident >> 32;
+ channel = c[u]->ident & 0xFFFFFFFF;
+ channel--;
+
+ //find the buffer
+ buffer = openpixel_buffer_find(data, strip, 0);
+ if(buffer < 0){
+ LOGPF("No buffer for channel %s.%d.%d\n", inst->name, strip, channel);
+ continue;
+ }
+
+ //mark buffer for output
+ data->buffer[buffer].flags |= OPENPIXEL_MARK;
+
+ //update data
+ switch(data->mode){
+ case rgb8:
+ data->buffer[buffer].data.u8[channel] = ((uint8_t)(v[u].normalised * 255.0));
+ break;
+ case rgb16:
+ data->buffer[buffer].data.u16[channel] = ((uint16_t)(v[u].normalised * 65535.0));
+ break;
+ }
+
+ if(strip == 0){
+ //update values in all other output strips, don't mark
+ for(p = 0; p < data->buffers; p++){
+ if(!(data->buffer[p].flags & OPENPIXEL_INPUT)){
+ //check whether the buffer is large enough
+ if(data->mode == rgb8 && data->buffer[p].bytes >= channel){
+ data->buffer[p].data.u8[channel] = ((uint8_t)(v[u].normalised * 255.0));
+ }
+ else if(data->mode == rgb16 && data->buffer[p].bytes >= channel * 2){
+ data->buffer[p].data.u16[channel] = ((uint16_t)(v[u].normalised * 65535.0));
+ }
+ }
+ }
+ }
+ }
+
+ return openpixel_output_data(data);
+}
+
+static int openpixel_client_new(instance* inst, int fd){
+ if(fd < 0){
+ return 1;
+ }
+ openpixel_instance_data* data = (openpixel_instance_data*) inst->impl;
+ size_t u;
+
+ //mark nonblocking
+ #ifdef _WIN32
+ unsigned long flags = 1;
+ if(ioctlsocket(fd, FIONBIO, &flags)){
+ #else
+ int flags = fcntl(fd, F_GETFL, 0);
+ if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){
+ #endif
+ LOGPF("Failed to set client descriptor on %s nonblocking", inst->name);
+ close(fd);
+ return 0;
+ }
+
+ //find a client block
+ for(u = 0; u < data->clients; u++){
+ if(data->client[u].fd <= 0){
+ break;
+ }
+ }
+
+ //if no free slot, make one
+ if(u == data->clients){
+ data->client = realloc(data->client, (data->clients + 1) * sizeof(openpixel_client));
+ if(!data->client){
+ data->clients = 0;
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+ data->clients++;
+ }
+
+ data->client[u].fd = fd;
+ data->client[u].buffer = -1;
+ data->client[u].offset = 0;
+
+ LOGPF("New client on instance %s", inst->name);
+ return mm_manage_fd(fd, BACKEND_NAME, 1, inst);
+}
+
+static size_t openpixel_strip_pixeldata8(instance* inst, openpixel_client* client, uint8_t* data, openpixel_buffer* buffer, size_t bytes_left){
+ channel* chan = NULL;
+ channel_value val;
+ size_t u;
+
+ for(u = 0; u < bytes_left; u++){
+ //if over buffer length, ignore
+ if(u + client->offset >= buffer->bytes){
+ client->buffer = -2;
+ break;
+ }
+
+ //FIXME if at start of trailing non-multiple of 3, ignore
+
+ //update changed channels
+ if(buffer->data.u8[u + client->offset] != data[u]){
+ buffer->data.u8[u + client->offset] = data[u];
+ chan = mm_channel(inst, ((uint64_t) buffer->strip << 32) | (u + client->offset + 1), 0);
+ if(chan){
+ //push event
+ val.raw.u64 = data[u];
+ val.normalised = (double) data[u] / 255.0;
+ if(mm_channel_event(chan, val)){
+ LOG("Failed to push channel event to core");
+ }
+ }
+ }
+ }
+ return u;
+}
+
+static size_t openpixel_strip_pixeldata16(instance* inst, openpixel_client* client, uint8_t* data, openpixel_buffer* buffer, size_t bytes_left){
+ channel* chan = NULL;
+ channel_value val;
+ size_t u;
+
+ for(u = 0; u < bytes_left; u++){
+ //if over buffer length, ignore
+ if(u + client->offset >= buffer->bytes){
+ client->buffer = -2;
+ break;
+ }
+
+ //if at start of trailing non-multiple of 6, ignore
+ if((client->offset + u) >= (client->offset + client->left) - ((client->offset + client->left) % 6)){
+ client->buffer = -2;
+ break;
+ }
+
+ //byte-order conversion may be on message boundary, do it via a buffer
+ client->boundary.u8[(client->offset + u) % 2] = data[u];
+
+ //detect and update changed channels
+ if((client->offset + u) % 2
+ && buffer->data.u16[(u + client->offset) / 2] != be16toh(client->boundary.u16)){
+ buffer->data.u16[(u + client->offset) / 2] = be16toh(client->boundary.u16);
+ chan = mm_channel(inst, ((uint64_t) buffer->strip << 32) | ((u + client->offset) / 2 + 1), 0);
+ if(chan){
+ //push event
+ val.raw.u64 = be16toh(client->boundary.u16);;
+ val.normalised = (double) val.raw.u64 / 65535.0;
+ if(mm_channel_event(chan, val)){
+ LOG("Failed to push channel event to core");
+ }
+ }
+
+ }
+ }
+ return u;
+}
+
+static ssize_t openpixel_client_pixeldata(instance* inst, openpixel_client* client, uint8_t* buffer, size_t bytes_left){
+ openpixel_instance_data* data = (openpixel_instance_data*) inst->impl;
+ openpixel_client temp_client = {
+ .fd = -1
+ };
+ ssize_t u, p;
+ uint8_t processing_done = 1;
+
+ //ignore data
+ if(client->buffer == -2){
+ //ignore data
+ client->offset += bytes_left;
+ client->left -= bytes_left;
+ return bytes_left;
+ }
+ //handle broadcast data
+ else if(client->buffer == -3){
+ //iterate all input strips
+ for(p = 0; p < data->buffers; p++){
+ if(data->buffer[p].flags & OPENPIXEL_INPUT){
+ //prepare temporary client
+ temp_client.buffer = p;
+ temp_client.hdr = client->hdr;
+ temp_client.hdr.strip = data->buffer[p].strip;
+ temp_client.offset = client->offset;
+ temp_client.left = client->left;
+
+ //run processing on strip
+ if(data->mode == rgb8){
+ openpixel_strip_pixeldata8(inst, &temp_client, buffer, data->buffer + p, bytes_left);
+ }
+ else{
+ openpixel_strip_pixeldata16(inst, &temp_client, buffer, data->buffer + p, bytes_left);
+ }
+ if(temp_client.buffer != -2){
+ processing_done = 0;
+ }
+ }
+ }
+
+ //if all strips report being done, ignore the rest of the data
+ if(processing_done){
+ client->buffer = -2;
+ }
+
+ //remove data
+ u = min(client->left, bytes_left);
+ client->offset += u;
+ client->left -= u;
+ return u;
+ }
+ //process data
+ else{
+ if(data->mode == rgb8){
+ u = openpixel_strip_pixeldata8(inst, client, buffer, data->buffer + client->buffer, bytes_left);
+ }
+ else{
+ u = openpixel_strip_pixeldata16(inst, client, buffer, data->buffer + client->buffer, bytes_left);
+ }
+
+ //update offsets
+ client->offset += u;
+ client->left -= u;
+ return u;
+ }
+ return -1;
+}
+
+static ssize_t openpixel_client_headerdata(instance* inst, openpixel_client* client, uint8_t* buffer, size_t bytes_left){
+ openpixel_instance_data* data = (openpixel_instance_data*) inst->impl;
+ size_t bytes_consumed = min(sizeof(openpixel_header) - client->offset, bytes_left);
+
+ DBGPF("Reading %" PRIsize_t " bytes to header at offset %" PRIsize_t ", header size %" PRIsize_t ", %" PRIsize_t " bytes left", bytes_consumed, client->offset, sizeof(openpixel_header), bytes_left);
+ memcpy(((uint8_t*) (&client->hdr)) + client->offset, buffer, bytes_consumed);
+
+ //if done, resolve buffer
+ if(sizeof(openpixel_header) - client->offset <= bytes_left){
+ //if broadcast strip, mark broadcast
+ if(client->hdr.strip == 0
+ && data->mode == client->hdr.mode){
+ client->buffer = -3;
+ }
+ else{
+ client->buffer = openpixel_buffer_find(data, client->hdr.strip, 1);
+ //if no buffer or mode mismatch, ignore data
+ if(client->buffer < 0
+ || data->mode != client->hdr.mode){
+ client->buffer = -2; //mark for ignore
+ }
+ }
+ client->left = be16toh(client->hdr.length);
+ client->offset = 0;
+ }
+ //if not, update client offset
+ else{
+ client->offset += bytes_consumed;
+ }
+
+ //update scan offset
+ return bytes_consumed;
+}
+
+static int openpixel_client_handle(instance* inst, int fd){
+ openpixel_instance_data* data = (openpixel_instance_data*) inst->impl;
+ uint8_t buffer[8192];
+ size_t c = 0, offset = 0;
+ ssize_t bytes_left = 0, bytes_handled;
+
+ for(c = 0; c < data->clients; c++){
+ if(data->client[c].fd == fd){
+ break;
+ }
+ }
+
+ if(c == data->clients){
+ LOGPF("Unknown client descriptor signaled on %s", inst->name);
+ return 1;
+ }
+
+ //FIXME might want to read until EAGAIN
+ ssize_t bytes = recv(fd, buffer, sizeof(buffer), 0);
+ if(bytes <= 0){
+ if(bytes < 0){
+ LOGPF("Failed to receive from client: %s", strerror(errno));
+ }
+
+ //close the connection
+ close(fd);
+ data->client[c].fd = -1;
+
+ //unmanage the fd
+ LOGPF("Client disconnected on %s", inst->name);
+ mm_manage_fd(fd, BACKEND_NAME, 0, NULL);
+ return 0;
+ }
+ DBGPF("Received %" PRIsize_t " bytes on %s", bytes, inst->name);
+
+ for(bytes_left = bytes - offset; bytes_left > 0; bytes_left = bytes - offset){
+ if(data->client[c].buffer == -1){
+ //read a header
+ bytes_handled = openpixel_client_headerdata(inst, data->client + c, buffer + offset, bytes_left);
+ if(bytes_handled < 0){
+ //FIXME handle errors
+ }
+ }
+ else{
+ //read data
+ bytes_handled = openpixel_client_pixeldata(inst, data->client + c, buffer + offset, min(bytes_left, data->client[c].left));
+ if(bytes_handled < 0){
+ //FIXME handle errors
+ }
+
+ //end of data, return to reading headers
+ if(data->client[c].left == 0){
+ data->client[c].buffer = -1;
+ data->client[c].offset = 0;
+ data->client[c].left = 0;
+ }
+ }
+ offset += bytes_handled;
+ }
+ DBGPF("Processing done on %s", inst->name);
+
+ return 0;
+}
+
+static int openpixel_handle(size_t num, managed_fd* fds){
+ size_t u;
+ instance* inst = NULL;
+ openpixel_instance_data* data = NULL;
+ uint8_t buffer[8192];
+ ssize_t bytes;
+
+ for(u = 0; u < num; u++){
+ inst = (instance*) fds[u].impl;
+ data = (openpixel_instance_data*) inst->impl;
+
+ if(fds[u].fd == data->dest_fd){
+ //destination fd ready to read
+ //since the protocol does not define any responses, the connection was probably closed
+ bytes = recv(data->dest_fd, buffer, sizeof(buffer), 0);
+ if(bytes <= 0){
+ LOGPF("Output descriptor closed on instance %s", inst->name);
+ //unmanage the fd to give the core some rest
+ mm_manage_fd(data->dest_fd, BACKEND_NAME, 0, NULL);
+ }
+ else{
+ LOGPF("Unhandled response data on %s (%" PRIsize_t" bytes)", inst->name, bytes);
+ }
+ }
+ else if(fds[u].fd == data->listen_fd){
+ //listen fd ready to read, accept a new client
+ if(openpixel_client_new(inst, accept(data->listen_fd, NULL, NULL))){
+ return 1;
+ }
+ }
+ else{
+ //handle client input
+ if(openpixel_client_handle(inst, fds[u].fd)){
+ return 1;
+ }
+ }
+ }
+ return 0;
+}
+
+static int openpixel_start(size_t n, instance** inst){
+ int rv = -1;
+ size_t u, nfds = 0;
+ openpixel_instance_data* data = NULL;
+
+ for(u = 0; u < n; u++){
+ data = (openpixel_instance_data*) inst[u]->impl;
+
+ //register fds
+ if(data->dest_fd >= 0){
+ if(mm_manage_fd(data->dest_fd, BACKEND_NAME, 1, inst[u])){
+ LOGPF("Failed to register destination descriptor for instance %s with core", inst[u]->name);
+ goto bail;
+ }
+ nfds++;
+ }
+ if(data->listen_fd >= 0){
+ if(mm_manage_fd(data->listen_fd, BACKEND_NAME, 1, inst[u])){
+ LOGPF("Failed to register host descriptor for instance %s with core", inst[u]->name);
+ goto bail;
+ }
+ nfds++;
+ }
+ }
+
+ LOGPF("Registered %" PRIsize_t " descriptors to core", nfds);
+ rv = 0;
+bail:
+ return rv;
+}
+
+static int openpixel_shutdown(size_t n, instance** inst){
+ size_t u, p;
+ openpixel_instance_data* data = NULL;
+
+ for(u = 0; u < n; u++){
+ data = (openpixel_instance_data*) inst[u]->impl;
+
+ //shutdown all clients
+ for(p = 0; p < data->clients; p++){
+ if(data->client[p].fd>= 0){
+ close(data->client[p].fd);
+ }
+ }
+ free(data->client);
+
+ //close all configured fds
+ if(data->listen_fd >= 0){
+ close(data->listen_fd);
+ }
+ if(data->dest_fd >= 0){
+ close(data->dest_fd);
+ }
+
+ //free all buffers
+ for(p = 0; p < data->buffers; p++){
+ free(data->buffer[p].data.u8);
+ }
+ free(data->buffer);
+
+ free(data);
+ inst[u]->impl = NULL;
+ }
+
+ LOG("Backend shut down");
+ return 0;
+}
diff --git a/backends/openpixelcontrol.h b/backends/openpixelcontrol.h
new file mode 100644
index 0000000..63e9664
--- /dev/null
+++ b/backends/openpixelcontrol.h
@@ -0,0 +1,59 @@
+#include "midimonster.h"
+
+MM_PLUGIN_API int init();
+static int openpixel_configure(char* option, char* value);
+static int openpixel_configure_instance(instance* inst, char* option, char* value);
+static int openpixel_instance(instance* inst);
+static channel* openpixel_channel(instance* inst, char* spec, uint8_t flags);
+static int openpixel_set(instance* inst, size_t num, channel** c, channel_value* v);
+static int openpixel_handle(size_t num, managed_fd* fds);
+static int openpixel_start(size_t n, instance** inst);
+static int openpixel_shutdown(size_t n, instance** inst);
+
+#define OPENPIXEL_INPUT 1
+#define OPENPIXEL_MARK 2
+
+typedef struct /*_data_buffer*/ {
+ uint8_t strip;
+ uint8_t flags;
+ uint16_t bytes;
+ union {
+ uint16_t* u16;
+ uint8_t* u8;
+ } data;
+} openpixel_buffer;
+
+#pragma pack(push, 1)
+typedef struct /*_openpixel_hdr*/ {
+ uint8_t strip;
+ uint8_t mode;
+ uint16_t length;
+} openpixel_header;
+#pragma pack(pop)
+
+typedef struct /*_openpixel_client*/ {
+ int fd;
+ ssize_t buffer; /* -1 header, -2 ignore, -3 bcast */
+ openpixel_header hdr;
+ size_t offset;
+ size_t left;
+ union {
+ uint8_t u8[2];
+ uint16_t u16;
+ } boundary;
+} openpixel_client;
+
+typedef struct {
+ enum {
+ rgb8 = 0,
+ rgb16 = 2
+ } mode;
+
+ size_t buffers;
+ openpixel_buffer* buffer;
+
+ int dest_fd;
+ int listen_fd;
+ size_t clients;
+ openpixel_client* client;
+} openpixel_instance_data;
diff --git a/backends/openpixelcontrol.md b/backends/openpixelcontrol.md
new file mode 100644
index 0000000..d09d412
--- /dev/null
+++ b/backends/openpixelcontrol.md
@@ -0,0 +1,55 @@
+### The `openpixelcontrol` backend
+
+This backend provides read-write access to the TCP-based OpenPixelControl protocol,
+used for controlling intelligent RGB led strips.
+
+This backend can both control a remote OpenPixelControl server as well as receive data
+from OpenPixelControl clients.
+
+#### Global configuration
+
+This backend does not take any global configuration.
+
+#### Instance configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|-----------------------|
+| `destination` | `10.11.12.1 9001` | none | Destination for output data. Setting this option enables the instance for output |
+| `listen` | `10.11.12.2 9002` | none | Local address to wait for client connections on. Setting this enables the instance for input |
+| `mode` | `16bit` | `8bit` | RGB channel resolution |
+
+#### Channel specification
+
+Each instance can control up to 255 strips of RGB LED lights. The OpenPixelControl specification
+confusingly calls these strips "channels".
+
+Strip `0` acts as a "broadcast" strip, setting values on all other strips at once.
+Consequently, components on strip 0 can only be mapped as output channels to a destination
+(setting components on all strips there), not as input channels. When such messages are received from
+a client, the corresponding mapped component channels on all strips will receive events.
+
+Every single component of any LED on any string can be mapped as an individual MIDIMonster channel.
+The components are laid out as sequences of Red - Green - Blue value triplets.
+
+Channels can be specified by their sequential index (one-based).
+
+Example mapping (data from Strip 2 LED 66's green component is mapped to the blue component of LED 2 on strip 1):
+```
+strip1.channel6 < strip2.channel200
+```
+
+Additionally, channels may be referred to by their color component and LED index:
+```
+strip1.blue2 < strip2.green66
+```
+
+#### Known bugs / problems
+
+If the connection is lost, it is currently not reestablished and may cause the MIDIMonster to exit entirely.
+This behaviour may be changed in future releases.
+
+While acting as an OpenPixelControl server, the backend allows multiple clients to connect.
+This may lead to confusing data output when multiple clients are trying to control the same strip.
+
+When acting as a 16bit OpenPixelControl server, input on the broadcast strip (strip 0) may cause erratic
+value events on a few channels, especially with longer strips and inputs.
diff --git a/backends/osc.c b/backends/osc.c
index 7b9a5a5..754c290 100644
--- a/backends/osc.c
+++ b/backends/osc.c
@@ -562,21 +562,16 @@ static int osc_configure_instance(instance* inst, char* option, char* value){
return 1;
}
-static instance* osc_instance(){
- instance* inst = mm_instance();
- if(!inst){
- return NULL;
- }
-
+static int osc_instance(instance* inst){
osc_instance_data* data = calloc(1, sizeof(osc_instance_data));
if(!data){
LOG("Failed to allocate memory");
- return NULL;
+ return 1;
}
data->fd = -1;
inst->impl = data;
- return inst;
+ return 0;
}
static channel* osc_map_channel(instance* inst, char* spec, uint8_t flags){
diff --git a/backends/osc.h b/backends/osc.h
index f8ff3ff..ec75e3f 100644
--- a/backends/osc.h
+++ b/backends/osc.h
@@ -10,7 +10,7 @@
MM_PLUGIN_API int init();
static int osc_configure(char* option, char* value);
static int osc_configure_instance(instance* inst, char* option, char* value);
-static instance* osc_instance();
+static int osc_instance(instance* inst);
static channel* osc_map_channel(instance* inst, char* spec, uint8_t flags);
static int osc_set(instance* inst, size_t num, channel** c, channel_value* v);
static int osc_handle(size_t num, managed_fd* fds);
diff --git a/backends/python.c b/backends/python.c
new file mode 100644
index 0000000..9f1d642
--- /dev/null
+++ b/backends/python.c
@@ -0,0 +1,733 @@
+#define BACKEND_NAME "python"
+
+#define PY_SSIZE_T_CLEAN
+#include <string.h>
+#include <Python.h>
+#include "python.h"
+
+#define MMPY_INSTANCE_KEY "midimonster_instance"
+
+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,
+ .conf = python_configure,
+ .create = python_instance,
+ .conf_instance = python_configure_instance,
+ .channel = python_channel,
+ .handle = python_set,
+ .process = python_handle,
+ .start = python_start,
+ .interval = python_interval,
+ .shutdown = python_shutdown
+ };
+
+ //register backend
+ if(mm_backend_register(python)){
+ LOG("Failed to register backend");
+ return 1;
+ }
+ 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){
+ next_interval = 10;
+ break;
+ }
+ }
+ }
+ }
+
+ timer_interval = next_interval;
+}
+
+static int python_configure(char* option, char* value){
+ LOG("No backend configuration possible");
+ return 1;
+}
+
+static int python_prepend_str(PyObject* list, char* str){
+ if(!list || !str){
+ return 1;
+ }
+
+ PyObject* item = PyUnicode_FromString(str);
+ if(!item){
+ return 1;
+ }
+
+ if(PyList_Insert(list, 0, item) < 0){
+ Py_DECREF(item);
+ return 1;
+ }
+ Py_DECREF(item);
+ return 0;
+}
+
+static PyObject* mmpy_output(PyObject* self, PyObject* args){
+ instance* inst = *((instance**) PyModule_GetState(self));
+ python_instance_data* data = (python_instance_data*) inst->impl;
+ const char* channel_name = NULL;
+ channel_value val = {
+ {0}
+ };
+ size_t u;
+
+ if(!PyArg_ParseTuple(args, "sd", &channel_name, &val.normalised)){
+ return NULL;
+ }
+
+ val.normalised = clamp(val.normalised, 1.0, 0.0);
+ //if not started yet, create any requested channels so we can set them at load time
+ if(!last_timestamp){
+ python_channel(inst, (char*) channel_name, mmchannel_output);
+ }
+
+ for(u = 0; u < data->channels; u++){
+ if(!strcmp(data->channel[u].name, channel_name)){
+ DBGPF("Setting channel %s.%s to %f", inst->name, channel_name, val.normalised);
+ data->channel[u].out = val.normalised;
+ if(!last_timestamp){
+ data->channel[u].mark = 1;
+ }
+ else{
+ mm_channel_event(mm_channel(inst, u, 0), val);
+ }
+ return 0;
+ }
+ }
+
+ if(u == data->channels){
+ DBGPF("Output on unknown channel %s.%s, no event pushed", inst->name, channel_name);
+ }
+
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+static PyObject* mmpy_channel_value(PyObject* self, PyObject* args, uint8_t in){
+ instance* inst = *((instance**) PyModule_GetState(self));
+ python_instance_data* data = (python_instance_data*) inst->impl;
+ const char* channel_name = NULL;
+ size_t u;
+
+ if(!PyArg_ParseTuple(args, "s", &channel_name)){
+ return NULL;
+ }
+
+ for(u = 0; u < data->channels; u++){
+ if(!strcmp(data->channel[u].name, channel_name)){
+ return PyFloat_FromDouble(in ? data->channel[u].in : data->channel[u].out);
+ }
+ }
+
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+static PyObject* mmpy_current_handler(PyObject* self, PyObject* args){
+ instance* inst = *((instance**) PyModule_GetState(self));
+ python_instance_data* data = (python_instance_data*) inst->impl;
+
+ if(data->current_channel){
+ return PyUnicode_FromString(data->current_channel->name);
+ }
+
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+static PyObject* mmpy_output_value(PyObject* self, PyObject* args){
+ return mmpy_channel_value(self, args, 0);
+}
+
+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
+ PyObject* capsule = PyDict_GetItemString(PyThreadState_GetDict(), MMPY_INSTANCE_KEY);
+ if(capsule && inst){
+ *inst = PyCapsule_GetPointer(capsule, NULL);
+ return 0;
+ }
+
+ PyErr_SetString(PyExc_AssertionError, "Failed to pass instance pointer for initialization");
+ return -1;
+}
+
+static int python_configure_instance(instance* inst, char* option, char* value){
+ python_instance_data* data = (python_instance_data*) inst->impl;
+ PyObject* module = NULL;
+
+ //load python script
+ if(!strcmp(option, "module")){
+ //swap to interpreter
+ PyEval_RestoreThread(data->interpreter);
+ //import the module
+ module = PyImport_ImportModule(value);
+ if(!module){
+ LOGPF("Failed to import module %s to instance %s", value, inst->name);
+ PyErr_Print();
+ }
+ Py_XDECREF(module);
+ 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;
+}
+
+static PyObject* mmpy_init(){
+ static PyModuleDef_Slot mmpy_slots[] = {
+ {Py_mod_exec, (void*) mmpy_exec},
+ {0}
+ };
+
+ 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"},
+ {"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}
+ };
+
+ static struct PyModuleDef mmpy = {
+ PyModuleDef_HEAD_INIT,
+ "midimonster",
+ NULL, /*doc size*/
+ sizeof(instance*),
+ mmpy_methods,
+ mmpy_slots
+ };
+
+ //single-phase init
+ //return PyModule_Create(&mmpy);
+
+ //multi-phase init
+ return PyModuleDef_Init(&mmpy);
+}
+
+static int python_instance(instance* inst){
+ python_instance_data* data = calloc(1, sizeof(python_instance_data));
+ PyObject* interpreter_dict = NULL;
+ char current_directory[8192];
+ if(!data){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+
+ //lazy-init because we need the interpreter running before _start,
+ //but don't want it running if no instances are defined
+ if(!python_main){
+ LOG("Initializing main python interpreter");
+ if(PyImport_AppendInittab("midimonster", &mmpy_init)){
+ LOG("Failed to extend python inittab for main interpreter");
+ }
+ program_name = Py_DecodeLocale("midimonster", NULL);
+ Py_SetProgramName(program_name);
+ //initialize python
+ Py_InitializeEx(0);
+ //create, acquire and release the GIL
+ PyEval_InitThreads();
+ python_main = PyEval_SaveThread();
+ }
+
+ //acquire the GIL before creating a new interpreter
+ PyEval_RestoreThread(python_main);
+ //create subinterpreter for new instance
+ data->interpreter = Py_NewInterpreter();
+
+ //push cwd as import path
+ if(getcwd(current_directory, sizeof(current_directory))){
+ if(python_prepend_str(PySys_GetObject("path"), current_directory)){
+ LOG("Failed to push current working directory to python");
+ goto bail;
+ }
+ }
+
+ //push the instance pointer for later module initialization
+ //FIXME python 3.8 introduces interpreter_dict = PyInterpreterState_GetDict(data->interpreter->interp);
+ //for now use thread state...
+ interpreter_dict = PyThreadState_GetDict();
+ if(!interpreter_dict){
+ LOG("Failed to access per-interpreter data storage");
+ goto bail;
+ }
+ //FIXME this might leak a reference to the capsule
+ if(PyDict_SetItemString(interpreter_dict, MMPY_INSTANCE_KEY, PyCapsule_New(inst, NULL, NULL))){
+ LOG("Failed to set per-interpreter instance pointer");
+ goto bail;
+ }
+
+ //NewInterpreter leaves us with the GIL, drop it
+ PyEval_ReleaseThread(data->interpreter);
+ inst->impl = data;
+ return 0;
+
+bail:
+ if(data->interpreter){
+ PyEval_ReleaseThread(data->interpreter);
+ }
+ free(data);
+ return 1;
+}
+
+static channel* python_channel(instance* inst, char* spec, uint8_t flags){
+ python_instance_data* data = (python_instance_data*) inst->impl;
+ size_t u;
+
+ for(u = 0; u < data->channels; u++){
+ if(!strcmp(data->channel[u].name, spec)){
+ break;
+ }
+ }
+
+ if(u == data->channels){
+ data->channel = realloc(data->channel, (data->channels + 1) * sizeof(mmpython_channel));
+ if(!data->channel){
+ data->channels = 0;
+ LOG("Failed to allocate memory");
+ return NULL;
+ }
+ memset(data->channel + u, 0, sizeof(mmpython_channel));
+
+ data->channel[u].name = strdup(spec);
+ if(!data->channel[u].name){
+ LOG("Failed to allocate memory");
+ return NULL;
+ }
+ data->channels++;
+ }
+
+ return mm_channel(inst, u, 1);
+}
+
+static int python_set(instance* inst, size_t num, channel** c, channel_value* v){
+ python_instance_data* data = (python_instance_data*) inst->impl;
+ mmpython_channel* chan = NULL;
+ PyObject* result = NULL;
+ size_t u;
+
+ //swap to interpreter
+ PyEval_RestoreThread(data->interpreter);
+
+ for(u = 0; u < num; u++){
+ chan = data->channel + c[u]->ident;
+
+ //update input value buffer
+ chan->in = v[u].normalised;
+
+ //call handler if present
+ if(chan->handler){
+ DBGPF("Calling handler for %s.%s", inst->name, chan->name);
+ data->current_channel = chan;
+ result = PyObject_CallFunction(chan->handler, "d", chan->in);
+ Py_XDECREF(result);
+ data->current_channel = NULL;
+ DBGPF("Done with handler for %s.%s", inst->name, chan->name);
+ }
+ }
+
+ //release interpreter
+ PyEval_ReleaseThread(data->interpreter);
+ return 0;
+}
+
+static int python_handle(size_t num, managed_fd* fds){
+ 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;
+}
+
+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;
+ size_t u, p;
+ channel_value v;
+
+ //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);
+
+ if(data->default_handler){
+ data->handler = python_resolve_symbol(data->default_handler);
+ }
+
+ 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);
+ }
+ //push initial values
+ if(data->channel[p].mark){
+ v.normalised = data->channel[p].out;
+ mm_channel_event(mm_channel(inst[u], p, 0), v);
+ }
+ }
+
+ //release interpreter
+ PyEval_ReleaseThread(data->interpreter);
+ }
+ return 0;
+}
+
+static int python_shutdown(size_t n, instance** inst){
+ size_t u, p;
+ 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);
+ }
+ 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
+ PyEval_RestoreThread(python_main);
+
+ 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);
+ }
+ Py_XDECREF(data->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
+ 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");
+ }
+ PyMem_RawFree(program_name);
+ }
+
+ LOG("Backend shut down");
+ return 0;
+}
diff --git a/backends/python.h b/backends/python.h
new file mode 100644
index 0000000..020aeac
--- /dev/null
+++ b/backends/python.h
@@ -0,0 +1,48 @@
+#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);
+static channel* python_channel(instance* inst, char* spec, uint8_t flags);
+static int python_set(instance* inst, size_t num, channel** c, channel_value* v);
+static int python_handle(size_t num, managed_fd* fds);
+static int python_start(size_t n, instance** inst);
+static int python_shutdown(size_t n, instance** inst);
+
+typedef struct /*_python_channel_data*/ {
+ char* name;
+ PyObject* handler;
+ double in;
+ double out;
+ uint8_t mark;
+} 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;
+
+ char* default_handler;
+ PyObject* handler;
+} python_instance_data;
diff --git a/backends/python.md b/backends/python.md
new file mode 100644
index 0000000..6852a79
--- /dev/null
+++ b/backends/python.md
@@ -0,0 +1,101 @@
+### The `python` backend
+
+The `python` backend provides a flexible programming environment, allowing users
+to route, generate and manipulate channel events using the Python 3 scripting language.
+
+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.
+
+To interact with the MIDIMonster core, import the `midimonster` module from within your module.
+
+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 |
+| `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:
+```python
+import socket
+import midimonster
+
+# Simple channel handler
+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.timestamp())
+
+# 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)
+```
+
+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.
+
+#### Instance configuration
+
+| 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).
+
+#### Channel specification
+
+Channel names may be any valid Python function name. To call handler functions in a module,
+specify the channel as the functions qualified path (by prefixing it with the module name and a dot).
+
+Example mappings:
+```
+py1.my_handlers.in1 < py1.foo
+py1.out1 > py2.module.handler
+```
+
+#### Known bugs / problems
+
+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` 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.
+
+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.
diff --git a/backends/sacn.c b/backends/sacn.c
index ff2b61e..bd5c75a 100644
--- a/backends/sacn.c
+++ b/backends/sacn.c
@@ -28,12 +28,14 @@ static struct /*_sacn_global_config*/ {
size_t fds;
sacn_fd* fd;
uint64_t last_announce;
+ uint32_t next_frame;
} global_cfg = {
.source_name = "MIDIMonster",
.cid = {'M', 'I', 'D', 'I', 'M', 'o', 'n', 's', 't', 'e', 'r'},
.fds = 0,
.fd = NULL,
- .last_announce = 0
+ .last_announce = 0,
+ .next_frame = 0
};
MM_PLUGIN_API int init(){
@@ -46,6 +48,7 @@ MM_PLUGIN_API int init(){
.handle = sacn_set,
.process = sacn_handle,
.start = sacn_start,
+ .interval = sacn_interval,
.shutdown = sacn_shutdown
};
@@ -63,6 +66,13 @@ MM_PLUGIN_API int init(){
return 0;
}
+static uint32_t sacn_interval(){
+ if(global_cfg.next_frame){
+ return global_cfg.next_frame;
+ }
+ return SACN_KEEPALIVE_INTERVAL;
+}
+
static int sacn_listener(char* host, char* port, uint8_t flags){
int fd = -1, yes = 1;
if(global_cfg.fds >= MAX_FDS){
@@ -87,7 +97,6 @@ static int sacn_listener(char* host, char* port, uint8_t flags){
global_cfg.fd[global_cfg.fds].fd = fd;
global_cfg.fd[global_cfg.fds].universes = 0;
global_cfg.fd[global_cfg.fds].universe = NULL;
- global_cfg.fd[global_cfg.fds].last_frame = NULL;
if(flags & mcast_loop){
//set IP_MCAST_LOOP to allow local applications to receive output
@@ -190,24 +199,31 @@ static int sacn_configure_instance(instance* inst, char* option, char* value){
data->unicast_input = strtoul(value, NULL, 10);
return 0;
}
+ else if(!strcmp(option, "realtime")){
+ data->realtime = strtoul(value, NULL, 10);
+ return 0;
+ }
LOGPF("Unknown instance configuration option %s for instance %s", option, inst->name);
return 1;
}
-static instance* sacn_instance(){
- instance* inst = mm_instance();
- if(!inst){
- return NULL;
- }
+static int sacn_instance(instance* inst){
+ sacn_instance_data* data = calloc(1, sizeof(sacn_instance_data));
+ size_t u;
- inst->impl = calloc(1, sizeof(sacn_instance_data));
- if(!inst->impl){
+ if(!data){
LOG("Failed to allocate memory");
- return NULL;
+ return 1;
+ }
+
+ for(u = 0; u < sizeof(data->data.channel) / sizeof(channel); u++){
+ data->data.channel[u].ident = u;
+ data->data.channel[u].instance = inst;
}
- return inst;
+ inst->impl = data;
+ return 0;
}
static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){
@@ -215,7 +231,7 @@ static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){
char* spec_next = spec;
unsigned chan_a = strtoul(spec, &spec_next, 10), chan_b = 0;
-
+
//range check
if(!chan_a || chan_a > 512){
LOGPF("Channel out of range on instance %s: %s", inst->name, spec);
@@ -223,6 +239,11 @@ static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){
}
chan_a--;
+ //check output capabilities
+ if((flags & mmchannel_output) && !data->xmit_prio){
+ LOGPF("Channel %s.%s mapped for output, but instance is not configured for output (no priority set)", inst->name, spec);
+ }
+
//if wide channel, mark fine
if(*spec_next == '+'){
chan_b = strtoul(spec_next + 1, NULL, 10);
@@ -251,7 +272,7 @@ static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){
}
data->data.map[chan_a] = (*spec_next == '+') ? (MAP_COARSE | chan_b) : (MAP_SINGLE | chan_a);
- return mm_channel(inst, chan_a, 1);
+ return data->data.channel + chan_a;
}
static int sacn_transmit(instance* inst){
@@ -294,10 +315,11 @@ static int sacn_transmit(instance* inst){
LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno));
}
- //update last transmit timestamp
+ //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] == data->uni){
- global_cfg.fd[data->fd_index].last_frame[u] = mm_timestamp();
+ 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;
}
}
return 0;
@@ -305,6 +327,7 @@ static int sacn_transmit(instance* inst){
static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){
size_t u, mark = 0;
+ uint32_t frame_delta = 0;
sacn_instance_data* data = (sacn_instance_data*) inst->impl;
if(!num){
@@ -338,6 +361,25 @@ 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;
+ }
+ }
+
+ frame_delta = mm_timestamp() - global_cfg.fd[data->fd_index].universe[u].last_frame;
+
+ //check if ratelimiting engaged
+ if(frame_delta < SACN_FRAME_TIMEOUT){
+ global_cfg.fd[data->fd_index].universe[u].mark = 1;
+ if(!global_cfg.next_frame || global_cfg.next_frame > (SACN_FRAME_TIMEOUT - frame_delta)){
+ global_cfg.next_frame = (SACN_FRAME_TIMEOUT - frame_delta);
+ }
+ return 0;
+ }
+ }
sacn_transmit(inst);
}
@@ -389,16 +431,9 @@ static int sacn_process_frame(instance* inst, sacn_frame_root* frame, sacn_frame
if(inst_data->data.map[u] & MAP_MARK){
//unmark and get channel
inst_data->data.map[u] &= ~MAP_MARK;
+ chan = inst_data->data.channel + u;
if(inst_data->data.map[u] & MAP_FINE){
- chan = mm_channel(inst, MAPPED_CHANNEL(inst_data->data.map[u]), 0);
- }
- else{
- chan = mm_channel(inst, u, 0);
- }
-
- if(!chan){
- LOGPF("Active channel %" PRIsize_t " on %s not known to core", u, inst->name);
- return 1;
+ chan = inst_data->data.channel + MAPPED_CHANNEL(inst_data->data.map[u]);
}
//generate value
@@ -473,6 +508,7 @@ static void sacn_discovery(size_t fd){
static int sacn_handle(size_t num, managed_fd* fds){
size_t u, c;
uint64_t timestamp = mm_timestamp();
+ uint32_t synthesize_delta = 0;
ssize_t bytes_read;
char recv_buf[SACN_RECV_BUF];
instance* inst = NULL;
@@ -482,7 +518,7 @@ static int sacn_handle(size_t num, managed_fd* fds){
sacn_frame_root* frame = (sacn_frame_root*) recv_buf;
sacn_frame_data* data = (sacn_frame_data*) (recv_buf + sizeof(sacn_frame_root));
- if(mm_timestamp() - global_cfg.last_announce > SACN_DISCOVERY_TIMEOUT){
+ if(timestamp - global_cfg.last_announce > SACN_DISCOVERY_TIMEOUT){
//send universe discovery pdu
for(u = 0; u < global_cfg.fds; u++){
if(global_cfg.fd[u].universes){
@@ -492,17 +528,29 @@ static int sacn_handle(size_t num, managed_fd* fds){
global_cfg.last_announce = timestamp;
}
- //check for keepalive frames
+ //check for keepalive frames, synthesize frames if necessary
+ global_cfg.next_frame = 0;
for(u = 0; u < global_cfg.fds; u++){
for(c = 0; c < global_cfg.fd[u].universes; c++){
- if(timestamp - global_cfg.fd[u].last_frame[c] >= SACN_KEEPALIVE_INTERVAL){
+ synthesize_delta = timestamp - global_cfg.fd[u].universe[c].last_frame;
+
+ if((global_cfg.fd[u].universe[c].mark
+ && synthesize_delta >= SACN_FRAME_TIMEOUT + SACN_SYNTHESIZE_MARGIN)
+ || synthesize_delta >= SACN_KEEPALIVE_INTERVAL){
instance_id.fields.fd_index = u;
- instance_id.fields.uni = global_cfg.fd[u].universe[c];
+ 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);
}
}
+
+ //update next frame request
+ if(global_cfg.fd[u].universe[c].mark
+ && (!global_cfg.next_frame || global_cfg.next_frame > SACN_FRAME_TIMEOUT + SACN_SYNTHESIZE_MARGIN - synthesize_delta)){
+ global_cfg.next_frame = SACN_FRAME_TIMEOUT + SACN_SYNTHESIZE_MARGIN - synthesize_delta;
+ }
+
}
}
@@ -562,7 +610,6 @@ static int sacn_start(size_t n, instance** inst){
if(!global_cfg.fds){
LOG("Failed to start, no descriptors bound");
- free(inst);
return 1;
}
@@ -595,13 +642,15 @@ static int sacn_start(size_t n, instance** inst){
if(data->xmit_prio){
//add to list of advertised universes for this fd
- global_cfg.fd[data->fd_index].universe = realloc(global_cfg.fd[data->fd_index].universe, (global_cfg.fd[data->fd_index].universes + 1) * sizeof(uint16_t));
+ global_cfg.fd[data->fd_index].universe = realloc(global_cfg.fd[data->fd_index].universe, (global_cfg.fd[data->fd_index].universes + 1) * sizeof(sacn_output_universe));
if(!global_cfg.fd[data->fd_index].universe){
LOG("Failed to allocate memory");
goto bail;
}
- global_cfg.fd[data->fd_index].universe[global_cfg.fd[data->fd_index].universes] = data->uni;
+ global_cfg.fd[data->fd_index].universe[global_cfg.fd[data->fd_index].universes].universe = data->uni;
+ global_cfg.fd[data->fd_index].universe[global_cfg.fd[data->fd_index].universes].last_frame = 0;
+ global_cfg.fd[data->fd_index].universe[global_cfg.fd[data->fd_index].universes].mark = 0;
global_cfg.fd[data->fd_index].universes++;
//generate multicast destination address if none set
@@ -617,12 +666,6 @@ static int sacn_start(size_t n, instance** inst){
LOGPF("Registering %" PRIsize_t " descriptors to core", global_cfg.fds);
for(u = 0; u < global_cfg.fds; u++){
- //allocate memory for storing last frame transmission timestamp
- global_cfg.fd[u].last_frame = calloc(global_cfg.fd[u].universes, sizeof(uint64_t));
- if(!global_cfg.fd[u].last_frame){
- LOG("Failed to allocate memory");
- goto bail;
- }
if(mm_manage_fd(global_cfg.fd[u].fd, BACKEND_NAME, 1, (void*) u)){
goto bail;
}
@@ -643,7 +686,6 @@ static int sacn_shutdown(size_t n, instance** inst){
for(p = 0; p < global_cfg.fds; p++){
close(global_cfg.fd[p].fd);
free(global_cfg.fd[p].universe);
- free(global_cfg.fd[p].last_frame);
}
free(global_cfg.fd);
LOG("Backend shut down");
diff --git a/backends/sacn.h b/backends/sacn.h
index c8d11e9..4138f45 100644
--- a/backends/sacn.h
+++ b/backends/sacn.h
@@ -1,9 +1,10 @@
#include "midimonster.h"
MM_PLUGIN_API int init();
+static uint32_t sacn_interval();
static int sacn_configure(char* option, char* value);
static int sacn_configure_instance(instance* instance, char* option, char* value);
-static instance* sacn_instance();
+static int sacn_instance(instance* inst);
static channel* sacn_channel(instance* instance, char* spec, uint8_t flags);
static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v);
static int sacn_handle(size_t num, managed_fd* fds);
@@ -12,7 +13,11 @@ static int sacn_shutdown(size_t n, instance** inst);
#define SACN_PORT "5568"
#define SACN_RECV_BUF 8192
-#define SACN_KEEPALIVE_INTERVAL 2000
+//spec 6.6.2.1
+#define SACN_KEEPALIVE_INTERVAL 1000
+//spec 6.6.1
+#define SACN_FRAME_TIMEOUT 20
+#define SACN_SYNTHESIZE_MARGIN 10
#define SACN_DISCOVERY_TIMEOUT 9000
#define SACN_PDU_MAGIC "ASC-E1.17\0\0\0"
@@ -31,10 +36,12 @@ typedef struct /*_sacn_universe_model*/ {
uint8_t in[512];
uint8_t out[512];
uint16_t map[512];
+ channel channel[512];
} sacn_universe;
typedef struct /*_sacn_instance_model*/ {
uint16_t uni;
+ uint8_t realtime;
uint8_t xmit_prio;
uint8_t cid_filter[16];
uint8_t filter_enabled;
@@ -54,11 +61,16 @@ typedef union /*_sacn_instance_id*/ {
uint64_t label;
} sacn_instance_id;
+typedef struct /*_sacn_output_universe*/ {
+ uint16_t universe;
+ uint64_t last_frame;
+ uint8_t mark;
+} sacn_output_universe;
+
typedef struct /*_sacn_socket*/ {
int fd;
size_t universes;
- uint16_t* universe;
- uint64_t* last_frame;
+ sacn_output_universe* universe;
} sacn_fd;
#pragma pack(push, 1)
diff --git a/backends/sacn.md b/backends/sacn.md
index f5f1db4..598f430 100644
--- a/backends/sacn.md
+++ b/backends/sacn.md
@@ -26,6 +26,7 @@ This has the side effect of mirroring the output of instances on those descripto
| `destination` | `10.2.2.2` | Universe multicast | Destination address for unicast output. If unset, the multicast destination for the specified universe is used. |
| `from` | `0xAA 0xBB` ... | none | 16-byte input source CID filter. Setting this option filters the input stream for this universe. |
| `unicast` | `1` | `0` | Prevent this instance from joining its universe multicast group |
+| `realtime` | `1` | `0` | Disable the recommended rate-limiting (approx. 44 packets per second) for this instance |
Note that instances accepting multicast input also process unicast frames directed at them, while
instances in `unicast` mode will not receive multicast frames.
@@ -50,9 +51,6 @@ A normal channel that is part of a wide channel can not be mapped individually.
The DMX start code of transmitted and received universes is fixed as `0`.
-The (upper) limit on packet transmission rate mandated by section 6.6.1 of the sACN specification is disregarded.
-The rate of packet transmission is influenced by the rate of incoming mapped events on the instance.
-
Universe synchronization is currently not supported, though this feature may be implemented in the future.
To use multicast input, all networking hardware in the path must support the IGMPv2 protocol.
diff --git a/backends/winmidi.c b/backends/winmidi.c
index 0722ca2..d9b3047 100644
--- a/backends/winmidi.c
+++ b/backends/winmidi.c
@@ -95,19 +95,14 @@ static int winmidi_configure_instance(instance* inst, char* option, char* value)
return 1;
}
-static instance* winmidi_instance(){
- instance* i = mm_instance();
- if(!i){
- return NULL;
- }
-
- i->impl = calloc(1, sizeof(winmidi_instance_data));
- if(!i->impl){
+static int winmidi_instance(instance* inst){
+ inst->impl = calloc(1, sizeof(winmidi_instance_data));
+ if(!inst->impl){
LOG("Failed to allocate memory");
- return NULL;
+ return 1;
}
- return i;
+ return 0;
}
static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags){
@@ -122,7 +117,7 @@ static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags){
next_token = spec + 7;
}
}
-
+
if(!next_token){
LOGPF("Invalid channel specification %s", spec);
return NULL;
@@ -218,7 +213,7 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v
midiOutShortMsg(data->device_out, output.dword);
}
-
+
return 0;
}
@@ -263,7 +258,7 @@ static int winmidi_handle(size_t num, managed_fd* fds){
backend_config.event[u].inst->name,
backend_config.event[u].channel.fields.channel,
winmidi_type_name(backend_config.event[u].channel.fields.type),
- backend_config.event[u].value);
+ backend_config.event[u].value.normalised);
}
else{
LOGPF("Incoming data on channel %s.ch%d.%s%d, value %f",
@@ -271,7 +266,7 @@ static int winmidi_handle(size_t num, managed_fd* fds){
backend_config.event[u].channel.fields.channel,
winmidi_type_name(backend_config.event[u].channel.fields.type),
backend_config.event[u].channel.fields.control,
- backend_config.event[u].value);
+ backend_config.event[u].value.normalised);
}
}
chan = mm_channel(backend_config.event[u].inst, backend_config.event[u].channel.label, 0);
@@ -315,7 +310,7 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW
ident.fields.type = input.components.status & 0xF0;
ident.fields.control = input.components.data1;
val.normalised = (double) input.components.data2 / 127.0;
-
+
if(ident.fields.type == 0x80){
ident.fields.type = note;
val.normalised = 0;
@@ -340,7 +335,6 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW
case MIM_CLOSE:
//device opened/closed
return;
-
}
DBGPF("Incoming message type %d channel %d control %d value %f",
@@ -396,7 +390,7 @@ static int winmidi_match_input(char* prefix){
for(n = 0; n < inputs; n++){
midiInGetDevCaps(n, &input_caps, sizeof(MIDIINCAPS));
if(!prefix){
- printf("\tID %d: %s", n, input_caps.szPname);
+ LOGPF("\tID %d: %s", n, input_caps.szPname);
}
else if(!strncmp(input_caps.szPname, prefix, strlen(prefix))){
LOGPF("Selected input device %s (ID %" PRIsize_t ") for name %s", input_caps.szPname, n, prefix);
@@ -429,7 +423,7 @@ static int winmidi_match_output(char* prefix){
for(n = 0; n < outputs; n++){
midiOutGetDevCaps(n, &output_caps, sizeof(MIDIOUTCAPS));
if(!prefix){
- printf("\tID %d: %s", n, output_caps.szPname);
+ LOGPF("\tID %d: %s", n, output_caps.szPname);
}
else if(!strncmp(output_caps.szPname, prefix, strlen(prefix))){
LOGPF("Selected output device %s (ID %" PRIsize_t " for name %s", output_caps.szPname, n, prefix);
@@ -440,32 +434,22 @@ static int winmidi_match_output(char* prefix){
return -1;
}
-static int winmidi_start(size_t n, instance** inst){
- size_t p;
- int device, rv = -1;
- winmidi_instance_data* data = NULL;
+static int winmidi_socket_pair(int* fds){
+ //this really should be a size_t but getsockname specifies int* for some reason
+ int sockadd_len = sizeof(struct sockaddr_storage);
+ char* error = NULL;
struct sockaddr_storage sockadd = {
0
};
- //this really should be a size_t but getsockname specifies int* for some reason
- int sockadd_len = sizeof(sockadd);
- char* error = NULL;
- DBGPF("Main thread ID is %ld", GetCurrentThreadId());
- //output device list if requested
- if(backend_config.list_devices){
- winmidi_match_input(NULL);
- winmidi_match_output(NULL);
- }
-
- //open the feedback sockets
//for some reason the feedback connection fails to work on 'real' windows with ipv6
- backend_config.socket_pair[0] = mmbackend_socket("127.0.0.1", "0", SOCK_DGRAM, 1, 0);
- if(backend_config.socket_pair[0] < 0){
+ fds[0] = mmbackend_socket("127.0.0.1", "0", SOCK_DGRAM, 1, 0);
+ if(fds[0] < 0){
LOG("Failed to open feedback socket");
return 1;
}
- if(getsockname(backend_config.socket_pair[0], (struct sockaddr*) &sockadd, &sockadd_len)){
+
+ if(getsockname(fds[0], (struct sockaddr*) &sockadd, &sockadd_len)){
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 query feedback socket information: %s", error);
@@ -488,8 +472,8 @@ static int winmidi_start(size_t n, instance** inst){
return 1;
}
DBGPF("Feedback socket family %d port %d", sockadd.ss_family, be16toh(((struct sockaddr_in*)&sockadd)->sin_port));
- backend_config.socket_pair[1] = socket(sockadd.ss_family, SOCK_DGRAM, IPPROTO_UDP);
- if(backend_config.socket_pair[1] < 0 || connect(backend_config.socket_pair[1], (struct sockaddr*) &sockadd, sockadd_len)){
+ fds[1] = socket(sockadd.ss_family, SOCK_DGRAM, IPPROTO_UDP);
+ if(fds[1] < 0 || connect(backend_config.socket_pair[1], (struct sockaddr*) &sockadd, sockadd_len)){
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 connect to feedback socket: %s", error);
@@ -497,6 +481,26 @@ static int winmidi_start(size_t n, instance** inst){
return 1;
}
+ return 0;
+}
+
+static int winmidi_start(size_t n, instance** inst){
+ size_t p;
+ int device, rv = -1;
+ winmidi_instance_data* data = NULL;
+ DBGPF("Main thread ID is %ld", GetCurrentThreadId());
+
+ //output device list if requested
+ if(backend_config.list_devices){
+ winmidi_match_input(NULL);
+ winmidi_match_output(NULL);
+ }
+
+ //open the feedback sockets
+ if(winmidi_socket_pair(backend_config.socket_pair)){
+ return 1;
+ }
+
//set up instances and start input
for(p = 0; p < n; p++){
data = (winmidi_instance_data*) inst[p]->impl;
diff --git a/backends/winmidi.h b/backends/winmidi.h
index 81e7439..4c740ea 100644
--- a/backends/winmidi.h
+++ b/backends/winmidi.h
@@ -3,7 +3,7 @@
MM_PLUGIN_API int init();
static int winmidi_configure(char* option, char* value);
static int winmidi_configure_instance(instance* inst, char* option, char* value);
-static instance* winmidi_instance();
+static int winmidi_instance(instance* inst);
static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags);
static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v);
static int winmidi_handle(size_t num, managed_fd* fds);