1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-09 23:34:20 +00:00

UMPMidi1ToBytestreamTranslator: Refactor to separate responsibilities between translator and extractor

This commit is contained in:
reuk 2025-03-05 21:19:44 +00:00
parent 835216c581
commit dd3d555bb9
No known key found for this signature in database
28 changed files with 661 additions and 197 deletions

View file

@ -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"

View file

@ -225,6 +225,9 @@
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiBuffer.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiDataConcatenator_test.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiFile.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>

View file

@ -910,6 +910,9 @@
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiBuffer.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiDataConcatenator_test.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiFile.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>

View file

@ -225,6 +225,9 @@
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiBuffer.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiDataConcatenator_test.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiFile.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>

View file

@ -910,6 +910,9 @@
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiBuffer.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiDataConcatenator_test.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiFile.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>

View file

@ -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"

View file

@ -183,6 +183,9 @@
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiBuffer.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiDataConcatenator_test.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiFile.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>

View file

@ -691,6 +691,9 @@
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiBuffer.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiDataConcatenator_test.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiFile.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>

View file

@ -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"

View file

@ -191,6 +191,9 @@
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiBuffer.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiDataConcatenator_test.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiFile.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>

View file

@ -766,6 +766,9 @@
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiBuffer.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiDataConcatenator_test.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiFile.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>

View file

@ -191,6 +191,9 @@
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiBuffer.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiDataConcatenator_test.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiFile.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>

View file

@ -766,6 +766,9 @@
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiBuffer.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiDataConcatenator_test.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiFile.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>

View file

@ -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"

View file

@ -183,6 +183,9 @@
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiBuffer.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiDataConcatenator_test.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiFile.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>

View file

@ -721,6 +721,9 @@
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiBuffer.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiDataConcatenator_test.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiFile.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>

View file

@ -199,6 +199,9 @@
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiBuffer.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiDataConcatenator_test.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiFile.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>

View file

@ -814,6 +814,9 @@
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiBuffer.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiDataConcatenator_test.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiFile.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>

View file

@ -199,6 +199,9 @@
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiBuffer.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiDataConcatenator_test.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiFile.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>

View file

@ -814,6 +814,9 @@
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiBuffer.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiDataConcatenator_test.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiFile.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>

View file

@ -182,6 +182,9 @@
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiBuffer.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiDataConcatenator_test.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiFile.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>

View file

@ -718,6 +718,9 @@
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiBuffer.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiDataConcatenator_test.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>
<ClCompile Include="..\..\..\..\modules\juce_audio_basics\midi\juce_MidiFile.cpp">
<Filter>JUCE Modules\juce_audio_basics\midi</Filter>
</ClCompile>

View file

@ -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

View file

