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 ++- backends/ola.cpp | 318 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/ola.h | 38 +++++++ 3 files changed, 367 insertions(+), 2 deletions(-) create mode 100644 backends/ola.cpp create mode 100644 backends/ola.h (limited to 'backends') 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) diff --git a/backends/ola.cpp b/backends/ola.cpp new file mode 100644 index 0000000..299c883 --- /dev/null +++ b/backends/ola.cpp @@ -0,0 +1,318 @@ +#include "ola.h" +#include +#include +#include +#include +#include +#include +#include + +#define BACKEND_NAME "ola" +static ola::io::SelectServer* ola_select = NULL; +static ola::OlaCallbackClient* ola_client = NULL; + +int init(){ + backend ola = { + .name = BACKEND_NAME, + .conf = ola_configure, + .create = ola_instance, + .conf_instance = ola_configure_instance, + .channel = ola_channel, + .handle = ola_set, + .process = ola_handle, + .start = ola_start, + .shutdown = ola_shutdown + }; + + //register backend + if(mm_backend_register(ola)){ + fprintf(stderr, "Failed to register OLA backend\n"); + return 1; + } + + ola::InitLogging(ola::OLA_LOG_WARN, ola::OLA_LOG_STDERR); + return 0; +} + +static int ola_configure(char* option, char* value){ + fprintf(stderr, "Unknown OLA backend option %s\n", option); + return 1; +} + +static instance* ola_instance(){ + ola_instance_data* data = NULL; + instance* inst = mm_instance(); + if(!inst){ + return NULL; + } + + data = (ola_instance_data*)calloc(1, sizeof(ola_instance_data)); + if(!data){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + + inst->impl = data; + return inst; +} + +static int ola_configure_instance(instance* inst, char* option, char* value){ + ola_instance_data* data = (ola_instance_data*) inst->impl; + + if(!strcmp(option, "universe")){ + data->universe_id = strtoul(value, NULL, 0); + return 0; + } + + fprintf(stderr, "Unknown OLA option %s for instance %s\n", option, inst->name); + return 1; +} + +static channel* ola_channel(instance* inst, char* spec){ + ola_instance_data* data = (ola_instance_data*) inst->impl; + char* spec_next = spec; + unsigned chan_a = strtoul(spec, &spec_next, 10); + unsigned chan_b = 0; + + //primary channel sanity check + if(!chan_a || chan_a > 512){ + fprintf(stderr, "Invalid OLA channel specification %s\n", spec); + return NULL; + } + chan_a--; + + //secondary channel setup + if(*spec_next == '+'){ + chan_b = strtoul(spec_next + 1, NULL, 10); + if(!chan_b || chan_b > 512){ + fprintf(stderr, "Invalid wide-channel spec %s\n", spec); + return NULL; + } + chan_b--; + + //if mapped mode differs, bail + if(IS_ACTIVE(data->data.map[chan_b]) && data->data.map[chan_b] != (MAP_FINE | chan_a)){ + fprintf(stderr, "Fine channel already mapped for OLA spec %s\n", spec); + return NULL; + } + + data->data.map[chan_b] = MAP_FINE | chan_a; + } + + //check current map mode + if(IS_ACTIVE(data->data.map[chan_a])){ + if((*spec_next == '+' && data->data.map[chan_a] != (MAP_COARSE | chan_b)) + || (*spec_next != '+' && data->data.map[chan_a] != (MAP_SINGLE | chan_a))){ + fprintf(stderr, "Primary OLA channel already mapped at differing mode: %s\n", spec); + return NULL; + } + } + data->data.map[chan_a] = (*spec_next == '+') ? (MAP_COARSE | chan_b) : (MAP_SINGLE | chan_a); + + return mm_channel(inst, chan_a, 1); +} + +static int ola_set(instance* inst, size_t num, channel** c, channel_value* v){ + size_t u, mark = 0; + ola_instance_data* data = (ola_instance_data*) inst->impl; + + for(u = 0; u < num; u++){ + if(IS_WIDE(data->data.map[c[u]->ident])){ + uint32_t val = v[u].normalised * ((double) 0xFFFF); + //the primary (coarse) channel is the one registered to the core, so we don't have to check for that + if(data->data.data[c[u]->ident] != ((val >> 8) & 0xFF)){ + mark = 1; + data->data.data[c[u]->ident] = (val >> 8) & 0xFF; + } + + if(data->data.data[MAPPED_CHANNEL(data->data.map[c[u]->ident])] != (val & 0xFF)){ + mark = 1; + data->data.data[MAPPED_CHANNEL(data->data.map[c[u]->ident])] = val & 0xFF; + } + } + else if(data->data.data[c[u]->ident] != (v[u].normalised * 255.0)){ + mark = 1; + data->data.data[c[u]->ident] = v[u].normalised * 255.0; + } + } + + if(mark){ + ola_client->SendDmx(data->universe_id, ola::DmxBuffer(data->data.data, 512)); + } + + return 0; +} + +static int ola_handle(size_t num, managed_fd* fds){ + if(!num){ + return 0; + } + + //defer input to ola via the scenic route... + ola_select->RunOnce(); + return 0; +} + +void ola_data_receive(unsigned int universe, const ola::DmxBuffer& ola_dmx, const std::string& error) { + size_t p, max_mark = 0; + //this should really be size_t but ola is weird... + unsigned int dmx_length = 512; + uint8_t raw_dmx[dmx_length]; + uint16_t wide_val; + channel* chan = NULL; + channel_value val; + instance* inst = mm_instance_find(BACKEND_NAME, universe); + if(!inst){ + return; + } + ola_instance_data* data = (ola_instance_data*) inst->impl; + ola_dmx.Get((uint8_t*)raw_dmx, &dmx_length); + + //read data into instance universe, mark changed channels + for(p = 0; p < dmx_length; p++){ + if(IS_ACTIVE(data->data.map[p]) && raw_dmx[p] != data->data.data[p]){ + data->data.data[p] = raw_dmx[p]; + data->data.map[p] |= MAP_MARK; + max_mark = p; + } + } + + //generate channel events + for(p = 0; p <= max_mark; p++){ + if(data->data.map[p] & MAP_MARK){ + data->data.map[p] &= ~MAP_MARK; + if(data->data.map[p] & MAP_FINE){ + chan = mm_channel(inst, MAPPED_CHANNEL(data->data.map[p]), 0); + } + else{ + chan = mm_channel(inst, p, 0); + } + + if(!chan){ + fprintf(stderr, "Active channel %zu on %s not known to core\n", p, inst->name); + return; + } + + if(IS_WIDE(data->data.map[p])){ + data->data.map[MAPPED_CHANNEL(data->data.map[p])] &= ~MAP_MARK; + wide_val = data->data.data[p] << ((data->data.map[p] & MAP_COARSE) ? 8 : 0); + wide_val |= data->data.data[MAPPED_CHANNEL(data->data.map[p])] << ((data->data.map[p] & MAP_COARSE) ? 0 : 8); + + val.raw.u64 = wide_val; + val.normalised = (double) wide_val / (double) 0xFFFF; + } + else{ + val.raw.u64 = data->data.data[p]; + val.normalised = (double) data->data.data[p] / 255.0; + } + + if(mm_channel_event(chan, val)){ + fprintf(stderr, "Failed to push OLA channel event to core\n"); + return; + } + } + } +} + +void ola_register_callback(const std::string &error) { + if(!error.empty()){ + fprintf(stderr, "OLA backend failed to register for universe: %s\n", error.c_str()); + } +} + +static int ola_start(){ + size_t n, u, p; + instance** inst = NULL; + ola_instance_data* data = NULL; + + ola_select = new ola::io::SelectServer(); + ola::network::IPV4SocketAddress ola_server(ola::network::IPV4Address::Loopback(), ola::OLA_DEFAULT_PORT); + ola::network::TCPSocket* ola_socket = ola::network::TCPSocket::Connect(ola_server); + if(!ola_socket){ + fprintf(stderr, "Failed to connect to OLA server\n"); + return 1; + } + + ola_client = new ola::OlaCallbackClient(ola_socket); + + if(!ola_client->Setup()){ + fprintf(stderr, "Failed to start OLA client\n"); + goto bail; + } + + ola_select->AddReadDescriptor(ola_socket); + + fprintf(stderr, "OLA backend registering %zu descriptors to core\n", 1); + if(mm_manage_fd(ola_socket->ReadDescriptor(), BACKEND_NAME, 1, NULL)){ + goto bail; + } + + ola_client->SetDmxCallback(ola::NewCallback(&ola_data_receive)); + + //fetch all defined instances + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + goto bail; + } + + //this should not happen anymore (backends without instances are not started anymore) + if(!n){ + free(inst); + return 0; + } + + for(u = 0; u < n; u++){ + data = (ola_instance_data*) inst[u]->impl; + inst[u]->ident = data->universe_id; + + //check for duplicate instances (using the same universe) + for(p = 0; p < u; p++){ + if(inst[u]->ident == inst[p]->ident){ + fprintf(stderr, "OLA universe used in multiple instances, use one instance: %s - %s\n", inst[u]->name, inst[p]->name); + goto bail; + } + } + ola_client->RegisterUniverse(data->universe_id, ola::REGISTER, ola::NewSingleCallback(&ola_register_callback)); + } + + //run the ola select implementation to run all commands + ola_select->RunOnce(); + free(inst); + return 0; +bail: + free(inst); + delete ola_client; + ola_client = NULL; + delete ola_select; + ola_select = NULL; + return 1; +} + +static int ola_shutdown(){ + size_t n, p; + instance** inst = NULL; + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + for(p = 0; p < n; p++){ + free(inst[p]->impl); + } + free(inst); + + if(ola_client){ + ola_client->Stop(); + delete ola_client; + ola_client = NULL; + } + + if(ola_select){ + ola_select->Terminate(); + delete ola_select; + ola_select = NULL; + } + + fprintf(stderr, "OLA backend shut down\n"); + return 0; +} diff --git a/backends/ola.h b/backends/ola.h new file mode 100644 index 0000000..c943d52 --- /dev/null +++ b/backends/ola.h @@ -0,0 +1,38 @@ +extern "C" { + #include "midimonster.h" + //C++ has it's own implementation of these... + #undef min + #undef max + + int init(); + static int ola_configure(char* option, char* value); + static int ola_configure_instance(instance* instance, char* option, char* value); + static instance* ola_instance(); + static channel* ola_channel(instance* instance, char* spec); + static int ola_set(instance* inst, size_t num, channel** c, channel_value* v); + static int ola_handle(size_t num, managed_fd* fds); + static int ola_start(); + static int ola_shutdown(); +} + +#define MAP_COARSE 0x0200 +#define MAP_FINE 0x0400 +#define MAP_SINGLE 0x0800 +#define MAP_MARK 0x1000 +#define MAPPED_CHANNEL(a) ((a) & 0x01FF) +#define IS_ACTIVE(a) ((a) & 0xFE00) +#define IS_WIDE(a) ((a) & (MAP_FINE | MAP_COARSE)) +#define IS_SINGLE(a) ((a) & MAP_SINGLE) + +//since ola seems to immediately loop back any sent data as input, we only use one buffer +//to avoid excessive event feedback loops +typedef struct /*_ola_universe_model*/ { + uint8_t data[512]; + uint16_t map[512]; +} ola_universe; + +typedef struct /*_ola_instance_model*/ { + /*TODO does ola support remote connections?*/ + unsigned int universe_id; + ola_universe data; +} ola_instance_data; -- cgit v1.2.3 From 90c58250ed8f10996000bc6cea10385f97969847 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 23 Mar 2019 14:30:11 +0100 Subject: Fix link visibilities --- backends/ola.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/ola.cpp b/backends/ola.cpp index 299c883..c9c535c 100644 --- a/backends/ola.cpp +++ b/backends/ola.cpp @@ -153,7 +153,7 @@ static int ola_handle(size_t num, managed_fd* fds){ return 0; } -void ola_data_receive(unsigned int universe, const ola::DmxBuffer& ola_dmx, const std::string& error) { +static void ola_data_receive(unsigned int universe, const ola::DmxBuffer& ola_dmx, const std::string& error) { size_t p, max_mark = 0; //this should really be size_t but ola is weird... unsigned int dmx_length = 512; @@ -214,7 +214,7 @@ void ola_data_receive(unsigned int universe, const ola::DmxBuffer& ola_dmx, cons } } -void ola_register_callback(const std::string &error) { +static void ola_register_callback(const std::string &error) { if(!error.empty()){ fprintf(stderr, "OLA backend failed to register for universe: %s\n", error.c_str()); } -- 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') 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 3949f80417f0361e79f22658a8b900b228f125f1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 24 Mar 2019 19:31:01 +0100 Subject: Fix minor issues reported by Coverity Scan --- backends/ola.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'backends') diff --git a/backends/ola.cpp b/backends/ola.cpp index c9c535c..632cef7 100644 --- a/backends/ola.cpp +++ b/backends/ola.cpp @@ -242,7 +242,7 @@ static int ola_start(){ ola_select->AddReadDescriptor(ola_socket); - fprintf(stderr, "OLA backend registering %zu descriptors to core\n", 1); + fprintf(stderr, "OLA backend registering connection descriptor to core\n"); if(mm_manage_fd(ola_socket->ReadDescriptor(), BACKEND_NAME, 1, NULL)){ goto bail; } -- 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') 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 a2b0728027dd8961ef84220c8c8eaf8a81154c71 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 30 Mar 2019 14:34:20 +0100 Subject: Fix MIDI mapping syntax --- backends/midi.c | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) (limited to 'backends') diff --git a/backends/midi.c b/backends/midi.c index d856ced..ad7f6fe 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -118,19 +118,30 @@ static channel* midi_channel(instance* instance, char* spec){ .label = 0 }; + //support deprecated syntax for a transition period... + uint8_t old_syntax = 0; char* channel; - if(!strncmp(spec, "cc", 2)){ + if(!strncmp(spec, "ch", 2)){ + channel = spec + 2; + if(!strncmp(spec, "channel", 7)){ + channel = spec + 7; + } + } + else if(!strncmp(spec, "cc", 2)){ ident.fields.type = cc; channel = spec + 2; + old_syntax = 1; } else if(!strncmp(spec, "note", 4)){ ident.fields.type = note; channel = spec + 4; + old_syntax = 1; } else if(!strncmp(spec, "nrpn", 4)){ ident.fields.type = nrpn; channel = spec + 4; + old_syntax = 1; } else{ fprintf(stderr, "Unknown MIDI channel specification %s\n", spec); @@ -138,19 +149,33 @@ static channel* midi_channel(instance* instance, char* spec){ } ident.fields.channel = strtoul(channel, &channel, 10); - - //FIXME test this - if(ident.fields.channel > 16){ + if(ident.fields.channel > 15){ fprintf(stderr, "MIDI channel out of range in channel spec %s\n", spec); return NULL; } if(*channel != '.'){ - fprintf(stderr, "Need MIDI channel specification of form channel.control, had %s\n", spec); + fprintf(stderr, "Need MIDI channel specification of form channel., had %s\n", spec); return NULL; } + //skip the period channel++; + if(!old_syntax){ + if(!strncmp(channel, "cc", 2)){ + ident.fields.type = cc; + channel += 2; + } + else if(!strncmp(channel, "note", 4)){ + ident.fields.type = note; + channel += 4; + } + else if(!strncmp(channel, "nrpn", 4)){ + ident.fields.type = nrpn; + channel += 4; + } + } + ident.fields.control = strtoul(channel, NULL, 10); if(ident.label){ -- cgit v1.2.3 From 0c333567f599206cb0be6b74f02e59820536e0b2 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 31 Mar 2019 09:00:12 +0200 Subject: Move backend documentation out of main README --- backends/artnet.md | 41 ++++++++++++++++++++++++++ backends/evdev.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++ backends/loopback.md | 28 ++++++++++++++++++ backends/midi.md | 54 +++++++++++++++++++++++++++++++++++ backends/ola.md | 41 ++++++++++++++++++++++++++ backends/osc.md | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/sacn.md | 58 +++++++++++++++++++++++++++++++++++++ 7 files changed, 375 insertions(+) create mode 100644 backends/artnet.md create mode 100644 backends/evdev.md create mode 100644 backends/loopback.md create mode 100644 backends/midi.md create mode 100644 backends/ola.md create mode 100644 backends/osc.md create mode 100644 backends/sacn.md (limited to 'backends') diff --git a/backends/artnet.md b/backends/artnet.md new file mode 100644 index 0000000..90a7697 --- /dev/null +++ b/backends/artnet.md @@ -0,0 +1,41 @@ +### The `artnet` backend + +The ArtNet backend provides read-write access to the UDP-based ArtNet protocol for lighting +fixture control. + +#### Global configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `bind` | `127.0.0.1 6454` | none | Binds a network address to listen for data. This option may be set multiple times, with each interface being assigned an index starting from 0 to be used with the `interface` instance configuration option. At least one interface is required for transmission. | +| `net` | `0` | `0` | The default net to use | + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `net` | `0` | `0` | ArtNet `net` to use | +| `universe` | `0` | `0` | Universe identifier | +| `destination` | `10.2.2.2` | none | Destination address for sent ArtNet frames. Setting this enables the universe for output | +| `interface` | `1` | `0` | The bound address to use for data input/output | + +#### Channel specification + +A channel is specified by it's universe index. Channel indices start at 1 and end at 512. + +Example mapping: +``` +net1.231 < net2.123 +``` + +A 16-bit channel (spanning any two normal 8-bit channels in the same universe, also called a wide channel) may be mapped with the syntax +``` +net1.1+2 > net2.5+123 +``` + +A normal channel that is part of a wide channel can not be mapped individually. + +#### Known bugs / problems + +The minimum inter-frame-time is disregarded, as the packet rate is determined by the rate of incoming +channel events. \ No newline at end of file diff --git a/backends/evdev.md b/backends/evdev.md new file mode 100644 index 0000000..dfe5ec9 --- /dev/null +++ b/backends/evdev.md @@ -0,0 +1,72 @@ +### The `evdev` backend + +This backend allows using Linux `evdev` devices such as mouses, keyboards, gamepads and joysticks +as input and output devices. All buttons and axes available to the Linux system are mappable. +Output is provided by the `uinput` kernel module, which allows creation of virtual input devices. +This functionality may require elevated privileges (such as special group membership or root access). + +#### Global configuration + +This backend does not take any global configuration. + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|---------------|-------------------------------------------------------| +| `device` | `/dev/input/event1` | none | `evdev` device to use as input device | +| `input` | `Xbox Wireless` | none | Presentation name of evdev device to use as input (prefix-matched) | +| `output` | `My Input Device` | none | Output device presentation name. Setting this option enables the instance for output | +| `exclusive` | `1` | `0` | Prevent other processes from using the device | +| `id` | `0x1 0x2 0x3` | none | Set output device bus identification (Vendor, Product and Version), optional | +| `axis.AXISNAME`| `34300 0 65536 255 4095` | none | Specify absolute axis details (see below) for output. This is required for any absolute axis to be output. + +The absolute axis details configuration (e.g. `axis.ABS_X`) is required for any absolute axis on output-enabled +instances. The configuration value contains, space-separated, the following values: + +* `value`: The value to assume for the axis until an event is received +* `minimum`: The axis minimum value +* `maximum`: The axis maximum value +* `fuzz`: A value used for filtering the input stream +* `flat`: An offset, below which all deviations will be ignored +* `resolution`: Axis resolution in units per millimeter (or units per radian for rotational axes) + +For real devices, all of these parameters for every axis can be found by running `evtest` on the device. + +#### Channel specification + +A channel is specified by its event type and event code, separated by `.`. For a complete list of event types and codes +see the [kernel documentation](https://www.kernel.org/doc/html/v4.12/input/event-codes.html). The most interesting event types are + +* `EV_KEY` for keys and buttons +* `EV_ABS` for absolute axes (such as Joysticks) +* `EV_REL` for relative axes (such as Mouses) + +The `evtest` tool is useful to gather information on devices active on the local system, including names, types, codes +and configuration supported by these devices. + +Example mapping: +``` +ev1.EV_KEY.KEY_A > ev1.EV_ABS.ABS_X +``` + +Note that to map an absolute axis on an output-enabled instance, additional information such as the axis minimum +and maximum are required. These must be specified in the instance configuration. When only mapping the instance +as a channel input, this is not required. + +#### Known bugs / problems + +Creating an `evdev` output device requires elevated privileges, namely, write access to the system's +`/dev/uinput`. Usually, this is granted for users in the `input` group and the `root` user. + +Input devices may synchronize logically connected event types (for example, X and Y axes) via `EV_SYN`-type +events. The MIDIMonster also generates these events after processing channel events, but may not keep the original +event grouping. + +Relative axes (`EV_REL`-type events), such as generated by mouses, are currently handled in a very basic fashion, +generating only the normalized channel values of `0`, `0.5` and `1` for any input less than, equal to and greater +than `0`, respectively. As for output, only the values `-1`, `0` and `1` are generated for the same interval. + +`EV_KEY` key-down events are sent for normalized channel values over `0.9`. + +Extended event type values such as `EV_LED`, `EV_SND`, etc are recognized in the MIDIMonster configuration file +but may or may not work with the internal channel mapping and normalization code. \ No newline at end of file diff --git a/backends/loopback.md b/backends/loopback.md new file mode 100644 index 0000000..a06c768 --- /dev/null +++ b/backends/loopback.md @@ -0,0 +1,28 @@ +### The `loopback` backend + +This backend allows the user to create logical mapping channels, for example to exchange triggering +channels easier later. All events that are input are immediately output again on the same channel. + +#### Global configuration + +All global configuration is ignored. + +#### Instance configuration + +All instance configuration is ignored + +#### Channel specification + +A channel may have any string for a name. + +Example mapping: +``` +loop.foo < loop.bar123 +``` + +#### Known bugs / problems + +It is possible (and very easy) to configure loops using this backend. Triggering a loop +will create a deadlock, preventing any other backends from generating events. +Be careful with bidirectional channel mappings, as any input will be immediately +output to the same channel again. \ No newline at end of file diff --git a/backends/midi.md b/backends/midi.md new file mode 100644 index 0000000..7d3e847 --- /dev/null +++ b/backends/midi.md @@ -0,0 +1,54 @@ +### The `midi` backend + +The MIDI backend provides read-write access to the MIDI protocol via virtual ports. + +#### Global configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `name` | `MIDIMonster` | none | MIDI client name | + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `read` | `20:0` | none | MIDI device to connect for input | +| `write` | `DeviceName` | none | MIDI device to connect for output | + +MIDI device names may either be `client:port` portnames or prefixes of MIDI device names. +Run `aconnect -i` to list input ports and `aconnect -o` to list output ports. + +Each instance also provides a virtual port, so MIDI devices can also be connected with `aconnect `. + +#### Channel specification + +The MIDI backend supports multiple channel types + +* `cc` - Control Changes +* `note` - Note On/Off messages +* `nrpn` - NRPNs (not yet implemented) + +A channel is specified using the syntax `channel.`. The shorthand `ch` may be used instead +of `channel`. +The earlier syntax of `.` is officially deprecated but still supported for compatability +reasons. This support may be removed at some future time. + +Channels range from `0` to `15`. Each channel consists of 128 notes (numbered `0` through `127`) and 128 CC's +(numbered likewise), a channel pressure control (also called 'channel aftertouch') and a pitch control. +Each Note also has an additional pressure value associated with it. + +Example mappings: +``` +midi1.ch0.note9 > midi2.channel1.cc4 +midi1.channel15.cc1 > midi1.channel0.note0 +``` +#### 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. + +NRPNs are not yet fully implemented, though rudimentary support is in the codebase. + +To see which events your MIDI devices output, ALSA provides the `aseqdump` utility. You can +list all incoming events using `aseqdump -p `. \ No newline at end of file diff --git a/backends/ola.md b/backends/ola.md new file mode 100644 index 0000000..e3a1197 --- /dev/null +++ b/backends/ola.md @@ -0,0 +1,41 @@ +### The `ola` backend + +This backend connects the MIDIMonster to the Open Lighting Architecture daemon. This can be useful +to take advantage of additional protocols implemented in OLA. This backend is currently marked as +optional and is only built with `make full` in the `backends/` directory, as the OLA is a large +dependency to require for all users. + +#### Global configuration + +This backend does not take any global configuration. + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|---------------|-------------------------------------------------------| +| `universe` | `7` | `0` | OLA universe to send/receive data on | + +#### Channel specification + +A channel is specified by it's universe index. Channel indices start at 1 and end at 512. + +Example mapping: +``` +ola1.231 < in2.123 +``` + +A 16-bit channel (spanning any two normal 8-bit channels in the same universe, also called a wide channel) may be mapped with the syntax +``` +ola1.1+2 > net2.5+123 +``` + +A normal channel that is part of a wide channel can not be mapped individually. + +#### Known bugs / problems + +The backend currently assumes that the OLA daemon is running on the same host as the MIDIMonster. +This may be made configurable in the future. + +This backend requires `libola-dev` to be installed, which pulls in a rather large and aggressive (in terms of probing +and taking over connected hardware) daemon. It is thus marked as optional and only built when executing the `full` target +within the `backends` directory. \ No newline at end of file diff --git a/backends/osc.md b/backends/osc.md new file mode 100644 index 0000000..c784cda --- /dev/null +++ b/backends/osc.md @@ -0,0 +1,81 @@ +### The `osc` backend + +This backend offers read and write access to the Open Sound Control protocol, +spoken primarily by visual interface tools and hardware such as TouchOSC. + +#### Global configuration + +This backend does not take any global configuration. + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `root` | `/my/osc/path` | none | An OSC path prefix to be prepended to all channels | +| `bind` | `:: 8000` | none | The host and port to listen on | +| `destination` | `10.11.12.13 8001` | none | Remote address to send OSC data to. Setting this enables the instance for output. The special value `learn` causes the MIDImonster to always reply to the address the last incoming packet came from. A different remote port for responses can be forced with the syntax `learn@` | + +Note that specifying an instance root speeds up matching, as packets not matching +it are ignored early in processing. + +Channels that are to be output or require a value range different from the default ranges (see below) +require special configuration, as their types and limits have to be set. + +This is done 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). + +**format** may be any sequence of valid OSC type characters. See below for a table of supported +OSC types. + +For each component of the path, the minimum and maximum values must be given separated by spaces. +Components may be accessed in the mapping section as detailed in the next section. + +An example configuration for transmission of an OSC message with 2 floating point components with +a range between 0.0 and 2.0 (for example, an X-Y control), would look as follows: + +``` +/1/xy1 = ff 0.0 2.0 0.0 2.0 +``` + +#### Channel specification + +A channel may be any valid OSC path, to which the instance root will be prepended if +set. Multi-value controls (such as X-Y pads) are supported by appending `:n` to the path, +where `n` is the parameter index, with the first (and default) one being `0`. + +Example mapping: +``` +osc1./1/xy1:0 > osc2./1/fader1 +``` + +Note that any channel that is to be output will need to be set up in the instance +configuration. + +#### Supported types & value ranges + +OSC allows controls to have individual value ranges and supports different parameter types. +The following types are currently supported by the MIDImonster: + +* **i**: 32-bit signed integer +* **f**: 32-bit IEEE floating point +* **h**: 64-bit signed integer +* **d**: 64-bit double precision floating point + +For each type, there is a default value range which will be assumed if the channel is not otherwise +configured using the instance configuration. Values out of a channels range will be clipped. + +The default ranges are: + +* **i**: `0` to `255` +* **f**: `0.0` to `1.0` +* **h**: `0` to `1024` +* **d**: `0.0` to `1.0` + +#### Known bugs / problems + +Ping requests are not yet answered. There may be some problems using broadcast output and input. \ No newline at end of file diff --git a/backends/sacn.md b/backends/sacn.md new file mode 100644 index 0000000..3d245a4 --- /dev/null +++ b/backends/sacn.md @@ -0,0 +1,58 @@ +### The `sacn` backend + +The sACN backend provides read-write access to the Multicast-UDP based streaming ACN protocol (ANSI E1.31-2016), +used for lighting fixture control. The backend sends universe discovery frames approximately every 10 seconds, +containing all write-enabled universes. + +#### Global configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `name` | `sACN source` | `MIDIMonster` | sACN source name | +| `cid` | `0xAA 0xBB 0xCC` ... | `MIDIMonster` | Source CID (16 bytes) | +| `bind` | `0.0.0.0 5568` | none | Binds a network address to listen for data. This option may be set multiple times, with each descriptor being assigned an index starting from 0 to be used with the `interface` instance configuration option. At least one descriptor is required for transmission. | + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `universe` | `0` | none | Universe identifier | +| `interface` | `1` | `0` | The bound address to use for data input/output | +| `priority` | `100` | none | The data priority to transmit for this instance. Setting this option enables the instance for output and includes it in the universe discovery report. | +| `destination` | `10.2.2.2` | Universe multicast | Destination address for unicast output. If unset, the multicast destination for the specified universe is used. | +| `from` | `0xAA 0xBB` ... | none | 16-byte input source CID filter. Setting this option filters the input stream for this universe. | +| `unicast` | `1` | `0` | Prevent this instance from joining its universe multicast group | + +Note that instances accepting multicast input also process unicast frames directed at them, while +instances in `unicast` mode will not receive multicast frames. + +#### Channel specification + +A channel is specified by it's universe index. Channel indices start at 1 and end at 512. + +Example mapping: +``` +sacn1.231 < sacn2.123 +``` + +A 16-bit channel (spanning any two normal 8-bit channels in the same universe, also called a wide channel) may be mapped with the syntax +``` +sacn.1+2 > sacn2.5+123 +``` + +A normal channel that is part of a wide channel can not be mapped individually. + +#### Known bugs / problems + +The DMX start code of transmitted and received universes is fixed as `0`. + +The (upper) limit on packet transmission rate mandated by section 6.6.1 of the sACN specification is disregarded. +The rate of packet transmission is influenced by the rate of incoming mapped events on the instance. + +Universe synchronization is currently not supported, though this feature may be implemented in the future. + +To use multicast input, all networking hardware in the path must support the IGMPv2 protocol. + +The Linux kernel limits the number of multicast groups an interface may join to 20. An instance configured +for input automatically joins the multicast group for its universe, unless configured in `unicast` mode. +This limit can be raised by changing the kernel option in `/proc/sys/net/ipv4/igmp_max_memberships`. \ No newline at end of file -- cgit v1.2.3 From 6b97a08dee55ac4807902fc7ae1b4efe2911b873 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 1 Apr 2019 20:13:22 +0200 Subject: Implement pitch, aftertouch and pressure events for the MIDI backend --- backends/midi.c | 40 +++++++++++++++++++++++++++++++++++++++- backends/midi.md | 23 +++++++++++++++-------- 2 files changed, 54 insertions(+), 9 deletions(-) (limited to 'backends') diff --git a/backends/midi.c b/backends/midi.c index ad7f6fe..2999e6b 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -24,6 +24,9 @@ enum /*_midi_channel_type*/ { none = 0, note, cc, + pressure, + aftertouch, + pitchbend, nrpn, sysmsg }; @@ -174,6 +177,16 @@ static channel* midi_channel(instance* instance, char* spec){ ident.fields.type = nrpn; channel += 4; } + else if(!strncmp(channel, "pressure", 8)){ + ident.fields.type = pressure; + channel += 8; + } + else if(!strncmp(channel, "pitch", 8)){ + ident.fields.type = pitchbend; + } + else if(!strncmp(channel, "aftertouch", 10)){ + ident.fields.type = aftertouch; + } } ident.fields.control = strtoul(channel, NULL, 10); @@ -209,6 +222,16 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){ case cc: snd_seq_ev_set_controller(&ev, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0); break; + case pressure: + snd_seq_ev_set_keypress(&ev, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0); + + break; + case pitchbend: + snd_seq_ev_set_pitchbend(&ev, ident.fields.channel, (v[u].normalised * 16383.0) - 8192); + break; + case aftertouch: + snd_seq_ev_set_chanpress(&ev, ident.fields.channel, v[u].normalised * 127.0); + break; case nrpn: //FIXME set to nrpn output break; @@ -239,13 +262,28 @@ static int midi_handle(size_t num, managed_fd* fds){ switch(ev->type){ case SND_SEQ_EVENT_NOTEON: case SND_SEQ_EVENT_NOTEOFF: - case SND_SEQ_EVENT_KEYPRESS: case SND_SEQ_EVENT_NOTE: ident.fields.type = note; ident.fields.channel = ev->data.note.channel; ident.fields.control = ev->data.note.note; val.normalised = (double)ev->data.note.velocity / 127.0; break; + case SND_SEQ_EVENT_KEYPRESS: + ident.fields.type = pressure; + ident.fields.channel = ev->data.note.channel; + ident.fields.control = ev->data.note.note; + val.normalised = (double)ev->data.note.velocity / 127.0; + break; + case SND_SEQ_EVENT_CHANPRESS: + ident.fields.type = aftertouch; + ident.fields.channel = ev->data.control.channel; + val.normalised = (double)ev->data.control.value / 127.0; + break; + case SND_SEQ_EVENT_PITCHBEND: + ident.fields.type = pitchbend; + ident.fields.channel = ev->data.control.channel; + val.normalised = ((double)ev->data.control.value + 8192) / 16383.0; + break; case SND_SEQ_EVENT_CONTROLLER: ident.fields.type = cc; ident.fields.channel = ev->data.control.channel; diff --git a/backends/midi.md b/backends/midi.md index 7d3e847..315edfe 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -22,25 +22,32 @@ Each instance also provides a virtual port, so MIDI devices can also be connecte #### Channel specification -The MIDI backend supports multiple channel types +The MIDI backend supports mapping different MIDI events to MIDIMonster channels. The currently supported event types are * `cc` - Control Changes * `note` - Note On/Off messages +* `pressure` - Note pressure/aftertouch messages +* `aftertouch` - Channel-wide aftertouch messages +* `pitch` - Channel pitchbend messages * `nrpn` - NRPNs (not yet implemented) -A channel is specified using the syntax `channel.`. The shorthand `ch` may be used instead -of `channel`. +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 earlier syntax of `.` is officially deprecated but still supported for compatability reasons. This support may be removed at some future time. -Channels range from `0` to `15`. Each channel consists of 128 notes (numbered `0` through `127`) and 128 CC's -(numbered likewise), a channel pressure control (also called 'channel aftertouch') and a pitch control. -Each Note also has an additional pressure value associated with it. +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.cc1 > midi1.channel0.note0 +midi1.channel15.pressure1 > midi1.channel0.note0 +midi1.aftertouch > midi2.cc0 +midi1.pitch > midi2.pitch ``` #### Known bugs / problems @@ -51,4 +58,4 @@ a configuration option at a later time. NRPNs are not yet fully implemented, though rudimentary support is in the codebase. To see which events your MIDI devices output, ALSA provides the `aseqdump` utility. You can -list all incoming events using `aseqdump -p `. \ No newline at end of file +list all incoming events using `aseqdump -p `. -- cgit v1.2.3 From dd3ecfd3312673c8cfcfd8fbe96586b23d873c07 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 1 Apr 2019 20:15:50 +0200 Subject: Fix spelling --- backends/midi.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'backends') diff --git a/backends/midi.md b/backends/midi.md index 315edfe..da9870f 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -33,7 +33,7 @@ The MIDI backend supports mapping different MIDI events to MIDIMonster channels. 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 earlier syntax of `.` is officially deprecated but still supported for compatability +The earlier syntax of `.` is officially deprecated but still supported for compatibility reasons. This support may be removed at some future time. The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel.`. -- cgit v1.2.3 From f4ca1869c3e1a106e4dbab39213ba81141ada1d5 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 1 Apr 2019 20:20:12 +0200 Subject: Fix MIDI example mappings --- backends/midi.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/midi.md b/backends/midi.md index da9870f..9733cc2 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -46,8 +46,8 @@ Example mappings: ``` midi1.ch0.note9 > midi2.channel1.cc4 midi1.channel15.pressure1 > midi1.channel0.note0 -midi1.aftertouch > midi2.cc0 -midi1.pitch > midi2.pitch +midi1.ch1.aftertouch > midi2.ch2.cc0 +midi1.ch0.pitch > midi2.ch1.pitch ``` #### Known bugs / problems -- cgit v1.2.3 From 68f1a58f13997cf052241d6e1177dcbec9a109ec Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 29 Jun 2019 11:54:23 +0200 Subject: Update loopback backend to match API design --- backends/evdev.c | 1 + backends/loopback.c | 42 ++++++++++++++++++++++-------------------- backends/loopback.h | 18 +++++++++--------- backends/osc.c | 1 + 4 files changed, 33 insertions(+), 29 deletions(-) (limited to 'backends') diff --git a/backends/evdev.c b/backends/evdev.c index 979698f..f528d06 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -468,5 +468,6 @@ static int evdev_shutdown(){ } free(instances); + fprintf(stderr, "evdev backend shut down\n"); return 0; } diff --git a/backends/loopback.c b/backends/loopback.c index bb93a1f..083a312 100644 --- a/backends/loopback.c +++ b/backends/loopback.c @@ -6,14 +6,14 @@ int init(){ backend loopback = { .name = BACKEND_NAME, - .conf = backend_configure, - .create = backend_instance, - .conf_instance = backend_configure_instance, - .channel = backend_channel, - .handle = backend_set, - .process = backend_handle, - .start = backend_start, - .shutdown = backend_shutdown + .conf = loopback_configure, + .create = loopback_instance, + .conf_instance = loopback_configure_instance, + .channel = loopback_channel, + .handle = loopback_set, + .process = loopback_handle, + .start = loopback_start, + .shutdown = loopback_shutdown }; //register backend @@ -24,23 +24,23 @@ int init(){ return 0; } -static int backend_configure(char* option, char* value){ +static int loopback_configure(char* option, char* value){ //intentionally ignored return 0; } -static int backend_configure_instance(instance* inst, char* option, char* value){ +static int loopback_configure_instance(instance* inst, char* option, char* value){ //intentionally ignored return 0; } -static instance* backend_instance(){ +static instance* loopback_instance(){ instance* i = mm_instance(); if(!i){ return NULL; } - i->impl = calloc(1, sizeof(loopback_instance)); + i->impl = calloc(1, sizeof(loopback_instance_data)); if(!i->impl){ fprintf(stderr, "Failed to allocate memory\n"); return NULL; @@ -49,9 +49,9 @@ static instance* backend_instance(){ return i; } -static channel* backend_channel(instance* inst, char* spec){ +static channel* loopback_channel(instance* inst, char* spec){ size_t u; - loopback_instance* data = (loopback_instance*) inst->impl; + loopback_instance_data* data = (loopback_instance_data*) inst->impl; //find matching channel for(u = 0; u < data->n; u++){ @@ -79,7 +79,7 @@ static channel* backend_channel(instance* inst, char* spec){ return mm_channel(inst, u, 1); } -static int backend_set(instance* inst, size_t num, channel** c, channel_value* v){ +static int loopback_set(instance* inst, size_t num, channel** c, channel_value* v){ size_t n; for(n = 0; n < num; n++){ mm_channel_event(c[n], v[n]); @@ -87,19 +87,19 @@ static int backend_set(instance* inst, size_t num, channel** c, channel_value* v return 0; } -static int backend_handle(size_t num, managed_fd* fds){ +static int loopback_handle(size_t num, managed_fd* fds){ //no events generated here return 0; } -static int backend_start(){ +static int loopback_start(){ return 0; } -static int backend_shutdown(){ +static int loopback_shutdown(){ size_t n, u, p; instance** inst = NULL; - loopback_instance* data = NULL; + loopback_instance_data* data = NULL; if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ fprintf(stderr, "Failed to fetch instance list\n"); @@ -107,7 +107,7 @@ static int backend_shutdown(){ } for(u = 0; u < n; u++){ - data = (loopback_instance*) inst[u]->impl; + data = (loopback_instance_data*) inst[u]->impl; for(p = 0; p < data->n; p++){ free(data->name[p]); } @@ -116,5 +116,7 @@ static int backend_shutdown(){ } free(inst); + + fprintf(stderr, "Loopback backend shut down\n"); return 0; } diff --git a/backends/loopback.h b/backends/loopback.h index fe44e91..c73ca20 100644 --- a/backends/loopback.h +++ b/backends/loopback.h @@ -1,16 +1,16 @@ #include "midimonster.h" int init(); -static int backend_configure(char* option, char* value); -static int backend_configure_instance(instance* instance, char* option, char* value); -static instance* backend_instance(); -static channel* backend_channel(instance* instance, char* spec); -static int backend_set(instance* inst, size_t num, channel** c, channel_value* v); -static int backend_handle(size_t num, managed_fd* fds); -static int backend_start(); -static int backend_shutdown(); +static int loopback_configure(char* option, char* value); +static int loopback_configure_instance(instance* inst, char* option, char* value); +static instance* loopback_instance(); +static channel* loopback_channel(instance* inst, char* spec); +static int loopback_set(instance* inst, size_t num, channel** c, channel_value* v); +static int loopback_handle(size_t num, managed_fd* fds); +static int loopback_start(); +static int loopback_shutdown(); typedef struct /*_loopback_instance_data*/ { size_t n; char** name; -} loopback_instance; +} loopback_instance_data; diff --git a/backends/osc.c b/backends/osc.c index 9996f68..130dd2d 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -705,5 +705,6 @@ static int backend_shutdown(){ } free(inst); + fprintf(stderr, "OSC backend shut down\n"); return 0; } -- cgit v1.2.3 From ee75bee08b8fb280fc1d76e8635cf29c576835da Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 30 Jun 2019 14:13:22 +0200 Subject: Update OSC backend to API design --- backends/osc.c | 52 ++++++++++++++++++++++++++-------------------------- backends/osc.h | 18 +++++++++--------- 2 files changed, 35 insertions(+), 35 deletions(-) (limited to 'backends') diff --git a/backends/osc.c b/backends/osc.c index 130dd2d..fd1bcd4 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -16,14 +16,14 @@ int init(){ backend osc = { .name = BACKEND_NAME, - .conf = backend_configure, - .create = backend_instance, - .conf_instance = backend_configure_instance, - .channel = backend_channel, - .handle = backend_set, - .process = backend_handle, - .start = backend_start, - .shutdown = backend_shutdown + .conf = osc_configure, + .create = osc_instance, + .conf_instance = osc_configure_instance, + .channel = osc_map_channel, + .handle = osc_set, + .process = osc_handle, + .start = osc_start, + .shutdown = osc_shutdown }; //register backend @@ -253,13 +253,13 @@ static int osc_validate_path(char* path){ return 0; } -static int backend_configure(char* option, char* value){ +static int osc_configure(char* option, char* value){ fprintf(stderr, "The OSC backend does not take any global configuration\n"); return 1; } -static int backend_configure_instance(instance* inst, char* option, char* value){ - osc_instance* data = (osc_instance*) inst->impl; +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; @@ -394,13 +394,13 @@ static int backend_configure_instance(instance* inst, char* option, char* value) return 1; } -static instance* backend_instance(){ +static instance* osc_instance(){ instance* inst = mm_instance(); if(!inst){ return NULL; } - osc_instance* data = calloc(1, sizeof(osc_instance)); + osc_instance_data* data = calloc(1, sizeof(osc_instance_data)); if(!data){ fprintf(stderr, "Failed to allocate memory\n"); return NULL; @@ -411,9 +411,9 @@ static instance* backend_instance(){ return inst; } -static channel* backend_channel(instance* inst, char* spec){ +static channel* osc_map_channel(instance* inst, char* spec){ size_t u; - osc_instance* data = (osc_instance*) inst->impl; + osc_instance_data* data = (osc_instance_data*) inst->impl; size_t param_index = 0; //check spec for correctness @@ -457,14 +457,14 @@ static channel* backend_channel(instance* inst, char* spec){ return mm_channel(inst, u, 1); } -static int backend_set(instance* inst, size_t num, channel** c, channel_value* v){ +static int 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; if(!num){ return 0; } - osc_instance* data = (osc_instance*) inst->impl; + osc_instance_data* data = (osc_instance_data*) inst->impl; if(!data->dest_len){ fprintf(stderr, "OSC instance %s does not have a destination, output is disabled (%zu channels)\n", inst->name, num); return 0; @@ -563,11 +563,11 @@ static int backend_set(instance* inst, size_t num, channel** c, channel_value* v return 0; } -static int backend_handle(size_t num, managed_fd* fds){ +static int osc_handle(size_t num, managed_fd* fds){ size_t fd; char recv_buf[OSC_RECV_BUF]; instance* inst = NULL; - osc_instance* data = NULL; + osc_instance_data* data = NULL; ssize_t bytes_read = 0; size_t c; char* osc_fmt = NULL; @@ -581,7 +581,7 @@ static int backend_handle(size_t num, managed_fd* fds){ continue; } - data = (osc_instance*) inst->impl; + data = (osc_instance_data*) inst->impl; do{ if(data->learn){ @@ -639,10 +639,10 @@ static int backend_handle(size_t num, managed_fd* fds){ return 0; } -static int backend_start(){ +static int osc_start(){ size_t n, u, fds = 0; instance** inst = NULL; - osc_instance* data = NULL; + osc_instance_data* data = NULL; //fetch all instances if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ @@ -657,7 +657,7 @@ static int backend_start(){ //update instance identifiers for(u = 0; u < n; u++){ - data = (osc_instance*) inst[u]->impl; + data = (osc_instance_data*) inst[u]->impl; if(data->fd >= 0){ inst[u]->ident = data->fd; @@ -679,10 +679,10 @@ static int backend_start(){ return 0; } -static int backend_shutdown(){ +static int osc_shutdown(){ size_t n, u, c; instance** inst = NULL; - osc_instance* data = NULL; + osc_instance_data* data = NULL; if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ fprintf(stderr, "Failed to fetch instance list\n"); @@ -690,7 +690,7 @@ static int backend_shutdown(){ } for(u = 0; u < n; u++){ - data = (osc_instance*) inst[u]->impl; + data = (osc_instance_data*) inst[u]->impl; for(c = 0; c < data->channels; c++){ free(data->channel[c].path); } diff --git a/backends/osc.h b/backends/osc.h index 5938f12..dc6cb3a 100644 --- a/backends/osc.h +++ b/backends/osc.h @@ -6,14 +6,14 @@ #define OSC_XMIT_BUF 8192 int init(); -static int backend_configure(char* option, char* value); -static int backend_configure_instance(instance* instance, char* option, char* value); -static instance* backend_instance(); -static channel* backend_channel(instance* instance, char* spec); -static int backend_set(instance* inst, size_t num, channel** c, channel_value* v); -static int backend_handle(size_t num, managed_fd* fds); -static int backend_start(); -static int backend_shutdown(); +static int osc_configure(char* option, char* value); +static int osc_configure_instance(instance* inst, char* option, char* value); +static instance* osc_instance(); +static channel* osc_map_channel(instance* inst, char* spec); +static int osc_set(instance* inst, size_t num, channel** c, channel_value* v); +static int osc_handle(size_t num, managed_fd* fds); +static int osc_start(); +static int osc_shutdown(); typedef enum { not_set = 0, @@ -52,4 +52,4 @@ typedef struct /*_osc_instance_data*/ { int fd; uint8_t learn; uint16_t forced_rport; -} osc_instance; +} osc_instance_data; -- 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 --- backends/Makefile | 4 +- backends/lua.c | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/lua.h | 21 ++++++ backends/lua.md | 49 +++++++++++++ 4 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 backends/lua.c create mode 100644 backends/lua.h create mode 100644 backends/lua.md (limited to 'backends') 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. -- cgit v1.2.3 From 62b41d0b2ea58b48961308ab24fe4287365b2d50 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 6 Jul 2019 17:44:10 +0200 Subject: evdev backend code style fixes --- backends/evdev.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'backends') diff --git a/backends/evdev.c b/backends/evdev.c index f528d06..8871ce4 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -176,23 +176,27 @@ static int evdev_configure_instance(instance* inst, char* option, char* value) { return 1; } free(next_token); + return 0; } else if(!strcmp(option, "exclusive")){ if(data->input_fd >= 0 && libevdev_grab(data->input_ev, LIBEVDEV_GRAB)){ fprintf(stderr, "Failed to obtain exclusive device access on %s\n", inst->name); } data->exclusive = 1; + return 0; } #ifndef EVDEV_NO_UINPUT else if(!strcmp(option, "output")){ data->output_enabled = 1; libevdev_set_name(data->output_proto, value); + return 0; } else if(!strcmp(option, "id")){ next_token = value; libevdev_set_id_vendor(data->output_proto, strtol(next_token, &next_token, 0)); libevdev_set_id_product(data->output_proto, strtol(next_token, &next_token, 0)); libevdev_set_id_version(data->output_proto, strtol(next_token, &next_token, 0)); + return 0; } else if(!strncmp(option, "axis.", 5)){ //value minimum maximum fuzz flat resolution @@ -207,13 +211,11 @@ static int evdev_configure_instance(instance* inst, char* option, char* value) { fprintf(stderr, "Failed to enable absolute axis %s for output\n", option + 5); return 1; } + return 0; } #endif - else{ - fprintf(stderr, "Unknown configuration parameter %s for evdev backend\n", option); - return 1; - } - return 0; + fprintf(stderr, "Unknown configuration parameter %s for evdev backend\n", option); + return 1; } static channel* evdev_channel(instance* inst, char* spec){ -- cgit v1.2.3 From 86b9706220ca285db961ea43ec0859ea99cc9f71 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 7 Jul 2019 12:30:10 +0200 Subject: Minor fixes --- backends/evdev.c | 4 ++++ backends/osc.c | 10 ++-------- 2 files changed, 6 insertions(+), 8 deletions(-) (limited to 'backends') diff --git a/backends/evdev.c b/backends/evdev.c index 8871ce4..ca8469a 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -378,6 +378,10 @@ static int evdev_start(){ fds++; } + if(data->input_fd <= 0 && !data->output_ev){ + fprintf(stderr, "Instance %s has neither input nor output device set up\n", inst[u]->name); + } + } fprintf(stderr, "evdev backend registered %zu descriptors to core\n", fds); diff --git a/backends/osc.c b/backends/osc.c index fd1bcd4..1305169 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -167,14 +167,8 @@ static inline channel_value osc_parameter_normalise(osc_parameter_type t, osc_pa fprintf(stderr, "Invalid OSC type passed to interpolation routine\n"); } - //fix overshoot - if(v.normalised > 1.0){ - v.normalised = 1.0; - } - else if(v.normalised < 0.0){ - v.normalised = 0.0; - } - + //clamp to range + v.normalised = clamp(v.normalised, 1.0, 0.0); return v; } -- cgit v1.2.3 From ee4a46105acecb6a7adc1e7189e8b0a66404b421 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 13 Jul 2019 17:51:11 +0200 Subject: Improved Lua backend with intervals --- backends/lua.c | 302 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- backends/lua.h | 10 ++ backends/lua.md | 32 ++++-- 3 files changed, 318 insertions(+), 26 deletions(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index ae2d460..08d8d3c 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -1,10 +1,18 @@ #include +#include +#include +#include #include "lua.h" #define BACKEND_NAME "lua" #define LUA_REGISTRY_KEY "_midimonster_lua_instance" -//TODO instance identification for callvacks +static size_t timers = 0; +static lua_timer* timer = NULL; +static struct itimerspec timer_config = { + 0 +}; +static int timer_fd = -1; int init(){ backend lua = { @@ -24,31 +32,83 @@ int init(){ fprintf(stderr, "Failed to register lua backend\n"); return 1; } + + //create the timer to expire intervals + timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK); + if(timer_fd < 0){ + fprintf(stderr, "Failed to create timer for Lua backend\n"); + return 1; + } + return 0; +} + +static int lua_update_timerfd(){ + uint64_t interval = 0, gcd, residual; + size_t n = 0; + + //find the minimum for the lower interval bounds + for(n = 0; n < timers; n++){ + if(timer[n].interval && (!interval || timer[n].interval < interval)){ + interval = timer[n].interval; + } + } + + //stop the timer + if(!interval){ + memset(&timer_config, 0, sizeof(struct itimerspec)); + } + //calculate gcd of all timers + else{ + for(n = 0; n < timers; n++){ + if(timer[n].interval){ + //calculate gcd of current interval and this timers interval + gcd = timer[n].interval; + while(gcd){ + residual = interval % gcd; + interval = gcd; + gcd = residual; + } + //since we round everything, 10 is the lowest interval we get + if(interval == 10){ + break; + } + } + } + + timer_config.it_interval.tv_sec = interval / 1000; + timer_config.it_interval.tv_nsec = (interval % 1000) * 1e6; + timer_config.it_value.tv_nsec = 1; + } + + //configure the new interval + timerfd_settime(timer_fd, 0, &timer_config, NULL); return 0; } static int lua_callback_output(lua_State* interpreter){ - int arguments = lua_gettop(interpreter); - size_t n; + size_t n = 0; 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); + if(lua_gettop(interpreter) != 2){ + fprintf(stderr, "Lua output function called with %d arguments, expected 2 (string, number)\n", lua_gettop(interpreter)); return 0; } - channel_name = lua_tostring(interpreter, 1); - val.normalised = clamp(lua_tonumber(interpreter, 2), 1.0, 0.0); - + //get instance pointer from registry lua_pushstring(interpreter, LUA_REGISTRY_KEY); lua_gettable(interpreter, LUA_REGISTRYINDEX); - inst = (instance *) lua_touserdata(interpreter, -1); + inst = (instance*) lua_touserdata(interpreter, -1); data = (lua_instance_data*) inst->impl; + //fetch function parameters + channel_name = lua_tostring(interpreter, 1); + val.normalised = clamp(luaL_checknumber(interpreter, 2), 1.0, 0.0); + + //find correct channel & output value for(n = 0; n < data->channels; n++){ if(!strcmp(channel_name, data->channel_name[n])){ channel = mm_channel(inst, n, 0); @@ -56,6 +116,7 @@ static int lua_callback_output(lua_State* interpreter){ return 0; } mm_channel_event(channel, val); + data->output[n] = val.normalised; return 0; } } @@ -64,6 +125,122 @@ static int lua_callback_output(lua_State* interpreter){ return 0; } +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; + + if(lua_gettop(interpreter) != 2){ + fprintf(stderr, "Lua output function called with %d arguments, expected 2 (string, number)\n", lua_gettop(interpreter)); + return 0; + } + + //get instance pointer from registry + lua_pushstring(interpreter, LUA_REGISTRY_KEY); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + inst = (instance*) lua_touserdata(interpreter, -1); + data = (lua_instance_data*) inst->impl; + + //fetch and round the interval + interval = luaL_checkinteger(interpreter, 2); + if(interval % 10 < 5){ + interval -= interval % 10; + } + else{ + interval += (10 - (interval % 10)); + } + + //push the function again + lua_pushvalue(interpreter, 1); + if(lua_gettable(interpreter, LUA_REGISTRYINDEX) == LUA_TNUMBER){ + //already interval'd + reference = luaL_checkinteger(interpreter, 4); + } + else if(interval){ + //get a reference to the function + lua_pushvalue(interpreter, 1); + reference = luaL_ref(interpreter, LUA_REGISTRYINDEX); + + //the function indexes the reference + lua_pushvalue(interpreter, 1); + lua_pushinteger(interpreter, reference); + lua_settable(interpreter, LUA_REGISTRYINDEX); + } + + //find matching timer + for(n = 0; n < timers; n++){ + if(timer[n].reference == reference && timer[n].interpreter == interpreter){ + break; + } + } + + if(n < timers){ + //set new interval + timer[n].interval = interval; + timer[n].delta = 0; + } + else if(interval){ + //append new timer + timer = realloc(timer, (timers + 1) * sizeof(lua_timer)); + if(!timer){ + fprintf(stderr, "Failed to allocate memory\n"); + timers = 0; + return 0; + } + timer[timers].interval = interval; + timer[timers].delta = 0; + timer[timers].interpreter = interpreter; + timer[timers].reference = reference; + timers++; + } + + //recalculate timerspec + lua_update_timerfd(); + return 0; +} + +static int lua_callback_value(lua_State* interpreter, uint8_t input){ + size_t n = 0; + instance* inst = NULL; + lua_instance_data* data = NULL; + const char* channel_name = NULL; + + if(lua_gettop(interpreter) != 1){ + fprintf(stderr, "Lua get_value function called with %d arguments, expected 1 (string)\n", lua_gettop(interpreter)); + return 0; + } + + //get instance pointer from registry + lua_pushstring(interpreter, LUA_REGISTRY_KEY); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + inst = (instance*) lua_touserdata(interpreter, -1); + data = (lua_instance_data*) inst->impl; + + //fetch argument + channel_name = lua_tostring(interpreter, 1); + + //find correct channel & return value + for(n = 0; n < data->channels; n++){ + if(!strcmp(channel_name, data->channel_name[n])){ + lua_pushnumber(data->interpreter, (input) ? data->input[n] : data->output[n]); + return 1; + } + } + + fprintf(stderr, "Tried to get unknown channel %s.%s\n", inst->name, channel_name); + return 0; +} + +static int lua_callback_input_value(lua_State* interpreter){ + return lua_callback_value(interpreter, 1); +} + +static int lua_callback_output_value(lua_State* interpreter){ + return lua_callback_value(interpreter, 0); +} + static int lua_configure(char* option, char* value){ fprintf(stderr, "The lua backend does not take any global configuration\n"); return 1; @@ -72,6 +249,7 @@ static int lua_configure(char* option, char* value){ static int lua_configure_instance(instance* inst, char* option, char* value){ lua_instance_data* data = (lua_instance_data*) inst->impl; + //load a lua file into the interpreter if(!strcmp(option, "script")){ 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)); @@ -105,13 +283,16 @@ static instance* lua_instance(){ } luaL_openlibs(data->interpreter); - //register lua api functions + //register lua interface functions lua_register(data->interpreter, "output", lua_callback_output); + lua_register(data->interpreter, "interval", lua_callback_interval); + lua_register(data->interpreter, "input_value", lua_callback_input_value); + lua_register(data->interpreter, "output_value", lua_callback_output_value); //store instance pointer to the lua state lua_pushstring(data->interpreter, LUA_REGISTRY_KEY); lua_pushlightuserdata(data->interpreter, (void *) inst); - lua_settable(data->interpreter, LUA_REGISTRYINDEX); + lua_settable(data->interpreter, LUA_REGISTRYINDEX); inst->impl = data; return inst; @@ -131,11 +312,16 @@ static channel* lua_channel(instance* inst, char* spec){ //allocate new channel if(u == data->channels){ data->channel_name = realloc(data->channel_name, (u + 1) * sizeof(char*)); - if(!data->channel_name){ + data->reference = realloc(data->reference, (u + 1) * sizeof(int)); + data->input = realloc(data->input, (u + 1) * sizeof(double)); + data->output = realloc(data->output, (u + 1) * sizeof(double)); + if(!data->channel_name || !data->reference || !data->input || !data->output){ fprintf(stderr, "Failed to allocate memory\n"); return NULL; } + data->reference[u] = LUA_NOREF; + data->input[u] = data->output[u] = 0.0; data->channel_name[u] = strdup(spec); if(!data->channel_name[u]){ fprintf(stderr, "Failed to allocate memory\n"); @@ -151,25 +337,91 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ size_t n = 0; lua_instance_data* data = (lua_instance_data*) inst->impl; + //handle all incoming events for(n = 0; n < num; n++){ - //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); + data->input[c[n]->ident] = v[n].normalised; + //call lua channel handlers if present + if(data->reference[n] != LUA_NOREF){ + lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->reference[c[n]->ident]); + lua_pushnumber(data->interpreter, v[n].normalised); + if(lua_pcall(data->interpreter, 1, 0, 0) != LUA_OK){ + fprintf(stderr, "Failed to call handler for %s.%s: %s\n", inst->name, data->channel_name[c[n]->ident], lua_tostring(data->interpreter, -1)); + lua_pop(data->interpreter, 1); + } } } return 0; } static int lua_handle(size_t num, managed_fd* fds){ - //TODO call timer callbacks + uint8_t read_buffer[100]; + uint64_t delta = timer_config.it_interval.tv_sec * 1000 + timer_config.it_interval.tv_nsec / 1e6; + size_t n; + + if(!num){ + return 0; + } + + //read the timer iteration to acknowledge the fd + if(read(timer_fd, read_buffer, sizeof(read_buffer)) < 0){ + fprintf(stderr, "Failed to read from Lua timer: %s\n", strerror(errno)); + return 1; + } + + //add delta to all active timers + for(n = 0; n < timers; n++){ + if(timer[n].interval){ + timer[n].delta += delta; + //call lua function if timer expired + if(timer[n].delta >= timer[n].interval){ + timer[n].delta %= timer[n].interval; + lua_rawgeti(timer[n].interpreter, LUA_REGISTRYINDEX, timer[n].reference); + lua_pcall(timer[n].interpreter, 0, 0, 0); + } + } + } return 0; } static int lua_start(){ - //TODO start timers / register fds + size_t n, u, p; + instance** inst = NULL; + lua_instance_data* data = NULL; + + //fetch all defined instances + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + //resolve channels to their handler functions + for(u = 0; u < n; u++){ + data = (lua_instance_data*) inst[u]->impl; + for(p = 0; p < data->channels; p++){ + //exclude reserved names + if(strcmp(data->channel_name[p], "output") + && strcmp(data->channel_name[p], "input_value") + && strcmp(data->channel_name[p], "output_value") + && strcmp(data->channel_name[p], "interval")){ + lua_getglobal(data->interpreter, data->channel_name[p]); + data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX); + if(data->reference[p] == LUA_REFNIL){ + data->reference[p] = LUA_NOREF; + } + } + } + } + + free(inst); + if(!n){ + return 0; + } + + //register the timer with the core + fprintf(stderr, "Lua backend registering 1 descriptor to core\n"); + if(mm_manage_fd(timer_fd, BACKEND_NAME, 1, NULL)){ + return 1; + } return 0; } @@ -178,6 +430,7 @@ static int lua_shutdown(){ instance** inst = NULL; lua_instance_data* data = NULL; + //fetch all instances if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ fprintf(stderr, "Failed to fetch instance list\n"); return 1; @@ -192,10 +445,19 @@ static int lua_shutdown(){ free(data->channel_name[p]); } free(data->channel_name); + free(data->reference); + free(data->input); + free(data->output); free(inst[u]->impl); } free(inst); + //free module-global data + free(timer); + timer = NULL; + timers = 0; + close(timer_fd); + timer_fd = -1; fprintf(stderr, "Lua backend shut down\n"); return 0; diff --git a/backends/lua.h b/backends/lua.h index 27e1afd..ccffef7 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -17,5 +17,15 @@ static int lua_shutdown(); typedef struct /*_lua_instance_data*/ { size_t channels; char** channel_name; + int* reference; + double* input; + double* output; lua_State* interpreter; } lua_instance_data; + +typedef struct /*_lua_interval_callback*/ { + uint64_t interval; + uint64_t delta; + lua_State* interpreter; + int reference; +} lua_timer; diff --git a/backends/lua.md b/backends/lua.md index 91e8fe2..970e8e2 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -5,15 +5,32 @@ 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)`. +To process incoming channel events, the MIDIMonster calls corresponding Lua functions (if they exist) +with the value (as a Lua `number` type) as parameter. + +The following functions are provided within the Lua interpreter for interaction with the MIDIMonster + +| Function | Usage example | Description | +|-------------------------------|-------------------------------|---------------------------------------| +| `output(string, number)` | `output("foo", 0.75)` | Output a value event to a channel | +| `interval(function, number)` | `interval(update, 100)` | Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms) | +| `input_value(string)` | `input_value("foo")` | Get the last input value on a channel | +| `output_value(string)` | `output_value("bar") | Get the last output value on a channel | + Example script: ``` function bar(value) output("foo", value / 2) end + +step = 0 +function toggle() + output("bar", step * 1.0) + step = (step + 1) % 2; +end + +interval(toggle, 1000) ``` Input values range between 0.0 and 1.0, output values are clamped to the same range. @@ -41,9 +58,12 @@ 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. +Using any of the interface functions (`output`, `interval`, `input\_value`, `output\_value`) as an +input channel name to a Lua instance will not call any handler functions. +Using these names as arguments to the output and value interface functions works as intended. + +Output values will not trigger corresponding input event handlers unless the channel is mapped +back in the MIDIMonster configuration. The path to the Lua source files is relative to the current working directory. This may lead to problems when copying configuration between installations. -- cgit v1.2.3 From c517b26ac559356650334a2d32d1b6249e66289d Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 13 Jul 2019 17:56:23 +0200 Subject: Fix indexing bug --- backends/lua.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index 08d8d3c..7f0c2de 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -341,7 +341,7 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ for(n = 0; n < num; n++){ data->input[c[n]->ident] = v[n].normalised; //call lua channel handlers if present - if(data->reference[n] != LUA_NOREF){ + if(data->reference[c[n]->ident] != LUA_NOREF){ lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->reference[c[n]->ident]); lua_pushnumber(data->interpreter, v[n].normalised); if(lua_pcall(data->interpreter, 1, 0, 0) != LUA_OK){ -- cgit v1.2.3 From f19d6e66b23ba719f474171b10e1ee294fb38d55 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 13 Jul 2019 18:13:36 +0200 Subject: Fix & simplify timer configuration --- backends/lua.c | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index 7f0c2de..8069f8f 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -9,9 +9,7 @@ static size_t timers = 0; static lua_timer* timer = NULL; -static struct itimerspec timer_config = { - 0 -}; +uint64_t timer_interval = 0; static int timer_fd = -1; int init(){ @@ -43,8 +41,11 @@ int init(){ } static int lua_update_timerfd(){ - uint64_t interval = 0, gcd, residual; + uint64_t interval, gcd, residual; size_t n = 0; + struct itimerspec timer_config = { + 0 + }; //find the minimum for the lower interval bounds for(n = 0; n < timers; n++){ @@ -53,12 +54,8 @@ static int lua_update_timerfd(){ } } - //stop the timer - if(!interval){ - memset(&timer_config, 0, sizeof(struct itimerspec)); - } - //calculate gcd of all timers - else{ + //calculate gcd of all timers if any are active + if(interval){ for(n = 0; n < timers; n++){ if(timer[n].interval){ //calculate gcd of current interval and this timers interval @@ -75,13 +72,17 @@ static int lua_update_timerfd(){ } } - timer_config.it_interval.tv_sec = interval / 1000; - timer_config.it_interval.tv_nsec = (interval % 1000) * 1e6; - timer_config.it_value.tv_nsec = 1; + timer_config.it_interval.tv_sec = timer_config.it_value.tv_sec = interval / 1000; + timer_config.it_interval.tv_nsec = timer_config.it_value.tv_nsec = (interval % 1000) * 1e6; + } + + if(interval == timer_interval){ + return 0; } //configure the new interval timerfd_settime(timer_fd, 0, &timer_config, NULL); + timer_interval = interval; return 0; } @@ -355,7 +356,7 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ static int lua_handle(size_t num, managed_fd* fds){ uint8_t read_buffer[100]; - uint64_t delta = timer_config.it_interval.tv_sec * 1000 + timer_config.it_interval.tv_nsec / 1e6; + uint64_t delta = timer_interval; size_t n; if(!num){ -- cgit v1.2.3 From e776e02531aca45f424f7139e5d7304ba3096b45 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 14 Jul 2019 16:49:31 +0200 Subject: Work around missing timerfd on OSX/Windows --- backends/lua.c | 40 ++++++++++++++++++++++++++++++++++++++++ backends/lua.h | 6 ++++++ backends/lua.md | 4 ++-- 3 files changed, 48 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index 8069f8f..7fcd0ab 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -10,10 +10,17 @@ static size_t timers = 0; static lua_timer* timer = NULL; uint64_t timer_interval = 0; +#ifdef MMBACKEND_LUA_TIMERFD static int timer_fd = -1; +#else +static uint64_t last_timestamp; +#endif int init(){ backend lua = { + #ifndef MMBACKEND_LUA_TIMERFD + .interval = lua_interval, + #endif .name = BACKEND_NAME, .conf = lua_configure, .create = lua_instance, @@ -31,21 +38,33 @@ int init(){ return 1; } + #ifdef MMBACKEND_LUA_TIMERFD //create the timer to expire intervals timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK); if(timer_fd < 0){ fprintf(stderr, "Failed to create timer for Lua backend\n"); return 1; } + #endif return 0; } +static uint32_t lua_interval(){ + //FIXME Return delta for next timer here + if(timer_interval){ + return timer_interval; + } + return 1000; +} + static int lua_update_timerfd(){ uint64_t interval, gcd, residual; size_t n = 0; + #ifdef MMBACKEND_LUA_TIMERFD struct itimerspec timer_config = { 0 }; + #endif //find the minimum for the lower interval bounds for(n = 0; n < timers; n++){ @@ -72,16 +91,20 @@ static int lua_update_timerfd(){ } } + #ifdef MMBACKEND_LUA_TIMERFD timer_config.it_interval.tv_sec = timer_config.it_value.tv_sec = interval / 1000; timer_config.it_interval.tv_nsec = timer_config.it_value.tv_nsec = (interval % 1000) * 1e6; + #endif } if(interval == timer_interval){ return 0; } + #ifdef MMBACKEND_LUA_TIMERFD //configure the new interval timerfd_settime(timer_fd, 0, &timer_config, NULL); + #endif timer_interval = interval; return 0; } @@ -359,6 +382,7 @@ static int lua_handle(size_t num, managed_fd* fds){ uint64_t delta = timer_interval; size_t n; + #ifdef MMBACKEND_LUA_TIMERFD if(!num){ return 0; } @@ -368,6 +392,18 @@ static int lua_handle(size_t num, managed_fd* fds){ fprintf(stderr, "Failed to read from Lua timer: %s\n", strerror(errno)); return 1; } + #else + if(!last_timestamp){ + last_timestamp = mm_timestamp(); + } + delta = mm_timestamp() - last_timestamp; + last_timestamp = mm_timestamp(); + #endif + + //no timers active + if(!timer_interval){ + return 0; + } //add delta to all active timers for(n = 0; n < timers; n++){ @@ -418,11 +454,13 @@ static int lua_start(){ return 0; } + #ifdef MMBACKEND_LUA_TIMERFD //register the timer with the core fprintf(stderr, "Lua backend registering 1 descriptor to core\n"); if(mm_manage_fd(timer_fd, BACKEND_NAME, 1, NULL)){ return 1; } + #endif return 0; } @@ -457,8 +495,10 @@ static int lua_shutdown(){ free(timer); timer = NULL; timers = 0; + #ifdef MMBACKEND_LUA_TIMERFD close(timer_fd); timer_fd = -1; + #endif fprintf(stderr, "Lua backend shut down\n"); return 0; diff --git a/backends/lua.h b/backends/lua.h index ccffef7..7aad891 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -4,6 +4,11 @@ #include #include +//OSX and Windows don't have the cool new toys... +#ifdef __linux__ + #define MMBACKEND_LUA_TIMERFD +#endif + int init(); static int lua_configure(char* option, char* value); static int lua_configure_instance(instance* inst, char* option, char* value); @@ -13,6 +18,7 @@ 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(); +static uint32_t lua_interval(); typedef struct /*_lua_instance_data*/ { size_t channels; diff --git a/backends/lua.md b/backends/lua.md index 970e8e2..e273b28 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -15,7 +15,7 @@ The following functions are provided within the Lua interpreter for interaction | `output(string, number)` | `output("foo", 0.75)` | Output a value event to a channel | | `interval(function, number)` | `interval(update, 100)` | Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms) | | `input_value(string)` | `input_value("foo")` | Get the last input value on a channel | -| `output_value(string)` | `output_value("bar") | Get the last output value on a channel | +| `output_value(string)` | `output_value("bar")` | Get the last output value on a channel | Example script: @@ -58,7 +58,7 @@ lua1.foo > lua2.bar #### Known bugs / problems -Using any of the interface functions (`output`, `interval`, `input\_value`, `output\_value`) as an +Using any of the interface functions (`output`, `interval`, `input_value`, `output_value`) as an input channel name to a Lua instance will not call any handler functions. Using these names as arguments to the output and value interface functions works as intended. -- cgit v1.2.3 From f4f6f8c84a685b83f2406f8efb148b6699be14e9 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 14 Jul 2019 16:51:40 +0200 Subject: Conditionally include timerfd headers --- backends/lua.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index 7fcd0ab..43a6fcc 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -1,7 +1,10 @@ #include -#include #include #include +#ifdef MMBACKEND_LUA_TIMERFD +#include +#endif + #include "lua.h" #define BACKEND_NAME "lua" -- cgit v1.2.3 From 99f54f6d15d9e286fb0b47cf21b32318767d3c2a Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 14 Jul 2019 16:55:19 +0200 Subject: Fix header ordering --- backends/lua.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index 43a6fcc..8e221d1 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -1,3 +1,5 @@ +#include "lua.h" + #include #include #include @@ -5,8 +7,6 @@ #include #endif -#include "lua.h" - #define BACKEND_NAME "lua" #define LUA_REGISTRY_KEY "_midimonster_lua_instance" -- cgit v1.2.3 From 59857ead2d439d450afc6f5144c9c08e8d0c8a5c Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 24 Jul 2019 21:12:01 +0200 Subject: Fix issues found by Coverity Scan --- backends/lua.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index 8e221d1..4d946cd 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -61,7 +61,7 @@ static uint32_t lua_interval(){ } static int lua_update_timerfd(){ - uint64_t interval, gcd, residual; + uint64_t interval = 0, gcd, residual; size_t n = 0; #ifdef MMBACKEND_LUA_TIMERFD struct itimerspec timer_config = { @@ -396,11 +396,11 @@ static int lua_handle(size_t num, managed_fd* fds){ return 1; } #else - if(!last_timestamp){ - last_timestamp = mm_timestamp(); - } - delta = mm_timestamp() - last_timestamp; + if(!last_timestamp){ last_timestamp = mm_timestamp(); + } + delta = mm_timestamp() - last_timestamp; + last_timestamp = mm_timestamp(); #endif //no timers active -- cgit v1.2.3 From c75721e77ecada3c88f4b493c1e3036c151bfe88 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 24 Jul 2019 21:33:23 +0200 Subject: Clarify backend documentation --- backends/evdev.md | 4 +++- backends/lua.c | 4 ++-- backends/lua.md | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) (limited to 'backends') diff --git a/backends/evdev.md b/backends/evdev.md index dfe5ec9..d750f1e 100644 --- a/backends/evdev.md +++ b/backends/evdev.md @@ -30,6 +30,8 @@ instances. The configuration value contains, space-separated, the following valu * `flat`: An offset, below which all deviations will be ignored * `resolution`: Axis resolution in units per millimeter (or units per radian for rotational axes) +If an axis is not used for output, this configuration can be omitted. + For real devices, all of these parameters for every axis can be found by running `evtest` on the device. #### Channel specification @@ -69,4 +71,4 @@ than `0`, respectively. As for output, only the values `-1`, `0` and `1` are gen `EV_KEY` key-down events are sent for normalized channel values over `0.9`. Extended event type values such as `EV_LED`, `EV_SND`, etc are recognized in the MIDIMonster configuration file -but may or may not work with the internal channel mapping and normalization code. \ No newline at end of file +but may or may not work with the internal channel mapping and normalization code. diff --git a/backends/lua.c b/backends/lua.c index 4d946cd..61e4e08 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -277,7 +277,7 @@ static int lua_configure_instance(instance* inst, char* option, char* value){ lua_instance_data* data = (lua_instance_data*) inst->impl; //load a lua file into the interpreter - if(!strcmp(option, "script")){ + if(!strcmp(option, "script") || !strcmp(option, "source")){ if(luaL_dofile(data->interpreter, value)){ fprintf(stderr, "Failed to load lua source file %s for instance %s: %s\n", value, inst->name, lua_tostring(data->interpreter, -1)); return 1; @@ -285,7 +285,7 @@ static int lua_configure_instance(instance* inst, char* option, char* value){ return 0; } - fprintf(stderr, "Unknown configuration parameter %s for lua backend\n", option); + fprintf(stderr, "Unknown configuration parameter %s for lua instance %s\n", option, inst->name); return 1; } diff --git a/backends/lua.md b/backends/lua.md index e273b28..1c67477 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -43,7 +43,7 @@ The backend does not take any global configuration. | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| -| `source` | `script.lua` | none | Lua source file | +| `script` | `script.lua` | none | Lua source file | A single instance may have multiple `source` options specified, which will all be read cumulatively. -- cgit v1.2.3 From 27eba094a067a0879250a3cf73c83df0da5c63a1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 24 Jul 2019 21:35:10 +0200 Subject: Add detect option to OSC backend --- backends/osc.c | 31 +++++++++++++++++++++++++------ backends/osc.md | 6 ++++-- 2 files changed, 29 insertions(+), 8 deletions(-) (limited to 'backends') diff --git a/backends/osc.c b/backends/osc.c index 1305169..36b0993 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -13,6 +13,12 @@ #define osc_align(a) ((((a) / 4) + (((a) % 4) ? 1 : 0)) * 4) #define BACKEND_NAME "osc" +static struct { + uint8_t detect; +} osc_global_config = { + .detect = 0 +}; + int init(){ backend osc = { .name = BACKEND_NAME, @@ -248,7 +254,15 @@ static int osc_validate_path(char* path){ } static int osc_configure(char* option, char* value){ - fprintf(stderr, "The OSC backend does not take any global configuration\n"); + if(!strcmp(option, "detect")){ + osc_global_config.detect = 1; + if(!strcmp(value, "off")){ + osc_global_config.detect = 0; + } + return 0; + } + + fprintf(stderr, "Unknown configuration parameter %s for OSC backend\n", option); return 1; } @@ -384,7 +398,7 @@ static int osc_configure_instance(instance* inst, char* option, char* value){ return 0; } - fprintf(stderr, "Unknown configuration parameter %s for OSC backend\n", option); + fprintf(stderr, "Unknown configuration parameter %s for OSC instance %s\n", option, inst->name); return 1; } @@ -585,16 +599,17 @@ static int osc_handle(size_t num, managed_fd* fds){ else{ bytes_read = recv(fds[fd].fd, recv_buf, sizeof(recv_buf), 0); } + + if(bytes_read < 0){ + break; + } + if(data->root && strncmp(recv_buf, data->root, min(bytes_read, strlen(data->root)))){ //ignore packet for different root continue; } osc_local = recv_buf + (data->root ? strlen(data->root) : 0); - if(bytes_read < 0){ - break; - } - osc_fmt = recv_buf + osc_align(strlen(recv_buf) + 1); if(*osc_fmt != ','){ //invalid format string @@ -603,6 +618,10 @@ static int osc_handle(size_t num, managed_fd* fds){ } osc_fmt++; + if(osc_global_config.detect){ + 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 diff --git a/backends/osc.md b/backends/osc.md index c784cda..e9aa4d5 100644 --- a/backends/osc.md +++ b/backends/osc.md @@ -5,7 +5,9 @@ spoken primarily by visual interface tools and hardware such as TouchOSC. #### Global configuration -This backend does not take any global configuration. +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `detect` | `on` | `off` | Output the path of all incoming OSC packets to allow for easier configuration. Any path filters configured using the `root` instance configuration options still apply. | #### Instance configuration @@ -78,4 +80,4 @@ The default ranges are: #### Known bugs / problems -Ping requests are not yet answered. There may be some problems using broadcast output and input. \ No newline at end of file +Ping requests are not yet answered. There may be some problems using broadcast output and input. -- 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 --- backends/Makefile | 4 +- backends/osc.c | 635 +++++++++++++++++++++++++++++++++++++----------------- backends/osc.h | 25 ++- backends/osc.md | 21 +- 4 files changed, 481 insertions(+), 204 deletions(-) (limited to 'backends') 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. -- cgit v1.2.3 From f30cf8fc7d7f7c02f7fae4fa113ef32fc790c31a Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 27 Jul 2019 19:44:52 +0200 Subject: Fix uninitialized value --- backends/osc.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/osc.c b/backends/osc.c index 3f19abf..18c8bad 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -499,7 +499,7 @@ static int osc_register_pattern(osc_instance_data* data, char* pattern_path, cha 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; + char* host = NULL, *port = NULL; if(!strcmp(option, "root")){ if(osc_path_validate(value, 0)){ @@ -708,7 +708,7 @@ static int osc_output_channel(instance* inst, size_t channel){ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ size_t evt = 0, mark = 0; - int rv; + int rv = 0; osc_channel_ident ident = { .label = 0 }; -- cgit v1.2.3 From aef72140273b3d98a4d86a42f0f3bcce6d5899ca Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 28 Jul 2019 23:19:54 +0200 Subject: Fix Lua timing on non-linux --- backends/lua.c | 11 +++++++++-- backends/osc.md | 3 +++ 2 files changed, 12 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index 61e4e08..4a910a2 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -53,9 +53,16 @@ int init(){ } static uint32_t lua_interval(){ - //FIXME Return delta for next timer here + size_t n = 0; + uint64_t next_timer = 1000; + if(timer_interval){ - return timer_interval; + for(n = 0; n < timers; n++){ + if(timer[n].interval && timer[n].interval - timer[n].delta < next_timer){ + next_timer = timer[n].interval - timer[n].delta; + } + } + return next_timer; } return 1000; } diff --git a/backends/osc.md b/backends/osc.md index b7ce527..1446e06 100644 --- a/backends/osc.md +++ b/backends/osc.md @@ -58,6 +58,9 @@ To configure a range of faders, an expression similar to the following line coul /1/fader* = f 0.0 1.0 ``` +When matching channels against the patterns to use, the first matching pattern (in the order in which they have been configured) will be used +as configuration for that channel. + #### Channel specification A channel may be any valid OSC path, to which the instance root will be prepended if -- cgit v1.2.3 From 4e604b493e0dc855b6ea6978e5cf8e2de5d2b8d5 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 28 Jul 2019 23:40:40 +0200 Subject: Fix evdev relative axes, add detect option --- backends/evdev.c | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++----- backends/evdev.h | 8 ++++++++ backends/evdev.md | 18 ++++++++++++------ 3 files changed, 72 insertions(+), 11 deletions(-) (limited to 'backends') diff --git a/backends/evdev.c b/backends/evdev.c index ca8469a..565eb17 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -27,6 +27,12 @@ typedef union { uint64_t label; } evdev_channel_ident; +static struct { + uint8_t detect; +} evdev_config = { + .detect = 0 +}; + int init(){ backend evdev = { .name = BACKEND_NAME, @@ -49,7 +55,15 @@ int init(){ } static int evdev_configure(char* option, char* value) { - fprintf(stderr, "The evdev backend does not take any global configuration\n"); + if(!strcmp(option, "detect")){ + evdev_config.detect = 1; + if(!strcmp(value, "off")){ + evdev_config.detect = 0; + } + return 0; + } + + fprintf(stderr, "Unknown configuration option %s for evdev backend\n", option); return 1; } @@ -185,6 +199,22 @@ static int evdev_configure_instance(instance* inst, char* option, char* value) { data->exclusive = 1; return 0; } + else if(!strncmp(option, "relaxis.", 8)){ + data->relative_axis = realloc(data->relative_axis, (data->relative_axes + 1) * sizeof(evdev_relaxis_config)); + if(!data->relative_axis){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + data->relative_axis[data->relative_axes].code = libevdev_event_code_from_name(EV_REL, option + 8); + data->relative_axis[data->relative_axes].max = strtoul(value, &next_token, 0); + data->relative_axis[data->relative_axes].current = strtoul(next_token, NULL, 0); + if(data->relative_axis[data->relative_axes].code < 0){ + fprintf(stderr, "Failed to configure relative axis extents for %s.%s\n", inst->name, option + 8); + return 1; + } + data->relative_axes++; + return 0; + } #ifndef EVDEV_NO_UINPUT else if(!strcmp(option, "output")){ data->output_enabled = 1; @@ -214,7 +244,7 @@ static int evdev_configure_instance(instance* inst, char* option, char* value) { return 0; } #endif - fprintf(stderr, "Unknown configuration parameter %s for evdev backend\n", option); + fprintf(stderr, "Unknown instance configuration parameter %s for evdev instance %s\n", option, inst->name); return 1; } @@ -275,21 +305,32 @@ static int evdev_push_event(instance* inst, evdev_instance_data* data, struct in .fields.code = event.code }; channel* chan = mm_channel(inst, ident.label, 0); + size_t axis; if(chan){ val.raw.u64 = event.value; switch(event.type){ case EV_REL: - val.normalised = 0.5 + ((event.value < 0) ? 0.5 : -0.5); + for(axis = 0; axis < data->relative_axes; axis++){ + if(data->relative_axis[axis].code == event.code){ + data->relative_axis[axis].current = clamp(data->relative_axis[axis].current + event.value, data->relative_axis[axis].max, 0); + val.normalised = (double) data->relative_axis[axis].current / (double) data->relative_axis[axis].max; + break; + } + } + if(axis == data->relative_axes){ + val.normalised = 0.5 + ((event.value < 0) ? 0.5 : -0.5); + break; + } break; case EV_ABS: range = libevdev_get_abs_maximum(data->input_ev, event.code) - libevdev_get_abs_minimum(data->input_ev, event.code); - val.normalised = (event.value - libevdev_get_abs_minimum(data->input_ev, event.code)) / (double) range; + val.normalised = clamp((event.value - libevdev_get_abs_minimum(data->input_ev, event.code)) / (double) range, 1.0, 0.0); break; case EV_KEY: case EV_SW: default: - val.normalised = 1.0 * event.value; + val.normalised = clamp(1.0 * event.value, 1.0, 0.0); break; } @@ -299,6 +340,10 @@ static int evdev_push_event(instance* inst, evdev_instance_data* data, struct in } } + if(evdev_config.detect){ + fprintf(stderr, "Incoming evdev data for channel %s.%s.%s\n", inst->name, libevdev_event_type_get_name(event.type), libevdev_event_code_get_name(event.type, event.code)); + } + return 0; } @@ -470,6 +515,8 @@ static int evdev_shutdown(){ libevdev_free(data->output_proto); #endif + data->relative_axes = 0; + free(data->relative_axis); free(data); } diff --git a/backends/evdev.h b/backends/evdev.h index c6e3a25..d719631 100644 --- a/backends/evdev.h +++ b/backends/evdev.h @@ -24,10 +24,18 @@ static int evdev_shutdown(); #define UINPUT_MAX_NAME_SIZE 512 #endif +typedef struct /*_evdev_relative_axis_config*/ { + int code; + int64_t max; + int64_t current; +} evdev_relaxis_config; + typedef struct /*_evdev_instance_model*/ { int input_fd; struct libevdev* input_ev; int exclusive; + size_t relative_axes; + evdev_relaxis_config* relative_axis; int output_enabled; #ifndef EVDEV_NO_UINPUT diff --git a/backends/evdev.md b/backends/evdev.md index d750f1e..995b44c 100644 --- a/backends/evdev.md +++ b/backends/evdev.md @@ -7,7 +7,9 @@ This functionality may require elevated privileges (such as special group member #### Global configuration -This backend does not take any global configuration. +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `detect` | `on` | `off` | Output the channel specification for all events coming in on configured instances to help with configuration. | #### Instance configuration @@ -18,7 +20,8 @@ This backend does not take any global configuration. | `output` | `My Input Device` | none | Output device presentation name. Setting this option enables the instance for output | | `exclusive` | `1` | `0` | Prevent other processes from using the device | | `id` | `0x1 0x2 0x3` | none | Set output device bus identification (Vendor, Product and Version), optional | -| `axis.AXISNAME`| `34300 0 65536 255 4095` | none | Specify absolute axis details (see below) for output. This is required for any absolute axis to be output. +| `axis.AXISNAME`| `34300 0 65536 255 4095` | none | Specify absolute axis details (see below) for output. This is required for any absolute axis to be output. | +| `relaxis.AXISNAME`| `65534 32767` | none | Specify relative axis details (extent and optional initial value) for output and input (see below). | The absolute axis details configuration (e.g. `axis.ABS_X`) is required for any absolute axis on output-enabled instances. The configuration value contains, space-separated, the following values: @@ -34,6 +37,13 @@ If an axis is not used for output, this configuration can be omitted. For real devices, all of these parameters for every axis can be found by running `evtest` on the device. +To use the input from relative axes in absolute-value based protocols, the backend needs a reference frame to +convert the relative movements to absolute values. + +If relative axes are used without specifying their extents, the channel will generate normalized values +of `0`, `0.5` and `1` for any input less than, equal to and greater than `0`, respectively. As for output, only +the values `-1`, `0` and `1` are generated for the same interval. + #### Channel specification A channel is specified by its event type and event code, separated by `.`. For a complete list of event types and codes @@ -64,10 +74,6 @@ Input devices may synchronize logically connected event types (for example, X an events. The MIDIMonster also generates these events after processing channel events, but may not keep the original event grouping. -Relative axes (`EV_REL`-type events), such as generated by mouses, are currently handled in a very basic fashion, -generating only the normalized channel values of `0`, `0.5` and `1` for any input less than, equal to and greater -than `0`, respectively. As for output, only the values `-1`, `0` and `1` are generated for the same interval. - `EV_KEY` key-down events are sent for normalized channel values over `0.9`. Extended event type values such as `EV_LED`, `EV_SND`, etc are recognized in the MIDIMonster configuration file -- cgit v1.2.3 From a23ce7718ab61e73586d2738329d6bbace0b764b Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 1 Aug 2019 20:54:17 +0200 Subject: Implement evdev relative axis inversion --- backends/evdev.c | 15 ++++++++++++++- backends/evdev.h | 1 + backends/evdev.md | 8 +++++++- 3 files changed, 22 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/evdev.c b/backends/evdev.c index 565eb17..7a7913d 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -205,8 +205,13 @@ static int evdev_configure_instance(instance* inst, char* option, char* value) { fprintf(stderr, "Failed to allocate memory\n"); return 1; } + data->relative_axis[data->relative_axes].inverted = 0; data->relative_axis[data->relative_axes].code = libevdev_event_code_from_name(EV_REL, option + 8); - data->relative_axis[data->relative_axes].max = strtoul(value, &next_token, 0); + data->relative_axis[data->relative_axes].max = strtoll(value, &next_token, 0); + if(data->relative_axis[data->relative_axes].max < 0){ + data->relative_axis[data->relative_axes].max *= -1; + data->relative_axis[data->relative_axes].inverted = 1; + } data->relative_axis[data->relative_axes].current = strtoul(next_token, NULL, 0); if(data->relative_axis[data->relative_axes].code < 0){ fprintf(stderr, "Failed to configure relative axis extents for %s.%s\n", inst->name, option + 8); @@ -313,6 +318,9 @@ static int evdev_push_event(instance* inst, evdev_instance_data* data, struct in case EV_REL: for(axis = 0; axis < data->relative_axes; axis++){ if(data->relative_axis[axis].code == event.code){ + if(data->relative_axis[axis].inverted){ + event.value *= -1; + } data->relative_axis[axis].current = clamp(data->relative_axis[axis].current + event.value, data->relative_axis[axis].max, 0); val.normalised = (double) data->relative_axis[axis].current / (double) data->relative_axis[axis].max; break; @@ -374,6 +382,11 @@ static int evdev_handle(size_t num, managed_fd* fds){ read_flags = LIBEVDEV_READ_FLAG_SYNC; } + //exclude synchronization events + if(ev.type == EV_SYN){ + continue; + } + //handle event if(evdev_push_event(inst, data, ev)){ return 1; diff --git a/backends/evdev.h b/backends/evdev.h index d719631..f89e362 100644 --- a/backends/evdev.h +++ b/backends/evdev.h @@ -25,6 +25,7 @@ static int evdev_shutdown(); #endif typedef struct /*_evdev_relative_axis_config*/ { + uint8_t inverted; int code; int64_t max; int64_t current; diff --git a/backends/evdev.md b/backends/evdev.md index 995b44c..88dfc85 100644 --- a/backends/evdev.md +++ b/backends/evdev.md @@ -38,12 +38,18 @@ If an axis is not used for output, this configuration can be omitted. For real devices, all of these parameters for every axis can be found by running `evtest` on the device. To use the input from relative axes in absolute-value based protocols, the backend needs a reference frame to -convert the relative movements to absolute values. +convert the relative movements to absolute values. To invert the mapping of the relative axis, specify the `max` value +as a negative number, for example: + +``` +relaxis.REL_X = -1024 512 +``` If relative axes are used without specifying their extents, the channel will generate normalized values of `0`, `0.5` and `1` for any input less than, equal to and greater than `0`, respectively. As for output, only the values `-1`, `0` and `1` are generated for the same interval. + #### Channel specification A channel is specified by its event type and event code, separated by `.`. For a complete list of event types and codes -- cgit v1.2.3 From bbeade8898200a8024169ece30c620016fd5eaf1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 1 Aug 2019 20:58:21 +0200 Subject: Implement midi backend detect option --- backends/evdev.md | 2 +- backends/midi.c | 41 +++++++++++++++++++++++++++++++++++------ backends/midi.md | 1 + 3 files changed, 37 insertions(+), 7 deletions(-) (limited to 'backends') diff --git a/backends/evdev.md b/backends/evdev.md index 88dfc85..d57201d 100644 --- a/backends/evdev.md +++ b/backends/evdev.md @@ -9,7 +9,7 @@ This functionality may require elevated privileges (such as special group member | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| -| `detect` | `on` | `off` | Output the channel specification for all events coming in on configured instances to help with configuration. | +| `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration. | #### Instance configuration diff --git a/backends/midi.c b/backends/midi.c index 2999e6b..c1480c0 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -31,6 +31,12 @@ enum /*_midi_channel_type*/ { sysmsg }; +static struct { + uint8_t detect; +} midi_config = { + .detect = 0 +}; + int init(){ backend midi = { .name = BACKEND_NAME, @@ -55,8 +61,8 @@ int init(){ return 1; } - snd_seq_nonblock(sequencer, 1); - + snd_seq_nonblock(sequencer, 1); + fprintf(stderr, "MIDI client ID is %d\n", snd_seq_client_id(sequencer)); return 0; } @@ -70,6 +76,14 @@ static int midi_configure(char* option, char* value){ return 0; } + if(!strcmp(option, "detect")){ + midi_config.detect = 1; + if(!strcmp(value, "off")){ + midi_config.detect = 0; + } + return 0; + } + fprintf(stderr, "Unknown MIDI backend option %s\n", option); return 1; } @@ -214,7 +228,7 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){ snd_seq_ev_set_source(&ev, data->port); snd_seq_ev_set_subs(&ev); snd_seq_ev_set_direct(&ev); - + switch(ident.fields.type){ case note: snd_seq_ev_set_noteon(&ev, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0); @@ -224,7 +238,6 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){ break; case pressure: snd_seq_ev_set_keypress(&ev, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0); - break; case pitchbend: snd_seq_ev_set_pitchbend(&ev, ident.fields.channel, (v[u].normalised * 16383.0) - 8192); @@ -249,6 +262,7 @@ static int midi_handle(size_t num, managed_fd* fds){ instance* inst = NULL; channel* changed = NULL; channel_value val; + char* event_type = NULL; midi_channel_ident ident = { .label = 0 }; @@ -258,6 +272,7 @@ static int midi_handle(size_t num, managed_fd* fds){ } while(snd_seq_event_input(sequencer, &ev) > 0){ + event_type = NULL; ident.label = 0; switch(ev->type){ case SND_SEQ_EVENT_NOTEON: @@ -267,22 +282,26 @@ static int midi_handle(size_t num, managed_fd* fds){ ident.fields.channel = ev->data.note.channel; ident.fields.control = ev->data.note.note; val.normalised = (double)ev->data.note.velocity / 127.0; + event_type = "note"; break; case SND_SEQ_EVENT_KEYPRESS: ident.fields.type = pressure; ident.fields.channel = ev->data.note.channel; ident.fields.control = ev->data.note.note; val.normalised = (double)ev->data.note.velocity / 127.0; + event_type = "pressure"; break; case SND_SEQ_EVENT_CHANPRESS: ident.fields.type = aftertouch; ident.fields.channel = ev->data.control.channel; val.normalised = (double)ev->data.control.value / 127.0; + event_type = "aftertouch"; break; case SND_SEQ_EVENT_PITCHBEND: ident.fields.type = pitchbend; ident.fields.channel = ev->data.control.channel; val.normalised = ((double)ev->data.control.value + 8192) / 16383.0; + event_type = "pitch"; break; case SND_SEQ_EVENT_CONTROLLER: ident.fields.type = cc; @@ -290,6 +309,7 @@ static int midi_handle(size_t num, managed_fd* fds){ ident.fields.control = ev->data.control.param; val.raw.u64 = ev->data.control.value; val.normalised = (double)ev->data.control.value / 127.0; + event_type = "cc"; break; case SND_SEQ_EVENT_CONTROL14: case SND_SEQ_EVENT_NONREGPARAM: @@ -318,6 +338,15 @@ static int midi_handle(size_t num, managed_fd* fds){ return 1; } } + + if(midi_config.detect && event_type){ + if(ident.fields.type == pitchbend || ident.fields.type == aftertouch){ + fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s\n", inst->name, ident.fields.channel, event_type); + } + else{ + fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s%d\n", inst->name, ident.fields.channel, event_type, ident.fields.control); + } + } } free(ev); return 0; @@ -375,13 +404,13 @@ static int midi_start(){ } //register all fds to core - nfds = snd_seq_poll_descriptors_count(sequencer, POLLIN | POLLOUT); + nfds = snd_seq_poll_descriptors_count(sequencer, POLLIN | POLLOUT); pfds = calloc(nfds, sizeof(struct pollfd)); if(!pfds){ fprintf(stderr, "Failed to allocate memory\n"); goto bail; } - nfds = snd_seq_poll_descriptors(sequencer, pfds, nfds, POLLIN | POLLOUT); + nfds = snd_seq_poll_descriptors(sequencer, pfds, nfds, POLLIN | POLLOUT); fprintf(stderr, "MIDI backend registering %d descriptors to core\n", nfds); for(p = 0; p < nfds; p++){ diff --git a/backends/midi.md b/backends/midi.md index 9733cc2..5e295ee 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -7,6 +7,7 @@ The MIDI backend provides read-write access to the MIDI protocol via virtual por | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| | `name` | `MIDIMonster` | none | MIDI client name | +| `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration. | #### Instance configuration -- 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 --- 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 - 10 files changed, 82 insertions(+), 33 deletions(-) (limited to 'backends') 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(); -- 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 --- backends/Makefile | 10 +++++++--- backends/midi.c | 6 ------ 2 files changed, 7 insertions(+), 9 deletions(-) (limited to 'backends') 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, -- 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') 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 cf93d280af47aea1bf8bdafa30eabb2c2de005b8 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 10 Aug 2019 15:30:48 +0200 Subject: Implement stream client connections in libmmbackend (Fixes #19) --- backends/artnet.c | 2 +- backends/libmmbackend.c | 62 +++++++++++++++++++++++++++++++++++-------------- backends/libmmbackend.h | 22 ++++++++++++++---- backends/lua.md | 2 +- backends/osc.c | 2 +- backends/sacn.c | 2 +- 6 files changed, 67 insertions(+), 25 deletions(-) (limited to 'backends') diff --git a/backends/artnet.c b/backends/artnet.c index a6df4ab..7f3f08c 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -19,7 +19,7 @@ static int artnet_listener(char* host, char* port){ return -1; } - fd = mmbackend_socket(host, port, SOCK_DGRAM, 1); + fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); if(fd < 0){ return -1; } diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index b27ebc5..2fd3b8b 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -52,7 +52,7 @@ int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* ad return 0; } -int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ +int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast){ int fd = -1, status, yes = 1; struct addrinfo hints = { .ai_family = AF_UNSPEC, @@ -80,20 +80,31 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ 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"); - } + if(mcast){ + yes = 1; + if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ + fprintf(stderr, "Failed to enable SO_BROADCAST on socket\n"); + } - yes = 0; - if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to disable IP_MULTICAST_LOOP on socket: %s\n", strerror(errno)); + 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; + if(listener){ + status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); + if(status < 0){ + close(fd); + continue; + } + } + else{ + status = connect(fd, addr_it->ai_addr, addr_it->ai_addrlen); + if(status < 0){ + close(fd); + continue; + } } break; @@ -107,11 +118,11 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ //set nonblocking #ifdef _WIN32 - u_long mode = 1; - if(ioctlsocket(fd, FIONBIO, &mode) != NO_ERROR){ - closesocket(fd); - return 1; - } + 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){ @@ -123,3 +134,20 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ return fd; } + +int mmbackend_send(int fd, uint8_t* data, size_t length){ + ssize_t total = 0, sent; + while(total < length){ + sent = send(fd, data + total, length - total, 0); + if(sent < 0){ + fprintf(stderr, "Failed to send: %s\n", strerror(errno)); + return 1; + } + total += sent; + } + return 0; +} + +int mmbackend_send_str(int fd, char* data){ + return mmbackend_send(fd, (uint8_t*) data, strlen(data)); +} diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index 77cad6a..31c4b96 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -16,7 +16,8 @@ #include #include "../portability.h" -/* Parse spec as host specification in the form +/* + * Parse spec as host specification in the form * host port * into its constituent parts. * Returns offsets into the original string and modifies it. @@ -25,13 +26,26 @@ */ void mmbackend_parse_hostspec(char* spec, char** host, char** port); -/* Parse a given host / port combination into a sockaddr_storage +/* + * 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. +/* + * 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); +int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast); + +/* + * Send arbitrary data over multiple writes if necessary + * Returns 1 on failure, 0 on success. + */ +int mmbackend_send(int fd, uint8_t* data, size_t length); + +/* + * Wraps mmbackend_send for cstrings + */ +int mmbackend_send_str(int fd, char* data); diff --git a/backends/lua.md b/backends/lua.md index 1c67477..6ad5c2a 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -37,7 +37,7 @@ Input values range between 0.0 and 1.0, output values are clamped to the same ra #### Global configuration -The backend does not take any global configuration. +The `lua` backend does not take any global configuration. #### Instance configuration diff --git a/backends/osc.c b/backends/osc.c index 03e431f..77bbde4 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -525,7 +525,7 @@ static int osc_configure_instance(instance* inst, char* option, char* value){ return 1; } - data->fd = mmbackend_socket(host, port, SOCK_DGRAM, 1); + data->fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); if(data->fd < 0){ fprintf(stderr, "Failed to bind for instance %s\n", inst->name); return 1; diff --git a/backends/sacn.c b/backends/sacn.c index 6f7d1a5..2f418e5 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -60,7 +60,7 @@ static int sacn_listener(char* host, char* port, uint8_t fd_flags){ return -1; } - fd = mmbackend_socket(host, port, SOCK_DGRAM, 1); + fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); if(fd < 0){ return -1; } -- cgit v1.2.3 From 48bf96602023b2ead855f13477b6f5e26b663b45 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 10 Aug 2019 20:30:07 +0200 Subject: Clean up & check unions --- backends/artnet.c | 5 +++++ backends/evdev.c | 14 +++++--------- backends/evdev.h | 9 +++++++++ backends/midi.c | 14 +++++--------- backends/midi.h | 10 ++++++++++ backends/osc.c | 13 +++++-------- backends/osc.h | 8 ++++++++ backends/sacn.c | 5 +++++ 8 files changed, 52 insertions(+), 26 deletions(-) (limited to 'backends') diff --git a/backends/artnet.c b/backends/artnet.c index 7f3f08c..8e47d4f 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -54,6 +54,11 @@ int init(){ .shutdown = artnet_shutdown }; + if(sizeof(artnet_instance_id) != sizeof(uint64_t)){ + fprintf(stderr, "ArtNet instance identification union out of bounds\n"); + return 1; + } + //register backend if(mm_backend_register(artnet)){ fprintf(stderr, "Failed to register ArtNet backend\n"); diff --git a/backends/evdev.c b/backends/evdev.c index 7a7913d..bd2098d 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -18,15 +18,6 @@ #define BACKEND_NAME "evdev" -typedef union { - struct { - uint32_t pad; - uint16_t type; - uint16_t code; - } fields; - uint64_t label; -} evdev_channel_ident; - static struct { uint8_t detect; } evdev_config = { @@ -46,6 +37,11 @@ int init(){ .shutdown = evdev_shutdown }; + if(sizeof(evdev_channel_ident) != sizeof(uint64_t)){ + fprintf(stderr, "evdev channel identification union out of bounds\n"); + return 1; + } + if(mm_backend_register(evdev)){ fprintf(stderr, "Failed to register evdev backend\n"); return 1; diff --git a/backends/evdev.h b/backends/evdev.h index f89e362..b26664b 100644 --- a/backends/evdev.h +++ b/backends/evdev.h @@ -44,3 +44,12 @@ typedef struct /*_evdev_instance_model*/ { struct libevdev_uinput* output_ev; #endif } evdev_instance_data; + +typedef union { + struct { + uint32_t pad; + uint16_t type; + uint16_t code; + } fields; + uint64_t label; +} evdev_channel_ident; \ No newline at end of file diff --git a/backends/midi.c b/backends/midi.c index 9c6ba80..5b8e561 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -4,15 +4,6 @@ #define BACKEND_NAME "midi" static snd_seq_t* sequencer = NULL; -typedef union { - struct { - uint8_t pad[5]; - uint8_t type; - uint8_t channel; - uint8_t control; - } fields; - uint64_t label; -} midi_channel_ident; enum /*_midi_channel_type*/ { none = 0, @@ -44,6 +35,11 @@ int init(){ .shutdown = midi_shutdown }; + if(sizeof(midi_channel_ident) != sizeof(uint64_t)){ + fprintf(stderr, "MIDI channel identification union out of bounds\n"); + return 1; + } + if(snd_seq_open(&sequencer, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0){ fprintf(stderr, "Failed to open ALSA sequencer\n"); return 1; diff --git a/backends/midi.h b/backends/midi.h index 556706f..5ec17ea 100644 --- a/backends/midi.h +++ b/backends/midi.h @@ -15,3 +15,13 @@ typedef struct /*_midi_instance_data*/ { char* read; char* write; } midi_instance_data; + +typedef union { + struct { + uint8_t pad[5]; + uint8_t type; + uint8_t channel; + uint8_t control; + } fields; + uint64_t label; +} midi_channel_ident; \ No newline at end of file diff --git a/backends/osc.c b/backends/osc.c index 77bbde4..beb5527 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -13,14 +13,6 @@ #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 = { @@ -40,6 +32,11 @@ int init(){ .shutdown = osc_shutdown }; + if(sizeof(osc_channel_ident) != sizeof(uint64_t)){ + fprintf(stderr, "OSC channel identification union out of bounds\n"); + return 1; + } + //register backend if(mm_backend_register(osc)){ fprintf(stderr, "Failed to register OSC backend\n"); diff --git a/backends/osc.h b/backends/osc.h index b2aaea7..ab19463 100644 --- a/backends/osc.h +++ b/backends/osc.h @@ -66,3 +66,11 @@ typedef struct /*_osc_instance_data*/ { //peer fd int fd; } osc_instance_data; + +typedef union { + struct { + uint32_t channel; + uint32_t parameter; + } fields; + uint64_t label; +} osc_channel_ident; \ No newline at end of file diff --git a/backends/sacn.c b/backends/sacn.c index 2f418e5..470e3bd 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -44,6 +44,11 @@ int init(){ .shutdown = sacn_shutdown }; + if(sizeof(sacn_instance_id) != sizeof(uint64_t)){ + fprintf(stderr, "sACN instance identification union out of bounds\n"); + return 1; + } + //register the backend if(mm_backend_register(sacn)){ fprintf(stderr, "Failed to register sACN backend\n"); -- 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 --- backends/Makefile | 2 + backends/libmmbackend.c | 217 +++++++++++++++ backends/libmmbackend.h | 75 ++++++ backends/maweb.c | 695 ++++++++++++++++++++++++++++++++++++++++++++++++ backends/maweb.h | 69 +++++ backends/maweb.md | 142 ++++++++++ 6 files changed, 1200 insertions(+) create mode 100644 backends/maweb.c create mode 100644 backends/maweb.h create mode 100644 backends/maweb.md (limited to 'backends') 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 -- cgit v1.2.3 From cc3aa7d8c1d680e75374a0c296a34d66a919f201 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 11 Aug 2019 20:29:17 +0200 Subject: Minor text fix --- backends/maweb.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/maweb.md b/backends/maweb.md index eb1ed44..3a7372c 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -42,7 +42,7 @@ mw1.page3.lower3 > mw1.page2.flash2 A button executor can likewise be mapped using the syntax ``` -mw1.page2.button3 > mw1.page3.fader1 +mw1.page2.button3 > mw1.page3.button1 ``` ##### Command line buttons @@ -136,7 +136,7 @@ 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 +* 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 -- cgit v1.2.3 From bad0fdac1e725b4b2efbbbfff4cef74c2e05efb8 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 12 Aug 2019 20:58:04 +0200 Subject: Fix maweb button execs --- backends/maweb.c | 27 ++++++++++++++++++++++++++- backends/maweb.md | 6 ++++-- 2 files changed, 30 insertions(+), 3 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index be4c2ac..38d3d69 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -218,10 +218,15 @@ static channel* maweb_channel(instance* inst, char* spec){ next_token += 5; } else if(!strncmp(next_token, "button", 6)){ - ident.fields.type = exec_fader; + ident.fields.type = exec_button; next_token += 6; } ident.fields.index = strtoul(next_token, NULL, 10); + + //fix up the identifiers for button execs + if(ident.fields.index > 100){ + ident.fields.index -= 100; + } } else{ for(n = 0; n < sizeof(cmdline_keys) / sizeof(char*); n++){ @@ -568,6 +573,26 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ 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\"," + //"\"cmdline\":\"\"," + "\"execIndex\":%d," + "\"pageIndex\":%d," + "\"buttonId\":%d," + "\"pressed\":%s," + "\"released\":%s," + "\"type\":0," + "\"session\":%ld" + "}", ident.fields.index + 100, + ident.fields.page, + 0, + (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\"," diff --git a/backends/maweb.md b/backends/maweb.md index 3a7372c..096f69c 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -27,7 +27,9 @@ 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. +Executors are arranged in pages, with each page having 90 fader executors (numbered 1 through 90) and +90 button executors (numbered 101 through 190). + 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. @@ -42,7 +44,7 @@ mw1.page3.lower3 > mw1.page2.flash2 A button executor can likewise be mapped using the syntax ``` -mw1.page2.button3 > mw1.page3.button1 +mw1.page2.button103 > mw1.page3.button101 ``` ##### Command line buttons -- cgit v1.2.3 From 047c76b48d4dd72b68ffaf99a210f9a3d5460f71 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 12 Aug 2019 20:59:48 +0200 Subject: Placate spellintian... --- backends/maweb.c | 2 +- backends/maweb.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index 38d3d69..b708015 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -355,7 +355,7 @@ static int maweb_connect(instance* inst){ || 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 + //the websocket key probably should not be hardcoded, but this is not security critical //and the whole websocket 'accept key' dance is plenty stupid as it is || mmbackend_send_str(data->fd, "Sec-WebSocket-Key: rbEQrXMEvCm4ZUjkj6juBQ==\r\n") || mmbackend_send_str(data->fd, "\r\n")){ diff --git a/backends/maweb.md b/backends/maweb.md index 096f69c..b54f1c3 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -34,7 +34,7 @@ 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 +These controls can be addressed like ``` mw1.page1.fader5 > mw1.page1.upper5 -- cgit v1.2.3 From c2bf894835d01c648e3f64826e69944a2a27373f Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 15 Aug 2019 10:55:23 +0200 Subject: Fix CI, add dot2 detection to maweb --- backends/maweb.c | 7 +++++ backends/maweb.md | 81 +++++++++---------------------------------------------- 2 files changed, 20 insertions(+), 68 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index b708015..07fce12 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -303,6 +303,12 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le if(json_obj(payload, "status") && json_obj(payload, "appType")){ fprintf(stderr, "maweb connection established\n"); + field = json_obj_str(payload, "appType", NULL); + if(!strncmp(field, "dot2", 4)){ + fprintf(stderr, "maweb peer detected as dot2, forcing user name 'remote'\n"); + free(data->user); + data->user = strdup("remote"); + } maweb_send_frame(inst, ws_text, (uint8_t*) "{\"session\":0}", 13); } @@ -535,6 +541,7 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ if(num && !data->login){ fprintf(stderr, "maweb instance %s can not send output, not logged in\n", inst->name); + return 0; } for(n = 0; n < num; n++){ diff --git a/backends/maweb.md b/backends/maweb.md index b54f1c3..dd13db9 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -15,10 +15,10 @@ The `maweb` backend does not take any global configuration. #### Instance configuration -| Option | Example value | Default value | Description | +| 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 | +| `host` | `10.23.42.21 80` | none | Host address (and optional port) of the MA Web Remote | +| `user` | `midimonster` | none | User for the remote session (GrandMA2) | | `password` | `midimonster` | `midimonster` | Password for the remote session | #### Channel specification @@ -58,71 +58,16 @@ 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` +| `SET` | `PREV` | `NEXT` | `CLEAR` | `FIXTURE_CHANNEL` | `FIXTURE_GROUP_PRESET` | `EXEC_CUE` | +| `STORE_UPDATE`| `OOPS` | `ESC` | `OFF` | `ON` | `MA` | `STORE` | +| `0` | `1` | `2` | `3` | `4` | `5` | `6` | +| `7` | `8` | `9` | `PUNKT` | `PLUS` | `MINUS` | `THRU` | +| `IF` | `AT` | `FULL` | `HIGH` | `ENTER` | `ASSIGN` | `LABEL` | +| `COPY` | `TIME` | `PAGE` | `MACRO` | `DELETE` | `GOTO` | `GO_PLUS` | +| `GO_MINUS` | `PAUSE` | `SELECT` | `FIXTURE` | `SEQU` | `CUE` | `PRESET` | +| `EDIT` | `UPDATE` | `EXEC` | `GROUP` | `PROG_ONLY` | `SPECIAL_DIALOGUE` | `SOLO` | +| `ODD` | `EVEN` | `WINGS` | `RESET` | `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 -- cgit v1.2.3 From 7997c74b421437c64ad3c25d5e7943470a6b9bc7 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 16 Aug 2019 19:57:25 +0200 Subject: Implement dot2 specific workarounds --- backends/maweb.c | 16 ++++++++-------- backends/maweb.h | 10 +++++++++- backends/maweb.md | 35 +++++++++++++++++++++++++++-------- 3 files changed, 44 insertions(+), 17 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index 07fce12..4d93987 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -239,8 +239,7 @@ static channel* maweb_channel(instance* inst, char* spec){ } } - if(ident.fields.type && ident.fields.index && ident.fields.page - && ident.fields.index <= 90){ + if(ident.fields.type && ident.fields.index && ident.fields.page){ //actually, those are zero-indexed... ident.fields.index--; ident.fields.page--; @@ -297,7 +296,7 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le 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); + (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)); } @@ -305,9 +304,10 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le fprintf(stderr, "maweb connection established\n"); field = json_obj_str(payload, "appType", NULL); if(!strncmp(field, "dot2", 4)){ - fprintf(stderr, "maweb peer detected as dot2, forcing user name 'remote'\n"); - free(data->user); - data->user = strdup("remote"); + data->peer_type = peer_dot2; + } + else if(!strncmp(field, "gma2", 4)){ + data->peer_type = peer_ma2; } maweb_send_frame(inst, ws_text, (uint8_t*) "{\"session\":0}", 13); } @@ -573,7 +573,7 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"type\":0," "\"session\":%ld" "}", ident.fields.index, ident.fields.page, - (exec_flash - ident.fields.type), + (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); @@ -591,7 +591,7 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"released\":%s," "\"type\":0," "\"session\":%ld" - "}", ident.fields.index + 100, + "}", ident.fields.index, ident.fields.page, 0, (v[n].normalised > 0.9) ? "true" : "false", diff --git a/backends/maweb.h b/backends/maweb.h index 6e6e652..5f59cc1 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -28,6 +28,13 @@ typedef enum /*_maweb_channel_type*/ { cmdline_button } maweb_channel_type; +typedef enum /*_maweb_peer_type*/ { + peer_unidentified = 0, + peer_ma2, + peer_ma3, + peer_dot2 +} maweb_peer_type; + typedef enum /*_ws_conn_state*/ { ws_new, ws_http, @@ -57,9 +64,10 @@ typedef struct /*_maweb_instance_data*/ { char* port; char* user; char* pass; - + uint8_t login; int64_t session; + maweb_peer_type peer_type; int fd; maweb_state state; diff --git a/backends/maweb.md b/backends/maweb.md index dd13db9..6076f44 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -5,10 +5,15 @@ 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`. +#### Setting up the console + +For the GrandMA2 enter the console configuration (`Setup` key), select `Console`/`Global Settings` and +set the `Remotes` option to `Login enabled`. Create an additional user that is able to log into the Web Remote using `Setup`/`Console`/`User & Profiles Setup`. +For the dot2, enter the console configuration using the `Setup` key, select `Global Settings` and enable the +Web Remote. Set a web remote password using the option below the activation setting. + #### Global configuration The `maweb` backend does not take any global configuration. @@ -27,12 +32,23 @@ Currently, three types of channels can be assigned ##### Executors -Executors are arranged in pages, with each page having 90 fader executors (numbered 1 through 90) and -90 button executors (numbered 101 through 190). - -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. +* 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. +* For the dot2, executors are also arranged in pages, but the controls are non-obviously numbered. + * For the faders, they are 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 + 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 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. These controls can be addressed like @@ -45,6 +61,7 @@ A button executor can likewise be mapped using the syntax ``` mw1.page2.button103 > mw1.page3.button101 +mw1.page2.button803 > mw1.page3.button516 ``` ##### Command line buttons @@ -58,6 +75,8 @@ mw1. The following button names are recognized by the backend: +| Supported | Command | Line | Keys | | | | +|---------------|---------------|---------------|---------------|-----------------------|-------------------------------|---------------| | `SET` | `PREV` | `NEXT` | `CLEAR` | `FIXTURE_CHANNEL` | `FIXTURE_GROUP_PRESET` | `EXEC_CUE` | | `STORE_UPDATE`| `OOPS` | `ESC` | `OFF` | `ON` | `MA` | `STORE` | | `0` | `1` | `2` | `3` | `4` | `5` | `6` | -- cgit v1.2.3 From 25d168454d53bb990118825f9bca808876aa4d59 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 17 Aug 2019 00:01:43 +0200 Subject: Hopefully clarify text a bit --- backends/maweb.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'backends') diff --git a/backends/maweb.md b/backends/maweb.md index 6076f44..d713d82 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -37,7 +37,7 @@ Currently, three types of channels can be assigned * 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. * For the dot2, executors are also arranged in pages, but the controls are non-obviously numbered. - * For the faders, they are right-to-left from the Core Fader section (Faders 6 to 1) over the F-Wing 1 (Faders 13 to 6) to + * 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. -- cgit v1.2.3 From 3adaad2a46cddaa4c79bda022c55f4eef7c3f5af Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 17 Aug 2019 00:04:51 +0200 Subject: Fix exec indexing --- backends/maweb.c | 5 ----- 1 file changed, 5 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index 4d93987..79e223f 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -222,11 +222,6 @@ static channel* maweb_channel(instance* inst, char* spec){ next_token += 6; } ident.fields.index = strtoul(next_token, NULL, 10); - - //fix up the identifiers for button execs - if(ident.fields.index > 100){ - ident.fields.index -= 100; - } } else{ for(n = 0; n < sizeof(cmdline_keys) / sizeof(char*); n++){ -- cgit v1.2.3 From 4863711ee2dac065151f5c9ab5508eb479b25b72 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 17 Aug 2019 01:46:06 +0200 Subject: Implement evdev relative axis output (Fixes #20) --- backends/evdev.c | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/evdev.c b/backends/evdev.c index bd2098d..b19cda8 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -445,7 +445,7 @@ static int evdev_start(){ static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v) { #ifndef EVDEV_NO_UINPUT - size_t evt = 0; + size_t evt = 0, axis = 0; evdev_instance_data* data = (evdev_instance_data*) inst->impl; evdev_channel_ident ident = { .label = 0 @@ -467,7 +467,20 @@ static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v) switch(ident.fields.type){ case EV_REL: - value = (v[evt].normalised < 0.5) ? -1 : ((v[evt].normalised > 0.5) ? 1 : 0); + for(axis = 0; axis < data->relative_axes; axis++){ + if(data->relative_axis[axis].code == ident.fields.code){ + value = (v[evt].normalised * data->relative_axis[axis].max) - data->relative_axis[axis].current; + data->relative_axis[axis].current = v[evt].normalised * data->relative_axis[axis].max; + + if(data->relative_axis[axis].inverted){ + value *= -1; + } + break; + } + } + if(axis == data->relative_axes){ + value = (v[evt].normalised < 0.5) ? -1 : ((v[evt].normalised > 0.5) ? 1 : 0); + } break; case EV_ABS: range = libevdev_get_abs_maximum(data->output_proto, ident.fields.code) - libevdev_get_abs_minimum(data->output_proto, ident.fields.code); -- 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) --- 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 +- 9 files changed, 581 insertions(+), 114 deletions(-) (limited to 'backends') 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 bfa4cbb012ce49f59d983d275da8837b50872c16 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 22 Aug 2019 21:38:06 +0200 Subject: Update travis dependencies --- backends/maweb.c | 1 - 1 file changed, 1 deletion(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index 07595be..9ba04fa 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -475,7 +475,6 @@ static int maweb_request_playbacks(instance* inst){ 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; } -- cgit v1.2.3 From 6464a418f2bf13c8c04b10abf5ebdc4c95aae861 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 22 Aug 2019 22:28:25 +0200 Subject: Try to fix failing build by providing additional hints to MacOS --- backends/maweb.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/maweb.md b/backends/maweb.md index fe430db..f3b40b6 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -53,8 +53,8 @@ Currently, three types of MA controls can be assigned, with each having some sub * 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. `fader` controls, when mapped as outputs from the MA, output their value, `button` controls -output 1 when the corresponding executor is running, 0 otherwise. +the console before being usable. When mapped as outputs, `fader` controls output their value, `button` controls output 1 when the corresponding +executor is running, 0 otherwise. These controls can be addressed like -- cgit v1.2.3 From 93de82b8ccab8fdbeaa2b1847c75488a03340bcc Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 22 Aug 2019 22:41:28 +0200 Subject: Update travis config for openssl, using CFLAGS this time --- backends/lua.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index ec02575..365cf3e 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -384,11 +384,11 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ } static int lua_handle(size_t num, managed_fd* fds){ - uint8_t read_buffer[100]; uint64_t delta = timer_interval; size_t n; #ifdef MMBACKEND_LUA_TIMERFD + uint8_t read_buffer[100]; if(!num){ return 0; } -- cgit v1.2.3 From b4b27aa4a90899d5b025bbef11d444d4c47800d9 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 23 Aug 2019 19:47:53 +0200 Subject: Finalize & publish maweb backend --- backends/maweb.c | 92 ++++++++++++++++++++++++++++++++----------------------- backends/maweb.md | 6 +--- 2 files changed, 55 insertions(+), 43 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index 9ba04fa..4e6fad1 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -266,14 +266,17 @@ static channel* maweb_channel(instance* inst, char* spec){ ident.fields.index--; ident.fields.page--; - //check if the channel is already known + //check if the (exec/meta) channel is already known for(n = 0; n < data->input_channels; n++){ - if(data->input_channel[n].label == ident.label){ + if(data->input_channel[n].fields.page == ident.fields.page + && data->input_channel[n].fields.index == ident.fields.index){ break; } } - if(n == data->input_channels){ + //FIXME only register channels that are mapped as outputs + //only register exec channels for updates + if(n == data->input_channels && ident.fields.type != cmdline_button){ 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"); @@ -324,9 +327,9 @@ static int maweb_send_frame(instance* inst, maweb_operation op, uint8_t* payload 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; + channel_value evt; maweb_channel_ident ident = { - .fields.page = page, + .fields.page = page - 1, .fields.index = json_obj_int(payload, "iExec", 191) }; @@ -335,10 +338,11 @@ static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_ty //ignore unused buttons return 0; } - fprintf(stderr, "maweb missing exec block data on exec %d\n", ident.fields.index); + fprintf(stderr, "maweb missing exec block data on exec %ld.%d\n", page, ident.fields.index); return 1; } + //the bottomButtons key has an additional subentry if(metatype == 3){ exec_blocks += json_obj_offset(payload + exec_blocks, "items"); } @@ -363,7 +367,7 @@ static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_ty 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)); + DBGPF("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++; } @@ -416,7 +420,7 @@ static int maweb_process_playbacks(instance* inst, int64_t page, char* payload, group++; } updates_inflight--; - fprintf(stderr, "maweb playback message processing done, %lu updates inflight\n", updates_inflight); + DBGPF("maweb playback message processing done, %lu updates inflight\n", updates_inflight); return 0; } @@ -425,45 +429,53 @@ static int maweb_request_playbacks(instance* inst){ 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; + char item_indices[1024] = "[300,400,500]", item_counts[1024] = "[16,16,16]", item_types[1024] = "[3,3,3]"; + size_t page_index = 0, view = 3, channel = 0, offsets[3], channel_offset, channels; if(updates_inflight){ fprintf(stderr, "maweb skipping update request, %lu updates still inflight\n", updates_inflight); return 0; } + //don't quote me on this whole segment for(channel = 0; channel < data->input_channels; channel++){ - offsets[0] = offsets[1] = offsets[2] = 0; + offsets[0] = offsets[1] = offsets[2] = 1; 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; + //blocks 0, 100 & 200 have 21 execs and need to be queried from fader view + view = (data->input_channel[channel].fields.index >= 300) ? 3 : 2; + + for(channel_offset = 1; channel + channel_offset <= data->input_channels; channel_offset++){ + channels = channel + channel_offset - 1; + //find end for this exec block + for(; channel + channel_offset < data->input_channels; channel_offset++){ + if(data->input_channel[channel + channel_offset].fields.page != page_index + || (data->input_channel[channels].fields.index / 100) != (data->input_channel[channel + channel_offset].fields.index / 100)){ + break; + } + } + + //add request block for the exec block + offsets[0] += snprintf(item_indices + offsets[0], sizeof(item_indices) - offsets[0], "%d,", data->input_channel[channels].fields.index); + offsets[1] += snprintf(item_counts + offsets[1], sizeof(item_counts) - offsets[1], "%d,", data->input_channel[channel + channel_offset - 1].fields.index - data->input_channel[channels].fields.index + 1); + offsets[2] += snprintf(item_types + offsets[2], sizeof(item_types) - offsets[2], "%d,", (data->input_channel[channels].fields.index < 100) ? 2 : 3); + + //send on page boundary, metamode boundary, last channel + if(channel + channel_offset >= data->input_channels + || data->input_channel[channel + channel_offset].fields.page != page_index + || (data->input_channel[channel].fields.index < 300) != (data->input_channel[channel + channel_offset].fields.index < 300)){ + break; + } + } + + //terminate arrays (overwriting the last array separator) + offsets[0] += snprintf(item_indices + offsets[0] - 1, sizeof(item_indices) - offsets[0], "]"); + offsets[1] += snprintf(item_counts + offsets[1] - 1, sizeof(item_counts) - offsets[1], "]"); + offsets[2] += snprintf(item_types + offsets[2] - 1, sizeof(item_types) - offsets[2], "]"); } else{ + //for the ma, the view equals the exec type requested (we can query all button execs from button view, all fader execs from fader view) view = (data->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); @@ -476,8 +488,12 @@ static int maweb_request_playbacks(instance* inst){ 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; } + + //advance base channel + channel += channel_offset - 1; + + //send current request snprintf(xmit_buffer, sizeof(xmit_buffer), "{" "\"requestType\":\"playbacks\"," @@ -497,7 +513,7 @@ static int maweb_request_playbacks(instance* inst){ view, data->session); rv |= maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); - //fprintf(stderr, "req: %s\n", xmit_buffer); + DBGPF("maweb poll request: %s\n", xmit_buffer); updates_inflight++; } @@ -530,7 +546,7 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le } } - fprintf(stderr, "maweb message (%lu): %s\n", payload_length, payload); + DBGPF("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); diff --git a/backends/maweb.md b/backends/maweb.md index f3b40b6..9fe4790 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -109,8 +109,4 @@ Data input from the console is done by actively querying the state of all mapped 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 -* Command line events are sent, but I'm not sure they're being handled yet -* For the dot2, currently only the Core & F-Wings are supported for input from the console, not the B-Wings +Command line events are sent, but I'm not sure they're being handled yet. -- cgit v1.2.3 From ee8776b114fc141355675a20e3d7e49ec5e6c4d8 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 23 Aug 2019 20:01:33 +0200 Subject: Fix Coverity CID 347886 --- backends/maweb.c | 1 + 1 file changed, 1 insertion(+) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index 4e6fad1..80ab579 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -916,6 +916,7 @@ static int maweb_start(){ if(maweb_connect(inst[u])){ fprintf(stderr, "Failed to open connection to MA Web Remote for instance %s\n", inst[u]->name); + free(inst); return 1; } } -- cgit v1.2.3 From fe952470ca4bae4cff25a9090e4c1b05a76a7961 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 23 Aug 2019 23:40:16 +0200 Subject: Update sACN backend documentation (Fixes #22) --- backends/sacn.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/sacn.md b/backends/sacn.md index 3d245a4..434beeb 100644 --- a/backends/sacn.md +++ b/backends/sacn.md @@ -16,7 +16,7 @@ containing all write-enabled universes. | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| -| `universe` | `0` | none | Universe identifier | +| `universe` | `1` | none | Universe identifier between 1 and 63999 | | `interface` | `1` | `0` | The bound address to use for data input/output | | `priority` | `100` | none | The data priority to transmit for this instance. Setting this option enables the instance for output and includes it in the universe discovery report. | | `destination` | `10.2.2.2` | Universe multicast | Destination address for unicast output. If unset, the multicast destination for the specified universe is used. | @@ -55,4 +55,4 @@ To use multicast input, all networking hardware in the path must support the IGM The Linux kernel limits the number of multicast groups an interface may join to 20. An instance configured for input automatically joins the multicast group for its universe, unless configured in `unicast` mode. -This limit can be raised by changing the kernel option in `/proc/sys/net/ipv4/igmp_max_memberships`. \ No newline at end of file +This limit can be raised by changing the kernel option in `/proc/sys/net/ipv4/igmp_max_memberships`. -- cgit v1.2.3 From 0906c00d23be220928a656476c26a490998e826e Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 24 Aug 2019 20:19:24 +0200 Subject: Only connect to ALSA when instances are requested --- backends/midi.c | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) (limited to 'backends') diff --git a/backends/midi.c b/backends/midi.c index 5b8e561..88b841c 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -3,6 +3,7 @@ #include "midi.h" #define BACKEND_NAME "midi" +static char* sequencer_name = NULL; static snd_seq_t* sequencer = NULL; enum /*_midi_channel_type*/ { @@ -40,29 +41,19 @@ int init(){ return 1; } - if(snd_seq_open(&sequencer, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0){ - fprintf(stderr, "Failed to open ALSA sequencer\n"); - return 1; - } - //register backend if(mm_backend_register(midi)){ fprintf(stderr, "Failed to register MIDI backend\n"); return 1; } - snd_seq_nonblock(sequencer, 1); - - fprintf(stderr, "MIDI client ID is %d\n", snd_seq_client_id(sequencer)); return 0; } static int midi_configure(char* option, char* value){ if(!strcmp(option, "name")){ - if(snd_seq_set_client_name(sequencer, value) < 0){ - fprintf(stderr, "Failed to set MIDI client name to %s\n", value); - return 1; - } + free(sequencer_name); + sequencer_name = strdup(value); return 0; } @@ -361,6 +352,21 @@ static int midi_start(){ return 0; } + //connect to the sequencer + if(snd_seq_open(&sequencer, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0){ + fprintf(stderr, "Failed to open ALSA sequencer\n"); + return 0; + } + + snd_seq_nonblock(sequencer, 1); + fprintf(stderr, "MIDI client ID is %d\n", snd_seq_client_id(sequencer)); + + //update the sequencer client name + if(snd_seq_set_client_name(sequencer, sequencer_name) < 0){ + fprintf(stderr, "Failed to set MIDI client name to %s\n", sequencer_name); + return 1; + } + //create all ports for(p = 0; p < n; p++){ data = (midi_instance_data*) inst[p]->impl; @@ -437,12 +443,17 @@ static int midi_shutdown(){ free(inst); //close midi - snd_seq_close(sequencer); - sequencer = NULL; + if(sequencer){ + snd_seq_close(sequencer); + sequencer = NULL; + } //free configuration cache snd_config_update_free_global(); + free(sequencer_name); + sequencer_name = NULL; + fprintf(stderr, "MIDI backend shut down\n"); return 0; } -- cgit v1.2.3 From ba8cb9bfaf0c3699a728cef6918d7433a8690936 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 24 Aug 2019 22:13:49 +0200 Subject: Fix NULL pointer access --- backends/midi.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'backends') diff --git a/backends/midi.c b/backends/midi.c index 88b841c..571a61d 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -362,7 +362,7 @@ static int midi_start(){ fprintf(stderr, "MIDI client ID is %d\n", snd_seq_client_id(sequencer)); //update the sequencer client name - if(snd_seq_set_client_name(sequencer, sequencer_name) < 0){ + if(snd_seq_set_client_name(sequencer, sequencer_name ? sequencer_name : "MIDIMonster") < 0){ fprintf(stderr, "Failed to set MIDI client name to %s\n", sequencer_name); return 1; } -- cgit v1.2.3 From 191949152d803229275d7b98b184ff722a9549a2 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 31 Aug 2019 17:23:22 +0200 Subject: Update documentation --- backends/maweb.md | 2 ++ backends/midi.md | 3 +++ 2 files changed, 5 insertions(+) (limited to 'backends') diff --git a/backends/maweb.md b/backends/maweb.md index 9fe4790..93cd776 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -109,4 +109,6 @@ Data input from the console is done by actively querying the state of all mapped at low latency. A lower input interval value will produce data with lower latency, at the cost of network & CPU usage. Higher values will make the input "step" more, but will not consume as many CPU cycles and network bandwidth. +When requesting button executor events on the fader pages (execs 101 to 222) of a dot2 console, map at least one fader control from the 0 - 22 range or input will not work due to strange limitations in the MA Web API. + Command line events are sent, but I'm not sure they're being handled yet. diff --git a/backends/midi.md b/backends/midi.md index 5e295ee..108860e 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -52,6 +52,9 @@ midi1.ch0.pitch > midi2.ch1.pitch ``` #### Known bugs / problems +To access MIDI data, the user running MIDIMonster needs read & write access to the ALSA sequencer. +This can usually be done by adding this user to the `audio` system group. + Currently, no Note Off messages are sent (instead, Note On messages with a velocity of 0 are generated, which amount to the same thing according to the spec). This may be implemented as a configuration option at a later time. -- cgit v1.2.3 From 49855046f683deb7fdadc2c3d8caf513099b4803 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 4 Sep 2019 20:26:24 +0200 Subject: Use platform-independent specifiers for printf calls --- backends/artnet.c | 8 ++++---- backends/maweb.c | 34 ++++++++++++++++++---------------- backends/osc.c | 24 ++++++++++++------------ backends/sacn.c | 10 +++++----- 4 files changed, 39 insertions(+), 37 deletions(-) (limited to 'backends') diff --git a/backends/artnet.c b/backends/artnet.c index 8e47d4f..e01ac94 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -32,7 +32,7 @@ static int artnet_listener(char* host, char* port){ return -1; } - fprintf(stderr, "ArtNet backend interface %lu bound to %s port %s\n", artnet_fds, host, port); + fprintf(stderr, "ArtNet backend interface %" PRIsize_t " bound to %s port %s\n", artnet_fds, host, port); artnet_fd[artnet_fds].fd = fd; artnet_fd[artnet_fds].output_instances = 0; artnet_fd[artnet_fds].output_instance = NULL; @@ -235,7 +235,7 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v) artnet_instance_data* data = (artnet_instance_data*) inst->impl; if(!data->dest_len){ - fprintf(stderr, "ArtNet instance %s not enabled for output (%lu channel events)\n", inst->name, num); + fprintf(stderr, "ArtNet instance %s not enabled for output (%" PRIsize_t " channel events)\n", inst->name, num); return 0; } @@ -300,7 +300,7 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){ } if(!chan){ - fprintf(stderr, "Active channel %lu on %s not known to core\n", p, inst->name); + fprintf(stderr, "Active channel %" PRIsize_t " on %s not known to core\n", p, inst->name); return 1; } @@ -447,7 +447,7 @@ static int artnet_start(){ } } - fprintf(stderr, "ArtNet backend registering %lu descriptors to core\n", artnet_fds); + fprintf(stderr, "ArtNet backend registering %" PRIsize_t " descriptors to core\n", artnet_fds); for(u = 0; u < artnet_fds; u++){ if(mm_manage_fd(artnet_fd[u].fd, BACKEND_NAME, 1, (void*) u)){ goto bail; diff --git a/backends/maweb.c b/backends/maweb.c index 80ab579..8d39f00 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -5,6 +5,8 @@ #include #endif +#define DEBUG + #include "libmmbackend.h" #include "maweb.h" @@ -338,7 +340,7 @@ static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_ty //ignore unused buttons return 0; } - fprintf(stderr, "maweb missing exec block data on exec %ld.%d\n", page, ident.fields.index); + fprintf(stderr, "maweb missing exec block data on exec %" PRIu64 ".%d\n", page, ident.fields.index); return 1; } @@ -367,7 +369,7 @@ static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_ty mm_channel_event(chan, evt); } - DBGPF("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)); + DBGPF("maweb page %" PRIu64 " exec %d value %f running %" PRIu64 "\n", page, ident.fields.index, json_obj_double(payload + control, "v", 0.0), json_obj_int(payload, "isRun", 0)); ident.fields.index++; block++; } @@ -420,7 +422,7 @@ static int maweb_process_playbacks(instance* inst, int64_t page, char* payload, group++; } updates_inflight--; - DBGPF("maweb playback message processing done, %lu updates inflight\n", updates_inflight); + DBGPF("maweb playback message processing done, %" PRIu64 " updates inflight\n", updates_inflight); return 0; } @@ -433,7 +435,7 @@ static int maweb_request_playbacks(instance* inst){ size_t page_index = 0, view = 3, channel = 0, offsets[3], channel_offset, channels; if(updates_inflight){ - fprintf(stderr, "maweb skipping update request, %lu updates still inflight\n", updates_inflight); + fprintf(stderr, "maweb skipping update request, %" PRIu64 " updates still inflight\n", updates_inflight); return 0; } @@ -476,7 +478,7 @@ static int maweb_request_playbacks(instance* inst){ else{ //for the ma, the view equals the exec type requested (we can query all button execs from button view, all fader execs from fader view) view = (data->input_channel[channel].fields.index >= 100) ? 3 : 2; - snprintf(item_types, sizeof(item_types), "[%lu]", view); + snprintf(item_types, sizeof(item_types), "[%" PRIsize_t "]", view); //this channel must be included, so it must be in range for the first startindex snprintf(item_indices, sizeof(item_indices), "[%d]", (data->input_channel[channel].fields.index / 5) * 5); @@ -487,7 +489,7 @@ static int maweb_request_playbacks(instance* inst){ 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)); + snprintf(item_counts, sizeof(item_indices), "[%" PRIsize_t "]", ((channels / 5) * 5 + 5)); } //advance base channel @@ -499,12 +501,12 @@ static int maweb_request_playbacks(instance* inst){ "\"requestType\":\"playbacks\"," "\"startIndex\":%s," "\"itemsCount\":%s," - "\"pageIndex\":%lu," + "\"pageIndex\":%" PRIsize_t "," "\"itemsType\":%s," - "\"view\":%lu," + "\"view\":%" PRIsize_t "," "\"execButtonViewMode\":2," //extended "\"buttonsViewMode\":0," //get vfader for button execs - "\"session\":%lu" + "\"session\":%" PRIu64 "}", item_indices, item_counts, @@ -546,16 +548,16 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le } } - DBGPF("maweb message (%lu): %s\n", payload_length, payload); + DBGPF("maweb message (%" PRIsize_t "): %s\n", payload_length, payload); if(json_obj(payload, "session") == JSON_NUMBER){ data->session = json_obj_int(payload, "session", data->session); - fprintf(stderr, "maweb session id is now %ld\n", data->session); + fprintf(stderr, "maweb session id is now %" PRIu64 "\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}", + "{\"requestType\":\"login\",\"username\":\"%s\",\"password\":\"%s\",\"session\":%" PRIu64 "}", (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)); } @@ -787,7 +789,7 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"pageIndex\":%d," "\"faderValue\":%f," "\"type\":1," - "\"session\":%ld" + "\"session\":%" PRIu64 "}", ident.fields.index, ident.fields.page, v[n].normalised, data->session); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); break; @@ -803,7 +805,7 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"pressed\":%s," "\"released\":%s," "\"type\":0," - "\"session\":%ld" + "\"session\":%" PRIu64 "}", 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", @@ -844,7 +846,7 @@ static int maweb_keepalive(){ 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); + snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"session\":%" PRIu64 "}", data->session); maweb_send_frame(inst[u], ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); } } @@ -926,7 +928,7 @@ static int maweb_start(){ return 0; } - fprintf(stderr, "maweb backend registering %lu descriptors to core\n", n); + fprintf(stderr, "maweb backend registering %" PRIsize_t " descriptors to core\n", n); //initialize timeouts last_keepalive = last_update = mm_timestamp(); diff --git a/backends/osc.c b/backends/osc.c index beb5527..bffbba8 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -239,20 +239,20 @@ static int osc_path_validate(char* path, uint8_t allow_patterns){ 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); + fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, illegal_chars[c], u); 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); + fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, pattern_chars[c], u); return 1; } 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); + fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, pattern_chars[c], u); return 1; } } @@ -261,14 +261,14 @@ static int osc_path_validate(char* path, uint8_t allow_patterns){ 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); + fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, pattern_chars[c], u); return 1; } curly_open = 1; break; case '[': if(square_open || curly_open){ - fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %lu\n", path, pattern_chars[c], u); + fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, pattern_chars[c], u); return 1; } square_open = 1; @@ -477,14 +477,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 %lu of OSC pattern %s\n", u, pattern_path); + fprintf(stderr, "Missing minimum specification for parameter %" PRIsize_t " of OSC pattern %s\n", u, pattern_path); return 1; } data->pattern[pattern].min[u] = osc_parse_value_spec(format[u], token); token = strtok(NULL, " "); if(!token){ - fprintf(stderr, "Missing maximum specification for parameter %lu of OSC pattern %s\n", u, pattern_path); + fprintf(stderr, "Missing maximum specification for parameter %" PRIsize_t " of OSC pattern %s\n", u, pattern_path); return 1; } data->pattern[pattern].max[u] = osc_parse_value_spec(format[u], token); @@ -686,7 +686,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 %lu\n", inst->name, data->channel[channel].path, p); + fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s at parameter %" PRIsize_t "\n", inst->name, data->channel[channel].path, p); return 1; } @@ -717,7 +717,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 (%lu channels)\n", inst->name, num); + fprintf(stderr, "OSC instance %s does not have a destination, output is disabled (%" PRIsize_t " channels)\n", inst->name, num); return 0; } @@ -775,7 +775,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 %lu\n", payload_len); + fprintf(stderr, "Invalid OSC packet, data length %" PRIsize_t "\n", payload_len); return 0; } @@ -784,7 +784,7 @@ static int osc_process_packet(instance* inst, char* local_path, char* format, ui 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); + fprintf(stderr, "OSC message %s.%s had format %s, internal representation has %" PRIsize_t " parameters\n", inst->name, local_path, format, data->channel[c].params); continue; } @@ -925,7 +925,7 @@ static int osc_start(){ } } - fprintf(stderr, "OSC backend registered %lu descriptors to core\n", fds); + fprintf(stderr, "OSC backend registered %" PRIsize_t " descriptors to core\n", fds); free(inst); return 0; diff --git a/backends/sacn.c b/backends/sacn.c index 470e3bd..6e1b20b 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -78,7 +78,7 @@ static int sacn_listener(char* host, char* port, uint8_t fd_flags){ return -1; } - fprintf(stderr, "sACN backend interface %lu bound to %s port %s\n", global_cfg.fds, host, port); + fprintf(stderr, "sACN backend interface %" PRIsize_t " bound to %s port %s\n", global_cfg.fds, host, port); global_cfg.fd[global_cfg.fds].fd = fd; global_cfg.fd[global_cfg.fds].flags = fd_flags; global_cfg.fd[global_cfg.fds].universes = 0; @@ -300,7 +300,7 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ } if(!data->xmit_prio){ - fprintf(stderr, "sACN instance %s not enabled for output (%lu channel events)\n", inst->name, num); + fprintf(stderr, "sACN instance %s not enabled for output (%" PRIsize_t " channel events)\n", inst->name, num); return 0; } @@ -385,7 +385,7 @@ static int sacn_process_frame(instance* inst, sacn_frame_root* frame, sacn_frame } if(!chan){ - fprintf(stderr, "Active channel %lu on %s not known to core", u, inst->name); + fprintf(stderr, "Active channel %" PRIsize_t " on %s not known to core", u, inst->name); return 1; } @@ -453,7 +453,7 @@ static void sacn_discovery(size_t fd){ memcpy(pdu.data.data, global_cfg.fd[fd].universe + page * 512, universes * sizeof(uint16_t)); if(sendto(global_cfg.fd[fd].fd, (uint8_t*) &pdu, sizeof(pdu) - (512 - universes) * sizeof(uint16_t), 0, (struct sockaddr*) &discovery_dest, sizeof(discovery_dest)) < 0){ - fprintf(stderr, "Failed to output sACN universe discovery frame for interface %lu: %s\n", fd, strerror(errno)); + fprintf(stderr, "Failed to output sACN universe discovery frame for interface %" PRIsize_t ": %s\n", fd, strerror(errno)); } } } @@ -615,7 +615,7 @@ static int sacn_start(){ } } - fprintf(stderr, "sACN backend registering %lu descriptors to core\n", global_cfg.fds); + fprintf(stderr, "sACN backend registering %" PRIsize_t " descriptors to core\n", global_cfg.fds); for(u = 0; u < global_cfg.fds; u++){ //allocate memory for storing last frame transmission timestamp global_cfg.fd[u].last_frame = calloc(global_cfg.fd[u].universes, sizeof(uint64_t)); -- cgit v1.2.3 From bab255a969acc93fb7628b0827fa8f93208b6fa0 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 4 Sep 2019 20:30:27 +0200 Subject: Don't build maweb as debug --- backends/maweb.c | 2 -- 1 file changed, 2 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index 8d39f00..2b7a37c 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -5,8 +5,6 @@ #include #endif -#define DEBUG - #include "libmmbackend.h" #include "maweb.h" -- cgit v1.2.3 From f19e3fe0d131c68d1e77fcd8706b260c28f508b8 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 7 Sep 2019 14:20:00 +0200 Subject: maweb I/O buffering --- backends/maweb.c | 245 ++++++++++++++++++++++++++++++++----------------------- backends/maweb.h | 27 +++--- 2 files changed, 155 insertions(+), 117 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index 2b7a37c..ca8a47d 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -14,9 +14,6 @@ #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; @@ -104,11 +101,6 @@ int init(){ .interval = maweb_interval }; - 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"); @@ -117,14 +109,31 @@ 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; +static ssize_t maweb_channel_index(maweb_instance_data* data, maweb_channel_type type, uint16_t page, uint16_t index){ + size_t n; + for(n = 0; n < data->channels; n++){ + if(data->channel[n].type == type + && data->channel[n].page == page + && data->channel[n].index == index){ + return n; + } } - return a->fields.index - b->fields.index; + return -1; +} + +static int channel_comparator(const void* raw_a, const void* raw_b){ + maweb_channel_data* a = (maweb_channel_data*) raw_a; + maweb_channel_data* b = (maweb_channel_data*) raw_b; + + //this needs to take into account command line channels + //they need to be sorted last so that the channel poll logic works properly + if(a->type != b->type){ + return a->type - b->type; + } + if(a->page != b->page){ + return a->page - b->page; + } + return a->index - b->index; } static uint32_t maweb_interval(){ @@ -214,14 +223,14 @@ 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 + maweb_channel_data chan = { + 0 }; char* next_token = NULL; size_t n; if(!strncmp(spec, "page", 4)){ - ident.fields.page = strtoul(spec + 4, &next_token, 10); + chan.page = strtoul(spec + 4, &next_token, 10); if(*next_token != '.'){ fprintf(stderr, "Failed to parse maweb channel spec %s: Missing separator\n", spec); return NULL; @@ -229,65 +238,57 @@ static channel* maweb_channel(instance* inst, char* spec){ next_token++; if(!strncmp(next_token, "fader", 5)){ - ident.fields.type = exec_fader; + chan.type = exec_fader; next_token += 5; } else if(!strncmp(next_token, "upper", 5)){ - ident.fields.type = exec_upper; + chan.type = exec_upper; next_token += 5; } else if(!strncmp(next_token, "lower", 5)){ - ident.fields.type = exec_lower; + chan.type = exec_lower; next_token += 5; } else if(!strncmp(next_token, "flash", 5)){ - ident.fields.type = exec_button; + chan.type = exec_button; next_token += 5; } else if(!strncmp(next_token, "button", 6)){ - ident.fields.type = exec_button; + chan.type = exec_button; next_token += 6; } - ident.fields.index = strtoul(next_token, NULL, 10); + chan.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; + //FIXME this is broken for layerMode + if(!strcmp(spec, cmdline_keys[n]) || (*spec == 'l' && !strcmp(spec + 1, cmdline_keys[n]))){ + chan.type = (*spec == 'l') ? cmdline_local : cmdline; + chan.index = n + 1; + chan.page = 1; break; } } } - if(ident.fields.type && ident.fields.index && ident.fields.page){ + if(chan.type && chan.index && chan.page){ //actually, those are zero-indexed... - ident.fields.index--; - ident.fields.page--; - - //check if the (exec/meta) channel is already known - for(n = 0; n < data->input_channels; n++){ - if(data->input_channel[n].fields.page == ident.fields.page - && data->input_channel[n].fields.index == ident.fields.index){ - break; - } - } + chan.index--; + chan.page--; - //FIXME only register channels that are mapped as outputs - //only register exec channels for updates - if(n == data->input_channels && ident.fields.type != cmdline_button){ - data->input_channel = realloc(data->input_channel, (data->input_channels + 1) * sizeof(maweb_channel_ident)); - if(!data->input_channel){ + if(maweb_channel_index(data, chan.type, chan.page, chan.index) == -1){ + data->channel = realloc(data->channel, (data->channels + 1) * sizeof(maweb_channel_data)); + if(!data->channel){ fprintf(stderr, "Failed to allocate memory\n"); return NULL; } - data->input_channel[n].label = ident.label; - data->input_channels++; + data->channel[data->channels] = chan; + data->channels++; } - return mm_channel(inst, ident.label, 1); + return mm_channel(inst, maweb_channel_index(data, chan.type, chan.page, chan.index), 1); } + fprintf(stderr, "Failed to parse maweb channel spec %s\n", spec); return NULL; } @@ -325,20 +326,18 @@ static int maweb_send_frame(instance* inst, maweb_operation op, uint8_t* payload } static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_type metatype, char* payload, size_t payload_length){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; size_t exec_blocks = json_obj_offset(payload, (metatype == 2) ? "executorBlocks" : "bottomButtons"), offset, block = 0, control; - channel* chan = NULL; channel_value evt; - maweb_channel_ident ident = { - .fields.page = page - 1, - .fields.index = json_obj_int(payload, "iExec", 191) - }; + int64_t exec_index = json_obj_int(payload, "iExec", 191); + ssize_t channel_index; if(!exec_blocks){ if(metatype == 3){ //ignore unused buttons return 0; } - fprintf(stderr, "maweb missing exec block data on exec %" PRIu64 ".%d\n", page, ident.fields.index); + fprintf(stderr, "maweb missing exec block data on exec %" PRIu64 ".%" PRIu64 "\n", page, exec_index); return 1; } @@ -348,27 +347,41 @@ static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_ty } //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); + + channel_index = maweb_channel_index(data, exec_fader, page - 1, exec_index); + if(channel_index >= 0){ + if(!data->channel[channel_index].input_blocked){ + evt.normalised = json_obj_double(payload + control, "v", 0.0); + if(evt.normalised != data->channel[channel_index].in){ + mm_channel_event(mm_channel(inst, channel_index, 0), evt); + data->channel[channel_index].in = evt.normalised; + } + } + else{ + data->channel[channel_index].input_blocked--; + } } - 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); + channel_index = maweb_channel_index(data, exec_button, page - 1, exec_index); + if(channel_index >= 0){ + if(!data->channel[channel_index].input_blocked){ + evt.normalised = json_obj_int(payload, "isRun", 0); + if(evt.normalised != data->channel[channel_index].in){ + mm_channel_event(mm_channel(inst, channel_index, 0), evt); + data->channel[channel_index].in = evt.normalised; + } + } + else{ + data->channel[channel_index].input_blocked--; + } } - DBGPF("maweb page %" PRIu64 " exec %d value %f running %" PRIu64 "\n", page, ident.fields.index, json_obj_double(payload + control, "v", 0.0), json_obj_int(payload, "isRun", 0)); - ident.fields.index++; + DBGPF("maweb page %" PRIu64 " exec %" PRIu64 " value %f running %" PRIu64 "\n", page, exec_index, json_obj_double(payload + control, "v", 0.0), json_obj_int(payload, "isRun", 0)); + exec_index++; block++; } @@ -437,33 +450,37 @@ static int maweb_request_playbacks(instance* inst){ return 0; } + //TODO this needs to ignore non-metamoded execs //don't quote me on this whole segment - for(channel = 0; channel < data->input_channels; channel++){ + + //only request faders and buttons + for(channel = 0; channel < data->channels && data->channel[channel].type <= exec_button; channel++){ offsets[0] = offsets[1] = offsets[2] = 1; - page_index = data->input_channel[channel].fields.page; + page_index = data->channel[channel].page; if(data->peer_type == peer_dot2){ //blocks 0, 100 & 200 have 21 execs and need to be queried from fader view - view = (data->input_channel[channel].fields.index >= 300) ? 3 : 2; + view = (data->channel[channel].index >= 300) ? 3 : 2; - for(channel_offset = 1; channel + channel_offset <= data->input_channels; channel_offset++){ + for(channel_offset = 1; channel + channel_offset <= data->channels && data->channel[channel + channel_offset - 1].type < exec_button; channel_offset++){ channels = channel + channel_offset - 1; //find end for this exec block - for(; channel + channel_offset < data->input_channels; channel_offset++){ - if(data->input_channel[channel + channel_offset].fields.page != page_index - || (data->input_channel[channels].fields.index / 100) != (data->input_channel[channel + channel_offset].fields.index / 100)){ + for(; channel + channel_offset < data->channels; channel_offset++){ + if(data->channel[channel + channel_offset].page != page_index + || data->channel[channels].type != data->channel[channel + channel_offset].type + || (data->channel[channels].index / 100) != (data->channel[channel + channel_offset].index / 100)){ break; } } //add request block for the exec block - offsets[0] += snprintf(item_indices + offsets[0], sizeof(item_indices) - offsets[0], "%d,", data->input_channel[channels].fields.index); - offsets[1] += snprintf(item_counts + offsets[1], sizeof(item_counts) - offsets[1], "%d,", data->input_channel[channel + channel_offset - 1].fields.index - data->input_channel[channels].fields.index + 1); - offsets[2] += snprintf(item_types + offsets[2], sizeof(item_types) - offsets[2], "%d,", (data->input_channel[channels].fields.index < 100) ? 2 : 3); - - //send on page boundary, metamode boundary, last channel - if(channel + channel_offset >= data->input_channels - || data->input_channel[channel + channel_offset].fields.page != page_index - || (data->input_channel[channel].fields.index < 300) != (data->input_channel[channel + channel_offset].fields.index < 300)){ + offsets[0] += snprintf(item_indices + offsets[0], sizeof(item_indices) - offsets[0], "%d,", data->channel[channels].index); + offsets[1] += snprintf(item_counts + offsets[1], sizeof(item_counts) - offsets[1], "%d,", data->channel[channel + channel_offset - 1].index - data->channel[channels].index + 1); + offsets[2] += snprintf(item_types + offsets[2], sizeof(item_types) - offsets[2], "%d,", (data->channel[channels].index < 100) ? 2 : 3); + + //send on last channel, page boundary, metamode boundary + if(channel + channel_offset >= data->channels + || data->channel[channel + channel_offset].page != page_index + || (data->channel[channel].index < 300) != (data->channel[channel + channel_offset].index < 300)){ break; } } @@ -475,17 +492,18 @@ static int maweb_request_playbacks(instance* inst){ } else{ //for the ma, the view equals the exec type requested (we can query all button execs from button view, all fader execs from fader view) - view = (data->input_channel[channel].fields.index >= 100) ? 3 : 2; + view = (data->channel[channel].index >= 100) ? 3 : 2; snprintf(item_types, sizeof(item_types), "[%" PRIsize_t "]", view); //this channel must be included, so it must be in range for the first startindex - snprintf(item_indices, sizeof(item_indices), "[%d]", (data->input_channel[channel].fields.index / 5) * 5); + snprintf(item_indices, sizeof(item_indices), "[%d]", (data->channel[channel].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++){ + for(channel_offset = 1; channel + channel_offset < data->channels + && data->channel[channel].type == data->channel[channel + channel_offset].type + && data->channel[channel].page == data->channel[channel + channel_offset].page + && data->channel[channel].index / 100 == data->channel[channel + channel_offset].index / 100; channel_offset++){ } - channels = data->input_channel[channel + channel_offset - 1].fields.index - (data->input_channel[channel].fields.index / 5) * 5; + channels = data->channel[channel + channel_offset - 1].index - (data->channel[channel].index / 5) * 5; snprintf(item_counts, sizeof(item_indices), "[%" PRIsize_t "]", ((channels / 5) * 5 + 5)); } @@ -549,7 +567,12 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le DBGPF("maweb message (%" PRIsize_t "): %s\n", payload_length, payload); if(json_obj(payload, "session") == JSON_NUMBER){ data->session = json_obj_int(payload, "session", data->session); - fprintf(stderr, "maweb session id is now %" PRIu64 "\n", data->session); + if(data->session < 0){ + fprintf(stderr, "maweb login failed\n"); + data->login = 0; + return 0; + } + fprintf(stderr, "maweb session id is now %" PRId64 "\n", data->session); } if(json_obj_bool(payload, "forceLogin", 0)){ @@ -768,8 +791,8 @@ static int maweb_handle_fd(instance* inst){ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ maweb_instance_data* data = (maweb_instance_data*) inst->impl; + maweb_channel_data* chan = NULL; char xmit_buffer[MAWEB_XMIT_CHUNK]; - maweb_channel_ident ident; size_t n; if(num && !data->login){ @@ -778,8 +801,23 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ } for(n = 0; n < num; n++){ - ident.label = c[n]->ident; - switch(ident.fields.type){ + //sanity check + if(c[n]->ident >= data->channels){ + return 1; + } + chan = data->channel + c[n]->ident; + + //channel state tracking + if(chan->out == v[n].normalised){ + continue; + } + chan->out = v[n].normalised; + + //i/o value space separation + chan->in = v[n].normalised; + chan->input_blocked = 1; + + switch(chan->type){ case exec_fader: snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"requestType\":\"playbacks_userInput\"," @@ -788,8 +826,7 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"faderValue\":%f," "\"type\":1," "\"session\":%" PRIu64 - "}", ident.fields.index, ident.fields.page, v[n].normalised, data->session); - maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + "}", chan->index, chan->page, v[n].normalised, data->session); break; case exec_upper: case exec_lower: @@ -804,26 +841,26 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"released\":%s," "\"type\":0," "\"session\":%" PRIu64 - "}", ident.fields.index, ident.fields.page, - (data->peer_type == peer_dot2 && ident.fields.type == exec_upper) ? 0 : (ident.fields.type - exec_button), + "}", chan->index, chan->page, + (data->peer_type == peer_dot2 && chan->type == exec_upper) ? 0 : (chan->type - exec_button), (v[n].normalised > 0.9) ? "true" : "false", (v[n].normalised > 0.9) ? "false" : "true", data->session); - maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); break; - case cmdline_button: + case cmdline: snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"keyname\":\"%s\"," //"\"autoSubmit\":false," "\"value\":%d" - "}", cmdline_keys[ident.fields.index], + "}", cmdline_keys[chan->index], (v[n].normalised > 0.9) ? 1 : 0); - maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); break; + //TODO cmdline_local default: fprintf(stderr, "maweb control not yet implemented\n"); - break; + return 1; } + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); } return 0; } @@ -912,7 +949,7 @@ 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); + qsort(data->channel, data->channels, sizeof(maweb_channel_data), channel_comparator); if(maweb_connect(inst[u])){ fprintf(stderr, "Failed to open connection to MA Web Remote for instance %s\n", inst[u]->name); @@ -964,9 +1001,9 @@ 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(data->channel); + data->channel = NULL; + data->channels = 0; } free(inst); diff --git a/backends/maweb.h b/backends/maweb.h index a868426..3738367 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -25,7 +25,8 @@ typedef enum /*_maweb_channel_type*/ { exec_button = 2, //gma: 0 dot: 0 exec_lower = 3, //gma: 1 dot: 1 exec_upper = 4, //gma: 2 dot: 0 - cmdline_button + cmdline, + cmdline_local } maweb_channel_type; typedef enum /*_maweb_peer_type*/ { @@ -49,15 +50,16 @@ typedef enum /*_ws_frame_op*/ { 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_channel*/ { + maweb_channel_type type; + uint16_t page; + uint16_t index; + + uint8_t input_blocked; + + double in; + double out; +} maweb_channel_data; typedef struct /*_maweb_instance_data*/ { char* host; @@ -69,9 +71,8 @@ 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; + size_t channels; + maweb_channel_data* channel; int fd; maweb_state state; -- cgit v1.2.3 From 2822e7b6d10084adac5d35f26c2b158ccb91cd50 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 8 Sep 2019 09:35:11 +0200 Subject: Fix duplicate poll requests --- backends/maweb.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index ca8a47d..905a1eb 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -346,8 +346,6 @@ static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_ty exec_blocks += json_obj_offset(payload + exec_blocks, "items"); } - //TODO detect unused faders - //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"); @@ -362,6 +360,7 @@ static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_ty } } else{ + //block input immediately after channel set to prevent feedback loops data->channel[channel_index].input_blocked--; } } @@ -508,9 +507,19 @@ static int maweb_request_playbacks(instance* inst){ snprintf(item_counts, sizeof(item_indices), "[%" PRIsize_t "]", ((channels / 5) * 5 + 5)); } + DBGPF("maweb poll range first %d: %d.%d last %d: %d.%d next %d: %d.%d\n", + data->channel[channel].type, data->channel[channel].page, data->channel[channel].index, + data->channel[channel + channel_offset - 1].type, data->channel[channel + channel_offset - 1].page, data->channel[channel + channel_offset - 1].index, + data->channel[channel + channel_offset].type, data->channel[channel + channel_offset].page, data->channel[channel + channel_offset].index); + //advance base channel channel += channel_offset - 1; + //fader flash buttons have the same type as button execs, but can't be requested + if(data->channel[channel].index < 100 && data->channel[channel].type == exec_button){ + continue; + } + //send current request snprintf(xmit_buffer, sizeof(xmit_buffer), "{" -- cgit v1.2.3 From 9997c446677e490a0d376701f838e804c3b28644 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 14 Sep 2019 21:19:56 +0200 Subject: Fix maweb polling --- backends/maweb.c | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index 905a1eb..88f7b9a 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -124,15 +124,24 @@ static ssize_t maweb_channel_index(maweb_instance_data* data, maweb_channel_type static int channel_comparator(const void* raw_a, const void* raw_b){ maweb_channel_data* a = (maweb_channel_data*) raw_a; maweb_channel_data* b = (maweb_channel_data*) raw_b; - + //this needs to take into account command line channels //they need to be sorted last so that the channel poll logic works properly - if(a->type != b->type){ - return a->type - b->type; - } if(a->page != b->page){ return a->page - b->page; } + //execs and their components are sorted by index first, type second + if(a->type < cmdline && b->type < cmdline){ + if(a->index != b->index){ + return a->index - b->index; + } + return a->type - b->type; + + } + //if either one is not an exec, sort by type first, index second + if(a->type != b->type){ + return a->type - b->type; + } return a->index - b->index; } @@ -153,9 +162,6 @@ static int maweb_configure(char* option, char* value){ 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); @@ -180,6 +186,8 @@ static int maweb_configure_instance(instance* inst, char* option, char* value){ else if(!strcmp(option, "password")){ #ifndef MAWEB_NO_LIBSSL size_t n; + uint8_t password_hash[MD5_DIGEST_LENGTH]; + MD5((uint8_t*) value, strlen(value), (uint8_t*) password_hash); data->pass = realloc(data->pass, (2 * MD5_DIGEST_LENGTH + 1) * sizeof(char)); for(n = 0; n < MD5_DIGEST_LENGTH; n++){ @@ -192,7 +200,7 @@ static int maweb_configure_instance(instance* inst, char* option, char* value){ #endif } - fprintf(stderr, "Unknown configuration parameter %s for manet instance %s\n", option, inst->name); + fprintf(stderr, "Unknown configuration parameter %s for maweb instance %s\n", option, inst->name); return 1; } @@ -328,9 +336,9 @@ static int maweb_send_frame(instance* inst, maweb_operation op, uint8_t* payload static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_type metatype, char* payload, size_t payload_length){ maweb_instance_data* data = (maweb_instance_data*) inst->impl; size_t exec_blocks = json_obj_offset(payload, (metatype == 2) ? "executorBlocks" : "bottomButtons"), offset, block = 0, control; - channel_value evt; int64_t exec_index = json_obj_int(payload, "iExec", 191); ssize_t channel_index; + channel_value evt; if(!exec_blocks){ if(metatype == 3){ @@ -449,23 +457,22 @@ static int maweb_request_playbacks(instance* inst){ return 0; } - //TODO this needs to ignore non-metamoded execs - //don't quote me on this whole segment - //only request faders and buttons - for(channel = 0; channel < data->channels && data->channel[channel].type <= exec_button; channel++){ + for(channel = 0; channel < data->channels && data->channel[channel].type < cmdline; channel++){ offsets[0] = offsets[1] = offsets[2] = 1; page_index = data->channel[channel].page; + //poll logic differs between the consoles because reasons + //dont quote me on this section if(data->peer_type == peer_dot2){ //blocks 0, 100 & 200 have 21 execs and need to be queried from fader view view = (data->channel[channel].index >= 300) ? 3 : 2; - for(channel_offset = 1; channel + channel_offset <= data->channels && data->channel[channel + channel_offset - 1].type < exec_button; channel_offset++){ + for(channel_offset = 1; channel + channel_offset <= data->channels + && data->channel[channel + channel_offset].type < cmdline; channel_offset++){ channels = channel + channel_offset - 1; //find end for this exec block for(; channel + channel_offset < data->channels; channel_offset++){ if(data->channel[channel + channel_offset].page != page_index - || data->channel[channels].type != data->channel[channel + channel_offset].type || (data->channel[channels].index / 100) != (data->channel[channel + channel_offset].index / 100)){ break; } @@ -496,14 +503,14 @@ static int maweb_request_playbacks(instance* inst){ //this channel must be included, so it must be in range for the first startindex snprintf(item_indices, sizeof(item_indices), "[%d]", (data->channel[channel].index / 5) * 5); + //find end of exec block for(channel_offset = 1; channel + channel_offset < data->channels - && data->channel[channel].type == data->channel[channel + channel_offset].type && data->channel[channel].page == data->channel[channel + channel_offset].page && data->channel[channel].index / 100 == data->channel[channel + channel_offset].index / 100; channel_offset++){ } + //gma execs are grouped in blocks of 5 channels = data->channel[channel + channel_offset - 1].index - (data->channel[channel].index / 5) * 5; - snprintf(item_counts, sizeof(item_indices), "[%" PRIsize_t "]", ((channels / 5) * 5 + 5)); } @@ -515,11 +522,6 @@ static int maweb_request_playbacks(instance* inst){ //advance base channel channel += channel_offset - 1; - //fader flash buttons have the same type as button execs, but can't be requested - if(data->channel[channel].index < 100 && data->channel[channel].type == exec_button){ - continue; - } - //send current request snprintf(xmit_buffer, sizeof(xmit_buffer), "{" @@ -544,6 +546,7 @@ static int maweb_request_playbacks(instance* inst){ updates_inflight++; } + DBGPF("maweb poll request handling done, %" PRIu64 " updates requested\n", updates_inflight); return rv; } -- cgit v1.2.3 From 1c8d7c570678c2f4f3bf61529489336f9d085f8d Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 15 Sep 2019 23:09:44 +0200 Subject: Fix minor MIDI issues --- backends/midi.c | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) (limited to 'backends') diff --git a/backends/midi.c b/backends/midi.c index 571a61d..65ce48a 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -84,14 +84,14 @@ static instance* midi_instance(){ return inst; } -static int midi_configure_instance(instance* instance, char* option, char* value){ - midi_instance_data* data = (midi_instance_data*) instance->impl; +static int midi_configure_instance(instance* inst, char* option, char* value){ + midi_instance_data* data = (midi_instance_data*) inst->impl; //FIXME maybe allow connecting more than one device if(!strcmp(option, "read")){ //connect input device if(data->read){ - fprintf(stderr, "MIDI port already connected to an input device\n"); + fprintf(stderr, "MIDI instance %s was already connected to an input device\n", inst->name); return 1; } data->read = strdup(value); @@ -100,7 +100,7 @@ static int midi_configure_instance(instance* instance, char* option, char* value else if(!strcmp(option, "write")){ //connect output device if(data->write){ - fprintf(stderr, "MIDI port already connected to an output device\n"); + fprintf(stderr, "MIDI instance %s was already connected to an output device\n", inst->name); return 1; } data->write = strdup(value); @@ -111,7 +111,7 @@ static int midi_configure_instance(instance* instance, char* option, char* value return 1; } -static channel* midi_channel(instance* instance, char* spec){ +static channel* midi_channel(instance* inst, char* spec){ midi_channel_ident ident = { .label = 0 }; @@ -142,13 +142,13 @@ static channel* midi_channel(instance* instance, char* spec){ old_syntax = 1; } else{ - fprintf(stderr, "Unknown MIDI channel specification %s\n", spec); + fprintf(stderr, "Unknown MIDI channel control type in %s\n", spec); return NULL; } ident.fields.channel = strtoul(channel, &channel, 10); if(ident.fields.channel > 15){ - fprintf(stderr, "MIDI channel out of range in channel spec %s\n", spec); + fprintf(stderr, "MIDI channel out of range in midi channel spec %s\n", spec); return NULL; } @@ -176,18 +176,22 @@ static channel* midi_channel(instance* instance, char* spec){ ident.fields.type = pressure; channel += 8; } - else if(!strncmp(channel, "pitch", 8)){ + else if(!strncmp(channel, "pitch", 5)){ ident.fields.type = pitchbend; } else if(!strncmp(channel, "aftertouch", 10)){ ident.fields.type = aftertouch; } + else{ + fprintf(stderr, "Unknown MIDI channel control type in %s\n", spec); + return NULL; + } } ident.fields.control = strtoul(channel, NULL, 10); if(ident.label){ - return mm_channel(instance, ident.label, 1); + return mm_channel(inst, ident.label, 1); } return NULL; @@ -196,13 +200,12 @@ static channel* midi_channel(instance* instance, char* spec){ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){ size_t u; snd_seq_event_t ev; - midi_instance_data* data; + midi_instance_data* data = (midi_instance_data*) inst->impl; midi_channel_ident ident = { .label = 0 }; for(u = 0; u < num; u++){ - data = (midi_instance_data*) c[u]->instance->impl; ident.label = c[u]->ident; snd_seq_ev_clear(&ev); @@ -334,7 +337,7 @@ static int midi_handle(size_t num, managed_fd* fds){ } static int midi_start(){ - size_t n, p; + size_t n = 0, p; int nfds, rv = 1; struct pollfd* pfds = NULL; instance** inst = NULL; -- 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 --- backends/Makefile | 5 +- backends/winmidi.c | 567 ++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/winmidi.h | 43 ++++ backends/winmidi.md | 59 ++++++ 4 files changed, 673 insertions(+), 1 deletion(-) create mode 100644 backends/winmidi.c create mode 100644 backends/winmidi.h create mode 100644 backends/winmidi.md (limited to 'backends') 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 678396ed4c1a146a110cb15c7b7ad8cf3d6ef224 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 17 Sep 2019 22:13:30 +0200 Subject: Minor documentation fixes --- backends/winmidi.c | 1 - backends/winmidi.md | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'backends') diff --git a/backends/winmidi.c b/backends/winmidi.c index dd8442b..e121add 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -21,7 +21,6 @@ static struct { .socket_pair = {-1, -1} }; -//TODO allow connect-device specification by index //TODO detect option int init(){ diff --git a/backends/winmidi.md b/backends/winmidi.md index b1fde1e..f648240 100644 --- a/backends/winmidi.md +++ b/backends/winmidi.md @@ -20,11 +20,12 @@ some deviations may still be present. | `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. +MIDI device names may either be prefixes of MIDI device names or a numeric indices corresponding to the listing shown +at startup when using the global `list` option. #### Channel specification -The MIDI backend supports mapping different MIDI events to MIDIMonster channels. The currently supported event types are +The `winmidi` backend supports mapping different MIDI events as MIDIMonster channels. The currently supported event types are * `cc` - Control Changes * `note` - Note On/Off messages -- cgit v1.2.3 From 3c44cd1ba564e9816f5cfb94fff6be5dca0ada3d Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 17 Sep 2019 22:18:50 +0200 Subject: Fix typo --- backends/winmidi.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/winmidi.md b/backends/winmidi.md index f648240..25a6378 100644 --- a/backends/winmidi.md +++ b/backends/winmidi.md @@ -20,8 +20,8 @@ some deviations may still be present. | `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 indices corresponding to the listing shown -at startup when using the global `list` option. +Input/output device names may either be prefixes of MIDI device names or numeric indices corresponding +to the listing shown at startup when using the global `list` option. #### Channel specification -- cgit v1.2.3 From 262210090bed5967157ce67dc1b52d43ba9cbdb9 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 17 Sep 2019 22:23:41 +0200 Subject: Don't build winmidi with DEBUG --- backends/winmidi.c | 1 - 1 file changed, 1 deletion(-) (limited to 'backends') diff --git a/backends/winmidi.c b/backends/winmidi.c index e121add..2c52a22 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -3,7 +3,6 @@ #include "libmmbackend.h" #include -#define DEBUG #include "winmidi.h" #define BACKEND_NAME "winmidi" -- cgit v1.2.3 From cd59a560d8b182ce50ea1edae1c0f2a0a89e76c0 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 18 Sep 2019 00:57:35 +0200 Subject: Fix winmidi feedback connection --- backends/maweb.c | 2 +- backends/winmidi.c | 39 +++++++++++++++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 5 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index 88f7b9a..c88ca8c 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -591,7 +591,7 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le fprintf(stderr, "maweb sending user credentials\n"); snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"requestType\":\"login\",\"username\":\"%s\",\"password\":\"%s\",\"session\":%" PRIu64 "}", - (data->peer_type == peer_dot2) ? "remote" : data->user, data->pass, data->session); + (data->peer_type == peer_dot2) ? "remote" : data->user, data->pass ? data->pass : MAWEB_DEFAULT_PASSWORD, data->session); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); } if(json_obj(payload, "status") && json_obj(payload, "appType")){ diff --git a/backends/winmidi.c b/backends/winmidi.c index 2c52a22..4c72c32 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -181,6 +181,11 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v }; size_t u; + //early exit + if(!num){ + return 0; + } + if(!data->device_out){ fprintf(stderr, "winmidi instance %s has no output device\n", inst->name); return 0; @@ -433,9 +438,12 @@ static int winmidi_start(){ int device, rv = -1; instance** inst = NULL; winmidi_instance_data* data = NULL; - struct sockaddr_storage sockadd; + struct sockaddr_storage sockadd = { + 0 + }; //this really should be a size_t but getsockname specifies int* for some reason int sockadd_len = sizeof(sockadd); + char* error = NULL; DBGPF("winmidi main thread ID is %ld\n", GetCurrentThreadId()); //fetch all instances @@ -457,18 +465,41 @@ static int winmidi_start(){ } //open the feedback sockets - backend_config.socket_pair[0] = mmbackend_socket(NULL, "0", SOCK_DGRAM, 1, 0); + //for some reason this while construct fails to work on 'real' windows with ipv6 + backend_config.socket_pair[0] = mmbackend_socket("127.0.0.1", "0", SOCK_DGRAM, 1, 0); if(backend_config.socket_pair[0] < 0){ fprintf(stderr, "winmidi failed to open feedback socket\n"); return 1; } if(getsockname(backend_config.socket_pair[0], (struct sockaddr*) &sockadd, &sockadd_len)){ - fprintf(stderr, "winmidi failed to query feedback socket information\n"); + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); + fprintf(stderr, "winmidi failed to query feedback socket information: %s\n", error); + LocalFree(error); return 1; } + //getsockname on 'real' windows may not set the adress - works on wine, though + switch(sockadd.ss_family){ + case AF_INET: + case AF_INET6: + ((struct sockaddr_in*) &sockadd)->sin_family = AF_INET; + ((struct sockaddr_in*) &sockadd)->sin_addr.s_addr = htobe32(INADDR_LOOPBACK); + break; + //for some absurd reason 'real' windows announces the socket as AF_INET6 but rejects any connection unless its AF_INET +// case AF_INET6: +// ((struct sockaddr_in6*) &sockadd)->sin6_addr = in6addr_any; +// break; + default: + fprintf(stderr, "winmidi invalid feedback socket family\n"); + return 1; + } + DBGPF("winmidi feedback socket family %d port %d\n", sockadd.ss_family, be16toh(((struct sockaddr_in*)&sockadd)->sin_port)); backend_config.socket_pair[1] = socket(sockadd.ss_family, SOCK_DGRAM, IPPROTO_UDP); if(backend_config.socket_pair[1] < 0 || connect(backend_config.socket_pair[1], (struct sockaddr*) &sockadd, sockadd_len)){ - fprintf(stderr, "winmidi failed to connect to feedback socket\n"); + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); + fprintf(stderr, "winmidi failed to connect to feedback socket: %s\n", error); + LocalFree(error); return 1; } -- cgit v1.2.3 From 82f6b03929315d65693155d3274a79d4a6285896 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 18 Sep 2019 01:10:26 +0200 Subject: Fix spelling & optics --- backends/winmidi.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'backends') diff --git a/backends/winmidi.c b/backends/winmidi.c index 4c72c32..67187ac 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -478,7 +478,7 @@ static int winmidi_start(){ LocalFree(error); return 1; } - //getsockname on 'real' windows may not set the adress - works on wine, though + //getsockname on 'real' windows may not set the address - works on wine, though switch(sockadd.ss_family){ case AF_INET: case AF_INET6: -- cgit v1.2.3 From 079baff220a963c365ab8448c421e22e896caaf1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 18 Sep 2019 23:44:36 +0200 Subject: Fix maweb command key handling --- backends/maweb.c | 189 +++++++++++++++++++++++++++++++---------------------- backends/maweb.h | 18 ++++- backends/maweb.md | 76 ++++++++++++++------- backends/winmidi.c | 3 +- 4 files changed, 181 insertions(+), 105 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index c88ca8c..be356d0 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -19,72 +19,57 @@ static uint64_t update_interval = 50; static uint64_t last_update = 0; static uint64_t updates_inflight = 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" +static maweb_command_key cmdline_keys[] = { + {"PREV", 109, 0, 1}, {"SET", 108, 1, 0, 1}, {"NEXT", 110, 0, 1}, + {"TIME", 58, 1, 1}, {"EDIT", 55, 1, 1}, {"UPDATE", 57, 1, 1}, + {"OOPS", 53, 1, 1}, {"ESC", 54, 1, 1}, {"CLEAR", 105, 1, 1}, + {"0", 86, 1, 1}, {"1", 87, 1, 1}, {"2", 88, 1, 1}, + {"3", 89, 1, 1}, {"4", 90, 1, 1}, {"5", 91, 1, 1}, + {"6", 92, 1, 1}, {"7", 93, 1, 1}, {"8", 94, 1, 1}, + {"9", 95, 1, 1}, {"PUNKT", 98, 1, 1}, {"ENTER", 106, 1, 1}, + {"PLUS", 96, 1, 1}, {"MINUS", 97, 1, 1}, {"THRU", 102, 1, 1}, + {"IF", 103, 1, 1}, {"AT", 104, 1, 1}, {"FULL", 99, 1, 1}, + {"MA", 68, 0, 1}, {"HIGH", 100, 1, 1, 1}, {"SOLO", 101, 1, 1, 1}, + {"SELECT", 42, 1, 1}, {"OFF", 43, 1, 1}, {"ON", 46, 1, 1}, + {"ASSIGN", 63, 1, 1}, {"LABEL", 0, 1, 1}, + {"COPY", 73, 1, 1}, {"DELETE", 69, 1, 1}, {"STORE", 59, 1, 1}, + {"GOTO", 56, 1, 1}, {"PAGE", 70, 1, 1}, {"MACRO", 71, 1, 1}, + {"PRESET", 72, 1, 1}, {"SEQU", 74, 1, 1}, {"CUE", 75, 1, 1}, + {"EXEC", 76, 1, 1}, {"FIXTURE", 83, 1, 1}, {"GROUP", 84, 1, 1}, + {"GO_MINUS", 10, 1, 1}, {"PAUSE", 9, 1, 1}, {"GO_PLUS", 11, 1, 1}, + + {"FIXTURE_CHANNEL", 0, 1, 1}, {"FIXTURE_GROUP_PRESET", 0, 1, 1}, + {"EXEC_CUE", 0, 1, 1}, {"STORE_UPDATE", 0, 1, 1}, {"PROG_ONLY", 0, 1, 1, 1}, + {"SPECIAL_DIALOGUE", 0, 1, 1}, + {"ODD", 0, 1, 1}, {"EVEN", 0, 1, 1}, + {"WINGS", 0, 1, 1}, {"RESET", 0, 1, 1}, + //gma2 internal only + {"CHPGPLUS", 3}, {"CHPGMINUS", 4}, + {"FDPGPLUS", 5}, {"FDPGMINUS", 6}, + {"BTPGPLUS", 7}, {"BTPGMINUS", 8}, + {"X1", 12}, {"X2", 13}, {"X3", 14}, + {"X4", 15}, {"X5", 16}, {"X6", 17}, + {"X7", 18}, {"X8", 19}, {"X9", 20}, + {"X10", 21}, {"X11", 22}, {"X12", 23}, + {"X13", 24}, {"X14", 25}, {"X15", 26}, + {"X16", 27}, {"X17", 28}, {"X18", 29}, + {"X19", 30}, {"X20", 31}, + {"V1", 120}, {"V2", 121}, {"V3", 122}, + {"V4", 123}, {"V5", 124}, {"V6", 125}, + {"V7", 126}, {"V8", 127}, {"V9", 128}, + {"V10", 129}, + {"NIPPLE", 40}, + {"TOOLS", 119}, {"SETUP", 117}, {"BACKUP", 117}, + {"BLIND", 60}, {"FREEZE", 61}, {"PREVIEW", 62}, + {"FIX", 41}, {"TEMP", 44}, {"TOP", 45}, + {"VIEW", 66}, {"EFFECT", 67}, {"CHANNEL", 82}, + {"MOVE", 85}, {"BLACKOUT", 65}, + {"PLEASE", 106}, + {"LIST", 32}, {"USER1", 33}, {"USER2", 34}, + {"ALIGN", 64}, {"HELP", 116}, + {"UP", 107}, {"DOWN", 111}, + {"FASTREVERSE", 47}, {"LEARN", 48}, {"FASTFORWARD", 49}, + {"GO_MINUS_SMALL", 50}, {"PAUSE_SMALL", 51}, {"GO_PLUS_SMALL", 52} }; int init(){ @@ -199,6 +184,22 @@ static int maweb_configure_instance(instance* inst, char* option, char* value){ return 1; #endif } + else if(!strcmp(option, "cmdline")){ + if(!strcmp(value, "console")){ + data->cmdline = cmd_console; + } + else if(!strcmp(value, "remote")){ + data->cmdline = cmd_remote; + } + else if(!strcmp(value, "downgrade")){ + data->cmdline = cmd_downgrade; + } + else{ + fprintf(stderr, "Unknown maweb commandline mode %s for instance %s\n", value, inst->name); + return 1; + } + return 0; + } fprintf(stderr, "Unknown configuration parameter %s for maweb instance %s\n", option, inst->name); return 1; @@ -268,10 +269,15 @@ static channel* maweb_channel(instance* inst, char* spec){ chan.index = strtoul(next_token, NULL, 10); } else{ - for(n = 0; n < sizeof(cmdline_keys) / sizeof(char*); n++){ - //FIXME this is broken for layerMode - if(!strcmp(spec, cmdline_keys[n]) || (*spec == 'l' && !strcmp(spec + 1, cmdline_keys[n]))){ - chan.type = (*spec == 'l') ? cmdline_local : cmdline; + for(n = 0; n < sizeof(cmdline_keys) / sizeof(maweb_command_key); n++){ + if(!strcmp(spec, cmdline_keys[n].name)){ + if((data->cmdline == cmd_remote && !cmdline_keys[n].press && !cmdline_keys[n].release) + || (data->cmdline == cmd_console && !cmdline_keys[n].lua)){ + fprintf(stderr, "maweb cmdline key %s does not work with the current commandline mode for instance %s\n", spec, inst->name); + return NULL; + } + + chan.type = cmdline; chan.index = n + 1; chan.page = 1; break; @@ -599,6 +605,8 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le field = json_obj_str(payload, "appType", NULL); if(!strncmp(field, "dot2", 4)){ data->peer_type = peer_dot2; + //the dot2 can't handle lua commands + data->cmdline = cmd_remote; } else if(!strncmp(field, "gma2", 4)){ data->peer_type = peer_ma2; @@ -860,14 +868,41 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ data->session); break; case cmdline: - snprintf(xmit_buffer, sizeof(xmit_buffer), - "{\"keyname\":\"%s\"," - //"\"autoSubmit\":false," - "\"value\":%d" - "}", cmdline_keys[chan->index], - (v[n].normalised > 0.9) ? 1 : 0); + if(cmdline_keys[chan->index].lua + && (data->cmdline == cmd_console || data->cmdline == cmd_downgrade) + && data->peer_type != peer_dot2){ + //push canbus events + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{\"command\":\"LUA 'gma.canbus.hardkey(%d, %s, false)'\"," + "\"requestType\":\"command\"," + "\"session\":%" PRIu64 + "}", cmdline_keys[chan->index].lua, + (v[n].normalised > 0.9) ? "true" : "false", + data->session); + } + else if((cmdline_keys[chan->index].press || cmdline_keys[chan->index].release) + && (data->cmdline != cmd_console)){ + //send press/release events if required + if((cmdline_keys[chan->index].press && v[n].normalised > 0.9) + || (cmdline_keys[chan->index].release && v[n].normalised < 0.9)){ + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{\"keyname\":\"%s\"," + "\"autoSubmit\":%s," + "\"value\":%d" + "}", cmdline_keys[chan->index].name, + cmdline_keys[chan->index].auto_submit ? "true" : "null", + (v[n].normalised > 0.9) ? 1 : 0); + } + else{ + continue; + } + } + else{ + fprintf(stderr, "maweb commandline key %s not executed on %s due to mode mismatch\n", + cmdline_keys[chan->index].name, inst->name); + continue; + } break; - //TODO cmdline_local default: fprintf(stderr, "maweb control not yet implemented\n"); return 1; diff --git a/backends/maweb.h b/backends/maweb.h index 3738367..b9de68f 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -25,8 +25,7 @@ typedef enum /*_maweb_channel_type*/ { exec_button = 2, //gma: 0 dot: 0 exec_lower = 3, //gma: 1 dot: 1 exec_upper = 4, //gma: 2 dot: 0 - cmdline, - cmdline_local + cmdline } maweb_channel_type; typedef enum /*_maweb_peer_type*/ { @@ -43,6 +42,12 @@ typedef enum /*_ws_conn_state*/ { ws_closed } maweb_state; +typedef enum /*_maweb_cmdline_mode*/ { + cmd_remote = 0, + cmd_console, + cmd_downgrade +} maweb_cmdline_mode; + typedef enum /*_ws_frame_op*/ { ws_text = 1, ws_binary = 2, @@ -50,6 +55,14 @@ typedef enum /*_ws_frame_op*/ { ws_pong = 10 } maweb_operation; +typedef struct { + char* name; + unsigned lua; + uint8_t press; + uint8_t release; + uint8_t auto_submit; +} maweb_command_key; + typedef struct /*_maweb_channel*/ { maweb_channel_type type; uint16_t page; @@ -73,6 +86,7 @@ typedef struct /*_maweb_instance_data*/ { size_t channels; maweb_channel_data* channel; + maweb_cmdline_mode cmdline; int fd; maweb_state state; diff --git a/backends/maweb.md b/backends/maweb.md index 93cd776..a18cde4 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -26,6 +26,14 @@ Web Remote. Set a web remote password using the option below the activation sett | `host` | `10.23.42.21 80` | none | Host address (and optional port) of the MA Web Remote | | `user` | `midimonster` | none | User for the remote session (GrandMA2) | | `password` | `midimonster` | `midimonster` | Password for the remote session | +| `cmdline` | `console` | `remote` | Commandline key handling mode (see below) | + +The per-instance command line mode may be one of `remote`, `console` or `downgrade`. The first option handles +command keys with a "virtual" commandline belonging to the Web Remote connection. Any commands entered are +not visible on the main console. The `console` mode is only available with GrandMA2 remotes and injects the +key events into the main console's command line. When connected to a dot2 console, the use of commandline keys +will not be possible. With the `downgrade` mode, keys are handled on the console if possible, falling back to +remote handling if not. #### Channel specification @@ -33,7 +41,7 @@ Currently, three types of MA controls can be assigned, with each having some sub * Fader executor * Button executor -* Command line buttons +* Command keys ##### Executors @@ -70,33 +78,52 @@ mw1.page2.button103 > mw1.page3.fader101 mw1.page2.button803 > mw1.page3.button516 ``` -##### Command line buttons +##### Command keys -Command line buttons will be pressed when the incoming event value is greater than `0.9` and released when it is less than that. +Command keys will be pressed when the incoming event value is greater than `0.9` and released when it is less than that. They can be mapped using the syntax ``` -mw1. +mw1. ``` -The following button names are recognized by the backend: - -| Supported | Command | Line | Keys | | | | -|---------------|---------------|---------------|---------------|-----------------------|-------------------------------|---------------| -| `SET` | `PREV` | `NEXT` | `CLEAR` | `FIXTURE_CHANNEL` | `FIXTURE_GROUP_PRESET` | `EXEC_CUE` | -| `STORE_UPDATE`| `OOPS` | `ESC` | `OFF` | `ON` | `MA` | `STORE` | -| `0` | `1` | `2` | `3` | `4` | `5` | `6` | -| `7` | `8` | `9` | `PUNKT` | `PLUS` | `MINUS` | `THRU` | -| `IF` | `AT` | `FULL` | `HIGH` | `ENTER` | `ASSIGN` | `LABEL` | -| `COPY` | `TIME` | `PAGE` | `MACRO` | `DELETE` | `GOTO` | `GO_PLUS` | -| `GO_MINUS` | `PAUSE` | `SELECT` | `FIXTURE` | `SEQU` | `CUE` | `PRESET` | -| `EDIT` | `UPDATE` | `EXEC` | `GROUP` | `PROG_ONLY` | `SPECIAL_DIALOGUE` | `SOLO` | -| `ODD` | `EVEN` | `WINGS` | `RESET` | `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. +The following keys are mappable in all commandline modes and work on all consoles + +| Supported | Command | Line | Keys | | | +|---------------|---------------|---------------|---------------|---------------|---------------| +| `PREV` | `SET` | `NEXT` | `TIME` | `EDIT` | `UPDATE` | +| `OOPS` | `ESC` | `CLEAR` | `0` | `1` | `2` | +| `3` | `4` | `5` | `6` | `7` | `8` | +| `9` | `PUNKT` | `ENTER` | `PLUS` | `MINUS` | `THRU` | +| `IF` | `AT` | `FULL` | `MA` | `HIGH` | `SOLO` | +| `SELECT` | `OFF` | `ON` | `ASSIGN` | `COPY` | `DELETE` | +| `STORE` | `GOTO` | `PAGE` | `MACRO` | `PRESET` | `SEQU` | +| `CUE` | `EXEC` | `FIXTURE` | `GROUP` | `GO_MINUS` | `PAUSE` | +| `GO_PLUS` | | | | | | + +The following keys only work in the `remote` or `downgrade` commandline mode, but on all consoles + +| Web | Remote | specific | | | +|---------------|-----------------------|-------------------------------|---------------|-----------------------| +| `LABEL` |`FIXTURE_CHANNEL` | `FIXTURE_GROUP_PRESET` | `EXEC_CUE` | `STORE_UPDATE` | +| `PROG_ONLY` | `SPECIAL_DIALOGUE` | `ODD` | `EVEN` | `WINGS` | +| `RESET` | | | | | + +The following keys only work in the `console` or `downgrade` command line modes on a GrandMA2 + +| GrandMA2 | console | only | | | | +|---------------|---------------|---------------|---------------|---------------|---------------| +| `CHPGPLUS` | `CHPGMINUS` | `FDPGPLUS` | `FDPGMINUS` | `BTPGPLUS` | `BTPGMINUS` | +| `X1` | `X2` | `X3` | `X4` | `X5` | `X6` | +| `X7` | `X8` | `X9` | `X10` | `X11` | `X12` | +| `X13` | `X14` | `X15` | `X16` | `X17` | `X18` | +| `X19` | `X20` | `V1` | `V2` | `V3` | `V4` | +| `V5` | `V6` | `V7` | `V8` | `V9` | `V10` | +| `NIPPLE` | `TOOLS` | `SETUP` | `BACKUP` | `BLIND` | `FREEZE` | +| `PREVIEW` | `FIX` | `TEMP` | `TOP` | `VIEW` | `EFFECT` | +| `CHANNEL` | `MOVE` | `BLACKOUT` | `PLEASE` | `LIST` | `USER1` | +| `USER2` | `ALIGN` | `HELP` | `UP` | `DOWN` | `FASTREVERSE` | +| `LEARN` | `FASTFORWARD` | `GO_MINUS_SMALL` | `PAUSE_SMALL` | `GO_PLUS_SMALL` | | #### Known bugs / problems @@ -109,6 +136,5 @@ Data input from the console is done by actively querying the state of all mapped at low latency. A lower input interval value will produce data with lower latency, at the cost of network & CPU usage. Higher values will make the input "step" more, but will not consume as many CPU cycles and network bandwidth. -When requesting button executor events on the fader pages (execs 101 to 222) of a dot2 console, map at least one fader control from the 0 - 22 range or input will not work due to strange limitations in the MA Web API. - -Command line events are sent, but I'm not sure they're being handled yet. +When requesting button executor events on the fader pages (execs 101 to 222) of a dot2 console, map at least one fader control from the 0 - 22 range +or input will not work due to strange limitations in the MA Web API. diff --git a/backends/winmidi.c b/backends/winmidi.c index 67187ac..13c4b4a 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -21,6 +21,7 @@ static struct { }; //TODO detect option +//TODO receive feedback socket until EAGAIN int init(){ backend winmidi = { @@ -465,7 +466,7 @@ static int winmidi_start(){ } //open the feedback sockets - //for some reason this while construct fails to work on 'real' windows with ipv6 + //for some reason the feedback connection fails to work on 'real' windows with ipv6 backend_config.socket_pair[0] = mmbackend_socket("127.0.0.1", "0", SOCK_DGRAM, 1, 0); if(backend_config.socket_pair[0] < 0){ fprintf(stderr, "winmidi failed to open feedback socket\n"); -- cgit v1.2.3 From fe4a7b538b9b6c53299d883bee258e4d3597be9d Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 20 Sep 2019 03:43:13 +0200 Subject: Update documentation, fix minor leaks --- backends/maweb.c | 1 - backends/maweb.md | 13 +++++++------ backends/midi.c | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index be356d0..450c388 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -121,7 +121,6 @@ static int channel_comparator(const void* raw_a, const void* raw_b){ return a->index - b->index; } return a->type - b->type; - } //if either one is not an exec, sort by type first, index second if(a->type != b->type){ diff --git a/backends/maweb.md b/backends/maweb.md index a18cde4..5b996a9 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -2,7 +2,8 @@ This backend connects directly with the integrated *MA Web Remote* of MA Lighting consoles and OnPC instances (GrandMA2 / GrandMA2 OnPC / GrandMA Dot2 / GrandMA Dot2 OnPC). -It grants read-write access to the console's playback controls as well as write access to the command line. +It grants read-write access to the console's playback controls as well as write access to most command +line and control keys. #### Setting up the console @@ -30,10 +31,10 @@ Web Remote. Set a web remote password using the option below the activation sett The per-instance command line mode may be one of `remote`, `console` or `downgrade`. The first option handles command keys with a "virtual" commandline belonging to the Web Remote connection. Any commands entered are -not visible on the main console. The `console` mode is only available with GrandMA2 remotes and injects the -key events into the main console's command line. When connected to a dot2 console, the use of commandline keys -will not be possible. With the `downgrade` mode, keys are handled on the console if possible, falling back to -remote handling if not. +not visible on the main console. The `console` mode is only available with GrandMA2 remotes and injects key events +into the main console. This mode also supports additional hardkeys that are only available on GrandMA consoles. +When connected to a dot2 console while this mode is active, the use of commandline keys will not be possible. +With the `downgrade` mode, keys are handled on the console if possible, falling back to remote handling if not. #### Channel specification @@ -101,7 +102,7 @@ The following keys are mappable in all commandline modes and work on all console | `CUE` | `EXEC` | `FIXTURE` | `GROUP` | `GO_MINUS` | `PAUSE` | | `GO_PLUS` | | | | | | -The following keys only work in the `remote` or `downgrade` commandline mode, but on all consoles +The following keys only work when keys are being handled with a virtual command line | Web | Remote | specific | | | |---------------|-----------------------|-------------------------------|---------------|-----------------------| diff --git a/backends/midi.c b/backends/midi.c index 65ce48a..d9ac698 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -358,7 +358,7 @@ static int midi_start(){ //connect to the sequencer if(snd_seq_open(&sequencer, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0){ fprintf(stderr, "Failed to open ALSA sequencer\n"); - return 0; + goto bail; } snd_seq_nonblock(sequencer, 1); @@ -367,7 +367,7 @@ static int midi_start(){ //update the sequencer client name if(snd_seq_set_client_name(sequencer, sequencer_name ? sequencer_name : "MIDIMonster") < 0){ fprintf(stderr, "Failed to set MIDI client name to %s\n", sequencer_name); - return 1; + goto bail; } //create all ports -- cgit v1.2.3 From e556b1719a8906c14d329eb3c08574428cad0aae Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 20 Sep 2019 17:58:27 +0200 Subject: Fix maweb channel ordering --- backends/maweb.c | 14 ++++++++++++-- backends/maweb.h | 4 ++++ 2 files changed, 16 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index 450c388..baa516a 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -5,6 +5,7 @@ #include #endif +#define DEBUG #include "libmmbackend.h" #include "maweb.h" @@ -235,6 +236,7 @@ static channel* maweb_channel(instance* inst, char* spec){ 0 }; char* next_token = NULL; + channel* channel_ref = NULL; size_t n; if(!strncmp(spec, "page", 4)){ @@ -299,7 +301,9 @@ static channel* maweb_channel(instance* inst, char* spec){ data->channels++; } - return mm_channel(inst, maweb_channel_index(data, chan.type, chan.page, chan.index), 1); + channel_ref = mm_channel(inst, maweb_channel_index(data, chan.type, chan.page, chan.index), 1); + data->channel[maweb_channel_index(data, chan.type, chan.page, chan.index)].chan = channel_ref; + return channel_ref; } fprintf(stderr, "Failed to parse maweb channel spec %s\n", spec); @@ -906,6 +910,7 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ fprintf(stderr, "maweb control not yet implemented\n"); return 1; } + DBGPF("maweb command out %s\n", xmit_buffer); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); } return 0; @@ -982,7 +987,7 @@ static int maweb_handle(size_t num, managed_fd* fds){ } static int maweb_start(){ - size_t n, u; + size_t n, u, p; instance** inst = NULL; maweb_instance_data* data = NULL; @@ -997,6 +1002,11 @@ static int maweb_start(){ data = (maweb_instance_data*) inst[u]->impl; qsort(data->channel, data->channels, sizeof(maweb_channel_data), channel_comparator); + //re-set channel identifiers + for(p = 0; p < data->channels; p++){ + data->channel[p].chan->ident = p; + } + if(maweb_connect(inst[u])){ fprintf(stderr, "Failed to open connection to MA Web Remote for instance %s\n", inst[u]->name); free(inst); diff --git a/backends/maweb.h b/backends/maweb.h index b9de68f..14e4755 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -72,6 +72,10 @@ typedef struct /*_maweb_channel*/ { double in; double out; + + //reverse reference required because the identifiers are not stable + //because we sort the backing store... + channel* chan; } maweb_channel_data; typedef struct /*_maweb_instance_data*/ { -- cgit v1.2.3 From 65bd41387c8dbf67812de1881198a47c9bb4b55e Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 5 Oct 2019 19:30:22 +0200 Subject: Implement detect option for winmidi --- backends/maweb.c | 7 ++++--- backends/midi.c | 3 +-- backends/winmidi.c | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 5 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index baa516a..c98d04b 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -5,7 +5,6 @@ #include #endif -#define DEBUG #include "libmmbackend.h" #include "maweb.h" @@ -891,10 +890,12 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"keyname\":\"%s\"," "\"autoSubmit\":%s," - "\"value\":%d" + "\"value\":%d," + "\"session\":%" PRIu64 "}", cmdline_keys[chan->index].name, cmdline_keys[chan->index].auto_submit ? "true" : "null", - (v[n].normalised > 0.9) ? 1 : 0); + (v[n].normalised > 0.9) ? 1 : 0, + data->session); } else{ continue; diff --git a/backends/midi.c b/backends/midi.c index d9ac698..536457d 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -56,8 +56,7 @@ static int midi_configure(char* option, char* value){ sequencer_name = strdup(value); return 0; } - - if(!strcmp(option, "detect")){ + else if(!strcmp(option, "detect")){ midi_config.detect = 1; if(!strcmp(value, "off")){ midi_config.detect = 0; diff --git a/backends/winmidi.c b/backends/winmidi.c index 13c4b4a..83d0c7e 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -9,6 +9,7 @@ static struct { uint8_t list_devices; + uint8_t detect; int socket_pair[2]; CRITICAL_SECTION push_events; @@ -60,6 +61,13 @@ static int winmidi_configure(char* option, char* value){ } return 0; } + else if(!strcmp(option, "detect")){ + backend_config.detect = 0; + if(!strcmp(value, "on")){ + backend_config.detect = 1; + } + return 0; + } fprintf(stderr, "Unknown winmidi backend option %s\n", option); return 1; @@ -232,6 +240,22 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v return 0; } +static char* winmidi_type_name(uint8_t typecode){ + switch(typecode){ + case note: + return "note"; + case cc: + return "cc"; + case pressure: + return "pressure"; + case aftertouch: + return "aftertouch"; + case pitchbend: + return "pitch"; + } + return "unknown"; +} + static int winmidi_handle(size_t num, managed_fd* fds){ size_t u; ssize_t bytes = 0; @@ -249,6 +273,23 @@ static int winmidi_handle(size_t num, managed_fd* fds){ //push queued events EnterCriticalSection(&backend_config.push_events); for(u = 0; u < backend_config.events_active; u++){ + if(backend_config.detect){ + //pretty-print channel-wide events + if(backend_config.event[u].channel.fields.type == pitchbend + || backend_config.event[u].channel.fields.type == aftertouch){ + fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s\n", + backend_config.event[u].inst->name, + backend_config.event[u].channel.fields.channel, + winmidi_type_name(backend_config.event[u].channel.fields.type)); + } + else{ + fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s%d\n", + backend_config.event[u].inst->name, + backend_config.event[u].channel.fields.channel, + winmidi_type_name(backend_config.event[u].channel.fields.type), + backend_config.event[u].channel.fields.control); + } + } 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); -- cgit v1.2.3 From 24e5594c754ec74918848d33d513db69d54aba47 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 3 Nov 2019 16:59:13 +0100 Subject: Fix winmidi wire format --- backends/lua.c | 3 --- backends/winmidi.c | 12 ++++++------ 2 files changed, 6 insertions(+), 9 deletions(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index 365cf3e..1cd965e 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -456,9 +456,6 @@ static int lua_start(){ } free(inst); - if(!n){ - return 0; - } #ifdef MMBACKEND_LUA_TIMERFD //register the timer with the core diff --git a/backends/winmidi.c b/backends/winmidi.c index 83d0c7e..de7d867 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -21,7 +21,6 @@ static struct { .socket_pair = {-1, -1} }; -//TODO detect option //TODO receive feedback socket until EAGAIN int init(){ @@ -123,8 +122,9 @@ static channel* winmidi_channel(instance* inst, char* spec){ next_token = spec + 7; } } - else{ - fprintf(stderr, "Unknown winmidi channel specification %s\n", spec); + + if(!next_token){ + fprintf(stderr, "Invalid winmidi channel specification %s\n", spec); return NULL; } @@ -226,8 +226,8 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v 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; + output.components.data1 = ((int)(v[u].normalised * 16384.0)) & 0x7F; + output.components.data2 = (((int)(v[u].normalised * 16384.0)) >> 7) & 0x7F; break; default: fprintf(stderr, "Unknown winmidi channel type %d\n", ident.fields.type); @@ -357,7 +357,7 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW case 0xE0: ident.fields.type = pitchbend; ident.fields.control = 0; - val.normalised = (double)((input.components.data2 << 8) | input.components.data1) / 32639.0; + val.normalised = (double)((input.components.data2 << 7) | input.components.data1) / 16384.0; break; default: fprintf(stderr, "winmidi unhandled status byte %02X\n", input.components.status); -- 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 --- backends/Makefile | 3 +- backends/jack.c | 742 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/jack.h | 76 ++++++ backends/jack.md | 84 +++++++ 4 files changed, 904 insertions(+), 1 deletion(-) create mode 100644 backends/jack.c create mode 100644 backends/jack.h create mode 100644 backends/jack.md (limited to 'backends') 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 7824feeec4477a1ade9bbf22a8a1609513bdaa2b Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 6 Nov 2019 19:05:01 +0100 Subject: Fix spelling to placate spellintian --- backends/jack.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'backends') diff --git a/backends/jack.c b/backends/jack.c index 5a88cf2..f73ada8 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -153,7 +153,7 @@ static int mmjack_process_cv(instance* inst, mmjack_port* port, size_t nframes, if(port->input){ //read updated data into the local buffer - //FIXME maybe we dont want to always use the first sample... + //FIXME maybe we don't want to always use the first sample... if((double) audio_buffer[0] != port->last){ port->last = audio_buffer[0]; port->mark = 1; -- 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 --- backends/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'backends') 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 20eb48ce4ccffe88b22ecd6a93bc9e097e5aa498 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 6 Nov 2019 21:22:09 +0100 Subject: Use default mutex type for OSX --- backends/jack.c | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'backends') diff --git a/backends/jack.c b/backends/jack.c index f73ada8..efcd8d5 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -607,7 +607,11 @@ static int mmjack_start(){ //prepare mutex attributes because the initializer macro for adaptive mutexes is a GNU extension... if(pthread_mutexattr_init(&mutex_attr) +#ifndef __APPLE__ || pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_ADAPTIVE_NP)){ +#else + } +#endif fprintf(stderr, "Failed to initialize mutex attributes\n"); goto bail; } -- cgit v1.2.3 From 5a79158ec0195cacbc8f4661dff5b26363797447 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 6 Nov 2019 21:26:56 +0100 Subject: Fix Coverity CIDs 350438, 350437 --- backends/jack.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'backends') diff --git a/backends/jack.c b/backends/jack.c index efcd8d5..48a6b68 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -550,13 +550,13 @@ static int mmjack_handle(size_t num, managed_fd* fds){ if(num){ for(u = 0; u < num; u++){ + inst = (instance*) fds[u].impl; + data = (mmjack_instance_data*) inst->impl; bytes = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); if(bytes < 0){ fprintf(stderr, "Failed to receive on feedback socket for instance %s\n", inst->name); return 1; } - 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){ @@ -672,14 +672,14 @@ static int mmjack_start(){ if(!data->port[p].port){ fprintf(stderr, "Failed to create jack port %s.%s\n", inst[u]->name, data->port[p].name); - return 1; + goto bail; } } //do the thing if(jack_activate(data->client)){ fprintf(stderr, "Failed to activate jack client for instance %s\n", inst[u]->name); - return 1; + goto bail; } } -- cgit v1.2.3 From b1f8732126f23cbd2345faa6ae326cd7b75bab5d Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 6 Nov 2019 21:30:46 +0100 Subject: Fix mutex type for OSX --- backends/jack.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'backends') diff --git a/backends/jack.c b/backends/jack.c index 48a6b68..b3aacd4 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -11,6 +11,12 @@ #define BACKEND_NAME "jack" #define JACKEY_SIGNAL_TYPE "http://jackaudio.org/metadata/signal-type" +#ifdef __APPLE__ + #ifndef PTHREAD_MUTEX_ADAPTIVE_NP + #define PTHREAD_MUTEX_ADAPTIVE_NP PTHREAD_MUTEX_DEFAULT + #endif +#endif + //FIXME pitchbend range is somewhat oob static struct /*_mmjack_backend_cfg*/ { @@ -607,11 +613,7 @@ static int mmjack_start(){ //prepare mutex attributes because the initializer macro for adaptive mutexes is a GNU extension... if(pthread_mutexattr_init(&mutex_attr) -#ifndef __APPLE__ || pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_ADAPTIVE_NP)){ -#else - } -#endif fprintf(stderr, "Failed to initialize mutex attributes\n"); goto bail; } -- cgit v1.2.3 From 350f0d2d2eaff5f0d57b09857102e2df1e96d733 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 7 Nov 2019 18:44:19 +0100 Subject: Makefile install target and packaging instructions (Fixes #28) --- backends/lua.md | 5 +---- backends/maweb.c | 2 +- backends/winmidi.c | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) (limited to 'backends') diff --git a/backends/lua.md b/backends/lua.md index 6ad5c2a..f38e189 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -43,7 +43,7 @@ The `lua` backend does not take any global configuration. | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| -| `script` | `script.lua` | none | Lua source file | +| `script` | `script.lua` | none | Lua source file (relative to configuration file)| A single instance may have multiple `source` options specified, which will all be read cumulatively. @@ -64,6 +64,3 @@ Using these names as arguments to the output and value interface functions works Output values will not trigger corresponding input event handlers unless the channel is mapped back in the MIDIMonster configuration. - -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/backends/maweb.c b/backends/maweb.c index c98d04b..57d04ae 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -470,7 +470,7 @@ static int maweb_request_playbacks(instance* inst){ offsets[0] = offsets[1] = offsets[2] = 1; page_index = data->channel[channel].page; //poll logic differs between the consoles because reasons - //dont quote me on this section + //don't quote me on this section if(data->peer_type == peer_dot2){ //blocks 0, 100 & 200 have 21 execs and need to be queried from fader view view = (data->channel[channel].index >= 300) ? 3 : 2; diff --git a/backends/winmidi.c b/backends/winmidi.c index de7d867..d6bc0bc 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -84,7 +84,7 @@ static int winmidi_configure_instance(instance* inst, char* option, char* value) } if(!strcmp(option, "write")){ if(data->write){ - fprintf(stderr, "winmidi instance %s already connected to an otput device\n", inst->name); + fprintf(stderr, "winmidi instance %s already connected to an output device\n", inst->name); return 1; } data->write = strdup(value); -- cgit v1.2.3 From 243d176936152bc2691a5594f76583948af70618 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 22 Nov 2019 14:45:33 +0100 Subject: Exclude buttons from feedback filtering for maweb backend --- backends/maweb.c | 4 +++- backends/winmidi.c | 14 ++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index 57d04ae..453dfa4 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -837,7 +837,9 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ //i/o value space separation chan->in = v[n].normalised; - chan->input_blocked = 1; + if(chan->type == exec_fader){ + chan->input_blocked = 1; + } switch(chan->type){ case exec_fader: diff --git a/backends/winmidi.c b/backends/winmidi.c index d6bc0bc..ffca3b4 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -277,17 +277,19 @@ static int winmidi_handle(size_t num, managed_fd* fds){ //pretty-print channel-wide events if(backend_config.event[u].channel.fields.type == pitchbend || backend_config.event[u].channel.fields.type == aftertouch){ - fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s\n", + fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s, value %f\n", backend_config.event[u].inst->name, backend_config.event[u].channel.fields.channel, - winmidi_type_name(backend_config.event[u].channel.fields.type)); + winmidi_type_name(backend_config.event[u].channel.fields.type), + backend_config.event[u].value); } else{ - fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s%d\n", + fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s%d, value %f\n", backend_config.event[u].inst->name, backend_config.event[u].channel.fields.channel, winmidi_type_name(backend_config.event[u].channel.fields.type), - backend_config.event[u].channel.fields.control); + backend_config.event[u].channel.fields.control, + backend_config.event[u].value); } } chan = mm_channel(backend_config.event[u].inst, backend_config.event[u].channel.label, 0); @@ -434,7 +436,7 @@ static int winmidi_match_input(char* 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); + fprintf(stderr, "winmidi selected input device %s (ID %" PRIsize_t ") for name %s\n", input_caps.szPname, n, prefix); return n; } } @@ -467,7 +469,7 @@ static int winmidi_match_output(char* 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); + fprintf(stderr, "winmidi selected output device %s (ID %" PRIsize_t " for name %s\n", output_caps.szPname, n, prefix); return n; } } -- cgit v1.2.3 From b60c1adbf3c66c42997020539f8bb8a0eb6250c9 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 22 Nov 2019 14:56:36 +0100 Subject: Update button control state on input not output --- backends/maweb.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index 453dfa4..c495512 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -835,10 +835,10 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ } chan->out = v[n].normalised; - //i/o value space separation - chan->in = v[n].normalised; + //i/o value space separation & feedback filtering for faders if(chan->type == exec_fader){ chan->input_blocked = 1; + chan->in = v[n].normalised; } switch(chan->type){ -- cgit v1.2.3 From d29a499113d389a8bee9bf21bf5969d41c9cf307 Mon Sep 17 00:00:00 2001 From: Leon Scheid Date: Thu, 21 Nov 2019 18:41:02 +0100 Subject: Chane NoteOff behavior --- backends/midi.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/midi.c b/backends/midi.c index 536457d..d609ad9 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -259,14 +259,20 @@ static int midi_handle(size_t num, managed_fd* fds){ ident.label = 0; switch(ev->type){ case SND_SEQ_EVENT_NOTEON: - case SND_SEQ_EVENT_NOTEOFF: - case SND_SEQ_EVENT_NOTE: ident.fields.type = note; ident.fields.channel = ev->data.note.channel; ident.fields.control = ev->data.note.note; val.normalised = (double)ev->data.note.velocity / 127.0; event_type = "note"; break; + case SND_SEQ_EVENT_NOTEOFF: + ident.fields.type = note; + ident.fields.channel = ev->data.note.channel; + ident.fields.control = ev->data.note.note; + val.normalised = (double)0; + event_type = "note"; + break; + case SND_SEQ_EVENT_NOTE: case SND_SEQ_EVENT_KEYPRESS: ident.fields.type = pressure; ident.fields.channel = ev->data.note.channel; -- cgit v1.2.3 From 1aa8b88a1df5539fa7ad4004cfcbf3b8438486b4 Mon Sep 17 00:00:00 2001 From: Leon Scheid Date: Fri, 22 Nov 2019 15:40:11 +0100 Subject: Remove unnecessary case --- backends/midi.c | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) (limited to 'backends') diff --git a/backends/midi.c b/backends/midi.c index d609ad9..cde278b 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -259,20 +259,17 @@ static int midi_handle(size_t num, managed_fd* fds){ ident.label = 0; switch(ev->type){ case SND_SEQ_EVENT_NOTEON: - ident.fields.type = note; - ident.fields.channel = ev->data.note.channel; - ident.fields.control = ev->data.note.note; - val.normalised = (double)ev->data.note.velocity / 127.0; - event_type = "note"; - break; case SND_SEQ_EVENT_NOTEOFF: + case SND_SEQ_EVENT_NOTE: ident.fields.type = note; ident.fields.channel = ev->data.note.channel; ident.fields.control = ev->data.note.note; - val.normalised = (double)0; + val.normalised = (double)ev->data.note.velocity / 127.0; + if(ev->type == SND_SEQ_EVENT_NOTEOFF){ + val.normalised = 0; + } event_type = "note"; break; - case SND_SEQ_EVENT_NOTE: case SND_SEQ_EVENT_KEYPRESS: ident.fields.type = pressure; ident.fields.channel = ev->data.note.channel; -- cgit v1.2.3 From 30feadde5b18f49fd853f8ce61d85168db912bb6 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 1 Dec 2019 13:30:43 +0100 Subject: Fix minor error in maweb documentation --- backends/maweb.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'backends') diff --git a/backends/maweb.md b/backends/maweb.md index 5b996a9..45dc778 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -53,7 +53,7 @@ Currently, three types of MA controls can be assigned, with each having some sub * 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), + * Above the fader sections are two rows of 21 `button` executors, numbered 122 through 101 (lower row) and 222 through 201 (upper row), in the same order as the faders are. * Fader executors have two buttons below them (`upper` and `lower`). * The button executor section consists of six rows of 16 buttons, divided into two button wings. Buttons on the wings -- 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 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'backends') 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) -- 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') 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') 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 From bd9ab579eba35db70ad1ce17d556aeda5866156d Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 4 Dec 2019 01:10:12 +0100 Subject: Hide lua_interval if timerfd callback mechanism is used --- backends/lua.c | 2 ++ backends/lua.h | 2 ++ 2 files changed, 4 insertions(+) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index 1cd965e..3555e72 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -52,6 +52,7 @@ int init(){ return 0; } +#ifndef MMBACKEND_LUA_TIMERFD static uint32_t lua_interval(){ size_t n = 0; uint64_t next_timer = 1000; @@ -66,6 +67,7 @@ static uint32_t lua_interval(){ } return 1000; } +#endif static int lua_update_timerfd(){ uint64_t interval = 0, gcd, residual; diff --git a/backends/lua.h b/backends/lua.h index 7aad891..f2583a8 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -18,7 +18,9 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v); static int lua_handle(size_t num, managed_fd* fds); static int lua_start(); static int lua_shutdown(); +#ifndef MMBACKEND_LUA_TIMERFD static uint32_t lua_interval(); +#endif typedef struct /*_lua_instance_data*/ { size_t channels; -- cgit v1.2.3 From 1107a91861189d28d771d02d721d61b403aac38a Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 4 Dec 2019 01:21:14 +0100 Subject: Explicitly mark the backend init symbol visible --- backends/artnet.c | 2 +- backends/artnet.h | 2 +- backends/evdev.c | 2 +- backends/evdev.h | 2 +- backends/jack.c | 4 +++- backends/jack.h | 2 +- backends/loopback.c | 2 +- backends/loopback.h | 2 +- backends/lua.c | 2 +- backends/lua.h | 2 +- backends/maweb.c | 2 +- backends/maweb.h | 2 +- backends/midi.c | 2 +- backends/midi.h | 2 +- backends/ola.cpp | 2 +- backends/ola.h | 2 +- backends/osc.c | 2 +- backends/osc.h | 2 +- backends/sacn.c | 2 +- backends/sacn.h | 2 +- backends/winmidi.c | 2 +- backends/winmidi.h | 2 +- 22 files changed, 24 insertions(+), 22 deletions(-) (limited to 'backends') diff --git a/backends/artnet.c b/backends/artnet.c index e01ac94..8a62a43 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -41,7 +41,7 @@ static int artnet_listener(char* host, char* port){ return 0; } -int init(){ +MM_PLUGIN_API int init(){ backend artnet = { .name = BACKEND_NAME, .conf = artnet_configure, diff --git a/backends/artnet.h b/backends/artnet.h index f5aa745..cce11d1 100644 --- a/backends/artnet.h +++ b/backends/artnet.h @@ -3,7 +3,7 @@ #endif #include "midimonster.h" -int init(); +MM_PLUGIN_API int init(); static int artnet_configure(char* option, char* value); static int artnet_configure_instance(instance* instance, char* option, char* value); static instance* artnet_instance(); diff --git a/backends/evdev.c b/backends/evdev.c index b19cda8..dd2231b 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -24,7 +24,7 @@ static struct { .detect = 0 }; -int init(){ +MM_PLUGIN_API int init(){ backend evdev = { .name = BACKEND_NAME, .conf = evdev_configure, diff --git a/backends/evdev.h b/backends/evdev.h index 48bd0ab..30ce892 100644 --- a/backends/evdev.h +++ b/backends/evdev.h @@ -8,7 +8,7 @@ * disabled by building with -DEVDEV_NO_UINPUT */ -int init(); +MM_PLUGIN_API int init(); static int evdev_configure(char* option, char* value); static int evdev_configure_instance(instance* instance, char* option, char* value); static instance* evdev_instance(); diff --git a/backends/jack.c b/backends/jack.c index b3aacd4..e8a63bc 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -4,6 +4,8 @@ #include #include +#define DEBUG + #include "jack.h" #include #include @@ -27,7 +29,7 @@ static struct /*_mmjack_backend_cfg*/ { .jack_shutdown = 0 }; -int init(){ +MM_PLUGIN_API int init(){ backend mmjack = { .name = BACKEND_NAME, .conf = mmjack_configure, diff --git a/backends/jack.h b/backends/jack.h index dd59cd2..5598042 100644 --- a/backends/jack.h +++ b/backends/jack.h @@ -2,7 +2,7 @@ #include #include -int init(); +MM_PLUGIN_API int init(); static int mmjack_configure(char* option, char* value); static int mmjack_configure_instance(instance* inst, char* option, char* value); static instance* mmjack_instance(); diff --git a/backends/loopback.c b/backends/loopback.c index 083a312..0a45bde 100644 --- a/backends/loopback.c +++ b/backends/loopback.c @@ -3,7 +3,7 @@ #define BACKEND_NAME "loopback" -int init(){ +MM_PLUGIN_API int init(){ backend loopback = { .name = BACKEND_NAME, .conf = loopback_configure, diff --git a/backends/loopback.h b/backends/loopback.h index c73ca20..a08417b 100644 --- a/backends/loopback.h +++ b/backends/loopback.h @@ -1,6 +1,6 @@ #include "midimonster.h" -int init(); +MM_PLUGIN_API int init(); static int loopback_configure(char* option, char* value); static int loopback_configure_instance(instance* inst, char* option, char* value); static instance* loopback_instance(); diff --git a/backends/lua.c b/backends/lua.c index 3555e72..0b47b2c 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -19,7 +19,7 @@ static int timer_fd = -1; static uint64_t last_timestamp; #endif -int init(){ +MM_PLUGIN_API int init(){ backend lua = { #ifndef MMBACKEND_LUA_TIMERFD .interval = lua_interval, diff --git a/backends/lua.h b/backends/lua.h index f2583a8..e187a8e 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -9,7 +9,7 @@ #define MMBACKEND_LUA_TIMERFD #endif -int init(); +MM_PLUGIN_API int init(); static int lua_configure(char* option, char* value); static int lua_configure_instance(instance* inst, char* option, char* value); static instance* lua_instance(); diff --git a/backends/maweb.c b/backends/maweb.c index c495512..08156f2 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -72,7 +72,7 @@ static maweb_command_key cmdline_keys[] = { {"GO_MINUS_SMALL", 50}, {"PAUSE_SMALL", 51}, {"GO_PLUS_SMALL", 52} }; -int init(){ +MM_PLUGIN_API int init(){ backend maweb = { .name = BACKEND_NAME, .conf = maweb_configure, diff --git a/backends/maweb.h b/backends/maweb.h index 14e4755..9091cda 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -1,6 +1,6 @@ #include "midimonster.h" -int init(); +MM_PLUGIN_API int init(); static int maweb_configure(char* option, char* value); static int maweb_configure_instance(instance* inst, char* option, char* value); static instance* maweb_instance(); diff --git a/backends/midi.c b/backends/midi.c index cde278b..f380f59 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -23,7 +23,7 @@ static struct { .detect = 0 }; -int init(){ +MM_PLUGIN_API int init(){ backend midi = { .name = BACKEND_NAME, .conf = midi_configure, diff --git a/backends/midi.h b/backends/midi.h index 6c3fcf9..b9934f1 100644 --- a/backends/midi.h +++ b/backends/midi.h @@ -1,6 +1,6 @@ #include "midimonster.h" -int init(); +MM_PLUGIN_API int init(); static int midi_configure(char* option, char* value); static int midi_configure_instance(instance* instance, char* option, char* value); static instance* midi_instance(); diff --git a/backends/ola.cpp b/backends/ola.cpp index 632cef7..d069a8c 100644 --- a/backends/ola.cpp +++ b/backends/ola.cpp @@ -11,7 +11,7 @@ static ola::io::SelectServer* ola_select = NULL; static ola::OlaCallbackClient* ola_client = NULL; -int init(){ +MM_PLUGIN_API int init(){ backend ola = { .name = BACKEND_NAME, .conf = ola_configure, diff --git a/backends/ola.h b/backends/ola.h index c943d52..1637495 100644 --- a/backends/ola.h +++ b/backends/ola.h @@ -4,7 +4,7 @@ extern "C" { #undef min #undef max - int init(); + MM_PLUGIN_API int init(); static int ola_configure(char* option, char* value); static int ola_configure_instance(instance* instance, char* option, char* value); static instance* ola_instance(); diff --git a/backends/osc.c b/backends/osc.c index bffbba8..d9f9139 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -19,7 +19,7 @@ static struct { .detect = 0 }; -int init(){ +MM_PLUGIN_API int init(){ backend osc = { .name = BACKEND_NAME, .conf = osc_configure, diff --git a/backends/osc.h b/backends/osc.h index dd5afb0..86be285 100644 --- a/backends/osc.h +++ b/backends/osc.h @@ -7,7 +7,7 @@ #define OSC_RECV_BUF 8192 #define OSC_XMIT_BUF 8192 -int init(); +MM_PLUGIN_API int init(); static int osc_configure(char* option, char* value); static int osc_configure_instance(instance* inst, char* option, char* value); static instance* osc_instance(); diff --git a/backends/sacn.c b/backends/sacn.c index 6e1b20b..d8b3eb3 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -31,7 +31,7 @@ static struct /*_sacn_global_config*/ { .last_announce = 0 }; -int init(){ +MM_PLUGIN_API int init(){ backend sacn = { .name = BACKEND_NAME, .conf = sacn_configure, diff --git a/backends/sacn.h b/backends/sacn.h index 7af2a36..631d3a4 100644 --- a/backends/sacn.h +++ b/backends/sacn.h @@ -1,6 +1,6 @@ #include "midimonster.h" -int init(); +MM_PLUGIN_API int init(); static int sacn_configure(char* option, char* value); static int sacn_configure_instance(instance* instance, char* option, char* value); static instance* sacn_instance(); diff --git a/backends/winmidi.c b/backends/winmidi.c index ffca3b4..bda5401 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -23,7 +23,7 @@ static struct { //TODO receive feedback socket until EAGAIN -int init(){ +MM_PLUGIN_API int init(){ backend winmidi = { .name = BACKEND_NAME, .conf = winmidi_configure, diff --git a/backends/winmidi.h b/backends/winmidi.h index e4abda1..ffa6a26 100644 --- a/backends/winmidi.h +++ b/backends/winmidi.h @@ -1,6 +1,6 @@ #include "midimonster.h" -int init(); +MM_PLUGIN_API int init(); static int winmidi_configure(char* option, char* value); static int winmidi_configure_instance(instance* inst, char* option, char* value); static instance* winmidi_instance(); -- cgit v1.2.3 From 0b72a1885f064666f9c7369e1d604feeb4b83d66 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 4 Dec 2019 01:23:35 +0100 Subject: Do not build the jack backend in debug mode --- backends/jack.c | 2 -- 1 file changed, 2 deletions(-) (limited to 'backends') diff --git a/backends/jack.c b/backends/jack.c index e8a63bc..192aab2 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -4,8 +4,6 @@ #include #include -#define DEBUG - #include "jack.h" #include #include -- cgit v1.2.3 From 9c37eddad24eb7e9bbc9aae723b3a992ec5b4c97 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 5 Dec 2019 19:31:14 +0100 Subject: Unify midi parsing/deparsing code --- backends/jack.c | 8 ++--- backends/winmidi.c | 89 ++++++++++++++++-------------------------------------- backends/winmidi.h | 10 +++--- 3 files changed, 35 insertions(+), 72 deletions(-) (limited to 'backends') diff --git a/backends/jack.c b/backends/jack.c index 192aab2..926f800 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -98,20 +98,20 @@ static int mmjack_process_midi(instance* inst, mmjack_port* port, size_t nframes //ident.fields.port set on output in mmjack_handle_midi ident.fields.sub_channel = event.buffer[0] & 0x0F; ident.fields.sub_type = event.buffer[0] & 0xF0; + ident.fields.sub_control = event.buffer[1]; + value = event.buffer[2]; if(ident.fields.sub_type == 0x80){ ident.fields.sub_type = midi_note; value = 0; } else if(ident.fields.sub_type == midi_pitchbend){ + ident.fields.sub_control = 0; value = event.buffer[1] | (event.buffer[2] << 7); } else if(ident.fields.sub_type == midi_aftertouch){ + ident.fields.sub_control = 0; value = event.buffer[1]; } - else{ - ident.fields.sub_control = event.buffer[1]; - value = event.buffer[2]; - } //append midi data mmjack_midiqueue_append(port, ident, value); } diff --git a/backends/winmidi.c b/backends/winmidi.c index bda5401..b274c06 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -203,35 +203,17 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v 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 * 16384.0)) & 0x7F; - output.components.data2 = (((int)(v[u].normalised * 16384.0)) >> 7) & 0x7F; - break; - default: - fprintf(stderr, "Unknown winmidi channel type %d\n", ident.fields.type); - continue; + //build output message + output.components.status = ident.fields.type | ident.fields.channel; + output.components.data1 = ident.fields.control; + output.components.data2 = v[u].normalised * 127.0; + if(ident.fields.type == pitchbend){ + output.components.data1 = ((int)(v[u].normalised * 16384.0)) & 0x7F; + output.components.data2 = (((int)(v[u].normalised * 16384.0)) >> 7) & 0x7F; + } + else if(ident.fields.type == aftertouch){ + output.components.data1 = v[u].normalised * 127.0; + output.components.data2 = 0; } midiOutShortMsg(data->device_out, output.dword); @@ -330,40 +312,21 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW //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 << 7) | input.components.data1) / 16384.0; - break; - default: - fprintf(stderr, "winmidi unhandled status byte %02X\n", input.components.status); - return; + ident.fields.type = input.components.status & 0xF0; + ident.fields.control = input.components.data1; + val.normalised = (double) input.components.data2 / 127.0; + + if(ident.fields.type == 0x80){ + ident.fields.type = note; + val.normalised = 0; + } + else if(ident.fields.type == pitchbend){ + ident.fields.control = 0; + val.normalised = (double)((input.components.data2 << 7) | input.components.data1) / 16384.0; + } + else if(ident.fields.type == aftertouch){ + ident.fields.control = 0; + val.normalised = (double) input.components.data1 / 127.0; } break; case MIM_LONGDATA: diff --git a/backends/winmidi.h b/backends/winmidi.h index ffa6a26..8c2d76b 100644 --- a/backends/winmidi.h +++ b/backends/winmidi.h @@ -19,11 +19,11 @@ typedef struct /*_winmidi_instance_data*/ { enum /*_winmidi_channel_type*/ { none = 0, - note, - cc, - pressure, - aftertouch, - pitchbend + note = 0x90, + cc = 0xB0, + pressure = 0xA0, + aftertouch = 0xD0, + pitchbend = 0xE0 }; typedef union { -- cgit v1.2.3 From 3eada28582b144519e95a44ee3adc3f46d39036e Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 5 Dec 2019 21:05:14 +0100 Subject: Add flags parameter to channel parser plugin API (Fixes #31) --- backends/artnet.c | 2 +- backends/artnet.h | 2 +- backends/evdev.c | 2 +- backends/evdev.h | 2 +- backends/jack.c | 2 +- backends/jack.h | 2 +- backends/loopback.c | 2 +- backends/loopback.h | 2 +- backends/lua.c | 2 +- backends/lua.h | 2 +- backends/maweb.c | 2 +- backends/maweb.h | 2 +- backends/midi.c | 2 +- backends/midi.h | 2 +- backends/ola.cpp | 2 +- backends/ola.h | 2 +- backends/osc.c | 2 +- backends/osc.h | 2 +- backends/sacn.c | 2 +- backends/sacn.h | 2 +- backends/winmidi.c | 2 +- backends/winmidi.h | 2 +- 22 files changed, 22 insertions(+), 22 deletions(-) (limited to 'backends') diff --git a/backends/artnet.c b/backends/artnet.c index 8a62a43..57eb7b1 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -156,7 +156,7 @@ static int artnet_configure_instance(instance* inst, char* option, char* value){ return 1; } -static channel* artnet_channel(instance* inst, char* spec){ +static channel* artnet_channel(instance* inst, char* spec, uint8_t flags){ artnet_instance_data* data = (artnet_instance_data*) inst->impl; char* spec_next = spec; unsigned chan_a = strtoul(spec, &spec_next, 10); diff --git a/backends/artnet.h b/backends/artnet.h index cce11d1..f6a6709 100644 --- a/backends/artnet.h +++ b/backends/artnet.h @@ -7,7 +7,7 @@ MM_PLUGIN_API int init(); static int artnet_configure(char* option, char* value); static int artnet_configure_instance(instance* instance, char* option, char* value); static instance* artnet_instance(); -static channel* artnet_channel(instance* instance, char* spec); +static channel* artnet_channel(instance* instance, char* spec, uint8_t flags); static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v); static int artnet_handle(size_t num, managed_fd* fds); static int artnet_start(); diff --git a/backends/evdev.c b/backends/evdev.c index dd2231b..0da5ae6 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -249,7 +249,7 @@ static int evdev_configure_instance(instance* inst, char* option, char* value) { return 1; } -static channel* evdev_channel(instance* inst, char* spec){ +static channel* evdev_channel(instance* inst, char* spec, uint8_t flags){ #ifndef EVDEV_NO_UINPUT evdev_instance_data* data = (evdev_instance_data*) inst->impl; #endif diff --git a/backends/evdev.h b/backends/evdev.h index 30ce892..6504416 100644 --- a/backends/evdev.h +++ b/backends/evdev.h @@ -12,7 +12,7 @@ MM_PLUGIN_API int init(); static int evdev_configure(char* option, char* value); static int evdev_configure_instance(instance* instance, char* option, char* value); static instance* evdev_instance(); -static channel* evdev_channel(instance* instance, char* spec); +static channel* evdev_channel(instance* instance, char* spec, uint8_t flags); static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v); static int evdev_handle(size_t num, managed_fd* fds); static int evdev_start(); diff --git a/backends/jack.c b/backends/jack.c index 926f800..e7bed04 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -409,7 +409,7 @@ static int mmjack_parse_midispec(mmjack_channel_ident* ident, char* spec){ return 0; } -static channel* mmjack_channel(instance* inst, char* spec){ +static channel* mmjack_channel(instance* inst, char* spec, uint8_t flags){ mmjack_instance_data* data = (mmjack_instance_data*) inst->impl; mmjack_channel_ident ident = { .label = 0 diff --git a/backends/jack.h b/backends/jack.h index 5598042..a7f3e8b 100644 --- a/backends/jack.h +++ b/backends/jack.h @@ -6,7 +6,7 @@ MM_PLUGIN_API int init(); static int mmjack_configure(char* option, char* value); static int mmjack_configure_instance(instance* inst, char* option, char* value); static instance* mmjack_instance(); -static channel* mmjack_channel(instance* inst, char* spec); +static channel* mmjack_channel(instance* inst, char* spec, uint8_t flags); static int mmjack_set(instance* inst, size_t num, channel** c, channel_value* v); static int mmjack_handle(size_t num, managed_fd* fds); static int mmjack_start(); diff --git a/backends/loopback.c b/backends/loopback.c index 0a45bde..41e6f85 100644 --- a/backends/loopback.c +++ b/backends/loopback.c @@ -49,7 +49,7 @@ static instance* loopback_instance(){ return i; } -static channel* loopback_channel(instance* inst, char* spec){ +static channel* loopback_channel(instance* inst, char* spec, uint8_t flags){ size_t u; loopback_instance_data* data = (loopback_instance_data*) inst->impl; diff --git a/backends/loopback.h b/backends/loopback.h index a08417b..ee51c66 100644 --- a/backends/loopback.h +++ b/backends/loopback.h @@ -4,7 +4,7 @@ MM_PLUGIN_API int init(); static int loopback_configure(char* option, char* value); static int loopback_configure_instance(instance* inst, char* option, char* value); static instance* loopback_instance(); -static channel* loopback_channel(instance* inst, char* spec); +static channel* loopback_channel(instance* inst, char* spec, uint8_t flags); static int loopback_set(instance* inst, size_t num, channel** c, channel_value* v); static int loopback_handle(size_t num, managed_fd* fds); static int loopback_start(); diff --git a/backends/lua.c b/backends/lua.c index 0b47b2c..40e6613 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -330,7 +330,7 @@ static instance* lua_instance(){ return inst; } -static channel* lua_channel(instance* inst, char* spec){ +static channel* lua_channel(instance* inst, char* spec, uint8_t flags){ size_t u; lua_instance_data* data = (lua_instance_data*) inst->impl; diff --git a/backends/lua.h b/backends/lua.h index e187a8e..4ea5b0a 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -13,7 +13,7 @@ MM_PLUGIN_API int init(); static int lua_configure(char* option, char* value); static int lua_configure_instance(instance* inst, char* option, char* value); static instance* lua_instance(); -static channel* lua_channel(instance* inst, char* spec); +static channel* lua_channel(instance* inst, char* spec, uint8_t flags); static int lua_set(instance* inst, size_t num, channel** c, channel_value* v); static int lua_handle(size_t num, managed_fd* fds); static int lua_start(); diff --git a/backends/maweb.c b/backends/maweb.c index 08156f2..d008cc0 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -229,7 +229,7 @@ static instance* maweb_instance(){ return inst; } -static channel* maweb_channel(instance* inst, char* spec){ +static channel* maweb_channel(instance* inst, char* spec, uint8_t flags){ maweb_instance_data* data = (maweb_instance_data*) inst->impl; maweb_channel_data chan = { 0 diff --git a/backends/maweb.h b/backends/maweb.h index 9091cda..05095f8 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -4,7 +4,7 @@ MM_PLUGIN_API int init(); static int maweb_configure(char* option, char* value); static int maweb_configure_instance(instance* inst, char* option, char* value); static instance* maweb_instance(); -static channel* maweb_channel(instance* inst, char* spec); +static channel* maweb_channel(instance* inst, char* spec, uint8_t flags); static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v); static int maweb_handle(size_t num, managed_fd* fds); static int maweb_start(); diff --git a/backends/midi.c b/backends/midi.c index f380f59..92776ca 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -110,7 +110,7 @@ static int midi_configure_instance(instance* inst, char* option, char* value){ return 1; } -static channel* midi_channel(instance* inst, char* spec){ +static channel* midi_channel(instance* inst, char* spec, uint8_t flags){ midi_channel_ident ident = { .label = 0 }; diff --git a/backends/midi.h b/backends/midi.h index b9934f1..4e16f90 100644 --- a/backends/midi.h +++ b/backends/midi.h @@ -4,7 +4,7 @@ MM_PLUGIN_API int init(); static int midi_configure(char* option, char* value); static int midi_configure_instance(instance* instance, char* option, char* value); static instance* midi_instance(); -static channel* midi_channel(instance* instance, char* spec); +static channel* midi_channel(instance* instance, char* spec, uint8_t flags); static int midi_set(instance* inst, size_t num, channel** c, channel_value* v); static int midi_handle(size_t num, managed_fd* fds); static int midi_start(); diff --git a/backends/ola.cpp b/backends/ola.cpp index d069a8c..c13e8f9 100644 --- a/backends/ola.cpp +++ b/backends/ola.cpp @@ -68,7 +68,7 @@ static int ola_configure_instance(instance* inst, char* option, char* value){ return 1; } -static channel* ola_channel(instance* inst, char* spec){ +static channel* ola_channel(instance* inst, char* spec, uint8_t flags){ ola_instance_data* data = (ola_instance_data*) inst->impl; char* spec_next = spec; unsigned chan_a = strtoul(spec, &spec_next, 10); diff --git a/backends/ola.h b/backends/ola.h index 1637495..0c42bac 100644 --- a/backends/ola.h +++ b/backends/ola.h @@ -8,7 +8,7 @@ extern "C" { static int ola_configure(char* option, char* value); static int ola_configure_instance(instance* instance, char* option, char* value); static instance* ola_instance(); - static channel* ola_channel(instance* instance, char* spec); + static channel* ola_channel(instance* instance, char* spec, uint8_t flags); static int ola_set(instance* inst, size_t num, channel** c, channel_value* v); static int ola_handle(size_t num, managed_fd* fds); static int ola_start(); diff --git a/backends/osc.c b/backends/osc.c index d9f9139..757ad89 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -577,7 +577,7 @@ static instance* osc_instance(){ return inst; } -static channel* osc_map_channel(instance* inst, char* spec){ +static channel* osc_map_channel(instance* inst, char* spec, uint8_t flags){ size_t u, p; osc_instance_data* data = (osc_instance_data*) inst->impl; osc_channel_ident ident = { diff --git a/backends/osc.h b/backends/osc.h index 86be285..6f3b923 100644 --- a/backends/osc.h +++ b/backends/osc.h @@ -11,7 +11,7 @@ MM_PLUGIN_API int init(); static int osc_configure(char* option, char* value); static int osc_configure_instance(instance* inst, char* option, char* value); static instance* osc_instance(); -static channel* osc_map_channel(instance* inst, char* spec); +static channel* osc_map_channel(instance* inst, char* spec, uint8_t flags); static int osc_set(instance* inst, size_t num, channel** c, channel_value* v); static int osc_handle(size_t num, managed_fd* fds); static int osc_start(); diff --git a/backends/sacn.c b/backends/sacn.c index d8b3eb3..2229b8a 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -198,7 +198,7 @@ static instance* sacn_instance(){ return inst; } -static channel* sacn_channel(instance* inst, char* spec){ +static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){ sacn_instance_data* data = (sacn_instance_data*) inst->impl; char* spec_next = spec; diff --git a/backends/sacn.h b/backends/sacn.h index 631d3a4..1d3268c 100644 --- a/backends/sacn.h +++ b/backends/sacn.h @@ -4,7 +4,7 @@ MM_PLUGIN_API int init(); static int sacn_configure(char* option, char* value); static int sacn_configure_instance(instance* instance, char* option, char* value); static instance* sacn_instance(); -static channel* sacn_channel(instance* instance, char* spec); +static channel* sacn_channel(instance* instance, char* spec, uint8_t flags); static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v); static int sacn_handle(size_t num, managed_fd* fds); static int sacn_start(); diff --git a/backends/winmidi.c b/backends/winmidi.c index b274c06..790257b 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -110,7 +110,7 @@ static instance* winmidi_instance(){ return i; } -static channel* winmidi_channel(instance* inst, char* spec){ +static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags){ char* next_token = NULL; winmidi_channel_ident ident = { .label = 0 diff --git a/backends/winmidi.h b/backends/winmidi.h index 8c2d76b..985c46a 100644 --- a/backends/winmidi.h +++ b/backends/winmidi.h @@ -4,7 +4,7 @@ MM_PLUGIN_API int init(); static int winmidi_configure(char* option, char* value); static int winmidi_configure_instance(instance* inst, char* option, char* value); static instance* winmidi_instance(); -static channel* winmidi_channel(instance* inst, char* spec); +static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags); static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v); static int winmidi_handle(size_t num, managed_fd* fds); static int winmidi_start(); -- cgit v1.2.3