aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--backends/rtpmidi.c180
-rw-r--r--backends/rtpmidi.h32
-rw-r--r--backends/rtpmidi.md6
3 files changed, 172 insertions, 46 deletions
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 <string.h>
#include <errno.h>
@@ -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` | `:: <random>` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) |
| `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS |
| `invite` | `pad` | 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