aboutsummaryrefslogtreecommitdiffhomepage
path: root/backends
diff options
context:
space:
mode:
authorcbdev <cb@cbcdn.com>2020-04-27 22:50:47 +0200
committercbdev <cb@cbcdn.com>2020-04-27 22:50:47 +0200
commit1d8ed32bf769bc99095cd32ec166e681437607f0 (patch)
treebc94fd3a0245e91c15659916fc1c3591c7fb44b7 /backends
parentd4714dfdd5c6e2d165d1ad9327fdce69a1b2b85b (diff)
parentc0bb55ff08faf2f89af947090d1c9bc412927d9f (diff)
downloadmidimonster-1d8ed32bf769bc99095cd32ec166e681437607f0.tar.gz
midimonster-1d8ed32bf769bc99095cd32ec166e681437607f0.tar.bz2
midimonster-1d8ed32bf769bc99095cd32ec166e681437607f0.zip
Merge branch 'master' into debian/master
Diffstat (limited to 'backends')
-rw-r--r--backends/Makefile10
-rw-r--r--backends/artnet.c87
-rw-r--r--backends/artnet.h3
-rw-r--r--backends/artnet.md2
-rw-r--r--backends/evdev.c8
-rw-r--r--backends/jack.c56
-rw-r--r--backends/libmmbackend.c94
-rw-r--r--backends/libmmbackend.h31
-rw-r--r--backends/lua.c321
-rw-r--r--backends/lua.h23
-rw-r--r--backends/lua.md78
-rw-r--r--backends/maweb.c137
-rw-r--r--backends/maweb.h7
-rw-r--r--backends/maweb.md12
-rw-r--r--backends/midi.c132
-rw-r--r--backends/midi.md5
-rw-r--r--backends/openpixelcontrol.c16
-rw-r--r--backends/osc.c125
-rw-r--r--backends/python.c202
-rw-r--r--backends/python.h5
-rw-r--r--backends/python.md27
-rw-r--r--backends/rtpmidi.c1790
-rw-r--r--backends/rtpmidi.h182
-rw-r--r--backends/rtpmidi.md97
-rw-r--r--backends/sacn.c104
-rw-r--r--backends/sacn.h3
-rw-r--r--backends/winmidi.c62
27 files changed, 3116 insertions, 503 deletions
diff --git a/backends/Makefile b/backends/Makefile
index e31ff24..700c9b3 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 openpixelcontrol.dll
-BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.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.so
OPTIONAL_BACKENDS = ola.so
BACKEND_LIB = libmmbackend.o
@@ -46,6 +46,10 @@ maweb.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
maweb.dll: LDLIBS += -lws2_32
maweb.dll: CFLAGS += -DMAWEB_NO_LIBSSL
+rtpmidi.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
+rtpmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
+rtpmidi.dll: LDLIBS += -lws2_32 -liphlpapi
+
winmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
winmidi.dll: LDLIBS += -lwinmm -lws2_32
@@ -61,7 +65,7 @@ ola.so: CPPFLAGS += -Wno-write-strings
lua.so: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"")
lua.so: LDLIBS += $(shell pkg-config --libs lua53 || pkg-config --libs lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"")
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
+lua.dll: LDLIBS += -L../ -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\"")
diff --git a/backends/artnet.c b/backends/artnet.c
index 9fac332..e07ea52 100644
--- a/backends/artnet.c
+++ b/backends/artnet.c
@@ -21,7 +21,7 @@ static int artnet_listener(char* host, char* port){
return -1;
}
- fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1);
+ fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1, 1);
if(fd < 0){
return -1;
}
@@ -104,12 +104,18 @@ static int artnet_configure(char* option, char* value){
static int artnet_instance(instance* inst){
artnet_instance_data* data = calloc(1, sizeof(artnet_instance_data));
+ size_t u;
+
if(!data){
LOG("Failed to allocate memory");
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 0;
@@ -164,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);
@@ -192,13 +203,13 @@ 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){
- size_t u;
+static int artnet_transmit(instance* inst, artnet_output_universe* output){
artnet_instance_data* data = (artnet_instance_data*) inst->impl;
- //output frame
+
+ //build output frame
artnet_pkt frame = {
.magic = {'A', 'r', 't', '-', 'N', 'e', 't', 0x00},
.opcode = htobe16(OpDmx),
@@ -213,22 +224,31 @@ static int artnet_transmit(instance* inst){
memcpy(frame.data, data->data.out, 512);
if(sendto(artnet_fd[data->fd_index].fd, (uint8_t*) &frame, sizeof(frame), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){
- LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno));
+ #ifdef _WIN32
+ if(WSAGetLastError() != WSAEWOULDBLOCK){
+ #else
+ if(errno != EAGAIN){
+ #endif
+ LOGPF("Failed to output frame for instance %s: %s", inst->name, mmbackend_socket_strerror(errno));
+ return 1;
+ }
+ //reschedule frame output
+ output->mark = 1;
+ if(!next_frame || next_frame > ARTNET_SYNTHESIZE_MARGIN){
+ next_frame = ARTNET_SYNTHESIZE_MARGIN;
+ }
+ return 0;
}
//update last frame timestamp
- for(u = 0; u < artnet_fd[data->fd_index].output_instances; u++){
- if(artnet_fd[data->fd_index].output_instance[u].label == inst->ident){
- artnet_fd[data->fd_index].output_instance[u].last_frame = mm_timestamp();
- artnet_fd[data->fd_index].output_instance[u].mark = 0;
- }
- }
+ output->last_frame = mm_timestamp();
+ output->mark = 0;
return 0;
}
static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v){
uint32_t frame_delta = 0;
- size_t u, mark = 0;
+ size_t u, mark = 0, channel_offset = 0;
artnet_instance_data* data = (artnet_instance_data*) inst->impl;
if(!data->dest_len){
@@ -237,22 +257,23 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v)
}
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;
}
}
@@ -268,12 +289,12 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v)
//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_KEEPALIVE_INTERVAL - frame_delta)){
- next_frame = (ARTNET_KEEPALIVE_INTERVAL - frame_delta);
+ if(!next_frame || next_frame > (ARTNET_FRAME_TIMEOUT - frame_delta)){
+ next_frame = (ARTNET_FRAME_TIMEOUT - frame_delta);
}
return 0;
}
- return artnet_transmit(inst);
+ return artnet_transmit(inst, artnet_fd[data->fd_index].output_instance + u);
}
return 0;
@@ -304,16 +325,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])){
@@ -361,7 +375,7 @@ static int artnet_handle(size_t num, managed_fd* fds){
|| synthesize_delta >= ARTNET_KEEPALIVE_INTERVAL){ //keepalive timeout
inst = mm_instance_find(BACKEND_NAME, artnet_fd[u].output_instance[c].label);
if(inst){
- artnet_transmit(inst);
+ artnet_transmit(inst, artnet_fd[u].output_instance + c);
}
}
@@ -373,11 +387,6 @@ static int artnet_handle(size_t num, managed_fd* fds){
}
}
- if(!num){
- //early exit
- return 0;
- }
-
for(u = 0; u < num; u++){
do{
bytes_read = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0);
@@ -400,7 +409,7 @@ static int artnet_handle(size_t num, managed_fd* fds){
#else
if(bytes_read < 0 && errno != EAGAIN){
#endif
- LOGPF("Failed to receive data: %s", strerror(errno));
+ LOGPF("Failed to receive data: %s", mmbackend_socket_strerror(errno));
}
if(bytes_read == 0){
diff --git a/backends/artnet.h b/backends/artnet.h
index d83999d..a517aa0 100644
--- a/backends/artnet.h
+++ b/backends/artnet.h
@@ -20,7 +20,7 @@ static int artnet_shutdown(size_t n, instance** inst);
#define ARTNET_KEEPALIVE_INTERVAL 1000
//limit transmit rate to at most 44 packets per second (1000/44 ~= 22)
-#define ARTNET_FRAME_TIMEOUT 15
+#define ARTNET_FRAME_TIMEOUT 20
#define ARTNET_SYNTHESIZE_MARGIN 10
#define MAP_COARSE 0x0200
@@ -37,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*/ {
diff --git a/backends/artnet.md b/backends/artnet.md
index 7e1ecff..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 |
diff --git a/backends/evdev.c b/backends/evdev.c
index af5ec74..8a14200 100644
--- a/backends/evdev.c
+++ b/backends/evdev.c
@@ -357,10 +357,6 @@ static int evdev_handle(size_t num, managed_fd* fds){
int read_status;
struct input_event ev;
- if(!num){
- return 0;
- }
-
for(fd = 0; fd < num; fd++){
inst = (instance*) fds[fd].impl;
if(!inst){
@@ -437,10 +433,6 @@ static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v)
int32_t value = 0;
uint64_t range = 0;
- if(!num){
- return 0;
- }
-
if(!data->output_enabled){
LOGPF("Instance %s not enabled for output (%" PRIsize_t " channel events)", inst->name, num);
return 0;
diff --git a/backends/jack.c b/backends/jack.c
index c862096..c84ed0f 100644
--- a/backends/jack.c
+++ b/backends/jack.c
@@ -205,7 +205,9 @@ static int mmjack_process(jack_nframes_t nframes, void* instp){
//notify the main thread
if(mark){
DBGPF("Notifying handler thread for instance %s", inst->name);
- send(data->fd, "c", 1, 0);
+ if(send(data->fd, "c", 1, 0) != 1){
+ DBGPF("Failed to notify main thread on %s", inst->name);
+ }
}
return rv;
}
@@ -549,38 +551,36 @@ static int mmjack_handle(size_t num, managed_fd* fds){
ssize_t bytes;
uint8_t recv_buf[1024];
- if(num){
- for(u = 0; u < num; u++){
- inst = (instance*) fds[u].impl;
- data = (mmjack_instance_data*) inst->impl;
- bytes = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0);
- if(bytes < 0){
- LOGPF("Failed to receive on feedback socket for instance %s", inst->name);
- return 1;
- }
+ for(u = 0; u < num; u++){
+ inst = (instance*) fds[u].impl;
+ data = (mmjack_instance_data*) inst->impl;
+ bytes = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0);
+ if(bytes < 0){
+ LOGPF("Failed to receive on feedback socket for instance %s", inst->name);
+ return 1;
+ }
- for(p = 0; p < data->ports; p++){
- if(data->port[p].input && data->port[p].mark){
- pthread_mutex_lock(&data->port[p].lock);
- switch(data->port[p].type){
- case port_cv:
- mmjack_handle_cv(inst, p, data->port + p);
- break;
- case port_midi:
- mmjack_handle_midi(inst, p, data->port + p);
- break;
- default:
- LOGPF("Output handler not implemented for unknown channel type on %s.%s", inst->name, data->port[p].name);
- break;
- }
-
- data->port[p].mark = 0;
- pthread_mutex_unlock(&data->port[p].lock);
+ for(p = 0; p < data->ports; p++){
+ if(data->port[p].input && data->port[p].mark){
+ pthread_mutex_lock(&data->port[p].lock);
+ switch(data->port[p].type){
+ case port_cv:
+ mmjack_handle_cv(inst, p, data->port + p);
+ break;
+ case port_midi:
+ mmjack_handle_midi(inst, p, data->port + p);
+ break;
+ default:
+ LOGPF("Output handler not implemented for unknown channel type on %s.%s", inst->name, data->port[p].name);
+ break;
}
+
+ data->port[p].mark = 0;
+ pthread_mutex_unlock(&data->port[p].lock);
}
}
}
-
+
if(config.jack_shutdown){
LOG("Server disconnected");
return 1;
diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c
index b9513ac..bad048c 100644
--- a/backends/libmmbackend.c
+++ b/backends/libmmbackend.c
@@ -1,6 +1,77 @@
#include "libmmbackend.h"
#define LOGPF(format, ...) fprintf(stderr, "libmmbe\t" format "\n", __VA_ARGS__)
+#define LOG(message) fprintf(stderr, "libmmbe\t%s\n", (message))
+
+int mmbackend_strdup(char** dest, char* src){
+ if(*dest){
+ free(*dest);
+ }
+
+ *dest = strdup(src);
+
+ if(!*dest){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+ return 0;
+}
+
+char* mmbackend_socket_strerror(int err_no){
+ #ifdef _WIN32
+ static char error[2048] = "";
+ ssize_t u;
+ FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, WSAGetLastError(),
+ MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), error, sizeof(error), NULL);
+ //remove trailing newline that for some reason is included in most of these...
+ for(u = strlen(error) - 1; u > 0; u--){
+ if(!isprint(error[u])){
+ error[u] = 0;
+ }
+ }
+ return error;
+ #else
+ return strerror(err_no);
+ #endif
+}
+
+const char* mmbackend_sockaddr_ntop(struct sockaddr* peer, char* buffer, size_t length){
+ union {
+ struct sockaddr* in;
+ struct sockaddr_in* in4;
+ struct sockaddr_in6* in6;
+ } addr;
+ addr.in = peer;
+ #ifdef _WIN32
+ uint8_t* data = NULL;
+ #endif
+
+ switch(addr.in->sa_family){
+ //inet_ntop has become available in the winapi with vista, but eh.
+ #ifdef _WIN32
+ case AF_INET6:
+ data = addr.in6->sin6_addr.s6_addr;
+ snprintf(buffer, length, "%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X",
+ data[0], data[1], data[2], data[3],
+ data[4], data[5], data[6], data[7],
+ data[8], data[9], data[10], data[11],
+ data[12], data[13], data[14], data[15]);
+ return buffer;
+ case AF_INET:
+ data = (uint8_t*) &(addr.in4->sin_addr.s_addr);
+ snprintf(buffer, length, "%d.%d.%d.%d", data[0], data[1], data[2], data[3]);
+ return buffer;
+ #else
+ case AF_INET6:
+ return inet_ntop(addr.in->sa_family, &(addr.in6->sin6_addr), buffer, length);
+ case AF_INET:
+ return inet_ntop(addr.in->sa_family, &(addr.in4->sin_addr), buffer, length);
+ #endif
+ default:
+ snprintf(buffer, length, "Socket family not implemented");
+ return buffer;
+ }
+}
void mmbackend_parse_hostspec(char* spec, char** host, char** port, char** options){
size_t u = 0;
@@ -67,7 +138,7 @@ int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* ad
return 0;
}
-int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast){
+int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast, uint8_t dualstack){
int fd = -1, status, yes = 1;
struct addrinfo hints = {
.ai_family = AF_UNSPEC,
@@ -91,19 +162,24 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin
//set required socket options
yes = 1;
- if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){
- LOGPF("Failed to enable SO_REUSEADDR on socket: %s", strerror(errno));
+ if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*) &yes, sizeof(yes)) < 0){
+ LOGPF("Failed to enable SO_REUSEADDR on socket: %s", mmbackend_socket_strerror(errno));
+ }
+
+ yes = dualstack ? 0 : 1;
+ if(addr_it->ai_family == AF_INET6 && setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (void*) &yes, sizeof(yes)) < 0){
+ LOGPF("Failed to %s dualstack operations on socket: %s", dualstack ? "enable" : "disable", mmbackend_socket_strerror(errno));
}
if(mcast){
yes = 1;
- if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){
- LOGPF("Failed to enable SO_BROADCAST on socket: %s", strerror(errno));
+ if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*) &yes, sizeof(yes)) < 0){
+ LOGPF("Failed to enable SO_BROADCAST on socket: %s", mmbackend_socket_strerror(errno));
}
yes = 0;
- if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){
- LOGPF("Failed to disable IP_MULTICAST_LOOP on socket: %s", strerror(errno));
+ if(setsockopt(fd, addr_it->ai_family == AF_INET ? IPPROTO_IP : IPPROTO_IPV6, addr_it->ai_family == AF_INET ? IP_MULTICAST_LOOP : IPV6_MULTICAST_LOOP, (void*) &yes, sizeof(yes)) < 0){
+ LOGPF("Failed to disable IP_MULTICAST_LOOP on socket: %s", mmbackend_socket_strerror(errno));
}
}
@@ -141,7 +217,7 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin
#else
int flags = fcntl(fd, F_GETFL, 0);
if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){
- LOGPF("Failed to set socket nonblocking: %s", strerror(errno));
+ LOGPF("Failed to set socket nonblocking: %s", mmbackend_socket_strerror(errno));
close(fd);
return -1;
}
@@ -159,7 +235,7 @@ int mmbackend_send(int fd, uint8_t* data, size_t length){
sent = send(fd, data + total, 1, 0);
#endif
if(sent < 0){
- LOGPF("Failed to send: %s", strerror(errno));
+ LOGPF("Failed to send: %s", mmbackend_socket_strerror(errno));
return 1;
}
total += sent;
diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h
index aa0d0f0..1f0b4d7 100644
--- a/backends/libmmbackend.h
+++ b/backends/libmmbackend.h
@@ -5,6 +5,7 @@
#include <ws2tcpip.h>
//#define close closesocket
#else
+#include <arpa/inet.h>
#include <sys/socket.h>
#include <netdb.h>
#endif
@@ -18,6 +19,34 @@
/*** BACKEND IMPLEMENTATION LIBRARY ***/
+/** Convenience functions **/
+
+/*
+ * Duplicate src into *dest, freeing earlier content of *dest if present
+ * On success, 0 is returned
+ * On failure, a message is printed, *dest is a NULL pointer and 1 is returned
+ */
+int mmbackend_strdup(char** dest, char* src);
+
+/*
+ * Return a formatted error message pertaining to the last socket operation.
+ * On Linux/OSX, this calls through to strerror using the provided err_no.
+ * On Windows, err_no is ignored and WSAGetLastError is called to retrieve
+ * the status of the last operation. This information is then processed via
+ * FormatMessage into a fixed buffer, which is returned. Thus, this function
+ * is not thread-safe on Windows. On Linux, refer to strerror's documentation
+ * for information on thread-safety.
+ */
+char* mmbackend_socket_strerror(int err_no);
+
+/*
+ * Wrap / reimplement (on Windows) inet_ntop to work with struct sockaddr* directly.
+ * Prints the address in a "human-readable" form into buffer.
+ * Will modify at most length bytes into buffer, output will be zero-terminated.
+ * This function only works with AF_INET and AF_INET6 addresses.
+ */
+const char* mmbackend_sockaddr_ntop(struct sockaddr* peer, char* buffer, size_t length);
+
/** Networking functions **/
/*
@@ -43,7 +72,7 @@ int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* ad
* Create a socket of given type and mode for a bind / connect host.
* Returns -1 on failure, a valid file descriptor for the socket on success.
*/
-int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast);
+int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast, uint8_t dualstack);
/*
* Send arbitrary data over multiple writes if necessary
diff --git a/backends/lua.c b/backends/lua.c
index e7ba9f9..98ce369 100644
--- a/backends/lua.c
+++ b/backends/lua.c
@@ -10,15 +10,18 @@
#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 = {
@@ -86,6 +89,12 @@ static int lua_update_timerfd(){
interval = timer[n].interval;
}
}
+
+ for(n = 0; n < threads; n++){
+ if(thread[n].timeout && (!interval || thread[n].timeout < interval)){
+ interval = thread[n].timeout;
+ }
+ }
DBGPF("Recalculating timers, minimum is %" PRIu64, interval);
//calculate gcd of all timers if any are active
@@ -100,7 +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;
}
}
@@ -126,38 +136,131 @@ static int lua_update_timerfd(){
return 0;
}
+static void lua_thread_resume(size_t current_thread){
+ //push coroutine reference
+ lua_pushstring(thread[current_thread].thread, LUA_REGISTRY_CURRENT_THREAD);
+ lua_pushnumber(thread[current_thread].thread, current_thread);
+ lua_settable(thread[current_thread].thread, LUA_REGISTRYINDEX);
+
+ //call thread main
+ DBGPF("Resuming thread %" PRIsize_t " on %s", current_thread, thread[current_thread].instance->name);
+ if(lua_resume(thread[current_thread].thread, NULL, 0) != LUA_YIELD){
+ DBGPF("Thread %" PRIsize_t " on %s terminated", current_thread, thread[current_thread].instance->name);
+ thread[current_thread].timeout = 0;
+ }
+
+ //remove coroutine reference
+ lua_pushstring(thread[current_thread].thread, LUA_REGISTRY_CURRENT_THREAD);
+ lua_pushnil(thread[current_thread].thread);
+ lua_settable(thread[current_thread].thread, LUA_REGISTRYINDEX);
+}
+
+static instance* lua_fetch_instance(lua_State* interpreter){
+ instance* inst = NULL;
+
+ //get instance pointer from registry
+ lua_pushstring(interpreter, LUA_REGISTRY_KEY);
+ lua_gettable(interpreter, LUA_REGISTRYINDEX);
+ inst = (instance*) lua_touserdata(interpreter, -1);
+ lua_pop(interpreter, 1);
+ return inst;
+}
+
+static int lua_callback_thread(lua_State* interpreter){
+ instance* inst = lua_fetch_instance(interpreter);
+ size_t u = threads;
+ if(lua_gettop(interpreter) != 1){
+ LOGPF("Thread function called with %d arguments, expected function", lua_gettop(interpreter));
+ return 0;
+ }
+
+ luaL_checktype(interpreter, 1, LUA_TFUNCTION);
+
+ //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;
+ instance* inst = lua_fetch_instance(interpreter);
+ lua_instance_data* data = (lua_instance_data*) inst->impl;
if(lua_gettop(interpreter) != 2){
LOGPF("Output function called with %d arguments, expected 2 (string, number)", lua_gettop(interpreter));
return 0;
}
- //get instance pointer from registry
- lua_pushstring(interpreter, LUA_REGISTRY_KEY);
- lua_gettable(interpreter, LUA_REGISTRYINDEX);
- inst = (instance*) lua_touserdata(interpreter, -1);
- data = (lua_instance_data*) inst->impl;
-
//fetch function parameters
channel_name = lua_tostring(interpreter, 1);
+ if(!channel_name){
+ LOG("Output function called with invalid channel specification");
+ return 0;
+ }
+
val.normalised = clamp(luaL_checknumber(interpreter, 2), 1.0, 0.0);
+ //if not started yet, create any requested channels so scripts may set them at load time
+ if(!last_timestamp){
+ 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;
}
}
@@ -166,6 +269,31 @@ static int lua_callback_output(lua_State* interpreter){
return 0;
}
+static int lua_callback_cleanup_handler(lua_State* interpreter){
+ instance* inst = lua_fetch_instance(interpreter);
+ lua_instance_data* data = (lua_instance_data*) inst->impl;
+ int current_handler = data->cleanup_handler;
+
+ if(lua_gettop(interpreter) != 1){
+ LOGPF("Cleanup handler function called with %d arguments, expected 1 (function)", lua_gettop(interpreter));
+ return 0;
+ }
+
+ if(lua_type(interpreter, 1) != LUA_TFUNCTION && lua_type(interpreter, 1) != LUA_TNIL){
+ LOG("Cleanup handler function parameter was neither nil nor a function");
+ return 0;
+ }
+
+ data->cleanup_handler = luaL_ref(interpreter, LUA_REGISTRYINDEX);
+ if(current_handler == LUA_NOREF || current_handler == LUA_REFNIL){
+ lua_pushnil(interpreter);
+ return 1;
+ }
+ lua_rawgeti(interpreter, LUA_REGISTRYINDEX, current_handler);
+ luaL_unref(interpreter, LUA_REGISTRYINDEX, current_handler);
+ return 1;
+}
+
static int lua_callback_interval(lua_State* interpreter){
size_t n = 0;
uint64_t interval = 0;
@@ -176,10 +304,6 @@ static int lua_callback_interval(lua_State* interpreter){
return 0;
}
- //get instance pointer from registry
- lua_pushstring(interpreter, LUA_REGISTRY_KEY);
- lua_gettable(interpreter, LUA_REGISTRYINDEX);
-
//fetch and round the interval
interval = luaL_checkinteger(interpreter, 2);
if(interval % 10 < 5){
@@ -243,28 +367,26 @@ static int lua_callback_interval(lua_State* interpreter){
static int lua_callback_value(lua_State* interpreter, uint8_t input){
size_t n = 0;
- instance* inst = NULL;
- lua_instance_data* data = NULL;
const char* channel_name = NULL;
+ instance* inst = lua_fetch_instance(interpreter);
+ lua_instance_data* data = (lua_instance_data*) inst->impl;
if(lua_gettop(interpreter) != 1){
LOGPF("get_value function called with %d arguments, expected 1 (string)", lua_gettop(interpreter));
return 0;
}
- //get instance pointer from registry
- lua_pushstring(interpreter, LUA_REGISTRY_KEY);
- lua_gettable(interpreter, LUA_REGISTRYINDEX);
- inst = (instance*) lua_touserdata(interpreter, -1);
- data = (lua_instance_data*) inst->impl;
-
//fetch argument
channel_name = lua_tostring(interpreter, 1);
+ if(!channel_name){
+ LOG("get_value function called with invalid channel specification");
+ return 0;
+ }
//find correct channel & return value
for(n = 0; n < data->channels; n++){
- if(!strcmp(channel_name, data->channel_name[n])){
- lua_pushnumber(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;
}
}
@@ -308,6 +430,11 @@ static int lua_configure_instance(instance* inst, char* option, char* value){
}
return 0;
}
+ else if(!strcmp(option, "default-handler")){
+ free(data->default_handler);
+ data->default_handler = strdup(value);
+ return 0;
+ }
LOGPF("Unknown instance configuration parameter %s for instance %s", option, inst->name);
return 1;
@@ -322,6 +449,7 @@ static int lua_instance(instance* inst){
//load the interpreter
data->interpreter = luaL_newstate();
+ data->cleanup_handler = LUA_NOREF;
if(!data->interpreter){
LOG("Failed to initialize interpreter");
free(data);
@@ -336,6 +464,9 @@ static int lua_instance(instance* inst){
lua_register(data->interpreter, "output_value", lua_callback_output_value);
lua_register(data->interpreter, "input_channel", lua_callback_input_channel);
lua_register(data->interpreter, "timestamp", lua_callback_timestamp);
+ lua_register(data->interpreter, "thread", lua_callback_thread);
+ lua_register(data->interpreter, "sleep", lua_callback_sleep);
+ lua_register(data->interpreter, "cleanup_handler", lua_callback_cleanup_handler);
//store instance pointer to the lua state
lua_pushstring(data->interpreter, LUA_REGISTRY_KEY);
@@ -352,26 +483,24 @@ 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]){
+ //initialize new channel
+ memset(data->channel + u, 0, sizeof(lua_channel_data));
+ data->channel[u].name = strdup(spec);
+ if(!data->channel[u].name){
LOG("Failed to allocate memory");
return NULL;
}
@@ -382,26 +511,28 @@ 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;
//call lua channel handlers if present
- if(data->reference[c[n]->ident] != LUA_NOREF){
+ 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_name[c[n]->ident]);
+ lua_pushstring(data->interpreter, data->channel[ident].name);
lua_settable(data->interpreter, LUA_REGISTRYINDEX);
- lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->reference[c[n]->ident]);
+ 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);
}
}
+ //update the channel input value after the handler call, so we can use both values there
+ data->channel[ident].in = v[n].normalised;
}
//clear the channel name
@@ -412,7 +543,8 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){
}
static int lua_handle(size_t num, managed_fd* fds){
- uint64_t delta = timer_interval;
+ uint64_t delta = mm_timestamp() - last_timestamp;
+ last_timestamp = mm_timestamp();
size_t n;
#ifdef MMBACKEND_LUA_TIMERFD
@@ -426,9 +558,6 @@ static int lua_handle(size_t num, managed_fd* fds){
LOGPF("Failed to read timer: %s", strerror(errno));
return 1;
}
- #else
- delta = mm_timestamp() - last_timestamp;
- last_timestamp = mm_timestamp();
#endif
//no timers active
@@ -449,29 +578,72 @@ static int lua_handle(size_t num, managed_fd* fds){
}
}
}
+
+ //check for threads to wake up
+ for(n = 0; n < threads; n++){
+ if(thread[n].timeout && delta >= thread[n].timeout){
+ lua_thread_resume(n);
+ lua_update_timerfd();
+ }
+ else if(thread[n].timeout){
+ thread[n].timeout -= delta;
+ }
+ }
return 0;
}
+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, "cleanup_handler")
+ || !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], "input_channel")
- && strcmp(data->channel_name[p], "timestamp")
- && strcmp(data->channel_name[p], "interval")){
- lua_getglobal(data->interpreter, data->channel_name[p]);
- data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX);
- 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);
}
}
}
@@ -482,9 +654,8 @@ static int lua_start(size_t n, instance** inst){
if(mm_manage_fd(timer_fd, BACKEND_NAME, 1, NULL)){
return 1;
}
- #else
- last_timestamp = mm_timestamp();
#endif
+ last_timestamp = mm_timestamp();
return 0;
}
@@ -494,16 +665,21 @@ static int lua_shutdown(size_t n, instance** inst){
for(u = 0; u < n; u++){
data = (lua_instance_data*) inst[u]->impl;
+
+ //call cleanup function if one is registered
+ if(data->cleanup_handler != LUA_NOREF && data->cleanup_handler != LUA_REFNIL){
+ lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->cleanup_handler);
+ lua_pcall(data->interpreter, 0, 0, 0);
+ }
+
//stop the interpreter
lua_close(data->interpreter);
//cleanup channel data
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);
}
@@ -511,6 +687,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 ebe2046..5587bf9 100644
--- a/backends/lua.h
+++ b/backends/lua.h
@@ -22,13 +22,21 @@ 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;
+ int cleanup_handler;
+ char* default_handler;
} lua_instance_data;
typedef struct /*_lua_interval_callback*/ {
@@ -37,3 +45,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 db4cf39..b2f40e0 100644
--- a/backends/lua.md
+++ b/backends/lua.md
@@ -3,48 +3,87 @@
The `lua` backend provides a flexible programming environment, allowing users to route, generate
and manipulate events using the Lua scripting language.
-Every instance has its own interpreter state which can be loaded with custom handler scripts.
+Every instance has its own interpreter state which can be loaded with custom 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
+which will receive events for all incoming channels may be supplied in the configuration.
+
+The backend can also call Lua functions repeatedly using a timer, allowing users to implement time-based
+functionality (such as evaluating a fixed mathematical function or outputting periodic updates).
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). 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 |
+| `output(string, number)` | `output("foo", 0.75)` | Output a value event to a channel on this instance |
+| `interval(function, number)` | `interval(update, 100)` | Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms). Calling `interval` on a Lua function multiple times updates the interval. Specifying `0` as interval stops periodic calls to the function. Do not call this function from within a Lua thread. |
+| `cleanup_handler(function)` | `cleanup_handler(shutdown)` | Register a function to be called when the instance is destroyed (on MIDIMonster shutdown). One cleanup handler can be registered per instance. Calling this function when the instance already has a cleanup handler registered replaces the handler, returning the old one. |
+| `input_value(string)` | `input_value("foo")` | Get the last input value on a channel on this instance |
+| `output_value(string)` | `output_value("bar")` | Get the last output value on a channel on this instance |
| `input_channel()` | `print(input_channel())` | Returns the name of the input channel whose handler function is currently running or `nil` if in an `interval`'ed function (or the initial parse step) |
| `timestamp()` | `print(timestamp())` | Returns the core timestamp for this iteration with millisecond resolution. This is not a performance timer, but intended for timeouting, etc |
+| `thread(function)` | `thread(run_show)` | Run a function as a Lua thread (see below) |
+| `sleep(number)` | `sleep(100)` | Suspend current thread for time specified in milliseconds |
+
+While a channel handler executes, calling `input_value` for that channel returns the previous value.
+The stored value is updated once the handler returns.
Example script:
-```
+```lua
+-- This function is called when there are incoming events on input channel `bar`
+-- It outputs half the input value on the channel `foo`
function bar(value)
- output("foo", value / 2)
+ output("foo", value / 2);
end
+-- This function is registered below to execute every second
+-- It toggles output channel `bar` every time it is called by storing the next state in the variable `step`
step = 0
function toggle()
- output("bar", step * 1.0)
+ output("bar", step * 1.0);
step = (step + 1) % 2;
end
+-- This function is registered below to run as a Lua thread
+-- It loops infinitely and toggles the output channel `narf` every second
+function run_show()
+ while(true) do
+ sleep(1000);
+ output("narf", 0);
+ sleep(1000);
+ output("narf", 1.0);
+ end
+end
+
+-- This function is registered below to be called when the MIDIMonster shuts down
+function save_values()
+ -- Store state to a file, for example
+end
+
+-- Register the functions
interval(toggle, 1000)
+thread(run_show)
+cleanup_handler(save_values)
```
Input values range between 0.0 and 1.0, output values are clamped to the same range.
+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 `script` options specified, which will all be read cumulatively.
@@ -59,20 +98,23 @@ lua1.foo > lua2.bar
#### Known bugs / problems
-Using any of the interface functions (`output`, `interval`, `input_value`, `output_value`, `input_channel`,
-`timestamp`) as an input channel name to a Lua instance will not call any handler functions.
-Using these names as arguments to the output and value interface functions works as intended.
+Using any of the interface functions (`output`, `interval`, etc.) as an input channel name to a
+Lua instance will not call any handler functions. Using these names as arguments to the output and
+value interface functions works as intended. When using a default handler, the default handler will
+be called.
Output values will not trigger corresponding input event handlers unless the channel is mapped
back in the MIDIMonster configuration. This is intentional.
+Output events generated from cleanup handlers called during shutdown will not be routed, as the core
+routing facility has already shut down at this point. There are no plans to change this behaviour.
+
To build (and run) the `lua` backend on Windows, a compiled version of the Lua 5.3 library is required.
For various reasons (legal, separations of concern, not wanting to ship binary data in the repository),
the MIDIMonster project can not provide this file within this repository.
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.
-
+Place this file in the project root directory and run `make lua.dll` inside the `backends/` directory
+to build the backend.
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 6861d75..97d4cea 100644
--- a/backends/maweb.c
+++ b/backends/maweb.c
@@ -163,19 +163,29 @@ static int maweb_configure_instance(instance* inst, char* option, char* value){
LOGPF("Invalid host specified for instance %s", inst->name);
return 1;
}
- free(data->host);
- data->host = strdup(host);
- free(data->port);
- data->port = NULL;
- if(port){
- data->port = strdup(port);
+
+ data->host = realloc(data->host, (data->hosts + 1) * sizeof(char*));
+ data->port = realloc(data->port, (data->hosts + 1) * sizeof(char*));
+
+ if(!data->host || !data->port){
+ LOG("Failed to allocate memory");
+ return 1;
}
+
+ data->host[data->hosts] = strdup(host);
+ data->port[data->hosts] = port ? strdup(port) : NULL;
+ if(!data->host[data->hosts] || (port && !data->port[data->hosts])){
+ LOG("Failed to allocate memory");
+ free(data->host[data->hosts]);
+ free(data->port[data->hosts]);
+ return 1;
+ }
+
+ data->hosts++;
return 0;
}
else if(!strcmp(option, "user")){
- free(data->user);
- data->user = strdup(value);
- return 0;
+ return mmbackend_strdup(&data->user, value);
}
else if(!strcmp(option, "password")){
#ifndef MAWEB_NO_LIBSSL
@@ -222,6 +232,7 @@ static int maweb_instance(instance* inst){
}
data->fd = -1;
+ data->state = ws_closed;
data->buffer = calloc(MAWEB_RECV_CHUNK, sizeof(uint8_t));
if(!data->buffer){
LOG("Failed to allocate memory");
@@ -340,6 +351,9 @@ static int maweb_send_frame(instance* inst, maweb_operation op, uint8_t* payload
if(mmbackend_send(data->fd, frame_header, header_bytes)
|| mmbackend_send(data->fd, payload, len)){
+ LOGPF("Failed to send on instance %s, assuming connection failure", inst->name);
+ data->state = ws_closed;
+ data->login = 0;
return 1;
}
@@ -556,7 +570,7 @@ static int maweb_request_playbacks(instance* inst){
item_types,
view,
data->session);
- rv |= maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer));
+ maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer));
DBGPF("Poll request: %s", xmit_buffer);
updates_inflight++;
}
@@ -629,19 +643,30 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le
}
static int maweb_connect(instance* inst){
+ int rv = 1;
maweb_instance_data* data = (maweb_instance_data*) inst->impl;
- if(!data->host){
- return 1;
+ if(!data->host || !data->host[data->next_host]){
+ LOGPF("Invalid host configuration on instance %s, host %" PRIsize_t, inst->name, data->next_host + 1);
+ goto bail;
}
//unregister old fd from core
if(data->fd >= 0){
mm_manage_fd(data->fd, BACKEND_NAME, 0, NULL);
+ close(data->fd);
+ data->fd = -1;
}
+ data->state = ws_closed;
+ data->login = 0;
+
+ LOGPF("Connecting to host %" PRIsize_t " of %" PRIsize_t " on %s", data->next_host + 1, data->hosts, inst->name);
+
+ data->fd = mmbackend_socket(data->host[data->next_host],
+ data->port[data->next_host] ? data->port[data->next_host] : MAWEB_DEFAULT_PORT,
+ SOCK_STREAM, 0, 0, 1);
- data->fd = mmbackend_socket(data->host, data->port ? data->port : MAWEB_DEFAULT_PORT, SOCK_STREAM, 0, 0);
if(data->fd < 0){
- return 1;
+ goto bail;
}
data->state = ws_new;
@@ -654,15 +679,20 @@ static int maweb_connect(instance* inst){
|| mmbackend_send_str(data->fd, "Sec-WebSocket-Key: rbEQrXMEvCm4ZUjkj6juBQ==\r\n")
|| mmbackend_send_str(data->fd, "\r\n")){
LOG("Failed to communicate with peer");
- return 1;
+ goto bail;
}
//register new fd
if(mm_manage_fd(data->fd, BACKEND_NAME, 1, (void*) inst)){
LOG("Failed to register FD");
- return 1;
+ goto bail;
}
- return 0;
+
+ rv = 0;
+bail:
+ data->next_host++;
+ data->next_host %= data->hosts;
+ return rv;
}
static ssize_t maweb_handle_lines(instance* inst, ssize_t bytes_read){
@@ -693,6 +723,19 @@ static ssize_t maweb_handle_lines(instance* inst, ssize_t bytes_read){
return data->offset + begin;
}
+static int maweb_establish(instance* inst){
+ maweb_instance_data* data = (maweb_instance_data*) inst->impl;
+ size_t start = data->next_host;
+
+ do{
+ if(!maweb_connect(inst)){
+ break;
+ }
+ } while(data->next_host != start);
+
+ return data->state != ws_closed ? 0 : 1;
+}
+
static ssize_t maweb_handle_ws(instance* inst, ssize_t bytes_read){
maweb_instance_data* data = (maweb_instance_data*) inst->impl;
size_t header_length = 2;
@@ -766,7 +809,7 @@ static int maweb_handle_fd(instance* inst){
data->buffer = realloc(data->buffer, (data->allocated + MAWEB_RECV_CHUNK) * sizeof(uint8_t));
if(!data->buffer){
LOG("Failed to allocate memory");
- return 1;
+ return -1;
}
data->allocated += MAWEB_RECV_CHUNK;
bytes_left += MAWEB_RECV_CHUNK;
@@ -774,14 +817,12 @@ static int maweb_handle_fd(instance* inst){
bytes_read = recv(data->fd, data->buffer + data->offset, bytes_left - 1, 0);
if(bytes_read < 0){
- LOGPF("Failed to receive: %s", strerror(errno));
- //TODO close, reopen
+ LOGPF("Failed to receive on %s: %s", inst->name, mmbackend_socket_strerror(errno));
return 1;
}
else if(bytes_read == 0){
- //client closed connection
- //TODO try to reopen
- return 0;
+ //client closed connection, try to reopen the connection
+ return 1;
}
do{
@@ -801,7 +842,6 @@ static int maweb_handle_fd(instance* inst){
if(bytes_handled < 0){
bytes_handled = data->offset + bytes_read;
data->offset = 0;
- //TODO close, reopen
LOG("Failed to handle incoming data");
return 1;
}
@@ -947,6 +987,12 @@ static int maweb_keepalive(){
snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"session\":%" PRIu64 "}", data->session);
maweb_send_frame(inst[u], ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer));
}
+ else if(data->state == ws_closed){
+ //try to reconnect to any remote
+ if(maweb_establish(inst[u])){
+ LOGPF("Failed to reconnect to any host on %s, will retry in %d seconds", inst[u]->name, MAWEB_CONNECTION_KEEPALIVE / 1000);
+ }
+ }
}
free(inst);
@@ -981,7 +1027,18 @@ static int maweb_handle(size_t num, managed_fd* fds){
int rv = 0;
for(n = 0; n < num; n++){
- rv |= maweb_handle_fd((instance*) fds[n].impl);
+ rv = maweb_handle_fd((instance*) fds[n].impl);
+ //try to reconnect soft failures
+ if(rv == 1 && maweb_establish((instance*) fds[n].impl)){
+ //keepalive will retry periodically
+ LOGPF("Failed to reconnect with any configured host on instance %s", ((instance*) fds[n].impl)->name);
+ }
+ else if(rv){
+ //propagate critical failures
+ return rv;
+ }
+ //errors handled
+ rv = 0;
}
//FIXME all keepalive processing allocates temporary buffers, this might an optimization target
@@ -1003,8 +1060,13 @@ static int maweb_start(size_t n, instance** inst){
maweb_instance_data* data = NULL;
for(u = 0; u < n; u++){
- //sort channels
data = (maweb_instance_data*) inst[u]->impl;
+ if(!data->hosts){
+ LOGPF("No hosts configured on instance %s", inst[u]->name);
+ return 1;
+ }
+
+ //sort channels
qsort(data->channel, data->channels, sizeof(maweb_channel_data), channel_comparator);
//re-set channel identifiers
@@ -1012,10 +1074,10 @@ static int maweb_start(size_t n, instance** inst){
data->channel[p].chan->ident = p;
}
- if(maweb_connect(inst[u])){
- LOGPF("Failed to open connection for instance %s", inst[u]->name);
- free(inst);
- return 1;
+ //try to connect to any available host
+ if(maweb_establish(inst[u])){
+ //do not return failure here, keepalive will periodically try to reconnect
+ LOGPF("Failed to connect to any host configured on instance %s", inst[u]->name);
}
}
@@ -1027,15 +1089,26 @@ static int maweb_start(size_t n, instance** inst){
}
static int maweb_shutdown(size_t n, instance** inst){
- size_t u;
+ size_t u, p;
maweb_instance_data* data = NULL;
for(u = 0; u < n; u++){
data = (maweb_instance_data*) inst[u]->impl;
+
+ for(p = 0; p < data->hosts; p++){
+ //one of these might have failed to allocate
+ if(data->host){
+ free(data->host[p]);
+ }
+ if(data->port){
+ free(data->port[p]);
+ }
+ }
free(data->host);
data->host = NULL;
free(data->port);
data->port = NULL;
+
free(data->user);
data->user = NULL;
free(data->pass);
@@ -1048,7 +1121,7 @@ static int maweb_shutdown(size_t n, instance** inst){
data->buffer = NULL;
data->offset = data->allocated = 0;
- data->state = ws_new;
+ data->state = ws_closed;
free(data->channel);
data->channel = NULL;
diff --git a/backends/maweb.h b/backends/maweb.h
index 80835d9..85ca09d 100644
--- a/backends/maweb.h
+++ b/backends/maweb.h
@@ -79,8 +79,11 @@ typedef struct /*_maweb_channel*/ {
} maweb_channel_data;
typedef struct /*_maweb_instance_data*/ {
- char* host;
- char* port;
+ size_t next_host;
+ size_t hosts;
+ char** host;
+ char** port;
+
char* user;
char* pass;
diff --git a/backends/maweb.md b/backends/maweb.md
index eddf1a5..1547919 100644
--- a/backends/maweb.md
+++ b/backends/maweb.md
@@ -18,17 +18,17 @@ 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 |
+| `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
| Option | Example value | Default value | Description |
|---------------|-----------------------|-----------------------|---------------------------------------------------------------|
-| `host` | `10.23.42.21 80` | none | Host address (and optional port) of the MA Web Remote |
-| `user` | `midimonster` | none | User for the remote session (GrandMA2) |
-| `password` | `midimonster` | `midimonster` | Password for the remote session |
-| `cmdline` | `console` | `remote` | Commandline key handling mode (see below) |
+| `host` | `10.23.42.21 80` | none | Host address (and optional port) of the MA Web Remote. When specified multiple times, the instance will connect the next address when the current connection fails. |
+| `user` | `midimonster` | none | User for the remote session (GrandMA2). |
+| `password` | `midimonster` | `midimonster` | Password for the remote session. |
+| `cmdline` | `console` | `remote` | Commandline key handling mode (see below). |
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
diff --git a/backends/midi.c b/backends/midi.c
index f73ebb4..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 {
@@ -111,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;
}
@@ -154,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);
@@ -224,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);
@@ -236,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;
@@ -253,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.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/openpixelcontrol.c b/backends/openpixelcontrol.c
index 168e077..f2dde23 100644
--- a/backends/openpixelcontrol.c
+++ b/backends/openpixelcontrol.c
@@ -48,7 +48,7 @@ static int openpixel_configure_instance(instance* inst, char* option, char* valu
return 1;
}
- data->dest_fd = mmbackend_socket(host, port, SOCK_STREAM, 0, 0);
+ data->dest_fd = mmbackend_socket(host, port, SOCK_STREAM, 0, 0, 1);
if(data->dest_fd >= 0){
return 0;
}
@@ -62,7 +62,7 @@ static int openpixel_configure_instance(instance* inst, char* option, char* valu
return 1;
}
- data->listen_fd = mmbackend_socket(host, port, SOCK_STREAM, 1, 0);
+ data->listen_fd = mmbackend_socket(host, port, SOCK_STREAM, 1, 0, 1);
if(data->listen_fd >= 0 && !listen(data->listen_fd, SOMAXCONN)){
return 0;
}
@@ -495,11 +495,11 @@ static ssize_t openpixel_client_headerdata(instance* inst, openpixel_client* cli
}
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
+ //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;
@@ -534,7 +534,7 @@ static int openpixel_client_handle(instance* inst, int fd){
ssize_t bytes = recv(fd, buffer, sizeof(buffer), 0);
if(bytes <= 0){
if(bytes < 0){
- LOGPF("Failed to receive from client: %s", strerror(errno));
+ LOGPF("Failed to receive from client: %s", mmbackend_socket_strerror(errno));
}
//close the connection
diff --git a/backends/osc.c b/backends/osc.c
index 754c290..5887a50 100644
--- a/backends/osc.c
+++ b/backends/osc.c
@@ -10,6 +10,7 @@
/*
* TODO
* ping method
+ * bundle output
*/
#define osc_align(a) ((((a) / 4) + (((a) % 4) ? 1 : 0)) * 4)
@@ -231,7 +232,7 @@ static int osc_path_validate(char* path, uint8_t allow_patterns){
char pattern_chars[] = "?[]{}*";
size_t u, c;
uint8_t square_open = 0, curly_open = 0;
-
+
if(path[0] != '/'){
LOGPF("%s is not a valid OSC path: Missing root /", path);
return 1;
@@ -331,7 +332,7 @@ static int osc_path_match(char* pattern, char* path){
}
if(pattern[match_end + 1] == '-' && pattern[match_end + 2] != ']'){
- if((pattern[match_end] > pattern[match_end + 2]
+ if((pattern[match_end] > pattern[match_end + 2]
&& path[u] >= pattern[match_end + 2]
&& path[u] <= pattern[match_end])
|| (pattern[match_end] <= pattern[match_end + 2]
@@ -524,7 +525,7 @@ static int osc_configure_instance(instance* inst, char* option, char* value){
}
//this requests a socket with SO_BROADCAST set, whether this is useful functionality for OSC is up for debate
- data->fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1);
+ data->fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1, 1);
if(data->fd < 0){
LOGPF("Failed to bind for instance %s", inst->name);
return 1;
@@ -666,7 +667,7 @@ static int osc_output_channel(instance* inst, size_t channel){
memcpy(xmit_buf, data->root, strlen(data->root));
offset += strlen(data->root);
}
-
+
memcpy(xmit_buf + offset, data->channel[channel].path, strlen(data->channel[channel].path));
offset += strlen(data->channel[channel].path) + 1;
offset = osc_align(offset);
@@ -695,7 +696,7 @@ static int osc_output_channel(instance* inst, size_t channel){
//output packet
if(sendto(data->fd, xmit_buf, offset, 0, (struct sockaddr*) &(data->dest), data->dest_len) < 0){
- LOGPF("Failed to transmit packet: %s", strerror(errno));
+ LOGPF("Failed to transmit packet: %s", mmbackend_socket_strerror(errno));
}
return 0;
}
@@ -703,16 +704,12 @@ static int osc_output_channel(instance* inst, size_t channel){
static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){
size_t evt = 0, mark = 0;
int rv = 0;
+ osc_instance_data* data = (osc_instance_data*) inst->impl;
osc_channel_ident ident = {
.label = 0
};
osc_parameter_value current;
- if(!num){
- return 0;
- }
-
- osc_instance_data* data = (osc_instance_data*) inst->impl;
if(!data->dest_len){
LOGPF("Instance %s does not have a destination, output is disabled (%" PRIsize_t " channels)", inst->name, num);
return 0;
@@ -724,7 +721,7 @@ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){
//sanity check
if(ident.fields.channel >= data->channels
|| ident.fields.parameter >= data->channel[ident.fields.channel].params){
- LOG("Channel identifier out of range");
+ LOG("Channel identifier out of range, possibly an output channel was not pre-configured");
return 1;
}
@@ -747,7 +744,7 @@ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){
mark = 1;
}
}
-
+
if(mark){
//output all marked channels
for(evt = 0; !rv && evt < num; evt++){
@@ -761,7 +758,7 @@ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){
return rv;
}
-static int osc_process_packet(instance* inst, char* local_path, char* format, uint8_t* payload, size_t payload_len){
+static int osc_process_message(instance* inst, char* local_path, char* format, uint8_t* payload, size_t payload_len){
osc_instance_data* data = (osc_instance_data*) inst->impl;
size_t c, p, offset = 0;
osc_parameter_value min, max, cur;
@@ -813,15 +810,82 @@ static int osc_process_packet(instance* inst, char* local_path, char* format, ui
return 0;
}
+static int osc_process_packet(instance* inst, uint8_t* buffer, size_t len){
+ osc_instance_data* data = (osc_instance_data*) inst->impl;
+ size_t offset = 0, message_length = len;
+ char* osc_local = NULL, *osc_fmt = NULL;
+ uint8_t* osc_data = NULL;
+ uint32_t* bundle_size = NULL;
+ uint8_t decode_bundle = 0;
+
+ //bundles need at least a header and timestamp
+ if(len >= 16 && !memcmp(buffer, "#bundle\0", 8)){
+ decode_bundle = 1;
+ offset = 16;
+ }
+
+ do{
+ if(decode_bundle){
+ if(len - offset < 4){
+ LOGPF("Failed to decode bundle size: %" PRIsize_t " bytes left at %" PRIsize_t " of %" PRIsize_t, len - offset, offset, len);
+ break;
+ }
+ bundle_size = (uint32_t*) (buffer + offset);
+ message_length = be32toh(*bundle_size);
+ DBGPF("Next bundle entry has %" PRIsize_t " bytes", message_length);
+ offset += 4;
+
+ if(len - offset < message_length){
+ LOGPF("Bundle member size out of bounds: %" PRIsize_t " bytes left", len - offset);
+ break;
+ }
+ }
+
+ //check for recursive bundles
+ if(message_length >= 16 && !memcmp(buffer + offset, "#bundle\0", 8)){
+ DBGPF("Recursing into sub-bundle of size %" PRIsize_t " on %s", message_length, inst->name);
+ osc_process_packet(inst, buffer + offset, message_length);
+ }
+ //ignore messages if root filter active
+ else if(data->root && strncmp((char*) (buffer + offset), data->root, min(message_length, strlen(data->root)))){
+ DBGPF("Ignoring message due to active root filter %s: data is for %s", data->root, buffer + offset);
+ }
+ else{
+ //FIXME all these accesses should be checked against message_length
+ osc_local = (char*) (buffer + offset + (data->root ? strlen(data->root) : 0));
+ osc_fmt = (char*) (buffer + offset + osc_align(strlen((char*) (buffer + offset)) + 1));
+
+ if(*osc_fmt != ','){
+ //invalid format string
+ LOGPF("Invalid format string in packet for instance %s: %s", inst->name, osc_fmt);
+ }
+ else{
+ osc_fmt++;
+
+ if(osc_global_config.detect){
+ LOGPF("Incoming data: Path %s.%s Format %s", inst->name, osc_local, osc_fmt);
+ }
+
+ osc_data = (uint8_t*) osc_fmt + (osc_align(strlen(osc_fmt) + 2) - 1);
+ if(osc_process_message(inst, osc_local, osc_fmt, osc_data, message_length - (osc_data - (uint8_t*) buffer))){
+ LOGPF("Failed to process OSC message on %s", inst->name);
+ }
+ }
+ }
+
+ offset += message_length;
+ }
+ while(offset < len);
+
+ return 0;
+}
+
static int osc_handle(size_t num, managed_fd* fds){
size_t fd;
- char recv_buf[OSC_RECV_BUF];
+ uint8_t recv_buf[OSC_RECV_BUF];
instance* inst = NULL;
osc_instance_data* data = NULL;
ssize_t bytes_read = 0;
- char* osc_fmt = NULL;
- char* osc_local = NULL;
- uint8_t* osc_data = NULL;
for(fd = 0; fd < num; fd++){
inst = (instance*) fds[fd].impl;
@@ -845,30 +909,7 @@ static int osc_handle(size_t num, managed_fd* fds){
break;
}
- if(data->root && strncmp(recv_buf, data->root, min(bytes_read, strlen(data->root)))){
- //ignore packet for different root
- continue;
- }
- osc_local = recv_buf + (data->root ? strlen(data->root) : 0);
-
- osc_fmt = recv_buf + osc_align(strlen(recv_buf) + 1);
- if(*osc_fmt != ','){
- //invalid format string
- LOGPF("Invalid format string in packet for instance %s", inst->name);
- continue;
- }
- osc_fmt++;
-
- if(osc_global_config.detect){
- LOGPF("Incoming data: Path %s.%s Format %s", inst->name, osc_local, osc_fmt);
- }
-
- //FIXME check supplied data length
- osc_data = (uint8_t*) osc_fmt + (osc_align(strlen(osc_fmt) + 2) - 1);
-
- if(osc_process_packet(inst, osc_local, osc_fmt, osc_data, bytes_read - (osc_data - (uint8_t*) recv_buf))){
- return 1;
- }
+ osc_process_packet(inst, recv_buf, bytes_read);
} while(bytes_read > 0);
#ifdef _WIN32
@@ -876,7 +917,7 @@ static int osc_handle(size_t num, managed_fd* fds){
#else
if(bytes_read < 0 && errno != EAGAIN){
#endif
- LOGPF("Failed to receive data for instance %s: %s", inst->name, strerror(errno));
+ LOGPF("Failed to receive data for instance %s: %s", inst->name, mmbackend_socket_strerror(errno));
}
if(bytes_read == 0){
diff --git a/backends/python.c b/backends/python.c
index 70c2548..bd73a20 100644
--- a/backends/python.c
+++ b/backends/python.c
@@ -78,7 +78,8 @@ static void python_timer_recalculate(){
}
//10msec is absolute lower limit and minimum gcd due to rounding
- if(next_interval == 10){
+ if(next_interval <= 10){
+ next_interval = 10;
break;
}
}
@@ -115,7 +116,6 @@ 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* chan = NULL;
channel_value val = {
{0}
};
@@ -126,19 +126,22 @@ static PyObject* mmpy_output(PyObject* self, PyObject* args){
}
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);
- chan = mm_channel(inst, u, 0);
- //this should never happen
- if(!chan){
- LOGPF("Failed to fetch parsed channel %s.%s", inst->name, channel_name);
- break;
- }
data->channel[u].out = val.normalised;
- mm_channel_event(chan, val);
- break;
+ if(!last_timestamp){
+ data->channel[u].mark = 1;
+ }
+ else{
+ mm_channel_event(mm_channel(inst, u, 0), val);
+ }
+ return 0;
}
}
@@ -254,6 +257,35 @@ static PyObject* mmpy_interval(PyObject* self, PyObject* args){
return Py_None;
}
+static PyObject* mmpy_cleanup_handler(PyObject* self, PyObject* args){
+ instance* inst = *((instance**) PyModule_GetState(self));
+ python_instance_data* data = (python_instance_data*) inst->impl;
+ PyObject* current_handler = data->cleanup_handler;
+
+ if(!PyArg_ParseTuple(args, "O", &(data->cleanup_handler))
+ || (data->cleanup_handler != Py_None && !PyCallable_Check(data->cleanup_handler))){
+ data->cleanup_handler = current_handler;
+ return NULL;
+ }
+
+ if(data->cleanup_handler == Py_None){
+ DBGPF("Cleanup handler removed on %s (previously %s)", inst->name, current_handler ? "active" : "inactive");
+ data->cleanup_handler = NULL;
+ }
+ else{
+ DBGPF("Cleanup handler installed on %s (previously %s)", inst->name, current_handler ? "active" : "inactive");
+ Py_INCREF(data->cleanup_handler);
+ }
+
+ if(!current_handler){
+ Py_INCREF(Py_None);
+ return Py_None;
+ }
+
+ //do not decrease refcount on current_handler here as the reference may be used by python code again
+ return current_handler;
+}
+
static PyObject* mmpy_manage_fd(PyObject* self, PyObject* args){
instance* inst = *((instance**) PyModule_GetState(self));
python_instance_data* data = (python_instance_data*) inst->impl;
@@ -261,12 +293,10 @@ static PyObject* mmpy_manage_fd(PyObject* self, PyObject* args){
size_t u = 0, last_free = 0;
int fd = -1;
- if(!PyArg_ParseTuple(args, "OO", &handler, &sock)){
- return NULL;
- }
-
- if(handler != Py_None && !PyCallable_Check(handler)){
- PyErr_SetString(PyExc_TypeError, "manage() requires either None or a callable");
+ if(!PyArg_ParseTuple(args, "OO", &handler, &sock)
+ || sock == Py_None
+ || (handler != Py_None && !PyCallable_Check(handler))){
+ PyErr_SetString(PyExc_TypeError, "manage() requires either None or a callable and a socket-like object");
return NULL;
}
@@ -378,6 +408,11 @@ static int python_configure_instance(instance* inst, char* option, char* value){
PyEval_ReleaseThread(data->interpreter);
return 0;
}
+ else if(!strcmp(option, "default-handler")){
+ free(data->default_handler);
+ data->default_handler = strdup(value);
+ return 0;
+ }
LOGPF("Unknown instance parameter %s for instance %s", option, inst->name);
return 1;
@@ -390,13 +425,14 @@ static PyObject* mmpy_init(){
};
static PyMethodDef mmpy_methods[] = {
- {"output", mmpy_output, METH_VARARGS, "Output a channel event"},
- {"inputvalue", mmpy_input_value, METH_VARARGS, "Get last input value for a channel"},
- {"outputvalue", mmpy_output_value, METH_VARARGS, "Get the last output value for a channel"},
+ {"output", mmpy_output, METH_VARARGS, "Output a channel event on the instance"},
+ {"inputvalue", mmpy_input_value, METH_VARARGS, "Get last input value for a channel on the instance"},
+ {"outputvalue", mmpy_output_value, METH_VARARGS, "Get the last output value for a channel on the instance"},
{"current", mmpy_current_handler, METH_VARARGS, "Get the name of the currently executing channel handler"},
{"timestamp", mmpy_timestamp, METH_VARARGS, "Get the core timestamp (in milliseconds)"},
{"manage", mmpy_manage_fd, METH_VARARGS, "(Un-)register a socket or file descriptor for notifications"},
{"interval", mmpy_interval, METH_VARARGS, "Register or update an interval handler"},
+ {"cleanup_handler", mmpy_cleanup_handler, METH_VARARGS, "Register or update the instances cleanup handler"},
{0}
};
@@ -523,18 +559,19 @@ static int python_set(instance* inst, size_t num, channel** c, channel_value* v)
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);
+ result = PyObject_CallFunction(chan->handler, "d", v[u].normalised);
Py_XDECREF(result);
data->current_channel = NULL;
DBGPF("Done with handler for %s.%s", inst->name, chan->name);
}
+
+ //update input value buffer after finishing the handler
+ chan->in = v[u].normalised;
+
}
//release interpreter
@@ -561,6 +598,7 @@ static int python_handle(size_t num, managed_fd* fds){
//if timer expired, call handler
if(interval[u].delta >= interval[u].interval){
interval[u].delta %= interval[u].interval;
+ DBGPF("Calling interval handler %" PRIsize_t ", last delta %" PRIu64, u, delta);
//swap to interpreter
PyEval_RestoreThread(interval[u].interpreter);
@@ -569,7 +607,6 @@ static int python_handle(size_t num, managed_fd* fds){
Py_XDECREF(result);
//release interpreter
PyEval_ReleaseThread(interval[u].interpreter);
- DBGPF("Calling interval handler %" PRIsize_t, u);
}
}
}
@@ -600,38 +637,62 @@ static int python_handle(size_t num, managed_fd* fds){
return 0;
}
+static PyObject* python_resolve_symbol(char* spec_raw){
+ char* module_name = NULL, *object_name = NULL, *spec = strdup(spec_raw);
+ PyObject* module = NULL, *result = NULL;
+
+ module = PyImport_AddModule("__main__");
+ object_name = spec;
+ module_name = strchr(object_name, '.');
+ if(module_name){
+ *module_name = 0;
+ //returns borrowed reference
+ module = PyImport_AddModule(object_name);
+
+ if(!module){
+ LOGPF("Module %s for symbol %s.%s is not loaded", object_name, object_name, module_name + 1);
+ return NULL;
+ }
+
+ object_name = module_name + 1;
+
+ //returns new reference
+ result = PyObject_GetAttrString(module, object_name);
+ }
+
+ free(spec);
+ return result;
+}
+
static int python_start(size_t n, instance** inst){
python_instance_data* data = NULL;
- PyObject* module = NULL;
size_t u, p;
- char* module_name = NULL, *channel_name = NULL;
+ 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);
- for(p = 0; p < data->channels; p++){
- module = PyImport_AddModule("__main__");
- channel_name = data->channel[p].name;
- module_name = strchr(channel_name, '.');
- if(module_name){
- *module_name = 0;
- //returns borrowed reference
- module = PyImport_AddModule(channel_name);
-
- if(!module){
- LOGPF("Module %s for qualified channel %s.%s is not loaded on instance %s", channel_name, channel_name, module_name + 1, inst[u]->name);
- return 1;
- }
- *module_name = '.';
- channel_name = module_name + 1;
- }
+ if(data->default_handler){
+ data->handler = python_resolve_symbol(data->default_handler);
+ }
- //returns new reference
- data->channel[p].handler = PyObject_GetAttrString(module, channel_name);
+ for(p = 0; p < data->channels; p++){
+ if(!strchr(data->channel[p].name, '.') && data->handler){
+ data->channel[p].handler = data->handler;
+ }
+ else{
+ data->channel[p].handler = python_resolve_symbol(data->channel[p].name);
+ }
+ //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
@@ -642,28 +703,44 @@ static int python_start(size_t n, instance** inst){
static int python_shutdown(size_t n, instance** inst){
size_t u, p;
+ PyObject* result = NULL;
python_instance_data* data = NULL;
- //clean up channels
- //this needs to be done before stopping the interpreters,
- //because the handler references are refcounted
- for(u = 0; u < n; u++){
- data = (python_instance_data*) inst[u]->impl;
- for(p = 0; p < data->channels; p++){
- free(data->channel[p].name);
- Py_XDECREF(data->channel[p].handler);
+ //if there are no instances, the python interpreter is not started, so cleanup can be skipped
+ if(python_main){
+ //release interval references
+ for(p = 0; p < intervals; p++){
+ //swap to interpreter
+ PyEval_RestoreThread(interval[p].interpreter);
+ Py_XDECREF(interval[p].reference);
+ PyEval_ReleaseThread(interval[p].interpreter);
}
- free(data->channel);
- //do not free data here, needed for shutting down interpreters
- }
- if(python_main){
- //just used to lock the GIL
+ //lock the GIL for later interpreter release
PyEval_RestoreThread(python_main);
for(u = 0; u < n; u++){
data = (python_instance_data*) inst[u]->impl;
+ //swap to interpreter to be safe for releasing the references
+ PyThreadState_Swap(data->interpreter);
+
+ //run cleanup handler before cleaning up channel data to allow reading channel data
+ if(data->cleanup_handler){
+ result = PyObject_CallFunction(data->cleanup_handler, NULL);
+ Py_XDECREF(result);
+ Py_XDECREF(data->cleanup_handler);
+ }
+
+ //clean up channels
+ for(p = 0; p < data->channels; p++){
+ free(data->channel[p].name);
+ Py_XDECREF(data->channel[p].handler);
+ }
+ free(data->channel);
+ free(data->default_handler);
+ Py_XDECREF(data->handler);
+
//close sockets
for(p = 0; p < data->sockets; p++){
close(data->socket[p].fd); //FIXME does python do this on its own?
@@ -671,25 +748,18 @@ static int python_shutdown(size_t n, instance** inst){
Py_XDECREF(data->socket[p].handler);
}
- //release interval references
- for(p = 0; p <intervals; p++){
- Py_XDECREF(interval[p].reference);
- }
-
+ //shut down interpreter, GIL is held after this but state is NULL
DBGPF("Shutting down interpreter for instance %s", inst[u]->name);
- //swap to interpreter and end it, GIL is held after this but state is NULL
- PyThreadState_Swap(data->interpreter);
PyErr_Clear();
//PyThreadState_Clear(data->interpreter);
Py_EndInterpreter(data->interpreter);
-
free(data);
}
//shut down main interpreter
PyThreadState_Swap(python_main);
if(Py_FinalizeEx()){
- LOG("Failed to destroy python interpreters");
+ LOG("Failed to shut down python library");
}
PyMem_RawFree(program_name);
}
diff --git a/backends/python.h b/backends/python.h
index 8ca12f9..539389b 100644
--- a/backends/python.h
+++ b/backends/python.h
@@ -16,6 +16,7 @@ typedef struct /*_python_channel_data*/ {
PyObject* handler;
double in;
double out;
+ uint8_t mark;
} mmpython_channel;
typedef struct /*_mmpy_registered_socket*/ {
@@ -41,4 +42,8 @@ typedef struct /*_python_instance_data*/ {
size_t channels;
mmpython_channel* channel;
mmpython_channel* current_channel;
+
+ char* default_handler;
+ PyObject* handler;
+ PyObject* cleanup_handler;
} python_instance_data;
diff --git a/backends/python.md b/backends/python.md
index f06e504..a78d972 100644
--- a/backends/python.md
+++ b/backends/python.md
@@ -6,6 +6,7 @@ to route, generate and manipulate channel events using the Python 3 scripting la
Every instance has its own interpreter, which can be loaded with multiple Python modules.
These modules may contain member functions accepting a single `float` parameter, which can
then be used as target channels. For each incoming event, the handler function is called.
+Channels in the global scope may be assigned a default handler function.
Python modules may also register `socket` objects (and an associated callback function) with
the MIDIMonster core, which will then alert the module when there is data ready to be read.
@@ -16,13 +17,17 @@ The `midimonster` module provides the following functions:
| Function | Usage example | Description |
|-------------------------------|---------------------------------------|-----------------------------------------------|
-| `output(string, float)` | `midimonster.output("foo", 0.75)` | Output a value event to a channel |
-| `inputvalue(string)` | `midimonster.inputvalue("foo")` | Get the last input value on a channel |
-| `outputvalue(string)` | `midimonster.outputvalue("bar")` | Get the last output value on a channel |
+| `output(string, float)` | `midimonster.output("foo", 0.75)` | Output a value event to a channel on this instance |
+| `inputvalue(string)` | `midimonster.inputvalue("foo")` | Get the last input value on a channel of this instance |
+| `outputvalue(string)` | `midimonster.outputvalue("bar")` | Get the last output value on a channel of this instance |
| `current()` | `print(midimonster.current())` | Returns the name of the input channel whose handler function is currently running or `None` if the interpreter was called from another context |
| `timestamp()` | `print(midimonster.timestamp())` | Get the internal core timestamp (in milliseconds) |
| `interval(function, long)` | `midimonster.interval(toggle, 100)` | Register a function to be called periodically. Interval is specified in milliseconds (accurate to 10msec). Calling `interval` with the same function again updates the interval. Specifying the interval as `0` cancels the interval |
-| `manage(function, socket)` | `midimonster.manage(handler, socket)`| Register a (connected/listening) socket to the MIDIMonster core. Calls `function(socket)` when the socket is ready to read. Calling this method with `None` as the function argument unregisters the socket. A socket may only have one associated handler |
+| `manage(function, socket)` | `midimonster.manage(handler, socket)` | Register a (connected/listening) socket to the MIDIMonster core. Calls `function(socket)` when the socket is ready to read. Calling this method with `None` as the function argument unregisters the socket. A socket may only have one associated handler |
+| `cleanup_handler(function)` | `midimonster.cleanup_handler(save_all)`| Register a function to be called when the instance is destroyed (on MIDIMonster shutdown). One cleanup handler can be registered per instance. Calling this function when the instance already has a cleanup handler registered replaces the handler, returning the old one. |
+
+When a channel handler executes, calling `midimonster.inputvalue()` for that exact channel returns the previous value,
+while the argument to the handler is the current value. The stored value is updated after the handler finishes executing.
Example Python module:
```python
@@ -47,12 +52,16 @@ def socket_handler(sock):
def ping():
print(midimonster.timestamp())
+def save_positions():
+ # Store some data to disk
+
# Register an interval
midimonster.interval(ping, 1000)
# Create and register a client socket (add error handling as you like)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("localhost", 8990))
midimonster.manage(socket_handler, s)
+midimonster.cleanup_handler(save_positions)
```
Input values range between 0.0 and 1.0, output values are clamped to the same range.
@@ -67,9 +76,10 @@ The `python` backend does not take any global configuration.
#### Instance configuration
-| Option | Example value | Default value | Description |
-|---------------|-----------------------|-----------------------|-----------------------------------------------|
-| `module` | `my_handlers.py` | none | (Path to) Python module source file, relative to configuration file location |
+| Option | Example value | Default value | Description |
+|-----------------------|-----------------------|-----------------------|-----------------------------------------------|
+| `module` | `my_handlers.py` | none | (Path to) Python module source file, relative to configuration file location |
+| `default-handler` | `mu_handlers.default` | none | Function to be called as handler for all top-level channels (not belonging to a module) |
A single instance may have multiple `module` options specified. This will make all handlers available within their
module namespaces (see the section on channel specification).
@@ -90,6 +100,9 @@ py1.out1 > py2.module.handler
Output values will not trigger corresponding input event handlers unless the channel is mapped
back in the MIDIMonster configuration. This is intentional.
+Output events generated from cleanup handlers called during shutdown will not be routed, as the core
+routing facility has already shut down at this point. There are no plans to change this behaviour.
+
Importing a Python module named `midimonster` is probably a bad idea and thus unsupported.
The MIDIMonster is, at its core, single-threaded. Do not try to use Python's `threading`
diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c
new file mode 100644
index 0000000..7c5aa69
--- /dev/null
+++ b/backends/rtpmidi.c
@@ -0,0 +1,1790 @@
+#define BACKEND_NAME "rtpmidi"
+//#define DEBUG
+
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <ctype.h>
+
+//mmbackend pulls in windows.h, required before more specific includes
+#include "libmmbackend.h"
+#include "rtpmidi.h"
+
+#ifdef _WIN32
+#include <iphlpapi.h>
+#else
+#include <arpa/inet.h>
+#include <net/if.h>
+#include <sys/types.h>
+#include <ifaddrs.h>
+#endif
+
+//TODO learn peer ssrcs
+//TODO default mode?
+//TODO internal loop mode
+//TODO for some reason, the announce packet generates an exception in the wireshark dns dissector
+//TODO rename and document most functions
+//TODO timeout non-responsive peers (connected = 0) to allow discovery to reconnect them
+
+/*
+ * CAVEAT EMPTOR: This is one of the largest backends yet, due to the
+ * sheer number of protocols involved and their respective complexity.
+ * The following RFCs may be useful for understanding this backend:
+ * * RFC 6295 (MIDI Payload for RTP)
+ * * RFC 1035 (DNS)
+ * * RFC 6762 (mDNS)
+ * * RFC 6763 (DNS Service Discovery)
+ * * RFC 2782 (SRV RR for DNS)
+ * * To a lesser extent, RFC3550 (RTP)
+ * Additionally, a strong understanding of the MIDI data stream as well as the details of multicast
+ * networking for IPv4 and IPv6 are very helpful.
+ */
+
+static struct /*_rtpmidi_global*/ {
+ //mdns is split into v6 and v4 to avoid having to translate ipv6-mapped-ipv4 source addresses
+ int mdns_fd;
+ int mdns4_fd;
+
+ char* mdns_name;
+ char* mdns_interface;
+ #ifdef _WIN32
+ unsigned mdns_adapter;
+ unsigned mdns6_adapter;
+ #endif
+
+ uint8_t detect;
+ uint64_t last_service;
+
+ size_t addresses;
+ rtpmidi_addr* address;
+
+ size_t invites;
+ rtpmidi_invite* invite;
+} cfg = {
+ .mdns_fd = -1,
+ .mdns4_fd = -1,
+ .mdns_name = NULL,
+ .mdns_interface = NULL,
+
+ .detect = 0,
+ .last_service = 0,
+
+ .addresses = 0,
+ .address = NULL,
+
+ .invites = 0,
+ .invite = NULL
+};
+
+MM_PLUGIN_API int init(){
+ backend rtpmidi = {
+ .name = BACKEND_NAME,
+ .conf = rtpmidi_configure,
+ .create = rtpmidi_instance,
+ .conf_instance = rtpmidi_configure_instance,
+ .channel = rtpmidi_channel,
+ .handle = rtpmidi_set,
+ .interval = rtpmidi_interval,
+ .process = rtpmidi_handle,
+ .start = rtpmidi_start,
+ .shutdown = rtpmidi_shutdown
+ };
+
+ if(sizeof(rtpmidi_channel_ident) != sizeof(uint64_t)){
+ LOG("Channel identification union out of bounds");
+ return 1;
+ }
+
+ if(mm_backend_register(rtpmidi)){
+ LOG("Failed to register backend");
+ return 1;
+ }
+
+ return 0;
+}
+
+static int dns_decode_name(uint8_t* buffer, size_t len, size_t start, dns_name* out){
+ size_t offset = 0, output_offset = 0;
+ uint8_t current_label = 0;
+ uint16_t ptr_target = 0;
+
+ //reset output data length and terminate null name
+ out->length = 0;
+ if(out->name){
+ out->name[0] = 0;
+ }
+
+ while(start + offset < len){
+ current_label = buffer[start + offset];
+
+ //if we're at a pointer, move there and stop counting data length
+ if(DNS_POINTER(current_label)){
+ if(start + offset + 1 >= len){
+ LOG("mDNS internal pointer out of bounds");
+ return 1;
+ }
+
+ //do this before setting the target
+ if(!ptr_target){
+ out->length += 2;
+ }
+
+ //calculate pointer target
+ ptr_target = DNS_LABEL_LENGTH(current_label) << 8 | buffer[start + offset + 1];
+
+ if(ptr_target >= len){
+ LOG("mDNS internal pointer target out of bounds");
+ return 1;
+ }
+ start = ptr_target;
+ offset = 0;
+ }
+ else{
+ if(DNS_LABEL_LENGTH(current_label) == 0){
+ if(!ptr_target){
+ out->length++;
+ }
+ break;
+ }
+
+ //check whether we have the bytes we need
+ if(start + offset + DNS_LABEL_LENGTH(current_label) > len){
+ LOG("mDNS bytes missing");
+ return 1;
+ }
+
+ //check whether we have space in the output
+ if(output_offset + DNS_LABEL_LENGTH(current_label) > out->alloc){
+ out->name = realloc(out->name, (output_offset + DNS_LABEL_LENGTH(current_label) + 2) * sizeof(uint8_t));
+ if(!out->name){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+ out->alloc = output_offset + DNS_LABEL_LENGTH(current_label);
+ }
+
+ //copy data from this label to output buffer
+ memcpy(out->name + output_offset, buffer + start + offset + 1, DNS_LABEL_LENGTH(current_label));
+ output_offset += DNS_LABEL_LENGTH(current_label) + 1;
+ offset += DNS_LABEL_LENGTH(current_label) + 1;
+ out->name[output_offset - 1] = '.';
+ out->name[output_offset] = 0;
+ if(!ptr_target){
+ out->length = offset;
+ }
+ }
+ }
+ return 0;
+}
+
+static int dns_encode_name(char* name, dns_name* out){
+ char* save = NULL, *token = NULL;
+ out->length = 0;
+
+ for(token = strtok_r(name, ".", &save); token; token = strtok_r(NULL, ".", &save)){
+ //make space for this label, its length and a trailing root label
+ if(out->alloc < out->length + strlen(token) + 1 + 1 || !out->name){
+ out->name = realloc(out->name, (out->length + strlen(token) + 2) * sizeof(char));
+ if(!out->name){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+ out->alloc = out->length + strlen(token) + 2;
+ }
+ //FIXME check label length before adding
+ out->name[out->length] = strlen(token);
+ memcpy(out->name + out->length + 1, token, strlen(token));
+ out->length += strlen(token) + 1;
+ }
+
+ //last-effort allocate a root buffer
+ if(!out->alloc){
+ out->name = calloc(1, sizeof(char));
+ if(!out->name){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+ out->alloc = 1;
+ }
+
+ //add root label
+ out->name[out->length] = 0;
+ out->length++;
+
+ return 0;
+}
+
+static ssize_t dns_push_rr(uint8_t* buffer, size_t length, dns_rr** out, char* name, uint16_t type, uint16_t class, uint32_t ttl, uint16_t len){
+ dns_rr* rr = NULL;
+ size_t offset = 0;
+ dns_name encode = {
+ .alloc = 0
+ };
+
+ //if requested, encode name
+ if(name && dns_encode_name(name, &encode)){
+ LOGPF("Failed to encode DNS name %s", name);
+ goto bail;
+ }
+
+ if(encode.length + sizeof(dns_rr) > length){
+ LOGPF("Failed to encode DNS name %s, insufficient space", name);
+ goto bail;
+ }
+
+ if(name){
+ //copy encoded name to buffer
+ memcpy(buffer, encode.name, encode.length);
+ offset += encode.length;
+ }
+
+ rr = (dns_rr*) (buffer + offset);
+ rr->rtype = htobe16(type);
+ rr->rclass = htobe16(class);
+ rr->ttl = htobe32(ttl);
+ rr->data = htobe16(len);
+ offset += sizeof(dns_rr);
+ if(out){
+ *out = rr;
+ }
+
+ free(encode.name);
+ return offset;
+
+bail:
+ free(encode.name);
+ return -1;
+}
+
+static int rtpmidi_announce_addrs(){
+ char repr[INET6_ADDRSTRLEN + 1] = "", iface[2048] = "";
+ union {
+ struct sockaddr_in* in4;
+ struct sockaddr_in6* in6;
+ struct sockaddr* in;
+ } addr;
+
+ #ifdef _WIN32
+ IP_ADAPTER_UNICAST_ADDRESS_LH* unicast_addr = NULL;
+ IP_ADAPTER_ADDRESSES addrs[250] , *iter = NULL;
+ size_t bytes_alloc = sizeof(addrs);
+
+ unsigned long status = GetAdaptersAddresses(0, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER,
+ NULL, addrs, (unsigned long*) &bytes_alloc);
+ if(status != ERROR_SUCCESS){
+ //FIXME might try to resize the result list and retry at some point...
+ FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, status,
+ MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), iface, sizeof(iface), NULL);
+ LOGPF("Failed to query local interface addresses (%lu): %s", status, iface);
+ return 1;
+ }
+
+ for(iter = addrs; iter; iter = iter->Next){
+ //friendlyname is a wide string, print it into interface for basic conversion and to avoid implementing wide string handling
+ snprintf(iface, sizeof(iface), "%S", iter->FriendlyName);
+ //filter interfaces if requested
+ if(cfg.mdns_interface && strncmp(iface, cfg.mdns_interface, min(strlen(iface), strlen(cfg.mdns_interface)))){
+ continue;
+ }
+
+ //for exact matches, use exactly this interface for multicasts
+ if(!strcmp(iface, cfg.mdns_interface)){
+ LOGPF("Using interface %s for mDNS discovery", iface);
+ cfg.mdns_adapter = iter->IfIndex;
+ cfg.mdns6_adapter = iter->Ipv6IfIndex;
+ }
+
+ for(unicast_addr = (IP_ADAPTER_UNICAST_ADDRESS_LH*) iter->FirstUnicastAddress; unicast_addr; unicast_addr = unicast_addr->Next){
+ addr.in = unicast_addr->Address.lpSockaddr;
+ #else
+ struct ifaddrs* ifa = NULL, *iter = NULL;
+
+ if(getifaddrs(&ifa)){
+ LOGPF("Failed to get adapter address information: %s", mmbackend_socket_strerror(errno));
+ return 1;
+ }
+
+ for(iter = ifa; iter; iter = iter->ifa_next){
+ if((!cfg.mdns_interface || !strcmp(cfg.mdns_interface, iter->ifa_name))
+ && strcmp(iter->ifa_name, "lo")
+ && iter->ifa_addr){
+ snprintf(iface, sizeof(iface), "%s", iter->ifa_name);
+ addr.in = iter->ifa_addr;
+ #endif
+ if(addr.in->sa_family != AF_INET && addr.in->sa_family != AF_INET6){
+ continue;
+ }
+
+ cfg.address = realloc(cfg.address, (cfg.addresses + 1) * sizeof(rtpmidi_addr));
+ if(!cfg.address){
+ cfg.addresses = 0;
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+
+ cfg.address[cfg.addresses].family = addr.in->sa_family;
+ memcpy(&cfg.address[cfg.addresses].addr,
+ (addr.in->sa_family == AF_INET) ? (void*) &addr.in4->sin_addr.s_addr : (void*) &addr.in6->sin6_addr.s6_addr,
+ (addr.in->sa_family == AF_INET) ? 4 : 16);
+
+ LOGPF("mDNS announce address %" PRIsize_t ": %s (from %s)", cfg.addresses, mmbackend_sockaddr_ntop(addr.in, repr, sizeof(repr)), iface);
+ cfg.addresses++;
+ }
+ }
+
+ #ifndef _WIN32
+ freeifaddrs(ifa);
+ #endif
+
+ if(!cfg.addresses){
+ LOG("Failed to gather local IP addresses for mDNS announce");
+ return 1;
+ }
+ return 0;
+}
+
+static uint32_t rtpmidi_interval(){
+ return max(0, (int64_t) RTPMIDI_SERVICE_INTERVAL - (int64_t) (mm_timestamp() - cfg.last_service));
+}
+
+static int rtpmidi_configure(char* option, char* value){
+ if(!strcmp(option, "mdns-name")){
+ if(cfg.mdns_name){
+ LOG("Duplicate mdns-name assignment");
+ return 1;
+ }
+
+ return mmbackend_strdup(&cfg.mdns_name, value);
+ }
+ else if(!strcmp(option, "mdns-interface")){
+ if(cfg.mdns_interface){
+ LOG("Duplicate mdns-interface assignment");
+ return 1;
+ }
+
+ return mmbackend_strdup(&cfg.mdns_interface, value);
+ }
+ else if(!strcmp(option, "detect")){
+ cfg.detect = 0;
+ if(!strcmp(value, "on")){
+ cfg.detect = 1;
+ }
+ return 0;
+ }
+
+ LOGPF("Unknown backend configuration option %s", option);
+ return 1;
+}
+
+static int rtpmidi_bind_instance(instance* inst, rtpmidi_instance_data* data, char* host, char* port){
+ struct sockaddr_storage sock_addr = {
+ 0
+ };
+ socklen_t sock_len = sizeof(sock_addr);
+ char control_port[32];
+
+ //bind to random port if none supplied
+ data->fd = mmbackend_socket(host, port ? port : "0", SOCK_DGRAM, 1, 0, 1);
+ if(data->fd < 0){
+ return 1;
+ }
+
+ if(getsockname(data->fd, (struct sockaddr*) &sock_addr, &sock_len)){
+ LOGPF("Failed to fetch data port information: %s", mmbackend_socket_strerror(errno));
+ return 1;
+ }
+
+ //bind control port
+ if(data->mode == apple){
+ data->control_port = be16toh(((struct sockaddr_in*) &sock_addr)->sin_port) - 1;
+ snprintf(control_port, sizeof(control_port), "%d", data->control_port);
+ data->control_fd = mmbackend_socket(host, control_port, SOCK_DGRAM, 1, 0, 1);
+ if(data->control_fd < 0){
+ LOGPF("Failed to bind control port %s for instance %s", control_port, inst->name);
+ return 1;
+ }
+
+ LOGPF("Apple mode instance %s listening on ports %d (control) and %d (data)", inst->name, data->control_port, data->control_port + 1);
+ }
+ else{
+ data->control_port = be16toh(((struct sockaddr_in*)&sock_addr)->sin_port);
+ LOGPF("Direct mode instance %s listening on port %d", inst->name, data->control_port);
+ }
+
+ return 0;
+}
+
+static char* rtpmidi_type_name(uint8_t type){
+ switch(type){
+ case note:
+ return "note";
+ case cc:
+ return "cc";
+ case pressure:
+ return "pressure";
+ case aftertouch:
+ return "aftertouch";
+ case pitchbend:
+ return "pitch";
+ }
+ return "unknown";
+}
+
+static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr* sock_addr, socklen_t sock_len, uint8_t learned, uint8_t connected, ssize_t invite_reference){
+ size_t u, p = data->peers;
+
+ for(u = 0; u < data->peers; u++){
+ //check whether the peer is already in the list
+ //TODO this probably should take into account the invite_reference (-1 for initiator peers or if unknown but may be present)
+ if(data->peer[u].active
+ && sock_len == data->peer[u].dest_len
+ && !memcmp(&data->peer[u].dest, sock_addr, sock_len)){
+ //if yes, update connection flag (but not learned flag because that doesn't change)
+ data->peer[u].connected = connected;
+ return 0;
+ }
+
+ if(!data->peer[u].active){
+ p = u;
+ }
+ }
+
+ if(p == data->peers){
+ data->peer = realloc(data->peer, (data->peers + 1) * sizeof(rtpmidi_peer));
+ if(!data->peer){
+ LOG("Failed to allocate memory");
+ data->peers = 0;
+ return 1;
+ }
+ data->peers++;
+ DBGPF("Extending peer registry to %" PRIsize_t " entries", data->peers);
+ }
+
+ data->peer[p].active = 1;
+ data->peer[p].learned = learned;
+ data->peer[p].connected = connected;
+ data->peer[p].invite = invite_reference;
+ memcpy(&(data->peer[p].dest), sock_addr, sock_len);
+ data->peer[p].dest_len = sock_len;
+ return 0;
+}
+
+static int rtpmidi_push_invite(instance* inst, char* peer){
+ size_t u, p;
+
+ //check whether the instance is already in the inviter list
+ for(u = 0; u < cfg.invites; u++){
+ if(cfg.invite[u].inst == inst){
+ break;
+ }
+ }
+
+ //add to the inviter list
+ if(u == cfg.invites){
+ cfg.invite = realloc(cfg.invite, (cfg.invites + 1) * sizeof(rtpmidi_invite));
+ if(!cfg.invite){
+ LOG("Failed to allocate memory");
+ cfg.invites = 0;
+ return 1;
+ }
+
+ cfg.invite[u].inst = inst;
+ cfg.invite[u].invites = 0;
+ cfg.invite[u].name = NULL;
+
+ cfg.invites++;
+ }
+
+ //check whether the requested name is already in the invite list for this instance
+ for(p = 0; p < cfg.invite[u].invites; p++){
+ if(!strcmp(cfg.invite[u].name[p], peer)){
+ return 0;
+ }
+ }
+
+ //extend the invite list
+ cfg.invite[u].name = realloc(cfg.invite[u].name, (cfg.invite[u].invites + 1) * sizeof(char*));
+ if(!cfg.invite[u].name){
+ LOG("Failed to allocate memory");
+ cfg.invite[u].invites = 0;
+ return 1;
+ }
+
+ //append the new invitee
+ cfg.invite[u].name[p] = strdup(peer);
+ if(!cfg.invite[u].name[p]){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+
+ cfg.invite[u].invites++;
+ return 0;
+}
+
+static int rtpmidi_applecommand(instance* inst, struct sockaddr* dest, socklen_t dest_len, uint8_t control, applemidi_command command, uint32_t token){
+ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl;
+ uint8_t frame[RTPMIDI_PACKET_BUFFER] = "";
+ ssize_t bytes = 0;
+
+ apple_command* cmd = (apple_command*) &frame;
+ cmd->res1 = 0xFFFF;
+ cmd->command = htobe16(command);
+ cmd->version = htobe32(2);
+ cmd->token = token ? token : (((uint32_t) rand()) << 16 | rand());
+ cmd->ssrc = htobe32(data->ssrc);
+
+ //append session name to packet
+ memcpy(frame + sizeof(apple_command), inst->name, strlen(inst->name) + 1);
+
+ //FIXME should we match sending/receiving ports? if the reference does this, it should be documented
+ bytes = sendto(control ? data->control_fd : data->fd, frame, sizeof(apple_command) + strlen(inst->name) + 1, 0, dest, dest_len);
+ if(bytes != sizeof(apple_command) + strlen(inst->name) + 1){
+ LOGPF("Failed to transmit session command on %s: %s", inst->name, mmbackend_socket_strerror(errno));
+ return 1;
+ }
+ return 0;
+}
+
+static int rtpmidi_peer_applecommand(instance* inst, size_t peer, uint8_t control, applemidi_command command, uint32_t token){
+ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl;
+ struct sockaddr_storage dest_addr;
+
+ memcpy(&dest_addr, &(data->peer[peer].dest), min(sizeof(dest_addr), data->peer[peer].dest_len));
+ if(control){
+ //calculate remote control port from data port
+ ((struct sockaddr_in*) &dest_addr)->sin_port = htobe16(be16toh(((struct sockaddr_in*) &dest_addr)->sin_port) - 1);
+ }
+
+ return rtpmidi_applecommand(inst, (struct sockaddr*) &dest_addr, data->peer[peer].dest_len, control, command, token);
+}
+
+static int rtpmidi_configure_instance(instance* inst, char* option, char* value){
+ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl;
+ char* host = NULL, *port = NULL;
+ struct sockaddr_storage sock_addr;
+ socklen_t sock_len = sizeof(sock_addr);
+
+ if(!strcmp(option, "mode")){
+ if(!strcmp(value, "direct")){
+ data->mode = direct;
+ return 0;
+ }
+ else if(!strcmp(value, "apple")){
+ data->mode = apple;
+ return 0;
+ }
+ LOGPF("Unknown instance mode %s for instance %s", value, inst->name);
+ return 1;
+ }
+ else if(!strcmp(option, "ssrc")){
+ data->ssrc = strtoul(value, NULL, 0);
+ if(!data->ssrc){
+ LOGPF("Random SSRC will be generated for instance %s", inst->name);
+ }
+ return 0;
+ }
+ else if(!strcmp(option, "bind")){
+ if(data->mode == unconfigured){
+ LOGPF("Please specify mode for instance %s before setting bind host", inst->name);
+ return 1;
+ }
+
+ mmbackend_parse_hostspec(value, &host, &port, NULL);
+
+ if(!host){
+ LOGPF("Could not parse bind host specification %s for instance %s", value, inst->name);
+ return 1;
+ }
+
+ return rtpmidi_bind_instance(inst, data, host, port);
+ }
+ else if(!strcmp(option, "learn")){
+ if(data->mode != direct){
+ LOG("'learn' option is only valid for direct mode instances");
+ return 1;
+ }
+ data->learn_peers = 0;
+ if(!strcmp(value, "true")){
+ data->learn_peers = 1;
+ }
+ return 0;
+ }
+ else if(!strcmp(option, "peer")){
+ if(data->mode == unconfigured){
+ LOGPF("Please specify mode for instance %s before configuring peers", inst->name);
+ return 1;
+ }
+
+ mmbackend_parse_hostspec(value, &host, &port, NULL);
+ if(!host || !port){
+ LOGPF("Invalid peer %s configured on instance %s", value, inst->name);
+ return 1;
+ }
+
+ if(mmbackend_parse_sockaddr(host, port, &sock_addr, &sock_len)){
+ LOGPF("Failed to resolve peer %s on instance %s", value, inst->name);
+ return 1;
+ }
+
+ //apple peers are specified using the control port, but we want to store the data port as peer
+ if(data->mode == apple){
+ ((struct sockaddr_in*) &sock_addr)->sin_port = htobe16(be16toh(((struct sockaddr_in*) &sock_addr)->sin_port) + 1);
+ }
+
+ return rtpmidi_push_peer(data, (struct sockaddr*) &sock_addr, sock_len, 0, 0, -1);
+ }
+ else if(!strcmp(option, "invite")){
+ if(data->mode != apple){
+ LOG("'invite' option is only valid for apple mode instances");
+ return 1;
+ }
+
+ return rtpmidi_push_invite(inst, value);
+ }
+ else if(!strcmp(option, "join")){
+ if(data->mode != apple){
+ LOG("'join' option is only valid for apple mode instances");
+ return 1;
+ }
+ return mmbackend_strdup(&data->accept, value);
+ }
+
+ LOGPF("Unknown instance configuration option %s on instance %s", option, inst->name);
+ return 1;
+}
+
+static int rtpmidi_instance(instance* inst){
+ rtpmidi_instance_data* data = calloc(1, sizeof(rtpmidi_instance_data));
+ if(!data){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+ data->fd = -1;
+ data->control_fd = -1;
+
+ inst->impl = data;
+ return 0;
+}
+
+static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){
+ char* next_token = spec;
+ rtpmidi_channel_ident ident = {
+ .label = 0
+ };
+
+ if(!strncmp(spec, "ch", 2)){
+ next_token += 2;
+ if(!strncmp(spec, "channel", 7)){
+ next_token = spec + 7;
+ }
+ }
+ else{
+ LOGPF("Invalid channel specification %s", spec);
+ return NULL;
+ }
+
+ ident.fields.channel = strtoul(next_token, &next_token, 10);
+ if(ident.fields.channel > 15){
+ LOGPF("Channel out of range in channel spec %s", spec);
+ return NULL;
+ }
+
+ if(*next_token != '.'){
+ LOGPF("Channel specification %s does not conform to channel<X>.<control><Y>", spec);
+ return NULL;
+ }
+
+ next_token++;
+
+ if(!strncmp(next_token, "cc", 2)){
+ ident.fields.type = cc;
+ next_token += 2;
+ }
+ else if(!strncmp(next_token, "note", 4)){
+ ident.fields.type = note;
+ next_token += 4;
+ }
+ else if(!strncmp(next_token, "pressure", 8)){
+ ident.fields.type = pressure;
+ next_token += 8;
+ }
+ else if(!strncmp(next_token, "pitch", 5)){
+ ident.fields.type = pitchbend;
+ }
+ else if(!strncmp(next_token, "aftertouch", 10)){
+ ident.fields.type = aftertouch;
+ }
+ else{
+ LOGPF("Unknown control type in spec %s", spec);
+ return NULL;
+ }
+
+ ident.fields.control = strtoul(next_token, NULL, 10);
+
+ if(ident.label){
+ return mm_channel(inst, ident.label, 1);
+ }
+ return NULL;
+}
+
+static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v){
+ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl;
+ uint8_t frame[RTPMIDI_PACKET_BUFFER] = "";
+ rtpmidi_header* rtp_header = (rtpmidi_header*) frame;
+ rtpmidi_command_header* command_header = (rtpmidi_command_header*) (frame + sizeof(rtpmidi_header));
+ size_t offset = sizeof(rtpmidi_header) + sizeof(rtpmidi_command_header), u = 0;
+ uint8_t* payload = frame + offset;
+ rtpmidi_channel_ident ident;
+
+ rtp_header->vpxcc = RTPMIDI_HEADER_MAGIC;
+ //some receivers seem to have problems reading rfcs and interpreting the marker bit correctly
+ rtp_header->mpt = (data->mode == apple ? 0 : 0x80) | RTPMIDI_HEADER_TYPE;
+ rtp_header->sequence = htobe16(data->sequence++);
+ rtp_header->timestamp = mm_timestamp() * 10; //just assume 100msec resolution because rfc4695 handwaves it
+ rtp_header->ssrc = htobe32(data->ssrc);
+
+ //midi command section header
+ //TODO enable the journal bit here
+ command_header->flags = 0xA0; //extended length header, first entry in list has dtime
+
+ //midi list
+ for(u = 0; u < num; u++){
+ ident.label = c[u]->ident;
+
+ //encode timestamp
+ payload[0] = 0;
+
+ //encode midi command
+ payload[1] = ident.fields.type | ident.fields.channel;
+ payload[2] = ident.fields.control;
+ payload[3] = v[u].normalised * 127.0;
+
+ if(ident.fields.type == pitchbend){
+ payload[2] = ((int)(v[u].normalised * 16384.0)) & 0x7F;
+ payload[3] = (((int)(v[u].normalised * 16384.0)) >> 7) & 0x7F;
+ }
+ //channel-wide aftertouch is only 2 bytes
+ else if(ident.fields.type == aftertouch){
+ payload[2] = payload[3];
+ payload -= 1;
+ offset -= 1;
+ }
+
+ payload += 4;
+ offset += 4;
+ }
+
+ //update command section length
+ //FIXME this might overrun, might check the number of events at some point
+ command_header->flags |= (((offset - sizeof(rtpmidi_header) - sizeof(rtpmidi_command_header)) & 0x0F00) >> 8);
+ command_header->length = ((offset - sizeof(rtpmidi_header) - sizeof(rtpmidi_command_header)) & 0xFF);
+
+ //TODO journal section
+
+ for(u = 0; u < data->peers; u++){
+ if(data->peer[u].active && data->peer[u].connected){
+ sendto(data->fd, frame, offset, 0, (struct sockaddr*) &data->peer[u].dest, data->peer[u].dest_len);
+ }
+ }
+
+ return 0;
+}
+
+static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size_t bytes, struct sockaddr_storage* peer, socklen_t peer_len){
+ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl;
+ uint8_t response[RTPMIDI_PACKET_BUFFER] = "";
+ apple_command* command = (apple_command*) frame;
+ char* session_name = (char*) frame + sizeof(apple_command);
+ size_t n, u;
+
+ command->command = be16toh(command->command);
+
+ //check command version (except for clock sync and receiver feedback)
+ if(command->command != apple_sync && command->command != apple_feedback
+ && be32toh(command->version) != 2){
+ LOGPF("Invalid AppleMIDI command version %" PRIu32 " on instance %s", be32toh(command->version), inst->name);
+ return 0;
+ }
+
+ if(command->command == apple_invite){
+ //check session name
+ for(n = sizeof(apple_command); n < bytes; n++){
+ if(!frame[n]){
+ break;
+ }
+
+ if(!isprint(frame[n])){
+ session_name = NULL;
+ break;
+ }
+ }
+
+ //unterminated string
+ if(n == bytes){
+ session_name = NULL;
+ }
+
+ //FIXME if already in session, reject the invitation
+ if(data->accept &&
+ (!strcmp(data->accept, "*") || (session_name && !strcmp(session_name, data->accept)))){
+ //accept the invitation
+ LOGPF("Instance %s accepting invitation to session %s%s", inst->name, session_name ? session_name : "UNNAMED", (fd == data->control_fd) ? " (control)":"");
+ //send accept message
+ rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, (fd == data->control_fd) ? 1 : 0, apple_accept, command->token);
+
+ //push peer
+ if(fd != data->control_fd){
+ return rtpmidi_push_peer(data, (struct sockaddr*) peer, peer_len, 1, 1, -1);
+ }
+ return 0;
+ }
+ else{
+ //send reject message
+ LOGPF("Instance %s rejecting invitation to session %s", inst->name, session_name ? session_name : "UNNAMED");
+ rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, (fd == data->control_fd) ? 1 : 0, apple_reject, command->token);
+ }
+ return 0;
+ }
+ else if(command->command == apple_accept){
+ if(fd != data->control_fd){
+ LOGPF("Instance %s negotiated new peer", inst->name);
+ return rtpmidi_push_peer(data, (struct sockaddr*) peer, peer_len, 1, 1, -1);
+ //FIXME store ssrc, start timesync
+ }
+ else{
+ //invite peer data port
+ LOGPF("Instance %s peer accepted on control port, inviting data port", inst->name);
+ //calculate data port
+ ((struct sockaddr_in*) peer)->sin_port = htobe16(be16toh(((struct sockaddr_in*) peer)->sin_port) + 1);
+ //send invite
+ rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, 0, apple_invite, 0);
+ }
+ return 0;
+ }
+ else if(command->command == apple_reject){
+ //just ignore this for now and retry the invitation
+ LOGPF("Invitation rejected on instance %s", inst->name);
+ }
+ else if(command->command == apple_leave){
+ //remove peer from list - this comes in on the control port, but we need to remove the data port...
+ ((struct sockaddr_in*) peer)->sin_port = htobe16(be16toh(((struct sockaddr_in*) peer)->sin_port) + 1);
+ for(u = 0; u < data->peers; u++){
+ if(data->peer[u].dest_len == peer_len
+ && !memcmp(&data->peer[u].dest, peer, peer_len)){
+ LOGPF("Instance %s removed peer", inst->name);
+ //learned peers are marked inactive, configured peers are marked unconnected
+ if(data->peer[u].learned){
+ data->peer[u].active = 0;
+ }
+ else{
+ data->peer[u].connected = 0;
+ }
+ }
+ }
+ return 0;
+ }
+ else if(command->command == apple_sync){
+ //respond with sync answer
+ memcpy(response, frame, bytes);
+ apple_sync_frame* sync = (apple_sync_frame*) response;
+ DBGPF("Incoming sync on instance %s (%d)", inst->name, sync->count);
+ sync->command = htobe16(apple_sync);
+ sync->ssrc = htobe32(data->ssrc);
+ switch(sync->count){
+ case 0:
+ //this happens if we're a participant
+ sync->count++;
+ sync->timestamp[1] = htobe64(mm_timestamp() * 10);
+ break;
+ case 1:
+ //this happens if we're an initiator
+ sync->count++;
+ sync->timestamp[2] = htobe64(mm_timestamp() * 10);
+ break;
+ default:
+ //ignore this one
+ return 0;
+ }
+
+ if(sendto(fd, response, sizeof(apple_sync_frame), 0, (struct sockaddr*) peer, peer_len) != sizeof(apple_sync_frame)){
+ LOG("Failed to output sync frame");
+ }
+ return 0;
+ }
+ else if(command->command == apple_feedback){
+ //TODO store this somewhere to properly update the recovery journal
+ LOGPF("Feedback on instance %s", inst->name);
+ return 0;
+ }
+ else{
+ LOGPF("Unknown AppleMIDI session command %04X", command->command);
+ }
+
+ return 0;
+}
+
+static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){
+ uint16_t length = 0;
+ size_t offset = 1, decode_time = 0, command_bytes = 0;
+ uint8_t midi_status = 0;
+ rtpmidi_channel_ident ident;
+ channel_value val;
+ channel* chan = NULL;
+
+ if(!bytes){
+ LOGPF("No command section in data on instance %s", inst->name);
+ return 1;
+ }
+
+ //calculate midi command section length
+ length = frame[0] & 0x0F;
+ if(frame[0] & 0x80){
+ //extended header
+ if(bytes < 2){
+ LOGPF("Short command section (%" PRIsize_t " bytes) on %s, missing extended header", bytes, inst->name);
+ return 1;
+ }
+ length <<= 8;
+ length |= frame[1];
+ offset = 2;
+ }
+
+ command_bytes = offset + length;
+ DBGPF("%u/%" PRIsize_t " bytes of command section on %s, %s header, %s initial dtime",
+ length, bytes, inst->name,
+ (frame[0] & 0x80) ? "extended" : "normal",
+ (frame[0] & 0x20) ? "has" : "no");
+
+ if(command_bytes > bytes){
+ LOGPF("Short command section on %s, indicated %" PRIsize_t ", had %" PRIsize_t, inst->name, command_bytes, bytes);
+ return 1;
+ }
+
+ if(frame[0] & 0x20){
+ decode_time = 1;
+ }
+
+ do{
+ //decode (and ignore) delta-time
+ if(decode_time){
+ for(; offset < command_bytes && frame[offset] & 0x80; offset++){
+ }
+ offset++;
+ }
+
+ //section 3 of rfc6295 states that the first dtime as well as the last command may be omitted
+ //this may make sense on a low-speed serial line, but on a network... come on.
+ if(offset >= command_bytes){
+ break;
+ }
+
+ //check for a status byte
+ //TODO filter sysex
+ if(frame[offset] & 0x80){
+ midi_status = frame[offset];
+ offset++;
+ }
+
+ //having variable encoding in each and every component is super annoying to check for...
+ if(offset >= command_bytes){
+ break;
+ }
+
+ ident.label = 0;
+ ident.fields.type = midi_status & 0xF0;
+ ident.fields.channel = midi_status & 0x0F;
+
+ //single byte command
+ if(ident.fields.type == aftertouch){
+ ident.fields.control = 0;
+ val.normalised = (double) frame[offset] / 127.0;
+ offset++;
+ }
+ //two-byte command
+ else{
+ offset++;
+ if(offset >= command_bytes){
+ break;
+ }
+
+ if(ident.fields.type == pitchbend){
+ ident.fields.control = 0;
+ val.normalised = (double)((frame[offset] << 7) | frame[offset - 1]) / 16384.0;
+ }
+ else{
+ ident.fields.control = frame[offset - 1];
+ val.normalised = (double) frame[offset] / 127.0;
+ }
+
+ //fix-up note off events
+ if(ident.fields.type == 0x80){
+ ident.fields.type = note;
+ val.normalised = 0;
+ }
+
+ offset++;
+ }
+
+ DBGPF("Decoded command type %02X channel %d control %d value %f",
+ ident.fields.type, ident.fields.channel, ident.fields.control, val.normalised);
+
+ if(cfg.detect){
+ if(ident.fields.type == pitchbend || ident.fields.type == aftertouch){
+ LOGPF("Incoming data on channel %s.ch%d.%s, value %f",
+ inst->name, ident.fields.channel,
+ rtpmidi_type_name(ident.fields.type), val.normalised);
+ }
+ else{
+ LOGPF("Incoming data on channel %s.ch%d.%s%d, value %f",
+ inst->name, ident.fields.channel,
+ rtpmidi_type_name(ident.fields.type),
+ ident.fields.control, val.normalised);
+ }
+ }
+
+ //push event
+ chan = mm_channel(inst, ident.label, 0);
+ if(chan){
+ mm_channel_event(chan, val);
+ }
+
+ decode_time = 1;
+ } while(offset < command_bytes);
+
+ return 0;
+}
+
+static int rtpmidi_handle_data(instance* inst){
+ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl;
+ uint8_t frame[RTPMIDI_PACKET_BUFFER] = "";
+ struct sockaddr_storage sock_addr;
+ socklen_t sock_len = sizeof(sock_addr);
+ rtpmidi_header* rtp_header = (rtpmidi_header*) frame;
+ ssize_t bytes_recv = recvfrom(data->fd, frame, sizeof(frame), 0, (struct sockaddr*) &sock_addr, &sock_len);
+ size_t u;
+
+ //TODO receive until EAGAIN
+ if(bytes_recv < 0){
+ LOGPF("Failed to receive for instance %s", inst->name);
+ return 1;
+ }
+
+ if(bytes_recv < sizeof(rtpmidi_header)){
+ LOGPF("Skipping short packet on instance %s", inst->name);
+ return 0;
+ }
+
+ //FIXME might want to filter data input from sources that are not registered peers
+ if(data->mode == apple && rtp_header->vpxcc == 0xFF && rtp_header->mpt == 0xFF){
+ return rtpmidi_handle_applemidi(inst, data->fd, frame, bytes_recv, &sock_addr, sock_len);
+ }
+ else if(rtp_header->vpxcc != RTPMIDI_HEADER_MAGIC || RTPMIDI_GET_TYPE(rtp_header->mpt) != RTPMIDI_HEADER_TYPE){
+ LOGPF("Frame with invalid header magic on %s", inst->name);
+ return 0;
+ }
+
+ //parse data
+ if(rtpmidi_parse(inst, frame + sizeof(rtpmidi_header), bytes_recv - sizeof(rtpmidi_header))){
+ //returning errors here fails the core loop, so just return 0 to have some logging
+ return 0;
+ }
+
+ //try to learn peers
+ if(data->learn_peers){
+ for(u = 0; u < data->peers; u++){
+ if(data->peer[u].active
+ && data->peer[u].dest_len == sock_len
+ && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){
+ break;
+ }
+ }
+
+ if(u == data->peers){
+ LOGPF("Learned new peer on %s", inst->name);
+ return rtpmidi_push_peer(data, (struct sockaddr*) &sock_addr, sock_len, 1, 1, -1);
+ }
+ }
+ return 0;
+}
+
+static int rtpmidi_handle_control(instance* inst){
+ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl;
+ uint8_t frame[RTPMIDI_PACKET_BUFFER] = "";
+ struct sockaddr_storage sock_addr;
+ socklen_t sock_len = sizeof(sock_addr);
+ ssize_t bytes_recv = recvfrom(data->control_fd, frame, sizeof(frame), 0, (struct sockaddr*) &sock_addr, &sock_len);
+
+ if(bytes_recv < 0){
+ LOGPF("Failed to receive on control socket for instance %s", inst->name);
+ return 1;
+ }
+
+ //the shortest applemidi packet is still larger than the rtpmidi header, so use that as bar
+ if(bytes_recv < sizeof(rtpmidi_header)){
+ LOGPF("Skipping short packet on control socket of instance %s", inst->name);
+ return 0;
+ }
+
+ if(data->mode == apple && frame[0] == 0xFF && frame[1] == 0xFF){
+ return rtpmidi_handle_applemidi(inst, data->control_fd, frame, bytes_recv, &sock_addr, sock_len);
+ }
+
+ LOGPF("Unknown session protocol frame received on instance %s", inst->name);
+ return 0;
+}
+
+static int rtpmidi_mdns_broadcast(uint8_t* frame, size_t len){
+ struct sockaddr_in mcast = {
+ .sin_family = AF_INET,
+ .sin_port = htobe16(5353),
+ .sin_addr.s_addr = htobe32(((uint32_t) 0xe00000fb))
+ };
+ struct sockaddr_in6 mcast6 = {
+ .sin6_family = AF_INET6,
+ .sin6_port = htobe16(5353),
+ .sin6_addr.s6_addr = {0xff, 0x02, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xfb}
+ };
+
+ //send to ipv4 and ipv6 mcasts
+ sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast6, sizeof(mcast6));
+ sendto(cfg.mdns4_fd, frame, len, 0, (struct sockaddr*) &mcast, sizeof(mcast));
+ return 0;
+}
+
+static int rtpmidi_mdns_detach(instance* inst){
+ uint8_t frame[RTPMIDI_PACKET_BUFFER] = "";
+ dns_header* hdr = (dns_header*) frame;
+ dns_rr* rr = NULL;
+ dns_name name = {
+ .alloc = 0
+ };
+ size_t offset = 0;
+ ssize_t bytes = 0;
+
+ hdr->id = 0;
+ hdr->flags[0] = 0x84;
+ hdr->flags[1] = 0;
+ hdr->questions = hdr->servers = hdr->additional = 0;
+ hdr->answers = htobe16(1);
+ offset = sizeof(dns_header);
+
+ //answer 1: _apple-midi PTR FQDN
+ snprintf((char*) frame + offset, sizeof(frame) - offset, "%s", RTPMIDI_MDNS_DOMAIN);
+ bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, 12, 1, 0, 0);
+ if(bytes < 0){
+ goto bail;
+ }
+ offset += bytes;
+
+ //TODO length-checks here
+ frame[offset++] = strlen(inst->name);
+ memcpy(frame + offset, inst->name, strlen(inst->name));
+ offset += strlen(inst->name);
+ frame[offset++] = 0xC0;
+ frame[offset++] = sizeof(dns_header);
+ rr->data = htobe16(1 + strlen(inst->name) + 2);
+
+ free(name.name);
+ return rtpmidi_mdns_broadcast(frame, offset);
+bail:
+ free(name.name);
+ return 1;
+}
+
+//FIXME this should not exceed 1500 bytes
+static int rtpmidi_mdns_announce(instance* inst){
+ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl;
+ uint8_t frame[RTPMIDI_PACKET_BUFFER] = "";
+ dns_header* hdr = (dns_header*) frame;
+ dns_rr* rr = NULL;
+ dns_rr_srv* srv = NULL;
+ dns_name name = {
+ .alloc = 0
+ };
+ size_t offset = 0, host_offset = 0, u = 0;
+ ssize_t bytes = 0;
+
+ hdr->id = 0;
+ hdr->flags[0] = 0x84;
+ hdr->flags[1] = 0;
+ hdr->questions = hdr->servers = 0;
+ hdr->answers = htobe16(4);
+ hdr->additional = htobe16(cfg.addresses);
+ offset = sizeof(dns_header);
+
+ //answer 1: SRV FQDN
+ snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.%s", inst->name, RTPMIDI_MDNS_DOMAIN);
+ bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, 33, 1, 120, 0);
+ if(bytes < 0){
+ goto bail;
+ }
+ offset += bytes;
+
+ srv = (dns_rr_srv*) (frame + offset);
+ srv->priority = 0;
+ srv->weight = 0;
+ srv->port = htobe16(data->control_port);
+ offset += sizeof(dns_rr_srv);
+
+ //rfc2782 (srv) says to not compress `target`, rfc6762 (mdns) 18.14 says to
+ //we don't do it because i don't want to
+ snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.local", cfg.mdns_name);
+ if(dns_encode_name((char*) frame + offset, &name)){
+ LOGPF("Failed to encode name for %s", frame + offset);
+ goto bail;
+ }
+ memcpy(frame + offset, name.name, name.length);
+ offset += name.length;
+ rr->data = htobe16(sizeof(dns_rr_srv) + name.length);
+
+ //answer 2: empty TXT (apple asks for it otherwise)
+ frame[offset++] = 0xC0;
+ frame[offset++] = sizeof(dns_header);
+
+ bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, NULL, 16, 1, 4500, 1);
+ if(bytes < 0){
+ goto bail;
+ }
+ offset += bytes;
+ frame[offset++] = 0x00; //zero-length TXT
+
+ //answer 3: dns-sd PTR _applemidi
+ snprintf((char*) frame + offset, sizeof(frame) - offset, "%s", RTPMIDI_DNSSD_DOMAIN);
+ bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, 12, 1, 4500, 2);
+ if(bytes < 0){
+ goto bail;
+ }
+ offset += bytes;
+
+ //add backref for PTR
+ frame[offset++] = 0xC0;
+ frame[offset++] = sizeof(dns_header) + frame[sizeof(dns_header)] + 1;
+
+ //answer 4: _applemidi PTR FQDN
+ frame[offset++] = 0xC0;
+ frame[offset++] = sizeof(dns_header) + frame[sizeof(dns_header)] + 1;
+
+ bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, NULL, 12, 1, 4500, 2);
+ if(bytes < 0){
+ goto bail;
+ }
+ offset += bytes;
+
+ //add backref for PTR
+ frame[offset++] = 0xC0;
+ frame[offset++] = sizeof(dns_header);
+
+ //additional 1: first announce addr
+ host_offset = offset;
+ snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.local", cfg.mdns_name);
+ bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset,
+ (cfg.address[0].family == AF_INET) ? 1 : 28, 1, 120,
+ (cfg.address[0].family == AF_INET) ? 4 : 16);
+ if(bytes < 0){
+ return 1;
+ }
+ offset += bytes;
+
+ memcpy(frame + offset, cfg.address[0].addr, (cfg.address[0].family == AF_INET) ? 4 : 16);
+ offset += (cfg.address[0].family == AF_INET) ? 4 : 16;
+
+ //push all other announce addresses with a pointer
+ for(u = 1; u < cfg.addresses; u++){
+ frame[offset++] = 0xC0 | (host_offset >> 8);
+ frame[offset++] = host_offset & 0xFF;
+ bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset,
+ (cfg.address[u].family == AF_INET) ? 1 : 28, 1, 120,
+ (cfg.address[u].family == AF_INET) ? 4 : 16);
+ if(bytes < 0){
+ return 1;
+ }
+ offset += bytes;
+
+ memcpy(frame + offset, cfg.address[u].addr, (cfg.address[u].family == AF_INET) ? 4 : 16);
+ offset += (cfg.address[u].family == AF_INET) ? 4 : 16;
+ }
+
+ data->last_announce = mm_timestamp();
+ free(name.name);
+ return rtpmidi_mdns_broadcast(frame, offset);
+bail:
+ free(name.name);
+ return 1;
+}
+
+static int rtpmidi_service(){
+ size_t n, u, p;
+ instance** inst = NULL;
+ rtpmidi_instance_data* data = NULL;
+ struct sockaddr_storage control_peer;
+
+ //prepare commands
+ apple_sync_frame sync = {
+ .res1 = 0xFFFF,
+ .command = htobe16(apple_sync),
+ .ssrc = 0,
+ .count = 0,
+ .timestamp = {
+ mm_timestamp() * 10
+ }
+ };
+
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ LOG("Failed to fetch instances");
+ return 1;
+ }
+
+ for(u = 0; u < n; u++){
+ data = (rtpmidi_instance_data*) inst[u]->impl;
+
+ if(data->mode == apple){
+ //mdns discovery
+ if((cfg.mdns_fd >= 0 || cfg.mdns4_fd >= 0)
+ && (!data->last_announce || mm_timestamp() - data->last_announce > RTPMIDI_ANNOUNCE_INTERVAL)){
+ rtpmidi_mdns_announce(inst[u]);
+ }
+
+ for(p = 0; p < data->peers; p++){
+ if(data->peer[p].active && data->peer[p].connected){
+ //apple sync
+ DBGPF("Instance %s initializing sync on peer %" PRIsize_t, inst[u]->name, p);
+ sync.ssrc = htobe32(data->ssrc);
+ //calculate remote control port from data port
+ memcpy(&control_peer, &(data->peer[u].dest), sizeof(control_peer));
+ ((struct sockaddr_in*) &control_peer)->sin_port = htobe16(be16toh(((struct sockaddr_in*) &control_peer)->sin_port) - 1);
+
+ if(sendto(data->control_fd, (char*) &sync, sizeof(apple_sync_frame), 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len) != sizeof(apple_sync_frame)){
+ LOG("Failed to output sync frame");
+ }
+ }
+ else if(data->peer[p].active && !data->peer[p].learned && (mm_timestamp() / 1000) % 10 == 0){
+ //try to invite pre-defined unconnected applemidi peers
+ DBGPF("Instance %s inviting configured peer %" PRIsize_t, inst[u]->name, p);
+ rtpmidi_peer_applecommand(inst[u], p, 1, apple_invite, 0);
+ }
+ }
+ }
+ }
+
+ free(inst);
+ return 0;
+}
+
+static int rtpmidi_apple_peermatch(uint8_t* session_raw, struct sockaddr* peer, socklen_t peer_len, uint16_t control_port){
+ //due to mdns restrictions, session names can at most be 255 characters long
+ char session_name[1024] = "";
+ rtpmidi_instance_data* data = NULL;
+ size_t u, n, p;
+ uint8_t done = 0;
+
+ //modify peer to match the data port for the indicated control port
+ ((struct sockaddr_in*) peer)->sin_port = htobe16(control_port + 1);
+ snprintf(session_name, sizeof(session_name), "%.*s", session_raw[0], session_raw + 1);
+
+ //find instances that invite exactly this peer
+ for(u = 0; u < cfg.invites; u++){
+ for(n = 0; n < cfg.invite[u].invites; n++){
+ if(strlen(cfg.invite[u].name[n]) == session_raw[0]
+ && !strcmp(cfg.invite[u].name[n], session_name)){
+ done = 1;
+ data = (rtpmidi_instance_data*) cfg.invite[u].inst->impl;
+ DBGPF("Peer %s explicitly invited on instance %s", session_name, cfg.invite[u].inst->name);
+
+ //check whether this peer (or its equivalent on another protocol) is already connected
+ for(p = 0; p < data->peers; p++){
+ //FIXME might want to scan for explicitly configured peers that match the announced peer
+ if(data->peer[p].active
+ && data->peer[p].learned
+ && data->peer[p].invite == n){
+ //we already learned of this peer
+ break;
+ }
+ }
+
+ if(p == data->peers){
+ //push a new peer
+ if(rtpmidi_push_peer(data, peer, peer_len, 1, 0, n)){
+ return 1;
+ }
+ //find it again
+ for(p = 0; p < data->peers; p++){
+ if(data->peer[p].active
+ && data->peer[p].learned
+ && data->peer[p].invite == n){
+ //we already learned of this peer
+ break;
+ }
+ }
+ }
+ else{
+ //if connected, we're done for this instance
+ //if not, at least the family should match
+ if(data->peer[p].connected
+ || data->peer[p].dest.ss_family != peer->sa_family){
+ break;
+ }
+
+ //if not connected and family matches, overwrite
+ memcpy(&(data->peer[p].dest), peer, data->peer[p].dest_len);
+ }
+
+ //connect either the pushed or overwritten peer
+ LOGPF("Inviting peer %s to instance %s", session_name, cfg.invite[u].inst->name);
+ rtpmidi_peer_applecommand(cfg.invite[u].inst, p, 1, apple_invite, 0);
+ }
+ }
+ }
+
+ //if we found at least one match before, we don't check wildcard invites
+ if(done){
+ return 0;
+ }
+
+ //find instances with a wildcard invite
+ for(u = 0; u < cfg.invites; u++){
+ for(n = 0; n < cfg.invite[u].invites; n++){
+ if(!strcmp(cfg.invite[u].name[n], "*")){
+ done = 1;
+ DBGPF("Peer %s implicitly invited on instance %s, converting to explicit invitation", session_name, cfg.invite[u].inst->name);
+ if(rtpmidi_push_invite(cfg.invite[u].inst, session_name)){
+ return 1;
+ }
+ }
+ }
+ }
+
+ //recurse to connect now-explicit invitations
+ if(done){
+ rtpmidi_apple_peermatch(session_raw, peer, peer_len, control_port);
+ }
+ return 0;
+}
+
+//TODO bounds check all accesses
+static int rtpmidi_parse_announce(uint8_t* buffer, size_t length, dns_header* hdr, dns_name* name, dns_name* host, struct sockaddr* source, socklen_t source_len){
+ dns_rr* rr = NULL;
+ dns_rr_srv* srv = NULL;
+ size_t u = 0, offset = sizeof(dns_header);
+ uint8_t* session_name = NULL;
+ char peer_name[1024];
+
+ for(u = 0; u < hdr->questions; u++){
+ if(dns_decode_name(buffer, length, offset, name)){
+ LOG("Failed to decode DNS label");
+ return 1;
+ }
+ offset += name->length;
+ offset += sizeof(dns_question);
+ }
+
+ //look for a SRV answer for ._apple-midi._udp.local.
+ for(u = 0; u < hdr->answers; u++){
+ if(dns_decode_name(buffer, length, offset, name)){
+ LOG("Failed to decode DNS label");
+ return 1;
+ }
+
+ //store a pointer to the first label in the current path
+ //since we decoded the name successfully before and dns_decode_name performs bounds checking, this _should_ be ok
+ session_name = (DNS_POINTER(buffer[offset])) ? buffer + (DNS_LABEL_LENGTH(buffer[offset]) << 8 | buffer[offset + 1]) : buffer + offset;
+
+ offset += name->length;
+ rr = (dns_rr*) (buffer + offset);
+ offset += sizeof(dns_rr);
+
+ if(be16toh(rr->rtype) == 33
+ && strlen(name->name) > strlen(RTPMIDI_MDNS_DOMAIN)
+ && !strcmp(name->name + (strlen(name->name) - strlen(RTPMIDI_MDNS_DOMAIN)), RTPMIDI_MDNS_DOMAIN)){
+ //decode the srv data
+ srv = (dns_rr_srv*) (buffer + offset);
+ offset += sizeof(dns_rr_srv);
+
+ if(dns_decode_name(buffer, length, offset, host)){
+ LOG("Failed to decode SRV target");
+ return 1;
+ }
+
+ if(!strncmp(host->name, cfg.mdns_name, strlen(cfg.mdns_name)) && host->name[strlen(cfg.mdns_name)] == '.'){
+ //ignore loopback packets, we don't care about them
+ return 0;
+ }
+
+ //we just use the packet's source as peer, because who would announce mdns for another host (also implementing an additional registry for this would bloat this backend further)
+ LOGPF("Detected possible peer %.*s on %s (%s) Port %d", session_name[0], session_name + 1, host->name, mmbackend_sockaddr_ntop(source, peer_name, sizeof(peer_name)), be16toh(srv->port));
+ offset -= sizeof(dns_rr_srv);
+
+ rtpmidi_apple_peermatch(session_name, source, source_len, be16toh(srv->port));
+ }
+
+ offset += be16toh(rr->data);
+ }
+
+
+ return 0;
+}
+
+static int rtpmidi_handle_mdns(int fd){
+ uint8_t buffer[RTPMIDI_PACKET_BUFFER];
+ dns_header* hdr = (dns_header*) buffer;
+ dns_name name = {
+ .alloc = 0
+ }, host = name;
+ ssize_t bytes = 0;
+ struct sockaddr_storage peer_addr;
+ socklen_t peer_len = sizeof(peer_addr);
+ #ifdef DEBUG
+ char peer_name[INET6_ADDRSTRLEN + 1];
+ #endif
+
+ for(bytes = recvfrom(fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &peer_addr, &peer_len);
+ bytes > 0;
+ bytes = recvfrom(fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &peer_addr, &peer_len)){
+ if(bytes < sizeof(dns_header)){
+ continue;
+ }
+
+ //decode basic header
+ hdr->id = be16toh(hdr->id);
+ hdr->questions = be16toh(hdr->questions);
+ hdr->answers = be16toh(hdr->answers);
+ hdr->servers = be16toh(hdr->servers);
+ hdr->additional = be16toh(hdr->additional);
+
+ //rfc6762 18.3: opcode != 0 -> ignore
+ //rfc6762 18.11: response code != 0 -> ignore
+
+ DBGPF("%" PRIsize_t " bytes on v%c, ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional, src %s",
+ bytes, (fd == cfg.mdns_fd ? '6' : '4'), hdr->id,
+ DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query",
+ hdr->questions, hdr->answers, hdr->servers, hdr->additional,
+ mmbackend_sockaddr_ntop((struct sockaddr*) &peer_addr, peer_name, sizeof(peer_name)));
+ rtpmidi_parse_announce(buffer, bytes, hdr, &name, &host, (struct sockaddr*) &peer_addr, peer_len);
+
+ peer_len = sizeof(peer_addr);
+ }
+
+ free(name.name);
+ free(host.name);
+ #ifdef _WIN32
+ if(WSAGetLastError() == WSAEWOULDBLOCK){
+ #else
+ if(errno == EAGAIN){
+ #endif
+ return 0;
+ }
+
+ LOGPF("Error reading from mDNS descriptor: %s", mmbackend_socket_strerror(errno));
+ return 1;
+}
+
+static int rtpmidi_handle(size_t num, managed_fd* fds){
+ size_t u;
+ int rv = 0;
+ instance* inst = NULL;
+ rtpmidi_instance_data* data = NULL;
+
+ //handle service tasks (mdns, clock sync, peer connections)
+ if(mm_timestamp() - cfg.last_service > RTPMIDI_SERVICE_INTERVAL){
+ //DBGPF("Performing service tasks, delta %" PRIu64, mm_timestamp() - cfg.last_service);
+ if(rtpmidi_service()){
+ return 1;
+ }
+ cfg.last_service = mm_timestamp();
+ }
+
+ for(u = 0; u < num; u++){
+ if(!fds[u].impl){
+ //handle mDNS discovery input
+ rtpmidi_handle_mdns(fds[u].fd);
+ }
+ else{
+ //handle rtp/control input
+ inst = (instance*) fds[u].impl;
+ data = (rtpmidi_instance_data*) inst->impl;
+ if(fds[u].fd == data->fd){
+ rv |= rtpmidi_handle_data(inst);
+ }
+ else if(fds[u].fd == data->control_fd){
+ rv |= rtpmidi_handle_control(inst);
+ }
+ else{
+ LOG("Signaled for unknown descriptor");
+ }
+ }
+ }
+
+ return rv;
+}
+
+static int rtpmidi_start_mdns(){
+ //use ip_mreqn where possible, but that renames the interface member
+ #ifdef _WIN32
+ struct ip_mreq mcast_req = {
+ .imr_interface.s_addr = INADDR_ANY,
+ #else
+ struct ip_mreqn mcast_req = {
+ .imr_address.s_addr = INADDR_ANY,
+ #endif
+ .imr_multiaddr.s_addr = htobe32(((uint32_t) 0xe00000fb))
+ };
+
+ struct ipv6_mreq mcast6_req = {
+ .ipv6mr_multiaddr.s6_addr = {0xff, 0x02, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xfb},
+ .ipv6mr_interface = 0
+ };
+
+ if(!cfg.mdns_name){
+ LOG("No mDNS name set, disabling AppleMIDI discovery");
+ return 0;
+ }
+
+ if(cfg.mdns_interface){
+ #ifdef _WIN32
+ mcast6_req.ipv6mr_interface = cfg.mdns6_adapter;
+ mcast_req.imr_interface.s_addr = htobe32(cfg.mdns_adapter);
+ #else
+ mcast6_req.ipv6mr_interface = if_nametoindex(cfg.mdns_interface);
+ mcast_req.imr_ifindex = if_nametoindex(cfg.mdns_interface);
+ #endif
+ }
+
+ //FIXME might try passing NULL as host here to work around possible windows ipv6 handicaps
+ cfg.mdns_fd = mmbackend_socket(RTPMIDI_DEFAULT_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1, 0);
+ cfg.mdns4_fd = mmbackend_socket(RTPMIDI_DEFAULT4_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1, 0);
+ if(cfg.mdns_fd < 0 && cfg.mdns4_fd < 0){
+ LOG("Failed to create requested mDNS descriptors");
+ return 1;
+ }
+
+ //join ipv4 multicast group
+ if(cfg.mdns4_fd >= 0 && setsockopt(cfg.mdns4_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){
+ LOGPF("Failed to join IPv4 multicast group for mDNS, discovery may be impaired: %s", mmbackend_socket_strerror(errno));
+ }
+
+ //join ipv6 multicast group
+ if(cfg.mdns_fd >= 0 && setsockopt(cfg.mdns_fd, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, (uint8_t*) &mcast6_req, sizeof(mcast6_req))){
+ LOGPF("Failed to join IPv6 multicast group for mDNS, discovery may be impaired: %s", mmbackend_socket_strerror(errno));
+ }
+
+ //register mdns fd to core
+ return mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL) | mm_manage_fd(cfg.mdns4_fd, BACKEND_NAME, 1, NULL);
+}
+
+static int rtpmidi_start(size_t n, instance** inst){
+ size_t u, p, fds = 0;
+ rtpmidi_instance_data* data = NULL;
+ uint8_t mdns_requested = 0;
+
+ for(u = 0; u < n; u++){
+ data = (rtpmidi_instance_data*) inst[u]->impl;
+ //check whether instances are explicitly configured to a mode
+ if(data->mode == unconfigured){
+ LOGPF("Instance %s is missing a mode configuration", inst[u]->name);
+ return 1;
+ }
+
+ //generate random ssrc's
+ if(!data->ssrc){
+ data->ssrc = ((uint32_t) rand()) << 16 | rand();
+ }
+
+ //if not bound, bind to default
+ if(data->fd < 0 && rtpmidi_bind_instance(inst[u], data, RTPMIDI_DEFAULT_HOST, NULL)){
+ LOGPF("Failed to bind default sockets for instance %s", inst[u]->name);
+ return 1;
+ }
+
+ //mark configured peers on direct instances as connected so output is sent
+ //apple mode instances go through the session negotiation before marking peers as active
+ if(data->mode == direct){
+ for(p = 0; p < data->peers; p++){
+ data->peer[p].connected = 1;
+ }
+ }
+ else if(data->mode == apple){
+ mdns_requested = 1;
+ }
+
+ //register fds to core
+ if(mm_manage_fd(data->fd, BACKEND_NAME, 1, inst[u]) || (data->control_fd >= 0 && mm_manage_fd(data->control_fd, BACKEND_NAME, 1, inst[u]))){
+ LOGPF("Failed to register descriptor for instance %s with core", inst[u]->name);
+ return 1;
+ }
+ fds += (data->control_fd >= 0) ? 2 : 1;
+ }
+
+ if(mdns_requested && (rtpmidi_announce_addrs() || rtpmidi_start_mdns())){
+ LOG("Failed to set up mDNS discovery, instances may not show up on remote hosts and may not find remote peers");
+ }
+ else if(mdns_requested){
+ fds += 2;
+ }
+
+ LOGPF("Registered %" PRIsize_t " descriptors to core", fds);
+ return 0;
+}
+
+static int rtpmidi_shutdown(size_t n, instance** inst){
+ rtpmidi_instance_data* data = NULL;
+ size_t u, p;
+
+ for(u = 0; u < n; u++){
+ data = (rtpmidi_instance_data*) inst[u]->impl;
+
+ if((cfg.mdns_fd >= 0 || cfg.mdns4_fd >= 0) && data->mode == apple){
+ rtpmidi_mdns_detach(inst[u]);
+ }
+
+ if(data->fd >= 0){
+ close(data->fd);
+ }
+
+ if(data->control_fd >= 0){
+ close(data->control_fd);
+ }
+
+ free(data->accept);
+ data->accept = NULL;
+
+ free(data->peer);
+ data->peer = NULL;
+ data->peers = 0;
+
+ free(inst[u]->impl);
+ inst[u]->impl = NULL;
+ }
+
+ for(u = 0; u < cfg.invites; u++){
+ for(p = 0; p < cfg.invite[u].invites; p++){
+ free(cfg.invite[u].name[p]);
+ }
+ free(cfg.invite[u].name);
+ }
+ free(cfg.invite);
+ cfg.invite = NULL;
+ cfg.invites = 0;
+
+ free(cfg.address);
+ cfg.addresses = 0;
+
+ free(cfg.mdns_name);
+ cfg.mdns_name = NULL;
+ free(cfg.mdns_interface);
+ cfg.mdns_interface = NULL;
+ if(cfg.mdns_fd >= 0){
+ close(cfg.mdns_fd);
+ }
+ if(cfg.mdns4_fd >= 0){
+ close(cfg.mdns4_fd);
+ }
+
+ LOG("Backend shut down");
+ return 0;
+}
diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h
new file mode 100644
index 0000000..7e6eccc
--- /dev/null
+++ b/backends/rtpmidi.h
@@ -0,0 +1,182 @@
+#ifndef _WIN32
+#include <sys/socket.h>
+#endif
+#include "midimonster.h"
+
+MM_PLUGIN_API int init();
+static int rtpmidi_configure(char* option, char* value);
+static int rtpmidi_configure_instance(instance* instance, char* option, char* value);
+static int rtpmidi_instance(instance* inst);
+static channel* rtpmidi_channel(instance* instance, char* spec, uint8_t flags);
+static uint32_t rtpmidi_interval();
+static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v);
+static int rtpmidi_handle(size_t num, managed_fd* fds);
+static int rtpmidi_start(size_t n, instance** inst);
+static int rtpmidi_shutdown(size_t n, instance** inst);
+
+#define RTPMIDI_PACKET_BUFFER 8192
+#define RTPMIDI_DEFAULT_HOST "::"
+#define RTPMIDI_DEFAULT4_HOST "0.0.0.0"
+#define RTPMIDI_MDNS_PORT "5353"
+#define RTPMIDI_HEADER_MAGIC 0x80
+#define RTPMIDI_HEADER_TYPE 0x61
+#define RTPMIDI_GET_TYPE(a) ((a) & 0x7F)
+#define RTPMIDI_DEFAULT_NAME "MIDIMonster"
+#define RTPMIDI_SERVICE_INTERVAL 1000
+#define RTPMIDI_MDNS_DOMAIN "_apple-midi._udp.local."
+#define RTPMIDI_DNSSD_DOMAIN "_services._dns-sd._udp.local."
+#define RTPMIDI_ANNOUNCE_INTERVAL (60 * 1000)
+
+#define DNS_POINTER(a) (((a) & 0xC0) == 0xC0)
+#define DNS_LABEL_LENGTH(a) ((a) & 0x3F)
+#define DNS_OPCODE(a) (((a) & 0x78) >> 3)
+#define DNS_RESPONSE(a) ((a) & 0x80)
+
+enum /*_rtpmidi_channel_type*/ {
+ none = 0,
+ note = 0x90,
+ cc = 0xB0,
+ pressure = 0xA0,
+ aftertouch = 0xD0,
+ pitchbend = 0xE0
+};
+
+typedef enum /*_rtpmidi_instance_mode*/ {
+ unconfigured = 0,
+ direct,
+ apple
+} rtpmidi_instance_mode;
+
+typedef union {
+ struct {
+ uint8_t pad[5];
+ uint8_t type;
+ uint8_t channel;
+ uint8_t control;
+ } fields;
+ uint64_t label;
+} rtpmidi_channel_ident;
+
+typedef struct /*_rtpmidi_peer*/ {
+ struct sockaddr_storage dest;
+ socklen_t dest_len;
+ //uint32_t ssrc;
+ uint8_t active; //marked for reuse
+ uint8_t learned; //learned / configured peer (learned peers are marked inactive on session shutdown)
+ uint8_t connected; //currently in active session
+ ssize_t invite; //invite-list index for apple-mode learned peers (used to track ipv6/ipv4 overlapping invitations)
+} rtpmidi_peer;
+
+typedef struct /*_rtmidi_instance_data*/ {
+ rtpmidi_instance_mode mode;
+
+ int fd;
+ int control_fd;
+ uint16_t control_port; /*convenience member set by rtpmidi_bind_instance*/
+
+ size_t peers;
+ rtpmidi_peer* peer;
+ uint32_t ssrc;
+ uint16_t sequence;
+
+ //apple-midi config
+ char* accept;
+ uint64_t last_announce;
+
+ //direct mode config
+ uint8_t learn_peers;
+} rtpmidi_instance_data;
+
+typedef struct /*rtpmidi_invited_peer*/ {
+ instance* inst;
+ size_t invites;
+ char** name;
+} rtpmidi_invite;
+
+typedef struct /*_rtpmidi_addr*/ {
+ int family;
+ //this is actually a fair bit too big, but whatever
+ uint8_t addr[sizeof(struct sockaddr_storage)];
+} rtpmidi_addr;
+
+typedef enum {
+ apple_invite = 0x494E, //IN
+ apple_accept = 0x4F4B, //OK
+ apple_reject = 0x4E4F, //NO
+ apple_leave = 0x4259, //BY
+ apple_sync = 0x434B, //CK
+ apple_feedback = 0x5253 //RS
+} applemidi_command;
+
+typedef struct /*_dns_name*/ {
+ size_t alloc;
+ char* name;
+ size_t length;
+} dns_name;
+
+#pragma pack(push, 1)
+typedef struct /*_apple_session_command*/ {
+ uint16_t res1;
+ uint16_t command;
+ uint32_t version;
+ uint32_t token;
+ uint32_t ssrc;
+ //char* name
+} apple_command;
+
+typedef struct /*_apple_session_sync*/ {
+ uint16_t res1;
+ uint16_t command;
+ uint32_t ssrc;
+ uint8_t count;
+ uint8_t res2[3];
+ uint64_t timestamp[3];
+} apple_sync_frame;
+
+typedef struct /*_apple_session_feedback*/ {
+ uint16_t res1;
+ uint8_t command[2];
+ uint32_t ssrc;
+ uint32_t sequence;
+} apple_journal_feedback;
+
+typedef struct /*_rtp_midi_header*/ {
+ uint8_t vpxcc;
+ uint8_t mpt;
+ uint16_t sequence;
+ uint32_t timestamp;
+ uint32_t ssrc;
+} rtpmidi_header;
+
+typedef struct /*_rtp_midi_command*/ {
+ uint8_t flags;
+ uint8_t length;
+} rtpmidi_command_header;
+
+typedef struct /*_dns_header*/ {
+ uint16_t id;
+ uint8_t flags[2];
+ uint16_t questions;
+ uint16_t answers;
+ uint16_t servers;
+ uint16_t additional;
+} dns_header;
+
+typedef struct /*_dns_question*/ {
+ uint16_t qtype;
+ uint16_t qclass;
+} dns_question;
+
+typedef struct /*_dns_rr*/ {
+ uint16_t rtype;
+ uint16_t rclass;
+ uint32_t ttl;
+ uint16_t data;
+} dns_rr;
+
+typedef struct /*_dns_rr_srv*/ {
+ uint16_t priority;
+ uint16_t weight;
+ uint16_t port;
+} dns_rr_srv;
+#pragma pack(pop)
diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md
new file mode 100644
index 0000000..82548bf
--- /dev/null
+++ b/backends/rtpmidi.md
@@ -0,0 +1,97 @@
+### The `rtpmidi` backend
+
+This backend provides read-write access to RTP MIDI streams, which transfer MIDI data
+over the network. Notably, RTP MIDI has native support in Apple devices including their
+tablets.
+
+As the specification for RTP MIDI does not normatively indicate any method
+for session management, most vendors define their own standards for this.
+The MIDIMonster supports the following session management methods, which are
+selectable per-instance, with some methods requiring additional global configuration:
+
+* Direct connection: The instance will send and receive data from peers configured in the
+ instance configuration
+* Direct connection with peer learning: The instance will send and receive data from peers
+ configured in the instance configuration as well as previously unknown peers that
+ voluntarily send data to the instance.
+* AppleMIDI session management: The instance will be able to communicate (either as participant
+ or initiator) in an AppleMIDI session, which will be announced via mDNS (better
+ known as "Bonjour" to Apple users) if possible.
+
+Note that instances that receive data from multiple peers will combine all inputs into one
+stream, which may lead to inconsistencies during playback.
+
+#### Global configuration
+
+| Option | Example value | Default value | Description |
+|-----------------------|-----------------------|-----------------------|-----------------------|
+| `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration. |
+| `mdns-name` | `computer1` | none | mDNS hostname to announce (`<mdns-name>.local`). Apple-mode instances will be announced via mDNS if set. |
+| `mdns-interface` | `wlan0` | none | Limit addresses announced via mDNS to this interface. On Windows, this is prefix-matched against the user-editable "friendly" interface name. If this name matches an interface exactly, discovery uses exactly this device. |
+
+#### Instance configuration
+
+Common instance configuration parameters
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|-----------------------|
+| `ssrc` | `0xDEADBEEF` | Randomly generated | 32-bit synchronization source identifier |
+| `mode` | `direct` | none | Instance session management mode (`direct` or `apple`) |
+| `peer` | `10.1.2.3 9001` | none | MIDI session peer, may be specified multiple times. Bypasses session discovery (but still performs session negotiation) |
+
+`direct` mode instance configuration parameters
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|-----------------------|
+| `bind` | `10.1.2.1 9001` | `:: <random>` | Local network address to bind to |
+| `learn` | `true` | `false` | Accept new peers for data exchange at runtime |
+
+`apple` mode instance configuration parameters
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|-----------------------|
+| `bind` | `10.1.2.1 9001` | `:: <random>` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated). |
+| `invite` | `pad` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). May be specified multiple times. |
+| `join` | `Just Jamming` | none | Session for which to accept invitations (the special value `*` accepts the first invitation seen). |
+
+#### Channel specification
+
+The `rtpmidi` backend supports mapping different MIDI events to MIDIMonster channels. The currently supported event types are
+
+* `cc` - Control Changes
+* `note` - Note On/Off messages
+* `pressure` - Note pressure/aftertouch messages
+* `aftertouch` - Channel-wide aftertouch messages
+* `pitch` - Channel pitchbend messages
+
+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 `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel<channel>.<type>`.
+
+MIDI channels range from `0` to `15`. Each MIDI channel consists of 128 notes (numbered `0` through `127`), which
+additionally each have a pressure control, 128 CC's (numbered likewise), a channel pressure control (also called
+'channel aftertouch') and a pitch control which may all be mapped to individual MIDIMonster channels.
+
+Example mappings:
+
+```
+rmidi1.ch0.note9 > rmidi2.channel1.cc4
+rmidi1.channel15.pressure1 > rmidi1.channel0.note0
+rmidi1.ch1.aftertouch > rmidi2.ch2.cc0
+rmidi1.ch0.pitch > rmidi2.ch1.pitch
+```
+
+#### Known bugs / problems
+
+This backend has been in development for a long time due to its complexity. There may still be bugs hidden in there.
+Critical feedback and tests across multiple devices are very welcome.
+
+The mDNS and DNS-SD implementations in this backend are extremely terse, to the point of violating the
+specifications in multiple cases. Due to the complexity involved in supporting these protocols, problems
+arising from this will be considered a bug only in cases where they hinder normal operation of the backend.
+
+mDNS discovery may announce flawed records when run on a host with multiple active interfaces.
+
+While this backend should be reasonably stable, there may be problematic edge cases simply due to the
+enormous size and scope of the protocols and implementations required to make this work.
diff --git a/backends/sacn.c b/backends/sacn.c
index 79ffb46..0c0fd10 100644
--- a/backends/sacn.c
+++ b/backends/sacn.c
@@ -80,7 +80,7 @@ static int sacn_listener(char* host, char* port, uint8_t flags){
return -1;
}
- fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1);
+ fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1, 1);
if(fd < 0){
return -1;
}
@@ -101,7 +101,7 @@ static int sacn_listener(char* host, char* port, uint8_t flags){
if(flags & mcast_loop){
//set IP_MCAST_LOOP to allow local applications to receive output
if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){
- LOGPF("Failed to re-enable IP_MULTICAST_LOOP on socket: %s", strerror(errno));
+ LOGPF("Failed to re-enable IP_MULTICAST_LOOP on socket: %s", mmbackend_socket_strerror(errno));
}
}
@@ -209,12 +209,20 @@ static int sacn_configure_instance(instance* inst, char* option, char* value){
}
static int sacn_instance(instance* inst){
- inst->impl = calloc(1, sizeof(sacn_instance_data));
- if(!inst->impl){
+ sacn_instance_data* data = calloc(1, sizeof(sacn_instance_data));
+ size_t u;
+
+ if(!data){
LOG("Failed to allocate memory");
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;
+ }
+
+ inst->impl = data;
return 0;
}
@@ -231,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);
@@ -259,12 +272,13 @@ 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){
- size_t u;
+static int sacn_transmit(instance* inst, sacn_output_universe* output){
sacn_instance_data* data = (sacn_instance_data*) inst->impl;
+
+ //build sacn frame
sacn_data_pdu pdu = {
.root = {
.preamble_size = htobe16(0x10),
@@ -299,16 +313,26 @@ static int sacn_transmit(instance* inst){
memcpy((((uint8_t*)pdu.data.data) + 1), data->data.out, 512);
if(sendto(global_cfg.fd[data->fd_index].fd, (uint8_t*) &pdu, sizeof(pdu), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){
- LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno));
- }
+ #ifdef _WIN32
+ if(WSAGetLastError() != WSAEWOULDBLOCK){
+ #else
+ if(errno != EAGAIN){
+ #endif
+ LOGPF("Failed to output frame for instance %s: %s", inst->name, mmbackend_socket_strerror(errno));
+ return 1;
+ }
- //update last transmit timestamp, unmark instance
- for(u = 0; u < global_cfg.fd[data->fd_index].universes; u++){
- if(global_cfg.fd[data->fd_index].universe[u].universe == data->uni){
- global_cfg.fd[data->fd_index].universe[u].last_frame = mm_timestamp();
- global_cfg.fd[data->fd_index].universe[u].mark = 0;
+ //reschedule output
+ output->mark = 1;
+ if(!global_cfg.next_frame || global_cfg.next_frame > SACN_SYNTHESIZE_MARGIN){
+ global_cfg.next_frame = SACN_SYNTHESIZE_MARGIN;
}
+ return 0;
}
+
+ //update last transmit timestamp, unmark instance
+ output->last_frame = mm_timestamp();
+ output->mark = 0;
return 0;
}
@@ -317,10 +341,6 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){
uint32_t frame_delta = 0;
sacn_instance_data* data = (sacn_instance_data*) inst->impl;
- if(!num){
- return 0;
- }
-
if(!data->xmit_prio){
LOGPF("Instance %s not enabled for output (%" PRIsize_t " channel events)", inst->name, num);
return 0;
@@ -348,26 +368,26 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){
//send packet if required
if(mark){
- if(!data->realtime){
- //find output instance data
- for(u = 0; u < global_cfg.fd[data->fd_index].universes; u++){
- if(global_cfg.fd[data->fd_index].universe[u].universe == data->uni){
- break;
- }
+ //find output instance data
+ for(u = 0; u < global_cfg.fd[data->fd_index].universes; u++){
+ if(global_cfg.fd[data->fd_index].universe[u].universe == data->uni){
+ break;
}
+ }
+ if(!data->realtime){
frame_delta = mm_timestamp() - global_cfg.fd[data->fd_index].universe[u].last_frame;
//check if ratelimiting engaged
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_KEEPALIVE_INTERVAL - frame_delta)){
- global_cfg.next_frame = (SACN_KEEPALIVE_INTERVAL - frame_delta);
+ 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);
+ sacn_transmit(inst, global_cfg.fd[data->fd_index].universe + u);
}
return 0;
@@ -418,16 +438,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
@@ -494,7 +507,13 @@ static void sacn_discovery(size_t fd){
memcpy(pdu.data.data, global_cfg.fd[fd].universe + page * 512, universes * sizeof(uint16_t));
if(sendto(global_cfg.fd[fd].fd, (uint8_t*) &pdu, sizeof(pdu) - (512 - universes) * sizeof(uint16_t), 0, (struct sockaddr*) &discovery_dest, sizeof(discovery_dest)) < 0){
- LOGPF("Failed to output universe discovery frame for interface %" PRIsize_t ": %s", fd, strerror(errno));
+ #ifdef _WIN32
+ if(WSAGetLastError() != WSAEWOULDBLOCK){
+ #else
+ if(errno != EAGAIN){
+ #endif
+ LOGPF("Failed to output universe discovery frame for interface %" PRIsize_t ": %s", fd, mmbackend_socket_strerror(errno));
+ }
}
}
}
@@ -535,7 +554,7 @@ static int sacn_handle(size_t num, managed_fd* fds){
instance_id.fields.uni = global_cfg.fd[u].universe[c].universe;
inst = mm_instance_find(BACKEND_NAME, instance_id.label);
if(inst){
- sacn_transmit(inst);
+ sacn_transmit(inst, global_cfg.fd[u].universe + c);
}
}
@@ -548,11 +567,6 @@ static int sacn_handle(size_t num, managed_fd* fds){
}
}
- //early exit
- if(!num){
- return 0;
- }
-
for(u = 0; u < num; u++){
do{
bytes_read = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0);
@@ -578,7 +592,7 @@ static int sacn_handle(size_t num, managed_fd* fds){
#else
if(bytes_read < 0 && errno != EAGAIN){
#endif
- LOGPF("Failed to receive data: %s", strerror(errno));
+ LOGPF("Failed to receive data: %s", mmbackend_socket_strerror(errno));
}
if(bytes_read == 0){
@@ -630,7 +644,7 @@ static int sacn_start(size_t n, instance** inst){
if(!data->unicast_input){
mcast_req.imr_multiaddr.s_addr = htobe32(((uint32_t) 0xefff0000) | ((uint32_t) data->uni));
if(setsockopt(global_cfg.fd[data->fd_index].fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){
- LOGPF("Failed to join Multicast group for universe %u on instance %s: %s", data->uni, inst[u]->name, strerror(errno));
+ LOGPF("Failed to join Multicast group for universe %u on instance %s: %s", data->uni, inst[u]->name, mmbackend_socket_strerror(errno));
}
}
diff --git a/backends/sacn.h b/backends/sacn.h
index 4642e59..4138f45 100644
--- a/backends/sacn.h
+++ b/backends/sacn.h
@@ -16,7 +16,7 @@ static int sacn_shutdown(size_t n, instance** inst);
//spec 6.6.2.1
#define SACN_KEEPALIVE_INTERVAL 1000
//spec 6.6.1
-#define SACN_FRAME_TIMEOUT 15
+#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"
@@ -36,6 +36,7 @@ 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*/ {
diff --git a/backends/winmidi.c b/backends/winmidi.c
index ad9b02d..030062d 100644
--- a/backends/winmidi.c
+++ b/backends/winmidi.c
@@ -117,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;
@@ -185,11 +185,6 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v
};
size_t u;
- //early exit
- if(!num){
- return 0;
- }
-
if(!data->device_out){
LOGPF("Instance %s has no output device", inst->name);
return 0;
@@ -213,7 +208,7 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v
midiOutShortMsg(data->device_out, output.dword);
}
-
+
return 0;
}
@@ -310,7 +305,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;
@@ -335,7 +330,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",
@@ -435,32 +429,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, 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);
@@ -483,8 +467,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);
@@ -492,6 +476,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;