aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorcbdev <cb@cbcdn.com>2019-07-20 18:10:56 +0200
committercbdev <cb@cbcdn.com>2019-07-20 18:10:56 +0200
commit5bd8e81e2821f1378c6773fbc1f06df063dbbd22 (patch)
treeba877e50a1ce7e245020692bb7b503bbedc370c1
parent99f54f6d15d9e286fb0b47cf21b32318767d3c2a (diff)
downloadmidimonster-5bd8e81e2821f1378c6773fbc1f06df063dbbd22.tar.gz
midimonster-5bd8e81e2821f1378c6773fbc1f06df063dbbd22.tar.bz2
midimonster-5bd8e81e2821f1378c6773fbc1f06df063dbbd22.zip
Implement multi-channel mapping syntax
-rw-r--r--README.md30
-rw-r--r--TODO2
-rw-r--r--config.c196
-rw-r--r--configs/launchctl-sacn.cfg25
-rw-r--r--midimonster.h53
-rw-r--r--monster.cfg46
6 files changed, 274 insertions, 78 deletions
diff --git a/README.md b/README.md
index ee907a6..bce7b71 100644
--- a/README.md
+++ b/README.md
@@ -56,10 +56,24 @@ lines of the form `option = value`.
Lines starting with a semicolon are treated as comments and ignored. Inline comments
are not currently supported.
+Example configuration files may be found in [configs/](configs/).
+
+### Backend and instance configuration
+
A configuration section may either be a *backend configuration* section, started by
`[backend <backend-name>]`, an *instance configuration* section, started by
`[<backend-name> <instance-name>]` or a *mapping* section started by `[map]`.
+Backends document their global options in their [backend documentation](#backend-documentation).
+Some backends may not require global configuration, in which case the configuration
+section for that particular backend can be omitted.
+
+To make an instance available for mapping channels, it requires at least the
+`[<backend-name> <instance-name>]` configuration stanza. Most backends require
+additional configuration for their instances.
+
+### Channel mapping
+
The `[map]` section consists of lines of channel-to-channel assignments, reading like
```
@@ -76,7 +90,21 @@ output eachothers events.
The last line is a shorter way to create a bi-directional mapping.
-Example configuration files may be found in [configs/](configs/).
+### Multi-channel mapping
+
+To make mapping large contiguous sets of channels easier, channel names may contain
+expressions of the form `{<start>..<end>}`, with *start* and *end* being positive integers
+delimiting a range of channels. Multiple such expressions may be used in one channel
+specification, with the rightmost expression being incremented (or decremented) first for
+evaluation.
+
+Both sides of a multi-channel assignment need to have the same number of channels.
+
+Example multi-channel mapping:
+
+```
+instance-a.channel{1..10} > instance-b.{10..1}
+```
## Backend documentation
diff --git a/TODO b/TODO
index f39dae0..76f2c4e 100644
--- a/TODO
+++ b/TODO
@@ -4,5 +4,5 @@ Optimize core channel search (store backend offset)
Printing backend
document example configs
-lua timer
evdev relative axis size
+mm_managed_fd.impl is not freed currently
diff --git a/config.c b/config.c
index b81aeaf..24f1223 100644
--- a/config.c
+++ b/config.c
@@ -21,25 +21,164 @@ static backend* current_backend = NULL;
static instance* current_instance = NULL;
static char* config_trim_line(char* in){
- ssize_t u;
+ ssize_t n;
//trim front
for(; *in && !isgraph(*in); in++){
}
//trim back
- for(u = strlen(in); u >= 0 && !isgraph(in[u]); u--){
- in[u] = 0;
+ for(n = strlen(in); n >= 0 && !isgraph(in[n]); n--){
+ in[n] = 0;
}
return in;
}
+static int config_glob_parse(channel_glob* glob, char* spec, size_t length){
+ char* parse_offset = NULL;
+ //FIXME might want to allow negative delimiters at some point
+
+ //first interval member
+ glob->limits.u64[0] = strtoul(spec, &parse_offset, 10);
+ if(!parse_offset || parse_offset - spec >= length || strncmp(parse_offset, "..", 2)){
+ return 1;
+ }
+
+ parse_offset += 2;
+ //second interval member
+ glob->limits.u64[1] = strtoul(parse_offset, &parse_offset, 10);
+ if(!parse_offset || parse_offset - spec != length || *parse_offset != '}'){
+ return 1;
+ }
+
+ //calculate number of channels within interval
+ if(glob->limits.u64[0] < glob->limits.u64[1]){
+ glob->values = glob->limits.u64[1] - glob->limits.u64[0] + 1;
+ }
+ else if(glob->limits.u64[0] > glob->limits.u64[1]){
+ glob->values = glob->limits.u64[0] - glob->limits.u64[1] + 1;
+ }
+ else{
+ glob->values = 1;
+ }
+
+ return 0;
+}
+
+static int config_glob_scan(instance* inst, channel_spec* spec){
+ char* glob_start = spec->spec, *glob_end = NULL;
+ size_t u;
+
+ //assume a spec is one channel as default
+ spec->channels = 1;
+
+ //scan and mark globs
+ for(glob_start = strchr(glob_start, '{'); glob_start; glob_start = strchr(glob_start, '{')){
+ glob_end = strchr(glob_start, '}');
+ if(!glob_end){
+ fprintf(stderr, "Failed to parse channel spec, unterminated glob: %s\n", spec->spec);
+ return 1;
+ }
+
+ spec->glob = realloc(spec->glob, (spec->globs + 1) * sizeof(channel_glob));
+ if(!spec->glob){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return 1;
+ }
+
+ spec->glob[spec->globs].offset[0] = glob_start - spec->spec;
+ spec->glob[spec->globs].offset[1] = glob_end - spec->spec;
+ spec->globs++;
+
+ //skip this opening brace
+ glob_start++;
+ }
+
+ //try to parse globs internally
+ spec->internal = 1;
+ for(u = 0; u < spec->globs; u++){
+ if(config_glob_parse(spec->glob + u,
+ spec->spec + spec->glob[u].offset[0] + 1,
+ spec->glob[u].offset[1] - spec->glob[u].offset[0] - 1)){
+ spec->internal = 0;
+ break;
+ }
+ }
+ if(!spec->internal){
+ //TODO try to parse globs externally
+ fprintf(stderr, "Failed to parse glob %lu in %s internally\n", u + 1, spec->spec);
+ return 1;
+ }
+
+ //calculate channel total
+ for(u = 0; u < spec->globs; u++){
+ spec->channels *= spec->glob[u].values;
+ }
+ return 0;
+}
+
+static channel* config_glob_resolve(instance* inst, channel_spec* spec, uint64_t n){
+ size_t glob = 0, glob_length;
+ ssize_t bytes = 0;
+ uint64_t current_value = 0;
+ channel* result = NULL;
+ char* resolved_spec = strdup(spec->spec);
+
+ if(!resolved_spec){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return NULL;
+ }
+
+ //TODO if not internal, try to resolve externally
+
+ //iterate and resolve globs
+ for(glob = spec->globs; glob > 0; glob--){
+ current_value = spec->glob[glob - 1].limits.u64[0] + (n % spec->glob[glob - 1].values);
+ if(spec->glob[glob - 1].limits.u64[0] > spec->glob[glob - 1].limits.u64[1]){
+ current_value = spec->glob[glob - 1].limits.u64[0] - (n % spec->glob[glob - 1].values);
+ }
+ glob_length = spec->glob[glob - 1].offset[1] - spec->glob[glob - 1].offset[0];
+ n /= spec->glob[glob - 1].values;
+
+ //write out value
+ bytes = snprintf(resolved_spec + spec->glob[glob - 1].offset[0],
+ glob_length,
+ "%lu",
+ current_value);
+ if(bytes > glob_length){
+ fprintf(stderr, "Internal error resolving glob %s\n", spec->spec);
+ goto bail;
+ }
+
+ //move trailing data
+ if(bytes < glob_length){
+ memmove(resolved_spec + spec->glob[glob - 1].offset[0] + bytes,
+ resolved_spec + spec->glob[glob - 1].offset[1] + 1,
+ strlen(spec->spec) - spec->glob[glob - 1].offset[1]);
+ }
+ }
+
+ result = inst->backend->channel(inst, resolved_spec);
+ if(spec->globs && !result){
+ fprintf(stderr, "Failed to match multichannel evaluation %s to a channel\n", resolved_spec);
+ }
+
+bail:
+ free(resolved_spec);
+ return result;
+}
+
static int config_map(char* to_raw, char* from_raw){
//create a copy because the original pointer may be used multiple times
char* to = strdup(to_raw), *from = strdup(from_raw);
- char* chanspec_to = to, *chanspec_from = from;
+ channel_spec spec_to = {
+ .spec = to
+ }, spec_from = {
+ .spec = from
+ };
instance* instance_to = NULL, *instance_from = NULL;
channel* channel_from = NULL, *channel_to = NULL;
+ uint64_t n = 0;
int rv = 1;
if(!from || !to){
@@ -50,21 +189,21 @@ static int config_map(char* to_raw, char* from_raw){
}
//separate channel spec from instance
- for(; *chanspec_to && *chanspec_to != '.'; chanspec_to++){
+ for(; *(spec_to.spec) && *(spec_to.spec) != '.'; spec_to.spec++){
}
- for(; *chanspec_from && *chanspec_from != '.'; chanspec_from++){
+ for(; *(spec_from.spec) && *(spec_from.spec) != '.'; spec_from.spec++){
}
- if(!*chanspec_to || !*chanspec_from){
+ if(!spec_from.spec[0] || !spec_to.spec[0]){
fprintf(stderr, "Mapping does not contain a proper instance specification\n");
goto done;
}
//terminate
- *chanspec_to = *chanspec_from = 0;
- chanspec_to++;
- chanspec_from++;
+ spec_from.spec[0] = spec_to.spec[0] = 0;
+ spec_from.spec++;
+ spec_to.spec++;
//find matching instances
instance_to = instance_match(to);
@@ -75,16 +214,41 @@ static int config_map(char* to_raw, char* from_raw){
goto done;
}
- channel_from = instance_from->backend->channel(instance_from, chanspec_from);
- channel_to = instance_to->backend->channel(instance_to, chanspec_to);
-
- if(!channel_from || !channel_to){
- fprintf(stderr, "Failed to parse channel specifications\n");
+ //scan for globs
+ if(config_glob_scan(instance_to, &spec_to)
+ || config_glob_scan(instance_from, &spec_from)){
goto done;
}
+
+ if(spec_to.channels != spec_from.channels
+ || spec_to.channels == 0
+ || spec_from.channels == 0){
+ fprintf(stderr, "Multi-channel specification size mismatch: %s.%s (%lu channels) - %s.%s (%lu channels)\n",
+ instance_from->name,
+ spec_from.spec,
+ spec_from.channels,
+ instance_to->name,
+ spec_to.spec,
+ spec_to.channels);
+ goto done;
+ }
+
+ //iterate, resolve globs and map
+ rv = 0;
+ for(n = 0; !rv && n < spec_from.channels; n++){
+ channel_from = config_glob_resolve(instance_from, &spec_from, n);
+ channel_to = config_glob_resolve(instance_to, &spec_to, n);
+
+ if(!channel_from || !channel_to){
+ rv = 1;
+ goto done;
+ }
+ rv |= mm_map_channel(channel_from, channel_to);
+ }
- rv = mm_map_channel(channel_from, channel_to);
done:
+ free(spec_from.glob);
+ free(spec_to.glob);
free(from);
free(to);
return rv;
diff --git a/configs/launchctl-sacn.cfg b/configs/launchctl-sacn.cfg
index c2dec84..02cd152 100644
--- a/configs/launchctl-sacn.cfg
+++ b/configs/launchctl-sacn.cfg
@@ -6,34 +6,19 @@
[backend midi]
name = MIDIMonster
-[backend sacn]
+[backend artnet]
bind = 0.0.0.0
[midi lc]
read = Launch Control
-[sacn out]
-universe = 1
-priority = 100
+[artnet out]
+universe = 0
+destination = 255.255.255.255
[map]
-lc.ch0.cc0 > out.1
-lc.ch0.cc1 > out.2
-lc.ch0.cc2 > out.3
-lc.ch0.cc3 > out.4
-lc.ch0.cc4 > out.5
-lc.ch0.cc5 > out.6
-lc.ch0.cc6 > out.7
-lc.ch0.cc7 > out.8
-lc.ch0.cc8 > out.9
-lc.ch0.cc9 > out.10
-lc.ch0.cc10 > out.11
-lc.ch0.cc11 > out.12
-lc.ch0.cc12 > out.13
-lc.ch0.cc13 > out.14
-lc.ch0.cc14 > out.15
-lc.ch0.cc15 > out.16
+lc.ch0.cc{0..15} > out.{1..16}
lc.ch0.note0 > out.1
lc.ch0.note1 > out.2
diff --git a/midimonster.h b/midimonster.h
index 8a18155..1fc85b7 100644
--- a/midimonster.h
+++ b/midimonster.h
@@ -3,9 +3,15 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
+
+/* Straight-forward min / max macros */
#define max(a,b) (((a) > (b)) ? (a) : (b))
#define min(a,b) (((a) < (b)) ? (a) : (b))
+
+/* Clamp a value to a range */
#define clamp(val,max,min) (((val) > (max)) ? (max) : (((val) < (min)) ? (min) : (val)))
+
+/* Debug messages only compile in when DEBUG is set */
#ifdef DEBUG
#define DBGPF(format, ...) fprintf(stderr, (format), __VA_ARGS__)
#define DBG(message) fprintf(stderr, "%s", (message))
@@ -14,10 +20,13 @@
#define DBG(message)
#endif
+/* Pull in additional defines for non-linux platforms */
#include "portability.h"
+/* Default configuration file name to read when no other is specified */
#define DEFAULT_CFG "monster.cfg"
+/* Forward declare some of the structs so we can use them in eachother */
struct _channel_value;
struct _backend_channel;
struct _backend_instance;
@@ -86,6 +95,7 @@ typedef int (*mmbackend_start)();
typedef uint32_t (*mmbackend_interval)();
typedef int (*mmbackend_shutdown)();
+/* Channel event value, .normalised is used by backends to determine channel values */
typedef struct _channel_value {
union {
double dbl;
@@ -94,6 +104,10 @@ typedef struct _channel_value {
double normalised;
} channel_value;
+/*
+ * Backend callback structure
+ * Used to register a backend with the core using mm_backend_register()
+ */
typedef struct /*_mm_backend*/ {
char* name;
mmbackend_configure conf;
@@ -108,6 +122,10 @@ typedef struct /*_mm_backend*/ {
mmbackend_interval interval;
} backend;
+/*
+ * Backend instance structure - do not allocate directly!
+ * Use the memory returned by mm_instance()
+ */
typedef struct _backend_instance {
backend* backend;
uint64_t ident;
@@ -115,20 +133,51 @@ typedef struct _backend_instance {
char* name;
} instance;
+/*
+ * Channel specification glob
+ */
+typedef struct /*_mm_channel_glob*/ {
+ size_t offset[2];
+ union {
+ void* impl;
+ uint64_t u64[2];
+ } limits;
+ uint64_t values;
+} channel_glob;
+
+/*
+ * (Multi-)Channel specification
+ */
+typedef struct /*_mm_channel_spec*/ {
+ char* spec;
+ uint8_t internal;
+ size_t channels;
+ size_t globs;
+ channel_glob* glob;
+} channel_spec;
+
+/*
+ * Instance channel structure
+ * Backends may either manage their own channel registry
+ * or use the memory returned by mm_channel()
+ */
typedef struct _backend_channel {
instance* instance;
uint64_t ident;
void* impl;
} channel;
-//FIXME might be replaced by struct pollfd
-//FIXME who frees impl
+/*
+ * File descriptor management structure
+ * Register for the core event loop using mm_manage_fd()
+ */
typedef struct _managed_fd {
int fd;
backend* backend;
void* impl;
} managed_fd;
+/* Internal channel mapping structure - Core use only */
typedef struct /*_mm_channel_mapping*/ {
channel* from;
size_t destinations;
diff --git a/monster.cfg b/monster.cfg
index 0760571..7db3ec3 100644
--- a/monster.cfg
+++ b/monster.cfg
@@ -1,46 +1,16 @@
-[backend sacn]
-name = sACN source
-bind = 0.0.0.0
-
[backend artnet]
bind = 0.0.0.0
-[backend ola]
-
[artnet art]
-universe = 1
-dest = 129.13.215.0
-
-[backend midi]
-name = Monster
-
-;[evdev in]
-;input = Xbox Wireless Controller
-
-[sacn sacn]
-universe = 1
-priority = 100
+universe = 0
+dest = 255.255.255.255
-[midi midi]
-read = Axiom
+[evdev mouse]
+input = TPPS
-;[ola ola]
+[loopback loop]
[map]
-;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
-midi.ch0.pitch > midi.ch0.cc1
-midi.ch0.aftertouch > midi.ch0.cc2
-midi.ch0.cc71 > midi.ch0.pitch
-midi.ch0.cc74 > midi.ch0.aftertouch
-midi.ch0.cc91 > midi.ch0.pressure1
+art.{3..4}{4..3} > loop.chan{4..3}{3..4}
+art.{1..10} > loop.data{1..10}
+art.{500..599} > loop.test{500..599}