1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

MIDI: Add support for MIDI 2.0 I/O using Universal MIDI Packets

Includes support for communication with USB and Bluetooth devices, as well as virtual devices.
This commit is contained in:
reuk 2025-04-01 20:32:54 +01:00
parent 3636f2c666
commit ba7593df26
No known key found for this signature in database
65 changed files with 16609 additions and 5301 deletions

View file

@ -36,7 +36,7 @@
namespace juce::universal_midi_packets
{
/** Represents a MIDI message that happened at a particular time.
/** Represents a MIDI message on bytestream transport that happened at a particular time.
Unlike MidiMessage, BytestreamMidiView is non-owning.
*/
@ -63,15 +63,18 @@ struct BytestreamMidiView
return MidiMessage (bytes.data(), (int) bytes.size(), timestamp);
}
bool isSysEx() const
MidiMessageMetadata getMidiMessageMetadata() const
{
return ! bytes.empty() && bytes.front() == std::byte { 0xf0 };
return MidiMessageMetadata { reinterpret_cast<const uint8*> (bytes.data()),
(int) bytes.size(),
(int) timestamp };
}
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.
@ -80,19 +83,39 @@ struct BytestreamMidiView
*/
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.
/** Converts 7-bit data (the most significant bit of each byte must be unset) to a series of
Universal MIDI Packets.
*/
template <typename PacketCallbackFunction>
static void toMidi1 (const BytesOnGroup& m, PacketCallbackFunction&& callback)
static void umpFrom7BitData (BytesOnGroup msg, PacketCallbackFunction&& callback)
{
const auto size = m.bytes.size();
// If this is hit, non-7-bit data was supplied.
// Maybe you forgot to trim the leading/trailing bytes that delimit a bytestream SysEx message.
jassert (std::all_of (msg.bytes.begin(), msg.bytes.end(), [] (std::byte b) { return (b & std::byte { 0x80 }) == std::byte{}; }));
Factory::splitIntoPackets (msg.bytes, 6, [&] (SysEx7::Kind kind, Span<const std::byte> bytesThisTime)
{
const auto packet = Factory::Detail::makeSysEx (msg.group, kind, bytesThisTime);
callback (View (packet.data()));
});
}
/** Converts from a MIDI 1 bytestream to MIDI 1 on Universal MIDI Packets.
@param bytes the bytes in a single well-formed bytestream MIDI message
@param callback a function that accepts a single View argument. This may be called several
times for each invocation of toMidi1 if the bytestream message converts
to multiple Universal MIDI Packets.
*/
template <typename PacketCallbackFunction>
static void toMidi1 (const BytesOnGroup& groupBytes, PacketCallbackFunction&& callback)
{
const auto size = groupBytes.bytes.size();
if (size <= 0)
return;
const auto* data = m.bytes.data();
const auto* data = groupBytes.bytes.data();
const auto firstByte = data[0];
if (firstByte != std::byte { 0xf0 })
@ -107,45 +130,19 @@ struct Conversion
case 3: return 0xffffffff;
}
// This function can only handle a single bytestream MIDI message at a time!
jassertfalse;
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]) };
const std::byte group { (uint8_t) (groupBytes.group & 0xf) };
const PacketX1 packet { mask & Utils::bytesToWord (extraByte | group, 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, { dataOffset, (size_t) numSysExBytes });
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, { dataOffset, (size_t) bytesNow });
callback (View (packet.data()));
}
umpFrom7BitData ({ groupBytes.group, Span (data + 1, size - 2) }, std::forward<PacketCallbackFunction> (callback));
}
/** Widens a 7-bit MIDI 1.0 value to a 8-bit MIDI 2.0 value. */

View file

