From f175b58cdb8796768893ad16d871617ecf6db3e6 Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 20:29:14 +0200 Subject: [PATCH 01/31] Reject unknown-SSRC MIDI and stop the RTP parser hanging on bad commands. Incoming RTP-MIDI could be delivered without a session, and a data byte with no running status or a LEN underflow could spin loop() forever. Co-authored-by: Cursor --- src/AppleMIDI.h | 1 + src/AppleMIDI.hpp | 58 ++++++++++++++++----------- src/rtpMIDI_Parser.h | 14 ++++++- src/rtpMIDI_Parser_CommandSection.hpp | 14 +++++-- 4 files changed, 59 insertions(+), 28 deletions(-) diff --git a/src/AppleMIDI.h b/src/AppleMIDI.h index 2c4b640..3f9f11b 100644 --- a/src/AppleMIDI.h +++ b/src/AppleMIDI.h @@ -329,6 +329,7 @@ class AppleMIDISession ssrc_t ssrc = 0; uint16_t port = DEFAULT_CONTROL_PORT; + bool _acceptIncomingMidi = false; #ifdef ONE_PARTICIPANT Participant participant; #else diff --git a/src/AppleMIDI.hpp b/src/AppleMIDI.hpp index 95f6c36..ddce169 100644 --- a/src/AppleMIDI.hpp +++ b/src/AppleMIDI.hpp @@ -1133,41 +1133,47 @@ void AppleMIDISession::ReceivedRtp(const Rtp_t& rt auto pParticipant = (participant.ssrc == rtp.ssrc) ? &participant : nullptr; #endif - if (nullptr != pParticipant) + _acceptIncomingMidi = (pParticipant != nullptr); + if (!_acceptIncomingMidi) { - if (pParticipant->doReceiverFeedback == false) - pParticipant->receiverFeedbackStartTime = now; - pParticipant->doReceiverFeedback = true; +#ifdef USE_EXT_CALLBACKS + if (nullptr != _exceptionCallback) + _exceptionCallback(ssrc, ParticipantNotFoundException, rtp.ssrc); +#endif + return; + } + + 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); - } + 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); + if (nullptr != _receivedRtpCallback) + _receivedRtpCallback(pParticipant->ssrc, rtp, latency); #endif - pParticipant->receiveSequenceNr = rtp.sequenceNr; - } - else - { - // TODO??? re-connect? - } + pParticipant->receiveSequenceNr = rtp.sequenceNr; } // Notify that a MIDI byte stream has started. template void AppleMIDISession::StartReceivedMidi() { + if (!_acceptIncomingMidi) + return; + #ifdef USE_EXT_CALLBACKS if (nullptr != _startReceivedMidiByteCallback) _startReceivedMidiByteCallback(ssrc); @@ -1178,6 +1184,9 @@ void AppleMIDISession::StartReceivedMidi() template void AppleMIDISession::ReceivedMidi(byte value) { + if (!_acceptIncomingMidi) + return; + #ifdef USE_EXT_CALLBACKS if (nullptr != _receivedMidiByteCallback) _receivedMidiByteCallback(ssrc, value); @@ -1190,6 +1199,9 @@ void AppleMIDISession::ReceivedMidi(byte value) template void AppleMIDISession::EndReceivedMidi() { + if (!_acceptIncomingMidi) + return; + #ifdef USE_EXT_CALLBACKS if (nullptr != _endReceivedMidiByteCallback) _endReceivedMidiByteCallback(ssrc); diff --git a/src/rtpMIDI_Parser.h b/src/rtpMIDI_Parser.h index 7d30cbd..5ebefcc 100644 --- a/src/rtpMIDI_Parser.h +++ b/src/rtpMIDI_Parser.h @@ -187,7 +187,19 @@ class rtpMIDIParser if (midiCommandLength > 0) { auto retVal = decodeMIDICommandSection(buffer); - if (retVal != parserReturn::Processed) return retVal; + if (retVal != parserReturn::Processed) + { + if (retVal != parserReturn::NotEnoughData) + { + _rtpHeadersComplete = false; + _journalSectionComplete = false; + _channelJournalSectionComplete = false; + _journalTotalChannels = 0; + midiCommandLength = 0; + runningstatus = 0; + } + return retVal; + } } // The payload MAY also contain a journal section. The journal section diff --git a/src/rtpMIDI_Parser_CommandSection.hpp b/src/rtpMIDI_Parser_CommandSection.hpp index e71d14e..a452216 100644 --- a/src/rtpMIDI_Parser_CommandSection.hpp +++ b/src/rtpMIDI_Parser_CommandSection.hpp @@ -49,8 +49,10 @@ parserReturn decodeMIDICommandSection(RtpBuffer_t &buffer) size_t consumed = 0; auto retVal = decodeTime(buffer, consumed); if (retVal != parserReturn::Processed) return retVal; + if (consumed == 0 || consumed > midiCommandLength) + return parserReturn::UnexpectedMidiData; - midiCommandLength -= consumed; + midiCommandLength = (uint16_t)(midiCommandLength - consumed); while (consumed--) buffer.pop_front(); } @@ -65,8 +67,12 @@ parserReturn decodeMIDICommandSection(RtpBuffer_t &buffer) cmdCount = 0; // avoid first command again return retVal; } + if (retVal != parserReturn::Processed) + return retVal; + if (consumed == 0 || consumed > midiCommandLength) + return parserReturn::UnexpectedMidiData; - midiCommandLength -= consumed; + midiCommandLength = (uint16_t)(midiCommandLength - consumed); while (consumed--) buffer.pop_front(); } @@ -124,9 +130,9 @@ parserReturn decodeMidi(RtpBuffer_t &buffer, uint8_t &runningstatus, size_t &con if ((octet & RTP_MIDI_COMMAND_STATUS_FLAG) == 0) { /* if we have no running status yet -> error */ - if (((runningstatus)&RTP_MIDI_COMMAND_STATUS_FLAG) == 0) + if ((runningstatus & RTP_MIDI_COMMAND_STATUS_FLAG) == 0) { - return parserReturn::Processed; + return parserReturn::UnexpectedMidiData; } /* our first octet is "virtual" coming from a preceding MIDI-command, * so actually we have not really consumed anything yet */ From 2fac894e48f8c0eda07cf2732678d5137b16706f Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 20:54:36 +0200 Subject: [PATCH 02/31] Parse Bitrate Receive Limit on listeners so initiator RL packets do not desync the control port. Co-authored-by: Cursor --- src/AppleMIDI_Parser.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AppleMIDI_Parser.h b/src/AppleMIDI_Parser.h index 60f4feb..be73088 100644 --- a/src/AppleMIDI_Parser.h +++ b/src/AppleMIDI_Parser.h @@ -381,6 +381,7 @@ class AppleMIDIParser return parserReturn::Processed; } +#endif else if (0 == memcmp(command, amBitrateReceiveLimit, sizeof(amBitrateReceiveLimit))) { AppleMIDI_BitrateReceiveLimit bitrateReceiveLimit; @@ -411,7 +412,6 @@ class AppleMIDIParser return parserReturn::Processed; } -#endif return parserReturn::UnexpectedData; } }; From 75e3200cb09c4f8c1a33fbdf73cccfd3eac6bfb5 Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 20:59:37 +0200 Subject: [PATCH 03/31] Update rtpMIDI_Parser_JournalSection.hpp --- src/rtpMIDI_Parser_JournalSection.hpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/rtpMIDI_Parser_JournalSection.hpp b/src/rtpMIDI_Parser_JournalSection.hpp index 55625ae..b8854b2 100644 --- a/src/rtpMIDI_Parser_JournalSection.hpp +++ b/src/rtpMIDI_Parser_JournalSection.hpp @@ -140,7 +140,10 @@ parserReturn decodeJournalSection(RtpBuffer_t &buffer) //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; + // LENGTH is 10 bits and includes the 3-octet header (RFC 4695) + uint16_t chanjourlen = (uint16_t)((chanflags & RTP_MIDI_CJ_MASK_LENGTH) >> 8); + if (chanjourlen < 3) + return parserReturn::UnexpectedJournalData; if ((chanflags & RTP_MIDI_CJ_FLAG_P)) { } @@ -170,9 +173,10 @@ parserReturn decodeJournalSection(RtpBuffer_t &buffer) } if (_bytesToFlush > 0) { - return parserReturn::NotEnoughData; + return parserReturn::NotEnoughData; // resume this channel on the next read } + _channelJournalSectionComplete = false; // next TOTCHAN journal, if any _journalTotalChannels--; } From 870ac989be10e2efd95fdc37aacb6aa181b66315 Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:02:04 +0200 Subject: [PATCH 04/31] fix: 16-bit sequence wrap looks like packet loss --- src/AppleMIDI.hpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/AppleMIDI.hpp b/src/AppleMIDI.hpp index ddce169..5c4a67f 100644 --- a/src/AppleMIDI.hpp +++ b/src/AppleMIDI.hpp @@ -1155,9 +1155,14 @@ void AppleMIDISession::ReceivedRtp(const Rtp_t& rt // 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); + else + { + // RFC 3550 serial arithmetic: uint16 wrap 65535 -> 0 is delta 1, not loss. + int16_t delta = (int16_t)(rtp.sequenceNr - pParticipant->receiveSequenceNr); + if (delta > 1) { + if (nullptr != _exceptionCallback) + _exceptionCallback(ssrc, ReceivedPacketsDropped, (int32_t)(delta - 1)); + } } if (nullptr != _receivedRtpCallback) From 385fef08af7f9d6312c3277a91ecaeee009a85c8 Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:06:04 +0200 Subject: [PATCH 05/31] fix Session timeout uses 32-bit millis (must be 64bit) --- src/AppleMIDI.cpp | 13 ++++++++++++- src/AppleMIDI.h | 5 +++-- src/AppleMIDI_Participant.h | 6 +++--- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/AppleMIDI.cpp b/src/AppleMIDI.cpp index 3079b40..17820a0 100644 --- a/src/AppleMIDI.cpp +++ b/src/AppleMIDI.cpp @@ -2,6 +2,17 @@ BEGIN_APPLEMIDI_NAMESPACE -unsigned long now = 0; +static uint32_t millisLow32 = 0; +static uint32_t millisHigh32 = 0; +uint64_t now = 0; + +void refreshNow() +{ + uint32_t low = millis(); + if (low < millisLow32) + millisHigh32++; + millisLow32 = low; + now = ((uint64_t)millisHigh32 << 32) | (uint64_t)low; +} END_APPLEMIDI_NAMESPACE diff --git a/src/AppleMIDI.h b/src/AppleMIDI.h index 3f9f11b..c31047e 100644 --- a/src/AppleMIDI.h +++ b/src/AppleMIDI.h @@ -26,7 +26,8 @@ using namespace MIDI_NAMESPACE; BEGIN_APPLEMIDI_NAMESPACE -extern unsigned long now; +extern uint64_t now; +void refreshNow(); struct AppleMIDISettings : public MIDI_NAMESPACE::DefaultSettings { @@ -262,7 +263,7 @@ class AppleMIDISession // MIDI-read() must be called at the start of loop() unsigned available() { - now = millis(); + refreshNow(); #ifdef APPLEMIDI_INITIATOR manageSessionInvites(); diff --git a/src/AppleMIDI_Participant.h b/src/AppleMIDI_Participant.h index 0f6c36d..1293a49 100644 --- a/src/AppleMIDI_Participant.h +++ b/src/AppleMIDI_Participant.h @@ -14,18 +14,18 @@ struct Participant IPAddress remoteIP = INADDR_NONE; uint16_t remotePort = 0; - unsigned long receiverFeedbackStartTime = 0; + uint64_t receiverFeedbackStartTime = 0; bool doReceiverFeedback = false; uint16_t sendSequenceNr = 0; // seeded when session/participant is created uint16_t receiveSequenceNr = 0; - unsigned long lastSyncExchangeTime = 0; + uint64_t lastSyncExchangeTime = 0; #ifdef APPLEMIDI_INITIATOR uint8_t connectionAttempts = 0; uint32_t initiatorToken = 0; - unsigned long lastInviteSentTime = 0; + uint64_t lastInviteSentTime = 0; InviteStatus invitationStatus = Initiating; uint8_t synchronizationHeartBeats = 0; From c3f53d60f26a3f6735c204a3846d5a85669e0d6e Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:09:00 +0200 Subject: [PATCH 06/31] fix for Outgoing MIDI is gated on the last UDP source --- src/AppleMIDI.h | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/AppleMIDI.h b/src/AppleMIDI.h index c31047e..cb9d149 100644 --- a/src/AppleMIDI.h +++ b/src/AppleMIDI.h @@ -220,9 +220,16 @@ class AppleMIDISession // 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); + for (size_t i = 0; i < participants.size(); i++) + { + if (participants[i].ssrc != 0 && + participants[i].remoteIP != (IPAddress)INADDR_NONE) + return true; + } + return false; #else - return (dataPort.remoteIP() != (IPAddress)INADDR_NONE && participant.ssrc != 0); + return (participant.ssrc != 0 && + participant.remoteIP != (IPAddress)INADDR_NONE); #endif }; From 6cb30f21dfa2abe214388360cabddd2e27447e58 Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:14:09 +0200 Subject: [PATCH 07/31] UDP datagrams are treated as a byte stream AppleMIDI and RTP-MIDI are one message per UDP datagram. The reader currently calls parsePacket() even when the buffer still holds leftover bytes, so datagram N+1 is glued onto an incomplete parse of N. Keep same-datagram chunking (that is how large SysEx fits in a 64-byte buffer). Only stop cross-datagram concat. --- src/AppleMIDI.h | 2 + src/AppleMIDI.hpp | 113 +++++++++++++++++++++++++++---------------- src/rtpMIDI_Parser.h | 2 + 3 files changed, 75 insertions(+), 42 deletions(-) diff --git a/src/AppleMIDI.h b/src/AppleMIDI.h index cb9d149..95d3ad3 100644 --- a/src/AppleMIDI.h +++ b/src/AppleMIDI.h @@ -351,6 +351,8 @@ class AppleMIDISession private: size_t readControlPackets(); size_t readDataPackets(); + size_t readUdpDatagram(UdpClass &, RtpBuffer_t &); + void drainUdpRemainder(UdpClass &); void parseControlPackets(); void parseDataPackets(); diff --git a/src/AppleMIDI.hpp b/src/AppleMIDI.hpp index 5c4a67f..a0eabe0 100644 --- a/src/AppleMIDI.hpp +++ b/src/AppleMIDI.hpp @@ -5,24 +5,44 @@ BEGIN_APPLEMIDI_NAMESPACE -// Read pending control UDP packets into the control buffer. +// Read one UDP datagram into buffer without concatenating the next packet. template -size_t AppleMIDISession::readControlPackets() +size_t AppleMIDISession::readUdpDatagram(UdpClass &port, RtpBuffer_t &buffer) { - size_t packetSize = controlPort.available(); - if (packetSize == 0) - packetSize = controlPort.parsePacket(); + size_t remaining = port.available(); // unread tail of the current datagram - while (packetSize > 0 && !controlBuffer.full()) + if (remaining == 0) { - auto bytesToRead = min( min(packetSize, controlBuffer.free()), sizeof(packetBuffer)); - auto bytesRead = controlPort.read(packetBuffer, bytesToRead); - packetSize -= bytesRead; + // Current datagram is fully pulled from the socket. + // Do not start the next one while leftover bytes are still being parsed. + if (!buffer.empty()) + return buffer.size(); - controlBuffer.push_back(packetBuffer, bytesRead); + remaining = port.parsePacket(); + } + + while (remaining > 0 && !buffer.full()) + { + auto bytesToRead = min(min(remaining, buffer.free()), sizeof(packetBuffer)); + auto bytesRead = port.read(packetBuffer, bytesToRead); + remaining -= bytesRead; + buffer.push_back(packetBuffer, bytesRead); } - return controlBuffer.size(); + return buffer.size(); +} + +template +void AppleMIDISession::drainUdpRemainder(UdpClass &port) +{ + while (port.available() > 0 && port.read() >= 0) {} +} + +// Read pending control UDP packets into the control buffer. +template +size_t AppleMIDISession::readControlPackets() +{ + return readUdpDatagram(controlPort, controlBuffer); } // Parse buffered control packets and handle errors. @@ -32,10 +52,17 @@ void AppleMIDISession::parseControlPackets() while (controlBuffer.size() > 0) { auto retVal = _appleMIDIParser.parse(controlBuffer, amPortType::Control); - if (retVal == parserReturn::Processed - || retVal == parserReturn::NotEnoughData - || retVal == parserReturn::NotSureGiveMeMoreData) + if (retVal == parserReturn::Processed) { + if (controlBuffer.empty()) + drainUdpRemainder(controlPort); + break; + } + if (retVal == parserReturn::NotEnoughData + || retVal == parserReturn::NotSureGiveMeMoreData) + { + if (controlPort.available() == 0) + controlBuffer.clear(); // truncated/incomplete datagram break; } else if (retVal == parserReturn::UnexpectedData) @@ -48,8 +75,7 @@ void AppleMIDISession::parseControlPackets() } else if (retVal == parserReturn::SessionNameVeryLong) { - // purge the rest of the data in controlPort - while (controlPort.read() >= 0) {} + drainUdpRemainder(controlPort); } } } @@ -58,20 +84,7 @@ void AppleMIDISession::parseControlPackets() template size_t AppleMIDISession::readDataPackets() { - size_t packetSize = dataPort.available(); - if (packetSize == 0) - packetSize = dataPort.parsePacket(); - - while (packetSize > 0 && !dataBuffer.full()) - { - auto bytesToRead = min( min(packetSize, dataBuffer.free()), sizeof(packetBuffer)); - auto bytesRead = dataPort.read(packetBuffer, bytesToRead); - packetSize -= bytesRead; - - dataBuffer.push_back(packetBuffer, bytesRead); - } - - return dataBuffer.size(); + return readUdpDatagram(dataPort, dataBuffer); } // Parse buffered data packets using RTP-MIDI and AppleMIDI parsers. @@ -81,25 +94,41 @@ void AppleMIDISession::parseDataPackets() while (dataBuffer.size() > 0) { auto retVal1 = _rtpMIDIParser.parse(dataBuffer); - if (retVal1 == parserReturn::Processed - || retVal1 == parserReturn::NotEnoughData) + if (retVal1 == parserReturn::Processed) + { + if (dataBuffer.empty()) + drainUdpRemainder(dataPort); break; - + } + if (retVal1 == parserReturn::NotEnoughData) + { + if (dataPort.available() == 0 && !_rtpMIDIParser.isMidMessage()) + dataBuffer.clear(); + break; + } + auto retVal2 = _appleMIDIParser.parse(dataBuffer, amPortType::Data); - if (retVal2 == parserReturn::Processed - || retVal2 == parserReturn::NotEnoughData) + if (retVal2 == parserReturn::Processed) + { + if (dataBuffer.empty()) + drainUdpRemainder(dataPort); break; + } + if (retVal2 == parserReturn::NotEnoughData) + { + if (dataPort.available() == 0) + dataBuffer.clear(); + break; + } - // // both don't have data to determine protocol if (retVal1 == parserReturn::NotSureGiveMeMoreData - && retVal2 == parserReturn::NotSureGiveMeMoreData) + || retVal2 == parserReturn::NotSureGiveMeMoreData) + { + if (dataPort.available() == 0 && !_rtpMIDIParser.isMidMessage()) + dataBuffer.clear(); 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); diff --git a/src/rtpMIDI_Parser.h b/src/rtpMIDI_Parser.h index 5ebefcc..4510aa3 100644 --- a/src/rtpMIDI_Parser.h +++ b/src/rtpMIDI_Parser.h @@ -51,6 +51,8 @@ class rtpMIDIParser public: AppleMIDISession * session; + + bool isMidMessage() const { return _rtpHeadersComplete; } // Parse the incoming string // return: From ff23a5b5766cb9cc0bb952d3adc20cf9cac6c5a1 Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:18:58 +0200 Subject: [PATCH 08/31] fix Invitation parser consumes the rest of the buffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Invitation parser eats the rest of the buffer (AppleMIDI_Parser.h) — stop the session name at NUL so leftover datagram bytes are not swallowed. Highest impact now that datagrams are no longer concatenated. --- src/AppleMIDI_Parser.h | 66 +++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/src/AppleMIDI_Parser.h b/src/AppleMIDI_Parser.h index be73088..3224aba 100644 --- a/src/AppleMIDI_Parser.h +++ b/src/AppleMIDI_Parser.h @@ -78,28 +78,35 @@ class AppleMIDIParser #ifdef KEEP_SESSION_NAME uint16_t bi = 0; + bool sawNul = false; while (i < buffer.size()) { + uint8_t ch = buffer[i++]; + if (ch == 0x00) + { + sawNul = true; + break; + } if (bi < Settings::MaxSessionNameLen) - invitation.sessionName[bi++] = buffer[i++]; - else - i++; + invitation.sessionName[bi++] = (char)ch; } - invitation.sessionName[bi++] = '\0'; + invitation.sessionName[bi] = '\0'; #else + bool sawNul = false; while (i < buffer.size()) - i++; + { + if (buffer[i++] == 0x00) + { + sawNul = true; + break; + } + } #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; + // Name continues past this buffer chunk (common for very long Mac session names). + if (!sawNul) + retVal = parserReturn::SessionNameVeryLong; while (i > 0) { @@ -282,29 +289,36 @@ class AppleMIDIParser #ifdef KEEP_SESSION_NAME uint16_t bi = 0; + bool sawNul = false; while (i < buffer.size()) { + uint8_t ch = buffer[i++]; + if (ch == 0x00) + { + sawNul = true; + break; + } if (bi < Settings::MaxSessionNameLen) - invitationAccepted.sessionName[bi++] = buffer[i++]; - else - i++; + invitationAccepted.sessionName[bi++] = (char)ch; } - invitationAccepted.sessionName[bi++] = '\0'; + invitationAccepted.sessionName[bi] = '\0'; #else + bool sawNul = false; while (i < buffer.size()) - i++; + { + if (buffer[i++] == 0x00) + { + sawNul = true; + break; + } + } #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; + // Name continues past this buffer chunk (common for very long Mac session names). + if (!sawNul) + retVal = parserReturn::SessionNameVeryLong; while (i > 0) { From 79a28c07ee61c47bbd791a98754ef91854b26422 Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:21:27 +0200 Subject: [PATCH 09/31] ONE_PARTICIPANT + APPLEMIDI_INITIATOR did not compile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Next is making ONE_PARTICIPANT + APPLEMIDI_INITIATOR compile. I’ll find every participants[i] use that isn’t gated. --- src/AppleMIDI.h | 2 +- src/AppleMIDI.hpp | 41 +++++++++++++++++++++++++++++------------ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/AppleMIDI.h b/src/AppleMIDI.h index 95d3ad3..e1cec1c 100644 --- a/src/AppleMIDI.h +++ b/src/AppleMIDI.h @@ -393,7 +393,7 @@ class AppleMIDISession void manageSynchronization(); void manageSynchronizationInitiator(); void manageSynchronizationInitiatorHeartBeat(Participant *); - void manageSynchronizationInitiatorInvites(size_t); + bool manageSynchronizationInitiatorInvites(Participant *); void sendSynchronization(Participant *); diff --git a/src/AppleMIDI.hpp b/src/AppleMIDI.hpp index a0eabe0..99143ce 100644 --- a/src/AppleMIDI.hpp +++ b/src/AppleMIDI.hpp @@ -331,18 +331,19 @@ void AppleMIDISession::ReceivedDataInvitationAccep template void AppleMIDISession::ReceivedInvitationRejected(AppleMIDI_InvitationRejected_t & invitationRejected) { +#ifndef ONE_PARTICIPANT 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; } } +#else + if (invitationRejected.ssrc == participant.ssrc) + participant.ssrc = 0; +#endif } #endif @@ -846,8 +847,19 @@ void AppleMIDISession::manageSynchronization() } else { - (pParticipant->synchronizing) ? manageSynchronizationInitiatorInvites(i) - : manageSynchronizationInitiatorHeartBeat(pParticipant); + if (pParticipant->synchronizing) + { + if (manageSynchronizationInitiatorInvites(pParticipant)) +#ifndef ONE_PARTICIPANT + continue; +#else + return; +#endif + } + else + { + manageSynchronizationInitiatorHeartBeat(pParticipant); + } } #endif #ifndef ONE_PARTICIPANT @@ -900,10 +912,8 @@ void AppleMIDISession::manageSynchronizationInitia // Retry sync invitations while establishing synchronization. template -void AppleMIDISession::manageSynchronizationInitiatorInvites(size_t i) +bool AppleMIDISession::manageSynchronizationInitiatorInvites(Participant* pParticipant) { - auto pParticipant = &participants[i]; - if (now - pParticipant->lastInviteSentTime > 10000) { if (pParticipant->synchronizationCount > Settings::MaxSynchronizationCK0Attempts) @@ -912,18 +922,25 @@ void AppleMIDISession::manageSynchronizationInitia if (nullptr != _exceptionCallback) _exceptionCallback(ssrc, MaxAttemptsException, 0); #endif - // After too many attempts, stop. sendEndSession(pParticipant); #ifndef ONE_PARTICIPANT - participants.erase(i); + for (size_t j = 0; j < participants.size(); j++) + { + if (&participants[j] == pParticipant) + { + participants.erase(j); + break; + } + } #else participant.ssrc = 0; #endif - return; + return true; } sendSynchronization(pParticipant); } + return false; } #endif From 3d6cec2d71a542ffacb74d1cbbcd59f1a0b32888 Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:23:51 +0200 Subject: [PATCH 10/31] Next is outgoing SysEx continuation (wrong front()) and silent inMidiBuffer overflow Outgoing SysEx: write() no longer looks at outMidiBuffer.front(). beginTransmission records whether this message is SysEx (_writingSysEx), so a zero timestamp or an earlier Note On in the same packet does not skip the F0/F7 split. Incoming overflow: ReceivedMidi returns if inMidiBuffer is full instead of dropping the byte with no signal. With USE_EXT_CALLBACKS, it fires BufferFullException with value 1 (outgoing full remains 0). --- src/AppleMIDI.h | 8 ++++++-- src/AppleMIDI.hpp | 9 +++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/AppleMIDI.h b/src/AppleMIDI.h index e1cec1c..62ea9bc 100644 --- a/src/AppleMIDI.h +++ b/src/AppleMIDI.h @@ -190,8 +190,11 @@ class AppleMIDISession dataPort.stop(); } - bool beginTransmission(MIDI_NAMESPACE::MidiType) + bool beginTransmission(MIDI_NAMESPACE::MidiType type) { + _writingSysEx = (type == MIDI_NAMESPACE::MidiType::SystemExclusive + || type == MIDI_NAMESPACE::MidiType::SystemExclusiveEnd); + // 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 @@ -239,7 +242,7 @@ class AppleMIDISession if ((outMidiBuffer.size()) + 2 > outMidiBuffer.max_size()) { // buffer is almost full, only 1 more character - if (MIDI_NAMESPACE::MidiType::SystemExclusive == outMidiBuffer.front()) + if (_writingSysEx) { // Add Sysex at the end of this partial SysEx (in the last availble slot) ... outMidiBuffer.push_back(MIDI_NAMESPACE::MidiType::SystemExclusiveStart); @@ -338,6 +341,7 @@ class AppleMIDISession ssrc_t ssrc = 0; uint16_t port = DEFAULT_CONTROL_PORT; bool _acceptIncomingMidi = false; + bool _writingSysEx = false; #ifdef ONE_PARTICIPANT Participant participant; #else diff --git a/src/AppleMIDI.hpp b/src/AppleMIDI.hpp index 99143ce..5121fad 100644 --- a/src/AppleMIDI.hpp +++ b/src/AppleMIDI.hpp @@ -1243,6 +1243,15 @@ void AppleMIDISession::ReceivedMidi(byte value) _receivedMidiByteCallback(ssrc, value); #endif + if (inMidiBuffer.full()) + { +#ifdef USE_EXT_CALLBACKS + if (nullptr != _exceptionCallback) + _exceptionCallback(ssrc, BufferFullException, 1); +#endif + return; + } + inMidiBuffer.push_back(value); } From 0e54957dc7fba2d202f400b3a14097be35dc8103 Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:26:16 +0200 Subject: [PATCH 11/31] system-journal length underflow, real data UDP port, and wrap-safe send-side sequence compare. System journal LENGTH: if sysjourlen < 2, return UnexpectedJournalData instead of underflowing remainingBytes. Data port: Participant now has remoteDataPort. Default is still control+1, then it is replaced with dataPort.remotePort() when the data invitation (listener) or data OK (initiator) arrives. CK, RTP, and data invites use that port. Send-side seq wrap: receiver feedback uses RFC 3550 serial compare (int16_t delta). Wrap no longer looks like SendPacketsDropped. --- src/AppleMIDI.hpp | 22 ++++++++++++++-------- src/AppleMIDI_Participant.h | 1 + src/rtpMIDI_Parser_JournalSection.hpp | 2 ++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/AppleMIDI.hpp b/src/AppleMIDI.hpp index 5121fad..e4650c2 100644 --- a/src/AppleMIDI.hpp +++ b/src/AppleMIDI.hpp @@ -181,6 +181,7 @@ void AppleMIDISession::ReceivedControlInvitation(A 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.remoteDataPort = (uint16_t)(controlPort.remotePort() + 1); participant.lastSyncExchangeTime = now; #ifdef KEEP_SESSION_NAME strncpy(participant.sessionName, invitation.sessionName, Settings::MaxSessionNameLen); @@ -254,7 +255,9 @@ void AppleMIDISession::ReceivedDataInvitation(Appl // of the ssrc here. auto ssrc_ = invitation.ssrc; - writeInvitation(dataPort, pParticipant->remoteIP, pParticipant->remotePort + 1, invitation, amInvitationAccepted); + pParticipant->remoteDataPort = dataPort.remotePort(); + + writeInvitation(dataPort, dataPort.remoteIP(), pParticipant->remoteDataPort, invitation, amInvitationAccepted); pParticipant->kind = Listener; @@ -325,6 +328,7 @@ void AppleMIDISession::ReceivedDataInvitationAccep } pParticipant->invitationStatus = DataInvitationAccepted; + pParticipant->remoteDataPort = dataPort.remotePort(); } // Remove participant on invitation rejection. @@ -429,13 +433,13 @@ void AppleMIDISession::ReceivedSynchronization(App case SYNC_CK0: /* From session APPLEMIDI_INITIATOR */ synchronization.timestamps[SYNC_CK1] = rtpMidiClock.Now(); synchronization.count = SYNC_CK1; - writeSynchronization(pParticipant->remoteIP, pParticipant->remotePort + 1, synchronization); + writeSynchronization(pParticipant->remoteIP, pParticipant->remoteDataPort, 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); + writeSynchronization(pParticipant->remoteIP, pParticipant->remoteDataPort, synchronization); pParticipant->synchronizing = false; #endif break; @@ -478,11 +482,12 @@ void AppleMIDISession::ReceivedReceiverFeedback(Ap return; } - if (pParticipant->sendSequenceNr < receiverFeedback.sequenceNr) + int16_t ahead = (int16_t)(receiverFeedback.sequenceNr - pParticipant->sendSequenceNr); + if (ahead > 0) { #ifdef USE_EXT_CALLBACKS if (nullptr != _exceptionCallback) - _exceptionCallback(pParticipant->ssrc, SendPacketsDropped, pParticipant->sendSequenceNr - receiverFeedback.sequenceNr); + _exceptionCallback(pParticipant->ssrc, SendPacketsDropped, (int32_t)ahead); #endif } } @@ -738,7 +743,7 @@ void AppleMIDISession::writeRtpMidiBuffer(Particip rtp.ssrc = __htonl(rtp.ssrc); rtp.sequenceNr = __htons(rtp.sequenceNr); - if (!dataPort.beginPacket(participant->remoteIP, participant->remotePort + 1)) + if (!dataPort.beginPacket(participant->remoteIP, participant->remoteDataPort)) { #ifdef USE_EXT_CALLBACKS if (nullptr != _exceptionCallback) @@ -955,7 +960,7 @@ void AppleMIDISession::sendSynchronization(Partici synchronization.timestamps[SYNC_CK2] = 0; synchronization.count = 0; - writeSynchronization(participant->remoteIP, participant->remotePort + 1, synchronization); + writeSynchronization(participant->remoteIP, participant->remoteDataPort, synchronization); participant->synchronizing = true; participant->synchronizationCount++; participant->lastInviteSentTime = now; @@ -1047,7 +1052,7 @@ void AppleMIDISession::manageSessionInvites() if (pParticipant->invitationStatus == ControlInvitationAccepted || pParticipant->invitationStatus == AwaitingDataInvitationAccepted) { - writeInvitation(dataPort, pParticipant->remoteIP, pParticipant->remotePort + 1, invitation, amInvitation); + writeInvitation(dataPort, pParticipant->remoteIP, pParticipant->remoteDataPort, invitation, amInvitation); pParticipant->invitationStatus = AwaitingDataInvitationAccepted; } } @@ -1122,6 +1127,7 @@ bool AppleMIDISession::sendInvite(IPAddress ip, ui 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.remoteDataPort = (uint16_t)(port + 1); participant.lastInviteSentTime = now - 1000; // forces invite to be send immediately participant.lastSyncExchangeTime = now; participant.initiatorToken = random(1, INT32_MAX) * 2; diff --git a/src/AppleMIDI_Participant.h b/src/AppleMIDI_Participant.h index 1293a49..86228f7 100644 --- a/src/AppleMIDI_Participant.h +++ b/src/AppleMIDI_Participant.h @@ -13,6 +13,7 @@ struct Participant ssrc_t ssrc = 0; IPAddress remoteIP = INADDR_NONE; uint16_t remotePort = 0; + uint16_t remoteDataPort = 0; uint64_t receiverFeedbackStartTime = 0; bool doReceiverFeedback = false; diff --git a/src/rtpMIDI_Parser_JournalSection.hpp b/src/rtpMIDI_Parser_JournalSection.hpp index b8854b2..90ac696 100644 --- a/src/rtpMIDI_Parser_JournalSection.hpp +++ b/src/rtpMIDI_Parser_JournalSection.hpp @@ -95,6 +95,8 @@ parserReturn decodeJournalSection(RtpBuffer_t &buffer) cb.buffer[1] = buffer[i++]; uint16_t systemflags = __ntohs(cb.value16); uint16_t sysjourlen = systemflags & RTP_MIDI_SJ_MASK_LENGTH; + if (sysjourlen < 2) + return parserReturn::UnexpectedJournalData; uint16_t remainingBytes = sysjourlen - 2; From 472923981e57dad3a6ad89c1381e2ef5a74786e9 Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:28:57 +0200 Subject: [PATCH 12/31] Endian test is always little-endian Endian: endian.h no longer uses #ifdef TEST_LITTLE_ENDIAN (always true). It uses __BYTE_ORDER__ / __ORDER_*__, then AVR/ESP/Windows fallbacks. --- src/utility/endian.h | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/utility/endian.h b/src/utility/endian.h index b4de809..02b923f 100644 --- a/src/utility/endian.h +++ b/src/utility/endian.h @@ -1,19 +1,21 @@ #pragma once -#if !defined(_BYTE_ORDER) +#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 +#if defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) #define _BYTE_ORDER _LITTLE_ENDIAN - #else +#elif defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) #define _BYTE_ORDER _BIG_ENDIAN - #endif - - #undef TEST_LITTLE_ENDIAN +#elif defined(__LITTLE_ENDIAN__) || defined(__ARMEL__) || defined(__AVR__) || defined(ESP32) || defined(ESP8266) || defined(_WIN32) + #define _BYTE_ORDER _LITTLE_ENDIAN +#elif defined(__BIG_ENDIAN__) + #define _BYTE_ORDER _BIG_ENDIAN +#else + #define _BYTE_ORDER _LITTLE_ENDIAN +#endif #include From 53c1665f64236790b39f694c6a657310ac99a324 Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:29:10 +0200 Subject: [PATCH 13/31] bump to 3.5.0 --- library.properties | 2 +- src/AppleMIDI_Defs.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library.properties b/library.properties index 8673196..794b168 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=AppleMIDI -version=3.3.0 +version=3.5.0 author=lathoub maintainer=lathoub sentence=AppleMIDI (aka rtpMIDI) MIDI I/Os for Arduino diff --git a/src/AppleMIDI_Defs.h b/src/AppleMIDI_Defs.h index 5c2bd5c..64c97bd 100644 --- a/src/AppleMIDI_Defs.h +++ b/src/AppleMIDI_Defs.h @@ -5,9 +5,9 @@ BEGIN_APPLEMIDI_NAMESPACE -#define APPLEMIDI_LIBRARY_VERSION 0x030000 +#define APPLEMIDI_LIBRARY_VERSION 0x030500 #define APPLEMIDI_LIBRARY_VERSION_MAJOR 3 -#define APPLEMIDI_LIBRARY_VERSION_MINOR 0 +#define APPLEMIDI_LIBRARY_VERSION_MINOR 5 #define APPLEMIDI_LIBRARY_VERSION_PATCH 0 #define DEFAULT_CONTROL_PORT 5004 From 3badb0f7f3a5756afd1e5bb265384d7160d892a6 Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:29:44 +0200 Subject: [PATCH 14/31] CI does not build ESP examples CI: actions/checkout@v4. ESP8266 (generic) and ESP32 (esp32) examples compile after installing those cores. --- .github/workflows/build.yml | 2 +- ci/build-arduino.sh | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ffcdb43..d42a2ae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,6 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Build on Arduino CLI run: bash ci/build-arduino.sh diff --git a/ci/build-arduino.sh b/ci/build-arduino.sh index 6cf1222..00aa2b8 100644 --- a/ci/build-arduino.sh +++ b/ci/build-arduino.sh @@ -13,15 +13,15 @@ export PATH=$PATH:$GITHUB_WORKSPACE/bin curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh arduino-cli config init arduino-cli config set library.enable_unsafe_install true -arduino-cli core update-index --additional-urls https://arduino.esp8266.com/stable/package_esp8266com_index.json -arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json +arduino-cli config add board_manager.additional_urls https://arduino.esp8266.com/stable/package_esp8266com_index.json +arduino-cli config add board_manager.additional_urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json arduino-cli core update-index -# Install Arduino AVR core +# Install cores arduino-cli core install arduino:avr arduino-cli core install arduino:samd -# arduino-cli core install arduino:esp8266 -# arduino-cli core install esp32:esp32:esp32doit-devkit-v1 +arduino-cli core install esp8266:esp8266 +arduino-cli core install esp32:esp32 # Link Arduino library ln -s $GITHUB_WORKSPACE $HOME/Arduino/libraries/CI_Test_Library @@ -42,11 +42,11 @@ for f in **/SAMD_*.ino ; do done # Compile all *.ino files for the ESP8266 -# for f in **/ESP8266_*.ino ; do -# arduino-cli compile -b arduino:esp8266:??? $f -# done +for f in **/ESP8266_*.ino ; do + arduino-cli compile -b esp8266:esp8266:generic $f +done # Compile all *.ino files for the ESP32 -# for f in **/ESP32_*.ino ; do -# arduino-cli compile -b arduino:esp32:??? $f -# done +for f in **/ESP32_*.ino ; do + arduino-cli compile -b esp32:esp32:esp32 $f +done From 13d212a33d23a580e10bc646899647e272749aa0 Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:39:43 +0200 Subject: [PATCH 15/31] Gate unused journal parse and outbound RS so small sketches can drop that flash without deleting the code. Co-authored-by: Cursor --- examples/AVR_MinMemUsage/AVR_MinMemUsage.ino | 1 + src/AppleMIDI.h | 12 +++++-- src/AppleMIDI.hpp | 36 +++++++++++++------- src/AppleMIDI_Defs.h | 19 ++++++++++- src/AppleMIDI_Participant.h | 2 ++ src/AppleMIDI_Settings.h | 4 +++ src/rtpMIDI_Parser.h | 18 ++++++++-- src/rtpMIDI_Parser_JournalSection.hpp | 4 +++ 8 files changed, 76 insertions(+), 20 deletions(-) diff --git a/examples/AVR_MinMemUsage/AVR_MinMemUsage.ino b/examples/AVR_MinMemUsage/AVR_MinMemUsage.ino index 165a8cb..237b49c 100644 --- a/examples/AVR_MinMemUsage/AVR_MinMemUsage.ino +++ b/examples/AVR_MinMemUsage/AVR_MinMemUsage.ino @@ -2,6 +2,7 @@ #define ONE_PARTICIPANT #define NO_SESSION_NAME +#define APPLEMIDI_SMALL #include // Enter a MAC address for your controller below. diff --git a/src/AppleMIDI.h b/src/AppleMIDI.h index 62ea9bc..c33f56d 100644 --- a/src/AppleMIDI.h +++ b/src/AppleMIDI.h @@ -61,7 +61,7 @@ class AppleMIDISession #endif }; - virtual ~AppleMIDISession(){}; + ~AppleMIDISession(){}; AppleMIDISession &setHandleConnected(void (*fptr)(const ssrc_t &, const char *)) { @@ -294,7 +294,9 @@ class AppleMIDISession if (readControlPackets()) // from socket into controlBuffer parseControlPackets(); // from controlBuffer to AppleMIDI +#ifndef APPLEMIDI_NO_RECEIVER_FEEDBACK manageReceiverFeedback(); +#endif manageSynchronization(); return inMidiBuffer.size(); @@ -333,8 +335,8 @@ class AppleMIDISession exceptionCallback _exceptionCallback = nullptr; #endif // buffer for incoming and outgoing MIDI messages - MidiBuffer_t inMidiBuffer; - MidiBuffer_t outMidiBuffer; + MidiInBuffer_t inMidiBuffer; + MidiOutBuffer_t outMidiBuffer; rtpMidi_Clock rtpMidiClock; @@ -382,7 +384,9 @@ class AppleMIDISession // Helpers void writeInvitation(UdpClass &, const IPAddress &, const uint16_t &, AppleMIDI_Invitation_t &, const byte *command); +#ifndef APPLEMIDI_NO_RECEIVER_FEEDBACK void writeReceiverFeedback(const IPAddress &, const uint16_t &, AppleMIDI_ReceiverFeedback_t &); +#endif void writeSynchronization(const IPAddress &, const uint16_t &, AppleMIDI_Synchronization_t &); void writeEndSession(const IPAddress &, const uint16_t &, AppleMIDI_EndSession_t &); @@ -391,7 +395,9 @@ class AppleMIDISession void writeRtpMidiToAllParticipants(); void writeRtpMidiBuffer(Participant *); +#ifndef APPLEMIDI_NO_RECEIVER_FEEDBACK void manageReceiverFeedback(); +#endif void manageSessionInvites(); void manageSynchronization(); diff --git a/src/AppleMIDI.hpp b/src/AppleMIDI.hpp index e4650c2..1a6b097 100644 --- a/src/AppleMIDI.hpp +++ b/src/AppleMIDI.hpp @@ -580,6 +580,7 @@ void AppleMIDISession::writeInvitation(UdpClass &p port.flush(); } +#ifndef APPLEMIDI_NO_RECEIVER_FEEDBACK // Send receiver feedback on the control port. template void AppleMIDISession::writeReceiverFeedback(const IPAddress& remoteIP, const uint16_t & remotePort, AppleMIDI_ReceiverFeedback_t & receiverFeedback) @@ -605,6 +606,7 @@ void AppleMIDISession::writeReceiverFeedback(const controlPort.endPacket(); controlPort.flush(); } +#endif // Send a synchronization packet on the data port. template @@ -752,11 +754,8 @@ void AppleMIDISession::writeRtpMidiBuffer(Particip 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); + // Write RTP + rtpMIDI without a MaxBufferSize stack copy. + dataPort.write(reinterpret_cast(&rtp), sizeof(rtp)); RtpMIDI_t rtpMidi; @@ -775,21 +774,28 @@ void AppleMIDISession::writeRtpMidiBuffer(Particip { // Short header rtpMidi.flags |= (uint8_t)bufferLen; rtpMidi.flags &= ~RTP_MIDI_CS_FLAG_B; // short header, clear B-FLAG - packet[offset++] = rtpMidi.flags; + dataPort.write(&rtpMidi.flags, 1); } 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); + uint8_t hdr[2] = { rtpMidi.flags, (uint8_t)bufferLen }; + dataPort.write(hdr, 2); } - // write out the MIDI Section - offset += outMidiBuffer.copy_out(packet + offset, bufferLen); - - // *No* journal section (Not supported) - dataPort.write(packet, offset); + size_t remaining = bufferLen; + size_t idx = 0; + while (remaining > 0) + { + size_t n = remaining; + if (n > sizeof(packetBuffer)) + n = sizeof(packetBuffer); + for (size_t j = 0; j < n; j++) + packetBuffer[j] = outMidiBuffer[idx++]; + dataPort.write(packetBuffer, n); + remaining -= n; + } dataPort.endPacket(); dataPort.flush(); @@ -1062,6 +1068,7 @@ void AppleMIDISession::manageSessionInvites() } } +#ifndef APPLEMIDI_NO_RECEIVER_FEEDBACK // 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 @@ -1104,6 +1111,7 @@ void AppleMIDISession::manageReceiverFeedback() } } } +#endif #ifdef APPLEMIDI_INITIATOR @@ -1195,9 +1203,11 @@ void AppleMIDISession::ReceivedRtp(const Rtp_t& rt return; } +#ifndef APPLEMIDI_NO_RECEIVER_FEEDBACK if (pParticipant->doReceiverFeedback == false) pParticipant->receiverFeedbackStartTime = now; pParticipant->doReceiverFeedback = true; +#endif #ifdef USE_EXT_CALLBACKS auto offset = (rtp.timestamp - pParticipant->offsetEstimate); diff --git a/src/AppleMIDI_Defs.h b/src/AppleMIDI_Defs.h index 64c97bd..117ab39 100644 --- a/src/AppleMIDI_Defs.h +++ b/src/AppleMIDI_Defs.h @@ -50,11 +50,28 @@ typedef const char* AppleMIDIConstStr; #endif #define RtpBuffer_t Deque -#define MidiBuffer_t Deque +#define MidiInBuffer_t Deque +#define MidiOutBuffer_t Deque +#define MidiBuffer_t MidiInBuffer_t // #define USE_EXT_CALLBACKS // #define ONE_PARTICIPANT // memory optimization // #define USE_DIRECTORY +// +// Size gates (define in the sketch before #include ): +// #define APPLEMIDI_SMALL // skip journals + outbound RS +// #define APPLEMIDI_SKIP_JOURNALS // drain journal bytes; parser file is kept +// #define APPLEMIDI_NO_RECEIVER_FEEDBACK // do not send RS; receive path is kept +// #define APPLEMIDI_PARSE_JOURNALS // keep journal parser when using APPLEMIDI_SMALL + +#ifdef APPLEMIDI_SMALL +#ifndef APPLEMIDI_PARSE_JOURNALS +#define APPLEMIDI_SKIP_JOURNALS +#endif +#ifndef APPLEMIDI_KEEP_RECEIVER_FEEDBACK +#define APPLEMIDI_NO_RECEIVER_FEEDBACK +#endif +#endif // By defining NO_SESSION_NAME in the sketch, you can save 100 bytes #ifndef NO_SESSION_NAME diff --git a/src/AppleMIDI_Participant.h b/src/AppleMIDI_Participant.h index 86228f7..259b23d 100644 --- a/src/AppleMIDI_Participant.h +++ b/src/AppleMIDI_Participant.h @@ -15,8 +15,10 @@ struct Participant uint16_t remotePort = 0; uint16_t remoteDataPort = 0; +#ifndef APPLEMIDI_NO_RECEIVER_FEEDBACK uint64_t receiverFeedbackStartTime = 0; bool doReceiverFeedback = false; +#endif uint16_t sendSequenceNr = 0; // seeded when session/participant is created uint16_t receiveSequenceNr = 0; diff --git a/src/AppleMIDI_Settings.h b/src/AppleMIDI_Settings.h index 6ad675a..664b078 100644 --- a/src/AppleMIDI_Settings.h +++ b/src/AppleMIDI_Settings.h @@ -11,6 +11,10 @@ struct DefaultSettings // MIDI buffer size in bytes; should be >= 3 * max message length. static const size_t MaxBufferSize = 64; + + // Override independently if inbound and outbound MIDI need different RAM. + static const size_t MaxMidiInSize = MaxBufferSize; + static const size_t MaxMidiOutSize = MaxBufferSize; static const size_t MaxSessionNameLen = 24; diff --git a/src/rtpMIDI_Parser.h b/src/rtpMIDI_Parser.h index 4510aa3..8c918ee 100644 --- a/src/rtpMIDI_Parser.h +++ b/src/rtpMIDI_Parser.h @@ -20,10 +20,12 @@ class rtpMIDIParser { private: bool _rtpHeadersComplete = false; +#ifndef APPLEMIDI_SKIP_JOURNALS bool _journalSectionComplete = false; bool _channelJournalSectionComplete = false; - uint16_t midiCommandLength; uint8_t _journalTotalChannels; +#endif + uint16_t midiCommandLength; uint8_t rtpMidi_Flags = 0; int cmdCount = 0; uint8_t runningstatus = 0; @@ -178,11 +180,12 @@ class rtpMIDIParser } _rtpHeadersComplete = true; - +#ifndef APPLEMIDI_SKIP_JOURNALS // initialize the Journal Section _journalSectionComplete = false; _channelJournalSectionComplete = false; _journalTotalChannels = 0; +#endif } // Always a MIDI section @@ -194,9 +197,11 @@ class rtpMIDIParser if (retVal != parserReturn::NotEnoughData) { _rtpHeadersComplete = false; +#ifndef APPLEMIDI_SKIP_JOURNALS _journalSectionComplete = false; _channelJournalSectionComplete = false; _journalTotalChannels = 0; +#endif midiCommandLength = 0; runningstatus = 0; } @@ -211,6 +216,11 @@ class rtpMIDIParser if (rtpMidi_Flags & RTP_MIDI_CS_FLAG_J) { +#ifdef APPLEMIDI_SKIP_JOURNALS + // Journals are never applied. Drop leftover payload; available() + // drains the unread tail of this UDP datagram when the buffer is empty. + buffer.clear(); +#else auto retVal = decodeJournalSection(buffer); switch (retVal) { case parserReturn::Processed: @@ -231,6 +241,7 @@ class rtpMIDIParser _journalTotalChannels = 0; return retVal; } +#endif } _rtpHeadersComplete = false; @@ -238,8 +249,9 @@ class rtpMIDIParser return parserReturn::Processed; } +#ifndef APPLEMIDI_SKIP_JOURNALS #include "rtpMIDI_Parser_JournalSection.hpp" - +#endif #include "rtpMIDI_Parser_CommandSection.hpp" }; diff --git a/src/rtpMIDI_Parser_JournalSection.hpp b/src/rtpMIDI_Parser_JournalSection.hpp index 90ac696..96b64c2 100644 --- a/src/rtpMIDI_Parser_JournalSection.hpp +++ b/src/rtpMIDI_Parser_JournalSection.hpp @@ -1,3 +1,7 @@ +// Compiled into rtpMIDIParser only when APPLEMIDI_SKIP_JOURNALS is not defined. +// Keep this file; do not delete it. Enable parsing with APPLEMIDI_PARSE_JOURNALS +// if you also defined APPLEMIDI_SMALL. +// // 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. From c6baf52b2d3c70739c138ad4da480174397f82fe Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:47:29 +0200 Subject: [PATCH 16/31] drop MIDI 2.0 will go into // repo --- doc/NetworkMIDI2_UMP.md | 50 ---------- src/NetworkMIDI2_Config.h | 16 ---- src/ump_Convert.cpp | 190 -------------------------------------- src/ump_Convert.h | 75 --------------- src/ump_Defs.h | 81 ---------------- 5 files changed, 412 deletions(-) delete mode 100644 doc/NetworkMIDI2_UMP.md delete mode 100644 src/NetworkMIDI2_Config.h delete mode 100644 src/ump_Convert.cpp delete mode 100644 src/ump_Convert.h delete mode 100644 src/ump_Defs.h diff --git a/doc/NetworkMIDI2_UMP.md b/doc/NetworkMIDI2_UMP.md deleted file mode 100644 index d473aba..0000000 --- a/doc/NetworkMIDI2_UMP.md +++ /dev/null @@ -1,50 +0,0 @@ -# Network MIDI 2.0 - UMP Base (Phase 1) - -Phase 1 adds **Universal MIDI Packet (UMP)** support for Network MIDI 2.0. The implementation is **parallel** to AppleMIDI – no dependency on the RTP-MIDI code. - -## Enabling - -Uncomment in `src/NetworkMIDI2_Config.h`: -```c -#define NETWORK_MIDI_2_SUPPORT -``` -Or add `-DNETWORK_MIDI_2_SUPPORT` to build flags. - -## Files - -| File | Purpose | -|------|---------| -| `NetworkMIDI2_Config.h` | Enable/disable switch | -| `ump_Defs.h` | UMP constants, Message Types, bit layouts | -| `ump_Convert.h` | Byte↔UMP conversion API | -| `ump_Convert.cpp` | Conversion implementation | - -## Examples (standalone, no AppleMIDI) - -- **UMP_Conversion** – Byte↔UMP conversion (Serial only) -- **ESP8266_NetworkMIDI2_mDNS** – mDNS `_midi2` + UMP (no transport yet) - -## API - -```c -// Byte stream -> UMP -ump_bytestream_parser_t parser; -ump_parser_init(&parser); -ump_byte_to_ump(&parser, byte, &ump, group); - -// UMP -> bytes -ump_to_bytes(ump, bytes, maxlen); - -// Direct build -ump_build_midi1_channel_voice(status, data1, data2, group); -``` - -## Supported - -- **MT 2** (MIDI 1.0 Channel Voice): Note On/Off, CC, Program Change, etc. -- **MT 1** (System Real-Time): Timing Clock, Start, Stop, etc. - -## References - -- M2-104-UM: UMP and MIDI 2.0 Protocol -- M2-124-UM: Network MIDI 2.0 UDP Transport diff --git a/src/NetworkMIDI2_Config.h b/src/NetworkMIDI2_Config.h deleted file mode 100644 index c8cbec5..0000000 --- a/src/NetworkMIDI2_Config.h +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @file NetworkMIDI2_Config.h - * @brief Enable Network MIDI 2.0 / UMP support - * - * Implementation is parallel to AppleMIDI - no dependency on RTP-MIDI. - * - * To enable: uncomment the define below, or add -DNETWORK_MIDI_2_SUPPORT - * to your compiler flags. - */ - -#ifndef NETWORK_MIDI_2_CONFIG_H -#define NETWORK_MIDI_2_CONFIG_H - -/* #define NETWORK_MIDI_2_SUPPORT */ - -#endif diff --git a/src/ump_Convert.cpp b/src/ump_Convert.cpp deleted file mode 100644 index 2020336..0000000 --- a/src/ump_Convert.cpp +++ /dev/null @@ -1,190 +0,0 @@ -/** - * @file ump_Convert.cpp - * @brief MIDI 1.0 <-> UMP conversion (standalone, no AppleMIDI dependency) - */ - -#include "NetworkMIDI2_Config.h" - -#if defined(NETWORK_MIDI_2_SUPPORT) - -#include "ump_Convert.h" - -#define MIDI1_NOTE_OFF 0x80 -#define MIDI1_NOTE_ON 0x90 -#define MIDI1_POLY_PRESSURE 0xA0 -#define MIDI1_CONTROL_CHANGE 0xB0 -#define MIDI1_PROGRAM_CHANGE 0xC0 -#define MIDI1_CHANNEL_PRESSURE 0xD0 -#define MIDI1_PITCH_BEND 0xE0 -#define MIDI1_SYSEX_START 0xF0 -#define MIDI1_SYSEX_END 0xF7 -#define MIDI1_REALTIME_MIN 0xF8 - -#define STATE_IDLE 0 -#define STATE_HAVE_STATUS 1 -#define STATE_HAVE_DATA1 2 - -namespace appleMidi { -namespace ump { - -static uint8_t midi1_status_nibble(uint8_t status) { - return (status >> 4) & 0x0F; -} -static uint8_t midi1_channel(uint8_t status) { - return status & 0x0F; -} -static bool is_channel_voice(uint8_t status) { - uint8_t n = midi1_status_nibble(status); - return (n >= 0x8 && n <= 0xE); -} -static uint8_t channel_voice_data_bytes(uint8_t status_nibble) { - switch (status_nibble) { - case UMP_MIDI1_NOTE_OFF: - case UMP_MIDI1_NOTE_ON: - case UMP_MIDI1_POLY_PRESSURE: - case UMP_MIDI1_CONTROL_CHANGE: - case UMP_MIDI1_PITCH_BEND: - return 2; - case UMP_MIDI1_PROGRAM_CHANGE: - case UMP_MIDI1_CHANNEL_PRESSURE: - return 1; - default: - return 0; - } -} - -void ump_parser_init(ump_bytestream_parser_t *p) { - if (p) { - p->state = STATE_IDLE; - p->running_status = 0; - p->data_count = 0; - } -} - -ump_convert_result_t ump_byte_to_ump(ump_bytestream_parser_t *p, uint8_t byte, - uint32_t *ump, uint8_t group) { - if (!p || !ump) return UMP_CONVERT_ERROR; - - if (byte >= MIDI1_REALTIME_MIN) { - if (byte == 0xFE) return UMP_CONVERT_SKIP; - *ump = (UMP_MT_SYSTEM_REALTIME << UMP_MT_SHIFT) | ((uint32_t)byte << 16); - return UMP_CONVERT_OK; - } - - if (byte >= MIDI1_SYSEX_START) { - p->running_status = 0; - p->state = STATE_IDLE; - return UMP_CONVERT_SKIP; - } - - if (byte & 0x80) { - if (!is_channel_voice(byte)) { - p->running_status = 0; - p->state = STATE_IDLE; - return UMP_CONVERT_SKIP; - } - p->running_status = byte; - p->data_count = 0; - p->state = STATE_HAVE_STATUS; - return UMP_CONVERT_NEED_MORE; - } - - if (p->state == STATE_IDLE && p->running_status) { - p->state = STATE_HAVE_STATUS; - p->data_count = 0; - } - - if (p->state != STATE_HAVE_STATUS && p->state != STATE_HAVE_DATA1) { - p->state = STATE_IDLE; - return UMP_CONVERT_ERROR; - } - - uint8_t status_nibble = midi1_status_nibble(p->running_status); - uint8_t n_data = channel_voice_data_bytes(status_nibble); - - if (n_data == 0) { - p->state = STATE_IDLE; - return UMP_CONVERT_ERROR; - } - - p->data[p->data_count++] = byte; - - if (p->data_count < n_data) { - p->state = STATE_HAVE_DATA1; - return UMP_CONVERT_NEED_MORE; - } - - *ump = (UMP_MT_MIDI1_CV << UMP_MT_SHIFT) - | ((uint32_t)(group & 0x0F) << UMP_GROUP_SHIFT) - | ((uint32_t)(status_nibble & 0x0F) << UMP_MT2_STATUS_SHIFT) - | ((uint32_t)(midi1_channel(p->running_status) & 0x0F) << UMP_MT2_CHANNEL_SHIFT) - | ((uint32_t)p->data[0] << UMP_MT2_DATA1_SHIFT); - if (n_data == 2) *ump |= (p->data[1] & 0xFF); - - p->state = STATE_IDLE; - return UMP_CONVERT_OK; -} - -bool ump_has_second_word(const ump_bytestream_parser_t *p) { - (void)p; - return false; -} - -uint32_t ump_build_midi1_channel_voice(uint8_t status, uint8_t data1, uint8_t data2, - uint8_t group) { - if (!is_channel_voice(status)) return 0; - uint8_t status_nibble = midi1_status_nibble(status); - uint8_t channel = midi1_channel(status); - uint8_t n_data = channel_voice_data_bytes(status_nibble); - - uint32_t ump = (UMP_MT_MIDI1_CV << UMP_MT_SHIFT) - | ((uint32_t)(group & 0x0F) << UMP_GROUP_SHIFT) - | ((uint32_t)(status_nibble & 0x0F) << UMP_MT2_STATUS_SHIFT) - | ((uint32_t)(channel & 0x0F) << UMP_MT2_CHANNEL_SHIFT) - | ((uint32_t)data1 << UMP_MT2_DATA1_SHIFT); - if (n_data >= 2) ump |= (data2 & 0xFF); - return ump; -} - -int ump_to_bytes(uint32_t ump, uint8_t *bytes, size_t maxlen) { - if (!bytes || maxlen < 1) return -1; - - uint8_t mt = (ump >> UMP_MT_SHIFT) & 0x0F; - - if (mt == UMP_MT_SYSTEM_REALTIME) { - bytes[0] = (ump >> 16) & 0xFF; - return 1; - } - - if (mt == UMP_MT_MIDI1_CV) { - uint8_t status_nibble = (ump >> UMP_MT2_STATUS_SHIFT) & 0x0F; - uint8_t channel = (ump >> UMP_MT2_CHANNEL_SHIFT) & 0x0F; - uint8_t data1 = (ump >> UMP_MT2_DATA1_SHIFT) & 0xFF; - uint8_t data2 = ump & 0xFF; - - uint8_t status = (status_nibble << 4) | channel; - uint8_t n_data = channel_voice_data_bytes(status_nibble); - - if (maxlen < (size_t)(1 + n_data)) return -1; - - bytes[0] = status; - bytes[1] = data1; - if (n_data == 2) { - bytes[2] = data2; - return 3; - } - return 2; - } - - return -1; -} - -bool ump_is_multi_word_start(uint32_t ump) { - uint8_t mt = (ump >> UMP_MT_SHIFT) & 0x0F; - return (mt == UMP_MT_DATA || mt == UMP_MT_SYSEX7 || mt == UMP_MT_SYSEX8_MDS); -} - -} /* namespace ump */ -} /* namespace appleMidi */ - -#endif /* NETWORK_MIDI_2_SUPPORT */ diff --git a/src/ump_Convert.h b/src/ump_Convert.h deleted file mode 100644 index 9981d45..0000000 --- a/src/ump_Convert.h +++ /dev/null @@ -1,75 +0,0 @@ -#pragma once - -#if defined(NETWORK_MIDI_2_SUPPORT) - -/** - * @file ump_Convert.h - * @brief MIDI 1.0 byte stream <-> Universal MIDI Packet (UMP) conversion - * - * Standalone - no dependency on AppleMIDI. For Network MIDI 2.0 transport. - * - * Supports: - * - MT 2: MIDI 1.0 Channel Voice (Note On/Off, CC, PC, etc.) - * - MT 1: System Real-Time (Timing Clock, Start, Stop, etc.) - */ - -#include "ump_Defs.h" -#include -#include -#include - -#ifdef __cplusplus -namespace appleMidi { -namespace ump { -#endif - -/* ============================================================================ - * Byte stream -> UMP conversion (stateful parser) - * ============================================================================ */ - -typedef enum { - UMP_CONVERT_NEED_MORE, - UMP_CONVERT_OK, - UMP_CONVERT_SKIP, - UMP_CONVERT_ERROR -} ump_convert_result_t; - -typedef struct { - uint8_t state; - uint8_t running_status; - uint8_t data_count; - uint8_t reserved; - uint8_t data[2]; -} ump_bytestream_parser_t; - -void ump_parser_init(ump_bytestream_parser_t *p); - -ump_convert_result_t ump_byte_to_ump(ump_bytestream_parser_t *p, uint8_t byte, - uint32_t *ump, uint8_t group); - -bool ump_has_second_word(const ump_bytestream_parser_t *p); - -/** - * Build UMP from complete MIDI 1.0 channel voice message. - * @param status MIDI status byte (0x80-0xEF) - * @param data1 First data byte - * @param data2 Second data byte (0 for PC, Channel Pressure) - * @param group UMP Group (0-15) - */ -uint32_t ump_build_midi1_channel_voice(uint8_t status, uint8_t data1, uint8_t data2, - uint8_t group); - -/* ============================================================================ - * UMP -> Byte stream conversion - * ============================================================================ */ - -int ump_to_bytes(uint32_t ump, uint8_t *bytes, size_t maxlen); - -bool ump_is_multi_word_start(uint32_t ump); - -#ifdef __cplusplus -} /* namespace ump */ -} /* namespace appleMidi */ -#endif - -#endif /* NETWORK_MIDI_2_SUPPORT */ diff --git a/src/ump_Defs.h b/src/ump_Defs.h deleted file mode 100644 index 1fa80c0..0000000 --- a/src/ump_Defs.h +++ /dev/null @@ -1,81 +0,0 @@ -#pragma once - -#if defined(NETWORK_MIDI_2_SUPPORT) - -/** - * @file ump_Defs.h - * @brief Universal MIDI Packet (UMP) definitions for Network MIDI 2.0 - * - * UMP format as per M2-104-UM (UMP and MIDI 2.0 Protocol Specification). - * Standalone - no dependency on AppleMIDI/RTP-MIDI implementation. - */ - -#include - -#ifdef __cplusplus -namespace appleMidi { -namespace ump { -#endif - -/* ============================================================================ - * UMP Message Types (MT) - bits 31-28 of first UMP word - * ============================================================================ */ -#define UMP_MT_MASK 0xF0000000u -#define UMP_MT_SHIFT 28 - -#define UMP_MT_UTILITY 0x0u /* Utility (no Group) */ -#define UMP_MT_SYSTEM_REALTIME 0x1u /* System Real-Time (no Group) */ -#define UMP_MT_MIDI1_CV 0x2u /* MIDI 1.0 Channel Voice */ -#define UMP_MT_DATA 0x3u /* SysEx (64/96/128-bit) */ -#define UMP_MT_MIDI2_CV 0x4u /* MIDI 2.0 Channel Voice */ -#define UMP_MT_MIDI2_CVM 0x5u /* MIDI 2.0 Channel Voice Mixed */ -#define UMP_MT_SYSEX7 0x6u /* SysEx 7-bit (64/96/128-bit) */ -#define UMP_MT_SYSEX8_MDS 0x7u /* SysEx 8-bit + MIDI-CI */ -#define UMP_MT_FLEX 0xFu /* Flexible (Stream, Endpoint, etc.) */ - -/* ============================================================================ - * UMP Group - bits 27-24 (0-15) - * ============================================================================ */ -#define UMP_GROUP_MASK 0x0F000000u -#define UMP_GROUP_SHIFT 24 - -/* ============================================================================ - * UMP Message Type 2: MIDI 1.0 Channel Voice (32-bit) - * Format: MT(4) | Group(4) | Status(4) | Channel(4) | Data1(8) | Data2(8) - * ============================================================================ */ -#define UMP_MT2_STATUS_MASK 0x00F00000u -#define UMP_MT2_STATUS_SHIFT 20 -#define UMP_MT2_CHANNEL_MASK 0x000F0000u -#define UMP_MT2_CHANNEL_SHIFT 16 -#define UMP_MT2_DATA1_MASK 0x0000FF00u -#define UMP_MT2_DATA1_SHIFT 8 -#define UMP_MT2_DATA2_MASK 0x000000FFu - -/* MIDI 1.0 status nibbles (high nibble of status byte) */ -#define UMP_MIDI1_NOTE_OFF 0x8u -#define UMP_MIDI1_NOTE_ON 0x9u -#define UMP_MIDI1_POLY_PRESSURE 0xAu -#define UMP_MIDI1_CONTROL_CHANGE 0xBu -#define UMP_MIDI1_PROGRAM_CHANGE 0xCu -#define UMP_MIDI1_CHANNEL_PRESSURE 0xDu -#define UMP_MIDI1_PITCH_BEND 0xEu - -/* ============================================================================ - * UMP Message Type 1: System Real-Time (32-bit) - * ============================================================================ */ -#define UMP_MIDI1_SYS_REALTIME_TIMING_CLOCK 0xF8u -#define UMP_MIDI1_SYS_REALTIME_START 0xFAu -#define UMP_MIDI1_SYS_REALTIME_CONTINUE 0xFBu -#define UMP_MIDI1_SYS_REALTIME_STOP 0xFCu -#define UMP_MIDI1_SYS_REALTIME_ACTIVE_SENSE 0xFEu -#define UMP_MIDI1_SYS_REALTIME_SYS_RESET 0xFFu - -/* Default UMP Group (Group 0 = first group) */ -#define UMP_DEFAULT_GROUP 0u - -#ifdef __cplusplus -} /* namespace ump */ -} /* namespace appleMidi */ -#endif - -#endif /* NETWORK_MIDI_2_SUPPORT */ From 3fcf7e08d6615cb377013f9faba46d813022da1e Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 22:01:19 +0200 Subject: [PATCH 17/31] Match invitation reject by initiator token so a NO before control OK is not ignored. Co-authored-by: Cursor --- src/AppleMIDI.hpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/AppleMIDI.hpp b/src/AppleMIDI.hpp index 1a6b097..87072fe 100644 --- a/src/AppleMIDI.hpp +++ b/src/AppleMIDI.hpp @@ -332,21 +332,25 @@ void AppleMIDISession::ReceivedDataInvitationAccep } // Remove participant on invitation rejection. +// Match by initiator token, same as OK: the peer SSRC is unknown until control OK. template void AppleMIDISession::ReceivedInvitationRejected(AppleMIDI_InvitationRejected_t & invitationRejected) { #ifndef ONE_PARTICIPANT - for (auto i = 0; i < participants.size(); i++) + for (size_t i = 0; i < participants.size(); i++) { - if (invitationRejected.ssrc == participants[i].ssrc) + if (invitationRejected.initiatorToken == participants[i].initiatorToken) { participants.erase(i); return; } } #else - if (invitationRejected.ssrc == participant.ssrc) - participant.ssrc = 0; + if (invitationRejected.initiatorToken != participant.initiatorToken) + return; + participant.ssrc = 0; + participant.kind = Listener; + participant.initiatorToken = 0; #endif } #endif From 4cb0536eb6358e397bf924750567415616bc5faa Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 22:03:28 +0200 Subject: [PATCH 18/31] Do not queue MIDI or send RTP until a participant has a real SSRC. Co-authored-by: Cursor --- src/AppleMIDI.h | 32 ++++++++++++++++++++------------ src/AppleMIDI.hpp | 6 ++++-- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/AppleMIDI.h b/src/AppleMIDI.h index c33f56d..24f1293 100644 --- a/src/AppleMIDI.h +++ b/src/AppleMIDI.h @@ -192,6 +192,25 @@ class AppleMIDISession bool beginTransmission(MIDI_NAMESPACE::MidiType type) { +#ifndef ONE_PARTICIPANT + bool connected = false; + for (size_t i = 0; i < participants.size(); i++) + { + if (participants[i].ssrc != 0 && + participants[i].remoteIP != (IPAddress)INADDR_NONE) + { + connected = true; + break; + } + } + if (!connected) + return false; +#else + if (participant.ssrc == 0 || + participant.remoteIP == (IPAddress)INADDR_NONE) + return false; +#endif + _writingSysEx = (type == MIDI_NAMESPACE::MidiType::SystemExclusive || type == MIDI_NAMESPACE::MidiType::SystemExclusiveEnd); @@ -222,18 +241,7 @@ class AppleMIDISession // 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 - for (size_t i = 0; i < participants.size(); i++) - { - if (participants[i].ssrc != 0 && - participants[i].remoteIP != (IPAddress)INADDR_NONE) - return true; - } - return false; -#else - return (participant.ssrc != 0 && - participant.remoteIP != (IPAddress)INADDR_NONE); -#endif + return true; }; void write(byte byte) diff --git a/src/AppleMIDI.hpp b/src/AppleMIDI.hpp index 87072fe..1ecd6a9 100644 --- a/src/AppleMIDI.hpp +++ b/src/AppleMIDI.hpp @@ -673,11 +673,13 @@ void AppleMIDISession::writeRtpMidiToAllParticipan for (size_t i = 0; i < participants.size(); i++) { auto pParticipant = &participants[i]; - + if (pParticipant->ssrc == 0) + continue; writeRtpMidiBuffer(pParticipant); } #else - writeRtpMidiBuffer(&participant); + if (participant.ssrc != 0) + writeRtpMidiBuffer(&participant); #endif outMidiBuffer.clear(); } From 2ae387f3c26cde21d86cfafa7d9edd602b6289e2 Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 22:05:34 +0200 Subject: [PATCH 19/31] Hold RTP until the data-port handshake sets remoteDataPort instead of guessing control+1. Co-authored-by: Cursor --- src/AppleMIDI.h | 2 ++ src/AppleMIDI.hpp | 9 ++++----- src/AppleMIDI_Participant.h | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/AppleMIDI.h b/src/AppleMIDI.h index 24f1293..436689a 100644 --- a/src/AppleMIDI.h +++ b/src/AppleMIDI.h @@ -197,6 +197,7 @@ class AppleMIDISession for (size_t i = 0; i < participants.size(); i++) { if (participants[i].ssrc != 0 && + participants[i].remoteDataPort != 0 && participants[i].remoteIP != (IPAddress)INADDR_NONE) { connected = true; @@ -207,6 +208,7 @@ class AppleMIDISession return false; #else if (participant.ssrc == 0 || + participant.remoteDataPort == 0 || participant.remoteIP == (IPAddress)INADDR_NONE) return false; #endif diff --git a/src/AppleMIDI.hpp b/src/AppleMIDI.hpp index 1ecd6a9..b792c99 100644 --- a/src/AppleMIDI.hpp +++ b/src/AppleMIDI.hpp @@ -181,7 +181,6 @@ void AppleMIDISession::ReceivedControlInvitation(A 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.remoteDataPort = (uint16_t)(controlPort.remotePort() + 1); participant.lastSyncExchangeTime = now; #ifdef KEEP_SESSION_NAME strncpy(participant.sessionName, invitation.sessionName, Settings::MaxSessionNameLen); @@ -673,12 +672,12 @@ void AppleMIDISession::writeRtpMidiToAllParticipan for (size_t i = 0; i < participants.size(); i++) { auto pParticipant = &participants[i]; - if (pParticipant->ssrc == 0) + if (pParticipant->ssrc == 0 || pParticipant->remoteDataPort == 0) continue; writeRtpMidiBuffer(pParticipant); } #else - if (participant.ssrc != 0) + if (participant.ssrc != 0 && participant.remoteDataPort != 0) writeRtpMidiBuffer(&participant); #endif outMidiBuffer.clear(); @@ -1064,7 +1063,8 @@ void AppleMIDISession::manageSessionInvites() if (pParticipant->invitationStatus == ControlInvitationAccepted || pParticipant->invitationStatus == AwaitingDataInvitationAccepted) { - writeInvitation(dataPort, pParticipant->remoteIP, pParticipant->remoteDataPort, invitation, amInvitation); + // Data invite goes to control+1; remoteDataPort is set from the OK. + writeInvitation(dataPort, pParticipant->remoteIP, (uint16_t)(pParticipant->remotePort + 1), invitation, amInvitation); pParticipant->invitationStatus = AwaitingDataInvitationAccepted; } } @@ -1141,7 +1141,6 @@ bool AppleMIDISession::sendInvite(IPAddress ip, ui 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.remoteDataPort = (uint16_t)(port + 1); participant.lastInviteSentTime = now - 1000; // forces invite to be send immediately participant.lastSyncExchangeTime = now; participant.initiatorToken = random(1, INT32_MAX) * 2; diff --git a/src/AppleMIDI_Participant.h b/src/AppleMIDI_Participant.h index 259b23d..70e9d98 100644 --- a/src/AppleMIDI_Participant.h +++ b/src/AppleMIDI_Participant.h @@ -13,6 +13,7 @@ struct Participant ssrc_t ssrc = 0; IPAddress remoteIP = INADDR_NONE; uint16_t remotePort = 0; + // 0 until the data-port handshake (data IN or data OK) reports the real port. uint16_t remoteDataPort = 0; #ifndef APPLEMIDI_NO_RECEIVER_FEEDBACK From f26b74b83d70755364149edb0a21ecd7765d4dce Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 22:07:06 +0200 Subject: [PATCH 20/31] Parse invitation reject names like IN/OK so a long Mac name does not stall the parser. Co-authored-by: Cursor --- src/AppleMIDI_Parser.h | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/AppleMIDI_Parser.h b/src/AppleMIDI_Parser.h index 3224aba..2001aa1 100644 --- a/src/AppleMIDI_Parser.h +++ b/src/AppleMIDI_Parser.h @@ -368,22 +368,36 @@ class AppleMIDIParser #ifdef KEEP_SESSION_NAME uint16_t bi = 0; - while ((i < buffer.size()) && (buffer[i] != 0x00)) + bool sawNul = false; + while (i < buffer.size()) { + uint8_t ch = buffer[i++]; + if (ch == 0x00) + { + sawNul = true; + break; + } if (bi < Settings::MaxSessionNameLen) - invitationRejected.sessionName[bi++] = buffer[i]; - i++; + invitationRejected.sessionName[bi++] = (char)ch; } - invitationRejected.sessionName[bi++] = '\0'; + invitationRejected.sessionName[bi] = '\0'; #else - while ((i < buffer.size()) && (buffer[i] != 0x00)) - i++; + bool sawNul = false; + while (i < buffer.size()) + { + if (buffer[i++] == 0x00) + { + sawNul = true; + break; + } + } #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; + + auto retVal = parserReturn::Processed; + + // Name continues past this buffer chunk (common for very long Mac session names). + if (!sawNul) + retVal = parserReturn::SessionNameVeryLong; while (i > 0) { @@ -393,7 +407,7 @@ class AppleMIDIParser session->ReceivedInvitationRejected(invitationRejected); - return parserReturn::Processed; + return retVal; } #endif else if (0 == memcmp(command, amBitrateReceiveLimit, sizeof(amBitrateReceiveLimit))) From 6ba845e67f6f841a45bb738dd0d4de162f5ef05f Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 22:08:44 +0200 Subject: [PATCH 21/31] Document that extra F0/F7 on large SysEx is RFC 6295 segmentation, not a USB-style framing bug. Co-authored-by: Cursor --- README.md | 4 ++++ src/AppleMIDI.h | 1 + src/AppleMIDI_Settings.h | 1 + 3 files changed, 6 insertions(+) diff --git a/README.md b/README.md index 22a2317..531a746 100755 --- a/README.md +++ b/README.md @@ -75,6 +75,10 @@ More usages in the [examples](https://github.com/lathoub/Arduino-AppleMIDI-Libra ## Notes +### SysEx and extra F0/F7 bytes + +Outgoing SysEx that does not fit in [`MaxMidiOutSize`](https://github.com/lathoub/Arduino-AppleMIDI-Library/blob/v3.5.0/src/AppleMIDI_Settings.h) (default 64) is split across RTP packets per [RFC 6295](https://www.rfc-editor.org/rfc/rfc6295.html#section-3): the current packet ends with `F0` and the next starts with `F7`. USB, serial, and BLE do not add those markers, so a capture can look malformed ([#169](https://github.com/lathoub/Arduino-AppleMIDI-Library/issues/169)). AppleMIDI/rtpMIDI receivers reassemble them. Raise `MaxMidiOutSize` (and usually `MaxBufferSize`) in a custom Settings struct if you need a larger SysEx in one command section. + ### Session names Session names can get really long on Macs (eg 'Macbook Pro of Johann Gambolputty .. von Hautkopft of Ulm') and will be truncated to the [`MaxSessionNameLen`](https://github.com/lathoub/Arduino-AppleMIDI-Library/blob/af4c7bd9a960a90e09e211f0ea00db2d9832d1f7/src/AppleMIDI_Settings.h#L13) diff --git a/src/AppleMIDI.h b/src/AppleMIDI.h index 436689a..f1f4132 100644 --- a/src/AppleMIDI.h +++ b/src/AppleMIDI.h @@ -254,6 +254,7 @@ class AppleMIDISession // buffer is almost full, only 1 more character if (_writingSysEx) { + // RFC 6295 SysEx split: this packet ends with F0, the next starts with F7. // Add Sysex at the end of this partial SysEx (in the last availble slot) ... outMidiBuffer.push_back(MIDI_NAMESPACE::MidiType::SystemExclusiveStart); diff --git a/src/AppleMIDI_Settings.h b/src/AppleMIDI_Settings.h index 664b078..ce64b08 100644 --- a/src/AppleMIDI_Settings.h +++ b/src/AppleMIDI_Settings.h @@ -10,6 +10,7 @@ struct DefaultSettings static const size_t UdpTxPacketMaxSize = 24; // MIDI buffer size in bytes; should be >= 3 * max message length. + // Outgoing SysEx larger than MaxMidiOutSize is split with RFC 6295 F0/F7 markers. static const size_t MaxBufferSize = 64; // Override independently if inbound and outbound MIDI need different RAM. From 931b33e7292f193a33efe2898b848ad745b2af01 Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 22:13:38 +0200 Subject: [PATCH 22/31] Document 3.5.0, APPLEMIDI_SMALL, and align the license badge and keywords with the library. Co-authored-by: Cursor --- README.md | 25 ++++++++++++++++--------- keywords.txt | 37 +++++++++++++++++++++++++------------ library.properties | 2 +- 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 531a746..9341cb2 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# AppleMIDI (aka rtpMIDI) for Arduino [![License: CC BY-SA 4.0](https://img.shields.io/badge/License-CC%20BY--SA%204.0-lightgrey.svg)](http://creativecommons.org/licenses/by-sa/4.0/) +# AppleMIDI (aka rtpMIDI) for Arduino [![License: Beerware](https://img.shields.io/badge/License-Beerware-yellow.svg)](LICENSE.md) Enables an Arduino with IP/UDP capabilities (Ethernet shield, ESP8266, ESP32, ...) to participate in an AppleMIDI session. @@ -9,14 +9,14 @@ Enables an Arduino with IP/UDP capabilities (Ethernet shield, ESP8266, ESP32, .. * Tested with AppleMIDI on Mac OS (Big Sur) and using [rtpMIDI](https://www.tobias-erichsen.de/software/rtpmidi.html) from Tobias Erichsen on Windows 10 * Send and receive all MIDI messages * Uses callbacks to receive MIDI commands (no need for polling) -* Automatic instantiation of AppleMIDI object (see at the end of 'AppleMidi.h') +* Automatic instantiation of AppleMIDI object (see at the end of 'AppleMIDI.h') * Compiles on Arduino, MacOS (XCode) and Windows (MSVS) -### New in 3.2.0 -* Event chaining - -### New in 3.3.0 -* Better parsing of large incoming MIDI messages with a small internal Arduino buffer +### New in 3.5.0 +* Safer session and RTP parsing (known-SSRC MIDI only, UDP datagram isolation, wrap-safe sequence numbers) +* RTP is sent only after the data-port handshake; invitation reject matches by initiator token +* Size gates: `APPLEMIDI_SMALL`, `ONE_PARTICIPANT`, `NO_SESSION_NAME` (see [Memory footprint](#memory-footprint)) +* Large SysEx is split per RFC 6295 (`F0`/`F7`); see [SysEx and extra F0/F7 bytes](#sysex-and-extra-f0f7-bytes) ## Installation From the Arduino IDE Library Manager, search for AppleMIDI @@ -81,10 +81,17 @@ Outgoing SysEx that does not fit in [`MaxMidiOutSize`](https://github.com/lathou ### Session names -Session names can get really long on Macs (eg 'Macbook Pro of Johann Gambolputty .. von Hautkopft of Ulm') and will be truncated to the [`MaxSessionNameLen`](https://github.com/lathoub/Arduino-AppleMIDI-Library/blob/af4c7bd9a960a90e09e211f0ea00db2d9832d1f7/src/AppleMIDI_Settings.h#L13) +Session names can get really long on Macs (eg 'Macbook Pro of Johann Gambolputty .. von Hautkopft of Ulm') and will be truncated to [`MaxSessionNameLen`](src/AppleMIDI_Settings.h). ### Memory footprint -The memory footprint of the library can be lowered significantly, read the [wiki](https://github.com/lathoub/Arduino-AppleMIDI-Library/wiki/Memory-footprint) +Define these in the sketch **before** `#include `: + +* `ONE_PARTICIPANT` — one remote peer (saves a participant slot) +* `NO_SESSION_NAME` — do not store session names (~100 bytes) +* `APPLEMIDI_SMALL` — skip unused journal parsing and outbound RS + (`APPLEMIDI_PARSE_JOURNALS` / `APPLEMIDI_KEEP_RECEIVER_FEEDBACK` keep those while still using `APPLEMIDI_SMALL`) + +`APPLEMIDI_SMALL` is used in `examples/AVR_MinMemUsage`. More on the [wiki](https://github.com/lathoub/Arduino-AppleMIDI-Library/wiki/Memory-footprint). ### Ethernet buffer size It's highly recommended to modify the [Ethernet library](https://github.com/arduino-libraries/Ethernet) or use the [Ethernet3 library](https://github.com/sstaub/Ethernet3) to avoid buffer overruns - [learn more](https://github.com/lathoub/Arduino-AppleMIDI-Library/wiki/Enlarge-Ethernet-buffer-size-to-avoid-dropping-UDP-packages) diff --git a/keywords.txt b/keywords.txt index e1ff976..6aa33e7 100644 --- a/keywords.txt +++ b/keywords.txt @@ -5,21 +5,25 @@ ####################################### # Datatypes (KEYWORD1) ####################################### -AppleMidi KEYWORD1 +AppleMIDI KEYWORD1 +AppleMIDISession KEYWORD1 ####################################### # Methods and Functions (KEYWORD2) ####################################### -setName KEYWORD2 -setPort KEYWORD2 -setHandleConnected KEYWORD2 -setHandleDisconnected KEYWORD2 -setHandleException KEYWORD2 -setHandleStartReceivedMidi KEYWORD2 -setHandleReceivedMidi KEYWORD2 -setHandleEndReceivedMidi KEYWORD2 -setHandleReceivedRtp KEYWORD2 -setHandleSendRtp KEYWORD2 +setName KEYWORD2 +setPort KEYWORD2 +getName KEYWORD2 +getPort KEYWORD2 +setHandleConnected KEYWORD2 +setHandleDisconnected KEYWORD2 +setHandleException KEYWORD2 +setHandleStartReceivedMidi KEYWORD2 +setHandleReceivedMidi KEYWORD2 +setHandleEndReceivedMidi KEYWORD2 +setHandleReceivedRtp KEYWORD2 +setHandleSentRtp KEYWORD2 +setHandleSentRtpMidi KEYWORD2 ####################################### # Instances (KEYWORD3) @@ -29,6 +33,15 @@ AppleMIDI KEYWORD3 ####################################### # Constants (LITERAL1) ####################################### - APPLEMIDI_CREATE_INSTANCE LITERAL1 APPLEMIDI_CREATE_DEFAULTSESSION_INSTANCE LITERAL1 +APPLEMIDI_SMALL LITERAL1 +APPLEMIDI_SKIP_JOURNALS LITERAL1 +APPLEMIDI_NO_RECEIVER_FEEDBACK LITERAL1 +APPLEMIDI_PARSE_JOURNALS LITERAL1 +APPLEMIDI_KEEP_RECEIVER_FEEDBACK LITERAL1 +ONE_PARTICIPANT LITERAL1 +NO_SESSION_NAME LITERAL1 +APPLEMIDI_INITIATOR LITERAL1 +USE_EXT_CALLBACKS LITERAL1 +USE_DIRECTORY LITERAL1 diff --git a/library.properties b/library.properties index 794b168..18dd0c3 100644 --- a/library.properties +++ b/library.properties @@ -5,7 +5,7 @@ maintainer=lathoub sentence=AppleMIDI (aka rtpMIDI) MIDI I/Os for Arduino paragraph=AppleMIDI (aka rtpMIDI) is a protocol to transport MIDI messages within RTP (Real-time Protocol) packets over Ethernet and WiFi networks. This major rewrite is faster, more stable and uses less memory. Read the Wiki page when migrating category=Communication -url=https://github.com/lathoub/Arduino-AppleMidi-Library +url=https://github.com/lathoub/Arduino-AppleMIDI-Library architectures=* includes=AppleMIDI.h depends=MIDI Library From fd584da91ddc853ba2beb0ae335c7455e74ab0db Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Mon, 17 Aug 2026 22:15:50 +0200 Subject: [PATCH 23/31] State the license as Creative Commons Attribution-ShareAlike 4.0 throughout. Co-authored-by: Cursor --- LICENSE.md | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 1d8ac5b..4f3f373 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,3 +1,3 @@ -The hardware is released under Creative Commons Share-alike 4.0. +This work is licensed under Creative Commons Attribution-ShareAlike 4.0 International. -All other code is open source so please feel free to do anything you want with it; you buy me a beer if you use this and we meet someday (Beerware license). +https://creativecommons.org/licenses/by-sa/4.0/ diff --git a/README.md b/README.md index 9341cb2..0bb69e9 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# AppleMIDI (aka rtpMIDI) for Arduino [![License: Beerware](https://img.shields.io/badge/License-Beerware-yellow.svg)](LICENSE.md) +# AppleMIDI (aka rtpMIDI) for Arduino [![License: CC BY-SA 4.0](https://img.shields.io/badge/License-CC%20BY--SA%204.0-lightgrey.svg)](http://creativecommons.org/licenses/by-sa/4.0/) Enables an Arduino with IP/UDP capabilities (Ethernet shield, ESP8266, ESP32, ...) to participate in an AppleMIDI session. From 17b0a17a1c31b10c7f1c8647b9904a980e534c89 Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:09:10 +0200 Subject: [PATCH 24/31] Update AppleMIDI_Defs.h --- src/AppleMIDI_Defs.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/AppleMIDI_Defs.h b/src/AppleMIDI_Defs.h index 117ab39..56f0b39 100644 --- a/src/AppleMIDI_Defs.h +++ b/src/AppleMIDI_Defs.h @@ -54,11 +54,10 @@ typedef const char* AppleMIDIConstStr; #define MidiOutBuffer_t Deque #define MidiBuffer_t MidiInBuffer_t +// Size gates (define in the sketch before #include ): // #define USE_EXT_CALLBACKS // #define ONE_PARTICIPANT // memory optimization // #define USE_DIRECTORY -// -// Size gates (define in the sketch before #include ): // #define APPLEMIDI_SMALL // skip journals + outbound RS // #define APPLEMIDI_SKIP_JOURNALS // drain journal bytes; parser file is kept // #define APPLEMIDI_NO_RECEIVER_FEEDBACK // do not send RS; receive path is kept From e1fc6cd1555d8a6f2f09f4245ef542d64ca879e0 Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:09:30 +0200 Subject: [PATCH 25/31] remove Ethernet3 --- .../ESP32_NoteOnOffEverySec.ino | 21 +++++++- .../ESP32_W5500_Callbacks.ino | 53 +++++++++++-------- examples/ESP32_W5500_Callbacks/ETH_Helper.h | 49 ++++------------- 3 files changed, 59 insertions(+), 64 deletions(-) diff --git a/examples/ESP32_NoteOnOffEverySec/ESP32_NoteOnOffEverySec.ino b/examples/ESP32_NoteOnOffEverySec/ESP32_NoteOnOffEverySec.ino index 6c506c3..6ae8fdd 100644 --- a/examples/ESP32_NoteOnOffEverySec/ESP32_NoteOnOffEverySec.ino +++ b/examples/ESP32_NoteOnOffEverySec/ESP32_NoteOnOffEverySec.ino @@ -1,12 +1,13 @@ #include #include #include +#include #define SerialMon Serial #include -char ssid[] = "ssid"; // your network SSID (name) -char pass[] = "password"; // your network password (use for WPA, or use as key for WEP) +char ssid[] = "Het internet ga weer nie"; // your network SSID (name) +char pass[] = "MEEN_con2door@nusm"; // your network password (use for WPA, or use as key for WEP) unsigned long t0 = millis(); int8_t isConnected = 0; @@ -53,6 +54,22 @@ void setup() AM_DBG(F("NoteOff"), note); }); + // Set up mDNS responder: + // - first argument is the domain name, in this example + // the fully-qualified domain name is "esp32.local" + // - second argument is the IP address to advertise + // we send our IP address on the WiFi network + if (!MDNS.begin("esp32")) { + AM_DBG("Error setting up MDNS responder!"); + while (1) { + delay(1000); + } + } + AM_DBG("mDNS responder started"); + + // Add service to MDNS-SD + MDNS.addService("apple-midi", "udp", 5004); + AM_DBG(F("Sending NoteOn/Off of note 45, every second")); } diff --git a/examples/ESP32_W5500_Callbacks/ESP32_W5500_Callbacks.ino b/examples/ESP32_W5500_Callbacks/ESP32_W5500_Callbacks.ino index cedb154..1e1a1c2 100644 --- a/examples/ESP32_W5500_Callbacks/ESP32_W5500_Callbacks.ino +++ b/examples/ESP32_W5500_Callbacks/ESP32_W5500_Callbacks.ino @@ -1,10 +1,11 @@ -#include "ETH_Helper.h" - #define SerialMon Serial #define ONE_PARTICIPANT #define USE_EXT_CALLBACKS +#define APPLEMIDI_SMALL #include +#include "ETH_Helper.h" + unsigned long t1 = millis(); int8_t isConnected = 0; @@ -15,8 +16,7 @@ void OnAppleMidiException(const APPLEMIDI_NAMESPACE::ssrc_t&, const APPLEMIDI_NA // ----------------------------------------------------------------------------- // // ----------------------------------------------------------------------------- -void setup() -{ +void setup() { AM_DBG_SETUP(115200); AM_DBG(F("Das Booting")); @@ -29,35 +29,47 @@ void setup() MIDI.begin(MIDI_CHANNEL_OMNI); + // Initialize the Bonjour/MDNS library. You can now reach or ping this + // Arduino via the host name "arduino.local", provided that your operating + // system is Bonjour-enabled (such as MacOS X). + // Always call this before any other method! + auto statusCode = EthernetBonjour.begin(); + + auto bonjourSessionName = std::string(AppleMIDI.getName()) + mdnsAppleMIDI; + + statusCode = EthernetBonjour.addServiceRecord(bonjourSessionName.c_str(), + 5004, + MDNSServiceUDP); + // Normal callbacks - always available // Stay informed on connection status - AppleMIDI.setHandleConnected([](const APPLEMIDI_NAMESPACE::ssrc_t & ssrc, const char* name) { + AppleMIDI.setHandleConnected([](const APPLEMIDI_NAMESPACE::ssrc_t& ssrc, const char* name) { isConnected++; AM_DBG(F("Connected to session"), ssrc, name); }); - AppleMIDI.setHandleDisconnected([](const APPLEMIDI_NAMESPACE::ssrc_t & ssrc) { + AppleMIDI.setHandleDisconnected([](const APPLEMIDI_NAMESPACE::ssrc_t& ssrc) { isConnected--; AM_DBG(F("Disconnected"), ssrc); }); // Extended callback, only available when defining USE_EXT_CALLBACKS - AppleMIDI.setHandleSentRtp([](const APPLEMIDI_NAMESPACE::Rtp_t & rtp) { - // AM_DBG(F("an rtpMessage has been sent with sequenceNr"), rtp.sequenceNr); + AppleMIDI.setHandleSentRtp([](const APPLEMIDI_NAMESPACE::Rtp_t& rtp) { + AM_DBG(F("an rtpMessage has been sent with sequenceNr"), rtp.sequenceNr); }); AppleMIDI.setHandleSentRtpMidi([](const APPLEMIDI_NAMESPACE::RtpMIDI_t& rtpMidi) { AM_DBG(F("an rtpMidiMessage has been sent"), rtpMidi.flags); }); - AppleMIDI.setHandleReceivedRtp([](const APPLEMIDI_NAMESPACE::ssrc_t & ssrc, const APPLEMIDI_NAMESPACE::Rtp_t & rtp, const int32_t& latency) { - // AM_DBG(F("setHandleReceivedRtp"), ssrc, rtp.sequenceNr , "with", latency, "ms latency"); + AppleMIDI.setHandleReceivedRtp([](const APPLEMIDI_NAMESPACE::ssrc_t& ssrc, const APPLEMIDI_NAMESPACE::Rtp_t& rtp, const int32_t& latency) { + AM_DBG(F("setHandleReceivedRtp"), ssrc, rtp.sequenceNr, "with", latency, "ms latency"); }); AppleMIDI.setHandleStartReceivedMidi([](const APPLEMIDI_NAMESPACE::ssrc_t& ssrc) { - // AM_DBG(F("setHandleStartReceivedMidi from SSRC"), ssrc); + AM_DBG(F("setHandleStartReceivedMidi from SSRC"), ssrc); }); AppleMIDI.setHandleReceivedMidi([](const APPLEMIDI_NAMESPACE::ssrc_t& ssrc, byte value) { - // AM_DBG(F("setHandleReceivedMidi from SSRC"), ssrc, ", value:", value); + AM_DBG(F("setHandleReceivedMidi from SSRC"), ssrc, ", value:", value); }); AppleMIDI.setHandleEndReceivedMidi([](const APPLEMIDI_NAMESPACE::ssrc_t& ssrc) { - // AM_DBG(F("setHandleEndReceivedMidi from SSRC"), ssrc); + AM_DBG(F("setHandleEndReceivedMidi from SSRC"), ssrc); }); AppleMIDI.setHandleException(OnAppleMidiException); @@ -83,15 +95,13 @@ void setup() // ----------------------------------------------------------------------------- // // ----------------------------------------------------------------------------- -void loop() -{ +void loop() { // Listen to incoming notes MIDI.read(); // send a note every second // (dont cáll delay(1000) as it will stall the pipeline) - if ((isConnected > 0) && (millis() - t1) > 100) - { + if ((isConnected > 0) && (millis() - t1) > 5) { t1 = millis(); byte note = random(1, 127); @@ -100,20 +110,17 @@ void loop() // AM_DBG(F("\nsendNoteOn"), note, velocity, channel); MIDI.sendNoteOn(note, velocity, channel); - //MIDI.sendNoteOff(note, velocity, channel); + MIDI.sendNoteOff(note, velocity, channel); } -#ifndef ETHERNET3 EthernetBonjour.run(); -#endif } // ----------------------------------------------------------------------------- // // ----------------------------------------------------------------------------- -void OnAppleMidiException(const APPLEMIDI_NAMESPACE::ssrc_t& ssrc, const APPLEMIDI_NAMESPACE::Exception& e, const int32_t value ) { - switch (e) - { +void OnAppleMidiException(const APPLEMIDI_NAMESPACE::ssrc_t& ssrc, const APPLEMIDI_NAMESPACE::Exception& e, const int32_t value) { + switch (e) { case APPLEMIDI_NAMESPACE::Exception::BufferFullException: AM_DBG(F("*** BufferFullException")); break; diff --git a/examples/ESP32_W5500_Callbacks/ETH_Helper.h b/examples/ESP32_W5500_Callbacks/ETH_Helper.h index 86621ba..4533010 100644 --- a/examples/ESP32_W5500_Callbacks/ETH_Helper.h +++ b/examples/ESP32_W5500_Callbacks/ETH_Helper.h @@ -1,9 +1,6 @@ -#ifdef ETHERNET3 -#include -#else #include #include // https://github.com/TrippyLighting/EthernetBonjour -#endif +#include // to get the Mac address #include "esp_mac.h" @@ -17,6 +14,8 @@ byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; +const char* mdnsAppleMIDI = "._apple-midi"; + /* Wiz W5500 reset function. Change this for the specific reset sequence required for your particular board or module. @@ -34,18 +33,11 @@ void hardreset() { bool ETH_startup() { -#ifdef ETHERNET3 - Ethernet.setRstPin(RESET_PIN); - Ethernet.setCsPin(CS_PIN); - Ethernet.init(4); // maxSockNum = 4 Socket 0...3 -> RX/TX Buffer 4k - Serial.println("Resetting Wiz W5500 Ethernet Board... "); - Ethernet.hardreset(); -#else Ethernet.init(CS_PIN); - Serial.println("Resetting Wiz Ethernet Board... "); + AM_DBG("Resetting Wiz Ethernet Board... "); hardreset(); -#endif + // Borrow MAC from the ESP32 wifi esp_read_mac(mac, ESP_MAC_WIFI_STA); /* @@ -56,27 +48,18 @@ bool ETH_startup() comments to the contrary elsewhere). You -must- supply a MAC address here. */ -#ifdef ETHERNET3 - Serial.println("Starting Ethernet3 connection..."); -#else - Serial.println("Starting Ethernet connection..."); -#endif + AM_DBG("Starting Ethernet connection..."); Ethernet.begin(mac); - Serial.print("Ethernet IP is: "); - Serial.println(Ethernet.localIP()); + AM_DBG("Ethernet IP is:", Ethernet.localIP()); /* Sanity checks for W5500 and cable connection. */ - Serial.println("Checking connection."); + AM_DBG("Checking connection."); bool rdy_flag = false; for (uint8_t i = 0; i <= 20; i++) { -#ifdef ETHERNET3 - if ((Ethernet.link() == 0)) { -#else if ((Ethernet.linkStatus() == Unknown)) { -#endif Serial.print("."); rdy_flag = false; delay(80); @@ -86,25 +69,13 @@ bool ETH_startup() } } if (rdy_flag == false) { - Serial.println("\n\r\tHardware fault, or cable problem... cannot continue."); + AM_DBG("\n\r\tHardware fault, or cable problem... cannot continue."); while (true) { delay(10); // Halt. } } else { - Serial.println("OK"); + AM_DBG("OK"); } -#ifndef ETHERNET3 - // Initialize the Bonjour/MDNS library. You can now reach or ping this - // Arduino via the host name "arduino.local", provided that your operating - // system is Bonjour-enabled (such as MacOS X). - // Always call this before any other method! - EthernetBonjour.begin("arduino"); - - EthernetBonjour.addServiceRecord("Arduino._apple-midi", - 5004, - MDNSServiceUDP); -#endif - return true; } From ab010f87b8d953c5ef83b79f57551261668356ad Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Tue, 18 Aug 2026 23:11:13 +0200 Subject: [PATCH 26/31] Update build-arduino.sh --- ci/build-arduino.sh | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ci/build-arduino.sh b/ci/build-arduino.sh index 00aa2b8..5d03014 100644 --- a/ci/build-arduino.sh +++ b/ci/build-arduino.sh @@ -31,22 +31,24 @@ arduino-cli lib install "MIDI library" arduino-cli lib install --git-url https://github.com/sstaub/Ethernet3.git arduino-cli lib install EthernetBonjour -# Compile all *.ino files for the Arduino Uno -for f in **/AVR_*.ino ; do - arduino-cli compile -b arduino:avr:uno $f -done +# Uno has 2 KiB SRAM; skip the full AVR_* set (e.g. AVR_MultipleSessions). +echo "Compiling examples/AVR_NoteOnOffEverySec/AVR_NoteOnOffEverySec.ino for arduino:avr:uno" +arduino-cli compile -b arduino:avr:uno examples/AVR_NoteOnOffEverySec/AVR_NoteOnOffEverySec.ino # Compile all *.ino files for the Arduino Zero for f in **/SAMD_*.ino ; do + echo "Compiling $f for arduino:samd:mkrzero" arduino-cli compile -b arduino:samd:mkrzero $f done # Compile all *.ino files for the ESP8266 for f in **/ESP8266_*.ino ; do + echo "Compiling $f for esp8266:esp8266:generic" arduino-cli compile -b esp8266:esp8266:generic $f done # Compile all *.ino files for the ESP32 for f in **/ESP32_*.ino ; do + echo "Compiling $f for esp32:esp32:esp32" arduino-cli compile -b esp32:esp32:esp32 $f done From 9ac19274da745aa8264dc424ba002478ab5ccd9b Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Tue, 18 Aug 2026 23:19:22 +0200 Subject: [PATCH 27/31] Update ESP32_DynamicInstantiation.ino --- .../ESP32_DynamicInstantiation/ESP32_DynamicInstantiation.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ESP32_DynamicInstantiation/ESP32_DynamicInstantiation.ino b/examples/ESP32_DynamicInstantiation/ESP32_DynamicInstantiation.ino index e1947e1..fb5c3c4 100644 --- a/examples/ESP32_DynamicInstantiation/ESP32_DynamicInstantiation.ino +++ b/examples/ESP32_DynamicInstantiation/ESP32_DynamicInstantiation.ino @@ -1,6 +1,6 @@ #include -#include "ETH_Helper.h" +#include "ETH_helper.h" #define SerialMon Serial #include "midiHelpers.h" From dc8ff5dd800dc508a67c04bd2b7cbece3a7a620a Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Tue, 18 Aug 2026 23:24:36 +0200 Subject: [PATCH 28/31] remove references to Ethernet3 --- ci/build-arduino.sh | 1 - .../AVR_E3_NoteOnOffEverySec.ino | 78 ------------------- .../ESP32_DynamicInstantiation/ETH_helper.h | 23 +----- 3 files changed, 1 insertion(+), 101 deletions(-) delete mode 100644 examples/AVR_E3_NoteOnOffEverySec/AVR_E3_NoteOnOffEverySec.ino diff --git a/ci/build-arduino.sh b/ci/build-arduino.sh index 5d03014..34c74bc 100644 --- a/ci/build-arduino.sh +++ b/ci/build-arduino.sh @@ -28,7 +28,6 @@ ln -s $GITHUB_WORKSPACE $HOME/Arduino/libraries/CI_Test_Library arduino-cli lib install Ethernet arduino-cli lib install "MIDI library" -arduino-cli lib install --git-url https://github.com/sstaub/Ethernet3.git arduino-cli lib install EthernetBonjour # Uno has 2 KiB SRAM; skip the full AVR_* set (e.g. AVR_MultipleSessions). diff --git a/examples/AVR_E3_NoteOnOffEverySec/AVR_E3_NoteOnOffEverySec.ino b/examples/AVR_E3_NoteOnOffEverySec/AVR_E3_NoteOnOffEverySec.ino deleted file mode 100644 index aee6d15..0000000 --- a/examples/AVR_E3_NoteOnOffEverySec/AVR_E3_NoteOnOffEverySec.ino +++ /dev/null @@ -1,78 +0,0 @@ -#include // from https://github.com/sstaub/Ethernet3 - -#define SerialMon Serial -#include - -// Enter a MAC address for your controller below. -// Newer Ethernet shields have a MAC address printed on a sticker on the shield -byte mac[] = { - 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED -}; - -unsigned long t1 = millis(); -int8_t isConnected = 0; - -APPLEMIDI_CREATE_DEFAULTSESSION_INSTANCE(); - -// ----------------------------------------------------------------------------- -// -// ----------------------------------------------------------------------------- -void setup() -{ - AM_DBG_SETUP(115200); - AM_DBG(F("Booting")); - - if (Ethernet.begin(mac) == 0) { - AM_DBG(F("Failed DHCP, check network cable & reboot")); - for (;;); - } - - AM_DBG(F("OK, now make sure you an rtpMIDI session that is Enabled")); - AM_DBG(F("Add device named Arduino with Host"), Ethernet.localIP(), "Port", AppleMIDI.getPort(), "(Name", AppleMIDI.getName(), ")"); - AM_DBG(F("Select and then press the Connect button")); - AM_DBG(F("Then open a MIDI listener and monitor incoming notes")); - - MIDI.begin(); - - // Stay informed on connection status - AppleMIDI.setHandleConnected([](const APPLEMIDI_NAMESPACE::ssrc_t & ssrc, const char* name) { - isConnected++; - AM_DBG(F("Connected to session"), ssrc, name); - }); - AppleMIDI.setHandleDisconnected([](const APPLEMIDI_NAMESPACE::ssrc_t & ssrc) { - isConnected--; - AM_DBG(F("Disconnected"), ssrc); - }); - - MIDI.setHandleNoteOn([](byte channel, byte note, byte velocity) { - AM_DBG(F("NoteOn"), note); - }); - MIDI.setHandleNoteOff([](byte channel, byte note, byte velocity) { - AM_DBG(F("NoteOff"), note); - }); - - AM_DBG(F("Sending MIDI messages every second")); -} - -// ----------------------------------------------------------------------------- -// -// ----------------------------------------------------------------------------- -void loop() -{ - // Listen to incoming notes - MIDI.read(); - - // send a note every second - // (dont cáll delay(1000) as it will stall the pipeline) - if ((isConnected > 0) && (millis() - t1) > 1000) - { - t1 = millis(); - - byte note = random(1, 127); - byte velocity = 55; - byte channel = 1; - - MIDI.sendNoteOn(note, velocity, channel); -// MIDI.sendNoteOff(note, velocity, channel); - } -} diff --git a/examples/ESP32_DynamicInstantiation/ETH_helper.h b/examples/ESP32_DynamicInstantiation/ETH_helper.h index c77aa05..03e07fa 100644 --- a/examples/ESP32_DynamicInstantiation/ETH_helper.h +++ b/examples/ESP32_DynamicInstantiation/ETH_helper.h @@ -1,9 +1,5 @@ -#ifdef ETHERNET3 -#include -#else #include #include // https://github.com/TrippyLighting/EthernetBonjour -#endif // to get the Mac address #include "esp_mac.h" @@ -34,17 +30,9 @@ void hardreset() { bool ETH_startup() { -#ifdef ETHERNET3 - Ethernet.setRstPin(RESET_PIN); - Ethernet.setCsPin(CS_PIN); - Ethernet.init(4); // maxSockNum = 4 Socket 0...3 -> RX/TX Buffer 4k - Serial.println("Resetting Wiz W5500 Ethernet Board... "); - Ethernet.hardreset(); -#else Ethernet.init(CS_PIN); Serial.println("Resetting Wiz Ethernet Board... "); hardreset(); -#endif esp_read_mac(mac, ESP_MAC_WIFI_STA); @@ -56,12 +44,9 @@ bool ETH_startup() comments to the contrary elsewhere). You -must- supply a MAC address here. */ -#ifdef ETHERNET3 - Serial.println("Starting Ethernet3 connection..."); -#else Serial.println("Starting Ethernet connection..."); //if (Ethernet.hardwareStatus() == EthernetNoHardware) Serial.println("No EthernetHW"); -#endif + Ethernet.begin(mac); Serial.print("Ethernet IP is: "); Serial.println(Ethernet.localIP()); @@ -72,11 +57,7 @@ bool ETH_startup() Serial.println("Checking connection."); bool rdy_flag = false; for (uint8_t i = 0; i <= 20; i++) { -#ifdef ETHERNET3 - if ((Ethernet.link() == 0)) { -#else if ((Ethernet.linkStatus() == Unknown)) { -#endif Serial.print("."); rdy_flag = false; delay(80); @@ -94,7 +75,6 @@ bool ETH_startup() Serial.println("OK"); } -#ifndef ETHERNET3 // Initialize the Bonjour/MDNS library. You can now reach or ping this // Arduino via the host name "arduino.local", provided that your operating // system is Bonjour-enabled (such as MacOS X). @@ -104,7 +84,6 @@ bool ETH_startup() EthernetBonjour.addServiceRecord("apple-midi", //Arduino._apple-midi doesnt work 5004, MDNSServiceUDP); -#endif return true; } \ No newline at end of file From 469fd3589c148ae9126803bc61e6d3ab4acc5c39 Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Tue, 18 Aug 2026 23:30:05 +0200 Subject: [PATCH 29/31] skip test using bespoke PIN nrs --- ci/build-arduino.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ci/build-arduino.sh b/ci/build-arduino.sh index 34c74bc..0066f67 100644 --- a/ci/build-arduino.sh +++ b/ci/build-arduino.sh @@ -46,8 +46,12 @@ for f in **/ESP8266_*.ino ; do arduino-cli compile -b esp8266:esp8266:generic $f done -# Compile all *.ino files for the ESP32 +# Compile ESP32 examples. Skip DynamicInstantiation: it needs a W5500 helper +# (D5/D7 pins) and is not a generic esp32:esp32:esp32 target. for f in **/ESP32_*.ino ; do + case "$f" in + *ESP32_DynamicInstantiation*) continue ;; + esac echo "Compiling $f for esp32:esp32:esp32" arduino-cli compile -b esp32:esp32:esp32 $f done From e7c9c22b993e58772d9c805f605aa4165437a219 Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Wed, 19 Aug 2026 08:04:14 +0200 Subject: [PATCH 30/31] Revise README for OS compatibility and installation Updated compatibility information and installation instructions in README. --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0bb69e9..36d597e 100755 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Enables an Arduino with IP/UDP capabilities (Ethernet shield, ESP8266, ESP32, .. ## Features * Build on top of the popular [FortySevenEffects MIDI library](https://github.com/FortySevenEffects/arduino_midi_library) -* Tested with AppleMIDI on Mac OS (Big Sur) and using [rtpMIDI](https://www.tobias-erichsen.de/software/rtpmidi.html) from Tobias Erichsen on Windows 10 +* Tested with AppleMIDI on Mac OS (Tahoe) and using [rtpMIDI](https://www.tobias-erichsen.de/software/rtpmidi.html) from Tobias Erichsen on Windows 10 & 11. * Send and receive all MIDI messages * Uses callbacks to receive MIDI commands (no need for polling) * Automatic instantiation of AppleMIDI object (see at the end of 'AppleMIDI.h') @@ -19,7 +19,7 @@ Enables an Arduino with IP/UDP capabilities (Ethernet shield, ESP8266, ESP32, .. * Large SysEx is split per RFC 6295 (`F0`/`F7`); see [SysEx and extra F0/F7 bytes](#sysex-and-extra-f0f7-bytes) ## Installation -From the Arduino IDE Library Manager, search for AppleMIDI +Clone locally or download from the Arduino IDE Library Manager, search for AppleMIDI Installation @@ -62,6 +62,7 @@ More usages in the [examples](https://github.com/lathoub/Arduino-AppleMIDI-Libra * ESP32 (Adafruit HUZZAH32 – ESP32 Feather Board) Wi-Fi * ESP32 with W5500 [Setup](https://github.com/lathoub/Arduino-AppleMIDI-Library/discussions/135) * ESP32 ETH W5500 (Native ESP32 W5500 support) +* Seeed Studio XIAO ESP32 C3 * Teensy 3.2 & 4.1 * Adafruit Feather M0 WiFi - ATSAMD21 + ATWINC1500 @@ -94,14 +95,14 @@ Define these in the sketch **before** `#include `: `APPLEMIDI_SMALL` is used in `examples/AVR_MinMemUsage`. More on the [wiki](https://github.com/lathoub/Arduino-AppleMIDI-Library/wiki/Memory-footprint). ### Ethernet buffer size -It's highly recommended to modify the [Ethernet library](https://github.com/arduino-libraries/Ethernet) or use the [Ethernet3 library](https://github.com/sstaub/Ethernet3) to avoid buffer overruns - [learn more](https://github.com/lathoub/Arduino-AppleMIDI-Library/wiki/Enlarge-Ethernet-buffer-size-to-avoid-dropping-UDP-packages) +It's highly recommended to modify the [Ethernet library](https://github.com/arduino-libraries/Ethernet) - [learn more](https://github.com/lathoub/Arduino-AppleMIDI-Library/wiki/Enlarge-Ethernet-buffer-size-to-avoid-dropping-UDP-packages) ### Latency Use wired Ethernet to reduce latency, Wi-Fi increases latency and latency varies. More of the [wiki](https://github.com/lathoub/Arduino-AppleMIDI-Library/wiki/Keeping-Latency-under-control) ## Arduino IDE (arduino.cc) * 1.8.16 -* 2.3.7 +* 2.3.10 ## Contributing I would love to include your enhancements or bug fixes! In lieu of a formal styleguide, please take care to maintain the existing coding style. Please test your code before sending a pull request. It would be very helpful if you include a detailed explanation of your changes in the pull request. From 131148e0b1121a60c777cfe9f2112373e27badde Mon Sep 17 00:00:00 2001 From: Bart De Lathouwer <4082369+lathoub@users.noreply.github.com> Date: Wed, 19 Aug 2026 08:09:33 +0200 Subject: [PATCH 31/31] Update README with GitHub release badge Added GitHub release badge to README and improved formatting. --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 36d597e..dca5153 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -# AppleMIDI (aka rtpMIDI) for Arduino [![License: CC BY-SA 4.0](https://img.shields.io/badge/License-CC%20BY--SA%204.0-lightgrey.svg)](http://creativecommons.org/licenses/by-sa/4.0/) +# AppleMIDI (aka rtpMIDI) for Arduino + +[![GitHub release](https://img.shields.io/github/release/lathoub/Arduino-AppleMIDI-Library.svg?maxAge=3600)](https://github.com/lathoub/Arduino-AppleMIDI-Library/releases/latest) +[![License: CC BY-SA 4.0](https://img.shields.io/badge/License-CC%20BY--SA%204.0-lightgrey.svg)](http://creativecommons.org/licenses/by-sa/4.0/) Enables an Arduino with IP/UDP capabilities (Ethernet shield, ESP8266, ESP32, ...) to participate in an AppleMIDI session.