aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorcbdev <cb@cbcdn.com>2021-01-15 21:35:21 +0100
committercbdev <cb@cbcdn.com>2021-01-15 21:35:21 +0100
commit35f4798673194733358cd3db19a4d2baf70887fd (patch)
treeb445b7012c1ef44d1d391cec178c2e39fe041c4f
parent8ff86335bc9f5233564a0f791174a9cc49ae2df4 (diff)
downloadmidimonster-35f4798673194733358cd3db19a4d2baf70887fd.tar.gz
midimonster-35f4798673194733358cd3db19a4d2baf70887fd.tar.bz2
midimonster-35f4798673194733358cd3db19a4d2baf70887fd.zip
Implement EPN's for the rtpmidi backend
-rw-r--r--backends/midi.c3
-rw-r--r--backends/rtpmidi.c177
-rw-r--r--backends/rtpmidi.h20
-rw-r--r--backends/rtpmidi.md13
-rw-r--r--backends/winmidi.c2
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;