diff options
-rw-r--r-- | backends/Makefile | 8 | ||||
-rw-r--r-- | backends/rtpmidi.c | 401 | ||||
-rw-r--r-- | backends/rtpmidi.h | 108 | ||||
-rw-r--r-- | backends/rtpmidi.md | 86 |
4 files changed, 601 insertions, 2 deletions
diff --git a/backends/Makefile b/backends/Makefile index feefd7b..df01ec8 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,7 +1,7 @@ .PHONY: all clean full LINUX_BACKENDS = midi.so evdev.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 jack.so +WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll rtpmidi.dll +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so rtpmidi.so OPTIONAL_BACKENDS = ola.so BACKEND_LIB = libmmbackend.o @@ -38,6 +38,10 @@ maweb.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) maweb.dll: LDLIBS += -lws2_32 maweb.dll: CFLAGS += -DMAWEB_NO_LIBSSL +rtpmidi.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +rtpmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +rtpmidi.dll: LDLIBS += -lws2_32 + winmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) winmidi.dll: LDLIBS += -lwinmm -lws2_32 diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c new file mode 100644 index 0000000..38cc9c1 --- /dev/null +++ b/backends/rtpmidi.c @@ -0,0 +1,401 @@ +#include <string.h> +#include <errno.h> +#include <unistd.h> +#include <fcntl.h> +#include <ctype.h> + +#include "libmmbackend.h" +#include "rtpmidi.h" + +#define BACKEND_NAME "rtpmidi" + +static struct /*_rtpmidi_global*/ { + int mdns_fd; + char* mdns_name; + uint8_t detect; +} cfg = { + .mdns_fd = -1, + .mdns_name = NULL, + .detect = 0 +}; + +int init(){ + backend rtpmidi = { + .name = BACKEND_NAME, + .conf = rtpmidi_configure, + .create = rtpmidi_instance, + .conf_instance = rtpmidi_configure_instance, + .channel = rtpmidi_channel, + .handle = rtpmidi_set, + .process = rtpmidi_handle, + .start = rtpmidi_start, + .shutdown = rtpmidi_shutdown + }; + + if(sizeof(rtpmidi_channel_ident) != sizeof(uint64_t)){ + fprintf(stderr, "rtpmidi channel identification union out of bounds\n"); + return 1; + } + + if(mm_backend_register(rtpmidi)){ + fprintf(stderr, "Failed to register rtpmidi backend\n"); + return 1; + } + + return 0; +} + +static int rtpmidi_configure(char* option, char* value){ + char* host = NULL, *port = NULL; + + if(!strcmp(option, "mdns-name")){ + if(cfg.mdns_name){ + fprintf(stderr, "Duplicate mdns-name assignment\n"); + return 1; + } + + cfg.mdns_name = strdup(value); + if(!cfg.mdns_name){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + return 0; + } + else if(!strcmp(option, "mdns-bind")){ + if(cfg.mdns_fd >= 0){ + fprintf(stderr, "Only one mDNS discovery bind is supported\n"); + return 1; + } + + mmbackend_parse_hostspec(value, &host, &port); + + if(!host){ + fprintf(stderr, "Not a valid mDNS bind address: %s\n", value); + return 1; + } + + cfg.mdns_fd = mmbackend_socket(host, (port ? port : RTPMIDI_MDNS_PORT), SOCK_DGRAM, 1, 1); + if(cfg.mdns_fd < 0){ + fprintf(stderr, "Failed to bind mDNS interface: %s\n", value); + return 1; + } + return 0; + } + else if(!strcmp(option, "detect")){ + cfg.detect = 0; + if(!strcmp(value, "on")){ + cfg.detect = 1; + } + return 0; + } + + fprintf(stderr, "Unknown rtpmidi backend option %s\n", option); + return 1; +} + +static int rtpmidi_bind_instance(rtpmidi_instance_data* data, char* host, char* port){ + struct sockaddr_storage sock_addr = { + 0 + }; + socklen_t sock_len = sizeof(sock_addr); + char control_port[32]; + + //bind to random port if none supplied + data->fd = mmbackend_socket(host, port ? port : "0", SOCK_DGRAM, 1, 0); + if(data->fd < 0){ + return 1; + } + + //bind control port + if(data->mode == apple){ + if(getsockname(data->fd, (struct sockaddr*) &sock_addr, &sock_len)){ + fprintf(stderr, "Failed to fetch data port information: %s\n", strerror(errno)); + return 1; + } + + snprintf(control_port, sizeof(control_port), "%d", be16toh(((struct sockaddr_in*)&sock_addr)->sin_port) - 1); + data->control_fd = mmbackend_socket(host, control_port, SOCK_DGRAM, 1, 0); + if(data->control_fd < 0){ + fprintf(stderr, "Failed to bind control port %s\n", control_port); + return 1; + } + } + + return 0; +} + +static int rtpmidi_configure_instance(instance* inst, char* option, char* value){ + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + char* host = NULL, *port = NULL; + + if(!strcmp(option, "mode")){ + if(!strcmp(value, "direct")){ + data->mode = direct; + return 0; + } + else if(!strcmp(value, "apple")){ + data->mode = apple; + return 0; + } + fprintf(stderr, "Unknown rtpmidi instance mode %s for instance %s\n", value, inst->name); + return 1; + } + else if(!strcmp(option, "ssrc")){ + data->ssrc = strtoul(value, NULL, 0); + if(!data->ssrc){ + fprintf(stderr, "Random SSRC will be generated for rtpmidi instance %s\n", inst->name); + } + return 0; + } + else if(!strcmp(option, "bind")){ + if(data->mode == unconfigured){ + fprintf(stderr, "Please specify mode for instance %s before setting bind host\n", inst->name); + return 1; + } + + mmbackend_parse_hostspec(value, &host, &port); + + if(!host){ + fprintf(stderr, "Could not parse bind host specification %s for instance %s\n", value, inst->name); + return 1; + } + + return rtpmidi_bind_instance(data, host, port); + } + else if(!strcmp(option, "learn")){ + if(data->mode != direct){ + fprintf(stderr, "The rtpmidi 'learn' option is only valid for direct mode instances\n"); + return 1; + } + data->learn_peers = 0; + if(!strcmp(value, "true")){ + data->learn_peers = 1; + } + return 0; + } + else if(!strcmp(option, "peer")){ + if(data->mode != direct){ + fprintf(stderr, "The rtpmidi 'peer' option is only valid for direct mode instances\n"); + return 1; + } + + //TODO add peer + return 0; + } + else if(!strcmp(option, "session")){ + if(data->mode != apple){ + fprintf(stderr, "The rtpmidi 'session' option is only valid for apple mode instances\n"); + return 1; + } + free(data->session_name); + data->session_name = strdup(value); + if(!data->session_name){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + return 0; + } + else if(!strcmp(option, "invite")){ + if(data->mode != apple){ + fprintf(stderr, "The rtpmidi 'invite' option is only valid for apple mode instances\n"); + return 1; + } + free(data->invite_peers); + data->invite_peers = strdup(value); + if(!data->invite_peers){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + return 0; + } + else if(!strcmp(option, "join")){ + if(data->mode != apple){ + fprintf(stderr, "The rtpmidi 'join' option is only valid for apple mode instances\n"); + return 1; + } + free(data->invite_accept); + data->invite_accept = strdup(value); + if(!data->invite_accept){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + return 0; + } + + fprintf(stderr, "Unknown rtpmidi instance option %s\n", option); + return 1; +} + +static instance* rtpmidi_instance(){ + rtpmidi_instance_data* data = NULL; + instance* inst = mm_instance(); + + if(!inst){ + return NULL; + } + + data = calloc(1, sizeof(rtpmidi_instance_data)); + if(!data){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + data->fd = -1; + data->control_fd = -1; + + inst->impl = data; + return inst; +} + +static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){ + char* next_token = spec; + rtpmidi_channel_ident ident = { + .label = 0 + }; + + if(!strncmp(spec, "ch", 2)){ + next_token += 2; + if(!strncmp(spec, "channel", 7)){ + next_token = spec + 7; + } + } + else{ + fprintf(stderr, "Invalid rtpmidi channel specification %s\n", spec); + return NULL; + } + + ident.fields.channel = strtoul(next_token, &next_token, 10); + if(ident.fields.channel > 15){ + fprintf(stderr, "rtpmidi channel out of range in channel spec %s\n", spec); + return NULL; + } + + if(*next_token != '.'){ + fprintf(stderr, "rtpmidi channel specification %s does not conform to channel<X>.<control><Y>\n", spec); + return NULL; + } + + next_token++; + + if(!strncmp(next_token, "cc", 2)){ + ident.fields.type = cc; + next_token += 2; + } + else if(!strncmp(next_token, "note", 4)){ + ident.fields.type = note; + next_token += 4; + } + else if(!strncmp(next_token, "pressure", 8)){ + ident.fields.type = pressure; + next_token += 8; + } + else if(!strncmp(next_token, "pitch", 5)){ + ident.fields.type = pitchbend; + } + else if(!strncmp(next_token, "aftertouch", 10)){ + ident.fields.type = aftertouch; + } + else{ + fprintf(stderr, "Unknown rtpmidi channel control type in spec %s\n", spec); + return NULL; + } + + ident.fields.control = strtoul(next_token, NULL, 10); + + if(ident.label){ + return mm_channel(inst, ident.label, 1); + } + return NULL; +} + +static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v){ + //TODO + return 1; +} + +static int rtpmidi_handle(size_t num, managed_fd* fds){ + //TODO handle discovery + + if(!num){ + return 0; + } + + //TODO + return 1; +} + +static int rtpmidi_start(){ + size_t n, u, fds = 0; + int rv = 1; + instance** inst = NULL; + rtpmidi_instance_data* data = NULL; + + //if mdns name defined and no socket, bind default values + if(cfg.mdns_name && cfg.mdns_fd < 0){ + cfg.mdns_fd = mmbackend_socket("::", RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1); + if(cfg.mdns_fd < 0){ + return 1; + } + } + + //register mdns fd to core + if(cfg.mdns_fd >= 0){ + if(mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL)){ + fprintf(stderr, "rtpmidi failed to register mDNS socket with core\n"); + goto bail; + } + fds++; + } + else{ + fprintf(stderr, "No mDNS discovery interface bound, AppleMIDI session discovery disabled\n"); + } + + //fetch all defined instances + 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 = (rtpmidi_instance_data*) inst[u]->impl; + //check whether instances are explicitly configured to a mode + if(data->mode == unconfigured){ + fprintf(stderr, "rtpmidi instance %s is missing a mode configuration\n", inst[u]->name); + goto bail; + } + + //generate random ssrc's + if(!data->ssrc){ + data->ssrc = rand() << 16 | rand(); + } + + //if not bound, bind to default + if(data->fd < 0 && rtpmidi_bind_instance(data, "::", NULL)){ + fprintf(stderr, "Failed to bind default sockets for rtpmidi instance %s\n", inst[u]->name); + goto bail; + } + + //register fds to core + if(mm_manage_fd(data->fd, BACKEND_NAME, 1, NULL) || (data->control_fd >= 0 && mm_manage_fd(data->control_fd, BACKEND_NAME, 1, NULL))){ + fprintf(stderr, "rtpmidi failed to register instance socket with core\n"); + goto bail; + } + fds += (data->control_fd >= 0) ? 2 : 1; + } + + fprintf(stderr, "rtpmidi backend registered %" PRIsize_t " descriptors to core\n", fds); + rv = 0; +bail: + free(inst); + return rv; +} + +static int rtpmidi_shutdown(){ + //TODO cleanup instance data + + free(cfg.mdns_name); + if(cfg.mdns_fd >= 0){ + close(cfg.mdns_fd); + } + + return 0; +} diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h new file mode 100644 index 0000000..6cab225 --- /dev/null +++ b/backends/rtpmidi.h @@ -0,0 +1,108 @@ +#ifndef _WIN32 +#include <sys/socket.h> +#endif +#include "midimonster.h" + +int init(); +static int rtpmidi_configure(char* option, char* value); +static int rtpmidi_configure_instance(instance* instance, char* option, char* value); +static instance* rtpmidi_instance(); +static channel* rtpmidi_channel(instance* instance, char* spec, uint8_t flags); +static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v); +static int rtpmidi_handle(size_t num, managed_fd* fds); +static int rtpmidi_start(); +static int rtpmidi_shutdown(); + +#define RTPMIDI_DEFAULT_PORTBASE "9001" +#define RTPMIDI_RECV_BUF 4096 +#define RTPMIDI_MDNS_PORT "5353" +#define RTPMIDI_HEADER_MAGIC htobe16(0x80E1) + +enum /*_rtpmidi_channel_type*/ { + none = 0, + note = 0x90, + cc = 0xB0, + pressure = 0xA0, + aftertouch = 0xD0, + pitchbend = 0xE0 +}; + +typedef enum /*_rtpmidi_instance_mode*/ { + unconfigured = 0, + direct, + apple +} rtpmidi_instance_mode; + +typedef union { + struct { + uint8_t pad[5]; + uint8_t type; + uint8_t channel; + uint8_t control; + } fields; + uint64_t label; +} rtpmidi_channel_ident; + +typedef struct /*_rtpmidi_peer*/ { + struct sockaddr_storage dest; + socklen_t dest_len; + uint32_t ssrc; +} rtpmidi_peer; + +typedef struct /*_rtmidi_instance_data*/ { + rtpmidi_instance_mode mode; + + int fd; + int control_fd; + + size_t peers; + rtpmidi_peer* peer; + uint32_t ssrc; + + //apple-midi config + char* session_name; + char* invite_peers; + char* invite_accept; + + //direct mode config + uint8_t learn_peers; +} rtpmidi_instance_data; + +#pragma pack(push, 1) +typedef struct /*_apple_session_command*/ { + uint16_t res1; + uint8_t command[2]; + uint32_t version; + uint32_t token; + uint32_t ssrc; + //char* name +} apple_command; + +typedef struct /*_apple_session_sync*/ { + uint16_t res1; + uint8_t command[2]; + uint32_t ssrc; + uint8_t count; + uint8_t res2[3]; + uint64_t timestamp[3]; +} apple_sync; + +typedef struct /*_apple_session_feedback*/ { + uint16_t res1; + uint8_t command[2]; + uint32_t ssrc; + uint32_t sequence; +} apple_feedback; + +typedef struct /*_rtp_midi_header*/ { + uint16_t vpxccmpt; //this is really just an amalgamated constant value + uint16_t sequence; + uint32_t timestamp; + uint32_t ssrc; +} rtpmidi_header; + +typedef struct /*_rtp_midi_command*/ { + uint8_t flags; + uint8_t additional_length; +} rtpmidi_command; +#pragma pack(pop) diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md new file mode 100644 index 0000000..c208bf7 --- /dev/null +++ b/backends/rtpmidi.md @@ -0,0 +1,86 @@ +### The `rtpmidi` backend + +This backend provides read-write access to RTP MIDI streams, which transfer MIDI data +over the network. + +As the specification for RTP MIDI does not normatively indicate any method +for session management, most vendors define their own standards for this. +The MIDIMonster supports the following session management methods, which are +selectable per-instance, with some methods requiring additional global configuration: + +* Direct connection: The instance will send and receive data from peers configured in the + instance configuration +* Direct connection with peer learning: The instance will send and receive data from peers + configured in the instance configuration as well as previously unknown peers that + voluntarily send data to the instance. +* AppleMIDI session management: + +Note that instances that receive data from multiple peers will combine all inputs into one +stream, which may lead to inconsistencies during playback. + +#### Global configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration | +| `mdns-bind` | `10.1.2.1 5353` | `:: 5353` | Bind host for the mDNS discovery server | +| `mdns-name` | `computer1` | none | mDNS hostname to announce, also used as AppleMIDI peer name | + +#### Instance configuration + +Common instance configuration parameters + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `ssrc` | `0xDEADBEEF` | Randomly generated | 32-bit synchronization source identifier | +| `mode` | `direct` | none | Instance session management mode (`direct` or `apple`) | + +`direct` mode instance configuration parameters + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `bind` | `10.1.2.1 9001` | `:: <random>` | Local network address to bind to | +| `learn` | `true` | `false` | Accept new peers for data exchange at runtime | +| `peer` | `10.1.2.3 9001` | none | MIDI session peer, may be specified multiple times | + +`apple` mode instance configuration parameters + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `bind` | `10.1.2.1 9001` | `:: <random>` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | +| `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | +| `invite` | `pad,piano` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator | +| `join` | `Just Jamming` | none | Sessions for which to accept invitations (the special value `*` accepts all invitations). Setting this option makes the instance a session participant | + +Note that AppleMIDI session discovery requires mDNS functionality, thus the `mdns-name` global parameter +(and, depending on your setup, the `mdns-bind` parameter) need to be configured properly. + +#### Channel specification + +The `rtpmidi` backend supports mapping different MIDI events to MIDIMonster channels. The currently supported event types are + +* `cc` - Control Changes +* `note` - Note On/Off messages +* `pressure` - Note pressure/aftertouch messages +* `aftertouch` - Channel-wide aftertouch messages +* `pitch` - Channel pitchbend messages + +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). + +The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel<channel>.<type>`. + +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 +'channel aftertouch') and a pitch control which may all be mapped to individual MIDIMonster channels. + +Example mappings: + +``` +rmidi1.ch0.note9 > rmidi2.channel1.cc4 +rmidi1.channel15.pressure1 > rmidi1.channel0.note0 +rmidi1.ch1.aftertouch > rmidi2.ch2.cc0 +rmidi1.ch0.pitch > rmidi2.ch1.pitch +``` + +#### Known bugs / problems |