aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--README.md60
-rw-r--r--backends/Makefile13
-rw-r--r--backends/ola.cpp318
-rw-r--r--backends/ola.h38
-rw-r--r--monster.cfg37
5 files changed, 443 insertions, 23 deletions
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 <cstring>
+#include <ola/DmxBuffer.h>
+#include <ola/Logging.h>
+#include <ola/OlaClientWrapper.h>
+#include <ola/client/OlaClient.h>
+#include <ola/io/SelectServer.h>
+#include <ola/network/Socket.h>
+
+#define BACKEND_NAME "ola"
+static ola::io::SelectServer* ola_select = NULL;
+static ola::OlaCallbackClient* ola_client = NULL;
+
+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