1,331 changes: 1,331 additions & 0 deletions libs/juce7/source/modules/juce_audio_basics/midi/juce_MidiMessage.cpp

Large diffs are not rendered by default.

986 changes: 986 additions & 0 deletions libs/juce7/source/modules/juce_audio_basics/midi/juce_MidiMessage.h

Large diffs are not rendered by default.

Large diffs are not rendered by default.

@@ -0,0 +1,315 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR 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
375 changes: 375 additions & 0 deletions libs/juce7/source/modules/juce_audio_basics/midi/juce_MidiRPN.cpp
@@ -0,0 +1,375 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/

namespace juce
{

MidiRPNDetector::MidiRPNDetector() noexcept
{
}

MidiRPNDetector::~MidiRPNDetector() noexcept
{
}

bool MidiRPNDetector::parseControllerMessage (int midiChannel,
int controllerNumber,
int controllerValue,
MidiRPNMessage& result) noexcept
{
jassert (midiChannel > 0 && midiChannel <= 16);
jassert (controllerNumber >= 0 && controllerNumber < 128);
jassert (controllerValue >= 0 && controllerValue < 128);

return states[midiChannel - 1].handleController (midiChannel, controllerNumber, controllerValue, result);
}

void MidiRPNDetector::reset() noexcept
{
for (int i = 0; i < 16; ++i)
{
states[i].parameterMSB = 0xff;
states[i].parameterLSB = 0xff;
states[i].resetValue();
states[i].isNRPN = false;
}
}

//==============================================================================
bool MidiRPNDetector::ChannelState::handleController (int channel,
int controllerNumber,
int value,
MidiRPNMessage& result) 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); return sendIfReady (channel, result);
case 0x26: valueLSB = uint8 (value); break;

default: break;
}

return false;
}

void MidiRPNDetector::ChannelState::resetValue() noexcept
{
valueMSB = 0xff;
valueLSB = 0xff;
}

//==============================================================================
bool MidiRPNDetector::ChannelState::sendIfReady (int channel, MidiRPNMessage& result) noexcept
{
if (parameterMSB < 0x80 && parameterLSB < 0x80)
{
if (valueMSB < 0x80)
{
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 true;
}
}

return false;
}

//==============================================================================
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));

uint8 parameterLSB = uint8 (parameterNumber & 0x0000007f);
uint8 parameterMSB = uint8 (parameterNumber >> 7);

uint8 valueLSB = use14BitValue ? uint8 (value & 0x0000007f) : 0x00;
uint8 valueMSB = use14BitValue ? uint8 (value >> 7) : uint8 (value);

uint8 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);

// sending the value LSB is optional, but must come before sending the value MSB:
if (use14BitValue)
buffer.addEvent (MidiMessage (channelByte, 0x26, valueLSB), 0);

buffer.addEvent (MidiMessage (channelByte, 0x06, valueMSB), 0);

return buffer;
}


//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS

class MidiRPNDetectorTests : public UnitTest
{
public:
MidiRPNDetectorTests()
: UnitTest ("MidiRPNDetector class", UnitTestCategories::midi)
{}

void runTest() override
{
beginTest ("7-bit RPN");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (2, 101, 0, rpn));
expect (! detector.parseControllerMessage (2, 100, 7, rpn));
expect (detector.parseControllerMessage (2, 6, 42, rpn));

expectEquals (rpn.channel, 2);
expectEquals (rpn.parameterNumber, 7);
expectEquals (rpn.value, 42);
expect (! rpn.isNRPN);
expect (! rpn.is14BitValue);
}

beginTest ("14-bit RPN");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (1, 100, 44, rpn));
expect (! detector.parseControllerMessage (1, 101, 2, rpn));
expect (! detector.parseControllerMessage (1, 38, 94, rpn));
expect (detector.parseControllerMessage (1, 6, 1, rpn));

expectEquals (rpn.channel, 1);
expectEquals (rpn.parameterNumber, 300);
expectEquals (rpn.value, 222);
expect (! rpn.isNRPN);
expect (rpn.is14BitValue);
}

beginTest ("RPNs on multiple channels simultaneously");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (1, 100, 44, rpn));
expect (! detector.parseControllerMessage (2, 101, 0, rpn));
expect (! detector.parseControllerMessage (1, 101, 2, rpn));
expect (! detector.parseControllerMessage (2, 100, 7, rpn));
expect (! detector.parseControllerMessage (1, 38, 94, rpn));
expect (detector.parseControllerMessage (2, 6, 42, rpn));

expectEquals (rpn.channel, 2);
expectEquals (rpn.parameterNumber, 7);
expectEquals (rpn.value, 42);
expect (! rpn.isNRPN);
expect (! rpn.is14BitValue);

