From 2187943124823e896b67084dbe8b2e87be2165ec Mon Sep 17 00:00:00 2001 From: pszsh Date: Sat, 7 Mar 2026 10:43:00 -0800 Subject: [PATCH] doozy --- CMakeLists.txt | 31 +- MIDI_Interfaces/AppleMIDI/LwIPUDP.cpp | 156 +++ MIDI_Interfaces/AppleMIDI/LwIPUDP.hpp | 59 + .../AppleMIDI/vendor/AppleMIDI.cpp | 7 + MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI.h | 402 ++++++ .../AppleMIDI/vendor/AppleMIDI.hpp | 1200 +++++++++++++++++ .../AppleMIDI/vendor/AppleMIDI_Debug.h | 33 + .../AppleMIDI/vendor/AppleMIDI_Defs.h | 214 +++ .../AppleMIDI/vendor/AppleMIDI_Namespace.h | 9 + .../AppleMIDI/vendor/AppleMIDI_Parser.h | 419 ++++++ .../AppleMIDI/vendor/AppleMIDI_Participant.h | 46 + .../vendor/AppleMIDI_PlatformBegin.h | 20 + .../AppleMIDI/vendor/AppleMIDI_PlatformEnd.h | 12 + .../AppleMIDI/vendor/AppleMIDI_Settings.h | 47 + .../AppleMIDI/vendor/rtpMIDI_Clock.h | 74 + .../AppleMIDI/vendor/rtpMIDI_Defs.h | 141 ++ .../AppleMIDI/vendor/rtpMIDI_Parser.h | 232 ++++ .../vendor/rtpMIDI_Parser_CommandSection.hpp | 279 ++++ .../vendor/rtpMIDI_Parser_JournalSection.hpp | 180 +++ MIDI_Interfaces/AppleMIDI/vendor/rtp_Defs.h | 55 + .../AppleMIDI/vendor/shim/IPAddress.h | 41 + MIDI_Interfaces/AppleMIDI/vendor/shim/MIDI.h | 21 + .../AppleMIDI/vendor/shim/midi_Defs.h | 42 + .../AppleMIDI/vendor/utility/Deque.h | 277 ++++ .../AppleMIDI/vendor/utility/endian.h | 78 ++ MIDI_Interfaces/AppleMIDI_Interface.cpp | 80 ++ MIDI_Interfaces/AppleMIDI_Interface.hpp | 63 + .../BLEMIDI/BTstack/advertising.cpp | 21 +- MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.cpp | 64 +- .../BLEMIDI/BTstack/gatt_midi_hog.gatt | 14 + .../BLEMIDI/BTstack/gatt_midi_hog.h | 147 ++ cs_midi.h | 4 + docs | 2 +- tests/CMakeLists.txt | 7 +- tests/examples/CMakeLists.txt | 16 + tests/examples/interfaces/applemidi.cpp | 29 + tests/examples/interfaces/applemidi_ble.cpp | 34 + tests/lwipopts.h | 6 +- 38 files changed, 4552 insertions(+), 10 deletions(-) create mode 100644 MIDI_Interfaces/AppleMIDI/LwIPUDP.cpp create mode 100644 MIDI_Interfaces/AppleMIDI/LwIPUDP.hpp create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI.cpp create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI.h create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI.hpp create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Debug.h create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Defs.h create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Namespace.h create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Parser.h create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Participant.h create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_PlatformBegin.h create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_PlatformEnd.h create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Settings.h create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Clock.h create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Defs.h create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Parser.h create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Parser_CommandSection.hpp create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Parser_JournalSection.hpp create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/rtp_Defs.h create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/shim/IPAddress.h create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/shim/MIDI.h create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/shim/midi_Defs.h create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/utility/Deque.h create mode 100644 MIDI_Interfaces/AppleMIDI/vendor/utility/endian.h create mode 100644 MIDI_Interfaces/AppleMIDI_Interface.cpp create mode 100644 MIDI_Interfaces/AppleMIDI_Interface.hpp create mode 100644 MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi_hog.gatt create mode 100644 MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi_hog.h create mode 100644 tests/examples/interfaces/applemidi.cpp create mode 100644 tests/examples/interfaces/applemidi_ble.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 07162eb..fa648e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,11 @@ cmake_minimum_required(VERSION 3.13) # Interface selection — consumers set these before add_subdirectory() -option(CS_MIDI_BLE "Enable BLE MIDI interface (BTstack)" ON) -option(CS_MIDI_USB "Enable USB MIDI interface (TinyUSB)" OFF) -option(CS_MIDI_SERIAL "Enable Serial MIDI interface (UART)" OFF) +option(CS_MIDI_BLE "Enable BLE MIDI interface (BTstack)" ON) +option(CS_MIDI_USB "Enable USB MIDI interface (TinyUSB)" OFF) +option(CS_MIDI_SERIAL "Enable Serial MIDI interface (UART)" OFF) +option(CS_MIDI_APPLEMIDI "Enable AppleMIDI interface (RTP-MIDI over WiFi)" OFF) +option(CS_MIDI_HID_MOUSE "Advertise as HID mouse for BLE auto-reconnect" OFF) # Core sources — always compiled set(CS_MIDI_CORE_SOURCES @@ -52,17 +54,33 @@ if(CS_MIDI_SERIAL) ) endif() +if(CS_MIDI_APPLEMIDI) + list(APPEND CS_MIDI_SOURCES + MIDI_Interfaces/AppleMIDI/LwIPUDP.cpp + MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI.cpp + MIDI_Interfaces/AppleMIDI_Interface.cpp + ) +endif() + add_library(cs_midi STATIC ${CS_MIDI_SOURCES}) target_include_directories(cs_midi PUBLIC ${CMAKE_CURRENT_LIST_DIR} ) +if(CS_MIDI_APPLEMIDI) + target_include_directories(cs_midi PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/MIDI_Interfaces/AppleMIDI/vendor + ) +endif() + target_compile_definitions(cs_midi PUBLIC MIDI_NUM_CABLES=1 $<$:CS_MIDI_BLE=1> $<$:CS_MIDI_USB=1> $<$:CS_MIDI_SERIAL=1> + $<$:CS_MIDI_APPLEMIDI=1> + $<$:CS_MIDI_HID_MOUSE=1> ) target_link_libraries(cs_midi @@ -88,4 +106,11 @@ if(CS_MIDI_USB) ) endif() +if(CS_MIDI_APPLEMIDI) + target_link_libraries(cs_midi + PRIVATE pico_cyw43_arch_lwip_threadsafe_background + pico_lwip_mdns + ) +endif() + target_compile_features(cs_midi PUBLIC cxx_std_17) diff --git a/MIDI_Interfaces/AppleMIDI/LwIPUDP.cpp b/MIDI_Interfaces/AppleMIDI/LwIPUDP.cpp new file mode 100644 index 0000000..391eb28 --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/LwIPUDP.cpp @@ -0,0 +1,156 @@ +#include "LwIPUDP.hpp" +#include "pico/cyw43_arch.h" +#include + +LwIPUDP::LwIPUDP() + : pcb_(nullptr), rxHead_(0), rxTail_(0), + current_(nullptr), txPort_(0), txBuf_(nullptr) +{ + memset(rxQueue_, 0, sizeof(rxQueue_)); + ip_addr_set_zero(&txAddr_); +} + +LwIPUDP::~LwIPUDP() { + stop(); +} + +bool LwIPUDP::begin(uint16_t port) { + if (pcb_) stop(); + + cyw43_arch_lwip_begin(); + pcb_ = udp_new(); + if (!pcb_) { + cyw43_arch_lwip_end(); + return false; + } + + if (udp_bind(pcb_, IP_ADDR_ANY, port) != ERR_OK) { + udp_remove(pcb_); + pcb_ = nullptr; + cyw43_arch_lwip_end(); + return false; + } + + udp_recv(pcb_, recvCb, this); + cyw43_arch_lwip_end(); + return true; +} + +void LwIPUDP::stop() { + if (pcb_) { + cyw43_arch_lwip_begin(); + udp_remove(pcb_); + cyw43_arch_lwip_end(); + pcb_ = nullptr; + } + + // Drain rx queue + while (rxTail_ != rxHead_) { + auto &pkt = rxQueue_[rxTail_ % RX_QUEUE_SIZE]; + if (pkt.p) { pbuf_free(pkt.p); pkt.p = nullptr; } + rxTail_++; + } + rxHead_ = rxTail_ = 0; + current_ = nullptr; + + if (txBuf_) { pbuf_free(txBuf_); txBuf_ = nullptr; } +} + +void LwIPUDP::recvCb(void *arg, struct udp_pcb *, struct pbuf *p, + const ip_addr_t *addr, u16_t port) { + auto *self = static_cast(arg); + uint8_t next = (self->rxHead_ + 1) % RX_QUEUE_SIZE; + if (next == self->rxTail_ % RX_QUEUE_SIZE) { + // Queue full — drop + pbuf_free(p); + return; + } + auto &slot = self->rxQueue_[self->rxHead_ % RX_QUEUE_SIZE]; + slot.addr = *addr; + slot.port = port; + slot.p = p; + slot.offset = 0; + self->rxHead_ = next; +} + +void LwIPUDP::advanceRx() { + if (current_) { + if (current_->p) { pbuf_free(current_->p); current_->p = nullptr; } + rxTail_ = (rxTail_ + 1) % RX_QUEUE_SIZE; + current_ = nullptr; + } +} + +int LwIPUDP::available() { + if (current_ && current_->p) + return current_->p->tot_len - current_->offset; + return 0; +} + +int LwIPUDP::parsePacket() { + // Drop any unfinished current packet + advanceRx(); + + if (rxTail_ == rxHead_) return 0; + + current_ = &rxQueue_[rxTail_ % RX_QUEUE_SIZE]; + return current_->p ? current_->p->tot_len : 0; +} + +int LwIPUDP::read(uint8_t *buf, size_t len) { + if (!current_ || !current_->p) return 0; + + uint16_t remaining = current_->p->tot_len - current_->offset; + uint16_t toRead = (len < remaining) ? (uint16_t)len : remaining; + uint16_t copied = pbuf_copy_partial(current_->p, buf, toRead, current_->offset); + current_->offset += copied; + return copied; +} + +int LwIPUDP::read() { + uint8_t b; + return (read(&b, 1) == 1) ? b : -1; +} + +IPAddress LwIPUDP::remoteIP() { + if (current_) return IPAddress(current_->addr); + return IPAddress(); +} + +uint16_t LwIPUDP::remotePort() { + if (current_) return current_->port; + return 0; +} + +int LwIPUDP::beginPacket(IPAddress ip, uint16_t port) { + if (txBuf_) { pbuf_free(txBuf_); txBuf_ = nullptr; } + txAddr_ = ip.toLwIP(); + txPort_ = port; + return 1; +} + +size_t LwIPUDP::write(const uint8_t *buf, size_t len) { + if (!len) return 0; + + struct pbuf *seg = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM); + if (!seg) return 0; + memcpy(seg->payload, buf, len); + + if (txBuf_) { + pbuf_cat(txBuf_, seg); + } else { + txBuf_ = seg; + } + return len; +} + +int LwIPUDP::endPacket() { + if (!pcb_ || !txBuf_) return 0; + + cyw43_arch_lwip_begin(); + err_t err = udp_sendto(pcb_, txBuf_, &txAddr_, txPort_); + cyw43_arch_lwip_end(); + pbuf_free(txBuf_); + txBuf_ = nullptr; + return (err == ERR_OK) ? 1 : 0; +} diff --git a/MIDI_Interfaces/AppleMIDI/LwIPUDP.hpp b/MIDI_Interfaces/AppleMIDI/LwIPUDP.hpp new file mode 100644 index 0000000..570a52c --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/LwIPUDP.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include "vendor/shim/IPAddress.h" + +#include "lwip/udp.h" +#include "lwip/pbuf.h" + +// Arduino WiFiUDP-compatible wrapper around lwIP raw UDP API. +// Template parameter for AppleMIDISession. + +class LwIPUDP { +public: + LwIPUDP(); + ~LwIPUDP(); + + bool begin(uint16_t port); + void stop(); + + // Rx — packet-oriented read + int available(); + int parsePacket(); + int read(uint8_t *buf, size_t len); + int read(); + IPAddress remoteIP(); + uint16_t remotePort(); + + // Tx — packet-oriented write + int beginPacket(IPAddress ip, uint16_t port); + size_t write(const uint8_t *buf, size_t len); + int endPacket(); + void flush() {} // no-op; UDP sends are immediate + +private: + static constexpr uint8_t RX_QUEUE_SIZE = 4; + + struct RxPacket { + ip_addr_t addr; + uint16_t port; + struct pbuf *p; + uint16_t offset; + }; + + struct udp_pcb *pcb_; + RxPacket rxQueue_[RX_QUEUE_SIZE]; + volatile uint8_t rxHead_; + volatile uint8_t rxTail_; + + // Current packet being read + RxPacket *current_; + void advanceRx(); + + // Outgoing + ip_addr_t txAddr_; + uint16_t txPort_; + struct pbuf *txBuf_; + + static void recvCb(void *arg, struct udp_pcb *pcb, + struct pbuf *p, const ip_addr_t *addr, u16_t port); +}; diff --git a/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI.cpp b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI.cpp new file mode 100644 index 0000000..3079b40 --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI.cpp @@ -0,0 +1,7 @@ +#include "AppleMIDI.h" + +BEGIN_APPLEMIDI_NAMESPACE + +unsigned long now = 0; + +END_APPLEMIDI_NAMESPACE diff --git a/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI.h b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI.h new file mode 100644 index 0000000..bfd24b7 --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI.h @@ -0,0 +1,402 @@ +#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 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; + friend class rtpMIDIParser; + +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 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 _appleMIDIParser; + rtpMIDIParser _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 participant; +#else + Deque, 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 *); + + void writeRtpMidiToAllParticipants(); + void writeRtpMidiBuffer(Participant *); + + void manageReceiverFeedback(); + + void manageSessionInvites(); + void manageSynchronization(); + void manageSynchronizationInitiator(); + void manageSynchronizationInitiatorHeartBeat(Participant *); + void manageSynchronizationInitiatorInvites(size_t); + + void sendSynchronization(Participant *); + +#ifndef ONE_PARTICIPANT + Participant *getParticipantBySSRC(const ssrc_t &); + Participant *getParticipantByInitiatorToken(const uint32_t &initiatorToken); +#endif +#ifdef USE_DIRECTORY + bool IsComputerInDirectory(IPAddress) const; +#endif +}; + +END_APPLEMIDI_NAMESPACE + +#include "AppleMIDI.hpp" + +#include "AppleMIDI_PlatformEnd.h" diff --git a/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI.hpp b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI.hpp new file mode 100644 index 0000000..dd4d259 --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI.hpp @@ -0,0 +1,1200 @@ +#pragma once + +#include "AppleMIDI_Namespace.h" +#include +#include + +BEGIN_APPLEMIDI_NAMESPACE + +// Read pending control UDP packets into the control buffer. +template +size_t AppleMIDISession::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 +void AppleMIDISession::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 +size_t AppleMIDISession::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 +void AppleMIDISession::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 +void AppleMIDISession::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 +void AppleMIDISession::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 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 +void AppleMIDISession::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 +void AppleMIDISession::ReceivedBitrateReceiveLimit(AppleMIDI_BitrateReceiveLimit &) +{ +} + +#ifdef APPLEMIDI_INITIATOR +// Route accepted invitations based on the incoming port type. +template +void AppleMIDISession::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 +void AppleMIDISession::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 +void AppleMIDISession::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 +void AppleMIDISession::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 +void AppleMIDISession::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 +void AppleMIDISession::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 +void AppleMIDISession::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 +bool AppleMIDISession::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 +Participant* AppleMIDISession::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 +Participant* AppleMIDISession::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 +void AppleMIDISession::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(&invitation), invitation.getLength()); + + port.endPacket(); + port.flush(); +} + +// Send receiver feedback on the control port. +template +void AppleMIDISession::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(&receiverFeedback), sizeof(AppleMIDI_ReceiverFeedback)); + + controlPort.endPacket(); + controlPort.flush(); +} + +// Send a synchronization packet on the data port. +template +void AppleMIDISession::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(&synchronization), sizeof(synchronization)); + + dataPort.endPacket(); + dataPort.flush(); +} + +// Send an end-session packet on the control port. +template +void AppleMIDISession::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(&endSession), sizeof(endSession)); + + controlPort.endPacket(); + controlPort.flush(); +} + +// Flush the outgoing MIDI buffer to all participants. +template +void AppleMIDISession::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 +void AppleMIDISession::writeRtpMidiBuffer(Participant* 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 +void AppleMIDISession::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 +void AppleMIDISession::manageSynchronizationInitiatorHeartBeat(Participant* 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 +void AppleMIDISession::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 +void AppleMIDISession::sendSynchronization(Participant* 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 +void AppleMIDISession::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 +void AppleMIDISession::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 +bool AppleMIDISession::sendInvite(IPAddress ip, uint16_t port) +{ +#ifndef ONE_PARTICIPANT + if (participants.full()) +#else + if (participant.ssrc != 0) +#endif + { + return false; + } + +#ifndef ONE_PARTICIPANT + Participant 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 +void AppleMIDISession::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 +void AppleMIDISession::sendEndSession(Participant* 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 +void AppleMIDISession::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 +void AppleMIDISession::StartReceivedMidi() +{ +#ifdef USE_EXT_CALLBACKS + if (nullptr != _startReceivedMidiByteCallback) + _startReceivedMidiByteCallback(ssrc); +#endif +} + +// Handle a received MIDI byte and buffer it. +template +void AppleMIDISession::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 +void AppleMIDISession::EndReceivedMidi() +{ +#ifdef USE_EXT_CALLBACKS + if (nullptr != _endReceivedMidiByteCallback) + _endReceivedMidiByteCallback(ssrc); +#endif +} + +END_APPLEMIDI_NAMESPACE diff --git a/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Debug.h b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Debug.h new file mode 100644 index 0000000..7a6ad66 --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Debug.h @@ -0,0 +1,33 @@ +#pragma once + +#ifdef SerialMon +static inline void AM_DBG_SETUP(unsigned long baud) { + SerialMon.begin(baud); + const unsigned long timeout_ms = 2000; + const unsigned long start_ms = millis(); + while (!SerialMon && (millis() - start_ms) < timeout_ms) { + // Avoid hard lock on boards without native USB or no host. + } +} + +template +static inline void AM_DBG_PLAIN(T last) { + SerialMon.println(last); +} + +template +static inline void AM_DBG_PLAIN(T head, Args... tail) { + SerialMon.print(head); + SerialMon.print(' '); + AM_DBG_PLAIN(tail...); +} + +template +static inline void AM_DBG(Args... args) { + AM_DBG_PLAIN(args...); +} +#else +#define AM_DBG_SETUP(...) +#define AM_DBG_PLAIN(...) +#define AM_DBG(...) +#endif diff --git a/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Defs.h b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Defs.h new file mode 100644 index 0000000..5c2bd5c --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Defs.h @@ -0,0 +1,214 @@ +#pragma once + +#include "AppleMIDI_Settings.h" +#include "AppleMIDI_Namespace.h" + +BEGIN_APPLEMIDI_NAMESPACE + +#define APPLEMIDI_LIBRARY_VERSION 0x030000 +#define APPLEMIDI_LIBRARY_VERSION_MAJOR 3 +#define APPLEMIDI_LIBRARY_VERSION_MINOR 0 +#define APPLEMIDI_LIBRARY_VERSION_PATCH 0 + +#define DEFAULT_CONTROL_PORT 5004 + +typedef uint32_t ssrc_t; +typedef uint32_t initiatorToken_t; +typedef uint64_t timestamp_t; + +union conversionBuffer +{ + uint8_t value8; + uint16_t value16; + uint32_t value32; + uint64_t value64; + uint8_t buffer[8]; +}; + + +enum parserReturn: uint8_t +{ + Processed, + NotSureGiveMeMoreData, + NotEnoughData, + UnexpectedData, + UnexpectedMidiData, + UnexpectedJournalData, + SessionNameVeryLong, +}; + +#if defined(__AVR__) +#define APPLEMIDI_PROGMEM PROGMEM +typedef const __FlashStringHelper* AppleMIDIConstStr; +#define GFP(x) (reinterpret_cast(x)) +#define GF(x) F(x) +#else +#define APPLEMIDI_PROGMEM +typedef const char* AppleMIDIConstStr; +#define GFP(x) x +#define GF(x) x +#endif + +#define RtpBuffer_t Deque +#define MidiBuffer_t Deque + +// #define USE_EXT_CALLBACKS +// #define ONE_PARTICIPANT // memory optimization +// #define USE_DIRECTORY + +// By defining NO_SESSION_NAME in the sketch, you can save 100 bytes +#ifndef NO_SESSION_NAME +#define KEEP_SESSION_NAME +#endif + +#define MIDI_SAMPLING_RATE_176K4HZ 176400 +#define MIDI_SAMPLING_RATE_192KHZ 192000 +#define MIDI_SAMPLING_RATE_DEFAULT 10000 + +struct Rtp; +typedef Rtp Rtp_t; + +struct RtpMIDI; +typedef RtpMIDI RtpMIDI_t; + +#ifdef USE_DIRECTORY +enum WhoCanConnectToMe : uint8_t +{ + None, + OnlyComputersInMyDirectory, + Anyone, +}; +#endif + +// from: https://en.wikipedia.org/wiki/RTP-MIDI +// Apple decided to create their own protocol, imposing all parameters related to +// synchronization like the sampling frequency. This session protocol is called "AppleMIDI" +// in Wireshark software. Session management with AppleMIDI protocol requires two UDP ports, +// the first one is called "Control Port", the second one is called "Data Port". When used +// within a multithread implementation, only the Data port requires a "real-time" thread, +// the other port can be controlled by a normal priority thread. These two ports must be +// located at two consecutive locations (n / n+1); the first one can be any of the 65536 +// possible ports. +enum amPortType : uint8_t +{ + Control = 0, + Data = 1, +}; + +// from: https://en.wikipedia.org/wiki/RTP-MIDI +// AppleMIDI implementation defines two kind of session controllers: session initiators +// and session listeners. Session initiators are in charge of inviting the session listeners, +// and are responsible of the clock synchronization sequence. Session initiators can generally +// be session listeners, but some devices, such as iOS devices, can be session listeners only. +enum ParticipantKind : uint8_t +{ + Listener, + Initiator, +}; + +enum InviteStatus : uint8_t +{ + Initiating, + AwaitingControlInvitationAccepted, + ControlInvitationAccepted, + AwaitingDataInvitationAccepted, + DataInvitationAccepted, + Connected +}; + +enum Exception : uint8_t +{ + BufferFullException, + ParseException, + UnexpectedParseException, + TooManyParticipantsException, + ComputerNotInDirectory, + NotAcceptingAnyone, + UnexpectedInviteException, + ParticipantNotFoundException, + ListenerTimeOutException, + MaxAttemptsException, + NoResponseFromConnectionRequestException, + SendPacketsDropped, + ReceivedPacketsDropped, + UdpBeginPacketFailed, +}; + +using connectedCallback = void (*)(const ssrc_t&, const char *); +using disconnectedCallback = void (*)(const ssrc_t&); +#ifdef USE_EXT_CALLBACKS +using startReceivedMidiByteCallback = void (*)(const ssrc_t&); +using receivedMidiByteCallback = void (*)(const ssrc_t&, byte); +using endReceivedMidiByteCallback = void (*)(const ssrc_t&); +using receivedRtpCallback = void (*)(const ssrc_t&, const Rtp_t&, const int32_t&); +using exceptionCallback = void (*)(const ssrc_t&, const Exception&, const int32_t value); +using sentRtpCallback = void (*)(const Rtp_t&); +using sentRtpMidiCallback = void (*)(const RtpMIDI_t&); +#endif + +/* Signature "Magic Value" for Apple network MIDI session establishment */ +static constexpr uint8_t amSignature[] = {0xff, 0xff}; + +/* 2 (stored in network byte order (big-endian)) */ +static constexpr uint8_t amProtocolVersion[] = {0x00, 0x00, 0x00, 0x02}; + +/* Apple network MIDI valid commands */ +static constexpr uint8_t amInvitation[] = {'I', 'N'}; +static constexpr uint8_t amEndSession[] = {'B', 'Y'}; +static constexpr uint8_t amSynchronization[] = {'C', 'K'}; +static constexpr uint8_t amInvitationAccepted[] = {'O', 'K'}; +static constexpr uint8_t amInvitationRejected[] = {'N', 'O'}; +static constexpr uint8_t amReceiverFeedback[] = {'R', 'S'}; +static constexpr uint8_t amBitrateReceiveLimit[] = {'R', 'L'}; + +const uint8_t SYNC_CK0 = 0; +const uint8_t SYNC_CK1 = 1; +const uint8_t SYNC_CK2 = 2; + +typedef struct PACKED AppleMIDI_Invitation +{ + initiatorToken_t initiatorToken; + ssrc_t ssrc; + +#ifdef KEEP_SESSION_NAME + char sessionName[DefaultSettings::MaxSessionNameLen + 1]; + const size_t getLength() const + { + return sizeof(AppleMIDI_Invitation) - (DefaultSettings::MaxSessionNameLen) + strlen(sessionName); + } +#else + const size_t getLength() const + { + return sizeof(AppleMIDI_Invitation); + } +#endif +} AppleMIDI_Invitation_t, AppleMIDI_InvitationAccepted_t, AppleMIDI_InvitationRejected_t; + +typedef struct PACKED AppleMIDI_BitrateReceiveLimit +{ + ssrc_t ssrc; + uint32_t bitratelimit; +} AppleMIDI_BitrateReceiveLimit_t; + +typedef struct PACKED AppleMIDI_Synchronization +{ + ssrc_t ssrc; + uint8_t count; + uint8_t padding[3] = {0,0,0}; + timestamp_t timestamps[3]; +} AppleMIDI_Synchronization_t; + +typedef struct PACKED AppleMIDI_ReceiverFeedback +{ + ssrc_t ssrc; + uint16_t sequenceNr; + uint16_t dummy; +} AppleMIDI_ReceiverFeedback_t; + +typedef struct PACKED AppleMIDI_EndSession +{ + initiatorToken_t initiatorToken; + ssrc_t ssrc; +} AppleMIDI_EndSession_t; + +END_APPLEMIDI_NAMESPACE diff --git a/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Namespace.h b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Namespace.h new file mode 100644 index 0000000..24a7d5f --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Namespace.h @@ -0,0 +1,9 @@ +#pragma once + +#define APPLEMIDI_NAMESPACE appleMidi +#define BEGIN_APPLEMIDI_NAMESPACE \ + namespace APPLEMIDI_NAMESPACE \ + { +#define END_APPLEMIDI_NAMESPACE } + +#define USING_NAMESPACE_APPLEMIDI using namespace APPLEMIDI_NAMESPACE; \ No newline at end of file diff --git a/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Parser.h b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Parser.h new file mode 100644 index 0000000..60f4feb --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Parser.h @@ -0,0 +1,419 @@ +#pragma once + +#include "utility/Deque.h" + +#include "AppleMIDI_Defs.h" +#include "AppleMIDI_Settings.h" +#include "AppleMIDI_Namespace.h" + +BEGIN_APPLEMIDI_NAMESPACE + +template +class AppleMIDISession; + +template +class AppleMIDIParser +{ +public: + AppleMIDISession *session; + + parserReturn parse(RtpBuffer_t &buffer, const amPortType &portType) + { + conversionBuffer cb; + + uint8_t signature[2]; // Signature "Magic Value" for Apple network MIDI session establishment + uint8_t command[2]; // 16-bit command identifier (two ASCII characters, first in high 8 bits, second in low 8 bits) + + size_t minimumLen = (sizeof(signature) + sizeof(command)); // Signature + Command + if (buffer.size() < minimumLen) + return parserReturn::NotSureGiveMeMoreData; + + size_t i = 0; + + signature[0] = buffer[i++]; + signature[1] = buffer[i++]; + if (0 != memcmp(signature, amSignature, sizeof(amSignature))) + { + return parserReturn::UnexpectedData; + } + + command[0] = buffer[i++]; + command[1] = buffer[i++]; + + if (0 == memcmp(command, amInvitation, sizeof(amInvitation))) + { + uint8_t protocolVersion[4]; + + minimumLen += (sizeof(protocolVersion) + sizeof(initiatorToken_t) + sizeof(ssrc_t)); + if (buffer.size() < minimumLen) + { + return parserReturn::NotEnoughData; + } + + // 2 (stored in network byte order (big-endian)) + protocolVersion[0] = buffer[i++]; + protocolVersion[1] = buffer[i++]; + protocolVersion[2] = buffer[i++]; + protocolVersion[3] = buffer[i++]; + if (0 != memcmp(protocolVersion, amProtocolVersion, sizeof(amProtocolVersion))) + { + return parserReturn::UnexpectedData; + } + + AppleMIDI_Invitation invitation; + + // A random number generated by the session's APPLEMIDI_INITIATOR. + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + cb.buffer[2] = buffer[i++]; + cb.buffer[3] = buffer[i++]; + invitation.initiatorToken = __ntohl(cb.value32); + + // The sender's synchronization source identifier. + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + cb.buffer[2] = buffer[i++]; + cb.buffer[3] = buffer[i++]; + invitation.ssrc = __ntohl(cb.value32); + +#ifdef KEEP_SESSION_NAME + uint16_t bi = 0; + while (i < buffer.size()) + { + if (bi < Settings::MaxSessionNameLen) + invitation.sessionName[bi++] = buffer[i++]; + else + i++; + } + invitation.sessionName[bi++] = '\0'; +#else + while (i < buffer.size()) + i++; +#endif + auto retVal = parserReturn::Processed; + + // when given a Session Name and the buffer has been fully processed and the + // last character is not 'endl', then we got a very long sessionName. It will + // continue in the next memory chunk of the packet. We don't care, so indicated + // flush the remainder of the packet. + // First part if the session name is kept, processing continues + if (i > minimumLen) + if (i == buffer.size() && buffer[buffer.size() - 1] != 0x00) + retVal = parserReturn::SessionNameVeryLong; + + while (i > 0) + { + buffer.pop_front(); // consume all the bytes that made up this message + --i; + } + + session->ReceivedInvitation(invitation, portType); + + return retVal; + } + else if (0 == memcmp(command, amEndSession, sizeof(amEndSession))) + { + uint8_t protocolVersion[4]; + + minimumLen += (sizeof(protocolVersion) + sizeof(initiatorToken_t) + sizeof(ssrc_t)); + if (buffer.size() < minimumLen) + return parserReturn::NotEnoughData; + + // 2 (stored in network byte order (big-endian)) + protocolVersion[0] = buffer[i++]; + protocolVersion[1] = buffer[i++]; + protocolVersion[2] = buffer[i++]; + protocolVersion[3] = buffer[i++]; + if (0 != memcmp(protocolVersion, amProtocolVersion, sizeof(amProtocolVersion))) + { + return parserReturn::UnexpectedData; + } + + AppleMIDI_EndSession_t endSession; + + // A random number generated by the session's APPLEMIDI_INITIATOR. + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + cb.buffer[2] = buffer[i++]; + cb.buffer[3] = buffer[i++]; + endSession.initiatorToken = __ntohl(cb.value32); + + // The sender's synchronization source identifier. + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + cb.buffer[2] = buffer[i++]; + cb.buffer[3] = buffer[i++]; + endSession.ssrc = __ntohl(cb.value32); + + while (i > 0) + { + buffer.pop_front(); // consume all the bytes that made up this message + --i; + } + + session->ReceivedEndSession(endSession); + + return parserReturn::Processed; + } + else if (0 == memcmp(command, amSynchronization, sizeof(amSynchronization))) + { + AppleMIDI_Synchronization_t synchronization; + + // minimum amount : 4 bytes for sender SSRC, 1 byte for count, 3 bytes padding and 3 times 8 bytes + minimumLen += (4 + 1 + 3 + (3 * 8)); + if (buffer.size() < minimumLen) + return parserReturn::NotEnoughData; + + // The sender's synchronization source identifier. + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + cb.buffer[2] = buffer[i++]; + cb.buffer[3] = buffer[i++]; + synchronization.ssrc = __ntohl(cb.value32); + + synchronization.count = buffer[i++]; + buffer[i++]; + buffer[i++]; + buffer[i++]; // padding, unused + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + cb.buffer[2] = buffer[i++]; + cb.buffer[3] = buffer[i++]; + cb.buffer[4] = buffer[i++]; + cb.buffer[5] = buffer[i++]; + cb.buffer[6] = buffer[i++]; + cb.buffer[7] = buffer[i++]; + synchronization.timestamps[0] = __ntohll(cb.value64); + + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + cb.buffer[2] = buffer[i++]; + cb.buffer[3] = buffer[i++]; + cb.buffer[4] = buffer[i++]; + cb.buffer[5] = buffer[i++]; + cb.buffer[6] = buffer[i++]; + cb.buffer[7] = buffer[i++]; + synchronization.timestamps[1] = __ntohll(cb.value64); + + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + cb.buffer[2] = buffer[i++]; + cb.buffer[3] = buffer[i++]; + cb.buffer[4] = buffer[i++]; + cb.buffer[5] = buffer[i++]; + cb.buffer[6] = buffer[i++]; + cb.buffer[7] = buffer[i++]; + synchronization.timestamps[2] = __ntohll(cb.value64); + + while (i > 0) + { + buffer.pop_front(); // consume all the bytes that made up this message + --i; + } + + session->ReceivedSynchronization(synchronization); + + return parserReturn::Processed; + } + else if (0 == memcmp(command, amReceiverFeedback, sizeof(amReceiverFeedback))) + { + AppleMIDI_ReceiverFeedback_t receiverFeedback; + + minimumLen += (sizeof(receiverFeedback.ssrc) + sizeof(receiverFeedback.sequenceNr) + sizeof(receiverFeedback.dummy)); + if (buffer.size() < minimumLen) + return parserReturn::NotEnoughData; + + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + cb.buffer[2] = buffer[i++]; + cb.buffer[3] = buffer[i++]; + receiverFeedback.ssrc = __ntohl(cb.value32); + + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + receiverFeedback.sequenceNr = __ntohs(cb.value16); + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + receiverFeedback.dummy = __ntohs(cb.value16); + + while (i--) + buffer.pop_front(); // consume all the bytes that made up this message + + session->ReceivedReceiverFeedback(receiverFeedback); + + return parserReturn::Processed; + } +#ifdef APPLEMIDI_INITIATOR + else if (0 == memcmp(command, amInvitationAccepted, sizeof(amInvitationAccepted))) + { + uint8_t protocolVersion[4]; + + minimumLen += (sizeof(protocolVersion) + sizeof(initiatorToken_t) + sizeof(ssrc_t)); + if (buffer.size() < minimumLen) + { + return parserReturn::NotEnoughData; + } + + // 2 (stored in network byte order (big-endian)) + protocolVersion[0] = buffer[i++]; + protocolVersion[1] = buffer[i++]; + protocolVersion[2] = buffer[i++]; + protocolVersion[3] = buffer[i++]; + if (0 != memcmp(protocolVersion, amProtocolVersion, sizeof(amProtocolVersion))) + { + return parserReturn::UnexpectedData; + } + + AppleMIDI_InvitationAccepted_t invitationAccepted; + + // A random number generated by the session's APPLEMIDI_INITIATOR. + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + cb.buffer[2] = buffer[i++]; + cb.buffer[3] = buffer[i++]; + invitationAccepted.initiatorToken = __ntohl(cb.value32); + + // The sender's synchronization source identifier. + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + cb.buffer[2] = buffer[i++]; + cb.buffer[3] = buffer[i++]; + invitationAccepted.ssrc = __ntohl(cb.value32); + +#ifdef KEEP_SESSION_NAME + uint16_t bi = 0; + while (i < buffer.size()) + { + if (bi < Settings::MaxSessionNameLen) + invitationAccepted.sessionName[bi++] = buffer[i++]; + else + i++; + } + invitationAccepted.sessionName[bi++] = '\0'; +#else + while (i < buffer.size()) + i++; +#endif + + auto retVal = parserReturn::Processed; + + // when given a Session Name and the buffer has been fully processed and the + // last character is not 'endl', then we got a very long sessionName. It will + // continue in the next memory chunk of the packet. We don't care, so indicated + // flush the remainder of the packet. + // First part if the session name is kept, processing continues + if (i > minimumLen) + if (i == buffer.size() && buffer[buffer.size() - 1] != 0x00) + retVal = parserReturn::SessionNameVeryLong; + + while (i > 0) + { + buffer.pop_front(); // consume all the bytes that made up this message + --i; + } + + session->ReceivedInvitationAccepted(invitationAccepted, portType); + + return retVal; + } + else if (0 == memcmp(command, amInvitationRejected, sizeof(amInvitationRejected))) + { + uint8_t protocolVersion[4]; + + minimumLen += (sizeof(protocolVersion) + sizeof(initiatorToken_t) + sizeof(ssrc_t)); + if (buffer.size() < minimumLen) + { + return parserReturn::NotEnoughData; + } + + // 2 (stored in network byte order (big-endian)) + protocolVersion[0] = buffer[i++]; + protocolVersion[1] = buffer[i++]; + protocolVersion[2] = buffer[i++]; + protocolVersion[3] = buffer[i++]; + if (0 != memcmp(protocolVersion, amProtocolVersion, sizeof(amProtocolVersion))) + { + return parserReturn::UnexpectedData; + } + + AppleMIDI_InvitationRejected_t invitationRejected; + + // A random number generated by the session's APPLEMIDI_INITIATOR. + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + cb.buffer[2] = buffer[i++]; + cb.buffer[3] = buffer[i++]; + invitationRejected.initiatorToken = __ntohl(cb.value32); + + // The sender's synchronization source identifier. + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + cb.buffer[2] = buffer[i++]; + cb.buffer[3] = buffer[i++]; + invitationRejected.ssrc = __ntohl(cb.value32); + +#ifdef KEEP_SESSION_NAME + uint16_t bi = 0; + while ((i < buffer.size()) && (buffer[i] != 0x00)) + { + if (bi < Settings::MaxSessionNameLen) + invitationRejected.sessionName[bi++] = buffer[i]; + i++; + } + invitationRejected.sessionName[bi++] = '\0'; +#else + while ((i < buffer.size()) && (buffer[i] != 0x00)) + i++; +#endif + // session name is optional. + // If i > minimum size (16), then a sessionName was provided and must include 0x00 + if (i > minimumLen) + if (i == buffer.size() || buffer[i++] != 0x00) + return parserReturn::NotEnoughData; + + while (i > 0) + { + buffer.pop_front(); // consume all the bytes that made up this message + --i; + } + + session->ReceivedInvitationRejected(invitationRejected); + + return parserReturn::Processed; + } + else if (0 == memcmp(command, amBitrateReceiveLimit, sizeof(amBitrateReceiveLimit))) + { + AppleMIDI_BitrateReceiveLimit bitrateReceiveLimit; + + minimumLen += (sizeof(bitrateReceiveLimit.ssrc) + sizeof(bitrateReceiveLimit.bitratelimit)); + if (buffer.size() < minimumLen) + return parserReturn::NotEnoughData; + + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + cb.buffer[2] = buffer[i++]; + cb.buffer[3] = buffer[i++]; + bitrateReceiveLimit.ssrc = __ntohl(cb.value32); + + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + cb.buffer[2] = buffer[i++]; + cb.buffer[3] = buffer[i++]; + bitrateReceiveLimit.bitratelimit = __ntohl(cb.value32); + + while (i > 0) + { + buffer.pop_front(); // consume all the bytes that made up this message + --i; + } + + session->ReceivedBitrateReceiveLimit(bitrateReceiveLimit); + + return parserReturn::Processed; + } +#endif + return parserReturn::UnexpectedData; + } +}; + +END_APPLEMIDI_NAMESPACE diff --git a/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Participant.h b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Participant.h new file mode 100644 index 0000000..0f6c36d --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Participant.h @@ -0,0 +1,46 @@ +#pragma once + +#include "AppleMIDI_Defs.h" + +#include "AppleMIDI_Namespace.h" + +BEGIN_APPLEMIDI_NAMESPACE + +template +struct Participant +{ + ParticipantKind kind = Listener; + ssrc_t ssrc = 0; + IPAddress remoteIP = INADDR_NONE; + uint16_t remotePort = 0; + + unsigned long receiverFeedbackStartTime = 0; + bool doReceiverFeedback = false; + + uint16_t sendSequenceNr = 0; // seeded when session/participant is created + uint16_t receiveSequenceNr = 0; + + unsigned long lastSyncExchangeTime = 0; + +#ifdef APPLEMIDI_INITIATOR + uint8_t connectionAttempts = 0; + uint32_t initiatorToken = 0; + unsigned long lastInviteSentTime = 0; + InviteStatus invitationStatus = Initiating; + + uint8_t synchronizationHeartBeats = 0; + uint8_t synchronizationCount = 0; + bool synchronizing = false; +#endif + +#ifdef USE_EXT_CALLBACKS + bool firstMessageReceived = true; + uint32_t offsetEstimate = 0; +#endif + +#ifdef KEEP_SESSION_NAME + char sessionName[Settings::MaxSessionNameLen + 1]; +#endif +} ; + +END_APPLEMIDI_NAMESPACE diff --git a/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_PlatformBegin.h b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_PlatformBegin.h new file mode 100644 index 0000000..9cb35e2 --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_PlatformBegin.h @@ -0,0 +1,20 @@ +#pragma once + +#include "AppleMIDI_Namespace.h" + +#include "utility/endian.h" + +BEGIN_APPLEMIDI_NAMESPACE + +#ifdef _MSC_VER +#pragma pack(push, 1) +#define PACKED +#else +#define PACKED __attribute__((__packed__)) +#endif + +struct DefaultPlatform +{ +}; + +END_APPLEMIDI_NAMESPACE diff --git a/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_PlatformEnd.h b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_PlatformEnd.h new file mode 100644 index 0000000..fec7b9f --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_PlatformEnd.h @@ -0,0 +1,12 @@ +#pragma once + +#include "AppleMIDI_Namespace.h" + +BEGIN_APPLEMIDI_NAMESPACE + +#ifdef WIN32 +#pragma pack(pop) +#endif +#undef PACKED + +END_APPLEMIDI_NAMESPACE diff --git a/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Settings.h b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Settings.h new file mode 100644 index 0000000..6ad675a --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/AppleMIDI_Settings.h @@ -0,0 +1,47 @@ +#pragma once + +#include "AppleMIDI_Namespace.h" + +BEGIN_APPLEMIDI_NAMESPACE + +struct DefaultSettings +{ + // Small default to fit constrained MCUs; raise if you send larger SysEx. + static const size_t UdpTxPacketMaxSize = 24; + + // MIDI buffer size in bytes; should be >= 3 * max message length. + static const size_t MaxBufferSize = 64; + + static const size_t MaxSessionNameLen = 24; + + static const uint8_t MaxNumberOfParticipants = 2; + + static const uint8_t MaxNumberOfComputersInDirectory = 5; + + // 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. + // + // Each partner then sends cyclically to the other partner the RS message, indicating + // the last sequence number received correctly, in other words, without any gap between + // two sequence numbers. The sender can then free the memory containing old journalling data if necessary. + static const unsigned long ReceiversFeedbackThreshold = 1000; + + // The initiator must initiate a new sync exchange at least once every 60 seconds. + // This value includes a small 1s slack. + // https://developer.apple.com/library/archive/documentation/Audio/Conceptual/MIDINetworkDriverProtocol/MIDI/MIDI.html + static const unsigned long CK_MaxTimeOut = 61000; + + // when set to true, the lower 32-bits of the rtpClock are sent + // when set to false, 0 will be set for immediate playout. + static const bool TimestampRtpPackets = true; + + static const uint8_t MaxSessionInvitesAttempts = 13; + + static const uint8_t MaxSynchronizationCK0Attempts = 5; + + static const unsigned long SynchronizationHeartBeat = 10000; +}; + +END_APPLEMIDI_NAMESPACE diff --git a/MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Clock.h b/MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Clock.h new file mode 100644 index 0000000..97d7ebb --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Clock.h @@ -0,0 +1,74 @@ +#pragma once + +#include + +#include "AppleMIDI_Namespace.h" + +BEGIN_APPLEMIDI_NAMESPACE + +#define MSEC_PER_SEC 1000 + +typedef struct rtpMidi_Clock +{ + uint32_t clockRate_; + + uint64_t startTime_; + uint64_t initialTimeStamp_; + uint32_t low32_; + uint32_t high32_; + + void Init(uint64_t initialTimeStamp, uint32_t clockRate) + { + initialTimeStamp_ = initialTimeStamp; + clockRate_ = clockRate; + + if (clockRate_ == 0) + { + clockRate_ = MIDI_SAMPLING_RATE_DEFAULT; + } + + low32_ = millis(); + high32_ = 0; + startTime_ = Ticks(); + } + + /// + /// Returns a timestamp value suitable for inclusion in a RTP packet header. + /// + uint64_t Now() + { + return CalculateCurrentTimeStamp(); + } + +private: + uint64_t CalculateCurrentTimeStamp() + { + return initialTimeStamp_ + (CalculateTimeSpent() * clockRate_) / MSEC_PER_SEC; + } + + /// + /// Returns the time spent since the initial clock timestamp value. + /// The returned value is expressed in milliseconds. + /// + uint64_t CalculateTimeSpent() + { + return Ticks() - startTime_; + } + + /// + /// millis() as a 64bit (not the default 32bit) + /// this prevents wrap around. + /// Note: rollover tracking is per instance; call Init() before use. + /// + uint64_t Ticks() + { + uint32_t new_low32 = millis(); + if (new_low32 < low32_) high32_++; + low32_ = new_low32; + return (uint64_t) high32_ << 32 | low32_; + } + + +} RtpMidiClock_t; + +END_APPLEMIDI_NAMESPACE diff --git a/MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Defs.h b/MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Defs.h new file mode 100644 index 0000000..4d2aa58 --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Defs.h @@ -0,0 +1,141 @@ +#pragma once + +#include "AppleMIDI_Namespace.h" + +BEGIN_APPLEMIDI_NAMESPACE + +/* used to mask the most significant bit, which flags the start of a new MIDI-command! */ +#define RTP_MIDI_COMMAND_STATUS_FLAG 0x80 + +/* used to mask the lower 7 bits of the single octets that make up the delta-time */ +#define RTP_MIDI_DELTA_TIME_OCTET_MASK 0x7f +/* used to mask the most significant bit, which flags the extension of the delta-time */ +#define RTP_MIDI_DELTA_TIME_EXTENSION 0x80 + +#define RTP_MIDI_CS_FLAG_B 0x80 +#define RTP_MIDI_CS_FLAG_J 0x40 +#define RTP_MIDI_CS_FLAG_Z 0x20 +#define RTP_MIDI_CS_FLAG_P 0x10 +#define RTP_MIDI_CS_MASK_SHORTLEN 0x0f +#define RTP_MIDI_CS_MASK_LONGLEN 0x0fff + +#define RTP_MIDI_CJ_CHAPTER_M_FLAG_J 0x80 +#define RTP_MIDI_CJ_CHAPTER_M_FLAG_K 0x40 +#define RTP_MIDI_CJ_CHAPTER_M_FLAG_L 0x20 +#define RTP_MIDI_CJ_CHAPTER_M_FLAG_M 0x10 +#define RTP_MIDI_CJ_CHAPTER_M_FLAG_N 0x08 +#define RTP_MIDI_CJ_CHAPTER_M_FLAG_T 0x04 +#define RTP_MIDI_CJ_CHAPTER_M_FLAG_V 0x02 +#define RTP_MIDI_CJ_CHAPTER_M_FLAG_R 0x01 + +#define RTP_MIDI_JS_FLAG_S 0x80 +#define RTP_MIDI_JS_FLAG_Y 0x40 +#define RTP_MIDI_JS_FLAG_A 0x20 +#define RTP_MIDI_JS_FLAG_H 0x10 +#define RTP_MIDI_JS_MASK_TOTALCHANNELS 0x0f + +#define RTP_MIDI_SJ_FLAG_S 0x8000 +#define RTP_MIDI_SJ_FLAG_D 0x4000 +#define RTP_MIDI_SJ_FLAG_V 0x2000 +#define RTP_MIDI_SJ_FLAG_Q 0x1000 +#define RTP_MIDI_SJ_FLAG_F 0x0800 +#define RTP_MIDI_SJ_FLAG_X 0x0400 +#define RTP_MIDI_SJ_MASK_LENGTH 0x03ff + +#define RTP_MIDI_SJ_CHAPTER_D_FLAG_S 0x80 +#define RTP_MIDI_SJ_CHAPTER_D_FLAG_B 0x40 +#define RTP_MIDI_SJ_CHAPTER_D_FLAG_G 0x20 +#define RTP_MIDI_SJ_CHAPTER_D_FLAG_H 0x10 +#define RTP_MIDI_SJ_CHAPTER_D_FLAG_J 0x08 +#define RTP_MIDI_SJ_CHAPTER_D_FLAG_K 0x04 +#define RTP_MIDI_SJ_CHAPTER_D_FLAG_Y 0x02 +#define RTP_MIDI_SJ_CHAPTER_D_FLAG_Z 0x01 + +#define RTP_MIDI_SJ_CHAPTER_D_RESET_FLAG_S 0x80 +#define RTP_MIDI_SJ_CHAPTER_D_RESET_COUNT 0x7f +#define RTP_MIDI_SJ_CHAPTER_D_TUNE_FLAG_S 0x80 +#define RTP_MIDI_SJ_CHAPTER_D_TUNE_COUNT 0x7f +#define RTP_MIDI_SJ_CHAPTER_D_SONG_SEL_FLAG_S 0x80 +#define RTP_MIDI_SJ_CHAPTER_D_SONG_SEL_VALUE 0x7f + +#define RTP_MIDI_SJ_CHAPTER_D_SYSCOM_FLAG_S 0x8000 +#define RTP_MIDI_SJ_CHAPTER_D_SYSCOM_FLAG_C 0x4000 +#define RTP_MIDI_SJ_CHAPTER_D_SYSCOM_FLAG_V 0x2000 +#define RTP_MIDI_SJ_CHAPTER_D_SYSCOM_FLAG_L 0x1000 +#define RTP_MIDI_SJ_CHAPTER_D_SYSCOM_MASK_DSZ 0x0c00 +#define RTP_MIDI_SJ_CHAPTER_D_SYSCOM_MASK_LENGTH 0x03ff +#define RTP_MIDI_SJ_CHAPTER_D_SYSCOM_MASK_COUNT 0xff + +#define RTP_MIDI_SJ_CHAPTER_D_SYSREAL_FLAG_S 0x80 +#define RTP_MIDI_SJ_CHAPTER_D_SYSREAL_FLAG_C 0x40 +#define RTP_MIDI_SJ_CHAPTER_D_SYSREAL_FLAG_L 0x20 +#define RTP_MIDI_SJ_CHAPTER_D_SYSREAL_MASK_LENGTH 0x1f +#define RTP_MIDI_SJ_CHAPTER_D_SYSREAL_MASK_COUNT 0xff + +#define RTP_MIDI_SJ_CHAPTER_Q_FLAG_S 0x80 +#define RTP_MIDI_SJ_CHAPTER_Q_FLAG_N 0x40 +#define RTP_MIDI_SJ_CHAPTER_Q_FLAG_D 0x20 +#define RTP_MIDI_SJ_CHAPTER_Q_FLAG_C 0x10 +#define RTP_MIDI_SJ_CHAPTER_Q_FLAG_T 0x80 +#define RTP_MIDI_SJ_CHAPTER_Q_MASK_TOP 0x07 +#define RTP_MIDI_SJ_CHAPTER_Q_MASK_CLOCK 0x07ffff +#define RTP_MIDI_SJ_CHAPTER_Q_MASK_TIMETOOLS 0xffffff + +#define RTP_MIDI_SJ_CHAPTER_F_FLAG_S 0x80 +#define RTP_MIDI_SJ_CHAPTER_F_FLAG_C 0x40 +#define RTP_MIDI_SJ_CHAPTER_F_FLAG_P 0x20 +#define RTP_MIDI_SJ_CHAPTER_F_FLAG_Q 0x10 +#define RTP_MIDI_SJ_CHAPTER_F_FLAG_D 0x08 +#define RTP_MIDI_SJ_CHAPTER_F_MASK_POINT 0x07 +#define RTP_MIDI_SJ_CHAPTER_F_MASK_MT0 0xf0000000 +#define RTP_MIDI_SJ_CHAPTER_F_MASK_MT1 0x0f000000 +#define RTP_MIDI_SJ_CHAPTER_F_MASK_MT2 0x00f00000 +#define RTP_MIDI_SJ_CHAPTER_F_MASK_MT3 0x000f0000 +#define RTP_MIDI_SJ_CHAPTER_F_MASK_MT4 0x0000f000 +#define RTP_MIDI_SJ_CHAPTER_F_MASK_MT5 0x00000f00 +#define RTP_MIDI_SJ_CHAPTER_F_MASK_MT6 0x000000f0 +#define RTP_MIDI_SJ_CHAPTER_F_MASK_MT7 0x0000000f +#define RTP_MIDI_SJ_CHAPTER_F_MASK_HR 0xff000000 +#define RTP_MIDI_SJ_CHAPTER_F_MASK_MN 0x00ff0000 +#define RTP_MIDI_SJ_CHAPTER_F_MASK_SC 0x0000ff00 +#define RTP_MIDI_SJ_CHAPTER_F_MASK_FR 0x000000ff + +#define RTP_MIDI_SJ_CHAPTER_X_FLAG_S 0x80 +#define RTP_MIDI_SJ_CHAPTER_X_FLAG_T 0x40 +#define RTP_MIDI_SJ_CHAPTER_X_FLAG_C 0x20 +#define RTP_MIDI_SJ_CHAPTER_X_FLAG_F 0x10 +#define RTP_MIDI_SJ_CHAPTER_X_FLAG_D 0x08 +#define RTP_MIDI_SJ_CHAPTER_X_FLAG_L 0x04 +#define RTP_MIDI_SJ_CHAPTER_X_MASK_STA 0x03 +#define RTP_MIDI_SJ_CHAPTER_X_MASK_TCOUNT 0xff +#define RTP_MIDI_SJ_CHAPTER_X_MASK_COUNT 0xff + +#define RTP_MIDI_CJ_FLAG_S 0x800000 +#define RTP_MIDI_CJ_FLAG_H 0x040000 +#define RTP_MIDI_CJ_FLAG_P 0x000080 +#define RTP_MIDI_CJ_FLAG_C 0x000040 +#define RTP_MIDI_CJ_FLAG_M 0x000020 +#define RTP_MIDI_CJ_FLAG_W 0x000010 +#define RTP_MIDI_CJ_FLAG_N 0x000008 +#define RTP_MIDI_CJ_FLAG_E 0x000004 +#define RTP_MIDI_CJ_FLAG_T 0x000002 +#define RTP_MIDI_CJ_FLAG_A 0x000001 +#define RTP_MIDI_CJ_MASK_LENGTH 0x03ff00 +#define RTP_MIDI_CJ_MASK_CHANNEL 0x780000 +#define RTP_MIDI_CJ_CHANNEL_SHIFT 19 + +#define RTP_MIDI_CJ_CHAPTER_M_MASK_LENGTH 0x3f + +#define RTP_MIDI_CJ_CHAPTER_N_MASK_LENGTH 0x7f00 +#define RTP_MIDI_CJ_CHAPTER_N_MASK_LOW 0x00f0 +#define RTP_MIDI_CJ_CHAPTER_N_MASK_HIGH 0x000f + +#define RTP_MIDI_CJ_CHAPTER_E_MASK_LENGTH 0x7f +#define RTP_MIDI_CJ_CHAPTER_A_MASK_LENGTH 0x7f + +typedef struct PACKED RtpMIDI +{ + uint8_t flags; +} RtpMIDI_t; + +END_APPLEMIDI_NAMESPACE diff --git a/MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Parser.h b/MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Parser.h new file mode 100644 index 0000000..49938e7 --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Parser.h @@ -0,0 +1,232 @@ +#pragma once + +#include "utility/Deque.h" + +#include "shim/midi_Defs.h" + +#include "rtpMIDI_Defs.h" +#include "rtp_Defs.h" + +#include "AppleMIDI_Settings.h" +#include "AppleMIDI_Namespace.h" + +BEGIN_APPLEMIDI_NAMESPACE + +template +class AppleMIDISession; + +template +class rtpMIDIParser +{ +private: + bool _rtpHeadersComplete = false; + bool _journalSectionComplete = false; + bool _channelJournalSectionComplete = false; + uint16_t midiCommandLength; + uint8_t _journalTotalChannels; + uint8_t rtpMidi_Flags = 0; + int cmdCount = 0; + uint8_t runningstatus = 0; + size_t _bytesToFlush = 0; + +protected: + void debugPrintBuffer(RtpBuffer_t &buffer) + { +#if defined(DEBUG) && defined(SerialMon) + for (size_t i = 0; i < buffer.size(); i++) + { + SerialMon.print(" "); + SerialMon.print(i); + SerialMon.print(i < 10 ? " " : " "); + } + for (size_t i = 0; i < buffer.size(); i++) + { + SerialMon.print("0x"); + SerialMon.print(buffer[i] < 16 ? "0" : ""); + SerialMon.print(buffer[i], HEX); + SerialMon.print(" "); + } +#endif + } + +public: + AppleMIDISession * session; + + // Parse the incoming string + // return: + // - return 0, when the parse does not have enough data + // - return a negative number, when the parser encounters invalid or + // unexpected data. The negative number indicates the amount of bytes + // that were processed. They can be purged safely + // - a positive number indicates the amount of valid bytes processed + // + parserReturn parse(RtpBuffer_t &buffer) + { + debugPrintBuffer(buffer); + + conversionBuffer cb; + + // [RFC3550] provides a complete description of the RTP header fields. + // In this section, we clarify the role of a few RTP header fields for + // MIDI applications. All fields are coded in network byte order (big- + // endian). + + // 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 + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | V |P|X| CC |M| PT | Sequence number | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | Timestamp | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | SSRC | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | MIDI command section ... | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | Journal section ... | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + if (_rtpHeadersComplete == false) + { + auto minimumLen = sizeof(Rtp_t); + if (buffer.size() < minimumLen) + return parserReturn::NotSureGiveMeMoreData; + + size_t i = 0; // todo: rename to consumed + + Rtp_t rtp; + rtp.vpxcc = buffer[i++]; + rtp.mpayload = buffer[i++]; + + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + rtp.sequenceNr = __ntohs(cb.value16); + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + cb.buffer[2] = buffer[i++]; + cb.buffer[3] = buffer[i++]; + rtp.timestamp = __ntohl(cb.value32); + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + cb.buffer[2] = buffer[i++]; + cb.buffer[3] = buffer[i++]; + rtp.ssrc = __ntohl(cb.value32); + + uint8_t version = RTP_VERSION(rtp.vpxcc); + #if defined(DEBUG) && defined(SerialMon) + bool padding = RTP_PADDING(rtp.vpxcc); + bool extension = RTP_EXTENSION(rtp.vpxcc); + uint8_t csrc_count = RTP_CSRC_COUNT(rtp.vpxcc); + #endif + + if (RTP_VERSION_2 != version) + { + return parserReturn::UnexpectedData; + } + + #if defined(DEBUG) && defined(SerialMon) + bool marker = RTP_MARKER(rtp.mpayload); + #endif + uint8_t payloadType = RTP_PAYLOAD_TYPE(rtp.mpayload); + + if (PAYLOADTYPE_RTPMIDI != payloadType) + { + return parserReturn::UnexpectedData; + } + + session->ReceivedRtp(rtp); + + // Next byte is the flag + minimumLen += 1; + if (buffer.size() < minimumLen) + return parserReturn::NotSureGiveMeMoreData; + + // 2.2. MIDI Payload (https://www.ietf.org/rfc/rfc4695.html#section-2.2) + // The payload MUST begin with the MIDI command section. The + // MIDI command section codes a (possibly empty) list of timestamped + // MIDI commands and provides the essential service of the payload + // format. + + /* RTP-MIDI starts with 4 bits of flags... */ + rtpMidi_Flags = buffer[i++]; + + // ...followed by a length-field of at least 4 bits + midiCommandLength = rtpMidi_Flags & RTP_MIDI_CS_MASK_SHORTLEN; + + /* see if we have small or large len-field */ + if (rtpMidi_Flags & RTP_MIDI_CS_FLAG_B) + { + minimumLen += 1; + if (buffer.size() < minimumLen) + return parserReturn::NotSureGiveMeMoreData; + + // long header + uint8_t octet = buffer[i++]; + midiCommandLength = (midiCommandLength << 8) | octet; + } + + cmdCount = 0; + runningstatus = 0; + + while (i > 0) + { + buffer.pop_front(); + --i; + } + + _rtpHeadersComplete = true; + + // initialize the Journal Section + _journalSectionComplete = false; + _channelJournalSectionComplete = false; + _journalTotalChannels = 0; + } + + // Always a MIDI section + if (midiCommandLength > 0) + { + auto retVal = decodeMIDICommandSection(buffer); + if (retVal != parserReturn::Processed) return retVal; + } + + // The payload MAY also contain a journal section. The journal section + // provides resiliency by coding the recent history of the stream. A + // flag in the MIDI command section codes the presence of a journal + // section in the payload. + + if (rtpMidi_Flags & RTP_MIDI_CS_FLAG_J) + { + auto retVal = decodeJournalSection(buffer); + switch (retVal) { + case parserReturn::Processed: + break; + case parserReturn::NotEnoughData: + return parserReturn::NotEnoughData; + case parserReturn::UnexpectedJournalData: + _rtpHeadersComplete = false; + _journalSectionComplete = false; + _channelJournalSectionComplete = false; + _journalTotalChannels = 0; + default: + // Reset all journal state on any non-recoverable error to avoid + // leaking partial state into the next packet. + _rtpHeadersComplete = false; + _journalSectionComplete = false; + _channelJournalSectionComplete = false; + _journalTotalChannels = 0; + return retVal; + } + } + + _rtpHeadersComplete = false; + + return parserReturn::Processed; + } + + #include "rtpMIDI_Parser_JournalSection.hpp" + + #include "rtpMIDI_Parser_CommandSection.hpp" +}; + +END_APPLEMIDI_NAMESPACE diff --git a/MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Parser_CommandSection.hpp b/MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Parser_CommandSection.hpp new file mode 100644 index 0000000..e71d14e --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Parser_CommandSection.hpp @@ -0,0 +1,279 @@ +// https://www.ietf.org/rfc/rfc4695.html#section-3 + +parserReturn decodeMIDICommandSection(RtpBuffer_t &buffer) +{ + debugPrintBuffer(buffer); + + // https://www.ietf.org/rfc/rfc4695.html#section-3.2 + // + // The first MIDI channel command in the MIDI list MUST include a status + // octet.Running status coding, as defined in[MIDI], MAY be used for + // all subsequent MIDI channel commands in the list.As in[MIDI], + // System Commonand System Exclusive messages(0xF0 ... 0xF7) cancel + // the running status state, but System Real - time messages(0xF8 ... + // 0xFF) do not affect the running status state. All System commands in + // the MIDI list MUST include a status octet. + + // As we note above, the first channel command in the MIDI list MUST + // include a status octet.However, the corresponding command in the + // original MIDI source data stream might not have a status octet(in + // this case, the source would be coding the command using running + // status). If the status octet of the first channel command in the + // MIDI list does not appear in the source data stream, the P(phantom) + // header bit MUST be set to 1. In all other cases, the P bit MUST be + // set to 0. + // + // Note that the P bit describes the MIDI source data stream, not the + // MIDI list encoding; regardless of the state of the P bit, the MIDI + // list MUST include the status octet. + // + // As receivers MUST be able to decode running status, sender + // implementors should feel free to use running status to improve + // bandwidth efficiency. However, senders SHOULD NOT introduce timing + // jitter into an existing MIDI command stream through an inappropriate + // use or removal of running status coding. This warning primarily + // applies to senders whose RTP MIDI streams may be transcoded onto a + // MIDI 1.0 DIN cable[MIDI] by the receiver : both the timestamps and + // the command coding (running status or not) must comply with the + // physical restrictions of implicit time coding over a slow serial + // line. + + // (lathoub: RTP_MIDI_CS_FLAG_P((phantom) not implemented + + /* Multiple MIDI-commands might follow - the exact number can only be discovered by really decoding the commands! */ + while (midiCommandLength) + { + /* for the first command we only have a delta-time if Z-Flag is set */ + if ((cmdCount) || (rtpMidi_Flags & RTP_MIDI_CS_FLAG_Z)) + { + size_t consumed = 0; + auto retVal = decodeTime(buffer, consumed); + if (retVal != parserReturn::Processed) return retVal; + + midiCommandLength -= consumed; + while (consumed--) + buffer.pop_front(); + } + + if (midiCommandLength > 0) + { + cmdCount++; + + size_t consumed = 0; + auto retVal = decodeMidi(buffer, runningstatus, consumed); + if (retVal == parserReturn::NotEnoughData) { + cmdCount = 0; // avoid first command again + return retVal; + } + + midiCommandLength -= consumed; + while (consumed--) + buffer.pop_front(); + } + } + + return parserReturn::Processed; +} + +parserReturn decodeTime(RtpBuffer_t &buffer, size_t &consumed) +{ + debugPrintBuffer(buffer); + + uint32_t deltatime = 0; + + /* RTP-MIDI deltatime is "compressed" using only the necessary amount of octets */ + for (uint8_t j = 0; j < 4; j++) + { + if (buffer.size() < 1) + return parserReturn::NotEnoughData; + + uint8_t octet = buffer[consumed]; + deltatime = (deltatime << 7) | (octet & RTP_MIDI_DELTA_TIME_OCTET_MASK); + consumed++; + + if ((octet & RTP_MIDI_DELTA_TIME_EXTENSION) == 0) + break; + } + + return parserReturn::Processed; +} + +parserReturn decodeMidi(RtpBuffer_t &buffer, uint8_t &runningstatus, size_t &consumed) +{ + debugPrintBuffer(buffer); + + if (buffer.size() < 1) + return parserReturn::NotEnoughData; + + auto octet = buffer.front(); + + /* MIDI realtime-data -> one octet -- unlike serial-wired MIDI realtime-commands in RTP-MIDI will + * not be intermingled with other MIDI-commands, so we handle this case right here and return */ + if (octet >= 0xf8) + { + consumed = 1; + + session->StartReceivedMidi(); + session->ReceivedMidi(octet); + session->EndReceivedMidi(); + + return parserReturn::Processed; + } + + /* see if this first octet is a status message */ + if ((octet & RTP_MIDI_COMMAND_STATUS_FLAG) == 0) + { + /* if we have no running status yet -> error */ + if (((runningstatus)&RTP_MIDI_COMMAND_STATUS_FLAG) == 0) + { + return parserReturn::Processed; + } + /* our first octet is "virtual" coming from a preceding MIDI-command, + * so actually we have not really consumed anything yet */ + octet = runningstatus; + } + else + { + /* Let's see how this octet influences our running-status */ + /* if we have a "normal" MIDI-command then the new status replaces the current running-status */ + if (octet < 0xf0) + { + runningstatus = octet; + } + else + { + /* system-realtime-commands maintain the current running-status + * other system-commands clear the running-status, since we + * already handled realtime, we can reset it here */ + runningstatus = 0; + } + consumed++; + } + + /* non-system MIDI-commands encode the command in the high nibble and the channel + * in the low nibble - so we will take care of those cases next */ + if (octet < 0xf0) + { + switch (octet & 0xf0) + { + case MIDI_NAMESPACE::MidiType::NoteOff: + consumed += 2; + break; + case MIDI_NAMESPACE::MidiType::NoteOn: + consumed += 2; + break; + case MIDI_NAMESPACE::MidiType::AfterTouchPoly: + consumed += 2; + break; + case MIDI_NAMESPACE::MidiType::ControlChange: + consumed += 2; + break; + case MIDI_NAMESPACE::MidiType::ProgramChange: + consumed += 1; + break; + case MIDI_NAMESPACE::MidiType::AfterTouchChannel: + consumed += 1; + break; + case MIDI_NAMESPACE::MidiType::PitchBend: + consumed += 2; + break; + } + + if (buffer.size() < consumed) { + return parserReturn::NotEnoughData; + } + + session->StartReceivedMidi(); + for (size_t j = 0; j < consumed; j++) + session->ReceivedMidi(buffer[j]); + session->EndReceivedMidi(); + + return parserReturn::Processed; + } + + /* Here we catch the remaining system-common commands */ + switch (octet) + { + case MIDI_NAMESPACE::MidiType::SystemExclusiveStart: + case MIDI_NAMESPACE::MidiType::SystemExclusiveEnd: + decodeMidiSysEx(buffer, consumed); + break; + case MIDI_NAMESPACE::MidiType::TimeCodeQuarterFrame: + consumed += 1; + break; + case MIDI_NAMESPACE::MidiType::SongPosition: + consumed += 2; + break; + case MIDI_NAMESPACE::MidiType::SongSelect: + consumed += 1; + break; + case MIDI_NAMESPACE::MidiType::TuneRequest: + break; + } + + if (buffer.size() < consumed) + return parserReturn::NotEnoughData; + + session->StartReceivedMidi(); + for (size_t j = 0; j < consumed; j++) + session->ReceivedMidi(buffer[j]); + session->EndReceivedMidi(); + + return parserReturn::Processed; +} + +parserReturn decodeMidiSysEx(RtpBuffer_t &buffer, size_t &consumed) +{ + debugPrintBuffer(buffer); + +// consumed = 1; // beginning SysEx Token is not counted (as it could remain) + size_t i = 1; // 0 = start of SysEx, so we can start with 1 + while (i < buffer.size()) + { + consumed++; + auto octet = buffer[i++]; + + AM_DBG("0x"); + AM_DBG(octet < 16 ? "0" : ""); + AM_DBG(octet, HEX); + AM_DBG(" "); + + if (octet == MIDI_NAMESPACE::MidiType::SystemExclusiveEnd) // Complete message + { + return parserReturn::Processed; + } + else if (octet == MIDI_NAMESPACE::MidiType::SystemExclusiveStart) // Start + { + return parserReturn::Processed; + } + } + + // begin of the SysEx is found, not the end. + // so transmit what we have, add a stop-token at the end, + // remove the bytes, modify the length and indicate + // not-enough data, so we buffer gets filled with the remaining bytes. + + // to compensate for adding the sysex at the end. + consumed--; + + // send MIDI data + session->StartReceivedMidi(); + for (size_t j = 0; j < consumed; j++) + session->ReceivedMidi(buffer[j]); + session->ReceivedMidi(MIDI_NAMESPACE::MidiType::SystemExclusiveStart); + session->EndReceivedMidi(); + + // Remove the bytes that were submitted + for (size_t j = 0; j < consumed; j++) + buffer.pop_front(); + // Start a new SysEx train + buffer.push_front(MIDI_NAMESPACE::MidiType::SystemExclusiveEnd); + + midiCommandLength -= consumed; + midiCommandLength += 1; // for adding the manual SysEx SystemExclusiveEnd in front + + // indicates split SysEx + consumed = buffer.max_size() + 1; + + return parserReturn::Processed; +} diff --git a/MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Parser_JournalSection.hpp b/MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Parser_JournalSection.hpp new file mode 100644 index 0000000..c0041b1 --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/rtpMIDI_Parser_JournalSection.hpp @@ -0,0 +1,180 @@ +// The recovery journal is the default resiliency tool for unreliable +// transport. In this section, we normatively define the roles that +// senders and receivers play in the recovery journal system. +// +// This section introduces the structure of the recovery journal and +// defines the bitfields of recovery journal headers. Appendices A and +// B complete the bitfield definition of the recovery journal. +// +// The recovery journal has a three-level structure: +// +// o Top-level header. +// +// o Channel and system journal headers. These headers encode recovery +// information for a single voice channel (channel journal) or for +// all system commands (system journal). +// +// o Chapters. Chapters describe recovery information for a single +// MIDI command type. +// +parserReturn decodeJournalSection(RtpBuffer_t &buffer) +{ + size_t minimumLen = 0; + + conversionBuffer cb; + + if (false == _journalSectionComplete) + { + size_t i = 0; + + // Minimum size for the Journal section is 3 + minimumLen += 3; + if (buffer.size() < minimumLen) + return parserReturn::NotEnoughData; + + /* lets get the main flags from the recovery journal header */ + uint8_t flags = buffer[i++]; + + // The 16-bit Checkpoint Packet Seqnum header field codes the sequence + // number of the checkpoint packet for this journal, in network byte + // order (big-endian). The choice of the checkpoint packet sets the + // depth of the checkpoint history for the journal (defined in Appendix A.1). + // + // Receivers may use the Checkpoint Packet Seqnum field of the packet + // that ends a loss event to verify that the journal checkpoint history + // covers the entire loss event. The checkpoint history covers the loss + // event if the Checkpoint Packet Seqnum field is less than or equal to + // one plus the highest RTP sequence number previously received on the + // stream (modulo 2^16). + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + // uint16_t checkPoint = __ntohs(cb.value16); ; // unused + + // (RFC 4695, 5 Recovery Journal Format) + // If A and Y are both zero, the recovery journal only contains its 3- + // octet header and is considered to be an "empty" journal. + if ((flags & RTP_MIDI_JS_FLAG_Y) == 0 && (flags & RTP_MIDI_JS_FLAG_A) == 0) + { + // Big fixed by @hugbug + while (minimumLen-- > 0) + buffer.pop_front(); + + _journalSectionComplete = true; + return parserReturn::Processed; + } + + // By default, the payload format does not use enhanced Chapter C + // encoding. In this default case, the H bit MUST be set to 0 for all + // packets in the stream. + if (flags & RTP_MIDI_JS_FLAG_H) + { + // The H bit indicates if MIDI channels in the stream have been + // configured to use the enhanced Chapter C encoding + } + + // The S (single-packet loss) bit appears in most recovery journal + // structures, including the recovery journal header. The S bit helps + // receivers efficiently parse the recovery journal in the common case + // of the loss of a single packet. + if (flags & RTP_MIDI_JS_FLAG_S) + { + // special encoding + } + + // If the Y header bit is set to 1, the system journal appears in the + // recovery journal, directly following the recovery journal header. + if (flags & RTP_MIDI_JS_FLAG_Y) + { + minimumLen += 2; + if (buffer.size() < minimumLen) + { + return parserReturn::NotEnoughData; + } + + cb.buffer[0] = buffer[i++]; + cb.buffer[1] = buffer[i++]; + uint16_t systemflags = __ntohs(cb.value16); + uint16_t sysjourlen = systemflags & RTP_MIDI_SJ_MASK_LENGTH; + + uint16_t remainingBytes = sysjourlen - 2; + + minimumLen += remainingBytes; + if (buffer.size() < minimumLen) + { + return parserReturn::NotEnoughData; + } + + i += remainingBytes; + } + + // If the A header bit is set to 1, the recovery journal ends with a + // list of (TOTCHAN + 1) channel journals (the 4-bit TOTCHAN header + // field is interpreted as an unsigned integer). + if (flags & RTP_MIDI_JS_FLAG_A) + { + /* At the same place we find the total channels encoded in the channel journal */ + _journalTotalChannels = (flags & RTP_MIDI_JS_MASK_TOTALCHANNELS) + 1; + } + + while (i-- > 0) // is that the same as while (i--) ?? + buffer.pop_front(); + + _journalSectionComplete = true; + } + + // iterate through all the channels specified in header + while (_journalTotalChannels > 0) + { + if (false == _channelJournalSectionComplete) { + + if (buffer.size() < 3) + return parserReturn::NotEnoughData; + + // 3 bytes for channel journal + cb.buffer[0] = 0x00; + cb.buffer[1] = buffer[0]; + cb.buffer[2] = buffer[1]; + cb.buffer[3] = buffer[2]; + uint32_t chanflags = __ntohl(cb.value32); + + bool S_flag = (chanflags & RTP_MIDI_CJ_FLAG_S) == 1; + uint8_t channelNr = (chanflags & RTP_MIDI_CJ_MASK_CHANNEL) >> RTP_MIDI_CJ_CHANNEL_SHIFT; + bool H_flag = (chanflags & RTP_MIDI_CJ_FLAG_H) == 1; + uint8_t chanjourlen = (chanflags & RTP_MIDI_CJ_MASK_LENGTH) >> 8; + + if ((chanflags & RTP_MIDI_CJ_FLAG_P)) { + } + if ((chanflags & RTP_MIDI_CJ_FLAG_C)) { + } + if ((chanflags & RTP_MIDI_CJ_FLAG_M)) { + } + if ((chanflags & RTP_MIDI_CJ_FLAG_W)) { + } + if ((chanflags & RTP_MIDI_CJ_FLAG_N)) { + } + if ((chanflags & RTP_MIDI_CJ_FLAG_E)) { + } + if ((chanflags & RTP_MIDI_CJ_FLAG_T)) { + } + if ((chanflags & RTP_MIDI_CJ_FLAG_A)) { + } + + _bytesToFlush = chanjourlen; + + _channelJournalSectionComplete = true; + } + + while (buffer.size() > 0 && _bytesToFlush > 0) { + _bytesToFlush--; + buffer.pop_front(); + } + + if (_bytesToFlush > 0) { + return parserReturn::NotEnoughData; + } + + _journalTotalChannels--; + } + + return parserReturn::Processed; +} diff --git a/MIDI_Interfaces/AppleMIDI/vendor/rtp_Defs.h b/MIDI_Interfaces/AppleMIDI/vendor/rtp_Defs.h new file mode 100644 index 0000000..7784909 --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/rtp_Defs.h @@ -0,0 +1,55 @@ +#pragma once + +#include "AppleMIDI_Namespace.h" + +BEGIN_APPLEMIDI_NAMESPACE + +// 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 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | V |P|X| CC |M| PT | Sequence number | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +#define RTP_VERSION_2 2 + +// first octet +#define RTP_P_FIELD 0x20 +#define RTP_X_FIELD 0x10 +#define RTP_CC_FIELD 0x0F + +// second octet +#define RTP_M_FIELD 0x80 +#define RTP_PT_FIELD 0x7F + +/* magic number */ +#define PAYLOADTYPE_RTPMIDI 97 + +/* Version is the first 2 bits of the first octet*/ +#define RTP_VERSION(octet) (((octet) >> 6) & 0x03) + +/* Padding is the third bit; No need to shift, because true is any value +other than 0! */ +#define RTP_PADDING(octet) ((octet)&RTP_P_FIELD) + +/* Extension bit is the fourth bit */ +#define RTP_EXTENSION(octet) ((octet)&RTP_X_FIELD) + +/* CSRC count is the last four bits */ +#define RTP_CSRC_COUNT(octet) ((octet)&RTP_CC_FIELD) + +/* Marker is the first bit of the second octet */ +#define RTP_MARKER(octet) ((octet)&RTP_M_FIELD) + +/* Payload type is the last 7 bits */ +#define RTP_PAYLOAD_TYPE(octet) ((octet)&RTP_PT_FIELD) + +typedef struct PACKED Rtp +{ + uint8_t vpxcc; + uint8_t mpayload; + uint16_t sequenceNr; + uint32_t timestamp; + ssrc_t ssrc; +} Rtp_t; + +END_APPLEMIDI_NAMESPACE diff --git a/MIDI_Interfaces/AppleMIDI/vendor/shim/IPAddress.h b/MIDI_Interfaces/AppleMIDI/vendor/shim/IPAddress.h new file mode 100644 index 0000000..8881072 --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/shim/IPAddress.h @@ -0,0 +1,41 @@ +#pragma once + +// Minimal IPAddress for Arduino-AppleMIDI-Library on pico-sdk / lwIP. +// Only implements what the vendored code actually uses. + +#include +#include + +#include "lwip/ip_addr.h" + +#ifndef INADDR_NONE +#define INADDR_NONE ((uint32_t)0xFFFFFFFFUL) +#endif + +class IPAddress { +public: + IPAddress() : addr_(INADDR_NONE) {} + IPAddress(uint32_t raw) : addr_(raw) {} + IPAddress(uint8_t a, uint8_t b, uint8_t c, uint8_t d) + : addr_((uint32_t)a | ((uint32_t)b << 8) | + ((uint32_t)c << 16) | ((uint32_t)d << 24)) {} + IPAddress(const ip_addr_t &lwip) : addr_(ip_addr_get_ip4_u32(&lwip)) {} + + operator uint32_t() const { return addr_; } + + bool operator==(const IPAddress &rhs) const { return addr_ == rhs.addr_; } + bool operator!=(const IPAddress &rhs) const { return addr_ != rhs.addr_; } + + ip_addr_t toLwIP() const { + ip_addr_t a; + ip_addr_set_ip4_u32(&a, addr_); + return a; + } + + uint8_t operator[](int idx) const { + return (addr_ >> (idx * 8)) & 0xFF; + } + +private: + uint32_t addr_; +}; diff --git a/MIDI_Interfaces/AppleMIDI/vendor/shim/MIDI.h b/MIDI_Interfaces/AppleMIDI/vendor/shim/MIDI.h new file mode 100644 index 0000000..a1ac603 --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/shim/MIDI.h @@ -0,0 +1,21 @@ +#pragma once + +// Shim replacing FortySevenEffects MIDI.h for Arduino-AppleMIDI-Library +// on pico-sdk. Provides the subset of types actually referenced. + +#include "midi_Defs.h" +#include + +#include +#include + +#ifndef byte +typedef uint8_t byte; +#endif + +inline void randomSeed(unsigned long seed) { srand((unsigned int)seed); } + +inline long random(long min_val, long max_val) { + if (min_val >= max_val) return min_val; + return min_val + (rand() % (max_val - min_val)); +} diff --git a/MIDI_Interfaces/AppleMIDI/vendor/shim/midi_Defs.h b/MIDI_Interfaces/AppleMIDI/vendor/shim/midi_Defs.h new file mode 100644 index 0000000..110e126 --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/shim/midi_Defs.h @@ -0,0 +1,42 @@ +#pragma once + +// Minimal shim for MIDI_NAMESPACE::MidiType used by Arduino-AppleMIDI-Library. +// Replaces the FortySevenEffects MIDI library dependency. + +#include + +#ifndef MIDI_NAMESPACE +#define MIDI_NAMESPACE applemidi_types +#endif + +namespace MIDI_NAMESPACE { + +enum MidiType : uint8_t { + InvalidType = 0x00, + NoteOff = 0x80, + NoteOn = 0x90, + AfterTouchPoly = 0xA0, + ControlChange = 0xB0, + ProgramChange = 0xC0, + AfterTouchChannel = 0xD0, + PitchBend = 0xE0, + SystemExclusive = 0xF0, + SystemExclusiveStart = 0xF0, + TimeCodeQuarterFrame = 0xF1, + SongPosition = 0xF2, + SongSelect = 0xF3, + TuneRequest = 0xF6, + SystemExclusiveEnd = 0xF7, + Clock = 0xF8, + Start = 0xFA, + Continue = 0xFB, + Stop = 0xFC, + ActiveSensing = 0xFE, + SystemReset = 0xFF, +}; + +struct DefaultSettings { + static const bool Use1ByteParsing = true; +}; + +} // namespace MIDI_NAMESPACE diff --git a/MIDI_Interfaces/AppleMIDI/vendor/utility/Deque.h b/MIDI_Interfaces/AppleMIDI/vendor/utility/Deque.h new file mode 100644 index 0000000..a66d117 --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/utility/Deque.h @@ -0,0 +1,277 @@ +#pragma once + +#include + +BEGIN_APPLEMIDI_NAMESPACE + +template +class Deque { +// class iterator; + +private: + int _head, _tail; + T _data[Size]; + +public: + Deque() + { + clear(); + }; + + size_t free(); + const size_t size() const; + const size_t max_size() const; + T & front(); + const T & front() const; + T & back(); + const T & back() const; + void push_front(const T &); + void push_back(const T &); + size_t push_back(const T *, size_t); + size_t copy_out(T *, size_t) const; + void pop_front(); + void pop_back(); + + T& operator[](size_t); + const T& operator[](size_t) const; + T& at(size_t); + const T& at(size_t) const; + + void clear(); + +// iterator begin(); +// iterator end(); + + void erase(size_t); + void erase(size_t, size_t); + + bool empty() const { + return size() == 0; + } + bool full() const { + return (size() == Size); + } +}; + +template +size_t Deque::free() +{ + return Size - size(); +} + +template +const size_t Deque::size() const +{ + if (_tail < 0) + return 0; // empty + else if (_head > _tail) + return _head - _tail; + else + return Size - _tail + _head; +} + +template +const size_t Deque::max_size() const +{ + return Size; +} + +template +T & Deque::front() +{ + return _data[_tail]; +} + +template +const T & Deque::front() const +{ + return _data[_tail]; +} + +template +T & Deque::back() +{ + int idx = _head - 1; + if (idx < 0) idx = Size - 1; + return _data[idx]; +} + +template +const T & Deque::back() const +{ + int idx = _head - 1; + if (idx < 0) idx = Size - 1; + return _data[idx]; +} + +template +void Deque::push_front(const T &value) +{ + //if container is full, do nothing. + if (free()){ + if (--_tail < 0) + _tail = Size - 1; + _data[_tail] = value; + } +} + +template +void Deque::push_back(const T &value) +{ + //if container is full, do nothing. + if (free()){ + _data[_head] = value; + if (empty()) + _tail = _head; + if (++_head >= Size) + _head %= Size; + } +} + +template +size_t Deque::push_back(const T *values, size_t count) +{ + if (values == nullptr || count == 0) + return 0; + + const size_t available = free(); + if (available == 0) + return 0; + + const size_t toWrite = (count < available) ? count : available; + + if (empty()) + _tail = _head; + + size_t first = toWrite; + if (_head + first > Size) + first = Size - _head; + + memcpy(&_data[_head], values, first * sizeof(T)); + _head = (_head + first) % Size; + + const size_t remaining = toWrite - first; + if (remaining > 0) + { + memcpy(&_data[_head], values + first, remaining * sizeof(T)); + _head = (_head + remaining) % Size; + } + + return toWrite; +} + +template +size_t Deque::copy_out(T *dest, size_t count) const +{ + if (dest == nullptr || count == 0) + return 0; + + const size_t available = size(); + if (available == 0) + return 0; + + const size_t toCopy = (count < available) ? count : available; + const size_t start = (size_t)_tail; + + size_t first = toCopy; + if (start + first > Size) + first = Size - start; + + memcpy(dest, &_data[start], first * sizeof(T)); + + const size_t remaining = toCopy - first; + if (remaining > 0) + memcpy(dest + first, &_data[0], remaining * sizeof(T)); + + return toCopy; +} + +template +void Deque::pop_front() { + if (empty()) // if empty, do nothing. + return; + if (++_tail >= Size) + _tail %= Size; + if (_tail == _head) + clear(); +} + +template +void Deque::pop_back() { + if (empty()) // if empty, do nothing. + return; + if (--_head < 0) + _head = Size - 1; + if (_head == _tail) //now buffer is empty + clear(); +} + +template +void Deque::erase(size_t position) { + if (position >= size()) // out-of-range! + return; // do nothing. + for (size_t i = position; i < size() - 1; i++){ + at(i) = at(i + 1); + } + pop_back(); +} + +template +void Deque::erase(size_t first, size_t last) { + if (first > last // invalid arguments + || first >= size()) // out-of-range + return; //do nothing. + + size_t tgt = first; + for (size_t i = last + 1; i < size(); i++){ + at(tgt++) = at(i); + } + for (size_t i = first; i <= last; i++){ + pop_back(); + } +} + +template +T& Deque::operator[](size_t index) +{ + auto i = _tail + index; + if (i >= Size) + i %= Size; + return _data[i]; +} + +template +const T& Deque::operator[](size_t index) const +{ + auto i = _tail + index; + if (i >= Size) + i %= Size; + return _data[i]; +} + +template +T& Deque::at(size_t index) +{ + auto i = _tail + index; + if (i >= Size) + i %= Size; + return _data[i]; +} + +template +const T& Deque::at(size_t index) const +{ + auto i = _tail + index; + if (i >= Size) + i %= Size; + return _data[i]; +} + +template +void Deque::clear() +{ + _tail = -1; + _head = 0; +} + +END_APPLEMIDI_NAMESPACE diff --git a/MIDI_Interfaces/AppleMIDI/vendor/utility/endian.h b/MIDI_Interfaces/AppleMIDI/vendor/utility/endian.h new file mode 100644 index 0000000..b4de809 --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI/vendor/utility/endian.h @@ -0,0 +1,78 @@ +#pragma once + +#if !defined(_BYTE_ORDER) + + #define _BIG_ENDIAN 4321 + #define _LITTLE_ENDIAN 1234 + + #define TEST_LITTLE_ENDIAN (((union { unsigned x; unsigned char c; }){1}).c) + + #ifdef TEST_LITTLE_ENDIAN + #define _BYTE_ORDER _LITTLE_ENDIAN + #else + #define _BYTE_ORDER _BIG_ENDIAN + #endif + + #undef TEST_LITTLE_ENDIAN + + #include + + #ifdef __GNUC__ + #define __bswap16(_x) __builtin_bswap16(_x) + #define __bswap32(_x) __builtin_bswap32(_x) + #define __bswap64(_x) __builtin_bswap64(_x) + #else /* __GNUC__ */ + + static __inline __uint16_t + __bswap16(__uint16_t _x) + { + return ((__uint16_t)((_x >> 8) | ((_x << 8) & 0xff00))); + } + + static __inline __uint32_t + __bswap32(__uint32_t _x) + { + return ((__uint32_t)((_x >> 24) | ((_x >> 8) & 0xff00) | + ((_x << 8) & 0xff0000) | ((_x << 24) & 0xff000000))); + } + + static __inline __uint64_t + __bswap64(__uint64_t _x) + { + return ((__uint64_t)((_x >> 56) | ((_x >> 40) & 0xff00) | + ((_x >> 24) & 0xff0000) | ((_x >> 8) & 0xff000000) | + ((_x << 8) & ((__uint64_t)0xff << 32)) | + ((_x << 24) & ((__uint64_t)0xff << 40)) | + ((_x << 40) & ((__uint64_t)0xff << 48)) | ((_x << 56)))); + } + #endif /* !__GNUC__ */ + + #ifndef __machine_host_to_from_network_defined + #if _BYTE_ORDER == _LITTLE_ENDIAN + #define __ntohs(x) __bswap16(x) + #define __htons(x) __bswap16(x) + #define __ntohl(x) __bswap32(x) + #define __htonl(x) __bswap32(x) + #define __ntohll(x) __bswap64(x) + #define __htonll(x) __bswap64(x) + #else // BIG_ENDIAN + #define __ntohl(x) ((uint32_t)(x)) + #define __ntohs(x) ((uint16_t)(x)) + #define __htonl(x) ((uint32_t)(x)) + #define __htons(x) ((uint16_t)(x)) + #define __ntohll(x) ((uint64_t)(x)) + #define __htonll(x) ((uint64_t)(x)) + #endif + #endif /* __machine_host_to_from_network_defined */ + +#endif /* _BYTE_ORDER */ + +#ifndef __machine_host_to_from_network_defined +#if _BYTE_ORDER == _LITTLE_ENDIAN +#define __ntohll(x) __bswap64(x) +#define __htonll(x) __bswap64(x) +#else // BIG_ENDIAN +#define __ntohll(x) ((uint64_t)(x)) +#define __htonll(x) ((uint64_t)(x)) +#endif +#endif /* __machine_host_to_from_network_defined */ diff --git a/MIDI_Interfaces/AppleMIDI_Interface.cpp b/MIDI_Interfaces/AppleMIDI_Interface.cpp new file mode 100644 index 0000000..c6c9417 --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI_Interface.cpp @@ -0,0 +1,80 @@ +#include "AppleMIDI_Interface.hpp" + +#include "lwip/apps/mdns.h" +#include "pico/cyw43_arch.h" + +BEGIN_CS_NAMESPACE + +AppleMIDI_Interface::AppleMIDI_Interface(const char *name, uint16_t port) + : session_(name, port), sessionName_(name), port_(port) {} + +void AppleMIDI_Interface::begin() { + session_.setName(sessionName_); + session_.setPort(port_); + session_.begin(); + initMDNS(); +} + +void AppleMIDI_Interface::initMDNS() { + cyw43_arch_lwip_begin(); + mdns_resp_init(); + struct netif *nif = &cyw43_state.netif[CYW43_ITF_STA]; + mdns_resp_add_netif(nif, sessionName_); + mdns_resp_add_service(nif, sessionName_, "_apple-midi", + DNSSD_PROTO_UDP, port_, nullptr, nullptr); + cyw43_arch_lwip_end(); +} + +void AppleMIDI_Interface::update() { + session_.available(); + MIDI_Interface::updateIncoming(this); +} + +MIDIReadEvent AppleMIDI_Interface::read() { + return parser_.pull(AppleMIDIBytePuller{session_}); +} + +ChannelMessage AppleMIDI_Interface::getChannelMessage() const { + return parser_.getChannelMessage(); +} + +SysCommonMessage AppleMIDI_Interface::getSysCommonMessage() const { + return parser_.getSysCommonMessage(); +} + +RealTimeMessage AppleMIDI_Interface::getRealTimeMessage() const { + return parser_.getRealTimeMessage(); +} + +SysExMessage AppleMIDI_Interface::getSysExMessage() const { + return parser_.getSysExMessage(); +} + +void AppleMIDI_Interface::sendChannelMessageImpl(ChannelMessage msg) { + session_.beginTransmission(MIDI_NAMESPACE::MidiType::InvalidType); + session_.write(msg.header); + session_.write(msg.data1); + if (msg.hasTwoDataBytes()) + session_.write(msg.data2); +} + +void AppleMIDI_Interface::sendSysCommonImpl(SysCommonMessage msg) { + session_.beginTransmission(MIDI_NAMESPACE::MidiType::InvalidType); + session_.write(msg.header); + uint8_t ndata = msg.getNumberOfDataBytes(); + if (ndata >= 1) session_.write(msg.data1); + if (ndata >= 2) session_.write(msg.data2); +} + +void AppleMIDI_Interface::sendSysExImpl(SysExMessage msg) { + session_.beginTransmission(MIDI_NAMESPACE::MidiType::SystemExclusive); + for (uint16_t i = 0; i < msg.length; i++) + session_.write(msg.data[i]); +} + +void AppleMIDI_Interface::sendRealTimeImpl(RealTimeMessage msg) { + session_.beginTransmission(MIDI_NAMESPACE::MidiType::InvalidType); + session_.write(msg.message); +} + +END_CS_NAMESPACE diff --git a/MIDI_Interfaces/AppleMIDI_Interface.hpp b/MIDI_Interfaces/AppleMIDI_Interface.hpp new file mode 100644 index 0000000..66b52d4 --- /dev/null +++ b/MIDI_Interfaces/AppleMIDI_Interface.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include "MIDI_Interface.hpp" +#include +#include "AppleMIDI/LwIPUDP.hpp" +#include "AppleMIDI/vendor/AppleMIDI.h" + +BEGIN_CS_NAMESPACE + +using AppleMIDISession_t = APPLEMIDI_NAMESPACE::AppleMIDISession; + +struct AppleMIDIBytePuller { + AppleMIDISession_t &session; + bool pull(uint8_t &byte) { + if (session.available() == 0) + return false; + byte = session.read(); + return true; + } +}; + +class AppleMIDI_Interface : public MIDI_Interface { +public: + /// @param name Session name visible in macOS Audio MIDI Setup + /// @param port AppleMIDI control port (data port = port + 1) + AppleMIDI_Interface(const char *name = "PicoW", uint16_t port = 5004); + + void begin() override; + void update() override; + + MIDIReadEvent read(); + ChannelMessage getChannelMessage() const; + SysCommonMessage getSysCommonMessage() const; + RealTimeMessage getRealTimeMessage() const; + SysExMessage getSysExMessage() const; + + /// Must be called after WiFi is connected but before begin() + void setName(const char *name) { sessionName_ = name; } + +protected: + void sendChannelMessageImpl(ChannelMessage msg) override; + void sendSysCommonImpl(SysCommonMessage msg) override; + void sendSysExImpl(SysExMessage msg) override; + void sendRealTimeImpl(RealTimeMessage msg) override; + void sendNowImpl() override {} + +private: +#if !DISABLE_PIPES + void handleStall() override { MIDI_Interface::handleStall(this); } +#ifdef DEBUG_OUT + const char *getName() const override { return "applemidi"; } +#endif +#endif + + void initMDNS(); + + AppleMIDISession_t session_; + SerialMIDI_Parser parser_; + const char *sessionName_; + uint16_t port_; +}; + +END_CS_NAMESPACE diff --git a/MIDI_Interfaces/BLEMIDI/BTstack/advertising.cpp b/MIDI_Interfaces/BLEMIDI/BTstack/advertising.cpp index e663548..3c051f1 100644 --- a/MIDI_Interfaces/BLEMIDI/BTstack/advertising.cpp +++ b/MIDI_Interfaces/BLEMIDI/BTstack/advertising.cpp @@ -8,16 +8,31 @@ namespace cs::midi_ble_btstack { namespace { +#ifdef CS_MIDI_HID_MOUSE +uint8_t adv_data[] { + // Flags general discoverable + 0x02, BLUETOOTH_DATA_TYPE_FLAGS, 0x06, + // HID Service UUID (16-bit) + 0x03, BLUETOOTH_DATA_TYPE_COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, + 0x12, 0x18, + // Appearance: Mouse (0x03C2) + 0x03, BLUETOOTH_DATA_TYPE_APPEARANCE, 0xC2, 0x03, + // MIDI Service UUID (128-bit) + 0x11, BLUETOOTH_DATA_TYPE_COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, + 0x00, 0xc7, 0xc4, 0x4e, 0xe3, 0x6c, 0x51, 0xa7, 0x33, 0x4b, 0xe8, 0xed, + 0x5a, 0x0e, 0xb8, 0x03}; +#else uint8_t adv_data[] { // Flags general discoverable 0x02, BLUETOOTH_DATA_TYPE_FLAGS, 0x06, // Connection interval range 0x05, BLUETOOTH_DATA_TYPE_SLAVE_CONNECTION_INTERVAL_RANGE, 0x0c, 0x00, 0x0c, 0x00, - // Service UUID + // MIDI Service UUID (128-bit) 0x11, BLUETOOTH_DATA_TYPE_COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, 0x00, 0xc7, 0xc4, 0x4e, 0xe3, 0x6c, 0x51, 0xa7, 0x33, 0x4b, 0xe8, 0xed, 0x5a, 0x0e, 0xb8, 0x03}; +#endif static_assert(sizeof(adv_data) <= LE_ADVERTISING_DATA_SIZE); uint8_t adv_rsp_data[LE_ADVERTISING_DATA_SIZE] { // Name header @@ -27,6 +42,9 @@ uint8_t adv_rsp_data[LE_ADVERTISING_DATA_SIZE] { ' ', 'M', 'I', 'D', 'I'}; uint8_t adv_rsp_data_len() { return adv_rsp_data[0] + 1; } +#ifdef CS_MIDI_HID_MOUSE +void set_adv_connection_interval(uint16_t, uint16_t) {} +#else void set_adv_connection_interval(uint16_t min_itvl, uint16_t max_itvl) { uint8_t *slave_itvl_range = adv_data + 5; slave_itvl_range[0] = (min_itvl >> 0) & 0xFF; @@ -34,6 +52,7 @@ void set_adv_connection_interval(uint16_t min_itvl, uint16_t max_itvl) { slave_itvl_range[2] = (max_itvl >> 0) & 0xFF; slave_itvl_range[3] = (max_itvl >> 8) & 0xFF; } +#endif void set_adv_name(const char *name) { auto len = std::min(std::strlen(name), sizeof(adv_rsp_data) - 2); diff --git a/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.cpp b/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.cpp index 8905d49..8aef9e6 100644 --- a/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.cpp +++ b/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi.cpp @@ -11,7 +11,12 @@ #include "../BLEAPI.hpp" #include "advertising.hpp" +#ifdef CS_MIDI_HID_MOUSE +#include "gatt_midi_hog.h" +#include +#else #include "gatt_midi.h" +#endif #include "hci_event_names.hpp" #include @@ -31,6 +36,47 @@ BLESettings settings; btstack_packet_callback_registration_t hci_event_callback_registration; btstack_packet_callback_registration_t sm_event_callback_registration; +#ifdef CS_MIDI_HID_MOUSE +const uint8_t hid_descriptor_mouse[] = { + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x09, 0x02, // Usage (Mouse) + 0xa1, 0x01, // Collection (Application) + 0x85, 0x01, // Report ID 1 + 0x09, 0x01, // Usage (Pointer) + 0xa1, 0x00, // Collection (Physical) + 0x05, 0x09, // Usage Page (Buttons) + 0x19, 0x01, // Usage Minimum (1) + 0x29, 0x03, // Usage Maximum (3) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x01, // Logical Maximum (1) + 0x95, 0x03, // Report Count (3) + 0x75, 0x01, // Report Size (1) + 0x81, 0x02, // Input (Data,Var,Abs) + 0x95, 0x01, // Report Count (1) + 0x75, 0x05, // Report Size (5) + 0x81, 0x03, // Input (Const,Var,Abs) — padding + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x09, 0x30, // Usage (X) + 0x09, 0x31, // Usage (Y) + 0x15, 0x81, // Logical Minimum (-127) + 0x25, 0x7f, // Logical Maximum (127) + 0x75, 0x08, // Report Size (8) + 0x95, 0x02, // Report Count (2) + 0x81, 0x06, // Input (Data,Var,Rel) + 0xc0, // End Collection + 0xc0 // End Collection +}; + +constexpr uint16_t gap_device_name_handle = + ATT_CHARACTERISTIC_GAP_DEVICE_NAME_01_VALUE_HANDLE; + +void hid_packet_handler(uint8_t packet_type, uint16_t, uint8_t *packet, uint16_t) { + if (packet_type != HCI_EVENT_PACKET) return; + if (hci_event_packet_get_type(packet) != HCI_EVENT_HIDS_META) return; + // Accept HID events but never send mouse reports +} +#endif + // callback/event functions // HCI_SUBEVENT_LE_CONNECTION_COMPLETE @@ -179,8 +225,20 @@ uint16_t att_read_callback([[maybe_unused]] hci_con_handle_t connection_handle, [[maybe_unused]] uint16_t offset, [[maybe_unused]] uint8_t *buffer, [[maybe_unused]] uint16_t buffer_size) { +#ifdef CS_MIDI_HID_MOUSE + if (att_handle == gap_device_name_handle) { + const char *name = settings.device_name; + auto len = static_cast(std::strlen(name)); + if (buffer) { + auto copy = static_cast(std::min(len - offset, buffer_size)); + std::memcpy(buffer, name + offset, copy); + return copy; + } + return len; + } +#endif if (att_handle == midi_char_value_handle) - return 0; // MIDI always responds with no data + return 0; return 0; } @@ -236,6 +294,10 @@ void le_midi_setup(const BLESettings &ble_settings) { SM_AUTHREQ_BONDING); // setup ATT server att_server_init(profile_data, att_read_callback, att_write_callback); +#ifdef CS_MIDI_HID_MOUSE + hids_device_init(0, hid_descriptor_mouse, sizeof(hid_descriptor_mouse)); + hids_device_register_packet_handler(hid_packet_handler); +#endif // setup advertisements le_midi_setup_adv(ble_settings); // register for HCI events diff --git a/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi_hog.gatt b/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi_hog.gatt new file mode 100644 index 0000000..e505a1b --- /dev/null +++ b/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi_hog.gatt @@ -0,0 +1,14 @@ +PRIMARY_SERVICE, GATT_SERVICE +CHARACTERISTIC, GATT_DATABASE_HASH, READ, + +PRIMARY_SERVICE, GAP_SERVICE +CHARACTERISTIC, GAP_DEVICE_NAME, READ | DYNAMIC, +// 0x03C2 = HID Mouse +CHARACTERISTIC, GAP_APPEARANCE, READ, C2 03 + +// MIDI Service +PRIMARY_SERVICE, 03B80E5A-EDE8-4B33-A751-6CE34EC4C700 +CHARACTERISTIC, 7772E5DB-3868-4112-A1A9-F2669D106BF3, READ | WRITE_WITHOUT_RESPONSE | NOTIFY | DYNAMIC, + +// HID Service +#import diff --git a/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi_hog.h b/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi_hog.h new file mode 100644 index 0000000..60de924 --- /dev/null +++ b/MIDI_Interfaces/BLEMIDI/BTstack/gatt_midi_hog.h @@ -0,0 +1,147 @@ +// Generated from gatt_midi_hog.gatt — MIDI + HoG Mouse combined GATT database +// Regenerate: compile_gatt.py gatt_midi_hog.gatt gatt_midi_hog.h + +// att db format version 1 +// binary attribute representation: +// - size in bytes (16), flags(16), handle (16), uuid (16/128), value(...) + +#include + +#if __cplusplus >= 200704L +constexpr +#endif +const uint8_t profile_data[] = +{ + // ATT DB Version + 1, + + // 0x0001 PRIMARY_SERVICE-GATT_SERVICE + 0x0a, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x28, 0x01, 0x18, + // 0x0002 CHARACTERISTIC-GATT_DATABASE_HASH - READ + 0x0d, 0x00, 0x02, 0x00, 0x02, 0x00, 0x03, 0x28, 0x02, 0x03, 0x00, 0x2a, 0x2b, + // 0x0003 VALUE CHARACTERISTIC-GATT_DATABASE_HASH - READ + 0x18, 0x00, 0x02, 0x00, 0x03, 0x00, 0x2a, 0x2b, 0xce, 0x0f, 0xc6, 0xd0, 0xa1, 0x9c, 0xd7, 0xa2, 0x98, 0xad, 0x7d, 0x4c, 0x9c, 0x85, 0xed, 0x81, + + // 0x0004 PRIMARY_SERVICE-GAP_SERVICE + 0x0a, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x28, 0x00, 0x18, + // 0x0005 CHARACTERISTIC-GAP_DEVICE_NAME - READ | DYNAMIC + 0x0d, 0x00, 0x02, 0x00, 0x05, 0x00, 0x03, 0x28, 0x02, 0x06, 0x00, 0x00, 0x2a, + // 0x0006 VALUE CHARACTERISTIC-GAP_DEVICE_NAME - READ | DYNAMIC + 0x08, 0x00, 0x02, 0x01, 0x06, 0x00, 0x00, 0x2a, + // 0x0007 CHARACTERISTIC-GAP_APPEARANCE - READ - Mouse (0x03C2) + 0x0d, 0x00, 0x02, 0x00, 0x07, 0x00, 0x03, 0x28, 0x02, 0x08, 0x00, 0x01, 0x2a, + // 0x0008 VALUE CHARACTERISTIC-GAP_APPEARANCE - READ + 0x0a, 0x00, 0x02, 0x00, 0x08, 0x00, 0x01, 0x2a, 0xC2, 0x03, + + // MIDI Service + // 0x0009 PRIMARY_SERVICE-03B80E5A-EDE8-4B33-A751-6CE34EC4C700 + 0x18, 0x00, 0x02, 0x00, 0x09, 0x00, 0x00, 0x28, 0x00, 0xc7, 0xc4, 0x4e, 0xe3, 0x6c, 0x51, 0xa7, 0x33, 0x4b, 0xe8, 0xed, 0x5a, 0x0e, 0xb8, 0x03, + // 0x000a CHARACTERISTIC-7772E5DB-3868-4112-A1A9-F2669D106BF3 - READ | WRITE_WITHOUT_RESPONSE | NOTIFY | DYNAMIC + 0x1b, 0x00, 0x02, 0x00, 0x0a, 0x00, 0x03, 0x28, 0x16, 0x0b, 0x00, 0xf3, 0x6b, 0x10, 0x9d, 0x66, 0xf2, 0xa9, 0xa1, 0x12, 0x41, 0x68, 0x38, 0xdb, 0xe5, 0x72, 0x77, + // 0x000b VALUE CHARACTERISTIC-7772E5DB-3868-4112-A1A9-F2669D106BF3 - READ | WRITE_WITHOUT_RESPONSE | NOTIFY | DYNAMIC + 0x16, 0x00, 0x06, 0x03, 0x0b, 0x00, 0xf3, 0x6b, 0x10, 0x9d, 0x66, 0xf2, 0xa9, 0xa1, 0x12, 0x41, 0x68, 0x38, 0xdb, 0xe5, 0x72, 0x77, + // 0x000c CLIENT_CHARACTERISTIC_CONFIGURATION + 0x0a, 0x00, 0x0e, 0x01, 0x0c, 0x00, 0x02, 0x29, 0x00, 0x00, + + // HID Service (hids.gatt) + // 0x000d PRIMARY_SERVICE-ORG_BLUETOOTH_SERVICE_HUMAN_INTERFACE_DEVICE + 0x0a, 0x00, 0x02, 0x00, 0x0d, 0x00, 0x00, 0x28, 0x12, 0x18, + // 0x000e CHARACTERISTIC-PROTOCOL_MODE - DYNAMIC | READ | WRITE_WITHOUT_RESPONSE + 0x0d, 0x00, 0x02, 0x00, 0x0e, 0x00, 0x03, 0x28, 0x06, 0x0f, 0x00, 0x4e, 0x2a, + // 0x000f VALUE CHARACTERISTIC-PROTOCOL_MODE + 0x08, 0x00, 0x06, 0x01, 0x0f, 0x00, 0x4e, 0x2a, + // 0x0010 CHARACTERISTIC-REPORT (Input, ID=1) - DYNAMIC | READ | WRITE | NOTIFY | ENCRYPTION_KEY_SIZE_16 + 0x0d, 0x00, 0x02, 0x00, 0x10, 0x00, 0x03, 0x28, 0x1a, 0x11, 0x00, 0x4d, 0x2a, + // 0x0011 VALUE CHARACTERISTIC-REPORT + 0x08, 0x00, 0x0b, 0xf5, 0x11, 0x00, 0x4d, 0x2a, + // 0x0012 CLIENT_CHARACTERISTIC_CONFIGURATION + 0x0a, 0x00, 0x0f, 0xf1, 0x12, 0x00, 0x02, 0x29, 0x00, 0x00, + // 0x0013 REPORT_REFERENCE - Report ID=1, Type=Input(1) + 0x0a, 0x00, 0x02, 0x00, 0x13, 0x00, 0x08, 0x29, 0x1, 0x1, + // 0x0014 CHARACTERISTIC-REPORT (Output, ID=2) - DYNAMIC | READ | WRITE | WRITE_WITHOUT_RESPONSE | ENCRYPTION_KEY_SIZE_16 + 0x0d, 0x00, 0x02, 0x00, 0x14, 0x00, 0x03, 0x28, 0x0e, 0x15, 0x00, 0x4d, 0x2a, + // 0x0015 VALUE CHARACTERISTIC-REPORT + 0x08, 0x00, 0x0f, 0xf5, 0x15, 0x00, 0x4d, 0x2a, + // 0x0016 REPORT_REFERENCE - Report ID=2, Type=Output(2) + 0x0a, 0x00, 0x02, 0x00, 0x16, 0x00, 0x08, 0x29, 0x2, 0x2, + // 0x0017 CHARACTERISTIC-REPORT (Feature, ID=3) - DYNAMIC | READ | WRITE | ENCRYPTION_KEY_SIZE_16 + 0x0d, 0x00, 0x02, 0x00, 0x17, 0x00, 0x03, 0x28, 0x0a, 0x18, 0x00, 0x4d, 0x2a, + // 0x0018 VALUE CHARACTERISTIC-REPORT + 0x08, 0x00, 0x0b, 0xf5, 0x18, 0x00, 0x4d, 0x2a, + // 0x0019 REPORT_REFERENCE - Report ID=3, Type=Feature(3) + 0x0a, 0x00, 0x02, 0x00, 0x19, 0x00, 0x08, 0x29, 0x3, 0x3, + // 0x001a CHARACTERISTIC-REPORT_MAP - DYNAMIC | READ + 0x0d, 0x00, 0x02, 0x00, 0x1a, 0x00, 0x03, 0x28, 0x02, 0x1b, 0x00, 0x4b, 0x2a, + // 0x001b VALUE CHARACTERISTIC-REPORT_MAP + 0x08, 0x00, 0x02, 0x01, 0x1b, 0x00, 0x4b, 0x2a, + // 0x001c CHARACTERISTIC-BOOT_KEYBOARD_INPUT_REPORT - DYNAMIC | READ | WRITE | NOTIFY + 0x0d, 0x00, 0x02, 0x00, 0x1c, 0x00, 0x03, 0x28, 0x1a, 0x1d, 0x00, 0x22, 0x2a, + // 0x001d VALUE CHARACTERISTIC-BOOT_KEYBOARD_INPUT_REPORT + 0x08, 0x00, 0x0a, 0x01, 0x1d, 0x00, 0x22, 0x2a, + // 0x001e CLIENT_CHARACTERISTIC_CONFIGURATION + 0x0a, 0x00, 0x0e, 0x01, 0x1e, 0x00, 0x02, 0x29, 0x00, 0x00, + // 0x001f CHARACTERISTIC-BOOT_KEYBOARD_OUTPUT_REPORT - DYNAMIC | READ | WRITE | WRITE_WITHOUT_RESPONSE + 0x0d, 0x00, 0x02, 0x00, 0x1f, 0x00, 0x03, 0x28, 0x0e, 0x20, 0x00, 0x32, 0x2a, + // 0x0020 VALUE CHARACTERISTIC-BOOT_KEYBOARD_OUTPUT_REPORT + 0x08, 0x00, 0x0e, 0x01, 0x20, 0x00, 0x32, 0x2a, + // 0x0021 CHARACTERISTIC-BOOT_MOUSE_INPUT_REPORT - DYNAMIC | READ | WRITE | NOTIFY + 0x0d, 0x00, 0x02, 0x00, 0x21, 0x00, 0x03, 0x28, 0x1a, 0x22, 0x00, 0x33, 0x2a, + // 0x0022 VALUE CHARACTERISTIC-BOOT_MOUSE_INPUT_REPORT + 0x08, 0x00, 0x0a, 0x01, 0x22, 0x00, 0x33, 0x2a, + // 0x0023 CLIENT_CHARACTERISTIC_CONFIGURATION + 0x0a, 0x00, 0x0e, 0x01, 0x23, 0x00, 0x02, 0x29, 0x00, 0x00, + // 0x0024 CHARACTERISTIC-HID_INFORMATION - READ + 0x0d, 0x00, 0x02, 0x00, 0x24, 0x00, 0x03, 0x28, 0x02, 0x25, 0x00, 0x4a, 0x2a, + // 0x0025 VALUE CHARACTERISTIC-HID_INFORMATION - bcdHID=0x0101, country=0, flags=0x02 + 0x0c, 0x00, 0x02, 0x00, 0x25, 0x00, 0x4a, 0x2a, 0x01, 0x01, 0x00, 0x02, + // 0x0026 CHARACTERISTIC-HID_CONTROL_POINT - DYNAMIC | WRITE_WITHOUT_RESPONSE + 0x0d, 0x00, 0x02, 0x00, 0x26, 0x00, 0x03, 0x28, 0x04, 0x27, 0x00, 0x4c, 0x2a, + // 0x0027 VALUE CHARACTERISTIC-HID_CONTROL_POINT + 0x08, 0x00, 0x04, 0x01, 0x27, 0x00, 0x4c, 0x2a, + + // END + 0x00, 0x00, +}; // total size 252 bytes + + +// +// list service handle ranges +// +#define ATT_SERVICE_GATT_SERVICE_START_HANDLE 0x0001 +#define ATT_SERVICE_GATT_SERVICE_END_HANDLE 0x0003 +#define ATT_SERVICE_GATT_SERVICE_01_START_HANDLE 0x0001 +#define ATT_SERVICE_GATT_SERVICE_01_END_HANDLE 0x0003 +#define ATT_SERVICE_GAP_SERVICE_START_HANDLE 0x0004 +#define ATT_SERVICE_GAP_SERVICE_END_HANDLE 0x0008 +#define ATT_SERVICE_GAP_SERVICE_01_START_HANDLE 0x0004 +#define ATT_SERVICE_GAP_SERVICE_01_END_HANDLE 0x0008 +#define ATT_SERVICE_03B80E5A_EDE8_4B33_A751_6CE34EC4C700_START_HANDLE 0x0009 +#define ATT_SERVICE_03B80E5A_EDE8_4B33_A751_6CE34EC4C700_END_HANDLE 0x000c +#define ATT_SERVICE_03B80E5A_EDE8_4B33_A751_6CE34EC4C700_01_START_HANDLE 0x0009 +#define ATT_SERVICE_03B80E5A_EDE8_4B33_A751_6CE34EC4C700_01_END_HANDLE 0x000c +#define ATT_SERVICE_ORG_BLUETOOTH_SERVICE_HUMAN_INTERFACE_DEVICE_START_HANDLE 0x000d +#define ATT_SERVICE_ORG_BLUETOOTH_SERVICE_HUMAN_INTERFACE_DEVICE_END_HANDLE 0x0027 +#define ATT_SERVICE_ORG_BLUETOOTH_SERVICE_HUMAN_INTERFACE_DEVICE_01_START_HANDLE 0x000d +#define ATT_SERVICE_ORG_BLUETOOTH_SERVICE_HUMAN_INTERFACE_DEVICE_01_END_HANDLE 0x0027 + +// +// list mapping between characteristics and handles +// +#define ATT_CHARACTERISTIC_GATT_DATABASE_HASH_01_VALUE_HANDLE 0x0003 +#define ATT_CHARACTERISTIC_GAP_DEVICE_NAME_01_VALUE_HANDLE 0x0006 +#define ATT_CHARACTERISTIC_GAP_APPEARANCE_01_VALUE_HANDLE 0x0008 +#define ATT_CHARACTERISTIC_7772E5DB_3868_4112_A1A9_F2669D106BF3_01_VALUE_HANDLE 0x000b +#define ATT_CHARACTERISTIC_7772E5DB_3868_4112_A1A9_F2669D106BF3_01_CLIENT_CONFIGURATION_HANDLE 0x000c +#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_PROTOCOL_MODE_01_VALUE_HANDLE 0x000f +#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_REPORT_01_VALUE_HANDLE 0x0011 +#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_REPORT_01_CLIENT_CONFIGURATION_HANDLE 0x0012 +#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_REPORT_02_VALUE_HANDLE 0x0015 +#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_REPORT_03_VALUE_HANDLE 0x0018 +#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_REPORT_MAP_01_VALUE_HANDLE 0x001b +#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_BOOT_KEYBOARD_INPUT_REPORT_01_VALUE_HANDLE 0x001d +#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_BOOT_KEYBOARD_INPUT_REPORT_01_CLIENT_CONFIGURATION_HANDLE 0x001e +#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_BOOT_KEYBOARD_OUTPUT_REPORT_01_VALUE_HANDLE 0x0020 +#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_BOOT_MOUSE_INPUT_REPORT_01_VALUE_HANDLE 0x0022 +#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_BOOT_MOUSE_INPUT_REPORT_01_CLIENT_CONFIGURATION_HANDLE 0x0023 +#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_HID_INFORMATION_01_VALUE_HANDLE 0x0025 +#define ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_HID_CONTROL_POINT_01_VALUE_HANDLE 0x0027 diff --git a/cs_midi.h b/cs_midi.h index b8b9262..91b99bc 100644 --- a/cs_midi.h +++ b/cs_midi.h @@ -38,6 +38,10 @@ #include #endif +#ifdef CS_MIDI_APPLEMIDI +#include +#endif + // Control Surface singleton #include diff --git a/docs b/docs index a8d3b61..3927d07 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit a8d3b6177fd623f1af91947b22c9cb28188f0419 +Subproject commit 3927d073e938973be113ee08db4e596c7d853ad3 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ccee34e..58c833d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,9 +12,10 @@ set(CMAKE_CXX_STANDARD 17) pico_sdk_init() -set(CS_MIDI_BLE ON CACHE BOOL "" FORCE) -set(CS_MIDI_USB ON CACHE BOOL "" FORCE) -set(CS_MIDI_SERIAL ON CACHE BOOL "" FORCE) +set(CS_MIDI_BLE ON CACHE BOOL "" FORCE) +set(CS_MIDI_USB ON CACHE BOOL "" FORCE) +set(CS_MIDI_SERIAL ON CACHE BOOL "" FORCE) +set(CS_MIDI_APPLEMIDI ON CACHE BOOL "" FORCE) add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/.. ${CMAKE_CURRENT_BINARY_DIR}/cs_midi) diff --git a/tests/examples/CMakeLists.txt b/tests/examples/CMakeLists.txt index 50a7ef7..ffa2a62 100644 --- a/tests/examples/CMakeLists.txt +++ b/tests/examples/CMakeLists.txt @@ -34,6 +34,14 @@ if(CS_MIDI_SERIAL) target_link_libraries(pico_deps PRIVATE hardware_uart) endif() +if(CS_MIDI_APPLEMIDI) + target_link_libraries(pico_deps PRIVATE pico_lwip_mdns) + if(NOT CS_MIDI_BLE) + target_link_libraries(pico_deps PRIVATE + pico_cyw43_arch_lwip_threadsafe_background) + endif() +endif() + # --------------------------------------------------------------------------- # # Example sources # --------------------------------------------------------------------------- # @@ -102,6 +110,14 @@ if(CS_MIDI_BLE AND CS_MIDI_USB) list(APPEND EXAMPLE_SOURCES interfaces/dual_midi.cpp) endif() +if(CS_MIDI_APPLEMIDI) + list(APPEND EXAMPLE_SOURCES interfaces/applemidi.cpp) +endif() + +if(CS_MIDI_APPLEMIDI AND CS_MIDI_BLE) + list(APPEND EXAMPLE_SOURCES interfaces/applemidi_ble.cpp) +endif() + # --------------------------------------------------------------------------- # # OBJECT library — compile only, no linking. # Uses resolved includes/defs from pico_deps directly to avoid diff --git a/tests/examples/interfaces/applemidi.cpp b/tests/examples/interfaces/applemidi.cpp new file mode 100644 index 0000000..a66f554 --- /dev/null +++ b/tests/examples/interfaces/applemidi.cpp @@ -0,0 +1,29 @@ +// AppleMIDI — RTP-MIDI over WiFi. +// Appears as a Bonjour MIDI device in macOS Audio MIDI Setup, +// Windows rtpMIDI, and iOS. +// Requires WiFi connection before Control_Surface.begin(). + +#include "pico/stdlib.h" +#include "pico/cyw43_arch.h" +#include + +using namespace cs; + +AppleMIDI_Interface midi {"PicoW-MIDI", 5004}; + +NoteButton button {5, {MIDI_Notes::C[4], Channel_1}}; +CCRotaryEncoder enc {{0, 2}, {16, Channel_1}, 1, 4}; + +int main() { + stdio_init_all(); + if (cyw43_arch_init()) return 1; + + cyw43_arch_enable_sta_mode(); + cyw43_arch_wifi_connect_blocking("SSID", "PASS", CYW43_AUTH_WPA2_AES_PSK); + + Control_Surface.begin(); + while (true) { + Control_Surface.loop(); + sleep_ms(1); + } +} diff --git a/tests/examples/interfaces/applemidi_ble.cpp b/tests/examples/interfaces/applemidi_ble.cpp new file mode 100644 index 0000000..5ed60e5 --- /dev/null +++ b/tests/examples/interfaces/applemidi_ble.cpp @@ -0,0 +1,34 @@ +// Dual transport — BLE + AppleMIDI with pipe routing. +// Elements send to both interfaces simultaneously. +// BLE for direct iOS/macOS connection, AppleMIDI for DAW integration. + +#include "pico/stdlib.h" +#include "pico/cyw43_arch.h" +#include + +using namespace cs; + +BluetoothMIDI_Interface ble; +AppleMIDI_Interface applemidi {"PicoW-MIDI", 5004}; + +MIDI_PipeFactory<2> pipes; + +NoteButton button {5, {MIDI_Notes::C[4], Channel_1}}; +CCRotaryEncoder enc {{0, 2}, {16, Channel_1}, 1, 4}; + +int main() { + stdio_init_all(); + if (cyw43_arch_init()) return 1; + + cyw43_arch_enable_sta_mode(); + cyw43_arch_wifi_connect_blocking("SSID", "PASS", CYW43_AUTH_WPA2_AES_PSK); + + Control_Surface >> pipes >> ble; + Control_Surface >> pipes >> applemidi; + + Control_Surface.begin(); + while (true) { + Control_Surface.loop(); + sleep_ms(1); + } +} diff --git a/tests/lwipopts.h b/tests/lwipopts.h index d008790..cb6978a 100644 --- a/tests/lwipopts.h +++ b/tests/lwipopts.h @@ -13,7 +13,11 @@ #define LWIP_UDP 1 #define LWIP_TCP 1 #define MEM_SIZE 4096 -#define MEMP_NUM_UDP_PCB 6 +#define MEMP_NUM_UDP_PCB 8 #define MEMP_NUM_TCP_PCB 4 +#define LWIP_MDNS_RESPONDER 1 +#define MDNS_MAX_SERVICES 2 +#define LWIP_NUM_NETIF_CLIENT_DATA 1 + #endif