aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--TODO2
-rw-r--r--backends/Makefile8
-rw-r--r--backends/libmmbackend.c74
-rw-r--r--backends/libmmbackend.h29
-rw-r--r--backends/rtpmidi.c1731
-rw-r--r--backends/rtpmidi.h181
-rw-r--r--backends/rtpmidi.md93
7 files changed, 2110 insertions, 8 deletions
diff --git a/TODO b/TODO
index ccad973..befa5e6 100644
--- a/TODO
+++ b/TODO
@@ -3,4 +3,4 @@ Note source in channel value struct
udp backends may ignore MTU
make event collectors threadsafe to stop marshalling data...
collect & check backend API version
-windows strerror
+move all connection establishment to _start to be able to hot-stop/start all backends
diff --git a/backends/Makefile b/backends/Makefile
index e31ff24..1e66995 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 openpixelcontrol.dll
-BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.so
+WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll openpixelcontrol.dll rtpmidi.dll
+BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.so rtpmidi.so
OPTIONAL_BACKENDS = ola.so
BACKEND_LIB = libmmbackend.o
@@ -46,6 +46,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 -liphlpapi
+
winmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
winmidi.dll: LDLIBS += -lwinmm -lws2_32
diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c
index b9513ac..2bbc226 100644
--- a/backends/libmmbackend.c
+++ b/backends/libmmbackend.c
@@ -1,6 +1,70 @@
#include "libmmbackend.h"
#define LOGPF(format, ...) fprintf(stderr, "libmmbe\t" format "\n", __VA_ARGS__)
+#define LOG(message) fprintf(stderr, "libmmbe\t%s\n", (message))
+
+int mmbackend_strdup(char** dest, char* src){
+ if(*dest){
+ free(*dest);
+ }
+
+ *dest = strdup(src);
+
+ if(!*dest){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+ return 0;
+}
+
+char* mmbackend_socket_strerror(int err_no){
+ #ifdef _WIN32
+ static char error[2048] = "";
+ FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, WSAGetLastError(),
+ MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), error, sizeof(error), NULL);
+ return error;
+ #else
+ return strerror(err_no);
+ #endif
+}
+
+const char* mmbackend_sockaddr_ntop(struct sockaddr* peer, char* buffer, size_t length){
+ union {
+ struct sockaddr* in;
+ struct sockaddr_in* in4;
+ struct sockaddr_in6* in6;
+ } addr;
+ addr.in = peer;
+ #ifdef _WIN32
+ uint8_t* data = NULL;
+ #endif
+
+ switch(addr.in->sa_family){
+ //inet_ntop has become available in the winapi with vista, but eh.
+ #ifdef _WIN32
+ case AF_INET6:
+ data = addr.in6->sin6_addr.s6_addr;
+ snprintf(buffer, length, "%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X",
+ data[0], data[1], data[2], data[3],
+ data[4], data[5], data[6], data[7],
+ data[8], data[9], data[10], data[11],
+ data[12], data[13], data[14], data[15]);
+ return buffer;
+ case AF_INET:
+ data = (uint8_t*) &(addr.in4->sin_addr.s_addr);
+ snprintf(buffer, length, "%d.%d.%d.%d", data[0], data[1], data[2], data[3]);
+ return buffer;
+ #else
+ case AF_INET6:
+ return inet_ntop(addr.in->sa_family, &(addr.in6->sin6_addr), buffer, length);
+ case AF_INET:
+ return inet_ntop(addr.in->sa_family, &(addr.in4->sin_addr), buffer, length);
+ #endif
+ default:
+ snprintf(buffer, length, "Socket family not implemented");
+ return buffer;
+ }
+}
void mmbackend_parse_hostspec(char* spec, char** host, char** port, char** options){
size_t u = 0;
@@ -92,18 +156,18 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin
//set required socket options
yes = 1;
if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){
- LOGPF("Failed to enable SO_REUSEADDR on socket: %s", strerror(errno));
+ LOGPF("Failed to enable SO_REUSEADDR on socket: %s", mmbackend_socket_strerror(errno));
}
if(mcast){
yes = 1;
if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){
- LOGPF("Failed to enable SO_BROADCAST on socket: %s", strerror(errno));
+ LOGPF("Failed to enable SO_BROADCAST on socket: %s", mmbackend_socket_strerror(errno));
}
yes = 0;
if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){
- LOGPF("Failed to disable IP_MULTICAST_LOOP on socket: %s", strerror(errno));
+ LOGPF("Failed to disable IP_MULTICAST_LOOP on socket: %s", mmbackend_socket_strerror(errno));
}
}
@@ -141,7 +205,7 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin
#else
int flags = fcntl(fd, F_GETFL, 0);
if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){
- LOGPF("Failed to set socket nonblocking: %s", strerror(errno));
+ LOGPF("Failed to set socket nonblocking: %s", mmbackend_socket_strerror(errno));
close(fd);
return -1;
}
@@ -159,7 +223,7 @@ int mmbackend_send(int fd, uint8_t* data, size_t length){
sent = send(fd, data + total, 1, 0);
#endif
if(sent < 0){
- LOGPF("Failed to send: %s", strerror(errno));
+ LOGPF("Failed to send: %s", mmbackend_socket_strerror(errno));
return 1;
}
total += sent;
diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h
index aa0d0f0..08f03aa 100644
--- a/backends/libmmbackend.h
+++ b/backends/libmmbackend.h
@@ -5,6 +5,7 @@
#include <ws2tcpip.h>
//#define close closesocket
#else
+#include <arpa/inet.h>
#include <sys/socket.h>
#include <netdb.h>
#endif
@@ -18,6 +19,34 @@
/*** BACKEND IMPLEMENTATION LIBRARY ***/
+/** Convenience functions **/
+
+/*
+ * Duplicate src into *dest, freeing earlier content of *dest if present
+ * On success, 0 is returned
+ * On failure, a message is printed, *dest is a NULL pointer and 1 is returned
+ */
+int mmbackend_strdup(char** dest, char* src);
+
+/*
+ * Return a formatted error message pertaining to the last socket operation.
+ * On Linux/OSX, this calls through to strerror using the provided err_no.
+ * On Windows, err_no is ignored and WSAGetLastError is called to retrieve
+ * the status of the last operation. This information is then processed via
+ * FormatMessage into a fixed buffer, which is returned. Thus, this function
+ * is not thread-safe on Windows. On Linux, refer to strerror's documentation
+ * for information on thread-safety.
+ */
+char* mmbackend_socket_strerror(int err_no);
+
+/*
+ * Wrap / reimplement (on Windows) inet_ntop to work with struct sockaddr* directly.
+ * Prints the address in a "human-readable" form into buffer.
+ * Will modify at most length bytes into buffer, output will be zero-terminated.
+ * This function only works with AF_INET and AF_INET6 addresses.
+ */
+const char* mmbackend_sockaddr_ntop(struct sockaddr* peer, char* buffer, size_t length);
+
/** Networking functions **/
/*
diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c
new file mode 100644
index 0000000..443967d
--- /dev/null
+++ b/backends/rtpmidi.c
@@ -0,0 +1,1731 @@
+#define BACKEND_NAME "rtpmidi"
+//#define DEBUG
+
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <ctype.h>
+
+//mmbackend pulls in windows.h, required before more specific includes
+#include "libmmbackend.h"
+#include "rtpmidi.h"
+
+#ifdef _WIN32
+#include <iphlpapi.h>
+#else
+#include <arpa/inet.h>
+#include <sys/types.h>
+#include <ifaddrs.h>
+#endif
+
+//#include "../tests/hexdump.c"
+
+//TODO learn peer ssrcs
+//TODO default mode?
+//TODO internal loop mode
+//TODO for some reason, the announce packet generates an exception in the wireshark dns dissector
+//TODO rename and document most functions
+//TODO timeout non-responsive peers (connected = 0) to allow discovery to reconnect them
+//TODO ipv6-mapped-ipv4 creates problens when connecting on a ipv4-bound instance
+
+static struct /*_rtpmidi_global*/ {
+ int mdns_fd;
+ char* mdns_name;
+ char* mdns_interface;
+
+ uint8_t detect;
+ uint64_t last_service;
+
+ size_t addresses;
+ rtpmidi_addr* address;
+
+ size_t invites;
+ rtpmidi_invite* invite;
+} cfg = {
+ .mdns_fd = -1,
+ .mdns_name = NULL,
+ .mdns_interface = NULL,
+
+ .detect = 0,
+ .last_service = 0,
+
+ .addresses = 0,
+ .address = NULL,
+
+ .invites = 0,
+ .invite = NULL
+};
+
+MM_PLUGIN_API int init(){
+ backend rtpmidi = {
+ .name = BACKEND_NAME,
+ .conf = rtpmidi_configure,
+ .create = rtpmidi_instance,
+ .conf_instance = rtpmidi_configure_instance,
+ .channel = rtpmidi_channel,
+ .handle = rtpmidi_set,
+ .interval = rtpmidi_interval,
+ .process = rtpmidi_handle,
+ .start = rtpmidi_start,
+ .shutdown = rtpmidi_shutdown
+ };
+
+ if(sizeof(rtpmidi_channel_ident) != sizeof(uint64_t)){
+ LOG("Channel identification union out of bounds");
+ return 1;
+ }
+
+ if(mm_backend_register(rtpmidi)){
+ LOG("Failed to register backend");
+ return 1;
+ }
+
+ return 0;
+}
+
+static int dns_decode_name(uint8_t* buffer, size_t len, size_t start, dns_name* out){
+ size_t offset = 0, output_offset = 0;
+ uint8_t current_label = 0;
+ uint16_t ptr_target = 0;
+
+ //reset output data length and terminate null name
+ out->length = 0;
+ if(out->name){
+ out->name[0] = 0;
+ }
+
+ while(start + offset < len){
+ current_label = buffer[start + offset];
+
+ //if we're at a pointer, move there and stop counting data length
+ if(DNS_POINTER(current_label)){
+ if(start + offset + 1 >= len){
+ LOG("mDNS internal pointer out of bounds");
+ return 1;
+ }
+
+ //do this before setting the target
+ if(!ptr_target){
+ out->length += 2;
+ }
+
+ //calculate pointer target
+ ptr_target = DNS_LABEL_LENGTH(current_label) << 8 | buffer[start + offset + 1];
+
+ if(ptr_target >= len){
+ LOG("mDNS internal pointer target out of bounds");
+ return 1;
+ }
+ start = ptr_target;
+ offset = 0;
+ }
+ else{
+ if(DNS_LABEL_LENGTH(current_label) == 0){
+ if(!ptr_target){
+ out->length++;
+ }
+ break;
+ }
+
+ //check whether we have the bytes we need
+ if(start + offset + DNS_LABEL_LENGTH(current_label) > len){
+ LOG("mDNS bytes missing");
+ return 1;
+ }
+
+ //check whether we have space in the output
+ if(output_offset + DNS_LABEL_LENGTH(current_label) > out->alloc){
+ out->name = realloc(out->name, (output_offset + DNS_LABEL_LENGTH(current_label) + 2) * sizeof(uint8_t));
+ if(!out->name){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+ out->alloc = output_offset + DNS_LABEL_LENGTH(current_label);
+ }
+
+ //copy data from this label to output buffer
+ memcpy(out->name + output_offset, buffer + start + offset + 1, DNS_LABEL_LENGTH(current_label));
+ output_offset += DNS_LABEL_LENGTH(current_label) + 1;
+ offset += DNS_LABEL_LENGTH(current_label) + 1;
+ out->name[output_offset - 1] = '.';
+ out->name[output_offset] = 0;
+ if(!ptr_target){
+ out->length = offset;
+ }
+ }
+ }
+ return 0;
+}
+
+static int dns_encode_name(char* name, dns_name* out){
+ char* save = NULL, *token = NULL;
+ out->length = 0;
+
+ for(token = strtok_r(name, ".", &save); token; token = strtok_r(NULL, ".", &save)){
+ //make space for this label, its length and a trailing root label
+ if(out->alloc < out->length + strlen(token) + 1 + 1){
+ out->name = realloc(out->name, (out->length + strlen(token) + 2) * sizeof(char));
+ if(!out->name){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+ out->alloc = out->length + strlen(token) + 2;
+ }
+ //FIXME check label length before adding
+ out->name[out->length] = strlen(token);
+ memcpy(out->name + out->length + 1, token, strlen(token));
+ out->length += strlen(token) + 1;
+ }
+
+ //last-effort allocate a root buffer
+ if(!out->alloc){
+ out->name = calloc(1, sizeof(char));
+ if(!out->name){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+ out->alloc = 1;
+ }
+
+ //add root label
+ out->name[out->length] = 0;
+ out->length++;
+
+ return 0;
+}
+
+static ssize_t dns_push_rr(uint8_t* buffer, size_t length, dns_rr** out, char* name, uint16_t type, uint16_t class, uint32_t ttl, uint16_t len){
+ dns_rr* rr = NULL;
+ size_t offset = 0;
+ dns_name encode = {
+ .alloc = 0
+ };
+
+ //if requested, encode name
+ if(name && dns_encode_name(name, &encode)){
+ LOGPF("Failed to encode DNS name %s", name);
+ goto bail;
+ }
+
+ if(encode.length + sizeof(dns_rr) > length){
+ LOGPF("Failed to encode DNS name %s, insufficient space", name);
+ goto bail;
+ }
+
+ if(name){
+ //copy encoded name to buffer
+ memcpy(buffer, encode.name, encode.length);
+ offset += encode.length;
+ }
+
+ rr = (dns_rr*) (buffer + offset);
+ rr->rtype = htobe16(type);
+ rr->rclass = htobe16(class);
+ rr->ttl = htobe32(ttl);
+ rr->data = htobe16(len);
+ offset += sizeof(dns_rr);
+ if(out){
+ *out = rr;
+ }
+
+ free(encode.name);
+ return offset;
+
+bail:
+ free(encode.name);
+ return -1;
+}
+
+//TODO this should be trimmed down a bit
+static int rtpmidi_announce_addrs(){
+ char repr[INET6_ADDRSTRLEN + 1] = "", iface[2048] = "";
+ union {
+ struct sockaddr_in* in4;
+ struct sockaddr_in6* in6;
+ struct sockaddr* in;
+ } addr;
+
+ #ifdef _WIN32
+ IP_ADAPTER_UNICAST_ADDRESS_LH* unicast_addr = NULL;
+ IP_ADAPTER_ADDRESSES addrs[250] , *iter = NULL;
+ size_t bytes_alloc = sizeof(addrs);
+
+ unsigned long status = GetAdaptersAddresses(0, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER,
+ NULL, addrs, (unsigned long*) &bytes_alloc);
+ if(status != ERROR_SUCCESS){
+ //FIXME might try to resize the result list and retry at some point...
+ FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, status,
+ MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), iface, sizeof(iface), NULL);
+ LOGPF("Failed to query local interface addresses (%lu): %s", status, iface);
+ return 1;
+ }
+
+ for(iter = addrs; iter; iter = iter->Next){
+ //friendlyname is a wide string, print it into interface for basic conversion and to avoid implementing wide string handling
+ snprintf(iface, sizeof(iface), "%S", iter->FriendlyName);
+ //filter interfaces if requested
+ if(cfg.mdns_interface && strncmp(iface, cfg.mdns_interface, min(strlen(iface), strlen(cfg.mdns_interface)))){
+ continue;
+ }
+
+ for(unicast_addr = (IP_ADAPTER_UNICAST_ADDRESS_LH*) iter->FirstUnicastAddress; unicast_addr; unicast_addr = unicast_addr->Next){
+ addr.in = unicast_addr->Address.lpSockaddr;
+ #else
+ struct ifaddrs* ifa = NULL, *iter = NULL;
+
+ if(getifaddrs(&ifa)){
+ LOGPF("Failed to get adapter address information: %s", mmbackend_socket_strerror(errno));
+ return 1;
+ }
+
+ for(iter = ifa; iter; iter = iter->ifa_next){
+ if((!cfg.mdns_interface || !strcmp(cfg.mdns_interface, iter->ifa_name))
+ && strcmp(iter->ifa_name, "lo")
+ && iter->ifa_addr){
+ snprintf(iface, sizeof(iface), "%s", iter->ifa_name);
+ addr.in = iter->ifa_addr;
+ #endif
+ if(addr.in->sa_family != AF_INET && addr.in->sa_family != AF_INET6){
+ continue;
+ }
+
+ cfg.address = realloc(cfg.address, (cfg.addresses + 1) * sizeof(rtpmidi_addr));
+ if(!cfg.address){
+ cfg.addresses = 0;
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+
+ cfg.address[cfg.addresses].family = addr.in->sa_family;
+ memcpy(&cfg.address[cfg.addresses].addr,
+ (addr.in->sa_family == AF_INET) ? (void*) &addr.in4->sin_addr.s_addr : (void*) &addr.in6->sin6_addr.s6_addr,
+ (addr.in->sa_family == AF_INET) ? 4 : 16);
+
+ LOGPF("mDNS announce address %" PRIsize_t ": %s (from %s)", cfg.addresses, mmbackend_sockaddr_ntop(addr.in, repr, sizeof(repr)), iface);
+ cfg.addresses++;
+ }
+ }
+
+ #ifndef _WIN32
+ freeifaddrs(ifa);
+ #endif
+
+ if(!cfg.addresses){
+ LOG("Failed to gather local IP addresses for mDNS announce");
+ return 1;
+ }
+ return 0;
+}
+
+static uint32_t rtpmidi_interval(){
+ return max(0, RTPMIDI_SERVICE_INTERVAL - (mm_timestamp() - cfg.last_service));
+}
+
+static int rtpmidi_configure(char* option, char* value){
+ if(!strcmp(option, "mdns-name")){
+ if(cfg.mdns_name){
+ LOG("Duplicate mdns-name assignment");
+ return 1;
+ }
+
+ return mmbackend_strdup(&cfg.mdns_name, value);
+ }
+ else if(!strcmp(option, "mdns-interface")){
+ if(cfg.mdns_interface){
+ LOG("Duplicate mdns-interface assignment");
+ return 1;
+ }
+
+ return mmbackend_strdup(&cfg.mdns_interface, value);
+ }
+ else if(!strcmp(option, "detect")){
+ cfg.detect = 0;
+ if(!strcmp(value, "on")){
+ cfg.detect = 1;
+ }
+ return 0;
+ }
+
+ LOGPF("Unknown backend configuration option %s", option);
+ return 1;
+}
+
+static int rtpmidi_bind_instance(instance* inst, 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;
+ }
+
+ if(getsockname(data->fd, (struct sockaddr*) &sock_addr, &sock_len)){
+ LOGPF("Failed to fetch data port information: %s", mmbackend_socket_strerror(errno));
+ return 1;
+ }
+
+ //bind control port
+ if(data->mode == apple){
+ data->control_port = be16toh(((struct sockaddr_in*) &sock_addr)->sin_port) - 1;
+ snprintf(control_port, sizeof(control_port), "%d", data->control_port);
+ data->control_fd = mmbackend_socket(host, control_port, SOCK_DGRAM, 1, 0);
+ if(data->control_fd < 0){
+ LOGPF("Failed to bind control port %s for instance %s", control_port, inst->name);
+ return 1;
+ }
+
+ LOGPF("Apple mode instance %s listening on ports %d (control) and %d (data)", inst->name, data->control_port, data->control_port + 1);
+ }
+ else{
+ data->control_port = be16toh(((struct sockaddr_in*)&sock_addr)->sin_port);
+ LOGPF("Direct mode instance %s listening on port %d", inst->name, data->control_port);
+ }
+
+ return 0;
+}
+
+static char* rtpmidi_type_name(uint8_t type){
+ switch(type){
+ case note:
+ return "note";
+ case cc:
+ return "cc";
+ case pressure:
+ return "pressure";
+ case aftertouch:
+ return "aftertouch";
+ case pitchbend:
+ return "pitch";
+ }
+ return "unknown";
+}
+
+static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr* sock_addr, socklen_t sock_len, uint8_t learned, uint8_t connected, ssize_t invite_reference){
+ size_t u, p = data->peers;
+
+ for(u = 0; u < data->peers; u++){
+ //check whether the peer is already in the list
+ //TODO this probably should take into account the invite_reference (-1 for initiator peers or if unknown but may be present)
+ if(data->peer[u].active
+ && sock_len == data->peer[u].dest_len
+ && !memcmp(&data->peer[u].dest, sock_addr, sock_len)){
+ //if yes, update connection flag (but not learned flag because that doesn't change)
+ data->peer[u].connected = connected;
+ return 0;
+ }
+
+ if(!data->peer[u].active){
+ p = u;
+ }
+ }
+
+ if(p == data->peers){
+ data->peer = realloc(data->peer, (data->peers + 1) * sizeof(rtpmidi_peer));
+ if(!data->peer){
+ LOG("Failed to allocate memory");
+ data->peers = 0;
+ return 1;
+ }
+ data->peers++;
+ DBGPF("Extending peer registry to %" PRIsize_t " entries", data->peers);
+ }
+
+ data->peer[p].active = 1;
+ data->peer[p].learned = learned;
+ data->peer[p].connected = connected;
+ data->peer[p].invite = invite_reference;
+ memcpy(&(data->peer[p].dest), sock_addr, sock_len);
+ data->peer[p].dest_len = sock_len;
+ return 0;
+}
+
+static int rtpmidi_push_invite(instance* inst, char* peer){
+ size_t u, p;
+
+ //check whether the instance is already in the inviter list
+ for(u = 0; u < cfg.invites; u++){
+ if(cfg.invite[u].inst == inst){
+ break;
+ }
+ }
+
+ //add to the inviter list
+ if(u == cfg.invites){
+ cfg.invite = realloc(cfg.invite, (cfg.invites + 1) * sizeof(rtpmidi_invite));
+ if(!cfg.invite){
+ LOG("Failed to allocate memory");
+ cfg.invites = 0;
+ return 1;
+ }
+
+ cfg.invite[u].inst = inst;
+ cfg.invite[u].invites = 0;
+ cfg.invite[u].name = NULL;
+
+ cfg.invites++;
+ }
+
+ //check whether the requested name is already in the invite list for this instance
+ for(p = 0; p < cfg.invite[u].invites; p++){
+ if(!strcmp(cfg.invite[u].name[p], peer)){
+ return 0;
+ }
+ }
+
+ //extend the invite list
+ cfg.invite[u].name = realloc(cfg.invite[u].name, (cfg.invite[u].invites + 1) * sizeof(char*));
+ if(!cfg.invite[u].name){
+ LOG("Failed to allocate memory");
+ cfg.invite[u].invites = 0;
+ return 1;
+ }
+
+ //append the new invitee
+ cfg.invite[u].name[p] = strdup(peer);
+ if(!cfg.invite[u].name[p]){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+
+ cfg.invite[u].invites++;
+ return 0;
+}
+
+static ssize_t rtpmidi_applecommand(instance* inst, struct sockaddr* dest, socklen_t dest_len, uint8_t control, applemidi_command command, uint32_t token){
+ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl;
+ uint8_t frame[RTPMIDI_PACKET_BUFFER] = "";
+
+ apple_command* cmd = (apple_command*) &frame;
+ cmd->res1 = 0xFFFF;
+ cmd->command = htobe16(command);
+ cmd->version = htobe32(2);
+ cmd->token = token ? token : (((uint32_t) rand()) << 16 | rand());
+ cmd->ssrc = htobe32(data->ssrc);
+
+ //append session name to packet
+ memcpy(frame + sizeof(apple_command), inst->name, strlen(inst->name) + 1);
+
+ //FIXME should we match sending/receiving ports? if the reference does this, it should be documented
+ return sendto(control ? data->control_fd : data->fd, frame, sizeof(apple_command) + strlen(inst->name) + 1, 0, dest, dest_len);
+}
+
+static ssize_t rtpmidi_peer_applecommand(instance* inst, size_t peer, uint8_t control, applemidi_command command, uint32_t token){
+ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl;
+ struct sockaddr_storage dest_addr;
+
+ memcpy(&dest_addr, &(data->peer[peer].dest), min(sizeof(dest_addr), data->peer[peer].dest_len));
+ if(control){
+ //calculate remote control port from data port
+ ((struct sockaddr_in*) &dest_addr)->sin_port = htobe16(be16toh(((struct sockaddr_in*) &dest_addr)->sin_port) - 1);
+ }
+
+ return rtpmidi_applecommand(inst, (struct sockaddr*) &dest_addr, data->peer[peer].dest_len, control, command, token);
+}
+
+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;
+ struct sockaddr_storage sock_addr;
+ socklen_t sock_len = sizeof(sock_addr);
+
+ if(!strcmp(option, "mode")){
+ if(!strcmp(value, "direct")){
+ data->mode = direct;
+ return 0;
+ }
+ else if(!strcmp(value, "apple")){
+ data->mode = apple;
+ return 0;
+ }
+ LOGPF("Unknown instance mode %s for instance %s", value, inst->name);
+ return 1;
+ }
+ else if(!strcmp(option, "ssrc")){
+ data->ssrc = strtoul(value, NULL, 0);
+ if(!data->ssrc){
+ LOGPF("Random SSRC will be generated for instance %s", inst->name);
+ }
+ return 0;
+ }
+ else if(!strcmp(option, "bind")){
+ if(data->mode == unconfigured){
+ LOGPF("Please specify mode for instance %s before setting bind host", inst->name);
+ return 1;
+ }
+
+ mmbackend_parse_hostspec(value, &host, &port, NULL);
+
+ if(!host){
+ LOGPF("Could not parse bind host specification %s for instance %s", value, inst->name);
+ return 1;
+ }
+
+ return rtpmidi_bind_instance(inst, data, host, port);
+ }
+ else if(!strcmp(option, "learn")){
+ if(data->mode != direct){
+ LOG("'learn' option is only valid for direct mode instances");
+ return 1;
+ }
+ data->learn_peers = 0;
+ if(!strcmp(value, "true")){
+ data->learn_peers = 1;
+ }
+ return 0;
+ }
+ else if(!strcmp(option, "peer")){
+ if(data->mode == unconfigured){
+ LOGPF("Please specify mode for instance %s before configuring peers", inst->name);
+ return 1;
+ }
+
+ mmbackend_parse_hostspec(value, &host, &port, NULL);
+ if(!host || !port){
+ LOGPF("Invalid peer %s configured on instance %s", value, inst->name);
+ return 1;
+ }
+
+ if(mmbackend_parse_sockaddr(host, port, &sock_addr, &sock_len)){
+ LOGPF("Failed to resolve peer %s on instance %s", value, inst->name);
+ return 1;
+ }
+
+ //apple peers are specified using the control port, but we want to store the data port as peer
+ if(data->mode == apple){
+ ((struct sockaddr_in*) &sock_addr)->sin_port = htobe16(be16toh(((struct sockaddr_in*) &sock_addr)->sin_port) + 1);
+ }
+
+ return rtpmidi_push_peer(data, (struct sockaddr*) &sock_addr, sock_len, 0, 0, -1);
+ }
+ else if(!strcmp(option, "invite")){
+ if(data->mode != apple){
+ LOG("'invite' option is only valid for apple mode instances");
+ return 1;
+ }
+
+ return rtpmidi_push_invite(inst, value);
+ }
+ else if(!strcmp(option, "join")){
+ if(data->mode != apple){
+ LOG("'join' option is only valid for apple mode instances");
+ return 1;
+ }
+ return mmbackend_strdup(&data->accept, value);
+ }
+
+ LOGPF("Unknown instance configuration option %s on instance %s", option, inst->name);
+ return 1;
+}
+
+static int rtpmidi_instance(instance* inst){
+ rtpmidi_instance_data* data = calloc(1, sizeof(rtpmidi_instance_data));
+ if(!data){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+ data->fd = -1;
+ data->control_fd = -1;
+
+ inst->impl = data;
+ return 0;
+}
+
+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{
+ LOGPF("Invalid channel specification %s", spec);
+ return NULL;
+ }
+
+ ident.fields.channel = strtoul(next_token, &next_token, 10);
+ if(ident.fields.channel > 15){
+ LOGPF("Channel out of range in channel spec %s", spec);
+ return NULL;
+ }
+
+ if(*next_token != '.'){
+ LOGPF("Channel specification %s does not conform to channel<X>.<control><Y>", 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{
+ LOGPF("Unknown control type in spec %s", 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){
+ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl;
+ uint8_t frame[RTPMIDI_PACKET_BUFFER] = "";
+ rtpmidi_header* rtp_header = (rtpmidi_header*) frame;
+ rtpmidi_command_header* command_header = (rtpmidi_command_header*) (frame + sizeof(rtpmidi_header));
+ size_t offset = sizeof(rtpmidi_header) + sizeof(rtpmidi_command_header), u = 0;
+ uint8_t* payload = frame + offset;
+ rtpmidi_channel_ident ident;
+
+ rtp_header->vpxcc = RTPMIDI_HEADER_MAGIC;
+ //some receivers seem to have problems reading rfcs and interpreting the marker bit correctly
+ rtp_header->mpt = (data->mode == apple ? 0 : 0x80) | RTPMIDI_HEADER_TYPE;
+ rtp_header->sequence = htobe16(data->sequence++);
+ rtp_header->timestamp = mm_timestamp() * 10; //just assume 100msec resolution because rfc4695 handwaves it
+ rtp_header->ssrc = htobe32(data->ssrc);
+
+ //midi command section header
+ //TODO enable the journal bit here
+ command_header->flags = 0xA0; //extended length header, first entry in list has dtime
+
+ //midi list
+ 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 * 16384.0)) & 0x7F;
+ payload[3] = (((int)(v[u].normalised * 16384.0)) >> 7) & 0x7F;
+ }
+ //channel-wide aftertouch is only 2 bytes
+ else if(ident.fields.type == aftertouch){
+ payload[2] = payload[3];
+ payload -= 1;
+ offset -= 1;
+ }
+
+ payload += 4;
+ offset += 4;
+ }
+
+ //update command section length
+ //FIXME this might overrun, might check the number of events at some point
+ command_header->flags |= (((offset - sizeof(rtpmidi_header) - sizeof(rtpmidi_command_header)) & 0x0F00) >> 8);
+ command_header->length = ((offset - sizeof(rtpmidi_header) - sizeof(rtpmidi_command_header)) & 0xFF);
+
+ //TODO journal section
+
+ for(u = 0; u < data->peers; u++){
+ if(data->peer[u].active && data->peer[u].connected){
+ sendto(data->fd, frame, offset, 0, (struct sockaddr*) &data->peer[u].dest, data->peer[u].dest_len);
+ }
+ }
+
+ return 0;
+}
+
+static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size_t bytes, struct sockaddr_storage* peer, socklen_t peer_len){
+ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl;
+ uint8_t response[RTPMIDI_PACKET_BUFFER] = "";
+ apple_command* command = (apple_command*) frame;
+ char* session_name = (char*) frame + sizeof(apple_command);
+ size_t n, u;
+
+ command->command = be16toh(command->command);
+
+ //check command version (except for clock sync and receiver feedback)
+ if(command->command != apple_sync && command->command != apple_feedback
+ && be32toh(command->version) != 2){
+ LOGPF("Invalid AppleMIDI command version %" PRIu32 " on instance %s", be32toh(command->version), inst->name);
+ return 0;
+ }
+
+ if(command->command == apple_invite){
+ //check session name
+ for(n = sizeof(apple_command); n < bytes; n++){
+ if(!frame[n]){
+ break;
+ }
+
+ if(!isprint(frame[n])){
+ session_name = NULL;
+ break;
+ }
+ }
+
+ //unterminated string
+ if(n == bytes){
+ session_name = NULL;
+ }
+
+ //FIXME if already in session, reject the invitation
+ if(data->accept &&
+ (!strcmp(data->accept, "*") || (session_name && !strcmp(session_name, data->accept)))){
+ //accept the invitation
+ LOGPF("Instance %s accepting invitation to session %s%s", inst->name, session_name ? session_name : "UNNAMED", (fd == data->control_fd) ? " (control)":"");
+ //send accept message
+ rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, (fd == data->control_fd) ? 1 : 0, apple_accept, command->token);
+
+ //push peer
+ if(fd != data->control_fd){
+ return rtpmidi_push_peer(data, (struct sockaddr*) peer, peer_len, 1, 1, -1);
+ }
+ return 0;
+ }
+ else{
+ //send reject message
+ LOGPF("Instance %s rejecting invitation to session %s", inst->name, session_name ? session_name : "UNNAMED");
+ rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, (fd == data->control_fd) ? 1 : 0, apple_reject, command->token);
+ }
+ return 0;
+ }
+ else if(command->command == apple_accept){
+ if(fd != data->control_fd){
+ LOGPF("Instance %s negotiated new peer", inst->name);
+ return rtpmidi_push_peer(data, (struct sockaddr*) peer, peer_len, 1, 1, -1);
+ //FIXME store ssrc, start timesync
+ }
+ else{
+ //invite peer data port
+ LOGPF("Instance %s peer accepted on control port, inviting data port", inst->name);
+ //calculate data port
+ ((struct sockaddr_in*) peer)->sin_port = htobe16(be16toh(((struct sockaddr_in*) peer)->sin_port) + 1);
+ //send invite
+ rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, 0, apple_invite, 0);
+ }
+ return 0;
+ }
+ else if(command->command == apple_reject){
+ //just ignore this for now and retry the invitation
+ LOGPF("Invitation rejected on instance %s", inst->name);
+ }
+ else if(command->command == apple_leave){
+ //remove peer from list - this comes in on the control port, but we need to remove the data port...
+ ((struct sockaddr_in*) peer)->sin_port = htobe16(be16toh(((struct sockaddr_in*) peer)->sin_port) + 1);
+ for(u = 0; u < data->peers; u++){
+ if(data->peer[u].dest_len == peer_len
+ && !memcmp(&data->peer[u].dest, peer, peer_len)){
+ LOGPF("Instance %s removed peer", inst->name);
+ //learned peers are marked inactive, configured peers are marked unconnected
+ if(data->peer[u].learned){
+ data->peer[u].active = 0;
+ }
+ else{
+ data->peer[u].connected = 0;
+ }
+ }
+ }
+ return 0;
+ }
+ else if(command->command == apple_sync){
+ //respond with sync answer
+ memcpy(response, frame, bytes);
+ apple_sync_frame* sync = (apple_sync_frame*) response;
+ DBGPF("Incoming sync on instance %s (%d)", inst->name, sync->count);
+ sync->command = htobe16(apple_sync);
+ sync->ssrc = htobe32(data->ssrc);
+ switch(sync->count){
+ case 0:
+ //this happens if we're a participant
+ sync->count++;
+ sync->timestamp[1] = htobe64(mm_timestamp() * 10);
+ break;
+ case 1:
+ //this happens if we're an initiator
+ sync->count++;
+ sync->timestamp[2] = htobe64(mm_timestamp() * 10);
+ break;
+ default:
+ //ignore this one
+ return 0;
+ }
+
+ sendto(fd, response, sizeof(apple_sync_frame), 0, (struct sockaddr*) peer, peer_len);
+ return 0;
+ }
+ else if(command->command == apple_feedback){
+ //TODO store this somewhere to properly update the recovery journal
+ LOGPF("Feedback on instance %s", inst->name);
+ return 0;
+ }
+ else{
+ LOGPF("Unknown AppleMIDI session command %04X", command->command);
+ }
+
+ return 0;
+}
+
+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;
+ uint8_t midi_status = 0;
+ rtpmidi_channel_ident ident;
+ channel_value val;
+ channel* chan = NULL;
+
+ if(!bytes){
+ LOGPF("No command section in data on instance %s", inst->name);
+ return 1;
+ }
+
+ //calculate midi command section length
+ length = frame[0] & 0x0F;
+ if(frame[0] & 0x80){
+ //extended header
+ if(bytes < 2){
+ LOGPF("Short command section (%" PRIsize_t " bytes) on %s, missing extended header", bytes, inst->name);
+ return 1;
+ }
+ length <<= 8;
+ length |= frame[1];
+ offset = 2;
+ }
+
+ command_bytes = offset + length;
+ DBGPF("%u/%" PRIsize_t " bytes of command section on %s, %s header, %s initial dtime",
+ length, bytes, inst->name,
+ (frame[0] & 0x80) ? "extended" : "normal",
+ (frame[0] & 0x20) ? "has" : "no");
+
+ if(command_bytes > bytes){
+ LOGPF("Short command section on %s, indicated %" PRIsize_t ", had %" PRIsize_t, inst->name, command_bytes, bytes);
+ return 1;
+ }
+
+ if(frame[0] & 0x20){
+ decode_time = 1;
+ }
+
+ do{
+ //decode (and ignore) delta-time
+ if(decode_time){
+ for(; offset < command_bytes && frame[offset] & 0x80; offset++){
+ }
+ offset++;
+ }
+
+ //section 3 of rfc6295 states that the first dtime as well as the last command may be omitted
+ //this may make sense on a low-speed serial line, but on a network... come on.
+ if(offset >= command_bytes){
+ break;
+ }
+
+ //check for a status byte
+ //TODO filter sysex
+ if(frame[offset] & 0x80){
+ midi_status = frame[offset];
+ offset++;
+ }
+
+ //having variable encoding in each and every component is super annoying to check for...
+ if(offset >= command_bytes){
+ break;
+ }
+
+ ident.label = 0;
+ ident.fields.type = midi_status & 0xF0;
+ ident.fields.channel = midi_status & 0x0F;
+
+ //single byte command
+ if(ident.fields.type == aftertouch){
+ ident.fields.control = 0;
+ val.normalised = (double) frame[offset] / 127.0;
+ offset++;
+ }
+ //two-byte command
+ else{
+ offset++;
+ if(offset >= command_bytes){
+ break;
+ }
+
+ if(ident.fields.type == pitchbend){
+ ident.fields.control = 0;
+ val.normalised = (double)((frame[offset] << 7) | frame[offset - 1]) / 16384.0;
+ }
+ else{
+ ident.fields.control = frame[offset - 1];
+ val.normalised = (double) frame[offset] / 127.0;
+ }
+
+ //fix-up note off events
+ if(ident.fields.type == 0x80){
+ ident.fields.type = note;
+ val.normalised = 0;
+ }
+
+ offset++;
+ }
+
+ DBGPF("Decoded command type %02X channel %d control %d value %f",
+ ident.fields.type, ident.fields.channel, ident.fields.control, val.normalised);
+
+ if(cfg.detect){
+ if(ident.fields.type == pitchbend || ident.fields.type == aftertouch){
+ LOGPF("Incoming data on channel %s.ch%d.%s, value %f",
+ inst->name, ident.fields.channel,
+ rtpmidi_type_name(ident.fields.type), val.normalised);
+ }
+ else{
+ LOGPF("Incoming data on channel %s.ch%d.%s%d, value %f",
+ inst->name, ident.fields.channel,
+ rtpmidi_type_name(ident.fields.type),
+ ident.fields.control, val.normalised);
+ }
+ }
+
+ //push event
+ chan = mm_channel(inst, ident.label, 0);
+ if(chan){
+ mm_channel_event(chan, val);
+ }
+
+ decode_time = 1;
+ } while(offset < command_bytes);
+
+ return 0;
+}
+
+static int rtpmidi_handle_data(instance* inst){
+ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl;
+ uint8_t frame[RTPMIDI_PACKET_BUFFER] = "";
+ struct sockaddr_storage sock_addr;
+ socklen_t sock_len = sizeof(sock_addr);
+ rtpmidi_header* rtp_header = (rtpmidi_header*) frame;
+ ssize_t bytes_recv = recvfrom(data->fd, frame, sizeof(frame), 0, (struct sockaddr*) &sock_addr, &sock_len);
+ size_t u;
+
+ //TODO receive until EAGAIN
+ if(bytes_recv < 0){
+ LOGPF("Failed to receive for instance %s", inst->name);
+ return 1;
+ }
+
+ if(bytes_recv < sizeof(rtpmidi_header)){
+ LOGPF("Skipping short packet on instance %s", inst->name);
+ return 0;
+ }
+
+ //FIXME might want to filter data input from sources that are not registered peers
+ if(data->mode == apple && rtp_header->vpxcc == 0xFF && rtp_header->mpt == 0xFF){
+ return rtpmidi_handle_applemidi(inst, data->fd, frame, bytes_recv, &sock_addr, sock_len);
+ }
+ else if(rtp_header->vpxcc != RTPMIDI_HEADER_MAGIC || RTPMIDI_GET_TYPE(rtp_header->mpt) != RTPMIDI_HEADER_TYPE){
+ LOGPF("Frame with invalid header magic on %s", inst->name);
+ return 0;
+ }
+
+ //parse data
+ if(rtpmidi_parse(inst, frame + sizeof(rtpmidi_header), bytes_recv - sizeof(rtpmidi_header))){
+ //returning errors here fails the core loop, so just return 0 to have some logging
+ return 0;
+ }
+
+ //try to learn peers
+ if(data->learn_peers){
+ for(u = 0; u < data->peers; u++){
+ if(data->peer[u].active
+ && data->peer[u].dest_len == sock_len
+ && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){
+ break;
+ }
+ }
+
+ if(u == data->peers){
+ LOGPF("Learned new peer on %s", inst->name);
+ return rtpmidi_push_peer(data, (struct sockaddr*) &sock_addr, sock_len, 1, 1, -1);
+ }
+ }
+ return 0;
+}
+
+static int rtpmidi_handle_control(instance* inst){
+ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl;
+ uint8_t frame[RTPMIDI_PACKET_BUFFER] = "";
+ struct sockaddr_storage sock_addr;
+ socklen_t sock_len = sizeof(sock_addr);
+ ssize_t bytes_recv = recvfrom(data->control_fd, frame, sizeof(frame), 0, (struct sockaddr*) &sock_addr, &sock_len);
+
+ if(bytes_recv < 0){
+ LOGPF("Failed to receive on control socket for instance %s", inst->name);
+ return 1;
+ }
+
+ //the shortest applemidi packet is still larger than the rtpmidi header, so use that as bar
+ if(bytes_recv < sizeof(rtpmidi_header)){
+ LOGPF("Skipping short packet on control socket of instance %s", inst->name);
+ return 0;
+ }
+
+ if(data->mode == apple && frame[0] == 0xFF && frame[1] == 0xFF){
+ return rtpmidi_handle_applemidi(inst, data->control_fd, frame, bytes_recv, &sock_addr, sock_len);
+ }
+
+ LOGPF("Unknown session protocol frame received on instance %s", inst->name);
+ return 0;
+}
+
+static int rtpmidi_mdns_broadcast(uint8_t* frame, size_t len){
+ struct sockaddr_in mcast = {
+ .sin_family = AF_INET,
+ .sin_port = htobe16(5353),
+ .sin_addr.s_addr = htobe32(((uint32_t) 0xe00000fb))
+ };
+ struct sockaddr_in6 mcast6 = {
+ .sin6_family = AF_INET6,
+ .sin6_port = htobe16(5353),
+ .sin6_addr.s6_addr = {0xff, 0x02, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xfb}
+ };
+
+ //send to ipv4 and ipv6 mcasts
+ sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast6, sizeof(mcast6));
+ sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast, sizeof(mcast));
+ return 0;
+}
+
+static int rtpmidi_mdns_detach(instance* inst){
+ uint8_t frame[RTPMIDI_PACKET_BUFFER] = "";
+ dns_header* hdr = (dns_header*) frame;
+ dns_rr* rr = NULL;
+ dns_name name = {
+ .alloc = 0
+ };
+ size_t offset = 0;
+ ssize_t bytes = 0;
+
+ hdr->id = 0;
+ hdr->flags[0] = 0x84;
+ hdr->flags[1] = 0;
+ hdr->questions = hdr->servers = hdr->additional = 0;
+ hdr->answers = htobe16(1);
+ offset = sizeof(dns_header);
+
+ //answer 1: _apple-midi PTR FQDN
+ snprintf((char*) frame + offset, sizeof(frame) - offset, "%s", RTPMIDI_MDNS_DOMAIN);
+ bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, 12, 1, 0, 0);
+ if(bytes < 0){
+ goto bail;
+ }
+ offset += bytes;
+
+ //TODO length-checks here
+ frame[offset++] = strlen(inst->name);
+ memcpy(frame + offset, inst->name, strlen(inst->name));
+ offset += strlen(inst->name);
+ frame[offset++] = 0xC0;
+ frame[offset++] = sizeof(dns_header);
+ rr->data = htobe16(1 + strlen(inst->name) + 2);
+
+ free(name.name);
+ return rtpmidi_mdns_broadcast(frame, offset);
+bail:
+ free(name.name);
+ return 1;
+}
+
+//FIXME this should not exceed 1500 bytes
+static int rtpmidi_mdns_announce(instance* inst){
+ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl;
+ uint8_t frame[RTPMIDI_PACKET_BUFFER] = "";
+ dns_header* hdr = (dns_header*) frame;
+ dns_rr* rr = NULL;
+ dns_rr_srv* srv = NULL;
+ dns_name name = {
+ .alloc = 0
+ };
+ size_t offset = 0, host_offset = 0, u = 0;
+ ssize_t bytes = 0;
+
+ hdr->id = 0;
+ hdr->flags[0] = 0x84;
+ hdr->flags[1] = 0;
+ hdr->questions = hdr->servers = 0;
+ hdr->answers = htobe16(4);
+ hdr->additional = htobe16(cfg.addresses);
+ offset = sizeof(dns_header);
+
+ //answer 1: SRV FQDN
+ snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.%s", inst->name, RTPMIDI_MDNS_DOMAIN);
+ bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, 33, 1, 120, 0);
+ if(bytes < 0){
+ goto bail;
+ }
+ offset += bytes;
+
+ srv = (dns_rr_srv*) (frame + offset);
+ srv->priority = 0;
+ srv->weight = 0;
+ srv->port = htobe16(data->control_port);
+ offset += sizeof(dns_rr_srv);
+
+ //rfc2782 (srv) says to not compress `target`, rfc6762 (mdns) 18.14 says to
+ //we don't do it because i don't want to
+ snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.local", cfg.mdns_name);
+ if(dns_encode_name((char*) frame + offset, &name)){
+ LOGPF("Failed to encode name for %s", frame + offset);
+ goto bail;
+ }
+ memcpy(frame + offset, name.name, name.length);
+ offset += name.length;
+ rr->data = htobe16(sizeof(dns_rr_srv) + name.length);
+
+ //answer 2: empty TXT (apple asks for it otherwise)
+ frame[offset++] = 0xC0;
+ frame[offset++] = sizeof(dns_header);
+
+ bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, NULL, 16, 1, 4500, 1);
+ if(bytes < 0){
+ goto bail;
+ }
+ offset += bytes;
+ frame[offset++] = 0x00; //zero-length TXT
+
+ //answer 3: dns-sd PTR _applemidi
+ snprintf((char*) frame + offset, sizeof(frame) - offset, "%s", RTPMIDI_DNSSD_DOMAIN);
+ bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, 12, 1, 4500, 2);
+ if(bytes < 0){
+ goto bail;
+ }
+ offset += bytes;
+
+ //add backref for PTR
+ frame[offset++] = 0xC0;
+ frame[offset++] = sizeof(dns_header) + frame[sizeof(dns_header)] + 1;
+
+ //answer 4: _applemidi PTR FQDN
+ frame[offset++] = 0xC0;
+ frame[offset++] = sizeof(dns_header) + frame[sizeof(dns_header)] + 1;
+
+ bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, NULL, 12, 1, 4500, 2);
+ if(bytes < 0){
+ goto bail;
+ }
+ offset += bytes;
+
+ //add backref for PTR
+ frame[offset++] = 0xC0;
+ frame[offset++] = sizeof(dns_header);
+
+ //additional 1: first announce addr
+ host_offset = offset;
+ snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.local", cfg.mdns_name);
+ bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset,
+ (cfg.address[0].family == AF_INET) ? 1 : 28, 1, 120,
+ (cfg.address[0].family == AF_INET) ? 4 : 16);
+ if(bytes < 0){
+ return 1;
+ }
+ offset += bytes;
+
+ memcpy(frame + offset, cfg.address[0].addr, (cfg.address[0].family == AF_INET) ? 4 : 16);
+ offset += (cfg.address[0].family == AF_INET) ? 4 : 16;
+
+ //push all other announce addresses with a pointer
+ for(u = 1; u < cfg.addresses; u++){
+ frame[offset++] = 0xC0 | (host_offset >> 8);
+ frame[offset++] = host_offset & 0xFF;
+ bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset,
+ (cfg.address[u].family == AF_INET) ? 1 : 28, 1, 120,
+ (cfg.address[u].family == AF_INET) ? 4 : 16);
+ if(bytes < 0){
+ return 1;
+ }
+ offset += bytes;
+
+ memcpy(frame + offset, cfg.address[u].addr, (cfg.address[u].family == AF_INET) ? 4 : 16);
+ offset += (cfg.address[u].family == AF_INET) ? 4 : 16;
+ }
+
+ data->last_announce = mm_timestamp();
+ free(name.name);
+ return rtpmidi_mdns_broadcast(frame, offset);
+bail:
+ free(name.name);
+ return 1;
+}
+
+static int rtpmidi_service(){
+ size_t n, u, p;
+ instance** inst = NULL;
+ rtpmidi_instance_data* data = NULL;
+ struct sockaddr_storage control_peer;
+
+ //prepare commands
+ apple_sync_frame sync = {
+ .res1 = 0xFFFF,
+ .command = htobe16(apple_sync),
+ .ssrc = 0,
+ .count = 0,
+ .timestamp = {
+ mm_timestamp() * 10
+ }
+ };
+
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ LOG("Failed to fetch instances");
+ return 1;
+ }
+
+ for(u = 0; u < n; u++){
+ data = (rtpmidi_instance_data*) inst[u]->impl;
+
+ if(data->mode == apple){
+ //mdns discovery
+ if(cfg.mdns_fd >= 0
+ && (!data->last_announce || mm_timestamp() - data->last_announce > RTPMIDI_ANNOUNCE_INTERVAL)){
+ rtpmidi_mdns_announce(inst[u]);
+ }
+
+ for(p = 0; p < data->peers; p++){
+ if(data->peer[p].active && data->peer[p].connected){
+ //apple sync
+ DBGPF("Instance %s initializing sync on peer %" PRIsize_t, inst[u]->name, p);
+ sync.ssrc = htobe32(data->ssrc);
+ //calculate remote control port from data port
+ memcpy(&control_peer, &(data->peer[u].dest), sizeof(control_peer));
+ ((struct sockaddr_in*) &control_peer)->sin_port = htobe16(be16toh(((struct sockaddr_in*) &control_peer)->sin_port) - 1);
+
+ sendto(data->control_fd, (char*) &sync, sizeof(apple_sync_frame), 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len);
+ }
+ else if(data->peer[p].active && !data->peer[p].learned && (mm_timestamp() / 1000) % 10 == 0){
+ //try to invite pre-defined unconnected applemidi peers
+ DBGPF("Instance %s inviting configured peer %" PRIsize_t, inst[u]->name, p);
+ rtpmidi_peer_applecommand(inst[u], p, 1, apple_invite, 0);
+ }
+ }
+ }
+ }
+
+ free(inst);
+ return 0;
+}
+
+static int rtpmidi_apple_peermatch(uint8_t* session_raw, struct sockaddr* peer, socklen_t peer_len, uint16_t control_port){
+ //due to mdns restrictions, session names can at most be 255 characters long
+ char session_name[1024] = "";
+ rtpmidi_instance_data* data = NULL;
+ size_t u, n, p;
+ uint8_t done = 0;
+
+ //modify peer to match the data port for the indicated control port
+ ((struct sockaddr_in*) peer)->sin_port = htobe16(control_port + 1);
+ snprintf(session_name, sizeof(session_name), "%.*s", session_raw[0], session_raw + 1);
+
+ //find instances that invite exactly this peer
+ for(u = 0; u < cfg.invites; u++){
+ for(n = 0; n < cfg.invite[u].invites; n++){
+ if(strlen(cfg.invite[u].name[n]) == session_raw[0]
+ && !strcmp(cfg.invite[u].name[n], session_name)){
+ done = 1;
+ data = (rtpmidi_instance_data*) cfg.invite[u].inst->impl;
+ DBGPF("Peer %s explicitly invited on instance %s", session_name, cfg.invite[u].inst->name);
+
+ //check whether this peer (or its equivalent on another protocol) is already connected
+ for(p = 0; p < data->peers; p++){
+ //FIXME might want to scan for explicitly configured peers that match the announced peer
+ if(data->peer[p].active
+ && data->peer[p].learned
+ && data->peer[p].invite == n){
+ //we already learned of this peer
+ break;
+ }
+ }
+
+ if(p == data->peers){
+ //push a new peer
+ if(rtpmidi_push_peer(data, peer, peer_len, 1, 0, n)){
+ return 1;
+ }
+ //find it again
+ for(p = 0; p < data->peers; p++){
+ if(data->peer[p].active
+ && data->peer[p].learned
+ && data->peer[p].invite == n){
+ //we already learned of this peer
+ break;
+ }
+ }
+ }
+ else{
+ //if connected, we're done for this instance
+ //if not, at least the family should match
+ if(data->peer[p].connected
+ || data->peer[p].dest.ss_family != peer->sa_family){
+ break;
+ }
+
+ //if not connected and family matches, overwrite
+ memcpy(&(data->peer[p].dest), peer, data->peer[p].dest_len);
+ }
+
+ //connect either the pushed or overwritten peer
+ LOGPF("Inviting peer %s to instance %s", session_name, cfg.invite[u].inst->name);
+ rtpmidi_peer_applecommand(cfg.invite[u].inst, p, 1, apple_invite, 0);
+ }
+ }
+ }
+
+ //if we found at least one match before, we don't check wildcard invites
+ if(done){
+ return 0;
+ }
+
+ //find instances with a wildcard invite
+ for(u = 0; u < cfg.invites; u++){
+ for(n = 0; n < cfg.invite[u].invites; n++){
+ if(!strcmp(cfg.invite[u].name[n], "*")){
+ done = 1;
+ DBGPF("Peer %.*s implicitly invited on instance %s, converting to explicit invitation", session_name[0], session_name + 1, cfg.invite[u].inst->name);
+ if(rtpmidi_push_invite(cfg.invite[u].inst, session_name)){
+ return 1;
+ }
+ }
+ }
+ }
+
+ //recurse to connect now-explicit invitations
+ if(done){
+ rtpmidi_apple_peermatch(session_raw, peer, peer_len, control_port);
+ }
+ return 0;
+}
+
+//TODO bounds check all accesses
+static int rtpmidi_parse_announce(uint8_t* buffer, size_t length, dns_header* hdr, dns_name* name, dns_name* host, struct sockaddr* source, socklen_t source_len){
+ dns_rr* rr = NULL;
+ dns_rr_srv* srv = NULL;
+ size_t u = 0, offset = sizeof(dns_header);
+ uint8_t* session_name = NULL;
+ char peer_name[1024];
+
+ for(u = 0; u < hdr->questions; u++){
+ if(dns_decode_name(buffer, length, offset, name)){
+ LOG("Failed to decode DNS label");
+ return 1;
+ }
+ offset += name->length;
+ offset += sizeof(dns_question);
+ }
+
+ //look for a SRV answer for ._apple-midi._udp.local.
+ for(u = 0; u < hdr->answers; u++){
+ if(dns_decode_name(buffer, length, offset, name)){
+ LOG("Failed to decode DNS label");
+ return 1;
+ }
+
+ //store a pointer to the first label in the current path
+ //since we decoded the name successfully before and dns_decode_name performs bounds checking, this _should_ be ok
+ session_name = (DNS_POINTER(buffer[offset])) ? buffer + (DNS_LABEL_LENGTH(buffer[offset]) << 8 | buffer[offset + 1]) : buffer + offset;
+
+ offset += name->length;
+ rr = (dns_rr*) (buffer + offset);
+ offset += sizeof(dns_rr);
+
+ if(be16toh(rr->rtype) == 33
+ && strlen(name->name) > strlen(RTPMIDI_MDNS_DOMAIN)
+ && !strcmp(name->name + (strlen(name->name) - strlen(RTPMIDI_MDNS_DOMAIN)), RTPMIDI_MDNS_DOMAIN)){
+ //decode the srv data
+ srv = (dns_rr_srv*) (buffer + offset);
+ offset += sizeof(dns_rr_srv);
+
+ if(dns_decode_name(buffer, length, offset, host)){
+ LOG("Failed to decode SRV target");
+ return 1;
+ }
+
+ if(!strncmp(host->name, cfg.mdns_name, strlen(cfg.mdns_name)) && host->name[strlen(cfg.mdns_name)] == '.'){
+ //ignore loopback packets, we don't care about them
+ return 0;
+ }
+
+ //we just use the packet's source as peer, because who would announce mdns for another host (also implementing an additional registry for this would bloat this backend further)
+ LOGPF("Detected possible peer %.*s on %s (%s) Port %d", session_name[0], session_name + 1, host->name, mmbackend_sockaddr_ntop(source, peer_name, sizeof(peer_name)), be16toh(srv->port));
+ offset -= sizeof(dns_rr_srv);
+
+ rtpmidi_apple_peermatch(session_name, source, source_len, be16toh(srv->port));
+ }
+
+ offset += be16toh(rr->data);
+ }
+
+
+ return 0;
+}
+
+static int rtpmidi_handle_mdns(){
+ uint8_t buffer[RTPMIDI_PACKET_BUFFER];
+ dns_header* hdr = (dns_header*) buffer;
+ dns_name name = {
+ .alloc = 0
+ }, host = name;
+ ssize_t bytes = 0;
+ struct sockaddr_storage peer_addr;
+ socklen_t peer_len = sizeof(peer_addr);
+
+ for(bytes = recvfrom(cfg.mdns_fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &peer_addr, &peer_len);
+ bytes > 0;
+ bytes = recvfrom(cfg.mdns_fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &peer_addr, &peer_len)){
+ if(bytes < sizeof(dns_header)){
+ continue;
+ }
+
+ //decode basic header
+ hdr->id = be16toh(hdr->id);
+ hdr->questions = be16toh(hdr->questions);
+ hdr->answers = be16toh(hdr->answers);
+ hdr->servers = be16toh(hdr->servers);
+ hdr->additional = be16toh(hdr->additional);
+
+ //rfc6762 18.3: opcode != 0 -> ignore
+ //rfc6762 18.11: response code != 0 -> ignore
+
+ DBGPF("%" PRIsize_t " bytes, ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", bytes, hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", hdr->questions, hdr->answers, hdr->servers, hdr->additional);
+ rtpmidi_parse_announce(buffer, bytes, hdr, &name, &host, (struct sockaddr*) &peer_addr, peer_len);
+
+ peer_len = sizeof(peer_addr);
+ }
+
+ free(name.name);
+ free(host.name);
+ if(bytes <= 0){
+ #ifdef _WIN32
+ if(WSAGetLastError() == WSAEWOULDBLOCK){
+ #else
+ if(errno == EAGAIN){
+ #endif
+ return 0;
+ }
+
+ LOGPF("Error reading from mDNS descriptor: %s", mmbackend_socket_strerror(errno));
+ return 1;
+ }
+
+ return 0;
+}
+
+static int rtpmidi_handle(size_t num, managed_fd* fds){
+ size_t u;
+ int rv = 0;
+ instance* inst = NULL;
+ rtpmidi_instance_data* data = NULL;
+
+ //handle service tasks (mdns, clock sync, peer connections)
+ if(mm_timestamp() - cfg.last_service > RTPMIDI_SERVICE_INTERVAL){
+ //DBGPF("Performing service tasks, delta %" PRIu64, mm_timestamp() - cfg.last_service);
+ if(rtpmidi_service()){
+ return 1;
+ }
+ cfg.last_service = mm_timestamp();
+ }
+
+ for(u = 0; u < num; u++){
+ if(!fds[u].impl){
+ //handle mDNS discovery input
+ rtpmidi_handle_mdns();
+ }
+ else{
+ //handle rtp/control input
+ inst = (instance*) fds[u].impl;
+ data = (rtpmidi_instance_data*) inst->impl;
+ if(fds[u].fd == data->fd){
+ rv |= rtpmidi_handle_data(inst);
+ }
+ else if(fds[u].fd == data->control_fd){
+ rv |= rtpmidi_handle_control(inst);
+ }
+ else{
+ LOG("Signaled for unknown descriptor");
+ }
+ }
+ }
+
+ return rv;
+}
+
+static int rtpmidi_start_mdns(){
+ struct ip_mreq mcast_req = {
+ .imr_multiaddr.s_addr = htobe32(((uint32_t) 0xe00000fb)),
+ .imr_interface.s_addr = INADDR_ANY
+ };
+
+ struct ipv6_mreq mcast6_req = {
+ .ipv6mr_multiaddr.s6_addr = {0xff, 0x02, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xfb},
+ .ipv6mr_interface = 0
+ };
+
+ if(!cfg.mdns_name){
+ LOG("No mDNS name set, disabling AppleMIDI discovery");
+ return 0;
+ }
+
+ //FIXME might try passing NULL as host here to work around possible windows ipv6 handicaps
+ cfg.mdns_fd = mmbackend_socket(RTPMIDI_DEFAULT_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1);
+ if(cfg.mdns_fd < 0){
+ LOG("Failed to create requested mDNS descriptor");
+ return 1;
+ }
+
+ //join ipv4 multicast group
+ if(setsockopt(cfg.mdns_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){
+ LOGPF("Failed to join IPv4 multicast group for mDNS, discovery may be impaired: %s", mmbackend_socket_strerror(errno));
+ }
+
+ //join ipv6 multicast group
+ if(setsockopt(cfg.mdns_fd, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, (uint8_t*) &mcast6_req, sizeof(mcast6_req))){
+ LOGPF("Failed to join IPv6 multicast group for mDNS, discovery may be impaired: %s", mmbackend_socket_strerror(errno));
+ }
+
+ //register mdns fd to core
+ return mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL);
+}
+
+static int rtpmidi_start(size_t n, instance** inst){
+ size_t u, p, fds = 0;
+ rtpmidi_instance_data* data = NULL;
+ uint8_t mdns_requested = 0;
+
+ 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){
+ LOGPF("Instance %s is missing a mode configuration", inst[u]->name);
+ return 1;
+ }
+
+ //generate random ssrc's
+ if(!data->ssrc){
+ data->ssrc = ((uint32_t) rand()) << 16 | rand();
+ }
+
+ //if not bound, bind to default
+ if(data->fd < 0 && rtpmidi_bind_instance(inst[u], data, RTPMIDI_DEFAULT_HOST, NULL)){
+ LOGPF("Failed to bind default sockets for instance %s", inst[u]->name);
+ return 1;
+ }
+
+ //mark configured peers on direct instances as connected so output is sent
+ //apple mode instances go through the session negotiation before marking peers as active
+ if(data->mode == direct){
+ for(p = 0; p < data->peers; p++){
+ data->peer[p].connected = 1;
+ }
+ }
+ else if(data->mode == apple){
+ mdns_requested = 1;
+ }
+
+ //register fds to core
+ if(mm_manage_fd(data->fd, BACKEND_NAME, 1, inst[u]) || (data->control_fd >= 0 && mm_manage_fd(data->control_fd, BACKEND_NAME, 1, inst[u]))){
+ LOGPF("Failed to register descriptor for instance %s with core", inst[u]->name);
+ return 1;
+ }
+ fds += (data->control_fd >= 0) ? 2 : 1;
+ }
+
+ if(mdns_requested && (rtpmidi_announce_addrs() || rtpmidi_start_mdns())){
+ LOG("Failed to set up mDNS discovery, instances may not show up on remote hosts and may not find remote peers");
+ }
+ else if(mdns_requested){
+ fds++;
+ }
+
+ LOGPF("Registered %" PRIsize_t " descriptors to core", fds);
+ return 0;
+}
+
+static int rtpmidi_shutdown(size_t n, instance** inst){
+ rtpmidi_instance_data* data = NULL;
+ size_t u, p;
+
+ for(u = 0; u < n; u++){
+ data = (rtpmidi_instance_data*) inst[u]->impl;
+
+ if(cfg.mdns_fd >= 0 && data->mode == apple){
+ rtpmidi_mdns_detach(inst[u]);
+ }
+
+ if(data->fd >= 0){
+ close(data->fd);
+ }
+
+ if(data->control_fd >= 0){
+ close(data->control_fd);
+ }
+
+ free(data->accept);
+ data->accept = NULL;
+
+ free(data->peer);
+ data->peer = NULL;
+ data->peers = 0;
+
+ free(inst[u]->impl);
+ inst[u]->impl = NULL;
+ }
+
+ for(u = 0; u < cfg.invites; u++){
+ for(p = 0; p < cfg.invite[u].invites; p++){
+ free(cfg.invite[u].name[p]);
+ }
+ free(cfg.invite[u].name);
+ }
+ free(cfg.invite);
+ cfg.invite = NULL;
+ cfg.invites = 0;
+
+ free(cfg.address);
+ cfg.addresses = 0;
+
+ free(cfg.mdns_name);
+ cfg.mdns_name = NULL;
+ free(cfg.mdns_interface);
+ cfg.mdns_interface = NULL;
+ if(cfg.mdns_fd >= 0){
+ close(cfg.mdns_fd);
+ }
+
+ LOG("Backend shut down");
+ return 0;
+}
diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h
new file mode 100644
index 0000000..9d46911
--- /dev/null
+++ b/backends/rtpmidi.h
@@ -0,0 +1,181 @@
+#ifndef _WIN32
+#include <sys/socket.h>
+#endif
+#include "midimonster.h"
+
+MM_PLUGIN_API int init();
+static int rtpmidi_configure(char* option, char* value);
+static int rtpmidi_configure_instance(instance* instance, char* option, char* value);
+static int rtpmidi_instance(instance* inst);
+static channel* rtpmidi_channel(instance* instance, char* spec, uint8_t flags);
+static uint32_t rtpmidi_interval();
+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(size_t n, instance** inst);
+static int rtpmidi_shutdown(size_t n, instance** inst);
+
+#define RTPMIDI_PACKET_BUFFER 8192
+#define RTPMIDI_DEFAULT_HOST "::"
+#define RTPMIDI_MDNS_PORT "5353"
+#define RTPMIDI_HEADER_MAGIC 0x80
+#define RTPMIDI_HEADER_TYPE 0x61
+#define RTPMIDI_GET_TYPE(a) ((a) & 0x7F)
+#define RTPMIDI_DEFAULT_NAME "MIDIMonster"
+#define RTPMIDI_SERVICE_INTERVAL 1000
+#define RTPMIDI_MDNS_DOMAIN "_apple-midi._udp.local."
+#define RTPMIDI_DNSSD_DOMAIN "_services._dns-sd._udp.local."
+#define RTPMIDI_ANNOUNCE_INTERVAL (60 * 1000)
+
+#define DNS_POINTER(a) (((a) & 0xC0) == 0xC0)
+#define DNS_LABEL_LENGTH(a) ((a) & 0x3F)
+#define DNS_OPCODE(a) (((a) & 0x78) >> 3)
+#define DNS_RESPONSE(a) ((a) & 0x80)
+
+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;
+ uint8_t active; //marked for reuse
+ uint8_t learned; //learned / configured peer (learned peers are marked inactive on session shutdown)
+ uint8_t connected; //currently in active session
+ 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*/ {
+ rtpmidi_instance_mode mode;
+
+ int fd;
+ int control_fd;
+ uint16_t control_port; /*convenience member set by rtpmidi_bind_instance*/
+
+ size_t peers;
+ rtpmidi_peer* peer;
+ uint32_t ssrc;
+ uint16_t sequence;
+
+ //apple-midi config
+ char* accept;
+ uint64_t last_announce;
+
+ //direct mode config
+ uint8_t learn_peers;
+} rtpmidi_instance_data;
+
+typedef struct /*rtpmidi_invited_peer*/ {
+ instance* inst;
+ size_t invites;
+ char** name;
+} rtpmidi_invite;
+
+typedef struct /*_rtpmidi_addr*/ {
+ int family;
+ //this is actually a fair bit too big, but whatever
+ uint8_t addr[sizeof(struct sockaddr_storage)];
+} rtpmidi_addr;
+
+typedef enum {
+ apple_invite = 0x494E, //IN
+ apple_accept = 0x4F4B, //OK
+ apple_reject = 0x4E4F, //NO
+ apple_leave = 0x4259, //BY
+ apple_sync = 0x434B, //CK
+ apple_feedback = 0x5253 //RS
+} applemidi_command;
+
+typedef struct /*_dns_name*/ {
+ size_t alloc;
+ char* name;
+ size_t length;
+} dns_name;
+
+#pragma pack(push, 1)
+typedef struct /*_apple_session_command*/ {
+ uint16_t res1;
+ uint16_t command;
+ uint32_t version;
+ uint32_t token;
+ uint32_t ssrc;
+ //char* name
+} apple_command;
+
+typedef struct /*_apple_session_sync*/ {
+ uint16_t res1;
+ uint16_t command;
+ uint32_t ssrc;
+ uint8_t count;
+ uint8_t res2[3];
+ uint64_t timestamp[3];
+} apple_sync_frame;
+
+typedef struct /*_apple_session_feedback*/ {
+ uint16_t res1;
+ uint8_t command[2];
+ uint32_t ssrc;
+ uint32_t sequence;
+} apple_journal_feedback;
+
+typedef struct /*_rtp_midi_header*/ {
+ uint8_t vpxcc;
+ uint8_t mpt;
+ uint16_t sequence;
+ uint32_t timestamp;
+ uint32_t ssrc;
+} rtpmidi_header;
+
+typedef struct /*_rtp_midi_command*/ {
+ uint8_t flags;
+ uint8_t length;
+} rtpmidi_command_header;
+
+typedef struct /*_dns_header*/ {
+ uint16_t id;
+ uint8_t flags[2];
+ uint16_t questions;
+ uint16_t answers;
+ uint16_t servers;
+ uint16_t additional;
+} dns_header;
+
+typedef struct /*_dns_question*/ {
+ uint16_t qtype;
+ uint16_t qclass;
+} dns_question;
+
+typedef struct /*_dns_rr*/ {
+ uint16_t rtype;
+ uint16_t rclass;
+ uint32_t ttl;
+ uint16_t data;
+} dns_rr;
+
+typedef struct /*_dns_rr_srv*/ {
+ uint16_t priority;
+ uint16_t weight;
+ uint16_t port;
+} dns_rr_srv;
+#pragma pack(pop)
diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md
new file mode 100644
index 0000000..a0098b0
--- /dev/null
+++ b/backends/rtpmidi.md
@@ -0,0 +1,93 @@
+### The `rtpmidi` backend
+
+This backend provides read-write access to RTP MIDI streams, which transfer MIDI data
+over the network. Notably, it has native support in Apple devices.
+
+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: The instance will be able to communicate (either as participant
+ or initiator) in an AppleMIDI session, which can optionally be announced via mDNS (better
+ known as "Bonjour" to Apple users).
+
+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-name` | `computer1` | none | mDNS hostname to announce (`<mdns-name>.local`). Apple-mode instances will be announced via mDNS if set. |
+| `mdns-interface` | `wlan0` | none | Limit addresses announced via mDNS to this interface. On Windows, this is prefix-matched against the user-editable "friendly" interface 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`) |
+| `peer` | `10.1.2.3 9001` | none | MIDI session peer, may be specified multiple times. Bypasses session discovery (but still performs session negotiation) |
+
+`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 |
+
+`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). |
+| `invite` | `pad` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). May be specified multiple times. |
+| `join` | `Just Jamming` | none | Session for which to accept invitations (the special value `*` accepts the first invitation seen). |
+
+#### 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
+
+The mDNS and DNS-SD implementations in this backend are extremely terse, to the point of violating 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.
+
+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
+enormous size and scope of the protocols and implementations required to make this work.