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) --- README.md | 60 ++++++++++- backends/Makefile | 13 ++- backends/ola.cpp | 318 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/ola.h | 38 +++++++ monster.cfg | 37 ++++--- 5 files changed, 443 insertions(+), 23 deletions(-) create mode 100644 backends/ola.cpp create mode 100644 backends/ola.h diff --git a/README.md b/README.md index c0b8184..3f5606a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Currently, the MIDIMonster supports the following protocols: * sACN / E1.31 * OSC * evdev input devices (Linux) +* Open Lighting Architecture (OLA) The MIDIMonster allows the user to translate any channel on one protocol into channel(s) on any other (or the same) supported protocol, for example to: @@ -48,18 +49,23 @@ on any other (or the same) supported protocol, for example to: - [Global configuration](#global-configuration-3) - [Instance configuration](#instance-configuration-3) - [Channel specification](#channel-specification-3) - - [Known bugs/problems](#known-bugs-problems) + - [Known bugs/problems](#known-bugs--problems-3) + [The `loopback` backend](#the-loopback-backend) - [Global configuration](#global-configuration-4) - [Instance configuration](#instance-configuration-4) - [Channel specification](#channel-specification-4) - - [Known bugs / problems](#known-bugs--problems-3) + - [Known bugs / problems](#known-bugs--problems-4) + [The `osc` backend](#the-osc-backend) - [Global configuration](#global-configuration-5) - [Instance configuration](#instance-configuration-5) - [Channel specification](#channel-specification-5) - [Supported types & value ranges](#supported-types--value-ranges) - - [Known bugs / problems](#known-bugs--problems-4) + - [Known bugs / problems](#known-bugs--problems-5) + + [The `ola` backend](#the-ola-backend) + - [Global configuration](#global-configuration-6) + - [Instance configuration](#instance-configuration-6) + - [Channel specification](#channel-specification-6) + - [Known bugs / problems](#known-bugs--problems-6) * [Building](#building) + [Prerequisites](#prerequisites) + [Build](#build) @@ -310,7 +316,7 @@ Note that to map an absolute axis on an output-enabled instance, additional info 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 +#### 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. @@ -439,6 +445,48 @@ The default ranges are: Ping requests are not yet answered. There may be some problems using broadcast output and input. +### 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. + ## Building This section will explain how to build the provided sources to be able to run @@ -451,6 +499,7 @@ support for the protocols to translate. * libasound2-dev (for the MIDI backend) * libevdev-dev (for the evdev backend) +* libola-dev (for the optional OLA backend) * pkg-config (as some projects and systems like to spread their files around) * A C compiler * GNUmake @@ -459,6 +508,9 @@ support for the protocols to translate. Just running `make` in the source directory should do the trick. +Some backends have been marked as optional as they require rather large additional software to be installed, +for example the `ola` backend. To build these, run `make full` in the backends directory. + ## Development The architecture is split into the `midimonster` core, handling mapping 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; diff --git a/monster.cfg b/monster.cfg index 34acbce..d7c31dc 100644 --- a/monster.cfg +++ b/monster.cfg @@ -1,6 +1,3 @@ -[backend midi] -name = MIDIMonster - [backend sacn] name = sACN source bind = 0.0.0.0 @@ -8,27 +5,33 @@ bind = 0.0.0.0 [backend artnet] bind = 0.0.0.0 +[backend ola] + [artnet art] universe = 1 dest = 129.13.215.0 -[evdev in] -input = Xbox Wireless Controller - -[midi midi] +;[evdev in] +;input = Xbox Wireless Controller [sacn sacn] universe = 1 priority = 100 +[midi midi] + +[ola ola] + [map] -in.EV_ABS.ABS_X > midi.cc0.0 -in.EV_ABS.ABS_Y > midi.cc0.1 -in.EV_ABS.ABS_X > sacn.1+2 -in.EV_ABS.ABS_Y > sacn.3 -in.EV_ABS.ABS_X > art.1+2 -in.EV_ABS.ABS_Y > art.3 -in.EV_KEY.BTN_THUMBL > sacn.4 -in.EV_KEY.BTN_THUMBR > sacn.5 -in.EV_ABS.ABS_GAS > sacn.6+7 -in.EV_ABS.ABS_BRAKE > sacn.8 +;in.EV_ABS.ABS_X > sacn.1+2 +;in.EV_ABS.ABS_Y > sacn.3 +;in.EV_ABS.ABS_X > art.1+2 +;in.EV_ABS.ABS_Y > art.3 +;in.EV_ABS.ABS_X > ola.1+2 +;in.EV_ABS.ABS_Y > ola.3 +;in.EV_KEY.BTN_THUMBL > sacn.4 +;in.EV_KEY.BTN_THUMBR > sacn.5 +;in.EV_ABS.ABS_GAS > sacn.6+7 +;in.EV_ABS.ABS_BRAKE > sacn.8 +ola.1 > midi.cc0.1 +ola.2+3 > midi.cc0.2 -- cgit v1.2.3