From 95f804bb5f8239d018e8fa440a2ca3e0111d4696 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 22 Mar 2019 21:16:41 +0100 Subject: Implement an OLA backend (Fixes #14) --- backends/Makefile | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'backends/Makefile') diff --git a/backends/Makefile b/backends/Makefile index 446ad70..aef39c4 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,10 +1,12 @@ -.PHONY: all clean +.PHONY: all clean full +OPTIONAL_BACKENDS = ola.so LINUX_BACKENDS = midi.so evdev.so BACKENDS = artnet.so osc.so loopback.so sacn.so SYSTEM := $(shell uname -s) CFLAGS += -fPIC -I../ +CPPFLAGS += -fPIC -I../ LDFLAGS += -shared # Build Linux backends if possible @@ -19,11 +21,18 @@ endif midi.so: LDLIBS = -lasound evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) evdev.so: LDLIBS = $(shell pkg-config --libs libevdev) +ola.so: LDLIBS = -lola +ola.so: CPPFLAGS += -Wno-write-strings %.so :: %.c %.h $(CC) $(CFLAGS) $(LDLIBS) $< -o $@ $(LDFLAGS) +%.so :: %.cpp %.h + $(CXX) $(CPPFLAGS) $(LDLIBS) $< -o $@ $(LDFLAGS) + all: $(BACKENDS) +full: $(BACKENDS) $(OPTIONAL_BACKENDS) + clean: - $(RM) $(BACKENDS) + $(RM) $(BACKENDS) $(OPTIONAL_BACKENDS) -- cgit v1.2.3 From 1ed17293bedaf5bf5182d863cd406d7eb66b4501 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 24 Mar 2019 19:22:30 +0100 Subject: Factor out socket operations (Fixes #13) --- backends/Makefile | 14 ++++-- backends/artnet.c | 131 ++++++++---------------------------------------- backends/libmmbackend.c | 117 ++++++++++++++++++++++++++++++++++++++++++ backends/libmmbackend.h | 31 ++++++++++++ backends/osc.c | 123 +++------------------------------------------ backends/sacn.c | 131 ++++++++---------------------------------------- 6 files changed, 206 insertions(+), 341 deletions(-) create mode 100644 backends/libmmbackend.c create mode 100644 backends/libmmbackend.h (limited to 'backends/Makefile') diff --git a/backends/Makefile b/backends/Makefile index aef39c4..a7ea35a 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -2,6 +2,7 @@ OPTIONAL_BACKENDS = ola.so LINUX_BACKENDS = midi.so evdev.so BACKENDS = artnet.so osc.so loopback.so sacn.so +BACKEND_LIB = libmmbackend.o SYSTEM := $(shell uname -s) @@ -18,6 +19,9 @@ ifeq ($(SYSTEM),Darwin) LDFLAGS += -undefined dynamic_lookup endif +artnet.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +osc.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +sacn.so: ADDITIONAL_OBJS += $(BACKEND_LIB) midi.so: LDLIBS = -lasound evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) evdev.so: LDLIBS = $(shell pkg-config --libs libevdev) @@ -25,14 +29,14 @@ ola.so: LDLIBS = -lola ola.so: CPPFLAGS += -Wno-write-strings %.so :: %.c %.h - $(CC) $(CFLAGS) $(LDLIBS) $< -o $@ $(LDFLAGS) + $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) %.so :: %.cpp %.h - $(CXX) $(CPPFLAGS) $(LDLIBS) $< -o $@ $(LDFLAGS) + $(CXX) $(CPPFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) -all: $(BACKENDS) +all: $(BACKEND_LIB) $(BACKENDS) -full: $(BACKENDS) $(OPTIONAL_BACKENDS) +full: $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) clean: - $(RM) $(BACKENDS) $(OPTIONAL_BACKENDS) + $(RM) $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) diff --git a/backends/artnet.c b/backends/artnet.c index d9ebfe5..8b404a6 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -1,12 +1,9 @@ #include -#include -#include -#include -#include -#include #include #include +#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); + if(fd < 0){ return -1; } @@ -98,50 +41,6 @@ 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(){ backend artnet = { .name = BACKEND_NAME, @@ -171,8 +70,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,12 +133,18 @@ 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); diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c new file mode 100644 index 0000000..6320611 --- /dev/null +++ b/backends/libmmbackend.c @@ -0,0 +1,117 @@ +#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){ + int fd = -1, status, yes = 1, flags; + 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"); + } + + 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)); + } + + 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 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 socket nonblocking\n"); + close(fd); + return -1; + } + + return fd; +} diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h new file mode 100644 index 0000000..38bfca0 --- /dev/null +++ b/backends/libmmbackend.h @@ -0,0 +1,31 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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); diff --git a/backends/osc.c b/backends/osc.c index 5f94ec2..9996f68 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -1,9 +1,8 @@ #include -#include #include -#include #include -#include +#include "libmmbackend.h" + #include "osc.h" /* @@ -254,114 +253,6 @@ static int osc_validate_path(char* path){ return 0; } -static int osc_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 = NULL; - } - return 0; -} - -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; - - 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){ - close(fd); - fprintf(stderr, "Failed to set OSC descriptor nonblocking\n"); - return -1; - } - - 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 - }; - - 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 backend_configure(char* option, char* value){ fprintf(stderr, "The OSC backend does not take any global configuration\n"); return 1; @@ -390,12 +281,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); if(data->fd < 0){ fprintf(stderr, "Failed to bind for instance %s\n", inst->name); return 1; @@ -413,12 +305,13 @@ 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; } diff --git a/backends/sacn.c b/backends/sacn.c index fde8d90..75bb76f 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -8,6 +8,8 @@ #include #include +#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 @@ -50,68 +52,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); + if(fd < 0){ return -1; } @@ -133,55 +81,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 +103,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 +147,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; -- cgit v1.2.3 From 6e8f195c36cb5f5cd6469658937336f2d31ab6e8 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 24 Mar 2019 19:57:45 +0100 Subject: Hack-fix parallel builds --- backends/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'backends/Makefile') diff --git a/backends/Makefile b/backends/Makefile index a7ea35a..c11de56 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -28,7 +28,7 @@ evdev.so: LDLIBS = $(shell pkg-config --libs libevdev) ola.so: LDLIBS = -lola ola.so: CPPFLAGS += -Wno-write-strings -%.so :: %.c %.h +%.so :: %.c %.h $(BACKEND_LIB) $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) %.so :: %.cpp %.h -- cgit v1.2.3 From b618c4a6b74a52f830ca53029e1cc680d56a2501 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 6 Jul 2019 17:25:12 +0200 Subject: Implement Lua backend --- .travis.yml | 1 + README.md | 13 ++-- backends/Makefile | 4 +- backends/lua.c | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/lua.h | 21 ++++++ backends/lua.md | 49 +++++++++++++ configs/demo.lua | 21 ++++++ configs/lua.cfg | 35 ++++++++++ midimonster.h | 1 + 9 files changed, 342 insertions(+), 5 deletions(-) create mode 100644 backends/lua.c create mode 100644 backends/lua.h create mode 100644 backends/lua.md create mode 100644 configs/demo.lua create mode 100644 configs/lua.cfg (limited to 'backends/Makefile') diff --git a/.travis.yml b/.travis.yml index ab047ce..66bfd9a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ addons: - libasound2-dev - libevdev-dev - libola-dev + - liblua5.3-dev packages: &core_build_gpp_latest - *core_build - gcc-8 diff --git a/README.md b/README.md index 6265581..ee907a6 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,15 @@ Currently, the MIDIMonster supports the following protocols: * evdev input devices (Linux) * Open Lighting Architecture (OLA) +with additional flexibility provided by a Lua scripting environment. + The MIDIMonster allows the user to translate any channel on one protocol into channel(s) on any other (or the same) supported protocol, for example to: * Translate MIDI Control Changes into Notes ([Example configuration](configs/unifest-17.cfg)) * Translate MIDI Notes into ArtNet or sACN ([Example configuration](configs/launchctl-sacn.cfg)) * Translate OSC messages into MIDI ([Example configuration](configs/midi-osc.cfg)) +* Dynamically route and modify events using the Lua programming language ([Example configuration](configs/lua.cfg) and [Script](configs/demo.lua)) to create your own lighting controller * Use an OSC app as a simple lighting controller via ArtNet or sACN * Visualize ArtNet data using OSC tools * Control lighting fixtures or DAWs using gamepad controllers ([Example configuration](configs/evdev.conf)) @@ -88,6 +91,7 @@ special information. These documentation files are located in the `backends/` di * [`loopback` backend documentation](backends/loopback.md) * [`ola` backend documentation](backends/ola.md) * [`osc` backend documentation](backends/osc.md) +* [`lua` backend documentation](backends/lua.md) ## Building @@ -99,10 +103,11 @@ This section will explain how to build the provided sources to be able to run In order to build the MIDIMonster, you'll need some libraries that provide support for the protocols to translate. -* libasound2-dev (for the MIDI backend) -* libevdev-dev (for the evdev backend) -* libola-dev (for the optional OLA backend) -* pkg-config (as some projects and systems like to spread their files around) +* `libasound2-dev` (for the MIDI backend) +* `libevdev-dev` (for the evdev backend) +* `liblua5.3-dev` (for the lua backend) +* `libola-dev` (for the optional OLA backend) +* `pkg-config` (as some projects and systems like to spread their files around) * A C compiler * GNUmake diff --git a/backends/Makefile b/backends/Makefile index c11de56..fe88669 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,7 +1,7 @@ .PHONY: all clean full OPTIONAL_BACKENDS = ola.so LINUX_BACKENDS = midi.so evdev.so -BACKENDS = artnet.so osc.so loopback.so sacn.so +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so BACKEND_LIB = libmmbackend.o SYSTEM := $(shell uname -s) @@ -27,6 +27,8 @@ evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) evdev.so: LDLIBS = $(shell pkg-config --libs libevdev) ola.so: LDLIBS = -lola ola.so: CPPFLAGS += -Wno-write-strings +lua.so: CFLAGS += $(shell pkg-config --cflags lua5.3) +lua.so: LDLIBS += $(shell pkg-config --libs lua5.3) %.so :: %.c %.h $(BACKEND_LIB) $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) diff --git a/backends/lua.c b/backends/lua.c new file mode 100644 index 0000000..ae2d460 --- /dev/null +++ b/backends/lua.c @@ -0,0 +1,202 @@ +#include +#include "lua.h" + +#define BACKEND_NAME "lua" +#define LUA_REGISTRY_KEY "_midimonster_lua_instance" + +//TODO instance identification for callvacks + +int init(){ + backend lua = { + .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; + } + return 0; +} + +static int lua_callback_output(lua_State* interpreter){ + int arguments = lua_gettop(interpreter); + size_t n; + channel_value val; + const char* channel_name = NULL; + channel* channel = NULL; + instance* inst = NULL; + lua_instance_data* data = NULL; + + if(arguments != 2){ + fprintf(stderr, "Lua output function called with %d arguments, expected 2\n", arguments); + return 0; + } + + channel_name = lua_tostring(interpreter, 1); + val.normalised = clamp(lua_tonumber(interpreter, 2), 1.0, 0.0); + + lua_pushstring(interpreter, LUA_REGISTRY_KEY); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + inst = (instance *) lua_touserdata(interpreter, -1); + data = (lua_instance_data*) inst->impl; + + 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); + return 0; + } + } + + fprintf(stderr, "Tried to set unknown channel %s.%s\n", inst->name, channel_name); + return 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; + + if(!strcmp(option, "script")){ + 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 backend\n", option); + 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 api functions + lua_register(data->interpreter, "output", lua_callback_output); + + //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){ + 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*)); + if(!data->channel_name){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + + 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; + + for(n = 0; n < num; n++){ + //call lua channel handlers + lua_getglobal(data->interpreter, data->channel_name[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){ + //TODO call timer callbacks + return 0; +} + +static int lua_start(){ + //TODO start timers / register fds + return 0; +} + +static int lua_shutdown(){ + size_t n, u, p; + instance** inst = NULL; + lua_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 = (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(inst[u]->impl); + } + + free(inst); + + 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..27e1afd --- /dev/null +++ b/backends/lua.h @@ -0,0 +1,21 @@ +#include "midimonster.h" + +#include +#include +#include + +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); +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(); + +typedef struct /*_lua_instance_data*/ { + size_t channels; + char** channel_name; + lua_State* interpreter; +} lua_instance_data; diff --git a/backends/lua.md b/backends/lua.md new file mode 100644 index 0000000..91e8fe2 --- /dev/null +++ b/backends/lua.md @@ -0,0 +1,49 @@ +### 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 with +the value (as a Lua `number` type) as parameter. To send output on a channel, the Lua environment +provides the function `output(channel-name, value)`. + +Example script: +``` +function bar(value) + output("foo", value / 2) +end +``` + +Input values range between 0.0 and 1.0, output values are clamped to the same range. + +#### Global configuration + +The backend does not take any global configuration. + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `source` | `script.lua` | none | Lua source 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 `output` as an input channel name to a Lua instance does not work, as the interpreter has +`output` globally assigned to the event output function. Using `output` as an output channel name +via `output("output", value)` works as intended. + +The path to the Lua source files is relative to the current working directory. This may lead +to problems when copying configuration between installations. diff --git a/configs/demo.lua b/configs/demo.lua new file mode 100644 index 0000000..e816ac4 --- /dev/null +++ b/configs/demo.lua @@ -0,0 +1,21 @@ +-- This example MIDIMonstaer Lua script spreads one input channel onto multiple output +-- channels using a polynomial function evaluated at multiple points. This effect can +-- be visualized e.g. with martrix (https://github.com/cbdevnet/martrix). + +-- This is just a demonstration of global variables +foo = 0 + +-- The polynomial to evaluate +function polynomial(offset, x) + return math.exp(-20 * (x - offset) ^ 2) +end + +-- Handler function for the input channel +function input(value) + foo = foo + 1 + print("input at ", value, foo) + + for chan=0,10 do + output("out" .. chan, polynomial(value, (1 / 10) * chan)) + end +end diff --git a/configs/lua.cfg b/configs/lua.cfg new file mode 100644 index 0000000..9182122 --- /dev/null +++ b/configs/lua.cfg @@ -0,0 +1,35 @@ +; This configuration uses a Lua script to distribute one input channel (from either a mouse +; button or an axis control) onto multiple output channels (on ArtNet). + +[backend artnet] +bind = 0.0.0.0 + +[evdev mouse] +device = /dev/input/by-path/platform-i8042-serio-2-event-mouse + +[evdev xbox] +input = Xbox Wireless +axis.ABS_X = 34300 0 65535 255 4095 + +[lua lua] +script = configs/demo.lua + +[artnet art] +universe = 0 +destination = 255.255.255.255 + +[map] +mouse.EV_KEY.BTN_LEFT > lua.input +xbox.EV_ABS.ABS_X > lua.input + +art.1 < lua.out0 +art.2 < lua.out1 +art.3 < lua.out2 +art.4 < lua.out3 +art.5 < lua.out4 +art.6 < lua.out5 +art.7 < lua.out6 +art.8 < lua.out7 +art.9 < lua.out8 +art.10 < lua.out9 +art.11 < lua.out10 diff --git a/midimonster.h b/midimonster.h index 572b5fb..8a18155 100644 --- a/midimonster.h +++ b/midimonster.h @@ -5,6 +5,7 @@ #include #define max(a,b) (((a) > (b)) ? (a) : (b)) #define min(a,b) (((a) < (b)) ? (a) : (b)) +#define clamp(val,max,min) (((val) > (max)) ? (max) : (((val) < (min)) ? (min) : (val))) #ifdef DEBUG #define DBGPF(format, ...) fprintf(stderr, (format), __VA_ARGS__) #define DBG(message) fprintf(stderr, "%s", (message)) -- cgit v1.2.3 From 26ee2eacc7d60aa379c9e4b9b9c6b8bcdcd4bc6b Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 27 Jul 2019 19:31:21 +0200 Subject: Refactor OSC backend, implement pattern matching --- README.md | 4 +- TODO | 1 - backends/Makefile | 4 +- backends/osc.c | 635 ++++++++++++++++++++++++++++++++-------------- backends/osc.h | 25 +- backends/osc.md | 21 +- configs/flying-faders.cfg | 24 ++ configs/flying-faders.lua | 10 + configs/osc-xy.cfg | 26 ++ 9 files changed, 543 insertions(+), 207 deletions(-) create mode 100644 configs/flying-faders.cfg create mode 100644 configs/flying-faders.lua create mode 100644 configs/osc-xy.cfg (limited to 'backends/Makefile') diff --git a/README.md b/README.md index 3704f5f..9647d4f 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ Currently, the MIDIMonster supports the following protocols: * MIDI (Linux, via ALSA) * ArtNet -* sACN / E1.31 -* OSC +* Streaming ACN (sACN / E1.31) +* OpenSoundControl (OSC) * evdev input devices (Linux) * Open Lighting Architecture (OLA) diff --git a/TODO b/TODO index d114640..2a97c30 100644 --- a/TODO +++ b/TODO @@ -3,6 +3,5 @@ Note source in channel value struct Optimize core channel search (store backend offset) Printing backend / Verbose mode -document example configs evdev relative axis size mm_managed_fd.impl is not freed currently diff --git a/backends/Makefile b/backends/Makefile index fe88669..22cb95b 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -6,8 +6,8 @@ BACKEND_LIB = libmmbackend.o SYSTEM := $(shell uname -s) -CFLAGS += -fPIC -I../ -CPPFLAGS += -fPIC -I../ +CFLAGS += -g -fPIC -I../ +CPPFLAGS += -g -fPIC -I../ LDFLAGS += -shared # Build Linux backends if possible diff --git a/backends/osc.c b/backends/osc.c index 36b0993..3f19abf 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -13,6 +13,14 @@ #define osc_align(a) ((((a) / 4) + (((a) % 4) ? 1 : 0)) * 4) #define BACKEND_NAME "osc" +typedef union { + struct { + uint32_t channel; + uint32_t parameter; + } fields; + uint64_t label; +} osc_channel_ident; + static struct { uint8_t detect; } osc_global_config = { @@ -41,6 +49,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: @@ -55,6 +64,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){ @@ -77,6 +87,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: @@ -94,6 +105,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){ @@ -115,6 +127,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: @@ -136,6 +149,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 @@ -179,6 +193,7 @@ static inline channel_value osc_parameter_normalise(osc_parameter_type t, osc_pa } 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 { @@ -212,45 +227,192 @@ 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; +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; } - osc_parameter_value min, max, cur; - channel_value evt; + 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 %lu\n", path, illegal_chars[c], u); + return 1; + } + } - if(!fmt || !data || data_len % 4 || !*fmt){ - fprintf(stderr, "Invalid OSC packet, data length %zu\n", data_len); - return 1; - } + if(!isgraph(path[u])){ + fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %lu\n", path, pattern_chars[c], u); + return 1; + } - //find offset for this parameter - for(p = 0; p < info->param_index; p++){ - off += osc_data_length(fmt[p]); - } + 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 %lu\n", path, pattern_chars[c], u); + return 1; + } + } + } - if(info->type != not_set){ - max = info->max; - min = info->min; + switch(path[u]){ + case '{': + if(square_open || curly_open){ + fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %lu\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 %lu\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; + } + } } - else{ - osc_defaults(fmt[info->param_index], &max, &min); + + if(square_open || curly_open){ + fprintf(stderr, "%s is not a valid OSC path: Unterminated pattern expression\n", path); + return 1; } + return 0; +} - cur = osc_parse(fmt[info->param_index], data + off); - evt = osc_parameter_normalise(fmt[info->param_index], min, max, cur); +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; - return mm_channel_event(c, evt); -} + 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; + } -static int osc_validate_path(char* path){ - if(path[0] != '/'){ - fprintf(stderr, "%s is not a valid OSC path: Missing root /\n", path); - return 1; + 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(pattern[match_end + 1] == ']' && match_any && !match + && path[u + 1] && path[u + 1] != '/'){ + match_end = match_begin - 1; + u++; + } + } + + if(match == inverted){ + return 0; + } + + 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++){ + } + + 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; + } + + 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 0; + return 1; } static int osc_configure(char* option, char* value){ @@ -266,13 +428,81 @@ static int osc_configure(char* option, char* value){ return 1; } +static int osc_register_pattern(osc_instance_data* data, char* pattern_path, char* configuration){ + size_t u, pattern; + char* format = NULL, *token = NULL; + + if(osc_path_validate(pattern_path, 1)){ + fprintf(stderr, "Not a valid OSC pattern: %s\n", pattern_path); + 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; + } + + //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; + + 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)); + + 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; + } + + //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; + } + + data->pattern[pattern].type[u] = format[u]; + + //parse min/max values + token = strtok(NULL, " "); + if(!token){ + fprintf(stderr, "Missing minimum specification for parameter %zu 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 %zu 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 osc_configure_instance(instance* inst, char* option, char* value){ osc_instance_data* data = (osc_instance_data*) inst->impl; - char* host = NULL, *port = NULL, *token = NULL, *format = NULL; - size_t u, p; + char* host = NULL, *port = NULL, *token = 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; } @@ -326,76 +556,7 @@ static int osc_configure_instance(instance* inst, char* option, char* value){ 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 instance %s\n", option, inst->name); @@ -420,31 +581,38 @@ static instance* osc_instance(){ } static channel* osc_map_channel(instance* inst, char* spec){ - size_t u; + size_t u, p; osc_instance_data* data = (osc_instance_data*) inst->impl; - size_t param_index = 0; + 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"); @@ -452,22 +620,100 @@ static channel* osc_map_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 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 %zu\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){ - uint8_t xmit_buf[OSC_XMIT_BUF], *format = NULL; - size_t evt = 0, off, members, p; + size_t evt = 0, mark = 0; + int rv; + osc_channel_ident ident = { + .label = 0 + }; + osc_parameter_value current; + if(!num){ return 0; } @@ -479,95 +725,97 @@ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ } 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 %zu\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 %lu 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; } @@ -577,7 +825,6 @@ static int osc_handle(size_t num, managed_fd* fds){ instance* inst = 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; @@ -600,7 +847,7 @@ static int osc_handle(size_t num, managed_fd* fds){ bytes_read = recv(fds[fd].fd, recv_buf, sizeof(recv_buf), 0); } - if(bytes_read < 0){ + if(bytes_read <= 0){ break; } @@ -622,20 +869,11 @@ static int osc_handle(size_t num, managed_fd* fds){ fprintf(stderr, "Incoming OSC data: Path %s.%s Format %s\n", inst->name, osc_local, osc_fmt); } - osc_data = (uint8_t*) osc_fmt + (osc_align(strlen(osc_fmt) + 2) - 1); //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); @@ -706,14 +944,25 @@ static int osc_shutdown(){ 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); } diff --git a/backends/osc.h b/backends/osc.h index dc6cb3a..4e9dec5 100644 --- a/backends/osc.h +++ b/backends/osc.h @@ -34,22 +34,33 @@ 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; + + //peer fd + int fd; } osc_instance_data; diff --git a/backends/osc.md b/backends/osc.md index e9aa4d5..b7ce527 100644 --- a/backends/osc.md +++ b/backends/osc.md @@ -23,13 +23,21 @@ 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 in the instance configuration using an assignment of the syntax +This is done by specifying *patterns* in the instance configuration using an assignment of the syntax ``` /local/osc/path = ... ``` -The OSC path to be configured must only be the local part (omitting a configured instance root). +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. @@ -44,6 +52,12 @@ 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 +``` + #### Channel specification A channel may be any valid OSC path, to which the instance root will be prepended if @@ -80,4 +94,7 @@ The default ranges are: #### 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 `*`. + Ping requests are not yet answered. There may be some problems using broadcast output and input. diff --git a/configs/flying-faders.cfg b/configs/flying-faders.cfg new file mode 100644 index 0000000..4197581 --- /dev/null +++ b/configs/flying-faders.cfg @@ -0,0 +1,24 @@ +; Create a 'flying faders' effect using lua and output +; it onto TouchOSC (Layout 'Mix16', Multifader view on page 4) + +[osc touch] +bind = * 8000 +dest = learn@9000 + +; Pre-declare the fader values so the range mapping is correct +/*/fader* = f 0.0 1.0 +/*/toggle* = f 0.0 1.0 +/*/push* = f 0.0 1.0 +/*/multifader*/* = f 0.0 1.0 +/1/xy = ff 0.0 1.0 0.0 1.0 + +[lua generator] +script = configs/flying-faders.lua + +[map] + +generator.wave{1..24} > touch./4/multifader1/{1..24} +;generator.wave{1..24} > touch./4/multifader2/{1..24} + +touch./4/multifader2/1 > generator.magnitude + diff --git a/configs/flying-faders.lua b/configs/flying-faders.lua new file mode 100644 index 0000000..0b0faef --- /dev/null +++ b/configs/flying-faders.lua @@ -0,0 +1,10 @@ +step = 0 + +function wave() + for chan=1,24 do + output("wave" .. chan, (math.sin(math.rad((step + chan * 360 / 24) % 360)) + 1) / 2) + end + step = (step + 5) % 360 +end + +interval(wave, 100) diff --git a/configs/osc-xy.cfg b/configs/osc-xy.cfg new file mode 100644 index 0000000..fc5c5f3 --- /dev/null +++ b/configs/osc-xy.cfg @@ -0,0 +1,26 @@ +; Test for bi-directional OSC with an XY pad (TouchOSC Layout 'Mix16', Page 1) + +[backend osc] +detect = on + +[osc touch] +bind = 0.0.0.0 8000 +dest = learn@9000 + +; Pre-declare the fader values so the range mapping is correct +/*/xy = ff 0.0 1.0 0.0 1.0 + +[evdev xbox] +device = /dev/input/event16 + +[midi launch] + +[map] +xbox.EV_ABS.ABS_X > touch./1/xy:1 +xbox.EV_ABS.ABS_Y > touch./1/xy:0 + +xbox.EV_ABS.ABS_X > launch.ch0.note2 +;xbox.EV_ABS.ABS_Y > launch.ch0.note3 + +launch.ch0.note0 <> touch./1/xy:0 +launch.ch0.note1 <> touch./1/xy:1 -- cgit v1.2.3 From 20a6882a063404858588596bd3f12bdd9e53460a Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 3 Aug 2019 18:42:39 +0200 Subject: Windows build compatiblity --- .gitignore | 3 +++ Makefile | 23 +++++++++++++++++-- backend.c | 31 ++++++++++++++----------- backend.h | 9 +++++++- backends/Makefile | 26 +++++++++++++++++++-- backends/artnet.c | 16 ++++++++----- backends/artnet.h | 2 ++ backends/libmmbackend.c | 12 ++++++++-- backends/libmmbackend.h | 6 +++++ backends/lua.c | 4 ---- backends/osc.c | 18 +++++++++------ backends/osc.h | 2 ++ backends/sacn.c | 28 ++++++++++++++--------- backends/sacn.h | 1 - config.c | 56 +++++++++++++++++++++++++++++++++++++++++++++ midimonster.c | 39 ++++++++++++++++++++++++++------ midimonster.h | 38 ++++++++++++++++++++++++------- monster.cfg | 12 +++------- plugin.c | 60 +++++++++++++++++++++++++++++++++++++++++++++---- portability.h | 17 ++++++++++++++ 20 files changed, 326 insertions(+), 77 deletions(-) (limited to 'backends/Makefile') diff --git a/.gitignore b/.gitignore index 3afd872..4396a38 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ midimonster +midimonster.exe +libmmapi.a *.swp *.o *.so +*.dll diff --git a/Makefile b/Makefile index 5ee9cd9..b82d6d8 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,14 @@ -.PHONY: all clean run sanitize backends full backends-full +.PHONY: all clean run sanitize backends windows full backends-full OBJS = config.o backend.o plugin.o PLUGINDIR = "\"./backends/\"" +PLUGINDIR_W32 = "\"backends\\\\\"" SYSTEM := $(shell uname -s) -CFLAGS ?= -g -Wall +CFLAGS ?= -g -Wall -Wpedantic +# Hide all non-API symbols for export +CFLAGS += -fvisibility=hidden + #CFLAGS += -DDEBUG midimonster: LDLIBS = -ldl midimonster: CFLAGS += -DPLUGINS=$(PLUGINDIR) @@ -21,6 +25,8 @@ all: midimonster backends full: midimonster backends-full +windows: midimonster.exe + backends: $(MAKE) -C backends @@ -31,8 +37,21 @@ backends-full: midimonster: midimonster.c portability.h $(OBJS) $(CC) $(CFLAGS) $(LDFLAGS) $< $(OBJS) $(LDLIBS) -o $@ +midimonster.exe: export CC = x86_64-w64-mingw32-gcc +#midimonster.exe: CFLAGS += -Wno-format +midimonster.exe: CFLAGS += -DPLUGINS=$(PLUGINDIR_W32) -Wno-format +midimonster.exe: LDLIBS = -lws2_32 +midimonster.exe: LDFLAGS += -Wl,--out-implib,libmmapi.a +midimonster.exe: midimonster.c portability.h $(OBJS) + $(CC) $(CFLAGS) $(LDFLAGS) $< $(OBJS) $(LDLIBS) -o $@ + # The windows build for backends requires the import library generated with the build, + # so the backends can't be a prerequisite for the executable... + $(MAKE) -C backends windows + clean: $(RM) midimonster + $(RM) midimonster.exe + $(RM) libmmapi.a $(RM) $(OBJS) $(MAKE) -C backends clean diff --git a/backend.c b/backend.c index 5df5d73..4fa7704 100644 --- a/backend.c +++ b/backend.c @@ -1,4 +1,9 @@ #include +#ifndef _WIN32 +#define MM_API __attribute__((visibility ("default"))) +#else +#define MM_API __attribute__((dllexport)) +#endif #include "midimonster.h" #include "backend.h" @@ -26,7 +31,7 @@ int backends_handle(size_t nfds, managed_fd* fds){ } } - DBGPF("Notifying backend %s of %zu waiting FDs\n", backends[u].name, n); + DBGPF("Notifying backend %s of %lu waiting FDs\n", backends[u].name, n); rv |= backends[u].process(n, fds); if(rv){ fprintf(stderr, "Backend %s failed to handle input\n", backends[u].name); @@ -59,28 +64,28 @@ int backends_notify(size_t nev, channel** c, channel_value* v){ } } - DBGPF("Calling handler for instance %s with %zu events\n", instances[u]->name, n); + DBGPF("Calling handler for instance %s with %lu events\n", instances[u]->name, n); rv |= instances[u]->backend->handle(instances[u], n, c, v); } return 0; } -channel* mm_channel(instance* i, uint64_t ident, uint8_t create){ +channel* MM_API mm_channel(instance* inst, uint64_t ident, uint8_t create){ size_t u; for(u = 0; u < nchannels; u++){ - if(channels[u]->instance == i && channels[u]->ident == ident){ - DBGPF("Requested channel %zu on instance %s already exists, reusing\n", ident, i->name); + if(channels[u]->instance == inst && channels[u]->ident == ident){ + DBGPF("Requested channel %lu on instance %s already exists, reusing\n", ident, inst->name); return channels[u]; } } if(!create){ - DBGPF("Requested unknown channel %zu on instance %s\n", ident, i->name); + DBGPF("Requested unknown channel %lu on instance %s\n", ident, inst->name); return NULL; } - DBGPF("Creating previously unknown channel %zu on instance %s\n", ident, i->name); + DBGPF("Creating previously unknown channel %lu on instance %s\n", ident, inst->name); channel** new_chan = realloc(channels, (nchannels + 1) * sizeof(channel*)); if(!new_chan){ fprintf(stderr, "Failed to allocate memory\n"); @@ -95,12 +100,12 @@ channel* mm_channel(instance* i, uint64_t ident, uint8_t create){ return NULL; } - channels[nchannels]->instance = i; + channels[nchannels]->instance = inst; channels[nchannels]->ident = ident; return channels[nchannels++]; } -instance* mm_instance(){ +instance* MM_API mm_instance(){ instance** new_inst = realloc(instances, (ninstances + 1) * sizeof(instance*)); if(!new_inst){ //TODO free @@ -118,7 +123,7 @@ instance* mm_instance(){ return instances[ninstances++]; } -instance* mm_instance_find(char* name, uint64_t ident){ +instance* MM_API mm_instance_find(char* name, uint64_t ident){ size_t u; backend* b = backend_match(name); if(!b){ @@ -134,7 +139,7 @@ instance* mm_instance_find(char* name, uint64_t ident){ return NULL; } -int mm_backend_instances(char* name, size_t* ninst, instance*** inst){ +int MM_API mm_backend_instances(char* name, size_t* ninst, instance*** inst){ backend* b = backend_match(name); size_t n = 0, u; //count number of affected instances @@ -177,7 +182,7 @@ void instances_free(){ void channels_free(){ size_t u; for(u = 0; u < nchannels; u++){ - DBGPF("Destroying channel %zu on instance %s\n", channels[u]->ident, channels[u]->instance->name); + DBGPF("Destroying channel %lu on instance %s\n", channels[u]->ident, channels[u]->instance->name); if(channels[u]->impl){ channels[u]->instance->backend->channel_free(channels[u]); } @@ -232,7 +237,7 @@ struct timeval backend_timeout(){ return tv; } -int mm_backend_register(backend b){ +int MM_API mm_backend_register(backend b){ if(!backend_match(b.name)){ backends = realloc(backends, (nbackends + 1) * sizeof(backend)); if(!backends){ diff --git a/backend.h b/backend.h index daf96bc..7529154 100644 --- a/backend.h +++ b/backend.h @@ -1,8 +1,8 @@ #include +/* Internal API */ int backends_handle(size_t nfds, managed_fd* fds); int backends_notify(size_t nev, channel** c, channel_value* v); - backend* backend_match(char* name); instance* instance_match(char* name); struct timeval backend_timeout(); @@ -10,3 +10,10 @@ int backends_start(); int backends_stop(); void instances_free(); void channels_free(); + +/* Backend API */ +channel* MM_API mm_channel(instance* inst, uint64_t ident, uint8_t create); +instance* MM_API mm_instance(); +instance* MM_API mm_instance_find(char* name, uint64_t ident); +int MM_API mm_backend_instances(char* name, size_t* ninst, instance*** inst); +int MM_API mm_backend_register(backend b); diff --git a/backends/Makefile b/backends/Makefile index 22cb95b..2374df0 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,12 +1,13 @@ .PHONY: all clean full OPTIONAL_BACKENDS = ola.so +WINDOWS_BACKENDS = loopback.dll artnet.dll osc.dll sacn.dll LINUX_BACKENDS = midi.so evdev.so BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so BACKEND_LIB = libmmbackend.o SYSTEM := $(shell uname -s) -CFLAGS += -g -fPIC -I../ +CFLAGS += -g -fPIC -I../ -Wall -Wpedantic CPPFLAGS += -g -fPIC -I../ LDFLAGS += -shared @@ -20,8 +21,17 @@ 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 + midi.so: LDLIBS = -lasound evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) evdev.so: LDLIBS = $(shell pkg-config --libs libevdev) @@ -33,12 +43,24 @@ lua.so: LDLIBS += $(shell pkg-config --libs lua5.3) %.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 + +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) + full: $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) clean: - $(RM) $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) + $(RM) $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) $(WINDOWS_BACKENDS) diff --git a/backends/artnet.c b/backends/artnet.c index 8b404a6..a6df4ab 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -3,8 +3,8 @@ #include #include "libmmbackend.h" - #include "artnet.h" + #define MAX_FDS 255 #define BACKEND_NAME "artnet" @@ -32,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 %lu 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; @@ -212,7 +212,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)); } @@ -230,7 +230,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 (%lu channel events)\n", inst->name, num); return 0; } @@ -295,7 +295,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 %lu on %s not known to core\n", p, inst->name); return 1; } @@ -367,7 +367,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)); } @@ -438,7 +442,7 @@ static int artnet_start(){ } } - fprintf(stderr, "ArtNet backend registering %zu descriptors to core\n", artnet_fds); + fprintf(stderr, "ArtNet backend registering %lu 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..f5aa745 100644 --- a/backends/artnet.h +++ b/backends/artnet.h @@ -1,4 +1,6 @@ +#ifndef _WIN32 #include +#endif #include "midimonster.h" int init(); diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index 6320611..b27ebc5 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -53,7 +53,7 @@ int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* ad } int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ - int fd = -1, status, yes = 1, flags; + int fd = -1, status, yes = 1; struct addrinfo hints = { .ai_family = AF_UNSPEC, .ai_socktype = socktype, @@ -106,12 +106,20 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ } //set nonblocking - flags = fcntl(fd, F_GETFL, 0); + #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; } diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index 38bfca0..77cad6a 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -1,14 +1,20 @@ #include #include #include +#ifdef _WIN32 +#include +//#define close closesocket +#else #include #include +#endif #include #include #include #include #include #include +#include "../portability.h" /* Parse spec as host specification in the form * host port diff --git a/backends/lua.c b/backends/lua.c index 4a910a2..ec02575 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -161,8 +161,6 @@ static int lua_callback_output(lua_State* interpreter){ static int lua_callback_interval(lua_State* interpreter){ size_t n = 0; - instance* inst = NULL; - lua_instance_data* data = NULL; uint64_t interval = 0; int reference = LUA_NOREF; @@ -174,8 +172,6 @@ static int lua_callback_interval(lua_State* interpreter){ //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 and round the interval interval = luaL_checkinteger(interpreter, 2); diff --git a/backends/osc.c b/backends/osc.c index 18c8bad..03e431f 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -1,8 +1,8 @@ #include #include #include -#include "libmmbackend.h" +#include "libmmbackend.h" #include "osc.h" /* @@ -480,14 +480,14 @@ static int osc_register_pattern(osc_instance_data* data, char* pattern_path, cha //parse min/max values token = strtok(NULL, " "); if(!token){ - fprintf(stderr, "Missing minimum specification for parameter %zu of OSC pattern %s\n", u, pattern_path); + fprintf(stderr, "Missing minimum specification for parameter %lu 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 %zu of OSC pattern %s\n", u, pattern_path); + fprintf(stderr, "Missing maximum specification for parameter %lu of OSC pattern %s\n", u, pattern_path); return 1; } data->pattern[pattern].max[u] = osc_parse_value_spec(format[u], token); @@ -689,7 +689,7 @@ static int osc_output_channel(instance* inst, size_t channel){ //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 %zu\n", inst->name, data->channel[channel].path, p); + fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s at parameter %lu\n", inst->name, data->channel[channel].path, p); return 1; } @@ -720,7 +720,7 @@ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ 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 (%lu channels)\n", inst->name, num); return 0; } @@ -778,7 +778,7 @@ static int osc_process_packet(instance* inst, char* local_path, char* format, ui channel* chan = NULL; if(payload_len % 4){ - fprintf(stderr, "Invalid OSC packet, data length %zu\n", payload_len); + fprintf(stderr, "Invalid OSC packet, data length %lu\n", payload_len); return 0; } @@ -877,7 +877,11 @@ static int osc_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, "OSC failed to receive data for instance %s: %s\n", inst->name, strerror(errno)); } @@ -924,7 +928,7 @@ static int osc_start(){ } } - fprintf(stderr, "OSC backend registered %zu descriptors to core\n", fds); + fprintf(stderr, "OSC backend registered %lu descriptors to core\n", fds); free(inst); return 0; diff --git a/backends/osc.h b/backends/osc.h index 4e9dec5..b2aaea7 100644 --- a/backends/osc.h +++ b/backends/osc.h @@ -1,6 +1,8 @@ #include "midimonster.h" #include +#ifndef _WIN32 #include +#endif #define OSC_RECV_BUF 8192 #define OSC_XMIT_BUF 8192 diff --git a/backends/sacn.c b/backends/sacn.c index 75bb76f..6f7d1a5 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -1,16 +1,18 @@ #include #include -#include -#include #include #include #include #include +#ifndef _WIN32 +#include #include +#include +#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" @@ -71,7 +73,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 %lu 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; @@ -271,7 +273,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)); } @@ -293,7 +295,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 (%lu channel events)\n", inst->name, num); return 0; } @@ -378,7 +380,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 %lu on %s not known to core", u, inst->name); return 1; } @@ -445,8 +447,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 %lu: %s\n", fd, strerror(errno)); } } } @@ -512,7 +514,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)); } @@ -577,7 +583,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)); } } @@ -604,7 +610,7 @@ static int sacn_start(){ } } - fprintf(stderr, "sACN backend registering %zu descriptors to core\n", global_cfg.fds); + fprintf(stderr, "sACN backend registering %lu 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..7af2a36 100644 --- a/backends/sacn.h +++ b/backends/sacn.h @@ -1,4 +1,3 @@ -#include #include "midimonster.h" int init(); diff --git a/config.c b/config.c index 6d5fd16..93fb56d 100644 --- a/config.c +++ b/config.c @@ -20,6 +20,62 @@ typedef enum { static backend* current_backend = NULL; static instance* current_instance = NULL; +#ifdef _WIN32 +#define GETLINE_BUFFER 4096 + +static ssize_t getline(char** line, size_t* alloc, FILE* stream){ + size_t bytes_read = 0; + char c; + //sanity checks + if(!line || !alloc || !stream){ + return -1; + } + + //allocate buffer if none provided + if(!*line || !*alloc){ + *alloc = GETLINE_BUFFER; + *line = calloc(GETLINE_BUFFER, sizeof(char)); + if(!*line){ + fprintf(stderr, "Failed to allocate memory\n"); + return -1; + } + } + + if(feof(stream)){ + return -1; + } + + for(c = fgetc(stream); 1; c = fgetc(stream)){ + //end of buffer, resize + if(bytes_read == (*alloc) - 1){ + *alloc += GETLINE_BUFFER; + *line = realloc(*line, (*alloc) * sizeof(char)); + if(!*line){ + fprintf(stderr, "Failed to allocate memory\n"); + return -1; + } + } + + //store character + (*line)[bytes_read] = c; + + //end of line + if(feof(stream) || c == '\n'){ + //terminate string + (*line)[bytes_read + 1] = 0; + return bytes_read; + } + + //input broken + if(ferror(stream) || c < 0){ + return -1; + } + + bytes_read++; + } +} +#endif + static char* config_trim_line(char* in){ ssize_t n; //trim front diff --git a/midimonster.c b/midimonster.c index fb664a4..df27ca3 100644 --- a/midimonster.c +++ b/midimonster.c @@ -1,9 +1,14 @@ #include #include -#include #include #include #include +#ifndef _WIN32 +#include +#define MM_API __attribute__((visibility("default"))) +#else +#define MM_API __attribute__((dllexport)) +#endif #include "midimonster.h" #include "config.h" #include "backend.h" @@ -35,11 +40,14 @@ static void signal_handler(int signum){ shutdown_requested = 1; } -uint64_t mm_timestamp(){ +uint64_t MM_API mm_timestamp(){ return global_timestamp; } static void update_timestamp(){ + #ifdef _WIN32 + global_timestamp = GetTickCount(); + #else struct timespec current; if(clock_gettime(CLOCK_MONOTONIC_COARSE, ¤t)){ fprintf(stderr, "Failed to update global timestamp, time-based processing for some backends may be impaired: %s\n", strerror(errno)); @@ -47,6 +55,7 @@ static void update_timestamp(){ } global_timestamp = current.tv_sec * 1000 + current.tv_nsec / 1000000; + #endif } int mm_map_channel(channel* from, channel* to){ @@ -99,7 +108,7 @@ void map_free(){ map = NULL; } -int mm_manage_fd(int new_fd, char* back, int manage, void* impl){ +int MM_API mm_manage_fd(int new_fd, char* back, int manage, void* impl){ backend* b = backend_match(back); size_t u; @@ -163,7 +172,7 @@ void fds_free(){ fd = NULL; } -int mm_channel_event(channel* c, channel_value v){ +int MM_API mm_channel_event(channel* c, channel_value v){ size_t u, p; //find mapped channels @@ -229,7 +238,7 @@ static fd_set fds_collect(int* max_fd){ *max_fd = -1; } - DBGPF("Building selector set from %zu FDs registered to core\n", fds); + DBGPF("Building selector set from %lu FDs registered to core\n", fds); FD_ZERO(&rv_fds); for(u = 0; u < fds; u++){ if(fd[u].fd >= 0){ @@ -243,6 +252,17 @@ static fd_set fds_collect(int* max_fd){ return rv_fds; } +int platform_initialize(){ +#ifdef _WIN32 + WSADATA wsa; + WORD version = MAKEWORD(2, 2); + if(WSAStartup(version, &wsa)){ + return 1; + } +#endif + return 0; +} + int main(int argc, char** argv){ fd_set all_fds, read_fds; event_collection* secondary = NULL; @@ -255,6 +275,11 @@ int main(int argc, char** argv){ cfg_file = argv[1]; } + if(platform_initialize()){ + fprintf(stderr, "Failed to perform platform-specific initialization\n"); + return EXIT_FAILURE; + } + FD_ZERO(&all_fds); //initialize backends if(plugins_load(PLUGINS)){ @@ -316,14 +341,14 @@ int main(int argc, char** argv){ update_timestamp(); //run backend processing, collect events - DBGPF("%zu backend FDs signaled\n", n); + DBGPF("%lu backend FDs signaled\n", n); if(backends_handle(n, signaled_fds)){ goto bail; } while(primary->n){ //swap primary and secondary event collectors - DBGPF("Swapping event collectors, %zu events in primary\n", primary->n); + DBGPF("Swapping event collectors, %lu events in primary\n", primary->n); for(u = 0; u < sizeof(event_pool) / sizeof(event_collection); u++){ if(primary != event_pool + u){ secondary = primary; diff --git a/midimonster.h b/midimonster.h index 7f70f5b..eb118c6 100644 --- a/midimonster.h +++ b/midimonster.h @@ -4,6 +4,21 @@ #include #include +#ifndef MM_API + #ifdef _WIN32 + #define MM_API __attribute__((dllimport)) + #else + #define MM_API + #endif +#endif + +/* GCC ignores the visibility attributes on some API functions, so override visibility */ +#if !defined(_WIN32) && defined(__GNUC__) && !defined(__clang__) + #undef MM_API + #define MM_API + #pragma GCC visibility push(default) +#endif + /* Straight-forward min / max macros */ #define max(a,b) (((a) > (b)) ? (a) : (b)) #define min(a,b) (((a) < (b)) ? (a) : (b)) @@ -187,7 +202,7 @@ typedef struct /*_mm_channel_mapping*/ { /* * Register a new backend. */ -int mm_backend_register(backend b); +int MM_API mm_backend_register(backend b); /* * Provides a pointer to a newly (zero-)allocated instance. @@ -201,7 +216,8 @@ int mm_backend_register(backend b); * mmbackend_shutdown procedure of the backend, eg. by querying * all instances for the backend. */ -instance* mm_instance(); +instance* MM_API mm_instance(); + /* * Finds an instance matching the specified backend and identifier. * Since setting an identifier for an instance is optional, @@ -209,7 +225,8 @@ instance* mm_instance(); * Instance identifiers may for example be set in the backends * mmbackend_start call. */ -instance* mm_instance_find(char* backend, uint64_t ident); +instance* MM_API mm_instance_find(char* backend, uint64_t ident); + /* * Provides a pointer to a channel structure, pre-filled with * the provided instance reference and identifier. @@ -224,30 +241,35 @@ instance* mm_instance_find(char* backend, uint64_t ident); * this function, the backend will receive a call to its channel_free * function. */ -channel* mm_channel(instance* i, uint64_t ident, uint8_t create); +channel* MM_API mm_channel(instance* i, uint64_t ident, uint8_t create); //TODO channel* mm_channel_find() + /* * Register (manage = 1) or unregister (manage = 0) a file descriptor * to be selected on. The backend will be notified when the descriptor * becomes ready to read via its registered mmbackend_process_fd call. */ -int mm_manage_fd(int fd, char* backend, int manage, void* impl); +int MM_API mm_manage_fd(int fd, char* backend, int manage, void* impl); + /* * Notifies the core of a channel event. Called by backends to * inject events gathered from their backing implementation. */ -int mm_channel_event(channel* c, channel_value v); +int MM_API mm_channel_event(channel* c, channel_value v); + /* * Query all active instances for a given backend. * *i will need to be freed by the caller. */ -int mm_backend_instances(char* backend, size_t* n, instance*** i); +int MM_API mm_backend_instances(char* backend, size_t* n, instance*** i); + /* * Query an internal timestamp, which is updated every core iteration. * This timestamp should not be used as a performance counter, but can be * used for timeouting. Resolution is milliseconds. */ -uint64_t mm_timestamp(); +uint64_t MM_API mm_timestamp(); + /* * Create a channel-to-channel mapping. This API should not * be used by backends. It is only exported for core modules. diff --git a/monster.cfg b/monster.cfg index 2e6f76f..2413f6d 100644 --- a/monster.cfg +++ b/monster.cfg @@ -1,18 +1,12 @@ [backend artnet] bind = 0.0.0.0 -[backend evdev] +[loopback loop] [artnet art] universe = 0 dest = 255.255.255.255 -[evdev mouse] -input = TPPS - -[loopback loop] - [map] -mouse.EV_REL.REL_X > loop.chan0 -art.{3..4}{4..3} > loop.chan{4..3}{3..4} -art.{1..10} > loop.data{1..10} + +art.1+2 > loop.b diff --git a/plugin.c b/plugin.c index fc642ac..a452559 100644 --- a/plugin.c +++ b/plugin.c @@ -1,11 +1,20 @@ #include #include -#include #include #include #include #include #include +#include "portability.h" +#ifdef _WIN32 +#define dlclose FreeLibrary +#define dlsym GetProcAddress +#define dlerror() "Failed" +#define dlopen(lib,ig) LoadLibrary(lib) +#else +#include +#endif + #include "plugin.h" static size_t plugins = 0; @@ -14,19 +23,29 @@ static void** plugin_handle = NULL; static int plugin_attach(char* path, char* file){ plugin_init init = NULL; void* handle = NULL; + char* lib = NULL; + char* error = NULL; - char* lib = calloc(strlen(path) + strlen(file) + 1, sizeof(char)); + lib = calloc(strlen(path) + strlen(file) + 1, sizeof(char)); if(!lib){ fprintf(stderr, "Failed to allocate memory\n"); return 1; } - snprintf(lib, strlen(path) + strlen(file) + 1, "%s%s", path, file); handle = dlopen(lib, RTLD_NOW); if(!handle){ - fprintf(stderr, "Failed to load plugin %s: %s\n", lib, dlerror()); + #ifdef _WIN32 + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); + #else + error = dlerror(); + #endif + fprintf(stderr, "Failed to load plugin %s: %s\n", lib, error); free(lib); + #ifdef _WIN32 + LocalFree(error); + #endif return 0; } @@ -62,6 +81,38 @@ static int plugin_attach(char* path, char* file){ int plugins_load(char* path){ int rv = -1; +#ifdef _WIN32 + char* search_expression = calloc(strlen(path) + strlen("*.dll") + 1, sizeof(char)); + if(!search_expression){ + fprintf(stderr, "Failed to allocate memory\n"); + return -1; + } + snprintf(search_expression, strlen(path) + strlen("*.dll"), "%s*.dll", path); + + WIN32_FIND_DATA result; + HANDLE hSearch = FindFirstFile(search_expression, &result); + + if(hSearch == INVALID_HANDLE_VALUE){ + LPVOID lpMsgBuf = NULL; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &lpMsgBuf, 0, NULL); + fprintf(stderr, "Failed to search for backend plugin files in %s: %s\n", path, lpMsgBuf); + LocalFree(lpMsgBuf); + return -1; + } + + do { + if(plugin_attach(path, result.cFileName)){ + goto load_done; + } + } while(FindNextFile(hSearch, &result)); + + rv = 0; +load_done: + free(search_expression); + FindClose(hSearch); + return rv; +#else struct dirent* entry; struct stat file_stat; DIR* directory = opendir(path); @@ -100,6 +151,7 @@ load_done: return -1; } return rv; +#endif } int plugins_close(){ diff --git a/portability.h b/portability.h index 25aee01..903ecd8 100644 --- a/portability.h +++ b/portability.h @@ -19,3 +19,20 @@ #define be64toh(x) OSSwapBigToHostInt64(x) #define le64toh(x) OSSwapLittleToHostInt64(x) #endif + +#ifdef _WIN32 + #define WIN32_LEAN_AND_MEAN + #include + #include + + #define htobe16(x) htons(x) + #define be16toh(x) ntohs(x) + + #define htobe32(x) htonl(x) + #define be32toh(x) ntohl(x) + + #define htobe64(x) _byteswap_uint64(x) + #define htole64(x) (x) + #define be64toh(x) _byteswap_uint64(x) + #define le64toh(x) (x) +#endif -- cgit v1.2.3 From 47a5f9a21bd661f9161d6175ebd074962daee255 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 7 Aug 2019 17:25:24 +0200 Subject: Fix export visibilities for GCC --- Makefile | 5 +---- backend.c | 10 +++++----- backend.h | 10 +++++----- backends/Makefile | 10 +++++++--- backends/midi.c | 6 ------ midimonster.c | 16 ++++++++-------- midimonster.h | 23 ++++++++--------------- 7 files changed, 34 insertions(+), 46 deletions(-) (limited to 'backends/Makefile') diff --git a/Makefile b/Makefile index b82d6d8..57fe089 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,7 @@ all: midimonster backends full: midimonster backends-full windows: midimonster.exe + $(MAKE) -C backends windows backends: $(MAKE) -C backends @@ -38,15 +39,11 @@ midimonster: midimonster.c portability.h $(OBJS) $(CC) $(CFLAGS) $(LDFLAGS) $< $(OBJS) $(LDLIBS) -o $@ midimonster.exe: export CC = x86_64-w64-mingw32-gcc -#midimonster.exe: CFLAGS += -Wno-format midimonster.exe: CFLAGS += -DPLUGINS=$(PLUGINDIR_W32) -Wno-format midimonster.exe: LDLIBS = -lws2_32 midimonster.exe: LDFLAGS += -Wl,--out-implib,libmmapi.a midimonster.exe: midimonster.c portability.h $(OBJS) $(CC) $(CFLAGS) $(LDFLAGS) $< $(OBJS) $(LDLIBS) -o $@ - # The windows build for backends requires the import library generated with the build, - # so the backends can't be a prerequisite for the executable... - $(MAKE) -C backends windows clean: $(RM) midimonster diff --git a/backend.c b/backend.c index 4fa7704..3a18f41 100644 --- a/backend.c +++ b/backend.c @@ -71,7 +71,7 @@ int backends_notify(size_t nev, channel** c, channel_value* v){ return 0; } -channel* MM_API mm_channel(instance* inst, uint64_t ident, uint8_t create){ +MM_API channel* mm_channel(instance* inst, uint64_t ident, uint8_t create){ size_t u; for(u = 0; u < nchannels; u++){ if(channels[u]->instance == inst && channels[u]->ident == ident){ @@ -105,7 +105,7 @@ channel* MM_API mm_channel(instance* inst, uint64_t ident, uint8_t create){ return channels[nchannels++]; } -instance* MM_API mm_instance(){ +MM_API instance* mm_instance(){ instance** new_inst = realloc(instances, (ninstances + 1) * sizeof(instance*)); if(!new_inst){ //TODO free @@ -123,7 +123,7 @@ instance* MM_API mm_instance(){ return instances[ninstances++]; } -instance* MM_API mm_instance_find(char* name, uint64_t ident){ +MM_API instance* mm_instance_find(char* name, uint64_t ident){ size_t u; backend* b = backend_match(name); if(!b){ @@ -139,7 +139,7 @@ instance* MM_API mm_instance_find(char* name, uint64_t ident){ return NULL; } -int MM_API mm_backend_instances(char* name, size_t* ninst, instance*** inst){ +MM_API int mm_backend_instances(char* name, size_t* ninst, instance*** inst){ backend* b = backend_match(name); size_t n = 0, u; //count number of affected instances @@ -237,7 +237,7 @@ struct timeval backend_timeout(){ return tv; } -int MM_API mm_backend_register(backend b){ +MM_API int mm_backend_register(backend b){ if(!backend_match(b.name)){ backends = realloc(backends, (nbackends + 1) * sizeof(backend)); if(!backends){ diff --git a/backend.h b/backend.h index 7529154..6573e17 100644 --- a/backend.h +++ b/backend.h @@ -12,8 +12,8 @@ void instances_free(); void channels_free(); /* Backend API */ -channel* MM_API mm_channel(instance* inst, uint64_t ident, uint8_t create); -instance* MM_API mm_instance(); -instance* MM_API mm_instance_find(char* name, uint64_t ident); -int MM_API mm_backend_instances(char* name, size_t* ninst, instance*** inst); -int MM_API mm_backend_register(backend b); +MM_API channel* mm_channel(instance* inst, uint64_t ident, uint8_t create); +MM_API instance* mm_instance(); +MM_API instance* mm_instance_find(char* name, uint64_t ident); +MM_API int mm_backend_instances(char* name, size_t* ninst, instance*** inst); +MM_API int mm_backend_register(backend b); diff --git a/backends/Makefile b/backends/Makefile index 2374df0..3308ef0 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,8 +1,8 @@ .PHONY: all clean full -OPTIONAL_BACKENDS = ola.so -WINDOWS_BACKENDS = loopback.dll artnet.dll osc.dll sacn.dll LINUX_BACKENDS = midi.so evdev.so -BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so +WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so +OPTIONAL_BACKENDS = ola.so BACKEND_LIB = libmmbackend.o SYSTEM := $(shell uname -s) @@ -32,6 +32,10 @@ sacn.so: ADDITIONAL_OBJS += $(BACKEND_LIB) sacn.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) sacn.dll: LDLIBS += -lws2_32 +maweb.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +maweb.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +maweb.dll: LDLIBS += -lws2_32 + midi.so: LDLIBS = -lasound evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) evdev.so: LDLIBS = $(shell pkg-config --libs libevdev) diff --git a/backends/midi.c b/backends/midi.c index c1480c0..9c6ba80 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -14,12 +14,6 @@ typedef union { 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, diff --git a/midimonster.c b/midimonster.c index df27ca3..1e47698 100644 --- a/midimonster.c +++ b/midimonster.c @@ -40,7 +40,7 @@ static void signal_handler(int signum){ shutdown_requested = 1; } -uint64_t MM_API mm_timestamp(){ +MM_API uint64_t mm_timestamp(){ return global_timestamp; } @@ -98,7 +98,7 @@ int mm_map_channel(channel* from, channel* to){ return 0; } -void map_free(){ +static void map_free(){ size_t u; for(u = 0; u < mappings; u++){ free(map[u].to); @@ -108,7 +108,7 @@ void map_free(){ map = NULL; } -int MM_API mm_manage_fd(int new_fd, char* back, int manage, void* impl){ +MM_API int mm_manage_fd(int new_fd, char* back, int manage, void* impl){ backend* b = backend_match(back); size_t u; @@ -158,7 +158,7 @@ int MM_API mm_manage_fd(int new_fd, char* back, int manage, void* impl){ return 0; } -void fds_free(){ +static void fds_free(){ size_t u; for(u = 0; u < fds; u++){ //TODO free impl @@ -172,7 +172,7 @@ void fds_free(){ fd = NULL; } -int MM_API mm_channel_event(channel* c, channel_value v){ +MM_API int mm_channel_event(channel* c, channel_value v){ size_t u, p; //find mapped channels @@ -213,7 +213,7 @@ int MM_API mm_channel_event(channel* c, channel_value v){ return 0; } -void event_free(){ +static void event_free(){ size_t u; for(u = 0; u < sizeof(event_pool) / sizeof(event_collection); u++){ @@ -223,7 +223,7 @@ void event_free(){ } } -int usage(char* fn){ +static int usage(char* fn){ fprintf(stderr, "MIDIMonster v0.1\n"); fprintf(stderr, "Usage:\n"); fprintf(stderr, "\t%s \n", fn); @@ -252,7 +252,7 @@ static fd_set fds_collect(int* max_fd){ return rv_fds; } -int platform_initialize(){ +static int platform_initialize(){ #ifdef _WIN32 WSADATA wsa; WORD version = MAKEWORD(2, 2); diff --git a/midimonster.h b/midimonster.h index eb118c6..270a61f 100644 --- a/midimonster.h +++ b/midimonster.h @@ -12,13 +12,6 @@ #endif #endif -/* GCC ignores the visibility attributes on some API functions, so override visibility */ -#if !defined(_WIN32) && defined(__GNUC__) && !defined(__clang__) - #undef MM_API - #define MM_API - #pragma GCC visibility push(default) -#endif - /* Straight-forward min / max macros */ #define max(a,b) (((a) > (b)) ? (a) : (b)) #define min(a,b) (((a) < (b)) ? (a) : (b)) @@ -202,7 +195,7 @@ typedef struct /*_mm_channel_mapping*/ { /* * Register a new backend. */ -int MM_API mm_backend_register(backend b); +MM_API int mm_backend_register(backend b); /* * Provides a pointer to a newly (zero-)allocated instance. @@ -216,7 +209,7 @@ int MM_API mm_backend_register(backend b); * mmbackend_shutdown procedure of the backend, eg. by querying * all instances for the backend. */ -instance* MM_API mm_instance(); +MM_API instance* mm_instance(); /* * Finds an instance matching the specified backend and identifier. @@ -225,7 +218,7 @@ instance* MM_API mm_instance(); * Instance identifiers may for example be set in the backends * mmbackend_start call. */ -instance* MM_API mm_instance_find(char* backend, uint64_t ident); +MM_API instance* mm_instance_find(char* backend, uint64_t ident); /* * Provides a pointer to a channel structure, pre-filled with @@ -241,7 +234,7 @@ instance* MM_API mm_instance_find(char* backend, uint64_t ident); * this function, the backend will receive a call to its channel_free * function. */ -channel* MM_API mm_channel(instance* i, uint64_t ident, uint8_t create); +MM_API channel* mm_channel(instance* i, uint64_t ident, uint8_t create); //TODO channel* mm_channel_find() /* @@ -249,26 +242,26 @@ channel* MM_API mm_channel(instance* i, uint64_t ident, uint8_t create); * to be selected on. The backend will be notified when the descriptor * becomes ready to read via its registered mmbackend_process_fd call. */ -int MM_API mm_manage_fd(int fd, char* backend, int manage, void* impl); +MM_API int mm_manage_fd(int fd, char* backend, int manage, void* impl); /* * Notifies the core of a channel event. Called by backends to * inject events gathered from their backing implementation. */ -int MM_API mm_channel_event(channel* c, channel_value v); +MM_API int mm_channel_event(channel* c, channel_value v); /* * Query all active instances for a given backend. * *i will need to be freed by the caller. */ -int MM_API mm_backend_instances(char* backend, size_t* n, instance*** i); +MM_API int mm_backend_instances(char* backend, size_t* n, instance*** i); /* * Query an internal timestamp, which is updated every core iteration. * This timestamp should not be used as a performance counter, but can be * used for timeouting. Resolution is milliseconds. */ -uint64_t MM_API mm_timestamp(); +MM_API uint64_t mm_timestamp(); /* * Create a channel-to-channel mapping. This API should not -- cgit v1.2.3 From 7c20eeea5b1ee8c3c93e29f92e907a14498f9b73 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 7 Aug 2019 22:59:02 +0200 Subject: Don't build the maweb backend yet --- backends/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'backends/Makefile') diff --git a/backends/Makefile b/backends/Makefile index 3308ef0..2635ddc 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,7 +1,7 @@ .PHONY: all clean full LINUX_BACKENDS = midi.so evdev.so -WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll -BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so +WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so OPTIONAL_BACKENDS = ola.so BACKEND_LIB = libmmbackend.o -- cgit v1.2.3 From bb6111986bf7a997055287b916d0822957c5d13c Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 11 Aug 2019 20:29:17 +0200 Subject: Initial maweb backend --- README.md | 1 + TODO | 6 + backends/Makefile | 2 + backends/libmmbackend.c | 217 +++++++++++++++ backends/libmmbackend.h | 75 ++++++ backends/maweb.c | 695 ++++++++++++++++++++++++++++++++++++++++++++++++ backends/maweb.h | 69 +++++ backends/maweb.md | 142 ++++++++++ midimonster.c | 3 + monster.cfg | 29 +- 10 files changed, 1238 insertions(+), 1 deletion(-) create mode 100644 backends/maweb.c create mode 100644 backends/maweb.h create mode 100644 backends/maweb.md (limited to 'backends/Makefile') diff --git a/README.md b/README.md index 8a0d7f9..3e9bb88 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ support for the protocols to translate. * `liblua5.3-dev` (for the lua backend) * `libola-dev` (for the optional OLA backend) * `pkg-config` (as some projects and systems like to spread their files around) +* `libssl-dev` (for the MA Web Remote backend) * A C compiler * GNUmake diff --git a/TODO b/TODO index 5f4ce91..d04773b 100644 --- a/TODO +++ b/TODO @@ -1,6 +1,12 @@ MIDI NRPN +keepalive channels per backend? +mm_backend_start might get some arguments so they don't have to fetch them all the time +mm_channel_resolver might get additional info about the mapping direction Note source in channel value struct Optimize core channel search (store backend offset) Printing backend / Verbose mode mm_managed_fd.impl is not freed currently + +rtpmidi mode=peer + mode=initiator diff --git a/backends/Makefile b/backends/Makefile index 2635ddc..582655c 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -33,8 +33,10 @@ 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 midi.so: LDLIBS = -lasound evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index 2fd3b8b..c98cfe3 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -151,3 +151,220 @@ int mmbackend_send(int fd, uint8_t* data, size_t length){ 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; + + for(offset = 0; json[offset] && offset < length && json[offset] != '"'; offset++){ + } + + if(offset == length){ + 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){ + //TODO + return 0; +} + +size_t json_validate_object(char* json, size_t length){ + //TODO + return 0; +} + +size_t json_validate_value(char* json, size_t length){ + //TODO + 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)); + + //find comma or closing brace + for(; json[offset] && json[offset] != ',' && json[offset] != '}'; offset++){ + } + + if(json[offset] == ','){ + offset++; + } + } + + 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; +} + +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; +} + +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; + int64_t 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; +} + +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 offset = json_obj_offset(json, key), raw_length; + char* rv = NULL; + if(offset){ + raw_length = json_validate_string(json + offset, strlen(json + offset)); + rv = calloc(raw_length - 1, sizeof(char)); + if(rv){ + memcpy(rv, json + offset + 1, raw_length - 2); + } + return rv; + } + return NULL; +} diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index 31c4b96..aa0ac0c 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -16,6 +16,10 @@ #include #include "../portability.h" +/*** BACKEND IMPLEMENTATION LIBRARY ***/ + +/** Networking functions **/ + /* * Parse spec as host specification in the form * host port @@ -49,3 +53,74 @@ 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 as input + * Returns offset on success, 0 on failure + */ +size_t json_obj_offset(char* json, char* key); + +/* + * Check for for a key within a JSON object + * Assumes a zero-terminated, validated JSON object as input + * Returns type of value + */ +json_type json_obj(char* json, char* key); + +//json_type json_array(char* json, size_t index) + +/* + * Fetch boolean value for an object key + * Assumes a zero-terminated, validated JSON object as input + */ +uint8_t json_obj_bool(char* json, char* key, uint8_t fallback); + +/* + * Fetch integer/double value for an object key + * Assumes a zero-terminated validated JSON object as input + */ +int64_t json_obj_int(char* json, char* key, int64_t fallback); +double json_obj_double(char* json, char* key, double fallback); + +/* + * Fetch a string value for an object key + * Assumes a zero-terminated validated JSON object as input + * json_obj_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); diff --git a/backends/maweb.c b/backends/maweb.c new file mode 100644 index 0000000..be4c2ac --- /dev/null +++ b/backends/maweb.c @@ -0,0 +1,695 @@ +#include +#include +#include +#ifndef MAWEB_NO_LIBSSL +#include +#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 char* cmdline_keys[] = { + "SET", + "PREV", + "NEXT", + "CLEAR", + "FIXTURE_CHANNEL", + "FIXTURE_GROUP_PRESET", + "EXEC_CUE", + "STORE_UPDATE", + "OOPS", + "ESC", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "PUNKT", + "PLUS", + "MINUS", + "THRU", + "IF", + "AT", + "FULL", + "HIGH", + "ENTER", + "OFF", + "ON", + "ASSIGN", + "LABEL", + "COPY", + "TIME", + "PAGE", + "MACRO", + "DELETE", + "GOTO", + "GO_PLUS", + "GO_MINUS", + "PAUSE", + "SELECT", + "FIXTURE", + "SEQU", + "CUE", + "PRESET", + "EDIT", + "UPDATE", + "EXEC", + "STORE", + "GROUP", + "PROG_ONLY", + "SPECIAL_DIALOGUE", + "SOLO", + "ODD", + "EVEN", + "WINGS", + "RESET", + "MA", + "layerMode", + "featureSort", + "fixtureSort", + "channelSort", + "hideName" +}; + +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 + }; + + if(sizeof(maweb_channel_ident) != sizeof(uint64_t)){ + fprintf(stderr, "maweb channel identification union out of bounds\n"); + return 1; + } + + //register backend + if(mm_backend_register(maweb)){ + fprintf(stderr, "Failed to register maweb backend\n"); + return 1; + } + return 0; +} + +static int maweb_configure(char* option, char* value){ + fprintf(stderr, "The maweb backend does not take any global configuration\n"); + 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; + #ifndef MAWEB_NO_LIBSSL + uint8_t password_hash[MD5_DIGEST_LENGTH]; + #endif + + 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; + 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 + } + + fprintf(stderr, "Unknown configuration parameter %s for manet 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){ + maweb_channel_ident ident = { + .label = 0 + }; + char* next_token = NULL; + size_t n; + + if(!strncmp(spec, "page", 4)){ + ident.fields.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)){ + ident.fields.type = exec_fader; + next_token += 5; + } + else if(!strncmp(next_token, "upper", 5)){ + ident.fields.type = exec_upper; + next_token += 5; + } + else if(!strncmp(next_token, "lower", 5)){ + ident.fields.type = exec_lower; + next_token += 5; + } + else if(!strncmp(next_token, "flash", 5)){ + ident.fields.type = exec_flash; + next_token += 5; + } + else if(!strncmp(next_token, "button", 6)){ + ident.fields.type = exec_fader; + next_token += 6; + } + ident.fields.index = strtoul(next_token, NULL, 10); + } + else{ + for(n = 0; n < sizeof(cmdline_keys) / sizeof(char*); n++){ + if(!strcmp(spec, cmdline_keys[n])){ + ident.fields.type = cmdline_button; + ident.fields.index = n + 1; + ident.fields.page = 1; + break; + } + } + } + + if(ident.fields.type && ident.fields.index && ident.fields.page + && ident.fields.index <= 90){ + //actually, those are zero-indexed... + ident.fields.index--; + ident.fields.page--; + return mm_channel(inst, ident.label, 1); + } + 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_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; + + fprintf(stderr, "maweb message (%lu): %s\n", payload_length, payload); + if(json_obj(payload, "session") == JSON_NUMBER){ + data->session = json_obj_int(payload, "session", data->session); + fprintf(stderr, "maweb session id is now %ld\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\":%ld}", + data->user, data->pass, 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"); + maweb_send_frame(inst, ws_text, (uint8_t*) "{\"session\":0}", 13); + } + + 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; + } + } + else if(!strncmp(field, "getdata", 7)){ + //FIXME stupid keepalive logic + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{\"requestType\":\"getdata\"," + "\"data\":\"set,clear,solo,high\"," + "\"realtime\":true," + "\"maxRequests\":10," + ",\"session\":%ld}", + data->session); + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + } + } + + 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 criticial + //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 - 2; 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 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; + //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); + + //FIXME this might be somewhat borked + bytes_read -= data->offset; + 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; + char xmit_buffer[MAWEB_XMIT_CHUNK]; + maweb_channel_ident ident; + size_t n; + + if(num && !data->login){ + fprintf(stderr, "maweb instance %s can not send output, not logged in\n", inst->name); + } + + for(n = 0; n < num; n++){ + ident.label = c[n]->ident; + switch(ident.fields.type){ + case exec_fader: + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{\"requestType\":\"playbacks_userInput\"," + "\"execIndex\":%d," + "\"pageIndex\":%d," + "\"faderValue\":%f," + "\"type\":1," + "\"session\":%ld" + "}", ident.fields.index, ident.fields.page, v[n].normalised, data->session); + fprintf(stderr, "maweb out %s\n", xmit_buffer); + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + break; + case exec_upper: + case exec_lower: + case exec_flash: + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{\"requestType\":\"playbacks_userInput\"," + //"\"cmdline\":\"\"," + "\"execIndex\":%d," + "\"pageIndex\":%d," + "\"buttonId\":%d," + "\"pressed\":%s," + "\"released\":%s," + "\"type\":0," + "\"session\":%ld" + "}", ident.fields.index, ident.fields.page, + (exec_flash - ident.fields.type), + (v[n].normalised > 0.9) ? "true" : "false", + (v[n].normalised > 0.9) ? "false" : "true", + data->session); + fprintf(stderr, "maweb out %s\n", xmit_buffer); + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + break; + case cmdline_button: + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{\"keyname\":\"%s\"," + //"\"autoSubmit\":false," + "\"value\":%d" + "}", cmdline_keys[ident.fields.index], + (v[n].normalised > 0.9) ? 1 : 0); + fprintf(stderr, "maweb out %s\n", xmit_buffer); + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + break; + default: + fprintf(stderr, "maweb control not yet implemented\n"); + break; + } + } + 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\":%ld}", data->session); + maweb_send_frame(inst[u], ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + } + } + + 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); + } + + if(last_keepalive && mm_timestamp() - last_keepalive >= MAWEB_CONNECTION_KEEPALIVE){ + rv |= maweb_keepalive(); + last_keepalive = mm_timestamp(); + } + + return rv; +} + +static int maweb_start(){ + size_t n, u; + instance** inst = 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++){ + if(maweb_connect(inst[u])){ + fprintf(stderr, "Failed to open connection to MA Web Remote for instance %s\n", inst[u]->name); + return 1; + } + } + + free(inst); + if(!n){ + return 0; + } + + fprintf(stderr, "maweb backend registering %lu descriptors to core\n", n); + + //initialize keepalive timeout + last_keepalive = 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(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..6e6e652 --- /dev/null +++ b/backends/maweb.h @@ -0,0 +1,69 @@ +#include "midimonster.h" + +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); +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(); + +//Default login password: MD5("midimonster") +#define MAWEB_DEFAULT_PASSWORD "2807623134739142b119aff358f8a219" +#define MAWEB_DEFAULT_PORT "80" +#define MAWEB_RECV_CHUNK 1024 +#define MAWEB_XMIT_CHUNK 2048 +#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, + exec_upper = 3, + exec_lower = 4, + exec_flash = 5, + cmdline_button +} maweb_channel_type; + +typedef enum /*_ws_conn_state*/ { + ws_new, + ws_http, + ws_open, + ws_closed +} maweb_state; + +typedef enum /*_ws_frame_op*/ { + ws_text = 1, + ws_binary = 2, + ws_ping = 9, + ws_pong = 10 +} maweb_operation; + +typedef union { + struct { + uint8_t padding[3]; + uint8_t type; + uint16_t page; + uint16_t index; + } fields; + uint64_t label; +} maweb_channel_ident; + +typedef struct /*_maweb_instance_data*/ { + char* host; + char* port; + char* user; + char* pass; + + uint8_t login; + int64_t session; + + 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..eb1ed44 --- /dev/null +++ b/backends/maweb.md @@ -0,0 +1,142 @@ +### 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 faders and buttons as well as write access to +the command line buttons. + +To allow this backend to connect to the console, 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`. + +#### Global configuration + +The `maweb` backend does not take any global configuration. + +#### 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 | +| `password` | `midimonster` | `midimonster` | Password for the remote session | + +#### Channel specification + +Currently, three types of channels can be assigned + +##### Executors + +Executors are arranged in pages, with each page having 90 fader executors and 90 button executors. +Note that when creating a new show, only the first page is created and active. + +A fader executor consists of a fader, two buttons (`upper`, `lower`) above it and one `flash` button below it. + +These controls can be adressed like + +``` +mw1.page1.fader5 > mw1.page1.upper5 +mw1.page3.lower3 > mw1.page2.flash2 +``` + +A button executor can likewise be mapped using the syntax + +``` +mw1.page2.button3 > mw1.page3.fader1 +``` + +##### Command line buttons + +Command line buttons 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. +``` + +The following button names are recognized by the backend: + +* `SET` +* `PREV` +* `NEXT` +* `CLEAR` +* `FIXTURE_CHANNEL` +* `FIXTURE_GROUP_PRESET` +* `EXEC_CUE` +* `STORE_UPDATE` +* `OOPS` +* `ESC` +* `0` +* `1` +* `2` +* `3` +* `4` +* `5` +* `6` +* `7` +* `8` +* `9` +* `PUNKT` +* `PLUS` +* `MINUS` +* `THRU` +* `IF` +* `AT` +* `FULL` +* `HIGH` +* `ENTER` +* `OFF` +* `ON` +* `ASSIGN` +* `LABEL` +* `COPY` +* `TIME` +* `PAGE` +* `MACRO` +* `DELETE` +* `GOTO` +* `GO_PLUS` +* `GO_MINUS` +* `PAUSE` +* `SELECT` +* `FIXTURE` +* `SEQU` +* `CUE` +* `PRESET` +* `EDIT` +* `UPDATE` +* `EXEC` +* `STORE` +* `GROUP` +* `PROG_ONLY` +* `SPECIAL_DIALOGUE` +* `SOLO` +* `ODD` +* `EVEN` +* `WINGS` +* `RESET` +* `MA` +* `layerMode` +* `featureSort` +* `fixtureSort` +* `channelSort` +* `hideName` + +Note that each Web Remote connection has it's own command line, as such commands entered using this backend will not affect +the command line on the main console. To do that, you will need to use another backend to feed input to the MA, such as +the ArtNet or MIDI backends. + +#### 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. + +This backend is currently in active development. It therefore has some limitations: + +* It outputs a lot of debug information +* It currently is write-only, channel events are only sent to the MA, not consumed by it +* Fader executors (and their buttons) seem to work, I haven't tested button executors yet. +* Command line events are sent, but I'm not sure they're being handled yet +* I have so far only tested it with GradMA2 OnPC diff --git a/midimonster.c b/midimonster.c index 1e47698..25cf4a0 100644 --- a/midimonster.c +++ b/midimonster.c @@ -298,6 +298,9 @@ int main(int argc, char** argv){ plugins_close(); return usage(argv[0]); } + + //load an initial timestamp + update_timestamp(); //start backends if(backends_start()){ diff --git a/monster.cfg b/monster.cfg index 2413f6d..d272cee 100644 --- a/monster.cfg +++ b/monster.cfg @@ -7,6 +7,33 @@ bind = 0.0.0.0 universe = 0 dest = 255.255.255.255 +[backend midi] +detect = on + +[backend evdev] +;detect = on + +[midi bcf] +read = BCF +write = BCF + +[evdev mouse] +input = TPPS +relaxis.REL_X = 255 +relaxis.REL_Y = -255 + +[maweb ma] +;host = 10.23.23.248 +host = 127.0.0.1 4040 +user = web +password = web + [map] +bcf.channel{0..7}.pitch > bcf.channel{0..7}.pitch +bcf.channel{0..7}.pitch > art.{1..8} -art.1+2 > loop.b +bcf.channel{0..7}.pitch > ma.page1.fader{1..8} +bcf.channel0.note{16..23} > ma.page1.upper{1..8} +bcf.channel0.note{24..31} > ma.page1.lower{1..8} +mouse.EV_REL.REL_Y > ma.page1.fader1 +mouse.EV_KEY.BTN_LEFT > ma.ASSIGN -- cgit v1.2.3 From 8b016f61a4b3d3be0c7b1e311209ab991276af0c Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 22 Aug 2019 21:13:48 +0200 Subject: Implement input for the maweb backend (with a few limitations) --- README.md | 2 +- backends/Makefile | 4 +- backends/evdev.h | 3 +- backends/libmmbackend.c | 241 ++++++++++++++++++++++++++++++-- backends/libmmbackend.h | 33 ++--- backends/maweb.c | 358 ++++++++++++++++++++++++++++++++++++++++-------- backends/maweb.h | 14 +- backends/maweb.md | 36 +++-- backends/midi.h | 3 +- backends/osc.h | 3 +- 10 files changed, 582 insertions(+), 115 deletions(-) (limited to 'backends/Makefile') diff --git a/README.md b/README.md index 4d8fe18..40f8fbb 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ on any other (or the same) supported protocol, for example to: * Translate MIDI Control Changes into Notes ([Example configuration](configs/unifest-17.cfg)) * Translate MIDI Notes into ArtNet or sACN ([Example configuration](configs/launchctl-sacn.cfg)) * Translate OSC messages into MIDI ([Example configuration](configs/midi-osc.cfg)) -* Dynamically route and modify events using the Lua programming language ([Example configuration](configs/lua.cfg) and [Script](configs/demo.lua)) to create your own lighting controller or run effects on TouchOSC (Flying faders demo [configuration](configs/flying-faders.cfg) and [script](configs/flying-faders.lua)) +* Dynamically generate, route and modify events using the Lua programming language ([Example configuration](configs/lua.cfg) and [Script](configs/demo.lua)) to create your own lighting controller or run effects on TouchOSC (Flying faders demo [configuration](configs/flying-faders.cfg) and [script](configs/flying-faders.lua)) * Use an OSC app as a simple lighting controller via ArtNet or sACN * Visualize ArtNet data using OSC tools * Control lighting fixtures or DAWs using gamepad controllers, trackballs, etc ([Example configuration](configs/evdev.cfg)) diff --git a/backends/Makefile b/backends/Makefile index 582655c..5c5b677 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,7 +1,7 @@ .PHONY: all clean full LINUX_BACKENDS = midi.so evdev.so -WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll -BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so +WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so OPTIONAL_BACKENDS = ola.so BACKEND_LIB = libmmbackend.o diff --git a/backends/evdev.h b/backends/evdev.h index b26664b..48bd0ab 100644 --- a/backends/evdev.h +++ b/backends/evdev.h @@ -52,4 +52,5 @@ typedef union { uint16_t code; } fields; uint64_t label; -} evdev_channel_ident; \ No newline at end of file +} evdev_channel_ident; + diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index c98cfe3..ccbeb52 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -208,10 +208,11 @@ size_t json_validate(char* json, size_t length){ size_t json_validate_string(char* json, size_t length){ size_t string_length = 0, offset; - for(offset = 0; json[offset] && offset < length && json[offset] != '"'; offset++){ + //skip leading whitespace + for(offset = 0; json[offset] && offset < length && isspace(json[offset]); offset++){ } - if(offset == length){ + if(offset == length || json[offset] != '"'){ return 0; } @@ -230,17 +231,122 @@ size_t json_validate_string(char* json, size_t length){ } size_t json_validate_array(char* json, size_t length){ - //TODO + 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){ - //TODO + 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){ - //TODO + 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; } @@ -284,13 +390,51 @@ size_t json_obj_offset(char* json, char* key){ //add length of value offset += json_validate(json + offset, strlen(json + offset)); - //find comma or closing brace - for(; json[offset] && json[offset] != ',' && json[offset] != '}'; 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; @@ -304,6 +448,14 @@ json_type json_obj(char* json, char* key){ 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){ @@ -317,6 +469,19 @@ uint8_t json_obj_bool(char* json, char* key, uint8_t fallback){ 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; @@ -332,7 +497,7 @@ int64_t json_obj_int(char* json, char* key, int64_t fallback){ double json_obj_double(char* json, char* key, double fallback){ char* next_token = NULL; - int64_t result; + double result; size_t offset = json_obj_offset(json, key); if(offset){ result = strtod(json + offset, &next_token); @@ -343,6 +508,32 @@ double json_obj_double(char* json, char* key, double fallback){ 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){ @@ -356,15 +547,37 @@ char* json_obj_str(char* json, char* key, size_t* length){ } char* json_obj_strdup(char* json, char* key){ - size_t offset = json_obj_offset(json, key), raw_length; - char* rv = NULL; + 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)); - rv = calloc(raw_length - 1, sizeof(char)); - if(rv){ - memcpy(rv, json + offset + 1, raw_length - 2); + if(length){ + *length = raw_length - 2; } - return rv; + 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 index aa0ac0c..5749119 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -78,49 +78,50 @@ json_type json_identify(char* json, size_t length); * 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 as input + * 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 - * Assumes a zero-terminated, validated JSON object as input + * 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, size_t index) +json_type json_array(char* json, uint64_t key); /* - * Fetch boolean value for an object key - * Assumes a zero-terminated, validated JSON object as input + * 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 key - * Assumes a zero-terminated validated JSON object as input + * 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 key - * Assumes a zero-terminated validated JSON object as input - * json_obj_strdup returns a newly-allocated buffer containing + * 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/maweb.c b/backends/maweb.c index 79e223f..07595be 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -14,7 +14,13 @@ #define WS_FLAG_FIN 0x80 #define WS_FLAG_MASK 0x80 +//TODO test using different pages simultaneously +//TODO test dot2 button virtual faders in fader view + 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 char* cmdline_keys[] = { "SET", @@ -94,7 +100,8 @@ int init(){ .handle = maweb_set, .process = maweb_handle, .start = maweb_start, - .shutdown = maweb_shutdown + .shutdown = maweb_shutdown, + .interval = maweb_interval }; if(sizeof(maweb_channel_ident) != sizeof(uint64_t)){ @@ -110,8 +117,27 @@ int init(){ return 0; } +static int channel_comparator(const void* raw_a, const void* raw_b){ + maweb_channel_ident* a = (maweb_channel_ident*) raw_a; + maweb_channel_ident* b = (maweb_channel_ident*) raw_b; + + if(a->fields.page != b->fields.page){ + return a->fields.page - b->fields.page; + } + return a->fields.index - b->fields.index; +} + +static uint32_t maweb_interval(){ + return update_interval - (last_update % update_interval); +} + static int maweb_configure(char* option, char* value){ - fprintf(stderr, "The maweb backend does not take any global configuration\n"); + if(!strcmp(option, "interval")){ + update_interval = strtoul(value, NULL, 10); + return 0; + } + + fprintf(stderr, "Unknown maweb backend configuration option %s\n", option); return 1; } @@ -187,6 +213,7 @@ static instance* maweb_instance(){ } static channel* maweb_channel(instance* inst, char* spec){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; maweb_channel_ident ident = { .label = 0 }; @@ -214,7 +241,7 @@ static channel* maweb_channel(instance* inst, char* spec){ next_token += 5; } else if(!strncmp(next_token, "flash", 5)){ - ident.fields.type = exec_flash; + ident.fields.type = exec_button; next_token += 5; } else if(!strncmp(next_token, "button", 6)){ @@ -238,6 +265,24 @@ static channel* maweb_channel(instance* inst, char* spec){ //actually, those are zero-indexed... ident.fields.index--; ident.fields.page--; + + //check if the channel is already known + for(n = 0; n < data->input_channels; n++){ + if(data->input_channel[n].label == ident.label){ + break; + } + } + + if(n == data->input_channels){ + data->input_channel = realloc(data->input_channel, (data->input_channels + 1) * sizeof(maweb_channel_ident)); + if(!data->input_channel){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + data->input_channel[n].label = ident.label; + data->input_channels++; + } + return mm_channel(inst, ident.label, 1); } fprintf(stderr, "Failed to parse maweb channel spec %s\n", spec); @@ -276,11 +321,216 @@ static int maweb_send_frame(instance* inst, maweb_operation op, uint8_t* payload return 0; } +static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_type metatype, char* payload, size_t payload_length){ + size_t exec_blocks = json_obj_offset(payload, (metatype == 2) ? "executorBlocks" : "bottomButtons"), offset, block = 0, control; + channel* chan = NULL; + channel_value evt; + maweb_channel_ident ident = { + .fields.page = page, + .fields.index = json_obj_int(payload, "iExec", 191) + }; + + if(!exec_blocks){ + if(metatype == 3){ + //ignore unused buttons + return 0; + } + fprintf(stderr, "maweb missing exec block data on exec %d\n", ident.fields.index); + return 1; + } + + if(metatype == 3){ + exec_blocks += json_obj_offset(payload + exec_blocks, "items"); + } + + //TODO detect unused faders + //TODO state tracking for fader values / exec run state + + //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"); + ident.fields.type = exec_fader; + chan = mm_channel(inst, ident.label, 0); + if(chan){ + evt.normalised = json_obj_double(payload + control, "v", 0.0); + mm_channel_event(chan, evt); + } + + ident.fields.type = exec_button; + chan = mm_channel(inst, ident.label, 0); + if(chan){ + evt.normalised = json_obj_int(payload, "isRun", 0); + mm_channel_event(chan, evt); + } + + //printf("maweb page %ld exec %d value %f running %lu\n", page, ident.fields.index, json_obj_double(payload + control, "v", 0.0), json_obj_int(payload, "isRun", 0)); + ident.fields.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--; + fprintf(stderr, "maweb playback message processing done, %lu 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] = "[0,100,200]", item_counts[1024] = "[21,21,21]", item_types[1024] = "[2,3,3]"; + //char item_indices[1024] = "[300,400]", item_counts[1024] = "[18,18]", item_types[1024] = "[3,3]"; + size_t page_index = 0, view = 2, channel = 0, offsets[3], channel_offset, channels; + + if(updates_inflight){ + fprintf(stderr, "maweb skipping update request, %lu updates still inflight\n", updates_inflight); + return 0; + } + + for(channel = 0; channel < data->input_channels; channel++){ + offsets[0] = offsets[1] = offsets[2] = 0; + page_index = data->input_channel[channel].fields.page; + if(data->peer_type == peer_dot2){ + //TODO implement poll segmentation for dot + //"\"startIndex\":[0,100,200]," + //"\"itemsCount\":[21,21,21]," + //"\"itemsType\":[2,3,3]," + //"\"view\":2," + //view = (data->input_channel[channel].fields.index >= 300) ? 3 : 2; + //observed + //"startIndex":[300,400,500,600,700,800], + //"itemsCount":[13,13,13,13,13,13] + //"itemsType":[3,3,3,3,3,3] + /*fprintf(stderr, "range start at %lu.%lu (%lu/%lu) end at %lu.%lu (%lu/%lu)\n", + page_index, + data->input_channel[channel].fields.index, + channel, + data->input_channels, + page_index, + data->input_channel[channel + channel_offset - 1].fields.index, + channel + channel_offset - 1, + data->input_channels + );*/ + //only send one request currently + channel = data->input_channels; + } + else{ + view = (data->input_channel[channel].fields.index >= 100) ? 3 : 2; + //for the ma, the view equals the exec type + snprintf(item_types, sizeof(item_types), "[%lu]", view); + //this channel must be included, so it must be in range for the first startindex + snprintf(item_indices, sizeof(item_indices), "[%d]", (data->input_channel[channel].fields.index / 5) * 5); + + for(channel_offset = 1; channel + channel_offset < data->input_channels + && data->input_channel[channel].fields.page == data->input_channel[channel + channel_offset].fields.page + && data->input_channel[channel].fields.index / 100 == data->input_channel[channel + channel_offset].fields.index / 100; channel_offset++){ + } + + channels = data->input_channel[channel + channel_offset - 1].fields.index - (data->input_channel[channel].fields.index / 5) * 5; + + + snprintf(item_counts, sizeof(item_indices), "[%lu]", ((channels / 5) * 5 + 5)); + channel += channel_offset - 1; + } + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{" + "\"requestType\":\"playbacks\"," + "\"startIndex\":%s," + "\"itemsCount\":%s," + "\"pageIndex\":%lu," + "\"itemsType\":%s," + "\"view\":%lu," + "\"execButtonViewMode\":2," //extended + "\"buttonsViewMode\":0," //get vfader for button execs + "\"session\":%lu" + "}", + 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)); + //fprintf(stderr, "req: %s\n", xmit_buffer); + 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; + } + } + fprintf(stderr, "maweb message (%lu): %s\n", payload_length, payload); if(json_obj(payload, "session") == JSON_NUMBER){ data->session = json_obj_int(payload, "session", data->session); @@ -294,7 +544,6 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le (data->peer_type == peer_dot2) ? "remote" : data->user, data->pass, 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); @@ -307,31 +556,6 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le maweb_send_frame(inst, ws_text, (uint8_t*) "{\"session\":0}", 13); } - 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; - } - } - else if(!strncmp(field, "getdata", 7)){ - //FIXME stupid keepalive logic - snprintf(xmit_buffer, sizeof(xmit_buffer), - "{\"requestType\":\"getdata\"," - "\"data\":\"set,clear,solo,high\"," - "\"realtime\":true," - "\"maxRequests\":10," - ",\"session\":%ld}", - data->session); - maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); - } - } - return 0; } @@ -376,7 +600,7 @@ 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 - 2; n++){ + 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)){ @@ -397,7 +621,7 @@ static ssize_t maweb_handle_lines(instance* inst, ssize_t bytes_read){ } } - return begin; + return data->offset + begin; } static ssize_t maweb_handle_ws(instance* inst, ssize_t bytes_read){ @@ -507,6 +731,7 @@ static int maweb_handle_fd(instance* inst){ 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; @@ -517,8 +742,6 @@ static int maweb_handle_fd(instance* inst){ memmove(data->buffer, data->buffer + bytes_handled, (data->offset + bytes_read) - bytes_handled); - //FIXME this might be somewhat borked - bytes_read -= data->offset; bytes_handled -= data->offset; bytes_read -= bytes_handled; data->offset = 0; @@ -551,30 +774,10 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"type\":1," "\"session\":%ld" "}", ident.fields.index, ident.fields.page, v[n].normalised, data->session); - fprintf(stderr, "maweb out %s\n", xmit_buffer); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); break; case exec_upper: case exec_lower: - case exec_flash: - snprintf(xmit_buffer, sizeof(xmit_buffer), - "{\"requestType\":\"playbacks_userInput\"," - //"\"cmdline\":\"\"," - "\"execIndex\":%d," - "\"pageIndex\":%d," - "\"buttonId\":%d," - "\"pressed\":%s," - "\"released\":%s," - "\"type\":0," - "\"session\":%ld" - "}", ident.fields.index, ident.fields.page, - (data->peer_type == peer_dot2) ? (ident.fields.type - 3) : (exec_flash - ident.fields.type), - (v[n].normalised > 0.9) ? "true" : "false", - (v[n].normalised > 0.9) ? "false" : "true", - data->session); - fprintf(stderr, "maweb out %s\n", xmit_buffer); - maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); - break; case exec_button: snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"requestType\":\"playbacks_userInput\"," @@ -586,13 +789,11 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"released\":%s," "\"type\":0," "\"session\":%ld" - "}", ident.fields.index, - ident.fields.page, - 0, + "}", ident.fields.index, ident.fields.page, + (data->peer_type == peer_dot2 && ident.fields.type == exec_upper) ? 0 : (ident.fields.type - exec_button), (v[n].normalised > 0.9) ? "true" : "false", (v[n].normalised > 0.9) ? "false" : "true", data->session); - fprintf(stderr, "maweb out %s\n", xmit_buffer); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); break; case cmdline_button: @@ -602,7 +803,6 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"value\":%d" "}", cmdline_keys[ident.fields.index], (v[n].normalised > 0.9) ? 1 : 0); - fprintf(stderr, "maweb out %s\n", xmit_buffer); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); break; default: @@ -638,6 +838,29 @@ static int maweb_keepalive(){ 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; @@ -646,17 +869,24 @@ static int maweb_handle(size_t num, managed_fd* fds){ 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; instance** inst = NULL; + maweb_instance_data* data = NULL; //fetch all defined instances if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ @@ -665,6 +895,10 @@ static int maweb_start(){ } for(u = 0; u < n; u++){ + //sort channels + data = (maweb_instance_data*) inst[u]->impl; + qsort(data->input_channel, data->input_channels, sizeof(maweb_channel_ident), channel_comparator); + if(maweb_connect(inst[u])){ fprintf(stderr, "Failed to open connection to MA Web Remote for instance %s\n", inst[u]->name); return 1; @@ -678,8 +912,8 @@ static int maweb_start(){ fprintf(stderr, "maweb backend registering %lu descriptors to core\n", n); - //initialize keepalive timeout - last_keepalive = mm_timestamp(); + //initialize timeouts + last_keepalive = last_update = mm_timestamp(); return 0; } @@ -713,6 +947,10 @@ static int maweb_shutdown(){ data->offset = data->allocated = 0; data->state = ws_new; + + free(data->input_channel); + data->input_channel = NULL; + data->input_channels = 0; } free(inst); diff --git a/backends/maweb.h b/backends/maweb.h index 5f59cc1..a868426 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -9,22 +9,22 @@ 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 2048 +#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, - exec_upper = 3, - exec_lower = 4, - exec_flash = 5, + exec_button = 2, //gma: 0 dot: 0 + exec_lower = 3, //gma: 1 dot: 1 + exec_upper = 4, //gma: 2 dot: 0 cmdline_button } maweb_channel_type; @@ -69,6 +69,10 @@ typedef struct /*_maweb_instance_data*/ { int64_t session; maweb_peer_type peer_type; + //need to keep an internal registry to optimize data polls + size_t input_channels; + maweb_channel_ident* input_channel; + int fd; maweb_state state; size_t offset; diff --git a/backends/maweb.md b/backends/maweb.md index d713d82..fe430db 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -2,8 +2,7 @@ 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 faders and buttons as well as write access to -the command line buttons. +It grants read-write access to the console's playback controls as well as write access to the command line. #### Setting up the console @@ -16,7 +15,9 @@ Web Remote. Set a web remote password using the option below the activation sett #### Global configuration -The `maweb` backend does not take any global configuration. +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|---------------------------------------------------------------| +| `interval` | `100` | `50` | Query interval for input data polling (in msec) | #### Instance configuration @@ -28,39 +29,44 @@ The `maweb` backend does not take any global configuration. #### Channel specification -Currently, three types of channels can be assigned +Currently, three types of MA controls can be assigned, with each having some subcontrols + +* Fader executor +* Button executor +* Command line buttons ##### 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 `flash` button below it. - * A button executor consists of a `button` control. + * 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 (upper row) and 222 through 201 (lower 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 18 buttons, divided into two button wings. Buttons on the wings + * 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` executors 308 to 301 (top row), 408 to 401 (second row), and so on until 808 through 801 (bottom row) + * 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. +the console before being usable. `fader` controls, when mapped as outputs from the MA, 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.flash2 +mw1.page3.lower3 > mw1.page2.button2 ``` A button executor can likewise be mapped using the syntax ``` -mw1.page2.button103 > mw1.page3.button101 +mw1.page2.button103 > mw1.page3.fader101 mw1.page2.button803 > mw1.page3.button516 ``` @@ -99,10 +105,12 @@ Since this may be a problem on some platforms, the backend can be built with thi 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. + This backend is currently in active development. It therefore has some limitations: * It outputs a lot of debug information -* It currently is write-only, channel events are only sent to the MA, not generated by it -* Fader executors (and their buttons) seem to work, I haven't tested button executors yet. * Command line events are sent, but I'm not sure they're being handled yet -* I have so far only tested it with GradMA2 OnPC +* For the dot2, currently only the Core & F-Wings are supported for input from the console, not the B-Wings diff --git a/backends/midi.h b/backends/midi.h index 5ec17ea..6c3fcf9 100644 --- a/backends/midi.h +++ b/backends/midi.h @@ -24,4 +24,5 @@ typedef union { uint8_t control; } fields; uint64_t label; -} midi_channel_ident; \ No newline at end of file +} midi_channel_ident; + diff --git a/backends/osc.h b/backends/osc.h index ab19463..dd5afb0 100644 --- a/backends/osc.h +++ b/backends/osc.h @@ -73,4 +73,5 @@ typedef union { uint32_t parameter; } fields; uint64_t label; -} osc_channel_ident; \ No newline at end of file +} osc_channel_ident; + -- cgit v1.2.3 From 1061c4a683df6ccef98c4307860d1c1db323131a Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 17 Sep 2019 22:08:14 +0200 Subject: Publish winmidi backend --- README.md | 3 +- backends/Makefile | 5 +- backends/winmidi.c | 567 ++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/winmidi.h | 43 ++++ backends/winmidi.md | 59 ++++++ 5 files changed, 675 insertions(+), 2 deletions(-) create mode 100644 backends/winmidi.c create mode 100644 backends/winmidi.h create mode 100644 backends/winmidi.md (limited to 'backends/Makefile') diff --git a/README.md b/README.md index 33c4f5a..3cdac99 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Currently, the MIDIMonster supports the following protocols: | Protocol | Operating Systems | Notes | Backends | |-------------------------------|-----------------------|-------------------------------|-------------------------------| -| MIDI | Linux | Via ALSA | [`midi`](backends/midi.md) | +| MIDI | Linux, Windows | Linux: via ALSA | [`midi`](backends/midi.md), [`winmidi`](backends/winmidi.md) | | ArtNet | Linux, Windows, OSX | Version 4 | [`artnet`](backends/artnet.md)| | Streaming ACN (sACN / E1.31) | Linux, Windows, OSX | | [`sacn`](backends/sacn.md) | | OpenSoundControl (OSC) | Linux, Windows, OSX | | [`osc`](backends/osc.md) | @@ -117,6 +117,7 @@ configuration options, channel specification syntax and any known problems or ot special information. These documentation files are located in the `backends/` directory. * [`midi` backend documentation](backends/midi.md) +* [`winmidi` backend documentation](backends/winmidi.md) * [`artnet` backend documentation](backends/artnet.md) * [`sacn` backend documentation](backends/sacn.md) * [`evdev` backend documentation](backends/evdev.md) diff --git a/backends/Makefile b/backends/Makefile index 5c5b677..293b434 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,6 +1,6 @@ .PHONY: all clean full LINUX_BACKENDS = midi.so evdev.so -WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll +WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so OPTIONAL_BACKENDS = ola.so BACKEND_LIB = libmmbackend.o @@ -38,6 +38,9 @@ maweb.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) maweb.dll: LDLIBS += -lws2_32 maweb.dll: CFLAGS += -DMAWEB_NO_LIBSSL +winmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +winmidi.dll: LDLIBS += -lwinmm -lws2_32 + midi.so: LDLIBS = -lasound evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) evdev.so: LDLIBS = $(shell pkg-config --libs libevdev) diff --git a/backends/winmidi.c b/backends/winmidi.c new file mode 100644 index 0000000..dd8442b --- /dev/null +++ b/backends/winmidi.c @@ -0,0 +1,567 @@ +#include + +#include "libmmbackend.h" +#include + +#define DEBUG +#include "winmidi.h" + +#define BACKEND_NAME "winmidi" + +static struct { + uint8_t list_devices; + 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 allow connect-device specification by index +//TODO detect option + +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; + } + + 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 otput 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){ + 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; + } + } + else{ + fprintf(stderr, "Unknown 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.\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; + + 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; + + switch(ident.fields.type){ + case note: + output.components.status = 0x90 | ident.fields.channel; + output.components.data1 = ident.fields.control; + output.components.data2 = v[u].normalised * 127.0; + break; + case cc: + output.components.status = 0xB0 | ident.fields.channel; + output.components.data1 = ident.fields.control; + output.components.data2 = v[u].normalised * 127.0; + break; + case pressure: + output.components.status = 0xA0 | ident.fields.channel; + output.components.data1 = ident.fields.control; + output.components.data2 = v[u].normalised * 127.0; + break; + case aftertouch: + output.components.status = 0xD0 | ident.fields.channel; + output.components.data1 = v[u].normalised * 127.0; + output.components.data2 = 0; + break; + case pitchbend: + output.components.status = 0xE0 | ident.fields.channel; + output.components.data1 = ((int)(v[u].normalised * 32639.0)) & 0xFF; + output.components.data2 = (((int)(v[u].normalised * 32639.0)) & 0xFF00) >> 8; + break; + default: + fprintf(stderr, "Unknown winmidi channel type %d\n", ident.fields.type); + continue; + } + + midiOutShortMsg(data->device_out, output.dword); + } + + return 0; +} + +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++){ + 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; + switch(input.components.status & 0xF0){ + case 0x80: + ident.fields.type = note; + ident.fields.control = input.components.data1; + val.normalised = 0.0; + break; + case 0x90: + ident.fields.type = note; + ident.fields.control = input.components.data1; + val.normalised = (double) input.components.data2 / 127.0; + break; + case 0xA0: + ident.fields.type = pressure; + ident.fields.control = input.components.data1; + val.normalised = (double) input.components.data2 / 127.0; + break; + case 0xB0: + ident.fields.type = cc; + ident.fields.control = input.components.data1; + val.normalised = (double) input.components.data2 / 127.0; + break; + case 0xD0: + ident.fields.type = aftertouch; + ident.fields.control = 0; + val.normalised = (double) input.components.data1 / 127.0; + break; + case 0xE0: + ident.fields.type = pitchbend; + ident.fields.control = 0; + val.normalised = (double)((input.components.data2 << 8) | input.components.data1) / 32639.0; + break; + default: + fprintf(stderr, "winmidi unhandled status byte %02X\n", input.components.status); + return; + } + 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 for name %s\n", input_caps.szPname, 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 for name %s\n", output_caps.szPname, 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; + //this really should be a size_t but getsockname specifies int* for some reason + int sockadd_len = sizeof(sockadd); + 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 + backend_config.socket_pair[0] = mmbackend_socket(NULL, "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)){ + fprintf(stderr, "winmidi failed to query feedback socket information\n"); + return 1; + } + 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)){ + fprintf(stderr, "winmidi failed to connect to feedback socket\n"); + 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..e4abda1 --- /dev/null +++ b/backends/winmidi.h @@ -0,0 +1,43 @@ +#include "midimonster.h" + +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); +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, + cc, + pressure, + aftertouch, + pitchbend +}; + +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..b1fde1e --- /dev/null +++ b/backends/winmidi.md @@ -0,0 +1,59 @@ +### 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 | + +MIDI device names may either be prefixes of MIDI device names or a numeric index corresponding to the list output at startup using the backend `list` option. + +#### 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 + +A MIDIMonster channel is specified using the syntax `channel.`. 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.`. + +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. -- cgit v1.2.3 From ff587cb77ee4a7e9169affbfefd84547da6fea38 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 6 Nov 2019 18:50:57 +0100 Subject: Implement JACK backend --- README.md | 7 +- backends/Makefile | 3 +- backends/jack.c | 742 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/jack.h | 76 ++++++ backends/jack.md | 84 +++++++ 5 files changed, 909 insertions(+), 3 deletions(-) create mode 100644 backends/jack.c create mode 100644 backends/jack.h create mode 100644 backends/jack.md (limited to 'backends/Makefile') diff --git a/README.md b/README.md index 3cdac99..f23e696 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,14 @@ Currently, the MIDIMonster supports the following protocols: | Protocol | Operating Systems | Notes | Backends | |-------------------------------|-----------------------|-------------------------------|-------------------------------| -| MIDI | Linux, Windows | Linux: via ALSA | [`midi`](backends/midi.md), [`winmidi`](backends/winmidi.md) | +| MIDI | Linux, Windows | Linux: via ALSA/JACK | [`midi`](backends/midi.md), [`winmidi`](backends/winmidi.md), [`jack`](backends/jack.md) | | ArtNet | Linux, Windows, OSX | Version 4 | [`artnet`](backends/artnet.md)| | Streaming ACN (sACN / E1.31) | Linux, Windows, OSX | | [`sacn`](backends/sacn.md) | | OpenSoundControl (OSC) | Linux, Windows, OSX | | [`osc`](backends/osc.md) | | evdev input devices | Linux | Virtual output supported | [`evdev`](backends/evdev.md) | | Open Lighting Architecture | Linux, OSX | | [`ola`](backends/ola.md) | | MA Lighting Web Remote | Linux, Windows, OSX | GrandMA and dot2 (incl. OnPC) | [`maweb`](backends/maweb.md) | +| JACK/LV2 Control Voltage (CV) | Linux | | [`jack`](backends/jack.md) | with additional flexibility provided by a [Lua scripting environment](backends/lua.md). @@ -117,6 +118,7 @@ configuration options, channel specification syntax and any known problems or ot special information. These documentation files are located in the `backends/` directory. * [`midi` backend documentation](backends/midi.md) +* [`jack` backend documentation](backends/jack.md) * [`winmidi` backend documentation](backends/winmidi.md) * [`artnet` backend documentation](backends/artnet.md) * [`sacn` backend documentation](backends/sacn.md) @@ -137,10 +139,11 @@ This section will explain how to build the provided sources to be able to run In order to build the MIDIMonster, you'll need some libraries that provide support for the protocols to translate. -* `libasound2-dev` (for the MIDI backend) +* `libasound2-dev` (for the ALSA MIDI backend) * `libevdev-dev` (for the evdev backend) * `liblua5.3-dev` (for the lua backend) * `libola-dev` (for the optional OLA backend) +* `libjack-jackd2-dev` (for the JACK backend) * `pkg-config` (as some projects and systems like to spread their files around) * `libssl-dev` (for the MA Web Remote backend) * A C compiler diff --git a/backends/Makefile b/backends/Makefile index 293b434..c5755c9 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,5 +1,5 @@ .PHONY: all clean full -LINUX_BACKENDS = midi.so evdev.so +LINUX_BACKENDS = midi.so evdev.so jack.so WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so OPTIONAL_BACKENDS = ola.so @@ -41,6 +41,7 @@ maweb.dll: CFLAGS += -DMAWEB_NO_LIBSSL 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) diff --git a/backends/jack.c b/backends/jack.c new file mode 100644 index 0000000..5a88cf2 --- /dev/null +++ b/backends/jack.c @@ -0,0 +1,742 @@ +#include +#include +#include +#include +#include + +#include "jack.h" +#include +#include + +#define BACKEND_NAME "jack" +#define JACKEY_SIGNAL_TYPE "http://jackaudio.org/metadata/signal-type" + +//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 +}; + +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; + if(ident.fields.sub_type == 0x80){ + ident.fields.sub_type = midi_note; + value = 0; + } + else if(ident.fields.sub_type == midi_pitchbend){ + value = event.buffer[1] | (event.buffer[2] << 7); + } + else if(ident.fields.sub_type == midi_aftertouch){ + value = event.buffer[1]; + } + else{ + ident.fields.sub_control = event.buffer[1]; + value = event.buffer[2]; + } + //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 dont 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){ + 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++){ + 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; + } + inst = (instance*) fds[u].impl; + data = (mmjack_instance_data*) inst->impl; + + 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); + return 1; + } + } + + //do the thing + if(jack_activate(data->client)){ + fprintf(stderr, "Failed to activate jack client for instance %s\n", inst[u]->name); + return 1; + } + } + + 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..dd59cd2 --- /dev/null +++ b/backends/jack.h @@ -0,0 +1,76 @@ +#include "midimonster.h" +#include +#include + +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); +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 = min max +``` + +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.`. 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.`. + +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. -- cgit v1.2.3 From be38eb9ddd1b82a87cf26884dd13ccb6dff5eebf Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 6 Nov 2019 21:09:16 +0100 Subject: Try to build the JACK backend on OSX --- .travis.yml | 2 +- backends/Makefile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'backends/Makefile') diff --git a/.travis.yml b/.travis.yml index 59274e4..e1cef00 100644 --- a/.travis.yml +++ b/.travis.yml @@ -180,7 +180,7 @@ install: before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ccache ola lua openssl; fi + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ccache ola lua openssl jack; fi # OpenSSL is not a proper install due to some Apple bull, so provide additional locations via the environment... - export CFLAGS="$CFLAGS -I/usr/local/opt/openssl/include" - export LDFLAGS="$LDFLAGS -L/usr/local/opt/openssl/lib" diff --git a/backends/Makefile b/backends/Makefile index c5755c9..901ec49 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,7 +1,7 @@ .PHONY: all clean full -LINUX_BACKENDS = midi.so evdev.so jack.so +LINUX_BACKENDS = midi.so evdev.so WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll -BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so OPTIONAL_BACKENDS = ola.so BACKEND_LIB = libmmbackend.o -- cgit v1.2.3 From a0831a2b970404eaa9b80ff97ab46ee759131414 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 3 Dec 2019 22:37:45 +0100 Subject: Add error checking for shell callouts during build --- backends/Makefile | 8 ++++---- midimonster.c | 2 +- midimonster.h | 5 +++++ 3 files changed, 10 insertions(+), 5 deletions(-) (limited to 'backends/Makefile') diff --git a/backends/Makefile b/backends/Makefile index 901ec49..4e37ca4 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -43,12 +43,12 @@ 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 -lua.so: CFLAGS += $(shell pkg-config --cflags lua5.3) -lua.so: LDLIBS += $(shell pkg-config --libs lua5.3) +lua.so: CFLAGS += $(shell pkg-config --cflags lua53 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") +lua.so: LDLIBS += $(shell pkg-config --libs lua53 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") %.so :: %.c %.h $(BACKEND_LIB) $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) diff --git a/midimonster.c b/midimonster.c index 25cf4a0..eb64974 100644 --- a/midimonster.c +++ b/midimonster.c @@ -224,7 +224,7 @@ static void event_free(){ } static int usage(char* fn){ - fprintf(stderr, "MIDIMonster v0.1\n"); + fprintf(stderr, "MIDIMonster v0.2\n"); fprintf(stderr, "Usage:\n"); fprintf(stderr, "\t%s \n", fn); return EXIT_FAILURE; diff --git a/midimonster.h b/midimonster.h index b05326c..3922b03 100644 --- a/midimonster.h +++ b/midimonster.h @@ -29,6 +29,11 @@ #define DBG(message) #endif +/* Stop compilation if the build system reports an error */ +#ifdef BUILD_ERROR + #error The build system reported an error, compilation stopped. Refer to the invocation for this compilation unit for more information. +#endif + /* Pull in additional defines for non-linux platforms */ #include "portability.h" -- cgit v1.2.3 From 4a86ad5ac36c54de1bff61d00b80734da226e37e Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 3 Dec 2019 23:04:45 +0100 Subject: Fix the build on OSX once again --- backends/Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'backends/Makefile') diff --git a/backends/Makefile b/backends/Makefile index 4e37ca4..75ce74b 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -47,8 +47,10 @@ evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev || echo "-DBUILD_ERROR= 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 -lua.so: CFLAGS += $(shell pkg-config --cflags lua53 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") -lua.so: LDLIBS += $(shell pkg-config --libs lua53 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") +# 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 --cflags lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") %.so :: %.c %.h $(BACKEND_LIB) $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) -- cgit v1.2.3 From dcdf802e10b8199bf04139b0e63912bda2b681ce Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 3 Dec 2019 23:06:51 +0100 Subject: Fix the flags for the pkg-build invocation on OSX --- backends/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'backends/Makefile') diff --git a/backends/Makefile b/backends/Makefile index 75ce74b..feefd7b 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -50,7 +50,7 @@ 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 --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) -- cgit v1.2.3