cs-midi/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI.hpp

1201 lines
43 KiB
C++

#pragma once
#include "AppleMIDI_Namespace.h"
#include <string.h>
#include <algorithm>
BEGIN_APPLEMIDI_NAMESPACE
// Read pending control UDP packets into the control buffer.
template <class UdpClass, class Settings, class Platform>
size_t AppleMIDISession<UdpClass, Settings, Platform>::readControlPackets()
{
size_t packetSize = controlPort.available();
if (packetSize == 0)
packetSize = controlPort.parsePacket();
while (packetSize > 0 && !controlBuffer.full())
{
auto bytesToRead = std::min({packetSize, controlBuffer.free(), sizeof(packetBuffer)});
auto bytesRead = controlPort.read(packetBuffer, bytesToRead);
packetSize -= bytesRead;
controlBuffer.push_back(packetBuffer, bytesRead);
}
return controlBuffer.size();
}
// Parse buffered control packets and handle errors.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::parseControlPackets()
{
while (controlBuffer.size() > 0)
{
auto retVal = _appleMIDIParser.parse(controlBuffer, amPortType::Control);
if (retVal == parserReturn::Processed
|| retVal == parserReturn::NotEnoughData
|| retVal == parserReturn::NotSureGiveMeMoreData)
{
break;
}
else if (retVal == parserReturn::UnexpectedData)
{
#ifdef USE_EXT_CALLBACKS
if (nullptr != _exceptionCallback)
_exceptionCallback(ssrc, ParseException, 0);
#endif
controlBuffer.pop_front();
}
else if (retVal == parserReturn::SessionNameVeryLong)
{
// purge the rest of the data in controlPort
while (controlPort.read() >= 0) {}
}
}
}
// Read pending data UDP packets into the data buffer.
template <class UdpClass, class Settings, class Platform>
size_t AppleMIDISession<UdpClass, Settings, Platform>::readDataPackets()
{
size_t packetSize = dataPort.available();
if (packetSize == 0)
packetSize = dataPort.parsePacket();
while (packetSize > 0 && !dataBuffer.full())
{
auto bytesToRead = std::min({packetSize, dataBuffer.free(), sizeof(packetBuffer)});
auto bytesRead = dataPort.read(packetBuffer, bytesToRead);
packetSize -= bytesRead;
dataBuffer.push_back(packetBuffer, bytesRead);
}
return dataBuffer.size();
}
// Parse buffered data packets using RTP-MIDI and AppleMIDI parsers.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::parseDataPackets()
{
while (dataBuffer.size() > 0)
{
auto retVal1 = _rtpMIDIParser.parse(dataBuffer);
if (retVal1 == parserReturn::Processed
|| retVal1 == parserReturn::NotEnoughData)
break;
auto retVal2 = _appleMIDIParser.parse(dataBuffer, amPortType::Data);
if (retVal2 == parserReturn::Processed
|| retVal2 == parserReturn::NotEnoughData)
break;
// // both don't have data to determine protocol
if (retVal1 == parserReturn::NotSureGiveMeMoreData
&& retVal2 == parserReturn::NotSureGiveMeMoreData)
break;
// one or the other don't have enough data to determine the protocol
if (retVal1 == parserReturn::NotSureGiveMeMoreData
|| retVal2 == parserReturn::NotSureGiveMeMoreData)
break; // one or the other buffer does not have enough data
#ifdef USE_EXT_CALLBACKS
if (nullptr != _exceptionCallback)
_exceptionCallback(ssrc, UnexpectedParseException, 0);
#endif
dataBuffer.pop_front();
}
}
// Route an invitation based on the incoming port type.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::ReceivedInvitation(AppleMIDI_Invitation_t &invitation, const amPortType &portType)
{
if (portType == amPortType::Control)
ReceivedControlInvitation(invitation);
else
ReceivedDataInvitation(invitation);
}
// Handle an incoming control invitation from a remote participant.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::ReceivedControlInvitation(AppleMIDI_Invitation_t &invitation)
{
// ignore invitation of a participant already in the participant list
#ifndef ONE_PARTICIPANT
if (nullptr != getParticipantBySSRC(invitation.ssrc))
#else
if (participant.ssrc == invitation.ssrc)
#endif
return;
#ifndef ONE_PARTICIPANT
if (participants.full())
#else
if (participant.ssrc != 0)
#endif
{
writeInvitation(controlPort, controlPort.remoteIP(), controlPort.remotePort(), invitation, amInvitationRejected);
#ifdef USE_EXT_CALLBACKS
if (nullptr != _exceptionCallback)
_exceptionCallback(ssrc, TooManyParticipantsException, 0);
#endif
return;
}
#ifndef ONE_PARTICIPANT
Participant<Settings> participant;
#endif
participant.kind = Listener;
participant.ssrc = invitation.ssrc;
participant.sendSequenceNr = random(1, UINT16_MAX); // http://www.rfc-editor.org/rfc/rfc6295.txt , 2.1. RTP Header
participant.remoteIP = controlPort.remoteIP();
participant.remotePort = controlPort.remotePort();
participant.lastSyncExchangeTime = now;
#ifdef KEEP_SESSION_NAME
strncpy(participant.sessionName, invitation.sessionName, Settings::MaxSessionNameLen);
participant.sessionName[Settings::MaxSessionNameLen] = '\0';
#endif
#ifdef KEEP_SESSION_NAME
// Re-use the invitation for acceptance. Overwrite sessionName with ours
strncpy(invitation.sessionName, localName, Settings::MaxSessionNameLen);
invitation.sessionName[Settings::MaxSessionNameLen] = '\0';
#endif
#ifdef USE_DIRECTORY
switch (whoCanConnectToMe) {
case None:
writeInvitation(controlPort, controlPort.remoteIP(), controlPort.remotePort(), invitation, amInvitationRejected);
#ifdef USE_EXT_CALLBACKS
if (nullptr != _exceptionCallback)
_exceptionCallback(ssrc, NotAcceptingAnyone, 0);
#endif
return;
case OnlyComputersInMyDirectory:
if (!IsComputerInDirectory(controlPort.remoteIP())) {
writeInvitation(controlPort, controlPort.remoteIP(), controlPort.remotePort(), invitation, amInvitationRejected);
#ifdef USE_EXT_CALLBACKS
if (nullptr != _exceptionCallback)
_exceptionCallback(ssrc, ComputerNotInDirectory, 0);
#endif
return;
}
case Anyone:
break;
}
#endif
#ifndef ONE_PARTICIPANT
participants.push_back(participant);
#endif
writeInvitation(controlPort, participant.remoteIP, participant.remotePort, invitation, amInvitationAccepted);
}
// Handle an incoming data invitation for an existing participant.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::ReceivedDataInvitation(AppleMIDI_Invitation &invitation)
{
#ifndef ONE_PARTICIPANT
auto pParticipant = getParticipantBySSRC(invitation.ssrc);
#else
auto pParticipant = (participant.ssrc == invitation.ssrc) ? &participant : nullptr;
#endif
if (nullptr == pParticipant)
{
writeInvitation(dataPort, dataPort.remoteIP(), dataPort.remotePort(), invitation, amInvitationRejected);
#ifdef USE_EXT_CALLBACKS
if (nullptr != _exceptionCallback)
_exceptionCallback(ssrc, ParticipantNotFoundException, invitation.ssrc);
#endif
return;
}
#ifdef KEEP_SESSION_NAME
// Re-use the invitation for acceptance. Overwrite sessionName with ours
strncpy(invitation.sessionName, localName, Settings::MaxSessionNameLen);
invitation.sessionName[Settings::MaxSessionNameLen] = '\0';
#endif
// writeInvitation will alter the values of the invitation,
// in order to safe memory and computing cycles its easier to make a copy
// of the ssrc here.
auto ssrc_ = invitation.ssrc;
writeInvitation(dataPort, pParticipant->remoteIP, pParticipant->remotePort + 1, invitation, amInvitationAccepted);
pParticipant->kind = Listener;
// Inform that we have an established connection
if (nullptr != _connectedCallback)
{
#ifdef KEEP_SESSION_NAME
_connectedCallback(ssrc_, pParticipant->sessionName);
#else
_connectedCallback(ssrc_, nullptr);
#endif
}
}
// Placeholder for bitrate receive limit messages (not used).
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::ReceivedBitrateReceiveLimit(AppleMIDI_BitrateReceiveLimit &)
{
}
#ifdef APPLEMIDI_INITIATOR
// Route accepted invitations based on the incoming port type.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::ReceivedInvitationAccepted(AppleMIDI_InvitationAccepted_t &invitationAccepted, const amPortType &portType)
{
if (portType == amPortType::Control)
ReceivedControlInvitationAccepted(invitationAccepted);
else
ReceivedDataInvitationAccepted(invitationAccepted);
}
// Update participant state after control invitation acceptance.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::ReceivedControlInvitationAccepted(AppleMIDI_InvitationAccepted_t &invitationAccepted)
{
#ifndef ONE_PARTICIPANT
auto pParticipant = this->getParticipantByInitiatorToken(invitationAccepted.initiatorToken);
#else
auto pParticipant = (participant.initiatorToken == invitationAccepted.initiatorToken) ? &participant : nullptr;
#endif
if (nullptr == pParticipant)
{
return;
}
pParticipant->ssrc = invitationAccepted.ssrc;
pParticipant->lastInviteSentTime = now - 1000; // forces invite to be send
pParticipant->connectionAttempts = 0; // reset back to 0
pParticipant->invitationStatus = ControlInvitationAccepted; // step it up
#ifdef KEEP_SESSION_NAME
strncpy(pParticipant->sessionName, invitationAccepted.sessionName, Settings::MaxSessionNameLen);
pParticipant->sessionName[Settings::MaxSessionNameLen] = '\0';
#endif
}
// Update participant state after data invitation acceptance.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::ReceivedDataInvitationAccepted(AppleMIDI_InvitationAccepted_t &invitationAccepted)
{
#ifndef ONE_PARTICIPANT
auto pParticipant = this->getParticipantByInitiatorToken(invitationAccepted.initiatorToken);
#else
auto pParticipant = (participant.initiatorToken == invitationAccepted.initiatorToken) ? &participant : nullptr;
#endif
if (nullptr == pParticipant)
{
return;
}
pParticipant->invitationStatus = DataInvitationAccepted;
}
// Remove participant on invitation rejection.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::ReceivedInvitationRejected(AppleMIDI_InvitationRejected_t & invitationRejected)
{
for (auto i = 0; i < participants.size(); i++)
{
if (invitationRejected.ssrc == participants[i].ssrc)
{
#ifndef ONE_PARTICIPANT
participants.erase(i);
#else
participant.ssrc = 0;
#endif
return;
}
}
}
#endif
// Handle an incoming synchronization exchange packet.
/*! \brief .
From: http://en.wikipedia.org/wiki/RTP_MIDI
The session initiator sends a first message (named CK0) to the remote partner, giving its local time on
64 bits (Note that this is not an absolute time, but a time related to a local reference, generally given
in microseconds since the startup of operating system kernel). This time is expressed on 10 kHz sampling
clock basis (100 microseconds per increment) The remote partner must answer to this message with a CK1 message,
containing its own local time on 64 bits. Both partners then know the difference between their respective clocks
and can determine the offset to apply to Timestamp and Deltatime fields in RTP-MIDI protocol. The session
initiator finishes this sequence by sending a last message called CK2, containing the local time when it
received the CK1 message. This technique allows to compute the average latency of the network, and also to
compensate a potential delay introduced by a slow starting thread (this situation can occur with non-realtime
operating systems like Linux, Windows or OS X)
Apple recommends to repeat this sequence a few times just after opening the session, in order to get better
synchronization accuracy (in case of one of the sequence has been delayed accidentally because of a temporary
network overload or a latency peak in a thread activation)
This sequence must repeat cyclically (between 2 and 6 times per minute typically), and always by the session
initiator, in order to maintain long term synchronization accuracy by compensation of local clock drift, and also
to detect a loss of communication partner. A partner not answering to multiple CK0 messages shall consider that
the remote partner is disconnected. In most cases, session initiators switch their state machine into "Invitation"
state in order to re-establish communication automatically as soon as the distant partner reconnects to the
network. Some implementations (especially on personal computers) display also an alert message and offer to the
user to choose between a new connection attempt or closing the session.
*/
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::ReceivedSynchronization(AppleMIDI_Synchronization_t &synchronization)
{
#ifndef ONE_PARTICIPANT
auto pParticipant = getParticipantBySSRC(synchronization.ssrc);
#else
auto pParticipant = (participant.ssrc == synchronization.ssrc) ? &participant : nullptr;
#endif
if (nullptr == pParticipant)
{
#ifdef USE_EXT_CALLBACKS
if (nullptr != _exceptionCallback)
_exceptionCallback(ssrc, ParticipantNotFoundException, synchronization.ssrc);
#endif
return;
}
// The session initiator sends a first message (named CK0) to the remote partner, giving its local time in
// 64 bits (Note that this is not an absolute time, but a time related to a local reference,
// generally given in microseconds since the startup of operating system kernel). This time
// is expressed on a 10 kHz sampling clock basis (100 microseconds per increment). The remote
// partner must answer this message with a CK1 message, containing its own local time in 64 bits.
// Both partners then know the difference between their respective clocks and can determine the
// offset to apply to Timestamp and Deltatime fields in the RTP-MIDI protocol.
//
// The session initiator finishes this sequence by sending a last message called CK2,
// containing the local time when it received the CK1 message. This technique makes it
// possible to compute the average latency of the network, and also to compensate for a
// potential delay introduced by a slow starting thread, which can occur with non-realtime
// operating systems like Linux, Windows or OS X.
// -----
// The original initiator initiates clock synchronization after the end of the initial invitation handshake packets.
// A full clock synchronization exchange is as follows:
//
// Initiator sends sync packet with count = 0, current time in timestamp 1
// Responder sends sync packet with count = 1, current time in timestamp 2, timestamp 1 copied from received packet
// Initiator sends sync packet with count = 2, current time in timestamp 3, timestamps 1 and 2 copied from received packet
// At the end of this exchange, each party can estimate the offset between the two clocks using the following formula:
//
// offset_estimate = ((timestamp3 + timestamp1) / 2) - timestamp2
//
// Furthermore, by maintaining a history of synchronization exchanges, each party can calculate a rate at which the clock offset is changing.
//
// The initiator must initiate a new sync exchange at least once every 60 seconds;
// otherwise the responder may assume that the initiator has died and terminate the session.
switch (synchronization.count)
{
case SYNC_CK0: /* From session APPLEMIDI_INITIATOR */
synchronization.timestamps[SYNC_CK1] = rtpMidiClock.Now();
synchronization.count = SYNC_CK1;
writeSynchronization(pParticipant->remoteIP, pParticipant->remotePort + 1, synchronization);
break;
case SYNC_CK1: /* From session LISTENER */
#ifdef APPLEMIDI_INITIATOR
synchronization.timestamps[SYNC_CK2] = rtpMidiClock.Now();
synchronization.count = SYNC_CK2;
writeSynchronization(pParticipant->remoteIP, pParticipant->remotePort + 1, synchronization);
pParticipant->synchronizing = false;
#endif
break;
case SYNC_CK2: /* From session APPLEMIDI_INITIATOR */
#ifdef USE_EXT_CALLBACKS
// each party can estimate the offset between the two clocks using the following formula
pParticipant->offsetEstimate = (uint32_t)(((synchronization.timestamps[2] + synchronization.timestamps[0]) / 2) - synchronization.timestamps[1]);
#endif
break;
}
// All particpants need to check in regularly,
// failing to do so will result in a lost connection.
pParticipant->lastSyncExchangeTime = now;
}
// The recovery journal mechanism requires that the receiver periodically
// inform the sender of the sequence number of the most recently received packet.
// This allows the sender to reduce the size of the recovery journal, to
// encapsulate only those changes to the MIDI stream state occurring after
// the specified packet number.
//
// Process receiver feedback about last received sequence numbers.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::ReceivedReceiverFeedback(AppleMIDI_ReceiverFeedback_t &receiverFeedback)
{
// We do not keep any recovery journals, no command history, nothing!
// Here is where you would correct if packets are dropped (send them again)
#ifndef ONE_PARTICIPANT
auto pParticipant = getParticipantBySSRC(receiverFeedback.ssrc);
#else
auto pParticipant = (participant.ssrc == receiverFeedback.ssrc) ? &participant : nullptr;
#endif
if (nullptr == pParticipant) {
#ifdef USE_EXT_CALLBACKS
if (nullptr != _exceptionCallback)
_exceptionCallback(ssrc, ParticipantNotFoundException, receiverFeedback.ssrc);
#endif
return;
}
if (pParticipant->sendSequenceNr < receiverFeedback.sequenceNr)
{
#ifdef USE_EXT_CALLBACKS
if (nullptr != _exceptionCallback)
_exceptionCallback(pParticipant->ssrc, SendPacketsDropped, pParticipant->sendSequenceNr - receiverFeedback.sequenceNr);
#endif
}
}
// Handle end-session requests and notify callbacks.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::ReceivedEndSession(AppleMIDI_EndSession_t &endSession)
{
#ifndef ONE_PARTICIPANT
for (size_t i = 0; i < participants.size(); i++)
{
auto participant = participants[i];
#else
{
#endif
if (endSession.ssrc == participant.ssrc)
{
auto ssrc = participant.ssrc;
#ifndef ONE_PARTICIPANT
participants.erase(i);
#else
participant.ssrc = 0;
#endif
if (nullptr != _disconnectedCallback)
_disconnectedCallback(ssrc);
return;
}
}
}
#ifdef USE_DIRECTORY
// Check whether a remote IP is in the allowed directory list.
template <class UdpClass, class Settings, class Platform>
bool AppleMIDISession<UdpClass, Settings, Platform>::IsComputerInDirectory(IPAddress remoteIP) const
{
for (size_t i = 0; i < directory.size(); i++)
if (remoteIP == directory[i])
return true;
return false;
}
#endif
#ifndef ONE_PARTICIPANT
// Find a participant by SSRC.
template <class UdpClass, class Settings, class Platform>
Participant<Settings>* AppleMIDISession<UdpClass, Settings, Platform>::getParticipantBySSRC(const ssrc_t& ssrc)
{
for (size_t i = 0; i < participants.size(); i++)
if (ssrc == participants[i].ssrc)
return &participants[i];
return nullptr;
}
// Find a participant by initiator token.
template <class UdpClass, class Settings, class Platform>
Participant<Settings>* AppleMIDISession<UdpClass, Settings, Platform>::getParticipantByInitiatorToken(const uint32_t& initiatorToken)
{
for (auto i = 0; i < participants.size(); i++)
if (initiatorToken == participants[i].initiatorToken)
return &participants[i];
return nullptr;
}
#endif
// Serialize and send an invitation packet on the given port.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::writeInvitation(UdpClass &port, const IPAddress& remoteIP, const uint16_t& remotePort, AppleMIDI_Invitation_t & invitation, const byte *command)
{
if (!port.beginPacket(remoteIP, remotePort))
{
#ifdef USE_EXT_CALLBACKS
if (nullptr != _exceptionCallback)
_exceptionCallback(ssrc, UdpBeginPacketFailed, 1);
#endif
return;
}
port.write((uint8_t *)amSignature, sizeof(amSignature));
port.write((uint8_t *)command, sizeof(amInvitation));
port.write((uint8_t *)amProtocolVersion, sizeof(amProtocolVersion));
invitation.initiatorToken = __htonl(invitation.initiatorToken);
invitation.ssrc = ssrc;
invitation.ssrc = __htonl(invitation.ssrc);
port.write(reinterpret_cast<uint8_t *>(&invitation), invitation.getLength());
port.endPacket();
port.flush();
}
// Send receiver feedback on the control port.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::writeReceiverFeedback(const IPAddress& remoteIP, const uint16_t & remotePort, AppleMIDI_ReceiverFeedback_t & receiverFeedback)
{
if (!controlPort.beginPacket(remoteIP, remotePort))
{
#ifdef USE_EXT_CALLBACKS
if (nullptr != _exceptionCallback)
_exceptionCallback(ssrc, UdpBeginPacketFailed, 2);
#endif
return;
}
controlPort.write((uint8_t *)amSignature, sizeof(amSignature));
controlPort.write((uint8_t *)amReceiverFeedback, sizeof(amReceiverFeedback));
receiverFeedback.ssrc = __htonl(receiverFeedback.ssrc);
receiverFeedback.sequenceNr = __htons(receiverFeedback.sequenceNr);
controlPort.write(reinterpret_cast<uint8_t *>(&receiverFeedback), sizeof(AppleMIDI_ReceiverFeedback));
controlPort.endPacket();
controlPort.flush();
}
// Send a synchronization packet on the data port.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::writeSynchronization(const IPAddress& remoteIP, const uint16_t & remotePort, AppleMIDI_Synchronization_t &synchronization)
{
if (!dataPort.beginPacket(remoteIP, remotePort))
{
#ifdef USE_EXT_CALLBACKS
if (nullptr != _exceptionCallback)
_exceptionCallback(ssrc, UdpBeginPacketFailed, 3);
#endif
return;
}
dataPort.write((uint8_t *)amSignature, sizeof(amSignature));
dataPort.write((uint8_t *)amSynchronization, sizeof(amSynchronization));
synchronization.ssrc = ssrc;
synchronization.ssrc = __htonl(synchronization.ssrc);
synchronization.timestamps[0] = __htonll(synchronization.timestamps[0]);
synchronization.timestamps[1] = __htonll(synchronization.timestamps[1]);
synchronization.timestamps[2] = __htonll(synchronization.timestamps[2]);
dataPort.write(reinterpret_cast<uint8_t *>(&synchronization), sizeof(synchronization));
dataPort.endPacket();
dataPort.flush();
}
// Send an end-session packet on the control port.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::writeEndSession(const IPAddress& remoteIP, const uint16_t & remotePort, AppleMIDI_EndSession_t &endSession)
{
if (!controlPort.beginPacket(remoteIP, remotePort))
{
#ifdef USE_EXT_CALLBACKS
if (nullptr != _exceptionCallback)
_exceptionCallback(ssrc, UdpBeginPacketFailed, 4);
#endif
return;
}
controlPort.write((uint8_t *)amSignature, sizeof(amSignature));
controlPort.write((uint8_t *)amEndSession, sizeof(amEndSession));
controlPort.write((uint8_t *)amProtocolVersion, sizeof(amProtocolVersion));
endSession.initiatorToken = __htonl(endSession.initiatorToken);
endSession.ssrc = __htonl(endSession.ssrc);
controlPort.write(reinterpret_cast<uint8_t *>(&endSession), sizeof(endSession));
controlPort.endPacket();
controlPort.flush();
}
// Flush the outgoing MIDI buffer to all participants.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::writeRtpMidiToAllParticipants()
{
#ifndef ONE_PARTICIPANT
for (size_t i = 0; i < participants.size(); i++)
{
auto pParticipant = &participants[i];
writeRtpMidiBuffer(pParticipant);
}
#else
writeRtpMidiBuffer(&participant);
#endif
outMidiBuffer.clear();
}
// Build and send an RTP-MIDI packet for a participant.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::writeRtpMidiBuffer(Participant<Settings>* participant)
{
const auto bufferLen = outMidiBuffer.size();
Rtp rtp;
// First octet
rtp.vpxcc = ((RTP_VERSION_2) << 6); // RTP version 2
rtp.vpxcc &= ~RTP_P_FIELD; // no padding
rtp.vpxcc &= ~RTP_X_FIELD; // no extension
// No CSRC
// second octet
rtp.mpayload = PAYLOADTYPE_RTPMIDI;
/*
// The behavior of the 1-bit M field depends on the media type of the
// stream. For native streams, the M bit MUST be set to 1 if the MIDI
// command section has a non-zero LEN field and MUST be set to 0
// otherwise. For mpeg4-generic streams, the M bit MUST be set to 1 for
// all packets (to conform to [RFC3640]).
if (bufferLen != 0)
rtp.mpayload |= RTP_M_FIELD;
else
rtp.mpayload &= ~RTP_M_FIELD;
*/
// Both https://developer.apple.com/library/archive/documentation/Audio/Conceptual/MIDINetworkDriverProtocol/MIDI/MIDI.html
// and https://tools.ietf.org/html/rfc6295#section-2.1 indicate that the M field needs to be set
// if the len in the MIDI section is NON-ZERO.
// However, doing so on, MacOS does not take the given MIDI commands
// Clear the M field
rtp.mpayload &= ~RTP_M_FIELD;
rtp.ssrc = ssrc;
// https://developer.apple.com/library/ios/documentation/CoreMidi/Reference/MIDIServices_Reference/#//apple_ref/doc/uid/TP40010316-CHMIDIServiceshFunctions-SW30
// The time at which the events occurred, if receiving MIDI, or, if sending MIDI,
// the time at which the events are to be played. Zero means "now." The time stamp
// applies to the first MIDI byte in the packet.
//
// https://developer.apple.com/library/archive/documentation/Audio/Conceptual/MIDINetworkDriverProtocol/MIDI/MIDI.html
//
// The timestamp is in the same units as described in Timestamp Synchronization
// (units of 100 microseconds since an arbitrary time in the past). The lower 32 bits of this value
// is encoded in the packet. The Apple driver may transmit packets with timestamps in the future.
// Such messages should not be played until the scheduled time. (A future version of the driver may
// have an option to not transmit messages with future timestamps, to accommodate hardware not
// prepared to defer rendering the messages until the proper time.)
//
rtp.timestamp = (Settings::TimestampRtpPackets) ? rtpMidiClock.Now() : 0;
// increment the sequenceNr
participant->sendSequenceNr++;
rtp.sequenceNr = participant->sendSequenceNr;
#ifdef USE_EXT_CALLBACKS
if (_sentRtpCallback)
_sentRtpCallback(rtp);
#endif
rtp.timestamp = __htonl(rtp.timestamp);
rtp.ssrc = __htonl(rtp.ssrc);
rtp.sequenceNr = __htons(rtp.sequenceNr);
if (!dataPort.beginPacket(participant->remoteIP, participant->remotePort + 1))
{
#ifdef USE_EXT_CALLBACKS
if (nullptr != _exceptionCallback)
_exceptionCallback(ssrc, UdpBeginPacketFailed, 5);
#endif
return;
}
// Write RTP + rtpMIDI in a single packet to reduce overhead.
uint8_t packet[sizeof(Rtp) + 2 + Settings::MaxBufferSize];
size_t offset = 0;
memcpy(packet + offset, &rtp, sizeof(rtp));
offset += sizeof(rtp);
RtpMIDI_t rtpMidi;
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |B|J|Z|P|LEN... | MIDI list ... |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
rtpMidi.flags = 0;
rtpMidi.flags &= ~RTP_MIDI_CS_FLAG_J; // no journal, clear J-FLAG
rtpMidi.flags &= ~RTP_MIDI_CS_FLAG_Z; // no Delta Time 0 field, clear Z flag
rtpMidi.flags &= ~RTP_MIDI_CS_FLAG_P; // no phantom flag
if (bufferLen <= 0x0F)
{ // Short header
rtpMidi.flags |= (uint8_t)bufferLen;
rtpMidi.flags &= ~RTP_MIDI_CS_FLAG_B; // short header, clear B-FLAG
packet[offset++] = rtpMidi.flags;
}
else
{ // Long header
rtpMidi.flags |= (uint8_t)(bufferLen >> 8);
rtpMidi.flags |= RTP_MIDI_CS_FLAG_B; // set B-FLAG for long header
packet[offset++] = rtpMidi.flags;
packet[offset++] = (uint8_t)(bufferLen);
}
// write out the MIDI Section
offset += outMidiBuffer.copy_out(packet + offset, bufferLen);
// *No* journal section (Not supported)
dataPort.write(packet, offset);
dataPort.endPacket();
dataPort.flush();
#ifdef USE_EXT_CALLBACKS
if (_sentRtpMidiCallback)
_sentRtpMidiCallback(rtpMidi);
#endif
}
// Manage synchronization state for all active participants.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::manageSynchronization()
{
#ifndef ONE_PARTICIPANT
for (size_t i = 0; i < participants.size();)
#endif
{
#ifndef ONE_PARTICIPANT
auto pParticipant = &participants[i];
if (pParticipant->ssrc == 0)
{
i++;
continue;
}
#else
auto pParticipant = &participant;
if (pParticipant->ssrc == 0) return;
#endif
#ifdef APPLEMIDI_INITIATOR
if (pParticipant->invitationStatus != Connected)
{
#ifndef ONE_PARTICIPANT
i++;
#endif
continue;
}
// Only for Initiators that are Connected
if (pParticipant->kind == Listener)
{
#endif
// The initiator must check in with the listener at least once every 60 seconds;
// otherwise the responder may assume that the initiator has died and terminate the session.
if (now - pParticipant->lastSyncExchangeTime > Settings::CK_MaxTimeOut)
{
#ifdef USE_EXT_CALLBACKS
if (nullptr != _exceptionCallback)
_exceptionCallback(ssrc, ListenerTimeOutException, 0);
#endif
sendEndSession(pParticipant);
#ifndef ONE_PARTICIPANT
participants.erase(i);
continue;
#else
participant.ssrc = 0;
#endif
}
#ifdef APPLEMIDI_INITIATOR
}
else
{
(pParticipant->synchronizing) ? manageSynchronizationInitiatorInvites(i)
: manageSynchronizationInitiatorHeartBeat(pParticipant);
}
#endif
#ifndef ONE_PARTICIPANT
i++;
#endif
}
}
#ifdef APPLEMIDI_INITIATOR
// Initiator heartbeat: schedule periodic synchronization exchanges.
//
// The initiator must initiate a new sync exchange at least once every 60 seconds;
// otherwise the responder may assume that the initiator has died and terminate the session.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::manageSynchronizationInitiatorHeartBeat(Participant<Settings>* pParticipant)
{
// Note: During startup, the initiator should send synchronization exchanges more frequently;
// empirical testing has determined that sending a few exchanges improves clock
// synchronization accuracy.
// (Here: twice every 0.5 seconds, then 6 times every 1.5 seconds, then every 10 seconds.)
bool doSyncronize = false;
if (pParticipant->synchronizationHeartBeats < 2)
{
if (now - pParticipant->lastInviteSentTime > 500) // 2 x every 0.5 seconds
{
pParticipant->synchronizationHeartBeats++;
doSyncronize = true;
}
}
else if (pParticipant->synchronizationHeartBeats < 7)
{
if (now - pParticipant->lastInviteSentTime > 1500) // 5 x every 1.5 seconds
{
pParticipant->synchronizationHeartBeats++;
doSyncronize = true;
}
}
else if (now - pParticipant->lastInviteSentTime > Settings::SynchronizationHeartBeat)
{
doSyncronize = true;
}
if (!doSyncronize)
return;
pParticipant->synchronizationCount = 0;
sendSynchronization(pParticipant);
}
// Retry sync invitations while establishing synchronization.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::manageSynchronizationInitiatorInvites(size_t i)
{
auto pParticipant = &participants[i];
if (now - pParticipant->lastInviteSentTime > 10000)
{
if (pParticipant->synchronizationCount > Settings::MaxSynchronizationCK0Attempts)
{
#ifdef USE_EXT_CALLBACKS
if (nullptr != _exceptionCallback)
_exceptionCallback(ssrc, MaxAttemptsException, 0);
#endif
// After too many attempts, stop.
sendEndSession(pParticipant);
#ifndef ONE_PARTICIPANT
participants.erase(i);
#else
participant.ssrc = 0;
#endif
return;
}
sendSynchronization(pParticipant);
}
}
#endif
// Send a CK0 synchronization message to a participant.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::sendSynchronization(Participant<Settings>* participant)
{
AppleMIDI_Synchronization_t synchronization;
synchronization.timestamps[SYNC_CK0] = rtpMidiClock.Now();
synchronization.timestamps[SYNC_CK1] = 0;
synchronization.timestamps[SYNC_CK2] = 0;
synchronization.count = 0;
writeSynchronization(participant->remoteIP, participant->remotePort + 1, synchronization);
participant->synchronizing = true;
participant->synchronizationCount++;
participant->lastInviteSentTime = now;
}
// Manage invitation retries for session establishment (initiators only).
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::manageSessionInvites()
{
#ifndef ONE_PARTICIPANT
for (auto i = 0; i < participants.size();)
#endif
{
#ifndef ONE_PARTICIPANT
auto pParticipant = &participants[i];
#else
auto pParticipant = &participant;
#endif
if (pParticipant->kind == Listener)
#ifndef ONE_PARTICIPANT
{
i++;
continue;
}
#else
return;
#endif
if (pParticipant->invitationStatus == DataInvitationAccepted)
{
// Inform that we have an established connection
if (nullptr != _connectedCallback)
#ifdef KEEP_SESSION_NAME
_connectedCallback(pParticipant->ssrc, pParticipant->sessionName);
#else
_connectedCallback(pParticipant->ssrc, nullptr);
#endif
pParticipant->invitationStatus = Connected;
}
if (pParticipant->invitationStatus == Connected)
#ifndef ONE_PARTICIPANT
{
i++;
continue;
}
#else
return;
#endif
// try to connect every 1 second (1000 ms)
if (now - pParticipant->lastInviteSentTime > 1000)
{
if (pParticipant->connectionAttempts >= Settings::MaxSessionInvitesAttempts)
{
#ifdef USE_EXT_CALLBACKS
if (nullptr != _exceptionCallback)
_exceptionCallback(ssrc, NoResponseFromConnectionRequestException, 0);
#endif
// After too many attempts, stop.
sendEndSession(pParticipant);
#ifndef ONE_PARTICIPANT
participants.erase(i);
continue;
#else
participant.ssrc = 0;
return;
#endif
}
pParticipant->lastInviteSentTime = now;
pParticipant->connectionAttempts++;
AppleMIDI_Invitation invitation;
invitation.ssrc = this->ssrc;
invitation.initiatorToken = pParticipant->initiatorToken;
#ifdef KEEP_SESSION_NAME
strncpy(invitation.sessionName, this->localName, Settings::MaxSessionNameLen);
invitation.sessionName[Settings::MaxSessionNameLen] = '\0';
#endif
if (pParticipant->invitationStatus == Initiating
|| pParticipant->invitationStatus == AwaitingControlInvitationAccepted)
{
writeInvitation(controlPort, pParticipant->remoteIP, pParticipant->remotePort, invitation, amInvitation);
pParticipant->invitationStatus = AwaitingControlInvitationAccepted;
}
else
if (pParticipant->invitationStatus == ControlInvitationAccepted
|| pParticipant->invitationStatus == AwaitingDataInvitationAccepted)
{
writeInvitation(dataPort, pParticipant->remoteIP, pParticipant->remotePort + 1, invitation, amInvitation);
pParticipant->invitationStatus = AwaitingDataInvitationAccepted;
}
}
#ifndef ONE_PARTICIPANT
i++;
#endif
}
}
// Periodically emit receiver feedback for active participants.
// The recovery journal mechanism requires that the receiver
// periodically inform the sender of the sequence number of the most
// recently received packet. This allows the sender to reduce the size
// of the recovery journal, to encapsulate only those changes to the
// MIDI stream state occurring after the specified packet number.
//
// This message is sent on the control port.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::manageReceiverFeedback()
{
#ifndef ONE_PARTICIPANT
for (uint8_t i = 0; i < participants.size(); i++)
#endif
{
#ifndef ONE_PARTICIPANT
auto pParticipant = &participants[i];
if (pParticipant->ssrc == 0) continue;
#else
auto pParticipant = &participant;
if (pParticipant->ssrc == 0) return;
#endif
if (pParticipant->doReceiverFeedback == false)
#ifndef ONE_PARTICIPANT
continue;
#else
return;
#endif
if ((now - pParticipant->receiverFeedbackStartTime) > Settings::ReceiversFeedbackThreshold)
{
AppleMIDI_ReceiverFeedback_t rf;
rf.ssrc = ssrc;
rf.sequenceNr = pParticipant->receiveSequenceNr;
writeReceiverFeedback(pParticipant->remoteIP, pParticipant->remotePort, rf);
// reset the clock. It is started when we receive MIDI
pParticipant->doReceiverFeedback = false;
}
}
}
#ifdef APPLEMIDI_INITIATOR
// Queue a new outgoing invitation for a remote endpoint.
template <class UdpClass, class Settings, class Platform>
bool AppleMIDISession<UdpClass, Settings, Platform>::sendInvite(IPAddress ip, uint16_t port)
{
#ifndef ONE_PARTICIPANT
if (participants.full())
#else
if (participant.ssrc != 0)
#endif
{
return false;
}
#ifndef ONE_PARTICIPANT
Participant<Settings> participant;
#endif
participant.kind = Initiator;
participant.sendSequenceNr = random(1, UINT16_MAX); // http://www.rfc-editor.org/rfc/rfc6295.txt , 2.1. RTP Header
participant.remoteIP = ip;
participant.remotePort = port;
participant.lastInviteSentTime = now - 1000; // forces invite to be send immediately
participant.lastSyncExchangeTime = now;
participant.initiatorToken = random(1, INT32_MAX) * 2;
#ifndef ONE_PARTICIPANT
participants.push_back(participant);
#endif
return true;
}
#endif
// Send end-session to all participants and clear them.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::sendEndSession()
{
#ifndef ONE_PARTICIPANT
while (participants.size() > 0)
{
auto participant = &participants.front();
sendEndSession(participant);
participants.pop_front();
}
#else
if (participant.ssrc != 0)
{
sendEndSession(&participant);
participant.ssrc = 0;
}
#endif
}
// Send end-session to a single participant and notify callbacks.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::sendEndSession(Participant<Settings>* participant)
{
AppleMIDI_EndSession_t endSession;
endSession.initiatorToken = 0;
endSession.ssrc = this->ssrc;
writeEndSession(participant->remoteIP, participant->remotePort, endSession);
if (nullptr != _disconnectedCallback)
_disconnectedCallback(participant->ssrc);
}
// Handle an incoming RTP header and track latency/sequence.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::ReceivedRtp(const Rtp_t& rtp)
{
#ifndef ONE_PARTICIPANT
auto pParticipant = getParticipantBySSRC(rtp.ssrc);
#else
auto pParticipant = (participant.ssrc == rtp.ssrc) ? &participant : nullptr;
#endif
if (nullptr != pParticipant)
{
if (pParticipant->doReceiverFeedback == false)
pParticipant->receiverFeedbackStartTime = now;
pParticipant->doReceiverFeedback = true;
#ifdef USE_EXT_CALLBACKS
auto offset = (rtp.timestamp - pParticipant->offsetEstimate);
auto latency = (int32_t)(rtpMidiClock.Now() - offset);
if (pParticipant->firstMessageReceived == true)
// avoids first message to generate sequence exception
// as we do not know the last sequenceNr received.
pParticipant->firstMessageReceived = false;
else if (rtp.sequenceNr - pParticipant->receiveSequenceNr - 1 != 0) {
if (nullptr != _exceptionCallback)
_exceptionCallback(ssrc, ReceivedPacketsDropped, rtp.sequenceNr - pParticipant->receiveSequenceNr - 1);
}
if (nullptr != _receivedRtpCallback)
_receivedRtpCallback(pParticipant->ssrc, rtp, latency);
#endif
pParticipant->receiveSequenceNr = rtp.sequenceNr;
}
else
{
// TODO??? re-connect?
}
}
// Notify that a MIDI byte stream has started.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::StartReceivedMidi()
{
#ifdef USE_EXT_CALLBACKS
if (nullptr != _startReceivedMidiByteCallback)
_startReceivedMidiByteCallback(ssrc);
#endif
}
// Handle a received MIDI byte and buffer it.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::ReceivedMidi(byte value)
{
#ifdef USE_EXT_CALLBACKS
if (nullptr != _receivedMidiByteCallback)
_receivedMidiByteCallback(ssrc, value);
#endif
inMidiBuffer.push_back(value);
}
// Notify that a MIDI byte stream has ended.
template <class UdpClass, class Settings, class Platform>
void AppleMIDISession<UdpClass, Settings, Platform>::EndReceivedMidi()
{
#ifdef USE_EXT_CALLBACKS
if (nullptr != _endReceivedMidiByteCallback)
_endReceivedMidiByteCallback(ssrc);
#endif
}
END_APPLEMIDI_NAMESPACE