aboutsummaryrefslogtreecommitdiffhomepage
path: root/backends
diff options
context:
space:
mode:
authorcbdev <cb@cbcdn.com>2018-03-02 03:20:11 +0100
committercbdev <cb@cbcdn.com>2018-03-02 03:20:11 +0100
commit2dfc564edc0c89c4a8de7e384806aae5d593426d (patch)
treeb1636ec14d3f35ed88b3f079e0c3d168f77b17b9 /backends
parentbe5df1c4e639ca6a7cd70a3122039a1de4588e28 (diff)
downloadmidimonster-2dfc564edc0c89c4a8de7e384806aae5d593426d.tar.gz
midimonster-2dfc564edc0c89c4a8de7e384806aae5d593426d.tar.bz2
midimonster-2dfc564edc0c89c4a8de7e384806aae5d593426d.zip
Move backend implementations to subdirectory
Diffstat (limited to 'backends')
-rw-r--r--backends/Makefile17
-rw-r--r--backends/artnet.c562
-rw-r--r--backends/artnet.h82
-rw-r--r--backends/evdev.c401
-rw-r--r--backends/evdev.h31
-rw-r--r--backends/loopback.c120
-rw-r--r--backends/loopback.h16
-rw-r--r--backends/midi.c366
-rw-r--r--backends/midi.h17
-rw-r--r--backends/osc.c813
-rw-r--r--backends/osc.h55
-rw-r--r--backends/sacn.c736
-rw-r--r--backends/sacn.h126
13 files changed, 3342 insertions, 0 deletions
diff --git a/backends/Makefile b/backends/Makefile
new file mode 100644
index 0000000..85fe152
--- /dev/null
+++ b/backends/Makefile
@@ -0,0 +1,17 @@
+.PHONY: all clean
+BACKENDS = artnet.so midi.so osc.so loopback.so evdev.so sacn.so
+
+CFLAGS += -fPIC -I../
+LDFLAGS += -shared
+
+midi.so: LDLIBS = -lasound
+evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev)
+evdev.so: LDLIBS = $(shell pkg-config --libs libevdev)
+
+%.so :: %.c %.h
+ $(CC) $(CFLAGS) $(LDLIBS) $< -o $@ $(LDFLAGS)
+
+all: $(BACKENDS)
+
+clean:
+ $(RM) $(BACKENDS)
diff --git a/backends/artnet.c b/backends/artnet.c
new file mode 100644
index 0000000..c16e630
--- /dev/null
+++ b/backends/artnet.c
@@ -0,0 +1,562 @@
+#include <string.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <ctype.h>
+#include <errno.h>
+
+#include "artnet.h"
+#define MAX_FDS 255
+#define BACKEND_NAME "artnet"
+
+static uint8_t default_net = 0;
+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;
+
+ 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");
+ return -1;
+ }
+
+ //store fd
+ artnet_fd = realloc(artnet_fd, (artnet_fds + 1) * sizeof(artnet_descriptor));
+ if(!artnet_fd){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return -1;
+ }
+
+ fprintf(stderr, "ArtNet backend interface %zu 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;
+ artnet_fd[artnet_fds].last_frame = NULL;
+ artnet_fds++;
+ 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,
+ .conf = artnet_configure,
+ .create = artnet_instance,
+ .conf_instance = artnet_configure_instance,
+ .channel = artnet_channel,
+ .handle = artnet_set,
+ .process = artnet_handle,
+ .start = artnet_start,
+ .shutdown = artnet_shutdown
+ };
+
+ //register backend
+ if(mm_backend_register(artnet)){
+ fprintf(stderr, "Failed to register ArtNet backend\n");
+ return 1;
+ }
+ return 0;
+}
+
+static int artnet_configure(char* option, char* value){
+ char* host = NULL, *port = NULL;
+ if(!strcmp(option, "net")){
+ //configure default net
+ default_net = strtoul(value, NULL, 0);
+ 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);
+ return 1;
+ }
+
+ if(artnet_listener(host, port)){
+ fprintf(stderr, "Failed to bind ArtNet descriptor: %s\n", value);
+ return 1;
+ }
+ return 0;
+ }
+
+ fprintf(stderr, "Unknown ArtNet backend option %s\n", option);
+ return 1;
+}
+
+static instance* artnet_instance(){
+ artnet_instance_data* data = NULL;
+ instance* inst = mm_instance();
+ if(!inst){
+ return NULL;
+ }
+
+ data = calloc(1, sizeof(artnet_instance_data));
+ if(!data){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return NULL;
+ }
+
+ data->net = default_net;
+
+ inst->impl = data;
+ return inst;
+}
+
+static int artnet_configure_instance(instance* inst, char* option, char* value){
+ char* host = NULL, *port = NULL;
+ artnet_instance_data* data = (artnet_instance_data*) inst->impl;
+
+ if(!strcmp(option, "net")){
+ data->net = strtoul(value, NULL, 0);
+ return 0;
+ }
+ else if(!strcmp(option, "uni") || !strcmp(option, "universe")){
+ data->uni = strtoul(value, NULL, 0);
+ return 0;
+ }
+ else if(!strcmp(option, "iface") || !strcmp(option, "interface")){
+ data->fd_index = strtoul(value, NULL, 0);
+
+ if(data->fd_index >= artnet_fds){
+ fprintf(stderr, "Invalid interface configured for ArtNet instance %s\n", inst->name);
+ return 1;
+ }
+ return 0;
+ }
+ else if(!strcmp(option, "dest") || !strcmp(option, "destination")){
+ if(artnet_separate_hostspec(value, &host, &port)){
+ 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);
+ }
+
+ fprintf(stderr, "Unknown ArtNet option %s for instance %s\n", option, inst->name);
+ return 1;
+}
+
+static channel* artnet_channel(instance* inst, char* spec){
+ artnet_instance_data* data = (artnet_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 ArtNet 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 ArtNet 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 ArtNet 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 artnet_transmit(instance* inst){
+ size_t u;
+ artnet_instance_data* data = (artnet_instance_data*) inst->impl;
+ //output frame
+ artnet_pkt frame = {
+ .magic = {'A', 'r', 't', '-', 'N', 'e', 't', 0x00},
+ .opcode = htobe16(OpDmx),
+ .version = htobe16(ARTNET_VERSION),
+ .sequence = data->data.seq++,
+ .port = 0,
+ .universe = data->uni,
+ .net = data->net,
+ .length = htobe16(512),
+ .data = {0}
+ };
+ 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){
+ fprintf(stderr, "Failed to output ArtNet frame for instance %s: %s\n", inst->name, strerror(errno));
+ }
+
+ //update last frame timestamp
+ for(u = 0; u < artnet_fd[data->fd_index].output_instances; u++){
+ if(artnet_fd[data->fd_index].output_instance[u].label == inst->ident){
+ artnet_fd[data->fd_index].last_frame[u] = mm_timestamp();
+ }
+ }
+ return 0;
+}
+
+static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v){
+ size_t u, mark = 0;
+ 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);
+ return 0;
+ }
+
+ //FIXME maybe introduce minimum frame interval
+ 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.out[c[u]->ident] != ((val >> 8) & 0xFF)){
+ mark = 1;
+ data->data.out[c[u]->ident] = (val >> 8) & 0xFF;
+ }
+
+ if(data->data.out[MAPPED_CHANNEL(data->data.map[c[u]->ident])] != (val & 0xFF)){
+ mark = 1;
+ data->data.out[MAPPED_CHANNEL(data->data.map[c[u]->ident])] = val & 0xFF;
+ }
+ }
+ else if(data->data.out[c[u]->ident] != (v[u].normalised * 255.0)){
+ mark = 1;
+ data->data.out[c[u]->ident] = v[u].normalised * 255.0;
+ }
+ }
+
+ if(mark){
+ return artnet_transmit(inst);
+ }
+
+ return 0;
+}
+
+static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){
+ size_t p, max_mark = 0;
+ uint16_t wide_val = 0;
+ channel* chan = NULL;
+ channel_value val;
+ artnet_instance_data* data = (artnet_instance_data*) inst->impl;
+
+ if(be16toh(frame->length) > 512){
+ fprintf(stderr, "Invalid ArtNet frame channel count\n");
+ return 1;
+ }
+
+ //read data, mark update channels
+ for(p = 0; p < be16toh(frame->length); p++){
+ if(IS_ACTIVE(data->data.map[p]) && frame->data[p] != data->data.in[p]){
+ data->data.in[p] = frame->data[p];
+ data->data.map[p] |= MAP_MARK;
+ max_mark = p;
+ }
+ }
+
+ //generate 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 1;
+ }
+
+ if(IS_WIDE(data->data.map[p])){
+ data->data.map[MAPPED_CHANNEL(data->data.map[p])] &= ~MAP_MARK;
+ wide_val = data->data.in[p] << ((data->data.map[p] & MAP_COARSE) ? 8 : 0);
+ wide_val |= data->data.in[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{
+ //single channel
+ val.raw.u64 = data->data.in[p];
+ val.normalised = (double) data->data.in[p] / 255.0;
+ }
+
+ if(mm_channel_event(chan, val)){
+ fprintf(stderr, "Failed to push ArtNet channel event to core\n");
+ return 1;
+ }
+ }
+ }
+ return 0;
+}
+
+static int artnet_handle(size_t num, managed_fd* fds){
+ size_t u, c;
+ uint64_t timestamp = mm_timestamp();
+ ssize_t bytes_read;
+ char recv_buf[ARTNET_RECV_BUF];
+ artnet_instance_id inst_id = {
+ .label = 0
+ };
+ instance* inst = NULL;
+ artnet_pkt* frame = (artnet_pkt*) recv_buf;
+
+ //transmit keepalive frames
+ for(u = 0; u < artnet_fds; u++){
+ for(c = 0; c < artnet_fd[u].output_instances; c++){
+ if(timestamp - artnet_fd[u].last_frame[c] >= ARTNET_KEEPALIVE_INTERVAL){
+ inst = mm_instance_find(BACKEND_NAME, artnet_fd[u].output_instance[c].label);
+ if(inst){
+ artnet_transmit(inst);
+ }
+ }
+ }
+ }
+
+ if(!num){
+ //early exit
+ return 0;
+ }
+
+ for(u = 0; u < num; u++){
+ do{
+ bytes_read = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0);
+ if(bytes_read > 0 && bytes_read > sizeof(artnet_hdr)){
+ if(!memcmp(frame->magic, "Art-Net\0", 8) && be16toh(frame->opcode) == OpDmx){
+ //find matching instance
+ inst_id.fields.fd_index = ((uint64_t) fds[u].impl) & 0xFF;
+ inst_id.fields.net = frame->net;
+ inst_id.fields.uni = frame->universe;
+ inst = mm_instance_find(BACKEND_NAME, inst_id.label);
+ if(inst && artnet_process_frame(inst, frame)){
+ fprintf(stderr, "Failed to process ArtNet frame\n");
+ }
+ }
+ }
+ } while(bytes_read > 0);
+
+ if(bytes_read < 0 && errno != EAGAIN){
+ fprintf(stderr, "ArtNet failed to receive data: %s\n", strerror(errno));
+ }
+
+ if(bytes_read == 0){
+ fprintf(stderr, "ArtNet listener closed\n");
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+static int artnet_start(){
+ size_t n, u, p;
+ int rv = 1;
+ instance** inst = NULL;
+ artnet_instance_data* data = NULL;
+ artnet_instance_id id = {
+ .label = 0
+ };
+
+ //fetch all defined instances
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ return 1;
+ }
+
+ if(!n){
+ free(inst);
+ return 0;
+ }
+
+ if(!artnet_fds){
+ fprintf(stderr, "Failed to start ArtNet backend: no descriptors bound\n");
+ return 1;
+ }
+
+ for(u = 0; u < n; u++){
+ data = (artnet_instance_data*) inst[u]->impl;
+ //set instance identifier
+ id.fields.fd_index = data->fd_index;
+ id.fields.net = data->net;
+ id.fields.uni = data->uni;
+ inst[u]->ident = id.label;
+
+ //check for duplicates
+ for(p = 0; p < u; p++){
+ if(inst[u]->ident == inst[p]->ident){
+ fprintf(stderr, "Universe specified multiple times, use one instance: %s - %s\n", inst[u]->name, inst[p]->name);
+ goto bail;
+ }
+ }
+
+ //if enabled for output, add to keepalive tracking
+ if(data->dest_len){
+ artnet_fd[data->fd_index].output_instance = realloc(artnet_fd[data->fd_index].output_instance, (artnet_fd[data->fd_index].output_instances + 1) * sizeof(artnet_instance_id));
+ artnet_fd[data->fd_index].last_frame = realloc(artnet_fd[data->fd_index].last_frame, (artnet_fd[data->fd_index].output_instances + 1) * sizeof(uint64_t));
+
+ if(!artnet_fd[data->fd_index].output_instance || !artnet_fd[data->fd_index].last_frame){
+ fprintf(stderr, "Failed to allocate memory\n");
+ goto bail;
+ }
+ artnet_fd[data->fd_index].output_instance[artnet_fd[data->fd_index].output_instances] = id;
+ artnet_fd[data->fd_index].last_frame[artnet_fd[data->fd_index].output_instances] = 0;
+
+ artnet_fd[data->fd_index].output_instances++;
+ }
+ }
+
+ fprintf(stderr, "ArtNet backend registering %zu 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;
+ }
+ }
+
+ rv = 0;
+bail:
+ free(inst);
+ return rv;
+}
+
+static int artnet_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);
+
+ for(p = 0; p < artnet_fds; p++){
+ close(artnet_fd[p].fd);
+ free(artnet_fd[p].output_instance);
+ free(artnet_fd[p].last_frame);
+ }
+ free(artnet_fd);
+
+ fprintf(stderr, "ArtNet backend shut down\n");
+ return 0;
+}
diff --git a/backends/artnet.h b/backends/artnet.h
new file mode 100644
index 0000000..90aedd5
--- /dev/null
+++ b/backends/artnet.h
@@ -0,0 +1,82 @@
+#include <sys/socket.h>
+#include "midimonster.h"
+
+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 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();
+static int artnet_shutdown();
+
+#define ARTNET_PORT "6454"
+#define ARTNET_VERSION 14
+#define ARTNET_RECV_BUF 4096
+#define ARTNET_KEEPALIVE_INTERVAL 2000
+
+#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)
+
+typedef struct /*_artnet_universe_model*/ {
+ uint8_t seq;
+ uint8_t in[512];
+ uint8_t out[512];
+ uint16_t map[512];
+} artnet_universe;
+
+typedef struct /*_artnet_instance_model*/ {
+ uint8_t net;
+ uint8_t uni;
+ struct sockaddr_storage dest_addr;
+ socklen_t dest_len;
+ artnet_universe data;
+ size_t fd_index;
+} artnet_instance_data;
+
+typedef union /*_artnet_instance_id*/ {
+ struct {
+ uint8_t fd_index;
+ uint8_t net;
+ uint8_t uni;
+ } fields;
+ uint64_t label;
+} artnet_instance_id;
+
+typedef struct /*_artnet_fd*/ {
+ int fd;
+ size_t output_instances;
+ artnet_instance_id* output_instance;
+ uint64_t* last_frame;
+} artnet_descriptor;
+
+#pragma pack(push, 1)
+typedef struct /*_artnet_hdr*/ {
+ uint8_t magic[8];
+ uint16_t opcode;
+ uint16_t version;
+} artnet_hdr;
+
+typedef struct /*_artnet_pkt*/ {
+ uint8_t magic[8];
+ uint16_t opcode;
+ uint16_t version;
+ uint8_t sequence;
+ uint8_t port;
+ uint8_t universe;
+ uint8_t net;
+ uint16_t length;
+ uint8_t data[512];
+} artnet_pkt;
+#pragma pack(pop)
+
+enum artnet_pkt_opcode {
+ OpDmx = 0x0050
+};
diff --git a/backends/evdev.c b/backends/evdev.c
new file mode 100644
index 0000000..ac63850
--- /dev/null
+++ b/backends/evdev.c
@@ -0,0 +1,401 @@
+#include <fcntl.h>
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <libevdev/libevdev.h>
+#ifndef EVDEV_NO_UINPUT
+#include <libevdev/libevdev-uinput.h>
+#endif
+
+#include "midimonster.h"
+#include "evdev.h"
+
+#define BACKEND_NAME "evdev"
+
+typedef union {
+ struct {
+ uint32_t pad;
+ uint16_t type;
+ uint16_t code;
+ } fields;
+ uint64_t label;
+} evdev_channel_ident;
+
+int init(){
+ backend evdev = {
+ .name = BACKEND_NAME,
+ .conf = evdev_configure,
+ .create = evdev_instance,
+ .conf_instance = evdev_configure_instance,
+ .channel = evdev_channel,
+ .handle = evdev_set,
+ .process = evdev_handle,
+ .start = evdev_start,
+ .shutdown = evdev_shutdown
+ };
+
+ if(mm_backend_register(evdev)){
+ fprintf(stderr, "Failed to register evdev backend\n");
+ return 1;
+ }
+
+ return 0;
+}
+
+static int evdev_configure(char* option, char* value) {
+ fprintf(stderr, "The evdev backend does not take any global configuration\n");
+ return 1;
+}
+
+static instance* evdev_instance(){
+ instance* inst = mm_instance();
+ if(!inst){
+ return NULL;
+ }
+
+ evdev_instance_data* data = calloc(1, sizeof(evdev_instance_data));
+ if(!data){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return NULL;
+ }
+
+ data->input_fd = -1;
+#ifndef EVDEV_NO_UINPUT
+ data->output_proto = libevdev_new();
+ if(!data->output_proto){
+ fprintf(stderr, "Failed to initialize libevdev output prototype device\n");
+ free(data);
+ return NULL;
+ }
+#endif
+
+ inst->impl = data;
+ return inst;
+}
+
+static int evdev_configure_instance(instance* inst, char* option, char* value) {
+ evdev_instance_data* data = (evdev_instance_data*) inst->impl;
+#ifndef EVDEV_NO_UINPUT
+ char* next_token = NULL;
+ struct input_absinfo abs_info = {
+ 0
+ };
+#endif
+
+ if(!strcmp(option, "input")){
+ if(data->input_fd >= 0){
+ fprintf(stderr, "Instance %s already was assigned an input device\n", inst->name);
+ return 1;
+ }
+
+ data->input_fd = open(value, O_RDONLY | O_NONBLOCK);
+ if(data->input_fd < 0){
+ fprintf(stderr, "Failed to open evdev input device node %s: %s\n", value, strerror(errno));
+ return 1;
+ }
+
+ if(libevdev_new_from_fd(data->input_fd, &data->input_ev)){
+ fprintf(stderr, "Failed to initialize libevdev for %s\n", value);
+ close(data->input_fd);
+ data->input_fd = -1;
+ return 1;
+ }
+
+ if(data->exclusive && libevdev_grab(data->input_ev, LIBEVDEV_GRAB)){
+ fprintf(stderr, "Failed to obtain exclusive device access on %s\n", value);
+ }
+ }
+ 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;
+ }
+#ifndef EVDEV_NO_UINPUT
+ else if(!strcmp(option, "name")){
+ data->output_enabled = 1;
+ libevdev_set_name(data->output_proto, value);
+ }
+ 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));
+ }
+ else if(!strncmp(option, "axis.", 5)){
+ //value minimum maximum fuzz flat resolution
+ next_token = value;
+ abs_info.value = strtol(next_token, &next_token, 0);
+ abs_info.minimum = strtol(next_token, &next_token, 0);
+ abs_info.maximum = strtol(next_token, &next_token, 0);
+ abs_info.fuzz = strtol(next_token, &next_token, 0);
+ abs_info.flat = strtol(next_token, &next_token, 0);
+ abs_info.resolution = strtol(next_token, &next_token, 0);
+ if(libevdev_enable_event_code(data->output_proto, EV_ABS, libevdev_event_code_from_name(EV_ABS, option + 5), &abs_info)){
+ fprintf(stderr, "Failed to enable absolute axis %s for output\n", option + 5);
+ return 1;
+ }
+ }
+#endif
+ else{
+ fprintf(stderr, "Unknown configuration parameter %s for evdev backend\n", option);
+ return 1;
+ }
+ return 0;
+}
+
+static channel* evdev_channel(instance* inst, char* spec){
+#ifndef EVDEV_NO_UINPUT
+ evdev_instance_data* data = (evdev_instance_data*) inst->impl;
+#endif
+ char* separator = strchr(spec, '.');
+ evdev_channel_ident ident = {
+ .label = 0
+ };
+
+ if(!separator){
+ fprintf(stderr, "Invalid evdev channel specification %s\n", spec);
+ return NULL;
+ }
+
+ *(separator++) = 0;
+
+ if(libevdev_event_type_from_name(spec) < 0){
+ fprintf(stderr, "Invalid evdev type specification: %s", spec);
+ return NULL;
+ }
+ ident.fields.type = libevdev_event_type_from_name(spec);
+
+ if(libevdev_event_code_from_name(ident.fields.type, separator) >= 0){
+ ident.fields.code = libevdev_event_code_from_name(ident.fields.type, separator);
+ }
+ else{
+ fprintf(stderr, "evdev Code name not recognized, using as number: %s\n", separator);
+ ident.fields.code = strtoul(separator, NULL, 10);
+ }
+
+#ifndef EVDEV_NO_UINPUT
+ if(data->output_enabled){
+ if(!libevdev_has_event_code(data->output_proto, ident.fields.type, ident.fields.code)){
+ //enable the event on the device
+ //the previous check is necessary to not fail while enabling axes, which require additional information
+ if(libevdev_enable_event_code(data->output_proto, ident.fields.type, ident.fields.code, NULL)){
+ fprintf(stderr, "Failed to enable output event %s.%s%s\n",
+ libevdev_event_type_get_name(ident.fields.type),
+ libevdev_event_code_get_name(ident.fields.type, ident.fields.code),
+ (ident.fields.type == EV_ABS) ? ": To output absolute axes, specify their details in the configuration":"");
+ return NULL;
+ }
+ }
+ }
+#endif
+
+ return mm_channel(inst, ident.label, 1);
+}
+
+static int evdev_push_event(instance* inst, evdev_instance_data* data, struct input_event event){
+ uint64_t range = 0;
+ channel_value val;
+ evdev_channel_ident ident = {
+ .fields.type = event.type,
+ .fields.code = event.code
+ };
+ channel* chan = mm_channel(inst, ident.label, 0);
+
+ if(chan){
+ val.raw.u64 = event.value;
+ switch(event.type){
+ case EV_REL:
+ val.normalised = 0.5 + ((event.value < 0) ? 0.5 : -0.5);
+ 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;
+ break;
+ case EV_KEY:
+ case EV_SW:
+ default:
+ val.normalised = 1.0 * event.value;
+ break;
+ }
+
+ if(mm_channel_event(chan, val)){
+ fprintf(stderr, "Failed to push evdev channel event to core\n");
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+static int evdev_handle(size_t num, managed_fd* fds){
+ instance* inst = NULL;
+ evdev_instance_data* data = NULL;
+ size_t fd;
+ unsigned int read_flags = LIBEVDEV_READ_FLAG_NORMAL;
+ int read_status;
+ struct input_event ev;
+
+ if(!num){
+ return 0;
+ }
+
+ for(fd = 0; fd < num; fd++){
+ inst = (instance*) fds[fd].impl;
+ if(!inst){
+ fprintf(stderr, "evdev backend signaled for unknown fd\n");
+ continue;
+ }
+
+ data = (evdev_instance_data*) inst->impl;
+
+ for(read_status = libevdev_next_event(data->input_ev, read_flags, &ev); read_status >= 0; read_status = libevdev_next_event(data->input_ev, read_flags, &ev)){
+ read_flags = LIBEVDEV_READ_FLAG_NORMAL;
+ if(read_status == LIBEVDEV_READ_STATUS_SYNC){
+ read_flags = LIBEVDEV_READ_FLAG_SYNC;
+ }
+
+ //handle event
+ if(evdev_push_event(inst, data, ev)){
+ return 1;
+ }
+ }
+ }
+
+ return 0;
+}
+
+static int evdev_start(){
+ size_t n, u, fds = 0;
+ instance** inst = NULL;
+ evdev_instance_data* data = NULL;
+
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ return 1;
+ }
+
+ if(!n){
+ free(inst);
+ return 0;
+ }
+
+ for(u = 0; u < n; u++){
+ data = (evdev_instance_data*) inst[u]->impl;
+
+#ifndef EVDEV_NO_UINPUT
+ if(data->output_enabled){
+ if(libevdev_uinput_create_from_device(data->output_proto, LIBEVDEV_UINPUT_OPEN_MANAGED, &data->output_ev)){
+ fprintf(stderr, "Failed to create evdev output device: %s\n", strerror(errno));
+ return 1;
+ }
+ fprintf(stderr, "Created device node %s for instance %s\n", libevdev_uinput_get_devnode(data->output_ev), inst[u]->name);
+ }
+#endif
+
+ inst[u]->ident = data->input_fd;
+ if(data->input_fd >= 0){
+ if(mm_manage_fd(data->input_fd, BACKEND_NAME, 1, inst[u])){
+ fprintf(stderr, "Failed to register event input descriptor for instance %s\n", inst[u]->name);
+ free(inst);
+ return 1;
+ }
+ fds++;
+ }
+
+ }
+
+ fprintf(stderr, "evdev backend registered %zu descriptors to core\n", fds);
+ free(inst);
+ return 0;
+}
+
+static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v) {
+#ifndef EVDEV_NO_UINPUT
+ size_t evt = 0;
+ evdev_instance_data* data = (evdev_instance_data*) inst->impl;
+ evdev_channel_ident ident = {
+ .label = 0
+ };
+ int32_t value = 0;
+ uint64_t range = 0;
+
+ if(!num){
+ return 0;
+ }
+
+ if(!data->output_enabled){
+ fprintf(stderr, "Instance %s not enabled for output\n", inst->name);
+ return 0;
+ }
+
+ for(evt = 0; evt < num; evt++){
+ ident.label = c[evt]->ident;
+
+ switch(ident.fields.type){
+ case EV_REL:
+ 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);
+ value = (range * v[evt].normalised) + libevdev_get_abs_minimum(data->output_proto, ident.fields.code);
+ break;
+ case EV_KEY:
+ case EV_SW:
+ default:
+ value = (v[evt].normalised > 0.9) ? 1 : 0;
+ break;
+ }
+
+ if(libevdev_uinput_write_event(data->output_ev, ident.fields.type, ident.fields.code, value)){
+ fprintf(stderr, "Failed to output event on instance %s\n", inst->name);
+ return 1;
+ }
+ }
+
+ //send syn event to publish all events
+ if(libevdev_uinput_write_event(data->output_ev, EV_SYN, SYN_REPORT, 0)){
+ fprintf(stderr, "Failed to output sync event on instance %s\n", inst->name);
+ return 1;
+ }
+
+ return 0;
+#else
+ fprintf(stderr, "The evdev backend does not support output on this platform\n");
+ return 1;
+#endif
+}
+
+static int evdev_shutdown(){
+ evdev_instance_data* data = NULL;
+ instance** instances = NULL;
+ size_t n, u;
+
+ if(mm_backend_instances(BACKEND_NAME, &n, &instances)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ return 1;
+ }
+
+ for(u = 0; u < n; u++){
+ data = (evdev_instance_data*) instances[u]->impl;
+
+ if(data->input_fd >= 0){
+ libevdev_free(data->input_ev);
+ close(data->input_fd);
+ }
+
+#ifndef EVDEV_NO_UINPUT
+ if(data->output_enabled){
+ libevdev_uinput_destroy(data->output_ev);
+ }
+
+ libevdev_free(data->output_proto);
+#endif
+ free(data);
+ }
+
+ free(instances);
+ return 0;
+}
diff --git a/backends/evdev.h b/backends/evdev.h
new file mode 100644
index 0000000..89a08ae
--- /dev/null
+++ b/backends/evdev.h
@@ -0,0 +1,31 @@
+#include <sys/types.h>
+
+#include "midimonster.h"
+
+/*
+ * This provides read-write access to the Linux kernel evdev subsystem
+ * via libevdev. On systems where uinput is not supported, output can be
+ * disabled by building with -DEVDEV_NO_UINPUT
+ */
+
+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 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();
+static int evdev_shutdown();
+
+typedef struct /*_evdev_instance_model*/ {
+ int input_fd;
+ struct libevdev* input_ev;
+ int exclusive;
+
+ int output_enabled;
+#ifndef EVDEV_NO_UINPUT
+ struct libevdev* output_proto;
+ struct libevdev_uinput* output_ev;
+#endif
+} evdev_instance_data;
diff --git a/backends/loopback.c b/backends/loopback.c
new file mode 100644
index 0000000..bb93a1f
--- /dev/null
+++ b/backends/loopback.c
@@ -0,0 +1,120 @@
+#include <string.h>
+#include "loopback.h"
+
+#define BACKEND_NAME "loopback"
+
+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
+ };
+
+ //register backend
+ if(mm_backend_register(loopback)){
+ fprintf(stderr, "Failed to register loopback backend\n");
+ return 1;
+ }
+ return 0;
+}
+
+static int backend_configure(char* option, char* value){
+ //intentionally ignored
+ return 0;
+}
+
+static int backend_configure_instance(instance* inst, char* option, char* value){
+ //intentionally ignored
+ return 0;
+}
+
+static instance* backend_instance(){
+ instance* i = mm_instance();
+ if(!i){
+ return NULL;
+ }
+
+ i->impl = calloc(1, sizeof(loopback_instance));
+ if(!i->impl){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return NULL;
+ }
+
+ return i;
+}
+
+static channel* backend_channel(instance* inst, char* spec){
+ size_t u;
+ loopback_instance* data = (loopback_instance*) inst->impl;
+
+ //find matching channel
+ for(u = 0; u < data->n; u++){
+ if(!strcmp(spec, data->name[u])){
+ break;
+ }
+ }
+
+ //allocate new channel
+ if(u == data->n){
+ data->name = realloc(data->name, (u + 1) * sizeof(char*));
+ if(!data->name){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return NULL;
+ }
+
+ data->name[u] = strdup(spec);
+ if(!data->name[u]){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return NULL;
+ }
+ data->n++;
+ }
+
+ return mm_channel(inst, u, 1);
+}
+
+static int backend_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]);
+ }
+ return 0;
+}
+
+static int backend_handle(size_t num, managed_fd* fds){
+ //no events generated here
+ return 0;
+}
+
+static int backend_start(){
+ return 0;
+}
+
+static int backend_shutdown(){
+ size_t n, u, p;
+ instance** inst = NULL;
+ loopback_instance* 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 = (loopback_instance*) inst[u]->impl;
+ for(p = 0; p < data->n; p++){
+ free(data->name[p]);
+ }
+ free(data->name);
+ free(inst[u]->impl);
+ }
+
+ free(inst);
+ return 0;
+}
diff --git a/backends/loopback.h b/backends/loopback.h
new file mode 100644
index 0000000..fe44e91
--- /dev/null
+++ b/backends/loopback.h
@@ -0,0 +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();
+
+typedef struct /*_loopback_instance_data*/ {
+ size_t n;
+ char** name;
+} loopback_instance;
diff --git a/backends/midi.c b/backends/midi.c
new file mode 100644
index 0000000..d856ced
--- /dev/null
+++ b/backends/midi.c
@@ -0,0 +1,366 @@
+#include <string.h>
+#include <alsa/asoundlib.h>
+#include "midi.h"
+
+#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;
+
+/*
+ * TODO
+ * Optionally send note-off messages
+ * Optionally send updates as after-touch
+ */
+
+enum /*_midi_channel_type*/ {
+ none = 0,
+ note,
+ cc,
+ nrpn,
+ sysmsg
+};
+
+int init(){
+ backend midi = {
+ .name = BACKEND_NAME,
+ .conf = midi_configure,
+ .create = midi_instance,
+ .conf_instance = midi_configure_instance,
+ .channel = midi_channel,
+ .handle = midi_set,
+ .process = midi_handle,
+ .start = midi_start,
+ .shutdown = midi_shutdown
+ };
+
+ 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;
+ }
+ return 0;
+ }
+
+ fprintf(stderr, "Unknown MIDI backend option %s\n", option);
+ return 1;
+}
+
+static instance* midi_instance(){
+ instance* inst = mm_instance();
+ if(!inst){
+ return NULL;
+ }
+
+ inst->impl = calloc(1, sizeof(midi_instance_data));
+ if(!inst->impl){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return NULL;
+ }
+
+ return inst;
+}
+
+static int midi_configure_instance(instance* instance, char* option, char* value){
+ midi_instance_data* data = (midi_instance_data*) instance->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");
+ return 1;
+ }
+ data->read = strdup(value);
+ return 0;
+ }
+ else if(!strcmp(option, "write")){
+ //connect output device
+ if(data->write){
+ fprintf(stderr, "MIDI port already connected to an output device\n");
+ return 1;
+ }
+ data->write = strdup(value);
+ return 0;
+ }
+
+ fprintf(stderr, "Unknown MIDI instance option %s\n", option);
+ return 1;
+}
+
+static channel* midi_channel(instance* instance, char* spec){
+ midi_channel_ident ident = {
+ .label = 0
+ };
+
+ char* channel;
+
+ if(!strncmp(spec, "cc", 2)){
+ ident.fields.type = cc;
+ channel = spec + 2;
+ }
+ else if(!strncmp(spec, "note", 4)){
+ ident.fields.type = note;
+ channel = spec + 4;
+ }
+ else if(!strncmp(spec, "nrpn", 4)){
+ ident.fields.type = nrpn;
+ channel = spec + 4;
+ }
+ else{
+ fprintf(stderr, "Unknown MIDI channel specification %s\n", spec);
+ return NULL;
+ }
+
+ ident.fields.channel = strtoul(channel, &channel, 10);
+
+ //FIXME test this
+ if(ident.fields.channel > 16){
+ 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);
+ return NULL;
+ }
+ channel++;
+
+ ident.fields.control = strtoul(channel, NULL, 10);
+
+ if(ident.label){
+ return mm_channel(instance, ident.label, 1);
+ }
+
+ return NULL;
+}
+
+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_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);
+ 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);
+ break;
+ case cc:
+ snd_seq_ev_set_controller(&ev, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0);
+ break;
+ case nrpn:
+ //FIXME set to nrpn output
+ break;
+ }
+
+ snd_seq_event_output(sequencer, &ev);
+ }
+
+ snd_seq_drain_output(sequencer);
+ return 0;
+}
+
+static int midi_handle(size_t num, managed_fd* fds){
+ snd_seq_event_t* ev = NULL;
+ instance* inst = NULL;
+ channel* changed = NULL;
+ channel_value val;
+ midi_channel_ident ident = {
+ .label = 0
+ };
+
+ if(!num){
+ return 0;
+ }
+
+ while(snd_seq_event_input(sequencer, &ev) > 0){
+ ident.label = 0;
+ 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_CONTROLLER:
+ ident.fields.type = cc;
+ ident.fields.channel = ev->data.control.channel;
+ ident.fields.control = ev->data.control.param;
+ val.raw.u64 = ev->data.control.value;
+ val.normalised = (double)ev->data.control.value / 127.0;
+ break;
+ case SND_SEQ_EVENT_CONTROL14:
+ case SND_SEQ_EVENT_NONREGPARAM:
+ case SND_SEQ_EVENT_REGPARAM:
+ //FIXME value calculation
+ ident.fields.type = nrpn;
+ ident.fields.channel = ev->data.control.channel;
+ ident.fields.control = ev->data.control.param;
+ break;
+ default:
+ fprintf(stderr, "Ignored MIDI event of unsupported type\n");
+ continue;
+ }
+
+ inst = mm_instance_find(BACKEND_NAME, ev->dest.port);
+ if(!inst){
+ //FIXME might want to return failure
+ fprintf(stderr, "Delivered MIDI event did not match any instance\n");
+ continue;
+ }
+
+ changed = mm_channel(inst, ident.label, 0);
+ if(changed){
+ if(mm_channel_event(changed, val)){
+ free(ev);
+ return 1;
+ }
+ }
+ }
+ free(ev);
+ return 0;
+}
+
+static int midi_start(){
+ size_t n, p;
+ int nfds, rv = 1;
+ struct pollfd* pfds = NULL;
+ instance** inst = NULL;
+ midi_instance_data* data = NULL;
+ snd_seq_addr_t addr;
+
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ return 1;
+ }
+
+ //if there are no ports, do nothing
+ if(!n){
+ free(inst);
+ return 0;
+ }
+
+ //create all ports
+ for(p = 0; p < n; p++){
+ data = (midi_instance_data*) inst[p]->impl;
+ data->port = snd_seq_create_simple_port(sequencer, inst[p]->name, SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE | SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ, SND_SEQ_PORT_TYPE_MIDI_GENERIC);
+ inst[p]->ident = data->port;
+
+ //make connections
+ if(data->write){
+ if(snd_seq_parse_address(sequencer, &addr, data->write) == 0){
+ fprintf(stderr, "Connecting output of instance %s to MIDI device %s (%d:%d)\n", inst[p]->name, data->write, addr.client, addr.port);
+ snd_seq_connect_to(sequencer, data->port, addr.client, addr.port);
+ }
+ else{
+ fprintf(stderr, "Failed to get destination MIDI device address: %s\n", data->write);
+ }
+ free(data->write);
+ data->write = NULL;
+ }
+
+ if(data->read){
+ if(snd_seq_parse_address(sequencer, &addr, data->read) == 0){
+ fprintf(stderr, "Connecting input from MIDI device %s to instance %s (%d:%d)\n", data->read, inst[p]->name, addr.client, addr.port);
+ snd_seq_connect_from(sequencer, data->port, addr.client, addr.port);
+ }
+ else{
+ fprintf(stderr, "Failed to get source MIDI device address: %s\n", data->read);
+ }
+ free(data->read);
+ data->read = NULL;
+ }
+ }
+
+ //register all fds to core
+ 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);
+
+ fprintf(stderr, "MIDI backend registering %d descriptors to core\n", nfds);
+ for(p = 0; p < nfds; p++){
+ if(mm_manage_fd(pfds[p].fd, BACKEND_NAME, 1, NULL)){
+ goto bail;
+ }
+ }
+
+ rv = 0;
+
+bail:
+ free(pfds);
+ free(inst);
+ return rv;
+}
+
+static int midi_shutdown(){
+ size_t n, p;
+ instance** inst = NULL;
+ midi_instance_data* data = 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++){
+ data = (midi_instance_data*) inst[p]->impl;
+ free(data->read);
+ free(data->write);
+ data->read = NULL;
+ data->write = NULL;
+ free(inst[p]->impl);
+ }
+ free(inst);
+
+ //close midi
+ snd_seq_close(sequencer);
+ sequencer = NULL;
+
+ //free configuration cache
+ snd_config_update_free_global();
+
+ fprintf(stderr, "MIDI backend shut down\n");
+ return 0;
+}
diff --git a/backends/midi.h b/backends/midi.h
new file mode 100644
index 0000000..556706f
--- /dev/null
+++ b/backends/midi.h
@@ -0,0 +1,17 @@
+#include "midimonster.h"
+
+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 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();
+static int midi_shutdown();
+
+typedef struct /*_midi_instance_data*/ {
+ int port;
+ char* read;
+ char* write;
+} midi_instance_data;
diff --git a/backends/osc.c b/backends/osc.c
new file mode 100644
index 0000000..adc91f5
--- /dev/null
+++ b/backends/osc.c
@@ -0,0 +1,813 @@
+#include <string.h>
+#include <unistd.h>
+#include <ctype.h>
+#include <netdb.h>
+#include <errno.h>
+#include <fcntl.h>
+#include "osc.h"
+
+/*
+ * TODO
+ * ping method
+ */
+
+#define osc_align(a) ((((a) / 4) + (((a) % 4) ? 1 : 0)) * 4)
+#define BACKEND_NAME "osc"
+
+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
+ };
+
+ //register backend
+ if(mm_backend_register(osc)){
+ fprintf(stderr, "Failed to register OSC backend\n");
+ return 1;
+ }
+ return 0;
+}
+
+static size_t osc_data_length(osc_parameter_type t){
+ switch(t){
+ case int32:
+ case float32:
+ return 4;
+ case int64:
+ case double64:
+ return 8;
+ default:
+ fprintf(stderr, "Invalid OSC format specified %c\n", t);
+ return 0;
+ }
+}
+
+static inline void osc_defaults(osc_parameter_type t, osc_parameter_value* max, osc_parameter_value* min){
+ memset(max, 0, sizeof(osc_parameter_value));
+ memset(min, 0, sizeof(osc_parameter_value));
+ switch(t){
+ case int32:
+ max->i32 = 255;
+ return;
+ case float32:
+ max->f = 1.0;
+ return;
+ case int64:
+ max->i64 = 1024;
+ return;
+ case double64:
+ max->d = 1.0;
+ return;
+ default:
+ fprintf(stderr, "Invalid OSC type, not setting any sane defaults\n");
+ return;
+ }
+}
+
+static inline osc_parameter_value osc_parse(osc_parameter_type t, uint8_t* data){
+ osc_parameter_value v = {0};
+ switch(t){
+ case int32:
+ case float32:
+ v.i32 = be32toh(*((int32_t*) data));
+ break;
+ case int64:
+ case double64:
+ v.i64 = be64toh(*((int64_t*) data));
+ break;
+ default:
+ fprintf(stderr, "Invalid OSC type passed to parsing routine\n");
+ }
+ return v;
+}
+
+static inline int osc_deparse(osc_parameter_type t, osc_parameter_value v, uint8_t* data){
+ uint64_t u64 = 0;
+ uint32_t u32 = 0;
+ switch(t){
+ case int32:
+ case float32:
+ u32 = htobe32(v.i32);
+ memcpy(data, &u32, sizeof(u32));
+ break;
+ case int64:
+ case double64:
+ u64 = htobe64(v.i64);
+ memcpy(data, &u64, sizeof(u64));
+ break;
+ default:
+ fprintf(stderr, "Invalid OSC type passed to parsing routine\n");
+ return 1;
+ }
+ return 0;
+}
+
+static inline osc_parameter_value osc_parse_value_spec(osc_parameter_type t, char* value){
+ osc_parameter_value v = {0};
+ switch(t){
+ case int32:
+ v.i32 = strtol(value, NULL, 0);
+ break;
+ case float32:
+ v.f = strtof(value, NULL);
+ break;
+ case int64:
+ v.i64 = strtoll(value, NULL, 0);
+ break;
+ case double64:
+ v.d = strtod(value, NULL);
+ break;
+ default:
+ fprintf(stderr, "Invalid OSC type passed to value parser\n");
+ }
+ return v;
+}
+
+static inline channel_value osc_parameter_normalise(osc_parameter_type t, osc_parameter_value min, osc_parameter_value max, osc_parameter_value cur){
+ channel_value v = {
+ .raw = {0},
+ .normalised = 0
+ };
+
+ union {
+ uint32_t u32;
+ float f32;
+ uint64_t u64;
+ double d64;
+ } range;
+
+ switch(t){
+ case int32:
+ range.u32 = max.i32 - min.i32;
+ v.raw.u64 = cur.i32 - min.i32;
+ v.normalised = v.raw.u64 / range.u32;
+ break;
+ case float32:
+ range.f32 = max.f - min.f;
+ v.raw.dbl = cur.f - min.f;
+ v.normalised = v.raw.dbl / range.f32;
+ break;
+ case int64:
+ range.u64 = max.i64 - min.i64;
+ v.raw.u64 = cur.i64 - min.i64;
+ v.normalised = v.raw.u64 / range.u64;
+ break;
+ case double64:
+ range.d64 = max.d - min.d;
+ v.raw.dbl = cur.d - min.d;
+ v.normalised = v.raw.dbl / range.d64;
+ break;
+ default:
+ 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;
+ }
+
+ return v;
+}
+
+static inline osc_parameter_value osc_parameter_denormalise(osc_parameter_type t, osc_parameter_value min, osc_parameter_value max, channel_value cur){
+ osc_parameter_value v = {0};
+
+ union {
+ uint32_t u32;
+ float f32;
+ uint64_t u64;
+ double d64;
+ } range;
+
+ switch(t){
+ case int32:
+ range.u32 = max.i32 - min.i32;
+ v.i32 = (range.u32 * cur.normalised) + min.i32;
+ break;
+ case float32:
+ range.f32 = max.f - min.f;
+ v.f = (range.f32 * cur.normalised) + min.f;
+ break;
+ case int64:
+ range.u64 = max.i64 - min.i64;
+ v.i64 = (range.u64 * cur.normalised) + min.i64;
+ break;
+ case double64:
+ range.d64 = max.d - min.d;
+ v.d = (range.d64 * cur.normalised) + min.d;
+ break;
+ default:
+ fprintf(stderr, "Invalid OSC type passed to interpolation routine\n");
+ }
+
+ 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;
+ }
+
+ osc_parameter_value min, max, cur;
+ channel_value evt;
+
+ if(!fmt || !data || data_len % 4 || !*fmt){
+ fprintf(stderr, "Invalid OSC packet, data length %zu\n", data_len);
+ return 1;
+ }
+
+ //find offset for this parameter
+ for(p = 0; p < info->param_index; p++){
+ off += osc_data_length(fmt[p]);
+ }
+
+ if(info->type != not_set){
+ max = info->max;
+ min = info->min;
+ }
+ else{
+ osc_defaults(fmt[info->param_index], &max, &min);
+ }
+
+ cur = osc_parse(fmt[info->param_index], data + off);
+ evt = osc_parameter_normalise(fmt[info->param_index], min, max, cur);
+
+ return mm_channel_event(c, evt);
+}
+
+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;
+ }
+ 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){
+ 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;
+}
+
+static int backend_configure_instance(instance* inst, char* option, char* value){
+ osc_instance* data = (osc_instance*) inst->impl;
+ char* host = NULL, *port = NULL, *token = NULL, *format = NULL;
+ size_t u, p;
+
+ if(!strcmp(option, "root")){
+ if(osc_validate_path(value)){
+ fprintf(stderr, "Not a valid OSC root: %s\n", value);
+ return 1;
+ }
+
+ if(data->root){
+ free(data->root);
+ }
+ data->root = strdup(value);
+
+ if(!data->root){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return 1;
+ }
+ return 0;
+ }
+ else if(!strcmp(option, "bind")){
+ if(osc_separate_hostspec(value, &host, &port)){
+ fprintf(stderr, "Invalid bind address for instance %s\n", inst->name);
+ return 1;
+ }
+
+ data->fd = osc_listener(host, port);
+ if(data->fd < 0){
+ fprintf(stderr, "Failed to bind for instance %s\n", inst->name);
+ return 1;
+ }
+ return 0;
+ }
+ else if(!strcmp(option, "dest") || !strcmp(option, "destination")){
+ if(!strncmp(value, "learn", 5)){
+ data->learn = 1;
+
+ //check if a forced port was provided
+ if(value[5] == '@'){
+ data->forced_rport = strtoul(value + 6, NULL, 0);
+ }
+ return 0;
+ }
+
+ if(osc_separate_hostspec(value, &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)){
+ fprintf(stderr, "Failed to parse destination address for instance %s\n", inst->name);
+ return 1;
+ }
+ 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;
+ }
+
+ fprintf(stderr, "Unknown configuration parameter %s for OSC backend\n", option);
+ return 1;
+}
+
+static instance* backend_instance(){
+ instance* inst = mm_instance();
+ if(!inst){
+ return NULL;
+ }
+
+ osc_instance* data = calloc(1, sizeof(osc_instance));
+ if(!data){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return NULL;
+ }
+
+ data->fd = -1;
+ inst->impl = data;
+ return inst;
+}
+
+static channel* backend_channel(instance* inst, char* spec){
+ size_t u;
+ osc_instance* data = (osc_instance*) inst->impl;
+ size_t param_index = 0;
+
+ //check spec for correctness
+ if(osc_validate_path(spec)){
+ return NULL;
+ }
+
+ //parse parameter offset
+ if(strrchr(spec, ':')){
+ param_index = 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);
+ break;
+ }
+ }
+
+ //allocate new channel
+ if(u == data->channels){
+ data->channel = realloc(data->channel, (u + 1) * sizeof(osc_channel));
+ if(!data->channel){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return NULL;
+ }
+
+ memset(data->channel + u, 0, sizeof(osc_channel));
+ data->channel[u].param_index = param_index;
+ data->channel[u].path = strdup(spec);
+
+ if(!data->channel[u].path){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return NULL;
+ }
+ data->channels++;
+ }
+
+ return mm_channel(inst, u, 1);
+}
+
+static int backend_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;
+ 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;
+ }
+
+ for(evt = 0; evt < num; evt++){
+ off = c[evt]->ident;
+
+ //sanity check
+ if(off >= data->channels){
+ 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);
+ 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;
+ }
+
+ //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);
+ }
+
+ //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;
+ }
+
+ 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;
+ }
+
+ //write format specifier
+ format[data->channel[p].param_index] = data->channel[p].type;
+
+ //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;
+ }
+
+ 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));
+ }
+ }
+ }
+ return 0;
+}
+
+static int backend_handle(size_t num, managed_fd* fds){
+ size_t fd;
+ char recv_buf[OSC_RECV_BUF];
+ instance* inst = NULL;
+ osc_instance* data = NULL;
+ ssize_t bytes_read = 0;
+ size_t c;
+ char* osc_fmt = NULL;
+ char* osc_local = NULL;
+ uint8_t* osc_data = NULL;
+
+ for(fd = 0; fd < num; fd++){
+ inst = (instance*) fds[fd].impl;
+ if(!inst){
+ fprintf(stderr, "OSC backend signaled for unknown fd\n");
+ continue;
+ }
+
+ data = (osc_instance*) inst->impl;
+
+ do{
+ if(data->learn){
+ data->dest_len = sizeof(data->dest);
+ bytes_read = recvfrom(fds[fd].fd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr*) &(data->dest), &(data->dest_len));
+ }
+ else{
+ bytes_read = recv(fds[fd].fd, recv_buf, sizeof(recv_buf), 0);
+ }
+ if(data->root && strncmp(recv_buf, data->root, 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
+ fprintf(stderr, "Invalid OSC format string in packet\n");
+ continue;
+ }
+ osc_fmt++;
+
+ osc_data = (uint8_t*) osc_fmt + (osc_align(strlen(osc_fmt) + 2) - 1);
+ //FIXME check supplied data length
+
+ 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");
+ }
+ }
+ }
+ }
+ } while(bytes_read > 0);
+
+ if(bytes_read < 0 && errno != EAGAIN){
+ fprintf(stderr, "OSC failed to receive data for instance %s: %s\n", inst->name, strerror(errno));
+ }
+
+ if(bytes_read == 0){
+ fprintf(stderr, "OSC descriptor for instance %s closed\n", inst->name);
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+static int backend_start(){
+ size_t n, u, fds = 0;
+ instance** inst = NULL;
+ osc_instance* data = NULL;
+
+ //fetch all instances
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ return 1;
+ }
+
+ if(!n){
+ free(inst);
+ return 0;
+ }
+
+ //update instance identifiers
+ for(u = 0; u < n; u++){
+ data = (osc_instance*) inst[u]->impl;
+
+ if(data->fd >= 0){
+ inst[u]->ident = data->fd;
+ if(mm_manage_fd(data->fd, BACKEND_NAME, 1, inst[u])){
+ fprintf(stderr, "Failed to register OSC descriptor for instance %s\n", inst[u]->name);
+ free(inst);
+ return 1;
+ }
+ fds++;
+ }
+ else{
+ inst[u]->ident = -1;
+ }
+ }
+
+ fprintf(stderr, "OSC backend registered %zu descriptors to core\n", fds);
+
+ free(inst);
+ return 0;
+}
+
+static int backend_shutdown(){
+ size_t n, u, c;
+ instance** inst = NULL;
+ osc_instance* 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 = (osc_instance*) inst[u]->impl;
+ for(c = 0; c < data->channels; c++){
+ free(data->channel[c].path);
+ }
+ free(data->channel);
+ free(data->root);
+ close(data->fd);
+ data->fd = -1;
+ data->channels = 0;
+ free(inst[u]->impl);
+ }
+
+ free(inst);
+ return 0;
+}
diff --git a/backends/osc.h b/backends/osc.h
new file mode 100644
index 0000000..5938f12
--- /dev/null
+++ b/backends/osc.h
@@ -0,0 +1,55 @@
+#include "midimonster.h"
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#define OSC_RECV_BUF 8192
+#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();
+
+typedef enum {
+ not_set = 0,
+ int32 = 'i',
+ float32 = 'f',
+ /*s, b*/ //ignored
+ int64 = 'h',
+ double64 = 'd',
+} osc_parameter_type;
+
+typedef union {
+ int32_t i32;
+ float f;
+ int64_t i64;
+ double d;
+} osc_parameter_value;
+
+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_channel;
+
+typedef struct /*_osc_instance_data*/ {
+ size_t channels;
+ osc_channel* channel;
+ char* root;
+ socklen_t dest_len;
+ struct sockaddr_storage dest;
+ int fd;
+ uint8_t learn;
+ uint16_t forced_rport;
+} osc_instance;
diff --git a/backends/sacn.c b/backends/sacn.c
new file mode 100644
index 0000000..9a3202d
--- /dev/null
+++ b/backends/sacn.c
@@ -0,0 +1,736 @@
+#include <string.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <ctype.h>
+#include <netinet/in.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"
+
+static struct /*_sacn_global_config*/ {
+ uint8_t source_name[64];
+ uint8_t cid[16];
+ size_t fds;
+ sacn_fd* fd;
+ uint64_t last_announce;
+} global_cfg = {
+ .source_name = "MIDIMonster",
+ .cid = {'M', 'I', 'D', 'I', 'M', 'o', 'n', 's', 't', 'e', 'r'},
+ .fds = 0,
+ .fd = NULL,
+ .last_announce = 0
+};
+
+int init(){
+ backend sacn = {
+ .name = BACKEND_NAME,
+ .conf = sacn_configure,
+ .create = sacn_instance,
+ .conf_instance = sacn_configure_instance,
+ .channel = sacn_channel,
+ .handle = sacn_set,
+ .process = sacn_handle,
+ .start = sacn_start,
+ .shutdown = sacn_shutdown
+ };
+
+ //register the backend
+ if(mm_backend_register(sacn)){
+ fprintf(stderr, "Failed to register sACN backend\n");
+ return 1;
+ }
+
+ return 0;
+}
+
+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;
+
+ 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");
+ return -1;
+ }
+
+ //store fd
+ global_cfg.fd = realloc(global_cfg.fd, (global_cfg.fds + 1) * sizeof(sacn_fd));
+ if(!global_cfg.fd){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return -1;
+ }
+
+ fprintf(stderr, "sACN backend interface %zu 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;
+ global_cfg.fd[global_cfg.fds].universe = NULL;
+ global_cfg.fd[global_cfg.fds].last_frame = NULL;
+ global_cfg.fds++;
+ 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;
+ size_t u;
+
+ if(!strcmp(option, "name")){
+ if(strlen(value) > 63){
+ fprintf(stderr, "Invalid sACN source name %s, limit is 63 characters\n", value);
+ return 1;
+ }
+
+ memset(global_cfg.source_name, 0, sizeof(global_cfg.source_name));
+ memcpy(global_cfg.source_name, value, strlen(value));
+ return 0;
+ }
+ else if(!strcmp(option, "cid")){
+ next = value;
+ for(u = 0; u < sizeof(global_cfg.cid); u++){
+ global_cfg.cid[u] = (strtoul(next, &next, 0) & 0xFF);
+ }
+ }
+ else if(!strcmp(option, "bind")){
+ if(sacn_parse_hostspec(value, &host, &port, &flags)){
+ fprintf(stderr, "Not a valid sACN bind address: %s\n", value);
+ return 1;
+ }
+
+ if(sacn_listener(host, port, flags)){
+ fprintf(stderr, "Failed to bind sACN descriptor: %s\n", value);
+ return 1;
+ }
+ return 0;
+ }
+
+ fprintf(stderr, "Unknown sACN backend option %s\n", option);
+ return 1;
+}
+
+static int sacn_configure_instance(instance* inst, char* option, char* value){
+ sacn_instance_data* data = (sacn_instance_data*) inst->impl;
+ char* host = NULL, *port = NULL, *next = NULL;
+ size_t u;
+
+ if(!strcmp(option, "universe")){
+ data->uni = strtoul(value, NULL, 10);
+ return 0;
+ }
+ else if(!strcmp(option, "interface")){
+ data->fd_index = strtoul(value, NULL, 10);
+
+ if(data->fd_index >= global_cfg.fds){
+ fprintf(stderr, "Configured sACN interface index is out of range on instance %s\n", inst->name);
+ return 1;
+ }
+ return 0;
+ }
+ else if(!strcmp(option, "priority")){
+ data->xmit_prio = strtoul(value, NULL, 10);
+ 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);
+ return 1;
+ }
+
+ return sacn_parse_addr(host, port, &data->dest_addr, &data->dest_len);
+ }
+ else if(!strcmp(option, "from")){
+ next = value;
+ data->filter_enabled = 1;
+ for(u = 0; u < sizeof(data->cid_filter); u++){
+ data->cid_filter[u] = (strtoul(next, &next, 0) & 0xFF);
+ }
+ fprintf(stderr, "Enabled source CID filter for instance %s\n", inst->name);
+ return 0;
+ }
+ else if(!strcmp(option, "unicast")){
+ data->unicast_input = strtoul(value, NULL, 10);
+ }
+
+ fprintf(stderr, "Unknown configuration option %s for sACN backend\n", option);
+ return 1;
+}
+
+static instance* sacn_instance(){
+ instance* inst = mm_instance();
+ if(!inst){
+ return NULL;
+ }
+
+ inst->impl = calloc(1, sizeof(sacn_instance_data));
+ if(!inst->impl){
+ fprintf(stderr, "Failed to allocate memory");
+ return NULL;
+ }
+
+ return inst;
+}
+
+static channel* sacn_channel(instance* inst, char* spec){
+ sacn_instance_data* data = (sacn_instance_data*) inst->impl;
+ char* spec_next = spec;
+
+ unsigned chan_a = strtoul(spec, &spec_next, 10), chan_b = 0;
+
+ //range check
+ if(!chan_a || chan_a > 512){
+ fprintf(stderr, "sACN channel out of range on instance %s: %s\n", inst->name, spec);
+ return NULL;
+ }
+ chan_a--;
+
+ //if wide channel, mark fine
+ if(*spec_next == '+'){
+ chan_b = strtoul(spec_next + 1, NULL, 10);
+ if(!chan_b || chan_b > 512){
+ fprintf(stderr, "Invalid wide-channel spec on instance %s: %s\n", inst->name, spec);
+ return NULL;
+ }
+ chan_b--;
+
+ //if already mapped, bail
+ if(IS_ACTIVE(data->data.map[chan_b]) && data->data.map[chan_b] != (MAP_FINE | chan_a)){
+ fprintf(stderr, "Fine channel %u already mapped on instance %s\n", chan_b, inst->name);
+ return NULL;
+ }
+
+ data->data.map[chan_b] = MAP_FINE | chan_a;
+ }
+
+ //if already active, assert that nothing changes
+ 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 sACN channel %u already mapped in another mode on instance %s\n", chan_a, inst->name);
+ 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 sacn_transmit(instance* inst){
+ size_t u;
+ sacn_instance_data* data = (sacn_instance_data*) inst->impl;
+ sacn_data_pdu pdu = {
+ .root = {
+ .preamble_size = htobe16(0x10),
+ .postamble_size = 0,
+ .magic = { 0 }, //memcpy'd
+ .flags = htobe16(0x7000 | 0x026e),
+ .vector = htobe32(ROOT_E131_DATA),
+ .sender_cid = { 0 }, //memcpy'd
+ .frame_flags = htobe16(0x7000 | 0x0258),
+ .frame_vector = htobe32(FRAME_E131_DATA)
+ },
+ .data = {
+ .source_name = "", //memcpy'd
+ .priority = data->xmit_prio,
+ .sync_addr = 0,
+ .sequence = data->data.last_seq++,
+ .options = 0,
+ .universe = htobe16(data->uni),
+ .flags = htobe16(0x7000 | 0x0205),
+ .vector = DMP_SET_PROPERTY,
+ .format = 0xA1,
+ .startcode_offset = 0,
+ .address_increment = htobe16(1),
+ .channels = htobe16(513),
+ .data = { 0 } //memcpy'd
+ }
+ };
+
+ memcpy(pdu.root.magic, SACN_PDU_MAGIC, sizeof(pdu.root.magic));
+ memcpy(pdu.root.sender_cid, global_cfg.cid, sizeof(pdu.root.sender_cid));
+ 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){
+ fprintf(stderr, "Failed to output sACN frame for instance %s: %s\n", inst->name, strerror(errno));
+ }
+
+ //update last transmit timestamp
+ for(u = 0; u < global_cfg.fd[data->fd_index].universes; u++){
+ if(global_cfg.fd[data->fd_index].universe[u] == data->uni){
+ global_cfg.fd[data->fd_index].last_frame[u] = mm_timestamp();
+ }
+ }
+ return 0;
+}
+
+static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){
+ size_t u, mark = 0;
+ sacn_instance_data* data = (sacn_instance_data*) inst->impl;
+
+ if(!num){
+ return 0;
+ }
+
+ if(!data->xmit_prio){
+ fprintf(stderr, "sACN instance %s not enabled for output (%zu channel events)\n", inst->name, num);
+ return 0;
+ }
+
+ for(u = 0; u < num; u++){
+ if(IS_WIDE(data->data.map[c[u]->ident])){
+ uint32_t val = v[u].normalised * ((double) 0xFFFF);
+
+ if(data->data.out[c[u]->ident] != ((val >> 8) & 0xFF)){
+ mark = 1;
+ data->data.out[c[u]->ident] = (val >> 8) & 0xFF;
+ }
+
+ if(data->data.out[MAPPED_CHANNEL(data->data.map[c[u]->ident])] != (val & 0xFF)){
+ mark = 1;
+ data->data.out[MAPPED_CHANNEL(data->data.map[c[u]->ident])] = val & 0xFF;
+ }
+ }
+ else if(data->data.out[c[u]->ident] != (v[u].normalised * 255.0)){
+ mark = 1;
+ data->data.out[c[u]->ident] = v[u].normalised * 255.0;
+ }
+ }
+
+ //send packet if required
+ if(mark){
+ sacn_transmit(inst);
+ }
+
+ return 0;
+}
+
+static int sacn_process_frame(instance* inst, sacn_frame_root* frame, sacn_frame_data* data){
+ size_t u, max_mark = 0;
+ channel* chan = NULL;
+ channel_value val;
+ sacn_instance_data* inst_data = (sacn_instance_data*) inst->impl;
+
+ //source filtering
+ if(inst_data->filter_enabled && memcmp(inst_data->cid_filter, frame->sender_cid, 16)){
+ return 0;
+ }
+
+ if(data->format != 0xa1
+ || data->startcode_offset
+ || be16toh(data->address_increment) != 1){
+ fprintf(stderr, "sACN framing not supported\n");
+ return 1;
+ }
+
+ if(be16toh(data->channels) > 513){
+ fprintf(stderr, "Invalid sACN frame channel count\n");
+ return 1;
+ }
+
+ //handle source priority (currently a 1-bit counter)
+ if(inst_data->data.last_priority > data->priority){
+ inst_data->data.last_priority = data->priority;
+ return 0;
+ }
+ inst_data->data.last_priority = data->priority;
+
+ //read data (except start code), mark changed channels
+ for(u = 1; u < be16toh(data->channels); u++){
+ if(IS_ACTIVE(inst_data->data.map[u - 1])
+ && data->data[u] != inst_data->data.in[u - 1]){
+ inst_data->data.in[u - 1] = data->data[u];
+ inst_data->data.map[u - 1] |= MAP_MARK;
+ max_mark = u - 1;
+ }
+ }
+
+ //generate events
+ for(u = 0; u <= max_mark; u++){
+ if(inst_data->data.map[u] & MAP_MARK){
+ //unmark and get channel
+ inst_data->data.map[u] &= ~MAP_MARK;
+ if(inst_data->data.map[u] & MAP_FINE){
+ chan = mm_channel(inst, MAPPED_CHANNEL(inst_data->data.map[u]), 0);
+ }
+ else{
+ chan = mm_channel(inst, u, 0);
+ }
+
+ if(!chan){
+ fprintf(stderr, "Active channel %zu on %s not known to core", u, inst->name);
+ return 1;
+ }
+
+ //generate value
+ if(IS_WIDE(inst_data->data.map[u])){
+ inst_data->data.map[MAPPED_CHANNEL(inst_data->data.map[u])] &= ~MAP_MARK;
+ val.raw.u64 = inst_data->data.in[u] << ((inst_data->data.map[u] & MAP_COARSE) ? 8 : 0);
+ val.raw.u64 |= inst_data->data.in[MAPPED_CHANNEL(inst_data->data.map[u])] << ((inst_data->data.map[u] & MAP_COARSE) ? 0 : 8);
+ val.normalised = (double) val.raw.u64 / (double) 0xFFFF;
+ }
+ else{
+ val.raw.u64 = inst_data->data.in[u];
+ val.normalised = (double) val.raw.u64 / 255.0;
+ }
+
+ if(mm_channel_event(chan, val)){
+ fprintf(stderr, "Failed to push sACN channel event to core\n");
+ return 1;
+ }
+ }
+ }
+ return 0;
+}
+
+static void sacn_discovery(size_t fd){
+ size_t page = 0, pages = (global_cfg.fd[fd].universes / 512) + 1, universes;
+ struct sockaddr_in discovery_dest = {
+ .sin_family = AF_INET,
+ .sin_port = htobe16(SACN_PORT),
+ .sin_addr.s_addr = htobe32(((uint32_t) 0xefff0000) | 64214)
+ };
+
+ sacn_discovery_pdu pdu = {
+ .root = {
+ .preamble_size = htobe16(0x10),
+ .postamble_size = 0,
+ .magic = { 0 }, //memcpy'd
+ .flags = 0, //filled later
+ .vector = htobe32(ROOT_E131_EXTENDED),
+ .sender_cid = { 0 }, //memcpy'd
+ .frame_flags = 0, //filled later
+ .frame_vector = htobe32(FRAME_E131_DISCOVERY)
+ },
+ .data = {
+ .source_name = "", //memcpy'd
+ .flags = 0, //filled later
+ .vector = htobe32(DISCOVERY_UNIVERSE_LIST),
+ .page = 0, //filled later
+ .max_page = pages - 1,
+ .data = { 0 } //memcpy'd
+ }
+ };
+
+ memcpy(pdu.root.magic, SACN_PDU_MAGIC, sizeof(pdu.root.magic));
+ memcpy(pdu.root.sender_cid, global_cfg.cid, sizeof(pdu.root.sender_cid));
+ memcpy(pdu.data.source_name, global_cfg.source_name, sizeof(pdu.data.source_name));
+
+ for(; page < pages; page++){
+ universes = (global_cfg.fd[fd].universes - page * 512 >= 512) ? 512 : (global_cfg.fd[fd].universes % 512);
+ pdu.root.flags = htobe16(0x7000 | (104 + universes * sizeof(uint16_t)));
+ pdu.root.frame_flags = htobe16(0x7000 | (82 + universes * sizeof(uint16_t)));
+ pdu.data.flags = htobe16(0x7000 | (8 + universes * sizeof(uint16_t)));
+
+ 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));
+ }
+ }
+}
+
+static int sacn_handle(size_t num, managed_fd* fds){
+ size_t u, c;
+ uint64_t timestamp = mm_timestamp();
+ ssize_t bytes_read;
+ char recv_buf[SACN_RECV_BUF];
+ instance* inst = NULL;
+ sacn_instance_id instance_id = {
+ .label = 0
+ };
+ sacn_frame_root* frame = (sacn_frame_root*) recv_buf;
+ sacn_frame_data* data = (sacn_frame_data*) (recv_buf + sizeof(sacn_frame_root));
+
+ if(mm_timestamp() - global_cfg.last_announce > SACN_DISCOVERY_TIMEOUT){
+ //send universe discovery pdu
+ for(u = 0; u < global_cfg.fds; u++){
+ if(global_cfg.fd[u].universes){
+ sacn_discovery(u);
+ }
+ }
+ global_cfg.last_announce = timestamp;
+ }
+
+ //check for keepalive frames
+ for(u = 0; u < global_cfg.fds; u++){
+ for(c = 0; c < global_cfg.fd[u].universes; c++){
+ if(timestamp - global_cfg.fd[u].last_frame[c] >= SACN_KEEPALIVE_INTERVAL){
+ instance_id.fields.fd_index = u;
+ instance_id.fields.uni = global_cfg.fd[u].universe[c];
+ inst = mm_instance_find(BACKEND_NAME, instance_id.label);
+ if(inst){
+ sacn_transmit(inst);
+ }
+ }
+ }
+ }
+
+ //early exit
+ if(!num){
+ return 0;
+ }
+
+ for(u = 0; u < num; u++){
+ do{
+ bytes_read = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0);
+ if(bytes_read > 0 && bytes_read > sizeof(sacn_frame_root)){
+ if(!memcmp(frame->magic, SACN_PDU_MAGIC, 12)
+ && be16toh(frame->preamble_size) == 0x10
+ && frame->postamble_size == 0
+ && be32toh(frame->vector) == ROOT_E131_DATA
+ && be32toh(frame->frame_vector) == FRAME_E131_DATA
+ && data->vector == DMP_SET_PROPERTY){
+ instance_id.fields.fd_index = ((uint64_t) fds[u].impl) & 0xFFFF;
+ instance_id.fields.uni = be16toh(data->universe);
+ inst = mm_instance_find(BACKEND_NAME, instance_id.label);
+ if(inst && sacn_process_frame(inst, frame, data)){
+ fprintf(stderr, "Failed to process sACN frame\n");
+ }
+ }
+ }
+ } while(bytes_read > 0);
+
+ if(bytes_read < 0 && errno != EAGAIN){
+ fprintf(stderr, "sACN failed to receive data: %s\n", strerror(errno));
+ }
+
+ if(bytes_read == 0){
+ fprintf(stderr, "sACN listener closed\n");
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+static int sacn_start(){
+ size_t n, u, p;
+ int rv = 1;
+ instance** inst = NULL;
+ sacn_instance_data* data = NULL;
+ sacn_instance_id id = {
+ .label = 0
+ };
+ struct ip_mreq mcast_req = {
+ .imr_interface = { INADDR_ANY }
+ };
+ struct sockaddr_in* dest_v4 = NULL;
+
+ //fetch all instances
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ return 1;
+ }
+
+ if(!n){
+ free(inst);
+ return 0;
+ }
+
+ if(!global_cfg.fds){
+ fprintf(stderr, "Failed to start sACN backend: no descriptors bound\n");
+ return 1;
+ }
+
+ //update instance identifiers, join multicast groups
+ for(u = 0; u < n; u++){
+ data = (sacn_instance_data*) inst[u]->impl;
+ id.fields.fd_index = data->fd_index;
+ id.fields.uni = data->uni;
+ inst[u]->ident = id.label;
+
+ if(!data->uni){
+ fprintf(stderr, "Please specify a universe on instance %s\n", inst[u]->name);
+ goto bail;
+ }
+
+ //find duplicates
+ for(p = 0; p < u; p++){
+ if(inst[u]->ident == inst[p]->ident){
+ fprintf(stderr, "Colliding sACN instances, use one: %s - %s\n", inst[u]->name, inst[p]->name);
+ goto bail;
+ }
+ }
+
+ 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))){
+ fprintf(stderr, "Failed to join Multicast group for sACN universe %u on instance %s: %s\n", data->uni, inst[u]->name, strerror(errno));
+ }
+ }
+
+ if(data->xmit_prio){
+ //add to list of advertised universes for this fd
+ global_cfg.fd[data->fd_index].universe = realloc(global_cfg.fd[data->fd_index].universe, (global_cfg.fd[data->fd_index].universes + 1) * sizeof(uint16_t));
+ if(!global_cfg.fd[data->fd_index].universe){
+ fprintf(stderr, "Failed to allocate memory\n");
+ goto bail;
+ }
+
+ global_cfg.fd[data->fd_index].universe[global_cfg.fd[data->fd_index].universes] = data->uni;
+ global_cfg.fd[data->fd_index].universes++;
+
+ //generate multicast destination address if none set
+ if(!data->dest_len){
+ data->dest_len = sizeof(struct sockaddr_in);
+ dest_v4 = (struct sockaddr_in*) (&data->dest_addr);
+ dest_v4->sin_family = AF_INET;
+ dest_v4->sin_port = htobe16(strtoul(SACN_PORT, NULL, 10));
+ dest_v4->sin_addr = mcast_req.imr_multiaddr;
+ }
+ }
+ }
+
+ fprintf(stderr, "sACN backend registering %zu 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));
+ if(!global_cfg.fd[u].last_frame){
+ fprintf(stderr, "Failed to allocate memory\n");
+ goto bail;
+ }
+ if(mm_manage_fd(global_cfg.fd[u].fd, BACKEND_NAME, 1, (void*) u)){
+ goto bail;
+ }
+ }
+
+ rv = 0;
+bail:
+ free(inst);
+ return rv;
+}
+
+static int sacn_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);
+
+ for(p = 0; p < global_cfg.fds; p++){
+ close(global_cfg.fd[p].fd);
+ free(global_cfg.fd[p].universe);
+ free(global_cfg.fd[p].last_frame);
+ }
+ free(global_cfg.fd);
+ fprintf(stderr, "sACN backend shut down\n");
+ return 0;
+}
diff --git a/backends/sacn.h b/backends/sacn.h
new file mode 100644
index 0000000..e7106f7
--- /dev/null
+++ b/backends/sacn.h
@@ -0,0 +1,126 @@
+#include <sys/socket.h>
+#include "midimonster.h"
+
+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 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();
+static int sacn_shutdown();
+
+#define SACN_PORT "5568"
+#define SACN_RECV_BUF 8192
+#define SACN_KEEPALIVE_INTERVAL 2000
+#define SACN_DISCOVERY_TIMEOUT 9000
+#define SACN_PDU_MAGIC "ASC-E1.17\0\0\0"
+
+#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)
+
+typedef struct /*_sacn_universe_model*/ {
+ uint8_t last_priority;
+ uint8_t last_seq;
+ uint8_t in[512];
+ uint8_t out[512];
+ uint16_t map[512];
+} sacn_universe;
+
+typedef struct /*_sacn_instance_model*/ {
+ uint16_t uni;
+ uint8_t xmit_prio;
+ uint8_t cid_filter[16];
+ uint8_t filter_enabled;
+ uint8_t unicast_input;
+ struct sockaddr_storage dest_addr;
+ socklen_t dest_len;
+ sacn_universe data;
+ size_t fd_index;
+} sacn_instance_data;
+
+typedef union /*_sacn_instance_id*/ {
+ struct {
+ uint16_t fd_index;
+ uint16_t uni;
+ uint8_t pad[4];
+ } fields;
+ uint64_t label;
+} sacn_instance_id;
+
+typedef struct /*_sacn_socket*/ {
+ int fd;
+ uint8_t flags;
+ size_t universes;
+ uint16_t* universe;
+ uint64_t* last_frame;
+} sacn_fd;
+
+#pragma pack(push, 1)
+typedef struct /*_sacn_frame_root*/ {
+ uint16_t preamble_size;
+ uint16_t postamble_size;
+ uint8_t magic[12];
+ uint16_t flags;
+ uint32_t vector;
+ uint8_t sender_cid[16];
+ //framing
+ uint16_t frame_flags;
+ uint32_t frame_vector;
+} sacn_frame_root;
+
+typedef struct /*_sacn_frame_data*/ {
+ //framing
+ uint8_t source_name[64];
+ uint8_t priority;
+ uint16_t sync_addr;
+ uint8_t sequence;
+ uint8_t options;
+ uint16_t universe;
+ //dmp
+ uint16_t flags;
+ uint8_t vector;
+ uint8_t format;
+ uint16_t startcode_offset;
+ uint16_t address_increment;
+ uint16_t channels;
+ uint8_t data[513];
+} sacn_frame_data;
+
+typedef struct /*_sacn_frame_discovery*/ {
+ //framing
+ uint8_t source_name[64];
+ uint32_t reserved;
+ //universe discovery
+ uint16_t flags;
+ uint32_t vector;
+ uint8_t page;
+ uint8_t max_page;
+ uint16_t data[512];
+} sacn_frame_discovery;
+
+typedef struct /*_sacn_xmit_data*/ {
+ sacn_frame_root root;
+ sacn_frame_data data;
+} sacn_data_pdu;
+
+typedef struct /*_sacn_xmit_discovery*/ {
+ sacn_frame_root root;
+ sacn_frame_discovery data;
+} sacn_discovery_pdu;
+#pragma pack(pop)
+
+#define ROOT_E131_DATA 0x4
+#define FRAME_E131_DATA 0x2
+#define DMP_SET_PROPERTY 0x2
+
+#define ROOT_E131_EXTENDED 0x8
+#define FRAME_E131_DISCOVERY 0x2
+#define DISCOVERY_UNIVERSE_LIST 0x1