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/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 22a2317..dca5153 100755
--- a/README.md
+++ b/README.md
@@ -1,4 +1,7 @@
-# AppleMIDI (aka rtpMIDI) for Arduino [](http://creativecommons.org/licenses/by-sa/4.0/)
+# AppleMIDI (aka rtpMIDI) for Arduino
+
+[](https://github.com/lathoub/Arduino-AppleMIDI-Library/releases/latest)
+[](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.
@@ -6,20 +9,20 @@ 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')
+* 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
+Clone locally or download from the Arduino IDE Library Manager, search for AppleMIDI
@@ -62,6 +65,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
@@ -75,22 +79,33 @@ 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)
+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)
+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.
diff --git a/ci/build-arduino.sh b/ci/build-arduino.sh
index 6cf1222..0066f67 100644
--- a/ci/build-arduino.sh
+++ b/ci/build-arduino.sh
@@ -13,40 +13,45 @@ 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
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
-# 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
-# arduino-cli compile -b arduino:esp8266:??? $f
-# done
+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
-# arduino-cli compile -b arduino:esp32:??? $f
-# done
+# 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
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/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/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/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"
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
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;
}
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 8673196..18dd0c3 100644
--- a/library.properties
+++ b/library.properties
@@ -1,11 +1,11 @@
name=AppleMIDI
-version=3.3.0
+version=3.5.0
author=lathoub
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
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 2c4b640..f1f4132 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
{
@@ -60,7 +61,7 @@ class AppleMIDISession
#endif
};
- virtual ~AppleMIDISession(){};
+ ~AppleMIDISession(){};
AppleMIDISession &setHandleConnected(void (*fptr)(const ssrc_t &, const char *))
{
@@ -189,8 +190,32 @@ class AppleMIDISession
dataPort.stop();
}
- bool beginTransmission(MIDI_NAMESPACE::MidiType)
+ 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].remoteDataPort != 0 &&
+ participants[i].remoteIP != (IPAddress)INADDR_NONE)
+ {
+ connected = true;
+ break;
+ }
+ }
+ if (!connected)
+ return false;
+#else
+ if (participant.ssrc == 0 ||
+ participant.remoteDataPort == 0 ||
+ participant.remoteIP == (IPAddress)INADDR_NONE)
+ return false;
+#endif
+
+ _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
@@ -218,11 +243,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
- return (dataPort.remoteIP() != (IPAddress)INADDR_NONE && participants.size() > 0);
-#else
- return (dataPort.remoteIP() != (IPAddress)INADDR_NONE && participant.ssrc != 0);
-#endif
+ return true;
};
void write(byte byte)
@@ -231,8 +252,9 @@ 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)
{
+ // 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);
@@ -262,7 +284,7 @@ class AppleMIDISession
// MIDI-read() must be called at the start of loop()
unsigned available()
{
- now = millis();
+ refreshNow();
#ifdef APPLEMIDI_INITIATOR
manageSessionInvites();
@@ -283,7 +305,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();
@@ -322,13 +346,15 @@ 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;
ssrc_t ssrc = 0;
uint16_t port = DEFAULT_CONTROL_PORT;
+ bool _acceptIncomingMidi = false;
+ bool _writingSysEx = false;
#ifdef ONE_PARTICIPANT
Participant participant;
#else
@@ -342,6 +368,8 @@ class AppleMIDISession
private:
size_t readControlPackets();
size_t readDataPackets();
+ size_t readUdpDatagram(UdpClass &, RtpBuffer_t &);
+ void drainUdpRemainder(UdpClass &);
void parseControlPackets();
void parseDataPackets();
@@ -367,7 +395,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 &);
@@ -376,13 +406,15 @@ class AppleMIDISession
void writeRtpMidiToAllParticipants();
void writeRtpMidiBuffer(Participant *);
+#ifndef APPLEMIDI_NO_RECEIVER_FEEDBACK
void manageReceiverFeedback();
+#endif
void manageSessionInvites();
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 95f6c36..b792c99 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();
}
- return controlBuffer.size();
+ 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 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);
@@ -225,7 +254,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;
@@ -296,24 +327,30 @@ void AppleMIDISession::ReceivedDataInvitationAccep
}
pParticipant->invitationStatus = DataInvitationAccepted;
+ pParticipant->remoteDataPort = dataPort.remotePort();
}
// 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)
{
- for (auto i = 0; i < participants.size(); i++)
+#ifndef ONE_PARTICIPANT
+ for (size_t i = 0; i < participants.size(); i++)
{
- if (invitationRejected.ssrc == participants[i].ssrc)
+ if (invitationRejected.initiatorToken == participants[i].initiatorToken)
{
-#ifndef ONE_PARTICIPANT
participants.erase(i);
-#else
- participant.ssrc = 0;
-#endif
return;
}
}
+#else
+ if (invitationRejected.initiatorToken != participant.initiatorToken)
+ return;
+ participant.ssrc = 0;
+ participant.kind = Listener;
+ participant.initiatorToken = 0;
+#endif
}
#endif
@@ -399,13 +436,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;
@@ -448,11 +485,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
}
}
@@ -545,6 +583,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)
@@ -570,6 +609,7 @@ void AppleMIDISession::writeReceiverFeedback(const
controlPort.endPacket();
controlPort.flush();
}
+#endif
// Send a synchronization packet on the data port.
template
@@ -632,11 +672,13 @@ void AppleMIDISession::writeRtpMidiToAllParticipan
for (size_t i = 0; i < participants.size(); i++)
{
auto pParticipant = &participants[i];
-
+ if (pParticipant->ssrc == 0 || pParticipant->remoteDataPort == 0)
+ continue;
writeRtpMidiBuffer(pParticipant);
}
#else
- writeRtpMidiBuffer(&participant);
+ if (participant.ssrc != 0 && participant.remoteDataPort != 0)
+ writeRtpMidiBuffer(&participant);
#endif
outMidiBuffer.clear();
}
@@ -708,7 +750,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)
@@ -717,11 +759,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;
@@ -740,21 +779,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();
@@ -817,8 +863,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
@@ -871,10 +928,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)
@@ -883,18 +938,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
@@ -909,7 +971,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;
@@ -1001,7 +1063,8 @@ void AppleMIDISession::manageSessionInvites()
if (pParticipant->invitationStatus == ControlInvitationAccepted
|| pParticipant->invitationStatus == AwaitingDataInvitationAccepted)
{
- writeInvitation(dataPort, pParticipant->remoteIP, pParticipant->remotePort + 1, 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;
}
}
@@ -1011,6 +1074,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
@@ -1053,6 +1117,7 @@ void AppleMIDISession::manageReceiverFeedback()
}
}
}
+#endif
#ifdef APPLEMIDI_INITIATOR
@@ -1133,41 +1198,54 @@ 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
- 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 != _exceptionCallback)
+ _exceptionCallback(ssrc, ParticipantNotFoundException, rtp.ssrc);
+#endif
+ return;
+ }
- if (nullptr != _receivedRtpCallback)
- _receivedRtpCallback(pParticipant->ssrc, rtp, latency);
+#ifndef APPLEMIDI_NO_RECEIVER_FEEDBACK
+ if (pParticipant->doReceiverFeedback == false)
+ pParticipant->receiverFeedbackStartTime = now;
+ pParticipant->doReceiverFeedback = true;
#endif
- pParticipant->receiveSequenceNr = rtp.sequenceNr;
- }
+#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
{
- // TODO??? re-connect?
+ // 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)
+ _receivedRtpCallback(pParticipant->ssrc, rtp, latency);
+#endif
+
+ 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,11 +1256,23 @@ void AppleMIDISession::StartReceivedMidi()
template
void AppleMIDISession::ReceivedMidi(byte value)
{
+ if (!_acceptIncomingMidi)
+ return;
+
#ifdef USE_EXT_CALLBACKS
if (nullptr != _receivedMidiByteCallback)
_receivedMidiByteCallback(ssrc, value);
#endif
+ if (inMidiBuffer.full())
+ {
+#ifdef USE_EXT_CALLBACKS
+ if (nullptr != _exceptionCallback)
+ _exceptionCallback(ssrc, BufferFullException, 1);
+#endif
+ return;
+ }
+
inMidiBuffer.push_back(value);
}
@@ -1190,6 +1280,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/AppleMIDI_Defs.h b/src/AppleMIDI_Defs.h
index 5c2bd5c..56f0b39 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
@@ -50,11 +50,27 @@ 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
+// Size gates (define in the sketch before #include ):
// #define USE_EXT_CALLBACKS
// #define ONE_PARTICIPANT // memory optimization
// #define USE_DIRECTORY
+// #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_Parser.h b/src/AppleMIDI_Parser.h
index 60f4feb..2001aa1 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)
{
@@ -354,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)
{
@@ -379,8 +407,9 @@ class AppleMIDIParser
session->ReceivedInvitationRejected(invitationRejected);
- return parserReturn::Processed;
+ return retVal;
}
+#endif
else if (0 == memcmp(command, amBitrateReceiveLimit, sizeof(amBitrateReceiveLimit)))
{
AppleMIDI_BitrateReceiveLimit bitrateReceiveLimit;
@@ -411,7 +440,6 @@ class AppleMIDIParser
return parserReturn::Processed;
}
-#endif
return parserReturn::UnexpectedData;
}
};
diff --git a/src/AppleMIDI_Participant.h b/src/AppleMIDI_Participant.h
index 0f6c36d..70e9d98 100644
--- a/src/AppleMIDI_Participant.h
+++ b/src/AppleMIDI_Participant.h
@@ -13,19 +13,23 @@ 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;
- unsigned long receiverFeedbackStartTime = 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;
- 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;
diff --git a/src/AppleMIDI_Settings.h b/src/AppleMIDI_Settings.h
index 6ad675a..ce64b08 100644
--- a/src/AppleMIDI_Settings.h
+++ b/src/AppleMIDI_Settings.h
@@ -10,7 +10,12 @@ 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.
+ static const size_t MaxMidiInSize = MaxBufferSize;
+ static const size_t MaxMidiOutSize = MaxBufferSize;
static const size_t MaxSessionNameLen = 24;
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/rtpMIDI_Parser.h b/src/rtpMIDI_Parser.h
index 7d30cbd..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;
@@ -51,6 +53,8 @@ class rtpMIDIParser
public:
AppleMIDISession * session;
+
+ bool isMidMessage() const { return _rtpHeadersComplete; }
// Parse the incoming string
// return:
@@ -176,18 +180,33 @@ class rtpMIDIParser
}
_rtpHeadersComplete = true;
-
+#ifndef APPLEMIDI_SKIP_JOURNALS
// initialize the Journal Section
_journalSectionComplete = false;
_channelJournalSectionComplete = false;
_journalTotalChannels = 0;
+#endif
}
// Always a MIDI section
if (midiCommandLength > 0)
{
auto retVal = decodeMIDICommandSection(buffer);
- if (retVal != parserReturn::Processed) return retVal;
+ if (retVal != parserReturn::Processed)
+ {
+ if (retVal != parserReturn::NotEnoughData)
+ {
+ _rtpHeadersComplete = false;
+#ifndef APPLEMIDI_SKIP_JOURNALS
+ _journalSectionComplete = false;
+ _channelJournalSectionComplete = false;
+ _journalTotalChannels = 0;
+#endif
+ midiCommandLength = 0;
+ runningstatus = 0;
+ }
+ return retVal;
+ }
}
// The payload MAY also contain a journal section. The journal section
@@ -197,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:
@@ -217,6 +241,7 @@ class rtpMIDIParser
_journalTotalChannels = 0;
return retVal;
}
+#endif
}
_rtpHeadersComplete = false;
@@ -224,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_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 */
diff --git a/src/rtpMIDI_Parser_JournalSection.hpp b/src/rtpMIDI_Parser_JournalSection.hpp
index 55625ae..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.
@@ -95,6 +99,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;
@@ -140,7 +146,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 +179,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--;
}
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 */
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