From dd3d555bb9f14d6175e3c4c16afaca3b34fb2b48 Mon Sep 17 00:00:00 2001 From: reuk Date: Wed, 5 Mar 2025 21:19:44 +0000 Subject: [PATCH] UMPMidi1ToBytestreamTranslator: Refactor to separate responsibilities between translator and extractor --- .../Builds/Android/app/CMakeLists.txt | 2 + .../VisualStudio2019/DemoRunner_App.vcxproj | 3 + .../DemoRunner_App.vcxproj.filters | 3 + .../VisualStudio2022/DemoRunner_App.vcxproj | 3 + .../DemoRunner_App.vcxproj.filters | 3 + .../Builds/Android/app/CMakeLists.txt | 2 + .../AudioPerformanceTest_App.vcxproj | 3 + .../AudioPerformanceTest_App.vcxproj.filters | 3 + .../Builds/Android/app/CMakeLists.txt | 2 + .../AudioPluginHost_App.vcxproj | 3 + .../AudioPluginHost_App.vcxproj.filters | 3 + .../AudioPluginHost_App.vcxproj | 3 + .../AudioPluginHost_App.vcxproj.filters | 3 + .../Builds/Android/app/CMakeLists.txt | 2 + .../NetworkGraphicsDemo_App.vcxproj | 3 + .../NetworkGraphicsDemo_App.vcxproj.filters | 3 + .../UnitTestRunner_ConsoleApp.vcxproj | 3 + .../UnitTestRunner_ConsoleApp.vcxproj.filters | 3 + .../UnitTestRunner_ConsoleApp.vcxproj | 3 + .../UnitTestRunner_ConsoleApp.vcxproj.filters | 3 + .../WindowsDLL_DynamicLibrary.vcxproj | 3 + .../WindowsDLL_DynamicLibrary.vcxproj.filters | 3 + .../juce_audio_basics/juce_audio_basics.cpp | 1 + .../midi/juce_MidiDataConcatenator.h | 347 +++++++++++------- .../midi/juce_MidiDataConcatenator_test.cpp | 227 ++++++++++++ .../midi/ump/juce_UMPConverters.h | 2 +- .../ump/juce_UMPMidi1ToBytestreamTranslator.h | 213 +++++++---- .../midi/ump/juce_UMP_test.cpp | 6 +- 28 files changed, 661 insertions(+), 197 deletions(-) create mode 100644 modules/juce_audio_basics/midi/juce_MidiDataConcatenator_test.cpp diff --git a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt index f10dcff831..7c5d728c4b 100644 --- a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt +++ b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt @@ -120,6 +120,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_basics/midi/juce_MidiBuffer.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiBuffer.h" "../../../../../modules/juce_audio_basics/midi/juce_MidiDataConcatenator.h" + "../../../../../modules/juce_audio_basics/midi/juce_MidiDataConcatenator_test.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiFile.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiFile.h" "../../../../../modules/juce_audio_basics/midi/juce_MidiKeyboardState.cpp" @@ -2784,6 +2785,7 @@ set_source_files_properties( "../../../../../modules/juce_audio_basics/midi/juce_MidiBuffer.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiBuffer.h" "../../../../../modules/juce_audio_basics/midi/juce_MidiDataConcatenator.h" + "../../../../../modules/juce_audio_basics/midi/juce_MidiDataConcatenator_test.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiFile.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiFile.h" "../../../../../modules/juce_audio_basics/midi/juce_MidiKeyboardState.cpp" diff --git a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj index e891a66b79..be33a6b21f 100644 --- a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj @@ -225,6 +225,9 @@ true + + true + true diff --git a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters index f32366ff80..1ec837e4fd 100644 --- a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters @@ -910,6 +910,9 @@ JUCE Modules\juce_audio_basics\midi + + JUCE Modules\juce_audio_basics\midi + JUCE Modules\juce_audio_basics\midi diff --git a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj index 7a29c1ea99..d7689e84d3 100644 --- a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj @@ -225,6 +225,9 @@ true + + true + true diff --git a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters index 87dd169013..cc8e61515f 100644 --- a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters @@ -910,6 +910,9 @@ JUCE Modules\juce_audio_basics\midi + + JUCE Modules\juce_audio_basics\midi + JUCE Modules\juce_audio_basics\midi diff --git a/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt b/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt index 54d4bd2bde..1419644aee 100644 --- a/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt +++ b/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt @@ -75,6 +75,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_basics/midi/juce_MidiBuffer.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiBuffer.h" "../../../../../modules/juce_audio_basics/midi/juce_MidiDataConcatenator.h" + "../../../../../modules/juce_audio_basics/midi/juce_MidiDataConcatenator_test.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiFile.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiFile.h" "../../../../../modules/juce_audio_basics/midi/juce_MidiKeyboardState.cpp" @@ -2353,6 +2354,7 @@ set_source_files_properties( "../../../../../modules/juce_audio_basics/midi/juce_MidiBuffer.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiBuffer.h" "../../../../../modules/juce_audio_basics/midi/juce_MidiDataConcatenator.h" + "../../../../../modules/juce_audio_basics/midi/juce_MidiDataConcatenator_test.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiFile.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiFile.h" "../../../../../modules/juce_audio_basics/midi/juce_MidiKeyboardState.cpp" diff --git a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj index 420f64c1b2..5552e25e20 100644 --- a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj +++ b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj @@ -183,6 +183,9 @@ true + + true + true diff --git a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters index 1070784acd..5ebc7ad92f 100644 --- a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters +++ b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters @@ -691,6 +691,9 @@ JUCE Modules\juce_audio_basics\midi + + JUCE Modules\juce_audio_basics\midi + JUCE Modules\juce_audio_basics\midi diff --git a/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt b/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt index ecd57a826d..725d183350 100644 --- a/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt +++ b/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt @@ -108,6 +108,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_basics/midi/juce_MidiBuffer.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiBuffer.h" "../../../../../modules/juce_audio_basics/midi/juce_MidiDataConcatenator.h" + "../../../../../modules/juce_audio_basics/midi/juce_MidiDataConcatenator_test.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiFile.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiFile.h" "../../../../../modules/juce_audio_basics/midi/juce_MidiKeyboardState.cpp" @@ -2539,6 +2540,7 @@ set_source_files_properties( "../../../../../modules/juce_audio_basics/midi/juce_MidiBuffer.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiBuffer.h" "../../../../../modules/juce_audio_basics/midi/juce_MidiDataConcatenator.h" + "../../../../../modules/juce_audio_basics/midi/juce_MidiDataConcatenator_test.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiFile.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiFile.h" "../../../../../modules/juce_audio_basics/midi/juce_MidiKeyboardState.cpp" diff --git a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj index 6634a0b4ff..2f4c8366e7 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj @@ -191,6 +191,9 @@ true + + true + true diff --git a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters index 8cce520998..053acdd3b0 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters @@ -766,6 +766,9 @@ JUCE Modules\juce_audio_basics\midi + + JUCE Modules\juce_audio_basics\midi + JUCE Modules\juce_audio_basics\midi diff --git a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj index 5d4eae80e8..2ed07e8ac6 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj @@ -191,6 +191,9 @@ true + + true + true diff --git a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters index 949e748965..0ce9355114 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters @@ -766,6 +766,9 @@ JUCE Modules\juce_audio_basics\midi + + JUCE Modules\juce_audio_basics\midi + JUCE Modules\juce_audio_basics\midi diff --git a/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt b/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt index 718467b2f9..1803ab537e 100644 --- a/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt +++ b/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt @@ -79,6 +79,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_basics/midi/juce_MidiBuffer.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiBuffer.h" "../../../../../modules/juce_audio_basics/midi/juce_MidiDataConcatenator.h" + "../../../../../modules/juce_audio_basics/midi/juce_MidiDataConcatenator_test.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiFile.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiFile.h" "../../../../../modules/juce_audio_basics/midi/juce_MidiKeyboardState.cpp" @@ -2437,6 +2438,7 @@ set_source_files_properties( "../../../../../modules/juce_audio_basics/midi/juce_MidiBuffer.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiBuffer.h" "../../../../../modules/juce_audio_basics/midi/juce_MidiDataConcatenator.h" + "../../../../../modules/juce_audio_basics/midi/juce_MidiDataConcatenator_test.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiFile.cpp" "../../../../../modules/juce_audio_basics/midi/juce_MidiFile.h" "../../../../../modules/juce_audio_basics/midi/juce_MidiKeyboardState.cpp" diff --git a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj index 29abe0dbf0..cf7f1df860 100644 --- a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj +++ b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj @@ -183,6 +183,9 @@ true + + true + true diff --git a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters index 117b396858..a305dc92a1 100644 --- a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters +++ b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters @@ -721,6 +721,9 @@ JUCE Modules\juce_audio_basics\midi + + JUCE Modules\juce_audio_basics\midi + JUCE Modules\juce_audio_basics\midi diff --git a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj index 8ffcc99109..cd20e16a02 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj +++ b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj @@ -199,6 +199,9 @@ true + + true + true diff --git a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters index 1d47d6d504..00b6db35ee 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters +++ b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters @@ -814,6 +814,9 @@ JUCE Modules\juce_audio_basics\midi + + JUCE Modules\juce_audio_basics\midi + JUCE Modules\juce_audio_basics\midi diff --git a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj index cb8e508360..0165f41d94 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj +++ b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj @@ -199,6 +199,9 @@ true + + true + true diff --git a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters index 5234ed2371..6773d7c1d9 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters +++ b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters @@ -814,6 +814,9 @@ JUCE Modules\juce_audio_basics\midi + + JUCE Modules\juce_audio_basics\midi + JUCE Modules\juce_audio_basics\midi diff --git a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj index c35b4dfb84..d21eefac9e 100644 --- a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj +++ b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj @@ -182,6 +182,9 @@ true + + true + true diff --git a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj.filters b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj.filters index 47c5735c77..fdc7807845 100644 --- a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj.filters +++ b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj.filters @@ -718,6 +718,9 @@ JUCE Modules\juce_audio_basics\midi + + JUCE Modules\juce_audio_basics\midi + JUCE Modules\juce_audio_basics\midi diff --git a/modules/juce_audio_basics/juce_audio_basics.cpp b/modules/juce_audio_basics/juce_audio_basics.cpp index c3830efe6e..20a2e17088 100644 --- a/modules/juce_audio_basics/juce_audio_basics.cpp +++ b/modules/juce_audio_basics/juce_audio_basics.cpp @@ -112,5 +112,6 @@ #if JUCE_UNIT_TESTS #include "utilities/juce_ADSR_test.cpp" + #include "midi/juce_MidiDataConcatenator_test.cpp" #include "midi/ump/juce_UMP_test.cpp" #endif diff --git a/modules/juce_audio_basics/midi/juce_MidiDataConcatenator.h b/modules/juce_audio_basics/midi/juce_MidiDataConcatenator.h index 12c2309bfb..27b3f9720f 100644 --- a/modules/juce_audio_basics/midi/juce_MidiDataConcatenator.h +++ b/modules/juce_audio_basics/midi/juce_MidiDataConcatenator.h @@ -35,6 +35,163 @@ namespace juce { +enum class SysexExtractorCallbackKind +{ + notSysex, + ongoingSysex, + lastSysex, +}; + +class BytestreamSysexExtractor +{ +public: + void reset() + { + state = RunningStatus{}; + } + + template + void push (Span bytes, Callback&& callback) + { + for (const auto pair : enumerate (bytes)) + { + const auto index = pair.index; + const auto byte = pair.value; + + state = std::invoke ([&]() -> State + { + if (auto* inSysex = std::get_if (&state)) + { + if (byte == std::byte { 0xf0 }) + { + callback (SysexExtractorCallbackKind::lastSysex, + Span { bytes.data() + index - inSysex->numBytes, inSysex->numBytes }); + return InSysex { 1 }; + } + + if (byte == std::byte { 0xf7 }) + { + callback (SysexExtractorCallbackKind::lastSysex, + Span { bytes.data() + index - inSysex->numBytes, inSysex->numBytes + 1 }); + return RunningStatus{}; + } + + if (isRealtimeMessage (byte)) + { + callback (SysexExtractorCallbackKind::ongoingSysex, + Span { bytes.data() + index - inSysex->numBytes, inSysex->numBytes }); + callback (SysexExtractorCallbackKind::notSysex, + Span { bytes.data() + index, 1 }); + return InSysex{}; + } + + if (isStatusByte (byte)) + { + callback (SysexExtractorCallbackKind::lastSysex, + Span { bytes.data() + index - inSysex->numBytes, inSysex->numBytes }); + return RunningStatus { 1, { byte } }; + } + + return InSysex { inSysex->numBytes + 1 }; + } + + if (auto* runningStatus = std::get_if (&state)) + { + if (byte == std::byte { 0xf0 }) + return InSysex { 1 }; + + const auto nextRunningStatus = std::invoke ([&] + { + if (isRealtimeMessage (byte)) + { + callback (SysexExtractorCallbackKind::notSysex, + Span { bytes.data() + index, 1 }); + return *runningStatus; + } + + if (isInitialByte (byte)) + return RunningStatus{}.withAppendedByte (byte); + + if (0 < runningStatus->size && runningStatus->size < runningStatus->data.size()) + return runningStatus->withAppendedByte (byte); + + // If we get to this branch, we're trying to process a non-status byte + // without having seen any previous status byte, so ignore the current byte + return RunningStatus{}; + }); + + if (const auto completeMessage = nextRunningStatus.getCompleteMessage(); ! completeMessage.empty()) + { + callback (SysexExtractorCallbackKind::notSysex, + completeMessage); + return RunningStatus{}.withAppendedByte (nextRunningStatus.data[0]); + } + + return nextRunningStatus; + } + + // Can only happen if the variant is valueless by exception, which indicates a much + // more severe problem! + std::terminate(); + }); + } + + if (auto* inSysex = std::get_if (&state)) + { + callback (SysexExtractorCallbackKind::ongoingSysex, + Span { bytes.data() + bytes.size() - inSysex->numBytes, inSysex->numBytes }); + state = InSysex{}; + } + } + +private: + static bool isRealtimeMessage (std::byte byte) { return std::byte (0xf8) <= byte && byte <= std::byte (0xfe); } + static bool isStatusByte (std::byte byte) { return std::byte (0x80) <= byte; } + static bool isInitialByte (std::byte byte) { return isStatusByte (byte) && byte != std::byte (0xf7); } + + struct InSysex + { + size_t numBytes{}; + }; + + struct RunningStatus + { + // These constructors are required to work around a bug in GCC 7 + RunningStatus() {} + + RunningStatus (uint8_t sizeIn, std::array dataIn) + : size (sizeIn), data (dataIn) {} + + uint8_t size{}; + std::array data{}; + + Span getCompleteMessage() const + { + if (size == 0) + return {}; + + const auto expectedSize = MidiMessage::getMessageLengthFromFirstByte ((uint8_t) data[0]); + return Span { data.data(), size == expectedSize ? size : (size_t) 0 }; + } + + void appendByte (std::byte x) + { + jassert (size < data.size()); + data[size++] = x; + } + + RunningStatus withAppendedByte (std::byte x) const + { + auto result = *this; + result.appendByte (x); + return result; + } + }; + + using State = std::variant; + State state; +}; + //============================================================================== /** Helper class that takes chunks of incoming midi bytes, packages them into @@ -46,153 +203,93 @@ class MidiDataConcatenator { public: MidiDataConcatenator (int initialBufferSize) - : pendingSysexData ((size_t) initialBufferSize) { + pendingSysexData.reserve ((size_t) initialBufferSize); } + MidiDataConcatenator (MidiDataConcatenator&&) noexcept = default; + MidiDataConcatenator& operator= (MidiDataConcatenator&&) noexcept = default; + void reset() { - currentMessageLen = 0; - pendingSysexSize = 0; + extractor.reset(); + pendingSysexData.clear(); pendingSysexTime = 0; } template - void pushMidiData (const void* inputData, int numBytes, double time, - UserDataType* input, CallbackType& callback) + void pushMidiData (Span bytes, + double time, + UserDataType* input, + CallbackType& callback) { - auto d = static_cast (inputData); - - while (numBytes > 0) + extractor.push (bytes, [&] (SysexExtractorCallbackKind kind, Span bytesThisTime) { - auto nextByte = *d; - - if (pendingSysexSize != 0 || nextByte == 0xf0) + switch (kind) { - processSysex (d, numBytes, time, input, callback); - currentMessageLen = 0; - continue; - } + case SysexExtractorCallbackKind::notSysex: + callback.handleIncomingMidiMessage (input, + MidiMessage (bytesThisTime.data(), + (int) bytesThisTime.size(), + time)); + return; - ++d; - --numBytes; + case SysexExtractorCallbackKind::ongoingSysex: + { + if (pendingSysexData.empty()) + pendingSysexTime = time; - if (isRealtimeMessage (nextByte)) - { - callback.handleIncomingMidiMessage (input, MidiMessage (nextByte, time)); - // These can be embedded in the middle of a normal message, so we won't - // reset the currentMessageLen here. - continue; - } + pendingSysexData.insert (pendingSysexData.end(), bytesThisTime.begin(), bytesThisTime.end()); + return; + } - if (isInitialByte (nextByte)) - { - currentMessage[0] = nextByte; - currentMessageLen = 1; - } - else if (currentMessageLen > 0 && currentMessageLen < 3) - { - currentMessage[currentMessageLen++] = nextByte; - } - else - { - // message is too long or invalid MIDI - abandon it and start again with the next byte - currentMessageLen = 0; - continue; - } + case SysexExtractorCallbackKind::lastSysex: + { + pendingSysexData.insert (pendingSysexData.end(), bytesThisTime.begin(), bytesThisTime.end()); - auto expectedLength = MidiMessage::getMessageLengthFromFirstByte (currentMessage[0]); + if (pendingSysexData.empty()) + { + jassertfalse; + return; + } - if (expectedLength == currentMessageLen) - { - callback.handleIncomingMidiMessage (input, MidiMessage (currentMessage, expectedLength, time)); - currentMessageLen = 1; // reset, but leave the first byte to use as the running status byte + if (pendingSysexData.back() == std::byte { 0xf7 }) + { + callback.handleIncomingMidiMessage (input, + MidiMessage (pendingSysexData.data(), + (int) pendingSysexData.size(), + pendingSysexTime)); + } + else + { + callback.handlePartialSysexMessage (input, + unalignedPointerCast (pendingSysexData.data()), + (int) pendingSysexData.size(), + pendingSysexTime); + } + + pendingSysexData.clear(); + + return; + } } - } + }); + } + + template + void pushMidiData (const void* inputData, + int numBytes, + double time, + UserDataType* input, + CallbackType& callback) + { + pushMidiData ({ static_cast (inputData), (size_t) numBytes }, time, input, callback); } private: - template - void processSysex (const uint8*& d, int& numBytes, double time, - UserDataType* input, CallbackType& callback) - { - if (*d == 0xf0) - { - pendingSysexSize = 0; - pendingSysexTime = time; - } - - pendingSysexData.ensureSize ((size_t) (pendingSysexSize + numBytes), false); - auto totalMessage = static_cast (pendingSysexData.getData()); - auto dest = totalMessage + pendingSysexSize; - - do - { - if (pendingSysexSize > 0 && isStatusByte (*d)) - { - if (*d == 0xf7) - { - *dest++ = *d++; - ++pendingSysexSize; - --numBytes; - break; - } - - if (*d >= 0xfa || *d == 0xf8) - { - callback.handleIncomingMidiMessage (input, MidiMessage (*d, time)); - ++d; - --numBytes; - } - else - { - pendingSysexSize = 0; - int used = 0; - const MidiMessage m (d, numBytes, used, 0, time); - - if (used > 0) - { - callback.handleIncomingMidiMessage (input, m); - numBytes -= used; - d += used; - } - - break; - } - } - else - { - *dest++ = *d++; - ++pendingSysexSize; - --numBytes; - } - } - while (numBytes > 0); - - if (pendingSysexSize > 0) - { - if (totalMessage [pendingSysexSize - 1] == 0xf7) - { - callback.handleIncomingMidiMessage (input, MidiMessage (totalMessage, pendingSysexSize, pendingSysexTime)); - pendingSysexSize = 0; - } - else - { - callback.handlePartialSysexMessage (input, totalMessage, pendingSysexSize, pendingSysexTime); - } - } - } - - static bool isRealtimeMessage (uint8 byte) { return byte >= 0xf8 && byte <= 0xfe; } - static bool isStatusByte (uint8 byte) { return byte >= 0x80; } - static bool isInitialByte (uint8 byte) { return isStatusByte (byte) && byte != 0xf7; } - - uint8 currentMessage[3]; - int currentMessageLen = 0; - - MemoryBlock pendingSysexData; + BytestreamSysexExtractor extractor; + std::vector pendingSysexData; double pendingSysexTime = 0; - int pendingSysexSize = 0; JUCE_DECLARE_NON_COPYABLE (MidiDataConcatenator) }; diff --git a/modules/juce_audio_basics/midi/juce_MidiDataConcatenator_test.cpp b/modules/juce_audio_basics/midi/juce_MidiDataConcatenator_test.cpp new file mode 100644 index 0000000000..9bcac827db --- /dev/null +++ b/modules/juce_audio_basics/midi/juce_MidiDataConcatenator_test.cpp @@ -0,0 +1,227 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +class BytestreamSysexExtractorTest : public UnitTest +{ +public: + BytestreamSysexExtractorTest() + : UnitTest ("BytestreamSysexExtractor", UnitTestCategories::midi) + { + } + + void runTest() override + { + beginTest ("Passing empty buffer while no message is in progress does nothing"); + { + BytestreamSysexExtractor extractor; + bool called = false; + extractor.push ({}, [&] (auto&&...) { called = true; }); + + expect (! called); + } + + beginTest ("Passing sysex with no payload reports an empty message"); + { + BytestreamSysexExtractor extractor; + bool called = false; + const std::byte message[] { std::byte (0xf0), std::byte (0xf7) }; + extractor.push (message, [&] (auto status, auto bytes) + { + called = true; + expect (status == SysexExtractorCallbackKind::lastSysex); + expect (bytes.size() == 2 && bytes[0] == std::byte (0xf0) && bytes[1] == std::byte (0xf7)); + }); + + expect (called); + } + + beginTest ("Sending only the sysex starting byte reports an ongoing message"); + { + BytestreamSysexExtractor extractor; + int numCalls = 0; + const std::byte message[] { std::byte (0xf0) }; + extractor.push (message, [&] (auto status, auto bytes) + { + ++numCalls; + expect (status == SysexExtractorCallbackKind::ongoingSysex); + expect (bytes.size() == 1 && bytes[0] == std::byte (0xf0)); + }); + + expect (numCalls == 1); + + // Sending a subsequent empty span should report an ongoing message + extractor.push (Span{}, [&] (auto status, auto bytes) + { + ++numCalls; + expect (status == SysexExtractorCallbackKind::ongoingSysex); + expect (bytes.empty()); + }); + + expect (numCalls == 2); + } + + beginTest ("Sending sysex interspersed with realtime messages filters out the realtime messages"); + { + BytestreamSysexExtractor extractor; + const std::byte message[] { std::byte (0xf0), + std::byte (0x50), // first data byte + std::byte (0xfe), // active sensing + std::byte (0x60), // second data byte + std::byte (0x70), // third data byte + std::byte (0xf7) }; + std::vector> vectors; + extractor.push (message, [&] (auto, auto bytes) { vectors.emplace_back (bytes.begin(), bytes.end()); }); + + expect (vectors == std::vector { std::vector { std::byte (0xf0), std::byte (0x50) }, + std::vector { std::byte (0xfe) }, + std::vector { std::byte (0x60), std::byte (0x70), std::byte (0xf7) } }); + } + + beginTest ("Sending a second f0 byte during an ongoing sysex terminates the previous sysex"); + { + BytestreamSysexExtractor extractor; + const std::byte message[] { std::byte (0xf0), // start of first sysex + std::byte (0x00), + std::byte (0x01), + std::byte (0xf0), // start of second sysex + std::byte (0x02), + std::byte (0x03) }; + + std::vector> vectors; + extractor.push (message, [&] (auto, auto bytes) { vectors.emplace_back (bytes.begin(), bytes.end()); }); + + expect (vectors == std::vector { std::vector { std::byte (0xf0), std::byte (0x00), std::byte (0x01) }, + std::vector { std::byte (0xf0), std::byte (0x02), std::byte (0x03) } }); + } + + beginTest ("Status bytes truncate ongoing sysex"); + { + BytestreamSysexExtractor extractor; + const std::byte message[] { std::byte (0xf0), // start of first sysex + std::byte (0x10), + std::byte (0x20), + std::byte (0x30), + std::byte (0x80), // status byte + std::byte (0x00), + std::byte (0x00) }; + + std::vector> vectors; + extractor.push (message, [&] (auto, auto bytes) { vectors.emplace_back (bytes.begin(), bytes.end()); }); + + expect (vectors == std::vector { std::vector { std::byte (0xf0), std::byte (0x10), std::byte (0x20), std::byte (0x30) }, + std::vector { std::byte (0x80), std::byte (0x00), std::byte (0x00) } }); + } + + beginTest ("Running status is preserved between calls"); + { + BytestreamSysexExtractor extractor; + const std::byte message[] { std::byte (0x90), // note on + std::byte (0x10), + std::byte (0x20), + std::byte (0x30), + std::byte (0x40), + std::byte (0x50) }; + + std::vector> vectors; + const auto callback = [&] (auto status, auto bytes) + { + expect (status == SysexExtractorCallbackKind::notSysex); + vectors.emplace_back (bytes.begin(), bytes.end()); + }; + extractor.push (message, callback); + extractor.push (std::array { std::byte (0x60) }, callback); + + expect (vectors == std::vector { std::vector { std::byte (0x90), std::byte (0x10), std::byte (0x20) }, + std::vector { std::byte (0x90), std::byte (0x30), std::byte (0x40) }, + std::vector { std::byte (0x90), std::byte (0x50), std::byte (0x60) } }); + } + + beginTest ("Realtime messages can intersperse bytes of non-sysex messages"); + { + BytestreamSysexExtractor extractor; + const std::byte message[] { std::byte (0xd0), // channel pressure + std::byte (0xfe), // active sensing + std::byte (0x70), // pressure cont. + std::byte (0xfe), // active sensing + std::byte (0x60), // second pressure message + std::byte (0xfe), // active sensing + std::byte (0x50) }; // third pressure message + + std::vector> vectors; + extractor.push (message, [&] (auto, auto bytes) + { + vectors.emplace_back (bytes.begin(), bytes.end()); + }); + + expect (vectors == std::vector { std::vector { std::byte (0xfe) }, + std::vector { std::byte (0xd0), std::byte (0x70) }, + std::vector { std::byte (0xfe) }, + std::vector { std::byte (0xd0), std::byte (0x60) }, + std::vector { std::byte (0xfe) }, + std::vector { std::byte (0xd0), std::byte (0x50) } }); + } + + beginTest ("Non-status bytes with no associated running status are ignored"); + { + BytestreamSysexExtractor extractor; + const std::byte message[] { std::byte (0x10), + std::byte (0x2e), + std::byte (0x30), + std::byte (0x4e), + std::byte (0x80), // note off + std::byte (0x0e), + std::byte (0x00), + std::byte (0xf0), // sysex + std::byte (0xf7), // end sysex + std::byte (0x00), // sysex resets running status + std::byte (0x10), }; + + std::vector> vectors; + extractor.push (message, [&] (auto, auto bytes) + { + vectors.emplace_back (bytes.begin(), bytes.end()); + }); + + expect (vectors == std::vector { std::vector { std::byte (0x80), std::byte (0x0e), std::byte (0x00) }, + std::vector { std::byte (0xf0), std::byte (0xf7) } }); + } + } +}; + +static BytestreamSysexExtractorTest bytestreamSysexExtractorTest; + +} // namespace juce diff --git a/modules/juce_audio_basics/midi/ump/juce_UMPConverters.h b/modules/juce_audio_basics/midi/ump/juce_UMPConverters.h index 38d5bee49c..6ea9ff8fe3 100644 --- a/modules/juce_audio_basics/midi/ump/juce_UMPConverters.h +++ b/modules/juce_audio_basics/midi/ump/juce_UMPConverters.h @@ -189,7 +189,7 @@ namespace juce::universal_midi_packets void reset() { translator.reset(); } - Midi1ToBytestreamTranslator translator; + SingleGroupMidi1ToBytestreamTranslator translator; }; } // namespace juce::universal_midi_packets /** @endcond */ diff --git a/modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToBytestreamTranslator.h b/modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToBytestreamTranslator.h index c55ef4cca2..57afcd05a0 100644 --- a/modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToBytestreamTranslator.h +++ b/modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToBytestreamTranslator.h @@ -37,44 +37,39 @@ namespace juce::universal_midi_packets { /** - Parses a raw stream of uint32_t holding a series of Universal MIDI Packets using - the MIDI 1.0 Protocol, converting to plain (non-UMP) MidiMessages. - - @tags{Audio} + Extracts from a series of Universal MIDI Packets the bytes that are also meaningful in the + bytestream MIDI 1.0 format. */ -class Midi1ToBytestreamTranslator +class SingleGroupMidi1ToBytestreamExtractor { public: - /** Ensures that there is room in the internal buffer for a sysex message of at least - `initialBufferSize` bytes. - */ - explicit Midi1ToBytestreamTranslator (int initialBufferSize) - { - pendingSysExData.reserve (size_t (initialBufferSize)); - } - - /** Clears the concatenator. */ void reset() { - pendingSysExData.clear(); - pendingSysExTime = 0.0; + sysexInProgress = false; } /** Converts a Universal MIDI Packet using the MIDI 1.0 Protocol to - an equivalent MidiMessage. Accumulates SysEx packets into a single - MidiMessage, as appropriate. + an equivalent MidiMessage. If the packet doesn't convert to a single bytestream message + (as may be the case for long sysex7 data), then the the callback will be passed just the + sysex bytes in the current packet. To reconstruct the entire sysex message, the caller + can bytes that are marked as ongoingSysex, and process the full message once the callback + receives bytes that are marked as lastSysex. @param packet a packet which is using the MIDI 1.0 Protocol. @param time the timestamp to be applied to these messages. - @param callback a callback which will be called with each converted MidiMessage. + @param callback a callback that will be called with each converted MidiMessage. */ template void dispatch (const View& packet, double time, MessageCallback&& callback) { const auto firstWord = *packet.data(); - if (! pendingSysExData.empty() && shouldPacketTerminateSysExEarly (firstWord)) - pendingSysExData.clear(); + if (sysexInProgress && shouldPacketTerminateSysExEarly (firstWord)) + { + // unexpected end of last sysex + callback (SysexExtractorCallbackKind::lastSysex, Span()); + sysexInProgress = false; + } switch (packet.size()) { @@ -83,8 +78,10 @@ public: // Utility messages don't translate to bytestream format if (Utils::getMessageType (firstWord) != Utils::MessageKind::utility) { - const auto message = fromUmp (PacketX1 { firstWord }, time); - callback (BytestreamMidiView (&message)); + const auto converted = fromUmp (PacketX1 { firstWord }, time); + callback (SysexExtractorCallbackKind::notSysex, + Span (unalignedPointerCast (converted.getRawData()), + (size_t) converted.getRawDataSize())); } break; @@ -93,7 +90,7 @@ public: case 2: { if (Utils::getMessageType (firstWord) == Utils::MessageKind::sysex7) - processSysEx (PacketX2 { packet[0], packet[1] }, time, callback); + processSysEx (PacketX2 { packet[0], packet[1] }, callback); break; } @@ -120,69 +117,72 @@ public: const auto word = m.front(); jassert (Utils::getNumWordsForMessageType (word) == 1); - const std::array bytes { { uint8_t ((word >> 0x10) & 0xff), - uint8_t ((word >> 0x08) & 0xff), - uint8_t ((word >> 0x00) & 0xff) } }; - const auto numBytes = MidiMessage::getMessageLengthFromFirstByte (bytes.front()); + const std::array bytes { { std::byte ((word >> 0x10) & 0xff), + std::byte ((word >> 0x08) & 0xff), + std::byte ((word >> 0x00) & 0xff) } }; + const auto numBytes = MidiMessage::getMessageLengthFromFirstByte ((uint8_t) bytes.front()); return MidiMessage (bytes.data(), numBytes, time); } private: template - void processSysEx (const PacketX2& packet, - double time, - MessageCallback&& callback) + void processSysEx (const PacketX2& packet, MessageCallback&& callback) { - switch (getSysEx7Kind (packet[0])) + const std::array initial { std::byte { 0xf0 } }, final { std::byte { 0xf7 } }; + std::array storage{}; + size_t validBytes = 0; + + const auto pushBytes = [&] (const Span b) + { + std::copy (b.begin(), b.end(), storage.data() + validBytes); + validBytes += b.size(); + }; + + const auto pushPacket = [&] (const PacketX2& p) + { + const auto newBytes = SysEx7::getDataBytes (p); + pushBytes (Span (newBytes.data.data(), newBytes.size)); + }; + + const auto kind = getSysEx7Kind (packet[0]); + + if ( ( sysexInProgress && (kind == SysEx7::Kind::begin || kind == SysEx7::Kind::complete)) + || (! sysexInProgress && (kind == SysEx7::Kind::continuation || kind == SysEx7::Kind::end))) + { + // Malformed SysEx, drop progress and return + callback (SysexExtractorCallbackKind::lastSysex, Span()); + sysexInProgress = false; + return; + } + + switch (kind) { case SysEx7::Kind::complete: - startSysExMessage (time); - pushBytes (packet); - terminateSysExMessage (callback); + pushBytes (Span (initial)); + pushPacket (packet); + pushBytes (Span (final)); break; case SysEx7::Kind::begin: - startSysExMessage (time); - pushBytes (packet); + pushBytes (Span (initial)); + pushPacket (packet); break; case SysEx7::Kind::continuation: - if (pendingSysExData.empty()) - break; - - pushBytes (packet); + pushPacket (packet); break; case SysEx7::Kind::end: - if (pendingSysExData.empty()) - break; - - pushBytes (packet); - terminateSysExMessage (callback); + pushPacket (packet); + pushBytes (Span (final)); break; } - } - void pushBytes (const PacketX2& packet) - { - const auto bytes = SysEx7::getDataBytes (packet); - pendingSysExData.insert (pendingSysExData.end(), - bytes.data.begin(), - bytes.data.begin() + bytes.size); - } - - void startSysExMessage (double time) - { - pendingSysExTime = time; - pendingSysExData.push_back (std::byte { 0xf0 }); - } - - template - void terminateSysExMessage (MessageCallback&& callback) - { - pendingSysExData.push_back (std::byte { 0xf7 }); - callback (BytestreamMidiView (pendingSysExData, pendingSysExTime)); - pendingSysExData.clear(); + sysexInProgress = sysexInProgress ? (kind == SysEx7::Kind::continuation) + : (kind == SysEx7::Kind::begin); + const auto callbackKind = sysexInProgress ? SysexExtractorCallbackKind::ongoingSysex + : SysexExtractorCallbackKind::lastSysex; + callback (callbackKind, Span (storage.data(), validBytes)); } static bool shouldPacketTerminateSysExEarly (uint32_t firstWord) @@ -216,8 +216,85 @@ private: return Utils::getMessageType (word) == Utils::MessageKind::commonRealtime && ((word >> 0x10) & 0xff) >= 0xf8; } - std::vector pendingSysExData; + bool sysexInProgress = false; +}; +/** + Parses a raw stream of uint32_t holding a series of Universal MIDI Packets using + the MIDI 1.0 Protocol, converting to plain (non-UMP) MidiMessages. + + @tags{Audio} +*/ +class SingleGroupMidi1ToBytestreamTranslator +{ +public: + /** Ensures that there is room in the internal buffer for a sysex message of at least + initialBufferSize bytes. + */ + explicit SingleGroupMidi1ToBytestreamTranslator (int initialBufferSize) + { + pendingSysExData.reserve (size_t (initialBufferSize)); + } + + /** Clears the concatenator. */ + void reset() + { + extractor.reset(); + pendingSysExData.clear(); + pendingSysExTime = 0.0; + } + + /** Converts a Universal MIDI Packet using the MIDI 1.0 Protocol to + an equivalent MidiMessage. Accumulates SysEx packets into a single + MidiMessage, as appropriate. + + @param packet a packet which is using the MIDI 1.0 Protocol. + @param time the timestamp to be applied to these messages. + @param callback a callback which will be called with each converted MidiMessage. + */ + template + void dispatch (const View& packet, double time, MessageCallback&& callback) + { + extractor.dispatch (packet, time, [&] (SysexExtractorCallbackKind kind, Span bytes) + { + switch (kind) + { + case SysexExtractorCallbackKind::notSysex: + callback (BytestreamMidiView (bytes, time)); + return; + + case SysexExtractorCallbackKind::ongoingSysex: + { + if (pendingSysExData.empty()) + pendingSysExTime = time; + + pendingSysExData.insert (pendingSysExData.end(), bytes.begin(), bytes.end()); + return; + } + + case SysexExtractorCallbackKind::lastSysex: + { + pendingSysExData.insert (pendingSysExData.end(), bytes.begin(), bytes.end()); + + if (pendingSysExData.empty()) + return; + + // If this is not true, then the sysex message was truncated somehow and we + // probably shouldn't allow it to propagate + if (pendingSysExData.back() == std::byte { 0xf7 }) + callback (BytestreamMidiView (Span (pendingSysExData), pendingSysExTime)); + + pendingSysExData.clear(); + + return; + } + } + }); + } + +private: + SingleGroupMidi1ToBytestreamExtractor extractor; + std::vector pendingSysExData; double pendingSysExTime = 0.0; }; diff --git a/modules/juce_audio_basics/midi/ump/juce_UMP_test.cpp b/modules/juce_audio_basics/midi/ump/juce_UMP_test.cpp index be6f1a0195..9edfd99acf 100644 --- a/modules/juce_audio_basics/midi/ump/juce_UMP_test.cpp +++ b/modules/juce_audio_basics/midi/ump/juce_UMP_test.cpp @@ -54,7 +54,7 @@ public: beginTest ("Short bytestream midi messages can be round-tripped through the UMP converter"); { - Midi1ToBytestreamTranslator translator (0); + SingleGroupMidi1ToBytestreamTranslator translator (0); forEachNonSysExTestMessage (random, [&] (const MidiMessage& m) { @@ -208,7 +208,7 @@ public: { const auto newPacket = createRandomRealtimeUMP (random); modifiedPackets.add (View (newPacket.data())); - realtimeMessages.addEvent (Midi1ToBytestreamTranslator::fromUmp (newPacket), 0); + realtimeMessages.addEvent (SingleGroupMidi1ToBytestreamExtractor::fromUmp (newPacket), 0); }; for (const auto& packet : originalPackets) @@ -264,7 +264,7 @@ public: { const auto newPacket = createRandomRealtimeUMP (random); modifiedPackets.add (View (newPacket.data())); - realtimeMessages.addEvent (Midi1ToBytestreamTranslator::fromUmp (newPacket), 0); + realtimeMessages.addEvent (SingleGroupMidi1ToBytestreamExtractor::fromUmp (newPacket), 0); }; const auto addRandomUtilityUMP = [&]