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 = [&]