aboutsummaryrefslogtreecommitdiffhomepage
path: root/backends
diff options
context:
space:
mode:
authorcbdev <cb@cbcdn.com>2019-11-06 18:50:57 +0100
committercbdev <cb@cbcdn.com>2019-11-06 18:50:57 +0100
commitff587cb77ee4a7e9169affbfefd84547da6fea38 (patch)
treefefa6e701021af25aee8371ede8682d554fd768c /backends
parent24e5594c754ec74918848d33d513db69d54aba47 (diff)
downloadmidimonster-ff587cb77ee4a7e9169affbfefd84547da6fea38.tar.gz
midimonster-ff587cb77ee4a7e9169affbfefd84547da6fea38.tar.bz2
midimonster-ff587cb77ee4a7e9169affbfefd84547da6fea38.zip
Implement JACK backend
Diffstat (limited to 'backends')
-rw-r--r--backends/Makefile3
-rw-r--r--backends/jack.c742
-rw-r--r--backends/jack.h76
-rw-r--r--backends/jack.md84
4 files changed, 904 insertions, 1 deletions
diff --git a/backends/Makefile b/backends/Makefile
index 293b434..c5755c9 100644
--- a/backends/Makefile
+++ b/backends/Makefile
@@ -1,5 +1,5 @@
.PHONY: all clean full
-LINUX_BACKENDS = midi.so evdev.so
+LINUX_BACKENDS = midi.so evdev.so jack.so
WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll
BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so
OPTIONAL_BACKENDS = ola.so
@@ -41,6 +41,7 @@ maweb.dll: CFLAGS += -DMAWEB_NO_LIBSSL
winmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
winmidi.dll: LDLIBS += -lwinmm -lws2_32
+jack.so: LDLIBS = -ljack -lpthread
midi.so: LDLIBS = -lasound
evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev)
evdev.so: LDLIBS = $(shell pkg-config --libs libevdev)
diff --git a/backends/jack.c b/backends/jack.c
new file mode 100644
index 0000000..5a88cf2
--- /dev/null
+++ b/backends/jack.c
@@ -0,0 +1,742 @@
+#include <string.h>
+#include <signal.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include "jack.h"
+#include <jack/midiport.h>
+#include <jack/metadata.h>
+
+#define BACKEND_NAME "jack"
+#define JACKEY_SIGNAL_TYPE "http://jackaudio.org/metadata/signal-type"
+
+//FIXME pitchbend range is somewhat oob
+
+static struct /*_mmjack_backend_cfg*/ {
+ unsigned verbosity;
+ volatile sig_atomic_t jack_shutdown;
+} config = {
+ .verbosity = 1,
+ .jack_shutdown = 0
+};
+
+int init(){
+ backend mmjack = {
+ .name = BACKEND_NAME,
+ .conf = mmjack_configure,
+ .create = mmjack_instance,
+ .conf_instance = mmjack_configure_instance,
+ .channel = mmjack_channel,
+ .handle = mmjack_set,
+ .process = mmjack_handle,
+ .start = mmjack_start,
+ .shutdown = mmjack_shutdown
+ };
+
+ if(sizeof(mmjack_channel_ident) != sizeof(uint64_t)){
+ fprintf(stderr, "jack channel identification union out of bounds\n");
+ return 1;
+ }
+
+ //register backend
+ if(mm_backend_register(mmjack)){
+ fprintf(stderr, "Failed to register jack backend\n");
+ return 1;
+ }
+ return 0;
+}
+
+static void mmjack_message_print(const char* msg){
+ fprintf(stderr, "JACK message: %s\n", msg);
+}
+
+static void mmjack_message_ignore(const char* msg){
+}
+
+static int mmjack_midiqueue_append(mmjack_port* port, mmjack_channel_ident ident, uint16_t value){
+ //append events
+ if(port->queue_len == port->queue_alloc){
+ //extend the queue
+ port->queue = realloc(port->queue, (port->queue_len + JACK_MIDIQUEUE_CHUNK) * sizeof(mmjack_midiqueue));
+ if(!port->queue){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return 1;
+ }
+ port->queue_alloc += JACK_MIDIQUEUE_CHUNK;
+ }
+
+ port->queue[port->queue_len].ident.label = ident.label;
+ port->queue[port->queue_len].raw = value;
+ port->queue_len++;
+ DBGPF("Appended event to queue for %s, now at %" PRIsize_t " entries\n", port->name, port->queue_len);
+ return 0;
+}
+
+static int mmjack_process_midi(instance* inst, mmjack_port* port, size_t nframes, size_t* mark){
+ void* buffer = jack_port_get_buffer(port->port, nframes);
+ jack_nframes_t event_count = jack_midi_get_event_count(buffer);
+ jack_midi_event_t event;
+ jack_midi_data_t* event_data;
+ mmjack_channel_ident ident;
+ size_t u;
+ uint16_t value;
+
+ if(port->input){
+ if(event_count){
+ DBGPF("Reading %u MIDI events from jack port %s\n", event_count, port->name);
+ for(u = 0; u < event_count; u++){
+ ident.label = 0;
+ //read midi data from stream
+ jack_midi_event_get(&event, buffer, u);
+ //ident.fields.port set on output in mmjack_handle_midi
+ ident.fields.sub_channel = event.buffer[0] & 0x0F;
+ ident.fields.sub_type = event.buffer[0] & 0xF0;
+ if(ident.fields.sub_type == 0x80){
+ ident.fields.sub_type = midi_note;
+ value = 0;
+ }
+ else if(ident.fields.sub_type == midi_pitchbend){
+ value = event.buffer[1] | (event.buffer[2] << 7);
+ }
+ else if(ident.fields.sub_type == midi_aftertouch){
+ value = event.buffer[1];
+ }
+ else{
+ ident.fields.sub_control = event.buffer[1];
+ value = event.buffer[2];
+ }
+ //append midi data
+ mmjack_midiqueue_append(port, ident, value);
+ }
+ port->mark = 1;
+ *mark = 1;
+ }
+ }
+ else{
+ //clear buffer
+ jack_midi_clear_buffer(buffer);
+
+ for(u = 0; u < port->queue_len; u++){
+ //build midi event
+ ident.label = port->queue[u].ident.label;
+ event_data = jack_midi_event_reserve(buffer, u, (ident.fields.sub_type == midi_aftertouch) ? 2 : 3);
+ if(!event_data){
+ fprintf(stderr, "Failed to reserve MIDI stream data\n");
+ return 1;
+ }
+ event_data[0] = ident.fields.sub_channel | ident.fields.sub_type;
+ if(ident.fields.sub_type == midi_pitchbend){
+ event_data[1] = port->queue[u].raw & 0x7F;
+ event_data[2] = (port->queue[u].raw >> 7) & 0x7F;
+ }
+ else if(ident.fields.sub_type == midi_aftertouch){
+ event_data[1] = port->queue[u].raw & 0x7F;
+ }
+ else{
+ event_data[1] = ident.fields.sub_control;
+ event_data[2] = port->queue[u].raw & 0x7F;
+ }
+ }
+
+ if(port->queue_len){
+ DBGPF("Wrote %" PRIsize_t " MIDI events to jack port %s\n", port->queue_len, port->name);
+ }
+ port->queue_len = 0;
+ }
+ return 0;
+}
+
+static int mmjack_process_cv(instance* inst, mmjack_port* port, size_t nframes, size_t* mark){
+ jack_default_audio_sample_t* audio_buffer = jack_port_get_buffer(port->port, nframes);
+ size_t u;
+
+ if(port->input){
+ //read updated data into the local buffer
+ //FIXME maybe we dont want to always use the first sample...
+ if((double) audio_buffer[0] != port->last){
+ port->last = audio_buffer[0];
+ port->mark = 1;
+ *mark = 1;
+ }
+ }
+ else{
+ for(u = 0; u < nframes; u++){
+ audio_buffer[u] = port->last;
+ }
+ }
+ return 0;
+}
+
+static int mmjack_process(jack_nframes_t nframes, void* instp){
+ instance* inst = (instance*) instp;
+ mmjack_instance_data* data = (mmjack_instance_data*) inst->impl;
+ size_t p, mark = 0;
+ int rv = 0;
+
+ //DBGPF("jack callback for %d frames on %s\n", nframes, inst->name);
+
+ for(p = 0; p < data->ports; p++){
+ pthread_mutex_lock(&data->port[p].lock);
+ switch(data->port[p].type){
+ case port_midi:
+ //DBGPF("Handling MIDI port %s.%s\n", inst->name, data->port[p].name);
+ rv |= mmjack_process_midi(inst, data->port + p, nframes, &mark);
+ break;
+ case port_cv:
+ //DBGPF("Handling CV port %s.%s\n", inst->name, data->port[p].name);
+ rv |= mmjack_process_cv(inst, data->port + p, nframes, &mark);
+ break;
+ default:
+ fprintf(stderr, "Unhandled jack port type in processing callback\n");
+ pthread_mutex_unlock(&data->port[p].lock);
+ return 1;
+ }
+ pthread_mutex_unlock(&data->port[p].lock);
+ }
+
+ //notify the main thread
+ if(mark){
+ DBGPF("Notifying handler thread for jack instance %s\n", inst->name);
+ send(data->fd, "c", 1, 0);
+ }
+ return rv;
+}
+
+static void mmjack_server_shutdown(void* inst){
+ fprintf(stderr, "jack server shutdown notification\n");
+ config.jack_shutdown = 1;
+}
+
+static int mmjack_configure(char* option, char* value){
+ if(!strcmp(option, "debug")){
+ if(!strcmp(value, "on")){
+ config.verbosity |= 2;
+ return 0;
+ }
+ config.verbosity &= ~2;
+ return 0;
+ }
+ if(!strcmp(option, "errors")){
+ if(!strcmp(value, "on")){
+ config.verbosity |= 1;
+ return 0;
+ }
+ config.verbosity &= ~1;
+ return 0;
+ }
+
+ fprintf(stderr, "Unknown jack backend option %s\n", option);
+ return 1;
+}
+
+static int mmjack_parse_portconfig(mmjack_port* port, char* spec){
+ char* token = NULL;
+
+ for(token = strtok(spec, " "); token; token = strtok(NULL, " ")){
+ if(!strcmp(token, "in")){
+ port->input = 1;
+ }
+ else if(!strcmp(token, "out")){
+ port->input = 0;
+ }
+ else if(!strcmp(token, "midi")){
+ port->type = port_midi;
+ }
+ else if(!strcmp(token, "osc")){
+ port->type = port_osc;
+ }
+ else if(!strcmp(token, "cv")){
+ port->type = port_cv;
+ }
+ else if(!strcmp(token, "max")){
+ token = strtok(NULL, " ");
+ if(!token){
+ fprintf(stderr, "jack port %s configuration missing argument\n", port->name);
+ return 1;
+ }
+ port->max = strtod(token, NULL);
+ }
+ else if(!strcmp(token, "min")){
+ token = strtok(NULL, " ");
+ if(!token){
+ fprintf(stderr, "jack port %s configuration missing argument\n", port->name);
+ return 1;
+ }
+ port->min = strtod(token, NULL);
+ }
+ else{
+ fprintf(stderr, "Unknown jack channel configuration token %s on port %s\n", token, port->name);
+ return 1;
+ }
+ }
+
+ if(port->type == port_none){
+ fprintf(stderr, "jack channel %s assigned no port type\n", port->name);
+ return 1;
+ }
+ return 0;
+}
+
+static int mmjack_configure_instance(instance* inst, char* option, char* value){
+ mmjack_instance_data* data = (mmjack_instance_data*) inst->impl;
+ size_t p;
+
+ if(!strcmp(option, "name")){
+ if(data->client_name){
+ free(data->client_name);
+ }
+ data->client_name = strdup(value);
+ return 0;
+ }
+ else if(!strcmp(option, "server")){
+ if(data->server_name){
+ free(data->server_name);
+ }
+ data->server_name = strdup(value);
+ return 0;
+ }
+
+ //register new port, first check for unique name
+ for(p = 0; p < data->ports; p++){
+ if(!strcmp(data->port[p].name, option)){
+ fprintf(stderr, "jack instance %s has duplicate port %s\n", inst->name, option);
+ return 1;
+ }
+ }
+ if(strchr(option, '.')){
+ fprintf(stderr, "Invalid jack channel spec %s.%s\n", inst->name, option);
+ }
+
+ //add port to registry
+ //TODO for OSC ports we need to configure subchannels for each message
+ data->port = realloc(data->port, (data->ports + 1) * sizeof(mmjack_port));
+ if(!data->port){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return 1;
+ }
+ data->port[data->ports].name = strdup(option);
+ if(!data->port[data->ports].name){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return 1;
+ }
+ if(mmjack_parse_portconfig(data->port + p, value)){
+ return 1;
+ }
+ data->ports++;
+ return 0;
+}
+
+static instance* mmjack_instance(){
+ instance* inst = mm_instance();
+ if(!inst){
+ return NULL;
+ }
+
+ inst->impl = calloc(1, sizeof(mmjack_instance_data));
+ if(!inst->impl){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return NULL;
+ }
+
+ return inst;
+}
+
+static int mmjack_parse_midispec(mmjack_channel_ident* ident, char* spec){
+ char* next_token = NULL;
+
+ if(!strncmp(spec, "ch", 2)){
+ next_token = spec + 2;
+ if(!strncmp(spec, "channel", 7)){
+ next_token = spec + 7;
+ }
+ }
+
+ if(!next_token){
+ fprintf(stderr, "Invalid jack MIDI spec %s\n", spec);
+ return 1;
+ }
+
+ ident->fields.sub_channel = strtoul(next_token, &next_token, 10);
+ if(ident->fields.sub_channel > 15){
+ fprintf(stderr, "Invalid jack MIDI spec %s, channel out of range\n", spec);
+ return 1;
+ }
+
+ if(*next_token != '.'){
+ fprintf(stderr, "Invalid jack MIDI spec %s\n", spec);
+ return 1;
+ }
+
+ next_token++;
+
+ if(!strncmp(next_token, "cc", 2)){
+ ident->fields.sub_type = midi_cc;
+ next_token += 2;
+ }
+ else if(!strncmp(next_token, "note", 4)){
+ ident->fields.sub_type = midi_note;
+ next_token += 4;
+ }
+ else if(!strncmp(next_token, "pressure", 8)){
+ ident->fields.sub_type = midi_pressure;
+ next_token += 8;
+ }
+ else if(!strncmp(next_token, "pitch", 5)){
+ ident->fields.sub_type = midi_pitchbend;
+ }
+ else if(!strncmp(next_token, "aftertouch", 10)){
+ ident->fields.sub_type = midi_aftertouch;
+ }
+ else{
+ fprintf(stderr, "Unknown jack MIDI control type in spec %s\n", spec);
+ return 1;
+ }
+
+ ident->fields.sub_control = strtoul(next_token, NULL, 10);
+
+ if(ident->fields.sub_type == midi_none
+ || ident->fields.sub_control > 127){
+ fprintf(stderr, "Invalid jack MIDI spec %s\n", spec);
+ return 1;
+ }
+ return 0;
+}
+
+static channel* mmjack_channel(instance* inst, char* spec){
+ mmjack_instance_data* data = (mmjack_instance_data*) inst->impl;
+ mmjack_channel_ident ident = {
+ .label = 0
+ };
+ size_t u;
+
+ for(u = 0; u < data->ports; u++){
+ if(!strncmp(spec, data->port[u].name, strlen(data->port[u].name))
+ && (spec[strlen(data->port[u].name)] == '.' || spec[strlen(data->port[u].name)] == 0)){
+ ident.fields.port = u;
+ break;
+ }
+ }
+
+ if(u == data->ports){
+ fprintf(stderr, "jack port %s.%s not found\n", inst->name, spec);
+ return NULL;
+ }
+
+ if(data->port[u].type == port_midi){
+ //parse midi subspec
+ if(!spec[strlen(data->port[u].name)]
+ || mmjack_parse_midispec(&ident, spec + strlen(data->port[u].name) + 1)){
+ return NULL;
+ }
+ }
+ else if(data->port[u].type == port_osc){
+ //TODO parse osc subspec
+ }
+
+ return mm_channel(inst, ident.label, 1);
+}
+
+static int mmjack_set(instance* inst, size_t num, channel** c, channel_value* v){
+ mmjack_instance_data* data = (mmjack_instance_data*) inst->impl;
+ mmjack_channel_ident ident = {
+ .label = 0
+ };
+ size_t u;
+ double range;
+ uint16_t value;
+
+ for(u = 0; u < num; u++){
+ ident.label = c[u]->ident;
+
+ if(data->port[ident.fields.port].input){
+ fprintf(stderr, "jack port %s.%s is an input port, no output is possible\n", inst->name, data->port[ident.fields.port].name);
+ continue;
+ }
+ range = data->port[ident.fields.port].max - data->port[ident.fields.port].min;
+
+ pthread_mutex_lock(&data->port[ident.fields.port].lock);
+ switch(data->port[ident.fields.port].type){
+ case port_cv:
+ //scale value to given range
+ data->port[ident.fields.port].last = (range * v[u].normalised) + data->port[ident.fields.port].min;
+ DBGPF("CV port %s updated to %f\n", data->port[ident.fields.port].name, data->port[ident.fields.port].last);
+ break;
+ case port_midi:
+ value = v[u].normalised * 127.0;
+ if(ident.fields.sub_type == midi_pitchbend){
+ value = ((uint16_t)(v[u].normalised * 16384.0));
+ }
+ if(mmjack_midiqueue_append(data->port + ident.fields.port, ident, value)){
+ pthread_mutex_unlock(&data->port[ident.fields.port].lock);
+ return 1;
+ }
+ break;
+ default:
+ fprintf(stderr, "No handler implemented for jack port type %s.%s\n", inst->name, data->port[ident.fields.port].name);
+ break;
+ }
+ pthread_mutex_unlock(&data->port[ident.fields.port].lock);
+ }
+
+ return 0;
+}
+
+static void mmjack_handle_midi(instance* inst, size_t index, mmjack_port* port){
+ size_t u;
+ channel* chan = NULL;
+ channel_value val;
+
+ for(u = 0; u < port->queue_len; u++){
+ port->queue[u].ident.fields.port = index;
+ chan = mm_channel(inst, port->queue[u].ident.label, 0);
+ if(chan){
+ if(port->queue[u].ident.fields.sub_type == midi_pitchbend){
+ val.normalised = ((double)port->queue[u].raw) / 16384.0;
+ }
+ else{
+ val.normalised = ((double)port->queue[u].raw) / 127.0;
+ }
+ DBGPF("Pushing MIDI channel %d type %02X control %d value %f raw %d label %" PRIu64 "\n",
+ port->queue[u].ident.fields.sub_channel,
+ port->queue[u].ident.fields.sub_type,
+ port->queue[u].ident.fields.sub_control,
+ val.normalised,
+ port->queue[u].raw,
+ port->queue[u].ident.label);
+ if(mm_channel_event(chan, val)){
+ fprintf(stderr, "Failed to push MIDI event to core on jack port %s.%s\n", inst->name, port->name);
+ }
+ }
+ }
+
+ if(port->queue_len){
+ DBGPF("Pushed %" PRIsize_t " MIDI events to core for jack port %s.%s\n", port->queue_len, inst->name, port->name);
+ }
+ port->queue_len = 0;
+}
+
+static void mmjack_handle_cv(instance* inst, size_t index, mmjack_port* port){
+ mmjack_channel_ident ident = {
+ .fields.port = index
+ };
+ double range;
+ channel_value val;
+
+ channel* chan = mm_channel(inst, ident.label, 0);
+ if(!chan){
+ //this might happen if a channel is registered but not mapped
+ DBGPF("Failed to match jack CV channel %s.%s to core channel\n", inst->name, port->name);
+ return;
+ }
+
+ //normalize value
+ range = port->max - port->min;
+ val.normalised = port->last - port->min;
+ val.normalised /= range;
+ val.normalised = clamp(val.normalised, 1.0, 0.0);
+ DBGPF("Pushing CV channel %s value %f raw %f min %f max %f\n", port->name, val.normalised, port->last, port->min, port->max);
+ if(mm_channel_event(chan, val)){
+ fprintf(stderr, "Failed to push CV event to core for %s.%s\n", inst->name, port->name);
+ }
+}
+
+static int mmjack_handle(size_t num, managed_fd* fds){
+ size_t u, p;
+ instance* inst = NULL;
+ mmjack_instance_data* data = NULL;
+ ssize_t bytes;
+ uint8_t recv_buf[1024];
+
+ if(num){
+ for(u = 0; u < num; u++){
+ bytes = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0);
+ if(bytes < 0){
+ fprintf(stderr, "Failed to receive on feedback socket for instance %s\n", inst->name);
+ return 1;
+ }
+ inst = (instance*) fds[u].impl;
+ data = (mmjack_instance_data*) inst->impl;
+
+ for(p = 0; p < data->ports; p++){
+ if(data->port[p].input && data->port[p].mark){
+ pthread_mutex_lock(&data->port[p].lock);
+ switch(data->port[p].type){
+ case port_cv:
+ mmjack_handle_cv(inst, p, data->port + p);
+ break;
+ case port_midi:
+ mmjack_handle_midi(inst, p, data->port + p);
+ break;
+ default:
+ fprintf(stderr, "Output handler not implemented for unknown jack channel type on %s.%s\n", inst->name, data->port[p].name);
+ break;
+ }
+
+ data->port[p].mark = 0;
+ pthread_mutex_unlock(&data->port[p].lock);
+ }
+ }
+ }
+ }
+
+ if(config.jack_shutdown){
+ fprintf(stderr, "JACK server disconnected\n");
+ return 1;
+ }
+ return 0;
+}
+
+static int mmjack_start(){
+ int rv = 1, feedback_fd[2];
+ size_t n, u, p;
+ instance** inst = NULL;
+ pthread_mutexattr_t mutex_attr;
+ mmjack_instance_data* data = NULL;
+ jack_status_t error;
+
+ //set jack logging functions
+ jack_set_error_function(mmjack_message_ignore);
+ if(config.verbosity & 1){
+ jack_set_error_function(mmjack_message_print);
+ }
+ jack_set_info_function(mmjack_message_ignore);
+ if(config.verbosity & 2){
+ jack_set_info_function(mmjack_message_print);
+ }
+
+ //prepare mutex attributes because the initializer macro for adaptive mutexes is a GNU extension...
+ if(pthread_mutexattr_init(&mutex_attr)
+ || pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_ADAPTIVE_NP)){
+ fprintf(stderr, "Failed to initialize mutex attributes\n");
+ goto bail;
+ }
+
+ //fetch all instances
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ fprintf(stderr, "Failed to fetch instance list\n");
+ goto bail;
+ }
+
+ for(u = 0; u < n; u++){
+ data = (mmjack_instance_data*) inst[u]->impl;
+
+ //connect to the jack server
+ data->client = jack_client_open(data->client_name ? data->client_name : JACK_DEFAULT_CLIENT_NAME,
+ JackServerName | JackNoStartServer,
+ &error,
+ data->server_name ? data->server_name : JACK_DEFAULT_SERVER_NAME);
+
+ if(!data->client){
+ //TODO pretty-print failures
+ fprintf(stderr, "jack backend failed to connect to server, return status %u\n", error);
+ goto bail;
+ }
+
+ //set up the feedback fd
+ if(socketpair(AF_LOCAL, SOCK_DGRAM, 0, feedback_fd)){
+ fprintf(stderr, "Failed to create feedback socket pair\n");
+ goto bail;
+ }
+
+ data->fd = feedback_fd[0];
+ if(mm_manage_fd(feedback_fd[1], BACKEND_NAME, 1, inst[u])){
+ fprintf(stderr, "jack backend failed to register feedback fd with core\n");
+ goto bail;
+ }
+
+ //connect jack callbacks
+ jack_set_process_callback(data->client, mmjack_process, inst[u]);
+ jack_on_shutdown(data->client, mmjack_server_shutdown, inst[u]);
+
+ fprintf(stderr, "jack instance %s assigned client name %s\n", inst[u]->name, jack_get_client_name(data->client));
+
+ //create and initialize jack ports
+ for(p = 0; p < data->ports; p++){
+ if(pthread_mutex_init(&(data->port[p].lock), &mutex_attr)){
+ fprintf(stderr, "Failed to create port mutex\n");
+ goto bail;
+ }
+
+ data->port[p].port = jack_port_register(data->client,
+ data->port[p].name,
+ (data->port[p].type == port_cv) ? JACK_DEFAULT_AUDIO_TYPE : JACK_DEFAULT_MIDI_TYPE,
+ data->port[p].input ? JackPortIsInput : JackPortIsOutput,
+ 0);
+
+ jack_set_property(data->client, jack_port_uuid(data->port[p].port), JACKEY_SIGNAL_TYPE, "CV", "text/plain");
+
+ if(!data->port[p].port){
+ fprintf(stderr, "Failed to create jack port %s.%s\n", inst[u]->name, data->port[p].name);
+ return 1;
+ }
+ }
+
+ //do the thing
+ if(jack_activate(data->client)){
+ fprintf(stderr, "Failed to activate jack client for instance %s\n", inst[u]->name);
+ return 1;
+ }
+ }
+
+ fprintf(stderr, "jack backend registered %" PRIsize_t " descriptors to core\n", n);
+ rv = 0;
+bail:
+ pthread_mutexattr_destroy(&mutex_attr);
+ free(inst);
+ return rv;
+}
+
+static int mmjack_shutdown(){
+ size_t n, u, p;
+ instance** inst = NULL;
+ mmjack_instance_data* 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 = (mmjack_instance_data*) inst[u]->impl;
+
+ //deactivate client to stop processing before free'ing channel data
+ if(data->client){
+ jack_deactivate(data->client);
+ }
+
+ //iterate and close ports
+ for(p = 0; p < data->ports; p++){
+ jack_remove_property(data->client, jack_port_uuid(data->port[p].port), JACKEY_SIGNAL_TYPE);
+ if(data->port[p].port){
+ jack_port_unregister(data->client, data->port[p].port);
+ }
+ free(data->port[p].name);
+ data->port[p].name = NULL;
+
+ free(data->port[p].queue);
+ data->port[p].queue = NULL;
+ data->port[p].queue_alloc = data->port[p].queue_len = 0;
+
+ pthread_mutex_destroy(&data->port[p].lock);
+ }
+
+ //terminate jack connection
+ if(data->client){
+ jack_client_close(data->client);
+ }
+
+ //clean up instance data
+ free(data->server_name);
+ data->server_name = NULL;
+ free(data->client_name);
+ data->client_name = NULL;
+ close(data->fd);
+ data->fd = -1;
+ }
+
+ free(inst);
+
+ fprintf(stderr, "jack backend shut down\n");
+ return 0;
+}
diff --git a/backends/jack.h b/backends/jack.h
new file mode 100644
index 0000000..dd59cd2
--- /dev/null
+++ b/backends/jack.h
@@ -0,0 +1,76 @@
+#include "midimonster.h"
+#include <jack/jack.h>
+#include <pthread.h>
+
+int init();
+static int mmjack_configure(char* option, char* value);
+static int mmjack_configure_instance(instance* inst, char* option, char* value);
+static instance* mmjack_instance();
+static channel* mmjack_channel(instance* inst, char* spec);
+static int mmjack_set(instance* inst, size_t num, channel** c, channel_value* v);
+static int mmjack_handle(size_t num, managed_fd* fds);
+static int mmjack_start();
+static int mmjack_shutdown();
+
+#define JACK_DEFAULT_CLIENT_NAME "MIDIMonster"
+#define JACK_DEFAULT_SERVER_NAME "default"
+#define JACK_MIDIQUEUE_CHUNK 10
+
+enum /*mmjack_midi_channel_type*/ {
+ midi_none = 0,
+ midi_note = 0x90,
+ midi_cc = 0xB0,
+ midi_pressure = 0xA0,
+ midi_aftertouch = 0xD0,
+ midi_pitchbend = 0xE0
+};
+
+typedef union {
+ struct {
+ uint32_t port;
+ uint8_t pad;
+ uint8_t sub_type;
+ uint8_t sub_channel;
+ uint8_t sub_control;
+ } fields;
+ uint64_t label;
+} mmjack_channel_ident;
+
+typedef enum /*_mmjack_port_type*/ {
+ port_none = 0,
+ port_midi,
+ port_osc,
+ port_cv
+} mmjack_port_type;
+
+typedef struct /*_mmjack_midiqueue_entry*/ {
+ mmjack_channel_ident ident;
+ uint16_t raw;
+} mmjack_midiqueue;
+
+typedef struct /*_mmjack_port_data*/ {
+ char* name;
+ mmjack_port_type type;
+ uint8_t input;
+ jack_port_t* port;
+
+ double max;
+ double min;
+ uint8_t mark;
+ double last;
+ size_t queue_len;
+ size_t queue_alloc;
+ mmjack_midiqueue* queue;
+
+ pthread_mutex_t lock;
+} mmjack_port;
+
+typedef struct /*_jack_instance_data*/ {
+ char* server_name;
+ char* client_name;
+ int fd;
+
+ jack_client_t* client;
+ size_t ports;
+ mmjack_port* port;
+} mmjack_instance_data;
diff --git a/backends/jack.md b/backends/jack.md
new file mode 100644
index 0000000..b6ff5a9
--- /dev/null
+++ b/backends/jack.md
@@ -0,0 +1,84 @@
+### The `jack` backend
+
+This backend provides read-write access to the JACK Audio Connection Kit low-latency audio transport server for the
+transport of control data via either JACK midi ports or control voltage (CV) inputs and outputs.
+
+#### Global configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|-----------------------|
+| `debug` | `on` | `off` | Print `info` level notices from the JACK connection |
+| `errors` | `on` | `off` | Print `error` level notices from the JACK connection |
+
+#### Instance configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|-----------------------|
+| `name` | `Controller` | `MIDIMonster` | Client name for the JACK connection |
+| `server` | `jackserver` | `default` | JACK server identifier to connect to |
+
+Channels (corresponding to JACK ports) need to be configured with their type and, if applicable, value limits.
+To configure a port, specify it in the instance configuration using the following syntax:
+
+```
+port_name = <type> <direction> min <minimum> max <maximum>
+```
+
+Port names may be any string except for the instance configuration keywords `name` and `server`.
+
+The following `type` values are currently supported:
+
+* `midi`: JACK MIDI port for transmitting MIDI event messages
+* `cv`: JACK audio port for transmitting DC offset "control voltage" samples (requires `min`/`max` configuration)
+
+`direction` may be one of `in` or `out`, as seen from the perspective of the MIDIMonster core, thus
+`in` means data is being read from the JACK server and `out` transfers data into the JACK server.
+
+The following example instance configuration would create a MIDI port sending data into JACK, a control voltage output
+sending data between `-1` and `1`, and a control voltage input receiving data with values between `0` and `10`.
+
+```
+midi_out = midi out
+cv_out = cv out min -1 max 1
+cv_in = cv in min 0.0 max 10.0
+```
+
+Input CV samples outside the configured range will be clipped. The MIDIMonster will not generate output CV samples
+outside of the configured range.
+
+#### Channel specification
+
+CV ports are exposed as single MIDIMonster channel and directly map to their normalised values.
+
+MIDI ports provide subchannels for the various MIDI controls available. Each MIDI port carries
+16 MIDI channels (numbered 0 through 15), each of which has 128 note controls (numbered 0 through 127),
+corresponding pressure controls for each note, 128 control change (CC) controls (numbered likewise),
+one channel wide "aftertouch" control and one channel-wide pitchbend control.
+
+A MIDI port subchannel is specified using the syntax `channel<channel>.<type><index>`. The shorthand `ch` may be
+used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number).
+
+The following values are recognized for `type`:
+
+* `cc` - Control Changes
+* `note` - Note On/Off messages
+* `pressure` - Note pressure/aftertouch messages
+* `aftertouch` - Channel-wide aftertouch messages
+* `pitch` - Channel pitchbend messages
+
+The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel<channel>.<type>`.
+
+Example mappings:
+```
+jack1.cv_in > jack1.midi_out.ch0.note3
+jack1.midi_in.ch0.pitch > jack1.cv_out
+```
+
+The MIDI subchannel syntax is intentionally kept compatible to the different MIDI backends also supported
+by the MIDIMonster
+
+#### Known bugs / problems
+
+While JACK has rudimentary capabilities for transporting OSC messages, configuring and parsing such channels
+with this backend would take a great amount of dedicated syntax & code. CV ports can provide fine-grained single
+control channels as an alternative to MIDI. This feature may be implemented at some point in the future.