@ -43,7 +43,7 @@ namespace juce::universal_midi_packets
*/
struct DeviceInfo
{
std::array<std::byte, 3> manufacturer; ///< LSB first
std::array<std::byte, 3> manufacturer;
std::array<std::byte, 2> family; ///< LSB first
std::array<std::byte, 2> modelNumber; ///< LSB first
std::array<std::byte, 4> revision;

View file

@ -36,6 +36,216 @@
namespace juce::universal_midi_packets
{
/**
Holds the data from a stream configuration notification message, with strong types.
@tags{Audio}
*/
class StreamConfiguration
{
public:
[[nodiscard]] StreamConfiguration withProtocol (PacketProtocol p) const { return withFlag (isMidi2, p == PacketProtocol::MIDI_2_0); }
[[nodiscard]] StreamConfiguration withTransmitTimestamp (bool b) const { return withFlag (transmitTimestamp, b); }
[[nodiscard]] StreamConfiguration withReceiveTimestamp (bool b) const { return withFlag (receiveTimestamp, b); }
/** The protocol in use by the endpoint. This protocol will be used for sending and receiving messages. */
[[nodiscard]] PacketProtocol getProtocol() const { return getFlag (isMidi2) ? PacketProtocol::MIDI_2_0 : PacketProtocol::MIDI_1_0; }
/** True if this endpoint intends to send JR timestamps. */
[[nodiscard]] bool getTransmitTimestamp() const { return getFlag (transmitTimestamp); }
/** True if this endpoint expects to receive JR timestamps. */
[[nodiscard]] bool getReceiveTimestamp() const { return getFlag (receiveTimestamp); }
bool operator== (const StreamConfiguration& other) const { return options == other.options; }
bool operator!= (const StreamConfiguration& other) const { return options != other.options; }
private:
enum Flags
{
isMidi2 = 1 << 0,
transmitTimestamp = 1 << 1,
receiveTimestamp = 1 << 2,
};
StreamConfiguration withFlag (Flags f, bool value) const
{
return withMember (*this, &StreamConfiguration::options, value ? (options | f) : (options & ~f));
}
bool getFlag (Flags f) const
{
return (options & f) != 0;
}
int options = 0;
};
/**
Holds the data from an endpoint info notification message, with strong types.
@tags{Audio}
*/
class EndpointInfo
{
auto tie() const { return std::tie (versionMajor, versionMinor, numFunctionBlocks, flags); }
public:
[[nodiscard]] EndpointInfo withVersion (uint8_t major, uint8_t minor) const
{
return withMember (withMember (*this, &EndpointInfo::versionMinor, minor), &EndpointInfo::versionMajor, major);
}
[[nodiscard]] EndpointInfo withNumFunctionBlocks (uint8_t x) const
{
return withMember (*this, &EndpointInfo::numFunctionBlocks, x);
}
[[nodiscard]] EndpointInfo withStaticFunctionBlocks (bool b) const { return withFlag (staticFunctionBlocks, b); }
[[nodiscard]] EndpointInfo withMidi1Support (bool b) const { return withFlag (supportsMidi1, b); }
[[nodiscard]] EndpointInfo withMidi2Support (bool b) const { return withFlag (supportsMidi2, b); }
[[nodiscard]] EndpointInfo withReceiveJRSupport (bool b) const { return withFlag (supportsReceiveJR, b); }
[[nodiscard]] EndpointInfo withTransmitJRSupport (bool b) const { return withFlag (supportsTransmitJR, b); }
/** The major version byte. */
[[nodiscard]] uint8_t getVersionMajor() const { return versionMajor; }
/** The minor version byte. */
[[nodiscard]] uint8_t getVersionMinor() const { return versionMinor; }
/** The number of function blocks declared on this endpoint. */
[[nodiscard]] uint8_t getNumFunctionBlocks() const { return numFunctionBlocks; }
/** True if the function block configuration cannot change. */
[[nodiscard]] bool hasStaticFunctionBlocks() const { return getFlag (staticFunctionBlocks); }
/** True if this endpoint is capable of supporting the MIDI 1.0 protocol. */
[[nodiscard]] bool hasMidi1Support() const { return getFlag (supportsMidi1); }
/** True if this endpoint is capable of supporting the MIDI 2.0 protocol. */
[[nodiscard]] bool hasMidi2Support() const { return getFlag (supportsMidi2); }
/** True if this endpoint is capable of receiving JR timestamps. */
[[nodiscard]] bool hasReceiveJRSupport() const { return getFlag (supportsReceiveJR); }
/** True if this endpoint is capable of transmitting JR timestamps. */
[[nodiscard]] bool hasTransmitJRSupport() const { return getFlag (supportsTransmitJR); }
bool operator== (const EndpointInfo& other) const { return tie() == other.tie(); }
bool operator!= (const EndpointInfo& other) const { return tie() != other.tie(); }
private:
enum Flags
{
staticFunctionBlocks = 1 << 0,
supportsMidi1 = 1 << 1,
supportsMidi2 = 1 << 2,
supportsReceiveJR = 1 << 3,
supportsTransmitJR = 1 << 4,
};
EndpointInfo withFlag (Flags f, bool value) const
{
return withMember (*this, &EndpointInfo::flags, (uint8_t) (value ? (flags | f) : (flags & ~f)));
}
bool getFlag (Flags f) const
{
return (flags & f) != 0;
}
uint8_t versionMajor, versionMinor, numFunctionBlocks, flags;
};
/** Directions that can apply to a Function Block or Group Terminal Block. */
enum class BlockDirection : uint8_t
{
unknown = 0b00, ///< Block direction is unknown or undeclared
receiver = 0b01, ///< Block is a receiver of messages
sender = 0b10, ///< Block is a sender of messages
bidirectional = 0b11, ///< Block both sends and receives messages
};
/** UI hints that can apply to a Function Block or Group Terminal Block. */
enum class BlockUiHint : uint8_t
{
unknown = 0b00, ///< Block direction is unknown or undeclared
receiver = 0b01, ///< Block is a receiver of messages
sender = 0b10, ///< Block is a sender of messages
bidirectional = 0b11, ///< Block both sends and receives messages
};
/** Describes how a MIDI 1.0 port maps to a given Block, if applicable. */
enum class BlockMIDI1ProxyKind : uint8_t
{
inapplicable = 0b00, ///< Block does not represent a MIDI 1.0 port
unrestrictedBandwidth = 0b01, ///< Block represents a MIDI 1.0 port and can handle high bandwidth
restrictedBandwidth = 0b10, ///< Block represents a MIDI 1.0 port that requires restricted bandwidth
};
/**
Holds the data from a function block info notification message, with strong types.
@tags{Audio}
*/
class BlockInfo
{
public:
[[nodiscard]] BlockInfo withEnabled (bool x) const { return withMember (*this, &BlockInfo::enabled, x); }
[[nodiscard]] BlockInfo withUiHint (BlockUiHint x) const { return withMember (*this, &BlockInfo::flags, replaceBits<4, 2> (flags, (uint8_t) x)); }
[[nodiscard]] BlockInfo withMIDI1ProxyKind (BlockMIDI1ProxyKind x) const { return withMember (*this, &BlockInfo::flags, replaceBits<2, 2> (flags, (uint8_t) x)); }
[[nodiscard]] BlockInfo withDirection (BlockDirection x) const { return withMember (*this, &BlockInfo::flags, replaceBits<0, 2> (flags, (uint8_t) x)); }
[[nodiscard]] BlockInfo withFirstGroup (uint8_t x) const { return withMember (*this, &BlockInfo::firstGroup, x); }
[[nodiscard]] BlockInfo withNumGroups (uint8_t x) const { return withMember (*this, &BlockInfo::numGroups, x); }
[[nodiscard]] BlockInfo withCiVersion (uint8_t x) const { return withMember (*this, &BlockInfo::ciVersion, x); }
[[nodiscard]] BlockInfo withMaxSysex8Streams (uint8_t x) const { return withMember (*this, &BlockInfo::numSysex8Streams, x); }
/** True if the block is enabled/active, false otherwise. */
bool isEnabled() const { return enabled; }
/** The directionality of the block, for display to the user. */
BlockUiHint getUiHint() const { return (BlockUiHint) getBits<4, 2> (flags); }
/** The kind of MIDI 1.0 proxy represented by this block, if any. */
BlockMIDI1ProxyKind getMIDI1ProxyKind() const { return (BlockMIDI1ProxyKind) getBits<2, 2> (flags); }
/** The actual directionality of the block. */
BlockDirection getDirection() const { return (BlockDirection) getBits<0, 2> (flags); }
/** The zero-based index of the first group in the block. */
uint8_t getFirstGroup() const { return firstGroup; }
/** The number of groups contained in the block, must be one or greater. */
uint8_t getNumGroups() const { return numGroups; }
/** The CI version supported by this block. Implies a bidirectional block. */
uint8_t getCiVersion() const { return ciVersion; }
/** The number of simultaneous SysEx8 streams supported on this block. */
uint8_t getMaxSysex8Streams() const { return numSysex8Streams; }
bool operator== (const BlockInfo& other) const
{
const auto tie = [] (auto& x)
{
return std::tuple (x.enabled, x.flags, x.firstGroup, x.numGroups, x.ciVersion, x.numSysex8Streams);
};
return tie (*this) == tie (other);
}
bool operator!= (const BlockInfo& other) const
{
return ! operator== (other);
}
private:
template <auto position, auto numBits, typename Value>
static Value replaceBits (Value value, Value replacement)
{
constexpr auto mask = ((Value) 1 << numBits) - 1;
const auto maskedValue = value & ~(mask << position);
return (Value) (maskedValue | (replacement << position));
}
template <auto position, auto numBits, typename Value>
static Value getBits (Value value)
{
constexpr auto mask = ((Value) 1 << numBits) - 1;
return (value >> position) & mask;
}
uint8_t enabled{};
uint8_t flags{};
uint8_t firstGroup{};
uint8_t numGroups{};
uint8_t ciVersion{};
uint8_t numSysex8Streams{};
};
/**
This struct holds functions that can be used to create different kinds
of Universal MIDI Packet.
@ -44,6 +254,36 @@ namespace juce::universal_midi_packets
*/
struct Factory
{
template <typename Callback>
static void splitIntoPackets (Span<const std::byte> bytes, size_t bytesPerPacket, Callback&& callback)
{
const auto numPackets = (bytes.size() / bytesPerPacket) + ((bytes.size() % bytesPerPacket) != 0);
auto* dataOffset = bytes.data();
if (numPackets <= 1)
{
callback (SysEx7::Kind::complete, bytes);
return;
}
for (auto i = static_cast<ssize_t> (bytes.size()); i > 0; i -= (ssize_t) bytesPerPacket, dataOffset += bytesPerPacket)
{
const auto kind = [&]
{
if (i == (ssize_t) bytes.size())
return SysEx7::Kind::begin;
if (i <= (ssize_t) bytesPerPacket)
return SysEx7::Kind::end;
return SysEx7::Kind::continuation;
}();
const auto bytesNow = std::min ((ssize_t) bytesPerPacket, i);
callback (kind, Span (dataOffset, (size_t) bytesNow));
}
}
/** @internal */
struct Detail
{
@ -90,6 +330,41 @@ struct Factory
return PacketX4 { words };
}
static PacketX4 makePacketX4 (Span<const std::byte> header,
Span<const std::byte> data)
{
jassert (data.size() <= 14);
std::array<std::byte, 16> bytes{{}};
std::copy (header.begin(), header.end(), bytes.begin());
std::copy (data.begin(), data.end(), std::next (bytes.begin(), (ptrdiff_t) header.size()));
std::array<uint32_t, 4> words{};
size_t index = 0;
for (auto& word : words)
word = ByteOrder::bigEndianInt (bytes.data() + 4 * index++);
return PacketX4 { words };
}
static PacketX4 makeStreamSubpacket (std::byte status,
SysEx7::Kind kind,
Span<const std::byte> data)
{
jassert (data.size() <= 14);
const std::byte header[] { std::byte (0xf0) | std::byte ((uint8_t) kind << 2), status, };
return makePacketX4 (header, data);
}
static PacketX4 makeStreamConfiguration (StreamConfiguration options)
{
return Detail::makeStream().withU8<0x2> (options.getProtocol() == PacketProtocol::MIDI_2_0 ? 0x2 : 0x1)
.withU8<0x3> ((options.getReceiveTimestamp() ? 0x2 : 0x0) | (options.getTransmitTimestamp() ? 0x1 : 0x0));
}
};
static PacketX1 makeNoop (uint8_t group)
@ -520,6 +795,23 @@ struct Factory
.withU8<7> ((uint8_t) filterBitmap);
}
static PacketX4 makeEndpointInfoNotification (const EndpointInfo& info)
{
return Detail::makeStream().withU8<1> (1)
.withU8<2> (info.getVersionMajor())
.withU8<3> (info.getVersionMinor())
.withU8<4> (info.getNumFunctionBlocks() | (info.hasStaticFunctionBlocks() ? 0x80 : 0x00))
.withU8<6> ((info.hasMidi1Support() ? 0x1 : 0x0) | (info.hasMidi2Support() ? 0x2 : 0x0))
.withU8<7> ((info.hasTransmitJRSupport() ? 0x1 : 0x0) | (info.hasReceiveJRSupport() ? 0x2 : 0x0));
}
static PacketX4 makeFunctionBlockDiscovery (uint8_t block, std::byte filterBitmap)
{
return Detail::makeStream().withU8<1> (0x10)
.withU8<2> (block)
.withU8<3> ((uint8_t) filterBitmap);
}
static PacketX4 makeDeviceIdentityNotification (DeviceInfo info)
{
return Detail::makeStream().withU8<0x1> (2)
@ -535,6 +827,87 @@ struct Factory
.withU8<0xe> ((uint8_t) info.revision[2])
.withU8<0xf> ((uint8_t) info.revision[3]);
}
template <typename Fn>
static bool makeEndpointNameNotification (const String& bytes, Fn&& fn)
{
constexpr auto maxSize = 98;
if (maxSize <= bytes.getNumBytesAsUTF8())
return false;
const Span byteSpan { reinterpret_cast<const std::byte*> (bytes.toRawUTF8()), bytes.getNumBytesAsUTF8() };
splitIntoPackets (byteSpan, 14, [&] (SysEx7::Kind kind, Span<const std::byte> bytesThisTime)
{
const auto packet = Detail::makeStreamSubpacket (std::byte (3), kind, bytesThisTime);
fn (View (packet.data()));
});
return true;
}
template <typename Fn>
static bool makeProductInstanceIdNotification (const String& bytes, Fn&& fn)
{
constexpr auto maxSize = 42;
if (maxSize < bytes.getNumBytesAsUTF8())
return false;
const Span byteSpan { reinterpret_cast<const std::byte*> (bytes.toRawUTF8()), bytes.getNumBytesAsUTF8() };
splitIntoPackets (byteSpan, 14, [&] (SysEx7::Kind kind, Span<const std::byte> bytesThisTime)
{
const auto packet = Detail::makeStreamSubpacket (std::byte (4), kind, bytesThisTime);
fn (View (packet.data()));
});
return true;
}
template <typename Fn>
static bool makeFunctionBlockNameNotification (uint8_t index, const String& bytes, Fn&& fn)
{
constexpr auto maxSize = 91;
if (maxSize < bytes.getNumBytesAsUTF8())
return false;
const Span byteSpan { reinterpret_cast<const std::byte*> (bytes.toRawUTF8()), bytes.getNumBytesAsUTF8() };
splitIntoPackets (byteSpan, 13, [&] (SysEx7::Kind kind, Span<const std::byte> bytesThisTime)
{
const std::byte header[] { std::byte (0xf0) | std::byte ((uint8_t) kind << 2), std::byte (0x12), std::byte (index) };
fn (View (Detail::makePacketX4 (header, bytesThisTime).data()));
});
return true;
}
static PacketX4 makeFunctionBlockInfoNotification (uint8_t index, const BlockInfo& info)
{
const auto flags = ((uint8_t) info.getDirection() << 0)
| ((uint8_t) info.getMIDI1ProxyKind() << 2)
| ((uint8_t) info.getUiHint() << 4);
return Detail::makeStream().withU8<0x1> (0x11)
.withU8<0x2> ((uint8_t) (index | (info.isEnabled() << 7)))
.withU8<0x3> ((uint8_t) flags)
.withU8<0x4> (info.getFirstGroup())
.withU8<0x5> (info.getNumGroups())
.withU8<0x6> (info.getCiVersion())
.withU8<0x7> (info.getMaxSysex8Streams());
}
static PacketX4 makeStreamConfigurationRequest (StreamConfiguration options)
{
return Detail::makeStreamConfiguration (options).withU8<0x1> (5);
}
static PacketX4 makeStreamConfigurationNotification (StreamConfiguration options)
{
return Detail::makeStreamConfiguration (options).withU8<0x1> (6);
}
};
} // namespace juce::universal_midi_packets

