First Commit

This commit is contained in:
Legaeli
2025-10-22 09:20:23 +02:00
commit a36f2ca925
4393 changed files with 1570219 additions and 0 deletions
@@ -0,0 +1,330 @@
/*
==============================================================================
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
{
namespace MidiBufferHelpers
{
inline int getEventTime (const void* d) noexcept
{
return readUnaligned<int32> (d);
}
inline uint16 getEventDataSize (const void* d) noexcept
{
return readUnaligned<uint16> (static_cast<const char*> (d) + sizeof (int32));
}
inline uint16 getEventTotalSize (const void* d) noexcept
{
return (uint16) (getEventDataSize (d) + sizeof (int32) + sizeof (uint16));
}
static int findActualEventLength (const uint8* data, int maxBytes) noexcept
{
auto byte = (unsigned int) *data;
if (byte == 0xf0 || byte == 0xf7)
{
int i = 1;
while (i < maxBytes)
if (data[i++] == 0xf7)
break;
return i;
}
if (byte == 0xff)
{
if (maxBytes == 1)
return 1;
const auto var = MidiMessage::readVariableLengthValue (data + 1, maxBytes - 1);
return jmin (maxBytes, var.value + 2 + var.bytesUsed);
}
if (byte >= 0x80)
return jmin (maxBytes, MidiMessage::getMessageLengthFromFirstByte ((uint8) byte));
return 0;
}
static uint8* findEventAfter (uint8* d, uint8* endData, int samplePosition) noexcept
{
while (d < endData && getEventTime (d) <= samplePosition)
d += getEventTotalSize (d);
return d;
}
}
//==============================================================================
MidiBufferIterator& MidiBufferIterator::operator++() noexcept
{
data += sizeof (int32) + sizeof (uint16) + size_t (MidiBufferHelpers::getEventDataSize (data));
return *this;
}
MidiBufferIterator MidiBufferIterator::operator++ (int) noexcept
{
auto copy = *this;
++(*this);
return copy;
}
MidiBufferIterator::reference MidiBufferIterator::operator*() const noexcept
{
return { data + sizeof (int32) + sizeof (uint16),
MidiBufferHelpers::getEventDataSize (data),
MidiBufferHelpers::getEventTime (data) };
}
//==============================================================================
MidiBuffer::MidiBuffer (const MidiMessage& message) noexcept
{
addEvent (message, 0);
}
void MidiBuffer::swapWith (MidiBuffer& other) noexcept { data.swapWith (other.data); }
void MidiBuffer::clear() noexcept { data.clearQuick(); }
void MidiBuffer::ensureSize (size_t minimumNumBytes) { data.ensureStorageAllocated ((int) minimumNumBytes); }
bool MidiBuffer::isEmpty() const noexcept { return data.size() == 0; }
void MidiBuffer::clear (int startSample, int numSamples)
{
auto start = MidiBufferHelpers::findEventAfter (data.begin(), data.end(), startSample - 1);
auto end = MidiBufferHelpers::findEventAfter (start, data.end(), startSample + numSamples - 1);
data.removeRange ((int) (start - data.begin()), (int) (end - start));
}
bool MidiBuffer::addEvent (const MidiMessage& m, int sampleNumber)
{
return addEvent (m.getRawData(), m.getRawDataSize(), sampleNumber);
}
bool MidiBuffer::addEvent (const void* newData, int maxBytes, int sampleNumber)
{
auto numBytes = MidiBufferHelpers::findActualEventLength (static_cast<const uint8*> (newData), maxBytes);
if (numBytes <= 0)
return true;
if (std::numeric_limits<uint16>::max() < numBytes)
{
// This method only supports messages smaller than (1 << 16) bytes
return false;
}
auto newItemSize = (size_t) numBytes + sizeof (int32) + sizeof (uint16);
auto offset = (int) (MidiBufferHelpers::findEventAfter (data.begin(), data.end(), sampleNumber) - data.begin());
data.insertMultiple (offset, 0, (int) newItemSize);
auto* d = data.begin() + offset;
writeUnaligned<int32> (d, sampleNumber);
d += sizeof (int32);
writeUnaligned<uint16> (d, static_cast<uint16> (numBytes));
d += sizeof (uint16);
memcpy (d, newData, (size_t) numBytes);
return true;
}
void MidiBuffer::addEvents (const MidiBuffer& otherBuffer,
int startSample, int numSamples, int sampleDeltaToAdd)
{
for (auto i = otherBuffer.findNextSamplePosition (startSample); i != otherBuffer.cend(); ++i)
{
const auto metadata = *i;
if (metadata.samplePosition >= startSample + numSamples && numSamples >= 0)
break;
addEvent (metadata.data, metadata.numBytes, metadata.samplePosition + sampleDeltaToAdd);
}
}
int MidiBuffer::getNumEvents() const noexcept
{
int n = 0;
auto end = data.end();
for (auto d = data.begin(); d < end; ++n)
d += MidiBufferHelpers::getEventTotalSize (d);
return n;
}
int MidiBuffer::getFirstEventTime() const noexcept
{
return data.size() > 0 ? MidiBufferHelpers::getEventTime (data.begin()) : 0;
}
int MidiBuffer::getLastEventTime() const noexcept
{
if (data.size() == 0)
return 0;
auto endData = data.end();
for (auto d = data.begin();;)
{
auto nextOne = d + MidiBufferHelpers::getEventTotalSize (d);
if (nextOne >= endData)
return MidiBufferHelpers::getEventTime (d);
d = nextOne;
}
}
MidiBufferIterator MidiBuffer::findNextSamplePosition (int samplePosition) const noexcept
{
return std::find_if (cbegin(), cend(), [&] (const MidiMessageMetadata& metadata) noexcept
{
return metadata.samplePosition >= samplePosition;
});
}
//==============================================================================
JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS
MidiBuffer::Iterator::Iterator (const MidiBuffer& b) noexcept
: buffer (b), iterator (b.data.begin())
{
}
void MidiBuffer::Iterator::setNextSamplePosition (int samplePosition) noexcept
{
iterator = buffer.findNextSamplePosition (samplePosition);
}
bool MidiBuffer::Iterator::getNextEvent (const uint8*& midiData, int& numBytes, int& samplePosition) noexcept
{
if (iterator == buffer.cend())
return false;
const auto metadata = *iterator++;
midiData = metadata.data;
numBytes = metadata.numBytes;
samplePosition = metadata.samplePosition;
return true;
}
bool MidiBuffer::Iterator::getNextEvent (MidiMessage& result, int& samplePosition) noexcept
{
if (iterator == buffer.cend())
return false;
const auto metadata = *iterator++;
result = metadata.getMessage();
samplePosition = metadata.samplePosition;
return true;
}
JUCE_END_IGNORE_DEPRECATION_WARNINGS
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
struct MidiBufferTest final : public UnitTest
{
MidiBufferTest()
: UnitTest ("MidiBuffer", UnitTestCategories::midi)
{}
void runTest() override
{
beginTest ("Clear messages");
{
const auto message = MidiMessage::noteOn (1, 64, 0.5f);
const auto testBuffer = [&]
{
MidiBuffer buffer;
buffer.addEvent (message, 0);
buffer.addEvent (message, 10);
buffer.addEvent (message, 20);
buffer.addEvent (message, 30);
return buffer;
}();
{
auto buffer = testBuffer;
buffer.clear (10, 0);
expectEquals (buffer.getNumEvents(), 4);
}
{
auto buffer = testBuffer;
buffer.clear (10, 1);
expectEquals (buffer.getNumEvents(), 3);
}
{
auto buffer = testBuffer;
buffer.clear (10, 10);
expectEquals (buffer.getNumEvents(), 3);
}
{
auto buffer = testBuffer;
buffer.clear (10, 20);
expectEquals (buffer.getNumEvents(), 2);
}
{
auto buffer = testBuffer;
buffer.clear (10, 30);
expectEquals (buffer.getNumEvents(), 1);
}
{
auto buffer = testBuffer;
buffer.clear (10, 300);
expectEquals (buffer.getNumEvents(), 1);
}
}
}
};
static MidiBufferTest midiBufferTest;
#endif
} // namespace juce
@@ -0,0 +1,357 @@
/*
==============================================================================
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
{
//==============================================================================
/**
A view of MIDI message data stored in a contiguous buffer.
Instances of this class do *not* own the midi data bytes that they point to.
Instead, they expect the midi data to live in a separate buffer that outlives
the MidiMessageMetadata instance.
@tags{Audio}
*/
struct MidiMessageMetadata
{
MidiMessageMetadata() noexcept = default;
MidiMessageMetadata (const uint8* dataIn, int numBytesIn, int positionIn) noexcept
: data (dataIn), numBytes (numBytesIn), samplePosition (positionIn)
{
}
/** Constructs a new MidiMessage instance from the data that this object is viewing.
Note that MidiMessage owns its data storage, whereas MidiMessageMetadata does not.
*/
MidiMessage getMessage() const { return MidiMessage (data, numBytes, samplePosition); }
/** Pointer to the first byte of a MIDI message. */
const uint8* data = nullptr;
/** The number of bytes in the MIDI message. */
int numBytes = 0;
/** The MIDI message's timestamp. */
int samplePosition = 0;
};
//==============================================================================
/**
An iterator to move over contiguous raw MIDI data, which Allows iterating
over a MidiBuffer using C++11 range-for syntax.
In the following example, we log all three-byte messages in a midi buffer.
@code
void processBlock (AudioBuffer<float>&, MidiBuffer& midiBuffer) override
{
for (const MidiMessageMetadata metadata : midiBuffer)
if (metadata.numBytes == 3)
Logger::writeToLog (metadata.getMessage().getDescription());
}
@endcode
@tags{Audio}
*/
class JUCE_API MidiBufferIterator
{
using Ptr = const uint8*;
public:
MidiBufferIterator() = default;
/** Constructs an iterator pointing at the message starting at the byte `dataIn`.
`dataIn` must point to the start of a valid MIDI message. If it does not,
calling other member functions on the iterator will result in undefined
behaviour.
*/
explicit MidiBufferIterator (const uint8* dataIn) noexcept
: data (dataIn)
{
}
using difference_type = std::iterator_traits<Ptr>::difference_type;
using value_type = MidiMessageMetadata;
using reference = MidiMessageMetadata;
using pointer = void;
using iterator_category = std::input_iterator_tag;
/** Make this iterator point to the next message in the buffer. */
MidiBufferIterator& operator++() noexcept;
/** Create a copy of this object, make this iterator point to the next message in
the buffer, then return the copy.
*/
MidiBufferIterator operator++ (int) noexcept;
/** Return true if this iterator points to the same message as another
iterator instance, otherwise return false.
*/
bool operator== (const MidiBufferIterator& other) const noexcept { return data == other.data; }
/** Return false if this iterator points to the same message as another
iterator instance, otherwise returns true.
*/
bool operator!= (const MidiBufferIterator& other) const noexcept { return ! operator== (other); }
/** Return an instance of MidiMessageMetadata which describes the message to which
the iterator is currently pointing.
*/
reference operator*() const noexcept;
private:
Ptr data = nullptr;
};
//==============================================================================
/**
Holds a sequence of time-stamped midi events.
Analogous to the AudioBuffer, this holds a set of midi events with
integer time-stamps. The buffer is kept sorted in order of the time-stamps.
If you're working with a sequence of midi events that may need to be manipulated
or read/written to a midi file, then MidiMessageSequence is probably a more
appropriate container. MidiBuffer is designed for lower-level streams of raw
midi data.
@see MidiMessage
@tags{Audio}
*/
class JUCE_API MidiBuffer
{
public:
//==============================================================================
/** Creates an empty MidiBuffer. */
MidiBuffer() noexcept = default;
/** Creates a MidiBuffer containing a single midi message. */
explicit MidiBuffer (const MidiMessage& message) noexcept;
//==============================================================================
/** Removes all events from the buffer. */
void clear() noexcept;
/** Removes all events between two times from the buffer.
All events for which (start <= event position < start + numSamples) will
be removed.
*/
void clear (int start, int numSamples);
/** Returns true if the buffer is empty.
To actually retrieve the events, use a MidiBufferIterator object
*/
bool isEmpty() const noexcept;
/** Counts the number of events in the buffer.
This is actually quite a slow operation, as it has to iterate through all
the events, so you might prefer to call isEmpty() if that's all you need
to know.
*/
int getNumEvents() const noexcept;
/** Adds an event to the buffer.
The sample number will be used to determine the position of the event in
the buffer, which is always kept sorted. The MidiMessage's timestamp is
ignored.
If an event is added whose sample position is the same as one or more events
already in the buffer, the new event will be placed after the existing ones.
To retrieve events, use a MidiBufferIterator object.
Returns true on success, or false on failure.
*/
bool addEvent (const MidiMessage& midiMessage, int sampleNumber);
/** Adds an event to the buffer from raw midi data.
The sample number will be used to determine the position of the event in
the buffer, which is always kept sorted.
If an event is added whose sample position is the same as one or more events
already in the buffer, the new event will be placed after the existing ones.
The event data will be inspected to calculate the number of bytes in length that
the midi event really takes up, so maxBytesOfMidiData may be longer than the data
that actually gets stored. E.g. if you pass in a note-on and a length of 4 bytes,
it'll actually only store 3 bytes. If the midi data is invalid, it might not
add an event at all.
To retrieve events, use a MidiBufferIterator object.
Returns true on success, or false on failure.
*/
bool addEvent (const void* rawMidiData,
int maxBytesOfMidiData,
int sampleNumber);
/** Adds some events from another buffer to this one.
@param otherBuffer the buffer containing the events you want to add
@param startSample the lowest sample number in the source buffer for which
events should be added. Any source events whose timestamp is
less than this will be ignored
@param numSamples the valid range of samples from the source buffer for which
events should be added - i.e. events in the source buffer whose
timestamp is greater than or equal to (startSample + numSamples)
will be ignored. If this value is less than 0, all events after
startSample will be taken.
@param sampleDeltaToAdd a value which will be added to the source timestamps of the events
that are added to this buffer
*/
void addEvents (const MidiBuffer& otherBuffer,
int startSample,
int numSamples,
int sampleDeltaToAdd);
/** Returns the sample number of the first event in the buffer.
If the buffer's empty, this will just return 0.
*/
int getFirstEventTime() const noexcept;
/** Returns the sample number of the last event in the buffer.
If the buffer's empty, this will just return 0.
*/
int getLastEventTime() const noexcept;
//==============================================================================
/** Exchanges the contents of this buffer with another one.
This is a quick operation, because no memory allocating or copying is done, it
just swaps the internal state of the two buffers.
*/
void swapWith (MidiBuffer&) noexcept;
/** Preallocates some memory for the buffer to use.
This helps to avoid needing to reallocate space when the buffer has messages
added to it.
*/
void ensureSize (size_t minimumNumBytes);
/** Get a read-only iterator pointing to the beginning of this buffer. */
MidiBufferIterator begin() const noexcept { return cbegin(); }
/** Get a read-only iterator pointing one past the end of this buffer. */
MidiBufferIterator end() const noexcept { return cend(); }
/** Get a read-only iterator pointing to the beginning of this buffer. */
MidiBufferIterator cbegin() const noexcept { return MidiBufferIterator (data.begin()); }
/** Get a read-only iterator pointing one past the end of this buffer. */
MidiBufferIterator cend() const noexcept { return MidiBufferIterator (data.end()); }
/** Get an iterator pointing to the first event with a timestamp greater-than or
equal-to `samplePosition`.
*/
MidiBufferIterator findNextSamplePosition (int samplePosition) const noexcept;
//==============================================================================
#ifndef DOXYGEN
/** This class is now deprecated in favour of MidiBufferIterator.
Used to iterate through the events in a MidiBuffer.
Note that altering the buffer while an iterator is using it will produce
undefined behaviour.
@see MidiBuffer
*/
class [[deprecated]] JUCE_API Iterator
{
public:
//==============================================================================
/** Creates an Iterator for this MidiBuffer. */
Iterator (const MidiBuffer& b) noexcept;
//==============================================================================
/** Repositions the iterator so that the next event retrieved will be the first
one whose sample position is at greater than or equal to the given position.
*/
void setNextSamplePosition (int samplePosition) noexcept;
/** Retrieves a copy of the next event from the buffer.
@param result on return, this will be the message. The MidiMessage's timestamp
is set to the same value as samplePosition.
@param samplePosition on return, this will be the position of the event, as a
sample index in the buffer
@returns true if an event was found, or false if the iterator has reached
the end of the buffer
*/
bool getNextEvent (MidiMessage& result,
int& samplePosition) noexcept;
/** Retrieves the next event from the buffer.
@param midiData on return, this pointer will be set to a block of data containing
the midi message. Note that to make it fast, this is a pointer
directly into the MidiBuffer's internal data, so is only valid
temporarily until the MidiBuffer is altered.
@param numBytesOfMidiData on return, this is the number of bytes of data used by the
midi message
@param samplePosition on return, this will be the position of the event, as a
sample index in the buffer
@returns true if an event was found, or false if the iterator has reached
the end of the buffer
*/
bool getNextEvent (const uint8* &midiData,
int& numBytesOfMidiData,
int& samplePosition) noexcept;
private:
//==============================================================================
const MidiBuffer& buffer;
MidiBufferIterator iterator;
};
#endif
/** The raw data holding this buffer.
Obviously access to this data is provided at your own risk. Its internal format could
change in future, so don't write code that relies on it!
*/
Array<uint8> data;
private:
JUCE_LEAK_DETECTOR (MidiBuffer)
};
} // namespace juce
@@ -0,0 +1,200 @@
/*
==============================================================================
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
{
//==============================================================================
/**
Helper class that takes chunks of incoming midi bytes, packages them into
messages, and dispatches them to a midi callback.
@tags{Audio}
*/
class MidiDataConcatenator
{
public:
MidiDataConcatenator (int initialBufferSize)
: pendingSysexData ((size_t) initialBufferSize)
{
}
void reset()
{
currentMessageLen = 0;
pendingSysexSize = 0;
pendingSysexTime = 0;
}
template <typename UserDataType, typename CallbackType>
void pushMidiData (const void* inputData, int numBytes, double time,
UserDataType* input, CallbackType& callback)
{
auto d = static_cast<const uint8*> (inputData);
while (numBytes > 0)
{
auto nextByte = *d;
if (pendingSysexSize != 0 || nextByte == 0xf0)
{
processSysex (d, numBytes, time, input, callback);
currentMessageLen = 0;
continue;
}
++d;
--numBytes;
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;
}
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;
}
auto expectedLength = MidiMessage::getMessageLengthFromFirstByte (currentMessage[0]);
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
}
}
}
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;
double pendingSysexTime = 0;
int pendingSysexSize = 0;
JUCE_DECLARE_NON_COPYABLE (MidiDataConcatenator)
};
} // namespace juce
@@ -0,0 +1,918 @@
/*
==============================================================================
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
{
namespace MidiFileHelpers
{
static void writeVariableLengthInt (OutputStream& out, uint32 v)
{
auto buffer = v & 0x7f;
while ((v >>= 7) != 0)
{
buffer <<= 8;
buffer |= ((v & 0x7f) | 0x80);
}
for (;;)
{
out.writeByte ((char) buffer);
if (buffer & 0x80)
buffer >>= 8;
else
break;
}
}
template <typename Integral>
struct ReadTrait;
template <>
struct ReadTrait<uint32> { static constexpr auto read = ByteOrder::bigEndianInt; };
template <>
struct ReadTrait<uint16> { static constexpr auto read = ByteOrder::bigEndianShort; };
template <typename Integral>
Optional<Integral> tryRead (const uint8*& data, size_t& remaining)
{
using Trait = ReadTrait<Integral>;
constexpr auto size = sizeof (Integral);
if (remaining < size)
return {};
const Optional<Integral> result { Trait::read (data) };
data += size;
remaining -= size;
return result;
}
struct HeaderDetails
{
size_t bytesRead = 0;
short timeFormat = 0;
short fileType = 0;
short numberOfTracks = 0;
};
static Optional<HeaderDetails> parseMidiHeader (const uint8* const initialData,
const size_t maxSize)
{
auto* data = initialData;
auto remaining = maxSize;
auto ch = tryRead<uint32> (data, remaining);
if (! ch.hasValue())
return {};
if (*ch != ByteOrder::bigEndianInt ("MThd"))
{
auto ok = false;
if (*ch == ByteOrder::bigEndianInt ("RIFF"))
{
for (int i = 0; i < 8; ++i)
{
ch = tryRead<uint32> (data, remaining);
if (! ch.hasValue())
return {};
if (*ch == ByteOrder::bigEndianInt ("MThd"))
{
ok = true;
break;
}
}
}
if (! ok)
return {};
}
const auto bytesRemaining = tryRead<uint32> (data, remaining);
if (! bytesRemaining.hasValue() || *bytesRemaining > remaining)
return {};
const auto optFileType = tryRead<uint16> (data, remaining);
if (! optFileType.hasValue() || 2 < *optFileType)
return {};
const auto optNumTracks = tryRead<uint16> (data, remaining);
if (! optNumTracks.hasValue() || (*optFileType == 0 && *optNumTracks != 1))
return {};
const auto optTimeFormat = tryRead<uint16> (data, remaining);
if (! optTimeFormat.hasValue())
return {};
HeaderDetails result;
result.fileType = (short) *optFileType;
result.timeFormat = (short) *optTimeFormat;
result.numberOfTracks = (short) *optNumTracks;
result.bytesRead = maxSize - remaining;
return { result };
}
static double convertTicksToSeconds (double time,
const MidiMessageSequence& tempoEvents,
int timeFormat)
{
if (timeFormat < 0)
return time / (-(timeFormat >> 8) * (timeFormat & 0xff));
double lastTime = 0, correctedTime = 0;
auto tickLen = 1.0 / (timeFormat & 0x7fff);
auto secsPerTick = 0.5 * tickLen;
auto numEvents = tempoEvents.getNumEvents();
for (int i = 0; i < numEvents; ++i)
{
auto& m = tempoEvents.getEventPointer (i)->message;
auto eventTime = m.getTimeStamp();
if (eventTime >= time)
break;
correctedTime += (eventTime - lastTime) * secsPerTick;
lastTime = eventTime;
if (m.isTempoMetaEvent())
secsPerTick = tickLen * m.getTempoSecondsPerQuarterNote();
while (i + 1 < numEvents)
{
auto& m2 = tempoEvents.getEventPointer (i + 1)->message;
if (! approximatelyEqual (m2.getTimeStamp(), eventTime))
break;
if (m2.isTempoMetaEvent())
secsPerTick = tickLen * m2.getTempoSecondsPerQuarterNote();
++i;
}
}
return correctedTime + (time - lastTime) * secsPerTick;
}
template <typename MethodType>
static void findAllMatchingEvents (const OwnedArray<MidiMessageSequence>& tracks,
MidiMessageSequence& results,
MethodType method)
{
for (auto* track : tracks)
{
auto numEvents = track->getNumEvents();
for (int j = 0; j < numEvents; ++j)
{
auto& m = track->getEventPointer (j)->message;
if ((m.*method)())
results.addEvent (m);
}
}
}
static MidiMessageSequence readTrack (const uint8* data, int size)
{
double time = 0;
uint8 lastStatusByte = 0;
MidiMessageSequence result;
while (size > 0)
{
const auto delay = MidiMessage::readVariableLengthValue (data, (int) size);
if (! delay.isValid())
break;
data += delay.bytesUsed;
size -= delay.bytesUsed;
time += delay.value;
if (size <= 0)
break;
int messSize = 0;
const MidiMessage mm (data, size, messSize, lastStatusByte, time);
if (messSize <= 0)
break;
size -= messSize;
data += messSize;
result.addEvent (mm);
auto firstByte = *(mm.getRawData());
if ((firstByte & 0xf0) != 0xf0)
lastStatusByte = firstByte;
}
return result;
}
}
//==============================================================================
MidiFile::MidiFile() : timeFormat ((short) (unsigned short) 0xe728) {}
MidiFile::MidiFile (const MidiFile& other) : timeFormat (other.timeFormat)
{
tracks.addCopiesOf (other.tracks);
}
MidiFile& MidiFile::operator= (const MidiFile& other)
{
tracks.clear();
tracks.addCopiesOf (other.tracks);
timeFormat = other.timeFormat;
return *this;
}
MidiFile::MidiFile (MidiFile&& other)
: tracks (std::move (other.tracks)),
timeFormat (other.timeFormat)
{
}
MidiFile& MidiFile::operator= (MidiFile&& other)
{
tracks = std::move (other.tracks);
timeFormat = other.timeFormat;
return *this;
}
void MidiFile::clear()
{
tracks.clear();
}
//==============================================================================
int MidiFile::getNumTracks() const noexcept
{
return tracks.size();
}
const MidiMessageSequence* MidiFile::getTrack (int index) const noexcept
{
return tracks[index];
}
void MidiFile::addTrack (const MidiMessageSequence& trackSequence)
{
tracks.add (new MidiMessageSequence (trackSequence));
}
//==============================================================================
short MidiFile::getTimeFormat() const noexcept
{
return timeFormat;
}
void MidiFile::setTicksPerQuarterNote (int ticks) noexcept
{
timeFormat = (short) ticks;
}
void MidiFile::setSmpteTimeFormat (int framesPerSecond, int subframeResolution) noexcept
{
timeFormat = (short) (((-framesPerSecond) << 8) | subframeResolution);
}
//==============================================================================
void MidiFile::findAllTempoEvents (MidiMessageSequence& results) const
{
MidiFileHelpers::findAllMatchingEvents (tracks, results, &MidiMessage::isTempoMetaEvent);
}
void MidiFile::findAllTimeSigEvents (MidiMessageSequence& results) const
{
MidiFileHelpers::findAllMatchingEvents (tracks, results, &MidiMessage::isTimeSignatureMetaEvent);
}
void MidiFile::findAllKeySigEvents (MidiMessageSequence& results) const
{
MidiFileHelpers::findAllMatchingEvents (tracks, results, &MidiMessage::isKeySignatureMetaEvent);
}
double MidiFile::getLastTimestamp() const
{
double t = 0.0;
for (auto* ms : tracks)
t = jmax (t, ms->getEndTime());
return t;
}
//==============================================================================
bool MidiFile::readFrom (InputStream& sourceStream,
bool createMatchingNoteOffs,
int* fileType)
{
clear();
MemoryBlock data;
const int maxSensibleMidiFileSize = 200 * 1024 * 1024;
// (put a sanity-check on the file size, as midi files are generally small)
if (! sourceStream.readIntoMemoryBlock (data, maxSensibleMidiFileSize))
return false;
auto size = data.getSize();
auto d = static_cast<const uint8*> (data.getData());
const auto optHeader = MidiFileHelpers::parseMidiHeader (d, size);
if (! optHeader.hasValue())
return false;
const auto header = *optHeader;
timeFormat = header.timeFormat;
d += header.bytesRead;
size -= (size_t) header.bytesRead;
for (int track = 0; track < header.numberOfTracks; ++track)
{
const auto optChunkType = MidiFileHelpers::tryRead<uint32> (d, size);
if (! optChunkType.hasValue())
return false;
const auto optChunkSize = MidiFileHelpers::tryRead<uint32> (d, size);
if (! optChunkSize.hasValue())
return false;
const auto chunkSize = *optChunkSize;
if (size < chunkSize)
return false;
if (*optChunkType == ByteOrder::bigEndianInt ("MTrk"))
readNextTrack (d, (int) chunkSize, createMatchingNoteOffs);
size -= chunkSize;
d += chunkSize;
}
const auto successful = (size == 0);
if (successful && fileType != nullptr)
*fileType = header.fileType;
return successful;
}
template <typename It>
static void reorderNoteOnsAfterNoteOffs (const It begin, const It end)
{
for (auto it = begin; it != end;)
{
const auto firstNoteOn = std::find_if (it, end, [] (const auto& x)
{
return x->message.isNoteOn();
});
if (firstNoteOn == end)
return;
const auto channel = (*firstNoteOn)->message.getChannel();
const auto noteNumber = (*firstNoteOn)->message.getNoteNumber();
const auto rEnd = std::make_reverse_iterator (firstNoteOn);
const auto lastNoteOff = std::find_if (std::make_reverse_iterator (end), rEnd, [&] (const auto& x)
{
return x->message.getChannel() == channel
&& x->message.getNoteNumber() == noteNumber
&& x->message.isNoteOff();
});
if (lastNoteOff == rEnd)
return;
std::iter_swap (firstNoteOn, std::prev (lastNoteOff.base()));
it = std::next (firstNoteOn);
}
}
void MidiFile::readNextTrack (const uint8* data, int size, bool createMatchingNoteOffs)
{
auto sequence = MidiFileHelpers::readTrack (data, size);
sequence.sort();
for (auto it = sequence.begin(); it != sequence.end();)
{
const auto stamp = (*it)->message.getTimeStamp();
const auto nextTime = std::find_if (it, sequence.end(), [stamp] (const auto& x)
{
return ! exactlyEqual (x->message.getTimeStamp(), stamp);
});
reorderNoteOnsAfterNoteOffs (it, nextTime);
it = nextTime;
}
if (createMatchingNoteOffs)
sequence.updateMatchedPairs();
addTrack (sequence);
}
//==============================================================================
void MidiFile::convertTimestampTicksToSeconds()
{
MidiMessageSequence tempoEvents;
findAllTempoEvents (tempoEvents);
findAllTimeSigEvents (tempoEvents);
if (timeFormat != 0)
{
for (auto* ms : tracks)
{
for (int j = ms->getNumEvents(); --j >= 0;)
{
auto& m = ms->getEventPointer (j)->message;
m.setTimeStamp (MidiFileHelpers::convertTicksToSeconds (m.getTimeStamp(), tempoEvents, timeFormat));
}
}
}
}
//==============================================================================
bool MidiFile::writeTo (OutputStream& out, int midiFileType) const
{
jassert (midiFileType >= 0 && midiFileType <= 2);
if (! out.writeIntBigEndian ((int) ByteOrder::bigEndianInt ("MThd"))) return false;
if (! out.writeIntBigEndian (6)) return false;
if (! out.writeShortBigEndian ((short) midiFileType)) return false;
if (! out.writeShortBigEndian ((short) tracks.size())) return false;
if (! out.writeShortBigEndian (timeFormat)) return false;
for (auto* ms : tracks)
if (! writeTrack (out, *ms))
return false;
out.flush();
return true;
}
bool MidiFile::writeTrack (OutputStream& mainOut, const MidiMessageSequence& ms) const
{
MemoryOutputStream out;
int lastTick = 0;
uint8 lastStatusByte = 0;
bool endOfTrackEventWritten = false;
for (int i = 0; i < ms.getNumEvents(); ++i)
{
auto& mm = ms.getEventPointer (i)->message;
if (mm.isEndOfTrackMetaEvent())
endOfTrackEventWritten = true;
auto tick = roundToInt (mm.getTimeStamp());
auto delta = jmax (0, tick - lastTick);
MidiFileHelpers::writeVariableLengthInt (out, (uint32) delta);
lastTick = tick;
auto* data = mm.getRawData();
auto dataSize = mm.getRawDataSize();
auto statusByte = data[0];
if (statusByte == lastStatusByte
&& (statusByte & 0xf0) != 0xf0
&& dataSize > 1
&& i > 0)
{
++data;
--dataSize;
}
else if (statusByte == 0xf0) // Write sysex message with length bytes.
{
out.writeByte ((char) statusByte);
++data;
--dataSize;
MidiFileHelpers::writeVariableLengthInt (out, (uint32) dataSize);
}
out.write (data, (size_t) dataSize);
lastStatusByte = statusByte;
}
if (! endOfTrackEventWritten)
{
out.writeByte (0); // (tick delta)
auto m = MidiMessage::endOfTrack();
out.write (m.getRawData(), (size_t) m.getRawDataSize());
}
if (! mainOut.writeIntBigEndian ((int) ByteOrder::bigEndianInt ("MTrk"))) return false;
if (! mainOut.writeIntBigEndian ((int) out.getDataSize())) return false;
mainOut << out;
return true;
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
struct MidiFileTest final : public UnitTest
{
MidiFileTest()
: UnitTest ("MidiFile", UnitTestCategories::midi)
{}
void runTest() override
{
beginTest ("ReadTrack respects running status");
{
const auto sequence = parseSequence ([] (OutputStream& os)
{
MidiFileHelpers::writeVariableLengthInt (os, 100);
writeBytes (os, { 0x90, 0x40, 0x40 });
MidiFileHelpers::writeVariableLengthInt (os, 200);
writeBytes (os, { 0x40, 0x40 });
MidiFileHelpers::writeVariableLengthInt (os, 300);
writeBytes (os, { 0xff, 0x2f, 0x00 });
});
expectEquals (sequence.getNumEvents(), 3);
expect (sequence.getEventPointer (0)->message.isNoteOn());
expect (sequence.getEventPointer (1)->message.isNoteOn());
expect (sequence.getEventPointer (2)->message.isEndOfTrackMetaEvent());
}
beginTest ("ReadTrack returns available messages if input is truncated");
{
{
const auto sequence = parseSequence ([] (OutputStream& os)
{
// Incomplete delta time
writeBytes (os, { 0xff });
});
expectEquals (sequence.getNumEvents(), 0);
}
{
const auto sequence = parseSequence ([] (OutputStream& os)
{
// Complete delta with no following event
MidiFileHelpers::writeVariableLengthInt (os, 0xffff);
});
expectEquals (sequence.getNumEvents(), 0);
}
{
const auto sequence = parseSequence ([] (OutputStream& os)
{
// Complete delta with malformed following event
MidiFileHelpers::writeVariableLengthInt (os, 0xffff);
writeBytes (os, { 0x90, 0x40 });
});
expectEquals (sequence.getNumEvents(), 1);
expect (sequence.getEventPointer (0)->message.isNoteOff());
expectEquals (sequence.getEventPointer (0)->message.getNoteNumber(), 0x40);
expectEquals (sequence.getEventPointer (0)->message.getVelocity(), (uint8) 0x00);
}
}
beginTest ("Header parsing works");
{
{
// No data
const auto header = parseHeader ([] (OutputStream&) {});
expect (! header.hasValue());
}
{
// Invalid initial byte
const auto header = parseHeader ([] (OutputStream& os)
{
writeBytes (os, { 0xff });
});
expect (! header.hasValue());
}
{
// Type block, but no header data
const auto header = parseHeader ([] (OutputStream& os)
{
writeBytes (os, { 'M', 'T', 'h', 'd' });
});
expect (! header.hasValue());
}
{
// We (ll-formed header, but track type is 0 and channels != 1
const auto header = parseHeader ([] (OutputStream& os)
{
writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 0, 0, 16, 0, 1 });
});
expect (! header.hasValue());
}
{
// Well-formed header, but track type is 5
const auto header = parseHeader ([] (OutputStream& os)
{
writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 5, 0, 16, 0, 1 });
});
expect (! header.hasValue());
}
{
// Well-formed header
const auto header = parseHeader ([] (OutputStream& os)
{
writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 16, 0, 1 });
});
expect (header.hasValue());
expectEquals (header->fileType, (short) 1);
expectEquals (header->numberOfTracks, (short) 16);
expectEquals (header->timeFormat, (short) 1);
expectEquals ((int) header->bytesRead, 14);
}
}
beginTest ("Read from stream");
{
{
// Empty input
const auto file = parseFile ([] (OutputStream&) {});
expect (! file.hasValue());
}
{
// Malformed header
const auto file = parseFile ([] (OutputStream& os)
{
writeBytes (os, { 'M', 'T', 'h', 'd' });
});
expect (! file.hasValue());
}
{
// Header, no channels
const auto file = parseFile ([] (OutputStream& os)
{
writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 0, 0, 1 });
});
expect (file.hasValue());
expectEquals (file->getNumTracks(), 0);
}
{
// Header, one malformed channel
const auto file = parseFile ([] (OutputStream& os)
{
writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 });
writeBytes (os, { 'M', 'T', 'r', '?' });
});
expect (! file.hasValue());
}
{
// Header, one channel with malformed message
const auto file = parseFile ([] (OutputStream& os)
{
writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 });
writeBytes (os, { 'M', 'T', 'r', 'k', 0, 0, 0, 1, 0xff });
});
expect (file.hasValue());
expectEquals (file->getNumTracks(), 1);
expectEquals (file->getTrack (0)->getNumEvents(), 0);
}
{
// Header, one channel with incorrect length message
const auto file = parseFile ([] (OutputStream& os)
{
writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 });
writeBytes (os, { 'M', 'T', 'r', 'k', 0x0f, 0, 0, 0, 0xff });
});
expect (! file.hasValue());
}
{
// Header, one channel, all well-formed
const auto file = parseFile ([] (OutputStream& os)
{
writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 });
writeBytes (os, { 'M', 'T', 'r', 'k', 0, 0, 0, 4 });
MidiFileHelpers::writeVariableLengthInt (os, 0x0f);
writeBytes (os, { 0x80, 0x00, 0x00 });
});
expect (file.hasValue());
expectEquals (file->getNumTracks(), 1);
auto& track = *file->getTrack (0);
expectEquals (track.getNumEvents(), 1);
expect (track.getEventPointer (0)->message.isNoteOff());
expectEquals (track.getEventPointer (0)->message.getTimeStamp(), (double) 0x0f);
}
}
beginTest ("Reorder note on/off");
{
// Note off before note on does not get reordered
{
const auto channel = 1;
const auto noteNumber = 64;
MidiMessageSequence sequence;
sequence.addEvent (MidiMessage::noteOff (channel, noteNumber).withTimeStamp (0));
sequence.addEvent (MidiMessage::noteOn (channel, noteNumber, 0.5f).withTimeStamp (0));
reorderNoteOnsAfterNoteOffs (sequence.begin(), sequence.end());
expect (sequence.getEventPointer (0)->message.isNoteOff());
expect (sequence.getEventPointer (1)->message.isNoteOn());
}
// Note on before note off gets swapped
{
const auto channel = 1;
const auto noteNumber = 64;
MidiMessageSequence sequence;
sequence.addEvent (MidiMessage::noteOn (channel, noteNumber, 0.5f).withTimeStamp (0));
sequence.addEvent (MidiMessage::noteOff (channel, noteNumber).withTimeStamp (0));
reorderNoteOnsAfterNoteOffs (sequence.begin(), sequence.end());
expect (sequence.getEventPointer (0)->message.isNoteOff());
expect (sequence.getEventPointer (1)->message.isNoteOn());
}
// Note on before note off is not swapped if note numbers differ
{
const auto channel = 1;
MidiMessageSequence sequence;
sequence.addEvent (MidiMessage::noteOn (channel, 64, 0.5f).withTimeStamp (0));
sequence.addEvent (MidiMessage::noteOff (channel, 65).withTimeStamp (0));
reorderNoteOnsAfterNoteOffs (sequence.begin(), sequence.end());
expect (sequence.getEventPointer (0)->message.isNoteOn());
expect (sequence.getEventPointer (1)->message.isNoteOff());
}
// Note on before note off is not swapped if channel numbers differ
{
const auto noteNumber = 64;
MidiMessageSequence sequence;
sequence.addEvent (MidiMessage::noteOn (1, noteNumber, 0.5f).withTimeStamp (0));
sequence.addEvent (MidiMessage::noteOff (2, noteNumber).withTimeStamp (0));
reorderNoteOnsAfterNoteOffs (sequence.begin(), sequence.end());
expect (sequence.getEventPointer (0)->message.isNoteOn());
expect (sequence.getEventPointer (1)->message.isNoteOff());
}
// If there are several note ons/offs, all offs come before all ons
{
const auto channel = 1;
const auto noteNumber = 64;
MidiMessageSequence sequence;
sequence.addEvent (MidiMessage::noteOn (channel, noteNumber, 0.5f).withTimeStamp (0));
sequence.addEvent (MidiMessage::noteOff (channel, noteNumber).withTimeStamp (0));
sequence.addEvent (MidiMessage::noteOn (channel, noteNumber, 0.5f).withTimeStamp (0));
sequence.addEvent (MidiMessage::noteOff (channel, noteNumber).withTimeStamp (0));
sequence.addEvent (MidiMessage::noteOn (channel, noteNumber, 0.5f).withTimeStamp (0));
sequence.addEvent (MidiMessage::noteOff (channel, noteNumber).withTimeStamp (0));
reorderNoteOnsAfterNoteOffs (sequence.begin(), sequence.end());
expect (sequence.getEventPointer (0)->message.isNoteOff());
expect (sequence.getEventPointer (1)->message.isNoteOff());
expect (sequence.getEventPointer (2)->message.isNoteOff());
expect (sequence.getEventPointer (3)->message.isNoteOn());
expect (sequence.getEventPointer (4)->message.isNoteOn());
expect (sequence.getEventPointer (5)->message.isNoteOn());
}
}
}
template <typename Fn>
static MidiMessageSequence parseSequence (Fn&& fn)
{
MemoryOutputStream os;
fn (os);
return MidiFileHelpers::readTrack (reinterpret_cast<const uint8*> (os.getData()),
(int) os.getDataSize());
}
template <typename Fn>
static Optional<MidiFileHelpers::HeaderDetails> parseHeader (Fn&& fn)
{
MemoryOutputStream os;
fn (os);
return MidiFileHelpers::parseMidiHeader (reinterpret_cast<const uint8*> (os.getData()),
os.getDataSize());
}
template <typename Fn>
static Optional<MidiFile> parseFile (Fn&& fn)
{
MemoryOutputStream os;
fn (os);
MemoryInputStream is (os.getData(), os.getDataSize(), false);
MidiFile mf;
int fileType = 0;
if (mf.readFrom (is, true, &fileType))
return mf;
return {};
}
static void writeBytes (OutputStream& os, const std::vector<uint8>& bytes)
{
for (const auto& byte : bytes)
os.writeByte ((char) byte);
}
};
static MidiFileTest midiFileTests;
#endif
} // namespace juce
@@ -0,0 +1,210 @@
/*
==============================================================================
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
{
//==============================================================================
/**
Reads/writes standard midi format files.
To read a midi file, create a MidiFile object and call its readFrom() method. You
can then get the individual midi tracks from it using the getTrack() method.
To write a file, create a MidiFile object, add some MidiMessageSequence objects
to it using the addTrack() method, and then call its writeTo() method to stream
it out.
@see MidiMessageSequence
@tags{Audio}
*/
class JUCE_API MidiFile
{
public:
//==============================================================================
/** Creates an empty MidiFile object. */
MidiFile();
/** Creates a copy of another MidiFile. */
MidiFile (const MidiFile&);
/** Copies from another MidiFile object */
MidiFile& operator= (const MidiFile&);
/** Creates a copy of another MidiFile. */
MidiFile (MidiFile&&);
/** Copies from another MidiFile object */
MidiFile& operator= (MidiFile&&);
//==============================================================================
/** Returns the number of tracks in the file.
@see getTrack, addTrack
*/
int getNumTracks() const noexcept;
/** Returns a pointer to one of the tracks in the file.
@returns a pointer to the track, or nullptr if the index is out-of-range
@see getNumTracks, addTrack
*/
const MidiMessageSequence* getTrack (int index) const noexcept;
/** Adds a midi track to the file.
This will make its own internal copy of the sequence that is passed-in.
@see getNumTracks, getTrack
*/
void addTrack (const MidiMessageSequence& trackSequence);
/** Removes all midi tracks from the file.
@see getNumTracks
*/
void clear();
/** Returns the raw time format code that will be written to a stream.
After reading a midi file, this method will return the time-format that
was read from the file's header. It can be changed using the setTicksPerQuarterNote()
or setSmpteTimeFormat() methods.
If the value returned is positive, it indicates the number of midi ticks
per quarter-note - see setTicksPerQuarterNote().
It it's negative, the upper byte indicates the frames-per-second (but negative), and
the lower byte is the number of ticks per frame - see setSmpteTimeFormat().
*/
short getTimeFormat() const noexcept;
/** Sets the time format to use when this file is written to a stream.
If this is called, the file will be written as bars/beats using the
specified resolution, rather than SMPTE absolute times, as would be
used if setSmpteTimeFormat() had been called instead.
@param ticksPerQuarterNote e.g. 96, 960
@see setSmpteTimeFormat
*/
void setTicksPerQuarterNote (int ticksPerQuarterNote) noexcept;
/** Sets the time format to use when this file is written to a stream.
If this is called, the file will be written using absolute times, rather
than bars/beats as would be the case if setTicksPerBeat() had been called
instead.
@param framesPerSecond must be 24, 25, 29 or 30
@param subframeResolution the sub-second resolution, e.g. 4 (midi time code),
8, 10, 80 (SMPTE bit resolution), or 100. For millisecond
timing, setSmpteTimeFormat (25, 40)
@see setTicksPerBeat
*/
void setSmpteTimeFormat (int framesPerSecond,
int subframeResolution) noexcept;
//==============================================================================
/** Makes a list of all the tempo-change meta-events from all tracks in the midi file.
Useful for finding the positions of all the tempo changes in a file.
@param tempoChangeEvents a list to which all the events will be added
*/
void findAllTempoEvents (MidiMessageSequence& tempoChangeEvents) const;
/** Makes a list of all the time-signature meta-events from all tracks in the midi file.
Useful for finding the positions of all the tempo changes in a file.
@param timeSigEvents a list to which all the events will be added
*/
void findAllTimeSigEvents (MidiMessageSequence& timeSigEvents) const;
/** Makes a list of all the key-signature meta-events from all tracks in the midi file.
@param keySigEvents a list to which all the events will be added
*/
void findAllKeySigEvents (MidiMessageSequence& keySigEvents) const;
/** Returns the latest timestamp in any of the tracks.
(Useful for finding the length of the file).
*/
double getLastTimestamp() const;
//==============================================================================
/** Reads a midi file format stream.
After calling this, you can get the tracks that were read from the file by using the
getNumTracks() and getTrack() methods.
The timestamps of the midi events in the tracks will represent their positions in
terms of midi ticks. To convert them to seconds, use the convertTimestampTicksToSeconds()
method.
@param sourceStream the source stream
@param createMatchingNoteOffs if true, any missing note-offs for previous note-ons will
be automatically added at the end of the file by calling
MidiMessageSequence::updateMatchedPairs on each track.
@param midiFileType if not nullptr, the integer at this address will be set
to 0, 1, or 2 depending on the type of the midi file
@returns true if the stream was read successfully
*/
bool readFrom (InputStream& sourceStream,
bool createMatchingNoteOffs = true,
int* midiFileType = nullptr);
/** Writes the midi tracks as a standard midi file.
The midiFileType value is written as the file's format type, which can be 0, 1
or 2 - see the midi file spec for more info about that.
@param destStream the destination stream
@param midiFileType the type of midi file
@returns true if the operation succeeded.
*/
bool writeTo (OutputStream& destStream, int midiFileType = 1) const;
/** Converts the timestamp of all the midi events from midi ticks to seconds.
This will use the midi time format and tempo/time signature info in the
tracks to convert all the timestamps to absolute values in seconds.
*/
void convertTimestampTicksToSeconds();
private:
//==============================================================================
OwnedArray<MidiMessageSequence> tracks;
short timeFormat;
void readNextTrack (const uint8*, int, bool);
bool writeTrack (OutputStream&, const MidiMessageSequence&) const;
JUCE_LEAK_DETECTOR (MidiFile)
};
} // namespace juce
@@ -0,0 +1,185 @@
/*
==============================================================================
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
{
MidiKeyboardState::MidiKeyboardState()
{
zerostruct (noteStates);
}
//==============================================================================
void MidiKeyboardState::reset()
{
const ScopedLock sl (lock);
zerostruct (noteStates);
eventsToAdd.clear();
}
bool MidiKeyboardState::isNoteOn (const int midiChannel, const int n) const noexcept
{
jassert (midiChannel > 0 && midiChannel <= 16);
return isPositiveAndBelow (n, 128)
&& (noteStates[n] & (1 << (midiChannel - 1))) != 0;
}
bool MidiKeyboardState::isNoteOnForChannels (const int midiChannelMask, const int n) const noexcept
{
return isPositiveAndBelow (n, 128)
&& (noteStates[n] & midiChannelMask) != 0;
}
void MidiKeyboardState::noteOn (const int midiChannel, const int midiNoteNumber, const float velocity)
{
jassert (midiChannel > 0 && midiChannel <= 16);
jassert (isPositiveAndBelow (midiNoteNumber, 128));
const ScopedLock sl (lock);
if (isPositiveAndBelow (midiNoteNumber, 128))
{
const int timeNow = (int) Time::getMillisecondCounter();
eventsToAdd.addEvent (MidiMessage::noteOn (midiChannel, midiNoteNumber, velocity), timeNow);
eventsToAdd.clear (0, timeNow - 500);
noteOnInternal (midiChannel, midiNoteNumber, velocity);
}
}
void MidiKeyboardState::noteOnInternal (const int midiChannel, const int midiNoteNumber, const float velocity)
{
if (isPositiveAndBelow (midiNoteNumber, 128))
{
noteStates[midiNoteNumber] = static_cast<uint16> (noteStates[midiNoteNumber] | (1 << (midiChannel - 1)));
listeners.call ([&] (Listener& l) { l.handleNoteOn (this, midiChannel, midiNoteNumber, velocity); });
}
}
void MidiKeyboardState::noteOff (const int midiChannel, const int midiNoteNumber, const float velocity)
{
const ScopedLock sl (lock);
if (isNoteOn (midiChannel, midiNoteNumber))
{
const int timeNow = (int) Time::getMillisecondCounter();
eventsToAdd.addEvent (MidiMessage::noteOff (midiChannel, midiNoteNumber), timeNow);
eventsToAdd.clear (0, timeNow - 500);
noteOffInternal (midiChannel, midiNoteNumber, velocity);
}
}
void MidiKeyboardState::noteOffInternal (const int midiChannel, const int midiNoteNumber, const float velocity)
{
if (isNoteOn (midiChannel, midiNoteNumber))
{
noteStates[midiNoteNumber] = static_cast<uint16> (noteStates[midiNoteNumber] & ~(1 << (midiChannel - 1)));
listeners.call ([&] (Listener& l) { l.handleNoteOff (this, midiChannel, midiNoteNumber, velocity); });
}
}
void MidiKeyboardState::allNotesOff (const int midiChannel)
{
const ScopedLock sl (lock);
if (midiChannel <= 0)
{
for (int i = 1; i <= 16; ++i)
allNotesOff (i);
}
else
{
for (int i = 0; i < 128; ++i)
noteOff (midiChannel, i, 0.0f);
}
}
void MidiKeyboardState::processNextMidiEvent (const MidiMessage& message)
{
if (message.isNoteOn())
{
noteOnInternal (message.getChannel(), message.getNoteNumber(), message.getFloatVelocity());
}
else if (message.isNoteOff())
{
noteOffInternal (message.getChannel(), message.getNoteNumber(), message.getFloatVelocity());
}
else if (message.isAllNotesOff())
{
for (int i = 0; i < 128; ++i)
noteOffInternal (message.getChannel(), i, 0.0f);
}
}
void MidiKeyboardState::processNextMidiBuffer (MidiBuffer& buffer,
const int startSample,
const int numSamples,
const bool injectIndirectEvents)
{
const ScopedLock sl (lock);
for (const auto metadata : buffer)
processNextMidiEvent (metadata.getMessage());
if (injectIndirectEvents)
{
const int firstEventToAdd = eventsToAdd.getFirstEventTime();
const double scaleFactor = numSamples / (double) (eventsToAdd.getLastEventTime() + 1 - firstEventToAdd);
for (const auto metadata : eventsToAdd)
{
const auto pos = jlimit (0, numSamples - 1, roundToInt ((metadata.samplePosition - firstEventToAdd) * scaleFactor));
buffer.addEvent (metadata.getMessage(), startSample + pos);
}
}
eventsToAdd.clear();
}
//==============================================================================
void MidiKeyboardState::addListener (Listener* listener)
{
const ScopedLock sl (lock);
listeners.add (listener);
}
void MidiKeyboardState::removeListener (Listener* listener)
{
const ScopedLock sl (lock);
listeners.remove (listener);
}
} // namespace juce
@@ -0,0 +1,207 @@
/*
==============================================================================
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
{
//==============================================================================
/**
Represents a piano keyboard, keeping track of which keys are currently pressed.
This object can parse a stream of midi events, using them to update its idea
of which keys are pressed for each individual midi channel.
When keys go up or down, it can broadcast these events to listener objects.
It also allows key up/down events to be triggered with its noteOn() and noteOff()
methods, and midi messages for these events will be merged into the
midi stream that gets processed by processNextMidiBuffer().
@tags{Audio}
*/
class JUCE_API MidiKeyboardState
{
public:
//==============================================================================
MidiKeyboardState();
//==============================================================================
/** Resets the state of the object.
All internal data for all the channels is reset, but no events are sent as a
result.
If you want to release any keys that are currently down, and to send out note-up
midi messages for this, use the allNotesOff() method instead.
*/
void reset();
/** Returns true if the given midi key is currently held down for the given midi channel.
The channel number must be between 1 and 16. If you want to see if any notes are
on for a range of channels, use the isNoteOnForChannels() method.
*/
bool isNoteOn (int midiChannel, int midiNoteNumber) const noexcept;
/** Returns true if the given midi key is currently held down on any of a set of midi channels.
The channel mask has a bit set for each midi channel you want to test for - bit
0 = midi channel 1, bit 1 = midi channel 2, etc.
If a note is on for at least one of the specified channels, this returns true.
*/
bool isNoteOnForChannels (int midiChannelMask, int midiNoteNumber) const noexcept;
/** Turns a specified note on.
This will cause a suitable midi note-on event to be injected into the midi buffer during the
next call to processNextMidiBuffer().
It will also trigger a synchronous callback to the listeners to tell them that the key has
gone down.
*/
void noteOn (int midiChannel, int midiNoteNumber, float velocity);
/** Turns a specified note off.
This will cause a suitable midi note-off event to be injected into the midi buffer during the
next call to processNextMidiBuffer().
It will also trigger a synchronous callback to the listeners to tell them that the key has
gone up.
But if the note isn't actually down for the given channel, this method will in fact do nothing.
*/
void noteOff (int midiChannel, int midiNoteNumber, float velocity);
/** This will turn off any currently-down notes for the given midi channel.
If you pass 0 for the midi channel, it will in fact turn off all notes on all channels.
Calling this method will make calls to noteOff(), so can trigger synchronous callbacks
and events being added to the midi stream.
*/
void allNotesOff (int midiChannel);
//==============================================================================
/** Looks at a key-up/down event and uses it to update the state of this object.
To process a buffer full of midi messages, use the processNextMidiBuffer() method
instead.
*/
void processNextMidiEvent (const MidiMessage& message);
/** Scans a midi stream for up/down events and adds its own events to it.
This will look for any up/down events and use them to update the internal state,
synchronously making suitable callbacks to the listeners.
If injectIndirectEvents is true, then midi events to produce the recent noteOn()
and noteOff() calls will be added into the buffer.
Only the section of the buffer whose timestamps are between startSample and
(startSample + numSamples) will be affected, and any events added will be placed
between these times.
If you're going to use this method, you'll need to keep calling it regularly for
it to work satisfactorily.
To process a single midi event at a time, use the processNextMidiEvent() method
instead.
*/
void processNextMidiBuffer (MidiBuffer& buffer,
int startSample,
int numSamples,
bool injectIndirectEvents);
//==============================================================================
/** Receives events from a MidiKeyboardState object. */
class JUCE_API Listener
{
public:
//==============================================================================
virtual ~Listener() = default;
//==============================================================================
/** Called when one of the MidiKeyboardState's keys is pressed.
This will be called synchronously when the state is either processing a
buffer in its MidiKeyboardState::processNextMidiBuffer() method, or
when a note is being played with its MidiKeyboardState::noteOn() method.
Note that this callback could happen from an audio callback thread, so be
careful not to block, and avoid any UI activity in the callback.
*/
virtual void handleNoteOn (MidiKeyboardState* source,
int midiChannel, int midiNoteNumber, float velocity) = 0;
/** Called when one of the MidiKeyboardState's keys is released.
This will be called synchronously when the state is either processing a
buffer in its MidiKeyboardState::processNextMidiBuffer() method, or
when a note is being played with its MidiKeyboardState::noteOff() method.
Note that this callback could happen from an audio callback thread, so be
careful not to block, and avoid any UI activity in the callback.
*/
virtual void handleNoteOff (MidiKeyboardState* source,
int midiChannel, int midiNoteNumber, float velocity) = 0;
};
/** Registers a listener for callbacks when keys go up or down.
@see removeListener
*/
void addListener (Listener* listener);
/** Deregisters a listener.
@see addListener
*/
void removeListener (Listener* listener);
private:
//==============================================================================
CriticalSection lock;
std::atomic<uint16> noteStates[128];
MidiBuffer eventsToAdd;
ListenerList<Listener> listeners;
void noteOnInternal (int midiChannel, int midiNoteNumber, float velocity);
void noteOffInternal (int midiChannel, int midiNoteNumber, float velocity);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiKeyboardState)
};
using MidiKeyboardStateListener = MidiKeyboardState::Listener;
} // namespace juce
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,882 @@
/*
==============================================================================
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
{
MidiMessageSequence::MidiEventHolder::MidiEventHolder (const MidiMessage& mm) : message (mm) {}
MidiMessageSequence::MidiEventHolder::MidiEventHolder (MidiMessage&& mm) : message (std::move (mm)) {}
//==============================================================================
MidiMessageSequence::MidiMessageSequence()
{
}
MidiMessageSequence::MidiMessageSequence (const MidiMessageSequence& other)
{
list.addCopiesOf (other.list);
for (int i = 0; i < list.size(); ++i)
{
auto noteOffIndex = other.getIndexOfMatchingKeyUp (i);
if (noteOffIndex >= 0)
list.getUnchecked (i)->noteOffObject = list.getUnchecked (noteOffIndex);
}
}
MidiMessageSequence& MidiMessageSequence::operator= (const MidiMessageSequence& other)
{
MidiMessageSequence otherCopy (other);
swapWith (otherCopy);
return *this;
}
MidiMessageSequence::MidiMessageSequence (MidiMessageSequence&& other) noexcept
: list (std::move (other.list))
{
}
MidiMessageSequence& MidiMessageSequence::operator= (MidiMessageSequence&& other) noexcept
{
list = std::move (other.list);
return *this;
}
void MidiMessageSequence::swapWith (MidiMessageSequence& other) noexcept
{
list.swapWith (other.list);
}
void MidiMessageSequence::clear()
{
list.clear();
}
int MidiMessageSequence::getNumEvents() const noexcept
{
return list.size();
}
MidiMessageSequence::MidiEventHolder* MidiMessageSequence::getEventPointer (int index) const noexcept
{
return list[index];
}
MidiMessageSequence::MidiEventHolder** MidiMessageSequence::begin() noexcept { return list.begin(); }
MidiMessageSequence::MidiEventHolder* const* MidiMessageSequence::begin() const noexcept { return list.begin(); }
MidiMessageSequence::MidiEventHolder** MidiMessageSequence::end() noexcept { return list.end(); }
MidiMessageSequence::MidiEventHolder* const* MidiMessageSequence::end() const noexcept { return list.end(); }
double MidiMessageSequence::getTimeOfMatchingKeyUp (int index) const noexcept
{
if (auto* meh = list[index])
if (auto* noteOff = meh->noteOffObject)
return noteOff->message.getTimeStamp();
return 0;
}
int MidiMessageSequence::getIndexOfMatchingKeyUp (int index) const noexcept
{
if (auto* meh = list[index])
{
if (auto* noteOff = meh->noteOffObject)
{
for (int i = index; i < list.size(); ++i)
if (list.getUnchecked (i) == noteOff)
return i;
jassertfalse; // we've somehow got a pointer to a note-off object that isn't in the sequence
}
}
return -1;
}
int MidiMessageSequence::getIndexOf (const MidiEventHolder* event) const noexcept
{
return list.indexOf (event);
}
int MidiMessageSequence::getNextIndexAtTime (double timeStamp) const noexcept
{
auto numEvents = list.size();
int i;
for (i = 0; i < numEvents; ++i)
if (list.getUnchecked (i)->message.getTimeStamp() >= timeStamp)
break;
return i;
}
//==============================================================================
double MidiMessageSequence::getStartTime() const noexcept
{
return getEventTime (0);
}
double MidiMessageSequence::getEndTime() const noexcept
{
return getEventTime (list.size() - 1);
}
double MidiMessageSequence::getEventTime (const int index) const noexcept
{
if (auto* meh = list[index])
return meh->message.getTimeStamp();
return 0;
}
//==============================================================================
MidiMessageSequence::MidiEventHolder* MidiMessageSequence::addEvent (MidiEventHolder* newEvent, double timeAdjustment)
{
newEvent->message.addToTimeStamp (timeAdjustment);
auto time = newEvent->message.getTimeStamp();
int i;
for (i = list.size(); --i >= 0;)
if (list.getUnchecked (i)->message.getTimeStamp() <= time)
break;
list.insert (i + 1, newEvent);
return newEvent;
}
MidiMessageSequence::MidiEventHolder* MidiMessageSequence::addEvent (const MidiMessage& newMessage, double timeAdjustment)
{
return addEvent (new MidiEventHolder (newMessage), timeAdjustment);
}
MidiMessageSequence::MidiEventHolder* MidiMessageSequence::addEvent (MidiMessage&& newMessage, double timeAdjustment)
{
return addEvent (new MidiEventHolder (std::move (newMessage)), timeAdjustment);
}
void MidiMessageSequence::deleteEvent (int index, bool deleteMatchingNoteUp)
{
if (isPositiveAndBelow (index, list.size()))
{
if (deleteMatchingNoteUp)
deleteEvent (getIndexOfMatchingKeyUp (index), false);
list.remove (index);
}
}
void MidiMessageSequence::addSequence (const MidiMessageSequence& other, double timeAdjustment)
{
for (auto* m : other)
{
auto newOne = new MidiEventHolder (m->message);
newOne->message.addToTimeStamp (timeAdjustment);
list.add (newOne);
}
sort();
}
void MidiMessageSequence::addSequence (const MidiMessageSequence& other,
double timeAdjustment,
double firstAllowableTime,
double endOfAllowableDestTimes)
{
for (auto* m : other)
{
auto t = m->message.getTimeStamp() + timeAdjustment;
if (t >= firstAllowableTime && t < endOfAllowableDestTimes)
{
auto newOne = new MidiEventHolder (m->message);
newOne->message.setTimeStamp (t);
list.add (newOne);
}
}
sort();
}
void MidiMessageSequence::sort() noexcept
{
std::stable_sort (list.begin(), list.end(),
[] (const MidiEventHolder* a, const MidiEventHolder* b) { return a->message.getTimeStamp() < b->message.getTimeStamp(); });
}
void MidiMessageSequence::updateMatchedPairs() noexcept
{
for (int i = 0; i < list.size(); ++i)
{
auto* meh = list.getUnchecked (i);
auto& m1 = meh->message;
if (m1.isNoteOn())
{
meh->noteOffObject = nullptr;
auto note = m1.getNoteNumber();
auto chan = m1.getChannel();
auto len = list.size();
for (int j = i + 1; j < len; ++j)
{
auto* meh2 = list.getUnchecked (j);
auto& m = meh2->message;
if (m.getNoteNumber() == note && m.getChannel() == chan)
{
if (m.isNoteOff())
{
meh->noteOffObject = meh2;
break;
}
if (m.isNoteOn())
{
auto newEvent = new MidiEventHolder (MidiMessage::noteOff (chan, note));
list.insert (j, newEvent);
newEvent->message.setTimeStamp (m.getTimeStamp());
meh->noteOffObject = newEvent;
break;
}
}
}
}
}
}
void MidiMessageSequence::addTimeToMessages (double delta) noexcept
{
if (! approximatelyEqual (delta, 0.0))
for (auto* m : list)
m->message.addToTimeStamp (delta);
}
//==============================================================================
void MidiMessageSequence::extractMidiChannelMessages (const int channelNumberToExtract,
MidiMessageSequence& destSequence,
const bool alsoIncludeMetaEvents) const
{
for (auto* meh : list)
if (meh->message.isForChannel (channelNumberToExtract)
|| (alsoIncludeMetaEvents && meh->message.isMetaEvent()))
destSequence.addEvent (meh->message);
}
void MidiMessageSequence::extractSysExMessages (MidiMessageSequence& destSequence) const
{
for (auto* meh : list)
if (meh->message.isSysEx())
destSequence.addEvent (meh->message);
}
void MidiMessageSequence::deleteMidiChannelMessages (const int channelNumberToRemove)
{
for (int i = list.size(); --i >= 0;)
if (list.getUnchecked (i)->message.isForChannel (channelNumberToRemove))
list.remove (i);
}
void MidiMessageSequence::deleteSysExMessages()
{
for (int i = list.size(); --i >= 0;)
if (list.getUnchecked (i)->message.isSysEx())
list.remove (i);
}
//==============================================================================
class OptionalPitchWheel
{
Optional<int> value;
public:
void emit (int channel, Array<MidiMessage>& out) const
{
if (value.hasValue())
out.add (MidiMessage::pitchWheel (channel, *value));
}
void set (int v)
{
value = v;
}
};
class OptionalControllerValues
{
Optional<char> values[128];
public:
void emit (int channel, Array<MidiMessage>& out) const
{
for (auto it = std::begin (values); it != std::end (values); ++it)
if (it->hasValue())
out.add (MidiMessage::controllerEvent (channel, (int) std::distance (std::begin (values), it), **it));
}
void set (int controller, int value)
{
values[controller] = (char) value;
}
};
class OptionalProgramChange
{
Optional<char> value, bankLSB, bankMSB;
public:
void emit (int channel, double time, Array<MidiMessage>& out) const
{
if (! value.hasValue())
return;
if (bankLSB.hasValue() && bankMSB.hasValue())
{
out.add (MidiMessage::controllerEvent (channel, 0x00, *bankMSB).withTimeStamp (time));
out.add (MidiMessage::controllerEvent (channel, 0x20, *bankLSB).withTimeStamp (time));
}
out.add (MidiMessage::programChange (channel, *value).withTimeStamp (time));
}
// Returns true if this is a bank number change, and false otherwise.
bool trySetBank (int controller, int v)
{
switch (controller)
{
case 0x00: bankMSB = (char) v; return true;
case 0x20: bankLSB = (char) v; return true;
}
return false;
}
void setProgram (int v) { value = (char) v; }
};
class ParameterNumberState
{
enum class Kind { rpn, nrpn };
Optional<char> newestRpnLsb, newestRpnMsb, newestNrpnLsb, newestNrpnMsb, lastSentLsb, lastSentMsb;
Kind lastSentKind = Kind::rpn, newestKind = Kind::rpn;
public:
// If the effective parameter number has changed since the last time this function was called,
// this will emit the current parameter in full (MSB and LSB).
// This should be called before each data message (entry, increment, decrement: 0x06, 0x26, 0x60, 0x61)
// to ensure that the data message operates on the correct parameter number.
void sendIfNecessary (int channel, double time, Array<MidiMessage>& out)
{
const auto newestMsb = newestKind == Kind::rpn ? newestRpnMsb : newestNrpnMsb;
const auto newestLsb = newestKind == Kind::rpn ? newestRpnLsb : newestNrpnLsb;
auto lastSent = std::tie (lastSentKind, lastSentMsb, lastSentLsb);
const auto newest = std::tie (newestKind, newestMsb, newestLsb);
if (lastSent == newest || ! newestMsb.hasValue() || ! newestLsb.hasValue())
return;
out.add (MidiMessage::controllerEvent (channel, newestKind == Kind::rpn ? 0x65 : 0x63, *newestMsb).withTimeStamp (time));
out.add (MidiMessage::controllerEvent (channel, newestKind == Kind::rpn ? 0x64 : 0x62, *newestLsb).withTimeStamp (time));
lastSent = newest;
}
// Returns true if this is a parameter number change, and false otherwise.
bool trySetProgramNumber (int controller, int value)
{
switch (controller)
{
case 0x65: newestRpnMsb = (char) value; newestKind = Kind::rpn; return true;
case 0x64: newestRpnLsb = (char) value; newestKind = Kind::rpn; return true;
case 0x63: newestNrpnMsb = (char) value; newestKind = Kind::nrpn; return true;
case 0x62: newestNrpnLsb = (char) value; newestKind = Kind::nrpn; return true;
}
return false;
}
};
void MidiMessageSequence::createControllerUpdatesForTime (int channel, double time, Array<MidiMessage>& dest)
{
OptionalProgramChange programChange;
OptionalControllerValues controllers;
OptionalPitchWheel pitchWheel;
ParameterNumberState parameterNumberState;
for (const auto& item : list)
{
const auto& mm = item->message;
if (! (mm.isForChannel (channel) && mm.getTimeStamp() <= time))
continue;
if (mm.isController())
{
const auto num = mm.getControllerNumber();
if (parameterNumberState.trySetProgramNumber (num, mm.getControllerValue()))
continue;
if (programChange.trySetBank (num, mm.getControllerValue()))
continue;
constexpr int passthroughs[] { 0x06, 0x26, 0x60, 0x61 };
if (std::find (std::begin (passthroughs), std::end (passthroughs), num) != std::end (passthroughs))
{
parameterNumberState.sendIfNecessary (channel, mm.getTimeStamp(), dest);
dest.add (mm);
}
else
{
controllers.set (num, mm.getControllerValue());
}
}
else if (mm.isProgramChange())
{
programChange.setProgram (mm.getProgramChangeNumber());
}
else if (mm.isPitchWheel())
{
pitchWheel.set (mm.getPitchWheelValue());
}
}
pitchWheel.emit (channel, dest);
controllers.emit (channel, dest);
// Also emits bank change messages if necessary.
programChange.emit (channel, time, dest);
// Set the parameter number to its final state.
parameterNumberState.sendIfNecessary (channel, time, dest);
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
struct MidiMessageSequenceTest final : public UnitTest
{
MidiMessageSequenceTest()
: UnitTest ("MidiMessageSequence", UnitTestCategories::midi)
{}
void runTest() override
{
MidiMessageSequence s;
s.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0));
s.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (4.0));
s.addEvent (MidiMessage::noteOn (1, 30, 0.5f).withTimeStamp (2.0));
s.addEvent (MidiMessage::noteOff (1, 30, 0.5f).withTimeStamp (8.0));
beginTest ("Start & end time");
expectEquals (s.getStartTime(), 0.0);
expectEquals (s.getEndTime(), 8.0);
expectEquals (s.getEventTime (1), 2.0);
beginTest ("Matching note off & ons");
s.updateMatchedPairs();
expectEquals (s.getTimeOfMatchingKeyUp (0), 4.0);
expectEquals (s.getTimeOfMatchingKeyUp (1), 8.0);
expectEquals (s.getIndexOfMatchingKeyUp (0), 2);
expectEquals (s.getIndexOfMatchingKeyUp (1), 3);
beginTest ("Time & indices");
expectEquals (s.getNextIndexAtTime (0.5), 1);
expectEquals (s.getNextIndexAtTime (2.5), 2);
expectEquals (s.getNextIndexAtTime (9.0), 4);
beginTest ("Deleting events");
s.deleteEvent (0, true);
expectEquals (s.getNumEvents(), 2);
beginTest ("Merging sequences");
MidiMessageSequence s2;
s2.addEvent (MidiMessage::noteOn (2, 25, 0.5f).withTimeStamp (0.0));
s2.addEvent (MidiMessage::noteOn (2, 40, 0.5f).withTimeStamp (1.0));
s2.addEvent (MidiMessage::noteOff (2, 40, 0.5f).withTimeStamp (5.0));
s2.addEvent (MidiMessage::noteOn (2, 80, 0.5f).withTimeStamp (3.0));
s2.addEvent (MidiMessage::noteOff (2, 80, 0.5f).withTimeStamp (7.0));
s2.addEvent (MidiMessage::noteOff (2, 25, 0.5f).withTimeStamp (9.0));
s.addSequence (s2, 0.0, 0.0, 8.0); // Intentionally cut off the last note off
s.updateMatchedPairs();
expectEquals (s.getNumEvents(), 7);
expectEquals (s.getIndexOfMatchingKeyUp (0), -1); // Truncated note, should be no note off
expectEquals (s.getTimeOfMatchingKeyUp (1), 5.0);
struct ControlValue { int control, value; };
struct DataEntry
{
int controllerBase, channel, parameter, value;
double time;
std::array<ControlValue, 4> getControlValues() const
{
return { { { controllerBase + 1, (parameter >> 7) & 0x7f },
{ controllerBase + 0, (parameter >> 0) & 0x7f },
{ 0x06, (value >> 7) & 0x7f },
{ 0x26, (value >> 0) & 0x7f } } };
}
void addToSequence (MidiMessageSequence& s) const
{
for (const auto& pair : getControlValues())
s.addEvent (MidiMessage::controllerEvent (channel, pair.control, pair.value), time);
}
bool matches (const MidiMessage* begin, const MidiMessage* end) const
{
const auto isEqual = [this] (const ControlValue& cv, const MidiMessage& msg)
{
return exactlyEqual (msg.getTimeStamp(), time)
&& msg.isController()
&& msg.getChannel() == channel
&& msg.getControllerNumber() == cv.control
&& msg.getControllerValue() == cv.value;
};
const auto pairs = getControlValues();
return std::equal (pairs.begin(), pairs.end(), begin, end, isEqual);
}
};
const auto addNrpn = [&] (MidiMessageSequence& seq, int channel, int parameter, int value, double time = 0.0)
{
DataEntry { 0x62, channel, parameter, value, time }.addToSequence (seq);
};
const auto addRpn = [&] (MidiMessageSequence& seq, int channel, int parameter, int value, double time = 0.0)
{
DataEntry { 0x64, channel, parameter, value, time }.addToSequence (seq);
};
const auto checkNrpn = [&] (const MidiMessage* begin, const MidiMessage* end, int channel, int parameter, int value, double time = 0.0)
{
expect (DataEntry { 0x62, channel, parameter, value, time }.matches (begin, end));
};
const auto checkRpn = [&] (const MidiMessage* begin, const MidiMessage* end, int channel, int parameter, int value, double time = 0.0)
{
expect (DataEntry { 0x64, channel, parameter, value, time }.matches (begin, end));
};
beginTest ("createControllerUpdatesForTime should emit (N)RPN components in the correct order");
{
const auto channel = 1;
const auto number = 200;
const auto value = 300;
MidiMessageSequence sequence;
addNrpn (sequence, channel, number, value);
Array<MidiMessage> m;
sequence.createControllerUpdatesForTime (channel, 1.0, m);
checkNrpn (m.begin(), m.end(), channel, number, value);
}
beginTest ("createControllerUpdatesForTime ignores (N)RPNs after the final requested time");
{
const auto channel = 2;
const auto number = 123;
const auto value = 456;
MidiMessageSequence sequence;
addRpn (sequence, channel, number, value, 0.5);
addRpn (sequence, channel, 111, 222, 1.5);
addRpn (sequence, channel, 333, 444, 2.5);
Array<MidiMessage> m;
sequence.createControllerUpdatesForTime (channel, 1.0, m);
checkRpn (m.begin(), std::next (m.begin(), 4), channel, number, value, 0.5);
}
beginTest ("createControllerUpdatesForTime should emit separate (N)RPN messages when appropriate");
{
const auto channel = 2;
const auto numberA = 1111;
const auto valueA = 9999;
const auto numberB = 8888;
const auto valueB = 2222;
const auto numberC = 7777;
const auto valueC = 3333;
const auto numberD = 6666;
const auto valueD = 4444;
const auto time = 0.5;
MidiMessageSequence sequence;
addRpn (sequence, channel, numberA, valueA, time);
addRpn (sequence, channel, numberB, valueB, time);
addNrpn (sequence, channel, numberC, valueC, time);
addNrpn (sequence, channel, numberD, valueD, time);
Array<MidiMessage> m;
sequence.createControllerUpdatesForTime (channel, time * 2, m);
checkRpn (std::next (m.begin(), 0), std::next (m.begin(), 4), channel, numberA, valueA, time);
checkRpn (std::next (m.begin(), 4), std::next (m.begin(), 8), channel, numberB, valueB, time);
checkNrpn (std::next (m.begin(), 8), std::next (m.begin(), 12), channel, numberC, valueC, time);
checkNrpn (std::next (m.begin(), 12), std::next (m.begin(), 16), channel, numberD, valueD, time);
}
beginTest ("createControllerUpdatesForTime correctly emits (N)RPN messages on multiple channels");
{
struct Info { int channel, number, value; };
const Info infos[] { { 2, 1111, 9999 },
{ 8, 8888, 2222 },
{ 5, 7777, 3333 },
{ 1, 6666, 4444 } };
const auto time = 0.5;
MidiMessageSequence sequence;
for (const auto& info : infos)
addRpn (sequence, info.channel, info.number, info.value, time);
for (const auto& info : infos)
{
Array<MidiMessage> m;
sequence.createControllerUpdatesForTime (info.channel, time * 2, m);
checkRpn (std::next (m.begin(), 0), std::next (m.begin(), 4), info.channel, info.number, info.value, time);
}
}
const auto messagesAreEqual = [] (const MidiMessage& a, const MidiMessage& b)
{
return std::equal (a.getRawData(), a.getRawData() + a.getRawDataSize(),
b.getRawData(), b.getRawData() + b.getRawDataSize());
};
beginTest ("createControllerUpdatesForTime sends bank select messages when the next program is in a new bank");
{
MidiMessageSequence sequence;
const auto time = 0.0;
const auto channel = 1;
sequence.addEvent (MidiMessage::programChange (channel, 5), time);
sequence.addEvent (MidiMessage::controllerEvent (channel, 0x00, 128), time);
sequence.addEvent (MidiMessage::controllerEvent (channel, 0x20, 64), time);
sequence.addEvent (MidiMessage::programChange (channel, 63), time);
const Array<MidiMessage> finalEvents { MidiMessage::controllerEvent (channel, 0x00, 50),
MidiMessage::controllerEvent (channel, 0x20, 40),
MidiMessage::programChange (channel, 30) };
for (const auto& e : finalEvents)
sequence.addEvent (e);
Array<MidiMessage> m;
sequence.createControllerUpdatesForTime (channel, 1.0, m);
expect (std::equal (m.begin(), m.end(), finalEvents.begin(), finalEvents.end(), messagesAreEqual));
}
beginTest ("createControllerUpdatesForTime preserves all Data Increment and Data Decrement messages");
{
MidiMessageSequence sequence;
const auto time = 0.0;
const auto channel = 1;
const Array<MidiMessage> messages { MidiMessage::controllerEvent (channel, 0x60, 0),
MidiMessage::controllerEvent (channel, 0x06, 100),
MidiMessage::controllerEvent (channel, 0x26, 50),
MidiMessage::controllerEvent (channel, 0x60, 10),
MidiMessage::controllerEvent (channel, 0x61, 10),
MidiMessage::controllerEvent (channel, 0x06, 20),
MidiMessage::controllerEvent (channel, 0x26, 30),
MidiMessage::controllerEvent (channel, 0x61, 10),
MidiMessage::controllerEvent (channel, 0x61, 20) };
for (const auto& m : messages)
sequence.addEvent (m, time);
Array<MidiMessage> m;
sequence.createControllerUpdatesForTime (channel, 1.0, m);
expect (std::equal (m.begin(), m.end(), messages.begin(), messages.end(), messagesAreEqual));
}
beginTest ("createControllerUpdatesForTime does not emit redundant parameter number changes");
{
MidiMessageSequence sequence;
const auto time = 0.0;
const auto channel = 1;
const Array<MidiMessage> messages { MidiMessage::controllerEvent (channel, 0x65, 0),
MidiMessage::controllerEvent (channel, 0x64, 100),
MidiMessage::controllerEvent (channel, 0x63, 50),
MidiMessage::controllerEvent (channel, 0x62, 10),
MidiMessage::controllerEvent (channel, 0x06, 10) };
for (const auto& m : messages)
sequence.addEvent (m, time);
Array<MidiMessage> m;
sequence.createControllerUpdatesForTime (channel, 1.0, m);
const Array<MidiMessage> expected { MidiMessage::controllerEvent (channel, 0x63, 50),
MidiMessage::controllerEvent (channel, 0x62, 10),
MidiMessage::controllerEvent (channel, 0x06, 10) };
expect (std::equal (m.begin(), m.end(), expected.begin(), expected.end(), messagesAreEqual));
}
beginTest ("createControllerUpdatesForTime sets parameter number correctly at end of sequence");
{
MidiMessageSequence sequence;
const auto time = 0.0;
const auto channel = 1;
const Array<MidiMessage> messages { MidiMessage::controllerEvent (channel, 0x65, 0),
MidiMessage::controllerEvent (channel, 0x64, 100),
MidiMessage::controllerEvent (channel, 0x63, 50),
MidiMessage::controllerEvent (channel, 0x62, 10),
MidiMessage::controllerEvent (channel, 0x06, 10),
MidiMessage::controllerEvent (channel, 0x64, 5) };
for (const auto& m : messages)
sequence.addEvent (m, time);
const auto finalTime = 1.0;
Array<MidiMessage> m;
sequence.createControllerUpdatesForTime (channel, finalTime, m);
const Array<MidiMessage> expected { MidiMessage::controllerEvent (channel, 0x63, 50),
MidiMessage::controllerEvent (channel, 0x62, 10),
MidiMessage::controllerEvent (channel, 0x06, 10),
// Note: we should send both the MSB and LSB!
MidiMessage::controllerEvent (channel, 0x65, 0).withTimeStamp (finalTime),
MidiMessage::controllerEvent (channel, 0x64, 5).withTimeStamp (finalTime) };
expect (std::equal (m.begin(), m.end(), expected.begin(), expected.end(), messagesAreEqual));
}
beginTest ("createControllerUpdatesForTime does not emit duplicate parameter number change messages");
{
MidiMessageSequence sequence;
const auto time = 0.0;
const auto channel = 1;
const Array<MidiMessage> messages { MidiMessage::controllerEvent (channel, 0x65, 1),
MidiMessage::controllerEvent (channel, 0x64, 2),
MidiMessage::controllerEvent (channel, 0x63, 3),
MidiMessage::controllerEvent (channel, 0x62, 4),
MidiMessage::controllerEvent (channel, 0x06, 10),
MidiMessage::controllerEvent (channel, 0x63, 30),
MidiMessage::controllerEvent (channel, 0x62, 40),
MidiMessage::controllerEvent (channel, 0x63, 3),
MidiMessage::controllerEvent (channel, 0x62, 4),
MidiMessage::controllerEvent (channel, 0x60, 5),
MidiMessage::controllerEvent (channel, 0x65, 10) };
for (const auto& m : messages)
sequence.addEvent (m, time);
const auto finalTime = 1.0;
Array<MidiMessage> m;
sequence.createControllerUpdatesForTime (channel, finalTime, m);
const Array<MidiMessage> expected { MidiMessage::controllerEvent (channel, 0x63, 3),
MidiMessage::controllerEvent (channel, 0x62, 4),
MidiMessage::controllerEvent (channel, 0x06, 10),
// Parameter number is set to (30, 40) then back to (3, 4),
// so there is no need to resend it
MidiMessage::controllerEvent (channel, 0x60, 5),
// Set parameter number to final value
MidiMessage::controllerEvent (channel, 0x65, 10).withTimeStamp (finalTime),
MidiMessage::controllerEvent (channel, 0x64, 2) .withTimeStamp (finalTime) };
expect (std::equal (m.begin(), m.end(), expected.begin(), expected.end(), messagesAreEqual));
}
beginTest ("createControllerUpdatesForTime emits bank change messages immediately before program change");
{
MidiMessageSequence sequence;
const auto time = 0.0;
const auto channel = 1;
const Array<MidiMessage> messages { MidiMessage::controllerEvent (channel, 0x00, 1),
MidiMessage::controllerEvent (channel, 0x20, 2),
MidiMessage::controllerEvent (channel, 0x65, 0),
MidiMessage::controllerEvent (channel, 0x64, 0),
MidiMessage::programChange (channel, 5) };
for (const auto& m : messages)
sequence.addEvent (m, time);
const auto finalTime = 1.0;
Array<MidiMessage> m;
sequence.createControllerUpdatesForTime (channel, finalTime, m);
const Array<MidiMessage> expected { MidiMessage::controllerEvent (channel, 0x00, 1),
MidiMessage::controllerEvent (channel, 0x20, 2),
MidiMessage::programChange (channel, 5),
MidiMessage::controllerEvent (channel, 0x65, 0).withTimeStamp (finalTime),
MidiMessage::controllerEvent (channel, 0x64, 0).withTimeStamp (finalTime) };
expect (std::equal (m.begin(), m.end(), expected.begin(), expected.end(), messagesAreEqual));
}
}
};
static MidiMessageSequenceTest midiMessageSequenceTests;
#endif
} // namespace juce
@@ -0,0 +1,327 @@
/*
==============================================================================
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
{
//==============================================================================
/**
A sequence of timestamped midi messages.
This allows the sequence to be manipulated, and also to be read from and
written to a standard midi file.
@see MidiMessage, MidiFile
@tags{Audio}
*/
class JUCE_API MidiMessageSequence
{
public:
//==============================================================================
/** Creates an empty midi sequence object. */
MidiMessageSequence();
/** Creates a copy of another sequence. */
MidiMessageSequence (const MidiMessageSequence&);
/** Replaces this sequence with another one. */
MidiMessageSequence& operator= (const MidiMessageSequence&);
/** Move constructor */
MidiMessageSequence (MidiMessageSequence&&) noexcept;
/** Move assignment operator */
MidiMessageSequence& operator= (MidiMessageSequence&&) noexcept;
//==============================================================================
/** Structure used to hold midi events in the sequence.
These structures act as 'handles' on the events as they are moved about in
the list, and make it quick to find the matching note-offs for note-on events.
@see MidiMessageSequence::getEventPointer
*/
class MidiEventHolder
{
public:
//==============================================================================
/** The message itself, whose timestamp is used to specify the event's time. */
MidiMessage message;
/** The matching note-off event (if this is a note-on event).
If this isn't a note-on, this pointer will be nullptr.
Use the MidiMessageSequence::updateMatchedPairs() method to keep these
note-offs up-to-date after events have been moved around in the sequence
or deleted.
*/
MidiEventHolder* noteOffObject = nullptr;
private:
//==============================================================================
friend class MidiMessageSequence;
MidiEventHolder (const MidiMessage&);
MidiEventHolder (MidiMessage&&);
JUCE_LEAK_DETECTOR (MidiEventHolder)
};
//==============================================================================
/** Clears the sequence. */
void clear();
/** Returns the number of events in the sequence. */
int getNumEvents() const noexcept;
/** Returns a pointer to one of the events. */
MidiEventHolder* getEventPointer (int index) const noexcept;
/** Iterator for the list of MidiEventHolders */
MidiEventHolder** begin() noexcept;
/** Iterator for the list of MidiEventHolders */
MidiEventHolder* const* begin() const noexcept;
/** Iterator for the list of MidiEventHolders */
MidiEventHolder** end() noexcept;
/** Iterator for the list of MidiEventHolders */
MidiEventHolder* const* end() const noexcept;
/** Returns the time of the note-up that matches the note-on at this index.
If the event at this index isn't a note-on, it'll just return 0.
@see MidiMessageSequence::MidiEventHolder::noteOffObject
*/
double getTimeOfMatchingKeyUp (int index) const noexcept;
/** Returns the index of the note-up that matches the note-on at this index.
If the event at this index isn't a note-on, it'll just return -1.
@see MidiMessageSequence::MidiEventHolder::noteOffObject
*/
int getIndexOfMatchingKeyUp (int index) const noexcept;
/** Returns the index of an event. */
int getIndexOf (const MidiEventHolder* event) const noexcept;
/** Returns the index of the first event on or after the given timestamp.
If the time is beyond the end of the sequence, this will return the
number of events.
*/
int getNextIndexAtTime (double timeStamp) const noexcept;
//==============================================================================
/** Returns the timestamp of the first event in the sequence.
@see getEndTime
*/
double getStartTime() const noexcept;
/** Returns the timestamp of the last event in the sequence.
@see getStartTime
*/
double getEndTime() const noexcept;
/** Returns the timestamp of the event at a given index.
If the index is out-of-range, this will return 0.0
*/
double getEventTime (int index) const noexcept;
//==============================================================================
/** Inserts a midi message into the sequence.
The index at which the new message gets inserted will depend on its timestamp,
because the sequence is kept sorted.
Remember to call updateMatchedPairs() after adding note-on events.
@param newMessage the new message to add (an internal copy will be made)
@param timeAdjustment an optional value to add to the timestamp of the message
that will be inserted
@see updateMatchedPairs
*/
MidiEventHolder* addEvent (const MidiMessage& newMessage, double timeAdjustment = 0);
/** Inserts a midi message into the sequence.
The index at which the new message gets inserted will depend on its timestamp,
because the sequence is kept sorted.
Remember to call updateMatchedPairs() after adding note-on events.
@param newMessage the new message to add (an internal copy will be made)
@param timeAdjustment an optional value to add to the timestamp of the message
that will be inserted
@see updateMatchedPairs
*/
MidiEventHolder* addEvent (MidiMessage&& newMessage, double timeAdjustment = 0);
/** Deletes one of the events in the sequence.
Remember to call updateMatchedPairs() after removing events.
@param index the index of the event to delete
@param deleteMatchingNoteUp whether to also remove the matching note-off
if the event you're removing is a note-on
*/
void deleteEvent (int index, bool deleteMatchingNoteUp);
/** Merges another sequence into this one.
Remember to call updateMatchedPairs() after using this method.
@param other the sequence to add from
@param timeAdjustmentDelta an amount to add to the timestamps of the midi events
as they are read from the other sequence
@param firstAllowableDestTime events will not be added if their time is earlier
than this time. (This is after their time has been adjusted
by the timeAdjustmentDelta)
@param endOfAllowableDestTimes events will not be added if their time is equal to
or greater than this time. (This is after their time has
been adjusted by the timeAdjustmentDelta)
*/
void addSequence (const MidiMessageSequence& other,
double timeAdjustmentDelta,
double firstAllowableDestTime,
double endOfAllowableDestTimes);
/** Merges another sequence into this one.
Remember to call updateMatchedPairs() after using this method.
@param other the sequence to add from
@param timeAdjustmentDelta an amount to add to the timestamps of the midi events
as they are read from the other sequence
*/
void addSequence (const MidiMessageSequence& other,
double timeAdjustmentDelta);
//==============================================================================
/** Makes sure all the note-on and note-off pairs are up-to-date.
Call this after re-ordering messages or deleting/adding messages, and it
will scan the list and make sure all the note-offs in the MidiEventHolder
structures are pointing at the correct ones.
*/
void updateMatchedPairs() noexcept;
/** Forces a sort of the sequence.
You may need to call this if you've manually modified the timestamps of some
events such that the overall order now needs updating.
*/
void sort() noexcept;
//==============================================================================
/** Copies all the messages for a particular midi channel to another sequence.
@param channelNumberToExtract the midi channel to look for, in the range 1 to 16
@param destSequence the sequence that the chosen events should be copied to
@param alsoIncludeMetaEvents if true, any meta-events (which don't apply to a specific
channel) will also be copied across.
@see extractSysExMessages
*/
void extractMidiChannelMessages (int channelNumberToExtract,
MidiMessageSequence& destSequence,
bool alsoIncludeMetaEvents) const;
/** Copies all midi sys-ex messages to another sequence.
@param destSequence this is the sequence to which any sys-exes in this sequence
will be added
@see extractMidiChannelMessages
*/
void extractSysExMessages (MidiMessageSequence& destSequence) const;
/** Removes any messages in this sequence that have a specific midi channel.
@param channelNumberToRemove the midi channel to look for, in the range 1 to 16
*/
void deleteMidiChannelMessages (int channelNumberToRemove);
/** Removes any sys-ex messages from this sequence. */
void deleteSysExMessages();
/** Adds an offset to the timestamps of all events in the sequence.
@param deltaTime the amount to add to each timestamp.
*/
void addTimeToMessages (double deltaTime) noexcept;
//==============================================================================
/** Scans through the sequence to determine the state of any midi controllers at
a given time.
This will create a sequence of midi controller changes that can be
used to set all midi controllers to the state they would be in at the
specified time within this sequence.
As well as controllers, it will also recreate the midi program number
and pitch bend position.
This function has special handling for the "bank select" and "data entry"
controllers (0x00, 0x20, 0x06, 0x26, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65).
If the sequence contains multiple bank select and program change messages,
only the bank select messages immediately preceding the final program change
message will be kept.
All "data increment" and "data decrement" messages will be retained. Some hardware will
ignore the requested increment/decrement values, so retaining all messages is the only
way to ensure compatibility with all hardware.
"Parameter number" changes will be slightly condensed. Only the parameter number
events immediately preceding each data entry event will be kept. The parameter number
will also be set to its final value at the end of the sequence, if necessary.
@param channelNumber the midi channel to look for, in the range 1 to 16. Controllers
for other channels will be ignored.
@param time the time at which you want to find out the state - there are
no explicit units for this time measurement, it's the same units
as used for the timestamps of the messages
@param resultMessages an array to which midi controller-change messages will be added. This
will be the minimum number of controller changes to recreate the
state at the required time.
*/
void createControllerUpdatesForTime (int channelNumber, double time,
Array<MidiMessage>& resultMessages);
//==============================================================================
/** Swaps this sequence with another one. */
void swapWith (MidiMessageSequence&) noexcept;
private:
//==============================================================================
friend class MidiFile;
OwnedArray<MidiEventHolder> list;
MidiEventHolder* addEvent (MidiEventHolder*, double);
JUCE_LEAK_DETECTOR (MidiMessageSequence)
};
} // namespace juce
@@ -0,0 +1,447 @@
/*
==============================================================================
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
{
bool MidiRPNDetector::parseControllerMessage (int midiChannel,
int controllerNumber,
int controllerValue,
MidiRPNMessage& result) noexcept
{
auto parsed = tryParse (midiChannel, controllerNumber, controllerValue);
if (! parsed.has_value())
return false;
result = *parsed;
return true;
}
std::optional<MidiRPNMessage> MidiRPNDetector::tryParse (int midiChannel,
int controllerNumber,
int controllerValue)
{
jassert (midiChannel > 0 && midiChannel <= 16);
jassert (controllerNumber >= 0 && controllerNumber < 128);
jassert (controllerValue >= 0 && controllerValue < 128);
return states[midiChannel - 1].handleController (midiChannel, controllerNumber, controllerValue);
}
void MidiRPNDetector::reset() noexcept
{
for (auto& state : states)
{
state.parameterMSB = 0xff;
state.parameterLSB = 0xff;
state.resetValue();
state.isNRPN = false;
}
}
//==============================================================================
std::optional<MidiRPNMessage> MidiRPNDetector::ChannelState::handleController (int channel,
int controllerNumber,
int value) noexcept
{
switch (controllerNumber)
{
case 0x62: parameterLSB = uint8 (value); resetValue(); isNRPN = true; break;
case 0x63: parameterMSB = uint8 (value); resetValue(); isNRPN = true; break;
case 0x64: parameterLSB = uint8 (value); resetValue(); isNRPN = false; break;
case 0x65: parameterMSB = uint8 (value); resetValue(); isNRPN = false; break;
case 0x06: valueMSB = uint8 (value); valueLSB = 0xff; return sendIfReady (channel);
case 0x26: valueLSB = uint8 (value); return sendIfReady (channel);
}
return {};
}
void MidiRPNDetector::ChannelState::resetValue() noexcept
{
valueMSB = 0xff;
valueLSB = 0xff;
}
//==============================================================================
std::optional<MidiRPNMessage> MidiRPNDetector::ChannelState::sendIfReady (int channel) noexcept
{
if (parameterMSB >= 0x80 || parameterLSB >= 0x80 || valueMSB >= 0x80)
return {};
MidiRPNMessage result{};
result.channel = channel;
result.parameterNumber = (parameterMSB << 7) + parameterLSB;
result.isNRPN = isNRPN;
if (valueLSB < 0x80)
{
result.value = (valueMSB << 7) + valueLSB;
result.is14BitValue = true;
}
else
{
result.value = valueMSB;
result.is14BitValue = false;
}
return result;
}
//==============================================================================
MidiBuffer MidiRPNGenerator::generate (MidiRPNMessage message)
{
return generate (message.channel,
message.parameterNumber,
message.value,
message.isNRPN,
message.is14BitValue);
}
MidiBuffer MidiRPNGenerator::generate (int midiChannel,
int parameterNumber,
int value,
bool isNRPN,
bool use14BitValue)
{
jassert (midiChannel > 0 && midiChannel <= 16);
jassert (parameterNumber >= 0 && parameterNumber < 16384);
jassert (value >= 0 && value < (use14BitValue ? 16384 : 128));
auto parameterLSB = uint8 (parameterNumber & 0x0000007f);
auto parameterMSB = uint8 (parameterNumber >> 7);
uint8 valueLSB = use14BitValue ? uint8 (value & 0x0000007f) : 0x00;
uint8 valueMSB = use14BitValue ? uint8 (value >> 7) : uint8 (value);
auto channelByte = uint8 (0xb0 + midiChannel - 1);
MidiBuffer buffer;
buffer.addEvent (MidiMessage (channelByte, isNRPN ? 0x62 : 0x64, parameterLSB), 0);
buffer.addEvent (MidiMessage (channelByte, isNRPN ? 0x63 : 0x65, parameterMSB), 0);
buffer.addEvent (MidiMessage (channelByte, 0x06, valueMSB), 0);
// According to the MIDI spec, whenever a MSB is received, the corresponding LSB will
// be reset. Therefore, the LSB should be sent after the MSB.
if (use14BitValue)
buffer.addEvent (MidiMessage (channelByte, 0x26, valueLSB), 0);
return buffer;
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
class MidiRPNDetectorTests final : public UnitTest
{
public:
MidiRPNDetectorTests()
: UnitTest ("MidiRPNDetector class", UnitTestCategories::midi)
{}
void runTest() override
{
// From the MIDI 1.0 spec:
// If 128 steps of resolution is sufficient the second byte (LSB) of the data value can be
// omitted. If both the MSB and LSB are sent initially, a subsequent fine adjustment only
// requires the sending of the LSB. The MSB does not have to be retransmitted. If a
// subsequent major adjustment is necessary the MSB must be transmitted again. When an MSB
// is received, the receiver should set its concept of the LSB to zero.
beginTest ("Individual MSB is parsed as 7-bit");
{
MidiRPNDetector detector;
expect (! detector.tryParse (2, 101, 0));
expect (! detector.tryParse (2, 100, 7));
auto parsed = detector.tryParse (2, 6, 42);
expect (parsed.has_value());
expectEquals (parsed->channel, 2);
expectEquals (parsed->parameterNumber, 7);
expectEquals (parsed->value, 42);
expect (! parsed->isNRPN);
expect (! parsed->is14BitValue);
}
beginTest ("LSB without preceding MSB is ignored");
{
MidiRPNDetector detector;
expect (! detector.tryParse (2, 101, 0));
expect (! detector.tryParse (2, 100, 7));
expect (! detector.tryParse (2, 38, 42));
}
beginTest ("LSB following MSB is parsed as 14-bit");
{
MidiRPNDetector detector;
expect (! detector.tryParse (1, 101, 2));
expect (! detector.tryParse (1, 100, 44));
expect (detector.tryParse (1, 6, 1).has_value());
auto lsbParsed = detector.tryParse (1, 38, 94);
expect (lsbParsed.has_value());
expectEquals (lsbParsed->channel, 1);
expectEquals (lsbParsed->parameterNumber, 300);
expectEquals (lsbParsed->value, 222);
expect (! lsbParsed->isNRPN);
expect (lsbParsed->is14BitValue);
}
beginTest ("Multiple LSB following MSB re-use the MSB");
{
MidiRPNDetector detector;
expect (! detector.tryParse (1, 101, 2));
expect (! detector.tryParse (1, 100, 43));
expect (detector.tryParse (1, 6, 1).has_value());
expect (detector.tryParse (1, 38, 94).has_value());
expect (detector.tryParse (1, 38, 95).has_value());
expect (detector.tryParse (1, 38, 96).has_value());
auto lsbParsed = detector.tryParse (1, 38, 97);
expect (lsbParsed.has_value());
expectEquals (lsbParsed->channel, 1);
expectEquals (lsbParsed->parameterNumber, 299);
expectEquals (lsbParsed->value, 225);
expect (! lsbParsed->isNRPN);
expect (lsbParsed->is14BitValue);
}
beginTest ("Sending a new MSB resets the LSB");
{
MidiRPNDetector detector;
expect (! detector.tryParse (1, 101, 3));
expect (! detector.tryParse (1, 100, 43));
expect (detector.tryParse (1, 6, 1).has_value());
expect (detector.tryParse (1, 38, 94).has_value());
auto newMsb = detector.tryParse (1, 6, 2);
expect (newMsb.has_value());
expectEquals (newMsb->channel, 1);
expectEquals (newMsb->parameterNumber, 427);
expectEquals (newMsb->value, 2);
expect (! newMsb->isNRPN);
expect (! newMsb->is14BitValue);
}
beginTest ("RPNs on multiple channels simultaneously");
{
MidiRPNDetector detector;
expect (! detector.tryParse (1, 100, 44));
expect (! detector.tryParse (2, 101, 0));
expect (! detector.tryParse (1, 101, 2));
expect (! detector.tryParse (2, 100, 7));
expect (detector.tryParse (1, 6, 1).has_value());
auto channelTwo = detector.tryParse (2, 6, 42);
expect (channelTwo.has_value());
expectEquals (channelTwo->channel, 2);
expectEquals (channelTwo->parameterNumber, 7);
expectEquals (channelTwo->value, 42);
expect (! channelTwo->isNRPN);
expect (! channelTwo->is14BitValue);
auto channelOne = detector.tryParse (1, 38, 94);
expect (channelOne.has_value());
expectEquals (channelOne->channel, 1);
expectEquals (channelOne->parameterNumber, 300);
expectEquals (channelOne->value, 222);
expect (! channelOne->isNRPN);
expect (channelOne->is14BitValue);
}
beginTest ("14-bit RPN with value within 7-bit range");
{
MidiRPNDetector detector;
expect (! detector.tryParse (16, 100, 0));
expect (! detector.tryParse (16, 101, 0));
expect (detector.tryParse (16, 6, 0).has_value());
auto parsed = detector.tryParse (16, 38, 3);
expect (parsed.has_value());
expectEquals (parsed->channel, 16);
expectEquals (parsed->parameterNumber, 0);
expectEquals (parsed->value, 3);
expect (! parsed->isNRPN);
expect (parsed->is14BitValue);
}
beginTest ("invalid RPN (wrong order)");
{
MidiRPNDetector detector;
expect (! detector.tryParse (2, 6, 42));
expect (! detector.tryParse (2, 101, 0));
expect (! detector.tryParse (2, 100, 7));
}
beginTest ("14-bit RPN interspersed with unrelated CC messages");
{
MidiRPNDetector detector;
expect (! detector.tryParse (16, 3, 80));
expect (! detector.tryParse (16, 100, 0));
expect (! detector.tryParse (16, 4, 81));
expect (! detector.tryParse (16, 101, 0));
expect (! detector.tryParse (16, 5, 82));
expect (! detector.tryParse (16, 5, 83));
expect (detector.tryParse (16, 6, 0).has_value());
expect (! detector.tryParse (16, 4, 84).has_value());
expect (! detector.tryParse (16, 3, 85).has_value());
auto parsed = detector.tryParse (16, 38, 3);
expect (parsed.has_value());
expectEquals (parsed->channel, 16);
expectEquals (parsed->parameterNumber, 0);
expectEquals (parsed->value, 3);
expect (! parsed->isNRPN);
expect (parsed->is14BitValue);
}
beginTest ("14-bit NRPN");
{
MidiRPNDetector detector;
expect (! detector.tryParse (1, 98, 44));
expect (! detector.tryParse (1, 99 , 2));
expect (detector.tryParse (1, 6, 1).has_value());
auto parsed = detector.tryParse (1, 38, 94);
expect (parsed.has_value());
expectEquals (parsed->channel, 1);
expectEquals (parsed->parameterNumber, 300);
expectEquals (parsed->value, 222);
expect (parsed->isNRPN);
expect (parsed->is14BitValue);
}
beginTest ("reset");
{
MidiRPNDetector detector;
expect (! detector.tryParse (2, 101, 0));
detector.reset();
expect (! detector.tryParse (2, 100, 7));
expect (! detector.tryParse (2, 6, 42));
}
}
};
static MidiRPNDetectorTests MidiRPNDetectorUnitTests;
//==============================================================================
class MidiRPNGeneratorTests final : public UnitTest
{
public:
MidiRPNGeneratorTests()
: UnitTest ("MidiRPNGenerator class", UnitTestCategories::midi)
{}
void runTest() override
{
beginTest ("generating RPN/NRPN");
{
{
MidiBuffer buffer = MidiRPNGenerator::generate (1, 23, 1337, true, true);
expectContainsRPN (buffer, 1, 23, 1337, true, true);
}
{
MidiBuffer buffer = MidiRPNGenerator::generate (16, 101, 34, false, false);
expectContainsRPN (buffer, 16, 101, 34, false, false);
}
{
MidiRPNMessage message = { 16, 101, 34, false, false };
MidiBuffer buffer = MidiRPNGenerator::generate (message);
expectContainsRPN (buffer, message);
}
}
}
private:
//==============================================================================
void expectContainsRPN (const MidiBuffer& midiBuffer,
int channel,
int parameterNumber,
int value,
bool isNRPN,
bool is14BitValue)
{
MidiRPNMessage expected = { channel, parameterNumber, value, isNRPN, is14BitValue };
expectContainsRPN (midiBuffer, expected);
}
//==============================================================================
void expectContainsRPN (const MidiBuffer& midiBuffer, MidiRPNMessage expected)
{
std::optional<MidiRPNMessage> result;
MidiRPNDetector detector;
for (const auto metadata : midiBuffer)
{
const auto midiMessage = metadata.getMessage();
result = detector.tryParse (midiMessage.getChannel(),
midiMessage.getControllerNumber(),
midiMessage.getControllerValue());
}
expect (result.has_value());
expectEquals (result->channel, expected.channel);
expectEquals (result->parameterNumber, expected.parameterNumber);
expectEquals (result->value, expected.value);
expect (result->isNRPN == expected.isNRPN);
expect (result->is14BitValue == expected.is14BitValue);
}
};
static MidiRPNGeneratorTests MidiRPNGeneratorUnitTests;
#endif
} // namespace juce
@@ -0,0 +1,178 @@
/*
==============================================================================
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
{
//==============================================================================
/** Represents a MIDI RPN (registered parameter number) or NRPN (non-registered
parameter number) message.
@tags{Audio}
*/
struct MidiRPNMessage
{
/** Midi channel of the message, in the range 1 to 16. */
int channel;
/** The 14-bit parameter index, in the range 0 to 16383 (0x3fff). */
int parameterNumber;
/** The parameter value, in the range 0 to 16383 (0x3fff).
If the message contains no value LSB, the value will be in the range
0 to 127 (0x7f).
*/
int value;
/** True if this message is an NRPN; false if it is an RPN. */
bool isNRPN;
/** True if the value uses 14-bit resolution (LSB + MSB); false if
the value is 7-bit (MSB only).
*/
bool is14BitValue;
};
//==============================================================================
/**
Parses a stream of MIDI data to assemble RPN and NRPN messages from their
constituent MIDI CC messages.
The detector uses the following parsing rules: the parameter number
LSB/MSB can be sent/received in either order and must both come before the
parameter value; for the parameter value, LSB always has to be sent/received
before the value MSB, otherwise it will be treated as 7-bit (MSB only).
@tags{Audio}
*/
class JUCE_API MidiRPNDetector
{
public:
/** Constructor. */
MidiRPNDetector() noexcept = default;
/** Destructor. */
~MidiRPNDetector() noexcept = default;
/** Resets the RPN detector's internal state, so that it forgets about
previously received MIDI CC messages.
*/
void reset() noexcept;
//==============================================================================
/** @see tryParse() */
[[deprecated ("Use tryParse() instead")]]
bool parseControllerMessage (int midiChannel,
int controllerNumber,
int controllerValue,
MidiRPNMessage& result) noexcept;
/** Takes the next in a stream of incoming MIDI CC messages and returns
a MidiRPNMessage if the current message produces a well-formed RPN or NRPN.
Note that senders are expected to send the MSB before the LSB, but senders are
not required to send a LSB at all. Therefore, tryParse() will return a non-null
optional on all MSB messages (provided a parameter number has been set), and will
also return a non-null optional for each LSB that follows the initial MSB.
This behaviour allows senders to transmit a single MSB followed by multiple LSB
messages to facilitate fine-tuning of parameters.
The result of parsing a MSB will always be a 7-bit value.
The result of parsing a LSB that follows an MSB will always be a 14-bit value.
*/
std::optional<MidiRPNMessage> tryParse (int midiChannel,
int controllerNumber,
int controllerValue);
private:
//==============================================================================
struct ChannelState
{
std::optional<MidiRPNMessage> handleController (int channel, int controllerNumber,
int value) noexcept;
void resetValue() noexcept;
std::optional<MidiRPNMessage> sendIfReady (int channel) noexcept;
uint8 parameterMSB = 0xff, parameterLSB = 0xff, valueMSB = 0xff, valueLSB = 0xff;
bool isNRPN = false;
};
//==============================================================================
ChannelState states[16];
JUCE_LEAK_DETECTOR (MidiRPNDetector)
};
//==============================================================================
/**
Generates an appropriate sequence of MIDI CC messages to represent an RPN
or NRPN message.
This sequence (as a MidiBuffer) can then be directly sent to a MidiOutput.
@tags{Audio}
*/
class JUCE_API MidiRPNGenerator
{
public:
//==============================================================================
/** Generates a MIDI sequence representing the given RPN or NRPN message. */
static MidiBuffer generate (MidiRPNMessage message);
//==============================================================================
/** Generates a MIDI sequence representing an RPN or NRPN message with the
given parameters.
@param channel The MIDI channel of the RPN/NRPN message.
@param parameterNumber The parameter number, in the range 0 to 16383.
@param value The parameter value, in the range 0 to 16383, or
in the range 0 to 127 if sendAs14BitValue is false.
@param isNRPN Whether you need a MIDI RPN or NRPN sequence (RPN is default).
@param use14BitValue If true (default), the value will have 14-bit precision
(two MIDI bytes). If false, instead the value will have
7-bit precision (a single MIDI byte).
*/
static MidiBuffer generate (int channel,
int parameterNumber,
int value,
bool isNRPN = false,
bool use14BitValue = true);
};
} // namespace juce
@@ -0,0 +1,57 @@
/*
==============================================================================
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.
==============================================================================
*/
#include "juce_UMPProtocols.h"
#include "juce_UMPUtils.h"
#include "juce_UMPacket.h"
#include "juce_UMPSysEx7.h"
#include "juce_UMPView.h"
#include "juce_UMPIterator.h"
#include "juce_UMPackets.h"
#include "juce_UMPFactory.h"
#include "juce_UMPConversion.h"
#include "juce_UMPMidi1ToBytestreamTranslator.h"
#include "juce_UMPMidi1ToMidi2DefaultTranslator.h"
#include "juce_UMPConverters.h"
#include "juce_UMPDispatcher.h"
#include "juce_UMPReceiver.h"
#ifndef DOXYGEN
namespace juce
{
namespace ump = universal_midi_packets;
}
#endif
@@ -0,0 +1,50 @@
/*
==============================================================================
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::universal_midi_packets
{
/**
Holds a UMP group, and a span of bytes that were received or are to be
sent on that group. Helpful when working with sysex messages.
@tags{Audio}
*/
struct BytesOnGroup
{
uint8_t group{};
Span<const std::byte> bytes;
};
} // namespace juce::universal_midi_packets
@@ -0,0 +1,367 @@
/*
==============================================================================
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.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce::universal_midi_packets
{
/** Represents a MIDI message that happened at a particular time.
Unlike MidiMessage, BytestreamMidiView is non-owning.
*/
struct BytestreamMidiView
{
constexpr BytestreamMidiView (Span<const std::byte> bytesIn, double timestampIn)
: bytes (bytesIn), timestamp (timestampIn) {}
/** Creates a view over the provided message.
Note that the argument is a pointer, not a reference, in order to avoid taking a reference
to a temporary.
*/
explicit BytestreamMidiView (const MidiMessage* msg)
: bytes (unalignedPointerCast<const std::byte*> (msg->getRawData()),
static_cast<size_t> (msg->getRawDataSize())),
timestamp (msg->getTimeStamp()) {}
explicit BytestreamMidiView (const MidiMessageMetadata msg)
: bytes (unalignedPointerCast<const std::byte*> (msg.data),
static_cast<size_t> (msg.numBytes)),
timestamp (msg.samplePosition) {}
MidiMessage getMessage() const
{
return MidiMessage (bytes.data(), (int) bytes.size(), timestamp);
}
bool isSysEx() const
{
return ! bytes.empty() && bytes.front() == std::byte { 0xf0 };
}
Span<const std::byte> bytes;
double timestamp = 0.0;
};
/**
Functions to assist conversion of UMP messages to/from other formats,
especially older 'bytestream' formatted MidiMessages.
@tags{Audio}
*/
struct Conversion
{
/** Converts from a MIDI 1 bytestream to MIDI 1 on Universal MIDI Packets.
`callback` is a function which accepts a single View argument.
*/
template <typename PacketCallbackFunction>
static void toMidi1 (const BytestreamMidiView& m, PacketCallbackFunction&& callback)
{
const auto size = m.bytes.size();
if (size <= 0)
return;
const auto* data = m.bytes.data();
const auto firstByte = data[0];
if (firstByte != std::byte { 0xf0 })
{
const auto mask = [size]() -> uint32_t
{
switch (size)
{
case 0: return 0xff000000;
case 1: return 0xffff0000;
case 2: return 0xffffff00;
case 3: return 0xffffffff;
}
return 0x00000000;
}();
const auto extraByte = ((((firstByte & std::byte { 0xf0 }) == std::byte { 0xf0 }) ? std::byte { 0x1 } : std::byte { 0x2 }) << 0x4);
const PacketX1 packet { mask & Utils::bytesToWord (extraByte, data[0], data[1], data[2]) };
callback (View (packet.data()));
return;
}
const auto numSysExBytes = (ssize_t) (size - 2);
const auto numMessages = SysEx7::getNumPacketsRequiredForDataSize ((uint32_t) numSysExBytes);
auto* dataOffset = data + 1;
if (numMessages <= 1)
{
const auto packet = Factory::makeSysExIn1Packet (0, (uint8_t) numSysExBytes, dataOffset);
callback (View (packet.data()));
return;
}
constexpr ssize_t byteIncrement = 6;
for (auto i = static_cast<ssize_t> (numSysExBytes); i > 0; i -= byteIncrement, dataOffset += byteIncrement)
{
const auto func = [&]
{
if (i == numSysExBytes)
return Factory::makeSysExStart;
if (i <= byteIncrement)
return Factory::makeSysExEnd;
return Factory::makeSysExContinue;
}();
const auto bytesNow = std::min (byteIncrement, i);
const auto packet = func (0, (uint8_t) bytesNow, dataOffset);
callback (View (packet.data()));
}
}
/** Widens a 7-bit MIDI 1.0 value to a 8-bit MIDI 2.0 value. */
static uint8_t scaleTo8 (uint8_t word7Bit)
{
const auto shifted = (uint8_t) (word7Bit << 0x1);
const auto repeat = (uint8_t) (word7Bit & 0x3f);
const auto mask = (uint8_t) (word7Bit <= 0x40 ? 0x0 : 0xff);
return (uint8_t) (shifted | ((repeat >> 5) & mask));
}
/** Widens a 7-bit MIDI 1.0 value to a 16-bit MIDI 2.0 value. */
static uint16_t scaleTo16 (uint8_t word7Bit)
{
const auto shifted = (uint16_t) (word7Bit << 0x9);
const auto repeat = (uint16_t) (word7Bit & 0x3f);
const auto mask = (uint16_t) (word7Bit <= 0x40 ? 0x0 : 0xffff);
return (uint16_t) (shifted | (((repeat << 3) | (repeat >> 3)) & mask));
}
/** Widens a 14-bit MIDI 1.0 value to a 16-bit MIDI 2.0 value. */
static uint16_t scaleTo16 (uint16_t word14Bit)
{
const auto shifted = (uint16_t) (word14Bit << 0x2);
const auto repeat = (uint16_t) (word14Bit & 0x1fff);
const auto mask = (uint16_t) (word14Bit <= 0x2000 ? 0x0 : 0xffff);
return (uint16_t) (shifted | ((repeat >> 11) & mask));
}
/** Widens a 7-bit MIDI 1.0 value to a 32-bit MIDI 2.0 value. */
static uint32_t scaleTo32 (uint8_t word7Bit)
{
const auto shifted = (uint32_t) (word7Bit << 0x19);
const auto repeat = (uint32_t) (word7Bit & 0x3f);
const auto mask = (uint32_t) (word7Bit <= 0x40 ? 0x0 : 0xffffffff);
return (uint32_t) (shifted | (((repeat << 19)
| (repeat << 13)
| (repeat << 7)
| (repeat << 1)
| (repeat >> 5)) & mask));
}
/** Widens a 14-bit MIDI 1.0 value to a 32-bit MIDI 2.0 value. */
static uint32_t scaleTo32 (uint16_t word14Bit)
{
const auto shifted = (uint32_t) (word14Bit << 0x12);
const auto repeat = (uint32_t) (word14Bit & 0x1fff);
const auto mask = (uint32_t) (word14Bit <= 0x2000 ? 0x0 : 0xffffffff);
return (uint32_t) (shifted | (((repeat << 5) | (repeat >> 8)) & mask));
}
/** Narrows a 16-bit MIDI 2.0 value to a 7-bit MIDI 1.0 value. */
static uint8_t scaleTo7 (uint8_t word8Bit) { return (uint8_t) (word8Bit >> 1); }
/** Narrows a 16-bit MIDI 2.0 value to a 7-bit MIDI 1.0 value. */
static uint8_t scaleTo7 (uint16_t word16Bit) { return (uint8_t) (word16Bit >> 9); }
/** Narrows a 32-bit MIDI 2.0 value to a 7-bit MIDI 1.0 value. */
static uint8_t scaleTo7 (uint32_t word32Bit) { return (uint8_t) (word32Bit >> 25); }
/** Narrows a 32-bit MIDI 2.0 value to a 14-bit MIDI 1.0 value. */
static uint16_t scaleTo14 (uint16_t word16Bit) { return (uint16_t) (word16Bit >> 2); }
/** Narrows a 32-bit MIDI 2.0 value to a 14-bit MIDI 1.0 value. */
static uint16_t scaleTo14 (uint32_t word32Bit) { return (uint16_t) (word32Bit >> 18); }
/** Converts UMP messages which may include MIDI 2.0 channel voice messages into
equivalent MIDI 1.0 messages (still in UMP format).
`callback` is a function that accepts a single View argument and will be
called with each converted packet.
Note that not all MIDI 2.0 messages have MIDI 1.0 equivalents, so such
messages will be ignored.
*/
template <typename Callback>
static void midi2ToMidi1DefaultTranslation (const View& v, Callback&& callback)
{
const auto firstWord = v[0];
if (Utils::getMessageType (firstWord) != 0x4)
{
callback (v);
return;
}
const auto status = Utils::getStatus (firstWord);
const auto typeAndGroup = ((std::byte { 0x2 } << 0x4) | std::byte { Utils::getGroup (firstWord) });
switch (status)
{
case 0x8: // note off
case 0x9: // note on
case 0xa: // poly pressure
case 0xb: // control change
{
const auto statusAndChannel = std::byte ((firstWord >> 0x10) & 0xff);
const auto byte2 = std::byte ((firstWord >> 0x08) & 0xff);
const auto byte3 = std::byte { scaleTo7 (v[1]) };
// If this is a note-on, and the scaled byte is 0,
// the scaled velocity should be 1 instead of 0
const auto needsCorrection = status == 0x9 && byte3 == std::byte { 0 };
const auto correctedByte = needsCorrection ? std::byte { 1 } : byte3;
const auto shouldIgnore = status == 0xb && [&]
{
switch (uint8_t (byte2))
{
case 0:
case 6:
case 32:
case 38:
case 98:
case 99:
case 100:
case 101:
return true;
}
return false;
}();
if (shouldIgnore)
return;
const PacketX1 packet { Utils::bytesToWord (typeAndGroup,
statusAndChannel,
byte2,
correctedByte) };
callback (View (packet.data()));
return;
}
case 0xd: // channel pressure
{
const auto statusAndChannel = std::byte ((firstWord >> 0x10) & 0xff);
const auto byte2 = std::byte { scaleTo7 (v[1]) };
const PacketX1 packet { Utils::bytesToWord (typeAndGroup,
statusAndChannel,
byte2,
std::byte { 0 }) };
callback (View (packet.data()));
return;
}
case 0x2: // rpn
case 0x3: // nrpn
{
const auto ccX = status == 0x2 ? std::byte { 101 } : std::byte { 99 };
const auto ccY = status == 0x2 ? std::byte { 100 } : std::byte { 98 };
const auto statusAndChannel = std::byte ((0xb << 0x4) | Utils::getChannel (firstWord));
const auto data = scaleTo14 (v[1]);
const PacketX1 packets[]
{
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, ccX, std::byte ((firstWord >> 0x8) & 0x7f)) },
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, ccY, std::byte ((firstWord >> 0x0) & 0x7f)) },
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, std::byte { 6 }, std::byte ((data >> 0x7) & 0x7f)) },
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, std::byte { 38 }, std::byte ((data >> 0x0) & 0x7f)) },
};
for (const auto& packet : packets)
callback (View (packet.data()));
return;
}
case 0xc: // program change / bank select
{
if (firstWord & 1)
{
const auto statusAndChannel = std::byte ((0xb << 0x4) | Utils::getChannel (firstWord));
const auto secondWord = v[1];
const PacketX1 packets[]
{
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, std::byte { 0 }, std::byte ((secondWord >> 0x8) & 0x7f)) },
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, std::byte { 32 }, std::byte ((secondWord >> 0x0) & 0x7f)) },
};
for (const auto& packet : packets)
callback (View (packet.data()));
}
const auto statusAndChannel = std::byte ((0xc << 0x4) | Utils::getChannel (firstWord));
const PacketX1 packet { Utils::bytesToWord (typeAndGroup,
statusAndChannel,
std::byte ((v[1] >> 0x18) & 0x7f),
std::byte { 0 }) };
callback (View (packet.data()));
return;
}
case 0xe: // pitch bend
{
const auto data = scaleTo14 (v[1]);
const auto statusAndChannel = std::byte ((firstWord >> 0x10) & 0xff);
const PacketX1 packet { Utils::bytesToWord (typeAndGroup,
statusAndChannel,
std::byte (data & 0x7f),
std::byte ((data >> 7) & 0x7f)) };
callback (View (packet.data()));
return;
}
default: // other message types do not translate
return;
}
}
};
} // namespace juce::universal_midi_packets
#endif
@@ -0,0 +1,197 @@
/*
==============================================================================
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.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce::universal_midi_packets
{
/**
Allows conversion from bytestream- or Universal MIDI Packet-formatted
messages to MIDI 1.0 messages in UMP format.
@tags{Audio}
*/
struct ToUMP1Converter
{
template <typename Fn>
void convert (const BytestreamMidiView& m, Fn&& fn)
{
Conversion::toMidi1 (m, std::forward<Fn> (fn));
}
template <typename Fn>
void convert (const View& v, Fn&& fn)
{
Conversion::midi2ToMidi1DefaultTranslation (v, std::forward<Fn> (fn));
}
};
/**
Allows conversion from bytestream- or Universal MIDI Packet-formatted
messages to MIDI 2.0 messages in UMP format.
@tags{Audio}
*/
struct ToUMP2Converter
{
template <typename Fn>
void convert (const BytestreamMidiView& m, Fn&& fn)
{
Conversion::toMidi1 (m, [&] (const View& v)
{
translator.dispatch (v, fn);
});
}
template <typename Fn>
void convert (const View& v, Fn&& fn)
{
translator.dispatch (v, std::forward<Fn> (fn));
}
void reset()
{
translator.reset();
}
Midi1ToMidi2DefaultTranslator translator;
};
/**
Allows conversion from bytestream- or Universal MIDI Packet-formatted
messages to UMP format.
The packet protocol can be selected using the constructor parameter.
@tags{Audio}
*/
class GenericUMPConverter
{
template <typename This, typename... Args>
static void visit (This& t, Args&&... args)
{
if (t.mode == PacketProtocol::MIDI_1_0)
convertImpl (std::get<0> (t.converters), std::forward<Args> (args)...);
else
convertImpl (std::get<1> (t.converters), std::forward<Args> (args)...);
}
public:
explicit GenericUMPConverter (PacketProtocol m)
: mode (m) {}
void reset()
{
std::get<1> (converters).reset();
}
template <typename Converter, typename Fn>
static void convertImpl (Converter& converter, const BytestreamMidiView& m, Fn&& fn)
{
converter.convert (m, std::forward<Fn> (fn));
}
template <typename Converter, typename Fn>
static void convertImpl (Converter& converter, const View& m, Fn&& fn)
{
converter.convert (m, std::forward<Fn> (fn));
}
template <typename Converter, typename Fn>
static void convertImpl (Converter& converter, Iterator b, Iterator e, Fn&& fn)
{
std::for_each (b, e, [&] (const auto& v)
{
convertImpl (converter, v, fn);
});
}
template <typename Fn>
void convert (const BytestreamMidiView& m, Fn&& fn)
{
visit (*this, m, std::forward<Fn> (fn));
}
template <typename Fn>
void convert (const View& v, Fn&& fn)
{
visit (*this, v, std::forward<Fn> (fn));
}
template <typename Fn>
void convert (Iterator begin, Iterator end, Fn&& fn)
{
visit (*this, begin, end, std::forward<Fn> (fn));
}
PacketProtocol getProtocol() const noexcept { return mode; }
private:
std::tuple<ToUMP1Converter, ToUMP2Converter> converters;
const PacketProtocol mode{};
};
/**
Allows conversion from bytestream- or Universal MIDI Packet-formatted
messages to bytestream format.
@tags{Audio}
*/
struct ToBytestreamConverter
{
explicit ToBytestreamConverter (int storageSize)
: translator (storageSize) {}
template <typename Fn>
void convert (const MidiMessage& m, Fn&& fn)
{
fn (m);
}
template <typename Fn>
void convert (const View& v, double time, Fn&& fn)
{
Conversion::midi2ToMidi1DefaultTranslation (v, [&] (const View& midi1)
{
translator.dispatch (midi1, time, fn);
});
}
void reset() { translator.reset(); }
Midi1ToBytestreamTranslator translator;
};
} // namespace juce::universal_midi_packets
#endif
@@ -0,0 +1,70 @@
/*
==============================================================================
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::universal_midi_packets
{
/**
Holds MIDI device info that may be required by certain UMP messages and
MIDI-CI messages.
@tags{Audio}
*/
struct DeviceInfo
{
std::array<std::byte, 3> manufacturer; ///< LSB first
std::array<std::byte, 2> family; ///< LSB first
std::array<std::byte, 2> modelNumber; ///< LSB first
std::array<std::byte, 4> revision;
private:
auto tie() const { return std::tie (manufacturer, family, modelNumber, revision); }
public:
bool operator== (const DeviceInfo& other) const { return tie() == other.tie(); }
bool operator!= (const DeviceInfo& other) const { return tie() != other.tie(); }
static constexpr auto marshallingVersion = std::nullopt;
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
return archive (named ("manufacturer", t.manufacturer),
named ("family", t.family),
named ("modelNumber", t.modelNumber),
named ("revision", t.revision));
}
};
} // namespace juce::universal_midi_packets
@@ -0,0 +1,203 @@
/*
==============================================================================
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.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce::universal_midi_packets
{
/**
Parses a raw stream of uint32_t, and calls a user-provided callback every time
a full Universal MIDI Packet is encountered.
@tags{Audio}
*/
class Dispatcher
{
public:
/** Clears the dispatcher. */
void reset() { currentPacketLen = 0; }
/** Calls `callback` with a View of each packet encountered in the range delimited
by `begin` and `end`.
If the range ends part-way through a packet, the next call to `dispatch` will
continue from that point in the packet (unless `reset` is called first).
*/
template <typename PacketCallbackFunction>
void dispatch (const uint32_t* begin,
const uint32_t* end,
double timeStamp,
PacketCallbackFunction&& callback)
{
std::for_each (begin, end, [&] (uint32_t word)
{
nextPacket[currentPacketLen++] = word;
if (currentPacketLen == Utils::getNumWordsForMessageType (nextPacket.front()))
{
callback (View (nextPacket.data()), timeStamp);
currentPacketLen = 0;
}
});
}
private:
std::array<uint32_t, 4> nextPacket;
size_t currentPacketLen = 0;
};
//==============================================================================
/**
Parses a stream of bytes representing a sequence of bytestream-encoded MIDI 1.0 messages,
converting the messages to UMP format and passing the packets to a user-provided callback
as they become ready.
@tags{Audio}
*/
class BytestreamToUMPDispatcher
{
public:
/** Initialises the dispatcher.
Channel messages will be converted to the requested protocol format `pp`.
`storageSize` bytes will be allocated to store incomplete messages.
*/
explicit BytestreamToUMPDispatcher (PacketProtocol pp, int storageSize)
: concatenator (storageSize),
converter (pp)
{}
void reset()
{
concatenator.reset();
converter.reset();
}
/** Calls `callback` with a View of each converted packet as it becomes ready.
@param begin the first byte in a range of bytes representing bytestream-encoded MIDI messages.
@param end one-past the last byte in a range of bytes representing bytestream-encoded MIDI messages.
@param timestamp a timestamp to apply to the created packets.
@param callback a callback which will be passed a View pointing to each new packet as it becomes ready.
*/
template <typename PacketCallbackFunction>
void dispatch (const uint8_t* begin,
const uint8_t* end,
double timestamp,
PacketCallbackFunction&& callback)
{
using CallbackPtr = decltype (std::addressof (callback));
struct Callback
{
Callback (BytestreamToUMPDispatcher& d, CallbackPtr c)
: dispatch (d), callbackPtr (c) {}
void handleIncomingMidiMessage (void*, const MidiMessage& msg) const
{
Conversion::toMidi1 (BytestreamMidiView (&msg), [&] (const View& view)
{
dispatch.converter.convert (view, *callbackPtr);
});
}
void handlePartialSysexMessage (void*, const uint8_t*, int, double) const {}
BytestreamToUMPDispatcher& dispatch;
CallbackPtr callbackPtr = nullptr;
};
Callback inputCallback { *this, &callback };
concatenator.pushMidiData (begin, int (end - begin), timestamp, (void*) nullptr, inputCallback);
}
private:
MidiDataConcatenator concatenator;
GenericUMPConverter converter;
};
//==============================================================================
/**
Parses a stream of 32-bit words representing a sequence of UMP-encoded MIDI messages,
converting the messages to MIDI 1.0 bytestream format and passing them to a user-provided
callback as they become ready.
@tags{Audio}
*/
class ToBytestreamDispatcher
{
public:
/** Initialises the dispatcher.
`storageSize` bytes will be allocated to store incomplete messages.
*/
explicit ToBytestreamDispatcher (int storageSize)
: converter (storageSize) {}
/** Clears the dispatcher. */
void reset()
{
dispatcher.reset();
converter.reset();
}
/** Calls `callback` with converted bytestream-formatted MidiMessage whenever
a new message becomes available.
@param begin the first word in a stream of words representing UMP-encoded MIDI packets.
@param end one-past the last word in a stream of words representing UMP-encoded MIDI packets.
@param timestamp a timestamp to apply to converted messages.
@param callback a callback which will be passed a MidiMessage each time a new message becomes ready.
*/
template <typename BytestreamMessageCallback>
void dispatch (const uint32_t* begin,
const uint32_t* end,
double timestamp,
BytestreamMessageCallback&& callback)
{
dispatcher.dispatch (begin, end, timestamp, [&] (const View& view, double time)
{
converter.convert (view, time, callback);
});
}
private:
Dispatcher dispatcher;
ToBytestreamConverter converter;
};
} // namespace juce::universal_midi_packets
#endif
@@ -0,0 +1,543 @@
/*
==============================================================================
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.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce::universal_midi_packets
{
/**
This struct holds functions that can be used to create different kinds
of Universal MIDI Packet.
@tags{Audio}
*/
struct Factory
{
/** @internal */
struct Detail
{
static PacketX1 makeSystem() { return PacketX1{}.withMessageType (1); }
static PacketX1 makeV1() { return PacketX1{}.withMessageType (2); }
static PacketX2 makeV2() { return PacketX2{}.withMessageType (4); }
static PacketX2 makeSysEx (uint8_t group,
uint8_t status,
uint8_t numBytes,
const std::byte* data)
{
jassert (numBytes <= 6);
std::array<uint8_t, 8> bytes{{}};
bytes[0] = (0x3 << 0x4) | group;
bytes[1] = (uint8_t) (status << 0x4) | numBytes;
std::memcpy (bytes.data() + 2, data, numBytes);
std::array<uint32_t, 2> words;
for (const auto [index, word] : enumerate (words))
word = ByteOrder::bigEndianInt (bytes.data() + 4 * index);
return PacketX2 { words };
}
static PacketX4 makeSysEx8 (uint8_t group,
uint8_t status,
uint8_t numBytes,
uint8_t dataStart,
const uint8_t* data)
{
jassert (numBytes <= 16 - dataStart);
std::array<uint8_t, 16> bytes{{}};
bytes[0] = (0x5 << 0x4) | group;
bytes[1] = (uint8_t) (status << 0x4) | numBytes;
std::memcpy (bytes.data() + dataStart, data, numBytes);
std::array<uint32_t, 4> words;
for (const auto [index, word] : enumerate (words))
word = ByteOrder::bigEndianInt (bytes.data() + 4 * index);
return PacketX4 { words };
}
};
static PacketX1 makeNoop (uint8_t group)
{
return PacketX1{}.withGroup (group);
}
static PacketX1 makeJRClock (uint8_t group, uint16_t time)
{
return PacketX1 { time }.withStatus (1).withGroup (group);
}
static PacketX1 makeJRTimestamp (uint8_t group, uint16_t time)
{
return PacketX1 { time }.withStatus (2).withGroup (group);
}
static PacketX1 makeTimeCode (uint8_t group, uint8_t code)
{
return Detail::makeSystem().withGroup (group)
.withU8<1> (0xf1)
.withU8<2> (code & 0x7f);
}
static PacketX1 makeSongPositionPointer (uint8_t group, uint16_t pos)
{
return Detail::makeSystem().withGroup (group)
.withU8<1> (0xf2)
.withU8<2> (pos & 0x7f)
.withU8<3> ((pos >> 7) & 0x7f);
}
static PacketX1 makeSongSelect (uint8_t group, uint8_t song)
{
return Detail::makeSystem().withGroup (group)
.withU8<1> (0xf3)
.withU8<2> (song & 0x7f);
}
static PacketX1 makeTuneRequest (uint8_t group)
{
return Detail::makeSystem().withGroup (group)
.withU8<1> (0xf6);
}
static PacketX1 makeTimingClock (uint8_t group)
{
return Detail::makeSystem().withGroup (group)
.withU8<1> (0xf8);
}
static PacketX1 makeStart (uint8_t group)
{
return Detail::makeSystem().withGroup (group)
.withU8<1> (0xfa);
}
static PacketX1 makeContinue (uint8_t group)
{
return Detail::makeSystem().withGroup (group)
.withU8<1> (0xfb);
}
static PacketX1 makeStop (uint8_t group)
{
return Detail::makeSystem().withGroup (group)
.withU8<1> (0xfc);
}
static PacketX1 makeActiveSensing (uint8_t group)
{
return Detail::makeSystem().withGroup (group)
.withU8<1> (0xfe);
}
static PacketX1 makeReset (uint8_t group)
{
return Detail::makeSystem().withGroup (group)
.withU8<1> (0xff);
}
static PacketX1 makeNoteOffV1 (uint8_t group,
uint8_t channel,
uint8_t note,
uint8_t velocity)
{
return Detail::makeV1().withGroup (group)
.withStatus (0x8)
.withChannel (channel)
.withU8<2> (note & 0x7f)
.withU8<3> (velocity & 0x7f);
}
static PacketX1 makeNoteOnV1 (uint8_t group,
uint8_t channel,
uint8_t note,
uint8_t velocity)
{
return Detail::makeV1().withGroup (group)
.withStatus (0x9)
.withChannel (channel)
.withU8<2> (note & 0x7f)
.withU8<3> (velocity & 0x7f);
}
static PacketX1 makePolyPressureV1 (uint8_t group,
uint8_t channel,
uint8_t note,
uint8_t pressure)
{
return Detail::makeV1().withGroup (group)
.withStatus (0xa)
.withChannel (channel)
.withU8<2> (note & 0x7f)
.withU8<3> (pressure & 0x7f);
}
static PacketX1 makeControlChangeV1 (uint8_t group,
uint8_t channel,
uint8_t controller,
uint8_t value)
{
return Detail::makeV1().withGroup (group)
.withStatus (0xb)
.withChannel (channel)
.withU8<2> (controller & 0x7f)
.withU8<3> (value & 0x7f);
}
static PacketX1 makeProgramChangeV1 (uint8_t group,
uint8_t channel,
uint8_t program)
{
return Detail::makeV1().withGroup (group)
.withStatus (0xc)
.withChannel (channel)
.withU8<2> (program & 0x7f);
}
static PacketX1 makeChannelPressureV1 (uint8_t group,
uint8_t channel,
uint8_t pressure)
{
return Detail::makeV1().withGroup (group)
.withStatus (0xd)
.withChannel (channel)
.withU8<2> (pressure & 0x7f);
}
static PacketX1 makePitchBend (uint8_t group,
uint8_t channel,
uint16_t pitchbend)
{
return Detail::makeV1().withGroup (group)
.withStatus (0xe)
.withChannel (channel)
.withU8<2> (pitchbend & 0x7f)
.withU8<3> ((pitchbend >> 7) & 0x7f);
}
static PacketX2 makeSysExIn1Packet (uint8_t group,
uint8_t numBytes,
const std::byte* data)
{
return Detail::makeSysEx (group, 0x0, numBytes, data);
}
static PacketX2 makeSysExStart (uint8_t group,
uint8_t numBytes,
const std::byte* data)
{
return Detail::makeSysEx (group, 0x1, numBytes, data);
}
static PacketX2 makeSysExContinue (uint8_t group,
uint8_t numBytes,
const std::byte* data)
{
return Detail::makeSysEx (group, 0x2, numBytes, data);
}
static PacketX2 makeSysExEnd (uint8_t group,
uint8_t numBytes,
const std::byte* data)
{
return Detail::makeSysEx (group, 0x3, numBytes, data);
}
static PacketX2 makeRegisteredPerNoteControllerV2 (uint8_t group,
uint8_t channel,
uint8_t note,
uint8_t controller,
uint32_t data)
{
return Detail::makeV2().withGroup (group)
.withStatus (0x0)
.withChannel (channel)
.withU8<2> (note & 0x7f)
.withU8<3> (controller & 0x7f)
.withU32<1> (data);
}
static PacketX2 makeAssignablePerNoteControllerV2 (uint8_t group,
uint8_t channel,
uint8_t note,
uint8_t controller,
uint32_t data)
{
return Detail::makeV2().withGroup (group)
.withStatus (0x1)
.withChannel (channel)
.withU8<2> (note & 0x7f)
.withU8<3> (controller & 0x7f)
.withU32<1> (data);
}
static PacketX2 makeRegisteredControllerV2 (uint8_t group,
uint8_t channel,
uint8_t bank,
uint8_t index,
uint32_t data)
{
return Detail::makeV2().withGroup (group)
.withStatus (0x2)
.withChannel (channel)
.withU8<2> (bank & 0x7f)
.withU8<3> (index & 0x7f)
.withU32<1> (data);
}
static PacketX2 makeAssignableControllerV2 (uint8_t group,
uint8_t channel,
uint8_t bank,
uint8_t index,
uint32_t data)
{
return Detail::makeV2().withGroup (group)
.withStatus (0x3)
.withChannel (channel)
.withU8<2> (bank & 0x7f)
.withU8<3> (index & 0x7f)
.withU32<1> (data);
}
static PacketX2 makeRelativeRegisteredControllerV2 (uint8_t group,
uint8_t channel,
uint8_t bank,
uint8_t index,
uint32_t data)
{
return Detail::makeV2().withGroup (group)
.withStatus (0x4)
.withChannel (channel)
.withU8<2> (bank & 0x7f)
.withU8<3> (index & 0x7f)
.withU32<1> (data);
}
static PacketX2 makeRelativeAssignableControllerV2 (uint8_t group,
uint8_t channel,
uint8_t bank,
uint8_t index,
uint32_t data)
{
return Detail::makeV2().withGroup (group)
.withStatus (0x5)
.withChannel (channel)
.withU8<2> (bank & 0x7f)
.withU8<3> (index & 0x7f)
.withU32<1> (data);
}
static PacketX2 makePerNotePitchBendV2 (uint8_t group,
uint8_t channel,
uint8_t note,
uint32_t data)
{
return Detail::makeV2().withGroup (group)
.withStatus (0x6)
.withChannel (channel)
.withU8<2> (note & 0x7f)
.withU32<1> (data);
}
enum class NoteAttributeKind : uint8_t
{
none = 0x00,
manufacturer = 0x01,
profile = 0x02,
pitch7_9 = 0x03
};
static PacketX2 makeNoteOffV2 (uint8_t group,
uint8_t channel,
uint8_t note,
NoteAttributeKind attribute,
uint16_t velocity,
uint16_t attributeValue)
{
return Detail::makeV2().withGroup (group)
.withStatus (0x8)
.withChannel (channel)
.withU8<2> (note & 0x7f)
.withU8<3> ((uint8_t) attribute)
.withU16<2> (velocity)
.withU16<3> (attributeValue);
}
static PacketX2 makeNoteOnV2 (uint8_t group,
uint8_t channel,
uint8_t note,
NoteAttributeKind attribute,
uint16_t velocity,
uint16_t attributeValue)
{
return Detail::makeV2().withGroup (group)
.withStatus (0x9)
.withChannel (channel)
.withU8<2> (note & 0x7f)
.withU8<3> ((uint8_t) attribute)
.withU16<2> (velocity)
.withU16<3> (attributeValue);
}
static PacketX2 makePolyPressureV2 (uint8_t group,
uint8_t channel,
uint8_t note,
uint32_t data)
{
return Detail::makeV2().withGroup (group)
.withStatus (0xa)
.withChannel (channel)
.withU8<2> (note & 0x7f)
.withU32<1> (data);
}
static PacketX2 makeControlChangeV2 (uint8_t group,
uint8_t channel,
uint8_t controller,
uint32_t data)
{
return Detail::makeV2().withGroup (group)
.withStatus (0xb)
.withChannel (channel)
.withU8<2> (controller & 0x7f)
.withU32<1> (data);
}
static PacketX2 makeProgramChangeV2 (uint8_t group,
uint8_t channel,
uint8_t optionFlags,
uint8_t program,
uint8_t bankMsb,
uint8_t bankLsb)
{
return Detail::makeV2().withGroup (group)
.withStatus (0xc)
.withChannel (channel)
.withU8<3> (optionFlags)
.withU8<4> (program)
.withU8<6> (bankMsb)
.withU8<7> (bankLsb);
}
static PacketX2 makeChannelPressureV2 (uint8_t group,
uint8_t channel,
uint32_t data)
{
return Detail::makeV2().withGroup (group)
.withStatus (0xd)
.withChannel (channel)
.withU32<1> (data);
}
static PacketX2 makePitchBendV2 (uint8_t group,
uint8_t channel,
uint32_t data)
{
return Detail::makeV2().withGroup (group)
.withStatus (0xe)
.withChannel (channel)
.withU32<1> (data);
}
static PacketX2 makePerNoteManagementV2 (uint8_t group,
uint8_t channel,
uint8_t note,
uint8_t optionFlags)
{
return Detail::makeV2().withGroup (group)
.withStatus (0xf)
.withChannel (channel)
.withU8<2> (note)
.withU8<3> (optionFlags);
}
static PacketX4 makeSysEx8in1Packet (uint8_t group,
uint8_t numBytes,
uint8_t streamId,
const uint8_t* data)
{
return Detail::makeSysEx8 (group, 0x0, numBytes, 3, data).withU8<2> (streamId);
}
static PacketX4 makeSysEx8Start (uint8_t group,
uint8_t numBytes,
uint8_t streamId,
const uint8_t* data)
{
return Detail::makeSysEx8 (group, 0x1, numBytes, 3, data).withU8<2> (streamId);
}
static PacketX4 makeSysEx8Continue (uint8_t group,
uint8_t numBytes,
uint8_t streamId,
const uint8_t* data)
{
return Detail::makeSysEx8 (group, 0x2, numBytes, 3, data).withU8<2> (streamId);
}
static PacketX4 makeSysEx8End (uint8_t group,
uint8_t numBytes,
uint8_t streamId,
const uint8_t* data)
{
return Detail::makeSysEx8 (group, 0x3, numBytes, 3, data).withU8<2> (streamId);
}
static PacketX4 makeMixedDataSetHeader (uint8_t group,
uint8_t dataSetId,
const uint8_t* data)
{
return Detail::makeSysEx8 (group, 0x8, 14, 2, data).withChannel (dataSetId);
}
static PacketX4 makeDataSetPayload (uint8_t group,
uint8_t dataSetId,
const uint8_t* data)
{
return Detail::makeSysEx8 (group, 0x9, 14, 2, data).withChannel (dataSetId);
}
};
} // namespace juce::universal_midi_packets
#endif
@@ -0,0 +1,46 @@
/*
==============================================================================
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::universal_midi_packets
{
Iterator::Iterator (const uint32_t* ptr, [[maybe_unused]] size_t bytes) noexcept
: view (ptr)
#if JUCE_DEBUG
, bytesRemaining (bytes)
#endif
{
}
} // namespace juce::universal_midi_packets
@@ -0,0 +1,132 @@
/*
==============================================================================
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.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce::universal_midi_packets
{
/**
Enables iteration over a collection of Universal MIDI Packets stored as
a contiguous range of 32-bit words.
This iterator is used by Packets to allow access to the messages
that it contains.
@tags{Audio}
*/
class Iterator
{
public:
/** Creates an invalid (singular) iterator. */
Iterator() noexcept = default;
/** Creates an iterator pointing at `ptr`. */
explicit Iterator (const uint32_t* ptr, size_t bytes) noexcept;
using difference_type = std::iterator_traits<const uint32_t*>::difference_type;
using value_type = View;
using reference = const View&;
using pointer = const View*;
using iterator_category = std::forward_iterator_tag;
/** Moves this iterator to the next packet in the range. */
Iterator& operator++() noexcept
{
const auto increment = view.size();
#if JUCE_DEBUG
// If you hit this, the memory region contained a truncated or otherwise
// malformed Universal MIDI Packet.
// The Iterator can only be used on regions containing complete packets!
jassert (increment <= bytesRemaining);
bytesRemaining -= increment;
#endif
view = View (view.data() + increment);
return *this;
}
/** Moves this iterator to the next packet in the range,
returning the value of the iterator before it was
incremented.
*/
Iterator operator++ (int) noexcept
{
auto copy = *this;
++(*this);
return copy;
}
/** Returns true if this iterator points to the same address
as another iterator.
*/
bool operator== (const Iterator& other) const noexcept
{
return view == other.view;
}
/** Returns false if this iterator points to the same address
as another iterator.
*/
bool operator!= (const Iterator& other) const noexcept
{
return ! operator== (other);
}
/** Returns a reference to a View of the packet currently
pointed-to by this iterator.
The View can be queried for its size and content.
*/
reference operator*() noexcept { return view; }
/** Returns a pointer to a View of the packet currently
pointed-to by this iterator.
The View can be queried for its size and content.
*/
pointer operator->() noexcept { return &view; }
private:
View view;
#if JUCE_DEBUG
size_t bytesRemaining = 0;
#endif
};
} // namespace juce::universal_midi_packets
#endif
@@ -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.
==============================================================================
*/
#ifndef DOXYGEN
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}
*/
class Midi1ToBytestreamTranslator
{
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;
}
/** 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)
{
const auto firstWord = *packet.data();
if (! pendingSysExData.empty() && shouldPacketTerminateSysExEarly (firstWord))
pendingSysExData.clear();
switch (packet.size())
{
case 1:
{
// Utility messages don't translate to bytestream format
if (Utils::getMessageType (firstWord) != 0x00)
{
const auto message = fromUmp (PacketX1 { firstWord }, time);
callback (BytestreamMidiView (&message));
}
break;
}
case 2:
{
if (Utils::getMessageType (firstWord) == 0x3)
processSysEx (PacketX2 { packet[0], packet[1] }, time, callback);
break;
}
case 3: // no 3-word packets in the current spec
case 4: // no 4-word packets translate to bytestream format
default:
break;
}
}
/** Converts from a Universal MIDI Packet to MIDI 1 bytestream format.
This is only capable of converting a single Universal MIDI Packet to
an equivalent bytestream MIDI message. This function cannot understand
multi-packet messages, like SysEx7 messages.
To convert multi-packet messages, use `Midi1ToBytestreamTranslator`
to convert from a UMP MIDI 1.0 stream, or `ToBytestreamDispatcher`
to convert from both MIDI 2.0 and MIDI 1.0.
*/
static MidiMessage fromUmp (const PacketX1& m, double time = 0)
{
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());
return MidiMessage (bytes.data(), numBytes, time);
}
private:
template <typename MessageCallback>
void processSysEx (const PacketX2& packet,
double time,
MessageCallback&& callback)
{
switch (getSysEx7Kind (packet[0]))
{
case SysEx7::Kind::complete:
startSysExMessage (time);
pushBytes (packet);
terminateSysExMessage (callback);
break;
case SysEx7::Kind::begin:
startSysExMessage (time);
pushBytes (packet);
break;
case SysEx7::Kind::continuation:
if (pendingSysExData.empty())
break;
pushBytes (packet);
break;
case SysEx7::Kind::end:
if (pendingSysExData.empty())
break;
pushBytes (packet);
terminateSysExMessage (callback);
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();
}
static bool shouldPacketTerminateSysExEarly (uint32_t firstWord)
{
return ! (isSysExContinuation (firstWord)
|| isSystemRealTime (firstWord)
|| isJROrNOP (firstWord));
}
static SysEx7::Kind getSysEx7Kind (uint32_t word)
{
return SysEx7::Kind ((word >> 0x14) & 0xf);
}
static bool isJROrNOP (uint32_t word)
{
return Utils::getMessageType (word) == 0x0;
}
static bool isSysExContinuation (uint32_t word)
{
if (Utils::getMessageType (word) != 0x3)
return false;
const auto kind = getSysEx7Kind (word);
return kind == SysEx7::Kind::continuation || kind == SysEx7::Kind::end;
}
static bool isSystemRealTime (uint32_t word)
{
return Utils::getMessageType (word) == 0x1 && ((word >> 0x10) & 0xff) >= 0xf8;
}
std::vector<std::byte> pendingSysExData;
double pendingSysExTime = 0.0;
};
} // namespace juce::universal_midi_packets
#endif
@@ -0,0 +1,210 @@
/*
==============================================================================
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::universal_midi_packets
{
PacketX2 Midi1ToMidi2DefaultTranslator::processNoteOnOrOff (const HelperValues helpers)
{
const auto velocity = helpers.byte2;
const auto needsConversion = (helpers.byte0 & std::byte { 0xf0 }) == std::byte { 0x90 } && velocity == std::byte { 0 };
const auto firstByte = needsConversion ? (std::byte { 0x80 } | (helpers.byte0 & std::byte { 0xf }))
: helpers.byte0;
return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, firstByte, helpers.byte1, std::byte { 0 }),
(uint32_t) (Conversion::scaleTo16 (uint8_t (velocity)) << 0x10)
};
}
PacketX2 Midi1ToMidi2DefaultTranslator::processPolyPressure (const HelperValues helpers)
{
return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, helpers.byte0, helpers.byte1, std::byte { 0 }),
Conversion::scaleTo32 (uint8_t (helpers.byte2))
};
}
bool Midi1ToMidi2DefaultTranslator::processControlChange (const HelperValues helpers,
PacketX2& packet)
{
const auto statusAndChannel = helpers.byte0;
const auto cc = uint8_t (helpers.byte1);
const auto shouldAccumulate = [&]
{
switch (cc)
{
case 6:
case 38:
case 98:
case 99:
case 100:
case 101:
return true;
}
return false;
}();
const auto group = (uint8_t) (helpers.typeAndGroup & std::byte { 0xf });
const auto channel = (uint8_t) (statusAndChannel & std::byte { 0xf });
const auto byte = helpers.byte2;
if (shouldAccumulate)
{
auto& accumulator = groupAccumulators[group][channel];
if (accumulator.addByte (cc, byte))
{
const auto& bytes = accumulator.getBytes();
const auto bank = bytes[0];
const auto index = bytes[1];
const auto msb = bytes[2];
const auto lsb = bytes[3];
const auto value = uint16_t ((uint16_t (msb & std::byte { 0x7f }) << 7) | uint16_t (lsb & std::byte { 0x7f }));
const auto newStatus = (uint8_t) (accumulator.getKind() == PnKind::nrpn ? 0x3 : 0x2);
packet = PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, std::byte ((newStatus << 0x4) | channel), bank, index),
Conversion::scaleTo32 (value)
};
return true;
}
return false;
}
if (cc == 0)
{
groupBanks[group][channel].setMsb (uint8_t (byte));
return false;
}
if (cc == 32)
{
groupBanks[group][channel].setLsb (uint8_t (byte));
return false;
}
packet = PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, statusAndChannel, std::byte { cc }, std::byte { 0 }),
Conversion::scaleTo32 (uint8_t (helpers.byte2))
};
return true;
}
PacketX2 Midi1ToMidi2DefaultTranslator::processProgramChange (const HelperValues helpers) const
{
const auto group = (uint8_t) (helpers.typeAndGroup & std::byte { 0xf });
const auto channel = (uint8_t) (helpers.byte0 & std::byte { 0xf });
const auto bank = groupBanks[group][channel];
const auto valid = bank.isValid();
return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup,
helpers.byte0,
std::byte { 0 },
valid ? std::byte { 1 } : std::byte { 0 }),
Utils::bytesToWord (helpers.byte1,
std::byte { 0 },
valid ? std::byte { bank.getMsb() } : std::byte { 0 },
valid ? std::byte { bank.getLsb() } : std::byte { 0 })
};
}
PacketX2 Midi1ToMidi2DefaultTranslator::processChannelPressure (const HelperValues helpers)
{
return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, helpers.byte0, std::byte { 0 }, std::byte { 0 }),
Conversion::scaleTo32 (uint8_t (helpers.byte1))
};
}
PacketX2 Midi1ToMidi2DefaultTranslator::processPitchBend (const HelperValues helpers)
{
const auto lsb = helpers.byte1;
const auto msb = helpers.byte2;
const auto value = uint16_t (uint16_t (msb) << 7 | uint16_t (lsb));
return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, helpers.byte0, std::byte { 0 }, std::byte { 0 }),
Conversion::scaleTo32 (value)
};
}
bool Midi1ToMidi2DefaultTranslator::PnAccumulator::addByte (uint8_t cc, std::byte byte)
{
const auto isStart = cc == 99 || cc == 101;
if (isStart)
{
kind = cc == 99 ? PnKind::nrpn : PnKind::rpn;
index = 0;
}
bytes[index] = byte;
const auto shouldContinue = [&]
{
switch (index)
{
case 0: return isStart;
case 1: return kind == PnKind::nrpn ? cc == 98 : cc == 100;
case 2: return cc == 6;
case 3: return cc == 38;
}
return false;
}();
index = shouldContinue ? index + 1 : 0;
if (index != bytes.size())
return false;
index = 0;
return true;
}
} // namespace juce::universal_midi_packets
@@ -0,0 +1,200 @@
/*
==============================================================================
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.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce::universal_midi_packets
{
/**
Translates a series of MIDI 1 Universal MIDI Packets to corresponding MIDI 2
packets.
@tags{Audio}
*/
class Midi1ToMidi2DefaultTranslator
{
public:
Midi1ToMidi2DefaultTranslator() = default;
/** Converts MIDI 1 Universal MIDI Packets to corresponding MIDI 2 packets,
calling `callback` with each converted packet.
In some cases (such as RPN/NRPN messages) multiple MIDI 1 packets will
convert to a single MIDI 2 packet. In these cases, the translator will
accumulate the full message internally, and send a single callback with
the completed message, once all the individual MIDI 1 packets have been
processed.
*/
template <typename PacketCallback>
void dispatch (const View& v, PacketCallback&& callback)
{
const auto firstWord = v[0];
const auto messageType = Utils::getMessageType (firstWord);
if (messageType != 0x2)
{
callback (v);
return;
}
const HelperValues helperValues
{
std::byte ((0x4 << 0x4) | Utils::getGroup (firstWord)),
std::byte ((firstWord >> 0x10) & 0xff),
std::byte ((firstWord >> 0x08) & 0x7f),
std::byte ((firstWord >> 0x00) & 0x7f),
};
switch (Utils::getStatus (firstWord))
{
case 0x8:
case 0x9:
{
const auto packet = processNoteOnOrOff (helperValues);
callback (View (packet.data()));
return;
}
case 0xa:
{
const auto packet = processPolyPressure (helperValues);
callback (View (packet.data()));
return;
}
case 0xb:
{
PacketX2 packet;
if (processControlChange (helperValues, packet))
callback (View (packet.data()));
return;
}
case 0xc:
{
const auto packet = processProgramChange (helperValues);
callback (View (packet.data()));
return;
}
case 0xd:
{
const auto packet = processChannelPressure (helperValues);
callback (View (packet.data()));
return;
}
case 0xe:
{
const auto packet = processPitchBend (helperValues);
callback (View (packet.data()));
return;
}
}
}
void reset()
{
groupAccumulators = {};
groupBanks = {};
}
private:
enum class PnKind { nrpn, rpn };
struct HelperValues
{
std::byte typeAndGroup;
std::byte byte0;
std::byte byte1;
std::byte byte2;
};
static PacketX2 processNoteOnOrOff (HelperValues helpers);
static PacketX2 processPolyPressure (HelperValues helpers);
bool processControlChange (HelperValues helpers, PacketX2& packet);
PacketX2 processProgramChange (HelperValues helpers) const;
static PacketX2 processChannelPressure (HelperValues helpers);
static PacketX2 processPitchBend (HelperValues helpers);
class PnAccumulator
{
public:
bool addByte (uint8_t cc, std::byte byte);
const std::array<std::byte, 4>& getBytes() const noexcept { return bytes; }
PnKind getKind() const noexcept { return kind; }
private:
std::array<std::byte, 4> bytes;
uint8_t index = 0;
PnKind kind = PnKind::nrpn;
};
class Bank
{
public:
bool isValid() const noexcept { return ! (msb & 0x80); }
uint8_t getMsb() const noexcept { return msb & 0x7f; }
uint8_t getLsb() const noexcept { return lsb & 0x7f; }
void setMsb (uint8_t i) noexcept { msb = i & 0x7f; }
void setLsb (uint8_t i) noexcept { msb &= 0x7f; lsb = i & 0x7f; }
private:
// We use the top bit to indicate whether this bank is valid.
// After reading the spec, it's not clear how we should determine whether
// there are valid values, so we'll just assume that the bank is valid
// once either the lsb or msb have been written.
uint8_t msb = 0x80;
uint8_t lsb = 0x00;
};
using ChannelAccumulators = std::array<PnAccumulator, 16>;
std::array<ChannelAccumulators, 16> groupAccumulators;
using ChannelBanks = std::array<Bank, 16>;
std::array<ChannelBanks, 16> groupBanks;
};
} // namespace juce::universal_midi_packets
#endif
@@ -0,0 +1,57 @@
/*
==============================================================================
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.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce::universal_midi_packets
{
/** The kinds of MIDI protocol that can be formatted into Universal MIDI Packets. */
enum class PacketProtocol
{
MIDI_1_0,
MIDI_2_0,
};
/** All kinds of MIDI protocol understood by JUCE. */
enum class MidiProtocol
{
bytestream,
UMP_MIDI_1_0,
UMP_MIDI_2_0,
};
} // namespace juce::universal_midi_packets
#endif
@@ -0,0 +1,55 @@
/*
==============================================================================
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.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce::universal_midi_packets
{
/**
A base class for classes which receive Universal MIDI Packets from an input.
@tags{Audio}
*/
struct Receiver
{
virtual ~Receiver() noexcept = default;
/** This will be called each time a new packet is ready for processing. */
virtual void packetReceived (const View& packet, double time) = 0;
};
} // namespace juce::universal_midi_packets
#endif
@@ -0,0 +1,62 @@
/*
==============================================================================
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::universal_midi_packets
{
uint32_t SysEx7::getNumPacketsRequiredForDataSize (uint32_t size)
{
constexpr auto denom = 6;
return (size / denom) + ((size % denom) != 0);
}
SysEx7::PacketBytes SysEx7::getDataBytes (const PacketX2& packet)
{
const auto numBytes = Utils::getChannel (packet[0]);
constexpr uint8_t maxBytes = 6;
jassert (numBytes <= maxBytes);
return
{
{ { std::byte { packet.getU8<2>() },
std::byte { packet.getU8<3>() },
std::byte { packet.getU8<4>() },
std::byte { packet.getU8<5>() },
std::byte { packet.getU8<6>() },
std::byte { packet.getU8<7>() } } },
jmin (numBytes, maxBytes)
};
}
} // namespace juce::universal_midi_packets
@@ -0,0 +1,86 @@
/*
==============================================================================
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.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce::universal_midi_packets
{
/**
This struct acts as a single-file namespace for Universal MIDI Packet
functionality related to 7-bit SysEx.
@tags{Audio}
*/
struct SysEx7
{
/** Returns the number of 64-bit packets required to hold a series of
SysEx bytes.
The number passed to this function should exclude the leading/trailing
SysEx bytes used in an old midi bytestream, as these are not required
when using Universal MIDI Packets.
*/
static uint32_t getNumPacketsRequiredForDataSize (uint32_t);
/** The different kinds of UMP SysEx-7 message. */
enum class Kind : uint8_t
{
/** The whole message fits in a single 2-word packet. */
complete = 0,
/** The packet begins a SysEx message that will continue in subsequent packets. */
begin = 1,
/** The packet is a continuation of an ongoing SysEx message. */
continuation = 2,
/** The packet terminates an ongoing SysEx message. */
end = 3
};
/** Holds the bytes from a single SysEx-7 packet. */
struct PacketBytes
{
std::array<std::byte, 6> data;
uint8_t size;
};
/** Extracts the data bytes from a 64-bit data message. */
static PacketBytes getDataBytes (const PacketX2& packet);
};
} // namespace juce::universal_midi_packets
#endif
@@ -0,0 +1,68 @@
/*
==============================================================================
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::universal_midi_packets
{
uint32_t Utils::getNumWordsForMessageType (uint32_t mt)
{
switch (Utils::getMessageType (mt))
{
case 0x0:
case 0x1:
case 0x2:
case 0x6:
case 0x7:
return 1;
case 0x3:
case 0x4:
case 0x8:
case 0x9:
case 0xa:
return 2;
case 0xb:
case 0xc:
return 3;
case 0x5:
case 0xd:
case 0xe:
case 0xf:
return 4;
}
jassertfalse;
return 1;
}
} // namespace juce::universal_midi_packets
@@ -0,0 +1,129 @@
/*
==============================================================================
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.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce::universal_midi_packets
{
/**
Helpful types and functions for interacting with Universal MIDI Packets.
@tags{Audio}
*/
struct Utils
{
/** Joins 4 bytes into a single 32-bit word. */
static constexpr uint32_t bytesToWord (std::byte a, std::byte b, std::byte c, std::byte d)
{
return uint32_t (a) << 0x18
| uint32_t (b) << 0x10
| uint32_t (c) << 0x08
| uint32_t (d) << 0x00;
}
/** Returns the expected number of 32-bit words in a Universal MIDI Packet, given
the first word of the packet.
The result will be between 1 and 4 inclusive.
A result of 1 means that the word is itself a complete packet.
*/
static uint32_t getNumWordsForMessageType (uint32_t);
/**
Helper functions for setting/getting 4-bit ranges inside a 32-bit word.
*/
template <size_t Index>
struct U4
{
static constexpr uint32_t shift = (uint32_t) 0x1c - Index * 4;
static constexpr uint32_t set (uint32_t word, uint8_t value)
{
return (word & ~((uint32_t) 0xf << shift)) | (uint32_t) ((value & 0xf) << shift);
}
static constexpr uint8_t get (uint32_t word)
{
return (uint8_t) ((word >> shift) & 0xf);
}
};
/**
Helper functions for setting/getting 8-bit ranges inside a 32-bit word.
*/
template <size_t Index>
struct U8
{
static constexpr uint32_t shift = (uint32_t) 0x18 - Index * 8;
static constexpr uint32_t set (uint32_t word, uint8_t value)
{
return (word & ~((uint32_t) 0xff << shift)) | (uint32_t) (value << shift);
}
static constexpr uint8_t get (uint32_t word)
{
return (uint8_t) ((word >> shift) & 0xff);
}
};
/**
Helper functions for setting/getting 16-bit ranges inside a 32-bit word.
*/
template <size_t Index>
struct U16
{
static constexpr uint32_t shift = (uint32_t) 0x10 - Index * 16;
static constexpr uint32_t set (uint32_t word, uint16_t value)
{
return (word & ~((uint32_t) 0xffff << shift)) | (uint32_t) (value << shift);
}
static constexpr uint16_t get (uint32_t word)
{
return (uint16_t) ((word >> shift) & 0xffff);
}
};
static constexpr uint8_t getMessageType (uint32_t w) noexcept { return U4<0>::get (w); }
static constexpr uint8_t getGroup (uint32_t w) noexcept { return U4<1>::get (w); }
static constexpr uint8_t getStatus (uint32_t w) noexcept { return U4<2>::get (w); }
static constexpr uint8_t getChannel (uint32_t w) noexcept { return U4<3>::get (w); }
};
} // namespace juce::universal_midi_packets
#endif
@@ -0,0 +1,44 @@
/*
==============================================================================
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::universal_midi_packets
{
uint32_t View::size() const noexcept
{
jassert (ptr != nullptr);
return Utils::getNumWordsForMessageType (*ptr);
}
} // namespace juce::universal_midi_packets
@@ -0,0 +1,101 @@
/*
==============================================================================
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.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce::universal_midi_packets
{
/**
Points to a single Universal MIDI Packet.
The packet must be well-formed for member functions to work correctly.
Specifically, the constructor argument must be the beginning of a region of
uint32_t that contains at least `getNumWordsForMessageType (*data)` items,
where `data` is the constructor argument.
NOTE: Instances of this class do not own the memory that they point to!
If you need to store a packet pointed-to by a View for later use, copy
the view contents to a Packets collection, or use the Utils::PacketX types.
@tags{Audio}
*/
class View
{
public:
/** Create an invalid view. */
View() noexcept = default;
/** Create a view of the packet starting at address `d`. */
explicit View (const uint32_t* data) noexcept : ptr (data) {}
/** Get a pointer to the first word in the Universal MIDI Packet currently
pointed-to by this view.
*/
const uint32_t* data() const noexcept { return ptr; }
/** Get the number of 32-words (between 1 and 4 inclusive) in the Universal
MIDI Packet currently pointed-to by this view.
*/
uint32_t size() const noexcept;
/** Get a specific word from this packet.
Passing an `index` that is greater than or equal to the result of `size`
will cause undefined behaviour.
*/
const uint32_t& operator[] (size_t index) const noexcept { return ptr[index]; }
/** Get an iterator pointing to the first word in the packet. */
const uint32_t* begin() const noexcept { return ptr; }
const uint32_t* cbegin() const noexcept { return ptr; }
/** Get an iterator pointing one-past the last word in the packet. */
const uint32_t* end() const noexcept { return ptr + size(); }
const uint32_t* cend() const noexcept { return ptr + size(); }
/** Return true if this view is pointing to the same address as another view. */
bool operator== (const View& other) const noexcept { return ptr == other.ptr; }
/** Return false if this view is pointing to the same address as another view. */
bool operator!= (const View& other) const noexcept { return ! operator== (other); }
private:
const uint32_t* ptr = nullptr;
};
} // namespace juce::universal_midi_packets
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,202 @@
/*
==============================================================================
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.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce::universal_midi_packets
{
/**
Holds a single Universal MIDI Packet.
@tags{Audio}
*/
template <size_t numWords>
class Packet
{
public:
Packet() = default;
template <size_t w = numWords, std::enable_if_t<w == 1, int> = 0>
Packet (uint32_t a)
: contents { { a } }
{
jassert (Utils::getNumWordsForMessageType (a) == 1);
}
template <size_t w = numWords, std::enable_if_t<w == 2, int> = 0>
Packet (uint32_t a, uint32_t b)
: contents { { a, b } }
{
jassert (Utils::getNumWordsForMessageType (a) == 2);
}
template <size_t w = numWords, std::enable_if_t<w == 3, int> = 0>
Packet (uint32_t a, uint32_t b, uint32_t c)
: contents { { a, b, c } }
{
jassert (Utils::getNumWordsForMessageType (a) == 3);
}
template <size_t w = numWords, std::enable_if_t<w == 4, int> = 0>
Packet (uint32_t a, uint32_t b, uint32_t c, uint32_t d)
: contents { { a, b, c, d } }
{
jassert (Utils::getNumWordsForMessageType (a) == 4);
}
template <size_t w, std::enable_if_t<w == numWords, int> = 0>
explicit Packet (const std::array<uint32_t, w>& fullPacket)
: contents (fullPacket)
{
jassert (Utils::getNumWordsForMessageType (fullPacket.front()) == numWords);
}
Packet withMessageType (uint8_t type) const noexcept
{
return withU4<0> (type);
}
Packet withGroup (uint8_t group) const noexcept
{
return withU4<1> (group);
}
Packet withStatus (uint8_t status) const noexcept
{
return withU4<2> (status);
}
Packet withChannel (uint8_t channel) const noexcept
{
return withU4<3> (channel);
}
uint8_t getMessageType() const noexcept { return getU4<0>(); }
uint8_t getGroup() const noexcept { return getU4<1>(); }
uint8_t getStatus() const noexcept { return getU4<2>(); }
uint8_t getChannel() const noexcept { return getU4<3>(); }
template <size_t index>
Packet withU4 (uint8_t value) const noexcept
{
constexpr auto word = index / 8;
auto copy = *this;
std::get<word> (copy.contents) = Utils::U4<index % 8>::set (copy.template getU32<word>(), value);
return copy;
}
template <size_t index>
Packet withU8 (uint8_t value) const noexcept
{
constexpr auto word = index / 4;
auto copy = *this;
std::get<word> (copy.contents) = Utils::U8<index % 4>::set (copy.template getU32<word>(), value);
return copy;
}
template <size_t index>
Packet withU16 (uint16_t value) const noexcept
{
constexpr auto word = index / 2;
auto copy = *this;
std::get<word> (copy.contents) = Utils::U16<index % 2>::set (copy.template getU32<word>(), value);
return copy;
}
template <size_t index>
Packet withU32 (uint32_t value) const noexcept
{
auto copy = *this;
std::get<index> (copy.contents) = value;
return copy;
}
template <size_t index>
uint8_t getU4() const noexcept
{
return Utils::U4<index % 8>::get (this->template getU32<index / 8>());
}
template <size_t index>
uint8_t getU8() const noexcept
{
return Utils::U8<index % 4>::get (this->template getU32<index / 4>());
}
template <size_t index>
uint16_t getU16() const noexcept
{
return Utils::U16<index % 2>::get (this->template getU32<index / 2>());
}
template <size_t index>
uint32_t getU32() const noexcept
{
return std::get<index> (contents);
}
//==============================================================================
using Contents = std::array<uint32_t, numWords>;
using const_iterator = typename Contents::const_iterator;
const_iterator begin() const noexcept { return contents.begin(); }
const_iterator cbegin() const noexcept { return contents.begin(); }
const_iterator end() const noexcept { return contents.end(); }
const_iterator cend() const noexcept { return contents.end(); }
const uint32_t* data() const noexcept { return contents.data(); }
const uint32_t& front() const noexcept { return contents.front(); }
const uint32_t& back() const noexcept { return contents.back(); }
const uint32_t& operator[] (size_t index) const noexcept { return contents[index]; }
private:
Contents contents { {} };
};
using PacketX1 = Packet<1>;
using PacketX2 = Packet<2>;
using PacketX3 = Packet<3>;
using PacketX4 = Packet<4>;
} // namespace juce::universal_midi_packets
#endif
@@ -0,0 +1,105 @@
/*
==============================================================================
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.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce::universal_midi_packets
{
/**
Holds a collection of Universal MIDI Packets.
Unlike MidiBuffer, this collection does not store any additional information
(e.g. timestamps) alongside the raw messages.
If timestamps are required, these can be added to the container in UMP format,
as Jitter Reduction Utility messages.
@tags{Audio}
*/
class Packets
{
public:
/** Adds a single packet to the collection.
The View must be valid for this to work. If the view
points to a malformed message, or if the view points to a region
too short for the contained message, this call will result in
undefined behaviour.
*/
void add (const View& v) { storage.insert (storage.end(), v.cbegin(), v.cend()); }
void add (const PacketX1& p) { addImpl (p); }
void add (const PacketX2& p) { addImpl (p); }
void add (const PacketX3& p) { addImpl (p); }
void add (const PacketX4& p) { addImpl (p); }
/** Pre-allocates space for at least `numWords` 32-bit words in this collection. */
void reserve (size_t numWords) { storage.reserve (numWords); }
/** Removes all previously-added packets from this collection. */
void clear() { storage.clear(); }
/** Gets an iterator pointing to the first packet in this collection. */
Iterator cbegin() const noexcept { return Iterator (data(), size()); }
Iterator begin() const noexcept { return cbegin(); }
/** Gets an iterator pointing one-past the last packet in this collection. */
Iterator cend() const noexcept { return Iterator (data() + size(), 0); }
Iterator end() const noexcept { return cend(); }
/** Gets a pointer to the contents of the collection as a range of raw 32-bit words. */
const uint32_t* data() const noexcept { return storage.data(); }
/** Returns the number of uint32_t words in storage.
Note that this is likely to be larger than the number of packets
currently being stored, as some packets span multiple words.
*/
size_t size() const noexcept { return storage.size(); }
private:
template <size_t numWords>
void addImpl (const Packet<numWords>& p)
{
jassert (Utils::getNumWordsForMessageType (p[0]) == numWords);
add (View (p.data()));
}
std::vector<uint32_t> storage;
};
} // namespace juce::universal_midi_packets
#endif