aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--backends/Makefile8
-rw-r--r--backends/rtpmidi.c401
-rw-r--r--backends/rtpmidi.h108
-rw-r--r--backends/rtpmidi.md86
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