View file

@ -36,15 +36,25 @@
namespace juce::universal_midi_packets
{
/** The kinds of MIDI protocol that can be formatted into Universal MIDI Packets. */
enum class PacketProtocol
/** Kinds of MIDI message transport.
*/
enum class Transport : uint8_t
{
bytestream, ///< A stream of variable-length messages. Suitable for MIDI 1.0.
ump, ///< A stream of 32-bit words. Suitable for MIDI-1UP and MIDI 2.0.
};
/** The kinds of MIDI protocol that can be formatted into Universal MIDI Packets.
*/
enum class PacketProtocol : uint8_t
{
MIDI_1_0,
MIDI_2_0,
};
/** All kinds of MIDI protocol understood by JUCE. */
enum class MidiProtocol
/** All kinds of MIDI protocol understood by JUCE.
*/
enum class MidiProtocol : uint8_t
{
bytestream,
UMP_MIDI_1_0,

View file

@ -128,10 +128,20 @@ struct Utils
stream = 0xf,
};
static constexpr bool hasGroup (MessageKind k)
{
return ! isGroupless (k);
}
static constexpr bool isGroupless (MessageKind k)
{
return k == MessageKind::utility || k == MessageKind::stream;
}
static constexpr MessageKind getMessageType (uint32_t w) noexcept { return MessageKind { U4<0>::get (w) }; }
static constexpr uint8_t getGroup (uint32_t w) noexcept { return U4<1>::get (w); }
static constexpr std::byte getStatus (uint32_t w) noexcept { return std::byte { U4<2>::get (w) }; }
static constexpr uint8_t getChannel (uint32_t w) noexcept { return U4<3>::get (w); }
static constexpr uint8_t getGroup (uint32_t w) noexcept { return U4<1>::get (w); }
};
} // namespace juce::universal_midi_packets

View file

@ -65,7 +65,7 @@ public:
*/
const uint32_t* data() const noexcept { return ptr; }
/** Get the number of 32-words (between 1 and 4 inclusive) in the Universal
/** Get the number of 32 bit words (between 1 and 4 inclusive) in the Universal
MIDI Packet currently pointed-to by this view.
*/
uint32_t size() const noexcept;

View file

@ -169,6 +169,16 @@ public:
return std::get<index> (contents);
}
bool operator== (const Packet& other) const
{
return contents == other.contents;
}
bool operator!= (const Packet& other) const
{
return contents != other.contents;
}
//==============================================================================
using Contents = std::array<uint32_t, numWords>;