@ -35,6 +35,163 @@
namespace juce
{
enum class SysexExtractorCallbackKind
{
notSysex,
ongoingSysex,
lastSysex,
};
class BytestreamSysexExtractor
{
public:
void reset()
{
state = RunningStatus{};
}
template <typename Callback>
void push (Span<const std::byte> 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<InSysex> (&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<RunningStatus> (&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<InSysex> (&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<std::byte, 3> dataIn)
: size (sizeIn), data (dataIn) {}
uint8_t size{};
std::array<std::byte, 3> data{};
Span<const std::byte> 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<RunningStatus, InSysex>;
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 <typename UserDataType, typename CallbackType>
void pushMidiData (const void* inputData, int numBytes, double time,
UserDataType* input, CallbackType& callback)
void pushMidiData (Span<const std::byte> bytes,
double time,
UserDataType* input,
CallbackType& callback)
{
auto d = static_cast<const uint8*> (inputData);
extractor.push (bytes, [&] (SysexExtractorCallbackKind kind, Span<const std::byte> bytesThisTime)
{
switch (kind)
{
case SysexExtractorCallbackKind::notSysex:
callback.handleIncomingMidiMessage (input,
MidiMessage (bytesThisTime.data(),
(int) bytesThisTime.size(),
time));
return;
while (numBytes > 0)
case SysexExtractorCallbackKind::ongoingSysex:
{
auto nextByte = *d;
if (pendingSysexData.empty())
pendingSysexTime = time;
if (pendingSysexSize != 0 || nextByte == 0xf0)
{
processSysex (d, numBytes, time, input, callback);
currentMessageLen = 0;
continue;
pendingSysexData.insert (pendingSysexData.end(), bytesThisTime.begin(), bytesThisTime.end());
return;
}
++d;
--numBytes;
if (isRealtimeMessage (nextByte))
case SysexExtractorCallbackKind::lastSysex:
{
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());
if (pendingSysexData.empty())
{
jassertfalse;
return;
}
if (isInitialByte (nextByte))
if (pendingSysexData.back() == std::byte { 0xf7 })
{
currentMessage[0] = nextByte;
currentMessageLen = 1;
}
else if (currentMessageLen > 0 && currentMessageLen < 3)
{
currentMessage[currentMessageLen++] = nextByte;
callback.handleIncomingMidiMessage (input,
MidiMessage (pendingSysexData.data(),
(int) pendingSysexData.size(),
pendingSysexTime));
}
else
{
// message is too long or invalid MIDI - abandon it and start again with the next byte
currentMessageLen = 0;
continue;
callback.handlePartialSysexMessage (input,
unalignedPointerCast<const uint8*> (pendingSysexData.data()),
(int) pendingSysexData.size(),
pendingSysexTime);
}
auto expectedLength = MidiMessage::getMessageLengthFromFirstByte (currentMessage[0]);
pendingSysexData.clear();
if (expectedLength == currentMessageLen)
return;
}
}
});
}
template <typename UserDataType, typename CallbackType>
void pushMidiData (const void* inputData,
int numBytes,
double time,
UserDataType* input,
CallbackType& callback)
{
callback.handleIncomingMidiMessage (input, MidiMessage (currentMessage, expectedLength, time));
currentMessageLen = 1; // reset, but leave the first byte to use as the running status byte
}
}
pushMidiData ({ static_cast<const std::byte*> (inputData), (size_t) numBytes }, time, input, callback);
}
private:
template <typename UserDataType, typename CallbackType>
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<uint8*> (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<std::byte> pendingSysexData;
double pendingSysexTime = 0;
int pendingSysexSize = 0;
JUCE_DECLARE_NON_COPYABLE (MidiDataConcatenator)
};

View file

@ -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<const std::byte>{}, [&] (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<std::vector<std::byte>> 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<std::vector<std::byte>> 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<std::vector<std::byte>> 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<std::vector<std::byte>> 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<std::vector<std::byte>> 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<std::vector<std::byte>> 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

View file

@ -189,7 +189,7 @@ namespace juce::universal_midi_packets
void reset() { translator.reset(); }
Midi1ToBytestreamTranslator translator;
SingleGroupMidi1ToBytestreamTranslator translator;
};
} // namespace juce::universal_midi_packets
/** @endcond */

View file

@ -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 <typename MessageCallback>
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<const std::byte>());
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<const std::byte*> (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<uint8_t, 3> 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<std::byte, 3> 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 <typename MessageCallback>
void processSysEx (const PacketX2& packet,
double time,
MessageCallback&& callback)
void processSysEx (const PacketX2& packet, MessageCallback&& callback)
{
switch (getSysEx7Kind (packet[0]))
const std::array<std::byte, 1> initial { std::byte { 0xf0 } }, final { std::byte { 0xf7 } };
std::array<std::byte, 8> storage{};
size_t validBytes = 0;
const auto pushBytes = [&] (const Span<const std::byte> 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<const std::byte> (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<const std::byte>());
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 <typename MessageCallback>
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<std::byte> 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 <typename MessageCallback>
void dispatch (const View& packet, double time, MessageCallback&& callback)
{
extractor.dispatch (packet, time, [&] (SysexExtractorCallbackKind kind, Span<const std::byte> 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<const std::byte> (pendingSysExData), pendingSysExTime));
pendingSysExData.clear();
return;
}
}
});
}
private:
SingleGroupMidi1ToBytestreamExtractor extractor;
std::vector<std::byte> pendingSysExData;
double pendingSysExTime = 0.0;
};

View file

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