From 474077a1ad13f945f2fd3dc6199e67eeeb86f517 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 24 Dec 2019 00:48:00 +0100 Subject: Implement AppleMIDI responder mode --- backends/rtpmidi.c | 180 +++++++++++++++++++++++++++++++++++++++++++--------- backends/rtpmidi.h | 32 ++++++---- backends/rtpmidi.md | 6 +- 3 files changed, 172 insertions(+), 46 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index e842a84..fc7af26 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1,4 +1,5 @@ #define BACKEND_NAME "rtpmidi" +#define DEBUG #include #include @@ -9,6 +10,9 @@ #include "libmmbackend.h" #include "rtpmidi.h" +//TODO learn peer ssrcs +//TODO participants need to initiate clock sync at some point + static struct /*_rtpmidi_global*/ { int mdns_fd; char* mdns_name; @@ -130,26 +134,35 @@ static int rtpmidi_bind_instance(rtpmidi_instance_data* data, char* host, char* } static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storage sock_addr, socklen_t sock_len){ - size_t u; + size_t u, p = data->peers; for(u = 0; u < data->peers; u++){ //check whether the peer is already in the list - if(sock_len == data->peer[u].dest_len && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ + if(!data->peer[u].inactive + && sock_len == data->peer[u].dest_len + && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ return 0; } - } - data->peer = realloc(data->peer, (data->peers + 1) * sizeof(rtpmidi_peer)); - if(!data->peer){ - LOG("Failed to allocate memory"); - data->peers = 0; - return 1; + if(data->peer[u].inactive){ + p = u; + } } - data->peer[data->peers].dest = sock_addr; - data->peer[data->peers].dest_len = sock_len; + 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->peers++; + data->peer[p].inactive = 0; + data->peer[p].dest = sock_addr; + data->peer[p].dest_len = sock_len; return 0; } @@ -445,7 +458,9 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v //TODO journal section for(u = 0; u < data->peers; u++){ - sendto(data->fd, frame, offset, 0, (struct sockaddr*) &data->peer[u].dest, data->peer[u].dest_len); + if(!data->peer[u].inactive){ + sendto(data->fd, frame, offset, 0, (struct sockaddr*) &data->peer[u].dest, data->peer[u].dest_len); + } } return 0; @@ -453,37 +468,141 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v 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; - size_t u; + char* session_name = (char*) frame + sizeof(apple_command); + size_t u, n; + + 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; + } //find peer if already in list for(u = 0; u < data->peers; u++){ - if(data->peer[u].dest_len == peer_len + if(!data->peer[u].inactive + && data->peer[u].dest_len == peer_len && !memcmp(&data->peer[u].dest, peer, peer_len)){ break; } } - if(!strncmp((char*) command->command, APPLEMIDI_INVITE, 2)){ - //TODO check whether the session is in the accept list - } - else if(!strncmp((char*) command->command, APPLEMIDI_ACCEPT, 2)){ - //TODO mark peer as in-session, start timesync + 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 + apple_command* accept = (apple_command*) response; + accept->res1 = 0xFFFF; + accept->command = htobe16(apple_accept); + accept->version = htobe32(2); + accept->token = command->token; + accept->ssrc = htobe32(data->ssrc); + //add local name to response + //FIXME might want to use the session name in case it is set + if(cfg.mdns_name){ + memcpy(response + sizeof(apple_command), cfg.mdns_name, strlen(cfg.mdns_name) + 1); + } + else{ + memcpy(response + sizeof(apple_command), RTPMIDI_DEFAULT_NAME, strlen(RTPMIDI_DEFAULT_NAME) + 1); + } + sendto(fd, response, sizeof(apple_command) + strlen(cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME) + 1, 0, (struct sockaddr*) peer, peer_len); + + //push peer + if(fd != data->control_fd){ + return rtpmidi_push_peer(data, *peer, peer_len); + } + return 0; + } + else{ + //send reject message + LOGPF("Instance %s rejecting invitation to session %s", inst->name, session_name ? session_name : "UNNAMED"); + apple_command reject = { + .res1 = 0xFFFF, + .command = htobe16(apple_reject), + .version = htobe32(2), + .token = command->token, + .ssrc = htobe32(data->ssrc) + }; + sendto(fd, (uint8_t*) &reject, sizeof(apple_command), 0, (struct sockaddr*) peer, peer_len); + } + } + else if(command->command == apple_accept){ + if(fd != data->control_fd){ + return rtpmidi_push_peer(data, *peer, peer_len); + //FIXME store ssrc, start timesync + } + else{ + //TODO send invite on data fd + + } } - else if(!strncmp((char*) command->command, APPLEMIDI_REJECT, 2)){ - //TODO mark peer as rejected (or retry invitation) + else if(command->command == apple_reject){ + //just ignore this for now and retry the invitation } - else if(!strncmp((char*) command->command, APPLEMIDI_LEAVE, 2)){ - //TODO mark peer as disconnected, retry invitation + else if(command->command == apple_leave){ + //remove peer from list + if(u != data->peers){ + data->peer[u].inactive = 1; + } } - else if(!strncmp((char*) command->command, APPLEMIDI_SYNC, 2)){ - //TODO respond with sync answer + 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(!strncmp((char*) command->command, APPLEMIDI_FEEDBACK, 2)){ - //ignore + else if(command->command == apple_feedback){ + if(u != data->peers){ + //TODO store this somewhere to properly update the recovery journal + LOGPF("Feedback on instance %s", inst->name); + } } else{ - LOGPF("Unknown AppleMIDI session command %02X %02X", command->command[0], command->command[1]); + LOGPF("Unknown AppleMIDI session command %04X", command->command); } return 0; @@ -523,7 +642,8 @@ static int rtpmidi_handle_data(instance* inst){ //try to learn peers if(data->learn_peers){ for(u = 0; u < data->peers; u++){ - if(data->peer[u].dest_len == sock_len + if(!data->peer[u].inactive + && data->peer[u].dest_len == sock_len && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ break; } @@ -632,7 +752,7 @@ static int rtpmidi_start(size_t n, instance** inst){ //generate random ssrc's if(!data->ssrc){ - data->ssrc = rand() << 16 | rand(); + data->ssrc = ((uint32_t) rand()) << 16 | rand(); } //if not bound, bind to default diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index 2652db7..a9effd8 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -19,13 +19,7 @@ static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_HEADER_MAGIC 0x80 #define RTPMIDI_HEADER_TYPE 0x61 #define RTPMIDI_GET_TYPE(a) ((a) & 0x7F) - -#define APPLEMIDI_INVITE "IN" -#define APPLEMIDI_ACCEPT "OK" -#define APPLEMIDI_REJECT "NO" -#define APPLEMIDI_LEAVE "BY" -#define APPLEMIDI_SYNC "CK" -#define APPLEMIDI_FEEDBACK "RS" +#define RTPMIDI_DEFAULT_NAME "MIDIMonster" enum /*_rtpmidi_channel_type*/ { none = 0, @@ -55,7 +49,8 @@ typedef union { typedef struct /*_rtpmidi_peer*/ { struct sockaddr_storage dest; socklen_t dest_len; - uint32_t ssrc; + //uint32_t ssrc; + uint8_t inactive; } rtpmidi_peer; typedef struct /*_rtmidi_instance_data*/ { @@ -70,8 +65,8 @@ typedef struct /*_rtmidi_instance_data*/ { uint16_t sequence; //apple-midi config - char* session_name; - char* accept; + char* session_name; /* initiator only */ + char* accept; /* participant only */ //direct mode config uint8_t learn_peers; @@ -83,10 +78,19 @@ typedef struct /*rtpmidi_announced_instance*/ { char** invite; } rtpmidi_announce; +enum applemidi_command { + apple_invite = 0x494E, //IN + apple_accept = 0x4F4B, //OK + apple_reject = 0x4E4F, //NO + apple_leave = 0x4259, //BY + apple_sync = 0x434B, //CK + apple_feedback = 0x5253 //RS +}; + #pragma pack(push, 1) typedef struct /*_apple_session_command*/ { uint16_t res1; - uint8_t command[2]; + uint16_t command; uint32_t version; uint32_t token; uint32_t ssrc; @@ -95,19 +99,19 @@ typedef struct /*_apple_session_command*/ { typedef struct /*_apple_session_sync*/ { uint16_t res1; - uint8_t command[2]; + uint16_t command; uint32_t ssrc; uint8_t count; uint8_t res2[3]; uint64_t timestamp[3]; -} apple_sync; +} apple_sync_frame; typedef struct /*_apple_session_feedback*/ { uint16_t res1; uint8_t command[2]; uint32_t ssrc; uint32_t sequence; -} apple_feedback; +} apple_journal_feedback; typedef struct /*_rtp_midi_header*/ { uint8_t vpxcc; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index d42df6f..93811c6 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -13,7 +13,9 @@ selectable per-instance, with some methods requiring additional global configura * 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: +* 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. @@ -50,7 +52,7 @@ Common instance configuration parameters | `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | | `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | | `invite` | `pad` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator. May be specified multiple times | -| `join` | `Just Jamming` | none | Session for which to accept invitations (the special value `*` accepts all invitations). Setting this option makes the instance a session participant | +| `join` | `Just Jamming` | none | Session for which to accept invitations (the special value `*` accepts the first invitation seen). Setting this option makes the instance a session participant | | `peer` | `10.1.2.3 9001` | none | Configure a direct session peer, bypassing AppleMIDI discovery. May be specified multiple times | Note that AppleMIDI session discovery requires mDNS functionality, thus the `mdns-name` global parameter -- cgit v1.2.3