diff options
author | cbdev <cb@cbcdn.com> | 2019-12-07 20:22:03 +0100 |
---|---|---|
committer | cbdev <cb@cbcdn.com> | 2019-12-07 20:22:03 +0100 |
commit | f95dae04478b32fccaf7c6ebe1ecfd4e2fef1358 (patch) | |
tree | 6f545dc81166ee1a0f1b21b5245a70bcbf05751d /backends | |
parent | 534207d16314a4f6a69d36cf2305a3fe435a769b (diff) | |
parent | 1bb3b9a3eaf94af045c39a1ff1ee8bf9b8e5b8ec (diff) | |
download | midimonster-f95dae04478b32fccaf7c6ebe1ecfd4e2fef1358.tar.gz midimonster-f95dae04478b32fccaf7c6ebe1ecfd4e2fef1358.tar.bz2 midimonster-f95dae04478b32fccaf7c6ebe1ecfd4e2fef1358.zip |
Merge current master
Diffstat (limited to 'backends')
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(¤t, &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. |