From ff587cb77ee4a7e9169affbfefd84547da6fea38 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 6 Nov 2019 18:50:57 +0100 Subject: Implement JACK backend --- backends/Makefile | 3 +- backends/jack.c | 742 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/jack.h | 76 ++++++ backends/jack.md | 84 +++++++ 4 files changed, 904 insertions(+), 1 deletion(-) create mode 100644 backends/jack.c create mode 100644 backends/jack.h create mode 100644 backends/jack.md (limited to 'backends') 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 +#include +#include +#include +#include + +#include "jack.h" +#include +#include + +#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 +#include + +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 = min max +``` + +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.`. 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.`. + +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. -- cgit v1.2.3