aboutsummaryrefslogtreecommitdiffhomepage
path: root/backends
diff options
context:
space:
mode:
Diffstat (limited to 'backends')
-rw-r--r--backends/Makefile71
-rw-r--r--backends/artnet.c154
-rw-r--r--backends/artnet.h6
-rw-r--r--backends/artnet.md41
-rw-r--r--backends/evdev.c120
-rw-r--r--backends/evdev.h23
-rw-r--r--backends/evdev.md86
-rw-r--r--backends/jack.c748
-rw-r--r--backends/jack.h76
-rw-r--r--backends/jack.md84
-rw-r--r--backends/libmmbackend.c583
-rw-r--r--backends/libmmbackend.h127
-rw-r--r--backends/loopback.c44
-rw-r--r--backends/loopback.h20
-rw-r--r--backends/loopback.md28
-rw-r--r--backends/lua.c510
-rw-r--r--backends/lua.h39
-rw-r--r--backends/lua.md66
-rw-r--r--backends/maweb.c1072
-rw-r--r--backends/maweb.h100
-rw-r--r--backends/maweb.md141
-rw-r--r--backends/midi.c190
-rw-r--r--backends/midi.h15
-rw-r--r--backends/midi.md65
-rw-r--r--backends/ola.cpp318
-rw-r--r--backends/ola.h38
-rw-r--r--backends/ola.md41
-rw-r--r--backends/osc.c811
-rw-r--r--backends/osc.h56
-rw-r--r--backends/osc.md103
-rw-r--r--backends/sacn.c166
-rw-r--r--backends/sacn.h5
-rw-r--r--backends/sacn.md58
-rw-r--r--backends/winmidi.c603
-rw-r--r--backends/winmidi.h43
-rw-r--r--backends/winmidi.md60
36 files changed, 6010 insertions, 701 deletions
diff --git a/backends/Makefile b/backends/Makefile
index 771e97e..df01ec8 100644
--- a/backends/Makefile
+++ b/backends/Makefile
@@ -1,10 +1,14 @@
-.PHONY: all clean
+.PHONY: all clean full
LINUX_BACKENDS = midi.so evdev.so
-BACKENDS = artnet.so osc.so loopback.so sacn.so rtpmidi.so
+WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll rtpmidi.dll
+BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so rtpmidi.so
+OPTIONAL_BACKENDS = ola.so
+BACKEND_LIB = libmmbackend.o
SYSTEM := $(shell uname -s)
-CFLAGS += -fPIC -I../
+CFLAGS += -g -fPIC -I../ -Wall -Wpedantic
+CPPFLAGS += -g -fPIC -I../
LDFLAGS += -shared
# Build Linux backends if possible
@@ -16,14 +20,63 @@ ifeq ($(SYSTEM),Darwin)
LDFLAGS += -undefined dynamic_lookup
endif
+artnet.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
+artnet.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
+artnet.dll: LDLIBS += -lws2_32
+
+osc.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
+osc.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
+osc.dll: LDLIBS += -lws2_32
+
+sacn.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
+sacn.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
+sacn.dll: LDLIBS += -lws2_32
+
+maweb.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
+maweb.so: LDLIBS = -lssl
+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
+
+winmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
+winmidi.dll: LDLIBS += -lwinmm -lws2_32
+
+jack.so: LDLIBS = -ljack -lpthread
midi.so: LDLIBS = -lasound
-evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev)
-evdev.so: LDLIBS = $(shell pkg-config --libs libevdev)
+evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev || echo "-DBUILD_ERROR=\"Missing pkg-config data for libevdev\"")
+evdev.so: LDLIBS = $(shell pkg-config --libs libevdev || echo "-DBUILD_ERROR=\"Missing pkg-config data for libevdev\"")
+ola.so: LDLIBS = -lola
+ola.so: CPPFLAGS += -Wno-write-strings
+# The pkg-config name for liblua5.3 is subject to discussion. I prefer 'lua5.3' (which works on Debian and OSX),
+# but Arch requires 'lua53' which works on Debian, too, but breaks on OSX.
+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\"")
+
+%.so :: %.c %.h $(BACKEND_LIB)
+ $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS)
+
+%.dll :: %.c %.h $(BACKEND_LIB)
+ $(CC) $(CFLAGS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) $(LDLIBS)
+
+%.so :: %.cpp %.h
+ $(CXX) $(CPPFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS)
+
+all: $(BACKEND_LIB) $(BACKENDS)
+
+../libmmapi.a:
+ $(MAKE) -C ../ midimonster.exe
-%.so :: %.c %.h
- $(CC) $(CFLAGS) $(LDLIBS) $< -o $@ $(LDFLAGS)
+windows: export CC = x86_64-w64-mingw32-gcc
+windows: LDLIBS += -lmmapi
+windows: LDFLAGS += -L../
+windows: CFLAGS += -Wno-format -Wno-pointer-sign
+windows: ../libmmapi.a $(BACKEND_LIB) $(WINDOWS_BACKENDS)
-all: $(BACKENDS)
+full: $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS)
clean:
- $(RM) $(BACKENDS)
+ $(RM) $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) $(WINDOWS_BACKENDS)
diff --git a/backends/artnet.c b/backends/artnet.c
index d9ebfe5..57eb7b1 100644
--- a/backends/artnet.c
+++ b/backends/artnet.c
@@ -1,13 +1,10 @@
#include <string.h>
-#include <sys/types.h>
-#include <sys/socket.h>
-#include <netdb.h>
-#include <unistd.h>
-#include <fcntl.h>
#include <ctype.h>
#include <errno.h>
+#include "libmmbackend.h"
#include "artnet.h"
+
#define MAX_FDS 255
#define BACKEND_NAME "artnet"
@@ -16,68 +13,14 @@ static size_t artnet_fds = 0;
static artnet_descriptor* artnet_fd = NULL;
static int artnet_listener(char* host, char* port){
- int fd = -1, status, yes = 1, flags;
- struct addrinfo hints = {
- .ai_family = AF_UNSPEC,
- .ai_socktype = SOCK_DGRAM,
- .ai_flags = AI_PASSIVE
- };
- struct addrinfo* info;
- struct addrinfo* addr_it;
-
+ int fd;
if(artnet_fds >= MAX_FDS){
fprintf(stderr, "ArtNet backend descriptor limit reached\n");
return -1;
}
- status = getaddrinfo(host, port, &hints, &info);
- if(status){
- fprintf(stderr, "Failed to get socket info for %s port %s: %s\n", host, port, gai_strerror(status));
- return -1;
- }
-
- for(addr_it = info; addr_it != NULL; addr_it = addr_it->ai_next){
- fd = socket(addr_it->ai_family, addr_it->ai_socktype, addr_it->ai_protocol);
- if(fd < 0){
- continue;
- }
-
- yes = 1;
- if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){
- fprintf(stderr, "Failed to set SO_REUSEADDR on socket\n");
- }
-
- yes = 1;
- if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){
- fprintf(stderr, "Failed to set SO_BROADCAST on socket\n");
- }
-
- yes = 0;
- if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){
- fprintf(stderr, "Failed to unset IP_MULTICAST_LOOP option: %s\n", strerror(errno));
- }
-
- status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen);
- if(status < 0){
- close(fd);
- continue;
- }
-
- break;
- }
-
- freeaddrinfo(info);
-
- if(!addr_it){
- fprintf(stderr, "Failed to create listening socket for %s port %s\n", host, port);
- return -1;
- }
-
- //set nonblocking
- flags = fcntl(fd, F_GETFL, 0);
- if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){
- fprintf(stderr, "Failed to set ArtNet descriptor nonblocking\n");
- close(fd);
+ fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1);
+ if(fd < 0){
return -1;
}
@@ -89,7 +32,7 @@ static int artnet_listener(char* host, char* port){
return -1;
}
- fprintf(stderr, "ArtNet backend interface %zu bound to %s port %s\n", artnet_fds, host, port);
+ fprintf(stderr, "ArtNet backend interface %" PRIsize_t " bound to %s port %s\n", artnet_fds, host, port);
artnet_fd[artnet_fds].fd = fd;
artnet_fd[artnet_fds].output_instances = 0;
artnet_fd[artnet_fds].output_instance = NULL;
@@ -98,51 +41,7 @@ static int artnet_listener(char* host, char* port){
return 0;
}
-static int artnet_parse_addr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len){
- struct addrinfo* head;
- struct addrinfo hints = {
- .ai_family = AF_UNSPEC,
- .ai_socktype = SOCK_DGRAM
- };
-
- int error = getaddrinfo(host, port, &hints, &head);
- if(error || !head){
- fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(error));
- return 1;
- }
-
- memcpy(addr, head->ai_addr, head->ai_addrlen);
- *len = head->ai_addrlen;
-
- freeaddrinfo(head);
- return 0;
-}
-
-static int artnet_separate_hostspec(char* in, char** host, char** port){
- size_t u;
-
- if(!in || !host || !port){
- return 1;
- }
-
- for(u = 0; in[u] && !isspace(in[u]); u++){
- }
-
- //guess
- *host = in;
-
- if(in[u]){
- in[u] = 0;
- *port = in + u + 1;
- }
- else{
- //no port given
- *port = ARTNET_PORT;
- }
- return 0;
-}
-
-int init(){
+MM_PLUGIN_API int init(){
backend artnet = {
.name = BACKEND_NAME,
.conf = artnet_configure,
@@ -155,6 +54,11 @@ int init(){
.shutdown = artnet_shutdown
};
+ if(sizeof(artnet_instance_id) != sizeof(uint64_t)){
+ fprintf(stderr, "ArtNet instance identification union out of bounds\n");
+ return 1;
+ }
+
//register backend
if(mm_backend_register(artnet)){
fprintf(stderr, "Failed to register ArtNet backend\n");
@@ -171,8 +75,14 @@ static int artnet_configure(char* option, char* value){
return 0;
}
else if(!strcmp(option, "bind")){
- if(artnet_separate_hostspec(value, &host, &port)){
- fprintf(stderr, "Not a valid ArtNet bind address: %s\n", value);
+ mmbackend_parse_hostspec(value, &host, &port);
+
+ if(!port){
+ port = ARTNET_PORT;
+ }
+
+ if(!host){
+ fprintf(stderr, "Not valid ArtNet bind address given\n");
return 1;
}
@@ -228,19 +138,25 @@ static int artnet_configure_instance(instance* inst, char* option, char* value){
return 0;
}
else if(!strcmp(option, "dest") || !strcmp(option, "destination")){
- if(artnet_separate_hostspec(value, &host, &port)){
+ mmbackend_parse_hostspec(value, &host, &port);
+
+ if(!port){
+ port = ARTNET_PORT;
+ }
+
+ if(!host){
fprintf(stderr, "Not a valid ArtNet destination for instance %s\n", inst->name);
return 1;
}
- return artnet_parse_addr(host, port, &data->dest_addr, &data->dest_len);
+ return mmbackend_parse_sockaddr(host, port, &data->dest_addr, &data->dest_len);
}
fprintf(stderr, "Unknown ArtNet option %s for instance %s\n", option, inst->name);
return 1;
}
-static channel* artnet_channel(instance* inst, char* spec){
+static channel* artnet_channel(instance* inst, char* spec, uint8_t flags){
artnet_instance_data* data = (artnet_instance_data*) inst->impl;
char* spec_next = spec;
unsigned chan_a = strtoul(spec, &spec_next, 10);
@@ -301,7 +217,7 @@ static int artnet_transmit(instance* inst){
};
memcpy(frame.data, data->data.out, 512);
- if(sendto(artnet_fd[data->fd_index].fd, &frame, sizeof(frame), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){
+ if(sendto(artnet_fd[data->fd_index].fd, (uint8_t*) &frame, sizeof(frame), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){
fprintf(stderr, "Failed to output ArtNet frame for instance %s: %s\n", inst->name, strerror(errno));
}
@@ -319,7 +235,7 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v)
artnet_instance_data* data = (artnet_instance_data*) inst->impl;
if(!data->dest_len){
- fprintf(stderr, "ArtNet instance %s not enabled for output (%zu channel events)\n", inst->name, num);
+ fprintf(stderr, "ArtNet instance %s not enabled for output (%" PRIsize_t " channel events)\n", inst->name, num);
return 0;
}
@@ -384,7 +300,7 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){
}
if(!chan){
- fprintf(stderr, "Active channel %zu on %s not known to core\n", p, inst->name);
+ fprintf(stderr, "Active channel %" PRIsize_t " on %s not known to core\n", p, inst->name);
return 1;
}
@@ -456,7 +372,11 @@ static int artnet_handle(size_t num, managed_fd* fds){
}
} while(bytes_read > 0);
+ #ifdef _WIN32
+ if(bytes_read < 0 && WSAGetLastError() != WSAEWOULDBLOCK){
+ #else
if(bytes_read < 0 && errno != EAGAIN){
+ #endif
fprintf(stderr, "ArtNet failed to receive data: %s\n", strerror(errno));
}
@@ -527,7 +447,7 @@ static int artnet_start(){
}
}
- fprintf(stderr, "ArtNet backend registering %zu descriptors to core\n", artnet_fds);
+ fprintf(stderr, "ArtNet backend registering %" PRIsize_t " descriptors to core\n", artnet_fds);
for(u = 0; u < artnet_fds; u++){
if(mm_manage_fd(artnet_fd[u].fd, BACKEND_NAME, 1, (void*) u)){
goto bail;
diff --git a/backends/artnet.h b/backends/artnet.h
index 90aedd5..f6a6709 100644
--- a/backends/artnet.h
+++ b/backends/artnet.h
@@ -1,11 +1,13 @@
+#ifndef _WIN32
#include <sys/socket.h>
+#endif
#include "midimonster.h"
-int init();
+MM_PLUGIN_API int init();
static int artnet_configure(char* option, char* value);
static int artnet_configure_instance(instance* instance, char* option, char* value);
static instance* artnet_instance();
-static channel* artnet_channel(instance* instance, char* spec);
+static channel* artnet_channel(instance* instance, char* spec, uint8_t flags);
static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v);
static int artnet_handle(size_t num, managed_fd* fds);
static int artnet_start();
diff --git a/backends/artnet.md b/backends/artnet.md
new file mode 100644
index 0000000..90a7697
--- /dev/null
+++ b/backends/artnet.md
@@ -0,0 +1,41 @@
+### The `artnet` backend
+
+The ArtNet backend provides read-write access to the UDP-based ArtNet protocol for lighting
+fixture control.
+
+#### Global configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|-----------------------|
+| `bind` | `127.0.0.1 6454` | none | Binds a network address to listen for data. This option may be set multiple times, with each interface being assigned an index starting from 0 to be used with the `interface` instance configuration option. At least one interface is required for transmission. |
+| `net` | `0` | `0` | The default net to use |
+
+#### Instance configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|-----------------------|
+| `net` | `0` | `0` | ArtNet `net` to use |
+| `universe` | `0` | `0` | Universe identifier |
+| `destination` | `10.2.2.2` | none | Destination address for sent ArtNet frames. Setting this enables the universe for output |
+| `interface` | `1` | `0` | The bound address to use for data input/output |
+
+#### Channel specification
+
+A channel is specified by it's universe index. Channel indices start at 1 and end at 512.
+
+Example mapping:
+```
+net1.231 < net2.123
+```
+
+A 16-bit channel (spanning any two normal 8-bit channels in the same universe, also called a wide channel) may be mapped with the syntax
+```
+net1.1+2 > net2.5+123
+```
+
+A normal channel that is part of a wide channel can not be mapped individually.
+
+#### Known bugs / problems
+
+The minimum inter-frame-time is disregarded, as the packet rate is determined by the rate of incoming
+channel events. \ No newline at end of file
diff --git a/backends/evdev.c b/backends/evdev.c
index 979698f..0da5ae6 100644
--- a/backends/evdev.c
+++ b/backends/evdev.c
@@ -18,16 +18,13 @@
#define BACKEND_NAME "evdev"
-typedef union {
- struct {
- uint32_t pad;
- uint16_t type;
- uint16_t code;
- } fields;
- uint64_t label;
-} evdev_channel_ident;
-
-int init(){
+static struct {
+ uint8_t detect;
+} evdev_config = {
+ .detect = 0
+};
+
+MM_PLUGIN_API int init(){
backend evdev = {
.name = BACKEND_NAME,
.conf = evdev_configure,
@@ -40,6 +37,11 @@ int init(){
.shutdown = evdev_shutdown
};
+ if(sizeof(evdev_channel_ident) != sizeof(uint64_t)){
+ fprintf(stderr, "evdev channel identification union out of bounds\n");
+ return 1;
+ }
+
if(mm_backend_register(evdev)){
fprintf(stderr, "Failed to register evdev backend\n");
return 1;
@@ -49,7 +51,15 @@ int init(){
}
static int evdev_configure(char* option, char* value) {
- fprintf(stderr, "The evdev backend does not take any global configuration\n");
+ if(!strcmp(option, "detect")){
+ evdev_config.detect = 1;
+ if(!strcmp(value, "off")){
+ evdev_config.detect = 0;
+ }
+ return 0;
+ }
+
+ fprintf(stderr, "Unknown configuration option %s for evdev backend\n", option);
return 1;
}
@@ -176,23 +186,48 @@ static int evdev_configure_instance(instance* inst, char* option, char* value) {
return 1;
}
free(next_token);
+ return 0;
}
else if(!strcmp(option, "exclusive")){
if(data->input_fd >= 0 && libevdev_grab(data->input_ev, LIBEVDEV_GRAB)){
fprintf(stderr, "Failed to obtain exclusive device access on %s\n", inst->name);
}
data->exclusive = 1;
+ return 0;
+ }
+ else if(!strncmp(option, "relaxis.", 8)){
+ data->relative_axis = realloc(data->relative_axis, (data->relative_axes + 1) * sizeof(evdev_relaxis_config));
+ if(!data->relative_axis){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return 1;
+ }
+ data->relative_axis[data->relative_axes].inverted = 0;
+ data->relative_axis[data->relative_axes].code = libevdev_event_code_from_name(EV_REL, option + 8);
+ data->relative_axis[data->relative_axes].max = strtoll(value, &next_token, 0);
+ if(data->relative_axis[data->relative_axes].max < 0){
+ data->relative_axis[data->relative_axes].max *= -1;
+ data->relative_axis[data->relative_axes].inverted = 1;
+ }
+ data->relative_axis[data->relative_axes].current = strtoul(next_token, NULL, 0);
+ if(data->relative_axis[data->relative_axes].code < 0){
+ fprintf(stderr, "Failed to configure relative axis extents for %s.%s\n", inst->name, option + 8);
+ return 1;
+ }
+ data->relative_axes++;
+ return 0;
}
#ifndef EVDEV_NO_UINPUT
else if(!strcmp(option, "output")){
data->output_enabled = 1;
libevdev_set_name(data->output_proto, value);
+ return 0;
}
else if(!strcmp(option, "id")){
next_token = value;
libevdev_set_id_vendor(data->output_proto, strtol(next_token, &next_token, 0));
libevdev_set_id_product(data->output_proto, strtol(next_token, &next_token, 0));
libevdev_set_id_version(data->output_proto, strtol(next_token, &next_token, 0));
+ return 0;
}
else if(!strncmp(option, "axis.", 5)){
//value minimum maximum fuzz flat resolution
@@ -207,16 +242,14 @@ static int evdev_configure_instance(instance* inst, char* option, char* value) {
fprintf(stderr, "Failed to enable absolute axis %s for output\n", option + 5);
return 1;
}
+ return 0;
}
#endif
- else{
- fprintf(stderr, "Unknown configuration parameter %s for evdev backend\n", option);
- return 1;
- }
- return 0;
+ fprintf(stderr, "Unknown instance configuration parameter %s for evdev instance %s\n", option, inst->name);
+ return 1;
}
-static channel* evdev_channel(instance* inst, char* spec){
+static channel* evdev_channel(instance* inst, char* spec, uint8_t flags){
#ifndef EVDEV_NO_UINPUT
evdev_instance_data* data = (evdev_instance_data*) inst->impl;
#endif
@@ -273,21 +306,35 @@ static int evdev_push_event(instance* inst, evdev_instance_data* data, struct in
.fields.code = event.code
};
channel* chan = mm_channel(inst, ident.label, 0);
+ size_t axis;
if(chan){
val.raw.u64 = event.value;
switch(event.type){
case EV_REL:
- val.normalised = 0.5 + ((event.value < 0) ? 0.5 : -0.5);
+ for(axis = 0; axis < data->relative_axes; axis++){
+ if(data->relative_axis[axis].code == event.code){
+ if(data->relative_axis[axis].inverted){
+ event.value *= -1;
+ }
+ data->relative_axis[axis].current = clamp(data->relative_axis[axis].current + event.value, data->relative_axis[axis].max, 0);
+ val.normalised = (double) data->relative_axis[axis].current / (double) data->relative_axis[axis].max;
+ break;
+ }
+ }
+ if(axis == data->relative_axes){
+ val.normalised = 0.5 + ((event.value < 0) ? 0.5 : -0.5);
+ break;
+ }
break;
case EV_ABS:
range = libevdev_get_abs_maximum(data->input_ev, event.code) - libevdev_get_abs_minimum(data->input_ev, event.code);
- val.normalised = (event.value - libevdev_get_abs_minimum(data->input_ev, event.code)) / (double) range;
+ val.normalised = clamp((event.value - libevdev_get_abs_minimum(data->input_ev, event.code)) / (double) range, 1.0, 0.0);
break;
case EV_KEY:
case EV_SW:
default:
- val.normalised = 1.0 * event.value;
+ val.normalised = clamp(1.0 * event.value, 1.0, 0.0);
break;
}
@@ -297,6 +344,10 @@ static int evdev_push_event(instance* inst, evdev_instance_data* data, struct in
}
}
+ if(evdev_config.detect){
+ fprintf(stderr, "Incoming evdev data for channel %s.%s.%s\n", inst->name, libevdev_event_type_get_name(event.type), libevdev_event_code_get_name(event.type, event.code));
+ }
+
return 0;
}
@@ -327,6 +378,11 @@ static int evdev_handle(size_t num, managed_fd* fds){
read_flags = LIBEVDEV_READ_FLAG_SYNC;
}
+ //exclude synchronization events
+ if(ev.type == EV_SYN){
+ continue;
+ }
+
//handle event
if(evdev_push_event(inst, data, ev)){
return 1;
@@ -376,6 +432,10 @@ static int evdev_start(){
fds++;
}
+ if(data->input_fd <= 0 && !data->output_ev){
+ fprintf(stderr, "Instance %s has neither input nor output device set up\n", inst[u]->name);
+ }
+
}
fprintf(stderr, "evdev backend registered %zu descriptors to core\n", fds);
@@ -385,7 +445,7 @@ static int evdev_start(){
static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v) {
#ifndef EVDEV_NO_UINPUT
- size_t evt = 0;
+ size_t evt = 0, axis = 0;
evdev_instance_data* data = (evdev_instance_data*) inst->impl;
evdev_channel_ident ident = {
.label = 0
@@ -407,7 +467,20 @@ static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v)
switch(ident.fields.type){
case EV_REL:
- value = (v[evt].normalised < 0.5) ? -1 : ((v[evt].normalised > 0.5) ? 1 : 0);
+ for(axis = 0; axis < data->relative_axes; axis++){
+ if(data->relative_axis[axis].code == ident.fields.code){
+ value = (v[evt].normalised * data->relative_axis[axis].max) - data->relative_axis[axis].current;
+ data->relative_axis[axis].current = v[evt].normalised * data->relative_axis[axis].max;
+
+ if(data->relative_axis[axis].inverted){
+ value *= -1;
+ }
+ break;
+ }
+ }
+ if(axis == data->relative_axes){
+ value = (v[evt].normalised < 0.5) ? -1 : ((v[evt].normalised > 0.5) ? 1 : 0);
+ }
break;
case EV_ABS:
range = libevdev_get_abs_maximum(data->output_proto, ident.fields.code) - libevdev_get_abs_minimum(data->output_proto, ident.fields.code);
@@ -464,9 +537,12 @@ static int evdev_shutdown(){
libevdev_free(data->output_proto);
#endif
+ data->relative_axes = 0;
+ free(data->relative_axis);
free(data);
}
free(instances);
+ fprintf(stderr, "evdev backend shut down\n");
return 0;
}
diff --git a/backends/evdev.h b/backends/evdev.h
index c6e3a25..6504416 100644
--- a/backends/evdev.h
+++ b/backends/evdev.h
@@ -8,11 +8,11 @@
* disabled by building with -DEVDEV_NO_UINPUT
*/
-int init();
+MM_PLUGIN_API int init();
static int evdev_configure(char* option, char* value);
static int evdev_configure_instance(instance* instance, char* option, char* value);
static instance* evdev_instance();
-static channel* evdev_channel(instance* instance, char* spec);
+static channel* evdev_channel(instance* instance, char* spec, uint8_t flags);
static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v);
static int evdev_handle(size_t num, managed_fd* fds);
static int evdev_start();
@@ -24,10 +24,19 @@ static int evdev_shutdown();
#define UINPUT_MAX_NAME_SIZE 512
#endif
+typedef struct /*_evdev_relative_axis_config*/ {
+ uint8_t inverted;
+ int code;
+ int64_t max;
+ int64_t current;
+} evdev_relaxis_config;
+
typedef struct /*_evdev_instance_model*/ {
int input_fd;
struct libevdev* input_ev;
int exclusive;
+ size_t relative_axes;
+ evdev_relaxis_config* relative_axis;
int output_enabled;
#ifndef EVDEV_NO_UINPUT
@@ -35,3 +44,13 @@ typedef struct /*_evdev_instance_model*/ {
struct libevdev_uinput* output_ev;
#endif
} evdev_instance_data;
+
+typedef union {
+ struct {
+ uint32_t pad;
+ uint16_t type;
+ uint16_t code;
+ } fields;
+ uint64_t label;
+} evdev_channel_ident;
+
diff --git a/backends/evdev.md b/backends/evdev.md
new file mode 100644
index 0000000..d57201d
--- /dev/null
+++ b/backends/evdev.md
@@ -0,0 +1,86 @@
+### The `evdev` backend
+
+This backend allows using Linux `evdev` devices such as mouses, keyboards, gamepads and joysticks
+as input and output devices. All buttons and axes available to the Linux system are mappable.
+Output is provided by the `uinput` kernel module, which allows creation of virtual input devices.
+This functionality may require elevated privileges (such as special group membership or root access).
+
+#### 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. |
+
+#### Instance configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|---------------|-------------------------------------------------------|
+| `device` | `/dev/input/event1` | none | `evdev` device to use as input device |
+| `input` | `Xbox Wireless` | none | Presentation name of evdev device to use as input (prefix-matched) |
+| `output` | `My Input Device` | none | Output device presentation name. Setting this option enables the instance for output |
+| `exclusive` | `1` | `0` | Prevent other processes from using the device |
+| `id` | `0x1 0x2 0x3` | none | Set output device bus identification (Vendor, Product and Version), optional |
+| `axis.AXISNAME`| `34300 0 65536 255 4095` | none | Specify absolute axis details (see below) for output. This is required for any absolute axis to be output. |
+| `relaxis.AXISNAME`| `65534 32767` | none | Specify relative axis details (extent and optional initial value) for output and input (see below). |
+
+The absolute axis details configuration (e.g. `axis.ABS_X`) is required for any absolute axis on output-enabled
+instances. The configuration value contains, space-separated, the following values:
+
+* `value`: The value to assume for the axis until an event is received
+* `minimum`: The axis minimum value
+* `maximum`: The axis maximum value
+* `fuzz`: A value used for filtering the input stream
+* `flat`: An offset, below which all deviations will be ignored
+* `resolution`: Axis resolution in units per millimeter (or units per radian for rotational axes)
+
+If an axis is not used for output, this configuration can be omitted.
+
+For real devices, all of these parameters for every axis can be found by running `evtest` on the device.
+
+To use the input from relative axes in absolute-value based protocols, the backend needs a reference frame to
+convert the relative movements to absolute values. To invert the mapping of the relative axis, specify the `max` value
+as a negative number, for example:
+
+```
+relaxis.REL_X = -1024 512
+```
+
+If relative axes are used without specifying their extents, the channel will generate normalized values
+of `0`, `0.5` and `1` for any input less than, equal to and greater than `0`, respectively. As for output, only
+the values `-1`, `0` and `1` are generated for the same interval.
+
+
+#### Channel specification
+
+A channel is specified by its event type and event code, separated by `.`. For a complete list of event types and codes
+see the [kernel documentation](https://www.kernel.org/doc/html/v4.12/input/event-codes.html). The most interesting event types are
+
+* `EV_KEY` for keys and buttons
+* `EV_ABS` for absolute axes (such as Joysticks)
+* `EV_REL` for relative axes (such as Mouses)
+
+The `evtest` tool is useful to gather information on devices active on the local system, including names, types, codes
+and configuration supported by these devices.
+
+Example mapping:
+```
+ev1.EV_KEY.KEY_A > ev1.EV_ABS.ABS_X
+```
+
+Note that to map an absolute axis on an output-enabled instance, additional information such as the axis minimum
+and maximum are required. These must be specified in the instance configuration. When only mapping the instance
+as a channel input, this is not required.
+
+#### Known bugs / problems
+
+Creating an `evdev` output device requires elevated privileges, namely, write access to the system's
+`/dev/uinput`. Usually, this is granted for users in the `input` group and the `root` user.
+
+Input devices may synchronize logically connected event types (for example, X and Y axes) via `EV_SYN`-type
+events. The MIDIMonster also generates these events after processing channel events, but may not keep the original
+event grouping.
+
+`EV_KEY` key-down events are sent for normalized channel values over `0.9`.
+
+Extended event type values such as `EV_LED`, `EV_SND`, etc are recognized in the MIDIMonster configuration file
+but may or may not work with the internal channel mapping and normalization code.
diff --git a/backends/jack.c b/backends/jack.c
new file mode 100644
index 0000000..e7bed04
--- /dev/null
+++ b/backends/jack.c
@@ -0,0 +1,748 @@
+#include <string.h>
+#include <signal.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include "jack.h"
+#include <jack/midiport.h>
+#include <jack/metadata.h>
+
+#define BACKEND_NAME "jack"
+#define JACKEY_SIGNAL_TYPE "http://jackaudio.org/metadata/signal-type"
+
+#ifdef __APPLE__
+ #ifndef PTHREAD_MUTEX_ADAPTIVE_NP
+ #define PTHREAD_MUTEX_ADAPTIVE_NP PTHREAD_MUTEX_DEFAULT
+ #endif
+#endif
+
+//FIXME pitchbend range is somewhat oob
+
+static struct /*_mmjack_backend_cfg*/ {
+ unsigned verbosity;
+ volatile sig_atomic_t jack_shutdown;
+} config = {
+ .verbosity = 1,
+ .jack_shutdown = 0
+};
+
+MM_PLUGIN_API int init(){
+ backend mmjack = {
+ .name = BACKEND_NAME,
+ .conf = mmjack_configure,
+ .create = mmjack_instance,
+ .conf_instance = mmjack_configure_instance,
+ .channel = mmjack_channel,
+ .handle = mmjack_set,
+ .process = mmjack_handle,
+ .start = mmjack_start,
+ .shutdown = mmjack_shutdown
+ };
+
+ if(sizeof(mmjack_channel_ident) != sizeof(uint64_t)){
+ fprintf(stderr, "jack channel identification union out of bounds\n");
+ return 1;
+ }
+
+ //register backend
+ if(mm_backend_register(mmjack)){
+ fprintf(stderr, "Failed to register jack backend\n");
+ return 1;
+ }
+ return 0;
+}
+
+static void mmjack_message_print(const char* msg){
+ fprintf(stderr, "JACK message: %s\n", msg);
+}
+
+static void mmjack_message_ignore(const char* msg){
+}
+
+static int mmjack_midiqueue_append(mmjack_port* port, mmjack_channel_ident ident, uint16_t value){
+ //append events
+ if(port->queue_len == port->queue_alloc){
+ //extend the queue
+ port->queue = realloc(port->queue, (port->queue_len + JACK_MIDIQUEUE_CHUNK) * sizeof(mmjack_midiqueue));
+ if(!port->queue){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return 1;
+ }
+ port->queue_alloc += JACK_MIDIQUEUE_CHUNK;
+ }
+
+ port->queue[port->queue_len].ident.label = ident.label;
+ port->queue[port->queue_len].raw = value;
+ port->queue_len++;
+ DBGPF("Appended event to queue for %s, now at %" PRIsize_t " entries\n", port->name, port->queue_len);
+ return 0;
+}
+
+static int mmjack_process_midi(instance* inst, mmjack_port* port, size_t nframes, size_t* mark){
+ void* buffer = jack_port_get_buffer(port->port, nframes);
+ jack_nframes_t event_count = jack_midi_get_event_count(buffer);
+ jack_midi_event_t event;
+ jack_midi_data_t* event_data;
+ mmjack_channel_ident ident;
+ size_t u;
+ uint16_t value;
+
+ if(port->input){
+ if(event_count){
+ DBGPF("Reading %u MIDI events from jack port %s\n", event_count, port->name);
+ for(u = 0; u < event_count; u++){
+ ident.label = 0;
+ //read midi data from stream
+ jack_midi_event_get(&event, buffer, u);
+ //ident.fields.port set on output in mmjack_handle_midi
+ ident.fields.sub_channel = event.buffer[0] & 0x0F;
+ ident.fields.sub_type = event.buffer[0] & 0xF0;
+ ident.fields.sub_control = event.buffer[1];
+ value = event.buffer[2];
+ if(ident.fields.sub_type == 0x80){
+ ident.fields.sub_type = midi_note;
+ value = 0;
+ }
+ else if(ident.fields.sub_type == midi_pitchbend){
+ ident.fields.sub_control = 0;
+ value = event.buffer[1] | (event.buffer[2] << 7);
+ }
+ else if(ident.fields.sub_type == midi_aftertouch){
+ ident.fields.sub_control = 0;
+ value = event.buffer[1];
+ }
+ //append midi data
+ mmjack_midiqueue_append(port, ident, value);
+ }
+ port->mark = 1;
+ *mark = 1;
+ }
+ }
+ else{
+ //clear buffer
+ jack_midi_clear_buffer(buffer);
+
+ for(u = 0; u < port->queue_len; u++){
+ //build midi event
+ ident.label = port->queue[u].ident.label;
+ event_data = jack_midi_event_reserve(buffer, u, (ident.fields.sub_type == midi_aftertouch) ? 2 : 3);
+ if(!event_data){
+ fprintf(stderr, "Failed to reserve MIDI stream data\n");
+ return 1;
+ }
+ event_data[0] = ident.fields.sub_channel | ident.fields.sub_type;
+ if(ident.fields.sub_type == midi_pitchbend){
+ event_data[1] = port->queue[u].raw & 0x7F;
+ event_data[2] = (port->queue[u].raw >> 7) & 0x7F;
+ }
+ else if(ident.fields.sub_type == midi_aftertouch){
+ event_data[1] = port->queue[u].raw & 0x7F;
+ }
+ else{
+ event_data[1] = ident.fields.sub_control;
+ event_data[2] = port->queue[u].raw & 0x7F;
+ }
+ }
+
+ if(port->queue_len){
+ DBGPF("Wrote %" PRIsize_t " MIDI events to jack port %s\n", port->queue_len, port->name);
+ }
+ port->queue_len = 0;
+ }
+ return 0;
+}
+
+static int mmjack_process_cv(instance* inst, mmjack_port* port, size_t nframes, size_t* mark){
+ jack_default_audio_sample_t* audio_buffer = jack_port_get_buffer(port->port, nframes);
+ size_t u;
+
+ if(port->input){
+ //read updated data into the local buffer
+ //FIXME maybe we don't want to always use the first sample...
+ if((double) audio_buffer[0] != port->last){
+ port->last = audio_buffer[0];
+ port->mark = 1;
+ *mark = 1;
+ }
+ }
+ else{
+ for(u = 0; u < nframes; u++){
+ audio_buffer[u] = port->last;
+ }
+ }
+ return 0;
+}
+
+static int mmjack_process(jack_nframes_t nframes, void* instp){
+ instance* inst = (instance*) instp;
+ mmjack_instance_data* data = (mmjack_instance_data*) inst->impl;
+ size_t p, mark = 0;
+ int rv = 0;
+
+ //DBGPF("jack callback for %d frames on %s\n", nframes, inst->name);
+
+ for(p = 0; p < data->ports; p++){
+ pthread_mutex_lock(&data->port[p].lock);
+ switch(data->port[p].type){
+ case port_midi:
+ //DBGPF("Handling MIDI port %s.%s\n", inst->name, data->port[p].name);
+ rv |= mmjack_process_midi(inst, data->port + p, nframes, &mark);
+ break;
+ case port_cv:
+ //DBGPF("Handling CV port %s.%s\n", inst->name, data->port[p].name);
+ rv |= mmjack_process_cv(inst, data->port + p, nframes, &mark);
+ break;
+ default:
+ fprintf(stderr, "Unhandled jack port type in processing callback\n");
+ pthread_mutex_unlock(&data->port[p].lock);
+ return 1;
+ }
+ pthread_mutex_unlock(&data->port[p].lock);
+ }
+
+ //notify the main thread
+ if(mark){
+ DBGPF("Notifying handler thread for jack instance %s\n", inst->name);
+ send(data->fd, "c", 1, 0);
+ }
+ return rv;
+}
+
+static void mmjack_server_shutdown(void* inst){
+ fprintf(stderr, "jack server shutdown notification\n");
+ config.jack_shutdown = 1;
+}
+
+static int mmjack_configure(char* option, char* value){
+ if(!strcmp(option, "debug")){
+ if(!strcmp(value, "on")){
+ config.verbosity |= 2;
+ return 0;
+ }
+ config.verbosity &= ~2;
+ return 0;
+ }
+ if(!strcmp(option, "errors")){
+ if(!strcmp(value, "on")){
+ config.verbosity |= 1;
+ return 0;
+ }
+ config.verbosity &= ~1;
+ return 0;
+ }
+
+ fprintf(stderr, "Unknown jack backend option %s\n", option);
+ return 1;
+}
+
+static int mmjack_parse_portconfig(mmjack_port* port, char* spec){
+ char* token = NULL;
+
+ for(token = strtok(spec, " "); token; token = strtok(NULL, " ")){
+ if(!strcmp(token, "in")){
+ port->input = 1;
+ }
+ else if(!strcmp(token, "out")){
+ port->input = 0;
+ }
+ else if(!strcmp(token, "midi")){
+ port->type = port_midi;
+ }
+ else if(!strcmp(token, "osc")){
+ port->type = port_osc;
+ }
+ else if(!strcmp(token, "cv")){
+ port->type = port_cv;
+ }
+ else if(!strcmp(token, "max")){
+ token = strtok(NULL, " ");
+ if(!token){
+ fprintf(stderr, "jack port %s configuration missing argument\n", port->name);
+ return 1;
+ }
+ port->max = strtod(token, NULL);
+ }
+ else if(!strcmp(token, "min")){
+ token = strtok(NULL, " ");
+ if(!token){
+ fprintf(stderr, "jack port %s configuration missing argument\n", port->name);
+ return 1;
+ }
+ port->min = strtod(token, NULL);
+ }
+ else{
+ fprintf(stderr, "Unknown jack channel configuration token %s on port %s\n", token, port->name);
+ return 1;
+ }
+ }
+
+ if(port->type == port_none){
+ fprintf(stderr, "jack channel %s assigned no port type\n", port->name);
+ return 1;
+ }
+ return 0;
+}
+
+static int mmjack_configure_instance(instance* inst, char* option, char* value){
+ mmjack_instance_data* data = (mmjack_instance_data*) inst->impl;
+ size_t p;
+
+ if(!strcmp(option, "name")){
+ if(data->client_name){
+ free(data->client_name);
+ }
+ data->client_name = strdup(value);
+ return 0;
+ }
+ else if(!strcmp(option, "server")){
+ if(data->server_name){
+ free(data->server_name);
+ }
+ data->server_name = strdup(value);
+ return 0;
+ }
+
+ //register new port, first check for unique name
+ for(p = 0; p < data->ports; p++){
+ if(!strcmp(data->port[p].name, option)){
+ fprintf(stderr, "jack instance %s has duplicate port %s\n", inst->name, option);
+ return 1;
+ }
+ }
+ if(strchr(option, '.')){
+ fprintf(stderr, "Invalid jack channel spec %s.%s\n", inst->name, option);
+ }
+
+ //add port to registry
+ //TODO for OSC ports we need to configure subchannels for each message
+ data->port = realloc(data->port, (data->ports + 1) * sizeof(mmjack_port));
+ if(!data->port){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return 1;
+ }
+ data->port[data->ports].name = strdup(option);
+ if(!data->port[data->ports].name){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return 1;
+ }
+ if(mmjack_parse_portconfig(data->port + p, value)){
+ return 1;
+ }
+ data->ports++;
+ return 0;
+}
+
+static instance* mmjack_instance(){
+ instance* inst = mm_instance();
+ if(!inst){
+ return NULL;
+ }
+
+ inst->impl = calloc(1, sizeof(mmjack_instance_data));
+ if(!inst->impl){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return NULL;
+ }
+
+ return inst;
+}
+
+static int mmjack_parse_midispec(mmjack_channel_ident* ident, char* spec){
+ char* next_token = NULL;
+
+ if(!strncmp(spec, "ch", 2)){
+ next_token = spec + 2;
+ if(!strncmp(spec, "channel", 7)){
+ next_token = spec + 7;
+ }
+ }
+
+ if(!next_token){
+ fprintf(stderr, "Invalid jack MIDI spec %s\n", spec);
+ return 1;
+ }
+
+ ident->fields.sub_channel = strtoul(next_token, &next_token, 10);
+ if(ident->fields.sub_channel > 15){
+ fprintf(stderr, "Invalid jack MIDI spec %s, channel out of range\n", spec);
+ return 1;
+ }
+
+ if(*next_token != '.'){
+ fprintf(stderr, "Invalid jack MIDI spec %s\n", spec);
+ return 1;
+ }
+
+ next_token++;
+
+ if(!strncmp(next_token, "cc", 2)){
+ ident->fields.sub_type = midi_cc;
+ next_token += 2;
+ }
+ else if(!strncmp(next_token, "note", 4)){
+ ident->fields.sub_type = midi_note;
+ next_token += 4;
+ }
+ else if(!strncmp(next_token, "pressure", 8)){
+ ident->fields.sub_type = midi_pressure;
+ next_token += 8;
+ }
+ else if(!strncmp(next_token, "pitch", 5)){
+ ident->fields.sub_type = midi_pitchbend;
+ }
+ else if(!strncmp(next_token, "aftertouch", 10)){
+ ident->fields.sub_type = midi_aftertouch;
+ }
+ else{
+ fprintf(stderr, "Unknown jack MIDI control type in spec %s\n", spec);
+ return 1;
+ }
+
+ ident->fields.sub_control = strtoul(next_token, NULL, 10);
+
+ if(ident->fields.sub_type == midi_none
+ || ident->fields.sub_control > 127){
+ fprintf(stderr, "Invalid jack MIDI spec %s\n", spec);
+ return 1;
+ }
+ return 0;
+}
+
+static channel* mmjack_channel(instance* inst, char* spec, uint8_t flags){
+ mmjack_instance_data* data = (mmjack_instance_data*) inst->impl;
+ mmjack_channel_ident ident = {
+ .label = 0
+ };
+ size_t u;
+
+ for(u = 0; u < data->ports; u++){
+ if(!strncmp(spec, data->port[u].name, strlen(data->port[u].name))
+ && (spec[strlen(data->port[u].name)] == '.' || spec[strlen(data->port[u].name)] == 0)){
+ ident.fields.port = u;
+ break;
+ }
+ }
+
+ if(u == data->ports){
+ fprintf(stderr, "jack port %s.%s not found\n", inst->name, spec);
+ return NULL;
+ }
+
+ if(data->port[u].type == port_midi){
+ //parse midi subspec
+ if(!spec[strlen(data->port[u].name)]
+ || mmjack_parse_midispec(&ident, spec + strlen(data->port[u].name) + 1)){
+ return NULL;
+ }
+ }
+ else if(data->port[u].type == port_osc){
+ //TODO parse osc subspec
+ }
+
+ return mm_channel(inst, ident.label, 1);
+}
+
+static int mmjack_set(instance* inst, size_t num, channel** c, channel_value* v){
+ mmjack_instance_data* data = (mmjack_instance_data*) inst->impl;
+ mmjack_channel_ident ident = {
+ .label = 0
+ };
+ size_t u;
+ double range;
+ uint16_t value;
+
+ for(u = 0; u < num; u++){
+ ident.label = c[u]->ident;
+
+ if(data->port[ident.fields.port].input){
+ fprintf(stderr, "jack port %s.%s is an input port, no output is possible\n", inst->name, data->port[ident.fields.port].name);
+ continue;
+ }
+ range = data->port[ident.fields.port].max - data->port[ident.fields.port].min;
+
+ pthread_mutex_lock(&data->port[ident.fields.port].lock);
+ switch(data->port[ident.fields.port].type){
+ case port_cv:
+ //scale value to given range
+ data->port[ident.fields.port].last = (range * v[u].normalised) + data->port[ident.fields.port].min;
+ DBGPF("CV port %s updated to %f\n", data->port[ident.fields.port].name, data->port[ident.fields.port].last);
+ break;
+ case port_midi:
+ value = v[u].normalised * 127.0;
+ if(ident.fields.sub_type == midi_pitchbend){
+ value = ((uint16_t)(v[u].normalised * 16384.0));
+ }
+ if(mmjack_midiqueue_append(data->port + ident.fields.port, ident, value)){
+ pthread_mutex_unlock(&data->port[ident.fields.port].lock);
+ return 1;
+ }
+ break;
+ default:
+ fprintf(stderr, "No handler implemented for jack port type %s.%s\n", inst->name, data->port[ident.fields.port].name);
+ break;
+ }
+ pthread_mutex_unlock(&data->port[ident.fields.port].lock);
+ }
+
+ return 0;
+}
+
+static void mmjack_handle_midi(instance* inst, size_t index, mmjack_port* port){
+ size_t u;
+ channel* chan = NULL;
+ channel_value val;
+
+ for(u = 0; u < port->queue_len; u++){
+ port->queue[u].ident.fields.port = index;
+ chan = mm_channel(inst, port->queue[u].ident.label, 0);
+ if(chan){
+ if(port->queue[u].ident.fields.sub_type == midi_pitchbend){
+ val.normalised = ((double)port->queue[u].raw) / 16384.0;
+ }
+ else{
+ val.normalised = ((double)port->queue[u].raw) / 127.0;
+ }
+ DBGPF("Pushing MIDI channel %d type %02X control %d value %f raw %d label %" PRIu64 "\n",
+ port->queue[u].ident.fields.sub_channel,
+ port->queue[u].ident.fields.sub_type,
+ port->queue[u].ident.fields.sub_control,
+ val.normalised,
+ port->queue[u].raw,
+ port->queue[u].ident.label);
+ if(mm_channel_event(chan, val)){
+ fprintf(stderr, "Failed to push MIDI event to core on jack port %s.%s\n", inst->name, port->name);
+ }
+ }
+ }
+
+ if(port->queue_len){
+ DBGPF("Pushed %" PRIsize_t " MIDI events to core for jack port %s.%s\n", port->queue_len, inst->name, port->name);
+ }
+ port->queue_len = 0;
+}
+
+static void mmjack_handle_cv(instance* inst, size_t index, mmjack_port* port){
+ mmjack_channel_ident ident = {
+ .fields.port = index
+ };
+ double range;
+ channel_value val;
+
+ channel* chan = mm_channel(inst, ident.label, 0);
+ if(!chan){
+ //this might happen if a channel is registered but not mapped
+ DBGPF("Failed to match jack CV channel %s.%s to core channel\n", inst->name, port->name);
+ return;
+ }
+
+ //normalize value
+ range = port->max - port->min;
+ val.normalised = port->last - port->min;
+ val.normalised /= range;
+ val.normalised = clamp(val.normalised, 1.0, 0.0);
+ DBGPF("Pushing CV channel %s value %f raw %f min %f max %f\n", port->name, val.normalised, port->last, port->min, port->max);
+ if(mm_channel_event(chan, val)){
+ fprintf(stderr, "Failed to push CV event to core for %s.%s\n", inst->name, port->name);
+ }
+}
+
+static int mmjack_handle(size_t num, managed_fd* fds){
+ size_t u, p;
+ instance* inst = NULL;
+ mmjack_instance_data* data = NULL;
+ 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){
+ fprintf(stderr, "Failed to receive on feedback socket for instance %s\n", 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:
+ fprintf(stderr, "Output handler not implemented for unknown jack channel type on %s.%s\n", inst->name, data->port[p].name);
+ break;
+ }
+
+ data->port[p].mark = 0;
+ pthread_mutex_unlock(&data->port[p].lock);
+ }
+ }
+ }
+ }
+
+ if(config.jack_shutdown){
+ fprintf(stderr, "JACK server disconnected\n");
+ return 1;
+ }
+ return 0;
+}
+
+static int mmjack_start(){
+ int rv = 1, feedback_fd[2];
+ size_t n, u, p;
+ instance** inst = NULL;
+ pthread_mutexattr_t mutex_attr;
+ mmjack_instance_data* data = NULL;
+ jack_status_t error;
+
+ //set jack logging functions
+ jack_set_error_function(mmjack_message_ignore);
+ if(config.verbosity & 1){
+ jack_set_error_function(mmjack_message_print);
+ }
+ jack_set_info_function(mmjack_message_ignore);
+ if(config.verbosity & 2){
+ jack_set_info_function(mmjack_message_print);
+ }
+
+ //prepare mutex attributes because the initializer macro for adaptive mutexes is a GNU extension...
+ if(pthread_mutexattr_init(&mutex_attr)
+ || pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_ADAPTIVE_NP)){
+ fprintf(stderr, "Failed to initialize mutex attributes\n");
+ goto bail;
+ }
+
+ //fetch all instances
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ goto bail;
+ }
+
+ for(u = 0; u < n; u++){
+ data = (mmjack_instance_data*) inst[u]->impl;
+
+ //connect to the jack server
+ data->client = jack_client_open(data->client_name ? data->client_name : JACK_DEFAULT_CLIENT_NAME,
+ JackServerName | JackNoStartServer,
+ &error,
+ data->server_name ? data->server_name : JACK_DEFAULT_SERVER_NAME);
+
+ if(!data->client){
+ //TODO pretty-print failures
+ fprintf(stderr, "jack backend failed to connect to server, return status %u\n", error);
+ goto bail;
+ }
+
+ //set up the feedback fd
+ if(socketpair(AF_LOCAL, SOCK_DGRAM, 0, feedback_fd)){
+ fprintf(stderr, "Failed to create feedback socket pair\n");
+ goto bail;
+ }
+
+ data->fd = feedback_fd[0];
+ if(mm_manage_fd(feedback_fd[1], BACKEND_NAME, 1, inst[u])){
+ fprintf(stderr, "jack backend failed to register feedback fd with core\n");
+ goto bail;
+ }
+
+ //connect jack callbacks
+ jack_set_process_callback(data->client, mmjack_process, inst[u]);
+ jack_on_shutdown(data->client, mmjack_server_shutdown, inst[u]);
+
+ fprintf(stderr, "jack instance %s assigned client name %s\n", inst[u]->name, jack_get_client_name(data->client));
+
+ //create and initialize jack ports
+ for(p = 0; p < data->ports; p++){
+ if(pthread_mutex_init(&(data->port[p].lock), &mutex_attr)){
+ fprintf(stderr, "Failed to create port mutex\n");
+ goto bail;
+ }
+
+ data->port[p].port = jack_port_register(data->client,
+ data->port[p].name,
+ (data->port[p].type == port_cv) ? JACK_DEFAULT_AUDIO_TYPE : JACK_DEFAULT_MIDI_TYPE,
+ data->port[p].input ? JackPortIsInput : JackPortIsOutput,
+ 0);
+
+ jack_set_property(data->client, jack_port_uuid(data->port[p].port), JACKEY_SIGNAL_TYPE, "CV", "text/plain");
+
+ if(!data->port[p].port){
+ fprintf(stderr, "Failed to create jack port %s.%s\n", inst[u]->name, data->port[p].name);
+ goto bail;
+ }
+ }
+
+ //do the thing
+ if(jack_activate(data->client)){
+ fprintf(stderr, "Failed to activate jack client for instance %s\n", inst[u]->name);
+ goto bail;
+ }
+ }
+
+ fprintf(stderr, "jack backend registered %" PRIsize_t " descriptors to core\n", n);
+ rv = 0;
+bail:
+ pthread_mutexattr_destroy(&mutex_attr);
+ free(inst);
+ return rv;
+}
+
+static int mmjack_shutdown(){
+ size_t n, u, p;
+ instance** inst = NULL;
+ mmjack_instance_data* data = NULL;
+
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ return 1;
+ }
+
+ for(u = 0; u < n; u++){
+ data = (mmjack_instance_data*) inst[u]->impl;
+
+ //deactivate client to stop processing before free'ing channel data
+ if(data->client){
+ jack_deactivate(data->client);
+ }
+
+ //iterate and close ports
+ for(p = 0; p < data->ports; p++){
+ jack_remove_property(data->client, jack_port_uuid(data->port[p].port), JACKEY_SIGNAL_TYPE);
+ if(data->port[p].port){
+ jack_port_unregister(data->client, data->port[p].port);
+ }
+ free(data->port[p].name);
+ data->port[p].name = NULL;
+
+ free(data->port[p].queue);
+ data->port[p].queue = NULL;
+ data->port[p].queue_alloc = data->port[p].queue_len = 0;
+
+ pthread_mutex_destroy(&data->port[p].lock);
+ }
+
+ //terminate jack connection
+ if(data->client){
+ jack_client_close(data->client);
+ }
+
+ //clean up instance data
+ free(data->server_name);
+ data->server_name = NULL;
+ free(data->client_name);
+ data->client_name = NULL;
+ close(data->fd);
+ data->fd = -1;
+ }
+
+ free(inst);
+
+ fprintf(stderr, "jack backend shut down\n");
+ return 0;
+}
diff --git a/backends/jack.h b/backends/jack.h
new file mode 100644
index 0000000..a7f3e8b
--- /dev/null
+++ b/backends/jack.h
@@ -0,0 +1,76 @@
+#include "midimonster.h"
+#include <jack/jack.h>
+#include <pthread.h>
+
+MM_PLUGIN_API int init();
+static int mmjack_configure(char* option, char* value);
+static int mmjack_configure_instance(instance* inst, char* option, char* value);
+static instance* mmjack_instance();
+static channel* mmjack_channel(instance* inst, char* spec, uint8_t flags);
+static int mmjack_set(instance* inst, size_t num, channel** c, channel_value* v);
+static int mmjack_handle(size_t num, managed_fd* fds);
+static int mmjack_start();
+static int mmjack_shutdown();
+
+#define JACK_DEFAULT_CLIENT_NAME "MIDIMonster"
+#define JACK_DEFAULT_SERVER_NAME "default"
+#define JACK_MIDIQUEUE_CHUNK 10
+
+enum /*mmjack_midi_channel_type*/ {
+ midi_none = 0,
+ midi_note = 0x90,
+ midi_cc = 0xB0,
+ midi_pressure = 0xA0,
+ midi_aftertouch = 0xD0,
+ midi_pitchbend = 0xE0
+};
+
+typedef union {
+ struct {
+ uint32_t port;
+ uint8_t pad;
+ uint8_t sub_type;
+ uint8_t sub_channel;
+ uint8_t sub_control;
+ } fields;
+ uint64_t label;
+} mmjack_channel_ident;
+
+typedef enum /*_mmjack_port_type*/ {
+ port_none = 0,
+ port_midi,
+ port_osc,
+ port_cv
+} mmjack_port_type;
+
+typedef struct /*_mmjack_midiqueue_entry*/ {
+ mmjack_channel_ident ident;
+ uint16_t raw;
+} mmjack_midiqueue;
+
+typedef struct /*_mmjack_port_data*/ {
+ char* name;
+ mmjack_port_type type;
+ uint8_t input;
+ jack_port_t* port;
+
+ double max;
+ double min;
+ uint8_t mark;
+ double last;
+ size_t queue_len;
+ size_t queue_alloc;
+ mmjack_midiqueue* queue;
+
+ pthread_mutex_t lock;
+} mmjack_port;
+
+typedef struct /*_jack_instance_data*/ {
+ char* server_name;
+ char* client_name;
+ int fd;
+
+ jack_client_t* client;
+ size_t ports;
+ mmjack_port* port;
+} mmjack_instance_data;
diff --git a/backends/jack.md b/backends/jack.md
new file mode 100644
index 0000000..b6ff5a9
--- /dev/null
+++ b/backends/jack.md
@@ -0,0 +1,84 @@
+### The `jack` backend
+
+This backend provides read-write access to the JACK Audio Connection Kit low-latency audio transport server for the
+transport of control data via either JACK midi ports or control voltage (CV) inputs and outputs.
+
+#### Global configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|-----------------------|
+| `debug` | `on` | `off` | Print `info` level notices from the JACK connection |
+| `errors` | `on` | `off` | Print `error` level notices from the JACK connection |
+
+#### Instance configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|-----------------------|
+| `name` | `Controller` | `MIDIMonster` | Client name for the JACK connection |
+| `server` | `jackserver` | `default` | JACK server identifier to connect to |
+
+Channels (corresponding to JACK ports) need to be configured with their type and, if applicable, value limits.
+To configure a port, specify it in the instance configuration using the following syntax:
+
+```
+port_name = <type> <direction> min <minimum> max <maximum>
+```
+
+Port names may be any string except for the instance configuration keywords `name` and `server`.
+
+The following `type` values are currently supported:
+
+* `midi`: JACK MIDI port for transmitting MIDI event messages
+* `cv`: JACK audio port for transmitting DC offset "control voltage" samples (requires `min`/`max` configuration)
+
+`direction` may be one of `in` or `out`, as seen from the perspective of the MIDIMonster core, thus
+`in` means data is being read from the JACK server and `out` transfers data into the JACK server.
+
+The following example instance configuration would create a MIDI port sending data into JACK, a control voltage output
+sending data between `-1` and `1`, and a control voltage input receiving data with values between `0` and `10`.
+
+```
+midi_out = midi out
+cv_out = cv out min -1 max 1
+cv_in = cv in min 0.0 max 10.0
+```
+
+Input CV samples outside the configured range will be clipped. The MIDIMonster will not generate output CV samples
+outside of the configured range.
+
+#### Channel specification
+
+CV ports are exposed as single MIDIMonster channel and directly map to their normalised values.
+
+MIDI ports provide subchannels for the various MIDI controls available. Each MIDI port carries
+16 MIDI channels (numbered 0 through 15), each of which has 128 note controls (numbered 0 through 127),
+corresponding pressure controls for each note, 128 control change (CC) controls (numbered likewise),
+one channel wide "aftertouch" control and one channel-wide pitchbend control.
+
+A MIDI port subchannel 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 following values are recognized for `type`:
+
+* `cc` - Control Changes
+* `note` - Note On/Off messages
+* `pressure` - Note pressure/aftertouch messages
+* `aftertouch` - Channel-wide aftertouch messages
+* `pitch` - Channel pitchbend messages
+
+The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel<channel>.<type>`.
+
+Example mappings:
+```
+jack1.cv_in > jack1.midi_out.ch0.note3
+jack1.midi_in.ch0.pitch > jack1.cv_out
+```
+
+The MIDI subchannel syntax is intentionally kept compatible to the different MIDI backends also supported
+by the MIDIMonster
+
+#### Known bugs / problems
+
+While JACK has rudimentary capabilities for transporting OSC messages, configuring and parsing such channels
+with this backend would take a great amount of dedicated syntax & code. CV ports can provide fine-grained single
+control channels as an alternative to MIDI. This feature may be implemented at some point in the future.
diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c
new file mode 100644
index 0000000..ccbeb52
--- /dev/null
+++ b/backends/libmmbackend.c
@@ -0,0 +1,583 @@
+#include "libmmbackend.h"
+
+void mmbackend_parse_hostspec(char* spec, char** host, char** port){
+ size_t u = 0;
+
+ if(!spec || !host || !port){
+ return;
+ }
+
+ *port = NULL;
+
+ //skip leading spaces
+ for(; spec[u] && isspace(spec[u]); u++){
+ }
+
+ if(!spec[u]){
+ *host = NULL;
+ return;
+ }
+
+ *host = spec + u;
+
+ //scan until string end or space
+ for(; spec[u] && !isspace(spec[u]); u++){
+ }
+
+ //if space, the rest should be the port
+ if(spec[u]){
+ spec[u] = 0;
+ *port = spec + u + 1;
+ }
+}
+
+int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len){
+ struct addrinfo* head;
+ struct addrinfo hints = {
+ .ai_family = AF_UNSPEC
+ };
+
+ int error = getaddrinfo(host, port, &hints, &head);
+ if(error || !head){
+ fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(error));
+ return 1;
+ }
+
+ memcpy(addr, head->ai_addr, head->ai_addrlen);
+ if(len){
+ *len = head->ai_addrlen;
+ }
+
+ freeaddrinfo(head);
+ return 0;
+}
+
+int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast){
+ int fd = -1, status, yes = 1;
+ struct addrinfo hints = {
+ .ai_family = AF_UNSPEC,
+ .ai_socktype = socktype,
+ .ai_flags = (listener ? AI_PASSIVE : 0)
+ };
+ struct addrinfo *info, *addr_it;
+
+ status = getaddrinfo(host, port, &hints, &info);
+ if(status){
+ fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(status));
+ return -1;
+ }
+
+ //traverse the result list
+ for(addr_it = info; addr_it; addr_it = addr_it->ai_next){
+ fd = socket(addr_it->ai_family, addr_it->ai_socktype, addr_it->ai_protocol);
+ if(fd < 0){
+ continue;
+ }
+
+ //set required socket options
+ yes = 1;
+ if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){
+ fprintf(stderr, "Failed to enable SO_REUSEADDR on socket\n");
+ }
+
+ if(mcast){
+ yes = 1;
+ if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){
+ fprintf(stderr, "Failed to enable SO_BROADCAST on socket\n");
+ }
+
+ yes = 0;
+ if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){
+ fprintf(stderr, "Failed to disable IP_MULTICAST_LOOP on socket: %s\n", strerror(errno));
+ }
+ }
+
+ if(listener){
+ status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen);
+ if(status < 0){
+ close(fd);
+ continue;
+ }
+ }
+ else{
+ status = connect(fd, addr_it->ai_addr, addr_it->ai_addrlen);
+ if(status < 0){
+ close(fd);
+ continue;
+ }
+ }
+
+ break;
+ }
+ freeaddrinfo(info);
+
+ if(!addr_it){
+ fprintf(stderr, "Failed to create socket for %s port %s\n", host, port);
+ return -1;
+ }
+
+ //set nonblocking
+ #ifdef _WIN32
+ u_long mode = 1;
+ if(ioctlsocket(fd, FIONBIO, &mode) != NO_ERROR){
+ closesocket(fd);
+ return 1;
+ }
+ #else
+ int flags = fcntl(fd, F_GETFL, 0);
+ if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){
+ fprintf(stderr, "Failed to set socket nonblocking\n");
+ close(fd);
+ return -1;
+ }
+ #endif
+
+ return fd;
+}
+
+int mmbackend_send(int fd, uint8_t* data, size_t length){
+ ssize_t total = 0, sent;
+ while(total < length){
+ sent = send(fd, data + total, length - total, 0);
+ if(sent < 0){
+ fprintf(stderr, "Failed to send: %s\n", strerror(errno));
+ return 1;
+ }
+ total += sent;
+ }
+ return 0;
+}
+
+int mmbackend_send_str(int fd, char* data){
+ return mmbackend_send(fd, (uint8_t*) data, strlen(data));
+}
+
+json_type json_identify(char* json, size_t length){
+ size_t n;
+
+ //skip leading blanks
+ for(n = 0; json[n] && n < length && isspace(json[n]); n++){
+ }
+
+ if(n == length){
+ return JSON_INVALID;
+ }
+
+ switch(json[n]){
+ case '{':
+ return JSON_OBJECT;
+ case '[':
+ return JSON_ARRAY;
+ case '"':
+ return JSON_STRING;
+ case '-':
+ case '+':
+ return JSON_NUMBER;
+ default:
+ //true false null number
+ if(!strncmp(json + n, "true", 4)
+ || !strncmp(json + n, "false", 5)){
+ return JSON_BOOL;
+ }
+ else if(!strncmp(json + n, "null", 4)){
+ return JSON_NULL;
+ }
+ //a bit simplistic but it should do
+ if(isdigit(json[n])){
+ return JSON_NUMBER;
+ }
+ }
+ return JSON_INVALID;
+}
+
+size_t json_validate(char* json, size_t length){
+ switch(json_identify(json, length)){
+ case JSON_STRING:
+ return json_validate_string(json, length);
+ case JSON_ARRAY:
+ return json_validate_array(json, length);
+ case JSON_OBJECT:
+ return json_validate_object(json, length);
+ case JSON_INVALID:
+ return 0;
+ default:
+ return json_validate_value(json, length);
+ }
+}
+
+size_t json_validate_string(char* json, size_t length){
+ size_t string_length = 0, offset;
+
+ //skip leading whitespace
+ for(offset = 0; json[offset] && offset < length && isspace(json[offset]); offset++){
+ }
+
+ if(offset == length || json[offset] != '"'){
+ return 0;
+ }
+
+ //find terminating quotation mark not preceded by escape
+ for(string_length = 1; offset + string_length < length
+ && isprint(json[offset + string_length])
+ && (json[offset + string_length] != '"' || json[offset + string_length - 1] == '\\'); string_length++){
+ }
+
+ //complete string found
+ if(json[offset + string_length] == '"' && json[offset + string_length - 1] != '\\'){
+ return offset + string_length + 1;
+ }
+
+ return 0;
+}
+
+size_t json_validate_array(char* json, size_t length){
+ size_t offset = 0;
+
+ //skip leading whitespace
+ for(offset = 0; json[offset] && offset < length && isspace(json[offset]); offset++){
+ }
+
+ if(offset == length || json[offset] != '['){
+ return 0;
+ }
+
+ for(offset++; offset < length; offset++){
+ offset += json_validate(json + offset, length - offset);
+
+ //skip trailing whitespace, find terminator
+ for(; offset < length && isspace(json[offset]); offset++){
+ }
+
+ if(json[offset] == ','){
+ continue;
+ }
+
+ if(json[offset] == ']'){
+ return offset + 1;
+ }
+
+ break;
+ }
+
+ return 0;
+}
+
+size_t json_validate_object(char* json, size_t length){
+ size_t offset = 0;
+
+ //skip whitespace
+ for(offset = 0; json[offset] && isspace(json[offset]); offset++){
+ }
+
+ if(offset == length || json[offset] != '{'){
+ return 0;
+ }
+
+ for(offset++; offset < length; offset++){
+ if(json_identify(json + offset, length - offset) != JSON_STRING){
+ //still could be an empty object...
+ for(; offset < length && isspace(json[offset]); offset++){
+ }
+ if(json[offset] == '}'){
+ return offset + 1;
+ }
+ return 0;
+ }
+ offset += json_validate(json + offset, length - offset);
+
+ //find value separator
+ for(; offset < length && isspace(json[offset]); offset++){
+ }
+
+ if(json[offset] != ':'){
+ return 0;
+ }
+
+ offset++;
+ offset += json_validate(json + offset, length - offset);
+
+ //skip trailing whitespace
+ for(; json[offset] && isspace(json[offset]); offset++){
+ }
+
+ if(json[offset] == '}'){
+ return offset + 1;
+ }
+ else if(json[offset] != ','){
+ return 0;
+ }
+ }
+ return 0;
+}
+
+size_t json_validate_value(char* json, size_t length){
+ size_t offset = 0, value_length;
+
+ //skip leading whitespace
+ for(offset = 0; json[offset] && offset < length && isspace(json[offset]); offset++){
+ }
+
+ if(offset == length){
+ return 0;
+ }
+
+ //match complete values
+ if(length - offset >= 4 && !strncmp(json + offset, "null", 4)){
+ return offset + 4;
+ }
+ else if(length - offset >= 4 && !strncmp(json + offset, "true", 4)){
+ return offset + 4;
+ }
+ else if(length - offset >= 5 && !strncmp(json + offset, "false", 5)){
+ return offset + 5;
+ }
+
+ if(json[offset] == '-' || isdigit(json[offset])){
+ //json number parsing is dumb.
+ for(value_length = 1; offset + value_length < length &&
+ (isdigit(json[offset + value_length])
+ || json[offset + value_length] == '+'
+ || json[offset + value_length] == '-'
+ || json[offset + value_length] == '.'
+ || tolower(json[offset + value_length]) == 'e'); value_length++){
+ }
+
+ if(value_length > 0){
+ return offset + value_length;
+ }
+ }
+
+ return 0;
+}
+
+size_t json_obj_offset(char* json, char* key){
+ size_t offset = 0;
+ uint8_t match = 0;
+
+ //skip whitespace
+ for(offset = 0; json[offset] && isspace(json[offset]); offset++){
+ }
+
+ if(json[offset] != '{'){
+ return 0;
+ }
+ offset++;
+
+ while(json_identify(json + offset, strlen(json + offset)) == JSON_STRING){
+ //skip to key begin
+ for(; json[offset] && json[offset] != '"'; offset++){
+ }
+
+ if(!strncmp(json + offset + 1, key, strlen(key)) && json[offset + 1 + strlen(key)] == '"'){
+ //key found
+ match = 1;
+ }
+
+ offset += json_validate_string(json + offset, strlen(json + offset));
+
+ //skip to value separator
+ for(; json[offset] && json[offset] != ':'; offset++){
+ }
+
+ //skip whitespace
+ for(offset++; json[offset] && isspace(json[offset]); offset++){
+ }
+
+ if(match){
+ return offset;
+ }
+
+ //add length of value
+ offset += json_validate(json + offset, strlen(json + offset));
+
+ //skip trailing whitespace
+ for(; json[offset] && isspace(json[offset]); offset++){
+ }
+
+ if(json[offset] == ','){
+ offset++;
+ continue;
+ }
+
+ break;
+ }
+
+ return 0;
+}
+
+size_t json_array_offset(char* json, uint64_t key){
+ size_t offset = 0, index = 0;
+
+ //skip leading whitespace
+ for(offset = 0; json[offset] && isspace(json[offset]); offset++){
+ }
+
+ if(json[offset] != '['){
+ return 0;
+ }
+
+ for(offset++; index <= key; offset++){
+ //skip whitespace
+ for(; json[offset] && isspace(json[offset]); offset++){
+ }
+
+ if(index == key){
+ return offset;
+ }
+
+ offset += json_validate(json + offset, strlen(json + offset));
+
+ //skip trailing whitespace, find terminator
+ for(; json[offset] && isspace(json[offset]); offset++){
+ }
+
+ if(json[offset] != ','){
+ break;
+ }
+ index++;
+ }
+
+ return 0;
+}
+
+json_type json_obj(char* json, char* key){
+ size_t offset = json_obj_offset(json, key);
+ if(offset){
+ return json_identify(json + offset, strlen(json + offset));
+ }
+ return JSON_INVALID;
+}
+
+json_type json_array(char* json, uint64_t key){
+ size_t offset = json_array_offset(json, key);
+ if(offset){
+ return json_identify(json + offset, strlen(json + offset));
+ }
+ return JSON_INVALID;
+}
+
+uint8_t json_obj_bool(char* json, char* key, uint8_t fallback){
+ size_t offset = json_obj_offset(json, key);
+ if(offset){
+ if(!strncmp(json + offset, "true", 4)){
+ return 1;
+ }
+ if(!strncmp(json + offset, "false", 5)){
+ return 0;
+ }
+ }
+ return fallback;
+}
+
+uint8_t json_array_bool(char* json, uint64_t key, uint8_t fallback){
+ size_t offset = json_array_offset(json, key);
+ if(offset){
+ if(!strncmp(json + offset, "true", 4)){
+ return 1;
+ }
+ if(!strncmp(json + offset, "false", 5)){
+ return 0;
+ }
+ }
+ return fallback;
+}
+
+int64_t json_obj_int(char* json, char* key, int64_t fallback){
+ char* next_token = NULL;
+ int64_t result;
+ size_t offset = json_obj_offset(json, key);
+ if(offset){
+ result = strtol(json + offset, &next_token, 10);
+ if(next_token != json + offset){
+ return result;
+ }
+ }
+ return fallback;
+}
+
+double json_obj_double(char* json, char* key, double fallback){
+ char* next_token = NULL;
+ double result;
+ size_t offset = json_obj_offset(json, key);
+ if(offset){
+ result = strtod(json + offset, &next_token);
+ if(next_token != json + offset){
+ return result;
+ }
+ }
+ return fallback;
+}
+
+int64_t json_array_int(char* json, uint64_t key, int64_t fallback){
+ char* next_token = NULL;
+ int64_t result;
+ size_t offset = json_array_offset(json, key);
+ if(offset){
+ result = strtol(json + offset, &next_token, 10);
+ if(next_token != json + offset){
+ return result;
+ }
+ }
+ return fallback;
+}
+
+double json_array_double(char* json, uint64_t key, double fallback){
+ char* next_token = NULL;
+ double result;
+ size_t offset = json_array_offset(json, key);
+ if(offset){
+ result = strtod(json + offset, &next_token);
+ if(next_token != json + offset){
+ return result;
+ }
+ }
+ return fallback;
+}
+
+char* json_obj_str(char* json, char* key, size_t* length){
+ size_t offset = json_obj_offset(json, key), raw_length;
+ if(offset){
+ raw_length = json_validate_string(json + offset, strlen(json + offset));
+ if(length){
+ *length = raw_length - 2;
+ }
+ return json + offset + 1;
+ }
+ return NULL;
+}
+
+char* json_obj_strdup(char* json, char* key){
+ size_t len = 0;
+ char* value = json_obj_str(json, key, &len), *rv = NULL;
+ if(len){
+ rv = calloc(len + 1, sizeof(char));
+ if(rv){
+ memcpy(rv, value, len);
+ }
+ }
+ return rv;
+}
+
+char* json_array_str(char* json, uint64_t key, size_t* length){
+ size_t offset = json_array_offset(json, key), raw_length;
+ if(offset){
+ raw_length = json_validate_string(json + offset, strlen(json + offset));
+ if(length){
+ *length = raw_length - 2;
+ }
+ return json + offset + 1;
+ }
+ return NULL;
+}
+
+char* json_array_strdup(char* json, uint64_t key){
+ size_t len = 0;
+ char* value = json_array_str(json, key, &len), *rv = NULL;
+ if(len){
+ rv = calloc(len + 1, sizeof(char));
+ if(rv){
+ memcpy(rv, value, len);
+ }
+ }
+ return rv;
+}
diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h
new file mode 100644
index 0000000..5749119
--- /dev/null
+++ b/backends/libmmbackend.h
@@ -0,0 +1,127 @@
+#include <stdint.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#ifdef _WIN32
+#include <ws2tcpip.h>
+//#define close closesocket
+#else
+#include <sys/socket.h>
+#include <netdb.h>
+#endif
+#include <ctype.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include "../portability.h"
+
+/*** BACKEND IMPLEMENTATION LIBRARY ***/
+
+/** Networking functions **/
+
+/*
+ * Parse spec as host specification in the form
+ * host port
+ * into its constituent parts.
+ * Returns offsets into the original string and modifies it.
+ * Returns NULL in *port if none given.
+ * Returns NULL in both *port and *host if spec was an empty string.
+ */
+void mmbackend_parse_hostspec(char* spec, char** host, char** port);
+
+/*
+ * Parse a given host / port combination into a sockaddr_storage
+ * suitable for usage with connect / sendto
+ * Returns 0 on success
+ */
+int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len);
+
+/*
+ * 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);
+
+/*
+ * Send arbitrary data over multiple writes if necessary
+ * Returns 1 on failure, 0 on success.
+ */
+int mmbackend_send(int fd, uint8_t* data, size_t length);
+
+/*
+ * Wraps mmbackend_send for cstrings
+ */
+int mmbackend_send_str(int fd, char* data);
+
+
+/** JSON parsing **/
+
+typedef enum /*_json_types*/ {
+ JSON_INVALID = 0,
+ JSON_STRING,
+ JSON_ARRAY,
+ JSON_OBJECT,
+ JSON_NUMBER,
+ JSON_BOOL,
+ JSON_NULL
+} json_type;
+
+/*
+ * Try to identify the type of JSON data next in the buffer
+ * Will access at most the next `length` bytes
+ */
+json_type json_identify(char* json, size_t length);
+
+/*
+ * Validate that a buffer contains a valid JSON document/data within `length` bytes
+ * Returns the length of a detected JSON document, 0 otherwise (ie. parse failures)
+ */
+size_t json_validate(char* json, size_t length);
+size_t json_validate_string(char* json, size_t length);
+size_t json_validate_array(char* json, size_t length);
+size_t json_validate_object(char* json, size_t length);
+size_t json_validate_value(char* json, size_t length);
+
+/*
+ * Calculate offset for value of `key`
+ * Assumes a zero-terminated, validated JSON object / array as input
+ * Returns offset on success, 0 on failure
+ */
+size_t json_obj_offset(char* json, char* key);
+size_t json_array_offset(char* json, uint64_t key);
+
+/*
+ * Check for for a key within a JSON object / index within an array
+ * Assumes a zero-terminated, validated JSON object / array as input
+ * Returns type of value
+ */
+json_type json_obj(char* json, char* key);
+json_type json_array(char* json, uint64_t key);
+
+/*
+ * Fetch boolean value for an object / array key
+ * Assumes a zero-terminated, validated JSON object / array as input
+ */
+uint8_t json_obj_bool(char* json, char* key, uint8_t fallback);
+uint8_t json_array_bool(char* json, uint64_t key, uint8_t fallback);
+
+/*
+ * Fetch integer/double value for an object / array key
+ * Assumes a zero-terminated validated JSON object / array as input
+ */
+int64_t json_obj_int(char* json, char* key, int64_t fallback);
+double json_obj_double(char* json, char* key, double fallback);
+int64_t json_array_int(char* json, uint64_t key, int64_t fallback);
+double json_array_double(char* json, uint64_t key, double fallback);
+
+/*
+ * Fetch a string value for an object / array key
+ * Assumes a zero-terminated validated JSON object / array as input
+ * json_*_strdup returns a newly-allocated buffer containing
+ * only the requested value
+ */
+char* json_obj_str(char* json, char* key, size_t* length);
+char* json_obj_strdup(char* json, char* key);
+char* json_array_str(char* json, uint64_t key, size_t* length);
+char* json_array_strdup(char* json, uint64_t key);
diff --git a/backends/loopback.c b/backends/loopback.c
index bb93a1f..41e6f85 100644
--- a/backends/loopback.c
+++ b/backends/loopback.c
@@ -3,17 +3,17 @@
#define BACKEND_NAME "loopback"
-int init(){
+MM_PLUGIN_API int init(){
backend loopback = {
.name = BACKEND_NAME,
- .conf = backend_configure,
- .create = backend_instance,
- .conf_instance = backend_configure_instance,
- .channel = backend_channel,
- .handle = backend_set,
- .process = backend_handle,
- .start = backend_start,
- .shutdown = backend_shutdown
+ .conf = loopback_configure,
+ .create = loopback_instance,
+ .conf_instance = loopback_configure_instance,
+ .channel = loopback_channel,
+ .handle = loopback_set,
+ .process = loopback_handle,
+ .start = loopback_start,
+ .shutdown = loopback_shutdown
};
//register backend
@@ -24,23 +24,23 @@ int init(){
return 0;
}
-static int backend_configure(char* option, char* value){
+static int loopback_configure(char* option, char* value){
//intentionally ignored
return 0;
}
-static int backend_configure_instance(instance* inst, char* option, char* value){
+static int loopback_configure_instance(instance* inst, char* option, char* value){
//intentionally ignored
return 0;
}
-static instance* backend_instance(){
+static instance* loopback_instance(){
instance* i = mm_instance();
if(!i){
return NULL;
}
- i->impl = calloc(1, sizeof(loopback_instance));
+ i->impl = calloc(1, sizeof(loopback_instance_data));
if(!i->impl){
fprintf(stderr, "Failed to allocate memory\n");
return NULL;
@@ -49,9 +49,9 @@ static instance* backend_instance(){
return i;
}
-static channel* backend_channel(instance* inst, char* spec){
+static channel* loopback_channel(instance* inst, char* spec, uint8_t flags){
size_t u;
- loopback_instance* data = (loopback_instance*) inst->impl;
+ loopback_instance_data* data = (loopback_instance_data*) inst->impl;
//find matching channel
for(u = 0; u < data->n; u++){
@@ -79,7 +79,7 @@ static channel* backend_channel(instance* inst, char* spec){
return mm_channel(inst, u, 1);
}
-static int backend_set(instance* inst, size_t num, channel** c, channel_value* v){
+static int loopback_set(instance* inst, size_t num, channel** c, channel_value* v){
size_t n;
for(n = 0; n < num; n++){
mm_channel_event(c[n], v[n]);
@@ -87,19 +87,19 @@ static int backend_set(instance* inst, size_t num, channel** c, channel_value* v
return 0;
}
-static int backend_handle(size_t num, managed_fd* fds){
+static int loopback_handle(size_t num, managed_fd* fds){
//no events generated here
return 0;
}
-static int backend_start(){
+static int loopback_start(){
return 0;
}
-static int backend_shutdown(){
+static int loopback_shutdown(){
size_t n, u, p;
instance** inst = NULL;
- loopback_instance* data = NULL;
+ loopback_instance_data* data = NULL;
if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
fprintf(stderr, "Failed to fetch instance list\n");
@@ -107,7 +107,7 @@ static int backend_shutdown(){
}
for(u = 0; u < n; u++){
- data = (loopback_instance*) inst[u]->impl;
+ data = (loopback_instance_data*) inst[u]->impl;
for(p = 0; p < data->n; p++){
free(data->name[p]);
}
@@ -116,5 +116,7 @@ static int backend_shutdown(){
}
free(inst);
+
+ fprintf(stderr, "Loopback backend shut down\n");
return 0;
}
diff --git a/backends/loopback.h b/backends/loopback.h
index fe44e91..ee51c66 100644
--- a/backends/loopback.h
+++ b/backends/loopback.h
@@ -1,16 +1,16 @@
#include "midimonster.h"
-int init();
-static int backend_configure(char* option, char* value);
-static int backend_configure_instance(instance* instance, char* option, char* value);
-static instance* backend_instance();
-static channel* backend_channel(instance* instance, char* spec);
-static int backend_set(instance* inst, size_t num, channel** c, channel_value* v);
-static int backend_handle(size_t num, managed_fd* fds);
-static int backend_start();
-static int backend_shutdown();
+MM_PLUGIN_API int init();
+static int loopback_configure(char* option, char* value);
+static int loopback_configure_instance(instance* inst, char* option, char* value);
+static instance* loopback_instance();
+static channel* loopback_channel(instance* inst, char* spec, uint8_t flags);
+static int loopback_set(instance* inst, size_t num, channel** c, channel_value* v);
+static int loopback_handle(size_t num, managed_fd* fds);
+static int loopback_start();
+static int loopback_shutdown();
typedef struct /*_loopback_instance_data*/ {
size_t n;
char** name;
-} loopback_instance;
+} loopback_instance_data;
diff --git a/backends/loopback.md b/backends/loopback.md
new file mode 100644
index 0000000..a06c768
--- /dev/null
+++ b/backends/loopback.md
@@ -0,0 +1,28 @@
+### The `loopback` backend
+
+This backend allows the user to create logical mapping channels, for example to exchange triggering
+channels easier later. All events that are input are immediately output again on the same channel.
+
+#### Global configuration
+
+All global configuration is ignored.
+
+#### Instance configuration
+
+All instance configuration is ignored
+
+#### Channel specification
+
+A channel may have any string for a name.
+
+Example mapping:
+```
+loop.foo < loop.bar123
+```
+
+#### Known bugs / problems
+
+It is possible (and very easy) to configure loops using this backend. Triggering a loop
+will create a deadlock, preventing any other backends from generating events.
+Be careful with bidirectional channel mappings, as any input will be immediately
+output to the same channel again. \ No newline at end of file
diff --git a/backends/lua.c b/backends/lua.c
new file mode 100644
index 0000000..40e6613
--- /dev/null
+++ b/backends/lua.c
@@ -0,0 +1,510 @@
+#include "lua.h"
+
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#ifdef MMBACKEND_LUA_TIMERFD
+#include <sys/timerfd.h>
+#endif
+
+#define BACKEND_NAME "lua"
+#define LUA_REGISTRY_KEY "_midimonster_lua_instance"
+
+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
+
+MM_PLUGIN_API int init(){
+ backend lua = {
+ #ifndef MMBACKEND_LUA_TIMERFD
+ .interval = lua_interval,
+ #endif
+ .name = BACKEND_NAME,
+ .conf = lua_configure,
+ .create = lua_instance,
+ .conf_instance = lua_configure_instance,
+ .channel = lua_channel,
+ .handle = lua_set,
+ .process = lua_handle,
+ .start = lua_start,
+ .shutdown = lua_shutdown
+ };
+
+ //register backend
+ if(mm_backend_register(lua)){
+ fprintf(stderr, "Failed to register lua backend\n");
+ return 1;
+ }
+
+ #ifdef MMBACKEND_LUA_TIMERFD
+ //create the timer to expire intervals
+ timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
+ if(timer_fd < 0){
+ fprintf(stderr, "Failed to create timer for Lua backend\n");
+ return 1;
+ }
+ #endif
+ return 0;
+}
+
+#ifndef MMBACKEND_LUA_TIMERFD
+static uint32_t lua_interval(){
+ size_t n = 0;
+ uint64_t next_timer = 1000;
+
+ if(timer_interval){
+ for(n = 0; n < timers; n++){
+ if(timer[n].interval && timer[n].interval - timer[n].delta < next_timer){
+ next_timer = timer[n].interval - timer[n].delta;
+ }
+ }
+ return next_timer;
+ }
+ return 1000;
+}
+#endif
+
+static int lua_update_timerfd(){
+ uint64_t interval = 0, gcd, residual;
+ size_t n = 0;
+ #ifdef MMBACKEND_LUA_TIMERFD
+ struct itimerspec timer_config = {
+ 0
+ };
+ #endif
+
+ //find the minimum for the lower interval bounds
+ for(n = 0; n < timers; n++){
+ if(timer[n].interval && (!interval || timer[n].interval < interval)){
+ interval = timer[n].interval;
+ }
+ }
+
+ //calculate gcd of all timers if any are active
+ if(interval){
+ for(n = 0; n < timers; n++){
+ if(timer[n].interval){
+ //calculate gcd of current interval and this timers interval
+ gcd = timer[n].interval;
+ while(gcd){
+ residual = interval % gcd;
+ interval = gcd;
+ gcd = residual;
+ }
+ //since we round everything, 10 is the lowest interval we get
+ if(interval == 10){
+ break;
+ }
+ }
+ }
+
+ #ifdef MMBACKEND_LUA_TIMERFD
+ timer_config.it_interval.tv_sec = timer_config.it_value.tv_sec = interval / 1000;
+ timer_config.it_interval.tv_nsec = timer_config.it_value.tv_nsec = (interval % 1000) * 1e6;
+ #endif
+ }
+
+ if(interval == timer_interval){
+ return 0;
+ }
+
+ #ifdef MMBACKEND_LUA_TIMERFD
+ //configure the new interval
+ timerfd_settime(timer_fd, 0, &timer_config, NULL);
+ #endif
+ timer_interval = interval;
+ 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;
+
+ if(lua_gettop(interpreter) != 2){
+ fprintf(stderr, "Lua output function called with %d arguments, expected 2 (string, number)\n", 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);
+ val.normalised = clamp(luaL_checknumber(interpreter, 2), 1.0, 0.0);
+
+ //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;
+ }
+ mm_channel_event(channel, val);
+ data->output[n] = val.normalised;
+ return 0;
+ }
+ }
+
+ fprintf(stderr, "Tried to set unknown channel %s.%s\n", inst->name, channel_name);
+ return 0;
+}
+
+static int lua_callback_interval(lua_State* interpreter){
+ size_t n = 0;
+ uint64_t interval = 0;
+ int reference = LUA_NOREF;
+
+ if(lua_gettop(interpreter) != 2){
+ fprintf(stderr, "Lua output function called with %d arguments, expected 2 (string, number)\n", lua_gettop(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){
+ interval -= interval % 10;
+ }
+ else{
+ interval += (10 - (interval % 10));
+ }
+
+ //push the function again
+ lua_pushvalue(interpreter, 1);
+ if(lua_gettable(interpreter, LUA_REGISTRYINDEX) == LUA_TNUMBER){
+ //already interval'd
+ reference = luaL_checkinteger(interpreter, 4);
+ }
+ else if(interval){
+ //get a reference to the function
+ lua_pushvalue(interpreter, 1);
+ reference = luaL_ref(interpreter, LUA_REGISTRYINDEX);
+
+ //the function indexes the reference
+ lua_pushvalue(interpreter, 1);
+ lua_pushinteger(interpreter, reference);
+ lua_settable(interpreter, LUA_REGISTRYINDEX);
+ }
+
+ //find matching timer
+ for(n = 0; n < timers; n++){
+ if(timer[n].reference == reference && timer[n].interpreter == interpreter){
+ break;
+ }
+ }
+
+ if(n < timers){
+ //set new interval
+ timer[n].interval = interval;
+ timer[n].delta = 0;
+ }
+ else if(interval){
+ //append new timer
+ timer = realloc(timer, (timers + 1) * sizeof(lua_timer));
+ if(!timer){
+ fprintf(stderr, "Failed to allocate memory\n");
+ timers = 0;
+ return 0;
+ }
+ timer[timers].interval = interval;
+ timer[timers].delta = 0;
+ timer[timers].interpreter = interpreter;
+ timer[timers].reference = reference;
+ timers++;
+ }
+
+ //recalculate timerspec
+ lua_update_timerfd();
+ return 0;
+}
+
+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;
+
+ if(lua_gettop(interpreter) != 1){
+ fprintf(stderr, "Lua get_value function called with %d arguments, expected 1 (string)\n", 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);
+
+ //find correct channel & return value
+ for(n = 0; n < data->channels; n++){
+ if(!strcmp(channel_name, data->channel_name[n])){
+ lua_pushnumber(data->interpreter, (input) ? data->input[n] : data->output[n]);
+ return 1;
+ }
+ }
+
+ fprintf(stderr, "Tried to get unknown channel %s.%s\n", inst->name, channel_name);
+ return 0;
+}
+
+static int lua_callback_input_value(lua_State* interpreter){
+ return lua_callback_value(interpreter, 1);
+}
+
+static int lua_callback_output_value(lua_State* interpreter){
+ return lua_callback_value(interpreter, 0);
+}
+
+static int lua_configure(char* option, char* value){
+ fprintf(stderr, "The lua backend does not take any global configuration\n");
+ return 1;
+}
+
+static int lua_configure_instance(instance* inst, char* option, char* value){
+ lua_instance_data* data = (lua_instance_data*) inst->impl;
+
+ //load a lua file into the interpreter
+ if(!strcmp(option, "script") || !strcmp(option, "source")){
+ if(luaL_dofile(data->interpreter, value)){
+ fprintf(stderr, "Failed to load lua source file %s for instance %s: %s\n", value, inst->name, lua_tostring(data->interpreter, -1));
+ return 1;
+ }
+ return 0;
+ }
+
+ fprintf(stderr, "Unknown configuration parameter %s for lua instance %s\n", option, inst->name);
+ return 1;
+}
+
+static instance* lua_instance(){
+ instance* inst = mm_instance();
+ if(!inst){
+ return NULL;
+ }
+
+ lua_instance_data* data = calloc(1, sizeof(lua_instance_data));
+ if(!data){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return NULL;
+ }
+
+ //load the interpreter
+ data->interpreter = luaL_newstate();
+ if(!data->interpreter){
+ fprintf(stderr, "Failed to initialize LUA\n");
+ free(data);
+ return NULL;
+ }
+ luaL_openlibs(data->interpreter);
+
+ //register lua interface functions
+ lua_register(data->interpreter, "output", lua_callback_output);
+ lua_register(data->interpreter, "interval", lua_callback_interval);
+ lua_register(data->interpreter, "input_value", lua_callback_input_value);
+ lua_register(data->interpreter, "output_value", lua_callback_output_value);
+
+ //store instance pointer to the lua state
+ lua_pushstring(data->interpreter, LUA_REGISTRY_KEY);
+ lua_pushlightuserdata(data->interpreter, (void *) inst);
+ lua_settable(data->interpreter, LUA_REGISTRYINDEX);
+
+ inst->impl = data;
+ return inst;
+}
+
+static channel* lua_channel(instance* inst, char* spec, uint8_t flags){
+ size_t u;
+ lua_instance_data* data = (lua_instance_data*) inst->impl;
+
+ //find matching channel
+ for(u = 0; u < data->channels; u++){
+ if(!strcmp(spec, data->channel_name[u])){
+ 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){
+ fprintf(stderr, "Failed to allocate memory\n");
+ 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]){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return NULL;
+ }
+ data->channels++;
+ }
+
+ return mm_channel(inst, u, 1);
+}
+
+static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){
+ size_t n = 0;
+ 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;
+ //call lua channel handlers if present
+ if(data->reference[c[n]->ident] != LUA_NOREF){
+ lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->reference[c[n]->ident]);
+ lua_pushnumber(data->interpreter, v[n].normalised);
+ if(lua_pcall(data->interpreter, 1, 0, 0) != LUA_OK){
+ fprintf(stderr, "Failed to call handler for %s.%s: %s\n", inst->name, data->channel_name[c[n]->ident], lua_tostring(data->interpreter, -1));
+ lua_pop(data->interpreter, 1);
+ }
+ }
+ }
+ return 0;
+}
+
+static int lua_handle(size_t num, managed_fd* fds){
+ uint64_t delta = timer_interval;
+ size_t n;
+
+ #ifdef MMBACKEND_LUA_TIMERFD
+ uint8_t read_buffer[100];
+ if(!num){
+ return 0;
+ }
+
+ //read the timer iteration to acknowledge the fd
+ if(read(timer_fd, read_buffer, sizeof(read_buffer)) < 0){
+ fprintf(stderr, "Failed to read from Lua timer: %s\n", strerror(errno));
+ return 1;
+ }
+ #else
+ if(!last_timestamp){
+ last_timestamp = mm_timestamp();
+ }
+ delta = mm_timestamp() - last_timestamp;
+ last_timestamp = mm_timestamp();
+ #endif
+
+ //no timers active
+ if(!timer_interval){
+ return 0;
+ }
+
+ //add delta to all active timers
+ for(n = 0; n < timers; n++){
+ if(timer[n].interval){
+ timer[n].delta += delta;
+ //call lua function if timer expired
+ if(timer[n].delta >= timer[n].interval){
+ timer[n].delta %= timer[n].interval;
+ lua_rawgeti(timer[n].interpreter, LUA_REGISTRYINDEX, timer[n].reference);
+ lua_pcall(timer[n].interpreter, 0, 0, 0);
+ }
+ }
+ }
+ return 0;
+}
+
+static int lua_start(){
+ size_t n, u, p;
+ instance** inst = NULL;
+ lua_instance_data* data = NULL;
+
+ //fetch all defined instances
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ return 1;
+ }
+
+ //resolve channels to their handler functions
+ for(u = 0; u < n; u++){
+ data = (lua_instance_data*) inst[u]->impl;
+ for(p = 0; p < data->channels; p++){
+ //exclude reserved names
+ if(strcmp(data->channel_name[p], "output")
+ && strcmp(data->channel_name[p], "input_value")
+ && strcmp(data->channel_name[p], "output_value")
+ && strcmp(data->channel_name[p], "interval")){
+ lua_getglobal(data->interpreter, data->channel_name[p]);
+ data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX);
+ if(data->reference[p] == LUA_REFNIL){
+ data->reference[p] = LUA_NOREF;
+ }
+ }
+ }
+ }
+
+ free(inst);
+
+ #ifdef MMBACKEND_LUA_TIMERFD
+ //register the timer with the core
+ fprintf(stderr, "Lua backend registering 1 descriptor to core\n");
+ if(mm_manage_fd(timer_fd, BACKEND_NAME, 1, NULL)){
+ return 1;
+ }
+ #endif
+ return 0;
+}
+
+static int lua_shutdown(){
+ size_t n, u, p;
+ instance** inst = NULL;
+ lua_instance_data* data = NULL;
+
+ //fetch all instances
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ return 1;
+ }
+
+ for(u = 0; u < n; u++){
+ data = (lua_instance_data*) inst[u]->impl;
+ //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_name);
+ free(data->reference);
+ free(data->input);
+ free(data->output);
+ free(inst[u]->impl);
+ }
+
+ free(inst);
+ //free module-global data
+ free(timer);
+ timer = NULL;
+ timers = 0;
+ #ifdef MMBACKEND_LUA_TIMERFD
+ close(timer_fd);
+ timer_fd = -1;
+ #endif
+
+ fprintf(stderr, "Lua backend shut down\n");
+ return 0;
+}
diff --git a/backends/lua.h b/backends/lua.h
new file mode 100644
index 0000000..4ea5b0a
--- /dev/null
+++ b/backends/lua.h
@@ -0,0 +1,39 @@
+#include "midimonster.h"
+
+#include <lua.h>
+#include <lualib.h>
+#include <lauxlib.h>
+
+//OSX and Windows don't have the cool new toys...
+#ifdef __linux__
+ #define MMBACKEND_LUA_TIMERFD
+#endif
+
+MM_PLUGIN_API int init();
+static int lua_configure(char* option, char* value);
+static int lua_configure_instance(instance* inst, char* option, char* value);
+static instance* lua_instance();
+static channel* lua_channel(instance* inst, char* spec, uint8_t flags);
+static int lua_set(instance* inst, size_t num, channel** c, channel_value* v);
+static int lua_handle(size_t num, managed_fd* fds);
+static int lua_start();
+static int lua_shutdown();
+#ifndef MMBACKEND_LUA_TIMERFD
+static uint32_t lua_interval();
+#endif
+
+typedef struct /*_lua_instance_data*/ {
+ size_t channels;
+ char** channel_name;
+ int* reference;
+ double* input;
+ double* output;
+ lua_State* interpreter;
+} lua_instance_data;
+
+typedef struct /*_lua_interval_callback*/ {
+ uint64_t interval;
+ uint64_t delta;
+ lua_State* interpreter;
+ int reference;
+} lua_timer;
diff --git a/backends/lua.md b/backends/lua.md
new file mode 100644
index 0000000..f38e189
--- /dev/null
+++ b/backends/lua.md
@@ -0,0 +1,66 @@
+### The `lua` backend
+
+The `lua` backend provides a flexible programming environment, allowing users to route and manipulate
+events using the Lua programming language.
+
+Every instance has it's own interpreter state which can be loaded with custom handler scripts.
+
+To process incoming channel events, the MIDIMonster calls corresponding Lua functions (if they exist)
+with the value (as a Lua `number` type) as parameter.
+
+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) |
+| `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 |
+
+
+Example script:
+```
+function bar(value)
+ output("foo", value / 2)
+end
+
+step = 0
+function toggle()
+ output("bar", step * 1.0)
+ step = (step + 1) % 2;
+end
+
+interval(toggle, 1000)
+```
+
+Input values range between 0.0 and 1.0, output values are clamped to the same range.
+
+#### 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)|
+
+A single instance may have multiple `source` options specified, which will all be read cumulatively.
+
+#### Channel specification
+
+Channel names may be any valid Lua function name.
+
+Example mapping:
+```
+lua1.foo > lua2.bar
+```
+
+#### Known bugs / problems
+
+Using any of the interface functions (`output`, `interval`, `input_value`, `output_value`) as an
+input channel name to a Lua instance will not call any handler functions.
+Using these names as arguments to the output and value interface functions works as intended.
+
+Output values will not trigger corresponding input event handlers unless the channel is mapped
+back in the MIDIMonster configuration.
diff --git a/backends/maweb.c b/backends/maweb.c
new file mode 100644
index 0000000..d008cc0
--- /dev/null
+++ b/backends/maweb.c
@@ -0,0 +1,1072 @@
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#ifndef MAWEB_NO_LIBSSL
+#include <openssl/md5.h>
+#endif
+
+#include "libmmbackend.h"
+#include "maweb.h"
+
+#define BACKEND_NAME "maweb"
+#define WS_LEN(a) ((a) & 0x7F)
+#define WS_OP(a) ((a) & 0x0F)
+#define WS_FLAG_FIN 0x80
+#define WS_FLAG_MASK 0x80
+
+static uint64_t last_keepalive = 0;
+static uint64_t update_interval = 50;
+static uint64_t last_update = 0;
+static uint64_t updates_inflight = 0;
+
+static maweb_command_key cmdline_keys[] = {
+ {"PREV", 109, 0, 1}, {"SET", 108, 1, 0, 1}, {"NEXT", 110, 0, 1},
+ {"TIME", 58, 1, 1}, {"EDIT", 55, 1, 1}, {"UPDATE", 57, 1, 1},
+ {"OOPS", 53, 1, 1}, {"ESC", 54, 1, 1}, {"CLEAR", 105, 1, 1},
+ {"0", 86, 1, 1}, {"1", 87, 1, 1}, {"2", 88, 1, 1},
+ {"3", 89, 1, 1}, {"4", 90, 1, 1}, {"5", 91, 1, 1},
+ {"6", 92, 1, 1}, {"7", 93, 1, 1}, {"8", 94, 1, 1},
+ {"9", 95, 1, 1}, {"PUNKT", 98, 1, 1}, {"ENTER", 106, 1, 1},
+ {"PLUS", 96, 1, 1}, {"MINUS", 97, 1, 1}, {"THRU", 102, 1, 1},
+ {"IF", 103, 1, 1}, {"AT", 104, 1, 1}, {"FULL", 99, 1, 1},
+ {"MA", 68, 0, 1}, {"HIGH", 100, 1, 1, 1}, {"SOLO", 101, 1, 1, 1},
+ {"SELECT", 42, 1, 1}, {"OFF", 43, 1, 1}, {"ON", 46, 1, 1},
+ {"ASSIGN", 63, 1, 1}, {"LABEL", 0, 1, 1},
+ {"COPY", 73, 1, 1}, {"DELETE", 69, 1, 1}, {"STORE", 59, 1, 1},
+ {"GOTO", 56, 1, 1}, {"PAGE", 70, 1, 1}, {"MACRO", 71, 1, 1},
+ {"PRESET", 72, 1, 1}, {"SEQU", 74, 1, 1}, {"CUE", 75, 1, 1},
+ {"EXEC", 76, 1, 1}, {"FIXTURE", 83, 1, 1}, {"GROUP", 84, 1, 1},
+ {"GO_MINUS", 10, 1, 1}, {"PAUSE", 9, 1, 1}, {"GO_PLUS", 11, 1, 1},
+
+ {"FIXTURE_CHANNEL", 0, 1, 1}, {"FIXTURE_GROUP_PRESET", 0, 1, 1},
+ {"EXEC_CUE", 0, 1, 1}, {"STORE_UPDATE", 0, 1, 1}, {"PROG_ONLY", 0, 1, 1, 1},
+ {"SPECIAL_DIALOGUE", 0, 1, 1},
+ {"ODD", 0, 1, 1}, {"EVEN", 0, 1, 1},
+ {"WINGS", 0, 1, 1}, {"RESET", 0, 1, 1},
+ //gma2 internal only
+ {"CHPGPLUS", 3}, {"CHPGMINUS", 4},
+ {"FDPGPLUS", 5}, {"FDPGMINUS", 6},
+ {"BTPGPLUS", 7}, {"BTPGMINUS", 8},
+ {"X1", 12}, {"X2", 13}, {"X3", 14},
+ {"X4", 15}, {"X5", 16}, {"X6", 17},
+ {"X7", 18}, {"X8", 19}, {"X9", 20},
+ {"X10", 21}, {"X11", 22}, {"X12", 23},
+ {"X13", 24}, {"X14", 25}, {"X15", 26},
+ {"X16", 27}, {"X17", 28}, {"X18", 29},
+ {"X19", 30}, {"X20", 31},
+ {"V1", 120}, {"V2", 121}, {"V3", 122},
+ {"V4", 123}, {"V5", 124}, {"V6", 125},
+ {"V7", 126}, {"V8", 127}, {"V9", 128},
+ {"V10", 129},
+ {"NIPPLE", 40},
+ {"TOOLS", 119}, {"SETUP", 117}, {"BACKUP", 117},
+ {"BLIND", 60}, {"FREEZE", 61}, {"PREVIEW", 62},
+ {"FIX", 41}, {"TEMP", 44}, {"TOP", 45},
+ {"VIEW", 66}, {"EFFECT", 67}, {"CHANNEL", 82},
+ {"MOVE", 85}, {"BLACKOUT", 65},
+ {"PLEASE", 106},
+ {"LIST", 32}, {"USER1", 33}, {"USER2", 34},
+ {"ALIGN", 64}, {"HELP", 116},
+ {"UP", 107}, {"DOWN", 111},
+ {"FASTREVERSE", 47}, {"LEARN", 48}, {"FASTFORWARD", 49},
+ {"GO_MINUS_SMALL", 50}, {"PAUSE_SMALL", 51}, {"GO_PLUS_SMALL", 52}
+};
+
+MM_PLUGIN_API int init(){
+ backend maweb = {
+ .name = BACKEND_NAME,
+ .conf = maweb_configure,
+ .create = maweb_instance,
+ .conf_instance = maweb_configure_instance,
+ .channel = maweb_channel,
+ .handle = maweb_set,
+ .process = maweb_handle,
+ .start = maweb_start,
+ .shutdown = maweb_shutdown,
+ .interval = maweb_interval
+ };
+
+ //register backend
+ if(mm_backend_register(maweb)){
+ fprintf(stderr, "Failed to register maweb backend\n");
+ return 1;
+ }
+ return 0;
+}
+
+static ssize_t maweb_channel_index(maweb_instance_data* data, maweb_channel_type type, uint16_t page, uint16_t index){
+ size_t n;
+ for(n = 0; n < data->channels; n++){
+ if(data->channel[n].type == type
+ && data->channel[n].page == page
+ && data->channel[n].index == index){
+ return n;
+ }
+ }
+ return -1;
+}
+
+static int channel_comparator(const void* raw_a, const void* raw_b){
+ maweb_channel_data* a = (maweb_channel_data*) raw_a;
+ maweb_channel_data* b = (maweb_channel_data*) raw_b;
+
+ //this needs to take into account command line channels
+ //they need to be sorted last so that the channel poll logic works properly
+ if(a->page != b->page){
+ return a->page - b->page;
+ }
+ //execs and their components are sorted by index first, type second
+ if(a->type < cmdline && b->type < cmdline){
+ if(a->index != b->index){
+ return a->index - b->index;
+ }
+ return a->type - b->type;
+ }
+ //if either one is not an exec, sort by type first, index second
+ if(a->type != b->type){
+ return a->type - b->type;
+ }
+ return a->index - b->index;
+}
+
+static uint32_t maweb_interval(){
+ return update_interval - (last_update % update_interval);
+}
+
+static int maweb_configure(char* option, char* value){
+ if(!strcmp(option, "interval")){
+ update_interval = strtoul(value, NULL, 10);
+ return 0;
+ }
+
+ fprintf(stderr, "Unknown maweb backend configuration option %s\n", option);
+ return 1;
+}
+
+static int maweb_configure_instance(instance* inst, char* option, char* value){
+ maweb_instance_data* data = (maweb_instance_data*) inst->impl;
+ char* host = NULL, *port = NULL;
+
+ if(!strcmp(option, "host")){
+ mmbackend_parse_hostspec(value, &host, &port);
+ if(!host){
+ fprintf(stderr, "Invalid host specified for maweb instance %s\n", inst->name);
+ return 1;
+ }
+ free(data->host);
+ data->host = strdup(host);
+ free(data->port);
+ data->port = NULL;
+ if(port){
+ data->port = strdup(port);
+ }
+ return 0;
+ }
+ else if(!strcmp(option, "user")){
+ free(data->user);
+ data->user = strdup(value);
+ return 0;
+ }
+ else if(!strcmp(option, "password")){
+ #ifndef MAWEB_NO_LIBSSL
+ size_t n;
+ uint8_t password_hash[MD5_DIGEST_LENGTH];
+
+ MD5((uint8_t*) value, strlen(value), (uint8_t*) password_hash);
+ data->pass = realloc(data->pass, (2 * MD5_DIGEST_LENGTH + 1) * sizeof(char));
+ for(n = 0; n < MD5_DIGEST_LENGTH; n++){
+ snprintf(data->pass + 2 * n, 3, "%02x", password_hash[n]);
+ }
+ return 0;
+ #else
+ fprintf(stderr, "This build of the maweb backend only supports the default password\n");
+ return 1;
+ #endif
+ }
+ else if(!strcmp(option, "cmdline")){
+ if(!strcmp(value, "console")){
+ data->cmdline = cmd_console;
+ }
+ else if(!strcmp(value, "remote")){
+ data->cmdline = cmd_remote;
+ }
+ else if(!strcmp(value, "downgrade")){
+ data->cmdline = cmd_downgrade;
+ }
+ else{
+ fprintf(stderr, "Unknown maweb commandline mode %s for instance %s\n", value, inst->name);
+ return 1;
+ }
+ return 0;
+ }
+
+ fprintf(stderr, "Unknown configuration parameter %s for maweb instance %s\n", option, inst->name);
+ return 1;
+}
+
+static instance* maweb_instance(){
+ instance* inst = mm_instance();
+ if(!inst){
+ return NULL;
+ }
+
+ maweb_instance_data* data = calloc(1, sizeof(maweb_instance_data));
+ if(!data){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return NULL;
+ }
+
+ data->fd = -1;
+ data->buffer = calloc(MAWEB_RECV_CHUNK, sizeof(uint8_t));
+ if(!data->buffer){
+ fprintf(stderr, "Failed to allocate memory\n");
+ free(data);
+ return NULL;
+ }
+ data->allocated = MAWEB_RECV_CHUNK;
+
+ inst->impl = data;
+ return inst;
+}
+
+static channel* maweb_channel(instance* inst, char* spec, uint8_t flags){
+ maweb_instance_data* data = (maweb_instance_data*) inst->impl;
+ maweb_channel_data chan = {
+ 0
+ };
+ char* next_token = NULL;
+ channel* channel_ref = NULL;
+ size_t n;
+
+ if(!strncmp(spec, "page", 4)){
+ chan.page = strtoul(spec + 4, &next_token, 10);
+ if(*next_token != '.'){
+ fprintf(stderr, "Failed to parse maweb channel spec %s: Missing separator\n", spec);
+ return NULL;
+ }
+
+ next_token++;
+ if(!strncmp(next_token, "fader", 5)){
+ chan.type = exec_fader;
+ next_token += 5;
+ }
+ else if(!strncmp(next_token, "upper", 5)){
+ chan.type = exec_upper;
+ next_token += 5;
+ }
+ else if(!strncmp(next_token, "lower", 5)){
+ chan.type = exec_lower;
+ next_token += 5;
+ }
+ else if(!strncmp(next_token, "flash", 5)){
+ chan.type = exec_button;
+ next_token += 5;
+ }
+ else if(!strncmp(next_token, "button", 6)){
+ chan.type = exec_button;
+ next_token += 6;
+ }
+ chan.index = strtoul(next_token, NULL, 10);
+ }
+ else{
+ for(n = 0; n < sizeof(cmdline_keys) / sizeof(maweb_command_key); n++){
+ if(!strcmp(spec, cmdline_keys[n].name)){
+ if((data->cmdline == cmd_remote && !cmdline_keys[n].press && !cmdline_keys[n].release)
+ || (data->cmdline == cmd_console && !cmdline_keys[n].lua)){
+ fprintf(stderr, "maweb cmdline key %s does not work with the current commandline mode for instance %s\n", spec, inst->name);
+ return NULL;
+ }
+
+ chan.type = cmdline;
+ chan.index = n + 1;
+ chan.page = 1;
+ break;
+ }
+ }
+ }
+
+ if(chan.type && chan.index && chan.page){
+ //actually, those are zero-indexed...
+ chan.index--;
+ chan.page--;
+
+ if(maweb_channel_index(data, chan.type, chan.page, chan.index) == -1){
+ data->channel = realloc(data->channel, (data->channels + 1) * sizeof(maweb_channel_data));
+ if(!data->channel){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return NULL;
+ }
+ data->channel[data->channels] = chan;
+ data->channels++;
+ }
+
+ channel_ref = mm_channel(inst, maweb_channel_index(data, chan.type, chan.page, chan.index), 1);
+ data->channel[maweb_channel_index(data, chan.type, chan.page, chan.index)].chan = channel_ref;
+ return channel_ref;
+ }
+
+ fprintf(stderr, "Failed to parse maweb channel spec %s\n", spec);
+ return NULL;
+}
+
+static int maweb_send_frame(instance* inst, maweb_operation op, uint8_t* payload, size_t len){
+ maweb_instance_data* data = (maweb_instance_data*) inst->impl;
+ uint8_t frame_header[MAWEB_FRAME_HEADER_LENGTH] = "";
+ size_t header_bytes = 2;
+ uint16_t* payload_len16 = (uint16_t*) (frame_header + 2);
+ uint64_t* payload_len64 = (uint64_t*) (frame_header + 2);
+
+ frame_header[0] = WS_FLAG_FIN | op;
+ if(len <= 125){
+ frame_header[1] = WS_FLAG_MASK | len;
+ }
+ else if(len <= 0xFFFF){
+ frame_header[1] = WS_FLAG_MASK | 126;
+ *payload_len16 = htobe16(len);
+ header_bytes += 2;
+ }
+ else{
+ frame_header[1] = WS_FLAG_MASK | 127;
+ *payload_len64 = htobe64(len);
+ header_bytes += 8;
+ }
+ //send a zero masking key because masking is stupid
+ header_bytes += 4;
+
+ if(mmbackend_send(data->fd, frame_header, header_bytes)
+ || mmbackend_send(data->fd, payload, len)){
+ return 1;
+ }
+
+ return 0;
+}
+
+static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_type metatype, char* payload, size_t payload_length){
+ maweb_instance_data* data = (maweb_instance_data*) inst->impl;
+ size_t exec_blocks = json_obj_offset(payload, (metatype == 2) ? "executorBlocks" : "bottomButtons"), offset, block = 0, control;
+ int64_t exec_index = json_obj_int(payload, "iExec", 191);
+ ssize_t channel_index;
+ channel_value evt;
+
+ if(!exec_blocks){
+ if(metatype == 3){
+ //ignore unused buttons
+ return 0;
+ }
+ fprintf(stderr, "maweb missing exec block data on exec %" PRIu64 ".%" PRIu64 "\n", page, exec_index);
+ return 1;
+ }
+
+ //the bottomButtons key has an additional subentry
+ if(metatype == 3){
+ exec_blocks += json_obj_offset(payload + exec_blocks, "items");
+ }
+
+ //iterate over executor blocks
+ for(offset = json_array_offset(payload + exec_blocks, block); offset; offset = json_array_offset(payload + exec_blocks, block)){
+ control = exec_blocks + offset + json_obj_offset(payload + exec_blocks + offset, "fader");
+
+ channel_index = maweb_channel_index(data, exec_fader, page - 1, exec_index);
+ if(channel_index >= 0){
+ if(!data->channel[channel_index].input_blocked){
+ evt.normalised = json_obj_double(payload + control, "v", 0.0);
+ if(evt.normalised != data->channel[channel_index].in){
+ mm_channel_event(mm_channel(inst, channel_index, 0), evt);
+ data->channel[channel_index].in = evt.normalised;
+ }
+ }
+ else{
+ //block input immediately after channel set to prevent feedback loops
+ data->channel[channel_index].input_blocked--;
+ }
+ }
+
+ channel_index = maweb_channel_index(data, exec_button, page - 1, exec_index);
+ if(channel_index >= 0){
+ if(!data->channel[channel_index].input_blocked){
+ evt.normalised = json_obj_int(payload, "isRun", 0);
+ if(evt.normalised != data->channel[channel_index].in){
+ mm_channel_event(mm_channel(inst, channel_index, 0), evt);
+ data->channel[channel_index].in = evt.normalised;
+ }
+ }
+ else{
+ data->channel[channel_index].input_blocked--;
+ }
+ }
+
+ DBGPF("maweb page %" PRIu64 " exec %" PRIu64 " value %f running %" PRIu64 "\n", page, exec_index, json_obj_double(payload + control, "v", 0.0), json_obj_int(payload, "isRun", 0));
+ exec_index++;
+ block++;
+ }
+
+ return 0;
+}
+
+static int maweb_process_playbacks(instance* inst, int64_t page, char* payload, size_t payload_length){
+ size_t base_offset = json_obj_offset(payload, "itemGroups"), group_offset, subgroup_offset, item_offset;
+ uint64_t group = 0, subgroup, item, metatype;
+
+ if(!page){
+ fprintf(stderr, "maweb received playbacks for invalid page\n");
+ return 0;
+ }
+
+ if(!base_offset){
+ fprintf(stderr, "maweb playback data missing item key\n");
+ return 0;
+ }
+
+ //iterate .itemGroups
+ for(group_offset = json_array_offset(payload + base_offset, group);
+ group_offset;
+ group_offset = json_array_offset(payload + base_offset, group)){
+ metatype = json_obj_int(payload + base_offset + group_offset, "itemsType", 0);
+ //iterate .itemGroups.items
+ //FIXME this is problematic if there is no "items" key
+ group_offset = group_offset + json_obj_offset(payload + base_offset + group_offset, "items");
+ if(group_offset){
+ subgroup = 0;
+ group_offset += base_offset;
+ for(subgroup_offset = json_array_offset(payload + group_offset, subgroup);
+ subgroup_offset;
+ subgroup_offset = json_array_offset(payload + group_offset, subgroup)){
+ //iterate .itemGroups.items[n]
+ item = 0;
+ subgroup_offset += group_offset;
+ for(item_offset = json_array_offset(payload + subgroup_offset, item);
+ item_offset;
+ item_offset = json_array_offset(payload + subgroup_offset, item)){
+ maweb_process_playback(inst, page, metatype,
+ payload + subgroup_offset + item_offset,
+ payload_length - subgroup_offset - item_offset);
+ item++;
+ }
+ subgroup++;
+ }
+ }
+ group++;
+ }
+ updates_inflight--;
+ DBGPF("maweb playback message processing done, %" PRIu64 " updates inflight\n", updates_inflight);
+ return 0;
+}
+
+static int maweb_request_playbacks(instance* inst){
+ maweb_instance_data* data = (maweb_instance_data*) inst->impl;
+ char xmit_buffer[MAWEB_XMIT_CHUNK];
+ int rv = 0;
+
+ char item_indices[1024] = "[300,400,500]", item_counts[1024] = "[16,16,16]", item_types[1024] = "[3,3,3]";
+ size_t page_index = 0, view = 3, channel = 0, offsets[3], channel_offset, channels;
+
+ if(updates_inflight){
+ fprintf(stderr, "maweb skipping update request, %" PRIu64 " updates still inflight\n", updates_inflight);
+ return 0;
+ }
+
+ //only request faders and buttons
+ for(channel = 0; channel < data->channels && data->channel[channel].type < cmdline; channel++){
+ offsets[0] = offsets[1] = offsets[2] = 1;
+ page_index = data->channel[channel].page;
+ //poll logic differs between the consoles because reasons
+ //don't quote me on this section
+ if(data->peer_type == peer_dot2){
+ //blocks 0, 100 & 200 have 21 execs and need to be queried from fader view
+ view = (data->channel[channel].index >= 300) ? 3 : 2;
+
+ for(channel_offset = 1; channel + channel_offset <= data->channels
+ && data->channel[channel + channel_offset].type < cmdline; channel_offset++){
+ channels = channel + channel_offset - 1;
+ //find end for this exec block
+ for(; channel + channel_offset < data->channels; channel_offset++){
+ if(data->channel[channel + channel_offset].page != page_index
+ || (data->channel[channels].index / 100) != (data->channel[channel + channel_offset].index / 100)){
+ break;
+ }
+ }
+
+ //add request block for the exec block
+ offsets[0] += snprintf(item_indices + offsets[0], sizeof(item_indices) - offsets[0], "%d,", data->channel[channels].index);
+ offsets[1] += snprintf(item_counts + offsets[1], sizeof(item_counts) - offsets[1], "%d,", data->channel[channel + channel_offset - 1].index - data->channel[channels].index + 1);
+ offsets[2] += snprintf(item_types + offsets[2], sizeof(item_types) - offsets[2], "%d,", (data->channel[channels].index < 100) ? 2 : 3);
+
+ //send on last channel, page boundary, metamode boundary
+ if(channel + channel_offset >= data->channels
+ || data->channel[channel + channel_offset].page != page_index
+ || (data->channel[channel].index < 300) != (data->channel[channel + channel_offset].index < 300)){
+ break;
+ }
+ }
+
+ //terminate arrays (overwriting the last array separator)
+ offsets[0] += snprintf(item_indices + offsets[0] - 1, sizeof(item_indices) - offsets[0], "]");
+ offsets[1] += snprintf(item_counts + offsets[1] - 1, sizeof(item_counts) - offsets[1], "]");
+ offsets[2] += snprintf(item_types + offsets[2] - 1, sizeof(item_types) - offsets[2], "]");
+ }
+ else{
+ //for the ma, the view equals the exec type requested (we can query all button execs from button view, all fader execs from fader view)
+ view = (data->channel[channel].index >= 100) ? 3 : 2;
+ snprintf(item_types, sizeof(item_types), "[%" PRIsize_t "]", view);
+ //this channel must be included, so it must be in range for the first startindex
+ snprintf(item_indices, sizeof(item_indices), "[%d]", (data->channel[channel].index / 5) * 5);
+
+ //find end of exec block
+ for(channel_offset = 1; channel + channel_offset < data->channels
+ && data->channel[channel].page == data->channel[channel + channel_offset].page
+ && data->channel[channel].index / 100 == data->channel[channel + channel_offset].index / 100; channel_offset++){
+ }
+
+ //gma execs are grouped in blocks of 5
+ channels = data->channel[channel + channel_offset - 1].index - (data->channel[channel].index / 5) * 5;
+ snprintf(item_counts, sizeof(item_indices), "[%" PRIsize_t "]", ((channels / 5) * 5 + 5));
+ }
+
+ DBGPF("maweb poll range first %d: %d.%d last %d: %d.%d next %d: %d.%d\n",
+ data->channel[channel].type, data->channel[channel].page, data->channel[channel].index,
+ data->channel[channel + channel_offset - 1].type, data->channel[channel + channel_offset - 1].page, data->channel[channel + channel_offset - 1].index,
+ data->channel[channel + channel_offset].type, data->channel[channel + channel_offset].page, data->channel[channel + channel_offset].index);
+
+ //advance base channel
+ channel += channel_offset - 1;
+
+ //send current request
+ snprintf(xmit_buffer, sizeof(xmit_buffer),
+ "{"
+ "\"requestType\":\"playbacks\","
+ "\"startIndex\":%s,"
+ "\"itemsCount\":%s,"
+ "\"pageIndex\":%" PRIsize_t ","
+ "\"itemsType\":%s,"
+ "\"view\":%" PRIsize_t ","
+ "\"execButtonViewMode\":2," //extended
+ "\"buttonsViewMode\":0," //get vfader for button execs
+ "\"session\":%" PRIu64
+ "}",
+ item_indices,
+ item_counts,
+ page_index,
+ item_types,
+ view,
+ data->session);
+ rv |= maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer));
+ DBGPF("maweb poll request: %s\n", xmit_buffer);
+ updates_inflight++;
+ }
+
+ DBGPF("maweb poll request handling done, %" PRIu64 " updates requested\n", updates_inflight);
+ return rv;
+}
+
+static int maweb_handle_message(instance* inst, char* payload, size_t payload_length){
+ char xmit_buffer[MAWEB_XMIT_CHUNK];
+ char* field;
+ maweb_instance_data* data = (maweb_instance_data*) inst->impl;
+
+ //query this early to save on unnecessary parser passes with stupid-huge data messages
+ if(json_obj(payload, "responseType") == JSON_STRING){
+ field = json_obj_str(payload, "responseType", NULL);
+ if(!strncmp(field, "login", 5)){
+ if(json_obj_bool(payload, "result", 0)){
+ fprintf(stderr, "maweb login successful\n");
+ data->login = 1;
+ }
+ else{
+ fprintf(stderr, "maweb login failed\n");
+ data->login = 0;
+ }
+ }
+ if(!strncmp(field, "playbacks", 9)){
+ if(maweb_process_playbacks(inst, json_obj_int(payload, "iPage", 0), payload, payload_length)){
+ fprintf(stderr, "maweb failed to handle/request input data\n");
+ }
+ return 0;
+ }
+ }
+
+ DBGPF("maweb message (%" PRIsize_t "): %s\n", payload_length, payload);
+ if(json_obj(payload, "session") == JSON_NUMBER){
+ data->session = json_obj_int(payload, "session", data->session);
+ if(data->session < 0){
+ fprintf(stderr, "maweb login failed\n");
+ data->login = 0;
+ return 0;
+ }
+ fprintf(stderr, "maweb session id is now %" PRId64 "\n", data->session);
+ }
+
+ if(json_obj_bool(payload, "forceLogin", 0)){
+ fprintf(stderr, "maweb sending user credentials\n");
+ snprintf(xmit_buffer, sizeof(xmit_buffer),
+ "{\"requestType\":\"login\",\"username\":\"%s\",\"password\":\"%s\",\"session\":%" PRIu64 "}",
+ (data->peer_type == peer_dot2) ? "remote" : data->user, data->pass ? data->pass : MAWEB_DEFAULT_PASSWORD, data->session);
+ maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer));
+ }
+ if(json_obj(payload, "status") && json_obj(payload, "appType")){
+ fprintf(stderr, "maweb connection established\n");
+ field = json_obj_str(payload, "appType", NULL);
+ if(!strncmp(field, "dot2", 4)){
+ data->peer_type = peer_dot2;
+ //the dot2 can't handle lua commands
+ data->cmdline = cmd_remote;
+ }
+ else if(!strncmp(field, "gma2", 4)){
+ data->peer_type = peer_ma2;
+ }
+ maweb_send_frame(inst, ws_text, (uint8_t*) "{\"session\":0}", 13);
+ }
+
+ return 0;
+}
+
+static int maweb_connect(instance* inst){
+ maweb_instance_data* data = (maweb_instance_data*) inst->impl;
+ if(!data->host){
+ return 1;
+ }
+
+ //unregister old fd from core
+ if(data->fd >= 0){
+ mm_manage_fd(data->fd, BACKEND_NAME, 0, NULL);
+ }
+
+ data->fd = mmbackend_socket(data->host, data->port ? data->port : MAWEB_DEFAULT_PORT, SOCK_STREAM, 0, 0);
+ if(data->fd < 0){
+ return 1;
+ }
+
+ data->state = ws_new;
+ if(mmbackend_send_str(data->fd, "GET /?ma=1 HTTP/1.1\r\n")
+ || mmbackend_send_str(data->fd, "Connection: Upgrade\r\n")
+ || mmbackend_send_str(data->fd, "Upgrade: websocket\r\n")
+ || mmbackend_send_str(data->fd, "Sec-WebSocket-Version: 13\r\n")
+ //the websocket key probably should not be hardcoded, but this is not security critical
+ //and the whole websocket 'accept key' dance is plenty stupid as it is
+ || mmbackend_send_str(data->fd, "Sec-WebSocket-Key: rbEQrXMEvCm4ZUjkj6juBQ==\r\n")
+ || mmbackend_send_str(data->fd, "\r\n")){
+ fprintf(stderr, "maweb backend failed to communicate with peer\n");
+ return 1;
+ }
+
+ //register new fd
+ if(mm_manage_fd(data->fd, BACKEND_NAME, 1, (void*) inst)){
+ fprintf(stderr, "maweb backend failed to register fd\n");
+ return 1;
+ }
+ return 0;
+}
+
+static ssize_t maweb_handle_lines(instance* inst, ssize_t bytes_read){
+ maweb_instance_data* data = (maweb_instance_data*) inst->impl;
+ size_t n, begin = 0;
+
+ for(n = 0; n < bytes_read - 1; n++){
+ if(!strncmp((char*) data->buffer + data->offset + n, "\r\n", 2)){
+ if(data->state == ws_new){
+ if(!strncmp((char*) data->buffer, "HTTP/1.1 101", 12)){
+ data->state = ws_http;
+ }
+ else{
+ fprintf(stderr, "maweb received invalid HTTP response for instance %s\n", inst->name);
+ return -1;
+ }
+ }
+ else{
+ //ignore all http stuff until the end of headers since we don't actually care...
+ if(n == begin){
+ data->state = ws_open;
+ }
+ }
+ begin = n + 2;
+ }
+ }
+
+ return data->offset + begin;
+}
+
+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;
+ uint64_t payload_length = 0;
+ uint16_t* payload_len16 = (uint16_t*) (data->buffer + 2);
+ uint64_t* payload_len64 = (uint64_t*) (data->buffer + 2);
+ uint8_t* payload = data->buffer + 2;
+ uint8_t terminator_temp = 0;
+
+ if(data->offset + bytes_read < 2){
+ return 0;
+ }
+
+ //using varint as payload length is stupid, but some people seem to think otherwise...
+ payload_length = WS_LEN(data->buffer[1]);
+ switch(payload_length){
+ case 126:
+ if(data->offset + bytes_read < 4){
+ return 0;
+ }
+ payload_length = htobe16(*payload_len16);
+ payload = data->buffer + 4;
+ header_length = 4;
+ break;
+ case 127:
+ if(data->offset + bytes_read < 10){
+ return 0;
+ }
+ payload_length = htobe64(*payload_len64);
+ payload = data->buffer + 10;
+ header_length = 10;
+ break;
+ default:
+ break;
+ }
+
+ if(data->offset + bytes_read < header_length + payload_length){
+ return 0;
+ }
+
+ switch(WS_OP(data->buffer[0])){
+ case ws_text:
+ //terminate message
+ terminator_temp = payload[payload_length];
+ payload[payload_length] = 0;
+ if(maweb_handle_message(inst, (char*) payload, payload_length)){
+ return data->offset + bytes_read;
+ }
+ payload[payload_length] = terminator_temp;
+ break;
+ case ws_ping:
+ //answer server ping with a pong
+ if(maweb_send_frame(inst, ws_pong, payload, payload_length)){
+ fprintf(stderr, "maweb failed to send pong\n");
+ }
+ return header_length + payload_length;
+ default:
+ fprintf(stderr, "maweb encountered unhandled frame type %02X\n", WS_OP(data->buffer[0]));
+ //this is somewhat dicey, it might be better to handle only header + payload length for known but unhandled types
+ return data->offset + bytes_read;
+ }
+
+ return header_length + payload_length;
+}
+
+static int maweb_handle_fd(instance* inst){
+ maweb_instance_data* data = (maweb_instance_data*) inst->impl;
+ ssize_t bytes_read, bytes_left = data->allocated - data->offset, bytes_handled;
+
+ if(bytes_left < 3){
+ data->buffer = realloc(data->buffer, (data->allocated + MAWEB_RECV_CHUNK) * sizeof(uint8_t));
+ if(!data->buffer){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return 1;
+ }
+ data->allocated += MAWEB_RECV_CHUNK;
+ bytes_left += MAWEB_RECV_CHUNK;
+ }
+
+ bytes_read = recv(data->fd, data->buffer + data->offset, bytes_left - 1, 0);
+ if(bytes_read < 0){
+ fprintf(stderr, "maweb backend failed to receive: %s\n", strerror(errno));
+ //TODO close, reopen
+ return 1;
+ }
+ else if(bytes_read == 0){
+ //client closed connection
+ //TODO try to reopen
+ return 0;
+ }
+
+ do{
+ switch(data->state){
+ case ws_new:
+ case ws_http:
+ bytes_handled = maweb_handle_lines(inst, bytes_read);
+ break;
+ case ws_open:
+ bytes_handled = maweb_handle_ws(inst, bytes_read);
+ break;
+ case ws_closed:
+ bytes_handled = data->offset + bytes_read;
+ break;
+ }
+
+ if(bytes_handled < 0){
+ bytes_handled = data->offset + bytes_read;
+ data->offset = 0;
+ //TODO close, reopen
+ fprintf(stderr, "maweb failed to handle incoming data\n");
+ return 1;
+ }
+ else if(bytes_handled == 0){
+ break;
+ }
+
+ memmove(data->buffer, data->buffer + bytes_handled, (data->offset + bytes_read) - bytes_handled);
+
+ bytes_handled -= data->offset;
+ bytes_read -= bytes_handled;
+ data->offset = 0;
+ } while(bytes_read > 0);
+
+ data->offset += bytes_read;
+ return 0;
+}
+
+static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){
+ maweb_instance_data* data = (maweb_instance_data*) inst->impl;
+ maweb_channel_data* chan = NULL;
+ char xmit_buffer[MAWEB_XMIT_CHUNK];
+ size_t n;
+
+ if(num && !data->login){
+ fprintf(stderr, "maweb instance %s can not send output, not logged in\n", inst->name);
+ return 0;
+ }
+
+ for(n = 0; n < num; n++){
+ //sanity check
+ if(c[n]->ident >= data->channels){
+ return 1;
+ }
+ chan = data->channel + c[n]->ident;
+
+ //channel state tracking
+ if(chan->out == v[n].normalised){
+ continue;
+ }
+ chan->out = v[n].normalised;
+
+ //i/o value space separation & feedback filtering for faders
+ if(chan->type == exec_fader){
+ chan->input_blocked = 1;
+ chan->in = v[n].normalised;
+ }
+
+ switch(chan->type){
+ case exec_fader:
+ snprintf(xmit_buffer, sizeof(xmit_buffer),
+ "{\"requestType\":\"playbacks_userInput\","
+ "\"execIndex\":%d,"
+ "\"pageIndex\":%d,"
+ "\"faderValue\":%f,"
+ "\"type\":1,"
+ "\"session\":%" PRIu64
+ "}", chan->index, chan->page, v[n].normalised, data->session);
+ break;
+ case exec_upper:
+ case exec_lower:
+ case exec_button:
+ snprintf(xmit_buffer, sizeof(xmit_buffer),
+ "{\"requestType\":\"playbacks_userInput\","
+ //"\"cmdline\":\"\","
+ "\"execIndex\":%d,"
+ "\"pageIndex\":%d,"
+ "\"buttonId\":%d,"
+ "\"pressed\":%s,"
+ "\"released\":%s,"
+ "\"type\":0,"
+ "\"session\":%" PRIu64
+ "}", chan->index, chan->page,
+ (data->peer_type == peer_dot2 && chan->type == exec_upper) ? 0 : (chan->type - exec_button),
+ (v[n].normalised > 0.9) ? "true" : "false",
+ (v[n].normalised > 0.9) ? "false" : "true",
+ data->session);
+ break;
+ case cmdline:
+ if(cmdline_keys[chan->index].lua
+ && (data->cmdline == cmd_console || data->cmdline == cmd_downgrade)
+ && data->peer_type != peer_dot2){
+ //push canbus events
+ snprintf(xmit_buffer, sizeof(xmit_buffer),
+ "{\"command\":\"LUA 'gma.canbus.hardkey(%d, %s, false)'\","
+ "\"requestType\":\"command\","
+ "\"session\":%" PRIu64
+ "}", cmdline_keys[chan->index].lua,
+ (v[n].normalised > 0.9) ? "true" : "false",
+ data->session);
+ }
+ else if((cmdline_keys[chan->index].press || cmdline_keys[chan->index].release)
+ && (data->cmdline != cmd_console)){
+ //send press/release events if required
+ if((cmdline_keys[chan->index].press && v[n].normalised > 0.9)
+ || (cmdline_keys[chan->index].release && v[n].normalised < 0.9)){
+ snprintf(xmit_buffer, sizeof(xmit_buffer),
+ "{\"keyname\":\"%s\","
+ "\"autoSubmit\":%s,"
+ "\"value\":%d,"
+ "\"session\":%" PRIu64
+ "}", cmdline_keys[chan->index].name,
+ cmdline_keys[chan->index].auto_submit ? "true" : "null",
+ (v[n].normalised > 0.9) ? 1 : 0,
+ data->session);
+ }
+ else{
+ continue;
+ }
+ }
+ else{
+ fprintf(stderr, "maweb commandline key %s not executed on %s due to mode mismatch\n",
+ cmdline_keys[chan->index].name, inst->name);
+ continue;
+ }
+ break;
+ default:
+ fprintf(stderr, "maweb control not yet implemented\n");
+ return 1;
+ }
+ DBGPF("maweb command out %s\n", xmit_buffer);
+ maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer));
+ }
+ return 0;
+}
+
+static int maweb_keepalive(){
+ size_t n, u;
+ instance** inst = NULL;
+ maweb_instance_data* data = NULL;
+ char xmit_buffer[MAWEB_XMIT_CHUNK];
+
+ //fetch all defined instances
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ return 1;
+ }
+
+ //send keep-alive messages for logged-in instances
+ for(u = 0; u < n; u++){
+ data = (maweb_instance_data*) inst[u]->impl;
+ if(data->login){
+ snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"session\":%" PRIu64 "}", data->session);
+ maweb_send_frame(inst[u], ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer));
+ }
+ }
+
+ free(inst);
+ return 0;
+}
+
+static int maweb_poll(){
+ size_t n, u;
+ instance** inst = NULL;
+ maweb_instance_data* data = NULL;
+
+ //fetch all defined instances
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ return 1;
+ }
+
+ //send data polls for logged-in instances
+ for(u = 0; u < n; u++){
+ data = (maweb_instance_data*) inst[u]->impl;
+ if(data->login){
+ maweb_request_playbacks(inst[u]);
+ }
+ }
+
+ free(inst);
+ return 0;
+}
+
+static int maweb_handle(size_t num, managed_fd* fds){
+ size_t n = 0;
+ int rv = 0;
+
+ for(n = 0; n < num; n++){
+ rv |= maweb_handle_fd((instance*) fds[n].impl);
+ }
+
+ //FIXME all keepalive processing allocates temporary buffers, this might an optimization target
+ if(last_keepalive && mm_timestamp() - last_keepalive >= MAWEB_CONNECTION_KEEPALIVE){
+ rv |= maweb_keepalive();
+ last_keepalive = mm_timestamp();
+ }
+
+ if(last_update && mm_timestamp() - last_update >= update_interval){
+ rv |= maweb_poll();
+ last_update = mm_timestamp();
+ }
+
+ return rv;
+}
+
+static int maweb_start(){
+ size_t n, u, p;
+ instance** inst = NULL;
+ maweb_instance_data* data = NULL;
+
+ //fetch all defined instances
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ return 1;
+ }
+
+ for(u = 0; u < n; u++){
+ //sort channels
+ data = (maweb_instance_data*) inst[u]->impl;
+ qsort(data->channel, data->channels, sizeof(maweb_channel_data), channel_comparator);
+
+ //re-set channel identifiers
+ for(p = 0; p < data->channels; p++){
+ data->channel[p].chan->ident = p;
+ }
+
+ if(maweb_connect(inst[u])){
+ fprintf(stderr, "Failed to open connection to MA Web Remote for instance %s\n", inst[u]->name);
+ free(inst);
+ return 1;
+ }
+ }
+
+ free(inst);
+ if(!n){
+ return 0;
+ }
+
+ fprintf(stderr, "maweb backend registering %" PRIsize_t " descriptors to core\n", n);
+
+ //initialize timeouts
+ last_keepalive = last_update = mm_timestamp();
+ return 0;
+}
+
+static int maweb_shutdown(){
+ size_t n, u;
+ instance** inst = NULL;
+ maweb_instance_data* data = NULL;
+
+ //fetch all instances
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ return 1;
+ }
+
+ for(u = 0; u < n; u++){
+ data = (maweb_instance_data*) inst[u]->impl;
+ free(data->host);
+ data->host = NULL;
+ free(data->port);
+ data->port = NULL;
+ free(data->user);
+ data->user = NULL;
+ free(data->pass);
+ data->pass = NULL;
+
+ close(data->fd);
+ data->fd = -1;
+
+ free(data->buffer);
+ data->buffer = NULL;
+
+ data->offset = data->allocated = 0;
+ data->state = ws_new;
+
+ free(data->channel);
+ data->channel = NULL;
+ data->channels = 0;
+ }
+
+ free(inst);
+
+ fprintf(stderr, "maweb backend shut down\n");
+ return 0;
+}
diff --git a/backends/maweb.h b/backends/maweb.h
new file mode 100644
index 0000000..05095f8
--- /dev/null
+++ b/backends/maweb.h
@@ -0,0 +1,100 @@
+#include "midimonster.h"
+
+MM_PLUGIN_API int init();
+static int maweb_configure(char* option, char* value);
+static int maweb_configure_instance(instance* inst, char* option, char* value);
+static instance* maweb_instance();
+static channel* maweb_channel(instance* inst, char* spec, uint8_t flags);
+static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v);
+static int maweb_handle(size_t num, managed_fd* fds);
+static int maweb_start();
+static int maweb_shutdown();
+static uint32_t maweb_interval();
+
+//Default login password: MD5("midimonster")
+#define MAWEB_DEFAULT_PASSWORD "2807623134739142b119aff358f8a219"
+#define MAWEB_DEFAULT_PORT "80"
+#define MAWEB_RECV_CHUNK 1024
+#define MAWEB_XMIT_CHUNK 4096
+#define MAWEB_FRAME_HEADER_LENGTH 16
+#define MAWEB_CONNECTION_KEEPALIVE 10000
+
+typedef enum /*_maweb_channel_type*/ {
+ type_unset = 0,
+ exec_fader = 1,
+ exec_button = 2, //gma: 0 dot: 0
+ exec_lower = 3, //gma: 1 dot: 1
+ exec_upper = 4, //gma: 2 dot: 0
+ cmdline
+} maweb_channel_type;
+
+typedef enum /*_maweb_peer_type*/ {
+ peer_unidentified = 0,
+ peer_ma2,
+ peer_ma3,
+ peer_dot2
+} maweb_peer_type;
+
+typedef enum /*_ws_conn_state*/ {
+ ws_new,
+ ws_http,
+ ws_open,
+ ws_closed
+} maweb_state;
+
+typedef enum /*_maweb_cmdline_mode*/ {
+ cmd_remote = 0,
+ cmd_console,
+ cmd_downgrade
+} maweb_cmdline_mode;
+
+typedef enum /*_ws_frame_op*/ {
+ ws_text = 1,
+ ws_binary = 2,
+ ws_ping = 9,
+ ws_pong = 10
+} maweb_operation;
+
+typedef struct {
+ char* name;
+ unsigned lua;
+ uint8_t press;
+ uint8_t release;
+ uint8_t auto_submit;
+} maweb_command_key;
+
+typedef struct /*_maweb_channel*/ {
+ maweb_channel_type type;
+ uint16_t page;
+ uint16_t index;
+
+ uint8_t input_blocked;
+
+ double in;
+ double out;
+
+ //reverse reference required because the identifiers are not stable
+ //because we sort the backing store...
+ channel* chan;
+} maweb_channel_data;
+
+typedef struct /*_maweb_instance_data*/ {
+ char* host;
+ char* port;
+ char* user;
+ char* pass;
+
+ uint8_t login;
+ int64_t session;
+ maweb_peer_type peer_type;
+
+ size_t channels;
+ maweb_channel_data* channel;
+ maweb_cmdline_mode cmdline;
+
+ int fd;
+ maweb_state state;
+ size_t offset;
+ size_t allocated;
+ uint8_t* buffer;
+} maweb_instance_data;
diff --git a/backends/maweb.md b/backends/maweb.md
new file mode 100644
index 0000000..45dc778
--- /dev/null
+++ b/backends/maweb.md
@@ -0,0 +1,141 @@
+### The `maweb` backend
+
+This backend connects directly with the integrated *MA Web Remote* of MA Lighting consoles and OnPC
+instances (GrandMA2 / GrandMA2 OnPC / GrandMA Dot2 / GrandMA Dot2 OnPC).
+It grants read-write access to the console's playback controls as well as write access to most command
+line and control keys.
+
+#### Setting up the console
+
+For the GrandMA2 enter the console configuration (`Setup` key), select `Console`/`Global Settings` and
+set the `Remotes` option to `Login enabled`.
+Create an additional user that is able to log into the Web Remote using `Setup`/`Console`/`User & Profiles Setup`.
+
+For the dot2, enter the console configuration using the `Setup` key, select `Global Settings` and enable the
+Web Remote. Set a web remote password using the option below the activation setting.
+
+#### Global configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|---------------------------------------------------------------|
+| `interval` | `100` | `50` | Query interval for input data polling (in msec) |
+
+#### 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) |
+
+The per-instance command line mode may be one of `remote`, `console` or `downgrade`. The first option handles
+command keys with a "virtual" commandline belonging to the Web Remote connection. Any commands entered are
+not visible on the main console. The `console` mode is only available with GrandMA2 remotes and injects key events
+into the main console. This mode also supports additional hardkeys that are only available on GrandMA consoles.
+When connected to a dot2 console while this mode is active, the use of commandline keys will not be possible.
+With the `downgrade` mode, keys are handled on the console if possible, falling back to remote handling if not.
+
+#### Channel specification
+
+Currently, three types of MA controls can be assigned, with each having some subcontrols
+
+* Fader executor
+* Button executor
+* Command keys
+
+##### Executors
+
+* For the GrandMA2, executors are arranged in pages, with each page having 90 fader executors (numbered 1 through 90)
+ and 90 button executors (numbered 101 through 190).
+ * A fader executor consists of a `fader`, two buttons above it (`upper`, `lower`) and one `button` below it.
+ * A button executor consists of a `button` control and a virtual `fader` (visible on the console in the "Action Buttons" view).
+* For the dot2, executors are also arranged in pages, but the controls are non-obviously numbered.
+ * For the faders, they are numerically right-to-left from the Core Fader section (Faders 6 to 1) over the F-Wing 1 (Faders 13 to 6) to
+ F-Wing 2 (Faders 21 to 14).
+ * Above the fader sections are two rows of 21 `button` executors, numbered 122 through 101 (lower row) and 222 through 201 (upper row),
+ in the same order as the faders are.
+ * Fader executors have two buttons below them (`upper` and `lower`).
+ * The button executor section consists of six rows of 16 buttons, divided into two button wings. Buttons on the wings
+ are once again numbered right-to-left.
+ * B-Wing 1 has `button` controls 308 to 301 (top row), 408 to 401 (second row), and so on until 808 through 801 (bottom row)
+ * B-Wing 2 has 316 to 309 (top row) through 816 to 809 (bottom row)
+
+When creating a new show, only the first page is created and active. Additional pages have to be created explicitly within
+the console before being usable. When mapped as outputs, `fader` controls output their value, `button` controls output 1 when the corresponding
+executor is running, 0 otherwise.
+
+These controls can be addressed like
+
+```
+mw1.page1.fader5 > mw1.page1.upper5
+mw1.page3.lower3 > mw1.page2.button2
+```
+
+A button executor can likewise be mapped using the syntax
+
+```
+mw1.page2.button103 > mw1.page3.fader101
+mw1.page2.button803 > mw1.page3.button516
+```
+
+##### Command keys
+
+Command keys will be pressed when the incoming event value is greater than `0.9` and released when it is less than that.
+They can be mapped using the syntax
+
+```
+mw1.<key-name>
+```
+
+The following keys are mappable in all commandline modes and work on all consoles
+
+| Supported | Command | Line | Keys | | |
+|---------------|---------------|---------------|---------------|---------------|---------------|
+| `PREV` | `SET` | `NEXT` | `TIME` | `EDIT` | `UPDATE` |
+| `OOPS` | `ESC` | `CLEAR` | `0` | `1` | `2` |
+| `3` | `4` | `5` | `6` | `7` | `8` |
+| `9` | `PUNKT` | `ENTER` | `PLUS` | `MINUS` | `THRU` |
+| `IF` | `AT` | `FULL` | `MA` | `HIGH` | `SOLO` |
+| `SELECT` | `OFF` | `ON` | `ASSIGN` | `COPY` | `DELETE` |
+| `STORE` | `GOTO` | `PAGE` | `MACRO` | `PRESET` | `SEQU` |
+| `CUE` | `EXEC` | `FIXTURE` | `GROUP` | `GO_MINUS` | `PAUSE` |
+| `GO_PLUS` | | | | | |
+
+The following keys only work when keys are being handled with a virtual command line
+
+| Web | Remote | specific | | |
+|---------------|-----------------------|-------------------------------|---------------|-----------------------|
+| `LABEL` |`FIXTURE_CHANNEL` | `FIXTURE_GROUP_PRESET` | `EXEC_CUE` | `STORE_UPDATE` |
+| `PROG_ONLY` | `SPECIAL_DIALOGUE` | `ODD` | `EVEN` | `WINGS` |
+| `RESET` | | | | |
+
+The following keys only work in the `console` or `downgrade` command line modes on a GrandMA2
+
+| GrandMA2 | console | only | | | |
+|---------------|---------------|---------------|---------------|---------------|---------------|
+| `CHPGPLUS` | `CHPGMINUS` | `FDPGPLUS` | `FDPGMINUS` | `BTPGPLUS` | `BTPGMINUS` |
+| `X1` | `X2` | `X3` | `X4` | `X5` | `X6` |
+| `X7` | `X8` | `X9` | `X10` | `X11` | `X12` |
+| `X13` | `X14` | `X15` | `X16` | `X17` | `X18` |
+| `X19` | `X20` | `V1` | `V2` | `V3` | `V4` |
+| `V5` | `V6` | `V7` | `V8` | `V9` | `V10` |
+| `NIPPLE` | `TOOLS` | `SETUP` | `BACKUP` | `BLIND` | `FREEZE` |
+| `PREVIEW` | `FIX` | `TEMP` | `TOP` | `VIEW` | `EFFECT` |
+| `CHANNEL` | `MOVE` | `BLACKOUT` | `PLEASE` | `LIST` | `USER1` |
+| `USER2` | `ALIGN` | `HELP` | `UP` | `DOWN` | `FASTREVERSE` |
+| `LEARN` | `FASTFORWARD` | `GO_MINUS_SMALL` | `PAUSE_SMALL` | `GO_PLUS_SMALL` | |
+
+#### Known bugs / problems
+
+To properly encode the user password, this backend depends on a library providing cryptographic functions (`libssl` / `openssl`).
+Since this may be a problem on some platforms, the backend can be built with this requirement disabled, which also disables the possibility
+to set arbitrary passwords. The backend will always try to log in with the default password `midimonster` in this case. The user name is still
+configurable.
+
+Data input from the console is done by actively querying the state of all mapped controls, which is resource-intensive if done
+at low latency. A lower input interval value will produce data with lower latency, at the cost of network & CPU usage.
+Higher values will make the input "step" more, but will not consume as many CPU cycles and network bandwidth.
+
+When requesting button executor events on the fader pages (execs 101 to 222) of a dot2 console, map at least one fader control from the 0 - 22 range
+or input will not work due to strange limitations in the MA Web API.
diff --git a/backends/midi.c b/backends/midi.c
index d856ced..92776ca 100644
--- a/backends/midi.c
+++ b/backends/midi.c
@@ -3,32 +3,27 @@
#include "midi.h"
#define BACKEND_NAME "midi"
+static char* sequencer_name = NULL;
static snd_seq_t* sequencer = NULL;
-typedef union {
- struct {
- uint8_t pad[5];
- uint8_t type;
- uint8_t channel;
- uint8_t control;
- } fields;
- uint64_t label;
-} midi_channel_ident;
-
-/*
- * TODO
- * Optionally send note-off messages
- * Optionally send updates as after-touch
- */
enum /*_midi_channel_type*/ {
none = 0,
note,
cc,
+ pressure,
+ aftertouch,
+ pitchbend,
nrpn,
sysmsg
};
-int init(){
+static struct {
+ uint8_t detect;
+} midi_config = {
+ .detect = 0
+};
+
+MM_PLUGIN_API int init(){
backend midi = {
.name = BACKEND_NAME,
.conf = midi_configure,
@@ -41,8 +36,8 @@ int init(){
.shutdown = midi_shutdown
};
- if(snd_seq_open(&sequencer, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0){
- fprintf(stderr, "Failed to open ALSA sequencer\n");
+ if(sizeof(midi_channel_ident) != sizeof(uint64_t)){
+ fprintf(stderr, "MIDI channel identification union out of bounds\n");
return 1;
}
@@ -52,17 +47,19 @@ int init(){
return 1;
}
- snd_seq_nonblock(sequencer, 1);
-
- fprintf(stderr, "MIDI client ID is %d\n", snd_seq_client_id(sequencer));
return 0;
}
static int midi_configure(char* option, char* value){
if(!strcmp(option, "name")){
- if(snd_seq_set_client_name(sequencer, value) < 0){
- fprintf(stderr, "Failed to set MIDI client name to %s\n", value);
- return 1;
+ free(sequencer_name);
+ sequencer_name = strdup(value);
+ return 0;
+ }
+ else if(!strcmp(option, "detect")){
+ midi_config.detect = 1;
+ if(!strcmp(value, "off")){
+ midi_config.detect = 0;
}
return 0;
}
@@ -86,14 +83,14 @@ static instance* midi_instance(){
return inst;
}
-static int midi_configure_instance(instance* instance, char* option, char* value){
- midi_instance_data* data = (midi_instance_data*) instance->impl;
+static int midi_configure_instance(instance* inst, char* option, char* value){
+ midi_instance_data* data = (midi_instance_data*) inst->impl;
//FIXME maybe allow connecting more than one device
if(!strcmp(option, "read")){
//connect input device
if(data->read){
- fprintf(stderr, "MIDI port already connected to an input device\n");
+ fprintf(stderr, "MIDI instance %s was already connected to an input device\n", inst->name);
return 1;
}
data->read = strdup(value);
@@ -102,7 +99,7 @@ static int midi_configure_instance(instance* instance, char* option, char* value
else if(!strcmp(option, "write")){
//connect output device
if(data->write){
- fprintf(stderr, "MIDI port already connected to an output device\n");
+ fprintf(stderr, "MIDI instance %s was already connected to an output device\n", inst->name);
return 1;
}
data->write = strdup(value);
@@ -113,48 +110,87 @@ static int midi_configure_instance(instance* instance, char* option, char* value
return 1;
}
-static channel* midi_channel(instance* instance, char* spec){
+static channel* midi_channel(instance* inst, char* spec, uint8_t flags){
midi_channel_ident ident = {
.label = 0
};
+ //support deprecated syntax for a transition period...
+ uint8_t old_syntax = 0;
char* channel;
- if(!strncmp(spec, "cc", 2)){
+ 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{
- fprintf(stderr, "Unknown MIDI channel specification %s\n", spec);
+ fprintf(stderr, "Unknown MIDI channel control type in %s\n", spec);
return NULL;
}
ident.fields.channel = strtoul(channel, &channel, 10);
-
- //FIXME test this
- if(ident.fields.channel > 16){
- fprintf(stderr, "MIDI channel out of range in channel spec %s\n", spec);
+ if(ident.fields.channel > 15){
+ fprintf(stderr, "MIDI channel out of range in midi channel spec %s\n", spec);
return NULL;
}
if(*channel != '.'){
- fprintf(stderr, "Need MIDI channel specification of form channel.control, had %s\n", spec);
+ fprintf(stderr, "Need MIDI channel specification of form channel<X>.<control><Y>, had %s\n", spec);
return NULL;
}
+ //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{
+ fprintf(stderr, "Unknown MIDI channel control type in %s\n", spec);
+ return NULL;
+ }
+ }
+
ident.fields.control = strtoul(channel, NULL, 10);
if(ident.label){
- return mm_channel(instance, ident.label, 1);
+ return mm_channel(inst, ident.label, 1);
}
return NULL;
@@ -163,20 +199,19 @@ static channel* midi_channel(instance* instance, char* spec){
static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){
size_t u;
snd_seq_event_t ev;
- midi_instance_data* data;
+ midi_instance_data* data = (midi_instance_data*) inst->impl;
midi_channel_ident ident = {
.label = 0
};
for(u = 0; u < num; u++){
- data = (midi_instance_data*) c[u]->instance->impl;
ident.label = c[u]->ident;
snd_seq_ev_clear(&ev);
snd_seq_ev_set_source(&ev, data->port);
snd_seq_ev_set_subs(&ev);
snd_seq_ev_set_direct(&ev);
-
+
switch(ident.fields.type){
case note:
snd_seq_ev_set_noteon(&ev, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0);
@@ -184,6 +219,15 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){
case cc:
snd_seq_ev_set_controller(&ev, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0);
break;
+ case pressure:
+ snd_seq_ev_set_keypress(&ev, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0);
+ break;
+ case pitchbend:
+ snd_seq_ev_set_pitchbend(&ev, ident.fields.channel, (v[u].normalised * 16383.0) - 8192);
+ break;
+ 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;
@@ -201,6 +245,7 @@ static int midi_handle(size_t num, managed_fd* fds){
instance* inst = NULL;
channel* changed = NULL;
channel_value val;
+ char* event_type = NULL;
midi_channel_ident ident = {
.label = 0
};
@@ -210,16 +255,39 @@ static int midi_handle(size_t num, managed_fd* fds){
}
while(snd_seq_event_input(sequencer, &ev) > 0){
+ event_type = NULL;
ident.label = 0;
switch(ev->type){
case SND_SEQ_EVENT_NOTEON:
case SND_SEQ_EVENT_NOTEOFF:
- case SND_SEQ_EVENT_KEYPRESS:
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";
+ 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";
break;
case SND_SEQ_EVENT_CONTROLLER:
ident.fields.type = cc;
@@ -227,6 +295,7 @@ static int midi_handle(size_t num, managed_fd* fds){
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:
@@ -255,13 +324,22 @@ static int midi_handle(size_t num, managed_fd* fds){
return 1;
}
}
+
+ if(midi_config.detect && event_type){
+ if(ident.fields.type == pitchbend || ident.fields.type == aftertouch){
+ fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s\n", inst->name, ident.fields.channel, event_type);
+ }
+ else{
+ fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s%d\n", inst->name, ident.fields.channel, event_type, ident.fields.control);
+ }
+ }
}
free(ev);
return 0;
}
static int midi_start(){
- size_t n, p;
+ size_t n = 0, p;
int nfds, rv = 1;
struct pollfd* pfds = NULL;
instance** inst = NULL;
@@ -279,6 +357,21 @@ static int midi_start(){
return 0;
}
+ //connect to the sequencer
+ if(snd_seq_open(&sequencer, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0){
+ fprintf(stderr, "Failed to open ALSA sequencer\n");
+ goto bail;
+ }
+
+ snd_seq_nonblock(sequencer, 1);
+ fprintf(stderr, "MIDI client ID is %d\n", snd_seq_client_id(sequencer));
+
+ //update the sequencer client name
+ if(snd_seq_set_client_name(sequencer, sequencer_name ? sequencer_name : "MIDIMonster") < 0){
+ fprintf(stderr, "Failed to set MIDI client name to %s\n", sequencer_name);
+ goto bail;
+ }
+
//create all ports
for(p = 0; p < n; p++){
data = (midi_instance_data*) inst[p]->impl;
@@ -312,13 +405,13 @@ static int midi_start(){
}
//register all fds to core
- nfds = snd_seq_poll_descriptors_count(sequencer, POLLIN | POLLOUT);
+ nfds = snd_seq_poll_descriptors_count(sequencer, POLLIN | POLLOUT);
pfds = calloc(nfds, sizeof(struct pollfd));
if(!pfds){
fprintf(stderr, "Failed to allocate memory\n");
goto bail;
}
- nfds = snd_seq_poll_descriptors(sequencer, pfds, nfds, POLLIN | POLLOUT);
+ nfds = snd_seq_poll_descriptors(sequencer, pfds, nfds, POLLIN | POLLOUT);
fprintf(stderr, "MIDI backend registering %d descriptors to core\n", nfds);
for(p = 0; p < nfds; p++){
@@ -355,12 +448,17 @@ static int midi_shutdown(){
free(inst);
//close midi
- snd_seq_close(sequencer);
- sequencer = NULL;
+ if(sequencer){
+ snd_seq_close(sequencer);
+ sequencer = NULL;
+ }
//free configuration cache
snd_config_update_free_global();
+ free(sequencer_name);
+ sequencer_name = NULL;
+
fprintf(stderr, "MIDI backend shut down\n");
return 0;
}
diff --git a/backends/midi.h b/backends/midi.h
index 556706f..4e16f90 100644
--- a/backends/midi.h
+++ b/backends/midi.h
@@ -1,10 +1,10 @@
#include "midimonster.h"
-int init();
+MM_PLUGIN_API int init();
static int midi_configure(char* option, char* value);
static int midi_configure_instance(instance* instance, char* option, char* value);
static instance* midi_instance();
-static channel* midi_channel(instance* instance, char* spec);
+static channel* midi_channel(instance* instance, char* spec, uint8_t flags);
static int midi_set(instance* inst, size_t num, channel** c, channel_value* v);
static int midi_handle(size_t num, managed_fd* fds);
static int midi_start();
@@ -15,3 +15,14 @@ typedef struct /*_midi_instance_data*/ {
char* read;
char* write;
} midi_instance_data;
+
+typedef union {
+ struct {
+ uint8_t pad[5];
+ uint8_t type;
+ uint8_t channel;
+ uint8_t control;
+ } fields;
+ uint64_t label;
+} midi_channel_ident;
+
diff --git a/backends/midi.md b/backends/midi.md
new file mode 100644
index 0000000..108860e
--- /dev/null
+++ b/backends/midi.md
@@ -0,0 +1,65 @@
+### The `midi` backend
+
+The MIDI backend provides read-write access to the MIDI protocol via virtual ports.
+
+#### Global configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|-----------------------|
+| `name` | `MIDIMonster` | none | MIDI client name |
+| `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration. |
+
+#### Instance configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|-----------------------|
+| `read` | `20:0` | none | MIDI device to connect for input |
+| `write` | `DeviceName` | none | MIDI device to connect for output |
+
+MIDI device names may either be `client:port` portnames or prefixes of MIDI device names.
+Run `aconnect -i` to list input ports and `aconnect -o` to list output ports.
+
+Each instance also provides a virtual port, so MIDI devices can also be connected with `aconnect <sender> <receiver>`.
+
+#### Channel specification
+
+The MIDI 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
+* `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>`.
+
+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:
+```
+midi1.ch0.note9 > midi2.channel1.cc4
+midi1.channel15.pressure1 > midi1.channel0.note0
+midi1.ch1.aftertouch > midi2.ch2.cc0
+midi1.ch0.pitch > midi2.ch1.pitch
+```
+#### Known bugs / problems
+
+To access MIDI data, the user running MIDIMonster needs read & write access to the ALSA sequencer.
+This can usually be done by adding this user to the `audio` system group.
+
+Currently, no Note Off messages are sent (instead, Note On messages with a velocity of 0 are
+generated, which amount to the same thing according to the spec). This may be implemented as
+a configuration option at a later time.
+
+NRPNs are not yet fully implemented, though rudimentary support is in the codebase.
+
+To see which events your MIDI devices output, ALSA provides the `aseqdump` utility. You can
+list all incoming events using `aseqdump -p <portname>`.
diff --git a/backends/ola.cpp b/backends/ola.cpp
new file mode 100644
index 0000000..c13e8f9
--- /dev/null
+++ b/backends/ola.cpp
@@ -0,0 +1,318 @@
+#include "ola.h"
+#include <cstring>
+#include <ola/DmxBuffer.h>
+#include <ola/Logging.h>
+#include <ola/OlaClientWrapper.h>
+#include <ola/client/OlaClient.h>
+#include <ola/io/SelectServer.h>
+#include <ola/network/Socket.h>
+
+#define BACKEND_NAME "ola"
+static ola::io::SelectServer* ola_select = NULL;
+static ola::OlaCallbackClient* ola_client = NULL;
+
+MM_PLUGIN_API int init(){
+ backend ola = {
+ .name = BACKEND_NAME,
+ .conf = ola_configure,
+ .create = ola_instance,
+ .conf_instance = ola_configure_instance,
+ .channel = ola_channel,
+ .handle = ola_set,
+ .process = ola_handle,
+ .start = ola_start,
+ .shutdown = ola_shutdown
+ };
+
+ //register backend
+ if(mm_backend_register(ola)){
+ fprintf(stderr, "Failed to register OLA backend\n");
+ return 1;
+ }
+
+ ola::InitLogging(ola::OLA_LOG_WARN, ola::OLA_LOG_STDERR);
+ return 0;
+}
+
+static int ola_configure(char* option, char* value){
+ fprintf(stderr, "Unknown OLA backend option %s\n", option);
+ return 1;
+}
+
+static instance* ola_instance(){
+ ola_instance_data* data = NULL;
+ instance* inst = mm_instance();
+ if(!inst){
+ return NULL;
+ }
+
+ data = (ola_instance_data*)calloc(1, sizeof(ola_instance_data));
+ if(!data){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return NULL;
+ }
+
+ inst->impl = data;
+ return inst;
+}
+
+static int ola_configure_instance(instance* inst, char* option, char* value){
+ ola_instance_data* data = (ola_instance_data*) inst->impl;
+
+ if(!strcmp(option, "universe")){
+ data->universe_id = strtoul(value, NULL, 0);
+ return 0;
+ }
+
+ fprintf(stderr, "Unknown OLA option %s for instance %s\n", option, inst->name);
+ return 1;
+}
+
+static channel* ola_channel(instance* inst, char* spec, uint8_t flags){
+ ola_instance_data* data = (ola_instance_data*) inst->impl;
+ char* spec_next = spec;
+ unsigned chan_a = strtoul(spec, &spec_next, 10);
+ unsigned chan_b = 0;
+
+ //primary channel sanity check
+ if(!chan_a || chan_a > 512){
+ fprintf(stderr, "Invalid OLA channel specification %s\n", spec);
+ return NULL;
+ }
+ chan_a--;
+
+ //secondary channel setup
+ if(*spec_next == '+'){
+ chan_b = strtoul(spec_next + 1, NULL, 10);
+ if(!chan_b || chan_b > 512){
+ fprintf(stderr, "Invalid wide-channel spec %s\n", spec);
+ return NULL;
+ }
+ chan_b--;
+
+ //if mapped mode differs, bail
+ if(IS_ACTIVE(data->data.map[chan_b]) && data->data.map[chan_b] != (MAP_FINE | chan_a)){
+ fprintf(stderr, "Fine channel already mapped for OLA spec %s\n", spec);
+ return NULL;
+ }
+
+ data->data.map[chan_b] = MAP_FINE | chan_a;
+ }
+
+ //check current map mode
+ if(IS_ACTIVE(data->data.map[chan_a])){
+ if((*spec_next == '+' && data->data.map[chan_a] != (MAP_COARSE | chan_b))
+ || (*spec_next != '+' && data->data.map[chan_a] != (MAP_SINGLE | chan_a))){
+ fprintf(stderr, "Primary OLA channel already mapped at differing mode: %s\n", spec);
+ return NULL;
+ }
+ }
+ data->data.map[chan_a] = (*spec_next == '+') ? (MAP_COARSE | chan_b) : (MAP_SINGLE | chan_a);
+
+ return mm_channel(inst, chan_a, 1);
+}
+
+static int ola_set(instance* inst, size_t num, channel** c, channel_value* v){
+ size_t u, mark = 0;
+ ola_instance_data* data = (ola_instance_data*) inst->impl;
+
+ for(u = 0; u < num; u++){
+ if(IS_WIDE(data->data.map[c[u]->ident])){
+ 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.data[c[u]->ident] != ((val >> 8) & 0xFF)){
+ mark = 1;
+ data->data.data[c[u]->ident] = (val >> 8) & 0xFF;
+ }
+
+ if(data->data.data[MAPPED_CHANNEL(data->data.map[c[u]->ident])] != (val & 0xFF)){
+ mark = 1;
+ data->data.data[MAPPED_CHANNEL(data->data.map[c[u]->ident])] = val & 0xFF;
+ }
+ }
+ else if(data->data.data[c[u]->ident] != (v[u].normalised * 255.0)){
+ mark = 1;
+ data->data.data[c[u]->ident] = v[u].normalised * 255.0;
+ }
+ }
+
+ if(mark){
+ ola_client->SendDmx(data->universe_id, ola::DmxBuffer(data->data.data, 512));
+ }
+
+ return 0;
+}
+
+static int ola_handle(size_t num, managed_fd* fds){
+ if(!num){
+ return 0;
+ }
+
+ //defer input to ola via the scenic route...
+ ola_select->RunOnce();
+ return 0;
+}
+
+static void ola_data_receive(unsigned int universe, const ola::DmxBuffer& ola_dmx, const std::string& error) {
+ size_t p, max_mark = 0;
+ //this should really be size_t but ola is weird...
+ unsigned int dmx_length = 512;
+ uint8_t raw_dmx[dmx_length];
+ uint16_t wide_val;
+ channel* chan = NULL;
+ channel_value val;
+ instance* inst = mm_instance_find(BACKEND_NAME, universe);
+ if(!inst){
+ return;
+ }
+ ola_instance_data* data = (ola_instance_data*) inst->impl;
+ ola_dmx.Get((uint8_t*)raw_dmx, &dmx_length);
+
+ //read data into instance universe, mark changed channels
+ for(p = 0; p < dmx_length; p++){
+ if(IS_ACTIVE(data->data.map[p]) && raw_dmx[p] != data->data.data[p]){
+ data->data.data[p] = raw_dmx[p];
+ data->data.map[p] |= MAP_MARK;
+ max_mark = p;
+ }
+ }
+
+ //generate channel events
+ for(p = 0; p <= max_mark; p++){
+ if(data->data.map[p] & MAP_MARK){
+ data->data.map[p] &= ~MAP_MARK;
+ 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){
+ fprintf(stderr, "Active channel %zu on %s not known to core\n", p, inst->name);
+ return;
+ }
+
+ if(IS_WIDE(data->data.map[p])){
+ data->data.map[MAPPED_CHANNEL(data->data.map[p])] &= ~MAP_MARK;
+ wide_val = data->data.data[p] << ((data->data.map[p] & MAP_COARSE) ? 8 : 0);
+ wide_val |= data->data.data[MAPPED_CHANNEL(data->data.map[p])] << ((data->data.map[p] & MAP_COARSE) ? 0 : 8);
+
+ val.raw.u64 = wide_val;
+ val.normalised = (double) wide_val / (double) 0xFFFF;
+ }
+ else{
+ val.raw.u64 = data->data.data[p];
+ val.normalised = (double) data->data.data[p] / 255.0;
+ }
+
+ if(mm_channel_event(chan, val)){
+ fprintf(stderr, "Failed to push OLA channel event to core\n");
+ return;
+ }
+ }
+ }
+}
+
+static void ola_register_callback(const std::string &error) {
+ if(!error.empty()){
+ fprintf(stderr, "OLA backend failed to register for universe: %s\n", error.c_str());
+ }
+}
+
+static int ola_start(){
+ size_t n, u, p;
+ instance** inst = NULL;
+ ola_instance_data* data = NULL;
+
+ ola_select = new ola::io::SelectServer();
+ ola::network::IPV4SocketAddress ola_server(ola::network::IPV4Address::Loopback(), ola::OLA_DEFAULT_PORT);
+ ola::network::TCPSocket* ola_socket = ola::network::TCPSocket::Connect(ola_server);
+ if(!ola_socket){
+ fprintf(stderr, "Failed to connect to OLA server\n");
+ return 1;
+ }
+
+ ola_client = new ola::OlaCallbackClient(ola_socket);
+
+ if(!ola_client->Setup()){
+ fprintf(stderr, "Failed to start OLA client\n");
+ goto bail;
+ }
+
+ ola_select->AddReadDescriptor(ola_socket);
+
+ fprintf(stderr, "OLA backend registering connection descriptor to core\n");
+ if(mm_manage_fd(ola_socket->ReadDescriptor(), BACKEND_NAME, 1, NULL)){
+ goto bail;
+ }
+
+ ola_client->SetDmxCallback(ola::NewCallback(&ola_data_receive));
+
+ //fetch all defined instances
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ goto bail;
+ }
+
+ //this should not happen anymore (backends without instances are not started anymore)
+ if(!n){
+ free(inst);
+ return 0;
+ }
+
+ for(u = 0; u < n; u++){
+ data = (ola_instance_data*) inst[u]->impl;
+ inst[u]->ident = data->universe_id;
+
+ //check for duplicate instances (using the same universe)
+ for(p = 0; p < u; p++){
+ if(inst[u]->ident == inst[p]->ident){
+ fprintf(stderr, "OLA universe used in multiple instances, use one instance: %s - %s\n", inst[u]->name, inst[p]->name);
+ goto bail;
+ }
+ }
+ ola_client->RegisterUniverse(data->universe_id, ola::REGISTER, ola::NewSingleCallback(&ola_register_callback));
+ }
+
+ //run the ola select implementation to run all commands
+ ola_select->RunOnce();
+ free(inst);
+ return 0;
+bail:
+ free(inst);
+ delete ola_client;
+ ola_client = NULL;
+ delete ola_select;
+ ola_select = NULL;
+ return 1;
+}
+
+static int ola_shutdown(){
+ size_t n, p;
+ instance** inst = NULL;
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ return 1;
+ }
+
+ for(p = 0; p < n; p++){
+ free(inst[p]->impl);
+ }
+ free(inst);
+
+ if(ola_client){
+ ola_client->Stop();
+ delete ola_client;
+ ola_client = NULL;
+ }
+
+ if(ola_select){
+ ola_select->Terminate();
+ delete ola_select;
+ ola_select = NULL;
+ }
+
+ fprintf(stderr, "OLA backend shut down\n");
+ return 0;
+}
diff --git a/backends/ola.h b/backends/ola.h
new file mode 100644
index 0000000..0c42bac
--- /dev/null
+++ b/backends/ola.h
@@ -0,0 +1,38 @@
+extern "C" {
+ #include "midimonster.h"
+ //C++ has it's own implementation of these...
+ #undef min
+ #undef max
+
+ MM_PLUGIN_API int init();
+ static int ola_configure(char* option, char* value);
+ static int ola_configure_instance(instance* instance, char* option, char* value);
+ static instance* ola_instance();
+ static channel* ola_channel(instance* instance, char* spec, uint8_t flags);
+ static int ola_set(instance* inst, size_t num, channel** c, channel_value* v);
+ static int ola_handle(size_t num, managed_fd* fds);
+ static int ola_start();
+ static int ola_shutdown();
+}
+
+#define MAP_COARSE 0x0200
+#define MAP_FINE 0x0400
+#define MAP_SINGLE 0x0800
+#define MAP_MARK 0x1000
+#define MAPPED_CHANNEL(a) ((a) & 0x01FF)
+#define IS_ACTIVE(a) ((a) & 0xFE00)
+#define IS_WIDE(a) ((a) & (MAP_FINE | MAP_COARSE))
+#define IS_SINGLE(a) ((a) & MAP_SINGLE)
+
+//since ola seems to immediately loop back any sent data as input, we only use one buffer
+//to avoid excessive event feedback loops
+typedef struct /*_ola_universe_model*/ {
+ uint8_t data[512];
+ uint16_t map[512];
+} ola_universe;
+
+typedef struct /*_ola_instance_model*/ {
+ /*TODO does ola support remote connections?*/
+ unsigned int universe_id;
+ ola_universe data;
+} ola_instance_data;
diff --git a/backends/ola.md b/backends/ola.md
new file mode 100644
index 0000000..e3a1197
--- /dev/null
+++ b/backends/ola.md
@@ -0,0 +1,41 @@
+### The `ola` backend
+
+This backend connects the MIDIMonster to the Open Lighting Architecture daemon. This can be useful
+to take advantage of additional protocols implemented in OLA. This backend is currently marked as
+optional and is only built with `make full` in the `backends/` directory, as the OLA is a large
+dependency to require for all users.
+
+#### Global configuration
+
+This backend does not take any global configuration.
+
+#### Instance configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|---------------|-------------------------------------------------------|
+| `universe` | `7` | `0` | OLA universe to send/receive data on |
+
+#### Channel specification
+
+A channel is specified by it's universe index. Channel indices start at 1 and end at 512.
+
+Example mapping:
+```
+ola1.231 < in2.123
+```
+
+A 16-bit channel (spanning any two normal 8-bit channels in the same universe, also called a wide channel) may be mapped with the syntax
+```
+ola1.1+2 > net2.5+123
+```
+
+A normal channel that is part of a wide channel can not be mapped individually.
+
+#### Known bugs / problems
+
+The backend currently assumes that the OLA daemon is running on the same host as the MIDIMonster.
+This may be made configurable in the future.
+
+This backend requires `libola-dev` to be installed, which pulls in a rather large and aggressive (in terms of probing
+and taking over connected hardware) daemon. It is thus marked as optional and only built when executing the `full` target
+within the `backends` directory. \ No newline at end of file
diff --git a/backends/osc.c b/backends/osc.c
index 5f94ec2..757ad89 100644
--- a/backends/osc.c
+++ b/backends/osc.c
@@ -1,9 +1,8 @@
#include <string.h>
-#include <unistd.h>
#include <ctype.h>
-#include <netdb.h>
#include <errno.h>
-#include <fcntl.h>
+
+#include "libmmbackend.h"
#include "osc.h"
/*
@@ -14,19 +13,30 @@
#define osc_align(a) ((((a) / 4) + (((a) % 4) ? 1 : 0)) * 4)
#define BACKEND_NAME "osc"
-int init(){
+static struct {
+ uint8_t detect;
+} osc_global_config = {
+ .detect = 0
+};
+
+MM_PLUGIN_API int init(){
backend osc = {
.name = BACKEND_NAME,
- .conf = backend_configure,
- .create = backend_instance,
- .conf_instance = backend_configure_instance,
- .channel = backend_channel,
- .handle = backend_set,
- .process = backend_handle,
- .start = backend_start,
- .shutdown = backend_shutdown
+ .conf = osc_configure,
+ .create = osc_instance,
+ .conf_instance = osc_configure_instance,
+ .channel = osc_map_channel,
+ .handle = osc_set,
+ .process = osc_handle,
+ .start = osc_start,
+ .shutdown = osc_shutdown
};
+ if(sizeof(osc_channel_ident) != sizeof(uint64_t)){
+ fprintf(stderr, "OSC channel identification union out of bounds\n");
+ return 1;
+ }
+
//register backend
if(mm_backend_register(osc)){
fprintf(stderr, "Failed to register OSC backend\n");
@@ -36,6 +46,7 @@ int init(){
}
static size_t osc_data_length(osc_parameter_type t){
+ //binary representation lengths for osc data types
switch(t){
case int32:
case float32:
@@ -50,6 +61,7 @@ static size_t osc_data_length(osc_parameter_type t){
}
static inline void osc_defaults(osc_parameter_type t, osc_parameter_value* max, osc_parameter_value* min){
+ //data type default ranges
memset(max, 0, sizeof(osc_parameter_value));
memset(min, 0, sizeof(osc_parameter_value));
switch(t){
@@ -72,6 +84,7 @@ static inline void osc_defaults(osc_parameter_type t, osc_parameter_value* max,
}
static inline osc_parameter_value osc_parse(osc_parameter_type t, uint8_t* data){
+ //read value from binary representation
osc_parameter_value v = {0};
switch(t){
case int32:
@@ -89,6 +102,7 @@ static inline osc_parameter_value osc_parse(osc_parameter_type t, uint8_t* data)
}
static inline int osc_deparse(osc_parameter_type t, osc_parameter_value v, uint8_t* data){
+ //write value to binary representation
uint64_t u64 = 0;
uint32_t u32 = 0;
switch(t){
@@ -110,6 +124,7 @@ static inline int osc_deparse(osc_parameter_type t, osc_parameter_value v, uint8
}
static inline osc_parameter_value osc_parse_value_spec(osc_parameter_type t, char* value){
+ //read value from string
osc_parameter_value v = {0};
switch(t){
case int32:
@@ -131,6 +146,7 @@ static inline osc_parameter_value osc_parse_value_spec(osc_parameter_type t, cha
}
static inline channel_value osc_parameter_normalise(osc_parameter_type t, osc_parameter_value min, osc_parameter_value max, osc_parameter_value cur){
+ //normalise osc value wrt given min/max
channel_value v = {
.raw = {0},
.normalised = 0
@@ -168,18 +184,13 @@ static inline channel_value osc_parameter_normalise(osc_parameter_type t, osc_pa
fprintf(stderr, "Invalid OSC type passed to interpolation routine\n");
}
- //fix overshoot
- if(v.normalised > 1.0){
- v.normalised = 1.0;
- }
- else if(v.normalised < 0.0){
- v.normalised = 0.0;
- }
-
+ //clamp to range
+ v.normalised = clamp(v.normalised, 1.0, 0.0);
return v;
}
static inline osc_parameter_value osc_parameter_denormalise(osc_parameter_type t, osc_parameter_value min, osc_parameter_value max, channel_value cur){
+ //convert normalised value to osc value wrt given min/max
osc_parameter_value v = {0};
union {
@@ -213,167 +224,282 @@ static inline osc_parameter_value osc_parameter_denormalise(osc_parameter_type t
return v;
}
-static int osc_generate_event(channel* c, osc_channel* info, char* fmt, uint8_t* data, size_t data_len){
- size_t p, off = 0;
- if(!c || !info){
- return 0;
- }
-
- osc_parameter_value min, max, cur;
- channel_value evt;
-
- if(!fmt || !data || data_len % 4 || !*fmt){
- fprintf(stderr, "Invalid OSC packet, data length %zu\n", data_len);
+static int osc_path_validate(char* path, uint8_t allow_patterns){
+ //validate osc path or pattern
+ char illegal_chars[] = " #,";
+ char pattern_chars[] = "?[]{}*";
+ size_t u, c;
+ uint8_t square_open = 0, curly_open = 0;
+
+ if(path[0] != '/'){
+ fprintf(stderr, "%s is not a valid OSC path: Missing root /\n", path);
return 1;
}
- //find offset for this parameter
- for(p = 0; p < info->param_index; p++){
- off += osc_data_length(fmt[p]);
- }
+ for(u = 0; u < strlen(path); u++){
+ for(c = 0; c < sizeof(illegal_chars); c++){
+ if(path[u] == illegal_chars[c]){
+ fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, illegal_chars[c], u);
+ return 1;
+ }
+ }
- if(info->type != not_set){
- max = info->max;
- min = info->min;
- }
- else{
- osc_defaults(fmt[info->param_index], &max, &min);
- }
+ if(!isgraph(path[u])){
+ fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, pattern_chars[c], u);
+ return 1;
+ }
- cur = osc_parse(fmt[info->param_index], data + off);
- evt = osc_parameter_normalise(fmt[info->param_index], min, max, cur);
+ if(!allow_patterns){
+ for(c = 0; c < sizeof(pattern_chars); c++){
+ if(path[u] == pattern_chars[c]){
+ fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, pattern_chars[c], u);
+ return 1;
+ }
+ }
+ }
- return mm_channel_event(c, evt);
-}
+ switch(path[u]){
+ case '{':
+ if(square_open || curly_open){
+ fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, pattern_chars[c], u);
+ return 1;
+ }
+ curly_open = 1;
+ break;
+ case '[':
+ if(square_open || curly_open){
+ fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, pattern_chars[c], u);
+ return 1;
+ }
+ square_open = 1;
+ break;
+ case '}':
+ curly_open = 0;
+ break;
+ case ']':
+ square_open = 0;
+ break;
+ case '/':
+ if(square_open || curly_open){
+ fprintf(stderr, "%s is not a valid OSC path: Pattern across part boundaries\n", path);
+ return 1;
+ }
+ }
+ }
-static int osc_validate_path(char* path){
- if(path[0] != '/'){
- fprintf(stderr, "%s is not a valid OSC path: Missing root /\n", path);
+ if(square_open || curly_open){
+ fprintf(stderr, "%s is not a valid OSC path: Unterminated pattern expression\n", path);
return 1;
}
return 0;
}
-static int osc_separate_hostspec(char* in, char** host, char** port){
- size_t u;
-
- if(!in || !host || !port){
- return 1;
- }
+static int osc_path_match(char* pattern, char* path){
+ size_t u, p = 0, match_begin, match_end;
+ uint8_t match_any = 0, inverted, match;
- for(u = 0; in[u] && !isspace(in[u]); u++){
- }
+ for(u = 0; u < strlen(path); u++){
+ switch(pattern[p]){
+ case '/':
+ if(match_any){
+ for(; path[u] && path[u] != '/'; u++){
+ }
+ }
+ if(path[u] != '/'){
+ return 0;
+ }
+ match_any = 0;
+ p++;
+ break;
+ case '?':
+ match_any = 0;
+ p++;
+ break;
+ case '*':
+ match_any = 1;
+ p++;
+ break;
+ case '[':
+ inverted = (pattern[p + 1] == '!') ? 1 : 0;
+ match_end = match_begin = inverted ? p + 2 : p + 1;
+ match = 0;
+ for(; pattern[match_end] != ']'; match_end++){
+ if(pattern[match_end] == path[u]){
+ match = 1;
+ break;
+ }
- //guess
- *host = in;
+ if(pattern[match_end + 1] == '-' && 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]
+ && path[u] >= pattern[match_end]
+ && path[u] <= pattern[match_end + 2])){
+ match = 1;
+ break;
+ }
+ match_end += 2;
+ }
- if(in[u]){
- in[u] = 0;
- *port = in + u + 1;
- }
- else{
- //no port given
- *port = NULL;
- }
- return 0;
-}
+ if(pattern[match_end + 1] == ']' && match_any && !match
+ && path[u + 1] && path[u + 1] != '/'){
+ match_end = match_begin - 1;
+ u++;
+ }
+ }
-static int osc_listener(char* host, char* port){
- int fd = -1, status, yes = 1, flags;
- struct addrinfo hints = {
- .ai_family = AF_UNSPEC,
- .ai_socktype = SOCK_DGRAM,
- .ai_flags = AI_PASSIVE
- };
- struct addrinfo* info;
- struct addrinfo* addr_it;
+ if(match == inverted){
+ return 0;
+ }
- status = getaddrinfo(host, port, &hints, &info);
- if(status){
- fprintf(stderr, "Failed to get socket info for %s port %s: %s\n", host, port, gai_strerror(status));
- return -1;
- }
+ match_any = 0;
+ //advance to end of pattern
+ for(; pattern[p] != ']'; p++){
+ }
+ p++;
+ break;
+ case '{':
+ for(match_begin = p + 1; pattern[match_begin] != '}'; match_begin++){
+ //find end
+ for(match_end = match_begin; pattern[match_end] != ',' && pattern[match_end] != '}'; match_end++){
+ }
- for(addr_it = info; addr_it != NULL; addr_it = addr_it->ai_next){
- fd = socket(addr_it->ai_family, addr_it->ai_socktype, addr_it->ai_protocol);
- if(fd < 0){
- continue;
- }
+ if(!strncmp(path + u, pattern + match_begin, match_end - match_begin)){
+ //advance pattern
+ for(; pattern[p] != '}'; p++){
+ }
+ p++;
+ //advance path
+ u += match_end - match_begin - 1;
+ break;
+ }
- yes = 1;
- if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){
- fprintf(stderr, "Failed to set SO_REUSEADDR on socket\n");
+ if(pattern[match_end] == '}'){
+ //retry with next if in match_any
+ if(match_any && path[u + 1] && path[u + 1] != '/'){
+ u++;
+ match_begin = p;
+ continue;
+ }
+ return 0;
+ }
+ match_begin = match_end;
+ }
+ match_any = 0;
+ break;
+ case 0:
+ if(match_any){
+ for(; path[u] && path[u] != '/'; u++){
+ }
+ }
+ if(path[u]){
+ return 0;
+ }
+ break;
+ default:
+ if(match_any){
+ for(; path[u] && path[u] != '/' && path[u] != pattern[p]; u++){
+ }
+ }
+ if(pattern[p] != path[u]){
+ return 0;
+ }
+ p++;
+ break;
}
+ }
+ return 1;
+}
- yes = 1;
- if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){
- fprintf(stderr, "Failed to set SO_BROADCAST on socket\n");
+static int osc_configure(char* option, char* value){
+ if(!strcmp(option, "detect")){
+ osc_global_config.detect = 1;
+ if(!strcmp(value, "off")){
+ osc_global_config.detect = 0;
}
+ return 0;
+ }
- yes = 0;
- if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){
- fprintf(stderr, "Failed to unset IP_MULTICAST_LOOP option: %s\n", strerror(errno));
- }
+ fprintf(stderr, "Unknown configuration parameter %s for OSC backend\n", option);
+ return 1;
+}
- status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen);
- if(status < 0){
- close(fd);
- continue;
- }
+static int osc_register_pattern(osc_instance_data* data, char* pattern_path, char* configuration){
+ size_t u, pattern;
+ char* format = NULL, *token = NULL;
- break;
+ if(osc_path_validate(pattern_path, 1)){
+ fprintf(stderr, "Not a valid OSC pattern: %s\n", pattern_path);
+ return 1;
}
- freeaddrinfo(info);
-
- if(!addr_it){
- fprintf(stderr, "Failed to create listening socket for %s port %s\n", host, port);
- return -1;
+ //tokenize configuration
+ format = strtok(configuration, " ");
+ if(!format || strlen(format) < 1){
+ fprintf(stderr, "Not a valid format specification for OSC pattern %s\n", pattern_path);
+ return 1;
}
- //set nonblocking
- flags = fcntl(fd, F_GETFL, 0);
- if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){
- close(fd);
- fprintf(stderr, "Failed to set OSC descriptor nonblocking\n");
- return -1;
+ //create pattern
+ data->pattern = realloc(data->pattern, (data->patterns + 1) * sizeof(osc_channel));
+ if(!data->pattern){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return 1;
}
+ pattern = data->patterns;
- return fd;
-}
-
-static int osc_parse_addr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len){
- struct addrinfo* head;
- struct addrinfo hints = {
- .ai_family = AF_UNSPEC,
- .ai_socktype = SOCK_DGRAM
- };
+ data->pattern[pattern].params = strlen(format);
+ data->pattern[pattern].path = strdup(pattern_path);
+ data->pattern[pattern].type = calloc(strlen(format), sizeof(osc_parameter_type));
+ data->pattern[pattern].max = calloc(strlen(format), sizeof(osc_parameter_value));
+ data->pattern[pattern].min = calloc(strlen(format), sizeof(osc_parameter_value));
- int error = getaddrinfo(host, port, &hints, &head);
- if(error || !head){
- fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(error));
+ if(!data->pattern[pattern].path
+ || !data->pattern[pattern].type
+ || !data->pattern[pattern].max
+ || !data->pattern[pattern].min){
+ //this should fail config parsing and thus call the shutdown function,
+ //which should properly free the rest of the data
+ fprintf(stderr, "Failed to allocate memory\n");
return 1;
}
- memcpy(addr, head->ai_addr, head->ai_addrlen);
- *len = head->ai_addrlen;
+ //check format validity and store min/max values
+ for(u = 0; u < strlen(format); u++){
+ if(!osc_data_length(format[u])){
+ fprintf(stderr, "Invalid format specifier %c for pattern %s\n", format[u], pattern_path);
+ return 1;
+ }
- freeaddrinfo(head);
- return 0;
-}
+ data->pattern[pattern].type[u] = format[u];
-static int backend_configure(char* option, char* value){
- fprintf(stderr, "The OSC backend does not take any global configuration\n");
- return 1;
+ //parse min/max values
+ token = strtok(NULL, " ");
+ if(!token){
+ fprintf(stderr, "Missing minimum specification for parameter %" PRIsize_t " of OSC pattern %s\n", u, pattern_path);
+ return 1;
+ }
+ data->pattern[pattern].min[u] = osc_parse_value_spec(format[u], token);
+
+ token = strtok(NULL, " ");
+ if(!token){
+ fprintf(stderr, "Missing maximum specification for parameter %" PRIsize_t " of OSC pattern %s\n", u, pattern_path);
+ return 1;
+ }
+ data->pattern[pattern].max[u] = osc_parse_value_spec(format[u], token);
+ }
+
+ data->patterns++;
+ return 0;
}
-static int backend_configure_instance(instance* inst, char* option, char* value){
- osc_instance* data = (osc_instance*) inst->impl;
- char* host = NULL, *port = NULL, *token = NULL, *format = NULL;
- size_t u, p;
+static int osc_configure_instance(instance* inst, char* option, char* value){
+ osc_instance_data* data = (osc_instance_data*) inst->impl;
+ char* host = NULL, *port = NULL;
if(!strcmp(option, "root")){
- if(osc_validate_path(value)){
+ if(osc_path_validate(value, 0)){
fprintf(stderr, "Not a valid OSC root: %s\n", value);
return 1;
}
@@ -390,12 +516,13 @@ static int backend_configure_instance(instance* inst, char* option, char* value)
return 0;
}
else if(!strcmp(option, "bind")){
- if(osc_separate_hostspec(value, &host, &port)){
+ mmbackend_parse_hostspec(value, &host, &port);
+ if(!host || !port){
fprintf(stderr, "Invalid bind address for instance %s\n", inst->name);
return 1;
}
- data->fd = osc_listener(host, port);
+ data->fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1);
if(data->fd < 0){
fprintf(stderr, "Failed to bind for instance %s\n", inst->name);
return 1;
@@ -413,101 +540,33 @@ static int backend_configure_instance(instance* inst, char* option, char* value)
return 0;
}
- if(osc_separate_hostspec(value, &host, &port)){
+ mmbackend_parse_hostspec(value, &host, &port);
+ if(!host || !port){
fprintf(stderr, "Invalid destination address for instance %s\n", inst->name);
return 1;
}
- if(osc_parse_addr(host, port, &data->dest, &data->dest_len)){
+ if(mmbackend_parse_sockaddr(host, port, &data->dest, &data->dest_len)){
fprintf(stderr, "Failed to parse destination address for instance %s\n", inst->name);
return 1;
}
return 0;
}
else if(*option == '/'){
- //pre-configure channel
- if(osc_validate_path(option)){
- fprintf(stderr, "Not a valid OSC path: %s\n", option);
- return 1;
- }
-
- for(u = 0; u < data->channels; u++){
- if(!strcmp(option, data->channel[u].path)){
- fprintf(stderr, "OSC channel %s already configured\n", option);
- return 1;
- }
- }
-
- //tokenize configuration
- format = strtok(value, " ");
- if(!format || strlen(format) < 1){
- fprintf(stderr, "Not a valid format for OSC path %s\n", option);
- return 1;
- }
-
- //check format validity, create subchannels
- for(p = 0; p < strlen(format); p++){
- if(!osc_data_length(format[p])){
- fprintf(stderr, "Invalid format specifier %c for path %s, ignoring\n", format[p], option);
- continue;
- }
-
- //register new sub-channel
- data->channel = realloc(data->channel, (data->channels + 1) * sizeof(osc_channel));
- if(!data->channel){
- fprintf(stderr, "Failed to allocate memory\n");
- return 1;
- }
-
- memset(data->channel + data->channels, 0, sizeof(osc_channel));
- data->channel[data->channels].params = strlen(format);
- data->channel[data->channels].param_index = p;
- data->channel[data->channels].type = format[p];
- data->channel[data->channels].path = strdup(option);
-
- if(!data->channel[data->channels].path){
- fprintf(stderr, "Failed to allocate memory\n");
- return 1;
- }
-
- //parse min/max values
- token = strtok(NULL, " ");
- if(!token){
- fprintf(stderr, "Missing minimum specification for parameter %zu of %s\n", p, option);
- return 1;
- }
- data->channel[data->channels].min = osc_parse_value_spec(format[p], token);
-
- token = strtok(NULL, " ");
- if(!token){
- fprintf(stderr, "Missing maximum specification for parameter %zu of %s\n", p, option);
- return 1;
- }
- data->channel[data->channels].max = osc_parse_value_spec(format[p], token);
-
- //allocate channel from core
- if(!mm_channel(inst, data->channels, 1)){
- fprintf(stderr, "Failed to register core channel\n");
- return 1;
- }
-
- //increase channel count
- data->channels++;
- }
- return 0;
+ return osc_register_pattern(data, option, value);
}
- fprintf(stderr, "Unknown configuration parameter %s for OSC backend\n", option);
+ fprintf(stderr, "Unknown configuration parameter %s for OSC instance %s\n", option, inst->name);
return 1;
}
-static instance* backend_instance(){
+static instance* osc_instance(){
instance* inst = mm_instance();
if(!inst){
return NULL;
}
- osc_instance* data = calloc(1, sizeof(osc_instance));
+ osc_instance_data* data = calloc(1, sizeof(osc_instance_data));
if(!data){
fprintf(stderr, "Failed to allocate memory\n");
return NULL;
@@ -518,32 +577,39 @@ static instance* backend_instance(){
return inst;
}
-static channel* backend_channel(instance* inst, char* spec){
- size_t u;
- osc_instance* data = (osc_instance*) inst->impl;
- size_t param_index = 0;
+static channel* osc_map_channel(instance* inst, char* spec, uint8_t flags){
+ size_t u, p;
+ osc_instance_data* data = (osc_instance_data*) inst->impl;
+ osc_channel_ident ident = {
+ .label = 0
+ };
//check spec for correctness
- if(osc_validate_path(spec)){
+ if(osc_path_validate(spec, 0)){
return NULL;
}
//parse parameter offset
if(strrchr(spec, ':')){
- param_index = strtoul(strrchr(spec, ':') + 1, NULL, 10);
+ ident.fields.parameter = strtoul(strrchr(spec, ':') + 1, NULL, 10);
*(strrchr(spec, ':')) = 0;
}
//find matching channel
for(u = 0; u < data->channels; u++){
- if(!strcmp(spec, data->channel[u].path) && data->channel[u].param_index == param_index){
- //fprintf(stderr, "Reusing previously created channel %s parameter %zu\n", data->channel[u].path, data->channel[u].param_index);
+ if(!strcmp(spec, data->channel[u].path)){
break;
}
}
//allocate new channel
if(u == data->channels){
+ for(p = 0; p < data->patterns; p++){
+ if(osc_path_match(data->pattern[p].path, spec)){
+ break;
+ }
+ }
+
data->channel = realloc(data->channel, (u + 1) * sizeof(osc_channel));
if(!data->channel){
fprintf(stderr, "Failed to allocate memory\n");
@@ -551,132 +617,211 @@ static channel* backend_channel(instance* inst, char* spec){
}
memset(data->channel + u, 0, sizeof(osc_channel));
- data->channel[u].param_index = param_index;
data->channel[u].path = strdup(spec);
+ if(p != data->patterns){
+ fprintf(stderr, "Matched pattern %s for %s\n", data->pattern[p].path, spec);
+ data->channel[u].params = data->pattern[p].params;
+ //just reuse the pointers from the pattern
+ data->channel[u].type = data->pattern[p].type;
+ data->channel[u].max = data->pattern[p].max;
+ data->channel[u].min = data->pattern[p].min;
+
+ //these are per channel
+ data->channel[u].in = calloc(data->channel[u].params, sizeof(osc_parameter_value));
+ data->channel[u].out = calloc(data->channel[u].params, sizeof(osc_parameter_value));
+ }
+ else if(data->patterns){
+ fprintf(stderr, "No pattern match found for %s\n", spec);
+ }
- if(!data->channel[u].path){
+ if(!data->channel[u].path
+ || (data->channel[u].params && (!data->channel[u].in || !data->channel[u].out))){
fprintf(stderr, "Failed to allocate memory\n");
return NULL;
}
data->channels++;
}
- return mm_channel(inst, u, 1);
+ ident.fields.channel = u;
+ return mm_channel(inst, ident.label, 1);
}
-static int backend_set(instance* inst, size_t num, channel** c, channel_value* v){
- uint8_t xmit_buf[OSC_XMIT_BUF], *format = NULL;
- size_t evt = 0, off, members, p;
+static int osc_output_channel(instance* inst, size_t channel){
+ osc_instance_data* data = (osc_instance_data*) inst->impl;
+ uint8_t xmit_buf[OSC_XMIT_BUF] = "", *format = NULL;
+ size_t offset = 0, p;
+
+ //fix destination rport if required
+ if(data->forced_rport){
+ //cheating a bit because both IPv4 and IPv6 have the port at the same offset
+ struct sockaddr_in* sockadd = (struct sockaddr_in*) &(data->dest);
+ sockadd->sin_port = htobe16(data->forced_rport);
+ }
+
+ //determine minimum packet size
+ if(osc_align((data->root ? strlen(data->root) : 0) + strlen(data->channel[channel].path) + 1) + osc_align(data->channel[channel].params + 2) >= sizeof(xmit_buf)){
+ fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s\n", inst->name, data->channel[channel].path);
+ return 1;
+ }
+
+ //copy osc target path
+ if(data->root){
+ 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);
+
+ //get format string offset, initialize
+ format = xmit_buf + offset;
+ offset += osc_align(data->channel[channel].params + 2);
+ *format = ',';
+ format++;
+
+ for(p = 0; p < data->channel[channel].params; p++){
+ //write format specifier
+ format[p] = data->channel[channel].type[p];
+
+ //write data
+ if(offset + osc_data_length(data->channel[channel].type[p]) >= sizeof(xmit_buf)){
+ fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s at parameter %" PRIsize_t "\n", inst->name, data->channel[channel].path, p);
+ return 1;
+ }
+
+ osc_deparse(data->channel[channel].type[p],
+ data->channel[channel].out[p],
+ xmit_buf + offset);
+ offset += osc_data_length(data->channel[channel].type[p]);
+ }
+
+ //output packet
+ if(sendto(data->fd, xmit_buf, offset, 0, (struct sockaddr*) &(data->dest), data->dest_len) < 0){
+ fprintf(stderr, "Failed to transmit OSC packet: %s\n", strerror(errno));
+ }
+ return 0;
+}
+
+static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){
+ size_t evt = 0, mark = 0;
+ int rv = 0;
+ osc_channel_ident ident = {
+ .label = 0
+ };
+ osc_parameter_value current;
+
if(!num){
return 0;
}
- osc_instance* data = (osc_instance*) inst->impl;
+ osc_instance_data* data = (osc_instance_data*) inst->impl;
if(!data->dest_len){
- fprintf(stderr, "OSC instance %s does not have a destination, output is disabled (%zu channels)\n", inst->name, num);
+ fprintf(stderr, "OSC instance %s does not have a destination, output is disabled (%" PRIsize_t " channels)\n", inst->name, num);
return 0;
}
for(evt = 0; evt < num; evt++){
- off = c[evt]->ident;
+ ident.label = c[evt]->ident;
//sanity check
- if(off >= data->channels){
+ if(ident.fields.channel >= data->channels
+ || ident.fields.parameter >= data->channel[ident.fields.channel].params){
fprintf(stderr, "OSC channel identifier out of range\n");
return 1;
}
//if the format is unknown, don't output
- if(data->channel[off].type == not_set || data->channel[off].params == 0){
- fprintf(stderr, "OSC channel %s.%s requires format specification for output\n", inst->name, data->channel[off].path);
+ if(!data->channel[ident.fields.channel].params){
+ fprintf(stderr, "OSC channel %s.%s requires format specification for output\n", inst->name, data->channel[ident.fields.channel].path);
continue;
}
- //update current value
- data->channel[off].current = osc_parameter_denormalise(data->channel[off].type, data->channel[off].min, data->channel[off].max, v[evt]);
- //mark channel
- data->channel[off].mark = 1;
+ //only output on change
+ current = osc_parameter_denormalise(data->channel[ident.fields.channel].type[ident.fields.parameter],
+ data->channel[ident.fields.channel].min[ident.fields.parameter],
+ data->channel[ident.fields.channel].max[ident.fields.parameter],
+ v[evt]);
+ if(memcmp(&current, &data->channel[ident.fields.channel].out[ident.fields.parameter], sizeof(current))){
+ //update current value
+ data->channel[ident.fields.channel].out[ident.fields.parameter] = current;
+ //mark channel
+ data->channel[ident.fields.channel].mark = 1;
+ mark = 1;
+ }
}
-
- //fix destination rport if required
- if(data->forced_rport){
- //cheating a bit because both IPv4 and IPv6 have the port at the same offset
- struct sockaddr_in* sockadd = (struct sockaddr_in*) &(data->dest);
- sockadd->sin_port = htobe16(data->forced_rport);
+
+ if(mark){
+ //output all marked channels
+ for(evt = 0; !rv && evt < num; evt++){
+ ident.label = c[evt]->ident;
+ if(data->channel[ident.fields.channel].mark){
+ rv |= osc_output_channel(inst, ident.fields.channel);
+ data->channel[ident.fields.channel].mark = 0;
+ }
+ }
}
+ return rv;
+}
- //find all marked channels
- for(evt = 0; evt < data->channels; evt++){
- //zero output buffer
- memset(xmit_buf, 0, sizeof(xmit_buf));
- if(data->channel[evt].mark){
- //determine minimum packet size
- if(osc_align((data->root ? strlen(data->root) : 0) + strlen(data->channel[evt].path) + 1) + osc_align(data->channel[evt].params + 2) >= sizeof(xmit_buf)){
- fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s\n", inst->name, data->channel[evt].path);
- return 1;
- }
+static int osc_process_packet(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;
+ channel_value evt;
+ osc_channel_ident ident = {
+ .label = 0
+ };
+ channel* chan = NULL;
- off = 0;
- //copy osc target path
- if(data->root){
- memcpy(xmit_buf, data->root, strlen(data->root));
- off += strlen(data->root);
- }
- memcpy(xmit_buf + off, data->channel[evt].path, strlen(data->channel[evt].path));
- off += strlen(data->channel[evt].path) + 1;
- off = osc_align(off);
-
- //get format string offset, initialize
- format = xmit_buf + off;
- off += osc_align(data->channel[evt].params + 2);
- *format = ',';
- format++;
-
- //gather subchannels, unmark
- members = 0;
- for(p = 0; p < data->channels && members < data->channel[evt].params; p++){
- if(!strcmp(data->channel[evt].path, data->channel[p].path)){
- //unmark channel
- data->channel[p].mark = 0;
-
- //sanity check
- if(data->channel[p].param_index >= data->channel[evt].params){
- fprintf(stderr, "OSC channel %s.%s has multiple parameter offset definitions\n", inst->name, data->channel[evt].path);
- return 1;
- }
+ if(payload_len % 4){
+ fprintf(stderr, "Invalid OSC packet, data length %" PRIsize_t "\n", payload_len);
+ return 0;
+ }
- //write format specifier
- format[data->channel[p].param_index] = data->channel[p].type;
+ for(c = 0; c < data->channels; c++){
+ if(!strcmp(local_path, data->channel[c].path)){
+ ident.fields.channel = c;
+ //unconfigured input should work without errors (using default limits)
+ if(data->channel[c].params && strlen(format) != data->channel[c].params){
+ fprintf(stderr, "OSC message %s.%s had format %s, internal representation has %" PRIsize_t " parameters\n", inst->name, local_path, format, data->channel[c].params);
+ continue;
+ }
- //write data
- //FIXME this currently depends on all channels being registered in the correct order, since it just appends data
- if(off + osc_data_length(data->channel[p].type) >= sizeof(xmit_buf)){
- fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s at parameter %zu\n", inst->name, data->channel[evt].path, members);
- return 1;
+ for(p = 0; p < strlen(format); p++){
+ ident.fields.parameter = p;
+ if(data->channel[c].params){
+ max = data->channel[c].max[p];
+ min = data->channel[c].min[p];
+ }
+ else{
+ osc_defaults(format[p], &max, &min);
+ }
+ cur = osc_parse(format[p], payload + offset);
+ if(!data->channel[c].params || memcmp(&cur, &data->channel[c].in, sizeof(cur))){
+ evt = osc_parameter_normalise(format[p], min, max, cur);
+ chan = mm_channel(inst, ident.label, 0);
+ if(chan){
+ mm_channel_event(chan, evt);
}
-
- osc_deparse(data->channel[p].type, data->channel[p].current, xmit_buf + off);
- off += osc_data_length(data->channel[p].type);
- members++;
}
- }
- //output packet
- if(sendto(data->fd, xmit_buf, off, 0, (struct sockaddr*) &(data->dest), data->dest_len) < 0){
- fprintf(stderr, "Failed to transmit OSC packet: %s\n", strerror(errno));
+ //skip to next parameter data
+ offset += osc_data_length(format[p]);
+ //TODO check offset against payload length
}
}
}
+
return 0;
}
-static int backend_handle(size_t num, managed_fd* fds){
+static int osc_handle(size_t num, managed_fd* fds){
size_t fd;
char recv_buf[OSC_RECV_BUF];
instance* inst = NULL;
- osc_instance* data = NULL;
+ osc_instance_data* data = NULL;
ssize_t bytes_read = 0;
- size_t c;
char* osc_fmt = NULL;
char* osc_local = NULL;
uint8_t* osc_data = NULL;
@@ -688,7 +833,7 @@ static int backend_handle(size_t num, managed_fd* fds){
continue;
}
- data = (osc_instance*) inst->impl;
+ data = (osc_instance_data*) inst->impl;
do{
if(data->learn){
@@ -698,16 +843,17 @@ static int backend_handle(size_t num, managed_fd* fds){
else{
bytes_read = recv(fds[fd].fd, recv_buf, sizeof(recv_buf), 0);
}
+
+ if(bytes_read <= 0){
+ 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);
- if(bytes_read < 0){
- break;
- }
-
osc_fmt = recv_buf + osc_align(strlen(recv_buf) + 1);
if(*osc_fmt != ','){
//invalid format string
@@ -716,24 +862,23 @@ static int backend_handle(size_t num, managed_fd* fds){
}
osc_fmt++;
- osc_data = (uint8_t*) osc_fmt + (osc_align(strlen(osc_fmt) + 2) - 1);
+ if(osc_global_config.detect){
+ fprintf(stderr, "Incoming OSC data: Path %s.%s Format %s\n", inst->name, osc_local, osc_fmt);
+ }
+
//FIXME check supplied data length
+ osc_data = (uint8_t*) osc_fmt + (osc_align(strlen(osc_fmt) + 2) - 1);
- for(c = 0; c < data->channels; c++){
- //FIXME implement proper OSC path match
- //prefix match
- if(!strcmp(osc_local, data->channel[c].path)){
- if(strlen(osc_fmt) > data->channel[c].param_index){
- //fprintf(stderr, "Taking parameter %zu of %s (%s), %zd bytes, data offset %zu\n", data->channel[c].param_index, recv_buf, osc_fmt, bytes_read, (osc_data - (uint8_t*)recv_buf));
- if(osc_generate_event(mm_channel(inst, c, 0), data->channel + c, osc_fmt, osc_data, bytes_read - (osc_data - (uint8_t*) recv_buf))){
- fprintf(stderr, "Failed to generate OSC channel event\n");
- }
- }
- }
+ if(osc_process_packet(inst, osc_local, osc_fmt, osc_data, bytes_read - (osc_data - (uint8_t*) recv_buf))){
+ return 1;
}
} while(bytes_read > 0);
+ #ifdef _WIN32
+ if(bytes_read < 0 && WSAGetLastError() != WSAEWOULDBLOCK){
+ #else
if(bytes_read < 0 && errno != EAGAIN){
+ #endif
fprintf(stderr, "OSC failed to receive data for instance %s: %s\n", inst->name, strerror(errno));
}
@@ -746,10 +891,10 @@ static int backend_handle(size_t num, managed_fd* fds){
return 0;
}
-static int backend_start(){
+static int osc_start(){
size_t n, u, fds = 0;
instance** inst = NULL;
- osc_instance* data = NULL;
+ osc_instance_data* data = NULL;
//fetch all instances
if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
@@ -764,7 +909,7 @@ static int backend_start(){
//update instance identifiers
for(u = 0; u < n; u++){
- data = (osc_instance*) inst[u]->impl;
+ data = (osc_instance_data*) inst[u]->impl;
if(data->fd >= 0){
inst[u]->ident = data->fd;
@@ -780,16 +925,16 @@ static int backend_start(){
}
}
- fprintf(stderr, "OSC backend registered %zu descriptors to core\n", fds);
+ fprintf(stderr, "OSC backend registered %" PRIsize_t " descriptors to core\n", fds);
free(inst);
return 0;
}
-static int backend_shutdown(){
+static int osc_shutdown(){
size_t n, u, c;
instance** inst = NULL;
- osc_instance* data = NULL;
+ osc_instance_data* data = NULL;
if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
fprintf(stderr, "Failed to fetch instance list\n");
@@ -797,20 +942,32 @@ static int backend_shutdown(){
}
for(u = 0; u < n; u++){
- data = (osc_instance*) inst[u]->impl;
+ data = (osc_instance_data*) inst[u]->impl;
for(c = 0; c < data->channels; c++){
free(data->channel[c].path);
+ free(data->channel[c].in);
+ free(data->channel[c].out);
}
free(data->channel);
+ for(c = 0; c < data->patterns; c++){
+ free(data->pattern[c].path);
+ free(data->pattern[c].type);
+ free(data->pattern[c].min);
+ free(data->pattern[c].max);
+ }
+ free(data->pattern);
+
free(data->root);
if(data->fd >= 0){
close(data->fd);
}
data->fd = -1;
data->channels = 0;
+ data->patterns = 0;
free(inst[u]->impl);
}
free(inst);
+ fprintf(stderr, "OSC backend shut down\n");
return 0;
}
diff --git a/backends/osc.h b/backends/osc.h
index 5938f12..6f3b923 100644
--- a/backends/osc.h
+++ b/backends/osc.h
@@ -1,19 +1,21 @@
#include "midimonster.h"
#include <sys/types.h>
+#ifndef _WIN32
#include <sys/socket.h>
+#endif
#define OSC_RECV_BUF 8192
#define OSC_XMIT_BUF 8192
-int init();
-static int backend_configure(char* option, char* value);
-static int backend_configure_instance(instance* instance, char* option, char* value);
-static instance* backend_instance();
-static channel* backend_channel(instance* instance, char* spec);
-static int backend_set(instance* inst, size_t num, channel** c, channel_value* v);
-static int backend_handle(size_t num, managed_fd* fds);
-static int backend_start();
-static int backend_shutdown();
+MM_PLUGIN_API int init();
+static int osc_configure(char* option, char* value);
+static int osc_configure_instance(instance* inst, char* option, char* value);
+static instance* osc_instance();
+static channel* osc_map_channel(instance* inst, char* spec, uint8_t flags);
+static int osc_set(instance* inst, size_t num, channel** c, channel_value* v);
+static int osc_handle(size_t num, managed_fd* fds);
+static int osc_start();
+static int osc_shutdown();
typedef enum {
not_set = 0,
@@ -34,22 +36,42 @@ typedef union {
typedef struct /*_osc_channel*/ {
char* path;
size_t params;
- size_t param_index;
uint8_t mark;
- osc_parameter_type type;
- osc_parameter_value max;
- osc_parameter_value min;
- osc_parameter_value current;
+ osc_parameter_type* type;
+ osc_parameter_value* max;
+ osc_parameter_value* min;
+ osc_parameter_value* in;
+ osc_parameter_value* out;
} osc_channel;
typedef struct /*_osc_instance_data*/ {
+ //pre-configured channel patterns
+ size_t patterns;
+ osc_channel* pattern;
+
+ //actual channel registry
size_t channels;
osc_channel* channel;
+
+ //instance config
char* root;
+ uint8_t learn;
+
+ //peer addressing
socklen_t dest_len;
struct sockaddr_storage dest;
- int fd;
- uint8_t learn;
uint16_t forced_rport;
-} osc_instance;
+
+ //peer fd
+ int fd;
+} osc_instance_data;
+
+typedef union {
+ struct {
+ uint32_t channel;
+ uint32_t parameter;
+ } fields;
+ uint64_t label;
+} osc_channel_ident;
+
diff --git a/backends/osc.md b/backends/osc.md
new file mode 100644
index 0000000..1446e06
--- /dev/null
+++ b/backends/osc.md
@@ -0,0 +1,103 @@
+### The `osc` backend
+
+This backend offers read and write access to the Open Sound Control protocol,
+spoken primarily by visual interface tools and hardware such as TouchOSC.
+
+#### Global configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|-----------------------|
+| `detect` | `on` | `off` | Output the path of all incoming OSC packets to allow for easier configuration. Any path filters configured using the `root` instance configuration options still apply. |
+
+#### Instance configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|-----------------------|
+| `root` | `/my/osc/path` | none | An OSC path prefix to be prepended to all channels |
+| `bind` | `:: 8000` | none | The host and port to listen on |
+| `destination` | `10.11.12.13 8001` | none | Remote address to send OSC data to. Setting this enables the instance for output. The special value `learn` causes the MIDImonster to always reply to the address the last incoming packet came from. A different remote port for responses can be forced with the syntax `learn@<port>` |
+
+Note that specifying an instance root speeds up matching, as packets not matching
+it are ignored early in processing.
+
+Channels that are to be output or require a value range different from the default ranges (see below)
+require special configuration, as their types and limits have to be set.
+
+This is done by specifying *patterns* in the instance configuration using an assignment of the syntax
+
+```
+/local/osc/path = <format> <min> <max> <min> <max> ...
+```
+
+The pattern will be matched only against the local part (that is, the path excluding any configured instance root).
+Patterns may contain the following expressions (conforming to the [OSC pattern matching specification](http://opensoundcontrol.org/spec-1_0)):
+* `?` matches any single legal character
+* `*` matches zero or more legal characters
+* A comma-separated list of strings inside curly braces `{}` matches any of the strings
+* A string of characters within square brackets `[]` matches any character in the string
+ * Two characters with a `-` between them specify a range of characters
+ * An exclamation mark immediately after the opening `[` negates the meaning of the expression (ie. it matches characters not in the range)
+* Any other legal character matches only itself
+
+**format** may be any sequence of valid OSC type characters. See below for a table of supported
+OSC types.
+
+For each component of the path, the minimum and maximum values must be given separated by spaces.
+Components may be accessed in the mapping section as detailed in the next section.
+
+An example configuration for transmission of an OSC message with 2 floating point components with
+a range between 0.0 and 2.0 (for example, an X-Y control), would look as follows:
+
+```
+/1/xy1 = ff 0.0 2.0 0.0 2.0
+```
+
+To configure a range of faders, an expression similar to the following line could be used
+
+```
+/1/fader* = f 0.0 1.0
+```
+
+When matching channels against the patterns to use, the first matching pattern (in the order in which they have been configured) will be used
+as configuration for that channel.
+
+#### Channel specification
+
+A channel may be any valid OSC path, to which the instance root will be prepended if
+set. Multi-value controls (such as X-Y pads) are supported by appending `:n` to the path,
+where `n` is the parameter index, with the first (and default) one being `0`.
+
+Example mapping:
+```
+osc1./1/xy1:0 > osc2./1/fader1
+```
+
+Note that any channel that is to be output will need to be set up in the instance
+configuration.
+
+#### Supported types & value ranges
+
+OSC allows controls to have individual value ranges and supports different parameter types.
+The following types are currently supported by the MIDImonster:
+
+* **i**: 32-bit signed integer
+* **f**: 32-bit IEEE floating point
+* **h**: 64-bit signed integer
+* **d**: 64-bit double precision floating point
+
+For each type, there is a default value range which will be assumed if the channel is not otherwise
+configured using the instance configuration. Values out of a channels range will be clipped.
+
+The default ranges are:
+
+* **i**: `0` to `255`
+* **f**: `0.0` to `1.0`
+* **h**: `0` to `1024`
+* **d**: `0.0` to `1.0`
+
+#### Known bugs / problems
+
+The OSC path match currently works on the unit of characters. This may lead to some unexpected results
+when matching expressions of the form `*<expr>`.
+
+Ping requests are not yet answered. There may be some problems using broadcast output and input.
diff --git a/backends/sacn.c b/backends/sacn.c
index fde8d90..2229b8a 100644
--- a/backends/sacn.c
+++ b/backends/sacn.c
@@ -1,14 +1,18 @@
#include <string.h>
#include <sys/types.h>
-#include <sys/socket.h>
-#include <netdb.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>
+#ifndef _WIN32
+#include <netdb.h>
#include <netinet/in.h>
+#include <sys/socket.h>
+#endif
+#include "libmmbackend.h"
#include "sacn.h"
+
//upper limit imposed by using the fd index as 16-bit part of the instance id
#define MAX_FDS 4096
#define BACKEND_NAME "sacn"
@@ -27,7 +31,7 @@ static struct /*_sacn_global_config*/ {
.last_announce = 0
};
-int init(){
+MM_PLUGIN_API int init(){
backend sacn = {
.name = BACKEND_NAME,
.conf = sacn_configure,
@@ -40,6 +44,11 @@ int init(){
.shutdown = sacn_shutdown
};
+ if(sizeof(sacn_instance_id) != sizeof(uint64_t)){
+ fprintf(stderr, "sACN instance identification union out of bounds\n");
+ return 1;
+ }
+
//register the backend
if(mm_backend_register(sacn)){
fprintf(stderr, "Failed to register sACN backend\n");
@@ -50,68 +59,14 @@ int init(){
}
static int sacn_listener(char* host, char* port, uint8_t fd_flags){
- int fd = -1, status, yes = 1, flags;
- struct addrinfo hints = {
- .ai_family = AF_UNSPEC,
- .ai_socktype = SOCK_DGRAM,
- .ai_flags = AI_PASSIVE
- };
- struct addrinfo* info;
- struct addrinfo* addr_it;
-
+ int fd = -1;
if(global_cfg.fds >= MAX_FDS){
fprintf(stderr, "sACN backend descriptor limit reached\n");
return -1;
}
- status = getaddrinfo(host, port, &hints, &info);
- if(status){
- fprintf(stderr, "Failed to get socket info for %s port %s: %s\n", host, port, gai_strerror(status));
- return -1;
- }
-
- for(addr_it = info; addr_it != NULL; addr_it = addr_it->ai_next){
- fd = socket(addr_it->ai_family, addr_it->ai_socktype, addr_it->ai_protocol);
- if(fd < 0){
- continue;
- }
-
- yes = 1;
- if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){
- fprintf(stderr, "Failed to set SO_REUSEADDR on socket\n");
- }
-
- yes = 1;
- if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){
- fprintf(stderr, "Failed to set SO_BROADCAST on socket\n");
- }
-
- yes = 0;
- if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){
- fprintf(stderr, "Failed to unset IP_MULTICAST_LOOP option: %s\n", strerror(errno));
- }
-
- status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen);
- if(status < 0){
- close(fd);
- continue;
- }
-
- break;
- }
-
- freeaddrinfo(info);
-
- if(!addr_it){
- fprintf(stderr, "Failed to create listening socket for %s port %s\n", host, port);
- return -1;
- }
-
- //set nonblocking
- flags = fcntl(fd, F_GETFL, 0);
- if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){
- fprintf(stderr, "Failed to set sACN descriptor nonblocking\n");
- close(fd);
+ fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1);
+ if(fd < 0){
return -1;
}
@@ -123,7 +78,7 @@ static int sacn_listener(char* host, char* port, uint8_t fd_flags){
return -1;
}
- fprintf(stderr, "sACN backend interface %zu bound to %s port %s\n", global_cfg.fds, host, port);
+ fprintf(stderr, "sACN backend interface %" PRIsize_t " bound to %s port %s\n", global_cfg.fds, host, port);
global_cfg.fd[global_cfg.fds].fd = fd;
global_cfg.fd[global_cfg.fds].flags = fd_flags;
global_cfg.fd[global_cfg.fds].universes = 0;
@@ -133,55 +88,6 @@ static int sacn_listener(char* host, char* port, uint8_t fd_flags){
return 0;
}
-static int sacn_parse_addr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len){
- struct addrinfo* head;
- struct addrinfo hints = {
- .ai_family = AF_UNSPEC,
- .ai_socktype = SOCK_DGRAM
- };
-
- int error = getaddrinfo(host, port, &hints, &head);
- if(error || !head){
- fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(error));
- return 1;
- }
-
- memcpy(addr, head->ai_addr, head->ai_addrlen);
- *len = head->ai_addrlen;
-
- freeaddrinfo(head);
- return 0;
-}
-
-static int sacn_parse_hostspec(char* in, char** host, char** port, uint8_t* flags){
- size_t u;
-
- if(!in || !host || !port){
- return 1;
- }
-
- for(u = 0; in[u] && !isspace(in[u]); u++){
- }
-
- //guess
- *host = in;
-
- if(in[u]){
- in[u] = 0;
- *port = in + u + 1;
- }
- else{
- //no port given
- *port = SACN_PORT;
- }
-
- if(flags){
- //TODO parse hostspec trailing data for options
- *flags = 0;
- }
- return 0;
-}
-
static int sacn_configure(char* option, char* value){
char* host = NULL, *port = NULL, *next = NULL;
uint8_t flags = 0;
@@ -204,8 +110,13 @@ static int sacn_configure(char* option, char* value){
}
}
else if(!strcmp(option, "bind")){
- if(sacn_parse_hostspec(value, &host, &port, &flags)){
- fprintf(stderr, "Not a valid sACN bind address: %s\n", value);
+ mmbackend_parse_hostspec(value, &host, &port);
+ if(!port){
+ port = SACN_PORT;
+ }
+
+ if(!host){
+ fprintf(stderr, "No valid sACN bind address provided\n");
return 1;
}
@@ -243,12 +154,17 @@ static int sacn_configure_instance(instance* inst, char* option, char* value){
return 0;
}
else if(!strcmp(option, "destination")){
- if(sacn_parse_hostspec(value, &host, &port, NULL)){
- fprintf(stderr, "Not a valid sACN destination for instance %s: %s\n", inst->name, value);
+ mmbackend_parse_hostspec(value, &host, &port);
+ if(!port){
+ port = SACN_PORT;
+ }
+
+ if(!host){
+ fprintf(stderr, "No valid sACN destination for instance %s\n", inst->name);
return 1;
}
- return sacn_parse_addr(host, port, &data->dest_addr, &data->dest_len);
+ return mmbackend_parse_sockaddr(host, port, &data->dest_addr, &data->dest_len);
}
else if(!strcmp(option, "from")){
next = value;
@@ -282,7 +198,7 @@ static instance* sacn_instance(){
return inst;
}
-static channel* sacn_channel(instance* inst, char* spec){
+static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){
sacn_instance_data* data = (sacn_instance_data*) inst->impl;
char* spec_next = spec;
@@ -362,7 +278,7 @@ static int sacn_transmit(instance* inst){
memcpy(pdu.data.source_name, global_cfg.source_name, sizeof(pdu.data.source_name));
memcpy((((uint8_t*)pdu.data.data) + 1), data->data.out, 512);
- if(sendto(global_cfg.fd[data->fd_index].fd, &pdu, sizeof(pdu), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){
+ if(sendto(global_cfg.fd[data->fd_index].fd, (uint8_t*) &pdu, sizeof(pdu), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){
fprintf(stderr, "Failed to output sACN frame for instance %s: %s\n", inst->name, strerror(errno));
}
@@ -384,7 +300,7 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){
}
if(!data->xmit_prio){
- fprintf(stderr, "sACN instance %s not enabled for output (%zu channel events)\n", inst->name, num);
+ fprintf(stderr, "sACN instance %s not enabled for output (%" PRIsize_t " channel events)\n", inst->name, num);
return 0;
}
@@ -469,7 +385,7 @@ static int sacn_process_frame(instance* inst, sacn_frame_root* frame, sacn_frame
}
if(!chan){
- fprintf(stderr, "Active channel %zu on %s not known to core", u, inst->name);
+ fprintf(stderr, "Active channel %" PRIsize_t " on %s not known to core", u, inst->name);
return 1;
}
@@ -536,8 +452,8 @@ static void sacn_discovery(size_t fd){
pdu.data.page = page;
memcpy(pdu.data.data, global_cfg.fd[fd].universe + page * 512, universes * sizeof(uint16_t));
- if(sendto(global_cfg.fd[fd].fd, &pdu, sizeof(pdu) - (512 - universes) * sizeof(uint16_t), 0, (struct sockaddr*) &discovery_dest, sizeof(discovery_dest)) < 0){
- fprintf(stderr, "Failed to output sACN universe discovery frame for interface %zu: %s\n", fd, strerror(errno));
+ 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){
+ fprintf(stderr, "Failed to output sACN universe discovery frame for interface %" PRIsize_t ": %s\n", fd, strerror(errno));
}
}
}
@@ -603,7 +519,11 @@ static int sacn_handle(size_t num, managed_fd* fds){
}
} while(bytes_read > 0);
+ #ifdef _WIN32
+ if(bytes_read < 0 && WSAGetLastError() != WSAEWOULDBLOCK){
+ #else
if(bytes_read < 0 && errno != EAGAIN){
+ #endif
fprintf(stderr, "sACN failed to receive data: %s\n", strerror(errno));
}
@@ -668,7 +588,7 @@ static int sacn_start(){
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, &mcast_req, sizeof(mcast_req))){
+ if(setsockopt(global_cfg.fd[data->fd_index].fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){
fprintf(stderr, "Failed to join Multicast group for sACN universe %u on instance %s: %s\n", data->uni, inst[u]->name, strerror(errno));
}
}
@@ -695,7 +615,7 @@ static int sacn_start(){
}
}
- fprintf(stderr, "sACN backend registering %zu descriptors to core\n", global_cfg.fds);
+ fprintf(stderr, "sACN backend registering %" PRIsize_t " descriptors to core\n", global_cfg.fds);
for(u = 0; u < global_cfg.fds; u++){
//allocate memory for storing last frame transmission timestamp
global_cfg.fd[u].last_frame = calloc(global_cfg.fd[u].universes, sizeof(uint64_t));
diff --git a/backends/sacn.h b/backends/sacn.h
index e7106f7..1d3268c 100644
--- a/backends/sacn.h
+++ b/backends/sacn.h
@@ -1,11 +1,10 @@
-#include <sys/socket.h>
#include "midimonster.h"
-int init();
+MM_PLUGIN_API int init();
static int sacn_configure(char* option, char* value);
static int sacn_configure_instance(instance* instance, char* option, char* value);
static instance* sacn_instance();
-static channel* sacn_channel(instance* instance, char* spec);
+static channel* sacn_channel(instance* instance, char* spec, uint8_t flags);
static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v);
static int sacn_handle(size_t num, managed_fd* fds);
static int sacn_start();
diff --git a/backends/sacn.md b/backends/sacn.md
new file mode 100644
index 0000000..434beeb
--- /dev/null
+++ b/backends/sacn.md
@@ -0,0 +1,58 @@
+### The `sacn` backend
+
+The sACN backend provides read-write access to the Multicast-UDP based streaming ACN protocol (ANSI E1.31-2016),
+used for lighting fixture control. The backend sends universe discovery frames approximately every 10 seconds,
+containing all write-enabled universes.
+
+#### Global configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|-----------------------|
+| `name` | `sACN source` | `MIDIMonster` | sACN source name |
+| `cid` | `0xAA 0xBB 0xCC` ... | `MIDIMonster` | Source CID (16 bytes) |
+| `bind` | `0.0.0.0 5568` | none | Binds a network address to listen for data. This option may be set multiple times, with each descriptor being assigned an index starting from 0 to be used with the `interface` instance configuration option. At least one descriptor is required for transmission. |
+
+#### Instance configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|-----------------------|
+| `universe` | `1` | none | Universe identifier between 1 and 63999 |
+| `interface` | `1` | `0` | The bound address to use for data input/output |
+| `priority` | `100` | none | The data priority to transmit for this instance. Setting this option enables the instance for output and includes it in the universe discovery report. |
+| `destination` | `10.2.2.2` | Universe multicast | Destination address for unicast output. If unset, the multicast destination for the specified universe is used. |
+| `from` | `0xAA 0xBB` ... | none | 16-byte input source CID filter. Setting this option filters the input stream for this universe. |
+| `unicast` | `1` | `0` | Prevent this instance from joining its universe multicast group |
+
+Note that instances accepting multicast input also process unicast frames directed at them, while
+instances in `unicast` mode will not receive multicast frames.
+
+#### Channel specification
+
+A channel is specified by it's universe index. Channel indices start at 1 and end at 512.
+
+Example mapping:
+```
+sacn1.231 < sacn2.123
+```
+
+A 16-bit channel (spanning any two normal 8-bit channels in the same universe, also called a wide channel) may be mapped with the syntax
+```
+sacn.1+2 > sacn2.5+123
+```
+
+A normal channel that is part of a wide channel can not be mapped individually.
+
+#### Known bugs / problems
+
+The DMX start code of transmitted and received universes is fixed as `0`.
+
+The (upper) limit on packet transmission rate mandated by section 6.6.1 of the sACN specification is disregarded.
+The rate of packet transmission is influenced by the rate of incoming mapped events on the instance.
+
+Universe synchronization is currently not supported, though this feature may be implemented in the future.
+
+To use multicast input, all networking hardware in the path must support the IGMPv2 protocol.
+
+The Linux kernel limits the number of multicast groups an interface may join to 20. An instance configured
+for input automatically joins the multicast group for its universe, unless configured in `unicast` mode.
+This limit can be raised by changing the kernel option in `/proc/sys/net/ipv4/igmp_max_memberships`.
diff --git a/backends/winmidi.c b/backends/winmidi.c
new file mode 100644
index 0000000..790257b
--- /dev/null
+++ b/backends/winmidi.c
@@ -0,0 +1,603 @@
+#include <string.h>
+
+#include "libmmbackend.h"
+#include <mmsystem.h>
+
+#include "winmidi.h"
+
+#define BACKEND_NAME "winmidi"
+
+static struct {
+ uint8_t list_devices;
+ uint8_t detect;
+ int socket_pair[2];
+
+ CRITICAL_SECTION push_events;
+ volatile size_t events_alloc;
+ volatile size_t events_active;
+ volatile winmidi_event* event;
+} backend_config = {
+ .list_devices = 0,
+ .socket_pair = {-1, -1}
+};
+
+//TODO receive feedback socket until EAGAIN
+
+MM_PLUGIN_API int init(){
+ backend winmidi = {
+ .name = BACKEND_NAME,
+ .conf = winmidi_configure,
+ .create = winmidi_instance,
+ .conf_instance = winmidi_configure_instance,
+ .channel = winmidi_channel,
+ .handle = winmidi_set,
+ .process = winmidi_handle,
+ .start = winmidi_start,
+ .shutdown = winmidi_shutdown
+ };
+
+ if(sizeof(winmidi_channel_ident) != sizeof(uint64_t)){
+ fprintf(stderr, "winmidi channel identification union out of bounds\n");
+ return 1;
+ }
+
+ //register backend
+ if(mm_backend_register(winmidi)){
+ fprintf(stderr, "Failed to register winmidi backend\n");
+ return 1;
+ }
+
+ //initialize critical section
+ InitializeCriticalSectionAndSpinCount(&backend_config.push_events, 4000);
+ return 0;
+}
+
+static int winmidi_configure(char* option, char* value){
+ if(!strcmp(option, "list")){
+ backend_config.list_devices = 0;
+ if(!strcmp(value, "on")){
+ backend_config.list_devices = 1;
+ }
+ return 0;
+ }
+ else if(!strcmp(option, "detect")){
+ backend_config.detect = 0;
+ if(!strcmp(value, "on")){
+ backend_config.detect = 1;
+ }
+ return 0;
+ }
+
+ fprintf(stderr, "Unknown winmidi backend option %s\n", option);
+ return 1;
+}
+
+static int winmidi_configure_instance(instance* inst, char* option, char* value){
+ winmidi_instance_data* data = (winmidi_instance_data*) inst->impl;
+ if(!strcmp(option, "read")){
+ if(data->read){
+ fprintf(stderr, "winmidi instance %s already connected to an input device\n", inst->name);
+ return 1;
+ }
+ data->read = strdup(value);
+ return 0;
+ }
+ if(!strcmp(option, "write")){
+ if(data->write){
+ fprintf(stderr, "winmidi instance %s already connected to an output device\n", inst->name);
+ return 1;
+ }
+ data->write = strdup(value);
+ return 0;
+ }
+
+ fprintf(stderr, "Unknown winmidi instance option %s\n", option);
+ return 1;
+}
+
+static instance* winmidi_instance(){
+ instance* i = mm_instance();
+ if(!i){
+ return NULL;
+ }
+
+ i->impl = calloc(1, sizeof(winmidi_instance_data));
+ if(!i->impl){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return NULL;
+ }
+
+ return i;
+}
+
+static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags){
+ char* next_token = NULL;
+ winmidi_channel_ident ident = {
+ .label = 0
+ };
+
+ if(!strncmp(spec, "ch", 2)){
+ next_token = spec + 2;
+ if(!strncmp(spec, "channel", 7)){
+ next_token = spec + 7;
+ }
+ }
+
+ if(!next_token){
+ fprintf(stderr, "Invalid winmidi channel specification %s\n", spec);
+ return NULL;
+ }
+
+ ident.fields.channel = strtoul(next_token, &next_token, 10);
+ if(ident.fields.channel > 15){
+ fprintf(stderr, "MIDI channel out of range in winmidi channel spec %s\n", spec);
+ return NULL;
+ }
+
+ if(*next_token != '.'){
+ fprintf(stderr, "winmidi channel specification %s does not conform to channel<X>.<control><Y>\n", 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{
+ fprintf(stderr, "Unknown winmidi channel control type in %s\n", 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 winmidi_set(instance* inst, size_t num, channel** c, channel_value* v){
+ winmidi_instance_data* data = (winmidi_instance_data*) inst->impl;
+ winmidi_channel_ident ident = {
+ .label = 0
+ };
+ union {
+ struct {
+ uint8_t status;
+ uint8_t data1;
+ uint8_t data2;
+ uint8_t unused;
+ } components;
+ DWORD dword;
+ } output = {
+ .dword = 0
+ };
+ size_t u;
+
+ //early exit
+ if(!num){
+ return 0;
+ }
+
+ if(!data->device_out){
+ fprintf(stderr, "winmidi instance %s has no output device\n", inst->name);
+ return 0;
+ }
+
+ for(u = 0; u < num; u++){
+ ident.label = c[u]->ident;
+
+ //build output message
+ output.components.status = ident.fields.type | ident.fields.channel;
+ output.components.data1 = ident.fields.control;
+ output.components.data2 = v[u].normalised * 127.0;
+ if(ident.fields.type == pitchbend){
+ output.components.data1 = ((int)(v[u].normalised * 16384.0)) & 0x7F;
+ output.components.data2 = (((int)(v[u].normalised * 16384.0)) >> 7) & 0x7F;
+ }
+ else if(ident.fields.type == aftertouch){
+ output.components.data1 = v[u].normalised * 127.0;
+ output.components.data2 = 0;
+ }
+
+ midiOutShortMsg(data->device_out, output.dword);
+ }
+
+ return 0;
+}
+
+static char* winmidi_type_name(uint8_t typecode){
+ switch(typecode){
+ case note:
+ return "note";
+ case cc:
+ return "cc";
+ case pressure:
+ return "pressure";
+ case aftertouch:
+ return "aftertouch";
+ case pitchbend:
+ return "pitch";
+ }
+ return "unknown";
+}
+
+static int winmidi_handle(size_t num, managed_fd* fds){
+ size_t u;
+ ssize_t bytes = 0;
+ char recv_buf[1024];
+ channel* chan = NULL;
+ if(!num){
+ return 0;
+ }
+
+ //flush the feedback socket
+ for(u = 0; u < num; u++){
+ bytes += recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0);
+ }
+
+ //push queued events
+ EnterCriticalSection(&backend_config.push_events);
+ for(u = 0; u < backend_config.events_active; u++){
+ if(backend_config.detect){
+ //pretty-print channel-wide events
+ if(backend_config.event[u].channel.fields.type == pitchbend
+ || backend_config.event[u].channel.fields.type == aftertouch){
+ fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s, value %f\n",
+ backend_config.event[u].inst->name,
+ backend_config.event[u].channel.fields.channel,
+ winmidi_type_name(backend_config.event[u].channel.fields.type),
+ backend_config.event[u].value);
+ }
+ else{
+ fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s%d, value %f\n",
+ backend_config.event[u].inst->name,
+ backend_config.event[u].channel.fields.channel,
+ winmidi_type_name(backend_config.event[u].channel.fields.type),
+ backend_config.event[u].channel.fields.control,
+ backend_config.event[u].value);
+ }
+ }
+ chan = mm_channel(backend_config.event[u].inst, backend_config.event[u].channel.label, 0);
+ if(chan){
+ mm_channel_event(chan, backend_config.event[u].value);
+ }
+ }
+ DBGPF("winmidi flushed %" PRIsize_t " wakeups, handled %" PRIsize_t " events\n", bytes, backend_config.events_active);
+ backend_config.events_active = 0;
+ LeaveCriticalSection(&backend_config.push_events);
+ return 0;
+}
+
+static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DWORD_PTR inst, DWORD param1, DWORD param2){
+ winmidi_channel_ident ident = {
+ .label = 0
+ };
+ channel_value val;
+ union {
+ struct {
+ uint8_t status;
+ uint8_t data1;
+ uint8_t data2;
+ uint8_t unused;
+ } components;
+ DWORD dword;
+ } input = {
+ .dword = 0
+ };
+
+ //callbacks may run on different threads, so we queue all events and alert the main thread via the feedback socket
+ DBGPF("winmidi input callback on thread %ld\n", GetCurrentThreadId());
+
+ switch(message){
+ case MIM_MOREDATA:
+ //processing too slow, do not immediately alert the main loop
+ case MIM_DATA:
+ //param1 has the message
+ input.dword = param1;
+ ident.fields.channel = input.components.status & 0x0F;
+ 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;
+ }
+ else if(ident.fields.type == pitchbend){
+ ident.fields.control = 0;
+ val.normalised = (double)((input.components.data2 << 7) | input.components.data1) / 16384.0;
+ }
+ else if(ident.fields.type == aftertouch){
+ ident.fields.control = 0;
+ val.normalised = (double) input.components.data1 / 127.0;
+ }
+ break;
+ case MIM_LONGDATA:
+ //sysex message, ignore
+ return;
+ case MIM_ERROR:
+ //error in input stream
+ fprintf(stderr, "winmidi warning: error in input stream\n");
+ return;
+ case MIM_OPEN:
+ case MIM_CLOSE:
+ //device opened/closed
+ return;
+
+ }
+
+ DBGPF("winmidi incoming message type %d channel %d control %d value %f\n",
+ ident.fields.type, ident.fields.channel, ident.fields.control, val.normalised);
+
+ EnterCriticalSection(&backend_config.push_events);
+ if(backend_config.events_alloc <= backend_config.events_active){
+ backend_config.event = realloc((void*) backend_config.event, (backend_config.events_alloc + 1) * sizeof(winmidi_event));
+ if(!backend_config.event){
+ fprintf(stderr, "Failed to allocate memory\n");
+ backend_config.events_alloc = 0;
+ backend_config.events_active = 0;
+ LeaveCriticalSection(&backend_config.push_events);
+ return;
+ }
+ backend_config.events_alloc++;
+ }
+ backend_config.event[backend_config.events_active].inst = (instance*) inst;
+ backend_config.event[backend_config.events_active].channel.label = ident.label;
+ backend_config.event[backend_config.events_active].value = val;
+ backend_config.events_active++;
+ LeaveCriticalSection(&backend_config.push_events);
+
+ if(message != MIM_MOREDATA){
+ //alert the main loop
+ send(backend_config.socket_pair[1], "w", 1, 0);
+ }
+}
+
+static void CALLBACK winmidi_output_callback(HMIDIOUT device, unsigned message, DWORD_PTR inst, DWORD param1, DWORD param2){
+ DBGPF("winmidi output callback on thread %ld\n", GetCurrentThreadId());
+}
+
+static int winmidi_match_input(char* prefix){
+ MIDIINCAPS input_caps;
+ unsigned inputs = midiInGetNumDevs();
+ char* next_token = NULL;
+ size_t n;
+
+ if(!prefix){
+ fprintf(stderr, "winmidi detected %u input devices\n", inputs);
+ }
+ else{
+ n = strtoul(prefix, &next_token, 10);
+ if(!(*next_token) && n < inputs){
+ midiInGetDevCaps(n, &input_caps, sizeof(MIDIINCAPS));
+ fprintf(stderr, "winmidi selected input device %s for ID %d\n", input_caps.szPname, n);
+ return n;
+ }
+ }
+
+ //find prefix match for input device
+ for(n = 0; n < inputs; n++){
+ midiInGetDevCaps(n, &input_caps, sizeof(MIDIINCAPS));
+ if(!prefix){
+ printf("\tID %d: %s\n", n, input_caps.szPname);
+ }
+ else if(!strncmp(input_caps.szPname, prefix, strlen(prefix))){
+ fprintf(stderr, "winmidi selected input device %s (ID %" PRIsize_t ") for name %s\n", input_caps.szPname, n, prefix);
+ return n;
+ }
+ }
+
+ return -1;
+}
+
+static int winmidi_match_output(char* prefix){
+ MIDIOUTCAPS output_caps;
+ unsigned outputs = midiOutGetNumDevs();
+ char* next_token = NULL;
+ size_t n;
+
+ if(!prefix){
+ fprintf(stderr, "winmidi detected %u output devices\n", outputs);
+ }
+ else{
+ n = strtoul(prefix, &next_token, 10);
+ if(!(*next_token) && n < outputs){
+ midiOutGetDevCaps(n, &output_caps, sizeof(MIDIOUTCAPS));
+ fprintf(stderr, "winmidi selected output device %s for ID %d\n", output_caps.szPname, n);
+ return n;
+ }
+ }
+
+ //find prefix match for output device
+ for(n = 0; n < outputs; n++){
+ midiOutGetDevCaps(n, &output_caps, sizeof(MIDIOUTCAPS));
+ if(!prefix){
+ printf("\tID %d: %s\n", n, output_caps.szPname);
+ }
+ else if(!strncmp(output_caps.szPname, prefix, strlen(prefix))){
+ fprintf(stderr, "winmidi selected output device %s (ID %" PRIsize_t " for name %s\n", output_caps.szPname, n, prefix);
+ return n;
+ }
+ }
+
+ return -1;
+}
+
+static int winmidi_start(){
+ size_t n = 0, p;
+ int device, rv = -1;
+ instance** inst = NULL;
+ winmidi_instance_data* data = 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("winmidi main thread ID is %ld\n", GetCurrentThreadId());
+
+ //fetch all instances
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ return 1;
+ }
+
+ //no instances, we're done
+ if(!n){
+ free(inst);
+ return 0;
+ }
+
+ //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){
+ fprintf(stderr, "winmidi failed to open feedback socket\n");
+ return 1;
+ }
+ if(getsockname(backend_config.socket_pair[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);
+ fprintf(stderr, "winmidi failed to query feedback socket information: %s\n", error);
+ LocalFree(error);
+ return 1;
+ }
+ //getsockname on 'real' windows may not set the address - works on wine, though
+ switch(sockadd.ss_family){
+ case AF_INET:
+ case AF_INET6:
+ ((struct sockaddr_in*) &sockadd)->sin_family = AF_INET;
+ ((struct sockaddr_in*) &sockadd)->sin_addr.s_addr = htobe32(INADDR_LOOPBACK);
+ break;
+ //for some absurd reason 'real' windows announces the socket as AF_INET6 but rejects any connection unless its AF_INET
+// case AF_INET6:
+// ((struct sockaddr_in6*) &sockadd)->sin6_addr = in6addr_any;
+// break;
+ default:
+ fprintf(stderr, "winmidi invalid feedback socket family\n");
+ return 1;
+ }
+ DBGPF("winmidi feedback socket family %d port %d\n", 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)){
+ FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
+ NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL);
+ fprintf(stderr, "winmidi failed to connect to feedback socket: %s\n", error);
+ LocalFree(error);
+ return 1;
+ }
+
+ //set up instances and start input
+ for(p = 0; p < n; p++){
+ data = (winmidi_instance_data*) inst[p]->impl;
+ inst[p]->ident = p;
+
+ //connect input device if requested
+ if(data->read){
+ device = winmidi_match_input(data->read);
+ if(device < 0){
+ fprintf(stderr, "Failed to match input device %s for instance %s\n", data->read, inst[p]->name);
+ goto bail;
+ }
+ if(midiInOpen(&(data->device_in), device, (DWORD_PTR) winmidi_input_callback, (DWORD_PTR) inst[p], CALLBACK_FUNCTION | MIDI_IO_STATUS) != MMSYSERR_NOERROR){
+ fprintf(stderr, "Failed to open input device for instance %s\n", inst[p]->name);
+ goto bail;
+ }
+ //start midi input callbacks
+ midiInStart(data->device_in);
+ }
+
+ //connect output device if requested
+ if(data->write){
+ device = winmidi_match_output(data->write);
+ if(device < 0){
+ fprintf(stderr, "Failed to match output device %s for instance %s\n", data->read, inst[p]->name);
+ goto bail;
+ }
+ if(midiOutOpen(&(data->device_out), device, (DWORD_PTR) winmidi_output_callback, (DWORD_PTR) inst[p], CALLBACK_FUNCTION) != MMSYSERR_NOERROR){
+ fprintf(stderr, "Failed to open output device for instance %s\n", inst[p]->name);
+ goto bail;
+ }
+ }
+ }
+
+ //register the feedback socket to the core
+ fprintf(stderr, "winmidi backend registering 1 descriptor to core\n");
+ if(mm_manage_fd(backend_config.socket_pair[0], BACKEND_NAME, 1, NULL)){
+ goto bail;
+ }
+
+ rv = 0;
+bail:
+ free(inst);
+ return rv;
+}
+
+static int winmidi_shutdown(){
+ size_t n, u;
+ instance** inst = NULL;
+ winmidi_instance_data* data = NULL;
+
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ return 1;
+ }
+
+ for(u = 0; u < n; u++){
+ data = (winmidi_instance_data*) inst[u]->impl;
+ free(data->read);
+ data->read = NULL;
+ free(data->write);
+ data->write = NULL;
+
+ if(data->device_in){
+ midiInStop(data->device_in);
+ midiInClose(data->device_in);
+ data->device_in = NULL;
+ }
+
+ if(data->device_out){
+ midiOutReset(data->device_out);
+ midiOutClose(data->device_out);
+ data->device_out = NULL;
+ }
+ }
+
+ free(inst);
+ closesocket(backend_config.socket_pair[0]);
+ closesocket(backend_config.socket_pair[1]);
+
+ EnterCriticalSection(&backend_config.push_events);
+ free((void*) backend_config.event);
+ backend_config.event = NULL;
+ backend_config.events_alloc = 0;
+ backend_config.events_active = 0;
+ LeaveCriticalSection(&backend_config.push_events);
+ DeleteCriticalSection(&backend_config.push_events);
+
+ fprintf(stderr, "winmidi backend shut down\n");
+ return 0;
+}
diff --git a/backends/winmidi.h b/backends/winmidi.h
new file mode 100644
index 0000000..985c46a
--- /dev/null
+++ b/backends/winmidi.h
@@ -0,0 +1,43 @@
+#include "midimonster.h"
+
+MM_PLUGIN_API int init();
+static int winmidi_configure(char* option, char* value);
+static int winmidi_configure_instance(instance* inst, char* option, char* value);
+static instance* winmidi_instance();
+static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags);
+static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v);
+static int winmidi_handle(size_t num, managed_fd* fds);
+static int winmidi_start();
+static int winmidi_shutdown();
+
+typedef struct /*_winmidi_instance_data*/ {
+ char* read;
+ char* write;
+ HMIDIIN device_in;
+ HMIDIOUT device_out;
+} winmidi_instance_data;
+
+enum /*_winmidi_channel_type*/ {
+ none = 0,
+ note = 0x90,
+ cc = 0xB0,
+ pressure = 0xA0,
+ aftertouch = 0xD0,
+ pitchbend = 0xE0
+};
+
+typedef union {
+ struct {
+ uint8_t pad[5];
+ uint8_t type;
+ uint8_t channel;
+ uint8_t control;
+ } fields;
+ uint64_t label;
+} winmidi_channel_ident;
+
+typedef struct /*_winmidi_event_queue_entry*/ {
+ instance* inst;
+ winmidi_channel_ident channel;
+ channel_value value;
+} winmidi_event;
diff --git a/backends/winmidi.md b/backends/winmidi.md
new file mode 100644
index 0000000..25a6378
--- /dev/null
+++ b/backends/winmidi.md
@@ -0,0 +1,60 @@
+### The `winmidi` backend
+
+This backend provides read-write access to the MIDI protocol via the Windows Multimedia API.
+
+It is only available when building for Windows. Care has been taken to keep the configuration
+syntax similar to the `midi` backend, but due to differences in the internal programming interfaces,
+some deviations may still be present.
+
+#### Global configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|-----------------------|
+| `list` | `on` | `off` | List available input/output devices on startup |
+| `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration. |
+
+#### Instance configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|-----------------------|
+| `read` | `2` | none | MIDI device to connect for input |
+| `write` | `DeviceName` | none | MIDI device to connect for output |
+
+Input/output device names may either be prefixes of MIDI device names or numeric indices corresponding
+to the listing shown at startup when using the global `list` option.
+
+#### Channel specification
+
+The `winmidi` backend supports mapping different MIDI events as 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:
+```
+midi1.ch0.note9 > midi2.channel1.cc4
+midi1.channel15.pressure1 > midi1.channel0.note0
+midi1.ch1.aftertouch > midi2.ch2.cc0
+midi1.ch0.pitch > midi2.ch1.pitch
+```
+
+#### Known bugs / problems
+
+Currently, no Note Off messages are sent (instead, Note On messages with a velocity of 0 are
+generated, which amount to the same thing according to the spec). This may be implemented as
+a configuration option at a later time.
+
+As this is a Windows-only backend, testing may not be as frequent or thorough as for the Linux / multiplatform
+backends.