expect (detector.parseControllerMessage (1, 6, 1, rpn));

expectEquals (rpn.channel, 1);
expectEquals (rpn.parameterNumber, 300);
expectEquals (rpn.value, 222);
expect (! rpn.isNRPN);
expect (rpn.is14BitValue);
}

beginTest ("14-bit RPN with value within 7-bit range");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (16, 100, 0 , rpn));
expect (! detector.parseControllerMessage (16, 101, 0, rpn));
expect (! detector.parseControllerMessage (16, 38, 3, rpn));
expect (detector.parseControllerMessage (16, 6, 0, rpn));

expectEquals (rpn.channel, 16);
expectEquals (rpn.parameterNumber, 0);
expectEquals (rpn.value, 3);
expect (! rpn.isNRPN);
expect (rpn.is14BitValue);
}

beginTest ("invalid RPN (wrong order)");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (2, 6, 42, rpn));
expect (! detector.parseControllerMessage (2, 101, 0, rpn));
expect (! detector.parseControllerMessage (2, 100, 7, rpn));
}

beginTest ("14-bit RPN interspersed with unrelated CC messages");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (16, 3, 80, rpn));
expect (! detector.parseControllerMessage (16, 100, 0 , rpn));
expect (! detector.parseControllerMessage (16, 4, 81, rpn));
expect (! detector.parseControllerMessage (16, 101, 0, rpn));
expect (! detector.parseControllerMessage (16, 5, 82, rpn));
expect (! detector.parseControllerMessage (16, 5, 83, rpn));
expect (! detector.parseControllerMessage (16, 38, 3, rpn));
expect (! detector.parseControllerMessage (16, 4, 84, rpn));
expect (! detector.parseControllerMessage (16, 3, 85, rpn));
expect (detector.parseControllerMessage (16, 6, 0, rpn));

expectEquals (rpn.channel, 16);
expectEquals (rpn.parameterNumber, 0);
expectEquals (rpn.value, 3);
expect (! rpn.isNRPN);
expect (rpn.is14BitValue);
}

beginTest ("14-bit NRPN");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (1, 98, 44, rpn));
expect (! detector.parseControllerMessage (1, 99 , 2, rpn));
expect (! detector.parseControllerMessage (1, 38, 94, rpn));
expect (detector.parseControllerMessage (1, 6, 1, rpn));

expectEquals (rpn.channel, 1);
expectEquals (rpn.parameterNumber, 300);
expectEquals (rpn.value, 222);
expect (rpn.isNRPN);
expect (rpn.is14BitValue);
}

beginTest ("reset");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (2, 101, 0, rpn));
detector.reset();
expect (! detector.parseControllerMessage (2, 100, 7, rpn));
expect (! detector.parseControllerMessage (2, 6, 42, rpn));
}
}
};

static MidiRPNDetectorTests MidiRPNDetectorUnitTests;

