diff options
-rw-r--r-- | TODO | 2 | ||||
-rw-r--r-- | backends/Makefile | 8 | ||||
-rw-r--r-- | backends/libmmbackend.c | 74 | ||||
-rw-r--r-- | backends/libmmbackend.h | 29 | ||||
-rw-r--r-- | backends/rtpmidi.c | 1731 | ||||
-rw-r--r-- | backends/rtpmidi.h | 181 | ||||
-rw-r--r-- | backends/rtpmidi.md | 93 |
7 files changed, 2110 insertions, 8 deletions
@@ -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. |