diff options
| author | cbdev <cb@cbcdn.com> | 2021-01-15 21:35:21 +0100 | 
|---|---|---|
| committer | cbdev <cb@cbcdn.com> | 2021-01-15 21:35:21 +0100 | 
| commit | 35f4798673194733358cd3db19a4d2baf70887fd (patch) | |
| tree | b445b7012c1ef44d1d391cec178c2e39fe041c4f /backends | |
| parent | 8ff86335bc9f5233564a0f791174a9cc49ae2df4 (diff) | |
| download | midimonster-35f4798673194733358cd3db19a4d2baf70887fd.tar.gz midimonster-35f4798673194733358cd3db19a4d2baf70887fd.tar.bz2 midimonster-35f4798673194733358cd3db19a4d2baf70887fd.zip  | |
Implement EPN's for the rtpmidi backend
Diffstat (limited to 'backends')
| -rw-r--r-- | backends/midi.c | 3 | ||||
| -rw-r--r-- | backends/rtpmidi.c | 177 | ||||
| -rw-r--r-- | backends/rtpmidi.h | 20 | ||||
| -rw-r--r-- | backends/rtpmidi.md | 13 | ||||
| -rw-r--r-- | backends/winmidi.c | 2 | 
5 files changed, 191 insertions, 24 deletions
diff --git a/backends/midi.c b/backends/midi.c index 10c8c4a..4bf846a 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -247,6 +247,7 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){  				}  				break;  			case pitchbend: +				//TODO check whether this actually works that well  				midi_tx(data->port, ident.fields.type, ident.fields.channel, ident.fields.control, (v[u].normalised * 16383.0) - 8192);  				break;  			default: @@ -282,7 +283,7 @@ static char* midi_type_name(uint8_t type){  	return "unknown";  } -//this state machine is used more-or-less verbatim in the winmidi and jack backends - fixes need to be applied there, too +//this state machine is used more-or-less verbatim in the winmidi, rtpmidi and jack backends - fixes need to be applied there, too  static void midi_handle_epn(instance* inst, uint8_t chan, uint16_t control, uint16_t value){  	midi_instance_data* data = (midi_instance_data*) inst->impl;  	midi_channel_ident ident = { diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 8d5525c..3a54e26 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -429,6 +429,10 @@ static char* rtpmidi_type_name(uint8_t type){  			return "pitch";  		case program:  			return "program"; +		case rpn: +			return "rpn"; +		case nrpn: +			return "nrpn";  	}  	return "unknown";  } @@ -579,6 +583,13 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value)  		LOGPF("Unknown instance mode %s for instance %s", value, inst->name);  		return 1;  	} +	else if(!strcmp(option, "epn-tx")){ +		data->epn_tx_short = 0; +		if(!strcmp(value, "short")){ +			data->epn_tx_short = 1; +		} +		return 0; +	}  	else if(!strcmp(option, "ssrc")){  		data->ssrc = strtoul(value, NULL, 0);  		if(!data->ssrc){ @@ -707,6 +718,14 @@ static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){  		ident.fields.type = note;  		next_token += 4;  	} +	else if(!strncmp(next_token, "rpn", 3)){ +		ident.fields.type = rpn; +		next_token += 3; +	} +	else if(!strncmp(next_token, "nrpn", 4)){ +		ident.fields.type = nrpn; +		next_token += 4; +	}  	else if(!strncmp(next_token, "pressure", 8)){  		ident.fields.type = pressure;  		next_token += 8; @@ -733,6 +752,32 @@ static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){  	return NULL;  } +static size_t rtpmidi_push_midi(uint8_t* payload, size_t bytes_left, uint8_t type, uint8_t channel, uint8_t control, uint16_t value){ +	//FIXME this is a bit simplistic but it works for now +	if(bytes_left < 4){ +		return 0; +	} + +	//encode timestamp +	payload[0] = 0; + +	//encode midi command +	payload[1] = type | channel; +	payload[2] = control; +	payload[3] = value & 0x7F; + +	if(type == pitchbend){ +		payload[2] = value & 0x7F; +		payload[3] = (value >> 7) & 0x7F; +	} +	//channel-wides aftertouch and program are only 2 bytes +	else if(type == aftertouch || type == program){ +		payload[2] = payload[3]; +		return 3; +	} +	return 4; +} +  static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v){  	rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl;  	uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; @@ -741,6 +786,7 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v  	size_t offset = sizeof(rtpmidi_header) + sizeof(rtpmidi_command_header), u = 0;  	uint8_t* payload = frame + offset;  	rtpmidi_channel_ident ident; +	size_t command_length = 0;  	rtp_header->vpxcc = RTPMIDI_HEADER_MAGIC;  	//some receivers seem to have problems reading rfcs and interpreting the marker bit correctly @@ -757,27 +803,37 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v  	for(u = 0; u < num; u++){  		ident.label = c[u]->ident; -		//encode timestamp -		payload[0] = 0; - -		//encode midi command -		payload[1] = ident.fields.type | ident.fields.channel; -		payload[2] = ident.fields.control; -		payload[3] = v[u].normalised * 127.0; - -		if(ident.fields.type == pitchbend){ -			payload[2] = ((int)(v[u].normalised * 16383.0)) & 0x7F; -			payload[3] = (((int)(v[u].normalised * 16383.0)) >> 7) & 0x7F; +		switch(ident.fields.type){ +			case rpn: +			case nrpn: +				//transmit parameter number +				command_length = rtpmidi_push_midi(payload + offset, sizeof(frame) - offset, cc, ident.fields.channel, (ident.fields.type == rpn) ? 101 : 99, (ident.fields.control >> 7) & 0x7F); +				command_length += rtpmidi_push_midi(payload + offset + command_length, sizeof(frame) - offset, cc, ident.fields.channel, (ident.fields.type == rpn) ? 100 : 98, ident.fields.control & 0x7F); + +				//transmit parameter value +				command_length += rtpmidi_push_midi(payload + offset + command_length, sizeof(frame) - offset, cc, ident.fields.channel, 6, (((uint16_t) (v[u].normalised * 16383.0)) >> 7) & 0x7F); +				command_length += rtpmidi_push_midi(payload + offset + command_length, sizeof(frame) - offset, cc, ident.fields.channel, 38, ((uint16_t) (v[u].normalised * 16383.0)) & 0x7F); + +				if(!data->epn_tx_short){ +					//clear active parameter +					command_length += rtpmidi_push_midi(payload + offset + command_length, sizeof(frame) - offset, cc, ident.fields.channel, 101, 127); +					command_length += rtpmidi_push_midi(payload + offset + command_length, sizeof(frame) - offset, cc, ident.fields.channel, 100, 127); +				} +				break; +			case pitchbend: +				//TODO check whether this works +				command_length = rtpmidi_push_midi(payload + offset, sizeof(frame) - offset, ident.fields.type, ident.fields.channel, ident.fields.control, v[u].normalised * 16383.0); +				break; +			default: +				command_length = rtpmidi_push_midi(payload + offset, sizeof(frame) - offset, ident.fields.type, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0);  		} -		//channel-wides aftertouch and program are only 2 bytes -		else if(ident.fields.type == aftertouch || ident.fields.type == program){ -			payload[2] = payload[3]; -			payload -= 1; -			offset -= 1; + +		if(command_length == 0){ +			LOGPF("Transmit buffer size exceeded on %s", inst->name); +			break;  		} -		payload += 4; -		offset += 4; +		offset += command_length;  	}  	//update command section length @@ -929,6 +985,79 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size  	return 0;  } +//this state machine was copied more-or-less verbatim from the alsa midi implementation - fixes there will need to be integrated +static void rtpmidi_handle_epn(instance* inst, uint8_t chan, uint16_t control, uint16_t value){ +	rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; +	rtpmidi_channel_ident ident = { +		.label = 0 +	}; +	channel* changed = NULL; +	channel_value val; + +	//switching between nrpn and rpn clears all valid bits +	if(((data->epn_status[chan] & EPN_NRPN) && (control == 101 || control == 100)) +				|| (!(data->epn_status[chan] & EPN_NRPN) && (control == 99 || control == 98))){ +		data->epn_status[chan] &= ~(EPN_NRPN | EPN_PARAMETER_LO | EPN_PARAMETER_HI); +	} + +	//setting an address always invalidates the value valid bits +	if(control >= 98 && control <= 101){ +		data->epn_status[chan] &= ~EPN_VALUE_HI; +	} + +	//parameter hi +	if(control == 101 || control == 99){ +		data->epn_control[chan] &= 0x7F; +		data->epn_control[chan] |= value << 7; +		data->epn_status[chan] |= EPN_PARAMETER_HI | ((control == 99) ? EPN_NRPN : 0); +		if(control == 101 && value == 127){ +			data->epn_status[chan] &= ~EPN_PARAMETER_HI; +		} +	} + +	//parameter lo +	if(control == 100 || control == 98){ +		data->epn_control[chan] &= ~0x7F; +		data->epn_control[chan] |= value & 0x7F; +		data->epn_status[chan] |= EPN_PARAMETER_LO | ((control == 98) ? EPN_NRPN : 0); +		if(control == 100 && value == 127){ +			data->epn_status[chan] &= ~EPN_PARAMETER_LO; +		} +	} + +	//value hi, clears low, mark as update candidate +	if(control == 6 +			//check if parameter is set before accepting value update +			&& ((data->epn_status[chan] & (EPN_PARAMETER_HI | EPN_PARAMETER_LO)) == (EPN_PARAMETER_HI | EPN_PARAMETER_LO))){ +		data->epn_value[chan] = value << 7; +		data->epn_status[chan] |= EPN_VALUE_HI; +	} + +	//value lo, flush the value +	if(control == 38 +			&& data->epn_status[chan] & EPN_VALUE_HI){ +		data->epn_value[chan] &= ~0x7F; +		data->epn_value[chan] |= value & 0x7F; +		data->epn_status[chan] &= ~EPN_VALUE_HI; + +		if(cfg.detect){ +			LOGPF("Incoming EPN data on channel %s.ch%d.%s%d", inst->name, chan, data->epn_status[chan] & EPN_NRPN ? "nrpn" : "rpn", data->epn_control[chan]); +		} + +		//find the updated channel +		ident.fields.type = data->epn_status[chan] & EPN_NRPN ? nrpn : rpn; +		ident.fields.channel = chan; +		ident.fields.control = data->epn_control[chan]; +		val.normalised = (double) data->epn_value[chan] / 16383.0; + +		//push the new value +		changed = mm_channel(inst, ident.label, 0); +		if(changed){ +			mm_channel_event(changed, val); +		} +	} +} +  static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){  	uint16_t length = 0;  	size_t offset = 1, decode_time = 0, command_bytes = 0; @@ -1004,6 +1133,7 @@ static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){  		if(ident.fields.type == aftertouch || ident.fields.type == program){  			ident.fields.control = 0;  			val.normalised = (double) frame[offset] / 127.0; +			val.raw.u64 = frame[offset];  			offset++;  		}  		//two-byte command @@ -1016,16 +1146,19 @@ static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){  			if(ident.fields.type == pitchbend){  				ident.fields.control = 0;  				val.normalised = (double)((frame[offset] << 7) | frame[offset - 1]) / 16383.0; +				val.raw.u64 = (frame[offset] << 7) | frame[offset - 1];  			}  			else{  				ident.fields.control = frame[offset - 1];  				val.normalised = (double) frame[offset] / 127.0; +				val.raw.u64 = frame[offset];  			}  			//fix-up note off events  			if(ident.fields.type == 0x80){  				ident.fields.type = note;  				val.normalised = 0; +				val.raw.u64 = 0;  			}  			offset++; @@ -1034,6 +1167,14 @@ static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){  		DBGPF("Decoded command type %02X channel %d control %d value %f",  				ident.fields.type, ident.fields.channel, ident.fields.control, val.normalised); +		//forward EPN CCs to the EPN state machine +		if(ident.fields.type == cc +				&& ((ident.fields.control <= 101 && ident.fields.control >= 98) +					|| ident.fields.control == 6 +					|| ident.fields.control == 38)){ +			rtpmidi_handle_epn(inst, ident.fields.channel, ident.fields.control, val.raw.u64); +		}	 +  		if(cfg.detect){  			if(ident.fields.type == pitchbend  					|| ident.fields.type == aftertouch diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index 5f1621e..e88530f 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -32,6 +32,11 @@ static int rtpmidi_shutdown(size_t n, instance** inst);  #define DNS_OPCODE(a) (((a) & 0x78) >> 3)  #define DNS_RESPONSE(a) ((a) & 0x80) +#define EPN_NRPN 8 +#define EPN_PARAMETER_HI 4 +#define EPN_PARAMETER_LO 2 +#define EPN_VALUE_HI 1 +  enum /*_rtpmidi_channel_type*/ {  	none = 0,  	note = 0x90, @@ -39,7 +44,9 @@ enum /*_rtpmidi_channel_type*/ {  	cc = 0xB0,  	program = 0xC0,  	aftertouch = 0xD0, -	pitchbend = 0xE0 +	pitchbend = 0xE0, +	rpn = 0xF1, +	nrpn = 0xF2  };  typedef enum /*_rtpmidi_instance_mode*/ { @@ -50,10 +57,10 @@ typedef enum /*_rtpmidi_instance_mode*/ {  typedef union {  	struct { -		uint8_t pad[5]; +		uint8_t pad[4];  		uint8_t type;  		uint8_t channel; -		uint8_t control; +		uint16_t control;  	} fields;  	uint64_t label;  } rtpmidi_channel_ident; @@ -68,7 +75,7 @@ typedef struct /*_rtpmidi_peer*/ {  	ssize_t invite; //invite-list index for apple-mode learned peers (used to track ipv6/ipv4 overlapping invitations)  } rtpmidi_peer; -typedef struct /*_rtmidi_instance_data*/ { +typedef struct /*_rtpmidi_instance_data*/ {  	rtpmidi_instance_mode mode;  	int fd; @@ -80,6 +87,11 @@ typedef struct /*_rtmidi_instance_data*/ {  	uint32_t ssrc;  	uint16_t sequence; +	uint8_t epn_tx_short; +	uint16_t epn_control[16]; +	uint16_t epn_value[16]; +	uint8_t epn_status[16]; +  	//apple-midi config  	char* accept;  	uint64_t last_announce; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index 9f56f3d..8014572 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -38,6 +38,7 @@ Common instance configuration parameters  | `ssrc`	| `0xDEADBEEF`		| Randomly generated	| 32-bit synchronization source identifier |  | `mode`	| `direct`		| none			| Instance session management mode (`direct` or `apple`) |  | `peer`	| `10.1.2.3 9001`	| none			| MIDI session peer, may be specified multiple times. Bypasses session discovery (but still performs session negotiation) | +| `epn-tx`	| `short`		| `full`		| Configure whether to clear the active parameter number after transmitting an `nrpn` or `rpn` parameter. |  `direct` mode instance configuration parameters @@ -64,6 +65,8 @@ The `rtpmidi` backend supports mapping different MIDI events to MIDIMonster chan  * `aftertouch` - Channel-wide aftertouch messages  * `pitch` - Channel pitchbend messages  * `program` - Channel program change messages +* `rpn` - Registered parameter numbers (14-bit extension) +* `nrpn` - Non-registered parameter numbers (14-bit extension)  A MIDIMonster channel 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). @@ -74,6 +77,9 @@ MIDI channels range from `0` to `15`. Each MIDI channel consists of 128 notes (n  additionally each have a pressure control, 128 CC's (numbered likewise), a channel pressure control (also called  'channel aftertouch') and a pitch control which may all be mapped to individual MIDIMonster channels. +Every MIDI channel also provides `rpn` and `nrpn` controls, which are implemented on top of the MIDI protocol, using +the CC controls 101/100/99/98/38/6. Both control types have 14-bit IDs and 14-bit values. +  Example mappings:  ``` @@ -82,6 +88,7 @@ rmidi1.channel15.pressure1 > rmidi1.channel0.note0  rmidi1.ch1.aftertouch > rmidi2.ch2.cc0  rmidi1.ch0.pitch > rmidi2.ch1.pitch  rmidi2.ch15.note1 > rmidi2.ch2.program +rmidi2.ch0.nrpn900 > rmidi1.ch1.rpn1  ```  #### Known bugs / problems @@ -93,6 +100,12 @@ The mDNS and DNS-SD implementations in this backend are extremely terse, to the  specifications in multiple cases. Due to the complexity involved in supporting these protocols, problems  arising from this will be considered a bug only in cases where they hinder normal operation of the backend. +Extended parameter numbers (EPNs, the `rpn` and `nrpn` control types) will also generate events on the controls (CC 101 through +98, 38 and 6) that are used as the lower layer transport. When using EPNs, mapping those controls is probably not useful. + +EPN control types support only the full 14-bit transfer encoding, not the shorter variant transmitting only the 7 +high-order bits. This may be changed if there is sufficient interest in the functionality. +  mDNS discovery may announce flawed records when run on a host with multiple active interfaces.  While this backend should be reasonably stable, there may be problematic edge cases simply due to the diff --git a/backends/winmidi.c b/backends/winmidi.c index a1fa686..649af2e 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -200,7 +200,7 @@ static void winmidi_tx(HMIDIOUT port, uint8_t type, uint8_t channel, uint8_t con  	output.components.status = type | channel;  	output.components.data1 = control; -	output.components.data2 = value; +	output.components.data2 = value & 0x7F;  	if(type == pitchbend){  		output.components.data1 = value & 0x7F;  | 