//==============================================================================
class MidiRPNGeneratorTests : 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)
{
MidiRPNMessage result = MidiRPNMessage();
MidiRPNDetector detector;

for (const auto metadata : midiBuffer)
{
const auto midiMessage = metadata.getMessage();

if (detector.parseControllerMessage (midiMessage.getChannel(),
midiMessage.getControllerNumber(),
midiMessage.getControllerValue(),
result))
break;
}

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
153 changes: 153 additions & 0 deletions libs/juce7/source/modules/juce_audio_basics/midi/juce_MidiRPN.h
@@ -0,0 +1,153 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR 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;

/** Destructor. */
~MidiRPNDetector() noexcept;

/** Resets the RPN detector's internal state, so that it forgets about
previously received MIDI CC messages.
*/
void reset() noexcept;

//==============================================================================
/** Takes the next in a stream of incoming MIDI CC messages and returns true
if it forms the last of a sequence that makes an RPN or NPRN.
If this returns true, then the RPNMessage object supplied will be
filled-out with the message's details.
(If it returns false then the RPNMessage object will be unchanged).
*/
bool parseControllerMessage (int midiChannel,
int controllerNumber,
int controllerValue,
MidiRPNMessage& result) noexcept;

private:
//==============================================================================
struct ChannelState
{
bool handleController (int channel, int controllerNumber,
int value, MidiRPNMessage&) noexcept;
void resetValue() noexcept;
bool sendIfReady (int channel, MidiRPNMessage&) 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
47 changes: 47 additions & 0 deletions libs/juce7/source/modules/juce_audio_basics/midi/ump/juce_UMP.h
@@ -0,0 +1,47 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/

#include "../juce_MidiDataConcatenator.h"

#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,330 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/

#ifndef DOXYGEN

namespace juce
{
namespace universal_midi_packets
{

/**
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 MidiMessage& m, PacketCallbackFunction&& callback)
{
const auto* data = m.getRawData();
const auto firstByte = data[0];
const auto size = m.getRawDataSize();

if (firstByte != 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 = (uint8_t) ((((firstByte & 0xf0) == 0xf0) ? 0x1 : 0x2) << 0x4);
const PacketX1 packet { mask & Utils::bytesToWord (extraByte, data[0], data[1], data[2]) };
callback (View (packet.data()));
return;
}

const auto numSysExBytes = m.getSysExDataSize();
const auto numMessages = SysEx7::getNumPacketsRequiredForDataSize ((uint32_t) numSysExBytes);
auto* dataOffset = m.getSysExData();

if (numMessages <= 1)
{
const auto packet = Factory::makeSysExIn1Packet (0, (uint8_t) numSysExBytes, dataOffset);
callback (View (packet.data()));
return;
}

constexpr auto byteIncrement = 6;

for (auto i = 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()));
}
}

/** Converts a MidiMessage to one or more messages in UMP format, using
the MIDI 1.0 Protocol.
`packets` is an out-param to allow the caller to control
allocation/deallocation. Returning a new Packets object would
require every call to toMidi1 to allocate. With this version, no
allocations will occur, provided that `packets` has adequate reserved
space.
*/
static void toMidi1 (const MidiMessage& m, Packets& packets)
{
toMidi1 (m, [&] (const View& view) { packets.add (view); });
}

/** 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 = (uint8_t) ((0x2 << 0x4) | Utils::getGroup (firstWord));

switch (status)
{
case 0x8: // note off
case 0x9: // note on
case 0xa: // poly pressure
case 0xb: // control change
{
const auto statusAndChannel = (uint8_t) ((firstWord >> 0x10) & 0xff);
const auto byte2 = (uint8_t) ((firstWord >> 0x08) & 0xff);
const auto byte3 = 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 == 0;
const auto correctedByte = (uint8_t) (needsCorrection ? 1 : byte3);

const auto shouldIgnore = status == 0xb && [&]
{
switch (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 = (uint8_t) ((firstWord >> 0x10) & 0xff);
const auto byte2 = scaleTo7 (v[1]);

const PacketX1 packet { Utils::bytesToWord (typeAndGroup,
statusAndChannel,
byte2,
0) };
callback (View (packet.data()));
return;
}

case 0x2: // rpn
case 0x3: // nrpn
{
const auto ccX = (uint8_t) (status == 0x2 ? 101 : 99);
const auto ccY = (uint8_t) (status == 0x2 ? 100 : 98);
const auto statusAndChannel = (uint8_t) ((0xb << 0x4) | Utils::getChannel (firstWord));
const auto data = scaleTo14 (v[1]);

const PacketX1 packets[]
{
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, ccX, (uint8_t) ((firstWord >> 0x8) & 0x7f)) },
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, ccY, (uint8_t) ((firstWord >> 0x0) & 0x7f)) },
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, 6, (uint8_t) ((data >> 0x7) & 0x7f)) },
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, 38, (uint8_t) ((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 = (uint8_t) ((0xb << 0x4) | Utils::getChannel (firstWord));
const auto secondWord = v[1];

const PacketX1 packets[]
{
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, 0, (uint8_t) ((secondWord >> 0x8) & 0x7f)) },
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, 32, (uint8_t) ((secondWord >> 0x0) & 0x7f)) },
};

for (const auto& packet : packets)
callback (View (packet.data()));
}

const auto statusAndChannel = (uint8_t) ((0xc << 0x4) | Utils::getChannel (firstWord));
const PacketX1 packet { Utils::bytesToWord (typeAndGroup,
statusAndChannel,
(uint8_t) ((v[1] >> 0x18) & 0x7f),
0) };
callback (View (packet.data()));
return;
}

case 0xe: // pitch bend
{
const auto data = scaleTo14 (v[1]);
const auto statusAndChannel = (uint8_t) ((firstWord >> 0x10) & 0xff);
const PacketX1 packet { Utils::bytesToWord (typeAndGroup,
statusAndChannel,
(uint8_t) (data & 0x7f),
(uint8_t) ((data >> 7) & 0x7f)) };
callback (View (packet.data()));
return;
}

default: // other message types do not translate
return;
}
}
};

}
}

#endif
@@ -0,0 +1,169 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/

#ifndef DOXYGEN

namespace juce
{
namespace 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 MidiMessage& 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 MidiMessage& 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
{
public:
explicit GenericUMPConverter (PacketProtocol m)
: mode (m) {}

void reset()
{
std::get<1> (converters).reset();
}

template <typename Fn>
void convert (const MidiMessage& m, Fn&& fn)
{
switch (mode)
{
case PacketProtocol::MIDI_1_0: return std::get<0> (converters).convert (m, std::forward<Fn> (fn));
case PacketProtocol::MIDI_2_0: return std::get<1> (converters).convert (m, std::forward<Fn> (fn));
}
}

template <typename Fn>
void convert (const View& v, Fn&& fn)
{
switch (mode)
{
case PacketProtocol::MIDI_1_0: return std::get<0> (converters).convert (v, std::forward<Fn> (fn));
case PacketProtocol::MIDI_2_0: return std::get<1> (converters).convert (v, std::forward<Fn> (fn));
}
}

template <typename Fn>
void convert (Iterator begin, Iterator end, Fn&& fn)
{
std::for_each (begin, end, [&] (const View& v)
{
convert (v, 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;
};
}
}

#endif
@@ -0,0 +1,202 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/

#ifndef DOXYGEN

namespace juce
{
namespace 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));

#if JUCE_MINGW
#define JUCE_MINGW_HIDDEN_VISIBILITY __attribute__ ((visibility ("hidden")))
#else
#define JUCE_MINGW_HIDDEN_VISIBILITY
#endif

struct JUCE_MINGW_HIDDEN_VISIBILITY Callback
{
Callback (BytestreamToUMPDispatcher& d, CallbackPtr c)
: dispatch (d), callbackPtr (c) {}

void handleIncomingMidiMessage (void*, const MidiMessage& msg) const
{
Conversion::toMidi1 (msg, [&] (const View& view)
{
dispatch.converter.convert (view, *callbackPtr);
});
}

void handlePartialSysexMessage (void*, const uint8_t*, int, double) const {}

BytestreamToUMPDispatcher& dispatch;
CallbackPtr callbackPtr = nullptr;
};

#undef JUCE_MINGW_HIDDEN_VISIBILITY

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;
};

}
}

#endif
538 changes: 538 additions & 0 deletions libs/juce7/source/modules/juce_audio_basics/midi/ump/juce_UMPFactory.h

Large diffs are not rendered by default.

@@ -0,0 +1,130 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/

#ifndef DOXYGEN

namespace juce
{
namespace 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
: view (ptr)
#if JUCE_DEBUG
, bytesRemaining (bytes)
#endif
{
ignoreUnused (bytes);
}

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
};

}
}

#endif
@@ -0,0 +1,217 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/

#ifndef DOXYGEN

namespace juce
{
namespace 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)
callback (fromUmp (PacketX1 { firstWord }, time));

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 (0xf0);
}

template <typename MessageCallback>
void terminateSysExMessage (MessageCallback&& callback)
{
pendingSysExData.push_back (0xf7);
callback (MidiMessage (pendingSysExData.data(),
int (pendingSysExData.size()),
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<uint8_t> pendingSysExData;

double pendingSysExTime = 0.0;
};

}
}

#endif
@@ -0,0 +1,195 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/

namespace juce
{
namespace universal_midi_packets
{

PacketX2 Midi1ToMidi2DefaultTranslator::processNoteOnOrOff (const HelperValues helpers)
{
const auto velocity = helpers.byte2;
const auto needsConversion = (helpers.byte0 >> 0x4) == 0x9 && velocity == 0;
const auto firstByte = needsConversion ? (uint8_t) ((0x8 << 0x4) | (helpers.byte0 & 0xf))
: helpers.byte0;

return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, firstByte, helpers.byte1, 0),
(uint32_t) (Conversion::scaleTo16 (velocity) << 0x10)
};
}

PacketX2 Midi1ToMidi2DefaultTranslator::processPolyPressure (const HelperValues helpers)
{
return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, helpers.byte0, helpers.byte1, 0),
Conversion::scaleTo32 (helpers.byte2)
};
}

bool Midi1ToMidi2DefaultTranslator::processControlChange (const HelperValues helpers,
PacketX2& packet)
{
const auto statusAndChannel = helpers.byte0;
const auto cc = 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 & 0xf);
const auto channel = (uint8_t) (statusAndChannel & 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) (((msb & 0x7f) << 7) | (lsb & 0x7f));

const auto newStatus = (uint8_t) (accumulator.getKind() == PnKind::nrpn ? 0x3 : 0x2);

packet = PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, (uint8_t) ((newStatus << 0x4) | channel), bank, index),
Conversion::scaleTo32 (value)
};
return true;
}

return false;
}

if (cc == 0)
{
groupBanks[group][channel].setMsb (byte);
return false;
}

if (cc == 32)
{
groupBanks[group][channel].setLsb (byte);
return false;
}

packet = PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, statusAndChannel, cc, 0),
Conversion::scaleTo32 (helpers.byte2)
};
return true;
}

PacketX2 Midi1ToMidi2DefaultTranslator::processProgramChange (const HelperValues helpers) const
{
const auto group = (uint8_t) (helpers.typeAndGroup & 0xf);
const auto channel = (uint8_t) (helpers.byte0 & 0xf);
const auto bank = groupBanks[group][channel];
const auto valid = bank.isValid();

return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, helpers.byte0, 0, valid ? 1 : 0),
Utils::bytesToWord (helpers.byte1, 0, valid ? bank.getMsb() : 0, valid ? bank.getLsb() : 0)
};
}

PacketX2 Midi1ToMidi2DefaultTranslator::processChannelPressure (const HelperValues helpers)
{
return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, helpers.byte0, 0, 0),
Conversion::scaleTo32 (helpers.byte1)
};
}

PacketX2 Midi1ToMidi2DefaultTranslator::processPitchBend (const HelperValues helpers)
{
const auto lsb = helpers.byte1;
const auto msb = helpers.byte2;
const auto value = (uint16_t) (msb << 7 | lsb);

return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, helpers.byte0, 0, 0),
Conversion::scaleTo32 (value)
};
}

bool Midi1ToMidi2DefaultTranslator::PnAccumulator::addByte (uint8_t cc, uint8_t 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;
}

}
}
@@ -0,0 +1,191 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/

#ifndef DOXYGEN

namespace juce
{
namespace 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
{
(uint8_t) ((0x4 << 0x4) | Utils::getGroup (firstWord)),
(uint8_t) ((firstWord >> 0x10) & 0xff),
(uint8_t) ((firstWord >> 0x08) & 0x7f),
(uint8_t) ((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
{
uint8_t typeAndGroup;
uint8_t byte0;
uint8_t byte1;
uint8_t byte2;
};

static PacketX2 processNoteOnOrOff (const HelperValues helpers);
static PacketX2 processPolyPressure (const HelperValues helpers);

bool processControlChange (const HelperValues helpers, PacketX2& packet);

PacketX2 processProgramChange (const HelperValues helpers) const;

static PacketX2 processChannelPressure (const HelperValues helpers);
static PacketX2 processPitchBend (const HelperValues helpers);

class PnAccumulator
{
public:
bool addByte (uint8_t cc, uint8_t byte);

const std::array<uint8_t, 4>& getBytes() const noexcept { return bytes; }
PnKind getKind() const noexcept { return kind; }

private:
std::array<uint8_t, 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;
};

}
}

#endif
@@ -0,0 +1,48 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/

#ifndef DOXYGEN

namespace juce
{
namespace 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,
};

}
}

#endif
@@ -0,0 +1,46 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/

#ifndef DOXYGEN

namespace juce
{
namespace 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;
};

}
}

#endif
@@ -0,0 +1,53 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/

namespace juce
{
namespace 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
{
{ { packet.getU8<2>(),
packet.getU8<3>(),
packet.getU8<4>(),
packet.getU8<5>(),
packet.getU8<6>(),
packet.getU8<7>() } },
jmin (numBytes, maxBytes)
};
}

}
}
@@ -0,0 +1,77 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/

#ifndef DOXYGEN

namespace juce
{
namespace 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<uint8_t, 6> data;
uint8_t size;
};

/** Extracts the data bytes from a 64-bit data message. */
static PacketBytes getDataBytes (const PacketX2& packet);
};

}
}

#endif
@@ -0,0 +1,59 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/

namespace juce
{
namespace 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;
}

}
}
117 changes: 117 additions & 0 deletions libs/juce7/source/modules/juce_audio_basics/midi/ump/juce_UMPUtils.h
@@ -0,0 +1,117 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/

#ifndef DOXYGEN

namespace juce
{
namespace 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 (uint8_t a, uint8_t b, uint8_t c, uint8_t d)
{
return uint32_t (a << 0x18 | b << 0x10 | c << 0x08 | 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); }
};

}
}

#endif
@@ -0,0 +1,35 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/

namespace juce
{
namespace universal_midi_packets
{

uint32_t View::size() const noexcept
{
jassert (ptr != nullptr);
return Utils::getNumWordsForMessageType (*ptr);
}

}
}
@@ -0,0 +1,92 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/

#ifndef DOXYGEN

namespace juce
{
namespace 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(*ddata)` 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;
};

}
}

#endif