From adc132b5fc039a185be947de3309bd11f4dee823 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 1 Jan 2021 19:22:01 +0100 Subject: Implement EPN transmission for the midi backend --- backends/midi.md | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'backends/midi.md') diff --git a/backends/midi.md b/backends/midi.md index d3d6e33..3ac011e 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -15,6 +15,7 @@ The MIDI backend provides read-write access to the MIDI protocol via virtual por |---------------|-----------------------|-----------------------|-----------------------| | `read` | `20:0` | none | MIDI device to connect for input | | `write` | `DeviceName` | none | MIDI device to connect for output | +| `epn-tx` | `short` | `full` | Configures whether to clear the active parameter number after transmitting an `nrpn` or `rpn` parameter | MIDI device names may either be `client:port` portnames or prefixes of MIDI device names. Run `aconnect -i` to list input ports and `aconnect -o` to list output ports. @@ -30,6 +31,8 @@ The MIDI backend supports mapping different MIDI events to MIDIMonster channels. * `pressure` - Note pressure/aftertouch messages * `aftertouch` - Channel-wide aftertouch messages * `pitch` - Channel pitchbend messages +* `rpn` - Registered parameter numbers (14bit extension) +* `nrpn` - Non-registered parameter numbers (14bit extension) A MIDIMonster channel 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). @@ -40,15 +43,22 @@ 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 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: ``` midi1.ch0.note9 > midi2.channel1.cc4 midi1.channel15.pressure1 > midi1.channel0.note0 midi1.ch1.aftertouch > midi2.ch2.cc0 midi1.ch0.pitch > midi2.ch1.pitch +midi1.ch0.nrpn900 > midi2.ch0.rpn1 ``` #### 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. + To access MIDI data, the user running MIDIMonster needs read & write access to the ALSA sequencer. This can usually be done by adding this user to the `audio` system group. -- cgit v1.2.3 From 26b91b849899976b455bc5d780688de6962569e1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 2 Jan 2021 17:15:57 +0100 Subject: Implement EPN transmission for the winmidi backend --- backends/midi.c | 6 ++-- backends/midi.md | 6 ++-- backends/winmidi.c | 85 +++++++++++++++++++++++++++++++++++++++++------------ backends/winmidi.h | 9 ++++-- backends/winmidi.md | 10 +++++++ 5 files changed, 88 insertions(+), 28 deletions(-) (limited to 'backends/midi.md') diff --git a/backends/midi.c b/backends/midi.c index 8a8887a..d581a01 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -109,7 +109,7 @@ static int midi_configure_instance(instance* inst, char* option, char* value){ return 0; } - LOGPF("Unknown instance option %s", option); + LOGPF("Unknown instance configuration option %s on instance %s", option, inst->name); return 1; } @@ -227,10 +227,10 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){ case rpn: case nrpn: //transmit parameter number - midi_tx(data->port, cc, ident.fields.channel, (ident.fields.type == rpn) ? 101 : 99, (ident.fields.control & 0x3F80) >> 7); + midi_tx(data->port, cc, ident.fields.channel, (ident.fields.type == rpn) ? 101 : 99, (ident.fields.control >> 7) & 0x7F); midi_tx(data->port, cc, ident.fields.channel, (ident.fields.type == rpn) ? 100 : 98, ident.fields.control & 0x7F); //transmit parameter value - midi_tx(data->port, cc, ident.fields.channel, 6, (((uint16_t) (v[u].normalised * 16383.0)) & 0x3F80) >> 7); + midi_tx(data->port, cc, ident.fields.channel, 6, (((uint16_t) (v[u].normalised * 16383.0)) >> 7) & 0x7F); midi_tx(data->port, cc, ident.fields.channel, 38, ((uint16_t) (v[u].normalised * 16383.0)) & 0x7F); if(!data->epn_tx_short){ diff --git a/backends/midi.md b/backends/midi.md index 3ac011e..4732452 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -31,8 +31,8 @@ The MIDI backend supports mapping different MIDI events to MIDIMonster channels. * `pressure` - Note pressure/aftertouch messages * `aftertouch` - Channel-wide aftertouch messages * `pitch` - Channel pitchbend messages -* `rpn` - Registered parameter numbers (14bit extension) -* `nrpn` - Non-registered parameter numbers (14bit extension) +* `rpn` - Registered parameter numbers (14-bit extension) +* `nrpn` - Non-registered parameter numbers (14-bit extension) A MIDIMonster channel 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). @@ -43,7 +43,7 @@ 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 channel also provides `rpn` and `nrpn` controls, which are implemented on top of the MIDI protocol, using +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: diff --git a/backends/winmidi.c b/backends/winmidi.c index 030062d..c89a098 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -82,7 +82,7 @@ static int winmidi_configure_instance(instance* inst, char* option, char* value) data->read = strdup(value); return 0; } - if(!strcmp(option, "write")){ + else if(!strcmp(option, "write")){ if(data->write){ LOGPF("Instance %s already connected to an output device", inst->name); return 1; @@ -90,6 +90,13 @@ static int winmidi_configure_instance(instance* inst, char* option, char* value) data->write = strdup(value); return 0; } + else if(!strcmp(option, "epn-tx")){ + data->epn_tx_short = 0; + if(!strcmp(value, "short")){ + data->epn_tx_short = 1; + } + return 0; + } LOGPF("Unknown instance configuration option %s on instance %s", option, inst->name); return 1; @@ -148,6 +155,14 @@ static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags){ ident.fields.type = pressure; next_token += 8; } + 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, "pitch", 5)){ ident.fields.type = pitchbend; } @@ -167,11 +182,7 @@ static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags){ return NULL; } -static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v){ - winmidi_instance_data* data = (winmidi_instance_data*) inst->impl; - winmidi_channel_ident ident = { - .label = 0 - }; +static void winmidi_tx(HMIDIOUT port, uint8_t type, uint8_t channel, uint8_t control, uint16_t value){ union { struct { uint8_t status; @@ -183,6 +194,28 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v } output = { .dword = 0 }; + + output.components.status = type | channel; + output.components.data1 = control; + output.components.data2 = value; + + if(type == pitchbend){ + output.components.data1 = value & 0x7F; + output.components.data2 = (value >> 7) & 0x7F; + } + else if(type == aftertouch){ + output.components.data1 = value; + output.components.data2 = 0; + } + + midiOutShortMsg(port, output.dword); +} + +static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v){ + winmidi_instance_data* data = (winmidi_instance_data*) inst->impl; + winmidi_channel_ident ident = { + .label = 0 + }; size_t u; if(!data->device_out){ @@ -193,20 +226,29 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v for(u = 0; u < num; u++){ ident.label = c[u]->ident; - //build output message - output.components.status = ident.fields.type | ident.fields.channel; - output.components.data1 = ident.fields.control; - output.components.data2 = v[u].normalised * 127.0; - if(ident.fields.type == pitchbend){ - output.components.data1 = ((int)(v[u].normalised * 16384.0)) & 0x7F; - output.components.data2 = (((int)(v[u].normalised * 16384.0)) >> 7) & 0x7F; - } - else if(ident.fields.type == aftertouch){ - output.components.data1 = v[u].normalised * 127.0; - output.components.data2 = 0; + switch(ident.fields.type){ + case rpn: + case nrpn: + //transmit parameter number + winmidi_tx(data->device_out, cc, ident.fields.channel, (ident.fields.type == rpn) ? 101 : 99, (ident.fields.control >> 7) & 0x7F); + winmidi_tx(data->device_out, cc, ident.fields.channel, (ident.fields.type == rpn) ? 100 : 98, ident.fields.control & 0x7F); + + //transmit parameter value + winmidi_tx(data->device_out, cc, ident.fields.channel, 6, (((uint16_t) (v[u].normalised * 16383.0)) >> 7) & 0x7F); + winmidi_tx(data->device_out, cc, ident.fields.channel, 38, ((uint16_t) (v[u].normalised * 16383.0)) & 0x7F); + + if(!data->epn_tx_short){ + //clear active parameter + winmidi_tx(data->device_out, cc, ident.fields.channel, 101, 127); + winmidi_tx(data->device_out, cc, ident.fields.channel, 100, 127); + } + break; + case pitchbend: + winmidi_tx(data->device_out, ident.fields.type, ident.fields.channel, ident.fields.control, v[u].normalised * 16383.0); + break; + default: + winmidi_tx(data->device_out, ident.fields.type, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0); } - - midiOutShortMsg(data->device_out, output.dword); } return 0; @@ -218,6 +260,10 @@ static char* winmidi_type_name(uint8_t typecode){ return "note"; case cc: return "cc"; + case rpn: + return "rpn"; + case nrpn: + return "nrpn"; case pressure: return "pressure"; case aftertouch: @@ -295,6 +341,7 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW //callbacks may run on different threads, so we queue all events and alert the main thread via the feedback socket DBGPF("Input callback on thread %ld", GetCurrentThreadId()); + //TODO handle (n)rpn RX switch(message){ case MIM_MOREDATA: //processing too slow, do not immediately alert the main loop diff --git a/backends/winmidi.h b/backends/winmidi.h index 4c740ea..fbb2c94 100644 --- a/backends/winmidi.h +++ b/backends/winmidi.h @@ -13,6 +13,7 @@ static int winmidi_shutdown(size_t n, instance** inst); typedef struct /*_winmidi_instance_data*/ { char* read; char* write; + uint8_t epn_tx_short; HMIDIIN device_in; HMIDIOUT device_out; } winmidi_instance_data; @@ -23,15 +24,17 @@ enum /*_winmidi_channel_type*/ { cc = 0xB0, pressure = 0xA0, aftertouch = 0xD0, - pitchbend = 0xE0 + pitchbend = 0xE0, + rpn = 0xF0, + nrpn = 0xF1 }; 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; } winmidi_channel_ident; diff --git a/backends/winmidi.md b/backends/winmidi.md index 25a6378..6b0fa98 100644 --- a/backends/winmidi.md +++ b/backends/winmidi.md @@ -19,6 +19,7 @@ some deviations may still be present. |---------------|-----------------------|-----------------------|-----------------------| | `read` | `2` | none | MIDI device to connect for input | | `write` | `DeviceName` | none | MIDI device to connect for output | +| `epn-tx` | `short` | `full` | Configure whether to clear the active parameter number after transmitting an `nrpn` or `rpn` parameter. | Input/output device names may either be prefixes of MIDI device names or numeric indices corresponding to the listing shown at startup when using the global `list` option. @@ -32,6 +33,8 @@ The `winmidi` backend supports mapping different MIDI events as MIDIMonster chan * `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) A MIDIMonster channel 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). @@ -42,16 +45,23 @@ 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: ``` midi1.ch0.note9 > midi2.channel1.cc4 midi1.channel15.pressure1 > midi1.channel0.note0 midi1.ch1.aftertouch > midi2.ch2.cc0 midi1.ch0.pitch > midi2.ch1.pitch +midi2.ch0.nrpn900 > midi1.ch1.rpn1 ``` #### 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. + Currently, no Note Off messages are sent (instead, Note On messages with a velocity of 0 are generated, which amount to the same thing according to the spec). This may be implemented as a configuration option at a later time. -- cgit v1.2.3 From 50ce7276315671b72ecbfbcb7bebe6d34654639a Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 4 Jan 2021 01:16:28 +0100 Subject: Implement EPN reception for the midi backend --- backends/midi.c | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++----- backends/midi.h | 10 ++++-- backends/midi.md | 7 ++-- 3 files changed, 110 insertions(+), 12 deletions(-) (limited to 'backends/midi.md') diff --git a/backends/midi.c b/backends/midi.c index d581a01..7883662 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -273,11 +273,91 @@ static char* midi_type_name(uint8_t type){ return "unknown"; } +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 = { + .label = 0 + }; + channel* changed = NULL; + channel_value val; + //check for 3-byte update TODO + + //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 /*| EPN_VALUE_LO*/); + } + + //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; + } + + //FIXME is the update order for the value bits fixed? + //FIXME can there be standalone updates on CC 38? + + //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; + //FIXME not clearing the valid bit would allow for fast low-order updates + data->epn_status[chan] &= ~EPN_VALUE_HI; + + if(midi_config.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 midi_handle(size_t num, managed_fd* fds){ snd_seq_event_t* ev = NULL; instance* inst = NULL; + midi_instance_data* data = NULL; + channel* changed = NULL; channel_value val; + char* event_type = NULL; midi_channel_ident ident = { .label = 0 @@ -295,7 +375,14 @@ static int midi_handle(size_t num, managed_fd* fds){ ident.fields.control = ev->data.note.note; val.normalised = (double) ev->data.note.velocity / 127.0; - //TODO (n)rpn RX + //scan for the instance before parsing incoming data, instance state is required for the EPN state machine + inst = mm_instance_find(BACKEND_NAME, ev->dest.port); + if(!inst){ + LOG("Delivered event did not match any instance"); + continue; + } + data = (midi_instance_data*) inst->impl; + switch(ev->type){ case SND_SEQ_EVENT_NOTEON: case SND_SEQ_EVENT_NOTEOFF: @@ -323,6 +410,15 @@ static int midi_handle(size_t num, managed_fd* fds){ ident.fields.channel = ev->data.control.channel; ident.fields.control = ev->data.control.param; val.normalised = (double) ev->data.control.value / 127.0; + + //check for EPN CCs and update the state machine + if((ident.fields.control <= 101 && ident.fields.control >= 98) + || ident.fields.control == 6 + || ident.fields.control == 38 + //if the high-order value bits are set, forward any control to the state machine for the short update form + || data->epn_status[ident.fields.channel] & EPN_VALUE_HI){ + midi_handle_epn(inst, ident.fields.channel, ident.fields.control, ev->data.control.value); + } break; default: LOG("Ignored event of unsupported type"); @@ -330,13 +426,6 @@ static int midi_handle(size_t num, managed_fd* fds){ } event_type = midi_type_name(ident.fields.type); - inst = mm_instance_find(BACKEND_NAME, ev->dest.port); - if(!inst){ - //FIXME might want to return failure - LOG("Delivered event did not match any instance"); - continue; - } - changed = mm_channel(inst, ident.label, 0); if(changed){ if(mm_channel_event(changed, val)){ diff --git a/backends/midi.h b/backends/midi.h index 4e2ac09..51b4a30 100644 --- a/backends/midi.h +++ b/backends/midi.h @@ -10,14 +10,20 @@ static int midi_handle(size_t num, managed_fd* fds); static int midi_start(size_t n, instance** inst); static int midi_shutdown(size_t n, instance** inst); +#define EPN_NRPN 8 +#define EPN_PARAMETER_HI 4 +#define EPN_PARAMETER_LO 2 +#define EPN_VALUE_HI 1 + typedef struct /*_midi_instance_data*/ { int port; char* read; char* write; uint8_t epn_tx_short; - uint16_t epn_control; - uint16_t epn_value; + uint16_t epn_control[16]; + uint16_t epn_value[16]; + uint8_t epn_status[16]; } midi_instance_data; typedef union { diff --git a/backends/midi.md b/backends/midi.md index 4732452..87d06a1 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -56,8 +56,11 @@ midi1.ch0.nrpn900 > midi2.ch0.rpn1 ``` #### 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. +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. To access MIDI data, the user running MIDIMonster needs read & write access to the ALSA sequencer. This can usually be done by adding this user to the `audio` system group. -- cgit v1.2.3 From 7902842bd10b17d8d5b6bfc586f440299646d48c Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 9 Jan 2021 15:21:24 +0100 Subject: Add source/target as aliases for MIDI read/write config options (#79) --- backends/midi.c | 4 ++-- backends/midi.md | 10 +++++----- backends/winmidi.c | 4 ++-- backends/winmidi.md | 10 +++++----- 4 files changed, 14 insertions(+), 14 deletions(-) (limited to 'backends/midi.md') diff --git a/backends/midi.c b/backends/midi.c index bddabb5..e32c975 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -83,7 +83,7 @@ static int midi_configure_instance(instance* inst, char* option, char* value){ midi_instance_data* data = (midi_instance_data*) inst->impl; //FIXME maybe allow connecting more than one device - if(!strcmp(option, "read")){ + if(!strcmp(option, "read") || !strcmp(option, "source")){ //connect input device if(data->read){ LOGPF("Instance %s was already connected to an input device", inst->name); @@ -92,7 +92,7 @@ static int midi_configure_instance(instance* inst, char* option, char* value){ data->read = strdup(value); return 0; } - else if(!strcmp(option, "write")){ + else if(!strcmp(option, "write") || !strcmp(option, "target")){ //connect output device if(data->write){ LOGPF("Instance %s was already connected to an output device", inst->name); diff --git a/backends/midi.md b/backends/midi.md index 87d06a1..60a4d06 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -11,11 +11,11 @@ The MIDI backend provides read-write access to the MIDI protocol via virtual por #### Instance configuration -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------| -| `read` | `20:0` | none | MIDI device to connect for input | -| `write` | `DeviceName` | none | MIDI device to connect for output | -| `epn-tx` | `short` | `full` | Configures whether to clear the active parameter number after transmitting an `nrpn` or `rpn` parameter | +| Option | Example value | Default value | Description | +|-----------------------|-----------------------|-----------------------|-----------------------| +| `read` / `source` | `20:0` | none | MIDI device to connect for input | +| `write` / `target` | `DeviceName` | none | MIDI device to connect for output | +| `epn-tx` | `short` | `full` | Configures whether to clear the active parameter number after transmitting an `nrpn` or `rpn` parameter | MIDI device names may either be `client:port` portnames or prefixes of MIDI device names. Run `aconnect -i` to list input ports and `aconnect -o` to list output ports. diff --git a/backends/winmidi.c b/backends/winmidi.c index d12dc71..ec0fb44 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -74,7 +74,7 @@ static int winmidi_configure(char* option, char* value){ static int winmidi_configure_instance(instance* inst, char* option, char* value){ winmidi_instance_data* data = (winmidi_instance_data*) inst->impl; - if(!strcmp(option, "read")){ + if(!strcmp(option, "read") || !strcmp(option, "source")){ if(data->read){ LOGPF("Instance %s already connected to an input device", inst->name); return 1; @@ -82,7 +82,7 @@ static int winmidi_configure_instance(instance* inst, char* option, char* value) data->read = strdup(value); return 0; } - else if(!strcmp(option, "write")){ + else if(!strcmp(option, "write") || !strcmp(option, "target")){ if(data->write){ LOGPF("Instance %s already connected to an output device", inst->name); return 1; diff --git a/backends/winmidi.md b/backends/winmidi.md index be14424..4de792a 100644 --- a/backends/winmidi.md +++ b/backends/winmidi.md @@ -15,11 +15,11 @@ some deviations may still be present. #### Instance configuration -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------| -| `read` | `2` | none | MIDI device to connect for input | -| `write` | `DeviceName` | none | MIDI device to connect for output | -| `epn-tx` | `short` | `full` | Configure whether to clear the active parameter number after transmitting an `nrpn` or `rpn` parameter. | +| Option | Example value | Default value | Description | +|-----------------------|-----------------------|-----------------------|-----------------------| +| `read` / `source` | `2` | none | MIDI device to connect for input | +| `write` / `target` | `DeviceName` | none | MIDI device to connect for output | +| `epn-tx` | `short` | `full` | Configure whether to clear the active parameter number after transmitting an `nrpn` or `rpn` parameter. | Input/output device names may either be prefixes of MIDI device names or numeric indices corresponding to the listing shown at startup when using the global `list` option. -- cgit v1.2.3 From a3a893f6b8b6c10ff281fcdfe0b4a4ddafe89023 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 10 Jan 2021 17:30:12 +0100 Subject: Implement program change control for the midi, winmidi and jack backends (Fixes #79) --- backends/jack.c | 9 ++++++--- backends/jack.h | 7 ++++--- backends/jack.md | 7 ++++++- backends/midi.c | 18 +++++++++++++++++- backends/midi.md | 4 +++- backends/winmidi.c | 12 +++++++++--- backends/winmidi.h | 7 ++++--- backends/winmidi.md | 4 +++- 8 files changed, 52 insertions(+), 16 deletions(-) (limited to 'backends/midi.md') diff --git a/backends/jack.c b/backends/jack.c index 176144f..fe74a80 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -79,7 +79,7 @@ static int mmjack_midiqueue_append(mmjack_port* port, mmjack_channel_ident ident } 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); + jack_midi_data_t* event_data = jack_midi_event_reserve(buffer, sample_offset, (type == midi_aftertouch || type == midi_program) ? 2 : 3); if(!event_data){ LOG("Failed to reserve MIDI stream data"); @@ -95,7 +95,7 @@ static void mmjack_process_midiout(void* buffer, size_t sample_offset, uint8_t t event_data[1] = value & 0x7F; event_data[2] = (value >> 7) & 0x7F; } - else if(type == midi_aftertouch){ + else if(type == midi_aftertouch || type == midi_program){ event_data[1] = value & 0x7F; event_data[2] = 0; } @@ -192,7 +192,7 @@ static int mmjack_process_midi(instance* inst, mmjack_port* port, size_t nframes ident.fields.sub_control = 0; value = event.buffer[1] | (event.buffer[2] << 7); } - else if(ident.fields.sub_type == midi_aftertouch){ + else if(ident.fields.sub_type == midi_aftertouch || ident.fields.sub_type == midi_program){ ident.fields.sub_control = 0; value = event.buffer[1]; } @@ -501,6 +501,9 @@ static int mmjack_parse_midispec(mmjack_channel_ident* ident, char* spec){ else if(!strncmp(next_token, "aftertouch", 10)){ ident->fields.sub_type = midi_aftertouch; } + else if(!strncmp(next_token, "program", 7)){ + ident->fields.sub_type = midi_program; + } else{ LOGPF("Unknown MIDI control type in spec %s", spec); return 1; diff --git a/backends/jack.h b/backends/jack.h index 762282b..42905f1 100644 --- a/backends/jack.h +++ b/backends/jack.h @@ -24,12 +24,13 @@ static int mmjack_shutdown(size_t n, instance** inst); enum /*mmjack_midi_channel_type*/ { midi_none = 0, midi_note = 0x90, - midi_cc = 0xB0, midi_pressure = 0xA0, + midi_cc = 0xB0, + midi_program = 0xC0, midi_aftertouch = 0xD0, midi_pitchbend = 0xE0, - midi_rpn = 0xF0, - midi_nrpn = 0xF1 + midi_rpn = 0xF1, + midi_nrpn = 0xF2 }; typedef union { diff --git a/backends/jack.md b/backends/jack.md index 4ff77f6..c67f060 100644 --- a/backends/jack.md +++ b/backends/jack.md @@ -56,6 +56,9 @@ MIDI ports provide subchannels for the various MIDI controls available. Each MID corresponding pressure controls for each note, 128 control change (CC) controls (numbered likewise), one channel wide "aftertouch" control and one channel-wide pitchbend control. +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. + 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). @@ -66,16 +69,18 @@ The following values are recognized for `type`: * `pressure` - Note pressure/aftertouch messages * `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) -The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel.`. +The `pitch`, `aftertouch` and `program` messages/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 jack2.midi_in.ch0.nrpn900 > jack1.midi_out.ch1.rpn1 +jack1.midi_in.ch15.note1 > jack1.midi_out.ch4.program ``` The MIDI subchannel syntax is intentionally kept compatible to the different MIDI backends also supported diff --git a/backends/midi.c b/backends/midi.c index e32c975..10c8c4a 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -14,6 +14,7 @@ enum /*_midi_channel_type*/ { pressure, aftertouch, pitchbend, + program, rpn, nrpn }; @@ -167,6 +168,9 @@ static channel* midi_channel(instance* inst, char* spec, uint8_t flags){ else if(!strncmp(channel, "pitch", 5)){ ident.fields.type = pitchbend; } + else if(!strncmp(channel, "program", 7)){ + ident.fields.type = program; + } else if(!strncmp(channel, "aftertouch", 10)){ ident.fields.type = aftertouch; } @@ -208,6 +212,9 @@ static void midi_tx(int port, uint8_t type, uint8_t channel, uint8_t control, ui case aftertouch: snd_seq_ev_set_chanpress(&ev, channel, value); break; + case program: + snd_seq_ev_set_pgmchange(&ev, channel, value); + break; } snd_seq_event_output(sequencer, &ev); @@ -269,6 +276,8 @@ static char* midi_type_name(uint8_t type){ return "aftertouch"; case pitchbend: return "pitch"; + case program: + return "program"; } return "unknown"; } @@ -399,6 +408,7 @@ static int midi_handle(size_t num, managed_fd* fds){ case SND_SEQ_EVENT_CHANPRESS: ident.fields.type = aftertouch; ident.fields.channel = ev->data.control.channel; + ident.fields.control = 0; val.normalised = (double) ev->data.control.value / 127.0; break; case SND_SEQ_EVENT_PITCHBEND: @@ -407,6 +417,12 @@ static int midi_handle(size_t num, managed_fd* fds){ ident.fields.channel = ev->data.control.channel; val.normalised = ((double) ev->data.control.value + 8192) / 16383.0; break; + case SND_SEQ_EVENT_PGMCHANGE: + ident.fields.type = program; + ident.fields.control = 0; + ident.fields.channel = ev->data.control.channel; + val.normalised = (double) ev->data.control.value / 127.0; + break; case SND_SEQ_EVENT_CONTROLLER: ident.fields.type = cc; ident.fields.channel = ev->data.control.channel; @@ -437,7 +453,7 @@ static int midi_handle(size_t num, managed_fd* fds){ } if(midi_config.detect && event_type){ - if(ident.fields.type == pitchbend || ident.fields.type == aftertouch){ + if(ident.fields.type == pitchbend || ident.fields.type == aftertouch || ident.fields.type == program){ LOGPF("Incoming data on channel %s.ch%d.%s", inst->name, ident.fields.channel, event_type); } else{ diff --git a/backends/midi.md b/backends/midi.md index 60a4d06..6280205 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -31,13 +31,14 @@ The MIDI backend supports mapping different MIDI events to MIDIMonster channels. * `pressure` - Note pressure/aftertouch messages * `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.`. The shorthand `ch` may be used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). -The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel.`. +The `pitch`, `aftertouch` and `program` messages/events are channel-wide, thus they can be specified as `channel.`. MIDI channels range from `0` to `15`. Each MIDI channel consists of 128 notes (numbered `0` through `127`), which additionally each have a pressure control, 128 CC's (numbered likewise), a channel pressure control (also called @@ -53,6 +54,7 @@ midi1.channel15.pressure1 > midi1.channel0.note0 midi1.ch1.aftertouch > midi2.ch2.cc0 midi1.ch0.pitch > midi2.ch1.pitch midi1.ch0.nrpn900 > midi2.ch0.rpn1 +midi2.ch15.note1 > midi1.ch2.program ``` #### Known bugs / problems diff --git a/backends/winmidi.c b/backends/winmidi.c index ec0fb44..a1fa686 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -169,6 +169,9 @@ static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags){ else if(!strncmp(next_token, "aftertouch", 10)){ ident.fields.type = aftertouch; } + else if(!strncmp(next_token, "program", 7)){ + ident.fields.type = program; + } else{ LOGPF("Unknown control type in %s", spec); return NULL; @@ -203,7 +206,7 @@ static void winmidi_tx(HMIDIOUT port, uint8_t type, uint8_t channel, uint8_t con output.components.data1 = value & 0x7F; output.components.data2 = (value >> 7) & 0x7F; } - else if(type == aftertouch){ + else if(type == aftertouch || type == program){ output.components.data1 = value; output.components.data2 = 0; } @@ -270,6 +273,8 @@ static char* winmidi_type_name(uint8_t typecode){ return "aftertouch"; case pitchbend: return "pitch"; + case program: + return "program"; } return "unknown"; } @@ -294,7 +299,8 @@ static int winmidi_handle(size_t num, managed_fd* fds){ if(backend_config.detect){ //pretty-print channel-wide events if(backend_config.event[u].channel.fields.type == pitchbend - || backend_config.event[u].channel.fields.type == aftertouch){ + || backend_config.event[u].channel.fields.type == aftertouch + || backend_config.event[u].channel.fields.type == program){ LOGPF("Incoming data on channel %s.ch%d.%s, value %f", backend_config.event[u].inst->name, backend_config.event[u].channel.fields.channel, @@ -450,7 +456,7 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW val.normalised = (double) ((input.components.data2 << 7) | input.components.data1) / 16383.0; val.raw.u64 = input.components.data2 << 7 | input.components.data1; } - else if(ident.fields.type == aftertouch){ + else if(ident.fields.type == aftertouch || ident.fields.type == program){ ident.fields.control = 0; val.normalised = (double) input.components.data1 / 127.0; val.raw.u64 = input.components.data1; diff --git a/backends/winmidi.h b/backends/winmidi.h index 4d3e2dd..40b3554 100644 --- a/backends/winmidi.h +++ b/backends/winmidi.h @@ -31,12 +31,13 @@ typedef struct /*_winmidi_instance_data*/ { enum /*_winmidi_channel_type*/ { none = 0, note = 0x90, - cc = 0xB0, pressure = 0xA0, + cc = 0xB0, + program = 0xC0, aftertouch = 0xD0, pitchbend = 0xE0, - rpn = 0xF0, - nrpn = 0xF1 + rpn = 0xF1, + nrpn = 0xF2 }; typedef union { diff --git a/backends/winmidi.md b/backends/winmidi.md index 4de792a..9e7d9cc 100644 --- a/backends/winmidi.md +++ b/backends/winmidi.md @@ -33,13 +33,14 @@ The `winmidi` backend supports mapping different MIDI events as MIDIMonster chan * `pressure` - Note pressure/aftertouch messages * `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.`. The shorthand `ch` may be used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). -The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel.`. +The `pitch`, `aftertouch` and `program` messages/events are channel-wide, thus they can be specified as `channel.`. MIDI channels range from `0` to `15`. Each MIDI channel consists of 128 notes (numbered `0` through `127`), which additionally each have a pressure control, 128 CC's (numbered likewise), a channel pressure control (also called @@ -55,6 +56,7 @@ midi1.channel15.pressure1 > midi1.channel0.note0 midi1.ch1.aftertouch > midi2.ch2.cc0 midi1.ch0.pitch > midi2.ch1.pitch midi2.ch0.nrpn900 > midi1.ch1.rpn1 +midi2.ch15.note1 > midi1.ch2.program ``` #### Known bugs / problems -- cgit v1.2.3