From 9032f589ebdc5f6fff00216b567175a39547c782 Mon Sep 17 00:00:00 2001 From: reuk Date: Wed, 11 Nov 2020 19:11:23 +0000 Subject: [PATCH] CoreMIDI: Enable support for new API --- .../juce_audio_devices/juce_audio_devices.cpp | 35 +- .../ump/juce_UMPBytestreamInputHandler.h | 120 ++ .../midi_io/ump/juce_UMPConversion.h | 326 +++++ .../midi_io/ump/juce_UMPConverters.h | 139 ++ .../midi_io/ump/juce_UMPDispatcher.h | 192 +++ .../midi_io/ump/juce_UMPFactory.h | 527 +++++++ .../midi_io/ump/juce_UMPIterator.h | 126 ++ .../ump/juce_UMPMidi1ToBytestreamTranslator.h | 213 +++ .../juce_UMPMidi1ToMidi2DefaultTranslator.cpp | 195 +++ .../juce_UMPMidi1ToMidi2DefaultTranslator.h | 187 +++ .../midi_io/ump/juce_UMPProtocols.h | 44 + .../midi_io/ump/juce_UMPReceiver.h | 40 + .../midi_io/ump/juce_UMPSysEx7.cpp | 53 + .../midi_io/ump/juce_UMPSysEx7.h | 66 + .../midi_io/ump/juce_UMPTests.cpp | 1020 +++++++++++++ .../midi_io/ump/juce_UMPU32InputHandler.h | 131 ++ .../midi_io/ump/juce_UMPUtils.cpp | 59 + .../midi_io/ump/juce_UMPUtils.h | 104 ++ .../midi_io/ump/juce_UMPView.cpp | 35 + .../midi_io/ump/juce_UMPView.h | 88 ++ .../midi_io/ump/juce_UMPacket.h | 187 +++ .../midi_io/ump/juce_UMPackets.h | 92 ++ .../native/juce_mac_CoreMidi.cpp | 732 ---------- .../native/juce_mac_CoreMidi.mm | 1269 +++++++++++++++++ modules/juce_core/memory/juce_Memory.h | 14 + 25 files changed, 5260 insertions(+), 734 deletions(-) create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPBytestreamInputHandler.h create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPConversion.h create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPConverters.h create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPDispatcher.h create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPFactory.h create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPIterator.h create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPMidi1ToBytestreamTranslator.h create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPMidi1ToMidi2DefaultTranslator.cpp create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPMidi1ToMidi2DefaultTranslator.h create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPProtocols.h create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPReceiver.h create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPSysEx7.cpp create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPSysEx7.h create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPTests.cpp create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPU32InputHandler.h create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPUtils.cpp create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPUtils.h create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPView.cpp create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPView.h create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPacket.h create mode 100644 modules/juce_audio_devices/midi_io/ump/juce_UMPackets.h delete mode 100644 modules/juce_audio_devices/native/juce_mac_CoreMidi.cpp create mode 100644 modules/juce_audio_devices/native/juce_mac_CoreMidi.mm diff --git a/modules/juce_audio_devices/juce_audio_devices.cpp b/modules/juce_audio_devices/juce_audio_devices.cpp index b3a315c9d9..1bd55992b1 100644 --- a/modules/juce_audio_devices/juce_audio_devices.cpp +++ b/modules/juce_audio_devices/juce_audio_devices.cpp @@ -47,6 +47,37 @@ #include "native/juce_MidiDataConcatenator.h" +#include + +#include "midi_io/ump/juce_UMPProtocols.h" +#include "midi_io/ump/juce_UMPUtils.h" +#include "midi_io/ump/juce_UMPacket.h" +#include "midi_io/ump/juce_UMPSysEx7.h" +#include "midi_io/ump/juce_UMPView.h" +#include "midi_io/ump/juce_UMPIterator.h" +#include "midi_io/ump/juce_UMPackets.h" +#include "midi_io/ump/juce_UMPFactory.h" +#include "midi_io/ump/juce_UMPConversion.h" +#include "midi_io/ump/juce_UMPMidi1ToBytestreamTranslator.h" +#include "midi_io/ump/juce_UMPMidi1ToMidi2DefaultTranslator.h" +#include "midi_io/ump/juce_UMPConverters.h" +#include "midi_io/ump/juce_UMPDispatcher.h" +#include "midi_io/ump/juce_UMPReceiver.h" +#include "midi_io/ump/juce_UMPBytestreamInputHandler.h" +#include "midi_io/ump/juce_UMPU32InputHandler.h" + +#include "midi_io/ump/juce_UMPUtils.cpp" +#include "midi_io/ump/juce_UMPView.cpp" +#include "midi_io/ump/juce_UMPSysEx7.cpp" +#include "midi_io/ump/juce_UMPMidi1ToMidi2DefaultTranslator.cpp" + +#include "midi_io/ump/juce_UMPTests.cpp" + +namespace juce +{ +namespace ump = universal_midi_packets; +} + //============================================================================== #if JUCE_MAC #define Point CarbonDummyPointName @@ -58,7 +89,7 @@ #undef Component #include "native/juce_mac_CoreAudio.cpp" - #include "native/juce_mac_CoreMidi.cpp" + #include "native/juce_mac_CoreMidi.mm" #elif JUCE_IOS #import @@ -70,7 +101,7 @@ #endif #include "native/juce_ios_Audio.cpp" - #include "native/juce_mac_CoreMidi.cpp" + #include "native/juce_mac_CoreMidi.mm" //============================================================================== #elif JUCE_WINDOWS diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPBytestreamInputHandler.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPBytestreamInputHandler.h new file mode 100644 index 0000000000..0ff560fef5 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPBytestreamInputHandler.h @@ -0,0 +1,120 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 +{ + +/** + A base class for classes which convert bytestream midi to other formats. +*/ +struct BytestreamInputHandler +{ + virtual ~BytestreamInputHandler() noexcept = default; + + virtual void reset() = 0; + virtual void pushMidiData (const void* data, int bytes, double time) = 0; +}; + +/** + Parses a continuous bytestream and emits complete MidiMessages whenever a full + message is received. +*/ +struct BytestreamToBytestreamHandler : public BytestreamInputHandler +{ + BytestreamToBytestreamHandler (MidiInput& i, MidiInputCallback& c) + : input (i), callback (c), concatenator (2048) {} + + class Factory + { + public: + explicit Factory (MidiInputCallback* c) + : callback (c) {} + + std::unique_ptr operator() (MidiInput& i) const + { + if (callback != nullptr) + return std::make_unique (i, *callback); + + jassertfalse; + return {}; + } + + private: + MidiInputCallback* callback = nullptr; + }; + + void reset() override { concatenator.reset(); } + + void pushMidiData (const void* data, int bytes, double time) override + { + concatenator.pushMidiData (data, bytes, time, &input, callback); + } + + MidiInput& input; + MidiInputCallback& callback; + MidiDataConcatenator concatenator; +}; + +/** + Parses a continuous MIDI 1.0 bytestream, and emits full messages in the requested + UMP format. +*/ +struct BytestreamToUMPHandler : public BytestreamInputHandler +{ + BytestreamToUMPHandler (PacketProtocol protocol, Receiver& c) + : recipient (c), dispatcher (protocol, 2048) {} + + class Factory + { + public: + Factory (PacketProtocol p, Receiver& c) + : protocol (p), callback (c) {} + + std::unique_ptr operator() (MidiInput&) const + { + return std::make_unique (protocol, callback); + } + + private: + PacketProtocol protocol; + Receiver& callback; + }; + + void reset() override { dispatcher.reset(); } + + void pushMidiData (const void* data, int bytes, double time) override + { + const auto* ptr = static_cast (data); + dispatcher.dispatch (ptr, ptr + bytes, time, [&] (const View& v) + { + recipient.packetReceived (v, time); + }); + } + + Receiver& recipient; + BytestreamToUMPDispatcher dispatcher; +}; + +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPConversion.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPConversion.h new file mode 100644 index 0000000000..c85a33f12c --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPConversion.h @@ -0,0 +1,326 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 +{ + +/** + 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 + 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 + 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; + } + } +}; + +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPConverters.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPConverters.h new file mode 100644 index 0000000000..4e14ce38e6 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPConverters.h @@ -0,0 +1,139 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 +{ + struct ToUMP1Converter + { + template + void convert (const MidiMessage& m, Fn&& fn) + { + Conversion::toMidi1 (m, std::forward (fn)); + } + + template + void convert (const View& v, Fn&& fn) + { + Conversion::midi2ToMidi1DefaultTranslation (v, std::forward (fn)); + } + }; + + struct ToUMP2Converter + { + template + void convert (const MidiMessage& m, Fn&& fn) + { + Conversion::toMidi1 (m, [&] (const View& v) + { + translator.dispatch (v, fn); + }); + } + + template + void convert (const View& v, Fn&& fn) + { + translator.dispatch (v, std::forward (fn)); + } + + void reset() + { + translator.reset(); + } + + Midi1ToMidi2DefaultTranslator translator; + }; + + class GenericUMPConverter + { + public: + explicit GenericUMPConverter (PacketProtocol m) + : mode (m) {} + + void reset() + { + std::get<1> (converters).reset(); + } + + template + void convert (const MidiMessage& m, Fn&& fn) + { + switch (mode) + { + case PacketProtocol::MIDI_1_0: return std::get<0> (converters).convert (m, std::forward (fn)); + case PacketProtocol::MIDI_2_0: return std::get<1> (converters).convert (m, std::forward (fn)); + } + } + + template + void convert (const View& v, Fn&& fn) + { + switch (mode) + { + case PacketProtocol::MIDI_1_0: return std::get<0> (converters).convert (v, std::forward (fn)); + case PacketProtocol::MIDI_2_0: return std::get<1> (converters).convert (v, std::forward (fn)); + } + } + + template + 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 converters; + const PacketProtocol mode{}; + }; + + struct ToBytestreamConverter + { + explicit ToBytestreamConverter (int storageSize) + : translator (storageSize) {} + + template + void convert (const MidiMessage& m, Fn&& fn) + { + fn (m); + } + + template + 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; + }; +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPDispatcher.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPDispatcher.h new file mode 100644 index 0000000000..a9d68a92ae --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPDispatcher.h @@ -0,0 +1,192 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 +{ + +/** + 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 + 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()), time); + currentPacketLen = 0; + time = timeStamp; + } + }); + } + +private: + std::array nextPacket; + size_t currentPacketLen = 0; + double time = 0.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 + void dispatch (const uint8_t* begin, + const uint8_t* end, + double timestamp, + PacketCallbackFunction&& callback) + { + using CallbackPtr = decltype (std::addressof (callback)); + + struct Callback + { + Callback (BytestreamToUMPDispatcher& d, CallbackPtr c) + : dispatch (d), callbackPtr (c) {} + + void handleIncomingMidiMessage (void*, const MidiMessage& msg) const + { + Conversion::toMidi1 (msg, [&] (const View& view) + { + dispatch.converter.convert (view, *callbackPtr); + }); + } + + void handlePartialSysexMessage (void*, const uint8_t*, int, double) const {} + + BytestreamToUMPDispatcher& dispatch; + CallbackPtr callbackPtr = nullptr; + }; + + Callback inputCallback { *this, &callback }; + concatenator.pushMidiData (begin, int (end - begin), timestamp, (void*) nullptr, inputCallback); + } + +private: + MidiDataConcatenator concatenator; + GenericUMPConverter converter; +}; + +//============================================================================== +/** + Parses a stream of 32-bit words representing a sequence of UMP-encoded MIDI messages, + converting the messages to MIDI 1.0 bytestream format and passing them to a user-provided + callback as they become ready. + + @tags{Audio} +*/ +class ToBytestreamDispatcher +{ +public: + /** Initialises the dispatcher. + + `storageSize` bytes will be allocated to store incomplete messages. + */ + explicit ToBytestreamDispatcher (int storageSize) + : converter (storageSize) {} + + /** Clears the dispatcher. */ + void reset() + { + dispatcher.reset(); + converter.reset(); + } + + /** Calls `callback` with converted bytestream-formatted MidiMessage whenever + a new message becomes available. + + @param begin the first word in a stream of words representing UMP-encoded MIDI packets. + @param end one-past the last word in a stream of words representing UMP-encoded MIDI packets. + @param timestamp a timestamp to apply to converted messages. + @param callback a callback which will be passed a MidiMessage each time a new message becomes ready. + */ + template + 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; +}; + +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPFactory.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPFactory.h new file mode 100644 index 0000000000..57c36b8ddf --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPFactory.h @@ -0,0 +1,527 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 +{ + +struct Factory +{ + struct Detail + { + static PacketX1 makeSystem() { return PacketX1{}.withMessageType (1); } + static PacketX1 makeV1() { return PacketX1{}.withMessageType (2); } + static PacketX2 makeV2() { return PacketX2{}.withMessageType (4); } + + static PacketX2 makeSysEx (uint8_t group, + uint8_t status, + uint8_t numBytes, + const uint8_t* data) + { + jassert (numBytes <= 6); + + std::array bytes{{}}; + bytes[0] = (0x3 << 0x4) | group; + bytes[1] = (uint8_t) (status << 0x4) | numBytes; + + std::memcpy (bytes.data() + 2, data, numBytes); + + std::array words; + + size_t index = 0; + + for (auto& word : words) + word = ByteOrder::bigEndianInt (bytes.data() + 4 * index++); + + return PacketX2 { words }; + } + + static PacketX4 makeSysEx8 (uint8_t group, + uint8_t status, + uint8_t numBytes, + uint8_t dataStart, + const uint8_t* data) + { + jassert (numBytes <= 16 - dataStart); + + std::array bytes{{}}; + bytes[0] = (0x5 << 0x4) | group; + bytes[1] = (uint8_t) (status << 0x4) | numBytes; + + std::memcpy (bytes.data() + dataStart, data, numBytes); + + std::array words; + + size_t index = 0; + + for (auto& word : words) + word = ByteOrder::bigEndianInt (bytes.data() + 4 * index++); + + return PacketX4 { words }; + } + }; + + static PacketX1 makeNoop (uint8_t group) + { + return PacketX1{}.withGroup (group); + } + + static PacketX1 makeJRClock (uint8_t group, uint16_t time) + { + return PacketX1 { time }.withStatus (1).withGroup (group); + } + + static PacketX1 makeJRTimestamp (uint8_t group, uint16_t time) + { + return PacketX1 { time }.withStatus (2).withGroup (group); + } + + static PacketX1 makeTimeCode (uint8_t group, uint8_t code) + { + return Detail::makeSystem().withGroup (group) + .withU8<1> (0xf1) + .withU8<2> (code & 0x7f); + } + + static PacketX1 makeSongPositionPointer (uint8_t group, uint16_t pos) + { + return Detail::makeSystem().withGroup (group) + .withU8<1> (0xf2) + .withU8<2> (pos & 0x7f) + .withU8<3> ((pos >> 7) & 0x7f); + } + + static PacketX1 makeSongSelect (uint8_t group, uint8_t song) + { + return Detail::makeSystem().withGroup (group) + .withU8<1> (0xf3) + .withU8<2> (song & 0x7f); + } + + static PacketX1 makeTuneRequest (uint8_t group) + { + return Detail::makeSystem().withGroup (group) + .withU8<1> (0xf6); + } + + static PacketX1 makeTimingClock (uint8_t group) + { + return Detail::makeSystem().withGroup (group) + .withU8<1> (0xf8); + } + + static PacketX1 makeStart (uint8_t group) + { + return Detail::makeSystem().withGroup (group) + .withU8<1> (0xfa); + } + + static PacketX1 makeContinue (uint8_t group) + { + return Detail::makeSystem().withGroup (group) + .withU8<1> (0xfb); + } + + static PacketX1 makeStop (uint8_t group) + { + return Detail::makeSystem().withGroup (group) + .withU8<1> (0xfc); + } + + static PacketX1 makeActiveSensing (uint8_t group) + { + return Detail::makeSystem().withGroup (group) + .withU8<1> (0xfe); + } + + static PacketX1 makeReset (uint8_t group) + { + return Detail::makeSystem().withGroup (group) + .withU8<1> (0xff); + } + + static PacketX1 makeNoteOffV1 (uint8_t group, + uint8_t channel, + uint8_t note, + uint8_t velocity) + { + return Detail::makeV1().withGroup (group) + .withStatus (0x8) + .withChannel (channel) + .withU8<2> (note & 0x7f) + .withU8<3> (velocity & 0x7f); + } + + static PacketX1 makeNoteOnV1 (uint8_t group, + uint8_t channel, + uint8_t note, + uint8_t velocity) + { + return Detail::makeV1().withGroup (group) + .withStatus (0x9) + .withChannel (channel) + .withU8<2> (note & 0x7f) + .withU8<3> (velocity & 0x7f); + } + + static PacketX1 makePolyPressureV1 (uint8_t group, + uint8_t channel, + uint8_t note, + uint8_t pressure) + { + return Detail::makeV1().withGroup (group) + .withStatus (0xa) + .withChannel (channel) + .withU8<2> (note & 0x7f) + .withU8<3> (pressure & 0x7f); + } + + static PacketX1 makeControlChangeV1 (uint8_t group, + uint8_t channel, + uint8_t controller, + uint8_t value) + { + return Detail::makeV1().withGroup (group) + .withStatus (0xb) + .withChannel (channel) + .withU8<2> (controller & 0x7f) + .withU8<3> (value & 0x7f); + } + + static PacketX1 makeProgramChangeV1 (uint8_t group, + uint8_t channel, + uint8_t program) + { + return Detail::makeV1().withGroup (group) + .withStatus (0xc) + .withChannel (channel) + .withU8<2> (program & 0x7f); + } + + static PacketX1 makeChannelPressureV1 (uint8_t group, + uint8_t channel, + uint8_t pressure) + { + return Detail::makeV1().withGroup (group) + .withStatus (0xd) + .withChannel (channel) + .withU8<2> (pressure & 0x7f); + } + + static PacketX1 makePitchBend (uint8_t group, + uint8_t channel, + uint16_t pitchbend) + { + return Detail::makeV1().withGroup (group) + .withStatus (0xe) + .withChannel (channel) + .withU8<2> (pitchbend & 0x7f) + .withU8<3> ((pitchbend >> 7) & 0x7f); + } + + static PacketX2 makeSysExIn1Packet (uint8_t group, + uint8_t numBytes, + const uint8_t* data) + { + return Detail::makeSysEx (group, 0x0, numBytes, data); + } + + static PacketX2 makeSysExStart (uint8_t group, + uint8_t numBytes, + const uint8_t* data) + { + return Detail::makeSysEx (group, 0x1, numBytes, data); + } + + static PacketX2 makeSysExContinue (uint8_t group, + uint8_t numBytes, + const uint8_t* data) + { + return Detail::makeSysEx (group, 0x2, numBytes, data); + } + + static PacketX2 makeSysExEnd (uint8_t group, + uint8_t numBytes, + const uint8_t* data) + { + return Detail::makeSysEx (group, 0x3, numBytes, data); + } + + static PacketX2 makeRegisteredPerNoteControllerV2 (uint8_t group, + uint8_t channel, + uint8_t note, + uint8_t controller, + uint32_t data) + { + return Detail::makeV2().withGroup (group) + .withStatus (0x0) + .withChannel (channel) + .withU8<2> (note & 0x7f) + .withU8<3> (controller & 0x7f) + .withU32<1> (data); + } + + static PacketX2 makeAssignablePerNoteControllerV2 (uint8_t group, + uint8_t channel, + uint8_t note, + uint8_t controller, + uint32_t data) + { + return Detail::makeV2().withGroup (group) + .withStatus (0x1) + .withChannel (channel) + .withU8<2> (note & 0x7f) + .withU8<3> (controller & 0x7f) + .withU32<1> (data); + } + + static PacketX2 makeRegisteredControllerV2 (uint8_t group, + uint8_t channel, + uint8_t bank, + uint8_t index, + uint32_t data) + { + return Detail::makeV2().withGroup (group) + .withStatus (0x2) + .withChannel (channel) + .withU8<2> (bank & 0x7f) + .withU8<3> (index & 0x7f) + .withU32<1> (data); + } + + static PacketX2 makeAssignableControllerV2 (uint8_t group, + uint8_t channel, + uint8_t bank, + uint8_t index, + uint32_t data) + { + return Detail::makeV2().withGroup (group) + .withStatus (0x3) + .withChannel (channel) + .withU8<2> (bank & 0x7f) + .withU8<3> (index & 0x7f) + .withU32<1> (data); + } + + static PacketX2 makeRelativeRegisteredControllerV2 (uint8_t group, + uint8_t channel, + uint8_t bank, + uint8_t index, + uint32_t data) + { + return Detail::makeV2().withGroup (group) + .withStatus (0x4) + .withChannel (channel) + .withU8<2> (bank & 0x7f) + .withU8<3> (index & 0x7f) + .withU32<1> (data); + } + + static PacketX2 makeRelativeAssignableControllerV2 (uint8_t group, + uint8_t channel, + uint8_t bank, + uint8_t index, + uint32_t data) + { + return Detail::makeV2().withGroup (group) + .withStatus (0x5) + .withChannel (channel) + .withU8<2> (bank & 0x7f) + .withU8<3> (index & 0x7f) + .withU32<1> (data); + } + + static PacketX2 makePerNotePitchBendV2 (uint8_t group, + uint8_t channel, + uint8_t note, + uint32_t data) + { + return Detail::makeV2().withGroup (group) + .withStatus (0x6) + .withChannel (channel) + .withU8<2> (note & 0x7f) + .withU32<1> (data); + } + + enum class NoteAttributeKind : uint8_t + { + none = 0x00, + manufacturer = 0x01, + profile = 0x02, + pitch7_9 = 0x03 + }; + + static PacketX2 makeNoteOffV2 (uint8_t group, + uint8_t channel, + uint8_t note, + NoteAttributeKind attribute, + uint16_t velocity, + uint16_t attributeValue) + { + return Detail::makeV2().withGroup (group) + .withStatus (0x8) + .withChannel (channel) + .withU8<2> (note & 0x7f) + .withU8<3> ((uint8_t) attribute) + .withU16<2> (velocity) + .withU16<3> (attributeValue); + } + + static PacketX2 makeNoteOnV2 (uint8_t group, + uint8_t channel, + uint8_t note, + NoteAttributeKind attribute, + uint16_t velocity, + uint16_t attributeValue) + { + return Detail::makeV2().withGroup (group) + .withStatus (0x9) + .withChannel (channel) + .withU8<2> (note & 0x7f) + .withU8<3> ((uint8_t) attribute) + .withU16<2> (velocity) + .withU16<3> (attributeValue); + } + + static PacketX2 makePolyPressureV2 (uint8_t group, + uint8_t channel, + uint8_t note, + uint32_t data) + { + return Detail::makeV2().withGroup (group) + .withStatus (0xa) + .withChannel (channel) + .withU8<2> (note & 0x7f) + .withU32<1> (data); + } + + static PacketX2 makeControlChangeV2 (uint8_t group, + uint8_t channel, + uint8_t controller, + uint32_t data) + { + return Detail::makeV2().withGroup (group) + .withStatus (0xb) + .withChannel (channel) + .withU8<2> (controller & 0x7f) + .withU32<1> (data); + } + + static PacketX2 makeProgramChangeV2 (uint8_t group, + uint8_t channel, + uint8_t optionFlags, + uint8_t program, + uint8_t bankMsb, + uint8_t bankLsb) + { + return Detail::makeV2().withGroup (group) + .withStatus (0xc) + .withChannel (channel) + .withU8<3> (optionFlags) + .withU8<4> (program) + .withU8<6> (bankMsb) + .withU8<7> (bankLsb); + } + + static PacketX2 makeChannelPressureV2 (uint8_t group, + uint8_t channel, + uint32_t data) + { + return Detail::makeV2().withGroup (group) + .withStatus (0xd) + .withChannel (channel) + .withU32<1> (data); + } + + static PacketX2 makePitchBendV2 (uint8_t group, + uint8_t channel, + uint32_t data) + { + return Detail::makeV2().withGroup (group) + .withStatus (0xe) + .withChannel (channel) + .withU32<1> (data); + } + + static PacketX2 makePerNoteManagementV2 (uint8_t group, + uint8_t channel, + uint8_t note, + uint8_t optionFlags) + { + return Detail::makeV2().withGroup (group) + .withStatus (0xf) + .withChannel (channel) + .withU8<2> (note) + .withU8<3> (optionFlags); + } + + + static PacketX4 makeSysEx8in1Packet (uint8_t group, + uint8_t numBytes, + uint8_t streamId, + const uint8_t* data) + { + return Detail::makeSysEx8 (group, 0x0, numBytes, 3, data).withU8<2> (streamId); + } + + static PacketX4 makeSysEx8Start (uint8_t group, + uint8_t numBytes, + uint8_t streamId, + const uint8_t* data) + { + return Detail::makeSysEx8 (group, 0x1, numBytes, 3, data).withU8<2> (streamId); + } + + static PacketX4 makeSysEx8Continue (uint8_t group, + uint8_t numBytes, + uint8_t streamId, + const uint8_t* data) + { + return Detail::makeSysEx8 (group, 0x2, numBytes, 3, data).withU8<2> (streamId); + } + + static PacketX4 makeSysEx8End (uint8_t group, + uint8_t numBytes, + uint8_t streamId, + const uint8_t* data) + { + return Detail::makeSysEx8 (group, 0x3, numBytes, 3, data).withU8<2> (streamId); + } + + static PacketX4 makeMixedDataSetHeader (uint8_t group, + uint8_t dataSetId, + const uint8_t* data) + { + return Detail::makeSysEx8 (group, 0x8, 14, 2, data).withChannel (dataSetId); + } + + static PacketX4 makeDataSetPayload (uint8_t group, + uint8_t dataSetId, + const uint8_t* data) + { + return Detail::makeSysEx8 (group, 0x9, 14, 2, data).withChannel (dataSetId); + } +}; + +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPIterator.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPIterator.h new file mode 100644 index 0000000000..a91b115d53 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPIterator.h @@ -0,0 +1,126 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 +{ + +/** + 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::difference_type; + using value_type = View; + using reference = const View&; + using pointer = const View*; + using iterator_category = std::input_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 +}; + +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPMidi1ToBytestreamTranslator.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPMidi1ToBytestreamTranslator.h new file mode 100644 index 0000000000..9621769b8e --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPMidi1ToBytestreamTranslator.h @@ -0,0 +1,213 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 +{ + +/** + 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 + 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 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 + 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::Continue: + 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 + 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::Continue || kind == SysEx7::Kind::End; + } + + static bool isSystemRealTime (uint32_t word) + { + return Utils::getMessageType (word) == 0x1 && ((word >> 0x10) & 0xff) >= 0xf8; + } + + std::vector pendingSysExData; + + double pendingSysExTime = 0.0; +}; + +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPMidi1ToMidi2DefaultTranslator.cpp b/modules/juce_audio_devices/midi_io/ump/juce_UMPMidi1ToMidi2DefaultTranslator.cpp new file mode 100644 index 0000000000..9da72c3451 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPMidi1ToMidi2DefaultTranslator.cpp @@ -0,0 +1,195 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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; +} + +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPMidi1ToMidi2DefaultTranslator.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPMidi1ToMidi2DefaultTranslator.h new file mode 100644 index 0000000000..61d566ff65 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPMidi1ToMidi2DefaultTranslator.h @@ -0,0 +1,187 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 +{ + +/** + 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 + 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& getBytes() const noexcept { return bytes; } + PnKind getKind() const noexcept { return kind; } + + private: + std::array 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; + std::array groupAccumulators; + + using ChannelBanks = std::array; + std::array groupBanks; +}; + +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPProtocols.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPProtocols.h new file mode 100644 index 0000000000..a5b5ca9f04 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPProtocols.h @@ -0,0 +1,44 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 +{ + +/** 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, +}; + +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPReceiver.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPReceiver.h new file mode 100644 index 0000000000..469efd3a3c --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPReceiver.h @@ -0,0 +1,40 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 +{ + +/** + A base class for classes which receive Universal MIDI Packets from an input. +*/ +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; +}; + +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPSysEx7.cpp b/modules/juce_audio_devices/midi_io/ump/juce_UMPSysEx7.cpp new file mode 100644 index 0000000000..bf4f790bed --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPSysEx7.cpp @@ -0,0 +1,53 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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) + }; +} + +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPSysEx7.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPSysEx7.h new file mode 100644 index 0000000000..5a0539a944 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPSysEx7.h @@ -0,0 +1,66 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 +{ + +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. */ + Continue = 2, + + /** The packet terminates an ongoing SysEx message. */ + End = 3 + }; + + struct PacketBytes + { + std::array data; + uint8_t size; + }; + + /** Extracts the data bytes from a 64-bit data message. */ + static PacketBytes getDataBytes (const PacketX2& packet); +}; + +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPTests.cpp b/modules/juce_audio_devices/midi_io/ump/juce_UMPTests.cpp new file mode 100644 index 0000000000..0ac2cce085 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPTests.cpp @@ -0,0 +1,1020 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 +{ + +#if JUCE_UNIT_TESTS + +constexpr uint8_t operator""_u8 (unsigned long long int i) { return static_cast (i); } +constexpr uint16_t operator""_u16 (unsigned long long int i) { return static_cast (i); } +constexpr uint32_t operator""_u32 (unsigned long long int i) { return static_cast (i); } +constexpr uint64_t operator""_u64 (unsigned long long int i) { return static_cast (i); } + +class UniversalMidiPacketTests : public UnitTest +{ +public: + UniversalMidiPacketTests() + : UnitTest ("Universal MIDI Packet", UnitTestCategories::midi) + { + } + + void runTest() override + { + auto random = getRandom(); + + beginTest ("Short bytestream midi messages can be round-tripped through the UMP converter"); + { + Midi1ToBytestreamTranslator translator (0); + + forEachNonSysExTestMessage (random, [&] (const MidiMessage& m) + { + Packets packets; + Conversion::toMidi1 (m, packets); + expect (packets.size() == 1); + + // Make sure that the message type is correct + expect (Utils::getMessageType (packets.data()[0]) == ((m.getRawData()[0] >> 0x4) == 0xf ? 0x1 : 0x2)); + + translator.dispatch (View {packets.data() }, + 0, + [&] (const MidiMessage& roundTripped) + { + expect (equal (m, roundTripped)); + }); + }); + } + + beginTest ("Bytestream SysEx converts to universal packets"); + { + { + // Zero length message + Packets packets; + Conversion::toMidi1 (createRandomSysEx (random, 0), packets); + expect (packets.size() == 2); + + expect (packets.data()[0] == 0x30000000); + expect (packets.data()[1] == 0x00000000); + } + + { + const auto message = createRandomSysEx (random, 1); + Packets packets; + Conversion::toMidi1 (message, packets); + expect (packets.size() == 2); + + const auto* sysEx = message.getSysExData(); + expect (packets.data()[0] == Utils::bytesToWord (0x30, 0x01, sysEx[0], 0)); + expect (packets.data()[1] == 0x00000000); + } + + { + const auto message = createRandomSysEx (random, 6); + Packets packets; + Conversion::toMidi1 (message, packets); + expect (packets.size() == 2); + + const auto* sysEx = message.getSysExData(); + expect (packets.data()[0] == Utils::bytesToWord (0x30, 0x06, sysEx[0], sysEx[1])); + expect (packets.data()[1] == Utils::bytesToWord (sysEx[2], sysEx[3], sysEx[4], sysEx[5])); + } + + { + const auto message = createRandomSysEx (random, 12); + Packets packets; + Conversion::toMidi1 (message, packets); + expect (packets.size() == 4); + + const auto* sysEx = message.getSysExData(); + expect (packets.data()[0] == Utils::bytesToWord (0x30, 0x16, sysEx[0], sysEx[1])); + expect (packets.data()[1] == Utils::bytesToWord (sysEx[2], sysEx[3], sysEx[4], sysEx[5])); + expect (packets.data()[2] == Utils::bytesToWord (0x30, 0x36, sysEx[6], sysEx[7])); + expect (packets.data()[3] == Utils::bytesToWord (sysEx[8], sysEx[9], sysEx[10], sysEx[11])); + } + + { + const auto message = createRandomSysEx (random, 13); + Packets packets; + Conversion::toMidi1 (message, packets); + expect (packets.size() == 6); + + const auto* sysEx = message.getSysExData(); + expect (packets.data()[0] == Utils::bytesToWord (0x30, 0x16, sysEx[0], sysEx[1])); + expect (packets.data()[1] == Utils::bytesToWord (sysEx[2], sysEx[3], sysEx[4], sysEx[5])); + expect (packets.data()[2] == Utils::bytesToWord (0x30, 0x26, sysEx[6], sysEx[7])); + expect (packets.data()[3] == Utils::bytesToWord (sysEx[8], sysEx[9], sysEx[10], sysEx[11])); + expect (packets.data()[4] == Utils::bytesToWord (0x30, 0x31, sysEx[12], 0)); + expect (packets.data()[5] == 0x00000000); + } + } + + ToBytestreamDispatcher converter (0); + Packets packets; + + const auto checkRoundTrip = [&] (const MidiBuffer& expected) + { + for (const auto meta : expected) + Conversion::toMidi1 (meta.getMessage(), packets); + + MidiBuffer output; + converter.dispatch (packets.data(), + packets.data() + packets.size(), + 0, + [&] (const MidiMessage& roundTripped) + { + output.addEvent (roundTripped, int (roundTripped.getTimeStamp())); + }); + packets.clear(); + + expect (equal (expected, output)); + }; + + beginTest ("Long SysEx bytestream midi messages can be round-tripped through the UMP converter"); + { + for (auto length : { 0, 1, 2, 3, 4, 5, 6, 7, 13, 20, 100, 1000 }) + { + MidiBuffer expected; + expected.addEvent (createRandomSysEx (random, size_t (length)), 0); + checkRoundTrip (expected); + } + } + + beginTest ("UMP SysEx7 messages interspersed with utility messages convert to bytestream"); + { + const auto sysEx = createRandomSysEx (random, 100); + Packets originalPackets; + Conversion::toMidi1 (sysEx, originalPackets); + + Packets modifiedPackets; + + const auto addRandomUtilityUMP = [&] + { + const auto newPacket = createRandomUtilityUMP (random); + modifiedPackets.add (View (newPacket.data())); + }; + + for (const auto& packet : originalPackets) + { + addRandomUtilityUMP(); + modifiedPackets.add (packet); + addRandomUtilityUMP(); + } + + MidiBuffer output; + converter.dispatch (modifiedPackets.data(), + modifiedPackets.data() + modifiedPackets.size(), + 0, + [&] (const MidiMessage& roundTripped) + { + output.addEvent (roundTripped, int (roundTripped.getTimeStamp())); + }); + + // All Utility messages should have been ignored + expect (output.getNumEvents() == 1); + + for (const auto meta : output) + expect (equal (meta.getMessage(), sysEx)); + } + + beginTest ("UMP SysEx7 messages interspersed with System Realtime messages convert to bytestream"); + { + const auto sysEx = createRandomSysEx (random, 200); + Packets originalPackets; + Conversion::toMidi1 (sysEx, originalPackets); + + Packets modifiedPackets; + MidiBuffer realtimeMessages; + + const auto addRandomRealtimeUMP = [&] + { + const auto newPacket = createRandomRealtimeUMP (random); + modifiedPackets.add (View (newPacket.data())); + realtimeMessages.addEvent (Midi1ToBytestreamTranslator::fromUmp (newPacket), 0); + }; + + for (const auto& packet : originalPackets) + { + addRandomRealtimeUMP(); + modifiedPackets.add (packet); + addRandomRealtimeUMP(); + } + + MidiBuffer output; + converter.dispatch (modifiedPackets.data(), + modifiedPackets.data() + modifiedPackets.size(), + 0, + [&] (const MidiMessage& roundTripped) + { + output.addEvent (roundTripped, int (roundTripped.getTimeStamp())); + }); + + const auto numOutputs = output.getNumEvents(); + const auto numInputs = realtimeMessages.getNumEvents(); + expect (numOutputs == numInputs + 1); + + if (numOutputs == numInputs + 1) + { + const auto isMetadataEquivalent = [] (const MidiMessageMetadata& a, + const MidiMessageMetadata& b) + { + return equal (a.getMessage(), b.getMessage()); + }; + + auto it = output.begin(); + + for (const auto meta : realtimeMessages) + { + if (! isMetadataEquivalent (*it, meta)) + { + expect (equal ((*it).getMessage(), sysEx)); + ++it; + } + + expect (isMetadataEquivalent (*it, meta)); + ++it; + } + } + } + + beginTest ("UMP SysEx7 messages interspersed with System Realtime and Utility messages convert to bytestream"); + { + const auto sysEx = createRandomSysEx (random, 300); + Packets originalPackets; + Conversion::toMidi1 (sysEx, originalPackets); + + Packets modifiedPackets; + MidiBuffer realtimeMessages; + + const auto addRandomRealtimeUMP = [&] + { + const auto newPacket = createRandomRealtimeUMP (random); + modifiedPackets.add (View (newPacket.data())); + realtimeMessages.addEvent (Midi1ToBytestreamTranslator::fromUmp (newPacket), 0); + }; + + const auto addRandomUtilityUMP = [&] + { + const auto newPacket = createRandomUtilityUMP (random); + modifiedPackets.add (View (newPacket.data())); + }; + + for (const auto& packet : originalPackets) + { + addRandomRealtimeUMP(); + addRandomUtilityUMP(); + modifiedPackets.add (packet); + addRandomRealtimeUMP(); + addRandomUtilityUMP(); + } + + MidiBuffer output; + converter.dispatch (modifiedPackets.data(), + modifiedPackets.data() + modifiedPackets.size(), + 0, + [&] (const MidiMessage& roundTripped) + { + output.addEvent (roundTripped, int (roundTripped.getTimeStamp())); + }); + + const auto numOutputs = output.getNumEvents(); + const auto numInputs = realtimeMessages.getNumEvents(); + expect (numOutputs == numInputs + 1); + + if (numOutputs == numInputs + 1) + { + const auto isMetadataEquivalent = [] (const MidiMessageMetadata& a, const MidiMessageMetadata& b) + { + return equal (a.getMessage(), b.getMessage()); + }; + + auto it = output.begin(); + + for (const auto meta : realtimeMessages) + { + if (! isMetadataEquivalent (*it, meta)) + { + expect (equal ((*it).getMessage(), sysEx)); + ++it; + } + + expect (isMetadataEquivalent (*it, meta)); + ++it; + } + } + } + + beginTest ("SysEx messages are terminated by non-Utility, non-Realtime messages"); + { + const auto noteOn = [&] + { + MidiBuffer b; + b.addEvent (MidiMessage::noteOn (1, uint8_t (64), uint8_t (64)), 0); + return b; + }(); + + const auto noteOnPackets = [&] + { + Packets p; + + for (const auto meta : noteOn) + Conversion::toMidi1 (meta.getMessage(), p); + + return p; + }(); + + const auto sysEx = createRandomSysEx (random, 300); + + const auto originalPackets = [&] + { + Packets p; + Conversion::toMidi1 (sysEx, p); + return p; + }(); + + const auto modifiedPackets = [&] + { + Packets p; + + const auto insertionPoint = std::next (originalPackets.begin(), 10); + std::for_each (originalPackets.begin(), + insertionPoint, + [&] (const View& view) { p.add (view); }); + + for (const auto& view : noteOnPackets) + p.add (view); + + std::for_each (insertionPoint, + originalPackets.end(), + [&] (const View& view) { p.add (view); }); + + return p; + }(); + + // modifiedPackets now contains some SysEx packets interrupted by a MIDI 1 noteOn + + MidiBuffer output; + + const auto pushToOutput = [&] (const Packets& p) + { + converter.dispatch (p.data(), + p.data() + p.size(), + 0, + [&] (const MidiMessage& roundTripped) + { + output.addEvent (roundTripped, int (roundTripped.getTimeStamp())); + }); + }; + + pushToOutput (modifiedPackets); + + // Interrupted sysEx shouldn't be present + expect (equal (output, noteOn)); + + const auto newSysEx = createRandomSysEx (random, 300); + Packets newSysExPackets; + Conversion::toMidi1 (newSysEx, newSysExPackets); + + // If we push another midi event without interrupting it, + // it should get through without being modified, + // and it shouldn't be affected by the previous (interrupted) sysex. + + output.clear(); + pushToOutput (newSysExPackets); + + expect (output.getNumEvents() == 1); + + for (const auto meta : output) + expect (equal (meta.getMessage(), newSysEx)); + } + + beginTest ("Widening conversions work"); + { + // This is similar to the 'slow' example code from the MIDI 2.0 spec + const auto baselineScale = [] (uint32_t srcVal, uint32_t srcBits, uint32_t dstBits) + { + const auto scaleBits = (uint32_t) (dstBits - srcBits); + + auto bitShiftedValue = (uint32_t) (srcVal << scaleBits); + + const auto srcCenter = (uint32_t) (1 << (srcBits - 1)); + + if (srcVal <= srcCenter) + return bitShiftedValue; + + const auto repeatBits = (uint32_t) (srcBits - 1); + const auto repeatMask = (uint32_t) ((1 << repeatBits) - 1); + + auto repeatValue = (uint32_t) (srcVal & repeatMask); + + if (scaleBits > repeatBits) + repeatValue <<= scaleBits - repeatBits; + else + repeatValue >>= repeatBits - scaleBits; + + while (repeatValue != 0) + { + bitShiftedValue |= repeatValue; + repeatValue >>= repeatBits; + } + + return bitShiftedValue; + }; + + const auto baselineScale7To8 = [&] (uint8_t in) + { + return baselineScale (in, 7, 8); + }; + + const auto baselineScale7To16 = [&] (uint8_t in) + { + return baselineScale (in, 7, 16); + }; + + const auto baselineScale14To16 = [&] (uint16_t in) + { + return baselineScale (in, 14, 16); + }; + + const auto baselineScale7To32 = [&] (uint8_t in) + { + return baselineScale (in, 7, 32); + }; + + const auto baselineScale14To32 = [&] (uint16_t in) + { + return baselineScale (in, 14, 32); + }; + + for (auto i = 0; i != 100; ++i) + { + const auto rand = (uint8_t) random.nextInt (0x80); + expectEquals ((int64_t) Conversion::scaleTo8 (rand), + (int64_t) baselineScale7To8 (rand)); + } + + expectEquals ((int64_t) Conversion::scaleTo16 ((uint8_t) 0x00), (int64_t) 0x0000); + expectEquals ((int64_t) Conversion::scaleTo16 ((uint8_t) 0x0a), (int64_t) 0x1400); + expectEquals ((int64_t) Conversion::scaleTo16 ((uint8_t) 0x40), (int64_t) 0x8000); + expectEquals ((int64_t) Conversion::scaleTo16 ((uint8_t) 0x57), (int64_t) 0xaeba); + expectEquals ((int64_t) Conversion::scaleTo16 ((uint8_t) 0x7f), (int64_t) 0xffff); + + for (auto i = 0; i != 100; ++i) + { + const auto rand = (uint8_t) random.nextInt (0x80); + expectEquals ((int64_t) Conversion::scaleTo16 (rand), + (int64_t) baselineScale7To16 (rand)); + } + + for (auto i = 0; i != 100; ++i) + { + const auto rand = (uint16_t) random.nextInt (0x4000); + expectEquals ((int64_t) Conversion::scaleTo16 (rand), + (int64_t) baselineScale14To16 (rand)); + } + + for (auto i = 0; i != 100; ++i) + { + const auto rand = (uint8_t) random.nextInt (0x80); + expectEquals ((int64_t) Conversion::scaleTo32 (rand), + (int64_t) baselineScale7To32 (rand)); + } + + expectEquals ((int64_t) Conversion::scaleTo32 ((uint16_t) 0x0000), (int64_t) 0x00000000); + expectEquals ((int64_t) Conversion::scaleTo32 ((uint16_t) 0x2000), (int64_t) 0x80000000); + expectEquals ((int64_t) Conversion::scaleTo32 ((uint16_t) 0x3fff), (int64_t) 0xffffffff); + + for (auto i = 0; i != 100; ++i) + { + const auto rand = (uint16_t) random.nextInt (0x4000); + expectEquals ((int64_t) Conversion::scaleTo32 (rand), + (int64_t) baselineScale14To32 (rand)); + } + } + + beginTest ("Round-trip widening/narrowing conversions work"); + { + for (auto i = 0; i != 100; ++i) + { + { + const auto rand = (uint8_t) random.nextInt (0x80); + expectEquals (Conversion::scaleTo7 (Conversion::scaleTo8 (rand)), rand); + } + + { + const auto rand = (uint8_t) random.nextInt (0x80); + expectEquals (Conversion::scaleTo7 (Conversion::scaleTo16 (rand)), rand); + } + + { + const auto rand = (uint8_t) random.nextInt (0x80); + expectEquals (Conversion::scaleTo7 (Conversion::scaleTo32 (rand)), rand); + } + + { + const auto rand = (uint16_t) random.nextInt (0x4000); + expectEquals ((uint64_t) Conversion::scaleTo14 (Conversion::scaleTo16 (rand)), (uint64_t) rand); + } + + { + const auto rand = (uint16_t) random.nextInt (0x4000); + expectEquals ((uint64_t) Conversion::scaleTo14 (Conversion::scaleTo32 (rand)), (uint64_t) rand); + } + } + } + + beginTest ("MIDI 2 -> 1 note on conversions"); + { + { + Packets midi2; + midi2.add (PacketX2 { 0x41946410, 0x12345678 }); + + Packets midi1; + midi1.add (PacketX1 { 0x21946409 }); + + checkMidi2ToMidi1Conversion (midi2, midi1); + } + + { + // If the velocity is close to 0, the output velocity should still be 1 + Packets midi2; + midi2.add (PacketX2 { 0x4295327f, 0x00345678 }); + + Packets midi1; + midi1.add (PacketX1 { 0x22953201 }); + + checkMidi2ToMidi1Conversion (midi2, midi1); + } + } + + beginTest ("MIDI 2 -> 1 note off conversion"); + { + Packets midi2; + midi2.add (PacketX2 { 0x448b0520, 0xfedcba98 }); + + Packets midi1; + midi1.add (PacketX1 { 0x248b057f }); + + checkMidi2ToMidi1Conversion (midi2, midi1); + } + + beginTest ("MIDI 2 -> 1 poly pressure conversion"); + { + Packets midi2; + midi2.add (PacketX2 { 0x49af0520, 0x80dcba98 }); + + Packets midi1; + midi1.add (PacketX1 { 0x29af0540 }); + + checkMidi2ToMidi1Conversion (midi2, midi1); + } + + beginTest ("MIDI 2 -> 1 control change conversion"); + { + Packets midi2; + midi2.add (PacketX2 { 0x49b00520, 0x80dcba98 }); + + Packets midi1; + midi1.add (PacketX1 { 0x29b00540 }); + + checkMidi2ToMidi1Conversion (midi2, midi1); + } + + beginTest ("MIDI 2 -> 1 channel pressure conversion"); + { + Packets midi2; + midi2.add (PacketX2 { 0x40d20520, 0x80dcba98 }); + + Packets midi1; + midi1.add (PacketX1 { 0x20d24000 }); + + checkMidi2ToMidi1Conversion (midi2, midi1); + } + + beginTest ("MIDI 2 -> 1 nrpn rpn conversion"); + { + { + Packets midi2; + midi2.add (PacketX2 { 0x44240123, 0x456789ab }); + + Packets midi1; + midi1.add (PacketX1 { 0x24b46501 }); + midi1.add (PacketX1 { 0x24b46423 }); + midi1.add (PacketX1 { 0x24b40622 }); + midi1.add (PacketX1 { 0x24b42659 }); + + checkMidi2ToMidi1Conversion (midi2, midi1); + } + + { + Packets midi2; + midi2.add (PacketX2 { 0x48347f7f, 0xffffffff }); + + Packets midi1; + midi1.add (PacketX1 { 0x28b4637f }); + midi1.add (PacketX1 { 0x28b4627f }); + midi1.add (PacketX1 { 0x28b4067f }); + midi1.add (PacketX1 { 0x28b4267f }); + + checkMidi2ToMidi1Conversion (midi2, midi1); + } + } + + beginTest ("MIDI 2 -> 1 program change and bank select conversion"); + { + { + // If the bank valid bit is 0, just emit a program change + Packets midi2; + midi2.add (PacketX2 { 0x4cc10000, 0x70004020 }); + + Packets midi1; + midi1.add (PacketX1 { 0x2cc17000 }); + + checkMidi2ToMidi1Conversion (midi2, midi1); + } + + { + // If the bank valid bit is 1, emit bank select control changes and a program change + Packets midi2; + midi2.add (PacketX2 { 0x4bc20001, 0x70004020 }); + + Packets midi1; + midi1.add (PacketX1 { 0x2bb20040 }); + midi1.add (PacketX1 { 0x2bb22020 }); + midi1.add (PacketX1 { 0x2bc27000 }); + + checkMidi2ToMidi1Conversion (midi2, midi1); + } + } + + beginTest ("MIDI 2 -> 1 pitch bend conversion"); + { + Packets midi2; + midi2.add (PacketX2 { 0x4eee0000, 0x12340000 }); + + Packets midi1; + midi1.add (PacketX1 { 0x2eee0d09 }); + + checkMidi2ToMidi1Conversion (midi2, midi1); + } + + beginTest ("MIDI 2 -> 1 messages which don't convert"); + { + const uint8_t opcodes[] { 0x0, 0x1, 0x4, 0x5, 0x6, 0xf }; + + for (const auto opcode : opcodes) + { + Packets midi2; + midi2.add (PacketX2 { Utils::bytesToWord (0x40, (uint8_t) (opcode << 0x4), 0, 0), 0x0 }); + checkMidi2ToMidi1Conversion (midi2, {}); + } + } + + beginTest ("MIDI 2 -> 1 messages which are passed through"); + { + const uint8_t typecodesX1[] { 0x0, 0x1, 0x2 }; + + for (const auto typecode : typecodesX1) + { + Packets p; + p.add (PacketX1 { (uint32_t) (typecode << 0x1c | (random.nextInt64() & 0xffffff)) }); + + checkMidi2ToMidi1Conversion (p, p); + } + + { + Packets p; + p.add (PacketX2 { (uint32_t) (0x3 << 0x1c | (random.nextInt64() & 0xffffff)), + (uint32_t) (random.nextInt64() & 0xffffffff) }); + + checkMidi2ToMidi1Conversion (p, p); + } + + { + Packets p; + p.add (PacketX4 { (uint32_t) (0x5 << 0x1c | (random.nextInt64() & 0xffffff)), + (uint32_t) (random.nextInt64() & 0xffffffff), + (uint32_t) (random.nextInt64() & 0xffffffff), + (uint32_t) (random.nextInt64() & 0xffffffff) }); + + checkMidi2ToMidi1Conversion (p, p); + } + } + + beginTest ("MIDI 2 -> 1 control changes which should be ignored"); + { + const uint8_t CCs[] { 6, 38, 98, 99, 100, 101, 0, 32 }; + + for (const auto cc : CCs) + { + Packets midi2; + midi2.add (PacketX2 { (uint32_t) (0x40b00000 | (cc << 0x8)), 0x00000000 }); + + checkMidi2ToMidi1Conversion (midi2, {}); + } + } + + beginTest ("MIDI 1 -> 2 note on conversions"); + { + { + Packets midi1; + midi1.add (PacketX1 { 0x20904040 }); + + Packets midi2; + midi2.add (PacketX2 { 0x40904000, static_cast (Conversion::scaleTo16 (0x40_u8)) << 0x10 }); + + checkMidi1ToMidi2Conversion (midi1, midi2); + } + + // If velocity is 0, convert to a note-off + { + Packets midi1; + midi1.add (PacketX1 { 0x23935100 }); + + Packets midi2; + midi2.add (PacketX2 { 0x43835100, 0x0 }); + + checkMidi1ToMidi2Conversion (midi1, midi2); + } + } + + beginTest ("MIDI 1 -> 2 note off conversions"); + { + Packets midi1; + midi1.add (PacketX1 { 0x21831020 }); + + Packets midi2; + midi2.add (PacketX2 { 0x41831000, static_cast (Conversion::scaleTo16 (0x20_u8)) << 0x10 }); + + checkMidi1ToMidi2Conversion (midi1, midi2); + } + + beginTest ("MIDI 1 -> 2 poly pressure conversions"); + { + Packets midi1; + midi1.add (PacketX1 { 0x20af7330 }); + + Packets midi2; + midi2.add (PacketX2 { 0x40af7300, Conversion::scaleTo32 (0x30_u8) }); + + checkMidi1ToMidi2Conversion (midi1, midi2); + } + + beginTest ("individual MIDI 1 -> 2 control changes which should be ignored"); + { + const uint8_t CCs[] { 6, 38, 98, 99, 100, 101, 0, 32 }; + + for (const auto cc : CCs) + { + Packets midi1; + midi1.add (PacketX1 { Utils::bytesToWord (0x20, 0xb0, cc, 0x00) }); + + checkMidi1ToMidi2Conversion (midi1, {}); + } + } + + beginTest ("MIDI 1 -> 2 control change conversions"); + { + // normal control change + { + Packets midi1; + midi1.add (PacketX1 { 0x29b1017f }); + + Packets midi2; + midi2.add (PacketX2 { 0x49b10100, Conversion::scaleTo32 (0x7f_u8) }); + + checkMidi1ToMidi2Conversion (midi1, midi2); + } + + // nrpn + { + Packets midi1; + midi1.add (PacketX1 { 0x20b06301 }); + midi1.add (PacketX1 { 0x20b06223 }); + midi1.add (PacketX1 { 0x20b00645 }); + midi1.add (PacketX1 { 0x20b02667 }); + + Packets midi2; + midi2.add (PacketX2 { 0x40300123, Conversion::scaleTo32 (static_cast ((0x45 << 7) | 0x67)) }); + + checkMidi1ToMidi2Conversion (midi1, midi2); + } + + // rpn + { + Packets midi1; + midi1.add (PacketX1 { 0x20b06543 }); + midi1.add (PacketX1 { 0x20b06421 }); + midi1.add (PacketX1 { 0x20b00601 }); + midi1.add (PacketX1 { 0x20b02623 }); + + Packets midi2; + midi2.add (PacketX2 { 0x40204321, Conversion::scaleTo32 (static_cast ((0x01 << 7) | 0x23)) }); + + checkMidi1ToMidi2Conversion (midi1, midi2); + } + } + + beginTest ("MIDI 1 -> MIDI 2 program change and bank select"); + { + Packets midi1; + // program change with bank + midi1.add (PacketX1 { 0x2bb20030 }); + midi1.add (PacketX1 { 0x2bb22010 }); + midi1.add (PacketX1 { 0x2bc24000 }); + // program change without bank (different group and channel) + midi1.add (PacketX1 { 0x20c01000 }); + + Packets midi2; + midi2.add (PacketX2 { 0x4bc20001, 0x40003010 }); + midi2.add (PacketX2 { 0x40c00000, 0x10000000 }); + + checkMidi1ToMidi2Conversion (midi1, midi2); + } + + beginTest ("MIDI 1 -> MIDI 2 channel pressure conversions"); + { + Packets midi1; + midi1.add (PacketX1 { 0x20df3000 }); + + Packets midi2; + midi2.add (PacketX2 { 0x40df0000, Conversion::scaleTo32 (0x30_u8) }); + + checkMidi1ToMidi2Conversion (midi1, midi2); + } + + beginTest ("MIDI 1 -> MIDI 2 pitch bend conversions"); + { + Packets midi1; + midi1.add (PacketX1 { 0x20e74567 }); + + Packets midi2; + midi2.add (PacketX2 { 0x40e70000, Conversion::scaleTo32 (static_cast ((0x67 << 7) | 0x45)) }); + + checkMidi1ToMidi2Conversion (midi1, midi2); + } + } + +private: + static Packets convertMidi2ToMidi1 (const Packets& midi2) + { + Packets r; + + for (const auto& packet : midi2) + Conversion::midi2ToMidi1DefaultTranslation (packet, [&r] (const View& v) { r.add (v); }); + + return r; + } + + static Packets convertMidi1ToMidi2 (const Packets& midi1) + { + Packets r; + Midi1ToMidi2DefaultTranslator translator; + + for (const auto& packet : midi1) + translator.dispatch (packet, [&r] (const View& v) { r.add (v); }); + + return r; + } + + void checkBytestreamConversion (const Packets& actual, const Packets& expected) + { + expectEquals ((int) actual.size(), (int) expected.size()); + + if (actual.size() != expected.size()) + return; + + auto actualPtr = actual.data(); + + std::for_each (expected.data(), + expected.data() + expected.size(), + [&] (const uint32_t word) { expectEquals ((uint64_t) *actualPtr++, (uint64_t) word); }); + } + + void checkMidi2ToMidi1Conversion (const Packets& midi2, const Packets& expected) + { + checkBytestreamConversion (convertMidi2ToMidi1 (midi2), expected); + } + + void checkMidi1ToMidi2Conversion (const Packets& midi1, const Packets& expected) + { + checkBytestreamConversion (convertMidi1ToMidi2 (midi1), expected); + } + + MidiMessage createRandomSysEx (Random& random, size_t sysExBytes) + { + std::vector data; + data.reserve (sysExBytes); + + for (size_t i = 0; i != sysExBytes; ++i) + data.push_back (uint8_t (random.nextInt (0x80))); + + return MidiMessage::createSysExMessage (data.data(), int (data.size())); + } + + PacketX1 createRandomUtilityUMP (Random& random) + { + const auto status = random.nextInt (3); + + return PacketX1 { Utils::bytesToWord (0, + uint8_t (status << 0x4), + uint8_t (status == 0 ? 0 : random.nextInt (0x100)), + uint8_t (status == 0 ? 0 : random.nextInt (0x100))) }; + } + + PacketX1 createRandomRealtimeUMP (Random& random) + { + const auto status = [&] + { + switch (random.nextInt (6)) + { + case 0: return 0xf8; + case 1: return 0xfa; + case 2: return 0xfb; + case 3: return 0xfc; + case 4: return 0xfe; + case 5: return 0xff; + } + + jassertfalse; + return 0x00; + }(); + + return PacketX1 { Utils::bytesToWord (0x10, uint8_t (status), 0x00, 0x00) }; + } + + template + void forEachNonSysExTestMessage (Random& random, Fn&& fn) + { + for (uint8_t firstByte = 0x80; firstByte != 0x00; ++firstByte) + { + if (firstByte == 0xf0 || firstByte == 0xf7) + continue; // sysEx is tested separately + + const auto length = MidiMessage::getMessageLengthFromFirstByte (firstByte); + const auto getDataByte = [&] { return uint8_t (random.nextInt (256) & 0x7f); }; + + const auto message = [&] + { + switch (length) + { + case 1: return MidiMessage (firstByte); + case 2: return MidiMessage (firstByte, getDataByte()); + case 3: return MidiMessage (firstByte, getDataByte(), getDataByte()); + } + + return MidiMessage(); + }(); + + fn (message); + } + } + + #if JUCE_WINDOWS + #define JUCE_CHECKED_ITERATOR(msg, size) \ + stdext::checked_array_iterator::type> ((msg), (size)) + #else + #define JUCE_CHECKED_ITERATOR(msg, size) (msg) + #endif + + static bool equal (const MidiMessage& a, const MidiMessage& b) noexcept + { + return a.getRawDataSize() == b.getRawDataSize() + && std::equal (a.getRawData(), a.getRawData() + a.getRawDataSize(), + JUCE_CHECKED_ITERATOR (b.getRawData(), b.getRawDataSize())); + } + + #undef JUCE_CHECKED_ITERATOR + + static bool equal (const MidiBuffer& a, const MidiBuffer& b) noexcept + { + return a.data == b.data; + } +}; + +static UniversalMidiPacketTests universalMidiPacketTests; + +#endif + +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPU32InputHandler.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPU32InputHandler.h new file mode 100644 index 0000000000..a9cd7880a1 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPU32InputHandler.h @@ -0,0 +1,131 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 +{ + +/** + A base class for classes which convert Universal MIDI Packets to other + formats. +*/ +struct U32InputHandler +{ + virtual ~U32InputHandler() noexcept = default; + + virtual void reset() = 0; + virtual void pushMidiData (const uint32_t* begin, const uint32_t* end, double time) = 0; +}; + +/** + Parses a continuous stream of U32 words and emits complete MidiMessages whenever a full + message is received. +*/ +struct U32ToBytestreamHandler : public U32InputHandler +{ + U32ToBytestreamHandler (MidiInput& i, MidiInputCallback& c) + : input (i), callback (c), dispatcher (2048) {} + + class Factory + { + public: + explicit Factory (MidiInputCallback* c) + : callback (c) {} + + std::unique_ptr operator() (MidiInput& i) const + { + if (callback != nullptr) + return std::make_unique (i, *callback); + + jassertfalse; + return {}; + } + + private: + MidiInputCallback* callback = nullptr; + }; + + void reset() override { dispatcher.reset(); } + + void pushMidiData (const uint32_t* begin, const uint32_t* end, double time) override + { + dispatcher.dispatch (begin, end, time, [this] (const MidiMessage& m) + { + callback.handleIncomingMidiMessage (&input, m); + }); + } + + MidiInput& input; + MidiInputCallback& callback; + ToBytestreamDispatcher dispatcher; +}; + +/** + Parses a continuous stream of U32 words and emits full messages in the requested + UMP format. +*/ +struct U32ToUMPHandler : public U32InputHandler +{ + U32ToUMPHandler (PacketProtocol protocol, Receiver& c) + : recipient (c), converter (protocol) {} + + class Factory + { + public: + Factory (PacketProtocol p, Receiver& c) + : protocol (p), callback (c) {} + + std::unique_ptr operator() (MidiInput&) const + { + return std::make_unique (protocol, callback); + } + + private: + PacketProtocol protocol; + Receiver& callback; + }; + + void reset() override + { + dispatcher.reset(); + converter.reset(); + } + + void pushMidiData (const uint32_t* begin, const uint32_t* end, double time) override + { + dispatcher.dispatch (begin, end, time, [this] (const View& view, double thisTime) + { + converter.convert (view, [&] (const View& converted) + { + recipient.packetReceived (converted, thisTime); + }); + }); + } + + Receiver& recipient; + Dispatcher dispatcher; + GenericUMPConverter converter; +}; + +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPUtils.cpp b/modules/juce_audio_devices/midi_io/ump/juce_UMPUtils.cpp new file mode 100644 index 0000000000..166c4bfa3a --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPUtils.cpp @@ -0,0 +1,59 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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; +} + +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPUtils.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPUtils.h new file mode 100644 index 0000000000..23742c3b1a --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPUtils.h @@ -0,0 +1,104 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 +{ + +/** + 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); + + template + 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); + } + }; + + template + 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); + } + }; + + template + 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); } +}; + +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPView.cpp b/modules/juce_audio_devices/midi_io/ump/juce_UMPView.cpp new file mode 100644 index 0000000000..c5f3db85c6 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPView.cpp @@ -0,0 +1,35 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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); +} + +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPView.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPView.h new file mode 100644 index 0000000000..504e024c4a --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPView.h @@ -0,0 +1,88 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 +{ + +/** + 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; +}; + +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPacket.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPacket.h new file mode 100644 index 0000000000..b58304d5e9 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPacket.h @@ -0,0 +1,187 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 +{ + +/** + Holds a single Universal MIDI Packet. +*/ +template +class Packet +{ +public: + Packet() = default; + + template ::type = 0> + Packet (uint32_t a) + : contents { { a } } + { + jassert (Utils::getNumWordsForMessageType (a) == 1); + } + + template ::type = 0> + Packet (uint32_t a, uint32_t b) + : contents { { a, b } } + { + jassert (Utils::getNumWordsForMessageType (a) == 2); + } + + template ::type = 0> + Packet (uint32_t a, uint32_t b, uint32_t c) + : contents { { a, b, c } } + { + jassert (Utils::getNumWordsForMessageType (a) == 3); + } + + template ::type = 0> + Packet (uint32_t a, uint32_t b, uint32_t c, uint32_t d) + : contents { { a, b, c, d } } + { + jassert (Utils::getNumWordsForMessageType (a) == 4); + } + + template ::type = 0> + explicit Packet (const std::array& fullPacket) + : contents (fullPacket) + { + jassert (Utils::getNumWordsForMessageType (fullPacket.front()) == numWords); + } + + Packet withMessageType (uint8_t type) const noexcept + { + return withU4<0> (type); + } + + Packet withGroup (uint8_t group) const noexcept + { + return withU4<1> (group); + } + + Packet withStatus (uint8_t status) const noexcept + { + return withU4<2> (status); + } + + Packet withChannel (uint8_t channel) const noexcept + { + return withU4<3> (channel); + } + + uint8_t getMessageType() const noexcept { return getU4<0>(); } + + uint8_t getGroup() const noexcept { return getU4<1>(); } + + uint8_t getStatus() const noexcept { return getU4<2>(); } + + uint8_t getChannel() const noexcept { return getU4<3>(); } + + template + Packet withU4 (uint8_t value) const noexcept + { + constexpr auto word = index / 8; + auto copy = *this; + std::get (copy.contents) = Utils::U4::set (copy.template getU32(), value); + return copy; + } + + template + Packet withU8 (uint8_t value) const noexcept + { + constexpr auto word = index / 4; + auto copy = *this; + std::get (copy.contents) = Utils::U8::set (copy.template getU32(), value); + return copy; + } + + template + Packet withU16 (uint16_t value) const noexcept + { + constexpr auto word = index / 2; + auto copy = *this; + std::get (copy.contents) = Utils::U16::set (copy.template getU32(), value); + return copy; + } + + template + Packet withU32 (uint32_t value) const noexcept + { + auto copy = *this; + std::get (copy.contents) = value; + return copy; + } + + template + uint8_t getU4() const noexcept + { + return Utils::U4::get (this->template getU32()); + } + + template + uint8_t getU8() const noexcept + { + return Utils::U8::get (this->template getU32()); + } + + template + uint16_t getU16() const noexcept + { + return Utils::U16::get (this->template getU32()); + } + + template + uint32_t getU32() const noexcept + { + return std::get (contents); + } + + //============================================================================== + using Contents = std::array; + + using const_iterator = typename Contents::const_iterator; + + const_iterator begin() const noexcept { return contents.begin(); } + const_iterator cbegin() const noexcept { return contents.begin(); } + + const_iterator end() const noexcept { return contents.end(); } + const_iterator cend() const noexcept { return contents.end(); } + + const uint32_t* data() const noexcept { return contents.data(); } + + const uint32_t& front() const noexcept { return contents.front(); } + const uint32_t& back() const noexcept { return contents.back(); } + + const uint32_t& operator[] (size_t index) const noexcept { return contents[index]; } + +private: + Contents contents { {} }; +}; + +using PacketX1 = Packet<1>; +using PacketX2 = Packet<2>; +using PacketX3 = Packet<3>; +using PacketX4 = Packet<4>; + +} +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPackets.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPackets.h new file mode 100644 index 0000000000..2a0a1b8f6d --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPackets.h @@ -0,0 +1,92 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 +{ + +/** + Holds a collection of Universal MIDI Packets. + + Unlike MidiBuffer, this collection does not store any additional information + (e.g. timestamps) alongside the raw messages. + + If timestamps are required, these can be added to the container in UMP format, + as Jitter Reduction Utility messages. + + @tags{Audio} +*/ +class Packets +{ +public: + /** Adds a single packet to the collection. + + The View must be valid for this to work. If the view + points to a malformed message, or if the view points to a region + too short for the contained message, this call will result in + undefined behaviour. + */ + void add (const View& v) { storage.insert (storage.end(), v.cbegin(), v.cend()); } + + void add (const PacketX1& p) { addImpl (p); } + void add (const PacketX2& p) { addImpl (p); } + void add (const PacketX3& p) { addImpl (p); } + void add (const PacketX4& p) { addImpl (p); } + + /** Pre-allocates space for at least `numWords` 32-bit words in this collection. */ + void reserve (size_t numWords) { storage.reserve (numWords); } + + /** Removes all previously-added packets from this collection. */ + void clear() { storage.clear(); } + + /** Gets an iterator pointing to the first packet in this collection. */ + Iterator cbegin() const noexcept { return Iterator (data(), size()); } + Iterator begin() const noexcept { return cbegin(); } + + /** Gets an iterator pointing one-past the last packet in this collection. */ + Iterator cend() const noexcept { return Iterator (data() + size(), 0); } + Iterator end() const noexcept { return cend(); } + + /** Gets a pointer to the contents of the collection as a range of raw 32-bit words. */ + const uint32_t* data() const noexcept { return storage.data(); } + + /** Returns the number of uint32_t words in storage. + + Note that this is likely to be larger than the number of packets + currently being stored, as some packets span multiple words. + */ + size_t size() const noexcept { return storage.size(); } + +private: + template + void addImpl (const Packet& p) + { + jassert (Utils::getNumWordsForMessageType (p[0]) == numWords); + add (View (p.data())); + } + + std::vector storage; +}; + +} +} diff --git a/modules/juce_audio_devices/native/juce_mac_CoreMidi.cpp b/modules/juce_audio_devices/native/juce_mac_CoreMidi.cpp deleted file mode 100644 index 704a05749f..0000000000 --- a/modules/juce_audio_devices/native/juce_mac_CoreMidi.cpp +++ /dev/null @@ -1,732 +0,0 @@ -/* - ============================================================================== - - This file is part of the JUCE library. - Copyright (c) 2020 - 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 -{ - -#ifndef JUCE_LOG_COREMIDI_ERRORS - #define JUCE_LOG_COREMIDI_ERRORS 1 -#endif - -namespace CoreMidiHelpers -{ - //============================================================================== - static bool checkError (OSStatus err, int lineNum) - { - if (err == noErr) - return true; - - #if JUCE_LOG_COREMIDI_ERRORS - Logger::writeToLog ("CoreMIDI error: " + String (lineNum) + " - " + String::toHexString ((int) err)); - #endif - - ignoreUnused (lineNum); - return false; - } - - #undef CHECK_ERROR - #define CHECK_ERROR(a) CoreMidiHelpers::checkError (a, __LINE__) - - static MidiDeviceInfo getMidiObjectInfo (MIDIObjectRef entity) - { - MidiDeviceInfo info; - - { - ScopedCFString str; - - if (CHECK_ERROR (MIDIObjectGetStringProperty (entity, kMIDIPropertyName, &str.cfString))) - info.name = String::fromCFString (str.cfString); - } - - SInt32 objectID = 0; - - if (CHECK_ERROR (MIDIObjectGetIntegerProperty (entity, kMIDIPropertyUniqueID, &objectID))) - { - info.identifier = String (objectID); - } - else - { - ScopedCFString str; - - if (CHECK_ERROR (MIDIObjectGetStringProperty (entity, kMIDIPropertyUniqueID, &str.cfString))) - info.identifier = String::fromCFString (str.cfString); - } - - return info; - } - - static MidiDeviceInfo getEndpointInfo (MIDIEndpointRef endpoint, bool isExternal) - { - // NB: don't attempt to use nullptr for refs - it fails in some types of build. - MIDIEntityRef entity = 0; - MIDIEndpointGetEntity (endpoint, &entity); - - // probably virtual - if (entity == 0) - return getMidiObjectInfo (endpoint); - - auto result = getMidiObjectInfo (endpoint); - - // endpoint is empty - try the entity - if (result == MidiDeviceInfo()) - result = getMidiObjectInfo (entity); - - // now consider the device - MIDIDeviceRef device = 0; - MIDIEntityGetDevice (entity, &device); - - if (device != 0) - { - auto info = getMidiObjectInfo (device); - - if (info != MidiDeviceInfo()) - { - // if an external device has only one entity, throw away - // the endpoint name and just use the device name - if (isExternal && MIDIDeviceGetNumberOfEntities (device) < 2) - { - result = info; - } - else if (! result.name.startsWithIgnoreCase (info.name)) - { - // prepend the device name and identifier to the entity's - result.name = (info.name + " " + result.name).trimEnd(); - result.identifier = info.identifier + " " + result.identifier; - } - } - } - - return result; - } - - static MidiDeviceInfo getConnectedEndpointInfo (MIDIEndpointRef endpoint) - { - MidiDeviceInfo result; - - // Does the endpoint have connections? - CFDataRef connections = nullptr; - int numConnections = 0; - - MIDIObjectGetDataProperty (endpoint, kMIDIPropertyConnectionUniqueID, &connections); - - if (connections != nullptr) - { - numConnections = ((int) CFDataGetLength (connections)) / (int) sizeof (MIDIUniqueID); - - if (numConnections > 0) - { - auto* pid = reinterpret_cast (CFDataGetBytePtr (connections)); - - for (int i = 0; i < numConnections; ++i, ++pid) - { - auto id = (MIDIUniqueID) ByteOrder::swapIfLittleEndian ((uint32) *pid); - MIDIObjectRef connObject; - MIDIObjectType connObjectType; - auto err = MIDIObjectFindByUniqueID (id, &connObject, &connObjectType); - - if (err == noErr) - { - MidiDeviceInfo deviceInfo; - - if (connObjectType == kMIDIObjectType_ExternalSource - || connObjectType == kMIDIObjectType_ExternalDestination) - { - // Connected to an external device's endpoint (10.3 and later). - deviceInfo = getEndpointInfo (static_cast (connObject), true); - } - else - { - // Connected to an external device (10.2) (or something else, catch-all) - deviceInfo = getMidiObjectInfo (connObject); - } - - if (deviceInfo != MidiDeviceInfo()) - { - if (result.name.isNotEmpty()) result.name += ", "; - if (result.identifier.isNotEmpty()) result.identifier += ", "; - - result.name += deviceInfo.name; - result.identifier += deviceInfo.identifier; - } - } - } - } - - CFRelease (connections); - } - - // Here, either the endpoint had no connections, or we failed to obtain names for them. - if (result == MidiDeviceInfo()) - return getEndpointInfo (endpoint, false); - - return result; - } - - static int createUniqueIDForMidiPort (String deviceName, bool isInput) - { - String uniqueID; - - #ifdef JucePlugin_CFBundleIdentifier - uniqueID = JUCE_STRINGIFY (JucePlugin_CFBundleIdentifier); - #else - auto appBundle = File::getSpecialLocation (File::currentApplicationFile); - ScopedCFString appBundlePath (appBundle.getFullPathName()); - - if (auto bundleURL = CFURLCreateWithFileSystemPath (kCFAllocatorDefault, appBundlePath.cfString, kCFURLPOSIXPathStyle, true)) - { - auto bundleRef = CFBundleCreate (kCFAllocatorDefault, bundleURL); - CFRelease (bundleURL); - - if (bundleRef != nullptr) - { - if (auto bundleId = CFBundleGetIdentifier (bundleRef)) - uniqueID = String::fromCFString (bundleId); - - CFRelease (bundleRef); - } - } - #endif - - if (uniqueID.isEmpty()) - uniqueID = String (Random::getSystemRandom().nextInt (1024)); - - uniqueID += "." + deviceName + (isInput ? ".input" : ".output"); - return uniqueID.hashCode(); - } - - static void enableSimulatorMidiSession() - { - #if TARGET_OS_SIMULATOR - static bool hasEnabledNetworkSession = false; - - if (! hasEnabledNetworkSession) - { - MIDINetworkSession* session = [MIDINetworkSession defaultSession]; - session.enabled = YES; - session.connectionPolicy = MIDINetworkConnectionPolicy_Anyone; - - hasEnabledNetworkSession = true; - } - #endif - } - - static void globalSystemChangeCallback (const MIDINotification*, void*) - { - // TODO.. Should pass-on this notification.. - } - - static String getGlobalMidiClientName() - { - if (auto* app = JUCEApplicationBase::getInstance()) - return app->getApplicationName(); - - return "JUCE"; - } - - static MIDIClientRef getGlobalMidiClient() - { - static MIDIClientRef globalMidiClient = 0; - - if (globalMidiClient == 0) - { - // Since OSX 10.6, the MIDIClientCreate function will only work - // correctly when called from the message thread! - JUCE_ASSERT_MESSAGE_THREAD - - enableSimulatorMidiSession(); - - ScopedCFString name (getGlobalMidiClientName()); - CHECK_ERROR (MIDIClientCreate (name.cfString, &globalSystemChangeCallback, nullptr, &globalMidiClient)); - } - - return globalMidiClient; - } - - static Array findDevices (bool forInput) - { - // It seems that OSX can be a bit picky about the thread that's first used to - // search for devices. It's safest to use the message thread for calling this. - JUCE_ASSERT_MESSAGE_THREAD - - if (getGlobalMidiClient() == 0) - { - jassertfalse; - return {}; - } - - enableSimulatorMidiSession(); - - Array devices; - auto numDevices = (forInput ? MIDIGetNumberOfSources() : MIDIGetNumberOfDestinations()); - - for (ItemCount i = 0; i < numDevices; ++i) - { - MidiDeviceInfo deviceInfo; - - if (auto dest = forInput ? MIDIGetSource (i) : MIDIGetDestination (i)) - deviceInfo = getConnectedEndpointInfo (dest); - - if (deviceInfo == MidiDeviceInfo()) - deviceInfo.name = deviceInfo.identifier = ""; - - devices.add (deviceInfo); - } - - return devices; - } - - //============================================================================== - class MidiPortAndEndpoint - { - public: - MidiPortAndEndpoint (MIDIPortRef p, MIDIEndpointRef ep) noexcept - : port (p), endpoint (ep) - { - } - - ~MidiPortAndEndpoint() noexcept - { - if (port != 0) - MIDIPortDispose (port); - - // if port == nullptr, it means we created the endpoint, so it's safe to delete it - if (port == 0 && endpoint != 0) - MIDIEndpointDispose (endpoint); - } - - void send (const MIDIPacketList* packets) noexcept - { - if (port != 0) - MIDISend (port, endpoint, packets); - else - MIDIReceived (endpoint, packets); - } - - MIDIPortRef port; - MIDIEndpointRef endpoint; - }; - - //============================================================================== - struct MidiPortAndCallback; - CriticalSection callbackLock; - Array activeCallbacks; - - struct MidiPortAndCallback - { - MidiPortAndCallback (MidiInputCallback& cb) : callback (cb) {} - - ~MidiPortAndCallback() - { - active = false; - - { - const ScopedLock sl (callbackLock); - activeCallbacks.removeFirstMatchingValue (this); - } - - if (portAndEndpoint != nullptr && portAndEndpoint->port != 0) - CHECK_ERROR (MIDIPortDisconnectSource (portAndEndpoint->port, portAndEndpoint->endpoint)); - } - - void handlePackets (const MIDIPacketList* pktlist) - { - auto time = Time::getMillisecondCounterHiRes() * 0.001; - - const ScopedLock sl (callbackLock); - - if (activeCallbacks.contains (this) && active) - { - auto* packet = &pktlist->packet[0]; - - for (unsigned int i = 0; i < pktlist->numPackets; ++i) - { - auto len = readUnalignedlength)> (&(packet->length)); - concatenator.pushMidiData (packet->data, (int) len, time, input, callback); - - packet = MIDIPacketNext (packet); - } - } - } - - MidiInput* input = nullptr; - std::unique_ptr portAndEndpoint; - std::atomic active { false }; - - private: - MidiInputCallback& callback; - MidiDataConcatenator concatenator { 2048 }; - }; - - static void midiInputProc (const MIDIPacketList* pktlist, void* readProcRefCon, void* /*srcConnRefCon*/) - { - static_cast (readProcRefCon)->handlePackets (pktlist); - } - - static Array getEndpoints (bool isInput) - { - Array endpoints; - auto numDevices = (isInput ? MIDIGetNumberOfSources() : MIDIGetNumberOfDestinations()); - - for (ItemCount i = 0; i < numDevices; ++i) - endpoints.add (isInput ? MIDIGetSource (i) : MIDIGetDestination (i)); - - return endpoints; - } -} - -class MidiInput::Pimpl : public CoreMidiHelpers::MidiPortAndCallback -{ -public: - using MidiPortAndCallback::MidiPortAndCallback; -}; - -//============================================================================== -Array MidiInput::getAvailableDevices() -{ - return CoreMidiHelpers::findDevices (true); -} - -MidiDeviceInfo MidiInput::getDefaultDevice() -{ - return getAvailableDevices().getFirst(); -} - -std::unique_ptr MidiInput::openDevice (const String& deviceIdentifier, MidiInputCallback* callback) -{ - if (deviceIdentifier.isEmpty()) - return nullptr; - - using namespace CoreMidiHelpers; - - if (auto client = getGlobalMidiClient()) - { - for (auto& endpoint : getEndpoints (true)) - { - auto endpointInfo = getConnectedEndpointInfo (endpoint); - - if (deviceIdentifier == endpointInfo.identifier) - { - ScopedCFString cfName; - - if (CHECK_ERROR (MIDIObjectGetStringProperty (endpoint, kMIDIPropertyName, &cfName.cfString))) - { - MIDIPortRef port; - auto mpc = std::make_unique (*callback); - - if (CHECK_ERROR (MIDIInputPortCreate (client, cfName.cfString, midiInputProc, mpc.get(), &port))) - { - if (CHECK_ERROR (MIDIPortConnectSource (port, endpoint, nullptr))) - { - mpc->portAndEndpoint = std::make_unique (port, endpoint); - - std::unique_ptr midiInput (new MidiInput (endpointInfo.name, endpointInfo.identifier)); - - mpc->input = midiInput.get(); - auto* ptr = mpc.get(); - midiInput->internal = std::move (mpc); - - const ScopedLock sl (callbackLock); - activeCallbacks.add (ptr); - - return midiInput; - } - else - { - CHECK_ERROR (MIDIPortDispose (port)); - } - } - } - } - } - } - - return {}; -} - -std::unique_ptr MidiInput::createNewDevice (const String& deviceName, MidiInputCallback* callback) -{ - using namespace CoreMidiHelpers; - jassert (callback != nullptr); - - if (auto client = getGlobalMidiClient()) - { - auto mpc = std::make_unique (*callback); - mpc->active = false; - - MIDIEndpointRef endpoint; - ScopedCFString name (deviceName); - - auto err = MIDIDestinationCreate (client, name.cfString, midiInputProc, mpc.get(), &endpoint); - - #if JUCE_IOS - if (err == kMIDINotPermitted) - { - // If you've hit this assertion then you probably haven't enabled the "Audio Background Capability" - // setting in the iOS exporter for your app - this is required if you want to create a MIDI device! - jassertfalse; - return nullptr; - } - #endif - - if (CHECK_ERROR (err)) - { - auto deviceIdentifier = createUniqueIDForMidiPort (deviceName, true); - - if (CHECK_ERROR (MIDIObjectSetIntegerProperty (endpoint, kMIDIPropertyUniqueID, (SInt32) deviceIdentifier))) - { - mpc->portAndEndpoint = std::make_unique ((UInt32) 0, endpoint); - - std::unique_ptr midiInput (new MidiInput (deviceName, String (deviceIdentifier))); - - mpc->input = midiInput.get(); - auto* ptr = mpc.get(); - midiInput->internal = std::move (mpc); - - const ScopedLock sl (callbackLock); - activeCallbacks.add (ptr); - - return midiInput; - } - } - } - - return {}; -} - -StringArray MidiInput::getDevices() -{ - StringArray deviceNames; - - for (auto& d : getAvailableDevices()) - deviceNames.add (d.name); - - return deviceNames; -} - -int MidiInput::getDefaultDeviceIndex() -{ - return 0; -} - -std::unique_ptr MidiInput::openDevice (int index, MidiInputCallback* callback) -{ - return openDevice (getAvailableDevices()[index].identifier, callback); -} - -MidiInput::MidiInput (const String& deviceName, const String& deviceIdentifier) - : deviceInfo (deviceName, deviceIdentifier) -{ -} - -MidiInput::~MidiInput() = default; - -void MidiInput::start() -{ - const ScopedLock sl (CoreMidiHelpers::callbackLock); - internal->active = true; -} - -void MidiInput::stop() -{ - const ScopedLock sl (CoreMidiHelpers::callbackLock); - internal->active = false; -} - -//============================================================================== -class MidiOutput::Pimpl : public CoreMidiHelpers::MidiPortAndEndpoint -{ -public: - using MidiPortAndEndpoint::MidiPortAndEndpoint; -}; - -Array MidiOutput::getAvailableDevices() -{ - return CoreMidiHelpers::findDevices (false); -} - -MidiDeviceInfo MidiOutput::getDefaultDevice() -{ - return getAvailableDevices().getFirst(); -} - -std::unique_ptr MidiOutput::openDevice (const String& deviceIdentifier) -{ - if (deviceIdentifier.isEmpty()) - return nullptr; - - using namespace CoreMidiHelpers; - - if (auto client = getGlobalMidiClient()) - { - for (auto& endpoint : getEndpoints (false)) - { - auto endpointInfo = getConnectedEndpointInfo (endpoint); - - if (deviceIdentifier == endpointInfo.identifier) - { - ScopedCFString cfName; - - if (CHECK_ERROR (MIDIObjectGetStringProperty (endpoint, kMIDIPropertyName, &cfName.cfString))) - { - MIDIPortRef port; - - if (CHECK_ERROR (MIDIOutputPortCreate (client, cfName.cfString, &port))) - { - std::unique_ptr midiOutput (new MidiOutput (endpointInfo.name, endpointInfo.identifier)); - midiOutput->internal = std::make_unique (port, endpoint); - - return midiOutput; - } - } - } - } - } - - return {}; -} - -std::unique_ptr MidiOutput::createNewDevice (const String& deviceName) -{ - using namespace CoreMidiHelpers; - - if (auto client = getGlobalMidiClient()) - { - MIDIEndpointRef endpoint; - - ScopedCFString name (deviceName); - - auto err = MIDISourceCreate (client, name.cfString, &endpoint); - - #if JUCE_IOS - if (err == kMIDINotPermitted) - { - // If you've hit this assertion then you probably haven't enabled the "Audio Background Capability" - // setting in the iOS exporter for your app - this is required if you want to create a MIDI device! - jassertfalse; - return nullptr; - } - #endif - - if (CHECK_ERROR (err)) - { - auto deviceIdentifier = createUniqueIDForMidiPort (deviceName, false); - - if (CHECK_ERROR (MIDIObjectSetIntegerProperty (endpoint, kMIDIPropertyUniqueID, (SInt32) deviceIdentifier))) - { - std::unique_ptr midiOutput (new MidiOutput (deviceName, String (deviceIdentifier))); - midiOutput->internal = std::make_unique ((UInt32) 0, endpoint); - - return midiOutput; - } - } - } - - return {}; -} - -StringArray MidiOutput::getDevices() -{ - StringArray deviceNames; - - for (auto& d : getAvailableDevices()) - deviceNames.add (d.name); - - return deviceNames; -} - -int MidiOutput::getDefaultDeviceIndex() -{ - return 0; -} - -std::unique_ptr MidiOutput::openDevice (int index) -{ - return openDevice (getAvailableDevices()[index].identifier); -} - -MidiOutput::~MidiOutput() -{ - stopBackgroundThread(); -} - -void MidiOutput::sendMessageNow (const MidiMessage& message) -{ - #if JUCE_IOS - const MIDITimeStamp timeStamp = mach_absolute_time(); - #else - const MIDITimeStamp timeStamp = AudioGetCurrentHostTime(); - #endif - - HeapBlock allocatedPackets; - MIDIPacketList stackPacket; - auto* packetToSend = &stackPacket; - auto dataSize = (size_t) message.getRawDataSize(); - - if (message.isSysEx()) - { - const int maxPacketSize = 256; - int pos = 0, bytesLeft = (int) dataSize; - const int numPackets = (bytesLeft + maxPacketSize - 1) / maxPacketSize; - allocatedPackets.malloc ((size_t) (32 * (size_t) numPackets + dataSize), 1); - packetToSend = allocatedPackets; - packetToSend->numPackets = (UInt32) numPackets; - - auto* p = packetToSend->packet; - - for (int i = 0; i < numPackets; ++i) - { - p->timeStamp = timeStamp; - p->length = (UInt16) jmin (maxPacketSize, bytesLeft); - memcpy (p->data, message.getRawData() + pos, p->length); - pos += p->length; - bytesLeft -= p->length; - p = MIDIPacketNext (p); - } - } - else if (dataSize < 65536) // max packet size - { - auto stackCapacity = sizeof (stackPacket.packet->data); - - if (dataSize > stackCapacity) - { - allocatedPackets.malloc ((sizeof (MIDIPacketList) - stackCapacity) + dataSize, 1); - packetToSend = allocatedPackets; - } - - packetToSend->numPackets = 1; - auto& p = *(packetToSend->packet); - p.timeStamp = timeStamp; - p.length = (UInt16) dataSize; - memcpy (p.data, message.getRawData(), dataSize); - } - else - { - jassertfalse; // packet too large to send! - return; - } - - internal->send (packetToSend); -} - -#undef CHECK_ERROR - -} // namespace juce diff --git a/modules/juce_audio_devices/native/juce_mac_CoreMidi.mm b/modules/juce_audio_devices/native/juce_mac_CoreMidi.mm new file mode 100644 index 0000000000..4c24bcc41b --- /dev/null +++ b/modules/juce_audio_devices/native/juce_mac_CoreMidi.mm @@ -0,0 +1,1269 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 +{ + +#ifndef JUCE_LOG_COREMIDI_ERRORS + #define JUCE_LOG_COREMIDI_ERRORS 1 +#endif + +namespace CoreMidiHelpers +{ + static bool checkError (OSStatus err, int lineNum) + { + if (err == noErr) + return true; + + #if JUCE_LOG_COREMIDI_ERRORS + Logger::writeToLog ("CoreMIDI error: " + String (lineNum) + " - " + String::toHexString ((int) err)); + #endif + + ignoreUnused (lineNum); + return false; + } + + #undef CHECK_ERROR + #define CHECK_ERROR(a) CoreMidiHelpers::checkError (a, __LINE__) + + enum class ImplementationStrategy + { + onlyNew, + both, + onlyOld + }; + + #if (defined (MAC_OS_VERSION_11_0) || defined (__IPHONE_14_0)) + #if (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_VERSION_11_0 || __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_14_0) + #define JUCE_HAS_NEW_COREMIDI_API 1 + #define JUCE_HAS_OLD_COREMIDI_API 0 + constexpr auto implementationStrategy = ImplementationStrategy::onlyNew; + #else + #define JUCE_HAS_NEW_COREMIDI_API 1 + #define JUCE_HAS_OLD_COREMIDI_API 1 + constexpr auto implementationStrategy = ImplementationStrategy::both; + #endif + #else + #define JUCE_HAS_NEW_COREMIDI_API 0 + #define JUCE_HAS_OLD_COREMIDI_API 1 + constexpr auto implementationStrategy = ImplementationStrategy::onlyOld; + #endif + + struct SenderBase + { + virtual ~SenderBase() noexcept = default; + + virtual void send (MIDIPortRef port, MIDIEndpointRef endpoint, const MidiMessage& m) = 0; + virtual void send (MIDIPortRef port, MIDIEndpointRef endpoint, ump::Iterator b, ump::Iterator e) = 0; + + virtual ump::MidiProtocol getProtocol() const noexcept = 0; + }; + + template + struct Sender; + + #if JUCE_HAS_NEW_COREMIDI_API + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wunguarded-availability-new") + + template <> + struct Sender : public SenderBase + { + explicit Sender (MIDIEndpointRef ep) + : umpConverter (getProtocolForEndpoint (ep)) + {} + + void send (MIDIPortRef port, MIDIEndpointRef endpoint, const MidiMessage& m) override + { + newSendImpl (port, endpoint, m); + } + + void send (MIDIPortRef port, MIDIEndpointRef endpoint, ump::Iterator b, ump::Iterator e) override + { + newSendImpl (port, endpoint, b, e); + } + + ump::MidiProtocol getProtocol() const noexcept override + { + return umpConverter.getProtocol() == ump::PacketProtocol::MIDI_2_0 ? ump::MidiProtocol::UMP_MIDI_2_0 + : ump::MidiProtocol::UMP_MIDI_1_0; + } + + private: + ump::GenericUMPConverter umpConverter; + + static ump::PacketProtocol getProtocolForEndpoint (MIDIEndpointRef ep) noexcept + { + SInt32 protocol = 0; + CHECK_ERROR (MIDIObjectGetIntegerProperty (ep, kMIDIPropertyProtocolID, &protocol)); + + return protocol == kMIDIProtocol_2_0 ? ump::PacketProtocol::MIDI_2_0 + : ump::PacketProtocol::MIDI_1_0; + } + + template + void newSendImpl (MIDIPortRef port, MIDIEndpointRef endpoint, Params&&... params) + { + // The converter protocol got out-of-sync with the device protocol + jassert (getProtocolForEndpoint (endpoint) == umpConverter.getProtocol()); + + #if JUCE_IOS + const MIDITimeStamp timeStamp = mach_absolute_time(); + #else + const MIDITimeStamp timeStamp = AudioGetCurrentHostTime(); + #endif + + MIDIEventList stackList = {}; + MIDIEventPacket* end = nullptr; + + const auto init = [&] + { + end = MIDIEventListInit (&stackList, + umpConverter.getProtocol() == ump::PacketProtocol::MIDI_2_0 ? kMIDIProtocol_2_0 + : kMIDIProtocol_1_0); + }; + + const auto send = [&] + { + CHECK_ERROR (port != 0 ? MIDISendEventList (port, endpoint, &stackList) + : MIDIReceivedEventList (endpoint, &stackList)); + }; + + const auto add = [&] (const ump::View& view) + { + static_assert (sizeof (uint32_t) == sizeof (UInt32) + && alignof (uint32_t) == alignof (UInt32), + "If this fails, the cast below will be broken too!"); + end = MIDIEventListAdd (&stackList, + sizeof (MIDIEventList::packet), + end, + timeStamp, + view.size(), + reinterpret_cast (view.data())); + }; + + init(); + + umpConverter.convert (params..., [&] (const ump::View& view) + { + add (view); + + if (end != nullptr) + return; + + send(); + init(); + add (view); + }); + + send(); + } + }; + + JUCE_END_IGNORE_WARNINGS_GCC_LIKE + #endif + + #if JUCE_HAS_OLD_COREMIDI_API + template <> + struct Sender : public SenderBase + { + explicit Sender (MIDIEndpointRef) {} + + void send (MIDIPortRef port, MIDIEndpointRef endpoint, const MidiMessage& m) override + { + oldSendImpl (port, endpoint, m); + } + + void send (MIDIPortRef port, MIDIEndpointRef endpoint, ump::Iterator b, ump::Iterator e) override + { + std::for_each (b, e, [&] (const ump::View& v) + { + bytestreamConverter.convert (v, 0.0, [&] (const MidiMessage& m) + { + send (port, endpoint, m); + }); + }); + } + + ump::MidiProtocol getProtocol() const noexcept override + { + return ump::MidiProtocol::bytestream; + } + + private: + ump::ToBytestreamConverter bytestreamConverter { 2048 }; + + void oldSendImpl (MIDIPortRef port, MIDIEndpointRef endpoint, const MidiMessage& message) + { + #if JUCE_IOS + const MIDITimeStamp timeStamp = mach_absolute_time(); + #else + const MIDITimeStamp timeStamp = AudioGetCurrentHostTime(); + #endif + + HeapBlock allocatedPackets; + MIDIPacketList stackPacket; + auto* packetToSend = &stackPacket; + auto dataSize = (size_t) message.getRawDataSize(); + + if (message.isSysEx()) + { + const int maxPacketSize = 256; + int pos = 0, bytesLeft = (int) dataSize; + const int numPackets = (bytesLeft + maxPacketSize - 1) / maxPacketSize; + allocatedPackets.malloc ((size_t) (32 * (size_t) numPackets + dataSize), 1); + packetToSend = allocatedPackets; + packetToSend->numPackets = (UInt32) numPackets; + + auto* p = packetToSend->packet; + + for (int i = 0; i < numPackets; ++i) + { + p->timeStamp = timeStamp; + p->length = (UInt16) jmin (maxPacketSize, bytesLeft); + memcpy (p->data, message.getRawData() + pos, p->length); + pos += p->length; + bytesLeft -= p->length; + p = MIDIPacketNext (p); + } + } + else if (dataSize < 65536) // max packet size + { + auto stackCapacity = sizeof (stackPacket.packet->data); + + if (dataSize > stackCapacity) + { + allocatedPackets.malloc ((sizeof (MIDIPacketList) - stackCapacity) + dataSize, 1); + packetToSend = allocatedPackets; + } + + packetToSend->numPackets = 1; + auto& p = *(packetToSend->packet); + p.timeStamp = timeStamp; + p.length = (UInt16) dataSize; + memcpy (p.data, message.getRawData(), dataSize); + } + else + { + jassertfalse; // packet too large to send! + return; + } + + if (port != 0) + MIDISend (port, endpoint, packetToSend); + else + MIDIReceived (endpoint, packetToSend); + } + }; + #endif + + #if JUCE_HAS_NEW_COREMIDI_API && JUCE_HAS_OLD_COREMIDI_API + template <> + struct Sender + { + explicit Sender (MIDIEndpointRef ep) + : sender (makeImpl (ep)) + {} + + void send (MIDIPortRef port, MIDIEndpointRef endpoint, const MidiMessage& m) + { + sender->send (port, endpoint, m); + } + + void send (MIDIPortRef port, MIDIEndpointRef endpoint, ump::Iterator b, ump::Iterator e) + { + sender->send (port, endpoint, b, e); + } + + ump::MidiProtocol getProtocol() const noexcept + { + return sender->getProtocol(); + } + + private: + static std::unique_ptr makeImpl (MIDIEndpointRef ep) + { + if (@available (macOS 11, iOS 14, *)) + return std::make_unique> (ep); + + return std::make_unique> (ep); + } + + std::unique_ptr sender; + }; + #endif + + using SenderToUse = Sender; + + //============================================================================== + class MidiPortAndEndpoint + { + public: + MidiPortAndEndpoint (MIDIPortRef p, MIDIEndpointRef ep) noexcept + : port (p), endpoint (ep), sender (ep) + {} + + ~MidiPortAndEndpoint() noexcept + { + if (port != 0) + MIDIPortDispose (port); + + // if port == nullptr, it means we created the endpoint, so it's safe to delete it + if (port == 0 && endpoint != 0) + MIDIEndpointDispose (endpoint); + } + + void send (const MidiMessage& m) + { + sender.send (port, endpoint, m); + } + + void send (ump::Iterator b, ump::Iterator e) + { + sender.send (port, endpoint, b, e); + } + + bool canStop() const noexcept { return port != 0; } + void stop() const { CHECK_ERROR (MIDIPortDisconnectSource (port, endpoint)); } + + ump::MidiProtocol getProtocol() const noexcept + { + return sender.getProtocol(); + } + + private: + MIDIPortRef port; + MIDIEndpointRef endpoint; + + SenderToUse sender; + }; + + static MidiDeviceInfo getMidiObjectInfo (MIDIObjectRef entity) + { + MidiDeviceInfo info; + + { + ScopedCFString str; + + if (CHECK_ERROR (MIDIObjectGetStringProperty (entity, kMIDIPropertyName, &str.cfString))) + info.name = String::fromCFString (str.cfString); + } + + SInt32 objectID = 0; + + if (CHECK_ERROR (MIDIObjectGetIntegerProperty (entity, kMIDIPropertyUniqueID, &objectID))) + { + info.identifier = String (objectID); + } + else + { + ScopedCFString str; + + if (CHECK_ERROR (MIDIObjectGetStringProperty (entity, kMIDIPropertyUniqueID, &str.cfString))) + info.identifier = String::fromCFString (str.cfString); + } + + return info; + } + + static MidiDeviceInfo getEndpointInfo (MIDIEndpointRef endpoint, bool isExternal) + { + // NB: don't attempt to use nullptr for refs - it fails in some types of build. + MIDIEntityRef entity = 0; + MIDIEndpointGetEntity (endpoint, &entity); + + // probably virtual + if (entity == 0) + return getMidiObjectInfo (endpoint); + + auto result = getMidiObjectInfo (endpoint); + + // endpoint is empty - try the entity + if (result == MidiDeviceInfo()) + result = getMidiObjectInfo (entity); + + // now consider the device + MIDIDeviceRef device = 0; + MIDIEntityGetDevice (entity, &device); + + if (device != 0) + { + auto info = getMidiObjectInfo (device); + + if (info != MidiDeviceInfo()) + { + // if an external device has only one entity, throw away + // the endpoint name and just use the device name + if (isExternal && MIDIDeviceGetNumberOfEntities (device) < 2) + { + result = info; + } + else if (! result.name.startsWithIgnoreCase (info.name)) + { + // prepend the device name and identifier to the entity's + result.name = (info.name + " " + result.name).trimEnd(); + result.identifier = info.identifier + " " + result.identifier; + } + } + } + + return result; + } + + static MidiDeviceInfo getConnectedEndpointInfo (MIDIEndpointRef endpoint) + { + MidiDeviceInfo result; + + // Does the endpoint have connections? + CFDataRef connections = nullptr; + int numConnections = 0; + + MIDIObjectGetDataProperty (endpoint, kMIDIPropertyConnectionUniqueID, &connections); + + if (connections != nullptr) + { + numConnections = ((int) CFDataGetLength (connections)) / (int) sizeof (MIDIUniqueID); + + if (numConnections > 0) + { + auto* pid = reinterpret_cast (CFDataGetBytePtr (connections)); + + for (int i = 0; i < numConnections; ++i, ++pid) + { + auto id = (MIDIUniqueID) ByteOrder::swapIfLittleEndian ((uint32) *pid); + MIDIObjectRef connObject; + MIDIObjectType connObjectType; + auto err = MIDIObjectFindByUniqueID (id, &connObject, &connObjectType); + + if (err == noErr) + { + MidiDeviceInfo deviceInfo; + + if (connObjectType == kMIDIObjectType_ExternalSource + || connObjectType == kMIDIObjectType_ExternalDestination) + { + // Connected to an external device's endpoint (10.3 and later). + deviceInfo = getEndpointInfo (static_cast (connObject), true); + } + else + { + // Connected to an external device (10.2) (or something else, catch-all) + deviceInfo = getMidiObjectInfo (connObject); + } + + if (deviceInfo != MidiDeviceInfo()) + { + if (result.name.isNotEmpty()) result.name += ", "; + if (result.identifier.isNotEmpty()) result.identifier += ", "; + + result.name += deviceInfo.name; + result.identifier += deviceInfo.identifier; + } + } + } + } + + CFRelease (connections); + } + + // Here, either the endpoint had no connections, or we failed to obtain names for them. + if (result == MidiDeviceInfo()) + return getEndpointInfo (endpoint, false); + + return result; + } + + static int createUniqueIDForMidiPort (String deviceName, bool isInput) + { + String uniqueID; + + #ifdef JucePlugin_CFBundleIdentifier + uniqueID = JUCE_STRINGIFY (JucePlugin_CFBundleIdentifier); + #else + auto appBundle = File::getSpecialLocation (File::currentApplicationFile); + ScopedCFString appBundlePath (appBundle.getFullPathName()); + + if (auto bundleURL = CFURLCreateWithFileSystemPath (kCFAllocatorDefault, + appBundlePath.cfString, + kCFURLPOSIXPathStyle, + true)) + { + auto bundleRef = CFBundleCreate (kCFAllocatorDefault, bundleURL); + CFRelease (bundleURL); + + if (bundleRef != nullptr) + { + if (auto bundleId = CFBundleGetIdentifier (bundleRef)) + uniqueID = String::fromCFString (bundleId); + + CFRelease (bundleRef); + } + } + #endif + + if (uniqueID.isEmpty()) + uniqueID = String (Random::getSystemRandom().nextInt (1024)); + + uniqueID += "." + deviceName + (isInput ? ".input" : ".output"); + return uniqueID.hashCode(); + } + + static void enableSimulatorMidiSession() + { + #if TARGET_OS_SIMULATOR + static bool hasEnabledNetworkSession = false; + + if (! hasEnabledNetworkSession) + { + MIDINetworkSession* session = [MIDINetworkSession defaultSession]; + session.enabled = YES; + session.connectionPolicy = MIDINetworkConnectionPolicy_Anyone; + + hasEnabledNetworkSession = true; + } + #endif + } + + static void globalSystemChangeCallback (const MIDINotification*, void*) + { + // TODO.. Should pass-on this notification.. + } + + static String getGlobalMidiClientName() + { + if (auto* app = JUCEApplicationBase::getInstance()) + return app->getApplicationName(); + + return "JUCE"; + } + + static MIDIClientRef getGlobalMidiClient() + { + static MIDIClientRef globalMidiClient = 0; + + if (globalMidiClient == 0) + { + // Since OSX 10.6, the MIDIClientCreate function will only work + // correctly when called from the message thread! + JUCE_ASSERT_MESSAGE_THREAD + + enableSimulatorMidiSession(); + + ScopedCFString name (getGlobalMidiClientName()); + CHECK_ERROR (MIDIClientCreate (name.cfString, &globalSystemChangeCallback, nullptr, &globalMidiClient)); + } + + return globalMidiClient; + } + + static Array findDevices (bool forInput) + { + // It seems that OSX can be a bit picky about the thread that's first used to + // search for devices. It's safest to use the message thread for calling this. + JUCE_ASSERT_MESSAGE_THREAD + + if (getGlobalMidiClient() == 0) + { + jassertfalse; + return {}; + } + + enableSimulatorMidiSession(); + + Array devices; + auto numDevices = (forInput ? MIDIGetNumberOfSources() : MIDIGetNumberOfDestinations()); + + for (ItemCount i = 0; i < numDevices; ++i) + { + MidiDeviceInfo deviceInfo; + + if (auto dest = forInput ? MIDIGetSource (i) : MIDIGetDestination (i)) + deviceInfo = getConnectedEndpointInfo (dest); + + if (deviceInfo == MidiDeviceInfo()) + deviceInfo.name = deviceInfo.identifier = ""; + + devices.add (deviceInfo); + } + + return devices; + } + + //============================================================================== + template + struct Receiver; + + #if JUCE_HAS_NEW_COREMIDI_API + template <> + struct Receiver + { + Receiver (ump::PacketProtocol protocol, ump::Receiver& receiver) + : u32InputHandler (std::make_unique (protocol, receiver)) + {} + + Receiver (MidiInput& input, MidiInputCallback& callback) + : u32InputHandler (std::make_unique (input, callback)) + {} + + void dispatch (const MIDIEventList& list, double time) const + { + auto* packet = &list.packet[0]; + + for (uint32_t i = 0; i < list.numPackets; ++i) + { + static_assert (sizeof (uint32_t) == sizeof (UInt32) + && alignof (uint32_t) == alignof (UInt32), + "If this fails, the cast below will be broken too!"); + u32InputHandler->pushMidiData (reinterpret_cast (packet->words), + reinterpret_cast (packet->words + packet->wordCount), + time); + + packet = MIDIEventPacketNext (packet); + } + } + + private: + std::unique_ptr u32InputHandler; + }; + #endif + + #if JUCE_HAS_OLD_COREMIDI_API + template <> + struct Receiver + { + Receiver (ump::PacketProtocol protocol, ump::Receiver& receiver) + : bytestreamInputHandler (std::make_unique (protocol, receiver)) + {} + + Receiver (MidiInput& input, MidiInputCallback& callback) + : bytestreamInputHandler (std::make_unique (input, callback)) + {} + + void dispatch (const MIDIPacketList& list, double time) const + { + auto* packet = &list.packet[0]; + + for (unsigned int i = 0; i < list.numPackets; ++i) + { + auto len = readUnalignedlength)> (&(packet->length)); + bytestreamInputHandler->pushMidiData (packet->data, len, time); + + packet = MIDIPacketNext (packet); + } + } + + private: + std::unique_ptr bytestreamInputHandler; + }; + #endif + + #if JUCE_HAS_NEW_COREMIDI_API && JUCE_HAS_OLD_COREMIDI_API + template <> + struct Receiver + { + Receiver (ump::PacketProtocol protocol, ump::Receiver& receiver) + : newReceiver (protocol, receiver), oldReceiver (protocol, receiver) + {} + + Receiver (MidiInput& input, MidiInputCallback& callback) + : newReceiver (input, callback), oldReceiver (input, callback) + {} + + void dispatch (const MIDIEventList& list, double time) const + { + newReceiver.dispatch (list, time); + } + + void dispatch (const MIDIPacketList& list, double time) const + { + oldReceiver.dispatch (list, time); + } + + private: + Receiver newReceiver; + Receiver oldReceiver; + }; + #endif + + using ReceiverToUse = Receiver; + + class MidiPortAndCallback; + CriticalSection callbackLock; + Array activeCallbacks; + + class MidiPortAndCallback + { + public: + MidiPortAndCallback (MidiInput& inputIn, ReceiverToUse receiverIn) + : input (&inputIn), receiver (std::move (receiverIn)) + {} + + ~MidiPortAndCallback() + { + active = false; + + { + const ScopedLock sl (callbackLock); + activeCallbacks.removeFirstMatchingValue (this); + } + + if (portAndEndpoint != nullptr && portAndEndpoint->canStop()) + portAndEndpoint->stop(); + } + + template + void handlePackets (const EventList& list) + { + const auto time = Time::getMillisecondCounterHiRes() * 0.001; + + const ScopedLock sl (callbackLock); + + if (activeCallbacks.contains (this) && active) + receiver.dispatch (list, time); + } + + MidiInput* input = nullptr; + std::atomic active { false }; + + ReceiverToUse receiver; + + std::unique_ptr portAndEndpoint; + + private: + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiPortAndCallback) + }; + + //============================================================================== + static Array getEndpoints (bool isInput) + { + Array endpoints; + auto numDevices = (isInput ? MIDIGetNumberOfSources() : MIDIGetNumberOfDestinations()); + + for (ItemCount i = 0; i < numDevices; ++i) + endpoints.add (isInput ? MIDIGetSource (i) : MIDIGetDestination (i)); + + return endpoints; + } + + struct CreatorFunctionPointers + { + OSStatus (*createInputPort) (ump::PacketProtocol protocol, + MIDIClientRef client, + CFStringRef portName, + void* refCon, + MIDIPortRef* outPort); + + OSStatus (*createDestination) (ump::PacketProtocol protocol, + MIDIClientRef client, + CFStringRef name, + void* refCon, + MIDIEndpointRef* outDest); + + OSStatus (*createSource) (ump::PacketProtocol protocol, + MIDIClientRef client, + CFStringRef name, + MIDIEndpointRef* outSrc); + }; + + template + struct CreatorFunctions; + + #if JUCE_HAS_NEW_COREMIDI_API + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wunguarded-availability-new") + + template <> + struct CreatorFunctions + { + static OSStatus createInputPort (ump::PacketProtocol protocol, + MIDIClientRef client, + CFStringRef portName, + void* refCon, + MIDIPortRef* outPort) + { + return MIDIInputPortCreateWithProtocol (client, + portName, + convertToPacketProtocol (protocol), + outPort, + ^void (const MIDIEventList* l, void* src) + { + newMidiInputProc (l, refCon, src); + }); + } + + static OSStatus createDestination (ump::PacketProtocol protocol, + MIDIClientRef client, + CFStringRef name, + void* refCon, + MIDIEndpointRef* outDest) + { + return MIDIDestinationCreateWithProtocol (client, + name, + convertToPacketProtocol (protocol), + outDest, + ^void (const MIDIEventList* l, void* src) + { + newMidiInputProc (l, refCon, src); + }); + } + + static OSStatus createSource (ump::PacketProtocol protocol, + MIDIClientRef client, + CFStringRef name, + MIDIEndpointRef* outSrc) + { + return MIDISourceCreateWithProtocol (client, + name, + convertToPacketProtocol (protocol), + outSrc); + } + + static constexpr CreatorFunctionPointers getCreatorFunctionPointers() + { + return { createInputPort, createDestination, createSource }; + } + + private: + static constexpr MIDIProtocolID convertToPacketProtocol (ump::PacketProtocol p) + { + return p == ump::PacketProtocol::MIDI_2_0 ? kMIDIProtocol_2_0 + : kMIDIProtocol_1_0; + } + + static void newMidiInputProc (const MIDIEventList* list, void* readProcRefCon, void*) + { + static_cast (readProcRefCon)->handlePackets (*list); + } + }; + + JUCE_END_IGNORE_WARNINGS_GCC_LIKE + #endif + + #if JUCE_HAS_OLD_COREMIDI_API + template <> + struct CreatorFunctions + { + static OSStatus createInputPort (ump::PacketProtocol, + MIDIClientRef client, + CFStringRef portName, + void* refCon, + MIDIPortRef* outPort) + { + return MIDIInputPortCreate (client, portName, oldMidiInputProc, refCon, outPort); + } + + static OSStatus createDestination (ump::PacketProtocol, + MIDIClientRef client, + CFStringRef name, + void* refCon, + MIDIEndpointRef* outDest) + { + return MIDIDestinationCreate (client, name, oldMidiInputProc, refCon, outDest); + } + + static OSStatus createSource (ump::PacketProtocol, + MIDIClientRef client, + CFStringRef name, + MIDIEndpointRef* outSrc) + { + return MIDISourceCreate (client, name, outSrc); + } + + static constexpr CreatorFunctionPointers getCreatorFunctionPointers() + { + return { createInputPort, createDestination, createSource }; + } + + private: + static void oldMidiInputProc (const MIDIPacketList* list, void* readProcRefCon, void*) + { + static_cast (readProcRefCon)->handlePackets (*list); + } + }; + #endif + + #if JUCE_HAS_NEW_COREMIDI_API && JUCE_HAS_OLD_COREMIDI_API + template <> + struct CreatorFunctions + { + static OSStatus createInputPort (ump::PacketProtocol protocol, + MIDIClientRef client, + CFStringRef portName, + void* refCon, + MIDIPortRef* outPort) + { + return getCreatorFunctionPointers().createInputPort (protocol, client, portName, refCon, outPort); + } + + static OSStatus createDestination (ump::PacketProtocol protocol, + MIDIClientRef client, + CFStringRef name, + void* refCon, + MIDIEndpointRef* outDest) + { + return getCreatorFunctionPointers().createDestination (protocol, client, name, refCon, outDest); + } + + static OSStatus createSource (ump::PacketProtocol protocol, + MIDIClientRef client, + CFStringRef name, + MIDIEndpointRef* outSrc) + { + return getCreatorFunctionPointers().createSource (protocol, client, name, outSrc); + } + + private: + static CreatorFunctionPointers getCreatorFunctionPointers() + { + if (@available (macOS 11, iOS 14, *)) + return CreatorFunctions::getCreatorFunctionPointers(); + + return CreatorFunctions::getCreatorFunctionPointers(); + } + }; + #endif + + using CreatorFunctionsToUse = CreatorFunctions; +} + +//============================================================================== +class MidiInput::Pimpl : public CoreMidiHelpers::MidiPortAndCallback +{ +public: + using MidiPortAndCallback::MidiPortAndCallback; + + static std::unique_ptr makePimpl (MidiInput& midiInput, + ump::PacketProtocol packetProtocol, + ump::Receiver& umpReceiver) + { + return std::make_unique (midiInput, CoreMidiHelpers::ReceiverToUse (packetProtocol, umpReceiver)); + } + + static std::unique_ptr makePimpl (MidiInput& midiInput, + MidiInputCallback* midiInputCallback) + { + if (midiInputCallback == nullptr) + return {}; + + return std::make_unique (midiInput, CoreMidiHelpers::ReceiverToUse (midiInput, *midiInputCallback)); + } + + template + static std::unique_ptr makeInput (const String& name, + const String& identifier, + Args&&... args) + { + using namespace CoreMidiHelpers; + + if (auto midiInput = rawToUniquePtr (new MidiInput (name, identifier))) + { + if ((midiInput->internal = makePimpl (*midiInput, std::forward (args)...))) + { + const ScopedLock sl (callbackLock); + activeCallbacks.add (midiInput->internal.get()); + + return midiInput; + } + } + + return {}; + } + + template + static std::unique_ptr openDevice (ump::PacketProtocol protocol, + const String& deviceIdentifier, + Args&&... args) + { + using namespace CoreMidiHelpers; + + if (deviceIdentifier.isEmpty()) + return {}; + + if (auto client = getGlobalMidiClient()) + { + for (auto& endpoint : getEndpoints (true)) + { + auto endpointInfo = getConnectedEndpointInfo (endpoint); + + if (deviceIdentifier != endpointInfo.identifier) + continue; + + ScopedCFString cfName; + + if (! CHECK_ERROR (MIDIObjectGetStringProperty (endpoint, kMIDIPropertyName, &cfName.cfString))) + continue; + + if (auto input = makeInput (endpointInfo.name, endpointInfo.identifier, std::forward (args)...)) + { + MIDIPortRef port; + + if (! CHECK_ERROR (CreatorFunctionsToUse::createInputPort (protocol, client, cfName.cfString, input->internal.get(), &port))) + continue; + + if (! CHECK_ERROR (MIDIPortConnectSource (port, endpoint, nullptr))) + { + CHECK_ERROR (MIDIPortDispose (port)); + continue; + } + + input->internal->portAndEndpoint = std::make_unique (port, endpoint); + return input; + } + } + } + + return {}; + } + + template + static std::unique_ptr createDevice (ump::PacketProtocol protocol, + const String& deviceName, + Args&&... args) + { + using namespace CoreMidiHelpers; + + if (auto client = getGlobalMidiClient()) + { + auto deviceIdentifier = createUniqueIDForMidiPort (deviceName, true); + + if (auto input = makeInput (deviceName, String (deviceIdentifier), std::forward (args)...)) + { + MIDIEndpointRef endpoint; + ScopedCFString name (deviceName); + + auto err = CreatorFunctionsToUse::createDestination (protocol, client, name.cfString, input->internal.get(), &endpoint); + + #if JUCE_IOS + if (err == kMIDINotPermitted) + { + // If you've hit this assertion then you probably haven't enabled the "Audio Background Capability" + // setting in the iOS exporter for your app - this is required if you want to create a MIDI device! + jassertfalse; + return {}; + } + #endif + + if (! CHECK_ERROR (err)) + return {}; + + if (! CHECK_ERROR (MIDIObjectSetIntegerProperty (endpoint, kMIDIPropertyUniqueID, (SInt32) deviceIdentifier))) + return {}; + + input->internal->portAndEndpoint = std::make_unique ((MIDIPortRef) 0, endpoint); + return input; + } + } + + return {}; + } +}; + +//============================================================================== +Array MidiInput::getAvailableDevices() +{ + return CoreMidiHelpers::findDevices (true); +} + +MidiDeviceInfo MidiInput::getDefaultDevice() +{ + return getAvailableDevices().getFirst(); +} + +std::unique_ptr MidiInput::openDevice (const String& deviceIdentifier, MidiInputCallback* callback) +{ + if (callback == nullptr) + return nullptr; + + return Pimpl::openDevice (ump::PacketProtocol::MIDI_1_0, + deviceIdentifier, + callback); +} + +std::unique_ptr MidiInput::createNewDevice (const String& deviceName, MidiInputCallback* callback) +{ + return Pimpl::createDevice (ump::PacketProtocol::MIDI_1_0, + deviceName, + callback); +} + +StringArray MidiInput::getDevices() +{ + StringArray deviceNames; + + for (auto& d : getAvailableDevices()) + deviceNames.add (d.name); + + return deviceNames; +} + +int MidiInput::getDefaultDeviceIndex() +{ + return 0; +} + +std::unique_ptr MidiInput::openDevice (int index, MidiInputCallback* callback) +{ + return openDevice (getAvailableDevices()[index].identifier, callback); +} + +MidiInput::MidiInput (const String& deviceName, const String& deviceIdentifier) + : deviceInfo (deviceName, deviceIdentifier) +{ +} + +MidiInput::~MidiInput() = default; + +void MidiInput::start() +{ + const ScopedLock sl (CoreMidiHelpers::callbackLock); + internal->active = true; +} + +void MidiInput::stop() +{ + const ScopedLock sl (CoreMidiHelpers::callbackLock); + internal->active = false; +} + +//============================================================================== +class MidiOutput::Pimpl : public CoreMidiHelpers::MidiPortAndEndpoint +{ +public: + using MidiPortAndEndpoint::MidiPortAndEndpoint; +}; + +Array MidiOutput::getAvailableDevices() +{ + return CoreMidiHelpers::findDevices (false); +} + +MidiDeviceInfo MidiOutput::getDefaultDevice() +{ + return getAvailableDevices().getFirst(); +} + +std::unique_ptr MidiOutput::openDevice (const String& deviceIdentifier) +{ + if (deviceIdentifier.isEmpty()) + return {}; + + using namespace CoreMidiHelpers; + + if (auto client = getGlobalMidiClient()) + { + for (auto& endpoint : getEndpoints (false)) + { + auto endpointInfo = getConnectedEndpointInfo (endpoint); + + if (deviceIdentifier != endpointInfo.identifier) + continue; + + ScopedCFString cfName; + + if (! CHECK_ERROR (MIDIObjectGetStringProperty (endpoint, kMIDIPropertyName, &cfName.cfString))) + continue; + + MIDIPortRef port; + + if (! CHECK_ERROR (MIDIOutputPortCreate (client, cfName.cfString, &port))) + continue; + + auto midiOutput = rawToUniquePtr (new MidiOutput (endpointInfo.name, endpointInfo.identifier)); + midiOutput->internal = std::make_unique (port, endpoint); + + return midiOutput; + } + } + + return {}; +} + +std::unique_ptr MidiOutput::createNewDevice (const String& deviceName) +{ + using namespace CoreMidiHelpers; + + if (auto client = getGlobalMidiClient()) + { + MIDIEndpointRef endpoint; + + ScopedCFString name (deviceName); + + auto err = CreatorFunctionsToUse::createSource (ump::PacketProtocol::MIDI_1_0, client, name.cfString, &endpoint); + + #if JUCE_IOS + if (err == kMIDINotPermitted) + { + // If you've hit this assertion then you probably haven't enabled the "Audio Background Capability" + // setting in the iOS exporter for your app - this is required if you want to create a MIDI device! + jassertfalse; + return {}; + } + #endif + + if (! CHECK_ERROR (err)) + return {}; + + auto deviceIdentifier = createUniqueIDForMidiPort (deviceName, false); + + if (! CHECK_ERROR (MIDIObjectSetIntegerProperty (endpoint, kMIDIPropertyUniqueID, (SInt32) deviceIdentifier))) + return {}; + + auto midiOutput = rawToUniquePtr (new MidiOutput (deviceName, String (deviceIdentifier))); + midiOutput->internal = std::make_unique ((UInt32) 0, endpoint); + + return midiOutput; + } + + return {}; +} + +StringArray MidiOutput::getDevices() +{ + StringArray deviceNames; + + for (auto& d : getAvailableDevices()) + deviceNames.add (d.name); + + return deviceNames; +} + +int MidiOutput::getDefaultDeviceIndex() +{ + return 0; +} + +std::unique_ptr MidiOutput::openDevice (int index) +{ + return openDevice (getAvailableDevices()[index].identifier); +} + +MidiOutput::~MidiOutput() +{ + stopBackgroundThread(); +} + +void MidiOutput::sendMessageNow (const MidiMessage& message) +{ + internal->send (message); +} + +#undef CHECK_ERROR + +} // namespace juce diff --git a/modules/juce_core/memory/juce_Memory.h b/modules/juce_core/memory/juce_Memory.h index 03c0de3ceb..0f21a5a8da 100644 --- a/modules/juce_core/memory/juce_Memory.h +++ b/modules/juce_core/memory/juce_Memory.h @@ -183,4 +183,18 @@ inline const Type* addBytesToPointer (const Type* basePointer, IntegerType bytes #define juce_UseDebuggingNewOperator #endif + /** Converts an owning raw pointer into a unique_ptr, deriving the + type of the unique_ptr automatically. + + This should only be used with pointers to single objects. + Do NOT pass a pointer to an array to this function, as the + destructor of the unique_ptr will incorrectly call `delete` + instead of `delete[]` on the pointer. + */ + template + std::unique_ptr rawToUniquePtr (T* ptr) + { + return std::unique_ptr (ptr); + } + } // namespace juce