From 1e4a11bd9848c40e6cd19632bef1981bb33b3b3d Mon Sep 17 00:00:00 2001
From: cbdev <cb@cbcdn.com>
Date: Sun, 3 Jan 2021 18:12:48 +0100
Subject: Implement EPN transmission for the jack backend

---
 backends/jack.c  | 99 +++++++++++++++++++++++++++++++++++++++++---------------
 backends/jack.h  |  9 ++++--
 backends/jack.md |  7 ++++
 3 files changed, 86 insertions(+), 29 deletions(-)

(limited to 'backends')

diff --git a/backends/jack.c b/backends/jack.c
index c84ed0f..a3caf73 100644
--- a/backends/jack.c
+++ b/backends/jack.c
@@ -18,8 +18,6 @@
 	#endif
 #endif
 
-//FIXME pitchbend range is somewhat oob
-
 static struct /*_mmjack_backend_cfg*/ {
 	unsigned verbosity;
 	volatile sig_atomic_t jack_shutdown;
@@ -80,18 +78,42 @@ static int mmjack_midiqueue_append(mmjack_port* port, mmjack_channel_ident ident
 	return 0;
 }
 
+static void mmjack_process_midiout(void* buffer, size_t sample_offset, uint8_t type, uint8_t channel, uint8_t control, uint16_t value){
+	jack_midi_data_t* event_data = jack_midi_event_reserve(buffer, sample_offset, (type == midi_aftertouch) ? 2 : 3);
+
+	if(!event_data){
+		LOG("Failed to reserve MIDI stream data");
+		return;
+	}
+
+	//build midi event
+	event_data[0] = channel | type;
+	event_data[1] = control & 0x7F;
+	event_data[2] = value & 0x7F;
+
+	if(type == midi_pitchbend){
+		event_data[1] = value & 0x7F;
+		event_data[2] = (value >> 7) & 0x7F;
+	}
+	else if(type == midi_aftertouch){
+		event_data[1] = value & 0x7F;
+		event_data[2] = 0;
+	}
+}
+
 static int mmjack_process_midi(instance* inst, mmjack_port* port, size_t nframes, size_t* mark){
+	mmjack_instance_data* data = (mmjack_instance_data*) inst->impl;
 	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;
+	size_t u, frame;
 	uint16_t value;
 
 	if(port->input){
 		if(event_count){
 			DBGPF("Reading %u MIDI events from port %s", event_count, port->name);
+			//TODO (n)rpn RX
 			for(u = 0; u < event_count; u++){
 				ident.label = 0;
 				//read midi data from stream
@@ -124,30 +146,33 @@ static int mmjack_process_midi(instance* inst, mmjack_port* port, size_t nframes
 		//clear buffer
 		jack_midi_clear_buffer(buffer);
 
+		frame = 0;
 		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){
-				LOG("Failed to reserve MIDI stream data");
-				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;
+
+			if(ident.fields.sub_type == midi_rpn
+					|| ident.fields.sub_type == midi_nrpn){
+				//transmit parameter number
+				mmjack_process_midiout(buffer, frame++, midi_cc, ident.fields.sub_channel, (ident.fields.sub_type == midi_rpn) ? 101 : 99, (ident.fields.sub_control >> 7) & 0x7F);
+				mmjack_process_midiout(buffer, frame++, midi_cc, ident.fields.sub_channel, (ident.fields.sub_type == midi_rpn) ? 100 : 98, ident.fields.sub_control & 0x7F);
+
+				//transmit parameter value
+				mmjack_process_midiout(buffer, frame++, midi_cc, ident.fields.sub_channel, 6, (port->queue[u].raw >> 7) & 0x7F);
+				mmjack_process_midiout(buffer, frame++, midi_cc, ident.fields.sub_channel, 38, port->queue[u].raw & 0x7F);
+
+				if(!data->midi_epn_tx_short){
+					//clear active parameter
+					mmjack_process_midiout(buffer, frame++, midi_cc, ident.fields.sub_channel, 101, 127);
+					mmjack_process_midiout(buffer, frame++, midi_cc, ident.fields.sub_channel, 100, 127);
+				}
 			}
 			else{
-				event_data[1] = ident.fields.sub_control;
-				event_data[2] = port->queue[u].raw & 0x7F;
+				mmjack_process_midiout(buffer, frame++, ident.fields.sub_type, ident.fields.sub_channel, ident.fields.sub_control, port->queue[u].raw);
 			}
 		}
 
-		if(port->queue_len){
-			DBGPF("Wrote %" PRIsize_t " MIDI events to port %s", port->queue_len, port->name);
+		if(frame){
+			DBGPF("Wrote %" PRIsize_t " MIDI events to port %s", frame, port->name);
 		}
 		port->queue_len = 0;
 	}
@@ -305,6 +330,13 @@ static int mmjack_configure_instance(instance* inst, char* option, char* value){
 		data->server_name = strdup(value);
 		return 0;
 	}
+	else if(!strcmp(option, "epn-tx")){
+		data->midi_epn_tx_short = 0;
+		if(!strcmp(value, "short")){
+			data->midi_epn_tx_short = 1;
+		}
+		return 0;
+	}
 
 	//register new port, first check for unique name
 	for(p = 0; p < data->ports; p++){
@@ -385,6 +417,14 @@ static int mmjack_parse_midispec(mmjack_channel_ident* ident, char* spec){
 		ident->fields.sub_type = midi_pressure;
 		next_token += 8;
 	}
+	else if(!strncmp(next_token, "rpn", 3)){
+		ident->fields.sub_type = midi_rpn;
+		next_token += 3;
+	}
+	else if(!strncmp(next_token, "nrpn", 4)){
+		ident->fields.sub_type = midi_nrpn;
+		next_token += 4;
+	}
 	else if(!strncmp(next_token, "pitch", 5)){
 		ident->fields.sub_type = midi_pitchbend;
 	}
@@ -399,7 +439,9 @@ static int mmjack_parse_midispec(mmjack_channel_ident* ident, char* spec){
 	ident->fields.sub_control = strtoul(next_token, NULL, 10);
 
 	if(ident->fields.sub_type == midi_none
-			|| ident->fields.sub_control > 127){
+			|| (ident->fields.sub_type != midi_nrpn
+				&& ident->fields.sub_type != midi_rpn
+				&& ident->fields.sub_control > 127)){
 		LOGPF("Invalid MIDI spec %s", spec);
 		return 1;
 	}
@@ -467,9 +509,12 @@ static int mmjack_set(instance* inst, size_t num, channel** c, channel_value* v)
 				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(ident.fields.sub_type == midi_pitchbend
+						|| ident.fields.sub_type == midi_nrpn
+						|| ident.fields.sub_type == midi_rpn){
+					value = ((uint16_t)(v[u].normalised * 16383.0));
 				}
+
 				if(mmjack_midiqueue_append(data->port + ident.fields.port, ident, value)){
 					pthread_mutex_unlock(&data->port[ident.fields.port].lock);
 					return 1;
@@ -494,8 +539,10 @@ static void mmjack_handle_midi(instance* inst, size_t index, mmjack_port* port){
 		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;
+			if(port->queue[u].ident.fields.sub_type == midi_pitchbend
+					|| port->queue[u].ident.fields.sub_type == midi_rpn
+					|| port->queue[u].ident.fields.sub_type == midi_nrpn){
+				val.normalised = ((double)port->queue[u].raw) / 16383.0;
 			}
 			else{
 				val.normalised = ((double)port->queue[u].raw) / 127.0;
diff --git a/backends/jack.h b/backends/jack.h
index 03ce052..ca62ea5 100644
--- a/backends/jack.h
+++ b/backends/jack.h
@@ -22,16 +22,17 @@ enum /*mmjack_midi_channel_type*/ {
 	midi_cc = 0xB0,
 	midi_pressure = 0xA0,
 	midi_aftertouch = 0xD0,
-	midi_pitchbend = 0xE0
+	midi_pitchbend = 0xE0,
+	midi_rpn = 0xF0,
+	midi_nrpn = 0xF1
 };
 
 typedef union {
 	struct {
 		uint32_t port;
-		uint8_t pad;
 		uint8_t sub_type;
 		uint8_t sub_channel;
-		uint8_t sub_control;
+		uint16_t sub_control;
 	} fields;
 	uint64_t label;
 } mmjack_channel_ident;
@@ -70,6 +71,8 @@ typedef struct /*_jack_instance_data*/ {
 	char* client_name;
 	int fd;
 
+	uint8_t midi_epn_tx_short;
+
 	jack_client_t* client;
 	size_t ports;
 	mmjack_port* port;
diff --git a/backends/jack.md b/backends/jack.md
index b6ff5a9..3d426f3 100644
--- a/backends/jack.md
+++ b/backends/jack.md
@@ -16,6 +16,7 @@ transport of control data via either JACK midi ports or control voltage (CV) inp
 |---------------|-----------------------|-----------------------|-----------------------|
 | `name`	| `Controller`		| `MIDIMonster`		| Client name for the JACK connection |
 | `server`	| `jackserver`		| `default`		| JACK server identifier to connect to |
+| `epn-tx`	| `short`		| `full`		| Configure whether to clear the active parameter number after transmitting a MIDI `nrpn` or `rpn` parameter. |
 
 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:
@@ -65,6 +66,8 @@ The following values are recognized for `type`:
 * `pressure` - Note pressure/aftertouch messages
 * `aftertouch` - Channel-wide aftertouch messages
 * `pitch` - Channel pitchbend messages
+* `rpn` - Registered parameter numbers (14-bit extension)
+* `nrpn` - Non-registered parameter numbers (14-bit extension)
 
 The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel<channel>.<type>`.
 
@@ -72,6 +75,7 @@ Example mappings:
 ```
 jack1.cv_in > jack1.midi_out.ch0.note3
 jack1.midi_in.ch0.pitch > jack1.cv_out
+jack2.midi_in.ch0.nrpn900 > jack1.midi_out.ch1.rpn1
 ```
 
 The MIDI subchannel syntax is intentionally kept compatible to the different MIDI backends also supported
@@ -79,6 +83,9 @@ by the MIDIMonster
 
 #### Known bugs / problems
 
+Extended parameter numbers (`rpn` and `nrpn` control types) can currently only be transmitted, not properly
+received as such. Support for this functionality is planned.
+
 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