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 [![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. @@ -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 Installation @@ -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