/** @file
* Map CONTROLLER or PGMCHANGE events between ALSA MIDI sequencer i/o ports.
*/
#include <alsa/asoundlib.h>
#include <limits.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include "alsa.h"
#define NAME "actlmap"
#define LOGS(fmt) (void)fprintf(stderr, NAME ": " fmt "\n")
#define LOGV(fmt, ...) (void)fprintf(stderr, NAME ": " fmt "\n", __VA_ARGS__)
#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))
#define MAX(X, Y) ((X) > (Y) ? (X) : (Y))
typedef enum { NONE = 0, CHANNEL, PARAM, VALUE } wildcard_t;
typedef struct {
snd_seq_event_type_t type;
snd_seq_ev_ctrl_t data;
wildcard_t wildcard;
} event_t;
typedef struct {
event_t mod;
event_t src;
event_t dst;
} mapping_t;
typedef struct {
size_t num;
const mapping_t* curr_mod;
mapping_t* rules;
} ruleset_t;
/** Interrupt has been received, so shutdown on next occasion. */
static volatile int SHUTDOWN = 0;
static void signal_handler(int sig) { SHUTDOWN = sig; }
/** Schedule shutdown upon INT, QUIT, or TERM. */
static int register_handlers() {
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
signal(SIGINT, &signal_handler);
signal(SIGQUIT, &signal_handler);
signal(SIGTERM, &signal_handler);
return 0;
}
/** Check whether the given sequencer event should be processed. */
static int match(const snd_seq_event_t* ev, const event_t* src) {
if (ev->type != src->type) return 0;
if (src->wildcard != CHANNEL && ev->data.control.channel != src->data.channel) return 0;
if (src->wildcard != PARAM && ev->data.control.param != src->data.param) return 0;
if (src->wildcard != VALUE && ev->data.control.value != src->data.value) return 0;
return 1;
}
/** Extract the source wildcard value. An int should cover all possible values. */
static int get(wildcard_t src, const snd_seq_ev_ctrl_t* ev) {
if (src == CHANNEL)
return (int)ev->channel;
else if (src == PARAM)
return (int)ev->param;
else if (src == VALUE)
return (int)ev->value;
else
return 0;
}
/** Clamp the source value to the target value range. */
static int clamp(wildcard_t dst, int value) {
if (dst == CHANNEL)
return MIN(MAX(value, 0), UCHAR_MAX);
else if (dst == PARAM)
return MIN(MAX(value, 0), INT_MAX); // UINT_MAX
else if (dst == VALUE)
return MIN(MAX(value, INT_MIN), INT_MAX);
else
return 0;
}
/** Apply the data transformation according to the configured mapping destination. */
static void map_data(const mapping_t* mapping, snd_seq_ev_ctrl_t* ev) {
int mapped = clamp(mapping->dst.wildcard, get(mapping->src.wildcard, ev));
ev->channel = (mapping->dst.wildcard == CHANNEL) ? (unsigned char)mapped : mapping->dst.data.channel;
ev->param = (mapping->dst.wildcard == PARAM) ? (unsigned)mapped : mapping->dst.data.param;
ev->value = (mapping->dst.wildcard == VALUE) ? (int)mapped : mapping->dst.data.value;
}
/** Rewrite the event according to the matching rule. */
static void map_event(const mapping_t* mapping, snd_seq_event_t* ev) {
ev->type = mapping->dst.type;
map_data(mapping, &ev->data.control);
}
/** Main sequencer callback to apply the transformation if a rule matches. */
static snd_seq_event_t* convert(snd_seq_event_t* ev, void* ctx) {
ruleset_t* ruleset = (ruleset_t*)ctx;
if (ev->type != SND_SEQ_EVENT_CONTROLLER && ev->type != SND_SEQ_EVENT_PGMCHANGE) {
return ev;
}
const mapping_t* escape_mod = NULL;
if (ruleset->curr_mod != NULL) {
if (match(ev, &ruleset->curr_mod->mod)) {
ev->type = SND_SEQ_EVENT_NONE;
return ev;
} else if (match(ev, &ruleset->curr_mod->src)) {
map_event(ruleset->curr_mod, ev);
return ev;
} else {
escape_mod = ruleset->curr_mod;
ruleset->curr_mod = NULL;
}
}
for (size_t i = 0; i < ruleset->num; ++i) {
const mapping_t* rule = &ruleset->rules[i];
if (escape_mod != NULL && memcmp(&escape_mod->mod, &rule->mod, sizeof(event_t)) == 0 && match(ev, &rule->src)) {
ruleset->curr_mod = rule;
map_event(rule, ev);
return ev;
} else if (match(ev, &rule->mod)) {
ruleset->curr_mod = rule;
ev->type = SND_SEQ_EVENT_NONE;
return ev;
} else if (rule->mod.type != SND_SEQ_EVENT_NONE) {
continue;
} else if (match(ev, &rule->src)) {
map_event(rule, ev);
return ev;
}
}
return ev;
}
/** Parse a source/destination definition of the form: 'CTL|PGM[channel|*,param|*,value|*]' */
static int parse_event(const char* s, event_t* e) {
memset(e, 0, sizeof(event_t));
if (s == NULL) {
e->type = SND_SEQ_EVENT_NONE;
return 0;
}
while (*s == ' ') ++s;
if (strncmp(s, "CTL", 3) == 0) {
e->type = SND_SEQ_EVENT_CONTROLLER;
} else if (strncmp(s, "PGM", 3) == 0) {
e->type = SND_SEQ_EVENT_PGMCHANGE;
} else {
return -1;
}
if (sscanf(s + 3, "[%hhu,%u,%d]", &e->data.channel, &e->data.param, &e->data.value) == 3) {
e->wildcard = NONE;
} else if (sscanf(s + 3, "[*,%u,%d]", &e->data.param, &e->data.value) == 2) {
e->wildcard = CHANNEL;
} else if (sscanf(s + 3, "[%hhu,*,%d]", &e->data.channel, &e->data.value) == 2) {
e->wildcard = PARAM;
} else if (sscanf(s + 3, "[%hhu,%u,*]", &e->data.channel, &e->data.param) == 2) {
e->wildcard = VALUE;
} else {
return -1;
}
return 0;
}
/** Parse a 'source -> destination' pair (mapping).*/
static int parse_mapping(const char* s, mapping_t* mapping) {
const char* mod = strstr(s, "+");
const char* sep = strstr(s, "->");
if (mod != NULL && sep != NULL && parse_event(s, &mapping->mod) == 0 && parse_event(mod + 1, &mapping->src) == 0 && parse_event(sep + 2, &mapping->dst) == 0) {
return 0;
} else if (mod == NULL && sep != NULL && parse_event(NULL, &mapping->mod) == 0 && parse_event(s, &mapping->src) == 0 && parse_event(sep + 2, &mapping->dst) == 0) {
return 0;
} else {
return -1;
}
}
/** Append a newly parsed mapping to the ruleset. */
static int push_mapping(ruleset_t* ruleset, const char* arg) {
if (*arg == '\0' || *arg == '#' || *arg == '\r' || *arg == '\n') {
return 0;
}
ruleset->rules = realloc(ruleset->rules, sizeof(mapping_t) * (ruleset->num + 1));
if (parse_mapping(arg, &ruleset->rules[ruleset->num]) == 0) {
ruleset->num++;
return 0;
}
return -1;
}
int main(int argc, char* const* argv) {
ruleset_t ruleset = {0, NULL, NULL};
for (int i = 1; i < argc; ++i) {
if (push_mapping(&ruleset, argv[i]) != 0) {
free(ruleset.rules);
LOGS(
"List of event mappings expected.\n\n"
"Map CONTROLLER or PGMCHANGE events between ALSA MIDI sequencer i/o ports.\n"
"A wildcard can be used in place of a channel, param, or value - for example:\n"
"'CTL[0,77,*] -> PGM[9,0,*]'\n"
"Modifier events let the configured rules apply until matched otherwise, such as:\n"
"'CTL[*,99,0] + PGM[9,0,*] -> CTL[0,77,127]'");
return EXIT_FAILURE;
}
}
LOGV("got %zu mappings", ruleset.num);
if (register_handlers() != 0) {
return EXIT_FAILURE;
}
alsa_seq_client_t client;
if (client_open(&client, NAME) != 0) {
LOGS("cannot open alsa sequencer client");
return EXIT_FAILURE;
}
const int npfd = snd_seq_poll_descriptors_count(client.handle, POLLIN);
struct pollfd* const pfd = (struct pollfd*)calloc((size_t)npfd, sizeof(struct pollfd));
(void)snd_seq_poll_descriptors(client.handle, pfd, (unsigned)npfd, POLLIN);
LOGV("running as client %d", client.client_id);
while (!SHUTDOWN) {
if (poll(pfd, (unsigned long)npfd, 1000) > 0) {
client_input_loop(&client, &convert, &ruleset);
}
}
LOGS("shutting down");
client_close(&client);
free(pfd);
free(ruleset.rules);
return EXIT_SUCCESS;
}