403 lines
12 KiB
C++
403 lines
12 KiB
C++
#pragma once
|
|
|
|
#include "AppleMIDI_Debug.h"
|
|
|
|
// https://developer.apple.com/library/archive/documentation/Audio/Conceptual/MIDINetworkDriverProtocol/MIDI/MIDI.html
|
|
|
|
#include "shim/MIDI.h"
|
|
using namespace MIDI_NAMESPACE;
|
|
|
|
#include "shim/IPAddress.h"
|
|
|
|
#include "AppleMIDI_PlatformBegin.h"
|
|
#include "AppleMIDI_Defs.h"
|
|
#include "AppleMIDI_Settings.h"
|
|
|
|
#include "rtp_Defs.h"
|
|
#include "rtpMIDI_Defs.h"
|
|
#include "rtpMIDI_Clock.h"
|
|
|
|
#include "AppleMIDI_Participant.h"
|
|
|
|
#include "AppleMIDI_Parser.h"
|
|
#include "rtpMIDI_Parser.h"
|
|
|
|
#include "AppleMIDI_Namespace.h"
|
|
|
|
BEGIN_APPLEMIDI_NAMESPACE
|
|
|
|
extern unsigned long now;
|
|
|
|
struct AppleMIDISettings : public MIDI_NAMESPACE::DefaultSettings
|
|
{
|
|
// Packet based protocols prefer the entire message to be parsed
|
|
// as a whole.
|
|
static const bool Use1ByteParsing = false;
|
|
};
|
|
|
|
template <class UdpClass, class _Settings = DefaultSettings, class _Platform = DefaultPlatform>
|
|
class AppleMIDISession
|
|
{
|
|
typedef _Settings Settings;
|
|
typedef _Platform Platform;
|
|
|
|
// Allow these internal classes access to our private members
|
|
// to avoid access by the .ino to internal messages
|
|
friend class AppleMIDIParser<UdpClass, Settings, Platform>;
|
|
friend class rtpMIDIParser<UdpClass, Settings, Platform>;
|
|
|
|
public:
|
|
AppleMIDISession(const char *sessionName, const uint16_t port = DEFAULT_CONTROL_PORT)
|
|
{
|
|
this->port = port;
|
|
#ifdef KEEP_SESSION_NAME
|
|
strncpy(this->localName, sessionName, Settings::MaxSessionNameLen);
|
|
this->localName[Settings::MaxSessionNameLen] = '\0';
|
|
#endif
|
|
|
|
#ifdef ONE_PARTICIPANT
|
|
participant.ssrc = 0;
|
|
#endif
|
|
};
|
|
|
|
virtual ~AppleMIDISession(){};
|
|
|
|
AppleMIDISession &setHandleConnected(void (*fptr)(const ssrc_t &, const char *))
|
|
{
|
|
_connectedCallback = fptr;
|
|
return *this;
|
|
}
|
|
AppleMIDISession &setHandleDisconnected(void (*fptr)(const ssrc_t &))
|
|
{
|
|
_disconnectedCallback = fptr;
|
|
return *this;
|
|
}
|
|
#ifdef USE_EXT_CALLBACKS
|
|
AppleMIDISession &setHandleException(void (*fptr)(const ssrc_t &, const Exception &, const int32_t value))
|
|
{
|
|
_exceptionCallback = fptr;
|
|
return *this;
|
|
}
|
|
AppleMIDISession &setHandleReceivedRtp(void (*fptr)(const ssrc_t &, const Rtp_t &, const int32_t &))
|
|
{
|
|
_receivedRtpCallback = fptr;
|
|
return *this;
|
|
}
|
|
AppleMIDISession &setHandleStartReceivedMidi(void (*fptr)(const ssrc_t &))
|
|
{
|
|
_startReceivedMidiByteCallback = fptr;
|
|
return *this;
|
|
}
|
|
AppleMIDISession &setHandleReceivedMidi(void (*fptr)(const ssrc_t &, byte))
|
|
{
|
|
_receivedMidiByteCallback = fptr;
|
|
return *this;
|
|
}
|
|
AppleMIDISession &setHandleEndReceivedMidi(void (*fptr)(const ssrc_t &))
|
|
{
|
|
_endReceivedMidiByteCallback = fptr;
|
|
return *this;
|
|
}
|
|
AppleMIDISession &setHandleSentRtp(void (*fptr)(const Rtp_t &))
|
|
{
|
|
_sentRtpCallback = fptr;
|
|
return *this;
|
|
}
|
|
AppleMIDISession &setHandleSentRtpMidi(void (*fptr)(const RtpMIDI_t &))
|
|
{
|
|
_sentRtpMidiCallback = fptr;
|
|
return *this;
|
|
}
|
|
#endif
|
|
|
|
#ifdef KEEP_SESSION_NAME
|
|
const char *getName() const
|
|
{
|
|
return this->localName;
|
|
};
|
|
AppleMIDISession &setName(const char *sessionName)
|
|
{
|
|
strncpy(this->localName, sessionName, Settings::MaxSessionNameLen);
|
|
this->localName[Settings::MaxSessionNameLen] = '\0';
|
|
return *this;
|
|
};
|
|
#else
|
|
const char *getName() const
|
|
{
|
|
return nullptr;
|
|
};
|
|
AppleMIDISession &setName(const char *sessionName) { return *this; };
|
|
#endif
|
|
|
|
const uint16_t getPort() const
|
|
{
|
|
return this->port;
|
|
};
|
|
|
|
// call this method *before* calling begin()
|
|
AppleMIDISession & setPort(const uint16_t port)
|
|
{
|
|
this->port = port;
|
|
return *this;
|
|
}
|
|
|
|
const ssrc_t getSynchronizationSource() const { return this->ssrc; };
|
|
|
|
#ifdef APPLEMIDI_INITIATOR
|
|
bool sendInvite(IPAddress ip, uint16_t port = DEFAULT_CONTROL_PORT);
|
|
#endif
|
|
void sendEndSession();
|
|
|
|
public:
|
|
// Override default thruActivated. Must be false for all packet based messages
|
|
static const bool thruActivated = false;
|
|
|
|
#ifdef USE_DIRECTORY
|
|
Deque<IPAddress, Settings::MaxNumberOfComputersInDirectory> directory;
|
|
WhoCanConnectToMe whoCanConnectToMe = Anyone;
|
|
#endif
|
|
|
|
void begin()
|
|
{
|
|
_appleMIDIParser.session = this;
|
|
_rtpMIDIParser.session = this;
|
|
|
|
// analogRead(0) is not available on all platforms. The use of millis()
|
|
// as it preceded by network calls, so timing is variable and usable
|
|
// for the random generator.
|
|
randomSeed(millis());
|
|
|
|
// Each stream is distinguished by a unique SSRC value and has a unique sequence
|
|
// number and RTP timestamp space.
|
|
// this is our SSRC
|
|
//
|
|
// NOTE: Arduino random only goes to INT32_MAX (not UINT32_MAX)
|
|
this->ssrc = random(1, INT32_MAX / 2) * 2;
|
|
|
|
controlPort.begin(port);
|
|
dataPort.begin(port + 1);
|
|
|
|
rtpMidiClock.Init(rtpMidiClock.Now(), MIDI_SAMPLING_RATE_DEFAULT);
|
|
}
|
|
|
|
void end()
|
|
{
|
|
#ifdef ONE_PARTICIPANT
|
|
participant.ssrc = 0;
|
|
#endif
|
|
controlPort.stop();
|
|
dataPort.stop();
|
|
}
|
|
|
|
bool beginTransmission(MIDI_NAMESPACE::MidiType)
|
|
{
|
|
// All MIDI commands queued up in the same cycle (during 1 loop execution)
|
|
// are send in a single MIDI packet
|
|
// (The actual sending happen in the available() method, called at the start of the
|
|
// event loop() method.
|
|
//
|
|
// http://www.rfc-editor.org/rfc/rfc4696.txt
|
|
//
|
|
// 4.1. Queuing and Coding Incoming MIDI Data
|
|
// ...
|
|
// More sophisticated sending algorithms
|
|
// [GRAME] improve efficiency by coding small groups of commands into a
|
|
// single packet, at the expense of increasing the sender queuing
|
|
// latency.
|
|
//
|
|
if (!outMidiBuffer.empty())
|
|
{
|
|
// Check if there is still room for more - like for 3 bytes or so)
|
|
if ((outMidiBuffer.size() + 1 + 3) > outMidiBuffer.max_size())
|
|
writeRtpMidiToAllParticipants();
|
|
else
|
|
outMidiBuffer.push_back(0x00); // zero timestamp
|
|
}
|
|
|
|
// We can't start the writing process here, as we do not know the length
|
|
// of what we are to send (The RtpMidi protocol start with writing the
|
|
// length of the buffer). So we'll copy to a buffer in the 'write' method,
|
|
// and actually serialize for real in the endTransmission method
|
|
#ifndef ONE_PARTICIPANT
|
|
return (dataPort.remoteIP() != (IPAddress)INADDR_NONE && participants.size() > 0);
|
|
#else
|
|
return (dataPort.remoteIP() != (IPAddress)INADDR_NONE && participant.ssrc != 0);
|
|
#endif
|
|
};
|
|
|
|
void write(byte byte)
|
|
{
|
|
// do we still have place in the buffer for 1 more character?
|
|
if ((outMidiBuffer.size()) + 2 > outMidiBuffer.max_size())
|
|
{
|
|
// buffer is almost full, only 1 more character
|
|
if (MIDI_NAMESPACE::MidiType::SystemExclusive == outMidiBuffer.front())
|
|
{
|
|
// Add Sysex at the end of this partial SysEx (in the last availble slot) ...
|
|
outMidiBuffer.push_back(MIDI_NAMESPACE::MidiType::SystemExclusiveStart);
|
|
|
|
writeRtpMidiToAllParticipants();
|
|
// and start again with a fresh continuation of
|
|
// a next SysEx block.
|
|
outMidiBuffer.clear();
|
|
outMidiBuffer.push_back(MIDI_NAMESPACE::MidiType::SystemExclusiveEnd);
|
|
}
|
|
else
|
|
{
|
|
#ifdef USE_EXT_CALLBACKS
|
|
if (nullptr != _exceptionCallback)
|
|
_exceptionCallback(ssrc, BufferFullException, 0);
|
|
#endif
|
|
return;
|
|
}
|
|
}
|
|
|
|
// store in local buffer, as we do *not* know the length of the message prior to sending
|
|
outMidiBuffer.push_back(byte);
|
|
};
|
|
|
|
void endTransmission(){};
|
|
|
|
// first things MIDI.read() calls in this method
|
|
// MIDI-read() must be called at the start of loop()
|
|
unsigned available()
|
|
{
|
|
now = millis();
|
|
|
|
#ifdef APPLEMIDI_INITIATOR
|
|
manageSessionInvites();
|
|
#endif
|
|
|
|
// All MIDI commands queued up in the same cycle (during 1 loop execution)
|
|
// are send in a single MIDI packet
|
|
if (outMidiBuffer.size() > 0)
|
|
writeRtpMidiToAllParticipants();
|
|
// assert(outMidiBuffer.size() == 0); // must be empty
|
|
|
|
if (inMidiBuffer.size() > 0)
|
|
return inMidiBuffer.size();
|
|
|
|
if (readDataPackets()) // from socket into dataBuffer
|
|
parseDataPackets(); // from dataBuffer into inMidiBuffer
|
|
|
|
if (readControlPackets()) // from socket into controlBuffer
|
|
parseControlPackets(); // from controlBuffer to AppleMIDI
|
|
|
|
manageReceiverFeedback();
|
|
manageSynchronization();
|
|
|
|
return inMidiBuffer.size();
|
|
};
|
|
|
|
byte read()
|
|
{
|
|
auto byte = inMidiBuffer.front();
|
|
inMidiBuffer.pop_front();
|
|
|
|
return byte;
|
|
};
|
|
|
|
protected:
|
|
UdpClass controlPort;
|
|
UdpClass dataPort;
|
|
|
|
private:
|
|
RtpBuffer_t controlBuffer;
|
|
RtpBuffer_t dataBuffer;
|
|
|
|
byte packetBuffer[Settings::UdpTxPacketMaxSize];
|
|
|
|
AppleMIDIParser<UdpClass, Settings, Platform> _appleMIDIParser;
|
|
rtpMIDIParser<UdpClass, Settings, Platform> _rtpMIDIParser;
|
|
|
|
connectedCallback _connectedCallback = nullptr;
|
|
disconnectedCallback _disconnectedCallback = nullptr;
|
|
#ifdef USE_EXT_CALLBACKS
|
|
startReceivedMidiByteCallback _startReceivedMidiByteCallback = nullptr;
|
|
receivedMidiByteCallback _receivedMidiByteCallback = nullptr;
|
|
endReceivedMidiByteCallback _endReceivedMidiByteCallback = nullptr;
|
|
receivedRtpCallback _receivedRtpCallback = nullptr;
|
|
sentRtpCallback _sentRtpCallback = nullptr;
|
|
sentRtpMidiCallback _sentRtpMidiCallback = nullptr;
|
|
exceptionCallback _exceptionCallback = nullptr;
|
|
#endif
|
|
// buffer for incoming and outgoing MIDI messages
|
|
MidiBuffer_t inMidiBuffer;
|
|
MidiBuffer_t outMidiBuffer;
|
|
|
|
rtpMidi_Clock rtpMidiClock;
|
|
|
|
ssrc_t ssrc = 0;
|
|
uint16_t port = DEFAULT_CONTROL_PORT;
|
|
#ifdef ONE_PARTICIPANT
|
|
Participant<Settings> participant;
|
|
#else
|
|
Deque<Participant<Settings>, Settings::MaxNumberOfParticipants> participants;
|
|
#endif
|
|
|
|
#ifdef KEEP_SESSION_NAME
|
|
char localName[Settings::MaxSessionNameLen + 1];
|
|
#endif
|
|
|
|
private:
|
|
size_t readControlPackets();
|
|
size_t readDataPackets();
|
|
|
|
void parseControlPackets();
|
|
void parseDataPackets();
|
|
|
|
void ReceivedInvitation(AppleMIDI_Invitation_t &, const amPortType &);
|
|
void ReceivedControlInvitation(AppleMIDI_Invitation_t &);
|
|
void ReceivedDataInvitation(AppleMIDI_Invitation_t &);
|
|
void ReceivedSynchronization(AppleMIDI_Synchronization_t &);
|
|
void ReceivedReceiverFeedback(AppleMIDI_ReceiverFeedback_t &);
|
|
void ReceivedEndSession(AppleMIDI_EndSession_t &);
|
|
void ReceivedBitrateReceiveLimit(AppleMIDI_BitrateReceiveLimit &);
|
|
|
|
void ReceivedInvitationAccepted(AppleMIDI_InvitationAccepted_t &, const amPortType &);
|
|
void ReceivedControlInvitationAccepted(AppleMIDI_InvitationAccepted_t &);
|
|
void ReceivedDataInvitationAccepted(AppleMIDI_InvitationAccepted_t &);
|
|
void ReceivedInvitationRejected(AppleMIDI_InvitationRejected_t &);
|
|
|
|
// rtpMIDI callback from parser
|
|
void ReceivedRtp(const Rtp_t &);
|
|
void StartReceivedMidi();
|
|
void ReceivedMidi(byte data);
|
|
void EndReceivedMidi();
|
|
|
|
// Helpers
|
|
void writeInvitation(UdpClass &, const IPAddress &, const uint16_t &, AppleMIDI_Invitation_t &, const byte *command);
|
|
void writeReceiverFeedback(const IPAddress &, const uint16_t &, AppleMIDI_ReceiverFeedback_t &);
|
|
void writeSynchronization(const IPAddress &, const uint16_t &, AppleMIDI_Synchronization_t &);
|
|
void writeEndSession(const IPAddress &, const uint16_t &, AppleMIDI_EndSession_t &);
|
|
|
|
void sendEndSession(Participant<Settings> *);
|
|
|
|
void writeRtpMidiToAllParticipants();
|
|
void writeRtpMidiBuffer(Participant<Settings> *);
|
|
|
|
void manageReceiverFeedback();
|
|
|
|
void manageSessionInvites();
|
|
void manageSynchronization();
|
|
void manageSynchronizationInitiator();
|
|
void manageSynchronizationInitiatorHeartBeat(Participant<Settings> *);
|
|
void manageSynchronizationInitiatorInvites(size_t);
|
|
|
|
void sendSynchronization(Participant<Settings> *);
|
|
|
|
#ifndef ONE_PARTICIPANT
|
|
Participant<Settings> *getParticipantBySSRC(const ssrc_t &);
|
|
Participant<Settings> *getParticipantByInitiatorToken(const uint32_t &initiatorToken);
|
|
#endif
|
|
#ifdef USE_DIRECTORY
|
|
bool IsComputerInDirectory(IPAddress) const;
|
|
#endif
|
|
};
|
|
|
|
END_APPLEMIDI_NAMESPACE
|
|
|
|
#include "AppleMIDI.hpp"
|
|
|
|
#include "AppleMIDI_PlatformEnd.h"
|