From d651ac3600bdebd57fe87c61aa78c54577cb971c Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 21 Dec 2025 22:49:15 +0100 Subject: [PATCH 01/20] FIrst midi2 support --- modules/yup_audio_basics/midi/ump/yup_UMP.h | 9 - .../midi/ump/yup_UMPBytesOnGroup.h | 4 +- .../midi/ump/yup_UMPConversion.h | 4 +- .../midi/ump/yup_UMPConverters.h | 4 +- .../midi/ump/yup_UMPDeviceInfo.h | 4 +- .../midi/ump/yup_UMPDispatcher.h | 4 +- .../midi/ump/yup_UMPFactory.h | 4 +- .../midi/ump/yup_UMPIterator.cpp | 4 +- .../midi/ump/yup_UMPIterator.h | 4 +- .../midi/ump/yup_UMPKeyboardState.cpp | 277 ++++++++ .../midi/ump/yup_UMPKeyboardState.h | 126 ++++ .../ump/yup_UMPMidi1ToBytestreamTranslator.h | 4 +- .../yup_UMPMidi1ToMidi2DefaultTranslator.cpp | 4 +- .../yup_UMPMidi1ToMidi2DefaultTranslator.h | 4 +- .../midi/ump/yup_UMPPacketBuffer.cpp | 176 +++++ .../midi/ump/yup_UMPPacketBuffer.h | 199 ++++++ .../midi/ump/yup_UMPProtocols.h | 4 +- .../midi/ump/yup_UMPReceiver.h | 4 +- .../midi/ump/yup_UMPSysEx7.cpp | 4 +- .../yup_audio_basics/midi/ump/yup_UMPSysEx7.h | 4 +- .../midi/ump/yup_UMPUtils.cpp | 4 +- .../yup_audio_basics/midi/ump/yup_UMPUtils.h | 4 +- .../yup_audio_basics/midi/ump/yup_UMPView.cpp | 4 +- .../yup_audio_basics/midi/ump/yup_UMPView.h | 4 +- .../yup_audio_basics/midi/ump/yup_UMPacket.h | 4 +- .../yup_audio_basics/midi/ump/yup_UMPackets.h | 4 +- modules/yup_audio_basics/yup_audio_basics.cpp | 2 + modules/yup_audio_basics/yup_audio_basics.h | 13 +- .../audio_io/yup_AudioDeviceManager.cpp | 46 +- .../audio_io/yup_AudioDeviceManager.h | 3 + .../ump/yup_UMPBytestreamInputHandler.h | 4 +- .../midi_io/ump/yup_UMPPacketCollector.cpp | 194 ++++++ .../midi_io/ump/yup_UMPPacketCollector.h | 96 +++ .../midi_io/ump/yup_UMPU32InputHandler.h | 4 +- .../midi_io/yup_MidiDevices.cpp | 6 +- .../midi_io/yup_MidiDevices.h | 34 +- .../native/yup_Bela_linux.cpp | 24 +- .../native/yup_CoreMidi_apple.mm | 636 +++++++++++------- .../native/yup_Midi_android.cpp | 42 +- .../native/yup_Midi_linux.cpp | 73 +- .../native/yup_Midi_wasm.cpp | 19 +- .../native/yup_Midi_windows.cpp | 42 +- .../yup_audio_devices/yup_audio_devices.cpp | 1 + modules/yup_audio_devices/yup_audio_devices.h | 1 + tests/yup_audio_basics/yup_UMP.cpp | 12 +- 45 files changed, 1788 insertions(+), 335 deletions(-) create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPKeyboardState.cpp create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPKeyboardState.h create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPPacketBuffer.cpp create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPPacketBuffer.h create mode 100644 modules/yup_audio_devices/midi_io/ump/yup_UMPPacketCollector.cpp create mode 100644 modules/yup_audio_devices/midi_io/ump/yup_UMPPacketCollector.h diff --git a/modules/yup_audio_basics/midi/ump/yup_UMP.h b/modules/yup_audio_basics/midi/ump/yup_UMP.h index cf8a721b6..7344e5b0e 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMP.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMP.h @@ -51,12 +51,3 @@ #include "yup_UMPConverters.h" #include "yup_UMPDispatcher.h" #include "yup_UMPReceiver.h" - -#ifndef DOXYGEN - -namespace yup -{ -namespace ump = universal_midi_packets; -} // namespace yup - -#endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPBytesOnGroup.h b/modules/yup_audio_basics/midi/ump/yup_UMPBytesOnGroup.h index 0693274a3..98f7ac662 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPBytesOnGroup.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPBytesOnGroup.h @@ -37,7 +37,7 @@ ============================================================================== */ -namespace yup::universal_midi_packets +namespace yup::ump { /** @@ -52,4 +52,4 @@ struct BytesOnGroup Span bytes; }; -} // namespace yup::universal_midi_packets +} // namespace yup::ump diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPConversion.h b/modules/yup_audio_basics/midi/ump/yup_UMPConversion.h index 9782cb342..5445e3a5c 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPConversion.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPConversion.h @@ -39,7 +39,7 @@ #ifndef DOXYGEN -namespace yup::universal_midi_packets +namespace yup::ump { /** Represents a MIDI message that happened at a particular time. @@ -372,6 +372,6 @@ struct Conversion } }; -} // namespace yup::universal_midi_packets +} // namespace yup::ump #endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPConverters.h b/modules/yup_audio_basics/midi/ump/yup_UMPConverters.h index c5634980b..1d7db930d 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPConverters.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPConverters.h @@ -39,7 +39,7 @@ #ifndef DOXYGEN -namespace yup::universal_midi_packets +namespace yup::ump { /** Allows conversion from bytestream- or Universal MIDI Packet-formatted @@ -201,6 +201,6 @@ struct ToBytestreamConverter Midi1ToBytestreamTranslator translator; }; -} // namespace yup::universal_midi_packets +} // namespace yup::ump #endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPDeviceInfo.h b/modules/yup_audio_basics/midi/ump/yup_UMPDeviceInfo.h index d1d804cbb..ab508b18e 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPDeviceInfo.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPDeviceInfo.h @@ -37,7 +37,7 @@ ============================================================================== */ -namespace yup::universal_midi_packets +namespace yup::ump { /** @@ -73,4 +73,4 @@ struct DeviceInfo } }; -} // namespace yup::universal_midi_packets +} // namespace yup::ump diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPDispatcher.h b/modules/yup_audio_basics/midi/ump/yup_UMPDispatcher.h index 9cb1682dd..7c7898602 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPDispatcher.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPDispatcher.h @@ -39,7 +39,7 @@ #ifndef DOXYGEN -namespace yup::universal_midi_packets +namespace yup::ump { /** @@ -209,6 +209,6 @@ class ToBytestreamDispatcher ToBytestreamConverter converter; }; -} // namespace yup::universal_midi_packets +} // namespace yup::ump #endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPFactory.h b/modules/yup_audio_basics/midi/ump/yup_UMPFactory.h index 54c7107fa..54fa46eef 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPFactory.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPFactory.h @@ -39,7 +39,7 @@ #ifndef DOXYGEN -namespace yup::universal_midi_packets +namespace yup::ump { /** @@ -434,6 +434,6 @@ struct Factory } }; -} // namespace yup::universal_midi_packets +} // namespace yup::ump #endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPIterator.cpp b/modules/yup_audio_basics/midi/ump/yup_UMPIterator.cpp index d88125142..8c122b5a5 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPIterator.cpp +++ b/modules/yup_audio_basics/midi/ump/yup_UMPIterator.cpp @@ -37,7 +37,7 @@ ============================================================================== */ -namespace yup::universal_midi_packets +namespace yup::ump { Iterator::Iterator (const uint32_t* ptr, [[maybe_unused]] size_t bytes) noexcept @@ -48,4 +48,4 @@ Iterator::Iterator (const uint32_t* ptr, [[maybe_unused]] size_t bytes) noexcept { } -} // namespace yup::universal_midi_packets +} // namespace yup::ump diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPIterator.h b/modules/yup_audio_basics/midi/ump/yup_UMPIterator.h index 640ad4cca..d593ce49b 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPIterator.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPIterator.h @@ -39,7 +39,7 @@ #ifndef DOXYGEN -namespace yup::universal_midi_packets +namespace yup::ump { /** @@ -132,6 +132,6 @@ class YUP_API Iterator #endif }; -} // namespace yup::universal_midi_packets +} // namespace yup::ump #endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPKeyboardState.cpp b/modules/yup_audio_basics/midi/ump/yup_UMPKeyboardState.cpp new file mode 100644 index 000000000..09f20ad82 --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPKeyboardState.cpp @@ -0,0 +1,277 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +namespace UMPKeyboardStateHelpers +{ +inline float toFloatVelocity (uint8_t velocity) noexcept +{ + return velocity / 127.0f; +} + +inline float toFloatVelocity (uint16_t velocity) noexcept +{ + return velocity / 65535.0f; +} + +inline uint8_t toVelocity7 (float velocity) noexcept +{ + return static_cast (jlimit (0, 127, roundToInt (velocity * 127.0f))); +} + +inline uint16_t toVelocity16 (float velocity) noexcept +{ + return static_cast (jlimit (0, 65535, roundToInt (velocity * 65535.0f))); +} + +inline bool isAllNotesOffController (uint8_t controller) noexcept +{ + return controller == 123; +} +} // namespace UMPKeyboardStateHelpers + +UMPKeyboardState::UMPKeyboardState (ump::PacketProtocol protocolIn) + : protocol (protocolIn) +{ + zerostruct (noteStates); +} + +//============================================================================== +void UMPKeyboardState::reset() +{ + const ScopedLock sl (lock); + zerostruct (noteStates); + eventsToAdd.clear(); +} + +bool UMPKeyboardState::isNoteOn (const int midiChannel, const int n) const noexcept +{ + jassert (midiChannel > 0 && midiChannel <= 16); + + return isPositiveAndBelow (n, 128) + && (noteStates[n] & (1 << (midiChannel - 1))) != 0; +} + +bool UMPKeyboardState::isNoteOnForChannels (const int midiChannelMask, const int n) const noexcept +{ + return isPositiveAndBelow (n, 128) + && (noteStates[n] & midiChannelMask) != 0; +} + +void UMPKeyboardState::noteOn (const int midiChannel, const int midiNoteNumber, const float velocity) +{ + jassert (midiChannel > 0 && midiChannel <= 16); + jassert (isPositiveAndBelow (midiNoteNumber, 128)); + + const ScopedLock sl (lock); + + if (isPositiveAndBelow (midiNoteNumber, 128)) + { + const int timeNow = (int) Time::getMillisecondCounter(); + + if (protocol == ump::PacketProtocol::MIDI_1_0) + { + const auto packet = ump::Factory::makeNoteOnV1 (group, + static_cast (midiChannel - 1), + static_cast (midiNoteNumber), + UMPKeyboardStateHelpers::toVelocity7 (velocity)); + eventsToAdd.addEvent (ump::View (packet.data()), timeNow); + } + else + { + const auto packet = ump::Factory::makeNoteOnV2 (group, + static_cast (midiChannel - 1), + static_cast (midiNoteNumber), + ump::Factory::NoteAttributeKind::none, + UMPKeyboardStateHelpers::toVelocity16 (velocity), + 0); + eventsToAdd.addEvent (ump::View (packet.data()), timeNow); + } + + eventsToAdd.clear (0, timeNow - 500); + + noteOnInternal (midiChannel, midiNoteNumber, velocity); + } +} + +void UMPKeyboardState::noteOnInternal (const int midiChannel, const int midiNoteNumber, const float velocity) +{ + if (isPositiveAndBelow (midiNoteNumber, 128)) + { + noteStates[midiNoteNumber] = static_cast (noteStates[midiNoteNumber] | (1 << (midiChannel - 1))); + listeners.call ([&] (Listener& l) + { + l.handleNoteOn (this, midiChannel, midiNoteNumber, velocity); + }); + } +} + +void UMPKeyboardState::noteOff (const int midiChannel, const int midiNoteNumber, const float velocity) +{ + const ScopedLock sl (lock); + + if (isNoteOn (midiChannel, midiNoteNumber)) + { + const int timeNow = (int) Time::getMillisecondCounter(); + + if (protocol == ump::PacketProtocol::MIDI_1_0) + { + const auto packet = ump::Factory::makeNoteOffV1 (group, + static_cast (midiChannel - 1), + static_cast (midiNoteNumber), + UMPKeyboardStateHelpers::toVelocity7 (velocity)); + eventsToAdd.addEvent (ump::View (packet.data()), timeNow); + } + else + { + const auto packet = ump::Factory::makeNoteOffV2 (group, + static_cast (midiChannel - 1), + static_cast (midiNoteNumber), + ump::Factory::NoteAttributeKind::none, + UMPKeyboardStateHelpers::toVelocity16 (velocity), + 0); + eventsToAdd.addEvent (ump::View (packet.data()), timeNow); + } + + eventsToAdd.clear (0, timeNow - 500); + + noteOffInternal (midiChannel, midiNoteNumber, velocity); + } +} + +void UMPKeyboardState::noteOffInternal (const int midiChannel, const int midiNoteNumber, const float velocity) +{ + if (isNoteOn (midiChannel, midiNoteNumber)) + { + noteStates[midiNoteNumber] = static_cast (noteStates[midiNoteNumber] & ~(1 << (midiChannel - 1))); + listeners.call ([&] (Listener& l) + { + l.handleNoteOff (this, midiChannel, midiNoteNumber, velocity); + }); + } +} + +void UMPKeyboardState::allNotesOff (const int midiChannel) +{ + const ScopedLock sl (lock); + + if (midiChannel <= 0) + { + for (int i = 1; i <= 16; ++i) + allNotesOff (i); + } + else + { + for (int i = 0; i < 128; ++i) + noteOff (midiChannel, i, 0.0f); + } +} + +void UMPKeyboardState::processNextUMPPacket (const ump::View& packet) +{ + using Utils = ump::Utils; + const auto firstWord = packet[0]; + const auto messageType = Utils::getMessageType (firstWord); + const auto packetGroup = Utils::getGroup (firstWord); + + if (messageType != 0x2 && messageType != 0x4) + return; + + if (packetGroup != group) + return; + + const auto status = Utils::getStatus (firstWord); + const auto channel = Utils::getChannel (firstWord) + 1; + + if (status == 0x8 || status == 0x9) + { + const auto note = Utils::U8<2>::get (firstWord); + + if (messageType == 0x2) + { + const auto velocity = Utils::U8<3>::get (firstWord); + + if (status == 0x9 && velocity != 0) + noteOnInternal (channel, note, UMPKeyboardStateHelpers::toFloatVelocity (velocity)); + else + noteOffInternal (channel, note, UMPKeyboardStateHelpers::toFloatVelocity (velocity)); + } + else + { + const auto velocityWord = static_cast (packet[1] >> 16); + + if (status == 0x9 && velocityWord != 0) + noteOnInternal (channel, note, UMPKeyboardStateHelpers::toFloatVelocity (velocityWord)); + else + noteOffInternal (channel, note, UMPKeyboardStateHelpers::toFloatVelocity (velocityWord)); + } + } + else if (status == 0xb) + { + const auto controller = Utils::U8<2>::get (firstWord); + + if (UMPKeyboardStateHelpers::isAllNotesOffController (controller)) + for (int i = 0; i < 128; ++i) + noteOffInternal (channel, i, 0.0f); + } +} + +void UMPKeyboardState::processNextUMPBuffer (UMPPacketBuffer& buffer, + const int startSample, + const int numSamples, + const bool injectIndirectEvents) +{ + const ScopedLock sl (lock); + + for (const auto metadata : buffer) + processNextUMPPacket (metadata.getView()); + + if (injectIndirectEvents) + { + const int firstEventToAdd = eventsToAdd.getFirstEventTime(); + const double scaleFactor = numSamples / (double) (eventsToAdd.getLastEventTime() + 1 - firstEventToAdd); + + for (const auto metadata : eventsToAdd) + { + const auto pos = jlimit (0, numSamples - 1, roundToInt ((metadata.samplePosition - firstEventToAdd) * scaleFactor)); + buffer.addEvent (metadata.data, metadata.numWords, startSample + pos); + } + } + + eventsToAdd.clear(); +} + +//============================================================================== +void UMPKeyboardState::addListener (Listener* listener) +{ + const ScopedLock sl (lock); + listeners.add (listener); +} + +void UMPKeyboardState::removeListener (Listener* listener) +{ + const ScopedLock sl (lock); + listeners.remove (listener); +} + +} // namespace yup diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPKeyboardState.h b/modules/yup_audio_basics/midi/ump/yup_UMPKeyboardState.h new file mode 100644 index 000000000..4e2c86079 --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPKeyboardState.h @@ -0,0 +1,126 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** + Represents a UMP keyboard, keeping track of which keys are currently pressed. + + This object can parse a stream of UMP channel voice events, using them to + update its idea of which keys are pressed for each individual channel. + Incoming packets from other groups are ignored. + + @tags{Audio} +*/ +class YUP_API UMPKeyboardState +{ +public: + //============================================================================== + explicit UMPKeyboardState (ump::PacketProtocol protocolIn = ump::PacketProtocol::MIDI_2_0); + + //============================================================================== + /** Resets the state of the object. */ + void reset(); + + /** Returns true if the given key is currently held down for the given channel. */ + bool isNoteOn (int midiChannel, int midiNoteNumber) const noexcept; + + /** Returns true if the given key is currently held down on any of a set of channels. */ + bool isNoteOnForChannels (int midiChannelMask, int midiNoteNumber) const noexcept; + + /** Turns a specified note on. */ + void noteOn (int midiChannel, int midiNoteNumber, float velocity); + + /** Turns a specified note off. */ + void noteOff (int midiChannel, int midiNoteNumber, float velocity); + + /** Turns off any currently-down notes for the given channel. */ + void allNotesOff (int midiChannel); + + //============================================================================== + /** Looks at a UMP packet and uses it to update the state of this object. */ + void processNextUMPPacket (const ump::View& packet); + + /** Scans a UMP buffer for up/down events and adds its own events to it. */ + void processNextUMPBuffer (UMPPacketBuffer& buffer, + int startSample, + int numSamples, + bool injectIndirectEvents); + + //============================================================================== + /** Receives events from a UMPKeyboardState object. */ + class YUP_API Listener + { + public: + virtual ~Listener() = default; + + /** Called when one of the UMPKeyboardState's keys is pressed. */ + virtual void handleNoteOn (UMPKeyboardState* source, + int midiChannel, + int midiNoteNumber, + float velocity) = 0; + + /** Called when one of the UMPKeyboardState's keys is released. */ + virtual void handleNoteOff (UMPKeyboardState* source, + int midiChannel, + int midiNoteNumber, + float velocity) = 0; + }; + + /** Registers a listener for callbacks when keys go up or down. */ + void addListener (Listener* listener); + + /** Deregisters a listener. */ + void removeListener (Listener* listener); + + /** Sets the UMP protocol to use when generating events. */ + void setProtocol (ump::PacketProtocol protocolIn) noexcept { protocol = protocolIn; } + + /** Returns the UMP protocol used when generating events. */ + ump::PacketProtocol getProtocol() const noexcept { return protocol; } + + /** Sets the UMP group to use when generating events. */ + void setGroup (uint8_t groupIn) noexcept { group = groupIn; } + + /** Returns the UMP group used when generating events. */ + uint8_t getGroup() const noexcept { return group; } + +private: + //============================================================================== + void noteOnInternal (int midiChannel, int midiNoteNumber, float velocity); + void noteOffInternal (int midiChannel, int midiNoteNumber, float velocity); + + ump::PacketProtocol protocol; + uint8_t group = 0; + + CriticalSection lock; + std::atomic noteStates[128]; + UMPPacketBuffer eventsToAdd; + ListenerList listeners; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UMPKeyboardState) +}; + +using UMPKeyboardStateListener = UMPKeyboardState::Listener; + +} // namespace yup diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToBytestreamTranslator.h b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToBytestreamTranslator.h index 8d8765a95..1ca6b0091 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToBytestreamTranslator.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToBytestreamTranslator.h @@ -39,7 +39,7 @@ #ifndef DOXYGEN -namespace yup::universal_midi_packets +namespace yup::ump { /** @@ -227,6 +227,6 @@ class Midi1ToBytestreamTranslator double pendingSysExTime = 0.0; }; -} // namespace yup::universal_midi_packets +} // namespace yup::ump #endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToMidi2DefaultTranslator.cpp b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToMidi2DefaultTranslator.cpp index 785be6f27..3ef1be150 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToMidi2DefaultTranslator.cpp +++ b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToMidi2DefaultTranslator.cpp @@ -37,7 +37,7 @@ ============================================================================== */ -namespace yup::universal_midi_packets +namespace yup::ump { PacketX2 Midi1ToMidi2DefaultTranslator::processNoteOnOrOff (const HelperValues helpers) @@ -209,4 +209,4 @@ bool Midi1ToMidi2DefaultTranslator::PnAccumulator::addByte (uint8_t cc, std::byt return true; } -} // namespace yup::universal_midi_packets +} // namespace yup::ump diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToMidi2DefaultTranslator.h b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToMidi2DefaultTranslator.h index f4bc59224..8ef7ae505 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToMidi2DefaultTranslator.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToMidi2DefaultTranslator.h @@ -39,7 +39,7 @@ #ifndef DOXYGEN -namespace yup::universal_midi_packets +namespace yup::ump { /** @@ -210,6 +210,6 @@ class YUP_API Midi1ToMidi2DefaultTranslator std::array groupBanks; }; -} // namespace yup::universal_midi_packets +} // namespace yup::ump #endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPPacketBuffer.cpp b/modules/yup_audio_basics/midi/ump/yup_UMPPacketBuffer.cpp new file mode 100644 index 000000000..745cac9ba --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPPacketBuffer.cpp @@ -0,0 +1,176 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +namespace UMPPacketBufferHelpers +{ +inline int getEventTime (const uint32_t* d) noexcept +{ + return static_cast (d[0]); +} + +inline uint16_t getEventWordCount (const uint32_t* d) noexcept +{ + return static_cast (d[1] & 0xffffu); +} + +inline uint32_t getEventTotalWords (const uint32_t* d) noexcept +{ + return 2u + getEventWordCount (d); +} + +static const uint32_t* findEventAfter (const uint32_t* d, + const uint32_t* endData, + int samplePosition) noexcept +{ + while (d < endData && getEventTime (d) <= samplePosition) + d += getEventTotalWords (d); + + return d; +} +} // namespace UMPPacketBufferHelpers + +//============================================================================== +UMPPacketBufferIterator& UMPPacketBufferIterator::operator++() noexcept +{ + data += UMPPacketBufferHelpers::getEventTotalWords (data); + return *this; +} + +UMPPacketBufferIterator UMPPacketBufferIterator::operator++ (int) noexcept +{ + auto copy = *this; + ++(*this); + return copy; +} + +UMPPacketBufferIterator::reference UMPPacketBufferIterator::operator*() const noexcept +{ + return { data + 2, UMPPacketBufferHelpers::getEventWordCount (data), UMPPacketBufferHelpers::getEventTime (data) }; +} + +//============================================================================== +UMPPacketBuffer::UMPPacketBuffer (const ump::View& packet) noexcept +{ + addEvent (packet, 0); +} + +void UMPPacketBuffer::swapWith (UMPPacketBuffer& other) noexcept { data.swapWith (other.data); } + +void UMPPacketBuffer::clear() noexcept { data.clearQuick(); } + +void UMPPacketBuffer::ensureSize (size_t minimumNumBytes) +{ + const auto minimumNumWords = (minimumNumBytes + sizeof (uint32_t) - 1) / sizeof (uint32_t); + data.ensureStorageAllocated ((int) minimumNumWords); +} + +bool UMPPacketBuffer::isEmpty() const noexcept { return data.size() == 0; } + +void UMPPacketBuffer::clear (int startSample, int numSamples) +{ + auto start = UMPPacketBufferHelpers::findEventAfter (data.begin(), data.end(), startSample - 1); + auto end = UMPPacketBufferHelpers::findEventAfter (start, data.end(), startSample + numSamples - 1); + + data.removeRange ((int) (start - data.begin()), (int) (end - start)); +} + +bool UMPPacketBuffer::addEvent (const ump::View& packet, int sampleNumber) +{ + return addEvent (packet.data(), static_cast (packet.size()), sampleNumber); +} + +bool UMPPacketBuffer::addEvent (const uint32_t* packetData, uint16_t numWords, int sampleNumber) +{ + if (packetData == nullptr || numWords == 0 || numWords > 4) + return false; + + const auto newItemSize = (int) (2u + numWords); + const auto offset = (int) (UMPPacketBufferHelpers::findEventAfter (data.begin(), data.end(), sampleNumber) - data.begin()); + + data.insertMultiple (offset, 0u, newItemSize); + + auto* d = data.begin() + offset; + d[0] = static_cast (sampleNumber); + d[1] = static_cast (numWords); + std::copy (packetData, packetData + numWords, d + 2); + + return true; +} + +void UMPPacketBuffer::addEvents (const UMPPacketBuffer& otherBuffer, + int startSample, + int numSamples, + int sampleDeltaToAdd) +{ + for (auto i = otherBuffer.findNextSamplePosition (startSample); i != otherBuffer.cend(); ++i) + { + const auto metadata = *i; + + if (metadata.samplePosition >= startSample + numSamples && numSamples >= 0) + break; + + addEvent (metadata.data, metadata.numWords, metadata.samplePosition + sampleDeltaToAdd); + } +} + +int UMPPacketBuffer::getNumEvents() const noexcept +{ + int n = 0; + auto end = data.end(); + + for (auto d = data.begin(); d < end; ++n) + d += UMPPacketBufferHelpers::getEventTotalWords (d); + + return n; +} + +int UMPPacketBuffer::getFirstEventTime() const noexcept +{ + return data.size() > 0 ? UMPPacketBufferHelpers::getEventTime (data.begin()) : 0; +} + +int UMPPacketBuffer::getLastEventTime() const noexcept +{ + if (data.size() == 0) + return 0; + + auto endData = data.end(); + + for (auto d = data.begin();;) + { + auto nextOne = d + UMPPacketBufferHelpers::getEventTotalWords (d); + + if (nextOne >= endData) + return UMPPacketBufferHelpers::getEventTime (d); + + d = nextOne; + } +} + +UMPPacketBufferIterator UMPPacketBuffer::findNextSamplePosition (int samplePosition) const noexcept +{ + return UMPPacketBufferIterator (UMPPacketBufferHelpers::findEventAfter (data.begin(), data.end(), samplePosition - 1)); +} + +} // namespace yup diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPPacketBuffer.h b/modules/yup_audio_basics/midi/ump/yup_UMPPacketBuffer.h new file mode 100644 index 000000000..e78ac9fdc --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPPacketBuffer.h @@ -0,0 +1,199 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** + A view of UMP packet data stored in a contiguous buffer. + + Instances of this class do *not* own the UMP data words that they point to. + Instead, they expect the UMP data to live in a separate buffer that outlives + the UMPPacketMetadata instance. + + @tags{Audio} +*/ +struct UMPPacketMetadata +{ + UMPPacketMetadata() noexcept = default; + + UMPPacketMetadata (const uint32_t* dataIn, uint16_t numWordsIn, int positionIn) noexcept + : data (dataIn) + , numWords (numWordsIn) + , samplePosition (positionIn) + { + } + + /** Constructs a UMP view from the data that this object is viewing. */ + ump::View getView() const { return ump::View (data); } + + /** Pointer to the first word of a UMP packet. */ + const uint32_t* data = nullptr; + + /** The number of 32-bit words in the UMP packet. */ + uint16_t numWords = 0; + + /** The packet's timestamp. */ + int samplePosition = 0; +}; + +//============================================================================== +/** + An iterator to move over contiguous UMP data, which allows iterating + over a UMPPacketBuffer using C++11 range-for syntax. + + @tags{Audio} +*/ +class YUP_API UMPPacketBufferIterator +{ + using Ptr = const uint32_t*; + +public: + UMPPacketBufferIterator() = default; + + /** Constructs an iterator pointing at the packet starting at the word `dataIn`. + `dataIn` must point to the start of a valid packet entry. + */ + explicit UMPPacketBufferIterator (const uint32_t* dataIn) noexcept + : data (dataIn) + { + } + + using difference_type = std::iterator_traits::difference_type; + using value_type = UMPPacketMetadata; + using reference = UMPPacketMetadata; + using pointer = void; + using iterator_category = std::input_iterator_tag; + + /** Make this iterator point to the next packet in the buffer. */ + UMPPacketBufferIterator& operator++() noexcept; + + /** Create a copy of this object, make this iterator point to the next packet in + the buffer, then return the copy. + */ + UMPPacketBufferIterator operator++ (int) noexcept; + + /** Return true if this iterator points to the same packet as another + iterator instance, otherwise return false. + */ + bool operator== (const UMPPacketBufferIterator& other) const noexcept { return data == other.data; } + + /** Return false if this iterator points to the same packet as another + iterator instance, otherwise returns true. + */ + bool operator!= (const UMPPacketBufferIterator& other) const noexcept { return ! operator== (other); } + + /** Return an instance of UMPPacketMetadata which describes the packet to which + the iterator is currently pointing. + */ + reference operator*() const noexcept; + +private: + Ptr data = nullptr; +}; + +//============================================================================== +/** + Holds a sequence of time-stamped UMP packets. + + Analogous to the MidiBuffer, this holds a set of UMP packets with + integer time-stamps. The buffer is kept sorted in order of the time-stamps. + + @tags{Audio} +*/ +class YUP_API UMPPacketBuffer +{ +public: + //============================================================================== + /** Creates an empty UMPPacketBuffer. */ + UMPPacketBuffer() noexcept = default; + + /** Creates a UMPPacketBuffer containing a single packet. */ + explicit UMPPacketBuffer (const ump::View& packet) noexcept; + + //============================================================================== + /** Removes all events from the buffer. */ + void clear() noexcept; + + /** Removes all events between two times from the buffer. */ + void clear (int startSample, int numSamples); + + /** Returns true if the buffer is empty. */ + bool isEmpty() const noexcept; + + /** Counts the number of events in the buffer. */ + int getNumEvents() const noexcept; + + /** Adds an event to the buffer. */ + bool addEvent (const ump::View& packet, int sampleNumber); + + /** Adds an event to the buffer from raw UMP data. */ + bool addEvent (const uint32_t* data, uint16_t numWords, int sampleNumber); + + /** Adds the events from another buffer. */ + void addEvents (const UMPPacketBuffer& otherBuffer, + int startSample, + int numSamples, + int sampleDeltaToAdd); + + /** Returns the sample number of the first event in the buffer. + If the buffer's empty, this will just return 0. + */ + int getFirstEventTime() const noexcept; + + /** Returns the sample number of the last event in the buffer. + If the buffer's empty, this will just return 0. + */ + int getLastEventTime() const noexcept; + + //============================================================================== + /** Exchanges the contents of this buffer with another one. */ + void swapWith (UMPPacketBuffer&) noexcept; + + /** Preallocates some memory for the buffer to use. */ + void ensureSize (size_t minimumNumBytes); + + /** Get a read-only iterator pointing to the beginning of this buffer. */ + UMPPacketBufferIterator begin() const noexcept { return cbegin(); } + + /** Get a read-only iterator pointing one past the end of this buffer. */ + UMPPacketBufferIterator end() const noexcept { return cend(); } + + /** Get a read-only iterator pointing to the beginning of this buffer. */ + UMPPacketBufferIterator cbegin() const noexcept { return UMPPacketBufferIterator (data.begin()); } + + /** Get a read-only iterator pointing one past the end of this buffer. */ + UMPPacketBufferIterator cend() const noexcept { return UMPPacketBufferIterator (data.end()); } + + /** Get an iterator pointing to the first event with a timestamp greater-than or + equal-to `samplePosition`. + */ + UMPPacketBufferIterator findNextSamplePosition (int samplePosition) const noexcept; + + /** The raw data holding this buffer. */ + Array data; + +private: + YUP_LEAK_DETECTOR (UMPPacketBuffer) +}; + +} // namespace yup diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPProtocols.h b/modules/yup_audio_basics/midi/ump/yup_UMPProtocols.h index a98a083b2..995551cd7 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPProtocols.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPProtocols.h @@ -39,7 +39,7 @@ #ifndef DOXYGEN -namespace yup::universal_midi_packets +namespace yup::ump { /** The kinds of MIDI protocol that can be formatted into Universal MIDI Packets. */ @@ -57,6 +57,6 @@ enum class MidiProtocol UMP_MIDI_2_0, }; -} // namespace yup::universal_midi_packets +} // namespace yup::ump #endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPReceiver.h b/modules/yup_audio_basics/midi/ump/yup_UMPReceiver.h index d8afae4bd..66011c8bf 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPReceiver.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPReceiver.h @@ -39,7 +39,7 @@ #ifndef DOXYGEN -namespace yup::universal_midi_packets +namespace yup::ump { /** @@ -55,6 +55,6 @@ struct Receiver virtual void packetReceived (const View& packet, double time) = 0; }; -} // namespace yup::universal_midi_packets +} // namespace yup::ump #endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPSysEx7.cpp b/modules/yup_audio_basics/midi/ump/yup_UMPSysEx7.cpp index c19c0e924..24d3ae31d 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPSysEx7.cpp +++ b/modules/yup_audio_basics/midi/ump/yup_UMPSysEx7.cpp @@ -37,7 +37,7 @@ ============================================================================== */ -namespace yup::universal_midi_packets +namespace yup::ump { uint32_t SysEx7::getNumPacketsRequiredForDataSize (uint32_t size) @@ -63,4 +63,4 @@ SysEx7::PacketBytes SysEx7::getDataBytes (const PacketX2& packet) }; } -} // namespace yup::universal_midi_packets +} // namespace yup::ump diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPSysEx7.h b/modules/yup_audio_basics/midi/ump/yup_UMPSysEx7.h index 2d85e9e8b..fed54db55 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPSysEx7.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPSysEx7.h @@ -39,7 +39,7 @@ #ifndef DOXYGEN -namespace yup::universal_midi_packets +namespace yup::ump { /** @@ -86,6 +86,6 @@ struct YUP_API SysEx7 static PacketBytes getDataBytes (const PacketX2& packet); }; -} // namespace yup::universal_midi_packets +} // namespace yup::ump #endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPUtils.cpp b/modules/yup_audio_basics/midi/ump/yup_UMPUtils.cpp index b7c278775..af7aa3564 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPUtils.cpp +++ b/modules/yup_audio_basics/midi/ump/yup_UMPUtils.cpp @@ -37,7 +37,7 @@ ============================================================================== */ -namespace yup::universal_midi_packets +namespace yup::ump { uint32_t Utils::getNumWordsForMessageType (uint32_t mt) @@ -70,4 +70,4 @@ uint32_t Utils::getNumWordsForMessageType (uint32_t mt) return 1; } -} // namespace yup::universal_midi_packets +} // namespace yup::ump diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPUtils.h b/modules/yup_audio_basics/midi/ump/yup_UMPUtils.h index 5f56a5dd4..0d4b0184d 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPUtils.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPUtils.h @@ -39,7 +39,7 @@ #ifndef DOXYGEN -namespace yup::universal_midi_packets +namespace yup::ump { /** @@ -132,6 +132,6 @@ struct YUP_API Utils static constexpr uint8_t getChannel (uint32_t w) noexcept { return U4<3>::get (w); } }; -} // namespace yup::universal_midi_packets +} // namespace yup::ump #endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPView.cpp b/modules/yup_audio_basics/midi/ump/yup_UMPView.cpp index f46301d80..4d43d3a28 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPView.cpp +++ b/modules/yup_audio_basics/midi/ump/yup_UMPView.cpp @@ -37,7 +37,7 @@ ============================================================================== */ -namespace yup::universal_midi_packets +namespace yup::ump { uint32_t View::size() const noexcept @@ -46,4 +46,4 @@ uint32_t View::size() const noexcept return Utils::getNumWordsForMessageType (*ptr); } -} // namespace yup::universal_midi_packets +} // namespace yup::ump diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPView.h b/modules/yup_audio_basics/midi/ump/yup_UMPView.h index b7131ef53..352bf2eef 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPView.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPView.h @@ -39,7 +39,7 @@ #ifndef DOXYGEN -namespace yup::universal_midi_packets +namespace yup::ump { /** @@ -106,6 +106,6 @@ class YUP_API View const uint32_t* ptr = nullptr; }; -} // namespace yup::universal_midi_packets +} // namespace yup::ump #endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPacket.h b/modules/yup_audio_basics/midi/ump/yup_UMPacket.h index 363937d64..60fbb5ca2 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPacket.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPacket.h @@ -39,7 +39,7 @@ #ifndef DOXYGEN -namespace yup::universal_midi_packets +namespace yup::ump { /** @@ -205,6 +205,6 @@ using PacketX2 = Packet<2>; using PacketX3 = Packet<3>; using PacketX4 = Packet<4>; -} // namespace yup::universal_midi_packets +} // namespace yup::ump #endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPackets.h b/modules/yup_audio_basics/midi/ump/yup_UMPackets.h index 04340b44e..20cbf34fd 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPackets.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPackets.h @@ -39,7 +39,7 @@ #ifndef DOXYGEN -namespace yup::universal_midi_packets +namespace yup::ump { /** @@ -110,6 +110,6 @@ class YUP_API Packets std::vector storage; }; -} // namespace yup::universal_midi_packets +} // namespace yup::ump #endif diff --git a/modules/yup_audio_basics/yup_audio_basics.cpp b/modules/yup_audio_basics/yup_audio_basics.cpp index 60c63befa..994e1d260 100644 --- a/modules/yup_audio_basics/yup_audio_basics.cpp +++ b/modules/yup_audio_basics/yup_audio_basics.cpp @@ -64,6 +64,8 @@ #include "midi/yup_MidiMessage.cpp" #include "midi/yup_MidiMessageSequence.cpp" #include "midi/yup_MidiRPN.cpp" +#include "midi/ump/yup_UMPPacketBuffer.cpp" +#include "midi/ump/yup_UMPKeyboardState.cpp" #include "mpe/yup_MPEValue.cpp" #include "mpe/yup_MPENote.cpp" #include "mpe/yup_MPEZoneLayout.cpp" diff --git a/modules/yup_audio_basics/yup_audio_basics.h b/modules/yup_audio_basics/yup_audio_basics.h index 22fc858d6..c97cadaa5 100644 --- a/modules/yup_audio_basics/yup_audio_basics.h +++ b/modules/yup_audio_basics/yup_audio_basics.h @@ -69,19 +69,19 @@ //============================================================================== #ifndef YUP_USE_SSE_INTRINSICS -#if defined (__SSE__) +#if defined(__SSE__) #define YUP_USE_SSE_INTRINSICS 1 #endif #endif #ifndef YUP_USE_AVX_INTRINSICS -#if defined (__AVX2__) +#if defined(__AVX2__) #define YUP_USE_AVX_INTRINSICS 1 #endif #endif #ifndef YUP_USE_FMA_INTRINSICS -#if defined (__FMA__) +#if defined(__FMA__) #define YUP_USE_FMA_INTRINSICS 1 #endif #endif @@ -180,8 +180,5 @@ YUP_END_IGNORE_WARNINGS_MSVC #include "midi/ump/yup_UMPBytesOnGroup.h" #include "midi/ump/yup_UMPDeviceInfo.h" #include "midi/ump/yup_UMP.h" - -namespace yup -{ -namespace ump = universal_midi_packets; -} // namespace yup +#include "midi/ump/yup_UMPPacketBuffer.h" +#include "midi/ump/yup_UMPKeyboardState.h" diff --git a/modules/yup_audio_devices/audio_io/yup_AudioDeviceManager.cpp b/modules/yup_audio_devices/audio_io/yup_AudioDeviceManager.cpp index b36d4a7af..685161930 100644 --- a/modules/yup_audio_devices/audio_io/yup_AudioDeviceManager.cpp +++ b/modules/yup_audio_devices/audio_io/yup_AudioDeviceManager.cpp @@ -67,6 +67,17 @@ static auto tie (const AudioDeviceManager::AudioDeviceSetup& s) s.useDefaultOutputChannels); } +static ump::PacketProtocol parseMidiProtocol (const String& value) +{ + return value.equalsIgnoreCase ("midi2") ? ump::PacketProtocol::MIDI_2_0 + : ump::PacketProtocol::MIDI_1_0; +} + +static String midiProtocolToString (ump::PacketProtocol protocol) +{ + return protocol == ump::PacketProtocol::MIDI_2_0 ? "midi2" : "midi1"; +} + bool AudioDeviceManager::AudioDeviceSetup::operator== (const AudioDeviceManager::AudioDeviceSetup& other) const { return tie (*this) == tie (other); @@ -462,13 +473,18 @@ String AudioDeviceManager::initialiseFromXML (const XmlElement& xml, Array result; for (auto* c : xml.getChildWithTagNameIterator ("MIDIINPUT")) - result.add ({ c->getStringAttribute ("name"), c->getStringAttribute ("identifier") }); + { + result.add ({ c->getStringAttribute ("name"), + c->getStringAttribute ("identifier"), + parseMidiProtocol (c->getStringAttribute ("protocol")) }); + } return result; }(); const MidiDeviceInfo defaultOutputDeviceInfo (xml.getStringAttribute ("defaultMidiOutput"), - xml.getStringAttribute ("defaultMidiOutputDevice")); + xml.getStringAttribute ("defaultMidiOutputDevice"), + parseMidiProtocol (xml.getStringAttribute ("defaultMidiOutputProtocol"))); openLastRequestedMidiDevices (midiInputs, defaultOutputDeviceInfo); @@ -515,7 +531,7 @@ void AudioDeviceManager::openLastRequestedMidiDevices (const ArraysetAttribute ("name", input->getName()); child->setAttribute ("identifier", input->getIdentifier()); + child->setAttribute ("protocol", midiProtocolToString (input->getDeviceInfo().protocol)); } if (midiDeviceInfosFromXml.size() > 0) @@ -969,12 +986,20 @@ void AudioDeviceManager::updateXml() for (auto& d : midiDeviceInfosFromXml) { - if (! availableMidiDevices.contains (d)) + const auto isAvailable = std::any_of (availableMidiDevices.begin(), + availableMidiDevices.end(), + [&] (const auto& info) + { + return info.identifier == d.identifier; + }); + + if (! isAvailable) { auto* child = lastExplicitSettings->createNewChildElement ("MIDIINPUT"); child->setAttribute ("name", d.name); child->setAttribute ("identifier", d.identifier); + child->setAttribute ("protocol", midiProtocolToString (d.protocol)); } } } @@ -983,6 +1008,8 @@ void AudioDeviceManager::updateXml() { lastExplicitSettings->setAttribute ("defaultMidiOutput", defaultMidiOutputDeviceInfo.name); lastExplicitSettings->setAttribute ("defaultMidiOutputDevice", defaultMidiOutputDeviceInfo.identifier); + lastExplicitSettings->setAttribute ("defaultMidiOutputProtocol", + midiProtocolToString (defaultMidiOutputDeviceInfo.protocol)); } } @@ -1209,7 +1236,14 @@ void AudioDeviceManager::handleIncomingMidiMessageInt (MidiInput* source, const //============================================================================== void AudioDeviceManager::setDefaultMidiOutputDevice (const String& identifier) { - if (defaultMidiOutputDeviceInfo.identifier != identifier) + setDefaultMidiOutputDevice (identifier, ump::PacketProtocol::MIDI_1_0); +} + +void AudioDeviceManager::setDefaultMidiOutputDevice (const String& identifier, + ump::PacketProtocol protocol) +{ + if (defaultMidiOutputDeviceInfo.identifier != identifier + || defaultMidiOutputDeviceInfo.protocol != protocol) { std::unique_ptr oldMidiPort; Array oldCallbacks; @@ -1226,7 +1260,7 @@ void AudioDeviceManager::setDefaultMidiOutputDevice (const String& identifier) std::swap (oldMidiPort, defaultMidiOutput); if (identifier.isNotEmpty()) - defaultMidiOutput = MidiOutput::openDevice (identifier); + defaultMidiOutput = MidiOutput::openDevice (identifier, protocol); if (defaultMidiOutput != nullptr) defaultMidiOutputDeviceInfo = defaultMidiOutput->getDeviceInfo(); diff --git a/modules/yup_audio_devices/audio_io/yup_AudioDeviceManager.h b/modules/yup_audio_devices/audio_io/yup_AudioDeviceManager.h index f917ae743..5f26b5c28 100644 --- a/modules/yup_audio_devices/audio_io/yup_AudioDeviceManager.h +++ b/modules/yup_audio_devices/audio_io/yup_AudioDeviceManager.h @@ -387,6 +387,9 @@ class YUP_API AudioDeviceManager : public yup::ChangeBroadcaster */ void setDefaultMidiOutputDevice (const String& deviceIdentifier); + /** Opens a default MIDI output device with the requested protocol. */ + void setDefaultMidiOutputDevice (const String& deviceIdentifier, ump::PacketProtocol protocol); + /** Returns the name of the default midi output. @see setDefaultMidiOutputDevice, getDefaultMidiOutput diff --git a/modules/yup_audio_devices/midi_io/ump/yup_UMPBytestreamInputHandler.h b/modules/yup_audio_devices/midi_io/ump/yup_UMPBytestreamInputHandler.h index 612b5c34d..2c76fc4bd 100644 --- a/modules/yup_audio_devices/midi_io/ump/yup_UMPBytestreamInputHandler.h +++ b/modules/yup_audio_devices/midi_io/ump/yup_UMPBytestreamInputHandler.h @@ -39,7 +39,7 @@ #ifndef DOXYGEN -namespace yup::universal_midi_packets +namespace yup::ump { /** @@ -165,6 +165,6 @@ struct BytestreamToUMPHandler : public BytestreamInputHandler BytestreamToUMPDispatcher dispatcher; }; -} // namespace yup::universal_midi_packets +} // namespace yup::ump #endif diff --git a/modules/yup_audio_devices/midi_io/ump/yup_UMPPacketCollector.cpp b/modules/yup_audio_devices/midi_io/ump/yup_UMPPacketCollector.cpp new file mode 100644 index 000000000..01c389bd9 --- /dev/null +++ b/modules/yup_audio_devices/midi_io/ump/yup_UMPPacketCollector.cpp @@ -0,0 +1,194 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +namespace UMPPacketCollectorHelpers +{ +inline uint8_t toVelocity7 (float velocity) noexcept +{ + return static_cast (jlimit (0, 127, roundToInt (velocity * 127.0f))); +} + +inline uint16_t toVelocity16 (float velocity) noexcept +{ + return static_cast (jlimit (0, 65535, roundToInt (velocity * 65535.0f))); +} +} // namespace UMPPacketCollectorHelpers + +UMPPacketCollector::UMPPacketCollector (ump::PacketProtocol protocolIn, + uint8_t groupIn) + : protocol (protocolIn) + , group (groupIn) +{ +} + +UMPPacketCollector::~UMPPacketCollector() = default; + +//============================================================================== +void UMPPacketCollector::reset (const double newSampleRate) +{ + const ScopedLock sl (midiCallbackLock); + + jassert (newSampleRate > 0); + +#if YUP_DEBUG + hasCalledReset = true; +#endif + sampleRate = newSampleRate; + incomingPackets.clear(); + lastCallbackTime = Time::getMillisecondCounterHiRes(); +} + +void UMPPacketCollector::addPacketToQueue (const ump::View& packet, double timeStamp) +{ + const ScopedLock sl (midiCallbackLock); + +#if YUP_DEBUG + jassert (hasCalledReset); +#endif + + jassert (! approximatelyEqual (timeStamp, 0.0)); + + auto sampleNumber = (int) ((timeStamp - 0.001 * lastCallbackTime) * sampleRate); + + incomingPackets.addEvent (packet, sampleNumber); + + if (sampleNumber > sampleRate) + incomingPackets.clear (0, sampleNumber - (int) sampleRate); +} + +void UMPPacketCollector::removeNextBlockOfPackets (UMPPacketBuffer& destBuffer, + const int numSamples) +{ + const ScopedLock sl (midiCallbackLock); + +#if YUP_DEBUG + jassert (hasCalledReset); +#endif + + jassert (numSamples > 0); + + auto timeNow = Time::getMillisecondCounterHiRes(); + auto msElapsed = timeNow - lastCallbackTime; + + lastCallbackTime = timeNow; + + if (! incomingPackets.isEmpty()) + { + int numSourceSamples = jmax (1, roundToInt (msElapsed * 0.001 * sampleRate)); + int startSample = 0; + int scale = 1 << 16; + + if (numSourceSamples > numSamples) + { + const int maxBlockLengthToUse = numSamples << 5; + + auto iter = incomingPackets.cbegin(); + + if (numSourceSamples > maxBlockLengthToUse) + { + startSample = numSourceSamples - maxBlockLengthToUse; + numSourceSamples = maxBlockLengthToUse; + iter = incomingPackets.findNextSamplePosition (startSample); + } + + scale = (numSamples << 10) / numSourceSamples; + + std::for_each (iter, incomingPackets.cend(), [&] (const UMPPacketMetadata& meta) + { + const auto pos = ((meta.samplePosition - startSample) * scale) >> 10; + destBuffer.addEvent (meta.data, meta.numWords, jlimit (0, numSamples - 1, pos)); + }); + } + else + { + startSample = numSamples - numSourceSamples; + + for (const auto metadata : incomingPackets) + destBuffer.addEvent (metadata.data, metadata.numWords, jlimit (0, numSamples - 1, metadata.samplePosition + startSample)); + } + + incomingPackets.clear(); + } +} + +void UMPPacketCollector::ensureStorageAllocated (size_t bytes) +{ + incomingPackets.ensureSize (bytes); +} + +//============================================================================== +void UMPPacketCollector::handleNoteOn (UMPKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) +{ + const auto timeStamp = Time::getMillisecondCounterHiRes() * 0.001; + + if (protocol == ump::PacketProtocol::MIDI_1_0) + { + const auto packet = ump::Factory::makeNoteOnV1 (group, + static_cast (midiChannel - 1), + static_cast (midiNoteNumber), + UMPPacketCollectorHelpers::toVelocity7 (velocity)); + addPacketToQueue (ump::View (packet.data()), timeStamp); + } + else + { + const auto packet = ump::Factory::makeNoteOnV2 (group, + static_cast (midiChannel - 1), + static_cast (midiNoteNumber), + ump::Factory::NoteAttributeKind::none, + UMPPacketCollectorHelpers::toVelocity16 (velocity), + 0); + addPacketToQueue (ump::View (packet.data()), timeStamp); + } +} + +void UMPPacketCollector::handleNoteOff (UMPKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) +{ + const auto timeStamp = Time::getMillisecondCounterHiRes() * 0.001; + + if (protocol == ump::PacketProtocol::MIDI_1_0) + { + const auto packet = ump::Factory::makeNoteOffV1 (group, + static_cast (midiChannel - 1), + static_cast (midiNoteNumber), + UMPPacketCollectorHelpers::toVelocity7 (velocity)); + addPacketToQueue (ump::View (packet.data()), timeStamp); + } + else + { + const auto packet = ump::Factory::makeNoteOffV2 (group, + static_cast (midiChannel - 1), + static_cast (midiNoteNumber), + ump::Factory::NoteAttributeKind::none, + UMPPacketCollectorHelpers::toVelocity16 (velocity), + 0); + addPacketToQueue (ump::View (packet.data()), timeStamp); + } +} + +void UMPPacketCollector::packetReceived (const ump::View& packet, double time) +{ + addPacketToQueue (packet, time); +} + +} // namespace yup diff --git a/modules/yup_audio_devices/midi_io/ump/yup_UMPPacketCollector.h b/modules/yup_audio_devices/midi_io/ump/yup_UMPPacketCollector.h new file mode 100644 index 000000000..e48f11bfc --- /dev/null +++ b/modules/yup_audio_devices/midi_io/ump/yup_UMPPacketCollector.h @@ -0,0 +1,96 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** + Collects incoming realtime UMP packets and turns them into blocks suitable for + processing by a block-based audio callback. + + The class can also be used as a UMPKeyboardState::Listener, and as a UMP + Receiver so it can easily use a UMP input source or keyboard component. + + @tags{Audio} +*/ +class YUP_API UMPPacketCollector + : public UMPKeyboardState::Listener + , public ump::Receiver +{ +public: + //============================================================================== + /** Creates a UMPPacketCollector. */ + explicit UMPPacketCollector (ump::PacketProtocol protocolIn = ump::PacketProtocol::MIDI_2_0, + uint8_t groupIn = 0); + + /** Destructor. */ + ~UMPPacketCollector() override; + + //============================================================================== + /** Clears any packets from the queue. */ + void reset (double sampleRate); + + /** Takes an incoming packet and adds it to the queue. */ + void addPacketToQueue (const ump::View& packet, double timeStamp); + + /** Removes all the pending packets from the queue as a buffer. */ + void removeNextBlockOfPackets (UMPPacketBuffer& destBuffer, int numSamples); + + /** Preallocates storage for collected packets. */ + void ensureStorageAllocated (size_t bytes); + + /** Sets the UMP protocol used for generated packets. */ + void setProtocol (ump::PacketProtocol protocolIn) noexcept { protocol = protocolIn; } + + /** Returns the UMP protocol used for generated packets. */ + ump::PacketProtocol getProtocol() const noexcept { return protocol; } + + /** Sets the UMP group used for generated packets. */ + void setGroup (uint8_t groupIn) noexcept { group = groupIn; } + + /** Returns the UMP group used for generated packets. */ + uint8_t getGroup() const noexcept { return group; } + + //============================================================================== + /** @internal */ + void handleNoteOn (UMPKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override; + /** @internal */ + void handleNoteOff (UMPKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override; + /** @internal */ + void packetReceived (const ump::View& packet, double time) override; + +private: + //============================================================================== + double lastCallbackTime = 0; + CriticalSection midiCallbackLock; + UMPPacketBuffer incomingPackets; + double sampleRate = 44100.0; + ump::PacketProtocol protocol; + uint8_t group = 0; +#if YUP_DEBUG + bool hasCalledReset = false; +#endif + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UMPPacketCollector) +}; + +} // namespace yup diff --git a/modules/yup_audio_devices/midi_io/ump/yup_UMPU32InputHandler.h b/modules/yup_audio_devices/midi_io/ump/yup_UMPU32InputHandler.h index 22288cbbc..1788fb831 100644 --- a/modules/yup_audio_devices/midi_io/ump/yup_UMPU32InputHandler.h +++ b/modules/yup_audio_devices/midi_io/ump/yup_UMPU32InputHandler.h @@ -39,7 +39,7 @@ #ifndef DOXYGEN -namespace yup::universal_midi_packets +namespace yup::ump { /** @@ -176,6 +176,6 @@ struct U32ToUMPHandler : public U32InputHandler GenericUMPConverter converter; }; -} // namespace yup::universal_midi_packets +} // namespace yup::ump #endif diff --git a/modules/yup_audio_devices/midi_io/yup_MidiDevices.cpp b/modules/yup_audio_devices/midi_io/yup_MidiDevices.cpp index 36624a220..5eca8c486 100644 --- a/modules/yup_audio_devices/midi_io/yup_MidiDevices.cpp +++ b/modules/yup_audio_devices/midi_io/yup_MidiDevices.cpp @@ -125,9 +125,11 @@ void MidiInputCallback::handlePartialSysexMessage ([[maybe_unused]] MidiInput* s [[maybe_unused]] double timestamp) {} //============================================================================== -MidiOutput::MidiOutput (const String& deviceName, const String& deviceIdentifier) +MidiOutput::MidiOutput (const String& deviceName, + const String& deviceIdentifier, + ump::PacketProtocol protocol) : Thread ("midi out") - , deviceInfo (deviceName, deviceIdentifier) + , deviceInfo (deviceName, deviceIdentifier, protocol) { } diff --git a/modules/yup_audio_devices/midi_io/yup_MidiDevices.h b/modules/yup_audio_devices/midi_io/yup_MidiDevices.h index e3f7d7224..4958797af 100644 --- a/modules/yup_audio_devices/midi_io/yup_MidiDevices.h +++ b/modules/yup_audio_devices/midi_io/yup_MidiDevices.h @@ -140,9 +140,14 @@ struct MidiDeviceInfo { MidiDeviceInfo() = default; - MidiDeviceInfo (const String& deviceName, const String& deviceIdentifier) + MidiDeviceInfo (const String& deviceName, + const String& deviceIdentifier, + ump::PacketProtocol deviceProtocol = ump::PacketProtocol::MIDI_1_0, + bool supportsMidi2In = false) : name (deviceName) , identifier (deviceIdentifier) + , protocol (deviceProtocol) + , supportsMidi2 (supportsMidi2In || deviceProtocol == ump::PacketProtocol::MIDI_2_0) { } @@ -164,8 +169,14 @@ struct MidiDeviceInfo */ String identifier; + /** The protocol associated with this device info. */ + ump::PacketProtocol protocol = ump::PacketProtocol::MIDI_1_0; + + /** True if the OS reports MIDI 2.0 support for this device. */ + bool supportsMidi2 = false; + //============================================================================== - auto tie() const { return std::tie (name, identifier); } + auto tie() const { return std::tie (name, identifier, protocol, supportsMidi2); } bool operator== (const MidiDeviceInfo& other) const noexcept { return tie() == other.tie(); } @@ -215,6 +226,11 @@ class YUP_API MidiInput final */ static std::unique_ptr openDevice (const String& deviceIdentifier, MidiInputCallback* callback); + /** Tries to open one of the midi input devices with the requested protocol. */ + static std::unique_ptr openDevice (const String& deviceIdentifier, + ump::PacketProtocol protocol, + ump::Receiver* receiver); + #if YUP_LINUX || YUP_BSD || YUP_MAC || YUP_IOS || YUP_WASM || DOXYGEN /** This will try to create a new midi input device (only available on Linux, macOS and iOS). @@ -269,7 +285,7 @@ class YUP_API MidiInput final private: //============================================================================== - explicit MidiInput (const String&, const String&); + explicit MidiInput (const String&, const String&, ump::PacketProtocol = ump::PacketProtocol::MIDI_1_0); MidiDeviceInfo deviceInfo; @@ -365,6 +381,10 @@ class YUP_API MidiOutput final : private Thread */ static std::unique_ptr openDevice (const String& deviceIdentifier); + /** Tries to open one of the midi output devices with the requested protocol. */ + static std::unique_ptr openDevice (const String& deviceIdentifier, + ump::PacketProtocol protocol); + #if YUP_LINUX || YUP_BSD || YUP_MAC || YUP_IOS || YUP_WASM || DOXYGEN /** This will try to create a new midi output device (only available on Linux, macOS and iOS). @@ -401,6 +421,12 @@ class YUP_API MidiOutput final : private Thread /** Sends out a MIDI message immediately. */ void sendMessageNow (const MidiMessage& message); + /** Sends out a UMP packet immediately. */ + void sendMessageNow (const ump::View& message); + + /** Sends out a sequence of UMP packets immediately. */ + void sendMessageNow (const ump::Packets& packets); + /** Sends out a sequence of MIDI messages immediately. */ void sendBlockOfMessagesNow (const MidiBuffer& buffer); @@ -461,7 +487,7 @@ class YUP_API MidiOutput final : private Thread }; //============================================================================== - explicit MidiOutput (const String&, const String&); + explicit MidiOutput (const String&, const String&, ump::PacketProtocol = ump::PacketProtocol::MIDI_1_0); void run() override; MidiDeviceInfo deviceInfo; diff --git a/modules/yup_audio_devices/native/yup_Bela_linux.cpp b/modules/yup_audio_devices/native/yup_Bela_linux.cpp index 3f197f709..65ce5a84c 100644 --- a/modules/yup_audio_devices/native/yup_Bela_linux.cpp +++ b/modules/yup_audio_devices/native/yup_Bela_linux.cpp @@ -582,8 +582,10 @@ struct BelaAudioIODeviceType final : public AudioIODeviceType }; //============================================================================== -MidiInput::MidiInput (const String& deviceName, const String& deviceID) - : deviceInfo (deviceName, deviceID) +MidiInput::MidiInput (const String& deviceName, + const String& deviceID, + ump::PacketProtocol protocol) + : deviceInfo (deviceName, deviceID, protocol) { } @@ -608,12 +610,22 @@ std::unique_ptr MidiInput::openDevice (const String& deviceIdentifier if (deviceIdentifier.isEmpty()) return {}; - std::unique_ptr midiInput (new MidiInput (deviceIdentifier, deviceIdentifier)); + std::unique_ptr midiInput (new MidiInput (deviceIdentifier, + deviceIdentifier, + ump::PacketProtocol::MIDI_1_0)); midiInput->internal = std::make_unique (deviceIdentifier, midiInput.get(), callback); return midiInput; } +std::unique_ptr MidiInput::openDevice (const String&, + ump::PacketProtocol, + ump::Receiver*) +{ + jassertfalse; + return {}; +} + std::unique_ptr MidiInput::createNewDevice (const String&, MidiInputCallback*) { // N/A on Bela @@ -631,12 +643,18 @@ MidiOutput::~MidiOutput() = default; void MidiOutput::sendMessageNow (const MidiMessage&) {} +void MidiOutput::sendMessageNow (const ump::View&) {} + +void MidiOutput::sendMessageNow (const ump::Packets&) {} + Array MidiOutput::getAvailableDevices() { return {}; } MidiDeviceInfo MidiOutput::getDefaultDevice() { return {}; } std::unique_ptr MidiOutput::openDevice (const String&) { return {}; } +std::unique_ptr MidiOutput::openDevice (const String&, ump::PacketProtocol) { return {}; } + std::unique_ptr MidiOutput::createNewDevice (const String&) { return {}; } } // namespace yup diff --git a/modules/yup_audio_devices/native/yup_CoreMidi_apple.mm b/modules/yup_audio_devices/native/yup_CoreMidi_apple.mm index e7e986853..cc1849096 100644 --- a/modules/yup_audio_devices/native/yup_CoreMidi_apple.mm +++ b/modules/yup_audio_devices/native/yup_CoreMidi_apple.mm @@ -84,62 +84,67 @@ static bool checkError(OSStatus err, [[maybe_unused]] int lineNum) constexpr auto implementationStrategy = ImplementationStrategy::onlyOld; #endif -struct SenderBase -{ - virtual ~SenderBase() noexcept = default; - - virtual void send(MIDIPortRef port, MIDIEndpointRef endpoint, const ump::BytestreamMidiView& m) = 0; - virtual void send(MIDIPortRef port, MIDIEndpointRef endpoint, ump::Iterator b, ump::Iterator e) = 0; -}; +struct SenderBase +{ + virtual ~SenderBase() noexcept = default; + + virtual void send(MIDIPortRef port, + MIDIEndpointRef endpoint, + ump::PacketProtocol protocol, + const ump::BytestreamMidiView& m) = 0; + virtual void send(MIDIPortRef port, + MIDIEndpointRef endpoint, + ump::PacketProtocol protocol, + ump::Iterator b, + ump::Iterator e) = 0; +}; template struct Sender; #if YUP_HAS_NEW_COREMIDI_API template <> -struct API_AVAILABLE(macos(11.0), ios(14.0)) Sender final : public SenderBase -{ - void send(MIDIPortRef port, MIDIEndpointRef endpoint, const ump::BytestreamMidiView& m) override - { - newSendImpl(port, endpoint, m); - } - - void send(MIDIPortRef port, MIDIEndpointRef endpoint, ump::Iterator b, ump::Iterator e) override - { - newSendImpl(port, endpoint, b, e); - } - - private: - ump::ToUMP1Converter 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) - { -#if YUP_IOS - const MIDITimeStamp timeStamp = mach_absolute_time(); -#else - const MIDITimeStamp timeStamp = AudioGetCurrentHostTime(); -#endif - - MIDIEventList stackList = {}; - MIDIEventPacket* end = nullptr; - - const auto init = [&] - { - // At the moment, we can only send MIDI 1.0 protocol. If the device is using MIDI - // 2.0 protocol (as may be the case for the IAC driver), we trust in the system to - // translate it. - end = MIDIEventListInit(&stackList, kMIDIProtocol_1_0); - }; +struct API_AVAILABLE(macos(11.0), ios(14.0)) Sender final : public SenderBase +{ + void send(MIDIPortRef port, + MIDIEndpointRef endpoint, + ump::PacketProtocol protocol, + const ump::BytestreamMidiView& m) override + { + newSendImpl(port, endpoint, protocol, m); + } + + void send(MIDIPortRef port, + MIDIEndpointRef endpoint, + ump::PacketProtocol protocol, + ump::Iterator b, + ump::Iterator e) override + { + newSendImpl(port, endpoint, protocol, b, e); + } + + private: + void newSendImpl(MIDIPortRef port, + MIDIEndpointRef endpoint, + ump::PacketProtocol protocol, + const ump::BytestreamMidiView& message) + { +#if YUP_IOS + const MIDITimeStamp timeStamp = mach_absolute_time(); +#else + const MIDITimeStamp timeStamp = AudioGetCurrentHostTime(); +#endif + + MIDIEventList stackList = {}; + MIDIEventPacket* end = nullptr; + + const auto init = [&] + { + const auto protocolId = protocol == ump::PacketProtocol::MIDI_2_0 + ? kMIDIProtocol_2_0 + : kMIDIProtocol_1_0; + end = MIDIEventListInit(&stackList, protocolId); + }; const auto send = [&] { @@ -159,40 +164,107 @@ void newSendImpl(MIDIPortRef port, MIDIEndpointRef endpoint, Params&&... params) reinterpret_cast(view.data())); }; - init(); - - ump::GenericUMPConverter::convertImpl(umpConverter, params..., [&](const auto& v) - { umpConverter.convert(v, [&](const ump::View& view) - { - add (view); - - if (end != nullptr) - return; - - send(); - init(); - add (view); }); }); - - send(); - } -}; -#endif + init(); + + ump::GenericUMPConverter converter { protocol }; + converter.convert (message, [&] (const ump::View& view) + { + add (view); + + if (end != nullptr) + return; + + send(); + init(); + add (view); + }); + + send(); + } + + void newSendImpl(MIDIPortRef port, + MIDIEndpointRef endpoint, + ump::PacketProtocol protocol, + ump::Iterator b, + ump::Iterator e) + { +#if YUP_IOS + const MIDITimeStamp timeStamp = mach_absolute_time(); +#else + const MIDITimeStamp timeStamp = AudioGetCurrentHostTime(); +#endif + + MIDIEventList stackList = {}; + MIDIEventPacket* end = nullptr; + + const auto init = [&] + { + const auto protocolId = protocol == ump::PacketProtocol::MIDI_2_0 + ? kMIDIProtocol_2_0 + : kMIDIProtocol_1_0; + end = MIDIEventListInit(&stackList, protocolId); + }; + + 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(); + + ump::GenericUMPConverter converter { protocol }; + converter.convert (b, e, [&] (const ump::View& view) + { + add (view); + + if (end != nullptr) + return; + + send(); + init(); + add (view); + }); + + send(); + } +}; +#endif #if YUP_HAS_OLD_COREMIDI_API template <> -struct Sender final : public SenderBase -{ - void send(MIDIPortRef port, MIDIEndpointRef endpoint, const ump::BytestreamMidiView& 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 ump::BytestreamMidiView& m) - { send(port, endpoint, m); }); }); - } +struct Sender final : public SenderBase +{ + void send(MIDIPortRef port, + MIDIEndpointRef endpoint, + [[maybe_unused]] ump::PacketProtocol protocol, + const ump::BytestreamMidiView& m) override + { + oldSendImpl(port, endpoint, m); + } + + void send(MIDIPortRef port, + MIDIEndpointRef endpoint, + [[maybe_unused]] ump::PacketProtocol protocol, + ump::Iterator b, + ump::Iterator e) override + { + std::for_each(b, e, [&](const ump::View& v) + { bytestreamConverter.convert(v, 0.0, [&](const ump::BytestreamMidiView& m) + { send(port, endpoint, protocol, m); }); }); + } private: ump::ToBytestreamConverter bytestreamConverter{2048}; @@ -263,22 +335,29 @@ void oldSendImpl(MIDIPortRef port, MIDIEndpointRef endpoint, const ump::Bytestre #if YUP_HAS_NEW_COREMIDI_API && YUP_HAS_OLD_COREMIDI_API template <> -struct Sender -{ - Sender() - : sender(makeImpl()) - { - } - - void send(MIDIPortRef port, MIDIEndpointRef endpoint, const ump::BytestreamMidiView& m) +struct Sender +{ + Sender() + : sender(makeImpl()) { - sender->send(port, endpoint, m); } - void send(MIDIPortRef port, MIDIEndpointRef endpoint, ump::Iterator b, ump::Iterator e) - { - sender->send(port, endpoint, b, e); - } + void send(MIDIPortRef port, + MIDIEndpointRef endpoint, + ump::PacketProtocol protocol, + const ump::BytestreamMidiView& m) + { + sender->send(port, endpoint, protocol, m); + } + + void send(MIDIPortRef port, + MIDIEndpointRef endpoint, + ump::PacketProtocol protocol, + ump::Iterator b, + ump::Iterator e) + { + sender->send(port, endpoint, protocol, b, e); + } private: static std::unique_ptr makeImpl() @@ -353,13 +432,14 @@ Resource release() noexcept using ScopedEndpointRef = ScopedMidiResource; //============================================================================== -class MidiPortAndEndpoint -{ - public: - MidiPortAndEndpoint(ScopedPortRef p, ScopedEndpointRef ep) noexcept - : port(std::move(p)), endpoint(std::move(ep)) - { - } +class MidiPortAndEndpoint +{ + public: + MidiPortAndEndpoint(ScopedPortRef p, ScopedEndpointRef ep, ump::PacketProtocol protocolIn) noexcept + : port(std::move(p)), endpoint(std::move(ep)) + , protocol(protocolIn) + { + } ~MidiPortAndEndpoint() noexcept { @@ -368,29 +448,53 @@ Resource release() noexcept endpoint.release(); } - void send(const ump::BytestreamMidiView& m) - { - sender.send(*port, *endpoint, m); - } - - void send(ump::Iterator b, ump::Iterator e) - { - sender.send(*port, *endpoint, b, e); - } + void send(const ump::BytestreamMidiView& m) + { + sender.send(*port, *endpoint, protocol, m); + } + + void send(ump::Iterator b, ump::Iterator e) + { + sender.send(*port, *endpoint, protocol, b, e); + } bool canStop() const noexcept { return *port != 0; } void stop() const { CHECK_ERROR(MIDIPortDisconnectSource(*port, *endpoint)); } private: ScopedPortRef port; - ScopedEndpointRef endpoint; - - SenderToUse sender; -}; - -static MidiDeviceInfo getMidiObjectInfo(MIDIObjectRef entity) -{ - MidiDeviceInfo info; + ScopedEndpointRef endpoint; + ump::PacketProtocol protocol; + + SenderToUse sender; +}; + +static void updateProtocolInfo(MIDIObjectRef entity, MidiDeviceInfo& info) +{ +#if YUP_HAS_NEW_COREMIDI_API + SInt32 protocol = 0; + + if (CHECK_ERROR(MIDIObjectGetIntegerProperty(entity, kMIDIPropertyProtocolID, &protocol))) + { + if (protocol == kMIDIProtocol_2_0) + { + info.protocol = ump::PacketProtocol::MIDI_2_0; + info.supportsMidi2 = true; + } + else + { + info.protocol = ump::PacketProtocol::MIDI_1_0; + info.supportsMidi2 = false; + } + } +#else + ignoreUnused(entity, info); +#endif +} + +static MidiDeviceInfo getMidiObjectInfo(MIDIObjectRef entity) +{ + MidiDeviceInfo info; { CFObjectHolder str; @@ -413,8 +517,9 @@ static MidiDeviceInfo getMidiObjectInfo(MIDIObjectRef entity) info.identifier = String::fromCFString(str.object); } - return info; -} + updateProtocolInfo(entity, info); + return info; +} static MidiDeviceInfo getEndpointInfo(MIDIEndpointRef endpoint, bool isExternal) { @@ -988,33 +1093,54 @@ static CreatorFunctionPointers getCreatorFunctionPointers() 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; + static std::unique_ptr makeInput(const String& name, + const String& identifier, + ump::PacketProtocol protocol, + ump::Receiver& receiver) + { + using namespace CoreMidiHelpers; + + if (auto midiInput = rawToUniquePtr(new MidiInput(name, identifier, protocol))) + { + if ((midiInput->internal = makePimpl(*midiInput, protocol, receiver))) + { + const ScopedLock sl(callbackLock); + activeCallbacks.add(midiInput->internal.get()); + + return midiInput; + } + } + + return {}; + } + + static std::unique_ptr makeInput(const String& name, + const String& identifier, + ump::PacketProtocol protocol, + MidiInputCallback* midiInputCallback) + { + using namespace CoreMidiHelpers; + + if (auto midiInput = rawToUniquePtr(new MidiInput(name, identifier, protocol))) + { + if ((midiInput->internal = makePimpl(*midiInput, midiInputCallback))) + { + 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 {}; @@ -1033,9 +1159,12 @@ static CreatorFunctionPointers getCreatorFunctionPointers() if (!CHECK_ERROR(MIDIObjectGetStringProperty(endpoint, kMIDIPropertyName, &cfName.object))) continue; - if (auto input = makeInput(endpointInfo.name, endpointInfo.identifier, std::forward(args)...)) - { - MIDIPortRef port; + if (auto input = makeInput(endpointInfo.name, + endpointInfo.identifier, + protocol, + std::forward(args)...)) + { + MIDIPortRef port; if (!CHECK_ERROR(CreatorFunctionsToUse::createInputPort(protocol, client, cfName.object, input->internal.get(), &port))) continue; @@ -1045,29 +1174,34 @@ static CreatorFunctionPointers getCreatorFunctionPointers() if (!CHECK_ERROR(MIDIPortConnectSource(*scopedPort, endpoint, nullptr))) continue; - input->internal->portAndEndpoint = std::make_unique(std::move(scopedPort), ScopedEndpointRef{endpoint}); - return input; - } - } + input->internal->portAndEndpoint = std::make_unique(std::move(scopedPort), + ScopedEndpointRef{endpoint}, + protocol); + return input; + } + } } return {}; } template - static std::unique_ptr createDevice(ump::PacketProtocol protocol, - const String& deviceName, - Args&&... args) - { - using namespace CoreMidiHelpers; + 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; + if (auto input = makeInput(deviceName, + String(deviceIdentifier), + protocol, + std::forward(args)...)) + { + MIDIEndpointRef endpoint; CFUniquePtr name(deviceName.toCFString()); auto err = CreatorFunctionsToUse::createDestination(protocol, client, name.get(), input->internal.get(), &endpoint); @@ -1089,10 +1223,12 @@ static CreatorFunctionPointers getCreatorFunctionPointers() if (!CHECK_ERROR(MIDIObjectSetIntegerProperty(endpoint, kMIDIPropertyUniqueID, (SInt32)deviceIdentifier))) return {}; - input->internal->portAndEndpoint = std::make_unique(ScopedPortRef{}, std::move(scopedEndpoint)); - return input; - } - } + input->internal->portAndEndpoint = std::make_unique(ScopedPortRef{}, + std::move(scopedEndpoint), + protocol); + return input; + } + } return {}; } @@ -1109,27 +1245,39 @@ static CreatorFunctionPointers getCreatorFunctionPointers() 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); -} - -MidiInput::MidiInput(const String& deviceName, const String& deviceIdentifier) - : deviceInfo(deviceName, deviceIdentifier) -{ -} +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::openDevice(const String& deviceIdentifier, + ump::PacketProtocol protocol, + ump::Receiver* receiver) +{ + if (receiver == nullptr) + return nullptr; + + return Pimpl::openDevice(protocol, deviceIdentifier, *receiver); +} + +std::unique_ptr MidiInput::createNewDevice(const String& deviceName, MidiInputCallback* callback) +{ + return Pimpl::createDevice(ump::PacketProtocol::MIDI_1_0, + deviceName, + callback); +} + +MidiInput::MidiInput(const String& deviceName, + const String& deviceIdentifier, + ump::PacketProtocol protocol) + : deviceInfo(deviceName, deviceIdentifier, protocol) +{ +} MidiInput::~MidiInput() = default; @@ -1162,43 +1310,51 @@ static CreatorFunctionPointers getCreatorFunctionPointers() 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; - - CFObjectHolder cfName; - - if (!CHECK_ERROR(MIDIObjectGetStringProperty(endpoint, kMIDIPropertyName, &cfName.object))) - continue; - - MIDIPortRef port; - - if (!CHECK_ERROR(MIDIOutputPortCreate(client, cfName.object, &port))) - continue; - - ScopedPortRef scopedPort{port}; - - auto midiOutput = rawToUniquePtr(new MidiOutput(endpointInfo.name, endpointInfo.identifier)); - midiOutput->internal = std::make_unique(std::move(scopedPort), ScopedEndpointRef{endpoint}); - - return midiOutput; - } - } - - return {}; -} +std::unique_ptr MidiOutput::openDevice(const String& deviceIdentifier) +{ + return openDevice(deviceIdentifier, ump::PacketProtocol::MIDI_1_0); +} + +std::unique_ptr MidiOutput::openDevice(const String& deviceIdentifier, + ump::PacketProtocol protocol) +{ + 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; + + CFObjectHolder cfName; + + if (!CHECK_ERROR(MIDIObjectGetStringProperty(endpoint, kMIDIPropertyName, &cfName.object))) + continue; + + MIDIPortRef port; + + if (!CHECK_ERROR(MIDIOutputPortCreate(client, cfName.object, &port))) + continue; + + ScopedPortRef scopedPort{port}; + + auto midiOutput = rawToUniquePtr(new MidiOutput(endpointInfo.name, endpointInfo.identifier, protocol)); + midiOutput->internal = std::make_unique(std::move(scopedPort), + ScopedEndpointRef{endpoint}, + protocol); + + return midiOutput; + } + } + + return {}; +} std::unique_ptr MidiOutput::createNewDevice(const String& deviceName) { @@ -1231,11 +1387,13 @@ static CreatorFunctionPointers getCreatorFunctionPointers() if (!CHECK_ERROR(MIDIObjectSetIntegerProperty(*scopedEndpoint, kMIDIPropertyUniqueID, (SInt32)deviceIdentifier))) return {}; - auto midiOutput = rawToUniquePtr(new MidiOutput(deviceName, String(deviceIdentifier))); - midiOutput->internal = std::make_unique(ScopedPortRef{}, std::move(scopedEndpoint)); - - return midiOutput; - } + auto midiOutput = rawToUniquePtr(new MidiOutput(deviceName, String(deviceIdentifier), ump::PacketProtocol::MIDI_1_0)); + midiOutput->internal = std::make_unique(ScopedPortRef{}, + std::move(scopedEndpoint), + ump::PacketProtocol::MIDI_1_0); + + return midiOutput; + } return {}; } @@ -1245,10 +1403,22 @@ static CreatorFunctionPointers getCreatorFunctionPointers() stopBackgroundThread(); } -void MidiOutput::sendMessageNow(const MidiMessage& message) -{ - internal->send(ump::BytestreamMidiView(&message)); -} +void MidiOutput::sendMessageNow(const MidiMessage& message) +{ + internal->send(ump::BytestreamMidiView(&message)); +} + +void MidiOutput::sendMessageNow(const ump::View& message) +{ + auto begin = ump::Iterator (message.data(), message.size() * sizeof (uint32_t)); + auto end = ump::Iterator (message.data() + message.size(), 0); + internal->send (begin, end); +} + +void MidiOutput::sendMessageNow (const ump::Packets& packets) +{ + internal->send (packets.cbegin(), packets.cend()); +} MidiDeviceListConnection MidiDeviceListConnection::make(std::function cb) { diff --git a/modules/yup_audio_devices/native/yup_Midi_android.cpp b/modules/yup_audio_devices/native/yup_Midi_android.cpp index fd0c61761..5d318ce14 100644 --- a/modules/yup_audio_devices/native/yup_Midi_android.cpp +++ b/modules/yup_audio_devices/native/yup_Midi_android.cpp @@ -289,7 +289,7 @@ std::unique_ptr MidiInput::openDevice (const String& deviceIdentifier AndroidMidiDeviceManager manager; - std::unique_ptr midiInput (new MidiInput ({}, deviceIdentifier)); + std::unique_ptr midiInput (new MidiInput ({}, deviceIdentifier, ump::PacketProtocol::MIDI_1_0)); if (auto* port = manager.openMidiInputPortWithID (deviceIdentifier.getIntValue(), midiInput.get(), callback)) { @@ -302,8 +302,18 @@ std::unique_ptr MidiInput::openDevice (const String& deviceIdentifier return {}; } -MidiInput::MidiInput (const String& deviceName, const String& deviceIdentifier) - : deviceInfo (deviceName, deviceIdentifier) +std::unique_ptr MidiInput::openDevice (const String&, + ump::PacketProtocol, + ump::Receiver*) +{ + jassertfalse; + return {}; +} + +MidiInput::MidiInput (const String& deviceName, + const String& deviceIdentifier, + ump::PacketProtocol protocol) + : deviceInfo (deviceName, deviceIdentifier, protocol) { } @@ -348,7 +358,7 @@ std::unique_ptr MidiOutput::openDevice (const String& deviceIdentifi if (auto* port = manager.openMidiOutputPortWithID (deviceIdentifier.getIntValue())) { - std::unique_ptr midiOutput (new MidiOutput ({}, deviceIdentifier)); + std::unique_ptr midiOutput (new MidiOutput ({}, deviceIdentifier, ump::PacketProtocol::MIDI_1_0)); midiOutput->internal.reset (port); midiOutput->setName (port->getName()); @@ -358,6 +368,15 @@ std::unique_ptr MidiOutput::openDevice (const String& deviceIdentifi return {}; } +std::unique_ptr MidiOutput::openDevice (const String& deviceIdentifier, + ump::PacketProtocol protocol) +{ + if (protocol != ump::PacketProtocol::MIDI_1_0) + return {}; + + return openDevice (deviceIdentifier); +} + MidiOutput::~MidiOutput() { stopBackgroundThread(); @@ -381,6 +400,21 @@ void MidiOutput::sendMessageNow (const MidiMessage& message) } } +void MidiOutput::sendMessageNow (const ump::View& message) +{ + ump::ToBytestreamConverter converter { 2048 }; + converter.convert (message, 0.0, [&] (const MidiMessage& midiMessage) + { + sendMessageNow (midiMessage); + }); +} + +void MidiOutput::sendMessageNow (const ump::Packets& packets) +{ + for (auto it = packets.cbegin(); it != packets.cend(); ++it) + sendMessageNow (*it); +} + MidiDeviceListConnection MidiDeviceListConnection::make (std::function callback) { auto& broadcaster = MidiDeviceListConnectionBroadcaster::get(); diff --git a/modules/yup_audio_devices/native/yup_Midi_linux.cpp b/modules/yup_audio_devices/native/yup_Midi_linux.cpp index f7185ea6e..15c871945 100644 --- a/modules/yup_audio_devices/native/yup_Midi_linux.cpp +++ b/modules/yup_audio_devices/native/yup_Midi_linux.cpp @@ -593,7 +593,9 @@ std::unique_ptr MidiInput::openDevice (const String& deviceIdentifier jassert (port->isValid()); - std::unique_ptr midiInput (new MidiInput (port->getPortName(), deviceIdentifier)); + std::unique_ptr midiInput (new MidiInput (port->getPortName(), + deviceIdentifier, + ump::PacketProtocol::MIDI_1_0)); port->setupInput (midiInput.get(), callback); midiInput->internal = std::make_unique (port); @@ -601,6 +603,14 @@ std::unique_ptr MidiInput::openDevice (const String& deviceIdentifier return midiInput; } +std::unique_ptr MidiInput::openDevice (const String&, + ump::PacketProtocol, + ump::Receiver*) +{ + jassertfalse; + return {}; +} + std::unique_ptr MidiInput::createNewDevice (const String& deviceName, MidiInputCallback* callback) { auto client = AlsaClient::getInstance(); @@ -609,7 +619,9 @@ std::unique_ptr MidiInput::createNewDevice (const String& deviceName, if (port == nullptr || ! port->isValid()) return {}; - std::unique_ptr midiInput (new MidiInput (deviceName, getFormattedPortIdentifier (client->getId(), port->getPortId()))); + std::unique_ptr midiInput (new MidiInput (deviceName, + getFormattedPortIdentifier (client->getId(), port->getPortId()), + ump::PacketProtocol::MIDI_1_0)); port->setupInput (midiInput.get(), callback); midiInput->internal = std::make_unique (port); @@ -617,8 +629,10 @@ std::unique_ptr MidiInput::createNewDevice (const String& deviceName, return midiInput; } -MidiInput::MidiInput (const String& deviceName, const String& deviceIdentifier) - : deviceInfo (deviceName, deviceIdentifier) +MidiInput::MidiInput (const String& deviceName, + const String& deviceIdentifier, + ump::PacketProtocol protocol) + : deviceInfo (deviceName, deviceIdentifier, protocol) { } @@ -668,7 +682,9 @@ std::unique_ptr MidiOutput::openDevice (const String& deviceIdentifi if (port == nullptr || ! port->isValid()) return {}; - std::unique_ptr midiOutput (new MidiOutput (port->getPortName(), deviceIdentifier)); + std::unique_ptr midiOutput (new MidiOutput (port->getPortName(), + deviceIdentifier, + ump::PacketProtocol::MIDI_1_0)); port->setupOutput(); midiOutput->internal = std::make_unique (port); @@ -676,6 +692,15 @@ std::unique_ptr MidiOutput::openDevice (const String& deviceIdentifi return midiOutput; } +std::unique_ptr MidiOutput::openDevice (const String& deviceIdentifier, + ump::PacketProtocol protocol) +{ + if (protocol != ump::PacketProtocol::MIDI_1_0) + return {}; + + return openDevice (deviceIdentifier); +} + std::unique_ptr MidiOutput::createNewDevice (const String& deviceName) { auto client = AlsaClient::getInstance(); @@ -684,7 +709,9 @@ std::unique_ptr MidiOutput::createNewDevice (const String& deviceNam if (port == nullptr || ! port->isValid()) return {}; - std::unique_ptr midiOutput (new MidiOutput (deviceName, getFormattedPortIdentifier (client->getId(), port->getPortId()))); + std::unique_ptr midiOutput (new MidiOutput (deviceName, + getFormattedPortIdentifier (client->getId(), port->getPortId()), + ump::PacketProtocol::MIDI_1_0)); port->setupOutput(); midiOutput->internal = std::make_unique (port); @@ -702,6 +729,21 @@ void MidiOutput::sendMessageNow (const MidiMessage& message) internal->ptr->sendMessageNow (message); } +void MidiOutput::sendMessageNow (const ump::View& message) +{ + ump::ToBytestreamConverter converter { 2048 }; + converter.convert (message, 0.0, [&] (const MidiMessage& midiMessage) + { + sendMessageNow (midiMessage); + }); +} + +void MidiOutput::sendMessageNow (const ump::Packets& packets) +{ + for (auto it = packets.cbegin(); it != packets.cend(); ++it) + sendMessageNow (*it); +} + MidiDeviceListConnection MidiDeviceListConnection::make (std::function cb) { auto& broadcaster = MidiDeviceListConnectionBroadcaster::get(); @@ -722,8 +764,10 @@ class MidiInput::Pimpl }; // (These are just stub functions if ALSA is unavailable...) -MidiInput::MidiInput (const String& deviceName, const String& deviceID) - : deviceInfo (deviceName, deviceID) +MidiInput::MidiInput (const String& deviceName, + const String& deviceID, + ump::PacketProtocol protocol) + : deviceInfo (deviceName, deviceID, protocol) { } @@ -739,6 +783,13 @@ MidiDeviceInfo MidiInput::getDefaultDevice() { return {}; } std::unique_ptr MidiInput::openDevice (const String&, MidiInputCallback*) { return {}; } +std::unique_ptr MidiInput::openDevice (const String&, + ump::PacketProtocol, + ump::Receiver*) +{ + return {}; +} + std::unique_ptr MidiInput::createNewDevice (const String&, MidiInputCallback*) { return {}; } class MidiOutput::Pimpl @@ -749,12 +800,18 @@ MidiOutput::~MidiOutput() {} void MidiOutput::sendMessageNow (const MidiMessage&) {} +void MidiOutput::sendMessageNow (const ump::View&) {} + +void MidiOutput::sendMessageNow (const ump::Packets&) {} + Array MidiOutput::getAvailableDevices() { return {}; } MidiDeviceInfo MidiOutput::getDefaultDevice() { return {}; } std::unique_ptr MidiOutput::openDevice (const String&) { return {}; } +std::unique_ptr MidiOutput::openDevice (const String&, ump::PacketProtocol) { return {}; } + std::unique_ptr MidiOutput::createNewDevice (const String&) { return {}; } MidiDeviceListConnection MidiDeviceListConnection::make (std::function cb) diff --git a/modules/yup_audio_devices/native/yup_Midi_wasm.cpp b/modules/yup_audio_devices/native/yup_Midi_wasm.cpp index 244c6133e..fc061b554 100644 --- a/modules/yup_audio_devices/native/yup_Midi_wasm.cpp +++ b/modules/yup_audio_devices/native/yup_Midi_wasm.cpp @@ -26,8 +26,10 @@ class MidiInput::Pimpl { }; -MidiInput::MidiInput (const String& deviceName, const String& deviceID) - : deviceInfo (deviceName, deviceID) +MidiInput::MidiInput (const String& deviceName, + const String& deviceID, + ump::PacketProtocol protocol) + : deviceInfo (deviceName, deviceID, protocol) { } @@ -43,6 +45,13 @@ MidiDeviceInfo MidiInput::getDefaultDevice() { return {}; } std::unique_ptr MidiInput::openDevice (const String&, MidiInputCallback*) { return {}; } +std::unique_ptr MidiInput::openDevice (const String&, + ump::PacketProtocol, + ump::Receiver*) +{ + return {}; +} + std::unique_ptr MidiInput::createNewDevice (const String&, MidiInputCallback*) { return {}; } class MidiOutput::Pimpl @@ -53,12 +62,18 @@ MidiOutput::~MidiOutput() {} void MidiOutput::sendMessageNow (const MidiMessage&) {} +void MidiOutput::sendMessageNow (const ump::View&) {} + +void MidiOutput::sendMessageNow (const ump::Packets&) {} + Array MidiOutput::getAvailableDevices() { return {}; } MidiDeviceInfo MidiOutput::getDefaultDevice() { return {}; } std::unique_ptr MidiOutput::openDevice (const String&) { return {}; } +std::unique_ptr MidiOutput::openDevice (const String&, ump::PacketProtocol) { return {}; } + std::unique_ptr MidiOutput::createNewDevice (const String&) { return {}; } MidiDeviceListConnection MidiDeviceListConnection::make (std::function cb) diff --git a/modules/yup_audio_devices/native/yup_Midi_windows.cpp b/modules/yup_audio_devices/native/yup_Midi_windows.cpp index c7b3e9f86..ede73e85e 100644 --- a/modules/yup_audio_devices/native/yup_Midi_windows.cpp +++ b/modules/yup_audio_devices/native/yup_Midi_windows.cpp @@ -1961,7 +1961,7 @@ std::unique_ptr MidiInput::openDevice (const String& deviceIdentifier if (deviceIdentifier.isEmpty() || callback == nullptr) return {}; - std::unique_ptr in (new MidiInput ({}, deviceIdentifier)); + std::unique_ptr in (new MidiInput ({}, deviceIdentifier, ump::PacketProtocol::MIDI_1_0)); std::unique_ptr wrapper; try @@ -1979,8 +1979,18 @@ std::unique_ptr MidiInput::openDevice (const String& deviceIdentifier return in; } -MidiInput::MidiInput (const String& deviceName, const String& deviceIdentifier) - : deviceInfo (deviceName, deviceIdentifier) +std::unique_ptr MidiInput::openDevice (const String&, + ump::PacketProtocol, + ump::Receiver*) +{ + jassertfalse; + return {}; +} + +MidiInput::MidiInput (const String& deviceName, + const String& deviceIdentifier, + ump::PacketProtocol protocol) + : deviceInfo (deviceName, deviceIdentifier, protocol) { } @@ -2018,13 +2028,22 @@ std::unique_ptr MidiOutput::openDevice (const String& deviceIdentifi } std::unique_ptr out; - out.reset (new MidiOutput (wrapper->getDeviceName(), deviceIdentifier)); + out.reset (new MidiOutput (wrapper->getDeviceName(), deviceIdentifier, ump::PacketProtocol::MIDI_1_0)); out->internal = std::move (wrapper); return out; } +std::unique_ptr MidiOutput::openDevice (const String& deviceIdentifier, + ump::PacketProtocol protocol) +{ + if (protocol != ump::PacketProtocol::MIDI_1_0) + return {}; + + return openDevice (deviceIdentifier); +} + MidiOutput::~MidiOutput() { stopBackgroundThread(); @@ -2035,6 +2054,21 @@ void MidiOutput::sendMessageNow (const MidiMessage& message) internal->sendMessageNow (message); } +void MidiOutput::sendMessageNow (const ump::View& message) +{ + ump::ToBytestreamConverter converter { 2048 }; + converter.convert (message, 0.0, [&] (const MidiMessage& midiMessage) + { + sendMessageNow (midiMessage); + }); +} + +void MidiOutput::sendMessageNow (const ump::Packets& packets) +{ + for (auto it = packets.cbegin(); it != packets.cend(); ++it) + sendMessageNow (*it); +} + MidiDeviceListConnection MidiDeviceListConnection::make (std::function cb) { auto& broadcaster = MidiDeviceListConnectionBroadcaster::get(); diff --git a/modules/yup_audio_devices/yup_audio_devices.cpp b/modules/yup_audio_devices/yup_audio_devices.cpp index e6d2cc294..73a83c92e 100644 --- a/modules/yup_audio_devices/yup_audio_devices.cpp +++ b/modules/yup_audio_devices/yup_audio_devices.cpp @@ -313,5 +313,6 @@ bool YUP_CALLTYPE SystemAudioVolume::setMuted (bool) #include "audio_io/yup_AudioIODevice.cpp" #include "audio_io/yup_AudioIODeviceType.cpp" #include "midi_io/yup_MidiMessageCollector.cpp" +#include "midi_io/ump/yup_UMPPacketCollector.cpp" #include "sources/yup_AudioSourcePlayer.cpp" #include "sources/yup_AudioTransportSource.cpp" diff --git a/modules/yup_audio_devices/yup_audio_devices.h b/modules/yup_audio_devices/yup_audio_devices.h index 98a632fc7..c4ae9e9b8 100644 --- a/modules/yup_audio_devices/yup_audio_devices.h +++ b/modules/yup_audio_devices/yup_audio_devices.h @@ -174,6 +174,7 @@ //============================================================================== #include "midi_io/yup_MidiDevices.h" #include "midi_io/yup_MidiMessageCollector.h" +#include "midi_io/ump/yup_UMPPacketCollector.h" namespace yup { diff --git a/tests/yup_audio_basics/yup_UMP.cpp b/tests/yup_audio_basics/yup_UMP.cpp index c5ae779a5..c2f3e64cc 100644 --- a/tests/yup_audio_basics/yup_UMP.cpp +++ b/tests/yup_audio_basics/yup_UMP.cpp @@ -498,7 +498,7 @@ TEST_F (UniversalMidiPacketTest, WideningConversionsWork) TEST (UMPUtilsTests, GetNumWordsForMessageType) { - using namespace yup::universal_midi_packets; + using namespace yup::ump; // Test 1-word message types EXPECT_EQ (1, Utils::getNumWordsForMessageType (0x00000000)); // Message type 0x0 @@ -527,7 +527,7 @@ TEST (UMPUtilsTests, GetNumWordsForMessageType) TEST (UMPUtilsTests, UtilityFunctionsGetMessageTypeGroupStatusChannel) { - using namespace yup::universal_midi_packets; + using namespace yup::ump; // Test a word with all nibbles set to different values uint32_t testWord = 0x12345678; @@ -540,7 +540,7 @@ TEST (UMPUtilsTests, UtilityFunctionsGetMessageTypeGroupStatusChannel) TEST (UMPUtilsTests, U4TemplateHelpers) { - using namespace yup::universal_midi_packets; + using namespace yup::ump; // Test setting and getting 4-bit values at different positions uint32_t word = 0x00000000; @@ -568,7 +568,7 @@ TEST (UMPUtilsTests, U4TemplateHelpers) TEST (UMPUtilsTests, U8TemplateHelpers) { - using namespace yup::universal_midi_packets; + using namespace yup::ump; uint32_t word = 0x00000000; @@ -590,7 +590,7 @@ TEST (UMPUtilsTests, U8TemplateHelpers) TEST (UMPUtilsTests, U16TemplateHelpers) { - using namespace yup::universal_midi_packets; + using namespace yup::ump; uint32_t word = 0x00000000; @@ -612,7 +612,7 @@ TEST (UMPUtilsTests, U16TemplateHelpers) TEST (UMPUtilsTests, BytesToWordFunction) { - using namespace yup::universal_midi_packets; + using namespace yup::ump; auto result = Utils::bytesToWord (std::byte { 0x12 }, std::byte { 0x34 }, std::byte { 0x56 }, std::byte { 0x78 }); EXPECT_EQ (0x12345678, result); From aa7005b6d1f02f458889d5662c7349d4bf57d009 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 21 Dec 2025 22:57:39 +0100 Subject: [PATCH 02/20] More work on midi 2 --- .../midi_io/yup_MidiDevices.h | 9 ++++ .../native/yup_Bela_linux.cpp | 10 ++++ .../native/yup_CoreMidi_apple.mm | 48 +++++++++++++------ .../native/yup_Midi_linux.cpp | 26 ++++++++++ .../native/yup_Midi_wasm.cpp | 9 ++++ 5 files changed, 87 insertions(+), 15 deletions(-) diff --git a/modules/yup_audio_devices/midi_io/yup_MidiDevices.h b/modules/yup_audio_devices/midi_io/yup_MidiDevices.h index 4958797af..81e151a72 100644 --- a/modules/yup_audio_devices/midi_io/yup_MidiDevices.h +++ b/modules/yup_audio_devices/midi_io/yup_MidiDevices.h @@ -246,6 +246,11 @@ class YUP_API MidiInput final @param callback the object that will receive the midi messages from this device */ static std::unique_ptr createNewDevice (const String& deviceName, MidiInputCallback* callback); + + /** This will try to create a new midi input device with the requested protocol. */ + static std::unique_ptr createNewDevice (const String& deviceName, + ump::PacketProtocol protocol, + ump::Receiver* receiver); #endif //============================================================================== @@ -399,6 +404,10 @@ class YUP_API MidiOutput final : private Thread @param deviceName the name of the device to create */ static std::unique_ptr createNewDevice (const String& deviceName); + + /** This will try to create a new midi output device with the requested protocol. */ + static std::unique_ptr createNewDevice (const String& deviceName, + ump::PacketProtocol protocol); #endif //============================================================================== diff --git a/modules/yup_audio_devices/native/yup_Bela_linux.cpp b/modules/yup_audio_devices/native/yup_Bela_linux.cpp index 65ce5a84c..7201c3e3c 100644 --- a/modules/yup_audio_devices/native/yup_Bela_linux.cpp +++ b/modules/yup_audio_devices/native/yup_Bela_linux.cpp @@ -633,6 +633,14 @@ std::unique_ptr MidiInput::createNewDevice (const String&, MidiInputC return {}; } +std::unique_ptr MidiInput::createNewDevice (const String&, + ump::PacketProtocol, + ump::Receiver*) +{ + jassertfalse; + return {}; +} + //============================================================================== // TODO: Add Bela MidiOutput support class MidiOutput::Pimpl @@ -657,4 +665,6 @@ std::unique_ptr MidiOutput::openDevice (const String&, ump::PacketPr std::unique_ptr MidiOutput::createNewDevice (const String&) { return {}; } +std::unique_ptr MidiOutput::createNewDevice (const String&, ump::PacketProtocol) { return {}; } + } // namespace yup diff --git a/modules/yup_audio_devices/native/yup_CoreMidi_apple.mm b/modules/yup_audio_devices/native/yup_CoreMidi_apple.mm index cc1849096..04457d6f4 100644 --- a/modules/yup_audio_devices/native/yup_CoreMidi_apple.mm +++ b/modules/yup_audio_devices/native/yup_CoreMidi_apple.mm @@ -474,7 +474,7 @@ static void updateProtocolInfo(MIDIObjectRef entity, MidiDeviceInfo& info) #if YUP_HAS_NEW_COREMIDI_API SInt32 protocol = 0; - if (CHECK_ERROR(MIDIObjectGetIntegerProperty(entity, kMIDIPropertyProtocolID, &protocol))) + if (MIDIObjectGetIntegerProperty(entity, kMIDIPropertyProtocolID, &protocol) == noErr) { if (protocol == kMIDIProtocol_2_0) { @@ -1272,6 +1272,18 @@ static CreatorFunctionPointers getCreatorFunctionPointers() callback); } +std::unique_ptr MidiInput::createNewDevice(const String& deviceName, + ump::PacketProtocol protocol, + ump::Receiver* receiver) +{ + if (receiver == nullptr) + return {}; + + return Pimpl::createDevice(protocol, + deviceName, + *receiver); +} + MidiInput::MidiInput(const String& deviceName, const String& deviceIdentifier, ump::PacketProtocol protocol) @@ -1356,18 +1368,24 @@ static CreatorFunctionPointers getCreatorFunctionPointers() return {}; } -std::unique_ptr MidiOutput::createNewDevice(const String& deviceName) -{ - using namespace CoreMidiHelpers; - - if (auto client = getGlobalMidiClient()) - { - MIDIEndpointRef endpoint; - - CFUniquePtr name(deviceName.toCFString()); - - auto err = CreatorFunctionsToUse::createSource(ump::PacketProtocol::MIDI_1_0, client, name.get(), &endpoint); - ScopedEndpointRef scopedEndpoint{endpoint}; +std::unique_ptr MidiOutput::createNewDevice(const String& deviceName) +{ + return createNewDevice(deviceName, ump::PacketProtocol::MIDI_1_0); +} + +std::unique_ptr MidiOutput::createNewDevice(const String& deviceName, + ump::PacketProtocol protocol) +{ + using namespace CoreMidiHelpers; + + if (auto client = getGlobalMidiClient()) + { + MIDIEndpointRef endpoint; + + CFUniquePtr name(deviceName.toCFString()); + + auto err = CreatorFunctionsToUse::createSource(protocol, client, name.get(), &endpoint); + ScopedEndpointRef scopedEndpoint{endpoint}; #if YUP_IOS if (err == kMIDINotPermitted) @@ -1387,10 +1405,10 @@ static CreatorFunctionPointers getCreatorFunctionPointers() if (!CHECK_ERROR(MIDIObjectSetIntegerProperty(*scopedEndpoint, kMIDIPropertyUniqueID, (SInt32)deviceIdentifier))) return {}; - auto midiOutput = rawToUniquePtr(new MidiOutput(deviceName, String(deviceIdentifier), ump::PacketProtocol::MIDI_1_0)); + auto midiOutput = rawToUniquePtr(new MidiOutput(deviceName, String(deviceIdentifier), protocol)); midiOutput->internal = std::make_unique(ScopedPortRef{}, std::move(scopedEndpoint), - ump::PacketProtocol::MIDI_1_0); + protocol); return midiOutput; } diff --git a/modules/yup_audio_devices/native/yup_Midi_linux.cpp b/modules/yup_audio_devices/native/yup_Midi_linux.cpp index 15c871945..7621b28bb 100644 --- a/modules/yup_audio_devices/native/yup_Midi_linux.cpp +++ b/modules/yup_audio_devices/native/yup_Midi_linux.cpp @@ -629,6 +629,14 @@ std::unique_ptr MidiInput::createNewDevice (const String& deviceName, return midiInput; } +std::unique_ptr MidiInput::createNewDevice (const String&, + ump::PacketProtocol, + ump::Receiver*) +{ + jassertfalse; + return {}; +} + MidiInput::MidiInput (const String& deviceName, const String& deviceIdentifier, ump::PacketProtocol protocol) @@ -719,6 +727,15 @@ std::unique_ptr MidiOutput::createNewDevice (const String& deviceNam return midiOutput; } +std::unique_ptr MidiOutput::createNewDevice (const String& deviceName, + ump::PacketProtocol protocol) +{ + if (protocol != ump::PacketProtocol::MIDI_1_0) + return {}; + + return createNewDevice (deviceName); +} + MidiOutput::~MidiOutput() { stopBackgroundThread(); @@ -792,6 +809,13 @@ std::unique_ptr MidiInput::openDevice (const String&, std::unique_ptr MidiInput::createNewDevice (const String&, MidiInputCallback*) { return {}; } +std::unique_ptr MidiInput::createNewDevice (const String&, + ump::PacketProtocol, + ump::Receiver*) +{ + return {}; +} + class MidiOutput::Pimpl { }; @@ -814,6 +838,8 @@ std::unique_ptr MidiOutput::openDevice (const String&, ump::PacketPr std::unique_ptr MidiOutput::createNewDevice (const String&) { return {}; } +std::unique_ptr MidiOutput::createNewDevice (const String&, ump::PacketProtocol) { return {}; } + MidiDeviceListConnection MidiDeviceListConnection::make (std::function cb) { auto& broadcaster = MidiDeviceListConnectionBroadcaster::get(); diff --git a/modules/yup_audio_devices/native/yup_Midi_wasm.cpp b/modules/yup_audio_devices/native/yup_Midi_wasm.cpp index fc061b554..9ee001a35 100644 --- a/modules/yup_audio_devices/native/yup_Midi_wasm.cpp +++ b/modules/yup_audio_devices/native/yup_Midi_wasm.cpp @@ -54,6 +54,13 @@ std::unique_ptr MidiInput::openDevice (const String&, std::unique_ptr MidiInput::createNewDevice (const String&, MidiInputCallback*) { return {}; } +std::unique_ptr MidiInput::createNewDevice (const String&, + ump::PacketProtocol, + ump::Receiver*) +{ + return {}; +} + class MidiOutput::Pimpl { }; @@ -76,6 +83,8 @@ std::unique_ptr MidiOutput::openDevice (const String&, ump::PacketPr std::unique_ptr MidiOutput::createNewDevice (const String&) { return {}; } +std::unique_ptr MidiOutput::createNewDevice (const String&, ump::PacketProtocol) { return {}; } + MidiDeviceListConnection MidiDeviceListConnection::make (std::function cb) { // MIDI is not implemented for WASM, so we return a no-op connection From 6e0c6f96372c49ea87e6c2647f87d7f0a2fee7c4 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 22 Dec 2025 00:20:32 +0100 Subject: [PATCH 03/20] More midi 2.0 work --- modules/yup_audio_basics/midi/ump/yup_UMP.h | 13 +- .../midi/ump/yup_UMPCapabilityInquiry.h | 436 ++++++++++++ .../midi/ump/yup_UMPChannelVoice.h | 357 ++++++++++ .../midi/ump/yup_UMPDataMessages.h | 137 ++++ .../midi/ump/yup_UMPExtendedDataMessages.h | 152 +++++ .../midi/ump/yup_UMPFlexDataMessages.h | 230 +++++++ .../ump/yup_UMPJitterReductionTimestamps.cpp | 122 ++++ .../ump/yup_UMPJitterReductionTimestamps.h | 156 +++++ .../midi/ump/yup_UMPManufacturer.h | 113 +++ .../midi/ump/yup_UMPMessages.h | 612 +++++++++++++++++ .../midi/ump/yup_UMPMidi1ByteStream.cpp | 321 +++++++++ .../midi/ump/yup_UMPMidi1ByteStream.h | 211 ++++++ .../midi/ump/yup_UMPStreamMessages.h | 608 +++++++++++++++++ .../yup_audio_basics/midi/ump/yup_UMPSysEx.h | 367 ++++++++++ .../midi/ump/yup_UMPSysEx7.cpp | 66 -- .../yup_audio_basics/midi/ump/yup_UMPSysEx7.h | 91 --- .../midi/ump/yup_UMPSysExCollectors.h | 284 ++++++++ .../yup_audio_basics/midi/ump/yup_UMPTypes.h | 641 ++++++++++++++++++ .../midi/ump/yup_UMPUniversalPacket.h | 375 ++++++++++ .../midi/ump/yup_UMPUniversalSysEx.h | 300 ++++++++ modules/yup_audio_basics/yup_audio_basics.cpp | 1 - 21 files changed, 5434 insertions(+), 159 deletions(-) create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPCapabilityInquiry.h create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPChannelVoice.h create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPDataMessages.h create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPExtendedDataMessages.h create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPFlexDataMessages.h create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPJitterReductionTimestamps.cpp create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPJitterReductionTimestamps.h create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPManufacturer.h create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPMessages.h create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.cpp create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.h create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPStreamMessages.h create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPSysEx.h delete mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPSysEx7.cpp delete mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPSysEx7.h create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPSysExCollectors.h create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPTypes.h create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPUniversalPacket.h create mode 100644 modules/yup_audio_basics/midi/ump/yup_UMPUniversalSysEx.h diff --git a/modules/yup_audio_basics/midi/ump/yup_UMP.h b/modules/yup_audio_basics/midi/ump/yup_UMP.h index 7344e5b0e..c799b568f 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMP.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMP.h @@ -38,12 +38,23 @@ */ #include "yup_UMPProtocols.h" +#include "yup_UMPTypes.h" +#include "yup_UMPUniversalPacket.h" #include "yup_UMPUtils.h" #include "yup_UMPacket.h" -#include "yup_UMPSysEx7.h" +#include "yup_UMPSysEx.h" #include "yup_UMPView.h" #include "yup_UMPIterator.h" #include "yup_UMPackets.h" +#include "yup_UMPMessages.h" +#include "yup_UMPChannelVoice.h" +#include "yup_UMPDataMessages.h" +#include "yup_UMPExtendedDataMessages.h" +#include "yup_UMPFlexDataMessages.h" +#include "yup_UMPStreamMessages.h" +#include "yup_UMPSysExCollectors.h" +#include "yup_UMPManufacturer.h" +#include "yup_UMPUniversalSysEx.h" #include "yup_UMPFactory.h" #include "yup_UMPConversion.h" #include "yup_UMPMidi1ToBytestreamTranslator.h" diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPCapabilityInquiry.h b/modules/yup_audio_basics/midi/ump/yup_UMPCapabilityInquiry.h new file mode 100644 index 000000000..dbf49e4cb --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPCapabilityInquiry.h @@ -0,0 +1,436 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifndef DOXYGEN + +namespace yup::ump +{ + +inline bool isCapabilityInquiryMessage (const SysEx7& message) +{ + if (! isUniversalSysExMessage (message)) + return false; + + const auto view = UniversalSysEx::MessageView { message }; + return view.getType() == UniversalSysEx::TypeId::capabilityInquiry; +} + +struct CapabilityInquiryView : UniversalSysEx::MessageView +{ + explicit CapabilityInquiryView (const SysEx7& message) + : UniversalSysEx::MessageView (message) + { + } + + explicit CapabilityInquiryView (const UniversalSysEx::MessageView& message) + : UniversalSysEx::MessageView (message) + { + } + + uint7_t getMessageVersion() const { return sysex.data[fieldOffsets.messageVersion]; } + + Muid getSourceMuid() const { return sysex.makeUInt28 (fieldOffsets.sourceMuid); } + + Muid getDestinationMuid() const { return sysex.makeUInt28 (fieldOffsets.destinationMuid); } + + static bool validate (const SysEx7& message) + { + return isCapabilityInquiryMessage (message) && message.data.size() >= fieldOffsets.payload; + } + + struct fieldOffsets + { + static constexpr size_t messageVersion = 3u; + static constexpr size_t sourceMuid = 4u; + static constexpr size_t destinationMuid = 8u; + static constexpr size_t payload = 12u; + }; +}; + +namespace ci +{ + +template +std::optional as (const SysEx7& message) +{ + if (View::validate (message)) + return View { message }; + + return std::nullopt; +} + +template +std::optional as (const CapabilityInquiryView& message) +{ + if (View::validate (message.sysex)) + return View { message.sysex }; + + return std::nullopt; +} + +constexpr uint7_t messageVersion1 = 0x01; +constexpr uint7_t messageVersion2 = 0x02; +constexpr uint7_t version = messageVersion2; + +constexpr Muid broadcastMuid = 0x0fffffff; + +namespace Category +{ +constexpr uint7_t profileConfiguration = (1 << 2); +constexpr uint7_t propertyExchange = (1 << 3); +constexpr uint7_t processInquiry = (1 << 4); +} // namespace Category + +struct Message : UniversalSysEx::Message +{ + static constexpr size_t offsetOfData = 12; + + uint7_t getMessageVersion() const { return data[fieldOffsets.messageVersion]; } + + Muid getSourceMuid() const { return makeUInt28 (fieldOffsets.sourceMuid); } + + Muid getDestinationMuid() const { return makeUInt28 (fieldOffsets.destinationMuid); } + + void setSourceMuid (Muid muid) + { + jassert (muid <= uint28Max); + data[fieldOffsets.sourceMuid] = uint7_t (muid & 0x7f); + data[fieldOffsets.sourceMuid + 1] = uint7_t ((muid >> 7) & 0x7f); + data[fieldOffsets.sourceMuid + 2] = uint7_t ((muid >> 14) & 0x7f); + data[fieldOffsets.sourceMuid + 3] = uint7_t ((muid >> 21) & 0x7f); + } + + void setDestinationMuid (Muid muid) + { + jassert (muid <= uint28Max); + data[fieldOffsets.destinationMuid] = uint7_t (muid & 0x7f); + data[fieldOffsets.destinationMuid + 1] = uint7_t ((muid >> 7) & 0x7f); + data[fieldOffsets.destinationMuid + 2] = uint7_t ((muid >> 14) & 0x7f); + data[fieldOffsets.destinationMuid + 3] = uint7_t ((muid >> 21) & 0x7f); + } + + Message (uint7_t subtype, Muid sourceMuid, Muid destinationMuid, uint7_t deviceId = 0x7f) + : UniversalSysEx::Message (Manufacturer::universalNonRealtime) + { + jassert (subtype <= uint7Max); + jassert (sourceMuid <= uint28Max); + jassert (destinationMuid <= uint28Max); + jassert (deviceId <= uint7Max); + + data = { deviceId, + 0x0d, + subtype, + version, + uint7_t (sourceMuid & 0x7f), + uint7_t ((sourceMuid >> 7) & 0x7f), + uint7_t ((sourceMuid >> 14) & 0x7f), + uint7_t ((sourceMuid >> 21) & 0x7f), + uint7_t (destinationMuid & 0x7f), + uint7_t ((destinationMuid >> 7) & 0x7f), + uint7_t ((destinationMuid >> 14) & 0x7f), + uint7_t ((destinationMuid >> 21) & 0x7f) }; + } + + static Message makeWithPayloadSize (size_t payloadSize, + uint7_t subtype, + Muid sourceMuid, + Muid destinationMuid, + uint7_t deviceId = 0x7f) + { + Message result (Manufacturer::universalNonRealtime, payloadSize + offsetOfData); + const uint7_t header[] = { deviceId, + 0x0d, + subtype, + version, + uint7_t (sourceMuid & 0x7f), + uint7_t ((sourceMuid >> 7) & 0x7f), + uint7_t ((sourceMuid >> 14) & 0x7f), + uint7_t ((sourceMuid >> 21) & 0x7f), + uint7_t (destinationMuid & 0x7f), + uint7_t ((destinationMuid >> 7) & 0x7f), + uint7_t ((destinationMuid >> 14) & 0x7f), + uint7_t ((destinationMuid >> 21) & 0x7f) }; + result.addData (header, sizeof (header)); + return result; + } + +private: + explicit Message (ManufacturerId manufacturer, size_t capacity) + : UniversalSysEx::Message (manufacturer, capacity) + { + } + + struct fieldOffsets + { + static constexpr size_t messageVersion = 3u; + static constexpr size_t sourceMuid = 4u; + static constexpr size_t destinationMuid = 8u; + }; +}; + +namespace Subtype +{ +constexpr uint7_t discoveryInquiry = 0x70; +constexpr uint7_t discoveryReply = 0x71; +constexpr uint7_t endpointInformationInquiry = 0x72; +constexpr uint7_t endpointInformationReply = 0x73; +constexpr uint7_t ack = 0x7d; +constexpr uint7_t invalidateMuid = 0x7e; +constexpr uint7_t nak = 0x7f; +} // namespace Subtype + +namespace Discovery +{ +struct MessageView : CapabilityInquiryView +{ + using CapabilityInquiryView::CapabilityInquiryView; + + DeviceIdentity getIdentity() const { return sysex.makeDeviceIdentity (fieldOffsets.identity); } + + uint7_t getCategories() const { return sysex.data[fieldOffsets.categories]; } + + uint28_t getMaximumMessageSize() const { return sysex.makeUInt28 (fieldOffsets.maxMessageSize); } + + uint7_t getOutputPathId() const { return sysex.data[fieldOffsets.outputPathId]; } + + struct fieldOffsets + { + static constexpr size_t identity = 12u; + static constexpr size_t categories = 23u; + static constexpr size_t maxMessageSize = 24u; + static constexpr size_t outputPathId = 28u; + }; +}; +} // namespace Discovery + +struct DiscoveryInquiryView : Discovery::MessageView +{ + using Discovery::MessageView::MessageView; + + static bool validate (const SysEx7& message) + { + return isCapabilityInquiryMessage (message) + && message.data.size() >= (Discovery::MessageView::fieldOffsets.outputPathId + 1) + && message.data[2] == Subtype::discoveryInquiry; + } +}; + +inline Message makeDiscoveryInquiry (Muid sourceMuid, + const DeviceIdentity& identity, + uint7_t categories, + uint28_t maxMessageSize, + uint7_t outputPathId = 0) +{ + auto result = Message::makeWithPayloadSize (17, Subtype::discoveryInquiry, sourceMuid, broadcastMuid); + result.addDeviceIdentity (identity); + result.addUInt7 (categories); + result.addUInt28 (maxMessageSize); + result.addUInt7 (outputPathId); + return result; +} + +inline Message makeDiscoveryInquiryV1 (Muid sourceMuid, + const DeviceIdentity& identity, + uint7_t categories, + uint28_t maxMessageSize) +{ + return makeDiscoveryInquiry (sourceMuid, identity, categories, maxMessageSize); +} + +struct DiscoveryReplyView : Discovery::MessageView +{ + using Discovery::MessageView::MessageView; + + uint7_t getFunctionBlock() const { return sysex.data[fieldOffsets.functionBlock]; } + + static bool validate (const SysEx7& message) + { + return isCapabilityInquiryMessage (message) + && message.data.size() >= (fieldOffsets.functionBlock + 1) + && message.data[2] == Subtype::discoveryReply; + } + + struct fieldOffsets + { + static constexpr size_t functionBlock = 29u; + }; +}; + +inline Message makeDiscoveryReply (Muid sourceMuid, + Muid destinationMuid, + const DeviceIdentity& identity, + uint7_t categories, + uint28_t maxMessageSize, + uint7_t outputPathId, + uint7_t functionBlock) +{ + auto result = Message::makeWithPayloadSize (18, Subtype::discoveryReply, sourceMuid, destinationMuid); + result.addDeviceIdentity (identity); + result.addUInt7 (categories); + result.addUInt28 (maxMessageSize); + result.addUInt7 (outputPathId); + result.addUInt7 (functionBlock); + return result; +} + +struct EndpointInformationInquiryView : CapabilityInquiryView +{ + using CapabilityInquiryView::CapabilityInquiryView; + + static bool validate (const SysEx7& message) + { + return CapabilityInquiryView::validate (message) + && message.data[2] == Subtype::endpointInformationInquiry; + } +}; + +inline Message makeEndpointInformationInquiry (Muid sourceMuid, Muid destinationMuid) +{ + return Message (Subtype::endpointInformationInquiry, sourceMuid, destinationMuid); +} + +struct EndpointInformationReplyView : CapabilityInquiryView +{ + using CapabilityInquiryView::CapabilityInquiryView; + + uint7_t getFirstFunctionBlock() const { return sysex.data[fieldOffsets.firstFunctionBlock]; } + + uint7_t getFunctionBlockCount() const { return sysex.data[fieldOffsets.functionBlockCount]; } + + bool hasStaticFunctionBlocks() const { return (sysex.data[fieldOffsets.functionBlockCount] & 0x80u) != 0; } + + uint7_t getProtocol() const { return sysex.data[fieldOffsets.protocol]; } + + uint7_t getExtensions() const { return sysex.data[fieldOffsets.extensions]; } + + static bool validate (const SysEx7& message) + { + return CapabilityInquiryView::validate (message) + && message.data.size() >= (fieldOffsets.extensions + 1) + && message.data[2] == Subtype::endpointInformationReply; + } + + struct fieldOffsets + { + static constexpr size_t firstFunctionBlock = 12u; + static constexpr size_t functionBlockCount = 13u; + static constexpr size_t protocol = 14u; + static constexpr size_t extensions = 15u; + }; +}; + +inline Message makeEndpointInformationReply (Muid sourceMuid, + Muid destinationMuid, + uint7_t firstFunctionBlock, + uint7_t functionBlockCount, + bool staticFunctionBlocks, + uint7_t protocol, + uint7_t extensions) +{ + auto result = Message::makeWithPayloadSize (4, Subtype::endpointInformationReply, sourceMuid, destinationMuid); + result.addUInt7 (firstFunctionBlock); + result.addUInt7 (uint7_t ((staticFunctionBlocks ? 0x80u : 0x00u) | (functionBlockCount & 0x7f))); + result.addUInt7 (protocol); + result.addUInt7 (extensions); + return result; +} + +struct AckView : CapabilityInquiryView +{ + using CapabilityInquiryView::CapabilityInquiryView; + + uint7_t getOriginalSubtype() const { return sysex.data[fieldOffsets.originalSubtype]; } + + uint7_t getStatus() const { return sysex.data[fieldOffsets.status]; } + + static bool validate (const SysEx7& message) + { + return CapabilityInquiryView::validate (message) + && message.data.size() >= (fieldOffsets.status + 1) + && message.data[2] == Subtype::ack; + } + + struct fieldOffsets + { + static constexpr size_t originalSubtype = 12u; + static constexpr size_t status = 13u; + }; +}; + +inline Message makeAck (Muid sourceMuid, Muid destinationMuid, uint7_t originalSubtype, uint7_t status) +{ + auto result = Message::makeWithPayloadSize (2, Subtype::ack, sourceMuid, destinationMuid); + result.addUInt7 (originalSubtype); + result.addUInt7 (status); + return result; +} + +struct NakView : CapabilityInquiryView +{ + using CapabilityInquiryView::CapabilityInquiryView; + + uint7_t getOriginalSubtype() const { return sysex.data[fieldOffsets.originalSubtype]; } + + uint7_t getStatus() const { return sysex.data[fieldOffsets.status]; } + + static bool validate (const SysEx7& message) + { + return CapabilityInquiryView::validate (message) + && message.data.size() >= (fieldOffsets.status + 1) + && message.data[2] == Subtype::nak; + } + + struct fieldOffsets + { + static constexpr size_t originalSubtype = 12u; + static constexpr size_t status = 13u; + }; +}; + +inline Message makeNak (Muid sourceMuid, Muid destinationMuid, uint7_t originalSubtype, uint7_t status) +{ + auto result = Message::makeWithPayloadSize (2, Subtype::nak, sourceMuid, destinationMuid); + result.addUInt7 (originalSubtype); + result.addUInt7 (status); + return result; +} + +struct InvalidateMuidView : CapabilityInquiryView +{ + using CapabilityInquiryView::CapabilityInquiryView; + + static bool validate (const SysEx7& message) + { + return CapabilityInquiryView::validate (message) + && message.data[2] == Subtype::invalidateMuid; + } +}; + +inline Message makeInvalidateMuid (Muid sourceMuid, Muid destinationMuid) +{ + return Message (Subtype::invalidateMuid, sourceMuid, destinationMuid); +} + +} // namespace ci + +} // namespace yup::ump + +#endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPChannelVoice.h b/modules/yup_audio_basics/midi/ump/yup_UMPChannelVoice.h new file mode 100644 index 000000000..d58a55569 --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPChannelVoice.h @@ -0,0 +1,357 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifndef DOXYGEN + +namespace yup::ump +{ + +constexpr ControllerValue getControllerValue (const UniversalPacket& p); +constexpr uint8_t getPerNoteControllerIndex (const UniversalPacket& p); + +constexpr bool isChannelVoiceMessageWithStatus (const UniversalPacket& p, Status status) +{ + return ((p.getStatus() & 0xf0) == status) && p.isChannelVoiceMessage(); +} + +constexpr bool isNoteOnMessage (const UniversalPacket& p) +{ + if (isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::note_on))) + { + return (p.getType() == PacketType::midi2_channel_voice) || (p.getByte4() != 0); + } + + return false; +} + +constexpr bool isNoteOffMessage (const UniversalPacket& p) +{ + if (isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::note_off))) + return true; + + return (p.getType() == PacketType::midi1_channel_voice) + && ((p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::note_on)) + && (p.getByte4() == 0); +} + +constexpr NoteNumber getNoteNumber (const UniversalPacket& p) +{ + return NoteNumber (p.getByte3() & 0x7f); +} + +constexpr Pitch7_9 getNotePitch (const UniversalPacket& p) +{ + if ((p.getType() == PacketType::midi2_channel_voice) + && ((p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::note_on)) + && (p.getByte4() == NoteAttribute::pitch_7_9)) + { + return Pitch7_9 { uint16_t (p.data[1] & 0xffff) }; + } + + return Pitch7_9 { getNoteNumber (p) }; +} + +constexpr Velocity getNoteVelocity (const UniversalPacket& p) +{ + if (p.getType() == PacketType::midi2_channel_voice) + return Velocity { uint16_t (p.data[1] >> 16) }; + + if ((p.getType() == PacketType::midi1_channel_voice) + && ((p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::note_on)) + && (p.getByte4() == 0)) + { + return Velocity { uint7_t { 64 } }; + } + + return Velocity { uint7_t (p.getByte4() & 0x7f) }; +} + +constexpr bool isPolyPressureMessage (const UniversalPacket& p) +{ + return isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::poly_pressure)); +} + +constexpr ControllerValue getPolyPressureValue (const UniversalPacket& p) +{ + return getControllerValue (p); +} + +constexpr bool isControlChangeMessage (const UniversalPacket& p) +{ + return isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::control_change)); +} + +constexpr ControllerNumber getControllerNumber (const UniversalPacket& p) +{ + return ControllerNumber (p.getByte3() & 0x7f); +} + +constexpr ControllerValue getControllerValue (const UniversalPacket& p) +{ + if (p.getType() == PacketType::midi1_channel_voice) + return ControllerValue { uint7_t (p.getByte4() & 0x7f) }; + + return ControllerValue { p.data[1] }; +} + +constexpr bool isProgramChangeMessage (const UniversalPacket& p) +{ + return isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::program_change)); +} + +constexpr uint7_t getProgramValue (const UniversalPacket& p) +{ + switch (p.getType()) + { + case PacketType::midi1_channel_voice: + return uint7_t (p.getByte3() & 0x7f); + case PacketType::midi2_channel_voice: + return uint7_t ((p.data[1] >> 24u) & 0x7f); + default: + break; + } + + return 0xff; +} + +constexpr bool isChannelPressureMessage (const UniversalPacket& p) +{ + return isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::channel_pressure)); +} + +constexpr ControllerValue getChannelPressureValue (const UniversalPacket& p) +{ + if (p.getType() == PacketType::midi1_channel_voice) + return ControllerValue { uint7_t (p.getByte3() & 0x7f) }; + + return ControllerValue { p.data[1] }; +} + +constexpr bool isChannelPitchBendMessage (const UniversalPacket& p) +{ + return isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::pitch_bend)); +} + +constexpr PitchBend getChannelPitchBendValue (const UniversalPacket& p) +{ + if (p.getType() == PacketType::midi1_channel_voice) + return PitchBend { uint14_t (p.getByte3() | (p.getByte4() << 7)) }; + + return PitchBend { p.data[1] }; +} + +//============================================================================== +constexpr std::optional asMidi1ChannelVoiceMessage (const Midi2ChannelVoiceMessageView& m) +{ + switch (m.getStatus()) + { + case Status (ChannelVoiceStatus::note_off): + if (m.getByte4() == 0) + return makeMidi1NoteOffMessage (m.getGroup(), m.getChannel(), m.getByte3(), Velocity { uint16_t (m.getData() >> 16) }); + break; + case Status (ChannelVoiceStatus::note_on): + if (m.getByte4() == 0) + { + auto vel = Velocity { uint16_t (m.getData() >> 16) }; + if (vel.asUInt7() == 0) + vel = Velocity { uint7_t { 1 } }; + return makeMidi1NoteOnMessage (m.getGroup(), m.getChannel(), m.getByte3(), vel); + } + break; + case Status (ChannelVoiceStatus::poly_pressure): + return makeMidi1PolyPressureMessage (m.getGroup(), m.getChannel(), m.getByte3(), ControllerValue { m.getData() }); + case Status (ChannelVoiceStatus::control_change): + switch (m.getByte3()) + { + case ControlChange::bankSelectMsb: + case ControlChange::dataEntryMsb: + case ControlChange::bankSelectLsb: + case ControlChange::dataEntryLsb: + case ControlChange::hiResVelocityPrefix: + case ControlChange::nrpnLsb: + case ControlChange::nrpnMsb: + case ControlChange::rpnLsb: + case ControlChange::rpnMsb: + break; + default: + return makeMidi1ControlChangeMessage (m.getGroup(), m.getChannel(), m.getByte3(), ControllerValue { m.getData() }); + } + break; + case Status (ChannelVoiceStatus::program_change): + if ((m.getByte4() & 0x1) == 0) + return makeMidi1ProgramChangeMessage (m.getGroup(), m.getChannel(), uint7_t (m.getData() >> 24)); + break; + case Status (ChannelVoiceStatus::channel_pressure): + return makeMidi1ChannelPressureMessage (m.getGroup(), m.getChannel(), ControllerValue { m.getData() }); + case Status (ChannelVoiceStatus::pitch_bend): + return makeMidi1PitchBendMessage (m.getGroup(), m.getChannel(), PitchBend { m.getData() }); + default: + break; + } + + return std::nullopt; +} + +constexpr std::optional asMidi2ChannelVoiceMessage (const Midi1ChannelVoiceMessageView& m) +{ + switch (m.getStatus()) + { + case Status (ChannelVoiceStatus::note_off): + return makeMidi2NoteOffMessage (m.getGroup(), m.getChannel(), m.getDataByte1(), Velocity { m.getDataByte2() }); + case Status (ChannelVoiceStatus::note_on): + if (m.getDataByte2() == 0) + return makeMidi2NoteOffMessage (m.getGroup(), m.getChannel(), m.getDataByte1(), Velocity { uint7_t { 64 } }); + return makeMidi2NoteOnMessage (m.getGroup(), m.getChannel(), m.getDataByte1(), Velocity { m.getDataByte2() }); + case Status (ChannelVoiceStatus::poly_pressure): + return makeMidi2PolyPressureMessage (m.getGroup(), m.getChannel(), m.getDataByte1(), ControllerValue { m.getDataByte2() }); + case Status (ChannelVoiceStatus::control_change): + switch (m.getDataByte1()) + { + case ControlChange::bankSelectMsb: + case ControlChange::dataEntryMsb: + case ControlChange::bankSelectLsb: + case ControlChange::dataEntryLsb: + case ControlChange::hiResVelocityPrefix: + case ControlChange::nrpnLsb: + case ControlChange::nrpnMsb: + case ControlChange::rpnLsb: + case ControlChange::rpnMsb: + break; + default: + return makeMidi2ControlChangeMessage (m.getGroup(), m.getChannel(), m.getDataByte1(), ControllerValue { m.getDataByte2() }); + } + break; + case Status (ChannelVoiceStatus::program_change): + return makeMidi2ProgramChangeMessage (m.getGroup(), m.getChannel(), m.getDataByte1()); + case Status (ChannelVoiceStatus::channel_pressure): + return makeMidi2ChannelPressureMessage (m.getGroup(), m.getChannel(), ControllerValue { m.getDataByte1() }); + case Status (ChannelVoiceStatus::pitch_bend): + return makeMidi2PitchBendMessage (m.getGroup(), m.getChannel(), PitchBend { m.get14BitValue() }); + default: + break; + } + + return std::nullopt; +} + +//============================================================================== +constexpr bool isRegisteredControllerMessage (const UniversalPacket& p) +{ + return isMidi2ChannelVoiceMessage (p) + && (p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::registered_controller); +} + +constexpr bool isAssignableControllerMessage (const UniversalPacket& p) +{ + return isMidi2ChannelVoiceMessage (p) + && (p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::assignable_controller); +} + +constexpr bool isRegisteredPerNoteControllerMessage (const UniversalPacket& p) +{ + return isMidi2ChannelVoiceMessage (p) + && (p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::registered_per_note_controller); +} + +constexpr bool isRegisteredPerNoteControllerPitchMessage (const UniversalPacket& p) +{ + return isRegisteredPerNoteControllerMessage (p) + && getPerNoteControllerIndex (p) == RegisteredPerNoteController::pitch7_25; +} + +constexpr bool isAssignablePerNoteControllerMessage (const UniversalPacket& p) +{ + return isMidi2ChannelVoiceMessage (p) + && (p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::assignable_per_note_controller); +} + +constexpr bool isPerNotePitchBendMessage (const UniversalPacket& p) +{ + return isMidi2ChannelVoiceMessage (p) + && (p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::per_note_pitch_bend); +} + +constexpr bool isNoteOnWithAttribute (const UniversalPacket& p, uint8_t attribute) +{ + return isMidi2ChannelVoiceMessage (p) + && ((p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::note_on)) + && (p.getByte4() == attribute); +} + +constexpr bool isNoteOffWithAttribute (const UniversalPacket& p, uint8_t attribute) +{ + return isMidi2ChannelVoiceMessage (p) + && ((p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::note_off)) + && (p.getByte4() == attribute); +} + +constexpr bool isNoteOnWithPitch7_9 (const UniversalPacket& p) +{ + return isNoteOnWithAttribute (p, NoteAttribute::pitch_7_9); +} + +constexpr uint8_t getMidi2NoteAttribute (const UniversalPacket& p) +{ + return p.getByte4(); +} + +constexpr uint16_t getMidi2NoteAttributeData (const UniversalPacket& p) +{ + return uint16_t (p.data[1] & 0xffff); +} + +constexpr uint8_t getPerNoteControllerIndex (const UniversalPacket& p) +{ + return p.getByte4(); +} + +constexpr bool isPitchBendSensitivityMessage (const UniversalPacket& p) +{ + return isRegisteredControllerMessage (p) + && (p.getByte3() == 0) + && (p.getByte4() == RegisteredParameterNumber::pitchBendSensitivity); +} + +constexpr bool isPerNotePitchBendSensitivityMessage (const UniversalPacket& p) +{ + return isRegisteredControllerMessage (p) + && (p.getByte3() == 0) + && (p.getByte4() == RegisteredParameterNumber::perNotePitchBendSensitivity); +} + +constexpr PitchBendSensitivity getPitchBendSensitivityValue (const UniversalPacket& p) +{ + return PitchBendSensitivity { p.data[1] & 0xfffc0000u }; +} + +constexpr PitchBendSensitivity getPerNotePitchBendSensitivityValue (const UniversalPacket& p) +{ + return PitchBendSensitivity { p.data[1] }; +} + +constexpr PitchBend getPerNotePitchBendValue (const UniversalPacket& p) +{ + return PitchBend { p.data[1] }; +} + +} // namespace yup::ump + +#endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPDataMessages.h b/modules/yup_audio_basics/midi/ump/yup_UMPDataMessages.h new file mode 100644 index 000000000..55f09d4e5 --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPDataMessages.h @@ -0,0 +1,137 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifndef DOXYGEN + +namespace yup::ump +{ + +struct DataMessage : UniversalPacket +{ + constexpr DataMessage() + : UniversalPacket (0x30000000u) + { + } + + constexpr explicit DataMessage (Status status) + : UniversalPacket (0x30000000u | (uint32_t (status) << 16u)) + { + } +}; + +struct SysEx7Packet : DataMessage +{ + constexpr SysEx7Packet() = default; + + constexpr SysEx7Packet (Status status, Group group) + : DataMessage (status) + { + setGroup (group); + } + + constexpr PacketFormat getFormat() const { return PacketFormat ((getStatus() >> 4) & 0x3); } + + constexpr uint8_t getPayloadByte (size_t b) const { return getByte (2 + b); } + + constexpr void setPayloadByte (size_t b, uint8_t data) { setByte7Bit (2 + b, data); } + + constexpr size_t getPayloadSize() const { return getStatus() & 0x0f; } + + constexpr void setPayloadSize (size_t size) + { + jassert (size <= 6); + setByte (1, uint8_t ((getStatus() & 0xf0) + (size & 0x0f))); + } + + constexpr void addPayloadByte (uint8_t byte) + { + const auto size = getPayloadSize(); + jassert (size < 6); + setByte7Bit (2 + size, byte); + setPayloadSize (size + 1); + } +}; + +constexpr bool isDataMessage (const UniversalPacket& p) +{ + return p.getType() == PacketType::data; +} + +constexpr bool isSysEx7Packet (const UniversalPacket& p) +{ + return isDataMessage (p) + && ((p.getStatus() & 0xf0) <= Status (DataStatus::sysex7_end)) + && ((p.getStatus() & 0x0f) <= 6); +} + +struct SysEx7PacketView +{ + constexpr explicit SysEx7PacketView (const UniversalPacket& ump) + : p (ump) + { + jassert (isSysEx7Packet (ump)); + } + + constexpr Group getGroup() const { return p.getGroup(); } + + constexpr Status getStatus() const { return Status (p.getStatus() & 0xf0); } + + constexpr PacketFormat getFormat() const { return PacketFormat ((p.getStatus() >> 4) & 0x3); } + + constexpr size_t getPayloadSize() const { return p.getStatus() & 0x0f; } + + constexpr uint8_t getPayloadByte (size_t b) const { return p.getByte (2 + b); } + +private: + const UniversalPacket& p; +}; + +constexpr std::optional asSysEx7PacketView (const UniversalPacket& p) +{ + if (isSysEx7Packet (p)) + return SysEx7PacketView { p }; + + return std::nullopt; +} + +constexpr SysEx7Packet makeSysEx7CompletePacket (Group group = 0) +{ + return SysEx7Packet { Status (DataStatus::sysex7_complete), group }; +} + +constexpr SysEx7Packet makeSysEx7StartPacket (Group group = 0) +{ + return SysEx7Packet { Status (DataStatus::sysex7_start), group }; +} + +constexpr SysEx7Packet makeSysEx7ContinuePacket (Group group = 0) +{ + return SysEx7Packet { Status (DataStatus::sysex7_continue), group }; +} + +constexpr SysEx7Packet makeSysEx7EndPacket (Group group = 0) +{ + return SysEx7Packet { Status (DataStatus::sysex7_end), group }; +} + +} // namespace yup::ump + +#endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPExtendedDataMessages.h b/modules/yup_audio_basics/midi/ump/yup_UMPExtendedDataMessages.h new file mode 100644 index 000000000..e5bc8531b --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPExtendedDataMessages.h @@ -0,0 +1,152 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifndef DOXYGEN + +namespace yup::ump +{ + +struct ExtendedDataMessage : UniversalPacket +{ + constexpr ExtendedDataMessage() + : UniversalPacket (0x50000000u) + { + } + + constexpr explicit ExtendedDataMessage (Status status) + : UniversalPacket (0x50000000u | (uint32_t (status) << 16u)) + { + } +}; + +struct SysEx8Packet : ExtendedDataMessage +{ + constexpr SysEx8Packet() + { + data[0] |= 0x00010000u; + } + + constexpr SysEx8Packet (Status status, uint8_t streamId, Group group) + { + data[0] = 0x50010000u | (uint32_t (group) << 24u) | (uint32_t (status) << 16u) | (uint32_t (streamId) << 8u); + } + + constexpr PacketFormat getFormat() const { return PacketFormat ((getStatus() >> 4) & 0x3); } + + constexpr uint8_t getStreamId() const { return getByte (2); } + + constexpr void setStreamId (uint8_t id) { setByte (2, id); } + + constexpr uint8_t getPayloadByte (size_t b) const { return getByte (3 + b); } + + constexpr void setPayloadByte (size_t b, uint8_t data) { setByte (3 + b, data); } + + constexpr size_t getPayloadSize() const + { + const auto s = size_t (getStatus() & 0x0f); + return (s > 0 ? s - 1 : 0); + } + + constexpr void setPayloadSize (size_t size) + { + jassert (size <= 13); + setByte (1, uint8_t ((getStatus() & 0xf0) + ((size + 1) & 0x0f))); + } + + constexpr void addPayloadByte (uint8_t byte) + { + const auto size = getPayloadSize(); + jassert (size < 13); + setByte (3 + size, byte); + setPayloadSize (size + 1); + } +}; + +constexpr bool isExtendedDataMessage (const UniversalPacket& p) +{ + return p.getType() == PacketType::extended_data; +} + +constexpr bool isSysEx8Packet (const UniversalPacket& p) +{ + return isExtendedDataMessage (p) + && ((p.getStatus() & 0xf0) <= Status (ExtendedDataStatus::sysex8_end)) + && ((p.getStatus() & 0x0f) > 0) + && ((p.getStatus() & 0x0f) <= 14); +} + +struct SysEx8PacketView +{ + constexpr explicit SysEx8PacketView (const UniversalPacket& ump) + : p (ump) + { + jassert (isSysEx8Packet (ump)); + } + + constexpr Group getGroup() const { return p.getGroup(); } + + constexpr PacketFormat getFormat() const { return PacketFormat ((p.getStatus() >> 4) & 0x3); } + + constexpr uint8_t getStreamId() const { return p.getByte (2); } + + constexpr size_t getPayloadSize() const + { + const auto s = size_t (p.getStatus() & 0x0f); + return (s > 0 ? s - 1 : 0); + } + + constexpr uint8_t getPayloadByte (size_t b) const { return p.getByte (3 + b); } + +private: + const UniversalPacket& p; +}; + +constexpr std::optional asSysEx8PacketView (const UniversalPacket& p) +{ + if (isSysEx8Packet (p)) + return SysEx8PacketView { p }; + + return std::nullopt; +} + +constexpr SysEx8Packet makeSysEx8CompletePacket (uint8_t streamId, Group group = 0) +{ + return SysEx8Packet { Status (ExtendedDataStatus::sysex8_complete), streamId, group }; +} + +constexpr SysEx8Packet makeSysEx8StartPacket (uint8_t streamId, Group group = 0) +{ + return SysEx8Packet { Status (ExtendedDataStatus::sysex8_start), streamId, group }; +} + +constexpr SysEx8Packet makeSysEx8ContinuePacket (uint8_t streamId, Group group = 0) +{ + return SysEx8Packet { Status (ExtendedDataStatus::sysex8_continue), streamId, group }; +} + +constexpr SysEx8Packet makeSysEx8EndPacket (uint8_t streamId, Group group = 0) +{ + return SysEx8Packet { Status (ExtendedDataStatus::sysex8_end), streamId, group }; +} + +} // namespace yup::ump + +#endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPFlexDataMessages.h b/modules/yup_audio_basics/midi/ump/yup_UMPFlexDataMessages.h new file mode 100644 index 000000000..386e794d7 --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPFlexDataMessages.h @@ -0,0 +1,230 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifndef DOXYGEN + +namespace yup::ump +{ + +struct FlexDataMessage : UniversalPacket +{ + constexpr explicit FlexDataMessage (Group group = 0) + : UniversalPacket (0xD0000000u | (uint32_t (group & 0x0f) << 24u)) + { + } + + constexpr FlexDataMessage (Group group, + PacketFormat format, + PacketAddress address, + uint4_t channel, + Status statusBank, + Status status, + uint32_t data1 = 0, + uint32_t data2 = 0, + uint32_t data3 = 0) + : UniversalPacket (0xD0000000u | (uint32_t (group & 0x0f) << 24u) + | ((uint32_t ((uint32_t (format) << 6u) + | (uint32_t (address) << 4u) + | (channel & 0x0f))) + << 16u) + | (uint32_t (statusBank) << 8u) + | uint32_t (status), + data1, + data2, + data3) + { + jassert (channel <= 15u); + jassert (address == PacketAddress::channel || channel == 0); + } + + constexpr PacketFormat getFormat() const { return PacketFormat ((getByte2() & 0xc0u) >> 6u); } + + constexpr PacketAddress getAddress() const { return PacketAddress ((getByte2() & 0x30u) >> 4u); } + + constexpr Status getStatusBank() const { return getByte3(); } + + constexpr Status getStatus() const { return getByte4(); } + + std::string getPayloadAsString() const + { + return getPayloadAsString (*this); + } + + static std::string getPayloadAsString (const UniversalPacket& packet) + { + std::string result; + result.reserve (12); + for (uint8_t b = 4; b < 16; ++b) + { + if (const auto c = char (packet.getByte (b))) + result.push_back (c); + else + break; + } + return result; + } +}; + +constexpr bool isFlexDataMessage (const UniversalPacket& p) +{ + return p.getType() == PacketType::flex_data; +} + +struct FlexDataMessageView +{ + constexpr explicit FlexDataMessageView (const UniversalPacket& ump) + : p (ump) + { + jassert (p.getType() == PacketType::flex_data); + } + + constexpr Group getGroup() const { return p.getGroup(); } + + constexpr PacketFormat getFormat() const { return PacketFormat ((p.getByte2() & 0xc0u) >> 6u); } + + constexpr PacketAddress getAddress() const { return PacketAddress ((p.getByte2() & 0x30u) >> 4u); } + + constexpr Channel getChannel() const { return Channel (p.getByte2() & 0x0f); } + + constexpr Status getStatusBank() const { return p.getByte3(); } + + constexpr Status getStatus() const { return p.getByte4(); } + + constexpr uint32_t getData1() const { return p.data[1]; } + + constexpr uint32_t getData2() const { return p.data[2]; } + + constexpr uint32_t getData3() const { return p.data[3]; } + + std::string getPayloadAsString() const { return FlexDataMessage::getPayloadAsString (p); } + +private: + const UniversalPacket& p; +}; + +constexpr std::optional asFlexDataMessageView (const UniversalPacket& p) +{ + if (isFlexDataMessage (p)) + return FlexDataMessageView { p }; + + return std::nullopt; +} + +constexpr FlexDataMessage makeFlexDataMessage (Group group, + PacketFormat format, + PacketAddress address, + Channel channel, + Status statusBank, + Status status, + uint32_t data1 = 0, + uint32_t data2 = 0, + uint32_t data3 = 0) +{ + return FlexDataMessage (group, format, address, channel, statusBank, status, data1, data2, data3); +} + +constexpr FlexDataMessage makeFlexDataTextMessage (Group group, + PacketFormat format, + PacketAddress address, + Channel channel, + Status statusBank, + Status status, + const std::string_view& text) +{ + jassert (text.length() <= 12); + jassert (format == PacketFormat::complete || format == PacketFormat::end || text.length() == 12); + + auto result = FlexDataMessage (group, format, address, channel, statusBank, status); + size_t byte = 4; + for (const auto c : text) + { + result.setByte (byte, uint8_t (c)); + if (++byte >= 16) + break; + } + return result; +} + +constexpr FlexDataMessage makeSetTempoMessage (Group group, uint32_t tenNsPerQuarterNote) +{ + return makeFlexDataMessage (group, + PacketFormat::complete, + PacketAddress::group, + 0, + 0x00, + 0x00, + tenNsPerQuarterNote); +} + +constexpr FlexDataMessage makeSetTimeSignatureMessage (Group group, + uint8_t numerator, + uint8_t denominator, + uint8_t num32ndNotes) +{ + auto result = makeFlexDataMessage (group, PacketFormat::complete, PacketAddress::group, 0, 0x00, 0x01); + result.setByte (4, numerator); + result.setByte (5, denominator); + result.setByte (6, num32ndNotes); + return result; +} + +constexpr FlexDataMessage makeSetMetronomeMessage (Group group, + uint8_t numClocksPerPrimaryClick, + uint8_t barAccentPart1, + uint8_t barAccentPart2, + uint8_t barAccentPart3, + uint8_t numSubdivisionClicks1, + uint8_t numSubdivisionClicks2) +{ + auto result = makeFlexDataMessage (group, PacketFormat::complete, PacketAddress::group, 0, 0x00, 0x02); + result.setByte (4, numClocksPerPrimaryClick); + result.setByte (5, barAccentPart1); + result.setByte (6, barAccentPart2); + result.setByte (7, barAccentPart3); + result.setByte (8, numSubdivisionClicks1); + result.setByte (9, numSubdivisionClicks2); + return result; +} + +constexpr FlexDataMessage makeSetKeySignatureMessage (Group group, + PacketAddress address, + Channel channel, + uint4_t sharpsOrFlats, + uint4_t tonicNote) +{ + auto result = makeFlexDataMessage (group, PacketFormat::complete, address, channel, 0x00, 0x05); + result.setByte (4, uint8_t ((sharpsOrFlats << 4) | (tonicNote & 0x0f))); + return result; +} + +constexpr FlexDataMessage makeSetChordMessage (Group group, + PacketAddress address, + Channel channel, + uint32_t data1, + uint32_t data2, + uint32_t data3) +{ + return makeFlexDataMessage (group, PacketFormat::complete, address, channel, 0x00, 0x06, data1, data2, data3); +} + +} // namespace yup::ump + +#endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPJitterReductionTimestamps.cpp b/modules/yup_audio_basics/midi/ump/yup_UMPJitterReductionTimestamps.cpp new file mode 100644 index 000000000..52842917d --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPJitterReductionTimestamps.cpp @@ -0,0 +1,122 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup::ump +{ + +JitterTimestamp JitterClock::now() +{ + using SystemClock = std::chrono::high_resolution_clock; + static const auto zero = SystemClock::now(); + + const auto jrTimestampNow = std::chrono::duration_cast (SystemClock::now() - zero); + return JitterTimestamp { uint16_t (jrTimestampNow.count() & 0xffff) }; +} + +void JitterClockFollower::reset() +{ + clockTimestamp = {}; + clockTime = zero; + messageTime = zero; + jitter = std::chrono::milliseconds (0); + securityOffset = std::chrono::milliseconds (2); +} + +void JitterClockFollower::setSecurityOffset (Duration offset) +{ + jassert (offset >= Duration { 0 }); + securityOffset = offset; +} + +void JitterClockFollower::processClock (TimePoint received, JitterTimestamp timestamp) +{ + if (clockTime == zero) + { + clockTime = received; + clockTimestamp = timestamp; + messageTime = received; + return; + } + + const auto diffTimestamp = timestamp - clockTimestamp; + clockTimestamp = timestamp; + + const auto diffDuration = std::chrono::duration_cast (diffTimestamp); + const auto expectedReceiveTime = clockTime + diffDuration; + const auto jitterValue = received - expectedReceiveTime; + + if (received < expectedReceiveTime) + { + clockTime = received; + + if (received > messageTime) + messageTime = received; + } + else + { + clockTime = expectedReceiveTime; + messageTime = received; + } + + updateStats (jitterValue); +} + +JitterClockFollower::TimePoint JitterClockFollower::scheduleMessage (TimePoint received, JitterTimestamp timestamp) +{ + if (clockTime != zero) + { + const auto diffTimestamp = timestamp - clockTimestamp; + const auto diffDuration = std::chrono::duration_cast (diffTimestamp); + const auto nextMessageTime = clockTime + diffDuration; + + if (nextMessageTime > messageTime) + messageTime = nextMessageTime; + } + else + { + messageTime = received; + } + + return messageTime + securityOffset; +} + +void JitterClockFollower::updateStats (Duration jitterValue) +{ + if (jitterValue < Duration { 0 }) + { + jitter -= jitterValue; + recalcSecurityOffset(); + } + else if (jitterValue > jitter) + { + jitter = jitterValue; + recalcSecurityOffset(); + } +} + +void JitterClockFollower::recalcSecurityOffset() +{ + const auto value = jitter * 12 / 10; + if (value > securityOffset) + securityOffset = value; +} + +} // namespace yup::ump diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPJitterReductionTimestamps.h b/modules/yup_audio_basics/midi/ump/yup_UMPJitterReductionTimestamps.h new file mode 100644 index 000000000..424564230 --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPJitterReductionTimestamps.h @@ -0,0 +1,156 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifndef DOXYGEN + +namespace yup::ump +{ + +constexpr uint16_t jrClockFrequency = 31250; + +using JitterTicks = std::chrono::duration>; + +struct JitterTimestamp +{ + uint16_t value {}; + + constexpr JitterTimestamp() = default; + + constexpr explicit JitterTimestamp (uint16_t v) + : value (v) + { + } + + constexpr bool operator== (JitterTimestamp other) const { return value == other.value; } + + constexpr bool operator!= (JitterTimestamp other) const { return value != other.value; } + + JitterTicks operator- (JitterTimestamp other) const + { + int diff = int (value) - int (other.value); + if (diff < 0) + diff += 0x10000; + return JitterTicks { static_cast (diff) }; + } +}; + +class JitterClock +{ +public: + using Duration = JitterTicks; + using Rep = Duration::rep; + using Period = Duration::period; + using TimePoint = uint16_t; + + static JitterTimestamp now(); +}; + +struct JitterMessage : UniversalPacket +{ + JitterTimestamp getTimestamp() const + { + return JitterTimestamp { uint16_t ((getByte (2) << 8) | getByte (3)) }; + } + + void setTimestamp (JitterTimestamp timestamp) + { + setByte (2, uint8_t (timestamp.value >> 8)); + setByte (3, uint8_t (timestamp.value & 0xff)); + } + +protected: + explicit JitterMessage (UtilityStatus status) + { + setType (PacketType::utility); + setByte (1, uint8_t (status)); + } + + JitterMessage (UtilityStatus status, JitterTimestamp timestamp) + : JitterMessage (status) + { + setTimestamp (timestamp); + } +}; + +struct JitterClockMessage : JitterMessage +{ + JitterClockMessage() + : JitterMessage (UtilityStatus::jitterClock, {}) + { + } + + explicit JitterClockMessage (JitterTimestamp timestamp) + : JitterMessage (UtilityStatus::jitterClock, timestamp) + { + } +}; + +struct JitterTimestampMessage : JitterMessage +{ + JitterTimestampMessage() + : JitterMessage (UtilityStatus::jitterTimestamp, {}) + { + } + + explicit JitterTimestampMessage (JitterTimestamp timestamp) + : JitterMessage (UtilityStatus::jitterTimestamp, timestamp) + { + } +}; + +class JitterClockFollower +{ +public: + JitterClockFollower() = default; + + void reset(); + + using SystemClock = std::chrono::high_resolution_clock; + using TimePoint = SystemClock::time_point; + using Duration = SystemClock::duration; + + void processClock (TimePoint received, JitterTimestamp timestamp); + TimePoint scheduleMessage (TimePoint received, JitterTimestamp timestamp); + + Duration getSecurityOffset() const { return securityOffset; } + + Duration getJitter() const { return jitter; } + + void setSecurityOffset (Duration offset); + +protected: + void updateStats (Duration jitterValue); + void recalcSecurityOffset(); + +private: + const SystemClock::time_point zero = SystemClock::now(); + + JitterTimestamp clockTimestamp { 0 }; + TimePoint clockTime { zero }; + TimePoint messageTime { zero }; + + Duration jitter { 0 }; + Duration securityOffset { std::chrono::milliseconds (2) }; +}; + +} // namespace yup::ump + +#endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPManufacturer.h b/modules/yup_audio_basics/midi/ump/yup_UMPManufacturer.h new file mode 100644 index 000000000..59223af46 --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPManufacturer.h @@ -0,0 +1,113 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifndef DOXYGEN + +namespace yup::ump +{ + +namespace Manufacturer +{ +constexpr ManufacturerId sequentialCircuits = 0x010000; +constexpr ManufacturerId moog = 0x040000; +constexpr ManufacturerId lexicon = 0x060000; +constexpr ManufacturerId kurzweil = 0x070000; +constexpr ManufacturerId fender = 0x080000; +constexpr ManufacturerId oberheim = 0x100000; +constexpr ManufacturerId midi9 = 0x090000; +constexpr ManufacturerId simmons = 0x120000; +constexpr ManufacturerId digidesign = 0x130000; +constexpr ManufacturerId fairlight = 0x140000; +constexpr ManufacturerId emu = 0x180000; +constexpr ManufacturerId hohner = 0x240000; +constexpr ManufacturerId ppg = 0x290000; +constexpr ManufacturerId wersi = 0x3B0000; +constexpr ManufacturerId waldorf = 0x3E0000; +constexpr ManufacturerId kawai = 0x400000; +constexpr ManufacturerId roland = 0x410000; +constexpr ManufacturerId korg = 0x420000; +constexpr ManufacturerId yamaha = 0x430000; +constexpr ManufacturerId casio = 0x440000; +constexpr ManufacturerId akai = 0x470000; +constexpr ManufacturerId fujitsu = 0x4B0000; +constexpr ManufacturerId sony = 0x4C0000; +constexpr ManufacturerId teac = 0x4E0000; +constexpr ManufacturerId fostex = 0x510000; +constexpr ManufacturerId zoom = 0x520000; +constexpr ManufacturerId suzuki = 0x550000; + +constexpr ManufacturerId educational = 0x7D0000; +constexpr ManufacturerId universalNonRealtime = 0x7E0000; +constexpr ManufacturerId universalRealtime = 0x7F0000; + +constexpr ManufacturerId rane = 0x000017; +constexpr ManufacturerId allenHeath = 0x00001A; +constexpr ManufacturerId motu = 0x00003B; +constexpr ManufacturerId atari = 0x000058; +constexpr ManufacturerId mackie = 0x000066; +constexpr ManufacturerId amd = 0x00006F; + +constexpr ManufacturerId mAudio = 0x000105; +constexpr ManufacturerId microsoft = 0x00010A; +constexpr ManufacturerId line6 = 0x00010C; +constexpr ManufacturerId cakewalk = 0x000121; +constexpr ManufacturerId numark = 0x00013F; + +constexpr ManufacturerId denon = 0x00020B; +constexpr ManufacturerId google = 0x00020D; +constexpr ManufacturerId imitone = 0x000213; +constexpr ManufacturerId universalAudio = 0x000218; +constexpr ManufacturerId sensel = 0x00021D; +constexpr ManufacturerId mk2Image = 0x000222; + +constexpr ManufacturerId bonTempi = 0x00200B; +constexpr ManufacturerId fatar = 0x00201A; +constexpr ManufacturerId pinnacle = 0x00201E; +constexpr ManufacturerId tcElectronics = 0x00201F; +constexpr ManufacturerId doepfer = 0x002020; +constexpr ManufacturerId focusrite = 0x002029; +constexpr ManufacturerId novation = 0x002029; +constexpr ManufacturerId emagic = 0x002031; +constexpr ManufacturerId behringer = 0x002032; +constexpr ManufacturerId access = 0x002033; +constexpr ManufacturerId terratec = 0x002036; +constexpr ManufacturerId propellerhead = 0x00203A; +constexpr ManufacturerId klavis = 0x002047; +constexpr ManufacturerId vermona = 0x00204D; +constexpr ManufacturerId nokia = 0x00204E; +constexpr ManufacturerId hartmann = 0x002050; +constexpr ManufacturerId waves = 0x002066; +constexpr ManufacturerId arturia = 0x00206B; +constexpr ManufacturerId hanpin = 0x002079; +constexpr ManufacturerId serato = 0x00207F; +constexpr ManufacturerId presonus = 0x002103; +constexpr ManufacturerId nativeInstruments = 0x002109; +constexpr ManufacturerId ploytec = 0x00210D; +constexpr ManufacturerId roli = 0x002110; +constexpr ManufacturerId ikMultimedia = 0x00211A; +constexpr ManufacturerId ableton = 0x00211D; +constexpr ManufacturerId bome = 0x002132; +constexpr ManufacturerId touchkeys = 0x002136; +} // namespace Manufacturer + +} // namespace yup::ump + +#endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPMessages.h b/modules/yup_audio_basics/midi/ump/yup_UMPMessages.h new file mode 100644 index 000000000..a6e114bff --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPMessages.h @@ -0,0 +1,612 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifndef DOXYGEN + +namespace yup::ump +{ + +//============================================================================== +struct UtilityMessage : UniversalPacket +{ + constexpr UtilityMessage() = default; + + constexpr explicit UtilityMessage (Status status, uint16_t payload = 0u) + : UniversalPacket (uint32_t (status) << 16u | payload) + { + } +}; + +struct UtilityMessageView +{ + constexpr explicit UtilityMessageView (const UniversalPacket& ump) + : p (ump) + { + jassert (p.getType() == PacketType::utility); + } + + constexpr Status getStatus() const { return p.getStatus(); } + + constexpr uint16_t getPayload() const { return uint16_t (p.getByte3() << 8u) | p.getByte4(); } + +private: + const UniversalPacket& p; +}; + +constexpr UtilityMessage makeUtilityMessage (Status status, uint16_t payload) +{ + return UtilityMessage { status, payload }; +} + +//============================================================================== +struct SystemMessage : UniversalPacket +{ + constexpr SystemMessage() + : UniversalPacket (0x10000000u) + { + } + + constexpr SystemMessage (Group group, Status status, uint7_t data1 = 0, uint7_t data2 = 0) + : UniversalPacket (0x10000000u | ((group & 0x0f) << 24u) | (uint32_t (status) << 16u) + | (uint32_t (data1) << 8u) | data2) + { + } +}; + +constexpr bool isSystemMessage (const UniversalPacket& p) +{ + return p.getType() == PacketType::system; +} + +struct SystemMessageView +{ + constexpr explicit SystemMessageView (const UniversalPacket& ump) + : p (ump) + { + jassert (p.getType() == PacketType::system); + } + + constexpr Group getGroup() const { return p.getGroup(); } + + constexpr Status getStatus() const { return p.getStatus(); } + + constexpr uint7_t getDataByte1() const { return uint7_t (p.getByte3() & 0x7f); } + + constexpr uint7_t getDataByte2() const { return uint7_t (p.getByte4() & 0x7f); } + + constexpr uint14_t getSongPosition() const + { + if (SystemStatus (p.getStatus()) == SystemStatus::song_position) + return uint14_t (getDataByte1() | (uint14_t (getDataByte2()) << 7u)); + + return 0; + } + +private: + const UniversalPacket& p; +}; + +constexpr std::optional asSystemMessageView (const UniversalPacket& p) +{ + if (isSystemMessage (p)) + return SystemMessageView { p }; + + return std::nullopt; +} + +constexpr SystemMessage makeSystemMessage (Group group, Status status, uint7_t data1 = 0, uint7_t data2 = 0) +{ + return SystemMessage { group, status, data1, data2 }; +} + +constexpr SystemMessage makeSongPositionMessage (Group group, uint14_t position) +{ + return makeSystemMessage (group, + Status (SystemStatus::song_position), + static_cast (position & 0x7f), + static_cast ((position >> 7) & 0x7f)); +} + +//============================================================================== +struct Midi1ChannelVoiceMessage : UniversalPacket +{ + constexpr Midi1ChannelVoiceMessage() = default; + + constexpr Midi1ChannelVoiceMessage (Group group, Status status, uint7_t data1 = 0, uint7_t data2 = 0) + : UniversalPacket (0x20000000u | ((group & 0x0f) << 24u) | (uint32_t (status) << 16u) + | (uint32_t (data1) << 8u) | data2) + { + } + + constexpr explicit Midi1ChannelVoiceMessage (const UniversalPacket& p) + : UniversalPacket (p) + { + jassert (p.getType() == PacketType::midi1_channel_voice); + } +}; + +constexpr bool isMidi1ChannelVoiceMessage (const UniversalPacket& p) +{ + return p.getType() == PacketType::midi1_channel_voice; +} + +struct Midi1ChannelVoiceMessageView +{ + constexpr explicit Midi1ChannelVoiceMessageView (const UniversalPacket& ump) + : p (ump) + { + jassert (isMidi1ChannelVoiceMessage (p)); + } + + constexpr Group getGroup() const { return p.getGroup(); } + + constexpr Status getStatus() const { return Status (p.getStatus() & 0xf0); } + + constexpr Channel getChannel() const { return Channel (p.getStatus() & 0x0f); } + + constexpr uint7_t getDataByte1() const { return uint7_t (p.getByte3() & 0x7f); } + + constexpr uint7_t getDataByte2() const { return uint7_t (p.getByte4() & 0x7f); } + + constexpr uint14_t get14BitValue() const + { + return uint14_t (getDataByte1() | (uint14_t (getDataByte2()) << 7u)); + } + +private: + const UniversalPacket& p; +}; + +constexpr std::optional asMidi1ChannelVoiceMessageView (const UniversalPacket& p) +{ + if (isMidi1ChannelVoiceMessage (p)) + return Midi1ChannelVoiceMessageView { p }; + + return std::nullopt; +} + +constexpr Midi1ChannelVoiceMessage makeMidi1ChannelVoiceMessage (Group group, + Status status, + Channel channel, + uint7_t data1, + uint7_t data2 = 0) +{ + return Midi1ChannelVoiceMessage { group, + Status (status | (channel & 0x0f)), + uint7_t (data1 & 0x7f), + uint7_t (data2 & 0x7f) }; +} + +constexpr Midi1ChannelVoiceMessage makeMidi1NoteOffMessage (Group group, + Channel channel, + NoteNumber noteNr, + Velocity vel = {}) +{ + return makeMidi1ChannelVoiceMessage (group, + Status (Midi1ChannelVoiceStatus::note_off), + channel, + noteNr, + vel.asUInt7()); +} + +constexpr Midi1ChannelVoiceMessage makeMidi1NoteOnMessage (Group group, + Channel channel, + NoteNumber noteNr, + Velocity vel) +{ + return makeMidi1ChannelVoiceMessage (group, + Status (Midi1ChannelVoiceStatus::note_on), + channel, + noteNr, + vel.asUInt7()); +} + +constexpr Midi1ChannelVoiceMessage makeMidi1PolyPressureMessage (Group group, + Channel channel, + NoteNumber noteNr, + ControllerValue pressure) +{ + return makeMidi1ChannelVoiceMessage (group, + Status (Midi1ChannelVoiceStatus::poly_pressure), + channel, + noteNr, + pressure.asUInt7()); +} + +constexpr Midi1ChannelVoiceMessage makeMidi1ControlChangeMessage (Group group, + Channel channel, + ControllerNumber controller, + ControllerValue value) +{ + return makeMidi1ChannelVoiceMessage (group, + Status (Midi1ChannelVoiceStatus::control_change), + channel, + controller, + value.asUInt7()); +} + +constexpr Midi1ChannelVoiceMessage makeMidi1ProgramChangeMessage (Group group, + Channel channel, + ProgramNumber program) +{ + return makeMidi1ChannelVoiceMessage (group, + Status (Midi1ChannelVoiceStatus::program_change), + channel, + program); +} + +constexpr Midi1ChannelVoiceMessage makeMidi1ChannelPressureMessage (Group group, + Channel channel, + ControllerValue pressure) +{ + return makeMidi1ChannelVoiceMessage (group, + Status (Midi1ChannelVoiceStatus::channel_pressure), + channel, + pressure.asUInt7(), + 0); +} + +constexpr Midi1ChannelVoiceMessage makeMidi1PitchBendMessage (Group group, + Channel channel, + PitchBend pb) +{ + const auto pb14 = pb.asUInt14(); + return makeMidi1ChannelVoiceMessage (group, + Status (Midi1ChannelVoiceStatus::pitch_bend), + channel, + uint7_t (pb14 & 0x7f), + uint7_t ((pb14 >> 7) & 0x7f)); +} + +//============================================================================== +struct Midi2ChannelVoiceMessage : UniversalPacket +{ + constexpr Midi2ChannelVoiceMessage() = default; + + constexpr Midi2ChannelVoiceMessage (Group group, + Status status, + Channel channel, + uint8_t byte3, + uint8_t byte4, + uint32_t data) + : UniversalPacket (0x40000000u | ((group & 0x0f) << 24u) + | (uint32_t (Status ((status & 0xf0) | (channel & 0x0f))) << 16u) + | (uint32_t (byte3) << 8u) | byte4, + data) + { + } + + constexpr explicit Midi2ChannelVoiceMessage (const UniversalPacket& p) + : UniversalPacket (p) + { + jassert (p.getType() == PacketType::midi2_channel_voice); + } +}; + +constexpr bool isMidi2ChannelVoiceMessage (const UniversalPacket& p) +{ + return p.getType() == PacketType::midi2_channel_voice; +} + +struct Midi2ChannelVoiceMessageView +{ + constexpr explicit Midi2ChannelVoiceMessageView (const UniversalPacket& ump) + : p (ump) + { + jassert (p.getType() == PacketType::midi2_channel_voice); + } + + constexpr Group getGroup() const { return p.getGroup(); } + + constexpr Status getStatus() const { return Status (p.getStatus() & 0xf0); } + + constexpr Channel getChannel() const { return Channel (p.getStatus() & 0x0f); } + + constexpr uint7_t getByte3() const { return uint7_t (p.getByte3() & 0x7f); } + + constexpr uint7_t getByte4() const { return uint7_t (p.getByte4() & 0x7f); } + + constexpr uint32_t getData() const { return p.data[1]; } + +private: + const UniversalPacket& p; +}; + +constexpr std::optional asMidi2ChannelVoiceMessageView (const UniversalPacket& p) +{ + if (isMidi2ChannelVoiceMessage (p)) + return Midi2ChannelVoiceMessageView { p }; + + return std::nullopt; +} + +using NoteManagementFlags = uint8_t; + +namespace NoteManagement +{ +constexpr uint8_t reset = 0x1; +constexpr uint8_t detach = 0x2; +constexpr uint8_t detach_and_reset = 0x3; +} // namespace NoteManagement + +namespace NoteAttribute +{ +constexpr uint8_t none = 0x0; +constexpr uint8_t manufacturer_specific = 0x1; +constexpr uint8_t profile_specific = 0x2; +constexpr uint8_t pitch_7_9 = 0x3; +} // namespace NoteAttribute + +constexpr Midi2ChannelVoiceMessage makeMidi2ChannelVoiceMessage (Group group, + Status status, + Channel channel, + uint7_t index1, + uint7_t index2, + uint32_t data) +{ + return Midi2ChannelVoiceMessage { group, status, channel, index1, index2, data }; +} + +constexpr Midi2ChannelVoiceMessage makeMidi2NoteOffMessage (Group group, + Channel channel, + NoteNumber noteNr, + Velocity vel, + uint8_t attribute = 0, + uint16_t attributeData = 0) +{ + return Midi2ChannelVoiceMessage { group, + Status (ChannelVoiceStatus::note_off), + channel, + noteNr, + attribute, + uint32_t ((uint32_t (vel.value) << 16u) | attributeData) }; +} + +constexpr Midi2ChannelVoiceMessage makeMidi2NoteOnMessage (Group group, + Channel channel, + NoteNumber noteNr, + Velocity vel) +{ + return Midi2ChannelVoiceMessage { group, + Status (ChannelVoiceStatus::note_on), + channel, + noteNr, + 0, + uint32_t (vel.value) << 16u }; +} + +constexpr Midi2ChannelVoiceMessage makeMidi2NoteOnMessage (Group group, + Channel channel, + NoteNumber noteNr, + Velocity vel, + Pitch7_9 pitch) +{ + return Midi2ChannelVoiceMessage { group, + Status (ChannelVoiceStatus::note_on), + channel, + noteNr, + NoteAttribute::pitch_7_9, + uint32_t ((uint32_t (vel.value) << 16u) | pitch.value) }; +} + +constexpr Midi2ChannelVoiceMessage makeMidi2NoteOnMessage (Group group, + Channel channel, + NoteNumber noteNr, + Velocity vel, + uint8_t attribute, + uint16_t attributeData) +{ + return Midi2ChannelVoiceMessage { group, + Status (ChannelVoiceStatus::note_on), + channel, + noteNr, + attribute, + uint32_t ((uint32_t (vel.value) << 16u) | attributeData) }; +} + +constexpr Midi2ChannelVoiceMessage makeMidi2PolyPressureMessage (Group group, + Channel channel, + NoteNumber noteNr, + ControllerValue pressure) +{ + return Midi2ChannelVoiceMessage { group, + Status (ChannelVoiceStatus::poly_pressure), + channel, + noteNr, + 0, + pressure.value }; +} + +constexpr Midi2ChannelVoiceMessage makeRegisteredPerNoteControllerMessage (Group group, + Channel channel, + NoteNumber noteNr, + uint8_t controller, + ControllerValue value) +{ + return Midi2ChannelVoiceMessage { group, + Status (ChannelVoiceStatus::registered_per_note_controller), + channel, + noteNr, + controller, + value.value }; +} + +constexpr Midi2ChannelVoiceMessage makeAssignablePerNoteControllerMessage (Group group, + Channel channel, + NoteNumber noteNr, + uint8_t controller, + ControllerValue value) +{ + return Midi2ChannelVoiceMessage { group, + Status (ChannelVoiceStatus::assignable_per_note_controller), + channel, + noteNr, + controller, + value.value }; +} + +constexpr Midi2ChannelVoiceMessage makePerNoteManagementMessage (Group group, + Channel channel, + NoteNumber noteNr, + NoteManagementFlags flags) +{ + return Midi2ChannelVoiceMessage { group, + Status (ChannelVoiceStatus::per_note_management), + channel, + noteNr, + flags, + 0 }; +} + +constexpr Midi2ChannelVoiceMessage makeMidi2ControlChangeMessage (Group group, + Channel channel, + uint7_t controller, + ControllerValue value) +{ + return Midi2ChannelVoiceMessage { group, + Status (ChannelVoiceStatus::control_change), + channel, + controller, + 0, + value.value }; +} + +constexpr Midi2ChannelVoiceMessage makeRegisteredControllerMessage (Group group, + Channel channel, + uint7_t bank, + uint7_t index, + ControllerValue value) +{ + return Midi2ChannelVoiceMessage { group, + Status (ChannelVoiceStatus::registered_controller), + channel, + bank, + index, + value.value }; +} + +constexpr Midi2ChannelVoiceMessage makeAssignableControllerMessage (Group group, + Channel channel, + uint7_t bank, + uint7_t index, + ControllerValue value) +{ + return Midi2ChannelVoiceMessage { group, + Status (ChannelVoiceStatus::assignable_controller), + channel, + bank, + index, + value.value }; +} + +constexpr Midi2ChannelVoiceMessage makeRelativeRegisteredControllerMessage (Group group, + Channel channel, + uint7_t bank, + uint7_t index, + ControllerIncrement inc) +{ + return Midi2ChannelVoiceMessage { group, + Status (ChannelVoiceStatus::relative_registered_controller), + channel, + bank, + index, + uint32_t (inc.value) }; +} + +constexpr Midi2ChannelVoiceMessage makeRelativeAssignableControllerMessage (Group group, + Channel channel, + uint7_t bank, + uint7_t index, + ControllerIncrement inc) +{ + return Midi2ChannelVoiceMessage { group, + Status (ChannelVoiceStatus::relative_assignable_controller), + channel, + bank, + index, + uint32_t (inc.value) }; +} + +constexpr Midi2ChannelVoiceMessage makeMidi2ProgramChangeMessage (Group group, + Channel channel, + ProgramNumber program) +{ + return Midi2ChannelVoiceMessage { group, + Status (ChannelVoiceStatus::program_change), + channel, + 0, + 0, + uint32_t ((program & 0x7f) << 24u) }; +} + +constexpr Midi2ChannelVoiceMessage makeMidi2ProgramChangeMessage (Group group, + Channel channel, + ProgramNumber program, + uint14_t bank) +{ + return Midi2ChannelVoiceMessage { group, + Status (ChannelVoiceStatus::program_change), + channel, + 0, + 1, + uint32_t (((program & 0x7f) << 24u) | ((bank & 0x3f80u) << 1u) | (bank & 0x7fu)) }; +} + +constexpr Midi2ChannelVoiceMessage makeMidi2ChannelPressureMessage (Group group, + Channel channel, + ControllerValue pressure) +{ + return Midi2ChannelVoiceMessage { group, + Status (ChannelVoiceStatus::channel_pressure), + channel, + 0, + 0, + pressure.value }; +} + +constexpr Midi2ChannelVoiceMessage makeMidi2PitchBendMessage (Group group, + Channel channel, + PitchBend bend) +{ + return Midi2ChannelVoiceMessage { group, + Status (ChannelVoiceStatus::pitch_bend), + channel, + 0, + 0, + bend.value }; +} + +constexpr Midi2ChannelVoiceMessage makePerNotePitchBendMessage (Group group, + Channel channel, + NoteNumber noteNr, + PitchBend bend) +{ + return Midi2ChannelVoiceMessage { group, + Status (ChannelVoiceStatus::per_note_pitch_bend), + channel, + noteNr, + 0, + bend.value }; +} + +} // namespace yup::ump + +#endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.cpp b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.cpp new file mode 100644 index 000000000..07aadc7b9 --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.cpp @@ -0,0 +1,321 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup::ump +{ + +void Midi1ByteStreamParser::feed (uint8_t byte) +{ + if (byte >= uint8_t (SystemStatus::clock)) + { + systemRealtime (byte); + return; + } + + if (packet.getType() == PacketType::data) + { + if (byte < 0x80) + { + if (hasSysExCallback()) + sysExContinueCallback (byte); + else + sysExContinuePacket (byte); + + return; + } + + if (byte != 0xf7) + { + packet = UniversalPacket {}; + } + else + { + if (hasSysExCallback()) + sysExEndCallback(); + else + sysExEndPacket(); + + return; + } + } + + if (byte == 0xf0) + { + sysExStart(); + } + else if (byte > 0xf0) + { + systemCommon (byte); + } + else if (byte >= 0x80) + { + channelVoice (byte); + } + else if (packet.getStatus() && numMissingBytes && (packetByte < 4)) + { + packet.setByte (packetByte, byte); + --numMissingBytes; + if (numMissingBytes == 0) + { + if (packet.getStatus() < 0xf0) + { + numMissingBytes = packetByte - 1; + packetByte = 2; + } + + if (invokeCallbacks) + packetCallback (packet); + + if (packet.getStatus() >= 0xf0) + packet = UniversalPacket {}; + } + else + { + ++packetByte; + } + } +} + +void Midi1ByteStreamParser::feed (const uint8_t* begin, const uint8_t* end) +{ + for (auto it = begin; it < end; ++it) + feed (*it); +} + +void Midi1ByteStreamParser::reset() +{ + packet = UniversalPacket {}; + sysex.clear(); + + packetByte = 1; + numMissingBytes = 0; +} + +void Midi1ByteStreamParser::systemRealtime (uint8_t byte) +{ + if ((byte == 0xf9) || (byte == 0xfd)) + return; + + if (invokeCallbacks) + packetCallback (makeSystemMessage (group, Status (byte))); +} + +void Midi1ByteStreamParser::systemCommon (uint8_t byte) +{ + packet = makeSystemMessage (group, Status (byte)); + packetByte = 2; + numMissingBytes = 0; + + switch (packet.getStatus()) + { + case Status (SystemStatus::mtc_quarter_frame): + case Status (SystemStatus::song_select): + numMissingBytes = 1; + break; + case Status (SystemStatus::song_position): + numMissingBytes = 2; + break; + case Status (SystemStatus::tune_request): + break; + default: + packet = UniversalPacket {}; + return; + } + + if (numMissingBytes == 0) + { + if (invokeCallbacks) + packetCallback (packet); + + packet = UniversalPacket {}; + } +} + +void Midi1ByteStreamParser::channelVoice (uint8_t byte) +{ + packet = makeMidi1ChannelVoiceMessage (group, Status (byte)); + packetByte = 2; + numMissingBytes = 0; + + switch (packet.getStatus() & 0xf0) + { + case Status (Midi1ChannelVoiceStatus::note_off): + case Status (Midi1ChannelVoiceStatus::note_on): + case Status (Midi1ChannelVoiceStatus::poly_pressure): + case Status (Midi1ChannelVoiceStatus::control_change): + case Status (Midi1ChannelVoiceStatus::pitch_bend): + numMissingBytes = 2; + break; + case Status (Midi1ChannelVoiceStatus::program_change): + case Status (Midi1ChannelVoiceStatus::channel_pressure): + numMissingBytes = 1; + break; + default: + break; + } + + if (numMissingBytes == 0 && invokeCallbacks) + packetCallback (packet); +} + +void Midi1ByteStreamParser::sysExStart() +{ + packet = makeSysEx7StartPacket (group); + packetByte = 2; + numMissingBytes = 0; + + if (hasSysExCallback()) + { + sysex.clear(); + if (sysex.data.capacity() < 1024) + sysex.data.reserve (1024); + + if (! invokeCallbacks) + packetByte = 0; + } +} + +void Midi1ByteStreamParser::sysExContinueCallback (uint8_t byte) +{ + switch (packetByte) + { + case 0: + case 1: + return; + case 2: + sysex.manufacturerId = (byte << 16); + ++packetByte; + return; + case 3: + case 4: + if ((sysex.manufacturerId & 0xff0000) == 0) + { + sysex.manufacturerId += (byte << ((4 - packetByte) * 8)); + ++packetByte; + return; + } + break; + default: + break; + } + + if (sysex.data.size() == sysex.data.capacity()) + sysex.data.reserve (sysex.data.capacity() * 2); + + sysex.data.push_back (byte); +} + +void Midi1ByteStreamParser::sysExEndCallback() +{ + packet = UniversalPacket {}; + + if (((sysex.manufacturerId & 0xff0000) == 0) && (packetByte < 5)) + return; + + if (invokeCallbacks) + sysexCallback (sysex); + + sysex.clear(); +} + +void Midi1ByteStreamParser::sysExContinuePacket (uint8_t byte) +{ + packet.setByte (packetByte, byte); + if (++packetByte == 8) + { + if (invokeCallbacks) + { + packet.setByte (1, uint8_t ((packet.getStatus() & 0xf0) + 6)); + packetCallback (packet); + } + + packet = makeSysEx7ContinuePacket (group); + packetByte = 2; + } +} + +void Midi1ByteStreamParser::sysExEndPacket() +{ + const auto currentSysExStatus = uint8_t (packet.getStatus() & 0xf0); + const auto currentPacketSize = uint8_t ((packetByte - 2) & 0x0f); + + if (currentSysExStatus == uint8_t (DataStatus::sysex7_start)) + { + if (packetByte < 3) + { + packet = UniversalPacket {}; + return; + } + + packet.setByte (1, uint8_t (DataStatus::sysex7_complete) + currentPacketSize); + } + else + { + packet.setByte (1, uint8_t (DataStatus::sysex7_end) + currentPacketSize); + } + + if (invokeCallbacks) + packetCallback (packet); + + packet = UniversalPacket {}; + packetByte = 2; +} + +size_t toMidi1ByteStream (const UniversalPacket& packet, uint8_t result[8]) +{ + const auto payloadBytes = getMidi1ByteStreamSize (packet); + size_t resultBytes = 0; + + switch (packet.getType()) + { + case PacketType::system: + case PacketType::midi1_channel_voice: + if (payloadBytes > 0) + { + for (; resultBytes < payloadBytes; ++resultBytes) + result[resultBytes] = packet.getByte (1 + resultBytes); + } + break; + case PacketType::data: + if (isSysEx7Packet (packet)) + { + const auto status = packet.getStatus() & 0xf0; + + if ((status == Status (DataStatus::sysex7_complete)) + || (status == Status (DataStatus::sysex7_start))) + result[resultBytes++] = 0xf0; + + for (size_t b = 0; b < payloadBytes; ++b) + result[resultBytes++] = packet.getByte (2 + b); + + if ((status == Status (DataStatus::sysex7_complete)) + || (status == Status (DataStatus::sysex7_end))) + result[resultBytes++] = 0xf7; + } + break; + default: + return 0; + } + + return resultBytes; +} + +} // namespace yup::ump diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.h b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.h new file mode 100644 index 000000000..b080cb3d8 --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.h @@ -0,0 +1,211 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifndef DOXYGEN + +namespace yup::ump +{ + +class Midi1ByteStreamParser +{ +public: + using PacketCallback = std::function; + using SysExCallback = std::function; + + explicit Midi1ByteStreamParser (PacketCallback packetCallback, + SysExCallback sysexCallback = {}, + bool enableCallbacks = true); + + Midi1ByteStreamParser (Group group, + PacketCallback packetCallback, + SysExCallback sysexCallback = {}, + bool enableCallbacks = true); + + bool callbacksEnabled() const { return invokeCallbacks; } + + void enableCallbacks (bool enable) { invokeCallbacks = enable; } + + Group getGroup() const; + void setGroup (Group group); + + void feed (uint8_t byte); + void feed (const uint8_t* data, size_t numBytes); + void feed (const uint8_t* begin, const uint8_t* end); + + void reset(); + +protected: + void systemRealtime (uint8_t byte); + void systemCommon (uint8_t byte); + void channelVoice (uint8_t byte); + + bool hasSysExCallback() const { return static_cast (sysexCallback); } + + void sysExStart(); + void sysExContinueCallback (uint8_t byte); + void sysExEndCallback(); + void sysExContinuePacket (uint8_t byte); + void sysExEndPacket(); + +private: + Group group { 0 }; + PacketCallback packetCallback; + SysExCallback sysexCallback; + bool invokeCallbacks { true }; + + UniversalPacket packet; + SysEx7 sysex; + + uint8_t packetByte { 0 }; + uint8_t numMissingBytes { 0 }; +}; + +constexpr UniversalPacket fromMidi1ByteStream (uint8_t status, uint7_t data1, uint7_t data2); + +constexpr size_t getMidi1ByteStreamSize (const UniversalPacket& packet); + +size_t toMidi1ByteStream (const UniversalPacket& packet, uint8_t bytes[8]); + +inline Midi1ByteStreamParser::Midi1ByteStreamParser (PacketCallback packetCallbackIn, + SysExCallback sysexCallbackIn, + bool enableCallbacks) + : packetCallback (std::move (packetCallbackIn)) + , sysexCallback (std::move (sysexCallbackIn)) + , invokeCallbacks (enableCallbacks) +{ +} + +inline Midi1ByteStreamParser::Midi1ByteStreamParser (Group groupIn, + PacketCallback packetCallbackIn, + SysExCallback sysexCallbackIn, + bool enableCallbacks) + : group (groupIn) + , packetCallback (std::move (packetCallbackIn)) + , sysexCallback (std::move (sysexCallbackIn)) + , invokeCallbacks (enableCallbacks) +{ +} + +inline Group Midi1ByteStreamParser::getGroup() const +{ + return group; +} + +inline void Midi1ByteStreamParser::setGroup (Group groupIn) +{ + group = groupIn; +} + +inline void Midi1ByteStreamParser::feed (const uint8_t* data, size_t numBytes) +{ + feed (data, data + numBytes); +} + +constexpr size_t getMidi1ByteStreamSize (const UniversalPacket& packet) +{ + switch (packet.getType()) + { + case PacketType::system: + switch (SystemStatus (packet.getStatus())) + { + case SystemStatus::song_position: + return 3u; + case SystemStatus::mtc_quarter_frame: + case SystemStatus::song_select: + return 2u; + case SystemStatus::tune_request: + case SystemStatus::clock: + case SystemStatus::start: + case SystemStatus::cont: + case SystemStatus::stop: + case SystemStatus::active_sense: + case SystemStatus::reset: + return 1u; + } + break; + case PacketType::midi1_channel_voice: + switch (packet.getStatus() & 0xf0) + { + case Status (Midi1ChannelVoiceStatus::note_off): + case Status (Midi1ChannelVoiceStatus::note_on): + case Status (Midi1ChannelVoiceStatus::poly_pressure): + case Status (Midi1ChannelVoiceStatus::control_change): + case Status (Midi1ChannelVoiceStatus::pitch_bend): + return 3u; + case Status (Midi1ChannelVoiceStatus::program_change): + case Status (Midi1ChannelVoiceStatus::channel_pressure): + return 2u; + } + break; + case PacketType::data: + switch (packet.getStatus() & 0xf0) + { + case Status (DataStatus::sysex7_complete): + case Status (DataStatus::sysex7_start): + case Status (DataStatus::sysex7_end): + case Status (DataStatus::sysex7_continue): + if ((packet.getStatus() & 0x0f) <= 6) + return packet.getStatus() & 0x0f; + break; + } + break; + default: + break; + } + + return 0u; +} + +constexpr UniversalPacket fromMidi1ByteStream (uint8_t status, uint7_t data1, uint7_t data2) +{ + auto type = PacketType::midi1_channel_voice; + + if ((status & 0xf0) == 0xf0) + { + type = PacketType::system; + + switch (status) + { + case 0xf0: + case 0xf7: + case 0xf4: + case 0xf5: + case 0xf9: + case 0xfd: + return {}; + default: + break; + } + } + else if (status < 0x80) + { + return {}; + } + + return UniversalPacket { uint32_t ((uint32_t (type) << 28) + | (uint32_t (status) << 16u) + | (uint32_t (data1) << 8u) + | uint32_t (data2)) }; +} + +} // namespace yup::ump + +#endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPStreamMessages.h b/modules/yup_audio_basics/midi/ump/yup_UMPStreamMessages.h new file mode 100644 index 000000000..1a1a1d640 --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPStreamMessages.h @@ -0,0 +1,608 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifndef DOXYGEN + +namespace yup::ump +{ + +struct StreamMessage : UniversalPacket +{ + constexpr StreamMessage() + : UniversalPacket (0xF0000000u) + { + } + + constexpr explicit StreamMessage (Status status, PacketFormat format = PacketFormat::complete) + : UniversalPacket (0xF0000000u | (uint32_t (format) << 26u) | (uint32_t (status) << 16u)) + { + } + + constexpr PacketFormat getFormat() const { return PacketFormat ((data[0] >> 26u) & 0x03u); } + + constexpr void setFormat (PacketFormat format) + { + data[0] = (data[0] & 0xF3FFFFFFu) | (uint32_t (format) << 26u); + } + + static std::string getPayloadAsString (const UniversalPacket& p, uint8_t offset) + { + std::string result; + result.reserve (16 - offset); + for (uint8_t b = offset; b < 16; ++b) + { + if (const auto c = char (p.getByte7Bit (b))) + result.push_back (c); + else + break; + } + return result; + } +}; + +constexpr bool isStreamMessage (const UniversalPacket& p) +{ + return p.getType() == PacketType::stream; +} + +namespace StreamDiscoveryFilter +{ +constexpr uint8_t endpointInfo = 0b00001; +constexpr uint8_t deviceIdentity = 0b00010; +constexpr uint8_t endpointName = 0b00100; +constexpr uint8_t productInstanceId = 0b01000; +constexpr uint8_t streamConfiguration = 0b10000; +constexpr uint8_t endpointAll = 0b11111; + +constexpr uint8_t functionBlockInfo = 0b01; +constexpr uint8_t functionBlockName = 0b10; +constexpr uint8_t functionBlockAll = 0b11; +} // namespace StreamDiscoveryFilter + +struct EndpointDiscoveryView +{ + constexpr explicit EndpointDiscoveryView (const UniversalPacket& ump) + : p (ump) + { + jassert (p.getType() == PacketType::stream); + jassert (StreamStatus (p.getStatus()) == StreamStatus::endpoint_discovery); + } + + constexpr uint8_t getUmpVersionMajor() const { return p.getByte3(); } + + constexpr uint8_t getUmpVersionMinor() const { return p.getByte4(); } + + constexpr uint16_t getUmpVersion() const { return uint16_t (p.data[0] & 0xffffu); } + + constexpr uint8_t getFilter() const { return uint8_t (p.data[1] & 0b11111); } + + constexpr bool requestsInfo() const { return (getFilter() & StreamDiscoveryFilter::endpointInfo) != 0; } + + constexpr bool requestsDeviceIdentity() const { return (getFilter() & StreamDiscoveryFilter::deviceIdentity) != 0; } + + constexpr bool requestsName() const { return (getFilter() & StreamDiscoveryFilter::endpointName) != 0; } + + constexpr bool requestsProductInstanceId() const { return (getFilter() & StreamDiscoveryFilter::productInstanceId) != 0; } + + constexpr bool requestsStreamConfiguration() const { return (getFilter() & StreamDiscoveryFilter::streamConfiguration) != 0; } + +private: + const UniversalPacket& p; +}; + +constexpr std::optional asEndpointDiscoveryView (const UniversalPacket& p) +{ + if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::endpoint_discovery) + return EndpointDiscoveryView { p }; + + return std::nullopt; +} + +struct EndpointInfoView +{ + constexpr explicit EndpointInfoView (const UniversalPacket& ump) + : p (ump) + { + jassert (p.getType() == PacketType::stream); + jassert (StreamStatus (p.getStatus()) == StreamStatus::endpoint_info); + } + + constexpr uint8_t getUmpVersionMajor() const { return p.getByte3(); } + + constexpr uint8_t getUmpVersionMinor() const { return p.getByte4(); } + + constexpr uint16_t getUmpVersion() const { return uint16_t (p.data[0] & 0xffffu); } + + constexpr uint8_t getNumFunctionBlocks() const { return p.getByte (4) & 0x7f; } + + constexpr bool hasStaticFunctionBlocks() const { return (p.getByte (4) & 0x80u) != 0; } + + constexpr uint8_t getProtocols() const { return p.getByte (6) & 0x3; } + + constexpr uint8_t getExtensions() const { return p.getByte (7) & 0x3; } + +private: + const UniversalPacket& p; +}; + +constexpr std::optional asEndpointInfoView (const UniversalPacket& p) +{ + if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::endpoint_info) + return EndpointInfoView { p }; + + return std::nullopt; +} + +struct DeviceIdentityView +{ + constexpr explicit DeviceIdentityView (const UniversalPacket& ump) + : p (ump) + { + jassert (p.getType() == PacketType::stream); + jassert (StreamStatus (p.getStatus()) == StreamStatus::device_identity); + } + + constexpr DeviceIdentity getIdentity() const + { + return { + p.data[1] & 0x007f7f7fu, + uint14_t (((p.data[2] >> 24) & 0x7fu) | ((p.data[2] >> 9) & 0x3f80u)), + uint14_t (((p.data[2] >> 8) & 0x7fu) | ((p.data[2] << 7) & 0x3f80u)), + uint28_t (((p.data[3] >> 24) & 0x000007fu) + | ((p.data[3] >> 9) & 0x0003f80u) + | ((p.data[3] << 6) & 0x1fc000u) + | ((p.data[3] << 21) & 0xfe00000u)) + }; + } + +private: + const UniversalPacket& p; +}; + +constexpr std::optional asDeviceIdentityView (const UniversalPacket& p) +{ + if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::device_identity) + return DeviceIdentityView { p }; + + return std::nullopt; +} + +struct EndpointNameView +{ + constexpr explicit EndpointNameView (const UniversalPacket& ump) + : p (ump) + { + jassert (p.getType() == PacketType::stream); + jassert (StreamStatus (p.getStatus()) == StreamStatus::endpoint_name); + } + + constexpr PacketFormat getFormat() const { return PacketFormat ((p.data[0] >> 26u) & 0x3u); } + + std::string getPayload() const { return StreamMessage::getPayloadAsString (p, 2); } + +private: + const UniversalPacket& p; +}; + +constexpr std::optional asEndpointNameView (const UniversalPacket& p) +{ + if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::endpoint_name) + return EndpointNameView { p }; + + return std::nullopt; +} + +struct ProductInstanceIdView +{ + constexpr explicit ProductInstanceIdView (const UniversalPacket& ump) + : p (ump) + { + jassert (p.getType() == PacketType::stream); + jassert (StreamStatus (p.getStatus()) == StreamStatus::product_instance_id); + } + + constexpr PacketFormat getFormat() const { return PacketFormat ((p.data[0] >> 26u) & 0x3u); } + + std::string getPayload() const { return StreamMessage::getPayloadAsString (p, 2); } + +private: + const UniversalPacket& p; +}; + +constexpr std::optional asProductInstanceIdView (const UniversalPacket& p) +{ + if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::product_instance_id) + return ProductInstanceIdView { p }; + + return std::nullopt; +} + +struct StreamConfigurationView +{ + constexpr explicit StreamConfigurationView (const UniversalPacket& ump) + : p (ump) + { + jassert (p.getType() == PacketType::stream); + jassert (StreamStatus (p.getStatus()) == StreamStatus::stream_configuration_request + || StreamStatus (p.getStatus()) == StreamStatus::stream_configuration_notify); + } + + constexpr Protocol getProtocol() const { return p.getByte3() & 0x3; } + + constexpr Extensions getExtensions() const { return p.getByte4() & 0x3; } + +private: + const UniversalPacket& p; +}; + +constexpr std::optional asStreamConfigurationView (const UniversalPacket& p) +{ + if (! isStreamMessage (p)) + return std::nullopt; + + const auto status = StreamStatus (p.getStatus()); + if (status == StreamStatus::stream_configuration_request || status == StreamStatus::stream_configuration_notify) + return StreamConfigurationView { p }; + + return std::nullopt; +} + +struct FunctionBlockDiscoveryView +{ + constexpr explicit FunctionBlockDiscoveryView (const UniversalPacket& ump) + : p (ump) + { + jassert (p.getType() == PacketType::stream); + jassert (StreamStatus (p.getStatus()) == StreamStatus::function_block_discovery); + } + + constexpr uint8_t getFunctionBlock() const { return p.getByte3(); } + + constexpr uint8_t getFilter() const { return p.getByte4() & 0x0f; } + + constexpr bool requestsFunctionBlock (uint8_t block) const + { + constexpr uint8_t allBlocks = 0xff; + return getFunctionBlock() == allBlocks || getFunctionBlock() == block; + } + + constexpr bool requestsInfo() const { return (getFilter() & StreamDiscoveryFilter::functionBlockInfo) != 0; } + + constexpr bool requestsName() const { return (getFilter() & StreamDiscoveryFilter::functionBlockName) != 0; } + +private: + const UniversalPacket& p; +}; + +constexpr std::optional asFunctionBlockDiscoveryView (const UniversalPacket& p) +{ + if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::function_block_discovery) + return FunctionBlockDiscoveryView { p }; + + return std::nullopt; +} + +struct FunctionBlockOptions +{ + bool active = true; + + static constexpr uint2_t directionInput = 0b01; + static constexpr uint2_t directionOutput = 0b10; + static constexpr uint2_t bidirectional = 0b11; + + uint2_t direction = bidirectional; + + static constexpr uint2_t notMidi1 = 0b00; + static constexpr uint2_t midi1Unrestricted = 0b01; + static constexpr uint2_t midi1_31250 = 0b10; + + uint2_t midi1 = notMidi1; + + static constexpr uint2_t uiHintAsDirection = 0b00; + static constexpr uint2_t uiHintReceiver = 0b01; + static constexpr uint2_t uiHintSender = 0b10; + + uint2_t uiHint = uiHintAsDirection; + + uint8_t ciMessageVersion = 0x00; + uint8_t maxNumSysEx8Streams = 0; +}; + +struct FunctionBlockInfoView +{ + constexpr explicit FunctionBlockInfoView (const UniversalPacket& ump) + : p (ump) + { + jassert (p.getType() == PacketType::stream); + jassert (StreamStatus (p.getStatus()) == StreamStatus::function_block_info); + } + + constexpr bool isActive() const { return (p.data[0] & 0x00008000u) != 0; } + + constexpr uint8_t getFunctionBlock() const { return p.getByte7Bit (2); } + + constexpr uint8_t getDirection() const { return uint8_t (p.data[0] & 0x3); } + + constexpr uint8_t getMidi1() const { return uint8_t ((p.data[0] >> 2) & 0x3); } + + constexpr uint8_t getUiHint() const { return uint8_t ((p.data[0] >> 4) & 0x3); } + + constexpr uint8_t getFirstGroup() const { return p.getByte (4); } + + constexpr uint8_t getNumGroupsSpanned() const { return p.getByte (5); } + + constexpr uint7_t getCiMessageVersion() const { return p.getByte (6); } + + constexpr uint8_t getMaxNumSysEx8Streams() const { return p.getByte (7); } + +private: + const UniversalPacket& p; +}; + +constexpr std::optional asFunctionBlockInfoView (const UniversalPacket& p) +{ + if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::function_block_info) + return FunctionBlockInfoView { p }; + + return std::nullopt; +} + +struct FunctionBlockNameView +{ + constexpr explicit FunctionBlockNameView (const UniversalPacket& ump) + : p (ump) + { + jassert (p.getType() == PacketType::stream); + jassert (StreamStatus (p.getStatus()) == StreamStatus::function_block_name); + } + + constexpr PacketFormat getFormat() const { return PacketFormat ((p.data[0] >> 26u) & 0x3u); } + + constexpr uint8_t getFunctionBlock() const { return p.getByte3() & 0x7f; } + + std::string getPayload() const { return StreamMessage::getPayloadAsString (p, 3); } + +private: + const UniversalPacket& p; +}; + +constexpr std::optional asFunctionBlockNameView (const UniversalPacket& p) +{ + if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::function_block_name) + return FunctionBlockNameView { p }; + + return std::nullopt; +} + +//============================================================================== +constexpr StreamMessage makeEndpointDiscoveryMessage (uint8_t filter, + uint8_t umpVersionMajor = 1, + uint8_t umpVersionMinor = 1) +{ + StreamMessage message { Status (StreamStatus::endpoint_discovery), PacketFormat::complete }; + message.setByte (2, umpVersionMajor); + message.setByte (3, umpVersionMinor); + message.data[1] = filter; + return message; +} + +constexpr StreamMessage makeEndpointInfoMessage (uint8_t numFunctionBlocks, + bool staticFunctionBlocks, + uint8_t protocols, + uint8_t extensions, + uint8_t umpVersionMajor = 1, + uint8_t umpVersionMinor = 1) +{ + StreamMessage message { Status (StreamStatus::endpoint_info), PacketFormat::complete }; + message.setByte (2, umpVersionMajor); + message.setByte (3, umpVersionMinor); + message.setByte (4, uint8_t ((staticFunctionBlocks ? 0x80u : 0x00u) | numFunctionBlocks)); + message.setByte (6, protocols); + message.setByte (7, extensions); + return message; +} + +constexpr StreamMessage makeDeviceIdentityMessage (const DeviceIdentity& identity) +{ + StreamMessage message { Status (StreamStatus::device_identity), PacketFormat::complete }; + message.data[1] = identity.manufacturer; + message.data[2] = ((uint32_t (identity.family) << 24) & 0x7F000000u) + | ((uint32_t (identity.family) << 9) & 0x007F0000u) + | ((uint32_t (identity.model) << 8) & 0x00007F00u) + | ((identity.model >> 7) & 0x0000007Fu); + message.data[3] = ((identity.revision << 24) & 0x7F000000u) + | ((identity.revision << 9) & 0x007F0000u) + | ((identity.revision >> 6) & 0x00007F00u) + | ((identity.revision >> 21) & 0x0000007Fu); + return message; +} + +constexpr StreamMessage makeEndpointNameMessage (PacketFormat format, const std::string_view& name) +{ + jassert (name.length() <= 14); + StreamMessage message { Status (StreamStatus::endpoint_name), format }; + uint8_t byte = 2; + for (const auto c : name) + { + message.setByte (byte, uint8_t (c)); + if (++byte >= 16) + break; + } + return message; +} + +constexpr StreamMessage makeProductInstanceIdMessage (PacketFormat format, const std::string_view& name) +{ + jassert (name.length() <= 14); + jassert (format != PacketFormat::cont); + StreamMessage message { Status (StreamStatus::product_instance_id), format }; + uint8_t byte = 2; + for (const auto c : name) + { + message.setByte7Bit (byte, uint8_t (c)); + if (++byte >= 16) + break; + } + return message; +} + +constexpr StreamMessage makeStreamConfigurationRequest (Protocol protocol, Extensions extensions = 0) +{ + jassert (protocol && protocol < 0x3); + StreamMessage message { Status (StreamStatus::stream_configuration_request), PacketFormat::complete }; + message.setByte (2, protocol); + message.setByte (3, extensions); + return message; +} + +constexpr StreamMessage makeStreamConfigurationNotification (Protocol protocol, Extensions extensions = 0) +{ + jassert (protocol && protocol < 0x3); + StreamMessage message { Status (StreamStatus::stream_configuration_notify), PacketFormat::complete }; + message.setByte (2, protocol); + message.setByte (3, extensions); + return message; +} + +constexpr StreamMessage makeFunctionBlockDiscoveryMessage (uint8_t functionBlock, uint8_t filter) +{ + jassert (functionBlock == 0xff || functionBlock < 32); + StreamMessage message { Status (StreamStatus::function_block_discovery), PacketFormat::complete }; + message.setByte (2, functionBlock); + message.setByte (3, filter); + return message; +} + +constexpr StreamMessage makeFunctionBlockInfoMessage (uint7_t functionBlock, + uint4_t direction, + Group firstGroup, + uint4_t numGroupsSpanned = 1) +{ + jassert (functionBlock < 32); + jassert (direction > 0 && direction < 4); + StreamMessage message { Status (StreamStatus::function_block_info), PacketFormat::complete }; + message.setByte (2, uint8_t (0x80u | (functionBlock & 0x1f))); + message.setByte (3, uint8_t (((direction & 0x03u) << 4) | (direction & 0x03u))); + message.setByte (4, firstGroup & 0x0f); + message.setByte (5, numGroupsSpanned & 0x0f); + return message; +} + +constexpr StreamMessage makeFunctionBlockInfoMessage (uint7_t functionBlock, + const FunctionBlockOptions& options, + Group firstGroup, + uint4_t numGroupsSpanned = 1) +{ + jassert (functionBlock < 32); + jassert (options.direction > 0 && options.direction < 4); + jassert (options.midi1 < 3); + jassert (options.uiHint < 4); + jassert (options.uiHint == 0 || (options.direction & options.uiHint)); + + StreamMessage message { Status (StreamStatus::function_block_info), PacketFormat::complete }; + message.setByte (2, uint8_t ((options.active ? 0x80u : 0x00u) | (functionBlock & 0x1f))); + message.setByte (3, uint8_t ((((options.uiHint ? options.uiHint : options.direction) & 0x03u) << 4) | ((options.midi1 & 0x03u) << 2) | (options.direction & 0x03u))); + message.setByte (4, firstGroup & 0x0f); + message.setByte (5, numGroupsSpanned & 0x0f); + message.setByte (6, options.ciMessageVersion); + message.setByte (7, options.maxNumSysEx8Streams); + return message; +} + +constexpr StreamMessage makeFunctionBlockNameMessage (PacketFormat format, + uint7_t functionBlock, + const std::string_view& name) +{ + jassert (name.length() <= 13); + StreamMessage message { Status (StreamStatus::function_block_name), format }; + message.setByte (2, functionBlock); + + uint8_t byte = 3; + for (const auto c : name) + { + message.setByte (byte, uint8_t (c)); + if (++byte >= 16) + break; + } + return message; +} + +template +void sendEndpointName (std::string_view name, Sender&& sender) +{ + if (name.length() <= 14) + { + sender (makeEndpointNameMessage (PacketFormat::complete, name)); + return; + } + + sender (makeEndpointNameMessage (PacketFormat::start, name.substr (0, 14))); + name.remove_prefix (14); + + while (name.size() > 14) + { + sender (makeEndpointNameMessage (PacketFormat::cont, name.substr (0, 14))); + name.remove_prefix (14); + } + + sender (makeEndpointNameMessage (PacketFormat::end, name)); +} + +template +void sendProductInstanceId (std::string_view productInstanceId, Sender&& sender) +{ + jassert (productInstanceId.length() <= 16); + + if (productInstanceId.length() <= 14) + { + sender (makeProductInstanceIdMessage (PacketFormat::complete, productInstanceId)); + return; + } + + sender (makeProductInstanceIdMessage (PacketFormat::start, productInstanceId.substr (0, 14))); + productInstanceId.remove_prefix (14); + sender (makeProductInstanceIdMessage (PacketFormat::end, productInstanceId.substr (0, 2))); +} + +template +void sendFunctionBlockName (uint7_t functionBlock, std::string_view name, Sender&& sender) +{ + if (name.length() <= 13) + { + sender (makeFunctionBlockNameMessage (PacketFormat::complete, functionBlock, name)); + return; + } + + sender (makeFunctionBlockNameMessage (PacketFormat::start, functionBlock, name.substr (0, 13))); + name.remove_prefix (13); + + while (name.size() > 13) + { + sender (makeFunctionBlockNameMessage (PacketFormat::cont, functionBlock, name.substr (0, 13))); + name.remove_prefix (13); + } + + sender (makeFunctionBlockNameMessage (PacketFormat::end, functionBlock, name)); +} + +} // namespace yup::ump + +#endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPSysEx.h b/modules/yup_audio_basics/midi/ump/yup_UMPSysEx.h new file mode 100644 index 000000000..8b9203fe0 --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPSysEx.h @@ -0,0 +1,367 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifndef DOXYGEN + +namespace yup::ump +{ + +struct SysEx +{ + using DataType = std::vector; + + ManufacturerId manufacturerId { 0 }; + DataType data; + + SysEx() = default; + + explicit SysEx (ManufacturerId manufacturer) + : manufacturerId (manufacturer) + { + } + + SysEx (ManufacturerId manufacturer, size_t capacity) + : manufacturerId (manufacturer) + { + data.reserve (capacity); + } + + SysEx (ManufacturerId manufacturer, const uint8_t* buffer, size_t bufferSize) + : manufacturerId (manufacturer) + , data (buffer, buffer + bufferSize) + { + } + + SysEx (ManufacturerId manufacturer, DataType d) + : manufacturerId (manufacturer) + , data (std::move (d)) + { + } + + SysEx (ManufacturerId manufacturer, std::initializer_list d) + : manufacturerId (manufacturer) + , data (d.begin(), d.end()) + { + } + + size_t totalDataSize() const + { + if (manufacturerId == 0) + return data.size(); + + if ((manufacturerId & 0x00ffff) != 0) + return data.size() + 3u; + + return data.size() + 1u; + } + + bool isEmpty() const + { + return manufacturerId == 0 && data.empty(); + } + + bool is7Bit() const + { + for (const auto byte : data) + if (byte & 0x80) + return false; + + return true; + } + + bool is8Bit() const + { + for (const auto byte : data) + if (byte & 0x80) + return true; + + return false; + } + + bool operator== (const SysEx& other) const + { + return manufacturerId == other.manufacturerId && data == other.data; + } + + bool operator!= (const SysEx& other) const + { + return ! operator== (other); + } + + void clear() + { + manufacturerId = 0; + data.clear(); + + if (data.capacity() > 16384) + data.shrink_to_fit(); + } +}; + +struct SysEx7 : SysEx +{ + using SysEx::SysEx; + + enum class Kind : uint8_t + { + complete = 0, + begin = 1, + continuation = 2, + end = 3 + }; + + struct PacketBytes + { + std::array data; + uint8_t size; + }; + + static constexpr size_t uint7Max = (1u << 7) - 1; + static constexpr size_t uint14Max = (1u << 14) - 1; + static constexpr size_t uint28Max = (1u << 28) - 1; + + static uint32_t getNumPacketsRequiredForDataSize (uint32_t size) + { + constexpr uint32_t bytesPerPacket = 6; + return (size / bytesPerPacket) + ((size % bytesPerPacket) != 0); + } + + static PacketBytes getDataBytes (const PacketX2& packet) + { + const auto numBytes = Utils::getChannel (packet[0]); + constexpr uint8_t maxBytes = 6; + jassert (numBytes <= maxBytes); + + return { + { { std::byte { packet.getU8<2>() }, + std::byte { packet.getU8<3>() }, + std::byte { packet.getU8<4>() }, + std::byte { packet.getU8<5>() }, + std::byte { packet.getU8<6>() }, + std::byte { packet.getU8<7>() } } }, + jmin (numBytes, maxBytes) + }; + } + + bool isValid() const { return ((manufacturerId & 0xff808080u) == 0) && is7Bit(); } + + bool operator== (const SysEx7& other) const { return SysEx::operator== (other); } + + bool operator!= (const SysEx7& other) const { return SysEx::operator!= (other); } + + void addUInt7 (uint7_t value) + { + jassert (value <= uint7Max); + data.push_back (uint8_t (value & 0x7f)); + } + + void addData (const uint7_t* d, size_t dataSize) + { + jassert (d != nullptr && dataSize > 0); + data.insert (data.end(), d, d + dataSize); + } + + void addUInt14 (uint14_t value) + { + jassert (value <= uint14Max); + const uint7_t d[] { uint7_t (value & 0x7f), uint7_t ((value >> 7) & 0x7f) }; + data.insert (data.end(), std::begin (d), std::end (d)); + } + + uint14_t makeUInt14 (size_t dataPos) const + { + jassert (dataPos + 1 < data.size()); + return uint14_t (data[dataPos] | (data[dataPos + 1] << 7)); + } + + void addUInt28 (uint28_t value) + { + jassert (value <= uint28Max); + const uint7_t d[] { uint7_t (value & 0x7f), + uint7_t ((value >> 7) & 0x7f), + uint7_t ((value >> 14) & 0x7f), + uint7_t ((value >> 21) & 0x7f) }; + data.insert (data.end(), std::begin (d), std::end (d)); + } + + uint28_t makeUInt28 (size_t dataPos) const + { + jassert (dataPos + 3 < data.size()); + return uint28_t (data[dataPos] | (data[dataPos + 1] << 7) + | (data[dataPos + 2] << 14) | (data[dataPos + 3] << 21)); + } + + void addUInt32 (uint32_t value) + { + const uint7_t d[] { uint7_t (value & 0x7f), + uint7_t ((value >> 7) & 0x7f), + uint7_t ((value >> 14) & 0x7f), + uint7_t ((value >> 21) & 0x7f), + uint7_t ((value >> 28) & 0x0f) }; + data.insert (data.end(), std::begin (d), std::end (d)); + } + + uint32_t makeUInt32 (size_t dataPos) const + { + jassert (dataPos + 4 < data.size()); + return uint32_t (data[dataPos] | (data[dataPos + 1] << 7) + | (data[dataPos + 2] << 14) | (data[dataPos + 3] << 21) + | ((data[dataPos + 4] & 0x0f) << 28)); + } + + void addDeviceIdentity (const DeviceIdentity& identity) + { + jassert ((identity.manufacturer & 0xff808080u) == 0); + jassert ((identity.family & 0xc000u) == 0); + jassert ((identity.model & 0xc000u) == 0); + jassert ((identity.revision & 0xf0000000u) == 0); + + data.push_back (uint8_t ((identity.manufacturer >> 16) & 0x7f)); + data.push_back (uint8_t ((identity.manufacturer >> 8) & 0x7f)); + data.push_back (uint8_t (identity.manufacturer & 0x7f)); + + addUInt14 (identity.family); + addUInt14 (identity.model); + addUInt28 (identity.revision); + } + + DeviceIdentity makeDeviceIdentity (size_t dataPos) const + { + jassert (dataPos + 10 < data.size()); + + const auto* d = data.data() + dataPos; + DeviceIdentity result; + result.manufacturer = (uint14_t (d[0]) << 16) | (uint14_t (d[1]) << 8) | d[2]; + result.family = makeUInt14 (dataPos + 3); + result.model = makeUInt14 (dataPos + 5); + result.revision = makeUInt28 (dataPos + 7); + return result; + } +}; + +struct SysEx8 : SysEx +{ + using SysEx::SysEx; + + bool operator== (const SysEx8& other) const { return SysEx::operator== (other); } + + bool operator!= (const SysEx8& other) const { return SysEx::operator!= (other); } +}; + +template +void sendSysEx7 (const SysEx7& sysex, Group group, Sender&& sender) +{ + constexpr size_t maxPayloadSize = 6; + + auto packet = (sysex.totalDataSize() <= maxPayloadSize) ? makeSysEx7CompletePacket (group) + : makeSysEx7StartPacket (group); + + packet.addPayloadByte ((sysex.manufacturerId >> 16) & 0x7f); + if (sysex.manufacturerId & 0x00ffff) + { + packet.addPayloadByte ((sysex.manufacturerId >> 8) & 0x7f); + packet.addPayloadByte (sysex.manufacturerId & 0x7f); + } + + size_t bytesLeft = sysex.data.size(); + for (const auto b : sysex.data) + { + packet.addPayloadByte (b); + --bytesLeft; + + if (packet.getPayloadSize() == maxPayloadSize) + { + sender (packet); + packet = (bytesLeft <= maxPayloadSize) ? makeSysEx7EndPacket (group) + : makeSysEx7ContinuePacket (group); + } + } + + if (packet.getPayloadSize() > 0) + sender (packet); +} + +inline std::vector asSysEx7Packets (const SysEx7& sysex, Group group = 0) +{ + std::vector result; + result.reserve (sysex.totalDataSize() / 6 + 1); + + sendSysEx7 (sysex, group, [&] (const DataMessage& packet) + { + result.push_back (packet); + }); + + return result; +} + +template +void sendSysEx8 (const SysEx8& sysex, uint8_t streamId, Group group, Sender&& sender) +{ + constexpr size_t maxPayloadSize = 13; + + auto packet = (sysex.totalDataSize() <= maxPayloadSize) ? makeSysEx8CompletePacket (streamId, group) + : makeSysEx8StartPacket (streamId, group); + + if (sysex.manufacturerId & 0x00ffff) + { + packet.addPayloadByte (uint8_t (0x80 + ((sysex.manufacturerId >> 8) & 0x7f))); + packet.addPayloadByte (uint8_t (sysex.manufacturerId & 0x7f)); + } + else + { + packet.addPayloadByte (0); + packet.addPayloadByte (uint8_t ((sysex.manufacturerId >> 16) & 0x7f)); + } + + size_t bytesLeft = sysex.data.size(); + for (const auto b : sysex.data) + { + packet.addPayloadByte (b); + --bytesLeft; + + if (packet.getPayloadSize() == maxPayloadSize) + { + sender (packet); + packet = (bytesLeft <= maxPayloadSize) ? makeSysEx8EndPacket (streamId, group) + : makeSysEx8ContinuePacket (streamId, group); + } + } + + if (packet.getPayloadSize() > 0) + sender (packet); +} + +inline std::vector asSysEx8Packets (const SysEx8& sysex, uint8_t streamId, Group group = 0) +{ + std::vector result; + result.reserve ((sysex.totalDataSize() + 1) / 14 + 1); + + sendSysEx8 (sysex, streamId, group, [&] (const ExtendedDataMessage& packet) + { + result.push_back (packet); + }); + + return result; +} + +} // namespace yup::ump + +#endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPSysEx7.cpp b/modules/yup_audio_basics/midi/ump/yup_UMPSysEx7.cpp deleted file mode 100644 index 24d3ae31d..000000000 --- a/modules/yup_audio_basics/midi/ump/yup_UMPSysEx7.cpp +++ /dev/null @@ -1,66 +0,0 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com - - YUP is an open source library subject to 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. - - YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== - - This file is part of the JUCE library. - Copyright (c) 2022 - Raw Material Software Limited - - JUCE is an open source library subject to commercial or open-source - licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - To use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -namespace yup::ump -{ - -uint32_t SysEx7::getNumPacketsRequiredForDataSize (uint32_t size) -{ - constexpr auto denom = 6; - return (size / denom) + ((size % denom) != 0); -} - -SysEx7::PacketBytes SysEx7::getDataBytes (const PacketX2& packet) -{ - const auto numBytes = Utils::getChannel (packet[0]); - constexpr uint8_t maxBytes = 6; - jassert (numBytes <= maxBytes); - - return { - { { std::byte { packet.getU8<2>() }, - std::byte { packet.getU8<3>() }, - std::byte { packet.getU8<4>() }, - std::byte { packet.getU8<5>() }, - std::byte { packet.getU8<6>() }, - std::byte { packet.getU8<7>() } } }, - jmin (numBytes, maxBytes) - }; -} - -} // namespace yup::ump diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPSysEx7.h b/modules/yup_audio_basics/midi/ump/yup_UMPSysEx7.h deleted file mode 100644 index fed54db55..000000000 --- a/modules/yup_audio_basics/midi/ump/yup_UMPSysEx7.h +++ /dev/null @@ -1,91 +0,0 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com - - YUP is an open source library subject to 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. - - YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== - - This file is part of the JUCE library. - Copyright (c) 2022 - Raw Material Software Limited - - JUCE is an open source library subject to commercial or open-source - licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - To use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -#ifndef DOXYGEN - -namespace yup::ump -{ - -/** - This struct acts as a single-file namespace for Universal MIDI Packet - functionality related to 7-bit SysEx. - - @tags{Audio} -*/ -struct YUP_API SysEx7 -{ - /** Returns the number of 64-bit packets required to hold a series of - SysEx bytes. - - The number passed to this function should exclude the leading/trailing - SysEx bytes used in an old midi bytestream, as these are not required - when using Universal MIDI Packets. - */ - static uint32_t getNumPacketsRequiredForDataSize (uint32_t); - - /** The different kinds of UMP SysEx-7 message. */ - enum class Kind : uint8_t - { - /** The whole message fits in a single 2-word packet. */ - complete = 0, - - /** The packet begins a SysEx message that will continue in subsequent packets. */ - begin = 1, - - /** The packet is a continuation of an ongoing SysEx message. */ - continuation = 2, - - /** The packet terminates an ongoing SysEx message. */ - end = 3 - }; - - /** Holds the bytes from a single SysEx-7 packet. */ - struct PacketBytes - { - std::array data; - uint8_t size; - }; - - /** Extracts the data bytes from a 64-bit data message. */ - static PacketBytes getDataBytes (const PacketX2& packet); -}; - -} // namespace yup::ump - -#endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPSysExCollectors.h b/modules/yup_audio_basics/midi/ump/yup_UMPSysExCollectors.h new file mode 100644 index 000000000..9eb5d1bdd --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPSysExCollectors.h @@ -0,0 +1,284 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifndef DOXYGEN + +namespace yup::ump +{ + +class SysEx7Collector +{ +public: + using Callback = std::function; + + explicit SysEx7Collector (Callback cb) + : callback (std::move (cb)) + { + } + + void setCallback (Callback cb) { callback = std::move (cb); } + + void setMaxSysExDataSize (size_t maxSize) + { + if ((maxSysExDataSize = maxSize)) + sysex.data.reserve (maxSysExDataSize); + } + + void feed (const UniversalPacket& packet) + { + if (! isSysEx7Packet (packet)) + return; + + const auto view = SysEx7PacketView { packet }; + + switch (DataStatus (view.getStatus())) + { + case DataStatus::sysex7_complete: + case DataStatus::sysex7_start: + if (state != Status (DataStatus::sysex7_start)) + reset(); + break; + default: + if (state != Status (DataStatus::sysex7_continue)) + { + reset(); + return; + } + break; + } + + const auto numBytes = view.getPayloadSize(); + if (sysex.data.size() + numBytes > sysex.data.capacity()) + { + const bool limitedSize = (maxSysExDataSize > 0); + size_t newCapacity = std::max (size_t { 128 }, 2 * sysex.data.capacity()); + if (limitedSize) + newCapacity = std::min (newCapacity, maxSysExDataSize); + + sysex.data.reserve (newCapacity); + + if (limitedSize && (sysex.data.size() + numBytes > sysex.data.capacity())) + { + state = Status (DataStatus::sysex7_start); + return; + } + } + + for (size_t b = 0; b < numBytes; ++b) + { + const auto byte = view.getPayloadByte (b); + switch (manufacturerIdBytesRead) + { + case 0: + if (byte != 0) + { + sysex.manufacturerId = ManufacturerId (byte) << 16; + manufacturerIdBytesRead = 3; + } + else + { + manufacturerIdBytesRead = 1; + } + break; + case 1: + sysex.manufacturerId = ManufacturerId (byte) << 8; + manufacturerIdBytesRead = 2; + break; + case 2: + sysex.manufacturerId |= ManufacturerId (byte); + manufacturerIdBytesRead = 3; + break; + default: + sysex.data.push_back (byte); + break; + } + } + + switch (DataStatus (view.getStatus())) + { + case DataStatus::sysex7_complete: + case DataStatus::sysex7_end: + if (callback) + callback (sysex); + reset(); + break; + default: + state = Status (DataStatus::sysex7_continue); + break; + } + } + + void reset() + { + sysex.clear(); + state = Status (DataStatus::sysex7_start); + manufacturerIdBytesRead = 0; + } + +private: + SysEx7 sysex; + size_t maxSysExDataSize { 0 }; + Status state { Status (DataStatus::sysex7_start) }; + uint8_t manufacturerIdBytesRead { 0 }; + Callback callback; +}; + +class SysEx8Collector +{ +public: + using Callback = std::function; + + explicit SysEx8Collector (Callback cb) + : callback (std::move (cb)) + { + } + + void setCallback (Callback cb) { callback = std::move (cb); } + + void setMaxSysExDataSize (size_t maxSize) + { + if ((maxSysExDataSize = maxSize)) + sysex.data.reserve (maxSysExDataSize); + } + + void feed (const UniversalPacket& packet) + { + if (! isSysEx8Packet (packet)) + return; + + const auto view = SysEx8PacketView { packet }; + + switch (view.getFormat()) + { + case PacketFormat::complete: + case PacketFormat::start: + if (state != PacketFormat::start) + reset(); + streamId = view.getStreamId(); + break; + default: + if (state != PacketFormat::cont) + { + reset(); + return; + } + if (view.getStreamId() != streamId) + return; + break; + } + + const auto numBytes = view.getPayloadSize(); + if (sysex.data.size() + numBytes > sysex.data.capacity()) + { + const bool limitedSize = (maxSysExDataSize > 0); + size_t newCapacity = std::max (size_t { 128 }, 2 * sysex.data.capacity()); + if (limitedSize) + newCapacity = std::min (newCapacity, maxSysExDataSize); + + sysex.data.reserve (newCapacity); + + if (limitedSize && (sysex.data.size() + numBytes > sysex.data.capacity())) + { + state = PacketFormat::start; + return; + } + } + + for (size_t b = 0; b < numBytes; ++b) + { + const auto byte = view.getPayloadByte (b); + switch (manufacturerIdState) + { + case ManufacturerIdState::detect: + if (byte & 0x80) + { + sysex.manufacturerId = ManufacturerId ((byte & 0x7f) << 8); + manufacturerIdState = ManufacturerIdState::threeBytes; + } + else + { + sysex.manufacturerId = 0; + manufacturerIdState = (byte == 0) ? ManufacturerIdState::oneByte + : ManufacturerIdState::invalid; + } + break; + case ManufacturerIdState::oneByte: + sysex.manufacturerId = ManufacturerId ((byte & 0x7f) << 16); + manufacturerIdState = ManufacturerIdState::done; + break; + case ManufacturerIdState::threeBytes: + sysex.manufacturerId |= ManufacturerId (byte & 0x7f); + manufacturerIdState = ManufacturerIdState::done; + break; + case ManufacturerIdState::invalid: + manufacturerIdState = ManufacturerIdState::done; + break; + default: + sysex.data.push_back (byte); + break; + } + } + + switch (view.getFormat()) + { + case PacketFormat::complete: + case PacketFormat::end: + if (callback) + callback (sysex, streamId); + reset(); + break; + default: + state = PacketFormat::cont; + break; + } + } + + void reset() + { + sysex.clear(); + streamId = 0; + state = PacketFormat::start; + manufacturerIdState = ManufacturerIdState::detect; + } + + uint8_t getStreamId() const { return streamId; } + +private: + enum class ManufacturerIdState + { + detect, + oneByte, + threeBytes, + invalid, + done + }; + + uint8_t streamId { 0 }; + SysEx8 sysex; + size_t maxSysExDataSize { 0 }; + PacketFormat state { PacketFormat::start }; + ManufacturerIdState manufacturerIdState { ManufacturerIdState::detect }; + Callback callback; +}; + +} // namespace yup::ump + +#endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPTypes.h b/modules/yup_audio_basics/midi/ump/yup_UMPTypes.h new file mode 100644 index 000000000..1cfeba33b --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPTypes.h @@ -0,0 +1,641 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifndef DOXYGEN + +namespace yup::ump +{ + +using uint2_t = std::uint8_t; +using uint4_t = std::uint8_t; +using uint7_t = std::uint8_t; +using uint8_t = std::uint8_t; +using uint14_t = std::uint16_t; +using uint16_t = std::uint16_t; +using uint28_t = std::uint32_t; +using uint32_t = std::uint32_t; +using int32_t = std::int32_t; +using size_t = std::size_t; + +using Group = uint4_t; +using Status = uint8_t; +using Channel = uint4_t; +using NoteNumber = uint7_t; +using ControllerNumber = uint7_t; +using ProgramNumber = uint7_t; +using Muid = uint28_t; +using ManufacturerId = uint32_t; +using Protocol = uint8_t; +using Extensions = uint8_t; + +constexpr uint7_t downsample16To7Bit (uint16_t v) { return uint7_t (v >> 9u); } + +constexpr uint7_t downsample32To7Bit (uint32_t v) { return uint7_t (v >> 25u); } + +constexpr uint14_t downsample32To14Bit (uint32_t v) { return uint14_t (v >> 18u); } + +constexpr uint16_t upsample7To16Bit (uint7_t v) +{ + uint16_t result = uint16_t (v) << 9u; + + if (v > 0x40) + { + const auto bits = uint16_t (v & 0x3f); + result |= (uint16_t) ((bits << 3u) | (bits >> 3u)); + } + + return result; +} + +constexpr uint32_t upsample7To32Bit (uint7_t v) +{ + uint32_t result = uint32_t (v) << 25u; + + if (v > 0x40) + { + uint32_t bits = uint32_t (v & 0x3f); + bits |= (bits << 6u); + result |= (bits << 13u) | (bits << 1u) | (bits >> 11u); + } + + return result; +} + +constexpr uint32_t upsample14To32Bit (uint14_t v) +{ + uint32_t result = uint32_t (v) << 18u; + + if (v > 0x2000) + { + const auto bits = uint32_t (v & 0x1fff); + result |= (bits << 5u) | (bits >> 8u); + } + + return result; +} + +constexpr uint32_t upsampleXToYBit (uint32_t v, uint8_t x, uint8_t y) +{ + jassert (x > 1 && y <= 32 && x < y); + + const auto scaleBits = uint8_t (y - x); + const auto center = uint32_t (1u << (x - 1u)); + + uint32_t result = v << scaleBits; + if (v <= center) + return result; + + const auto repeatBits = uint8_t (x - 1u); + const auto repeatMask = uint32_t ((1u << repeatBits) - 1u); + uint32_t repeatValue = v & repeatMask; + + if (scaleBits > repeatBits) + repeatValue <<= (scaleBits - repeatBits); + else + repeatValue >>= (repeatBits - scaleBits); + + while (repeatValue != 0) + { + result |= repeatValue; + repeatValue >>= repeatBits; + } + + return result; +} + +struct Velocity +{ + uint16_t value { 0x8000 }; + + constexpr Velocity() = default; + + constexpr explicit Velocity (uint16_t v) + : value (v) + { + } + + constexpr explicit Velocity (uint7_t v) + : value (upsample7To16Bit (v)) + { + } + + constexpr explicit Velocity (float v); + constexpr explicit Velocity (double v); + + constexpr float asFloat() const; + constexpr double asDouble() const; + + constexpr uint7_t asUInt7() const { return downsample16To7Bit (value); } + + constexpr bool operator== (const Velocity& other) const { return value == other.value; } + + constexpr bool operator!= (const Velocity& other) const { return value != other.value; } +}; + +struct PitchBend +{ + uint32_t value { 0x80000000 }; + + constexpr PitchBend() = default; + + constexpr explicit PitchBend (uint32_t v) + : value (v) + { + } + + constexpr explicit PitchBend (uint14_t v) + : value (upsample14To32Bit (v)) + { + } + + constexpr explicit PitchBend (float v); + constexpr explicit PitchBend (double v); + + constexpr float asFloat() const { return static_cast (asDouble()); } + + constexpr double asDouble() const; + + constexpr uint14_t asUInt14() const { return downsample32To14Bit (value); } + + constexpr void reset() { value = 0x80000000; } + + constexpr bool operator== (const PitchBend& other) const { return value == other.value; } + + constexpr bool operator!= (const PitchBend& other) const { return value != other.value; } +}; + +struct PitchIncrement +{ + int32_t value { 0 }; + + constexpr PitchIncrement() = default; + + constexpr explicit PitchIncrement (int32_t v) + : value (v) + { + } + + constexpr explicit PitchIncrement (float v); + constexpr explicit PitchIncrement (double v); + + constexpr void operator+= (const PitchIncrement& inc); + constexpr PitchIncrement operator+ (const PitchIncrement& inc) const; + + constexpr bool operator== (const PitchIncrement& other) const { return value == other.value; } + + constexpr bool operator!= (const PitchIncrement& other) const { return value != other.value; } +}; + +struct Pitch7_9 +{ + uint16_t value { 0 }; + + constexpr Pitch7_9() = default; + + constexpr explicit Pitch7_9 (uint16_t v) + : value (v) + { + } + + constexpr explicit Pitch7_9 (NoteNumber v) + : value (uint16_t (v) << 9) + { + } + + constexpr explicit Pitch7_9 (float v); + constexpr explicit Pitch7_9 (double v); + + constexpr float asFloat() const; + constexpr double asDouble() const; + + constexpr NoteNumber noteNumber() const { return NoteNumber (value >> 9); } + + constexpr bool operator== (const Pitch7_9& other) const { return value == other.value; } + + constexpr bool operator!= (const Pitch7_9& other) const { return value != other.value; } +}; + +struct ControllerValue; + +struct Pitch7_25 +{ + uint32_t value { 0 }; + + constexpr Pitch7_25() = default; + + constexpr explicit Pitch7_25 (uint32_t v) + : value (v) + { + } + + constexpr explicit Pitch7_25 (NoteNumber v) + : value (uint32_t (v) << 25) + { + } + + constexpr explicit Pitch7_25 (Pitch7_9 v) { *this = v; } + + constexpr explicit Pitch7_25 (const ControllerValue& v); + constexpr explicit Pitch7_25 (float v); + constexpr explicit Pitch7_25 (double v); + + constexpr float asFloat() const { return static_cast (asDouble()); } + + constexpr double asDouble() const; + + constexpr NoteNumber noteNumber() const { return NoteNumber (value >> 25); } + + constexpr Pitch7_25& operator= (Pitch7_9 v) + { + value = uint32_t (v.value) << 16; + return *this; + } + + constexpr Pitch7_25 operator+ (const PitchIncrement& inc) const; + + constexpr Pitch7_25 operator+ (float v) const { return Pitch7_25 { asFloat() + v }; } + + constexpr Pitch7_25 operator+ (double v) const { return Pitch7_25 { asDouble() + v }; } + + constexpr void operator+= (const PitchIncrement& inc); + + constexpr bool operator== (const Pitch7_25& other) const { return value == other.value; } + + constexpr bool operator!= (const Pitch7_25& other) const { return value != other.value; } +}; + +struct PitchBendSensitivity : Pitch7_25 +{ + constexpr PitchBendSensitivity() + : Pitch7_25 { NoteNumber { 2 } } + { + } + + using Pitch7_25::Pitch7_25; +}; + +constexpr PitchIncrement operator* (const PitchBend& bend, const PitchBendSensitivity& sens) +{ + auto result = int64_t (bend.value) - int64_t (0x80000000u); + if (result == 0) + return PitchIncrement { int32_t { 0 } }; + + result *= sens.value; + result /= 2147483648; + + return PitchIncrement { static_cast (std::clamp (result, + int64_t (std::numeric_limits::min()), + int64_t (std::numeric_limits::max()))) }; +} + +constexpr PitchIncrement operator* (const PitchBendSensitivity& sens, const PitchBend& bend) +{ + return bend * sens; +} + +struct ControllerIncrement +{ + int32_t value { 0 }; + + constexpr ControllerIncrement() = default; + + constexpr explicit ControllerIncrement (int32_t v) + : value (v) + { + } + + constexpr bool operator== (const ControllerIncrement& other) const { return value == other.value; } + + constexpr bool operator!= (const ControllerIncrement& other) const { return value != other.value; } +}; + +struct ControllerValue +{ + uint32_t value { 0 }; + + constexpr ControllerValue() = default; + + constexpr explicit ControllerValue (uint32_t v) + : value (v) + { + } + + constexpr explicit ControllerValue (uint14_t v) + : value (upsample14To32Bit (v)) + { + } + + constexpr explicit ControllerValue (uint7_t v) + : value (upsample7To32Bit (v)) + { + } + + constexpr explicit ControllerValue (const Pitch7_25& v) + : value (v.value) + { + } + + constexpr explicit ControllerValue (float v); + constexpr explicit ControllerValue (double v); + + constexpr float asFloat() const; + constexpr double asDouble() const; + + constexpr uint14_t asUInt14() const { return downsample32To14Bit (value); } + + constexpr uint7_t asUInt7() const { return downsample32To7Bit (value); } + + constexpr void operator+= (ControllerIncrement inc); + constexpr ControllerValue operator+ (ControllerIncrement inc) const; + + constexpr bool operator== (const ControllerValue& other) const { return value == other.value; } + + constexpr bool operator!= (const ControllerValue& other) const { return value != other.value; } +}; + +#pragma pack(push, 1) + +struct DeviceIdentity +{ + ManufacturerId manufacturer {}; + uint14_t family {}; + uint14_t model {}; + uint28_t revision {}; +}; + +#pragma pack(pop) + +namespace detail +{ +template +constexpr uint64_t round_to_uint (F v) +{ + return v <= F (0) ? uint64_t (0) : uint64_t (v + F (0.5)); +} + +template +constexpr T from_float_0_1 (F v) +{ + if (v <= F (0)) + return 0; + + if (v >= F (1)) + return std::numeric_limits::max(); + + constexpr auto max = std::numeric_limits::max(); + if (v <= F (0.5)) + { + constexpr auto scale = static_cast (max) + 1.0; + return static_cast (v * scale); + } + + constexpr auto mid = (max >> 1) + 1; + constexpr auto scale = static_cast (max); + return mid + static_cast ((v - F (0.5)) * scale); +} + +template +constexpr F to_float_0_1 (T value) +{ + constexpr auto max = std::numeric_limits::max(); + constexpr auto center = (max >> 1) + 1; + + if (value <= center) + return static_cast (value / static_cast (center) / 2.0); + + return static_cast (value / static_cast (max)); +} +} // namespace detail + +constexpr Velocity::Velocity (float v) + : value (detail::from_float_0_1 (v)) +{ +} + +constexpr Velocity::Velocity (double v) + : value (detail::from_float_0_1 (v)) +{ +} + +constexpr float Velocity::asFloat() const { return detail::to_float_0_1 (value); } + +constexpr double Velocity::asDouble() const { return detail::to_float_0_1 (value); } + +constexpr PitchBend::PitchBend (float v) + : value (detail::from_float_0_1 ((v + 1.0f) / 2.0f)) +{ +} + +constexpr PitchBend::PitchBend (double v) + : value (detail::from_float_0_1 ((v + 1.0) / 2.0)) +{ +} + +constexpr double PitchBend::asDouble() const +{ + if (value >= 0x80000000u) + return (value - 0x80000000u) / static_cast (0x7fffffffu); + + return (0x80000000u - value) / (-static_cast (0x80000000u)); +} + +constexpr PitchIncrement::PitchIncrement (float v) +{ + if (v >= 64.0f) + { + value = std::numeric_limits::max(); + return; + } + + if (v <= -64.0f) + { + value = std::numeric_limits::min(); + return; + } + + if (v >= 0.0f) + { + value = static_cast (Pitch7_25 { v }.value); + return; + } + + value = -static_cast (Pitch7_25 { -v }.value); +} + +constexpr PitchIncrement::PitchIncrement (double v) +{ + if (v >= 64.0) + { + value = std::numeric_limits::max(); + return; + } + + if (v <= -64.0) + { + value = std::numeric_limits::min(); + return; + } + + if (v >= 0.0) + { + const auto result = Pitch7_25 { v }.value; + value = result <= uint32_t (std::numeric_limits::max()) ? static_cast (result) + : std::numeric_limits::max(); + return; + } + + value = -static_cast (Pitch7_25 { -v }.value); +} + +constexpr void PitchIncrement::operator+= (const PitchIncrement& inc) +{ + const auto sum = static_cast (value) + inc.value; + value = static_cast (std::clamp (sum, + int64_t (std::numeric_limits::min()), + int64_t (std::numeric_limits::max()))); +} + +constexpr PitchIncrement PitchIncrement::operator+ (const PitchIncrement& inc) const +{ + PitchIncrement out { *this }; + out += inc; + return out; +} + +constexpr Pitch7_9::Pitch7_9 (float v) +{ + constexpr int fractionalBits = 9; + + if (v <= 0.0f) + value = 0; + else if (v >= 128.0f) + value = 0xffff; + else + value = static_cast (detail::round_to_uint (v * float (1 << fractionalBits))); +} + +constexpr Pitch7_9::Pitch7_9 (double v) +{ + constexpr int fractionalBits = 9; + + if (v <= 0.0) + value = 0; + else if (v >= 128.0) + value = 0xffff; + else + value = static_cast (detail::round_to_uint (v * double (1 << fractionalBits))); +} + +constexpr float Pitch7_9::asFloat() const +{ + constexpr int fractionalBits = 9; + return static_cast (value) / float (1 << fractionalBits); +} + +constexpr double Pitch7_9::asDouble() const +{ + constexpr int fractionalBits = 9; + return static_cast (value) / double (1 << fractionalBits); +} + +constexpr Pitch7_25::Pitch7_25 (float v) +{ + constexpr int fractionalBits = 25; + + if (v <= 0.0f) + value = 0; + else if (v >= 128.0f) + value = 0xffffffffu; + else + value = static_cast (detail::round_to_uint (v * float (1 << fractionalBits))); +} + +constexpr Pitch7_25::Pitch7_25 (double v) +{ + constexpr int fractionalBits = 25; + + if (v <= 0.0) + value = 0; + else if (v >= 128.0) + value = 0xffffffffu; + else + value = static_cast (detail::round_to_uint (v * double (1 << fractionalBits))); +} + +constexpr double Pitch7_25::asDouble() const +{ + constexpr int fractionalBits = 25; + return static_cast (value) / double (1 << fractionalBits); +} + +constexpr Pitch7_25::Pitch7_25 (const ControllerValue& v) + : value (v.value) +{ +} + +constexpr Pitch7_25 Pitch7_25::operator+ (const PitchIncrement& inc) const +{ + const auto sum = static_cast (value) + inc.value; + const auto clamped = std::clamp (sum, int64_t (0), int64_t (0xffffffffu)); + return Pitch7_25 { static_cast (clamped) }; +} + +constexpr void Pitch7_25::operator+= (const PitchIncrement& inc) +{ + *this = *this + inc; +} + +constexpr ControllerValue::ControllerValue (float v) + : value (detail::from_float_0_1 (v)) +{ +} + +constexpr ControllerValue::ControllerValue (double v) + : value (detail::from_float_0_1 (v)) +{ +} + +constexpr float ControllerValue::asFloat() const +{ + return detail::to_float_0_1 (value); +} + +constexpr double ControllerValue::asDouble() const +{ + return detail::to_float_0_1 (value); +} + +constexpr void ControllerValue::operator+= (ControllerIncrement inc) +{ + const auto sum = static_cast (value) + inc.value; + value = static_cast (std::clamp (sum, int64_t (0), int64_t (0xffffffffu))); +} + +constexpr ControllerValue ControllerValue::operator+ (ControllerIncrement inc) const +{ + ControllerValue out { *this }; + out += inc; + return out; +} + +} // namespace yup::ump + +#endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPUniversalPacket.h b/modules/yup_audio_basics/midi/ump/yup_UMPUniversalPacket.h new file mode 100644 index 000000000..9538d6dc1 --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPUniversalPacket.h @@ -0,0 +1,375 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifndef DOXYGEN + +namespace yup::ump +{ + +enum class PacketType : uint4_t +{ + utility = 0x0, + system = 0x1, + midi1_channel_voice = 0x2, + data = 0x3, + midi2_channel_voice = 0x4, + extended_data = 0x5, + flex_data = 0xD, + stream = 0xF +}; + +enum class PacketFormat : uint2_t +{ + complete = 0b00, + start = 0b01, + cont = 0b10, + end = 0b11 +}; + +enum class PacketAddress : uint2_t +{ + channel = 0b00, + group = 0b01 +}; + +enum class UtilityStatus : Status +{ + noop = 0x00, + jitterClock = 0x10, + jitterTimestamp = 0x20 +}; + +enum class SystemStatus : Status +{ + mtc_quarter_frame = 0xF1, + song_position = 0xF2, + song_select = 0xF3, + tune_request = 0xF6, + clock = 0xF8, + start = 0xFA, + cont = 0xFB, + stop = 0xFC, + active_sense = 0xFE, + reset = 0xFF +}; + +enum class Midi1ChannelVoiceStatus : Status +{ + note_off = 0x80, + note_on = 0x90, + poly_pressure = 0xA0, + control_change = 0xB0, + program_change = 0xC0, + channel_pressure = 0xD0, + pitch_bend = 0xE0 +}; + +enum class DataStatus : Status +{ + sysex7_complete = (Status (PacketFormat::complete) << 4), + sysex7_start = (Status (PacketFormat::start) << 4), + sysex7_continue = (Status (PacketFormat::cont) << 4), + sysex7_end = (Status (PacketFormat::end) << 4) +}; + +enum class ChannelVoiceStatus : Status +{ + registered_per_note_controller = 0x00, + assignable_per_note_controller = 0x10, + registered_controller = 0x20, + assignable_controller = 0x30, + relative_registered_controller = 0x40, + relative_assignable_controller = 0x50, + per_note_pitch_bend = 0x60, + note_off = 0x80, + note_on = 0x90, + poly_pressure = 0xA0, + control_change = 0xB0, + program_change = 0xC0, + channel_pressure = 0xD0, + pitch_bend = 0xE0, + per_note_management = 0xF0 +}; + +enum class ExtendedDataStatus : Status +{ + sysex8_complete = (Status (PacketFormat::complete) << 4), + sysex8_start = (Status (PacketFormat::start) << 4), + sysex8_continue = (Status (PacketFormat::cont) << 4), + sysex8_end = (Status (PacketFormat::end) << 4), + mixed_data_set_header = 0x80, + mixed_data_set_payload = 0x90 +}; + +enum class StreamProtocol : Protocol +{ + midi1 = 0x1, + midi2 = 0x2 +}; + +enum class StreamExtensions : Extensions +{ + jitter_reduction_transmit = 0x1, + jitter_reduction_receive = 0x2 +}; + +enum class StreamStatus : Status +{ + endpoint_discovery = 0x00, + endpoint_info = 0x01, + device_identity = 0x02, + endpoint_name = 0x03, + product_instance_id = 0x04, + stream_configuration_request = 0x05, + stream_configuration_notify = 0x06, + function_block_discovery = 0x10, + function_block_info = 0x11, + function_block_name = 0x12 +}; + +namespace ControlChange +{ +constexpr ControllerNumber bankSelectMsb = 0; +constexpr ControllerNumber dataEntryMsb = 6; +constexpr ControllerNumber bankSelectLsb = 32; +constexpr ControllerNumber dataEntryLsb = 38; +constexpr ControllerNumber nrpnLsb = 98; +constexpr ControllerNumber nrpnMsb = 99; +constexpr ControllerNumber rpnLsb = 100; +constexpr ControllerNumber rpnMsb = 101; +constexpr ControllerNumber hiResVelocityPrefix = 88; +} // namespace ControlChange + +namespace RegisteredParameterNumber +{ +constexpr ControllerNumber pitchBendSensitivity = 0; +constexpr ControllerNumber fineTuning = 1; +constexpr ControllerNumber coarseTuning = 2; +constexpr ControllerNumber tuningProgramSelect = 3; +constexpr ControllerNumber tuningBankSelect = 4; +constexpr ControllerNumber perNotePitchBendSensitivity = 7; +} // namespace RegisteredParameterNumber + +namespace RegisteredPerNoteController +{ +constexpr ControllerNumber modulation = 1; +constexpr ControllerNumber breath = 2; +constexpr ControllerNumber pitch7_25 = 3; + +constexpr ControllerNumber volume = 7; +constexpr ControllerNumber balance = 8; + +constexpr ControllerNumber pan = 10; +constexpr ControllerNumber expression = 11; + +constexpr ControllerNumber soundController1 = 70; +constexpr ControllerNumber soundVariation = 70; +constexpr ControllerNumber soundController2 = 71; +constexpr ControllerNumber timbre = 71; +constexpr ControllerNumber harmonicIntensity = 71; +constexpr ControllerNumber soundController3 = 72; +constexpr ControllerNumber releaseTime = 72; +constexpr ControllerNumber soundController4 = 73; +constexpr ControllerNumber attackTime = 73; +constexpr ControllerNumber soundController5 = 74; +constexpr ControllerNumber brightness = 74; +constexpr ControllerNumber soundController6 = 75; +constexpr ControllerNumber decayTime = 75; +constexpr ControllerNumber soundController7 = 76; +constexpr ControllerNumber vibratoRate = 76; +constexpr ControllerNumber soundController8 = 77; +constexpr ControllerNumber vibratoDepth = 77; +constexpr ControllerNumber soundController9 = 78; +constexpr ControllerNumber vibratoDelay = 78; +constexpr ControllerNumber soundController10 = 79; + +constexpr ControllerNumber effects1Depth = 91; +constexpr ControllerNumber reverbSendLevel = 91; +constexpr ControllerNumber effects2Depth = 92; +constexpr ControllerNumber effects3Depth = 93; +constexpr ControllerNumber chorusSendLevel = 93; +constexpr ControllerNumber effects4Depth = 94; +constexpr ControllerNumber effects5Depth = 95; +} // namespace RegisteredPerNoteController + +struct UniversalPacket +{ + uint32_t data[4] { 0u, 0u, 0u, 0u }; + + constexpr PacketType getType() const { return static_cast ((data[0] >> 28u) & 0x0f); } + + constexpr size_t getSize() const + { + constexpr size_t sizeLookup[16] { 1, 1, 1, 2, 2, 4, 1, 1, 2, 2, 2, 3, 3, 4, 4, 4 }; + return sizeLookup[static_cast (getType())]; + } + + constexpr Group getGroup() const { return Group ((data[0] >> 24u) & 0x0f); } + + constexpr Status getStatus() const { return getByte2(); } + + constexpr uint8_t getByte2() const { return uint8_t ((data[0] >> 16u) & 0xff); } + + constexpr uint8_t getByte3() const { return uint8_t ((data[0] >> 8u) & 0xff); } + + constexpr uint8_t getByte4() const { return uint8_t (data[0] & 0xff); } + + constexpr uint8_t getByte (size_t b) const + { + jassert (b < 16); + const auto word = b / 4; + const auto byte = b % 4; + const auto shift = (3 - byte) * 8; + return uint8_t ((data[word] >> shift) & 0xff); + } + + constexpr uint7_t getByte7Bit (size_t b) const { return uint7_t (getByte (b) & 0x7f); } + + constexpr bool hasChannel() const + { + return getType() == PacketType::midi1_channel_voice + || getType() == PacketType::midi2_channel_voice + || getType() == PacketType::flex_data; + } + + constexpr Channel getChannel() const + { + jassert (hasChannel()); + return Channel (getByte2() & 0x0f); + } + + constexpr void setType (PacketType t) + { + data[0] = (data[0] & 0x0fffffff) | (uint32_t (t) << 28u); + } + + constexpr void setGroup (Group c) + { + data[0] = (data[0] & 0xf0ffffff) | (uint32_t (c) << 24u); + } + + constexpr void setByte (size_t b, uint8_t v) + { + jassert (b < 16); + if (b >= 16) + return; + + uint32_t& word = data[b / 4]; + const auto byteIndex = b % 4; + const auto shift = (3 - byteIndex) * 8; + const auto mask = uint32_t (0xffu << shift); + word = (word & ~mask) | (uint32_t (v) << shift); + } + + constexpr void setByte7Bit (size_t b, uint8_t v) { setByte (b, uint8_t (v & 0x7f)); } + + constexpr UniversalPacket() = default; + + constexpr explicit UniversalPacket (uint32_t w) { data[0] = w; } + + constexpr UniversalPacket (uint32_t w1, uint32_t w2) + { + data[0] = w1; + data[1] = w2; + } + + constexpr UniversalPacket (uint32_t w1, uint32_t w2, uint32_t w3) + { + data[0] = w1; + data[1] = w2; + data[2] = w3; + } + + constexpr UniversalPacket (uint32_t w1, uint32_t w2, uint32_t w3, uint32_t w4) + { + data[0] = w1; + data[1] = w2; + data[2] = w3; + data[3] = w4; + } + + constexpr bool operator== (const UniversalPacket& other) const + { + if (this == &other) + return true; + + const auto len = getSize(); + for (size_t i = 0; i < len; ++i) + if (data[i] != other.data[i]) + return false; + + return true; + } + + constexpr bool operator!= (const UniversalPacket& other) const { return ! operator== (other); } + + constexpr void reset() + { + for (auto& d : data) + d = 0; + } + + constexpr bool isUtilityMessage() const { return getType() == PacketType::utility; } + + constexpr bool isSystemMessage() const { return getType() == PacketType::system; } + + constexpr bool isChannelVoiceMessage() const + { + return getType() == PacketType::midi1_channel_voice || getType() == PacketType::midi2_channel_voice; + } + + constexpr bool isDataMessage() const + { + return getType() == PacketType::data || getType() == PacketType::extended_data; + } + + constexpr bool isMidi1ProtocolMessage() const + { + if (getType() == PacketType::system) + { + switch (SystemStatus (getStatus())) + { + case SystemStatus::mtc_quarter_frame: + case SystemStatus::song_position: + case SystemStatus::song_select: + case SystemStatus::tune_request: + case SystemStatus::clock: + case SystemStatus::start: + case SystemStatus::cont: + case SystemStatus::stop: + case SystemStatus::active_sense: + case SystemStatus::reset: + return true; + default: + return false; + } + } + + if (getType() == PacketType::midi1_channel_voice) + return getStatus() >= 0x80 && getStatus() < 0xf0; + + return false; + } +}; + +} // namespace yup::ump + +#endif diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPUniversalSysEx.h b/modules/yup_audio_basics/midi/ump/yup_UMPUniversalSysEx.h new file mode 100644 index 000000000..3172669c9 --- /dev/null +++ b/modules/yup_audio_basics/midi/ump/yup_UMPUniversalSysEx.h @@ -0,0 +1,300 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifndef DOXYGEN + +namespace yup::ump +{ + +namespace UniversalSysEx +{ + +using Type = uint16_t; +using Subtype = uint7_t; + +namespace TypeId +{ +constexpr Type sampleDumpHeader = 0x7E01; +constexpr Type sampleDataPacket = 0x7E02; +constexpr Type sampleDumpRequest = 0x7E03; +constexpr Type midiTimeCodeNonRealtime = 0x7E04; +constexpr Type sampleDumpExtensions = 0x7E05; +constexpr Type generalInformation = 0x7E06; +constexpr Type fileDump = 0x7E07; +constexpr Type midiTuningNonRealtime = 0x7E08; +constexpr Type generalMidi = 0x7E09; +constexpr Type downloadableSounds = 0x7E0A; +constexpr Type fileReferenceMessage = 0x7E0B; +constexpr Type midiVisualControl = 0x7E0C; +constexpr Type capabilityInquiry = 0x7E0D; +constexpr Type endOfFile = 0x7E7B; +constexpr Type wait = 0x7E7C; +constexpr Type cancel = 0x7E7D; +constexpr Type nak = 0x7E7E; +constexpr Type ack = 0x7E7F; + +constexpr Type midiTimeCodeRealtime = 0x7F01; +constexpr Type midiShowControl = 0x7F02; +constexpr Type notationInformation = 0x7F03; +constexpr Type deviceControl = 0x7F04; +constexpr Type realtimeMtcCueing = 0x7F05; +constexpr Type midiMachineControlCommands = 0x7F06; +constexpr Type midiMachineControlResponses = 0x7F07; +constexpr Type midiTuningRealtime = 0x7F08; +constexpr Type controllerDestinationSetting = 0x7F09; + +constexpr Type none = 0x0000; +} // namespace TypeId + +namespace SubtypeId +{ +constexpr Subtype identityRequest = 0x01; +constexpr Subtype identityReply = 0x02; + +constexpr Subtype gm1SystemOn = 0x01; +constexpr Subtype gmSystemOff = 0x02; +constexpr Subtype gm2SystemOn = 0x03; + +constexpr Subtype mtcFullMessage = 0x01; +constexpr Subtype mtcUserBits = 0x02; +} // namespace SubtypeId + +struct MessageView +{ + explicit MessageView (const SysEx7& message) + : sysex (message) + { + jassert (sysex.manufacturerId == Manufacturer::universalRealtime + || sysex.manufacturerId == Manufacturer::universalNonRealtime); + jassert (sysex.data.size() >= 2); + } + + uint7_t getDeviceId() const { return sysex.data[0]; } + + Type getType() const { return Type ((sysex.manufacturerId >> 8) | sysex.data[1]); } + + Subtype getSubtype() const { return sysex.data.size() > 2 ? sysex.data[2] : 0; } + + size_t getDataSize() const { return sysex.data.size(); } + + template + std::optional as() const + { + if (ViewClass::validate (sysex)) + return ViewClass { sysex }; + + return std::nullopt; + } + + template + static std::optional makeOptional (const SysEx7& message) + { + if (ViewClass::validate (message)) + return ViewClass { message }; + + return std::nullopt; + } + + const SysEx7& sysex; +}; + +struct Message : SysEx7 +{ + using SysEx7::SysEx7; + + uint7_t getDeviceId() const + { + if (data.empty()) + return 0; + + return data[0]; + } + + Type getType() const + { + if (data.size() < 2) + return TypeId::none; + + return Type ((manufacturerId >> 8) | data[1]); + } + + Subtype getSubtype() const + { + if (data.size() < 3) + return 0; + + return data[2]; + } + + void setDeviceId (uint7_t deviceId) + { + if (data.empty()) + data.resize (1, 0); + + data[0] = deviceId; + } +}; + +struct IdentityRequest : Message +{ + explicit IdentityRequest (uint7_t deviceId = 0x7f) + : Message (Manufacturer::universalNonRealtime) + { + data.push_back (deviceId); + data.push_back (TypeId::generalInformation & 0x7f); + data.push_back (SubtypeId::identityRequest); + } +}; + +inline Message makeIdentityRequest (uint7_t deviceId = 0x7f) +{ + return IdentityRequest { deviceId }; +} + +inline bool isIdentityRequest (const SysEx7& message) +{ + if (! isUniversalSysExMessage (message)) + return false; + + return UniversalSysEx::TypeId::generalInformation == UniversalSysEx::MessageView { message }.getType() + && UniversalSysEx::MessageView { message }.getSubtype() == UniversalSysEx::SubtypeId::identityRequest; +} + +struct IdentityReplyView : MessageView +{ + using MessageView::MessageView; + + DeviceIdentity getIdentity() const + { + return sysex.makeDeviceIdentity (3); + } + + static bool validate (const SysEx7& message) + { + if (! isUniversalSysExMessage (message)) + return false; + + if (message.data.size() < 13) + return false; + + const auto view = MessageView { message }; + return view.getType() == TypeId::generalInformation && view.getSubtype() == SubtypeId::identityReply; + } +}; + +inline std::optional asIdentityReplyView (const SysEx7& message) +{ + if (IdentityReplyView::validate (message)) + return IdentityReplyView { message }; + + return std::nullopt; +} + +struct IdentityReply : Message +{ + IdentityReply (ManufacturerId sysexId, + uint14_t family, + uint14_t familyMember, + uint28_t revision, + uint7_t deviceId = 0x7f) + : Message (Manufacturer::universalNonRealtime) + { + data.push_back (deviceId); + data.push_back (TypeId::generalInformation & 0x7f); + data.push_back (SubtypeId::identityReply); + addDeviceIdentity ({ sysexId, family, familyMember, revision }); + } + + explicit IdentityReply (const DeviceIdentity& identity) + : IdentityReply (identity.manufacturer, identity.family, identity.model, identity.revision) + { + } +}; + +inline Message makeIdentityReply (ManufacturerId sysexId, + uint14_t family, + uint14_t familyMember, + uint28_t revision, + uint7_t deviceId = 0x7f) +{ + return IdentityReply { sysexId, family, familyMember, revision, deviceId }; +} + +inline Message makeIdentityReply (const DeviceIdentity& identity) +{ + return IdentityReply { identity }; +} + +inline bool isIdentityReply (const SysEx7& message) +{ + if (! isUniversalSysExMessage (message)) + return false; + + return UniversalSysEx::TypeId::generalInformation == UniversalSysEx::MessageView { message }.getType() + && UniversalSysEx::MessageView { message }.getSubtype() == UniversalSysEx::SubtypeId::identityReply; +} + +} // namespace UniversalSysEx + +inline bool isUniversalSysExMessage (const SysEx7& message) +{ + return (message.manufacturerId == Manufacturer::universalRealtime + || message.manufacturerId == Manufacturer::universalNonRealtime) + && message.data.size() >= 2; +} + +inline UniversalSysEx::Type getUniversalSysExType (const SysEx7& message) +{ + if (! isUniversalSysExMessage (message)) + return UniversalSysEx::TypeId::none; + + return UniversalSysEx::Type ((message.manufacturerId >> 8) | message.data[1]); +} + +inline UniversalSysEx::Subtype getUniversalSysExSubtype (const SysEx7& message) +{ + if (! isUniversalSysExMessage (message) || message.data.size() <= 2) + return 0; + + return message.data[2]; +} + +inline uint7_t getUniversalSysExDeviceId (const SysEx7& message) +{ + if (! isUniversalSysExMessage (message)) + return 0xff; + + return message.data[0]; +} + +using UniversalSysExView = UniversalSysEx::MessageView; + +inline std::optional asUniversalSysExView (const SysEx7& message) +{ + if (isUniversalSysExMessage (message)) + return UniversalSysExView { message }; + + return std::nullopt; +} + +} // namespace yup::ump + +#endif diff --git a/modules/yup_audio_basics/yup_audio_basics.cpp b/modules/yup_audio_basics/yup_audio_basics.cpp index 994e1d260..d1b04c541 100644 --- a/modules/yup_audio_basics/yup_audio_basics.cpp +++ b/modules/yup_audio_basics/yup_audio_basics.cpp @@ -88,7 +88,6 @@ #include "audio_play_head/yup_AudioPlayHead.cpp" #include "midi/ump/yup_UMPUtils.cpp" #include "midi/ump/yup_UMPView.cpp" -#include "midi/ump/yup_UMPSysEx7.cpp" #include "midi/ump/yup_UMPMidi1ToMidi2DefaultTranslator.cpp" #include "midi/ump/yup_UMPIterator.cpp" #include "utilities/yup_AudioWorkgroup.cpp" From 1abbb6cde85f8037089cb549f169e6620277ea44 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 22 Dec 2025 09:33:31 +0100 Subject: [PATCH 04/20] Fixes to allow compiling --- modules/yup_audio_basics/midi/ump/yup_UMP.h | 5 +- .../midi/ump/yup_UMPCapabilityInquiry.h | 1510 ++++++++++++++++- .../midi/ump/yup_UMPMidi1ByteStream.cpp | 6 +- .../midi/ump/yup_UMPUniversalSysEx.h | 2 + modules/yup_audio_basics/yup_audio_basics.cpp | 2 + 5 files changed, 1489 insertions(+), 36 deletions(-) diff --git a/modules/yup_audio_basics/midi/ump/yup_UMP.h b/modules/yup_audio_basics/midi/ump/yup_UMP.h index c799b568f..3cfe78c95 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMP.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMP.h @@ -42,7 +42,6 @@ #include "yup_UMPUniversalPacket.h" #include "yup_UMPUtils.h" #include "yup_UMPacket.h" -#include "yup_UMPSysEx.h" #include "yup_UMPView.h" #include "yup_UMPIterator.h" #include "yup_UMPackets.h" @@ -50,15 +49,19 @@ #include "yup_UMPChannelVoice.h" #include "yup_UMPDataMessages.h" #include "yup_UMPExtendedDataMessages.h" +#include "yup_UMPSysEx.h" #include "yup_UMPFlexDataMessages.h" #include "yup_UMPStreamMessages.h" #include "yup_UMPSysExCollectors.h" #include "yup_UMPManufacturer.h" #include "yup_UMPUniversalSysEx.h" +#include "yup_UMPCapabilityInquiry.h" #include "yup_UMPFactory.h" #include "yup_UMPConversion.h" +#include "yup_UMPMidi1ByteStream.h" #include "yup_UMPMidi1ToBytestreamTranslator.h" #include "yup_UMPMidi1ToMidi2DefaultTranslator.h" #include "yup_UMPConverters.h" #include "yup_UMPDispatcher.h" #include "yup_UMPReceiver.h" +#include "yup_UMPJitterReductionTimestamps.h" diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPCapabilityInquiry.h b/modules/yup_audio_basics/midi/ump/yup_UMPCapabilityInquiry.h index dbf49e4cb..80d5aef18 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPCapabilityInquiry.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPCapabilityInquiry.h @@ -45,15 +45,15 @@ struct CapabilityInquiryView : UniversalSysEx::MessageView { } - uint7_t getMessageVersion() const { return sysex.data[fieldOffsets.messageVersion]; } + uint7_t getMessageVersion() const { return sysex.data[fieldOffsets::messageVersion]; } - Muid getSourceMuid() const { return sysex.makeUInt28 (fieldOffsets.sourceMuid); } + Muid getSourceMuid() const { return sysex.makeUInt28 (fieldOffsets::sourceMuid); } - Muid getDestinationMuid() const { return sysex.makeUInt28 (fieldOffsets.destinationMuid); } + Muid getDestinationMuid() const { return sysex.makeUInt28 (fieldOffsets::destinationMuid); } static bool validate (const SysEx7& message) { - return isCapabilityInquiryMessage (message) && message.data.size() >= fieldOffsets.payload; + return isCapabilityInquiryMessage (message) && message.data.size() >= fieldOffsets::payload; } struct fieldOffsets @@ -103,28 +103,28 @@ struct Message : UniversalSysEx::Message { static constexpr size_t offsetOfData = 12; - uint7_t getMessageVersion() const { return data[fieldOffsets.messageVersion]; } + uint7_t getMessageVersion() const { return data[fieldOffsets::messageVersion]; } - Muid getSourceMuid() const { return makeUInt28 (fieldOffsets.sourceMuid); } + Muid getSourceMuid() const { return makeUInt28 (fieldOffsets::sourceMuid); } - Muid getDestinationMuid() const { return makeUInt28 (fieldOffsets.destinationMuid); } + Muid getDestinationMuid() const { return makeUInt28 (fieldOffsets::destinationMuid); } void setSourceMuid (Muid muid) { jassert (muid <= uint28Max); - data[fieldOffsets.sourceMuid] = uint7_t (muid & 0x7f); - data[fieldOffsets.sourceMuid + 1] = uint7_t ((muid >> 7) & 0x7f); - data[fieldOffsets.sourceMuid + 2] = uint7_t ((muid >> 14) & 0x7f); - data[fieldOffsets.sourceMuid + 3] = uint7_t ((muid >> 21) & 0x7f); + data[fieldOffsets::sourceMuid] = uint7_t (muid & 0x7f); + data[fieldOffsets::sourceMuid + 1] = uint7_t ((muid >> 7) & 0x7f); + data[fieldOffsets::sourceMuid + 2] = uint7_t ((muid >> 14) & 0x7f); + data[fieldOffsets::sourceMuid + 3] = uint7_t ((muid >> 21) & 0x7f); } void setDestinationMuid (Muid muid) { jassert (muid <= uint28Max); - data[fieldOffsets.destinationMuid] = uint7_t (muid & 0x7f); - data[fieldOffsets.destinationMuid + 1] = uint7_t ((muid >> 7) & 0x7f); - data[fieldOffsets.destinationMuid + 2] = uint7_t ((muid >> 14) & 0x7f); - data[fieldOffsets.destinationMuid + 3] = uint7_t ((muid >> 21) & 0x7f); + data[fieldOffsets::destinationMuid] = uint7_t (muid & 0x7f); + data[fieldOffsets::destinationMuid + 1] = uint7_t ((muid >> 7) & 0x7f); + data[fieldOffsets::destinationMuid + 2] = uint7_t ((muid >> 14) & 0x7f); + data[fieldOffsets::destinationMuid + 3] = uint7_t ((muid >> 21) & 0x7f); } Message (uint7_t subtype, Muid sourceMuid, Muid destinationMuid, uint7_t deviceId = 0x7f) @@ -203,13 +203,13 @@ struct MessageView : CapabilityInquiryView { using CapabilityInquiryView::CapabilityInquiryView; - DeviceIdentity getIdentity() const { return sysex.makeDeviceIdentity (fieldOffsets.identity); } + DeviceIdentity getIdentity() const { return sysex.makeDeviceIdentity (fieldOffsets::identity); } - uint7_t getCategories() const { return sysex.data[fieldOffsets.categories]; } + uint7_t getCategories() const { return sysex.data[fieldOffsets::categories]; } - uint28_t getMaximumMessageSize() const { return sysex.makeUInt28 (fieldOffsets.maxMessageSize); } + uint28_t getMaximumMessageSize() const { return sysex.makeUInt28 (fieldOffsets::maxMessageSize); } - uint7_t getOutputPathId() const { return sysex.data[fieldOffsets.outputPathId]; } + uint7_t getOutputPathId() const { return sysex.data[fieldOffsets::outputPathId]; } struct fieldOffsets { @@ -228,7 +228,7 @@ struct DiscoveryInquiryView : Discovery::MessageView static bool validate (const SysEx7& message) { return isCapabilityInquiryMessage (message) - && message.data.size() >= (Discovery::MessageView::fieldOffsets.outputPathId + 1) + && message.data.size() >= (Discovery::MessageView::fieldOffsets::outputPathId + 1) && message.data[2] == Subtype::discoveryInquiry; } }; @@ -259,12 +259,12 @@ struct DiscoveryReplyView : Discovery::MessageView { using Discovery::MessageView::MessageView; - uint7_t getFunctionBlock() const { return sysex.data[fieldOffsets.functionBlock]; } + uint7_t getFunctionBlock() const { return sysex.data[fieldOffsets::functionBlock]; } static bool validate (const SysEx7& message) { return isCapabilityInquiryMessage (message) - && message.data.size() >= (fieldOffsets.functionBlock + 1) + && message.data.size() >= (fieldOffsets::functionBlock + 1) && message.data[2] == Subtype::discoveryReply; } @@ -311,20 +311,20 @@ struct EndpointInformationReplyView : CapabilityInquiryView { using CapabilityInquiryView::CapabilityInquiryView; - uint7_t getFirstFunctionBlock() const { return sysex.data[fieldOffsets.firstFunctionBlock]; } + uint7_t getFirstFunctionBlock() const { return sysex.data[fieldOffsets::firstFunctionBlock]; } - uint7_t getFunctionBlockCount() const { return sysex.data[fieldOffsets.functionBlockCount]; } + uint7_t getFunctionBlockCount() const { return sysex.data[fieldOffsets::functionBlockCount]; } - bool hasStaticFunctionBlocks() const { return (sysex.data[fieldOffsets.functionBlockCount] & 0x80u) != 0; } + bool hasStaticFunctionBlocks() const { return (sysex.data[fieldOffsets::functionBlockCount] & 0x80u) != 0; } - uint7_t getProtocol() const { return sysex.data[fieldOffsets.protocol]; } + uint7_t getProtocol() const { return sysex.data[fieldOffsets::protocol]; } - uint7_t getExtensions() const { return sysex.data[fieldOffsets.extensions]; } + uint7_t getExtensions() const { return sysex.data[fieldOffsets::extensions]; } static bool validate (const SysEx7& message) { return CapabilityInquiryView::validate (message) - && message.data.size() >= (fieldOffsets.extensions + 1) + && message.data.size() >= (fieldOffsets::extensions + 1) && message.data[2] == Subtype::endpointInformationReply; } @@ -357,14 +357,14 @@ struct AckView : CapabilityInquiryView { using CapabilityInquiryView::CapabilityInquiryView; - uint7_t getOriginalSubtype() const { return sysex.data[fieldOffsets.originalSubtype]; } + uint7_t getOriginalSubtype() const { return sysex.data[fieldOffsets::originalSubtype]; } - uint7_t getStatus() const { return sysex.data[fieldOffsets.status]; } + uint7_t getStatus() const { return sysex.data[fieldOffsets::status]; } static bool validate (const SysEx7& message) { return CapabilityInquiryView::validate (message) - && message.data.size() >= (fieldOffsets.status + 1) + && message.data.size() >= (fieldOffsets::status + 1) && message.data[2] == Subtype::ack; } @@ -387,14 +387,14 @@ struct NakView : CapabilityInquiryView { using CapabilityInquiryView::CapabilityInquiryView; - uint7_t getOriginalSubtype() const { return sysex.data[fieldOffsets.originalSubtype]; } + uint7_t getOriginalSubtype() const { return sysex.data[fieldOffsets::originalSubtype]; } - uint7_t getStatus() const { return sysex.data[fieldOffsets.status]; } + uint7_t getStatus() const { return sysex.data[fieldOffsets::status]; } static bool validate (const SysEx7& message) { return CapabilityInquiryView::validate (message) - && message.data.size() >= (fieldOffsets.status + 1) + && message.data.size() >= (fieldOffsets::status + 1) && message.data[2] == Subtype::nak; } @@ -429,6 +429,1448 @@ inline Message makeInvalidateMuid (Muid sourceMuid, Muid destinationMuid) return Message (Subtype::invalidateMuid, sourceMuid, destinationMuid); } +//============================================================================== +// Profile Configuration +namespace Subtype +{ +constexpr uint7_t profileInquiry = 0x20; +constexpr uint7_t profileInquiryReply = 0x21; +constexpr uint7_t setProfileOn = 0x22; +constexpr uint7_t setProfileOff = 0x23; +constexpr uint7_t profileEnabled = 0x24; +constexpr uint7_t profileDisabled = 0x25; +constexpr uint7_t profileAdded = 0x26; +constexpr uint7_t profileRemoved = 0x27; +constexpr uint7_t profileDetailsInquiry = 0x28; +constexpr uint7_t profileDetailsReply = 0x29; +constexpr uint7_t profileSpecificData = 0x2f; +} // namespace Subtype + +struct ProfileId +{ + uint7_t byte1 { 0x7e }; + uint7_t byte2 { 0x00 }; + uint7_t byte3 { 0x00 }; + uint7_t byte4 { 0x00 }; + uint7_t byte5 { 0x00 }; + + ProfileId() = default; + + ProfileId (uint7_t b1, uint7_t b2, uint7_t b3, uint7_t b4, uint7_t b5) + : byte1 (b1) + , byte2 (b2) + , byte3 (b3) + , byte4 (b4) + , byte5 (b5) + { + } +}; + +inline bool operator== (const ProfileId& a, const ProfileId& b) +{ + return a.byte1 == b.byte1 + && a.byte2 == b.byte2 + && a.byte3 == b.byte3 + && a.byte4 == b.byte4 + && a.byte5 == b.byte5; +} + +struct ProfileDestination +{ + uint7_t scope { 0x7f }; + uint14_t numChannels { 0 }; + + static constexpr uint7_t group = 0x7e; + static constexpr uint7_t functionBlock = 0x7f; +}; + +constexpr ProfileDestination makeChannelProfileDestination (uint7_t firstChannel, uint14_t numChannels = 1) +{ + return ProfileDestination { firstChannel, numChannels }; +} + +constexpr ProfileDestination groupProfileDestination { ProfileDestination::group, 0 }; +constexpr ProfileDestination functionBlockProfileDestination { ProfileDestination::functionBlock, 0 }; + +struct ProfileInquiryView : CapabilityInquiryView +{ + using CapabilityInquiryView::CapabilityInquiryView; + + static bool validate (const SysEx7& message) + { + return isCapabilityInquiryMessage (message) + && message.data.size() >= CapabilityInquiryView::fieldOffsets::payload + && message.data[2] == Subtype::profileInquiry; + } +}; + +inline Message makeProfileInquiryMessage (Muid sourceMuid, Muid destinationMuid, uint7_t deviceId = 0x7f) +{ + return Message (Subtype::profileInquiry, sourceMuid, destinationMuid, deviceId); +} + +struct ProfileInquiryReplyView : CapabilityInquiryView +{ + using CapabilityInquiryView::CapabilityInquiryView; + + bool hasEnabledProfiles() const + { + return ((sysex.data[fieldOffsets::numEnabledProfiles] + | sysex.data[fieldOffsets::numEnabledProfiles + 1]) + & 0x7f) + != 0; + } + + bool hasDisabledProfiles() const + { + const auto p = offsetOfDisabledProfiles(); + return ((sysex.data[p] | sysex.data[p + 1]) & 0x7f) != 0; + } + + uint14_t getNumEnabledProfiles() const { return sysex.makeUInt14 (fieldOffsets::numEnabledProfiles); } + + uint14_t getNumDisabledProfiles() const + { + const auto p = offsetOfDisabledProfiles(); + return sysex.makeUInt14 (p); + } + + std::vector getEnabledProfiles() const { return makeProfiles (fieldOffsets::numEnabledProfiles); } + + std::vector getDisabledProfiles() const { return makeProfiles (offsetOfDisabledProfiles()); } + + static bool validate (const SysEx7& message) + { + if (! isCapabilityInquiryMessage (message)) + return false; + + size_t expectedSize = fieldOffsets::minimumMessageSize; + if (message.data.size() < expectedSize) + return false; + + if (message.data[2] != Subtype::profileInquiryReply) + return false; + + expectedSize += message.makeUInt14 (fieldOffsets::numEnabledProfiles) * sizeof (ProfileId); + if (message.data.size() < expectedSize) + return false; + + expectedSize += message.makeUInt14 (expectedSize - 2) * sizeof (ProfileId); + return message.data.size() >= expectedSize; + } + + struct fieldOffsets + { + static constexpr size_t numEnabledProfiles = CapabilityInquiryView::fieldOffsets::payload; + static constexpr size_t numDisabledProfiles = CapabilityInquiryView::fieldOffsets::payload + 2u; + static constexpr size_t minimumMessageSize = CapabilityInquiryView::fieldOffsets::payload + 4u; + }; + +protected: + size_t offsetOfDisabledProfiles() const + { + return fieldOffsets::numDisabledProfiles + getNumEnabledProfiles() * sizeof (ProfileId); + } + + std::vector makeProfiles (size_t pos) const + { + std::vector result; + + jassert (sysex.data.size() > pos + 1); + const auto numProfiles = sysex.makeUInt14 (pos); + + jassert (sysex.data.size() > pos + 1 + numProfiles * sizeof (ProfileId)); + + result.reserve (numProfiles); + const auto* d = sysex.data.data() + pos + 2; + for (uint14_t i = 0; i < numProfiles; ++i) + { + result.push_back ({ d[0], d[1], d[2], d[3], d[4] }); + d += sizeof (ProfileId); + } + + return result; + } +}; + +inline Message makeProfileInquiryReply (Muid sourceMuid, + Muid destinationMuid, + const ProfileId* enabledProfiles, + uint14_t numEnabledProfiles, + const ProfileId* disabledProfiles, + uint14_t numDisabledProfiles, + uint7_t deviceId = 0x7f) +{ + jassert (numEnabledProfiles <= SysEx7::uint14Max); + jassert (numDisabledProfiles <= SysEx7::uint14Max); + + const size_t payloadSize = ProfileInquiryReplyView::fieldOffsets::minimumMessageSize + + (numEnabledProfiles + numDisabledProfiles) * sizeof (ProfileId) + - Message::offsetOfData; + + auto result = Message::makeWithPayloadSize (payloadSize, + Subtype::profileInquiryReply, + sourceMuid, + destinationMuid, + deviceId); + + auto addProfile = [&result] (const ProfileId& profile) + { + result.addUInt7 (profile.byte1); + result.addUInt7 (profile.byte2); + result.addUInt7 (profile.byte3); + result.addUInt7 (profile.byte4); + result.addUInt7 (profile.byte5); + }; + + result.addUInt14 (numEnabledProfiles); + for (uint14_t i = 0; i < numEnabledProfiles; ++i) + addProfile (enabledProfiles[i]); + + result.addUInt14 (numDisabledProfiles); + for (uint14_t i = 0; i < numDisabledProfiles; ++i) + addProfile (disabledProfiles[i]); + + return result; +} + +inline Message makeProfileInquiryReply (Muid sourceMuid, + Muid destinationMuid, + const std::vector& enabledProfiles, + const std::vector& disabledProfiles, + uint7_t deviceId = 0x7f) +{ + return makeProfileInquiryReply (sourceMuid, + destinationMuid, + enabledProfiles.data(), + uint14_t (enabledProfiles.size()), + disabledProfiles.data(), + uint14_t (disabledProfiles.size()), + deviceId); +} + +struct ProfileIdView : CapabilityInquiryView +{ + using CapabilityInquiryView::CapabilityInquiryView; + + ProfileId getProfile() const + { + return { sysex.data[fieldOffsets::profileId], + sysex.data[fieldOffsets::profileId + 1], + sysex.data[fieldOffsets::profileId + 2], + sysex.data[fieldOffsets::profileId + 3], + sysex.data[fieldOffsets::profileId + 4] }; + } + + static bool validate (const SysEx7& message) + { + return (message.manufacturerId == Manufacturer::universalNonRealtime) + && (message.data.size() >= fieldOffsets::profileId + 5u) + && (message.data[1] == 0x0d) + && (message.data[2] >= Subtype::setProfileOn) + && (message.data[2] <= Subtype::profileRemoved); + } + + struct fieldOffsets + { + static constexpr size_t profileId = CapabilityInquiryView::fieldOffsets::payload; + }; +}; + +struct ProfileDestinationView : ProfileIdView +{ + using ProfileIdView::ProfileIdView; + + uint14_t getNumChannels() const { return sysex.makeUInt14 (fieldOffsets::numChannels); } + + ProfileDestination getDestination() const { return ProfileDestination { getDeviceId(), getNumChannels() }; } + + static bool validate (const SysEx7& message) + { + return (message.manufacturerId == Manufacturer::universalNonRealtime) + && (message.data.size() >= fieldOffsets::profileId + 7u) + && (message.data[1] == 0x0d) + && (message.data[2] >= Subtype::setProfileOn) + && (message.data[2] <= Subtype::profileDisabled); + } + + struct fieldOffsets : ProfileIdView::fieldOffsets + { + static constexpr size_t numChannels = CapabilityInquiryView::fieldOffsets::payload + 5u; + }; +}; + +inline Message makeProfileOnRequest (Muid sourceMuid, + Muid destinationMuid, + const ProfileId& profile, + const ProfileDestination& destination) +{ + auto result = Message::makeWithPayloadSize (7, + Subtype::setProfileOn, + sourceMuid, + destinationMuid, + destination.scope); + result.addUInt7 (profile.byte1); + result.addUInt7 (profile.byte2); + result.addUInt7 (profile.byte3); + result.addUInt7 (profile.byte4); + result.addUInt7 (profile.byte5); + result.addUInt14 (destination.numChannels); + return result; +} + +inline Message makeProfileOffRequest (Muid sourceMuid, + Muid destinationMuid, + const ProfileId& profile, + const ProfileDestination& destination) +{ + auto result = Message::makeWithPayloadSize (7, + Subtype::setProfileOff, + sourceMuid, + destinationMuid, + destination.scope); + result.addUInt7 (profile.byte1); + result.addUInt7 (profile.byte2); + result.addUInt7 (profile.byte3); + result.addUInt7 (profile.byte4); + result.addUInt7 (profile.byte5); + result.addUInt14 (0); + return result; +} + +inline Message makeProfileEnabledNotification (Muid sourceMuid, + Muid destinationMuid, + const ProfileId& profile, + const ProfileDestination& destination) +{ + auto result = Message::makeWithPayloadSize (7, + Subtype::profileEnabled, + sourceMuid, + destinationMuid, + destination.scope); + result.addUInt7 (profile.byte1); + result.addUInt7 (profile.byte2); + result.addUInt7 (profile.byte3); + result.addUInt7 (profile.byte4); + result.addUInt7 (profile.byte5); + result.addUInt14 (destination.numChannels); + return result; +} + +inline Message makeProfileDisabledNotification (Muid sourceMuid, + Muid destinationMuid, + const ProfileId& profile, + const ProfileDestination& destination) +{ + auto result = Message::makeWithPayloadSize (7, + Subtype::profileDisabled, + sourceMuid, + destinationMuid, + destination.scope); + result.addUInt7 (profile.byte1); + result.addUInt7 (profile.byte2); + result.addUInt7 (profile.byte3); + result.addUInt7 (profile.byte4); + result.addUInt7 (profile.byte5); + result.addUInt14 (destination.numChannels); + return result; +} + +inline Message makeProfileAddedNotification (Muid sourceMuid, + Muid destinationMuid, + const ProfileId& profile, + uint7_t deviceId = 0x7f) +{ + auto result = Message::makeWithPayloadSize (5, + Subtype::profileAdded, + sourceMuid, + destinationMuid, + deviceId); + result.addUInt7 (profile.byte1); + result.addUInt7 (profile.byte2); + result.addUInt7 (profile.byte3); + result.addUInt7 (profile.byte4); + result.addUInt7 (profile.byte5); + return result; +} + +inline Message makeProfileRemovedNotification (Muid sourceMuid, + Muid destinationMuid, + const ProfileId& profile, + uint7_t deviceId = 0x7f) +{ + auto result = Message::makeWithPayloadSize (5, + Subtype::profileRemoved, + sourceMuid, + destinationMuid, + deviceId); + result.addUInt7 (profile.byte1); + result.addUInt7 (profile.byte2); + result.addUInt7 (profile.byte3); + result.addUInt7 (profile.byte4); + result.addUInt7 (profile.byte5); + return result; +} + +struct ProfileDetailsInquiryView : CapabilityInquiryView +{ + using CapabilityInquiryView::CapabilityInquiryView; + + ProfileId getProfile() const + { + return { sysex.data[fieldOffsets::profileId], + sysex.data[fieldOffsets::profileId + 1], + sysex.data[fieldOffsets::profileId + 2], + sysex.data[fieldOffsets::profileId + 3], + sysex.data[fieldOffsets::profileId + 4] }; + } + + uint7_t getTarget() const { return sysex.data[fieldOffsets::target]; } + + static bool validate (const SysEx7& message) + { + return (message.manufacturerId == Manufacturer::universalNonRealtime) + && (message.data.size() >= fieldOffsets::target + 1u) + && (message.data[1] == 0x0d) + && (message.data[2] == Subtype::profileDetailsInquiry); + } + + struct fieldOffsets + { + static constexpr size_t profileId = CapabilityInquiryView::fieldOffsets::payload; + static constexpr size_t target = CapabilityInquiryView::fieldOffsets::payload + 5u; + }; +}; + +inline Message makeProfileDetailsInquiry (Muid sourceMuid, + Muid destinationMuid, + const ProfileId& profile, + uint7_t target, + uint7_t deviceId = 0x7f) +{ + auto result = Message::makeWithPayloadSize (6, + Subtype::profileDetailsInquiry, + sourceMuid, + destinationMuid, + deviceId); + result.addUInt7 (profile.byte1); + result.addUInt7 (profile.byte2); + result.addUInt7 (profile.byte3); + result.addUInt7 (profile.byte4); + result.addUInt7 (profile.byte5); + result.addUInt7 (target); + return result; +} + +struct ProfileDetailsReplyView : CapabilityInquiryView +{ + using CapabilityInquiryView::CapabilityInquiryView; + + ProfileId getProfile() const + { + return { sysex.data[fieldOffsets::profileId], + sysex.data[fieldOffsets::profileId + 1], + sysex.data[fieldOffsets::profileId + 2], + sysex.data[fieldOffsets::profileId + 3], + sysex.data[fieldOffsets::profileId + 4] }; + } + + uint7_t getTarget() const { return sysex.data[fieldOffsets::target]; } + + uint14_t getTargetDataLength() const { return sysex.makeUInt14 (fieldOffsets::targetDataLength); } + + const uint7_t* getTargetData() const { return sysex.data.data() + fieldOffsets::targetData; } + + static bool validate (const SysEx7& message) + { + return (message.manufacturerId == Manufacturer::universalNonRealtime) + && (message.data.size() >= fieldOffsets::targetData) + && (message.data.size() >= fieldOffsets::targetData + + message.makeUInt14 (fieldOffsets::targetDataLength)) + && (message.data[1] == 0x0d) + && (message.data[2] == Subtype::profileDetailsReply); + } + + struct fieldOffsets + { + static constexpr size_t profileId = CapabilityInquiryView::fieldOffsets::payload; + static constexpr size_t target = CapabilityInquiryView::fieldOffsets::payload + 5u; + static constexpr size_t targetDataLength = CapabilityInquiryView::fieldOffsets::payload + 6u; + static constexpr size_t targetData = CapabilityInquiryView::fieldOffsets::payload + 8u; + }; +}; + +inline Message makeProfileDetailsReply (Muid sourceMuid, + Muid destinationMuid, + const ProfileId& profile, + uint7_t target, + const uint7_t* data, + size_t dataSize, + uint7_t deviceId = 0x7f) +{ + auto result = Message::makeWithPayloadSize (8 + dataSize, + Subtype::profileDetailsReply, + sourceMuid, + destinationMuid, + deviceId); + result.addUInt7 (profile.byte1); + result.addUInt7 (profile.byte2); + result.addUInt7 (profile.byte3); + result.addUInt7 (profile.byte4); + result.addUInt7 (profile.byte5); + result.addUInt7 (target); + result.addUInt14 (uint14_t (dataSize)); + if (dataSize != 0 && data != nullptr) + result.addData (data, dataSize); + return result; +} + +struct ProfileSpecificDataView : CapabilityInquiryView +{ + using CapabilityInquiryView::CapabilityInquiryView; + + size_t getDataSize() const { return sysex.makeUInt28 (fieldOffsets::dataSize); } + + const uint7_t* getDataBegin() const { return sysex.data.data() + fieldOffsets::data; } + + const uint7_t* getDataEnd() const { return getDataBegin() + getDataSize(); } + + static bool validate (const SysEx7& message) + { + return (message.manufacturerId == Manufacturer::universalNonRealtime) + && (message.data.size() >= fieldOffsets::data) + && (message.data.size() >= fieldOffsets::data + message.makeUInt28 (fieldOffsets::dataSize)) + && (message.data[1] == 0x0d) + && (message.data[2] == Subtype::profileSpecificData); + } + + struct fieldOffsets + { + static constexpr size_t profileId = CapabilityInquiryView::fieldOffsets::payload; + static constexpr size_t dataSize = CapabilityInquiryView::fieldOffsets::payload + 5u; + static constexpr size_t data = CapabilityInquiryView::fieldOffsets::payload + 9u; + }; +}; + +inline Message makeProfileSpecificDataMessage (Muid sourceMuid, + Muid destinationMuid, + const ProfileId& profile, + const uint7_t* data, + size_t dataSize, + uint7_t deviceId = 0x7f) +{ + auto result = Message::makeWithPayloadSize (9 + dataSize, + Subtype::profileSpecificData, + sourceMuid, + destinationMuid, + deviceId); + result.addUInt7 (profile.byte1); + result.addUInt7 (profile.byte2); + result.addUInt7 (profile.byte3); + result.addUInt7 (profile.byte4); + result.addUInt7 (profile.byte5); + result.addUInt28 (uint28_t (dataSize)); + if (dataSize != 0 && data != nullptr) + result.addData (data, dataSize); + return result; +} + +inline Message makeProfileSpecificDataMessage (Muid sourceMuid, + Muid destinationMuid, + const ProfileId& profile, + const std::vector& data, + uint7_t deviceId = 0x7f) +{ + return makeProfileSpecificDataMessage (sourceMuid, destinationMuid, profile, data.data(), data.size(), deviceId); +} + +//============================================================================== +// Property Exchange +namespace Subtype +{ +constexpr uint7_t propertyExchangeCapabilitiesInquiry = 0x30; +constexpr uint7_t propertyExchangeCapabilitiesReply = 0x31; +constexpr uint7_t getPropertyDataInquiry = 0x34; +constexpr uint7_t getPropertyDataReply = 0x35; +constexpr uint7_t setPropertyDataInquiry = 0x36; +constexpr uint7_t setPropertyDataReply = 0x37; +constexpr uint7_t subscriptionInquiry = 0x38; +constexpr uint7_t subscriptionReply = 0x39; +constexpr uint7_t notify = 0x3f; +} // namespace Subtype + +namespace propertyExchange +{ +constexpr uint7_t versionMajor = 0x00; +constexpr uint7_t versionMinor = 0x00; + +struct HeaderOptions +{ + std::vector> options; +}; + +struct Tags +{ + static constexpr std::string_view resource { "resource" }; + static constexpr std::string_view command { "command" }; + static constexpr std::string_view status { "status" }; + static constexpr std::string_view id { "id" }; + static constexpr std::string_view offset { "offset" }; + static constexpr std::string_view limit { "limit" }; + static constexpr std::string_view encoding { "encoding" }; + static constexpr std::string_view message { "message" }; + static constexpr std::string_view subscribeId { "subscribeId" }; +}; + +inline std::string makeRjson (std::string_view key, std::string_view value) +{ + return std::string { "{\"" } + std::string { key } + "\":\"" + std::string { value } + "\"}"; +} + +inline std::string makeRjson (std::string_view key, int value) +{ + return std::string { "{\"" } + std::string { key } + "\":" + std::to_string (value) + "}"; +} + +namespace detail +{ +inline bool isNumber (std::string_view s) +{ + if (s.empty()) + return false; + + if (s[0] == '-') + s.remove_prefix (1); + + while (! s.empty()) + { + if ((s[0] < '0') || (s[0] > '9')) + return false; + s.remove_prefix (1); + } + + return true; +} +} // namespace detail + +inline std::string makeRjson (std::string_view key, std::string_view value, const HeaderOptions& options) +{ + auto result = std::string { "{\"" } + std::string { key } + "\":\"" + std::string { value } + "\""; + for (const auto& o : options.options) + { + if (detail::isNumber (o.second)) + result += ",\"" + std::string { o.first } + "\":" + std::string { o.second }; + else + result += ",\"" + std::string { o.first } + "\":\"" + std::string { o.second } + "\""; + } + result.push_back ('}'); + return result; +} + +inline std::string makeRjson (std::string_view key, int value, const HeaderOptions& options) +{ + auto result = std::string { "{\"" } + std::string { key } + "\":" + std::to_string (value); + for (const auto& o : options.options) + { + if (detail::isNumber (o.second)) + result += ",\"" + std::string { o.first } + "\":" + std::string { o.second }; + else + result += ",\"" + std::string { o.first } + "\":\"" + std::string { o.second } + "\""; + } + result.push_back ('}'); + return result; +} + +struct PrivateDataView +{ + const uint7_t* data { nullptr }; + size_t size { 0 }; + + PrivateDataView() = default; + + PrivateDataView (const uint7_t* d, size_t s) + : data (d) + , size (s) + { + } + + explicit PrivateDataView (const std::vector& d) + : data (d.data()) + , size (d.size()) + { + } + + explicit PrivateDataView (std::string_view d) + : data (reinterpret_cast (d.data())) + , size (d.length()) + { + } + + std::string_view asStringView() const + { + return std::string_view (reinterpret_cast (data), size); + } +}; + +struct Header : PrivateDataView +{ + using PrivateDataView::PrivateDataView; +}; + +struct Chunk : PrivateDataView +{ + using PrivateDataView::PrivateDataView; +}; + +inline Message makeCapabilitiesMessage (uint7_t subtype, + Muid sourceMuid, + Muid destinationMuid, + uint7_t maxNumRequests, + uint7_t deviceId) +{ + auto result = Message::makeWithPayloadSize (3, subtype, sourceMuid, destinationMuid, deviceId); + const uint7_t data[] = { maxNumRequests, versionMajor, versionMinor }; + result.addData (data, sizeof (data)); + return result; +} + +inline Message makePropertyDataMessage (uint7_t subtype, + Muid sourceMuid, + Muid destinationMuid, + const Header& header, + uint14_t numberOfChunks, + uint14_t numberOfThisChunk, + const Chunk& chunk, + uint7_t requestId = 0, + uint7_t deviceId = 0x7f) +{ + auto result = Message::makeWithPayloadSize (9 + header.size + chunk.size, + subtype, + sourceMuid, + destinationMuid, + deviceId); + + jassert (numberOfThisChunk || ((chunk.data == nullptr) && (chunk.size == 0))); + jassert ((numberOfChunks == 0) || (numberOfThisChunk <= numberOfChunks)); + jassert (! header.size || header.data); + jassert (! chunk.size || chunk.data); + + result.addUInt7 (requestId); + result.addUInt14 (uint14_t (header.size)); + if (header.size) + result.addData (header.data, header.size); + + result.addUInt14 (numberOfChunks); + result.addUInt14 (numberOfThisChunk); + + result.addUInt14 (uint14_t (chunk.size)); + if (chunk.size) + result.addData (chunk.data, chunk.size); + + return result; +} + +struct PropertyDataMessageView : CapabilityInquiryView +{ + using CapabilityInquiryView::CapabilityInquiryView; + + uint7_t getRequestId() const { return sysex.data[fieldOffsets::requestId]; } + + const uint7_t* getHeaderBegin() const { return sysex.data.data() + fieldOffsets::headerData; } + + const uint7_t* getHeaderEnd() const { return getHeaderBegin() + getHeaderSize(); } + + size_t getHeaderSize() const { return sysex.makeUInt14 (fieldOffsets::headerSize); } + + uint14_t getNumberOfChunks() const { return sysex.makeUInt14 (fieldOffsets::numChunks + getHeaderSize()); } + + uint14_t getNumberOfThisChunk() const { return sysex.makeUInt14 (fieldOffsets::thisChunk + getHeaderSize()); } + + const uint7_t* getChunkBegin() const { return sysex.data.data() + fieldOffsets::chunkData + getHeaderSize(); } + + const uint7_t* getChunkEnd() const { return getChunkBegin() + getChunkSize(); } + + size_t getChunkSize() const { return sysex.makeUInt14 (fieldOffsets::chunkSize + getHeaderSize()); } + + static bool validate (const SysEx7& message) + { + constexpr size_t minMessageSize = fieldOffsets::chunkData; + + if ((message.manufacturerId != Manufacturer::universalNonRealtime) + || (message.data.size() < minMessageSize) + || (message.data[1] != 0x0d) + || (message.data[2] < Subtype::getPropertyDataInquiry)) + return false; + + if ((message.data[2] <= Subtype::subscriptionReply) || (message.data[2] == Subtype::notify)) + { + const auto headerBytes = message.makeUInt14 (fieldOffsets::headerSize); + if (message.data.size() < size_t (fieldOffsets::headerData + headerBytes + 6)) + return false; + + const auto numChunks = message.makeUInt14 (fieldOffsets::numChunks + headerBytes); + const auto curChunk = message.makeUInt14 (fieldOffsets::thisChunk + headerBytes); + const auto chunkBytes = message.makeUInt14 (fieldOffsets::chunkSize + headerBytes); + + if (numChunks && (curChunk > numChunks)) + return false; + + if (headerBytes && (curChunk > 1)) + return false; + if (chunkBytes && (curChunk < 1)) + return false; + + return (minMessageSize + headerBytes + chunkBytes <= message.data.size()); + } + + return false; + } + + struct fieldOffsets + { + static constexpr size_t requestId = CapabilityInquiryView::fieldOffsets::payload; + static constexpr size_t headerSize = CapabilityInquiryView::fieldOffsets::payload + 1u; + static constexpr size_t headerData = CapabilityInquiryView::fieldOffsets::payload + 3u; + static constexpr size_t numChunks = CapabilityInquiryView::fieldOffsets::payload + 3u; + static constexpr size_t thisChunk = CapabilityInquiryView::fieldOffsets::payload + 5u; + static constexpr size_t chunkSize = CapabilityInquiryView::fieldOffsets::payload + 7u; + static constexpr size_t chunkData = CapabilityInquiryView::fieldOffsets::payload + 9u; + }; +}; + +} // namespace propertyExchange + +struct PropertyExchangeCapabilitiesView : CapabilityInquiryView +{ + using CapabilityInquiryView::CapabilityInquiryView; + + uint7_t getMaximumNumberOfRequests() const { return sysex.data[fieldOffsets::maxNumRequests]; } + + uint7_t getMajorVersion() const + { + return (getMessageVersion() >= messageVersion2) ? sysex.data[fieldOffsets::versionMajor] : 0; + } + + uint7_t getMinorVersion() const + { + return (getMessageVersion() >= messageVersion2) ? sysex.data[fieldOffsets::versionMinor] : 0; + } + + static bool validate (const SysEx7& message) + { + return isCapabilityInquiryMessage (message) + && message.data.size() >= (fieldOffsets::maxNumRequests + 1u) + && (message.data[2] >= Subtype::propertyExchangeCapabilitiesInquiry) + && (message.data[2] <= Subtype::propertyExchangeCapabilitiesReply) + && ((message.data[3] < messageVersion2) || (message.data.size() >= fieldOffsets::versionMinor + 1u)); + } + + struct fieldOffsets + { + static constexpr size_t maxNumRequests = CapabilityInquiryView::fieldOffsets::payload; + static constexpr size_t versionMajor = CapabilityInquiryView::fieldOffsets::payload + 1u; + static constexpr size_t versionMinor = CapabilityInquiryView::fieldOffsets::payload + 2u; + }; +}; + +inline Message makePropertyExchangeCapabilitiesInquiry (Muid sourceMuid, + Muid destinationMuid, + uint7_t maxNumRequests = 1, + uint7_t deviceId = 0x7f) +{ + return propertyExchange::makeCapabilitiesMessage (Subtype::propertyExchangeCapabilitiesInquiry, + sourceMuid, + destinationMuid, + maxNumRequests, + deviceId); +} + +inline Message makePropertyExchangeCapabilitiesReply (Muid sourceMuid, + Muid destinationMuid, + uint7_t maxNumRequests = 1, + uint7_t deviceId = 0x7f) +{ + return propertyExchange::makeCapabilitiesMessage (Subtype::propertyExchangeCapabilitiesReply, + sourceMuid, + destinationMuid, + maxNumRequests, + deviceId); +} + +struct GetPropertyDataView : propertyExchange::PropertyDataMessageView +{ + using propertyExchange::PropertyDataMessageView::PropertyDataMessageView; + + static bool validate (const SysEx7& message) + { + return propertyExchange::PropertyDataMessageView::validate (message) + && (message.data[2] == Subtype::getPropertyDataInquiry); + } +}; + +inline Message makeGetPropertyDataInquiry (Muid sourceMuid, + Muid destinationMuid, + std::string_view resource, + uint7_t requestId = 0, + uint7_t deviceId = 0x7f) +{ + propertyExchange::Header header { propertyExchange::makeRjson (propertyExchange::Tags::resource, resource) }; + return propertyExchange::makePropertyDataMessage (Subtype::getPropertyDataInquiry, + sourceMuid, + destinationMuid, + header, + 0, + 0, + {}, + requestId, + deviceId); +} + +inline Message makeGetPropertyDataInquiry (Muid sourceMuid, + Muid destinationMuid, + std::string_view resource, + const propertyExchange::HeaderOptions& options, + uint7_t requestId = 0, + uint7_t deviceId = 0x7f) +{ + propertyExchange::Header header { propertyExchange::makeRjson (propertyExchange::Tags::resource, + resource, + options) }; + return propertyExchange::makePropertyDataMessage (Subtype::getPropertyDataInquiry, + sourceMuid, + destinationMuid, + header, + 0, + 0, + {}, + requestId, + deviceId); +} + +inline Message makeGetPropertyDataReply (Muid sourceMuid, + Muid destinationMuid, + const propertyExchange::Header& header, + uint14_t numberOfChunks, + uint14_t numberOfThisChunk, + const propertyExchange::Chunk& chunk, + uint7_t requestId = 0, + uint7_t deviceId = 0x7f) +{ + return propertyExchange::makePropertyDataMessage (Subtype::getPropertyDataReply, + sourceMuid, + destinationMuid, + header, + numberOfChunks, + numberOfThisChunk, + chunk, + requestId, + deviceId); +} + +inline Message makeGetPropertyDataReply (Muid sourceMuid, + Muid destinationMuid, + int status, + uint14_t numberOfChunks, + uint14_t numberOfThisChunk, + const propertyExchange::Chunk& chunk, + uint7_t requestId = 0, + uint7_t deviceId = 0x7f) +{ + propertyExchange::Header header { propertyExchange::makeRjson (propertyExchange::Tags::status, status) }; + return makeGetPropertyDataReply (sourceMuid, + destinationMuid, + header, + numberOfChunks, + numberOfThisChunk, + chunk, + requestId, + deviceId); +} + +inline Message makeGetPropertyDataReply (Muid sourceMuid, + Muid destinationMuid, + int status, + std::string_view message, + uint14_t numberOfChunks, + uint14_t numberOfThisChunk, + const propertyExchange::Chunk& chunk, + uint7_t requestId = 0, + uint7_t deviceId = 0x7f) +{ + propertyExchange::HeaderOptions options; + options.options.emplace_back (propertyExchange::Tags::message, message); + propertyExchange::Header header { propertyExchange::makeRjson (propertyExchange::Tags::status, + status, + options) }; + return makeGetPropertyDataReply (sourceMuid, + destinationMuid, + header, + numberOfChunks, + numberOfThisChunk, + chunk, + requestId, + deviceId); +} + +inline Message makeGetPropertyDataReply (Muid sourceMuid, + Muid destinationMuid, + uint14_t numberOfChunks, + uint14_t numberOfThisChunk, + const propertyExchange::Chunk& chunk, + uint7_t requestId = 0, + uint7_t deviceId = 0x7f) +{ + propertyExchange::Header header; + return makeGetPropertyDataReply (sourceMuid, + destinationMuid, + header, + numberOfChunks, + numberOfThisChunk, + chunk, + requestId, + deviceId); +} + +struct SetPropertyDataView : propertyExchange::PropertyDataMessageView +{ + using propertyExchange::PropertyDataMessageView::PropertyDataMessageView; + + static bool validate (const SysEx7& message) + { + return propertyExchange::PropertyDataMessageView::validate (message) + && (message.data[2] == Subtype::setPropertyDataInquiry); + } +}; + +inline Message makeSetPropertyDataInquiry (Muid sourceMuid, + Muid destinationMuid, + std::string_view resource, + uint14_t numberOfChunks, + uint14_t numberOfThisChunk, + const propertyExchange::Chunk& chunk, + uint7_t requestId = 0, + uint7_t deviceId = 0x7f) +{ + propertyExchange::Header header { propertyExchange::makeRjson (propertyExchange::Tags::resource, resource) }; + return propertyExchange::makePropertyDataMessage (Subtype::setPropertyDataInquiry, + sourceMuid, + destinationMuid, + header, + numberOfChunks, + numberOfThisChunk, + chunk, + requestId, + deviceId); +} + +inline Message makeSetPropertyDataInquiry (Muid sourceMuid, + Muid destinationMuid, + std::string_view resource, + const propertyExchange::HeaderOptions& options, + uint14_t numberOfChunks, + uint14_t numberOfThisChunk, + const propertyExchange::Chunk& chunk, + uint7_t requestId = 0, + uint7_t deviceId = 0x7f) +{ + propertyExchange::Header header { propertyExchange::makeRjson (propertyExchange::Tags::resource, + resource, + options) }; + return propertyExchange::makePropertyDataMessage (Subtype::setPropertyDataInquiry, + sourceMuid, + destinationMuid, + header, + numberOfChunks, + numberOfThisChunk, + chunk, + requestId, + deviceId); +} + +inline Message makeSetPropertyDataInquiry (Muid sourceMuid, + Muid destinationMuid, + uint14_t numberOfChunks, + uint14_t numberOfThisChunk, + const propertyExchange::Chunk& chunk, + uint7_t requestId = 0, + uint7_t deviceId = 0x7f) +{ + propertyExchange::Header header; + return propertyExchange::makePropertyDataMessage (Subtype::setPropertyDataInquiry, + sourceMuid, + destinationMuid, + header, + numberOfChunks, + numberOfThisChunk, + chunk, + requestId, + deviceId); +} + +inline Message makeSetPropertyDataReply (Muid sourceMuid, + Muid destinationMuid, + int status, + uint7_t requestId = 0, + uint7_t deviceId = 0x7f) +{ + propertyExchange::Header header { propertyExchange::makeRjson (propertyExchange::Tags::status, status) }; + return propertyExchange::makePropertyDataMessage (Subtype::setPropertyDataReply, + sourceMuid, + destinationMuid, + header, + 0, + 0, + {}, + requestId, + deviceId); +} + +inline Message makeSetPropertyDataReply (Muid sourceMuid, + Muid destinationMuid, + int status, + std::string_view message, + uint7_t requestId = 0, + uint7_t deviceId = 0x7f) +{ + propertyExchange::HeaderOptions options; + options.options.emplace_back (propertyExchange::Tags::message, message); + propertyExchange::Header header { propertyExchange::makeRjson (propertyExchange::Tags::status, + status, + options) }; + return propertyExchange::makePropertyDataMessage (Subtype::setPropertyDataReply, + sourceMuid, + destinationMuid, + header, + 0, + 0, + {}, + requestId, + deviceId); +} + +struct SubscriptionView : propertyExchange::PropertyDataMessageView +{ + using propertyExchange::PropertyDataMessageView::PropertyDataMessageView; + + static bool validate (const SysEx7& message) + { + return propertyExchange::PropertyDataMessageView::validate (message) + && (message.data[2] == Subtype::subscriptionInquiry); + } +}; + +inline Message makeSubscriptionInquiry (Muid sourceMuid, + Muid destinationMuid, + std::string_view resource, + std::string_view command, + std::string_view subscribeId, + uint7_t requestId = 0, + uint7_t deviceId = 0x7f) +{ + propertyExchange::HeaderOptions options; + options.options.emplace_back (propertyExchange::Tags::command, command); + options.options.emplace_back (propertyExchange::Tags::subscribeId, subscribeId); + propertyExchange::Header header { propertyExchange::makeRjson (propertyExchange::Tags::resource, + resource, + options) }; + return propertyExchange::makePropertyDataMessage (Subtype::subscriptionInquiry, + sourceMuid, + destinationMuid, + header, + 0, + 0, + {}, + requestId, + deviceId); +} + +inline Message makeSubscriptionInquiry (Muid sourceMuid, + Muid destinationMuid, + std::string_view resource, + std::string_view command, + std::string_view subscribeId, + uint14_t numberOfChunks, + uint14_t numberOfThisChunk, + const propertyExchange::Chunk& chunk, + uint7_t requestId = 0, + uint7_t deviceId = 0x7f) +{ + propertyExchange::HeaderOptions options; + options.options.emplace_back (propertyExchange::Tags::command, command); + options.options.emplace_back (propertyExchange::Tags::subscribeId, subscribeId); + propertyExchange::Header header { propertyExchange::makeRjson (propertyExchange::Tags::resource, + resource, + options) }; + return propertyExchange::makePropertyDataMessage (Subtype::subscriptionInquiry, + sourceMuid, + destinationMuid, + header, + numberOfChunks, + numberOfThisChunk, + chunk, + requestId, + deviceId); +} + +inline Message makeSubscriptionInquiry (Muid sourceMuid, + Muid destinationMuid, + uint14_t numberOfChunks, + uint14_t numberOfThisChunk, + const propertyExchange::Chunk& chunk, + uint7_t requestId = 0, + uint7_t deviceId = 0x7f) +{ + propertyExchange::Header header; + return propertyExchange::makePropertyDataMessage (Subtype::subscriptionInquiry, + sourceMuid, + destinationMuid, + header, + numberOfChunks, + numberOfThisChunk, + chunk, + requestId, + deviceId); +} + +inline Message makeSubscriptionReply (Muid sourceMuid, + Muid destinationMuid, + int status, + uint7_t requestId = 0, + uint7_t deviceId = 0x7f) +{ + propertyExchange::Header header { propertyExchange::makeRjson (propertyExchange::Tags::status, status) }; + return propertyExchange::makePropertyDataMessage (Subtype::subscriptionReply, + sourceMuid, + destinationMuid, + header, + 0, + 0, + {}, + requestId, + deviceId); +} + +inline Message makeSubscriptionReply (Muid sourceMuid, + Muid destinationMuid, + int status, + std::string_view message, + uint7_t requestId = 0, + uint7_t deviceId = 0x7f) +{ + propertyExchange::HeaderOptions options; + options.options.emplace_back (propertyExchange::Tags::message, message); + propertyExchange::Header header { propertyExchange::makeRjson (propertyExchange::Tags::status, + status, + options) }; + return propertyExchange::makePropertyDataMessage (Subtype::subscriptionReply, + sourceMuid, + destinationMuid, + header, + 0, + 0, + {}, + requestId, + deviceId); +} + +struct NotifyView : propertyExchange::PropertyDataMessageView +{ + using propertyExchange::PropertyDataMessageView::PropertyDataMessageView; + + static bool validate (const SysEx7& message) + { + return propertyExchange::PropertyDataMessageView::validate (message) + && (message.data[2] == Subtype::notify); + } +}; + +inline Message makeNotifyMessage (Muid sourceMuid, + Muid destinationMuid, + int status, + uint7_t requestId = 0, + uint7_t deviceId = 0x7f) +{ + propertyExchange::Header header { propertyExchange::makeRjson (propertyExchange::Tags::status, status) }; + return propertyExchange::makePropertyDataMessage (Subtype::notify, + sourceMuid, + destinationMuid, + header, + 0, + 0, + {}, + requestId, + deviceId); +} + +inline Message makeNotifyMessage (Muid sourceMuid, + Muid destinationMuid, + int status, + std::string_view message, + uint7_t requestId = 0, + uint7_t deviceId = 0x7f) +{ + propertyExchange::HeaderOptions options; + options.options.emplace_back (propertyExchange::Tags::message, message); + propertyExchange::Header header { propertyExchange::makeRjson (propertyExchange::Tags::status, + status, + options) }; + return propertyExchange::makePropertyDataMessage (Subtype::notify, + sourceMuid, + destinationMuid, + header, + 0, + 0, + {}, + requestId, + deviceId); +} + +//============================================================================== +// Process Inquiry +namespace Subtype +{ +constexpr uint7_t processInquiryCapabilitiesInquiry = 0x40; +constexpr uint7_t processInquiryCapabilitiesReply = 0x41; +constexpr uint7_t midiMessageReportInquiry = 0x42; +constexpr uint7_t midiMessageReportReply = 0x43; +constexpr uint7_t midiMessageReportEnd = 0x44; +} // namespace Subtype + +inline Message makeProcessInquiryCapabilitiesInquiry (Muid sourceMuid, + Muid destinationMuid, + uint7_t deviceId = 0x7f) +{ + return Message (Subtype::processInquiryCapabilitiesInquiry, sourceMuid, destinationMuid, deviceId); +} + +struct ProcessInquiryCapabilitiesReplyView : CapabilityInquiryView +{ + using CapabilityInquiryView::CapabilityInquiryView; + + uint7_t getSupportedFeatures() const { return sysex.data[fieldOffsets::supportedFeatures]; } + + static bool validate (const SysEx7& message) + { + return isCapabilityInquiryMessage (message) + && message.data.size() >= (fieldOffsets::supportedFeatures + 1) + && message.data[2] == Subtype::processInquiryCapabilitiesReply; + } + + struct fieldOffsets + { + static constexpr size_t supportedFeatures = CapabilityInquiryView::fieldOffsets::payload; + }; +}; + +inline Message makeProcessInquiryCapabilitiesReply (Muid sourceMuid, + Muid destinationMuid, + uint7_t features, + uint7_t deviceId = 0x7f) +{ + auto result = Message::makeWithPayloadSize (1, + Subtype::processInquiryCapabilitiesReply, + sourceMuid, + destinationMuid, + deviceId); + result.addUInt7 (features); + return result; +} + +struct MidiMessageReportInquiryView : CapabilityInquiryView +{ + using CapabilityInquiryView::CapabilityInquiryView; + + uint7_t getMessageDataControl() const { return sysex.data[fieldOffsets::messageDataControl]; } + + uint7_t getSystemMessageTypes() const { return sysex.data[fieldOffsets::systemMessageTypes]; } + + uint7_t getChannelControllerMessageTypes() const { return sysex.data[fieldOffsets::channelControllerMessageTypes]; } + + uint7_t getNoteDataMessageTypes() const { return sysex.data[fieldOffsets::noteDataMessageTypes]; } + + static bool validate (const SysEx7& message) + { + return isCapabilityInquiryMessage (message) + && message.data.size() >= (fieldOffsets::noteDataMessageTypes + 1) + && message.data[2] == Subtype::midiMessageReportInquiry; + } + + struct fieldOffsets + { + static constexpr size_t messageDataControl = CapabilityInquiryView::fieldOffsets::payload; + static constexpr size_t systemMessageTypes = CapabilityInquiryView::fieldOffsets::payload + 1u; + static constexpr size_t channelControllerMessageTypes = CapabilityInquiryView::fieldOffsets::payload + 2u; + static constexpr size_t noteDataMessageTypes = CapabilityInquiryView::fieldOffsets::payload + 3u; + }; +}; + +inline Message makeMidiMessageReportInquiry (Muid sourceMuid, + Muid destinationMuid, + uint7_t dataControl, + uint7_t systemMessages, + uint7_t channelControllerMessages, + uint7_t noteDataMessages, + uint7_t deviceId = 0x7f) +{ + auto result = Message::makeWithPayloadSize (4, + Subtype::midiMessageReportInquiry, + sourceMuid, + destinationMuid, + deviceId); + result.addUInt7 (dataControl); + result.addUInt7 (systemMessages); + result.addUInt7 (channelControllerMessages); + result.addUInt7 (noteDataMessages); + return result; +} + +struct MidiMessageReportReplyView : CapabilityInquiryView +{ + using CapabilityInquiryView::CapabilityInquiryView; + + uint7_t getSystemMessageTypes() const { return sysex.data[fieldOffsets::systemMessageTypes]; } + + uint7_t getChannelControllerMessageTypes() const { return sysex.data[fieldOffsets::channelControllerMessageTypes]; } + + uint7_t getNoteDataMessageTypes() const { return sysex.data[fieldOffsets::noteDataMessageTypes]; } + + static bool validate (const SysEx7& message) + { + return isCapabilityInquiryMessage (message) + && message.data.size() >= (fieldOffsets::noteDataMessageTypes + 1) + && message.data[2] == Subtype::midiMessageReportReply; + } + + struct fieldOffsets + { + static constexpr size_t systemMessageTypes = CapabilityInquiryView::fieldOffsets::payload; + static constexpr size_t channelControllerMessageTypes = CapabilityInquiryView::fieldOffsets::payload + 1u; + static constexpr size_t noteDataMessageTypes = CapabilityInquiryView::fieldOffsets::payload + 2u; + }; +}; + +inline Message makeMidiMessageReportReply (Muid sourceMuid, + uint7_t systemMessages, + uint7_t channelControllerMessages, + uint7_t noteDataMessages, + uint7_t deviceId = 0x7f) +{ + auto result = Message::makeWithPayloadSize (3, + Subtype::midiMessageReportReply, + sourceMuid, + broadcastMuid, + deviceId); + result.addUInt7 (systemMessages); + result.addUInt7 (channelControllerMessages); + result.addUInt7 (noteDataMessages); + return result; +} + +inline Message makeMidiMessageReportEnd (Muid sourceMuid, uint7_t deviceId = 0x7f) +{ + return Message (Subtype::midiMessageReportEnd, sourceMuid, broadcastMuid, deviceId); +} + } // namespace ci } // namespace yup::ump diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.cpp b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.cpp index 07aadc7b9..84d92ac9a 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.cpp +++ b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.cpp @@ -151,7 +151,11 @@ void Midi1ByteStreamParser::systemCommon (uint8_t byte) void Midi1ByteStreamParser::channelVoice (uint8_t byte) { - packet = makeMidi1ChannelVoiceMessage (group, Status (byte)); + packet = makeMidi1ChannelVoiceMessage (group, + Status (byte & 0xf0), + Channel (byte & 0x0f), + 0, + 0); packetByte = 2; numMissingBytes = 0; diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPUniversalSysEx.h b/modules/yup_audio_basics/midi/ump/yup_UMPUniversalSysEx.h index 3172669c9..5720e63eb 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPUniversalSysEx.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPUniversalSysEx.h @@ -24,6 +24,8 @@ namespace yup::ump { +inline bool isUniversalSysExMessage (const SysEx7& message); + namespace UniversalSysEx { diff --git a/modules/yup_audio_basics/yup_audio_basics.cpp b/modules/yup_audio_basics/yup_audio_basics.cpp index d1b04c541..c63d78458 100644 --- a/modules/yup_audio_basics/yup_audio_basics.cpp +++ b/modules/yup_audio_basics/yup_audio_basics.cpp @@ -66,6 +66,8 @@ #include "midi/yup_MidiRPN.cpp" #include "midi/ump/yup_UMPPacketBuffer.cpp" #include "midi/ump/yup_UMPKeyboardState.cpp" +#include "midi/ump/yup_UMPMidi1ByteStream.cpp" +#include "midi/ump/yup_UMPJitterReductionTimestamps.cpp" #include "mpe/yup_MPEValue.cpp" #include "mpe/yup_MPENote.cpp" #include "mpe/yup_MPEZoneLayout.cpp" From 7d6d638ecc0628d80ad5e94c43a4dd7a71b251d2 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 22 Dec 2025 10:33:12 +0100 Subject: [PATCH 05/20] More tests fixes --- .../midi/ump/yup_UMPChannelVoice.h | 82 ++++++------ .../midi/ump/yup_UMPDataMessages.h | 10 +- .../midi/ump/yup_UMPExtendedDataMessages.h | 12 +- .../midi/ump/yup_UMPFlexDataMessages.h | 4 +- .../midi/ump/yup_UMPMessages.h | 64 ++++----- .../midi/ump/yup_UMPMidi1ByteStream.cpp | 38 +++--- .../midi/ump/yup_UMPMidi1ByteStream.h | 36 ++--- .../midi/ump/yup_UMPStreamMessages.h | 60 ++++----- .../midi/ump/yup_UMPSysExCollectors.h | 20 +-- .../midi/ump/yup_UMPUniversalPacket.h | 124 +++++++++--------- 10 files changed, 225 insertions(+), 225 deletions(-) diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPChannelVoice.h b/modules/yup_audio_basics/midi/ump/yup_UMPChannelVoice.h index d58a55569..b1ca1e652 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPChannelVoice.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPChannelVoice.h @@ -34,9 +34,9 @@ constexpr bool isChannelVoiceMessageWithStatus (const UniversalPacket& p, Status constexpr bool isNoteOnMessage (const UniversalPacket& p) { - if (isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::note_on))) + if (isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::noteOn))) { - return (p.getType() == PacketType::midi2_channel_voice) || (p.getByte4() != 0); + return (p.getType() == PacketType::midi2ChannelVoice) || (p.getByte4() != 0); } return false; @@ -44,11 +44,11 @@ constexpr bool isNoteOnMessage (const UniversalPacket& p) constexpr bool isNoteOffMessage (const UniversalPacket& p) { - if (isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::note_off))) + if (isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::noteOff))) return true; - return (p.getType() == PacketType::midi1_channel_voice) - && ((p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::note_on)) + return (p.getType() == PacketType::midi1ChannelVoice) + && ((p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::noteOn)) && (p.getByte4() == 0); } @@ -59,8 +59,8 @@ constexpr NoteNumber getNoteNumber (const UniversalPacket& p) constexpr Pitch7_9 getNotePitch (const UniversalPacket& p) { - if ((p.getType() == PacketType::midi2_channel_voice) - && ((p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::note_on)) + if ((p.getType() == PacketType::midi2ChannelVoice) + && ((p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::noteOn)) && (p.getByte4() == NoteAttribute::pitch_7_9)) { return Pitch7_9 { uint16_t (p.data[1] & 0xffff) }; @@ -71,11 +71,11 @@ constexpr Pitch7_9 getNotePitch (const UniversalPacket& p) constexpr Velocity getNoteVelocity (const UniversalPacket& p) { - if (p.getType() == PacketType::midi2_channel_voice) + if (p.getType() == PacketType::midi2ChannelVoice) return Velocity { uint16_t (p.data[1] >> 16) }; - if ((p.getType() == PacketType::midi1_channel_voice) - && ((p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::note_on)) + if ((p.getType() == PacketType::midi1ChannelVoice) + && ((p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::noteOn)) && (p.getByte4() == 0)) { return Velocity { uint7_t { 64 } }; @@ -86,7 +86,7 @@ constexpr Velocity getNoteVelocity (const UniversalPacket& p) constexpr bool isPolyPressureMessage (const UniversalPacket& p) { - return isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::poly_pressure)); + return isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::polyPressure)); } constexpr ControllerValue getPolyPressureValue (const UniversalPacket& p) @@ -96,7 +96,7 @@ constexpr ControllerValue getPolyPressureValue (const UniversalPacket& p) constexpr bool isControlChangeMessage (const UniversalPacket& p) { - return isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::control_change)); + return isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::controlChange)); } constexpr ControllerNumber getControllerNumber (const UniversalPacket& p) @@ -106,7 +106,7 @@ constexpr ControllerNumber getControllerNumber (const UniversalPacket& p) constexpr ControllerValue getControllerValue (const UniversalPacket& p) { - if (p.getType() == PacketType::midi1_channel_voice) + if (p.getType() == PacketType::midi1ChannelVoice) return ControllerValue { uint7_t (p.getByte4() & 0x7f) }; return ControllerValue { p.data[1] }; @@ -114,16 +114,16 @@ constexpr ControllerValue getControllerValue (const UniversalPacket& p) constexpr bool isProgramChangeMessage (const UniversalPacket& p) { - return isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::program_change)); + return isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::programChange)); } constexpr uint7_t getProgramValue (const UniversalPacket& p) { switch (p.getType()) { - case PacketType::midi1_channel_voice: + case PacketType::midi1ChannelVoice: return uint7_t (p.getByte3() & 0x7f); - case PacketType::midi2_channel_voice: + case PacketType::midi2ChannelVoice: return uint7_t ((p.data[1] >> 24u) & 0x7f); default: break; @@ -134,12 +134,12 @@ constexpr uint7_t getProgramValue (const UniversalPacket& p) constexpr bool isChannelPressureMessage (const UniversalPacket& p) { - return isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::channel_pressure)); + return isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::channelPressure)); } constexpr ControllerValue getChannelPressureValue (const UniversalPacket& p) { - if (p.getType() == PacketType::midi1_channel_voice) + if (p.getType() == PacketType::midi1ChannelVoice) return ControllerValue { uint7_t (p.getByte3() & 0x7f) }; return ControllerValue { p.data[1] }; @@ -147,12 +147,12 @@ constexpr ControllerValue getChannelPressureValue (const UniversalPacket& p) constexpr bool isChannelPitchBendMessage (const UniversalPacket& p) { - return isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::pitch_bend)); + return isChannelVoiceMessageWithStatus (p, Status (ChannelVoiceStatus::pitchBend)); } constexpr PitchBend getChannelPitchBendValue (const UniversalPacket& p) { - if (p.getType() == PacketType::midi1_channel_voice) + if (p.getType() == PacketType::midi1ChannelVoice) return PitchBend { uint14_t (p.getByte3() | (p.getByte4() << 7)) }; return PitchBend { p.data[1] }; @@ -163,11 +163,11 @@ constexpr std::optional asMidi1ChannelVoiceMessage (co { switch (m.getStatus()) { - case Status (ChannelVoiceStatus::note_off): + case Status (ChannelVoiceStatus::noteOff): if (m.getByte4() == 0) return makeMidi1NoteOffMessage (m.getGroup(), m.getChannel(), m.getByte3(), Velocity { uint16_t (m.getData() >> 16) }); break; - case Status (ChannelVoiceStatus::note_on): + case Status (ChannelVoiceStatus::noteOn): if (m.getByte4() == 0) { auto vel = Velocity { uint16_t (m.getData() >> 16) }; @@ -176,9 +176,9 @@ constexpr std::optional asMidi1ChannelVoiceMessage (co return makeMidi1NoteOnMessage (m.getGroup(), m.getChannel(), m.getByte3(), vel); } break; - case Status (ChannelVoiceStatus::poly_pressure): + case Status (ChannelVoiceStatus::polyPressure): return makeMidi1PolyPressureMessage (m.getGroup(), m.getChannel(), m.getByte3(), ControllerValue { m.getData() }); - case Status (ChannelVoiceStatus::control_change): + case Status (ChannelVoiceStatus::controlChange): switch (m.getByte3()) { case ControlChange::bankSelectMsb: @@ -195,13 +195,13 @@ constexpr std::optional asMidi1ChannelVoiceMessage (co return makeMidi1ControlChangeMessage (m.getGroup(), m.getChannel(), m.getByte3(), ControllerValue { m.getData() }); } break; - case Status (ChannelVoiceStatus::program_change): + case Status (ChannelVoiceStatus::programChange): if ((m.getByte4() & 0x1) == 0) return makeMidi1ProgramChangeMessage (m.getGroup(), m.getChannel(), uint7_t (m.getData() >> 24)); break; - case Status (ChannelVoiceStatus::channel_pressure): + case Status (ChannelVoiceStatus::channelPressure): return makeMidi1ChannelPressureMessage (m.getGroup(), m.getChannel(), ControllerValue { m.getData() }); - case Status (ChannelVoiceStatus::pitch_bend): + case Status (ChannelVoiceStatus::pitchBend): return makeMidi1PitchBendMessage (m.getGroup(), m.getChannel(), PitchBend { m.getData() }); default: break; @@ -214,15 +214,15 @@ constexpr std::optional asMidi2ChannelVoiceMessage (co { switch (m.getStatus()) { - case Status (ChannelVoiceStatus::note_off): + case Status (ChannelVoiceStatus::noteOff): return makeMidi2NoteOffMessage (m.getGroup(), m.getChannel(), m.getDataByte1(), Velocity { m.getDataByte2() }); - case Status (ChannelVoiceStatus::note_on): + case Status (ChannelVoiceStatus::noteOn): if (m.getDataByte2() == 0) return makeMidi2NoteOffMessage (m.getGroup(), m.getChannel(), m.getDataByte1(), Velocity { uint7_t { 64 } }); return makeMidi2NoteOnMessage (m.getGroup(), m.getChannel(), m.getDataByte1(), Velocity { m.getDataByte2() }); - case Status (ChannelVoiceStatus::poly_pressure): + case Status (ChannelVoiceStatus::polyPressure): return makeMidi2PolyPressureMessage (m.getGroup(), m.getChannel(), m.getDataByte1(), ControllerValue { m.getDataByte2() }); - case Status (ChannelVoiceStatus::control_change): + case Status (ChannelVoiceStatus::controlChange): switch (m.getDataByte1()) { case ControlChange::bankSelectMsb: @@ -239,11 +239,11 @@ constexpr std::optional asMidi2ChannelVoiceMessage (co return makeMidi2ControlChangeMessage (m.getGroup(), m.getChannel(), m.getDataByte1(), ControllerValue { m.getDataByte2() }); } break; - case Status (ChannelVoiceStatus::program_change): + case Status (ChannelVoiceStatus::programChange): return makeMidi2ProgramChangeMessage (m.getGroup(), m.getChannel(), m.getDataByte1()); - case Status (ChannelVoiceStatus::channel_pressure): + case Status (ChannelVoiceStatus::channelPressure): return makeMidi2ChannelPressureMessage (m.getGroup(), m.getChannel(), ControllerValue { m.getDataByte1() }); - case Status (ChannelVoiceStatus::pitch_bend): + case Status (ChannelVoiceStatus::pitchBend): return makeMidi2PitchBendMessage (m.getGroup(), m.getChannel(), PitchBend { m.get14BitValue() }); default: break; @@ -256,19 +256,19 @@ constexpr std::optional asMidi2ChannelVoiceMessage (co constexpr bool isRegisteredControllerMessage (const UniversalPacket& p) { return isMidi2ChannelVoiceMessage (p) - && (p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::registered_controller); + && (p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::registeredController); } constexpr bool isAssignableControllerMessage (const UniversalPacket& p) { return isMidi2ChannelVoiceMessage (p) - && (p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::assignable_controller); + && (p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::assignableController); } constexpr bool isRegisteredPerNoteControllerMessage (const UniversalPacket& p) { return isMidi2ChannelVoiceMessage (p) - && (p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::registered_per_note_controller); + && (p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::registeredPerNoteController); } constexpr bool isRegisteredPerNoteControllerPitchMessage (const UniversalPacket& p) @@ -280,26 +280,26 @@ constexpr bool isRegisteredPerNoteControllerPitchMessage (const UniversalPacket& constexpr bool isAssignablePerNoteControllerMessage (const UniversalPacket& p) { return isMidi2ChannelVoiceMessage (p) - && (p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::assignable_per_note_controller); + && (p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::assignablePerNoteController); } constexpr bool isPerNotePitchBendMessage (const UniversalPacket& p) { return isMidi2ChannelVoiceMessage (p) - && (p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::per_note_pitch_bend); + && (p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::perNotePitchBend); } constexpr bool isNoteOnWithAttribute (const UniversalPacket& p, uint8_t attribute) { return isMidi2ChannelVoiceMessage (p) - && ((p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::note_on)) + && ((p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::noteOn)) && (p.getByte4() == attribute); } constexpr bool isNoteOffWithAttribute (const UniversalPacket& p, uint8_t attribute) { return isMidi2ChannelVoiceMessage (p) - && ((p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::note_off)) + && ((p.getStatus() & 0xf0) == uint8_t (ChannelVoiceStatus::noteOff)) && (p.getByte4() == attribute); } diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPDataMessages.h b/modules/yup_audio_basics/midi/ump/yup_UMPDataMessages.h index 55f09d4e5..27366093c 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPDataMessages.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPDataMessages.h @@ -78,7 +78,7 @@ constexpr bool isDataMessage (const UniversalPacket& p) constexpr bool isSysEx7Packet (const UniversalPacket& p) { return isDataMessage (p) - && ((p.getStatus() & 0xf0) <= Status (DataStatus::sysex7_end)) + && ((p.getStatus() & 0xf0) <= Status (DataStatus::sysex7End)) && ((p.getStatus() & 0x0f) <= 6); } @@ -114,22 +114,22 @@ constexpr std::optional asSysEx7PacketView (const UniversalPac constexpr SysEx7Packet makeSysEx7CompletePacket (Group group = 0) { - return SysEx7Packet { Status (DataStatus::sysex7_complete), group }; + return SysEx7Packet { Status (DataStatus::sysex7Complete), group }; } constexpr SysEx7Packet makeSysEx7StartPacket (Group group = 0) { - return SysEx7Packet { Status (DataStatus::sysex7_start), group }; + return SysEx7Packet { Status (DataStatus::sysex7Start), group }; } constexpr SysEx7Packet makeSysEx7ContinuePacket (Group group = 0) { - return SysEx7Packet { Status (DataStatus::sysex7_continue), group }; + return SysEx7Packet { Status (DataStatus::sysex7Continue), group }; } constexpr SysEx7Packet makeSysEx7EndPacket (Group group = 0) { - return SysEx7Packet { Status (DataStatus::sysex7_end), group }; + return SysEx7Packet { Status (DataStatus::sysex7End), group }; } } // namespace yup::ump diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPExtendedDataMessages.h b/modules/yup_audio_basics/midi/ump/yup_UMPExtendedDataMessages.h index e5bc8531b..e84a0bcd7 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPExtendedDataMessages.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPExtendedDataMessages.h @@ -82,13 +82,13 @@ struct SysEx8Packet : ExtendedDataMessage constexpr bool isExtendedDataMessage (const UniversalPacket& p) { - return p.getType() == PacketType::extended_data; + return p.getType() == PacketType::extendedData; } constexpr bool isSysEx8Packet (const UniversalPacket& p) { return isExtendedDataMessage (p) - && ((p.getStatus() & 0xf0) <= Status (ExtendedDataStatus::sysex8_end)) + && ((p.getStatus() & 0xf0) <= Status (ExtendedDataStatus::sysex8End)) && ((p.getStatus() & 0x0f) > 0) && ((p.getStatus() & 0x0f) <= 14); } @@ -129,22 +129,22 @@ constexpr std::optional asSysEx8PacketView (const UniversalPac constexpr SysEx8Packet makeSysEx8CompletePacket (uint8_t streamId, Group group = 0) { - return SysEx8Packet { Status (ExtendedDataStatus::sysex8_complete), streamId, group }; + return SysEx8Packet { Status (ExtendedDataStatus::sysex8Complete), streamId, group }; } constexpr SysEx8Packet makeSysEx8StartPacket (uint8_t streamId, Group group = 0) { - return SysEx8Packet { Status (ExtendedDataStatus::sysex8_start), streamId, group }; + return SysEx8Packet { Status (ExtendedDataStatus::sysex8Start), streamId, group }; } constexpr SysEx8Packet makeSysEx8ContinuePacket (uint8_t streamId, Group group = 0) { - return SysEx8Packet { Status (ExtendedDataStatus::sysex8_continue), streamId, group }; + return SysEx8Packet { Status (ExtendedDataStatus::sysex8Continue), streamId, group }; } constexpr SysEx8Packet makeSysEx8EndPacket (uint8_t streamId, Group group = 0) { - return SysEx8Packet { Status (ExtendedDataStatus::sysex8_end), streamId, group }; + return SysEx8Packet { Status (ExtendedDataStatus::sysex8End), streamId, group }; } } // namespace yup::ump diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPFlexDataMessages.h b/modules/yup_audio_basics/midi/ump/yup_UMPFlexDataMessages.h index 386e794d7..3f999f3c0 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPFlexDataMessages.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPFlexDataMessages.h @@ -85,7 +85,7 @@ struct FlexDataMessage : UniversalPacket constexpr bool isFlexDataMessage (const UniversalPacket& p) { - return p.getType() == PacketType::flex_data; + return p.getType() == PacketType::flexData; } struct FlexDataMessageView @@ -93,7 +93,7 @@ struct FlexDataMessageView constexpr explicit FlexDataMessageView (const UniversalPacket& ump) : p (ump) { - jassert (p.getType() == PacketType::flex_data); + jassert (p.getType() == PacketType::flexData); } constexpr Group getGroup() const { return p.getGroup(); } diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPMessages.h b/modules/yup_audio_basics/midi/ump/yup_UMPMessages.h index a6e114bff..63a52e26f 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPMessages.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPMessages.h @@ -94,7 +94,7 @@ struct SystemMessageView constexpr uint14_t getSongPosition() const { - if (SystemStatus (p.getStatus()) == SystemStatus::song_position) + if (SystemStatus (p.getStatus()) == SystemStatus::songPosition) return uint14_t (getDataByte1() | (uint14_t (getDataByte2()) << 7u)); return 0; @@ -120,7 +120,7 @@ constexpr SystemMessage makeSystemMessage (Group group, Status status, uint7_t d constexpr SystemMessage makeSongPositionMessage (Group group, uint14_t position) { return makeSystemMessage (group, - Status (SystemStatus::song_position), + Status (SystemStatus::songPosition), static_cast (position & 0x7f), static_cast ((position >> 7) & 0x7f)); } @@ -139,13 +139,13 @@ struct Midi1ChannelVoiceMessage : UniversalPacket constexpr explicit Midi1ChannelVoiceMessage (const UniversalPacket& p) : UniversalPacket (p) { - jassert (p.getType() == PacketType::midi1_channel_voice); + jassert (p.getType() == PacketType::midi1ChannelVoice); } }; constexpr bool isMidi1ChannelVoiceMessage (const UniversalPacket& p) { - return p.getType() == PacketType::midi1_channel_voice; + return p.getType() == PacketType::midi1ChannelVoice; } struct Midi1ChannelVoiceMessageView @@ -201,7 +201,7 @@ constexpr Midi1ChannelVoiceMessage makeMidi1NoteOffMessage (Group group, Velocity vel = {}) { return makeMidi1ChannelVoiceMessage (group, - Status (Midi1ChannelVoiceStatus::note_off), + Status (Midi1ChannelVoiceStatus::noteOff), channel, noteNr, vel.asUInt7()); @@ -213,7 +213,7 @@ constexpr Midi1ChannelVoiceMessage makeMidi1NoteOnMessage (Group group, Velocity vel) { return makeMidi1ChannelVoiceMessage (group, - Status (Midi1ChannelVoiceStatus::note_on), + Status (Midi1ChannelVoiceStatus::noteOn), channel, noteNr, vel.asUInt7()); @@ -225,7 +225,7 @@ constexpr Midi1ChannelVoiceMessage makeMidi1PolyPressureMessage (Group group, ControllerValue pressure) { return makeMidi1ChannelVoiceMessage (group, - Status (Midi1ChannelVoiceStatus::poly_pressure), + Status (Midi1ChannelVoiceStatus::polyPressure), channel, noteNr, pressure.asUInt7()); @@ -237,7 +237,7 @@ constexpr Midi1ChannelVoiceMessage makeMidi1ControlChangeMessage (Group group, ControllerValue value) { return makeMidi1ChannelVoiceMessage (group, - Status (Midi1ChannelVoiceStatus::control_change), + Status (Midi1ChannelVoiceStatus::controlChange), channel, controller, value.asUInt7()); @@ -248,7 +248,7 @@ constexpr Midi1ChannelVoiceMessage makeMidi1ProgramChangeMessage (Group group, ProgramNumber program) { return makeMidi1ChannelVoiceMessage (group, - Status (Midi1ChannelVoiceStatus::program_change), + Status (Midi1ChannelVoiceStatus::programChange), channel, program); } @@ -258,7 +258,7 @@ constexpr Midi1ChannelVoiceMessage makeMidi1ChannelPressureMessage (Group group, ControllerValue pressure) { return makeMidi1ChannelVoiceMessage (group, - Status (Midi1ChannelVoiceStatus::channel_pressure), + Status (Midi1ChannelVoiceStatus::channelPressure), channel, pressure.asUInt7(), 0); @@ -270,7 +270,7 @@ constexpr Midi1ChannelVoiceMessage makeMidi1PitchBendMessage (Group group, { const auto pb14 = pb.asUInt14(); return makeMidi1ChannelVoiceMessage (group, - Status (Midi1ChannelVoiceStatus::pitch_bend), + Status (Midi1ChannelVoiceStatus::pitchBend), channel, uint7_t (pb14 & 0x7f), uint7_t ((pb14 >> 7) & 0x7f)); @@ -297,13 +297,13 @@ struct Midi2ChannelVoiceMessage : UniversalPacket constexpr explicit Midi2ChannelVoiceMessage (const UniversalPacket& p) : UniversalPacket (p) { - jassert (p.getType() == PacketType::midi2_channel_voice); + jassert (p.getType() == PacketType::midi2ChannelVoice); } }; constexpr bool isMidi2ChannelVoiceMessage (const UniversalPacket& p) { - return p.getType() == PacketType::midi2_channel_voice; + return p.getType() == PacketType::midi2ChannelVoice; } struct Midi2ChannelVoiceMessageView @@ -311,7 +311,7 @@ struct Midi2ChannelVoiceMessageView constexpr explicit Midi2ChannelVoiceMessageView (const UniversalPacket& ump) : p (ump) { - jassert (p.getType() == PacketType::midi2_channel_voice); + jassert (p.getType() == PacketType::midi2ChannelVoice); } constexpr Group getGroup() const { return p.getGroup(); } @@ -373,7 +373,7 @@ constexpr Midi2ChannelVoiceMessage makeMidi2NoteOffMessage (Group group, uint16_t attributeData = 0) { return Midi2ChannelVoiceMessage { group, - Status (ChannelVoiceStatus::note_off), + Status (ChannelVoiceStatus::noteOff), channel, noteNr, attribute, @@ -386,7 +386,7 @@ constexpr Midi2ChannelVoiceMessage makeMidi2NoteOnMessage (Group group, Velocity vel) { return Midi2ChannelVoiceMessage { group, - Status (ChannelVoiceStatus::note_on), + Status (ChannelVoiceStatus::noteOn), channel, noteNr, 0, @@ -400,7 +400,7 @@ constexpr Midi2ChannelVoiceMessage makeMidi2NoteOnMessage (Group group, Pitch7_9 pitch) { return Midi2ChannelVoiceMessage { group, - Status (ChannelVoiceStatus::note_on), + Status (ChannelVoiceStatus::noteOn), channel, noteNr, NoteAttribute::pitch_7_9, @@ -415,7 +415,7 @@ constexpr Midi2ChannelVoiceMessage makeMidi2NoteOnMessage (Group group, uint16_t attributeData) { return Midi2ChannelVoiceMessage { group, - Status (ChannelVoiceStatus::note_on), + Status (ChannelVoiceStatus::noteOn), channel, noteNr, attribute, @@ -428,7 +428,7 @@ constexpr Midi2ChannelVoiceMessage makeMidi2PolyPressureMessage (Group group, ControllerValue pressure) { return Midi2ChannelVoiceMessage { group, - Status (ChannelVoiceStatus::poly_pressure), + Status (ChannelVoiceStatus::polyPressure), channel, noteNr, 0, @@ -442,7 +442,7 @@ constexpr Midi2ChannelVoiceMessage makeRegisteredPerNoteControllerMessage (Group ControllerValue value) { return Midi2ChannelVoiceMessage { group, - Status (ChannelVoiceStatus::registered_per_note_controller), + Status (ChannelVoiceStatus::registeredPerNoteController), channel, noteNr, controller, @@ -456,7 +456,7 @@ constexpr Midi2ChannelVoiceMessage makeAssignablePerNoteControllerMessage (Group ControllerValue value) { return Midi2ChannelVoiceMessage { group, - Status (ChannelVoiceStatus::assignable_per_note_controller), + Status (ChannelVoiceStatus::assignablePerNoteController), channel, noteNr, controller, @@ -469,7 +469,7 @@ constexpr Midi2ChannelVoiceMessage makePerNoteManagementMessage (Group group, NoteManagementFlags flags) { return Midi2ChannelVoiceMessage { group, - Status (ChannelVoiceStatus::per_note_management), + Status (ChannelVoiceStatus::perNoteManagement), channel, noteNr, flags, @@ -482,7 +482,7 @@ constexpr Midi2ChannelVoiceMessage makeMidi2ControlChangeMessage (Group group, ControllerValue value) { return Midi2ChannelVoiceMessage { group, - Status (ChannelVoiceStatus::control_change), + Status (ChannelVoiceStatus::controlChange), channel, controller, 0, @@ -496,7 +496,7 @@ constexpr Midi2ChannelVoiceMessage makeRegisteredControllerMessage (Group group, ControllerValue value) { return Midi2ChannelVoiceMessage { group, - Status (ChannelVoiceStatus::registered_controller), + Status (ChannelVoiceStatus::registeredController), channel, bank, index, @@ -510,7 +510,7 @@ constexpr Midi2ChannelVoiceMessage makeAssignableControllerMessage (Group group, ControllerValue value) { return Midi2ChannelVoiceMessage { group, - Status (ChannelVoiceStatus::assignable_controller), + Status (ChannelVoiceStatus::assignableController), channel, bank, index, @@ -524,7 +524,7 @@ constexpr Midi2ChannelVoiceMessage makeRelativeRegisteredControllerMessage (Grou ControllerIncrement inc) { return Midi2ChannelVoiceMessage { group, - Status (ChannelVoiceStatus::relative_registered_controller), + Status (ChannelVoiceStatus::relativeRegisteredController), channel, bank, index, @@ -538,7 +538,7 @@ constexpr Midi2ChannelVoiceMessage makeRelativeAssignableControllerMessage (Grou ControllerIncrement inc) { return Midi2ChannelVoiceMessage { group, - Status (ChannelVoiceStatus::relative_assignable_controller), + Status (ChannelVoiceStatus::relativeAssignableController), channel, bank, index, @@ -550,7 +550,7 @@ constexpr Midi2ChannelVoiceMessage makeMidi2ProgramChangeMessage (Group group, ProgramNumber program) { return Midi2ChannelVoiceMessage { group, - Status (ChannelVoiceStatus::program_change), + Status (ChannelVoiceStatus::programChange), channel, 0, 0, @@ -563,7 +563,7 @@ constexpr Midi2ChannelVoiceMessage makeMidi2ProgramChangeMessage (Group group, uint14_t bank) { return Midi2ChannelVoiceMessage { group, - Status (ChannelVoiceStatus::program_change), + Status (ChannelVoiceStatus::programChange), channel, 0, 1, @@ -575,7 +575,7 @@ constexpr Midi2ChannelVoiceMessage makeMidi2ChannelPressureMessage (Group group, ControllerValue pressure) { return Midi2ChannelVoiceMessage { group, - Status (ChannelVoiceStatus::channel_pressure), + Status (ChannelVoiceStatus::channelPressure), channel, 0, 0, @@ -587,7 +587,7 @@ constexpr Midi2ChannelVoiceMessage makeMidi2PitchBendMessage (Group group, PitchBend bend) { return Midi2ChannelVoiceMessage { group, - Status (ChannelVoiceStatus::pitch_bend), + Status (ChannelVoiceStatus::pitchBend), channel, 0, 0, @@ -600,7 +600,7 @@ constexpr Midi2ChannelVoiceMessage makePerNotePitchBendMessage (Group group, PitchBend bend) { return Midi2ChannelVoiceMessage { group, - Status (ChannelVoiceStatus::per_note_pitch_bend), + Status (ChannelVoiceStatus::perNotePitchBend), channel, noteNr, 0, diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.cpp b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.cpp index 84d92ac9a..fed44e7e8 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.cpp +++ b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.cpp @@ -126,14 +126,14 @@ void Midi1ByteStreamParser::systemCommon (uint8_t byte) switch (packet.getStatus()) { - case Status (SystemStatus::mtc_quarter_frame): - case Status (SystemStatus::song_select): + case Status (SystemStatus::mtcQuarterFrame): + case Status (SystemStatus::songSelect): numMissingBytes = 1; break; - case Status (SystemStatus::song_position): + case Status (SystemStatus::songPosition): numMissingBytes = 2; break; - case Status (SystemStatus::tune_request): + case Status (SystemStatus::tuneRequest): break; default: packet = UniversalPacket {}; @@ -161,15 +161,15 @@ void Midi1ByteStreamParser::channelVoice (uint8_t byte) switch (packet.getStatus() & 0xf0) { - case Status (Midi1ChannelVoiceStatus::note_off): - case Status (Midi1ChannelVoiceStatus::note_on): - case Status (Midi1ChannelVoiceStatus::poly_pressure): - case Status (Midi1ChannelVoiceStatus::control_change): - case Status (Midi1ChannelVoiceStatus::pitch_bend): + case Status (Midi1ChannelVoiceStatus::noteOff): + case Status (Midi1ChannelVoiceStatus::noteOn): + case Status (Midi1ChannelVoiceStatus::polyPressure): + case Status (Midi1ChannelVoiceStatus::controlChange): + case Status (Midi1ChannelVoiceStatus::pitchBend): numMissingBytes = 2; break; - case Status (Midi1ChannelVoiceStatus::program_change): - case Status (Midi1ChannelVoiceStatus::channel_pressure): + case Status (Midi1ChannelVoiceStatus::programChange): + case Status (Midi1ChannelVoiceStatus::channelPressure): numMissingBytes = 1; break; default: @@ -261,7 +261,7 @@ void Midi1ByteStreamParser::sysExEndPacket() const auto currentSysExStatus = uint8_t (packet.getStatus() & 0xf0); const auto currentPacketSize = uint8_t ((packetByte - 2) & 0x0f); - if (currentSysExStatus == uint8_t (DataStatus::sysex7_start)) + if (currentSysExStatus == uint8_t (DataStatus::sysex7Start)) { if (packetByte < 3) { @@ -269,11 +269,11 @@ void Midi1ByteStreamParser::sysExEndPacket() return; } - packet.setByte (1, uint8_t (DataStatus::sysex7_complete) + currentPacketSize); + packet.setByte (1, uint8_t (DataStatus::sysex7Complete) + currentPacketSize); } else { - packet.setByte (1, uint8_t (DataStatus::sysex7_end) + currentPacketSize); + packet.setByte (1, uint8_t (DataStatus::sysex7End) + currentPacketSize); } if (invokeCallbacks) @@ -291,7 +291,7 @@ size_t toMidi1ByteStream (const UniversalPacket& packet, uint8_t result[8]) switch (packet.getType()) { case PacketType::system: - case PacketType::midi1_channel_voice: + case PacketType::midi1ChannelVoice: if (payloadBytes > 0) { for (; resultBytes < payloadBytes; ++resultBytes) @@ -303,15 +303,15 @@ size_t toMidi1ByteStream (const UniversalPacket& packet, uint8_t result[8]) { const auto status = packet.getStatus() & 0xf0; - if ((status == Status (DataStatus::sysex7_complete)) - || (status == Status (DataStatus::sysex7_start))) + if ((status == Status (DataStatus::sysex7Complete)) + || (status == Status (DataStatus::sysex7Start))) result[resultBytes++] = 0xf0; for (size_t b = 0; b < payloadBytes; ++b) result[resultBytes++] = packet.getByte (2 + b); - if ((status == Status (DataStatus::sysex7_complete)) - || (status == Status (DataStatus::sysex7_end))) + if ((status == Status (DataStatus::sysex7Complete)) + || (status == Status (DataStatus::sysex7End))) result[resultBytes++] = 0xf7; } break; diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.h b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.h index b080cb3d8..584d650c8 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ByteStream.h @@ -126,42 +126,42 @@ constexpr size_t getMidi1ByteStreamSize (const UniversalPacket& packet) case PacketType::system: switch (SystemStatus (packet.getStatus())) { - case SystemStatus::song_position: + case SystemStatus::songPosition: return 3u; - case SystemStatus::mtc_quarter_frame: - case SystemStatus::song_select: + case SystemStatus::mtcQuarterFrame: + case SystemStatus::songSelect: return 2u; - case SystemStatus::tune_request: + case SystemStatus::tuneRequest: case SystemStatus::clock: case SystemStatus::start: case SystemStatus::cont: case SystemStatus::stop: - case SystemStatus::active_sense: + case SystemStatus::activeSense: case SystemStatus::reset: return 1u; } break; - case PacketType::midi1_channel_voice: + case PacketType::midi1ChannelVoice: switch (packet.getStatus() & 0xf0) { - case Status (Midi1ChannelVoiceStatus::note_off): - case Status (Midi1ChannelVoiceStatus::note_on): - case Status (Midi1ChannelVoiceStatus::poly_pressure): - case Status (Midi1ChannelVoiceStatus::control_change): - case Status (Midi1ChannelVoiceStatus::pitch_bend): + case Status (Midi1ChannelVoiceStatus::noteOff): + case Status (Midi1ChannelVoiceStatus::noteOn): + case Status (Midi1ChannelVoiceStatus::polyPressure): + case Status (Midi1ChannelVoiceStatus::controlChange): + case Status (Midi1ChannelVoiceStatus::pitchBend): return 3u; - case Status (Midi1ChannelVoiceStatus::program_change): - case Status (Midi1ChannelVoiceStatus::channel_pressure): + case Status (Midi1ChannelVoiceStatus::programChange): + case Status (Midi1ChannelVoiceStatus::channelPressure): return 2u; } break; case PacketType::data: switch (packet.getStatus() & 0xf0) { - case Status (DataStatus::sysex7_complete): - case Status (DataStatus::sysex7_start): - case Status (DataStatus::sysex7_end): - case Status (DataStatus::sysex7_continue): + case Status (DataStatus::sysex7Complete): + case Status (DataStatus::sysex7Start): + case Status (DataStatus::sysex7End): + case Status (DataStatus::sysex7Continue): if ((packet.getStatus() & 0x0f) <= 6) return packet.getStatus() & 0x0f; break; @@ -176,7 +176,7 @@ constexpr size_t getMidi1ByteStreamSize (const UniversalPacket& packet) constexpr UniversalPacket fromMidi1ByteStream (uint8_t status, uint7_t data1, uint7_t data2) { - auto type = PacketType::midi1_channel_voice; + auto type = PacketType::midi1ChannelVoice; if ((status & 0xf0) == 0xf0) { diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPStreamMessages.h b/modules/yup_audio_basics/midi/ump/yup_UMPStreamMessages.h index 1a1a1d640..38596d6e4 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPStreamMessages.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPStreamMessages.h @@ -83,7 +83,7 @@ struct EndpointDiscoveryView : p (ump) { jassert (p.getType() == PacketType::stream); - jassert (StreamStatus (p.getStatus()) == StreamStatus::endpoint_discovery); + jassert (StreamStatus (p.getStatus()) == StreamStatus::endpointDiscovery); } constexpr uint8_t getUmpVersionMajor() const { return p.getByte3(); } @@ -110,7 +110,7 @@ struct EndpointDiscoveryView constexpr std::optional asEndpointDiscoveryView (const UniversalPacket& p) { - if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::endpoint_discovery) + if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::endpointDiscovery) return EndpointDiscoveryView { p }; return std::nullopt; @@ -122,7 +122,7 @@ struct EndpointInfoView : p (ump) { jassert (p.getType() == PacketType::stream); - jassert (StreamStatus (p.getStatus()) == StreamStatus::endpoint_info); + jassert (StreamStatus (p.getStatus()) == StreamStatus::endpointInfo); } constexpr uint8_t getUmpVersionMajor() const { return p.getByte3(); } @@ -145,7 +145,7 @@ struct EndpointInfoView constexpr std::optional asEndpointInfoView (const UniversalPacket& p) { - if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::endpoint_info) + if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::endpointInfo) return EndpointInfoView { p }; return std::nullopt; @@ -157,7 +157,7 @@ struct DeviceIdentityView : p (ump) { jassert (p.getType() == PacketType::stream); - jassert (StreamStatus (p.getStatus()) == StreamStatus::device_identity); + jassert (StreamStatus (p.getStatus()) == StreamStatus::deviceIdentity); } constexpr DeviceIdentity getIdentity() const @@ -179,7 +179,7 @@ struct DeviceIdentityView constexpr std::optional asDeviceIdentityView (const UniversalPacket& p) { - if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::device_identity) + if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::deviceIdentity) return DeviceIdentityView { p }; return std::nullopt; @@ -191,7 +191,7 @@ struct EndpointNameView : p (ump) { jassert (p.getType() == PacketType::stream); - jassert (StreamStatus (p.getStatus()) == StreamStatus::endpoint_name); + jassert (StreamStatus (p.getStatus()) == StreamStatus::endpointName); } constexpr PacketFormat getFormat() const { return PacketFormat ((p.data[0] >> 26u) & 0x3u); } @@ -204,7 +204,7 @@ struct EndpointNameView constexpr std::optional asEndpointNameView (const UniversalPacket& p) { - if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::endpoint_name) + if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::endpointName) return EndpointNameView { p }; return std::nullopt; @@ -216,7 +216,7 @@ struct ProductInstanceIdView : p (ump) { jassert (p.getType() == PacketType::stream); - jassert (StreamStatus (p.getStatus()) == StreamStatus::product_instance_id); + jassert (StreamStatus (p.getStatus()) == StreamStatus::productInstanceId); } constexpr PacketFormat getFormat() const { return PacketFormat ((p.data[0] >> 26u) & 0x3u); } @@ -229,7 +229,7 @@ struct ProductInstanceIdView constexpr std::optional asProductInstanceIdView (const UniversalPacket& p) { - if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::product_instance_id) + if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::productInstanceId) return ProductInstanceIdView { p }; return std::nullopt; @@ -241,8 +241,8 @@ struct StreamConfigurationView : p (ump) { jassert (p.getType() == PacketType::stream); - jassert (StreamStatus (p.getStatus()) == StreamStatus::stream_configuration_request - || StreamStatus (p.getStatus()) == StreamStatus::stream_configuration_notify); + jassert (StreamStatus (p.getStatus()) == StreamStatus::streamConfigurationRequest + || StreamStatus (p.getStatus()) == StreamStatus::streamConfigurationNotify); } constexpr Protocol getProtocol() const { return p.getByte3() & 0x3; } @@ -259,7 +259,7 @@ constexpr std::optional asStreamConfigurationView (cons return std::nullopt; const auto status = StreamStatus (p.getStatus()); - if (status == StreamStatus::stream_configuration_request || status == StreamStatus::stream_configuration_notify) + if (status == StreamStatus::streamConfigurationRequest || status == StreamStatus::streamConfigurationNotify) return StreamConfigurationView { p }; return std::nullopt; @@ -271,7 +271,7 @@ struct FunctionBlockDiscoveryView : p (ump) { jassert (p.getType() == PacketType::stream); - jassert (StreamStatus (p.getStatus()) == StreamStatus::function_block_discovery); + jassert (StreamStatus (p.getStatus()) == StreamStatus::functionBlockDiscovery); } constexpr uint8_t getFunctionBlock() const { return p.getByte3(); } @@ -294,7 +294,7 @@ struct FunctionBlockDiscoveryView constexpr std::optional asFunctionBlockDiscoveryView (const UniversalPacket& p) { - if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::function_block_discovery) + if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::functionBlockDiscovery) return FunctionBlockDiscoveryView { p }; return std::nullopt; @@ -332,7 +332,7 @@ struct FunctionBlockInfoView : p (ump) { jassert (p.getType() == PacketType::stream); - jassert (StreamStatus (p.getStatus()) == StreamStatus::function_block_info); + jassert (StreamStatus (p.getStatus()) == StreamStatus::functionBlockInfo); } constexpr bool isActive() const { return (p.data[0] & 0x00008000u) != 0; } @@ -359,7 +359,7 @@ struct FunctionBlockInfoView constexpr std::optional asFunctionBlockInfoView (const UniversalPacket& p) { - if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::function_block_info) + if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::functionBlockInfo) return FunctionBlockInfoView { p }; return std::nullopt; @@ -371,7 +371,7 @@ struct FunctionBlockNameView : p (ump) { jassert (p.getType() == PacketType::stream); - jassert (StreamStatus (p.getStatus()) == StreamStatus::function_block_name); + jassert (StreamStatus (p.getStatus()) == StreamStatus::functionBlockName); } constexpr PacketFormat getFormat() const { return PacketFormat ((p.data[0] >> 26u) & 0x3u); } @@ -386,7 +386,7 @@ struct FunctionBlockNameView constexpr std::optional asFunctionBlockNameView (const UniversalPacket& p) { - if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::function_block_name) + if (isStreamMessage (p) && StreamStatus (p.getStatus()) == StreamStatus::functionBlockName) return FunctionBlockNameView { p }; return std::nullopt; @@ -397,7 +397,7 @@ constexpr StreamMessage makeEndpointDiscoveryMessage (uint8_t filter, uint8_t umpVersionMajor = 1, uint8_t umpVersionMinor = 1) { - StreamMessage message { Status (StreamStatus::endpoint_discovery), PacketFormat::complete }; + StreamMessage message { Status (StreamStatus::endpointDiscovery), PacketFormat::complete }; message.setByte (2, umpVersionMajor); message.setByte (3, umpVersionMinor); message.data[1] = filter; @@ -411,7 +411,7 @@ constexpr StreamMessage makeEndpointInfoMessage (uint8_t numFunctionBlocks, uint8_t umpVersionMajor = 1, uint8_t umpVersionMinor = 1) { - StreamMessage message { Status (StreamStatus::endpoint_info), PacketFormat::complete }; + StreamMessage message { Status (StreamStatus::endpointInfo), PacketFormat::complete }; message.setByte (2, umpVersionMajor); message.setByte (3, umpVersionMinor); message.setByte (4, uint8_t ((staticFunctionBlocks ? 0x80u : 0x00u) | numFunctionBlocks)); @@ -422,7 +422,7 @@ constexpr StreamMessage makeEndpointInfoMessage (uint8_t numFunctionBlocks, constexpr StreamMessage makeDeviceIdentityMessage (const DeviceIdentity& identity) { - StreamMessage message { Status (StreamStatus::device_identity), PacketFormat::complete }; + StreamMessage message { Status (StreamStatus::deviceIdentity), PacketFormat::complete }; message.data[1] = identity.manufacturer; message.data[2] = ((uint32_t (identity.family) << 24) & 0x7F000000u) | ((uint32_t (identity.family) << 9) & 0x007F0000u) @@ -438,7 +438,7 @@ constexpr StreamMessage makeDeviceIdentityMessage (const DeviceIdentity& identit constexpr StreamMessage makeEndpointNameMessage (PacketFormat format, const std::string_view& name) { jassert (name.length() <= 14); - StreamMessage message { Status (StreamStatus::endpoint_name), format }; + StreamMessage message { Status (StreamStatus::endpointName), format }; uint8_t byte = 2; for (const auto c : name) { @@ -453,7 +453,7 @@ constexpr StreamMessage makeProductInstanceIdMessage (PacketFormat format, const { jassert (name.length() <= 14); jassert (format != PacketFormat::cont); - StreamMessage message { Status (StreamStatus::product_instance_id), format }; + StreamMessage message { Status (StreamStatus::productInstanceId), format }; uint8_t byte = 2; for (const auto c : name) { @@ -467,7 +467,7 @@ constexpr StreamMessage makeProductInstanceIdMessage (PacketFormat format, const constexpr StreamMessage makeStreamConfigurationRequest (Protocol protocol, Extensions extensions = 0) { jassert (protocol && protocol < 0x3); - StreamMessage message { Status (StreamStatus::stream_configuration_request), PacketFormat::complete }; + StreamMessage message { Status (StreamStatus::streamConfigurationRequest), PacketFormat::complete }; message.setByte (2, protocol); message.setByte (3, extensions); return message; @@ -476,7 +476,7 @@ constexpr StreamMessage makeStreamConfigurationRequest (Protocol protocol, Exten constexpr StreamMessage makeStreamConfigurationNotification (Protocol protocol, Extensions extensions = 0) { jassert (protocol && protocol < 0x3); - StreamMessage message { Status (StreamStatus::stream_configuration_notify), PacketFormat::complete }; + StreamMessage message { Status (StreamStatus::streamConfigurationNotify), PacketFormat::complete }; message.setByte (2, protocol); message.setByte (3, extensions); return message; @@ -485,7 +485,7 @@ constexpr StreamMessage makeStreamConfigurationNotification (Protocol protocol, constexpr StreamMessage makeFunctionBlockDiscoveryMessage (uint8_t functionBlock, uint8_t filter) { jassert (functionBlock == 0xff || functionBlock < 32); - StreamMessage message { Status (StreamStatus::function_block_discovery), PacketFormat::complete }; + StreamMessage message { Status (StreamStatus::functionBlockDiscovery), PacketFormat::complete }; message.setByte (2, functionBlock); message.setByte (3, filter); return message; @@ -498,7 +498,7 @@ constexpr StreamMessage makeFunctionBlockInfoMessage (uint7_t functionBlock, { jassert (functionBlock < 32); jassert (direction > 0 && direction < 4); - StreamMessage message { Status (StreamStatus::function_block_info), PacketFormat::complete }; + StreamMessage message { Status (StreamStatus::functionBlockInfo), PacketFormat::complete }; message.setByte (2, uint8_t (0x80u | (functionBlock & 0x1f))); message.setByte (3, uint8_t (((direction & 0x03u) << 4) | (direction & 0x03u))); message.setByte (4, firstGroup & 0x0f); @@ -517,7 +517,7 @@ constexpr StreamMessage makeFunctionBlockInfoMessage (uint7_t functionBlock, jassert (options.uiHint < 4); jassert (options.uiHint == 0 || (options.direction & options.uiHint)); - StreamMessage message { Status (StreamStatus::function_block_info), PacketFormat::complete }; + StreamMessage message { Status (StreamStatus::functionBlockInfo), PacketFormat::complete }; message.setByte (2, uint8_t ((options.active ? 0x80u : 0x00u) | (functionBlock & 0x1f))); message.setByte (3, uint8_t ((((options.uiHint ? options.uiHint : options.direction) & 0x03u) << 4) | ((options.midi1 & 0x03u) << 2) | (options.direction & 0x03u))); message.setByte (4, firstGroup & 0x0f); @@ -532,7 +532,7 @@ constexpr StreamMessage makeFunctionBlockNameMessage (PacketFormat format, const std::string_view& name) { jassert (name.length() <= 13); - StreamMessage message { Status (StreamStatus::function_block_name), format }; + StreamMessage message { Status (StreamStatus::functionBlockName), format }; message.setByte (2, functionBlock); uint8_t byte = 3; diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPSysExCollectors.h b/modules/yup_audio_basics/midi/ump/yup_UMPSysExCollectors.h index 9eb5d1bdd..e6693e983 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPSysExCollectors.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPSysExCollectors.h @@ -51,13 +51,13 @@ class SysEx7Collector switch (DataStatus (view.getStatus())) { - case DataStatus::sysex7_complete: - case DataStatus::sysex7_start: - if (state != Status (DataStatus::sysex7_start)) + case DataStatus::sysex7Complete: + case DataStatus::sysex7Start: + if (state != Status (DataStatus::sysex7Start)) reset(); break; default: - if (state != Status (DataStatus::sysex7_continue)) + if (state != Status (DataStatus::sysex7Continue)) { reset(); return; @@ -77,7 +77,7 @@ class SysEx7Collector if (limitedSize && (sysex.data.size() + numBytes > sysex.data.capacity())) { - state = Status (DataStatus::sysex7_start); + state = Status (DataStatus::sysex7Start); return; } } @@ -114,14 +114,14 @@ class SysEx7Collector switch (DataStatus (view.getStatus())) { - case DataStatus::sysex7_complete: - case DataStatus::sysex7_end: + case DataStatus::sysex7Complete: + case DataStatus::sysex7End: if (callback) callback (sysex); reset(); break; default: - state = Status (DataStatus::sysex7_continue); + state = Status (DataStatus::sysex7Continue); break; } } @@ -129,14 +129,14 @@ class SysEx7Collector void reset() { sysex.clear(); - state = Status (DataStatus::sysex7_start); + state = Status (DataStatus::sysex7Start); manufacturerIdBytesRead = 0; } private: SysEx7 sysex; size_t maxSysExDataSize { 0 }; - Status state { Status (DataStatus::sysex7_start) }; + Status state { Status (DataStatus::sysex7Start) }; uint8_t manufacturerIdBytesRead { 0 }; Callback callback; }; diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPUniversalPacket.h b/modules/yup_audio_basics/midi/ump/yup_UMPUniversalPacket.h index 9538d6dc1..a454b9670 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPUniversalPacket.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPUniversalPacket.h @@ -28,11 +28,11 @@ enum class PacketType : uint4_t { utility = 0x0, system = 0x1, - midi1_channel_voice = 0x2, + midi1ChannelVoice = 0x2, data = 0x3, - midi2_channel_voice = 0x4, - extended_data = 0x5, - flex_data = 0xD, + midi2ChannelVoice = 0x4, + extendedData = 0x5, + flexData = 0xD, stream = 0xF }; @@ -59,64 +59,64 @@ enum class UtilityStatus : Status enum class SystemStatus : Status { - mtc_quarter_frame = 0xF1, - song_position = 0xF2, - song_select = 0xF3, - tune_request = 0xF6, + mtcQuarterFrame = 0xF1, + songPosition = 0xF2, + songSelect = 0xF3, + tuneRequest = 0xF6, clock = 0xF8, start = 0xFA, cont = 0xFB, stop = 0xFC, - active_sense = 0xFE, + activeSense = 0xFE, reset = 0xFF }; enum class Midi1ChannelVoiceStatus : Status { - note_off = 0x80, - note_on = 0x90, - poly_pressure = 0xA0, - control_change = 0xB0, - program_change = 0xC0, - channel_pressure = 0xD0, - pitch_bend = 0xE0 + noteOff = 0x80, + noteOn = 0x90, + polyPressure = 0xA0, + controlChange = 0xB0, + programChange = 0xC0, + channelPressure = 0xD0, + pitchBend = 0xE0 }; enum class DataStatus : Status { - sysex7_complete = (Status (PacketFormat::complete) << 4), - sysex7_start = (Status (PacketFormat::start) << 4), - sysex7_continue = (Status (PacketFormat::cont) << 4), - sysex7_end = (Status (PacketFormat::end) << 4) + sysex7Complete = (Status (PacketFormat::complete) << 4), + sysex7Start = (Status (PacketFormat::start) << 4), + sysex7Continue = (Status (PacketFormat::cont) << 4), + sysex7End = (Status (PacketFormat::end) << 4) }; enum class ChannelVoiceStatus : Status { - registered_per_note_controller = 0x00, - assignable_per_note_controller = 0x10, - registered_controller = 0x20, - assignable_controller = 0x30, - relative_registered_controller = 0x40, - relative_assignable_controller = 0x50, - per_note_pitch_bend = 0x60, - note_off = 0x80, - note_on = 0x90, - poly_pressure = 0xA0, - control_change = 0xB0, - program_change = 0xC0, - channel_pressure = 0xD0, - pitch_bend = 0xE0, - per_note_management = 0xF0 + registeredPerNoteController = 0x00, + assignablePerNoteController = 0x10, + registeredController = 0x20, + assignableController = 0x30, + relativeRegisteredController = 0x40, + relativeAssignableController = 0x50, + perNotePitchBend = 0x60, + noteOff = 0x80, + noteOn = 0x90, + polyPressure = 0xA0, + controlChange = 0xB0, + programChange = 0xC0, + channelPressure = 0xD0, + pitchBend = 0xE0, + perNoteManagement = 0xF0 }; enum class ExtendedDataStatus : Status { - sysex8_complete = (Status (PacketFormat::complete) << 4), - sysex8_start = (Status (PacketFormat::start) << 4), - sysex8_continue = (Status (PacketFormat::cont) << 4), - sysex8_end = (Status (PacketFormat::end) << 4), - mixed_data_set_header = 0x80, - mixed_data_set_payload = 0x90 + sysex8Complete = (Status (PacketFormat::complete) << 4), + sysex8Start = (Status (PacketFormat::start) << 4), + sysex8Continue = (Status (PacketFormat::cont) << 4), + sysex8End = (Status (PacketFormat::end) << 4), + mixedDataSetHeader = 0x80, + mixedDataSetPayload = 0x90 }; enum class StreamProtocol : Protocol @@ -133,16 +133,16 @@ enum class StreamExtensions : Extensions enum class StreamStatus : Status { - endpoint_discovery = 0x00, - endpoint_info = 0x01, - device_identity = 0x02, - endpoint_name = 0x03, - product_instance_id = 0x04, - stream_configuration_request = 0x05, - stream_configuration_notify = 0x06, - function_block_discovery = 0x10, - function_block_info = 0x11, - function_block_name = 0x12 + endpointDiscovery = 0x00, + endpointInfo = 0x01, + deviceIdentity = 0x02, + endpointName = 0x03, + productInstanceId = 0x04, + streamConfigurationRequest = 0x05, + streamConfigurationNotify = 0x06, + functionBlockDiscovery = 0x10, + functionBlockInfo = 0x11, + functionBlockName = 0x12 }; namespace ControlChange @@ -245,9 +245,9 @@ struct UniversalPacket constexpr bool hasChannel() const { - return getType() == PacketType::midi1_channel_voice - || getType() == PacketType::midi2_channel_voice - || getType() == PacketType::flex_data; + return getType() == PacketType::midi1ChannelVoice + || getType() == PacketType::midi2ChannelVoice + || getType() == PacketType::flexData; } constexpr Channel getChannel() const @@ -333,12 +333,12 @@ struct UniversalPacket constexpr bool isChannelVoiceMessage() const { - return getType() == PacketType::midi1_channel_voice || getType() == PacketType::midi2_channel_voice; + return getType() == PacketType::midi1ChannelVoice || getType() == PacketType::midi2ChannelVoice; } constexpr bool isDataMessage() const { - return getType() == PacketType::data || getType() == PacketType::extended_data; + return getType() == PacketType::data || getType() == PacketType::extendedData; } constexpr bool isMidi1ProtocolMessage() const @@ -347,15 +347,15 @@ struct UniversalPacket { switch (SystemStatus (getStatus())) { - case SystemStatus::mtc_quarter_frame: - case SystemStatus::song_position: - case SystemStatus::song_select: - case SystemStatus::tune_request: + case SystemStatus::mtcQuarterFrame: + case SystemStatus::songPosition: + case SystemStatus::songSelect: + case SystemStatus::tuneRequest: case SystemStatus::clock: case SystemStatus::start: case SystemStatus::cont: case SystemStatus::stop: - case SystemStatus::active_sense: + case SystemStatus::activeSense: case SystemStatus::reset: return true; default: @@ -363,7 +363,7 @@ struct UniversalPacket } } - if (getType() == PacketType::midi1_channel_voice) + if (getType() == PacketType::midi1ChannelVoice) return getStatus() >= 0x80 && getStatus() < 0xf0; return false; From ed858d1ae1aeeaa17bcd7ce492430866a62dbc07 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 22 Dec 2025 11:11:08 +0100 Subject: [PATCH 06/20] More midi2 work --- .../midi/ump/yup_UMPUniversalPacket.h | 59 +- .../yup_UMPCapabilityInquiry.cpp | 189 +++++++ .../yup_audio_basics/yup_UMPChannelVoice.cpp | 141 +++++ .../yup_audio_basics/yup_UMPDataMessages.cpp | 105 ++++ .../yup_UMPExtendedDataMessages.cpp | 85 +++ .../yup_UMPFlexDataMessages.cpp | 86 +++ .../yup_UMPJitterReductionTimestamps.cpp | 97 ++++ tests/yup_audio_basics/yup_UMPMessages.cpp | 308 +++++++++++ .../yup_UMPMidi1ByteStream.cpp | 138 +++++ .../yup_UMPMidi1ChannelVoiceMessage.cpp | 86 +++ .../yup_UMPMidi2ChannelVoiceMessage.cpp | 73 +++ .../yup_UMPStreamMessages.cpp | 99 ++++ .../yup_UMPSysExCollectors.cpp | 238 ++++++++ tests/yup_audio_basics/yup_UMPTypes.cpp | 81 +++ .../yup_UMPUniversalPacket.cpp | 520 ++++++++++++++++++ .../yup_UMPUniversalSysEx.cpp | 79 +++ 16 files changed, 2382 insertions(+), 2 deletions(-) create mode 100644 tests/yup_audio_basics/yup_UMPCapabilityInquiry.cpp create mode 100644 tests/yup_audio_basics/yup_UMPChannelVoice.cpp create mode 100644 tests/yup_audio_basics/yup_UMPDataMessages.cpp create mode 100644 tests/yup_audio_basics/yup_UMPExtendedDataMessages.cpp create mode 100644 tests/yup_audio_basics/yup_UMPFlexDataMessages.cpp create mode 100644 tests/yup_audio_basics/yup_UMPJitterReductionTimestamps.cpp create mode 100644 tests/yup_audio_basics/yup_UMPMessages.cpp create mode 100644 tests/yup_audio_basics/yup_UMPMidi1ByteStream.cpp create mode 100644 tests/yup_audio_basics/yup_UMPMidi1ChannelVoiceMessage.cpp create mode 100644 tests/yup_audio_basics/yup_UMPMidi2ChannelVoiceMessage.cpp create mode 100644 tests/yup_audio_basics/yup_UMPStreamMessages.cpp create mode 100644 tests/yup_audio_basics/yup_UMPSysExCollectors.cpp create mode 100644 tests/yup_audio_basics/yup_UMPTypes.cpp create mode 100644 tests/yup_audio_basics/yup_UMPUniversalPacket.cpp create mode 100644 tests/yup_audio_basics/yup_UMPUniversalSysEx.cpp diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPUniversalPacket.h b/modules/yup_audio_basics/midi/ump/yup_UMPUniversalPacket.h index a454b9670..b96f04e5d 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPUniversalPacket.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPUniversalPacket.h @@ -21,6 +21,11 @@ #ifndef DOXYGEN +#include +#include +#include +#include + namespace yup::ump { @@ -127,8 +132,8 @@ enum class StreamProtocol : Protocol enum class StreamExtensions : Extensions { - jitter_reduction_transmit = 0x1, - jitter_reduction_receive = 0x2 + jitterReductionTransmit = 0x1, + jitterReductionReceive = 0x2 }; enum class StreamStatus : Status @@ -370,6 +375,56 @@ struct UniversalPacket } }; +namespace detail +{ +struct IosBaseFlagsRestorer +{ + explicit IosBaseFlagsRestorer (std::ios_base& stream) + : strm (stream) + , flags (stream.flags()) + { + } + + ~IosBaseFlagsRestorer() { strm.flags (flags); } + + std::ios_base& strm; + std::ios_base::fmtflags flags; +}; +} // namespace detail + +inline std::ostream& operator<< (std::ostream& out, const UniversalPacket& p) +{ + detail::IosBaseFlagsRestorer flagRestorer (out); + + for (size_t word = 0; word < p.getSize(); ++word) + { + if (word != 0) + out << ' '; + out << std::hex << std::setfill ('0') << std::setw (8) << p.data[word]; + } + + return out; +} + +inline std::istream& operator>> (std::istream& in, UniversalPacket& p) +{ + detail::IosBaseFlagsRestorer flagRestorer (in); + in >> std::hex >> p.data[0]; + + if (in.good()) + { + const auto words = p.getSize(); + for (size_t word = 1; word < words; ++word) + { + in >> std::hex >> p.data[word]; + if (! in.good()) + break; + } + } + + return in; +} + } // namespace yup::ump #endif diff --git a/tests/yup_audio_basics/yup_UMPCapabilityInquiry.cpp b/tests/yup_audio_basics/yup_UMPCapabilityInquiry.cpp new file mode 100644 index 000000000..dcda237f9 --- /dev/null +++ b/tests/yup_audio_basics/yup_UMPCapabilityInquiry.cpp @@ -0,0 +1,189 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include +#include +#include + +using namespace yup; +using namespace yup::ump; +using namespace yup::ump::ci; + +TEST (UMPCapabilityInquiryProfileTests, ProfileIdEquality) +{ + ProfileId a; + EXPECT_EQ (a.byte1, 0x7e); + EXPECT_EQ (a.byte2, 0x00); + EXPECT_EQ (a.byte3, 0x00); + EXPECT_EQ (a.byte4, 0x00); + EXPECT_EQ (a.byte5, 0x00); + + ProfileId b { 0x7e, 0x02, 0x03, 0x04, 0x05 }; + ProfileId c { 0x00, 0x21, 0x09, 0x7f, 0x01 }; + ProfileId d { b }; + + EXPECT_TRUE (a == a); + EXPECT_FALSE (a == b); + EXPECT_FALSE (a == c); + EXPECT_FALSE (a == d); + + EXPECT_FALSE (b == a); + EXPECT_TRUE (b == b); + EXPECT_FALSE (b == c); + EXPECT_TRUE (b == d); + + EXPECT_FALSE (c == a); + EXPECT_FALSE (c == b); + EXPECT_TRUE (c == c); + EXPECT_FALSE (c == d); + + EXPECT_FALSE (d == a); + EXPECT_TRUE (d == b); + EXPECT_FALSE (d == c); + EXPECT_TRUE (d == d); +} + +TEST (UMPCapabilityInquiryProfileTests, ProfileInquiryViewAndMessage) +{ + SysEx7 sx { Manufacturer::universalNonRealtime, + { 0x7f, 0x0d, Subtype::profileInquiry, 0x02, 0x78, 0x56, 0x34, 0x12, 0x77, 0x55, 0x33, 0x11 } }; + + EXPECT_TRUE (ProfileInquiryView::validate (sx)); + EXPECT_TRUE (isCapabilityInquiryMessage (sx)); + + auto mut = makeProfileInquiryMessage (0x7665544, 0x24d2b78, 0x08); + EXPECT_TRUE (ProfileInquiryView::validate (mut)); + EXPECT_EQ (mut.data.size(), 12u); +} + +TEST (UMPCapabilityInquiryProfileTests, ProfileInquiryReplyProfiles) +{ + SysEx7 sx { Manufacturer::universalNonRealtime, + { 0x7f, 0x0d, Subtype::profileInquiryReply, 0x00, 0x78, 0x56, 0x34, 0x12, 0x44, 0x33, 0x22, 0x11, 0x02, 0x00, 0x00, 0x21, 0x09, 42, 7, 0x7e, 0x03, 0x02, 0x01, 0x00, 0x01, 0x00, 0x7e, 0x01, 0x02, 0x03, 0x04, 0x00, 0x00 } }; + + EXPECT_TRUE (ProfileInquiryReplyView::validate (sx)); + + const auto view = ProfileInquiryReplyView { sx }; + const auto enabled = view.getEnabledProfiles(); + const auto disabled = view.getDisabledProfiles(); + + ASSERT_EQ (enabled.size(), 2u); + EXPECT_EQ (enabled[0].byte1, 0x00); + EXPECT_EQ (enabled[0].byte2, 0x21); + EXPECT_EQ (enabled[0].byte3, 0x09); + EXPECT_EQ (enabled[0].byte4, 42); + EXPECT_EQ (enabled[0].byte5, 7); + + ASSERT_EQ (disabled.size(), 1u); + EXPECT_EQ (disabled[0].byte1, 0x7e); + EXPECT_EQ (disabled[0].byte2, 0x01); + EXPECT_EQ (disabled[0].byte3, 0x02); + EXPECT_EQ (disabled[0].byte4, 0x03); + EXPECT_EQ (disabled[0].byte5, 0x04); +} + +TEST (UMPCapabilityInquiryProfileTests, ProfileInquiryReplyBuilder) +{ + const std::vector enabled { { 0x7e, 0x02, 0x03, 0x04, 0x05 } }; + const std::vector disabled { { 0x00, 0x21, 0x09, 0x7f, 0x01 } }; + + auto sx = makeProfileInquiryReply (0x7665544, 0x4711, enabled, disabled, 0x08); + EXPECT_TRUE (ProfileInquiryReplyView::validate (sx)); + + const auto view = ProfileInquiryReplyView { sx }; + EXPECT_EQ (view.getNumEnabledProfiles(), 1u); + EXPECT_EQ (view.getNumDisabledProfiles(), 1u); +} + +TEST (UMPCapabilityInquiryProcessTests, ProcessInquiryCapabilities) +{ + auto inquiry = makeProcessInquiryCapabilitiesInquiry (0x7665544, 0x24d2b78, 0x08); + EXPECT_TRUE (CapabilityInquiryView::validate (inquiry)); + EXPECT_EQ (inquiry.data.size(), 12u); + + auto reply = makeProcessInquiryCapabilitiesReply (0x7665544, 0x24d2b78, 19, 0x08); + EXPECT_TRUE (ProcessInquiryCapabilitiesReplyView::validate (reply)); + EXPECT_EQ (reply.data.size(), 13u); + + const auto view = ProcessInquiryCapabilitiesReplyView { reply }; + EXPECT_EQ (view.getSupportedFeatures(), 19); +} + +TEST (UMPCapabilityInquiryProcessTests, MidiMessageReportMessages) +{ + auto inquiry = makeMidiMessageReportInquiry (0x7665544, 0x24d2b78, 0x12, 0x34, 0x56, 0x78, 0x04); + EXPECT_TRUE (MidiMessageReportInquiryView::validate (inquiry)); + EXPECT_EQ (inquiry.data.size(), 16u); + + auto reply = makeMidiMessageReportReply (0x7665544, 0x76, 0x54, 0x32, 0x0a); + EXPECT_TRUE (MidiMessageReportReplyView::validate (reply)); + EXPECT_EQ (reply.data.size(), 15u); + + auto end = makeMidiMessageReportEnd (0x7665544, 0x03); + EXPECT_TRUE (CapabilityInquiryView::validate (end)); + EXPECT_EQ (end.data.size(), 12u); +} + +TEST (UMPCapabilityInquiryPropertyExchangeTests, CapabilitiesView) +{ + SysEx7 sx { Manufacturer::universalNonRealtime, + { 0x7f, 0x0d, Subtype::propertyExchangeCapabilitiesInquiry, 0x01, 0x78, 0x56, 0x34, 0x12, 0x77, 0x55, 0x33, 0x11, 2 } }; + + EXPECT_TRUE (PropertyExchangeCapabilitiesView::validate (sx)); + const auto view = PropertyExchangeCapabilitiesView { sx }; + EXPECT_EQ (view.getMaximumNumberOfRequests(), 2); + EXPECT_EQ (view.getMajorVersion(), 0); + EXPECT_EQ (view.getMinorVersion(), 0); + + SysEx7 missingVersion { Manufacturer::universalNonRealtime, + { 0x7f, 0x0d, Subtype::propertyExchangeCapabilitiesReply, 0x02, 0x78, 0x56, 0x34, 0x12, 0x77, 0x55, 0x33, 0x11, 2 } }; + EXPECT_FALSE (PropertyExchangeCapabilitiesView::validate (missingVersion)); +} + +TEST (UMPCapabilityInquiryPropertyExchangeTests, PropertyDataMessageValidation) +{ + const auto headerJson = propertyExchange::makeRjson (propertyExchange::Tags::resource, + "ResourceList"); + const auto header = propertyExchange::Header { std::string_view { headerJson } }; + const auto chunk = propertyExchange::Chunk { std::string_view { "ABC" } }; + + auto sx = propertyExchange::makePropertyDataMessage (Subtype::getPropertyDataInquiry, + 0x1234567, + 0x4332211, + header, + 1, + 1, + chunk, + 0x11, + 0x0a); + EXPECT_TRUE (propertyExchange::PropertyDataMessageView::validate (sx)); + EXPECT_TRUE (GetPropertyDataView::validate (sx)); + + SysEx7 invalid { Manufacturer::universalNonRealtime, + { 0x7f, 0x0d, Subtype::getPropertyDataInquiry, 0x02, 0x78, 0x56, 0x34, 0x12, 0x77, 0x55, 0x33, 0x11, 0x01, 0x7f, 0x7f } }; + EXPECT_FALSE (propertyExchange::PropertyDataMessageView::validate (invalid)); + + SysEx7 invalidChunk { Manufacturer::universalNonRealtime, + { 0x7f, 0x0d, Subtype::getPropertyDataInquiry, 0x02, 0x78, 0x56, 0x34, 0x12, 0x77, 0x55, 0x33, 0x11, 0x01, 0x01, 0x00, 0x7f, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00 } }; + EXPECT_FALSE (propertyExchange::PropertyDataMessageView::validate (invalidChunk)); +} diff --git a/tests/yup_audio_basics/yup_UMPChannelVoice.cpp b/tests/yup_audio_basics/yup_UMPChannelVoice.cpp new file mode 100644 index 000000000..7be14c64a --- /dev/null +++ b/tests/yup_audio_basics/yup_UMPChannelVoice.cpp @@ -0,0 +1,141 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; +using namespace yup::ump; + +namespace +{ +UniversalPacket makePacket (PacketType type, Group group, Status status, uint32_t data) +{ + return UniversalPacket { (uint32_t (type) << 28u) | ((group & 0x0f) << 24u) | (uint32_t (status) << 16u), data }; +} +} // namespace + +TEST (UMPChannelVoiceTests, DISABLED_IsChannelVoiceMessageWithStatus) +{ + for (uint8_t match = 0x00; match <= 0xf0; match += 0x10) + { + for (uint8_t t = 0; t < 16; ++t) + { + const bool channelVoice = (t == uint8_t (PacketType::midi1ChannelVoice)) + || (t == uint8_t (PacketType::midi2ChannelVoice)); + for (uint8_t s = 0; s <= 0xf0; s += 0x10) + { + auto p = UniversalPacket { uint32_t ((t << 28) | (s << 16) | t | (s >> 4)) }; + EXPECT_EQ (channelVoice && (s == match), isChannelVoiceMessageWithStatus (p, Status (match))); + } + } + } +} + +TEST (UMPChannelVoiceTests, NoteOnOffDetection) +{ + EXPECT_TRUE (isNoteOnMessage (makeMidi1NoteOnMessage (4, 7, 99, Velocity { uint7_t { 100 } }))); + EXPECT_FALSE (isNoteOnMessage (makeMidi1NoteOnMessage (13, 5, 60, Velocity { uint7_t { 0 } }))); + EXPECT_TRUE (isNoteOffMessage (makeMidi1NoteOnMessage (13, 5, 60, Velocity { uint7_t { 0 } }))); + EXPECT_TRUE (isNoteOffMessage (makeMidi1NoteOffMessage (0, 2, 67))); + + EXPECT_TRUE (isNoteOnMessage (makeMidi2NoteOnMessage (4, 7, 99, Velocity { uint16_t { 0x4567 } }))); + EXPECT_TRUE (isNoteOnMessage (makeMidi2NoteOnMessage (13, 5, 60, Velocity { uint16_t { 0 } }))); + EXPECT_TRUE (isNoteOffMessage (makeMidi2NoteOffMessage (0, 2, 67, Velocity { uint16_t { 0x1234 } }))); +} + +TEST (UMPChannelVoiceTests, NoteNumberPitchVelocity) +{ + EXPECT_EQ (getNoteNumber (makeMidi1NoteOffMessage (0, 2, 67)), NoteNumber (67)); + EXPECT_EQ (getNoteNumber (makeMidi2NoteOnMessage (4, 7, 99, Velocity { uint16_t { 0x4567 } })), NoteNumber (99)); + + EXPECT_EQ (getNotePitch (makeMidi1NoteOnMessage (13, 5, 60, Velocity { uint7_t { 0 } })), + Pitch7_9 { NoteNumber (60) }); + EXPECT_EQ (getNotePitch (makeMidi2NoteOnMessage (9, 10, 127, Velocity { uint16_t { 0xa000 } }, Pitch7_9 { 89.45f })), + Pitch7_9 { 89.45f }); + + EXPECT_EQ (getNoteVelocity (makeMidi1NoteOffMessage (0, 2, 67)), Velocity { uint7_t { 64 } }); + EXPECT_EQ (getNoteVelocity (makeMidi2NoteOnMessage (4, 7, 99, Velocity { uint16_t { 0x4567 } })), + Velocity { uint16_t { 0x4567 } }); +} + +TEST (UMPChannelVoiceTests, ControllerMessages) +{ + EXPECT_TRUE (isControlChangeMessage (makeMidi1ControlChangeMessage (5, 15, 7, ControllerValue { uint7_t { 100 } }))); + EXPECT_EQ (getControllerNumber (makeMidi1ControlChangeMessage (5, 15, 7, ControllerValue { uint7_t { 100 } })), + ControllerNumber (7)); + EXPECT_EQ (getControllerValue (makeMidi1ControlChangeMessage (5, 15, 7, ControllerValue { uint7_t { 100 } })), + ControllerValue { uint7_t { 100 } }); + + EXPECT_TRUE (isControlChangeMessage (makeMidi2ControlChangeMessage (5, 15, 7, ControllerValue { 0x89abcdefu }))); + EXPECT_EQ (getControllerValue (makeMidi2ControlChangeMessage (5, 15, 7, ControllerValue { 0x89abcdefu })), + ControllerValue { 0x89abcdefu }); +} + +TEST (UMPChannelVoiceTests, PressureAndPitchBend) +{ + EXPECT_TRUE (isPolyPressureMessage (makeMidi1PolyPressureMessage (14, 2, 64, ControllerValue { uint7_t { 77 } }))); + EXPECT_EQ (getPolyPressureValue (makeMidi2PolyPressureMessage (1, 4, 99, ControllerValue { 12345u })), + ControllerValue { 12345u }); + + EXPECT_TRUE (isChannelPressureMessage (makeMidi1ChannelPressureMessage (8, 8, ControllerValue { uint7_t { 77 } }))); + EXPECT_EQ (getChannelPressureValue (makeMidi2ChannelPressureMessage (9, 0, ControllerValue { 12345u })), + ControllerValue { 12345u }); + + EXPECT_TRUE (isChannelPitchBendMessage (makeMidi1PitchBendMessage (1, 13, PitchBend { uint14_t { 8177 } }))); + EXPECT_EQ (getChannelPitchBendValue (makeMidi2PitchBendMessage (2, 14, PitchBend { 0x81234567u })), + PitchBend { 0x81234567u }); +} + +TEST (UMPChannelVoiceTests, Midi2SpecificPredicates) +{ + EXPECT_TRUE (isRegisteredControllerMessage (makeRegisteredControllerMessage (2, 9, 0, 4, ControllerValue { 123456u }))); + EXPECT_TRUE (isAssignableControllerMessage (makeAssignableControllerMessage (8, 0, 4, 12, ControllerValue { 987654u }))); + EXPECT_TRUE (isRegisteredPerNoteControllerMessage (makeRegisteredPerNoteControllerMessage (15, 10, 44, 2, ControllerValue { 123456u }))); + EXPECT_TRUE (isAssignablePerNoteControllerMessage (makeAssignablePerNoteControllerMessage (3, 7, 64, 99, ControllerValue { 987654u }))); + EXPECT_TRUE (isPerNotePitchBendMessage (makePerNotePitchBendMessage (11, 12, 13, PitchBend { 0x80000001u }))); +} + +TEST (UMPChannelVoiceTests, Midi2AttributesAndSensitivity) +{ + const auto noteOn = makeMidi2NoteOnMessage (9, 10, 127, Velocity { uint16_t { 0xa000 } }, Pitch7_9 { NoteNumber { 60 } }); + EXPECT_TRUE (isNoteOnWithPitch7_9 (noteOn)); + EXPECT_EQ (getMidi2NoteAttribute (noteOn), NoteAttribute::pitch_7_9); + + const auto reg = makeRegisteredControllerMessage (2, 9, 0, RegisteredParameterNumber::pitchBendSensitivity, ControllerValue { 0x12340000u }); + EXPECT_TRUE (isPitchBendSensitivityMessage (reg)); + EXPECT_EQ (getPitchBendSensitivityValue (reg), PitchBendSensitivity { 0x12340000u }); +} + +TEST (UMPChannelVoiceTests, Midi2ToMidi1Translation) +{ + const auto noteOn = Midi2ChannelVoiceMessageView { makeMidi2NoteOnMessage (4, 7, 99, Velocity { uint16_t { 0x4567 } }) }; + const auto noteOff = Midi2ChannelVoiceMessageView { makeMidi2NoteOffMessage (3, 9, 66, Velocity { uint16_t { 0x1234 } }) }; + + auto m1 = asMidi1ChannelVoiceMessage (noteOn); + ASSERT_TRUE (m1.has_value()); + EXPECT_EQ (*m1, makeMidi1NoteOnMessage (4, 7, 99, Velocity { uint16_t { 0x4567 } })); + + auto m2 = asMidi1ChannelVoiceMessage (noteOff); + ASSERT_TRUE (m2.has_value()); + EXPECT_EQ (*m2, makeMidi1NoteOffMessage (3, 9, 66, Velocity { uint16_t { 0x1234 } })); +} diff --git a/tests/yup_audio_basics/yup_UMPDataMessages.cpp b/tests/yup_audio_basics/yup_UMPDataMessages.cpp new file mode 100644 index 000000000..6ed5988fd --- /dev/null +++ b/tests/yup_audio_basics/yup_UMPDataMessages.cpp @@ -0,0 +1,105 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; +using namespace yup::ump; + +TEST (UMPDataMessagesTests, DataMessageConstructors) +{ + { + constexpr DataMessage m; + EXPECT_EQ (m.getType(), PacketType::data); + EXPECT_EQ (m.getGroup(), 0u); + EXPECT_EQ (m.getStatus(), Status (DataStatus::sysex7Complete)); + } + + { + constexpr DataMessage m { Status (DataStatus::sysex7End) }; + EXPECT_EQ (m.getType(), PacketType::data); + EXPECT_EQ (m.getGroup(), 0u); + EXPECT_EQ (m.getStatus(), Status (DataStatus::sysex7End)); + } +} + +TEST (UMPDataMessagesTests, SysEx7PacketConstructors) +{ + { + constexpr SysEx7Packet p; + EXPECT_EQ (p.getType(), PacketType::data); + EXPECT_EQ (p.getGroup(), 0u); + EXPECT_EQ (p.getStatus(), Status (DataStatus::sysex7Complete)); + EXPECT_EQ (p.getFormat(), PacketFormat::complete); + EXPECT_EQ (p.getPayloadSize(), 0u); + } + + { + constexpr SysEx7Packet p { Status (DataStatus::sysex7End), 9 }; + EXPECT_EQ (p.getType(), PacketType::data); + EXPECT_EQ (p.getGroup(), 9u); + EXPECT_EQ (p.getStatus(), Status (DataStatus::sysex7End)); + EXPECT_EQ (p.getFormat(), PacketFormat::end); + EXPECT_EQ (p.getPayloadSize(), 0u); + } +} + +TEST (UMPDataMessagesTests, MakeSysEx7Packets) +{ + { + constexpr auto m = makeSysEx7CompletePacket(); + EXPECT_EQ (m.getFormat(), PacketFormat::complete); + EXPECT_EQ (m.getPayloadSize(), 0u); + } + + { + constexpr auto m = makeSysEx7StartPacket (0xf); + EXPECT_EQ (m.getGroup(), 0xf); + EXPECT_EQ (m.getFormat(), PacketFormat::start); + } + + { + constexpr auto m = makeSysEx7ContinuePacket (9); + EXPECT_EQ (m.getGroup(), 9u); + EXPECT_EQ (m.getFormat(), PacketFormat::cont); + } + + { + constexpr auto m = makeSysEx7EndPacket (1); + EXPECT_EQ (m.getGroup(), 1u); + EXPECT_EQ (m.getFormat(), PacketFormat::end); + } +} + +TEST (UMPDataMessagesTests, SysEx7PacketPayloadHelpers) +{ + auto m = makeSysEx7CompletePacket(); + EXPECT_EQ (m.getPayloadSize(), 0u); + + m.addPayloadByte (0x12); + m.addPayloadByte (0x34); + + EXPECT_EQ (m.getPayloadSize(), 2u); + EXPECT_EQ (m.getPayloadByte (0), 0x12); + EXPECT_EQ (m.getPayloadByte (1), 0x34); +} diff --git a/tests/yup_audio_basics/yup_UMPExtendedDataMessages.cpp b/tests/yup_audio_basics/yup_UMPExtendedDataMessages.cpp new file mode 100644 index 000000000..687daba6a --- /dev/null +++ b/tests/yup_audio_basics/yup_UMPExtendedDataMessages.cpp @@ -0,0 +1,85 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; +using namespace yup::ump; + +TEST (UMPExtendedDataMessagesTests, ExtendedDataMessageConstructors) +{ + { + constexpr ExtendedDataMessage m; + EXPECT_EQ (m.getType(), PacketType::extendedData); + EXPECT_EQ (m.getGroup(), 0u); + } + + { + constexpr ExtendedDataMessage m { Status (ExtendedDataStatus::sysex8End) }; + EXPECT_EQ (m.getType(), PacketType::extendedData); + EXPECT_EQ (m.getGroup(), 0u); + EXPECT_EQ (m.getStatus(), Status (ExtendedDataStatus::sysex8End)); + } +} + +TEST (UMPExtendedDataMessagesTests, SysEx8PacketConstructors) +{ + { + constexpr SysEx8Packet m; + EXPECT_EQ (m.getType(), PacketType::extendedData); + EXPECT_EQ (m.getGroup(), 0u); + EXPECT_EQ (m.getFormat(), PacketFormat::complete); + EXPECT_EQ (m.getPayloadSize(), 0u); + EXPECT_TRUE (isExtendedDataMessage (m)); + } + + { + constexpr SysEx8Packet m { Status (ExtendedDataStatus::sysex8End), 0xac, 0x0c }; + EXPECT_EQ (m.getGroup(), 0x0c); + EXPECT_EQ (m.getFormat(), PacketFormat::end); + EXPECT_EQ (m.getStreamId(), 0xac); + EXPECT_TRUE (isExtendedDataMessage (m)); + } +} + +TEST (UMPExtendedDataMessagesTests, SysEx8PacketHelpers) +{ + auto m = makeSysEx8CompletePacket (0); + EXPECT_EQ (m.getFormat(), PacketFormat::complete); + EXPECT_EQ (m.getStreamId(), 0u); + + m.setStreamId (99); + EXPECT_EQ (m.getStreamId(), 99u); + + m.addPayloadByte (0x12); + EXPECT_EQ (m.getPayloadSize(), 1u); + EXPECT_EQ (m.getPayloadByte (0), 0x12); +} + +TEST (UMPExtendedDataMessagesTests, SysEx8PacketFormats) +{ + EXPECT_EQ (makeSysEx8CompletePacket (0).getFormat(), PacketFormat::complete); + EXPECT_EQ (makeSysEx8StartPacket (0).getFormat(), PacketFormat::start); + EXPECT_EQ (makeSysEx8ContinuePacket (0).getFormat(), PacketFormat::cont); + EXPECT_EQ (makeSysEx8EndPacket (0).getFormat(), PacketFormat::end); +} diff --git a/tests/yup_audio_basics/yup_UMPFlexDataMessages.cpp b/tests/yup_audio_basics/yup_UMPFlexDataMessages.cpp new file mode 100644 index 000000000..027d817bf --- /dev/null +++ b/tests/yup_audio_basics/yup_UMPFlexDataMessages.cpp @@ -0,0 +1,86 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +#include + +using namespace yup; +using namespace yup::ump; + +TEST (UMPFlexDataMessagesTests, ConstructorsAndView) +{ + FlexDataMessage m (2); + EXPECT_EQ (m.getType(), PacketType::flexData); + EXPECT_EQ (m.getGroup(), 2u); + + auto view = FlexDataMessageView { m }; + EXPECT_EQ (view.getGroup(), 2u); +} + +TEST (UMPFlexDataMessagesTests, MakeTextMessage) +{ + auto msg = makeFlexDataTextMessage (1, + PacketFormat::complete, + PacketAddress::group, + 0, + 0x01, + 0x02, + "Hello"); + + EXPECT_EQ (msg.getFormat(), PacketFormat::complete); + EXPECT_EQ (FlexDataMessage::getPayloadAsString (msg), "Hello"); +} + +TEST (UMPFlexDataMessagesTests, SetTempoAndTimeSignature) +{ + auto tempo = makeSetTempoMessage (3, 0x11223344u); + EXPECT_EQ (tempo.getGroup(), 3u); + EXPECT_EQ (tempo.data[1], 0x11223344u); + + auto timeSig = makeSetTimeSignatureMessage (4, 7, 8, 12); + EXPECT_EQ (timeSig.getGroup(), 4u); + EXPECT_EQ (timeSig.getByte (4), 7u); + EXPECT_EQ (timeSig.getByte (5), 8u); + EXPECT_EQ (timeSig.getByte (6), 12u); +} + +TEST (UMPFlexDataMessagesTests, SetMetronomeAndKeySignature) +{ + auto metro = makeSetMetronomeMessage (1, 24, 1, 2, 3, 4, 5); + EXPECT_EQ (metro.getByte (4), 24u); + EXPECT_EQ (metro.getByte (5), 1u); + EXPECT_EQ (metro.getByte (6), 2u); + EXPECT_EQ (metro.getByte (7), 3u); + + auto key = makeSetKeySignatureMessage (2, PacketAddress::group, 0, 3, 5); + EXPECT_EQ (key.getByte (4), 0x35u); +} + +TEST (UMPFlexDataMessagesTests, SetChord) +{ + auto chord = makeSetChordMessage (3, PacketAddress::group, 0, 0x11223344u, 0x55667788u, 0x99aabbccu); + EXPECT_EQ (chord.data[1], 0x11223344u); + EXPECT_EQ (chord.data[2], 0x55667788u); + EXPECT_EQ (chord.data[3], 0x99aabbccu); +} diff --git a/tests/yup_audio_basics/yup_UMPJitterReductionTimestamps.cpp b/tests/yup_audio_basics/yup_UMPJitterReductionTimestamps.cpp new file mode 100644 index 000000000..b41568211 --- /dev/null +++ b/tests/yup_audio_basics/yup_UMPJitterReductionTimestamps.cpp @@ -0,0 +1,97 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; +using namespace yup::ump; + +TEST (JitterReductionTimestampTests, TimestampValue) +{ + JitterTimestamp t0; + EXPECT_EQ (0u, t0.value); + + JitterTimestamp t1 { 55u }; + EXPECT_EQ (55u, t1.value); + + JitterTimestamp t2 { 0xffff }; + EXPECT_EQ (0xffffu, t2.value); +} + +TEST (JitterReductionTimestampTests, TimestampEquality) +{ + JitterTimestamp t1 { 55u }; + JitterTimestamp t2 { 17283u }; + JitterTimestamp t3 { 0xffff }; + JitterTimestamp t4 { 55u }; + + EXPECT_FALSE (t1 == t2); + EXPECT_TRUE (t1 != t2); + EXPECT_FALSE (t2 == t3); + EXPECT_TRUE (t2 != t3); + EXPECT_TRUE (t1 == t4); + EXPECT_FALSE (t1 != t4); +} + +TEST (JitterReductionTimestampTests, TimestampDifference) +{ + JitterTimestamp t1 { 55u }; + JitterTimestamp t2 { 17283u }; + JitterTimestamp t3 { 0xffff }; + JitterTimestamp t4 { 55u }; + + EXPECT_EQ ((t2 - t1).count(), 17228); + EXPECT_EQ ((t3 - t2).count(), 48252); + EXPECT_EQ ((t4 - t3).count(), 56); + EXPECT_EQ ((t1 - t4).count(), 0); +} + +TEST (JitterReductionTimestampTests, JrClockMessage) +{ + JitterClockMessage clockMsg; + EXPECT_EQ (clockMsg.getType(), PacketType::utility); + EXPECT_EQ (clockMsg.getStatus(), uint8_t (UtilityStatus::jitterClock)); + EXPECT_EQ (clockMsg.getByte3(), 0u); + EXPECT_EQ (clockMsg.getByte4(), 0u); + + JitterTimestamp timestamp { 0xf3f4 }; + clockMsg.setTimestamp (timestamp); + EXPECT_EQ (clockMsg.getTimestamp(), timestamp); + EXPECT_EQ (clockMsg.getByte3(), 0xf3u); + EXPECT_EQ (clockMsg.getByte4(), 0xf4u); +} + +TEST (JitterReductionTimestampTests, JitterTimestampMessage) +{ + JitterTimestampMessage tsMsg; + EXPECT_EQ (tsMsg.getType(), PacketType::utility); + EXPECT_EQ (tsMsg.getStatus(), uint8_t (UtilityStatus::jitterTimestamp)); + EXPECT_EQ (tsMsg.getByte3(), 0u); + EXPECT_EQ (tsMsg.getByte4(), 0u); + + JitterTimestamp timestamp { 0x7b7c }; + tsMsg.setTimestamp (timestamp); + EXPECT_EQ (tsMsg.getTimestamp(), timestamp); + EXPECT_EQ (tsMsg.getByte3(), 0x7bu); + EXPECT_EQ (tsMsg.getByte4(), 0x7cu); +} diff --git a/tests/yup_audio_basics/yup_UMPMessages.cpp b/tests/yup_audio_basics/yup_UMPMessages.cpp new file mode 100644 index 000000000..e47c05d96 --- /dev/null +++ b/tests/yup_audio_basics/yup_UMPMessages.cpp @@ -0,0 +1,308 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; +using namespace yup::ump; + +TEST (UMPMessagesTests, UtilityMessageConstructors) +{ + { + UtilityMessage m; + + EXPECT_EQ (m.data[0], 0u); + EXPECT_EQ (m.data[1], 0u); + EXPECT_EQ (m.data[2], 0u); + EXPECT_EQ (m.data[3], 0u); + EXPECT_EQ (m.getType(), PacketType::utility); + EXPECT_EQ (m.getStatus(), 0u); + EXPECT_EQ (m.getSize(), 1u); + } + + { + UtilityMessage m { Status (UtilityStatus::jitterClock) }; + + EXPECT_EQ (m.data[0], 0x00100000u); + EXPECT_EQ (m.data[1], 0u); + EXPECT_EQ (m.data[2], 0u); + EXPECT_EQ (m.data[3], 0u); + EXPECT_EQ (m.getType(), PacketType::utility); + EXPECT_EQ (m.getStatus(), Status (UtilityStatus::jitterClock)); + EXPECT_EQ (m.getSize(), 1u); + } + + { + UtilityMessage m { Status (UtilityStatus::jitterTimestamp), 0xabcd }; + + EXPECT_EQ (m.data[0], 0x0020abcd); + EXPECT_EQ (m.data[1], 0u); + EXPECT_EQ (m.data[2], 0u); + EXPECT_EQ (m.data[3], 0u); + EXPECT_EQ (m.getType(), PacketType::utility); + EXPECT_EQ (m.getStatus(), Status (UtilityStatus::jitterTimestamp)); + EXPECT_EQ (m.getSize(), 1u); + } +} + +TEST (UMPMessagesTests, UtilityMessageView) +{ + { + UtilityMessage m {}; + UtilityMessageView view { m }; + + EXPECT_EQ (view.getStatus(), Status (UtilityStatus::noop)); + EXPECT_EQ (view.getPayload(), 0u); + } + + { + UtilityMessage m { Status (UtilityStatus::jitterTimestamp), 0xabcd }; + UtilityMessageView view { m }; + + EXPECT_EQ (view.getStatus(), Status (UtilityStatus::jitterTimestamp)); + EXPECT_EQ (view.getPayload(), 0xabcd); + } +} + +TEST (UMPMessagesTests, MakeUtilityMessage) +{ + { + const auto m = makeUtilityMessage (Status (UtilityStatus::jitterClock), 0xf499); + EXPECT_EQ (m.data[0], 0x0010f499u); + + const UtilityMessageView view { m }; + EXPECT_EQ (view.getStatus(), Status (UtilityStatus::jitterClock)); + EXPECT_EQ (view.getPayload(), 0xf499); + } + + { + const auto m = makeUtilityMessage (Status (UtilityStatus::jitterTimestamp), 0x17cc); + EXPECT_EQ (m.data[0], 0x002017ccu); + + const UtilityMessageView view { m }; + EXPECT_EQ (view.getStatus(), Status (UtilityStatus::jitterTimestamp)); + EXPECT_EQ (view.getPayload(), 0x17ccu); + } +} + +TEST (UMPMessagesTests, SystemMessageConstructors) +{ + { + SystemMessage m; + + EXPECT_TRUE (isSystemMessage (m)); + EXPECT_EQ (m.data[0], 0x10000000u); + EXPECT_EQ (m.data[1], 0u); + EXPECT_EQ (m.data[2], 0u); + EXPECT_EQ (m.data[3], 0u); + EXPECT_EQ (m.getType(), PacketType::system); + EXPECT_EQ (m.getStatus(), 0u); + EXPECT_EQ (m.getGroup(), 0u); + EXPECT_EQ (m.getSize(), 1u); + } + + { + SystemMessage m { 4, Status (SystemStatus::clock) }; + + EXPECT_TRUE (isSystemMessage (m)); + EXPECT_EQ (m.data[0], 0x14f80000u); + EXPECT_EQ (m.data[1], 0u); + EXPECT_EQ (m.data[2], 0u); + EXPECT_EQ (m.data[3], 0u); + EXPECT_EQ (m.getStatus(), Status (SystemStatus::clock)); + EXPECT_EQ (m.getGroup(), 4u); + EXPECT_EQ (m.getSize(), 1u); + } + + { + SystemMessage m { 9, Status (SystemStatus::mtcQuarterFrame), 0x46 }; + + EXPECT_TRUE (isSystemMessage (m)); + EXPECT_EQ (m.data[0], 0x19f14600u); + EXPECT_EQ (m.data[1], 0u); + EXPECT_EQ (m.data[2], 0u); + EXPECT_EQ (m.data[3], 0u); + EXPECT_EQ (m.getStatus(), Status (SystemStatus::mtcQuarterFrame)); + EXPECT_EQ (m.getGroup(), 9u); + EXPECT_EQ (m.getSize(), 1u); + } + + { + SystemMessage m { 12, Status (SystemStatus::songSelect), 66 }; + + EXPECT_TRUE (isSystemMessage (m)); + EXPECT_EQ (m.data[0], 0x1cf34200u); + EXPECT_EQ (m.data[1], 0u); + EXPECT_EQ (m.data[2], 0u); + EXPECT_EQ (m.data[3], 0u); + EXPECT_EQ (m.getStatus(), Status (SystemStatus::songSelect)); + EXPECT_EQ (m.getGroup(), 12u); + EXPECT_EQ (m.getSize(), 1u); + } +} + +TEST (UMPMessagesTests, SystemMessageView) +{ + { + SystemMessage m {}; + EXPECT_TRUE (isSystemMessage (m)); + EXPECT_TRUE (asSystemMessageView (m)); + + const auto view = SystemMessageView { m }; + EXPECT_EQ (view.getGroup(), 0u); + EXPECT_EQ (view.getStatus(), 0u); + EXPECT_EQ (view.getDataByte1(), 0u); + EXPECT_EQ (view.getDataByte2(), 0u); + EXPECT_EQ (view.getSongPosition(), 0u); + } + + { + const auto m = SystemMessage { 5, Status (SystemStatus::clock) }; + EXPECT_TRUE (isSystemMessage (m)); + EXPECT_TRUE (asSystemMessageView (m)); + + const auto view = SystemMessageView { m }; + EXPECT_EQ (view.getGroup(), 5u); + EXPECT_EQ (view.getStatus(), Status (SystemStatus::clock)); + EXPECT_EQ (view.getDataByte1(), 0u); + EXPECT_EQ (view.getDataByte2(), 0u); + EXPECT_EQ (view.getSongPosition(), 0u); + } + + { + const auto m = SystemMessage { 11, Status (SystemStatus::mtcQuarterFrame), 0x46 }; + EXPECT_TRUE (isSystemMessage (m)); + EXPECT_TRUE (asSystemMessageView (m)); + + const auto view = SystemMessageView { m }; + EXPECT_EQ (view.getGroup(), 11u); + EXPECT_EQ (view.getStatus(), Status (SystemStatus::mtcQuarterFrame)); + EXPECT_EQ (view.getDataByte1(), 0x46u); + EXPECT_EQ (view.getDataByte2(), 0u); + EXPECT_EQ (view.getSongPosition(), 0u); + } + + { + SystemMessage m { 12, Status (SystemStatus::songSelect), 66 }; + EXPECT_TRUE (isSystemMessage (m)); + EXPECT_TRUE (asSystemMessageView (m)); + + auto view = SystemMessageView { m }; + EXPECT_EQ (view.getGroup(), 12u); + EXPECT_EQ (view.getStatus(), Status (SystemStatus::songSelect)); + EXPECT_EQ (view.getDataByte1(), 66u); + EXPECT_EQ (view.getDataByte2(), 0u); + EXPECT_EQ (view.getSongPosition(), 0u); + } + + { + SystemMessage m { 3, Status (SystemStatus::songPosition), 0x34u, 0x24u }; + EXPECT_TRUE (isSystemMessage (m)); + EXPECT_TRUE (asSystemMessageView (m)); + + const auto view = SystemMessageView { m }; + EXPECT_EQ (view.getGroup(), 3u); + EXPECT_EQ (view.getStatus(), Status (SystemStatus::songPosition)); + EXPECT_EQ (view.getDataByte1(), 0x34u); + EXPECT_EQ (view.getDataByte2(), 0x24u); + EXPECT_EQ (view.getSongPosition(), 0x1234u); + } + + EXPECT_FALSE (asSystemMessageView (UniversalPacket { 0x21112233u })); +} + +TEST (UMPMessagesTests, MakeSystemMessage) +{ + { + const auto m = makeSystemMessage (9, Status (SystemStatus::songPosition), 0x74, 0x69); + EXPECT_EQ (m.data[0], 0x19f27469u); + EXPECT_TRUE (isSystemMessage (m)); + EXPECT_TRUE (asSystemMessageView (m)); + + const SystemMessageView view { m }; + EXPECT_EQ (view.getGroup(), 9u); + EXPECT_EQ (view.getStatus(), Status (SystemStatus::songPosition)); + EXPECT_EQ (view.getDataByte1(), 0x74u); + EXPECT_EQ (view.getDataByte2(), 0x69u); + EXPECT_EQ (view.getSongPosition(), 0x34f4u); + } + + { + const auto m = makeSystemMessage (4, Status (SystemStatus::mtcQuarterFrame), 0x43); + EXPECT_EQ (m.data[0], 0x14f14300u); + EXPECT_TRUE (isSystemMessage (m)); + EXPECT_TRUE (asSystemMessageView (m)); + + const auto view = SystemMessageView { m }; + EXPECT_EQ (view.getGroup(), 4u); + EXPECT_EQ (view.getStatus(), Status (SystemStatus::mtcQuarterFrame)); + EXPECT_EQ (view.getDataByte1(), 0x43u); + EXPECT_EQ (view.getDataByte2(), 0u); + EXPECT_EQ (view.getSongPosition(), 0u); + } + + { + const auto m = makeSystemMessage (12, Status (SystemStatus::clock)); + EXPECT_EQ (m.data[0], 0x1cf80000u); + EXPECT_TRUE (isSystemMessage (m)); + EXPECT_TRUE (asSystemMessageView (m)); + + const auto view = SystemMessageView { m }; + EXPECT_EQ (view.getGroup(), 12u); + EXPECT_EQ (view.getStatus(), Status (SystemStatus::clock)); + EXPECT_EQ (view.getDataByte1(), 0u); + EXPECT_EQ (view.getDataByte2(), 0u); + EXPECT_EQ (view.getSongPosition(), 0u); + } +} + +TEST (UMPMessagesTests, MakeSongPositionMessage) +{ + { + const auto m = makeSongPositionMessage (3, 0x1234); + EXPECT_EQ (m.data[0], 0x13f23424u); + EXPECT_TRUE (isSystemMessage (m)); + EXPECT_TRUE (asSystemMessageView (m)); + + const auto view = SystemMessageView { m }; + EXPECT_EQ (view.getGroup(), 3u); + EXPECT_EQ (view.getStatus(), Status (SystemStatus::songPosition)); + EXPECT_EQ (view.getDataByte1(), 0x34u); + EXPECT_EQ (view.getDataByte2(), 0x24u); + EXPECT_EQ (view.getSongPosition(), 0x1234u); + } + + { + const auto m = makeSongPositionMessage (13, 0xfafa); + EXPECT_EQ (m.data[0], 0x1df27a75u); + EXPECT_TRUE (isSystemMessage (m)); + EXPECT_TRUE (asSystemMessageView (m)); + + const auto view = SystemMessageView { m }; + EXPECT_EQ (view.getGroup(), 13u); + EXPECT_EQ (view.getStatus(), Status (SystemStatus::songPosition)); + EXPECT_EQ (view.getDataByte1(), 0x7au); + EXPECT_EQ (view.getDataByte2(), 0x75u); + EXPECT_EQ (view.getSongPosition(), 0x3afau); + } +} diff --git a/tests/yup_audio_basics/yup_UMPMidi1ByteStream.cpp b/tests/yup_audio_basics/yup_UMPMidi1ByteStream.cpp new file mode 100644 index 000000000..276d8cf1f --- /dev/null +++ b/tests/yup_audio_basics/yup_UMPMidi1ByteStream.cpp @@ -0,0 +1,138 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; +using namespace yup::ump; + +namespace +{ +bool packetEquals (const UniversalPacket& a, const UniversalPacket& b) +{ + return a.data[0] == b.data[0] && a.data[1] == b.data[1] && a.data[2] == b.data[2] && a.data[3] == b.data[3]; +} +} // namespace + +TEST (Midi1ByteStreamParserTests, ConstructorAndCallbacks) +{ + bool invoked = false; + Midi1ByteStreamParser parser ([&] (UniversalPacket) + { + invoked = true; + }); + EXPECT_TRUE (parser.callbacksEnabled()); + + parser.feed (0xf8); + EXPECT_TRUE (invoked); + + parser.enableCallbacks (false); + invoked = false; + parser.feed (0xfa); + EXPECT_FALSE (invoked); +} + +TEST (Midi1ByteStreamParserTests, GroupHandling) +{ + int calls = 0; + Midi1ByteStreamParser parser ( + [&] (UniversalPacket packet) + { + ++calls; + EXPECT_EQ (Group { 9 }, packet.getGroup()); + }); + + parser.setGroup (9); + parser.feed (0xfa); + EXPECT_EQ (1, calls); +} + +TEST (Midi1ByteStreamParserTests, SystemMessages) +{ + std::vector received; + Midi1ByteStreamParser parser ([&] (UniversalPacket packet) + { + received.push_back (packet); + }); + + const uint8_t bytes[] = { 0xf8, 0xf1, 0x09, 0xfa, 0xf2, 0x11, 0x44, 0xfb, 0xf3, 0x75, 0xfc }; + for (const auto b : bytes) + parser.feed (b); + + std::vector expected { + makeSystemMessage (0, Status (SystemStatus::clock)), + makeSystemMessage (0, Status (SystemStatus::mtcQuarterFrame), 0x09), + makeSystemMessage (0, Status (SystemStatus::start)), + makeSystemMessage (0, Status (SystemStatus::songPosition), 0x11, 0x44), + makeSystemMessage (0, Status (SystemStatus::cont)), + makeSystemMessage (0, Status (SystemStatus::songSelect), 0x75), + makeSystemMessage (0, Status (SystemStatus::stop)) + }; + + ASSERT_EQ (expected.size(), received.size()); + for (size_t i = 0; i < expected.size(); ++i) + EXPECT_TRUE (packetEquals (expected[i], received[i])); +} + +TEST (Midi1ByteStreamParserTests, ChannelVoiceMessages) +{ + std::vector received; + Midi1ByteStreamParser parser ([&] (UniversalPacket packet) + { + received.push_back (packet); + }); + + const uint8_t bytes[] = { 0x83, 0x45, 0x6e, 0x9e, 0x30, 0x7f, 0xaa, 0x44, 0x03, 0x77, 0xb0, 0x07, 0x70 }; + for (const auto b : bytes) + parser.feed (b); + + std::vector expected { + makeMidi1ChannelVoiceMessage (0, Status (Midi1ChannelVoiceStatus::noteOff), 3, 0x45, 0x6e), + makeMidi1ChannelVoiceMessage (0, Status (Midi1ChannelVoiceStatus::noteOn), 14, 0x30, 0x7f), + makeMidi1ChannelVoiceMessage (0, Status (Midi1ChannelVoiceStatus::polyPressure), 10, 0x44, 0x03), + makeMidi1ChannelVoiceMessage (0, Status (Midi1ChannelVoiceStatus::controlChange), 0, 0x07, 0x70) + }; + + ASSERT_EQ (expected.size(), received.size()); + for (size_t i = 0; i < expected.size(); ++i) + EXPECT_TRUE (packetEquals (expected[i], received[i])); +} + +TEST (Midi1ByteStreamParserTests, Midi1ByteStreamRoundTrip) +{ + const auto packet = makeMidi1ChannelVoiceMessage (0, Status (Midi1ChannelVoiceStatus::noteOn), 1, 60, 90); + + uint8_t bytes[8] = {}; + const auto size = toMidi1ByteStream (packet, bytes); + EXPECT_EQ (size, 3u); + + std::vector received; + Midi1ByteStreamParser parser ([&] (UniversalPacket p) + { + received.push_back (p); + }); + parser.feed (bytes, size); + + ASSERT_EQ (received.size(), 1u); + EXPECT_TRUE (packetEquals (packet, received.front())); +} diff --git a/tests/yup_audio_basics/yup_UMPMidi1ChannelVoiceMessage.cpp b/tests/yup_audio_basics/yup_UMPMidi1ChannelVoiceMessage.cpp new file mode 100644 index 000000000..65c9a0005 --- /dev/null +++ b/tests/yup_audio_basics/yup_UMPMidi1ChannelVoiceMessage.cpp @@ -0,0 +1,86 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; +using namespace yup::ump; + +TEST (UMPMidi1ChannelVoiceMessageTests, Constructors) +{ + Midi1ChannelVoiceMessage m (4, Status (Midi1ChannelVoiceStatus::noteOn) + 6, 44, 101); + + EXPECT_TRUE (isMidi1ChannelVoiceMessage (m)); + EXPECT_EQ (m.data[0], 0x24962c65u); + EXPECT_EQ (m.getGroup(), 4u); + + auto view = Midi1ChannelVoiceMessageView { m }; + EXPECT_EQ (view.getGroup(), 4u); + EXPECT_EQ (view.getStatus(), Status (Midi1ChannelVoiceStatus::noteOn)); + EXPECT_EQ (view.getChannel(), 6u); + EXPECT_EQ (view.getDataByte1(), 0x2cu); + EXPECT_EQ (view.getDataByte2(), 0x65u); +} + +TEST (UMPMidi1ChannelVoiceMessageTests, ViewAndChannel) +{ + constexpr uint32_t testCases[] = { + 0x208e2345u, 0x23922345u, 0x2aa54621u, 0x26b12345u, 0x27cd2345u, 0x29ca2345u, 0x2ce74621u + }; + + for (const auto data : testCases) + { + Midi1ChannelVoiceMessage m { UniversalPacket { data } }; + const auto status = uint8_t ((data >> 16u) & 0xffu); + EXPECT_EQ (m.getChannel(), Channel (status & 0x0f)); + } +} + +TEST (UMPMidi1ChannelVoiceMessageTests, DataBytesAnd14Bit) +{ + auto m = makeMidi1ChannelVoiceMessage (1, Status (Midi1ChannelVoiceStatus::noteOn), 9, 0x12, 0x34); + auto view = Midi1ChannelVoiceMessageView { m }; + + EXPECT_EQ (view.getDataByte1(), 0x12u); + EXPECT_EQ (view.getDataByte2(), 0x34u); + EXPECT_EQ (view.get14BitValue(), 0x1a12u); +} + +TEST (UMPMidi1ChannelVoiceMessageTests, MakeMessageHelpers) +{ + auto noteOn = makeMidi1NoteOnMessage (1, 2, 60, Velocity { uint7_t { 12 } }); + auto noteOff = makeMidi1NoteOffMessage (3, 4, 61, Velocity { uint7_t { 20 } }); + auto pressure = makeMidi1PolyPressureMessage (1, 5, 62, ControllerValue { uint7_t { 33 } }); + auto control = makeMidi1ControlChangeMessage (2, 6, 7, ControllerValue { uint7_t { 99 } }); + auto program = makeMidi1ProgramChangeMessage (3, 7, 42); + auto channelPressure = makeMidi1ChannelPressureMessage (4, 8, ControllerValue { uint7_t { 77 } }); + auto pitchBend = makeMidi1PitchBendMessage (5, 9, PitchBend { uint14_t { 0x1234 } }); + + EXPECT_TRUE (isMidi1ChannelVoiceMessage (noteOn)); + EXPECT_TRUE (isMidi1ChannelVoiceMessage (noteOff)); + EXPECT_TRUE (isMidi1ChannelVoiceMessage (pressure)); + EXPECT_TRUE (isMidi1ChannelVoiceMessage (control)); + EXPECT_TRUE (isMidi1ChannelVoiceMessage (program)); + EXPECT_TRUE (isMidi1ChannelVoiceMessage (channelPressure)); + EXPECT_TRUE (isMidi1ChannelVoiceMessage (pitchBend)); +} diff --git a/tests/yup_audio_basics/yup_UMPMidi2ChannelVoiceMessage.cpp b/tests/yup_audio_basics/yup_UMPMidi2ChannelVoiceMessage.cpp new file mode 100644 index 000000000..f377e0823 --- /dev/null +++ b/tests/yup_audio_basics/yup_UMPMidi2ChannelVoiceMessage.cpp @@ -0,0 +1,73 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; +using namespace yup::ump; + +TEST (UMPMidi2ChannelVoiceMessageTests, Constructors) +{ + Midi2ChannelVoiceMessage m (4, Status (ChannelVoiceStatus::noteOn), 6, 44, 101, 0x12345678u); + + EXPECT_TRUE (isMidi2ChannelVoiceMessage (m)); + EXPECT_EQ (m.getGroup(), 4u); + + auto view = Midi2ChannelVoiceMessageView { m }; + EXPECT_EQ (view.getGroup(), 4u); + EXPECT_EQ (view.getStatus(), Status (ChannelVoiceStatus::noteOn)); + EXPECT_EQ (view.getChannel(), 6u); + EXPECT_EQ (view.getByte3(), 44u); + EXPECT_EQ (view.getByte4(), 101u); + EXPECT_EQ (view.getData(), 0x12345678u); +} + +TEST (UMPMidi2ChannelVoiceMessageTests, MakeMessageHelpers) +{ + auto noteOn = makeMidi2NoteOnMessage (1, 2, 60, Velocity { uint16_t { 0x1234 } }); + auto noteOff = makeMidi2NoteOffMessage (3, 4, 61, Velocity { uint16_t { 0x5678 } }); + auto poly = makeMidi2PolyPressureMessage (1, 5, 62, ControllerValue { uint32_t { 0x12345678 } }); + auto control = makeMidi2ControlChangeMessage (2, 6, 7, ControllerValue { uint32_t { 0xabcdef01 } }); + auto program = makeMidi2ProgramChangeMessage (3, 7, 42); + auto channelPressure = makeMidi2ChannelPressureMessage (4, 8, ControllerValue { uint32_t { 0xdeadbeef } }); + auto pitchBend = makeMidi2PitchBendMessage (5, 9, PitchBend { uint32_t { 0x81234567 } }); + + EXPECT_TRUE (isMidi2ChannelVoiceMessage (noteOn)); + EXPECT_TRUE (isMidi2ChannelVoiceMessage (noteOff)); + EXPECT_TRUE (isMidi2ChannelVoiceMessage (poly)); + EXPECT_TRUE (isMidi2ChannelVoiceMessage (control)); + EXPECT_TRUE (isMidi2ChannelVoiceMessage (program)); + EXPECT_TRUE (isMidi2ChannelVoiceMessage (channelPressure)); + EXPECT_TRUE (isMidi2ChannelVoiceMessage (pitchBend)); +} + +TEST (UMPMidi2ChannelVoiceMessageTests, PerNoteMessages) +{ + auto reg = makeRegisteredPerNoteControllerMessage (15, 10, 44, 2, ControllerValue { 123456u }); + auto assign = makeAssignablePerNoteControllerMessage (3, 7, 64, 99, ControllerValue { 987654u }); + auto perNote = makePerNotePitchBendMessage (11, 12, 13, PitchBend { uint32_t { 0x80000001u } }); + + EXPECT_TRUE (isMidi2ChannelVoiceMessage (reg)); + EXPECT_TRUE (isMidi2ChannelVoiceMessage (assign)); + EXPECT_TRUE (isMidi2ChannelVoiceMessage (perNote)); +} diff --git a/tests/yup_audio_basics/yup_UMPStreamMessages.cpp b/tests/yup_audio_basics/yup_UMPStreamMessages.cpp new file mode 100644 index 000000000..493f63f4c --- /dev/null +++ b/tests/yup_audio_basics/yup_UMPStreamMessages.cpp @@ -0,0 +1,99 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; +using namespace yup::ump; + +TEST (UMPStreamMessagesTests, ConstructorsAndFormat) +{ + StreamMessage m; + EXPECT_TRUE (isStreamMessage (m)); + EXPECT_EQ (m.getFormat(), PacketFormat::complete); + EXPECT_EQ (m.getType(), PacketType::stream); + + StreamMessage info { Status (StreamStatus::endpointInfo) }; + EXPECT_EQ (info.getFormat(), PacketFormat::complete); + EXPECT_EQ (info.getStatus(), Status (StreamStatus::endpointInfo)); + + StreamMessage cont { Status (StreamStatus::endpointName), PacketFormat::cont }; + EXPECT_EQ (cont.getFormat(), PacketFormat::cont); + + StreamMessage start { Status (StreamStatus::productInstanceId), PacketFormat::start }; + EXPECT_EQ (start.getFormat(), PacketFormat::start); + + StreamMessage end { Status (StreamStatus::endpointName), PacketFormat::end }; + EXPECT_EQ (end.getFormat(), PacketFormat::end); +} + +TEST (UMPStreamMessagesTests, SetFormat) +{ + StreamMessage m; + m.setFormat (PacketFormat::end); + EXPECT_EQ (m.getFormat(), PacketFormat::end); +} + +TEST (UMPStreamMessagesTests, EndpointDiscoveryView) +{ + auto msg = makeEndpointDiscoveryMessage (StreamDiscoveryFilter::endpointAll, 1, 2); + auto view = EndpointDiscoveryView { msg }; + + EXPECT_EQ (view.getUmpVersionMajor(), 1u); + EXPECT_EQ (view.getUmpVersionMinor(), 2u); + EXPECT_TRUE (view.requestsInfo()); + EXPECT_TRUE (view.requestsDeviceIdentity()); +} + +TEST (UMPStreamMessagesTests, EndpointInfoView) +{ + auto msg = makeEndpointInfoMessage (3, + true, + uint8_t (StreamProtocol::midi2), + uint8_t (StreamExtensions::jitterReductionTransmit)); + auto view = EndpointInfoView { msg }; + + EXPECT_EQ (view.getNumFunctionBlocks(), 3u); + EXPECT_TRUE (view.hasStaticFunctionBlocks()); + EXPECT_EQ (view.getProtocols(), 0x2u); +} + +TEST (UMPStreamMessagesTests, DeviceIdentityView) +{ + const DeviceIdentity identity { Manufacturer::nativeInstruments, 0x1234, 0x1678, 0x0abcdef0 }; + auto msg = makeDeviceIdentityMessage (identity); + auto view = DeviceIdentityView { msg }; + + const auto parsed = view.getIdentity(); + EXPECT_EQ (parsed.manufacturer, identity.manufacturer); + EXPECT_EQ (parsed.family, identity.family); + EXPECT_EQ (parsed.model, identity.model); + EXPECT_EQ (parsed.revision, identity.revision); +} + +TEST (UMPStreamMessagesTests, EndpointNamePayload) +{ + auto msg = makeEndpointNameMessage (PacketFormat::complete, "Endpoint"); + auto view = EndpointNameView { msg }; + EXPECT_EQ (view.getPayload(), "Endpoint"); +} diff --git a/tests/yup_audio_basics/yup_UMPSysExCollectors.cpp b/tests/yup_audio_basics/yup_UMPSysExCollectors.cpp new file mode 100644 index 000000000..0d27a0bc5 --- /dev/null +++ b/tests/yup_audio_basics/yup_UMPSysExCollectors.cpp @@ -0,0 +1,238 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +#include +#include + +using namespace yup; +using namespace yup::ump; + +namespace +{ +struct SysEx7TestCase +{ + std::string description; + std::vector packets; + SysEx7 sysex; +}; + +struct SysEx8TestCase +{ + std::string description; + std::vector packets; + uint8_t streamId; + SysEx8 sysex; +}; + +const std::vector sysEx7TestCases { + { "empty sysex", + { UniversalPacket { 0x30010000u, 0x00000000u } }, + SysEx7 {} }, + { "one byte manufacturer only", + { UniversalPacket { 0x31010400u, 0x00000000u } }, + SysEx7 { Manufacturer::moog } }, + { "three byte manufacturer only", + { UniversalPacket { 0x35030021u, 0x09000000u } }, + SysEx7 { Manufacturer::nativeInstruments } }, + { "three byte manufacturer, complete message", + { UniversalPacket { 0x31050002u, 0x0d152600u } }, + SysEx7 { Manufacturer::google, { 0x15, 0x26 } } }, + { "one byte manufacturer, complete message", + { UniversalPacket { 0x3f060905u, 0x04030201u } }, + SysEx7 { Manufacturer::midi9, { 5, 4, 3, 2, 1 } } }, + { "two messages", + { UniversalPacket { 0x3a164e09u, 0x08070605u }, + UniversalPacket { 0x3a340403u, 0x02010000u } }, + SysEx7 { Manufacturer::teac, { 9, 8, 7, 6, 5, 4, 3, 2, 1 } } }, + { "three messages", + { UniversalPacket { 0x36160021u, 0x1d112233u }, + UniversalPacket { 0x36264455u, 0x66771829u }, + UniversalPacket { 0x36353a4bu, 0x5c6d7c00u } }, + SysEx7 { Manufacturer::ableton, + { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x18, 0x29, 0x3a, 0x4b, 0x5c, 0x6d, 0x7c } } }, + { "four messages", + { UniversalPacket { 0x31160020u, 0x6b112233u }, + UniversalPacket { 0x31264455u, 0x66771829u }, + UniversalPacket { 0x31263a4bu, 0x5c6d7c0fu }, + UniversalPacket { 0x31311000u, 0x00000000u } }, + SysEx7 { Manufacturer::arturia, + { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x18, 0x29, 0x3a, 0x4b, 0x5c, 0x6d, 0x7c, 0x0f, 0x10 } } } +}; + +const std::vector sysEx8TestCases { + { "empty sysex", + { UniversalPacket { 0x50034400u, 0x00000000u } }, + 0x44, + SysEx8 {} }, + { "one byte manufacturer only", + { UniversalPacket { 0x51033400u, 0x04000000u } }, + 0x34, + SysEx8 { Manufacturer::moog } }, + { "three byte manufacturer only", + { UniversalPacket { 0x550300a1u, 0x09000000u } }, + 0x00, + SysEx8 { Manufacturer::nativeInstruments } }, + { "three byte manufacturer, complete packet", + { UniversalPacket { 0x5106aa82u, 0x0d85a600u } }, + 0xaa, + SysEx8 { Manufacturer::google, { 0x85, 0xa6, 0x00 } } }, + { "one byte manufacturer, complete packet", + { UniversalPacket { 0x5f089300u, 0x09f5e4d3u, 0xc2b10000u, 0 } }, + 0x93, + SysEx8 { Manufacturer::midi9, { 0xf5, 0xe4, 0xd3, 0xc2, 0xb1 } } }, + { "two packets", + { UniversalPacket { 0x5a1e3900u, 0x4e090807u, 0x06050403u, 0x020100ffu }, + UniversalPacket { 0x5a3639eeu, 0xddccbbaau } }, + 0x39, + SysEx8 { Manufacturer::teac, { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa } } }, + { "three packets", + { UniversalPacket { 0x561ec3a1u, 0x1d112233u, 0x44556677u, 0x18293a4bu }, + UniversalPacket { 0x562ec35cu, 0x6d7c8b9au, 0xa9b8c7d6u, 0xe5f40312u }, + UniversalPacket { 0x5632c331u, 0 } }, + 0xc3, + SysEx8 { Manufacturer::ableton, + { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x18, 0x29, 0x3a, 0x4b, 0x5c, 0x6d, 0x7c, 0x8b, 0x9a, 0xa9, 0xb8, 0xc7, 0xd6, 0xe5, 0xf4, 0x03, 0x12, 0x31 } } }, + { "lots of data", + { UniversalPacket { 0x511e83a1u, 0x09112233u, 0x44556677u, 0x8899aabbu }, + UniversalPacket { 0x512e83ccu, 0xddeeff18u, 0x293a4b5cu, 0x6d7e8f90u }, + UniversalPacket { 0x512e83a1u, 0xb2c3d4e5u, 0xf6e7d8f0u, 0xe1d2c3b4u }, + UniversalPacket { 0x512e83a5u, 0x96877869u, 0x5a4b3c2du, 0x1e0f420fu }, + UniversalPacket { 0x51328310u, 0x00000000u } }, + 0x83, + SysEx8 { Manufacturer::nativeInstruments, + { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x18, 0x29, 0x3a, 0x4b, 0x5c, 0x6d, 0x7e, 0x8f, 0x90, 0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xe7, 0xd8, 0xf0, 0xe1, 0xd2, 0xc3, 0xb4, 0xa5, 0x96, 0x87, 0x78, 0x69, 0x5a, 0x4b, 0x3c, 0x2d, 0x1e, 0x0f, 0x42, 0x0f, 0x10 } } } +}; +} // namespace + +TEST (SysEx7CollectorTests, CollectsPacketSequences) +{ + for (const auto& entry : sysEx7TestCases) + { + bool outputGenerated = false; + SysEx7Collector collector ([&] (const SysEx7& sx) + { + outputGenerated = true; + EXPECT_EQ (sx, entry.sysex) << entry.description; + }); + + for (const auto& packet : entry.packets) + collector.feed (packet); + + EXPECT_TRUE (outputGenerated) << entry.description; + } +} + +TEST (SysEx7CollectorTests, RoundTripPacketization) +{ + const SysEx7 sysex { Manufacturer::nativeInstruments, { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77 } }; + const auto packets = asSysEx7Packets (sysex); + + bool outputGenerated = false; + SysEx7Collector collector ([&] (const SysEx7& sx) + { + outputGenerated = true; + EXPECT_EQ (sx, sysex); + }); + + for (const auto& packet : packets) + collector.feed (packet); + + EXPECT_TRUE (outputGenerated); +} + +TEST (SysEx8CollectorTests, CollectsPacketSequences) +{ + for (const auto& entry : sysEx8TestCases) + { + bool outputGenerated = false; + SysEx8Collector collector ([&] (const SysEx8& sx, uint8_t streamId) + { + outputGenerated = true; + EXPECT_EQ (sx, entry.sysex) << entry.description; + EXPECT_EQ (streamId, entry.streamId) << entry.description; + }); + + for (const auto& packet : entry.packets) + collector.feed (packet); + + EXPECT_TRUE (outputGenerated) << entry.description; + } +} + +TEST (SysEx8CollectorTests, RoundTripPacketization) +{ + const SysEx8 sysex { Manufacturer::google, { 0x11, 0x22, 0x33, 0x44, 0x55 } }; + const uint8_t streamId = 0x7f; + const auto packets = asSysEx8Packets (sysex, streamId); + + bool outputGenerated = false; + SysEx8Collector collector ([&] (const SysEx8& sx, uint8_t stream) + { + outputGenerated = true; + EXPECT_EQ (sx, sysex); + EXPECT_EQ (stream, streamId); + }); + + for (const auto& packet : packets) + collector.feed (packet); + + EXPECT_TRUE (outputGenerated); +} + +TEST (SysEx7CollectorTests, HonorsMaxSize) +{ + bool outputGenerated = false; + SysEx7Collector collector ([&] (const SysEx7&) + { + outputGenerated = true; + }); + collector.setMaxSysExDataSize (12); + + const std::vector packets { + UniversalPacket { 0x30167e12u, 0x34567809u }, + UniversalPacket { 0x30261a2bu, 0x3c4d5e6fu }, + UniversalPacket { 0x30320000u, 0 } + }; + + for (const auto& packet : packets) + collector.feed (packet); + + EXPECT_FALSE (outputGenerated); +} + +TEST (SysEx8CollectorTests, StreamIdSwitchTriggersNewMessage) +{ + bool outputGenerated = false; + SysEx8Collector collector ([&] (const SysEx8&, uint8_t) + { + outputGenerated = true; + }); + + collector.feed (UniversalPacket { 0x55130f80u, 0x3b000000u }); + EXPECT_EQ (collector.getStreamId(), 0x0fu); + + collector.feed (UniversalPacket { 0x55310f44u, 0x00000000u }); + EXPECT_TRUE (outputGenerated); +} diff --git a/tests/yup_audio_basics/yup_UMPTypes.cpp b/tests/yup_audio_basics/yup_UMPTypes.cpp new file mode 100644 index 000000000..4c4a7aad3 --- /dev/null +++ b/tests/yup_audio_basics/yup_UMPTypes.cpp @@ -0,0 +1,81 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; +using namespace yup::ump; + +TEST (UMPTypesTests, DownsampleMinCenterMax) +{ + EXPECT_EQ (downsample16To7Bit (0), 0u); + EXPECT_EQ (downsample32To7Bit (0), 0u); + EXPECT_EQ (downsample32To14Bit (0), 0u); + + EXPECT_EQ (downsample16To7Bit (0x8000), 0x40u); + EXPECT_EQ (downsample32To7Bit (0x80000000u), 0x40u); + EXPECT_EQ (downsample32To14Bit (0x80000000u), 0x2000u); + + EXPECT_EQ (downsample16To7Bit (0xffff), 0x7fu); + EXPECT_EQ (downsample32To7Bit (0xffffffffu), 0x7fu); + EXPECT_EQ (downsample32To14Bit (0xffffffffu), 0x3fffu); +} + +TEST (UMPTypesTests, UpsampleMinCenterMax) +{ + EXPECT_EQ (upsample7To16Bit (0), 0u); + EXPECT_EQ (upsample7To32Bit (0), 0u); + EXPECT_EQ (upsample14To32Bit (0), 0u); + + EXPECT_EQ (upsample7To16Bit (0x40), 0x8000u); + EXPECT_EQ (upsample7To32Bit (0x40), 0x80000000u); + EXPECT_EQ (upsample14To32Bit (0x2000), 0x80000000u); + + EXPECT_EQ (upsample7To16Bit (0x7f), 0xffffu); + EXPECT_EQ (upsample7To32Bit (0x7f), 0xffffffffu); + EXPECT_EQ (upsample14To32Bit (0x3fff), 0xffffffffu); +} + +TEST (UMPTypesTests, PreserveConversions) +{ + for (uint7_t v = 0u; v < 0x80; ++v) + EXPECT_EQ (v, downsample16To7Bit (upsample7To16Bit (v))); + + for (uint7_t v = 0u; v < 0x80; ++v) + EXPECT_EQ (v, downsample32To7Bit (upsample7To32Bit (v))); + + for (uint14_t v = 0u; v < 0x4000; ++v) + EXPECT_EQ (v, downsample32To14Bit (upsample14To32Bit (v))); +} + +TEST (UMPTypesTests, UpsampleXToYBit) +{ + for (uint7_t v = 0u; v < 0x80; ++v) + EXPECT_EQ (upsample7To16Bit (v), upsampleXToYBit (v, 7, 16)); + + for (uint7_t v = 0u; v < 0x80; ++v) + EXPECT_EQ (upsample7To32Bit (v), upsampleXToYBit (v, 7, 32)); + + for (uint14_t v = 0u; v < 0x4000; ++v) + EXPECT_EQ (upsample14To32Bit (v), upsampleXToYBit (v, 14, 32)); +} diff --git a/tests/yup_audio_basics/yup_UMPUniversalPacket.cpp b/tests/yup_audio_basics/yup_UMPUniversalPacket.cpp new file mode 100644 index 000000000..94924a927 --- /dev/null +++ b/tests/yup_audio_basics/yup_UMPUniversalPacket.cpp @@ -0,0 +1,520 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +#include +#include + +using namespace yup; +using namespace yup::ump; + +TEST (UMPUniversalPacketTests, Constructors) +{ + { + UniversalPacket p; + + EXPECT_EQ (p.data[0], 0u); + EXPECT_EQ (p.data[1], 0u); + EXPECT_EQ (p.data[2], 0u); + EXPECT_EQ (p.data[3], 0u); + + EXPECT_EQ (p.getSize(), 1u); + EXPECT_EQ (p.getType(), PacketType::utility); + EXPECT_EQ (p.getGroup(), 0u); + EXPECT_EQ (p.getStatus(), 0u); + EXPECT_EQ (p.getByte2(), 0u); + EXPECT_EQ (p.getByte3(), 0u); + EXPECT_EQ (p.getByte4(), 0u); + } + + { + UniversalPacket p { 0x12345678u }; + EXPECT_EQ (p.data[0], 0x12345678u); + EXPECT_EQ (p.data[1], 0u); + EXPECT_EQ (p.data[2], 0u); + EXPECT_EQ (p.data[3], 0u); + } + + { + UniversalPacket p { 1933467u }; + EXPECT_EQ (p.data[0], 1933467u); + EXPECT_EQ (p.data[1], 0u); + EXPECT_EQ (p.data[2], 0u); + EXPECT_EQ (p.data[3], 0u); + } + + { + UniversalPacket p { 0x21004567u, 0x145938aau }; + EXPECT_EQ (p.data[0], 0x21004567u); + EXPECT_EQ (p.data[1], 0x145938aau); + EXPECT_EQ (p.data[2], 0u); + EXPECT_EQ (p.data[3], 0u); + } + + { + UniversalPacket p { 0xbc352890u, 0x9d2a445cu, 77777u }; + EXPECT_EQ (p.data[0], 0xbc352890u); + EXPECT_EQ (p.data[1], 0x9d2a445cu); + EXPECT_EQ (p.data[2], 77777u); + EXPECT_EQ (p.data[3], 0u); + } + + { + UniversalPacket p { 0x54abcdefu, 0x12345678u, 0xabadbabeu, 0xcdcdcdcdu }; + EXPECT_EQ (p.data[0], 0x54abcdefu); + EXPECT_EQ (p.data[1], 0x12345678u); + EXPECT_EQ (p.data[2], 0xabadbabeu); + EXPECT_EQ (p.data[3], 0xcdcdcdcdu); + } +} + +TEST (UMPUniversalPacketTests, Equality) +{ + { + UniversalPacket p1; + UniversalPacket p2 { 0x12345678u }; + UniversalPacket p3 { 1933467u }; + UniversalPacket p4 { 0x31004567u, 0x145938aau }; + UniversalPacket p5 { 0x12345678u }; + + EXPECT_EQ (p1, p1); + EXPECT_NE (p1, p2); + EXPECT_NE (p1, p3); + EXPECT_NE (p1, p4); + EXPECT_NE (p1, p5); + + EXPECT_NE (p2, p1); + EXPECT_EQ (p2, p2); + EXPECT_NE (p2, p3); + EXPECT_NE (p2, p4); + EXPECT_EQ (p2, p5); + + EXPECT_NE (p3, p1); + EXPECT_NE (p3, p2); + EXPECT_EQ (p3, p3); + EXPECT_NE (p3, p4); + EXPECT_NE (p3, p5); + + EXPECT_NE (p4, p1); + EXPECT_NE (p4, p2); + EXPECT_NE (p4, p3); + EXPECT_EQ (p4, p4); + EXPECT_NE (p4, p5); + + EXPECT_NE (p5, p1); + EXPECT_EQ (p5, p2); + EXPECT_NE (p5, p3); + EXPECT_NE (p5, p4); + EXPECT_EQ (p5, p5); + } + + { + UniversalPacket p1 { 0xba345678u, 99u, 55u, 42u }; + UniversalPacket p2 { 0xba345678u, 99u, 55u, 43u }; + UniversalPacket p3 { 0xba345679u, 99u, 55u, 42u }; + UniversalPacket p4 { 0xba345678u, 98u, 55u, 42u }; + UniversalPacket p5 { 0xba345678u, 99u, 56u, 42u }; + + EXPECT_EQ (p1, p1); + EXPECT_EQ (p1, p2); + EXPECT_NE (p1, p3); + EXPECT_NE (p1, p4); + EXPECT_NE (p1, p5); + + EXPECT_EQ (p2, p1); + EXPECT_EQ (p2, p2); + EXPECT_NE (p2, p3); + EXPECT_NE (p2, p4); + EXPECT_NE (p2, p5); + + EXPECT_NE (p3, p1); + EXPECT_NE (p3, p2); + EXPECT_EQ (p3, p3); + EXPECT_NE (p3, p4); + EXPECT_NE (p3, p5); + + EXPECT_NE (p4, p1); + EXPECT_NE (p4, p2); + EXPECT_NE (p4, p3); + EXPECT_EQ (p4, p4); + EXPECT_NE (p4, p5); + + EXPECT_NE (p5, p1); + EXPECT_NE (p5, p2); + EXPECT_NE (p5, p3); + EXPECT_NE (p5, p4); + EXPECT_EQ (p5, p5); + } +} + +TEST (UMPUniversalPacketTests, EqualityIgnoresUnusedWords) +{ + UniversalPacket p1 { 0x12345678u, 1000 }; + UniversalPacket p2 { 0x12345678u, 1001 }; + UniversalPacket px { 0x12345677u, 1000 }; + + EXPECT_EQ (p1, p2); + EXPECT_NE (p1, px); + + UniversalPacket p3 { 0x31004567u, 0x145938aau, 2939 }; + UniversalPacket p4 { 0x31004567u, 0x145938aau, 55 }; + UniversalPacket py { 0x31004568u, 0x145938aau, 2939 }; + UniversalPacket pz { 0x31004567u, 0x5938aa12u, 2939 }; + + EXPECT_EQ (p3, p4); + EXPECT_NE (p3, py); + EXPECT_NE (p3, pz); +} + +TEST (UMPUniversalPacketTests, ByteAccess) +{ + UniversalPacket p { 0x11223344u, 0x55667788u, 0x99aabbccu, 0xddeeff00u }; + + EXPECT_EQ (p.getByte (0), 0x11u); + EXPECT_EQ (p.getByte (3), 0x44u); + EXPECT_EQ (p.getByte (4), 0x55u); + EXPECT_EQ (p.getByte (15), 0x00u); + + p.setByte (0, 0xaa); + p.setByte (15, 0xbb); + + EXPECT_EQ (p.getByte (0), 0xaau); + EXPECT_EQ (p.getByte (15), 0xbbu); +} + +TEST (UMPUniversalPacketTests, Type) +{ + UniversalPacket p0 { 0x011f1234u }; + EXPECT_EQ (p0.getType(), PacketType::utility); + + UniversalPacket p1 { 0x15f12345u }; + EXPECT_EQ (p1.getType(), PacketType::system); + + UniversalPacket p2 { 0x2cbf8765u }; + EXPECT_EQ (p2.getType(), PacketType::midi1ChannelVoice); + + UniversalPacket p3 { 0x34317f03u, 0x12345678u }; + EXPECT_EQ (p3.getType(), PacketType::data); + + UniversalPacket p4 { 0x41937788u, 0x81114080u }; + EXPECT_EQ (p4.getType(), PacketType::midi2ChannelVoice); + + UniversalPacket p5 { 0x59937788u, 0x11111111u, 0x22222222u, 0x22222222u }; + EXPECT_EQ (p5.getType(), PacketType::extendedData); + + p0.setType (PacketType::midi1ChannelVoice); + EXPECT_EQ (p0.getType(), PacketType::midi1ChannelVoice); + + p5.setType (PacketType::midi2ChannelVoice); + EXPECT_EQ (p5.getType(), PacketType::midi2ChannelVoice); +} + +TEST (UMPUniversalPacketTests, Size) +{ + EXPECT_EQ (UniversalPacket { 0x011f1234u }.getSize(), 1u); + EXPECT_EQ (UniversalPacket { 0x15f12345u }.getSize(), 1u); + EXPECT_EQ (UniversalPacket { 0x2cbf8765u }.getSize(), 1u); + EXPECT_EQ ((UniversalPacket { 0x34317f03u, 0x12345678u }.getSize()), 2u); + EXPECT_EQ ((UniversalPacket { 0x41937788u, 0x81114080u }.getSize()), 2u); + EXPECT_EQ ((UniversalPacket { 0x59937788u, 0x11111111u, 0x22222222u, 0x22222222u }.getSize()), 4u); + + EXPECT_EQ (UniversalPacket { 0x60000000u }.getSize(), 1u); + EXPECT_EQ (UniversalPacket { 0x70000000u }.getSize(), 1u); + EXPECT_EQ (UniversalPacket { 0x80000000u }.getSize(), 2u); + EXPECT_EQ (UniversalPacket { 0x90000000u }.getSize(), 2u); + EXPECT_EQ (UniversalPacket { 0xa0000000u }.getSize(), 2u); + EXPECT_EQ (UniversalPacket { 0xb0000000u }.getSize(), 3u); + EXPECT_EQ (UniversalPacket { 0xc0000000u }.getSize(), 3u); + EXPECT_EQ (UniversalPacket { 0xd0000000u }.getSize(), 4u); + EXPECT_EQ (UniversalPacket { 0xe0000000u }.getSize(), 4u); + EXPECT_EQ (UniversalPacket { 0xf0000000u }.getSize(), 4u); +} + +TEST (UMPUniversalPacketTests, Group) +{ + UniversalPacket p0 { 0x011f1234u }; + EXPECT_EQ (p0.getGroup(), 1u); + + UniversalPacket p1 { 0x15f12345u }; + EXPECT_EQ (p1.getGroup(), 5u); + + UniversalPacket p2 { 0x2cbf8765u }; + EXPECT_EQ (p2.getGroup(), 12u); + + UniversalPacket p3 { 0x34317f03u, 0x12345678u }; + EXPECT_EQ (p3.getGroup(), 4u); + + UniversalPacket p4 { 0x41937788u, 0x81114080u }; + EXPECT_EQ (p4.getGroup(), 1u); + + UniversalPacket p5 { 0x59937788u, 0x11111111u, 0x22222222u, 0x22222222u }; + EXPECT_EQ (p5.getGroup(), 9u); + + p1.setGroup (4); + EXPECT_EQ (p1.getGroup(), 4u); + + p3.setGroup (0x0f); + EXPECT_EQ (p3.getGroup(), 15u); +} + +TEST (UMPUniversalPacketTests, Status) +{ + UniversalPacket p0 { 0x011f1234u }; + EXPECT_EQ (p0.getStatus(), 0x1fu); + + UniversalPacket p1 { 0x15f12345u }; + EXPECT_EQ (p1.getStatus(), 0xf1u); + + UniversalPacket p2 { 0x2cbf8765u }; + EXPECT_EQ (p2.getStatus(), 0xbfu); + + UniversalPacket p3 { 0x34317f03u, 0x12345678u }; + EXPECT_EQ (p3.getStatus(), 0x31u); + + UniversalPacket p4 { 0x41937788u, 0x81114080u }; + EXPECT_EQ (p4.getStatus(), 0x93u); + + UniversalPacket p5 { 0x599a7788u, 0x11111111u, 0x22222222u, 0x22222222u }; + EXPECT_EQ (p5.getStatus(), 0x9au); + + p2.setByte (1, 0xabu); + EXPECT_EQ (p2.getStatus(), 0xabu); + + p4.setByte (1, 0x01u); + EXPECT_EQ (p4.getStatus(), 0x01u); +} + +TEST (UMPUniversalPacketTests, Bytes) +{ + UniversalPacket p { 0x599a7788u, 0x4f95a278u, 0x34317f03u, 0x011f1234u }; + + EXPECT_EQ (p.getByte (0), 0x59u); + EXPECT_EQ (p.getByte (1), 0x9au); + EXPECT_EQ (p.getByte2(), 0x9au); + EXPECT_EQ (p.getByte (2), 0x77u); + EXPECT_EQ (p.getByte3(), 0x77u); + EXPECT_EQ (p.getByte (3), 0x88u); + EXPECT_EQ (p.getByte4(), 0x88u); + + EXPECT_EQ (p.getByte (4), 0x4fu); + EXPECT_EQ (p.getByte (5), 0x95u); + EXPECT_EQ (p.getByte (6), 0xa2u); + EXPECT_EQ (p.getByte (7), 0x78u); + + EXPECT_EQ (p.getByte (8), 0x34u); + EXPECT_EQ (p.getByte (9), 0x31u); + EXPECT_EQ (p.getByte (10), 0x7fu); + EXPECT_EQ (p.getByte (11), 0x03u); + + EXPECT_EQ (p.getByte (12), 0x01u); + EXPECT_EQ (p.getByte (13), 0x1fu); + EXPECT_EQ (p.getByte (14), 0x12u); + EXPECT_EQ (p.getByte (15), 0x34u); + + for (uint8_t i = 0; i < 16; ++i) + { + const auto sevenBits = uint7_t (p.getByte (i) & 0x7f); + EXPECT_EQ (p.getByte7Bit (i), sevenBits); + + p.setByte (i, uint8_t (i + 1)); + EXPECT_EQ (p.getByte (i), uint8_t (i + 1)); + + p.setByte7Bit (i, uint8_t (0x80u + i)); + EXPECT_EQ (p.getByte (i), i); + } +} + +TEST (UMPUniversalPacketTests, Channel) +{ + UniversalPacket p0 { 0x011f1234u }; + EXPECT_FALSE (p0.hasChannel()); + + UniversalPacket p1 { 0x15f12345u }; + EXPECT_FALSE (p1.hasChannel()); + + UniversalPacket p2 { 0x2cbf8765u }; + EXPECT_TRUE (p2.hasChannel()); + EXPECT_EQ (p2.getChannel(), 15u); + + UniversalPacket p3 { 0x34317f03u, 0x12345678u }; + EXPECT_FALSE (p3.hasChannel()); + + UniversalPacket p4 { 0x41937788u, 0x81114080u }; + EXPECT_TRUE (p4.hasChannel()); + EXPECT_EQ (p4.getChannel(), 3u); + + UniversalPacket p5 { 0xd99a7788u, 0x11111111u, 0x22222222u, 0x22222222u }; + EXPECT_TRUE (p5.hasChannel()); + EXPECT_EQ (p5.getChannel(), 10u); + + p2.setByte (1, 0x11u); + EXPECT_EQ (p2.getChannel(), 1u); + + p4.setByte (1, 0xdeu); + EXPECT_EQ (p4.getChannel(), 14u); + + EXPECT_FALSE (UniversalPacket { 0x60000000u }.hasChannel()); + EXPECT_FALSE (UniversalPacket { 0x70000000u }.hasChannel()); + EXPECT_FALSE (UniversalPacket { 0x80000000u }.hasChannel()); + EXPECT_FALSE (UniversalPacket { 0x90000000u }.hasChannel()); + EXPECT_FALSE (UniversalPacket { 0xa0000000u }.hasChannel()); + EXPECT_FALSE (UniversalPacket { 0xb0000000u }.hasChannel()); + EXPECT_FALSE (UniversalPacket { 0xc0000000u }.hasChannel()); + EXPECT_TRUE (UniversalPacket { 0xd0000000u }.hasChannel()); + EXPECT_FALSE (UniversalPacket { 0xe0000000u }.hasChannel()); + EXPECT_FALSE (UniversalPacket { 0xf0000000u }.hasChannel()); +} + +TEST (UMPUniversalPacketTests, Reset) +{ + UniversalPacket p { 0x50123456u, 0x789abcdeu, 0xfedcba98u, 0x76543210u }; + + p.reset(); + + EXPECT_EQ (p.data[0], 0u); + EXPECT_EQ (p.data[1], 0u); + EXPECT_EQ (p.data[2], 0u); + EXPECT_EQ (p.data[3], 0u); +} + +TEST (UMPUniversalPacketTests, IsUtilityMessage) +{ + for (uint32_t t = 0; t < 0x10; ++t) + { + UniversalPacket p { t << 28u }; + EXPECT_EQ (p.isUtilityMessage(), t == 0); + } +} + +TEST (UMPUniversalPacketTests, IsSystemMessage) +{ + for (uint32_t t = 0; t < 0x10; ++t) + { + UniversalPacket p { t << 28u }; + EXPECT_EQ (p.isSystemMessage(), t == 1); + } +} + +TEST (UMPUniversalPacketTests, IsChannelVoiceMessage) +{ + for (uint32_t t = 0; t < 0x10; ++t) + { + UniversalPacket p { t << 28u }; + EXPECT_EQ (p.isChannelVoiceMessage(), t == 2 || t == 4); + } +} + +TEST (UMPUniversalPacketTests, IsDataMessage) +{ + for (uint32_t t = 0; t < 0x10; ++t) + { + UniversalPacket p { t << 28u }; + EXPECT_EQ (p.isDataMessage(), t == 3 || t == 5); + } +} + +TEST (UMPUniversalPacketTests, IsMidi1ProtocolMessage) +{ + for (uint32_t t = 0; t < 0x10; ++t) + { + for (uint32_t s = 0; s < 0xff; ++s) + { + UniversalPacket p { (t << 28u) | (s << 16u) }; + + bool expected = false; + if (t == 1) + { + if (p.getStatus() > 0xf0) + { + switch (p.getStatus()) + { + case 0xf4: + case 0xf5: + case 0xf7: + case 0xf9: + case 0xfd: + expected = false; + break; + default: + expected = true; + break; + } + } + } + else if (t == 2) + { + expected = p.getStatus() >= 0x80 && p.getStatus() < 0xf0; + } + + EXPECT_EQ (p.isMidi1ProtocolMessage(), expected); + } + } +} + +TEST (UMPUniversalPacketTests, StreamOperators) +{ + UniversalPacket p { 0x50123456u, 0x789abcdeu, 0xfedcba98u, 0x76543210u }; + + { + std::stringstream stream; + stream << p << '\n'; + + UniversalPacket out; + stream >> out; + EXPECT_EQ (out, p); + EXPECT_TRUE (stream.good()); + } + + { + std::stringstream stream; + stream << p; + + std::string w1, w2, w3, w4; + stream >> w1 >> w2 >> w3 >> w4; + EXPECT_EQ (w1, "50123456"); + EXPECT_EQ (w2, "789abcde"); + EXPECT_EQ (w3, "fedcba98"); + EXPECT_EQ (w4, "76543210"); + } + + { + std::stringstream stream; + stream << "invaliddata"; + + UniversalPacket out; + stream >> out; + EXPECT_FALSE (stream.good()); + EXPECT_EQ (out, UniversalPacket {}); + } + + { + std::stringstream stream; + stream << "50123456p1"; + + UniversalPacket out; + stream >> out; + EXPECT_FALSE (stream.good()); + EXPECT_EQ (out, UniversalPacket { 0x50123456u }); + } +} diff --git a/tests/yup_audio_basics/yup_UMPUniversalSysEx.cpp b/tests/yup_audio_basics/yup_UMPUniversalSysEx.cpp new file mode 100644 index 000000000..31ec3d81d --- /dev/null +++ b/tests/yup_audio_basics/yup_UMPUniversalSysEx.cpp @@ -0,0 +1,79 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; +using namespace yup::ump; + +TEST (UMPUniversalSysExTests, UniversalSysExMessageBasics) +{ + { + SysEx7 sx { Manufacturer::universalNonRealtime }; + EXPECT_FALSE (isUniversalSysExMessage (sx)); + } + + { + SysEx7 sx { Manufacturer::moog, { 0x12, 0x34, 0x56 } }; + EXPECT_FALSE (isUniversalSysExMessage (sx)); + } + + { + SysEx7 sx { Manufacturer::universalNonRealtime, { 0x7f, 0x01 } }; + EXPECT_TRUE (isUniversalSysExMessage (sx)); + + const auto view = UniversalSysEx::MessageView { sx }; + EXPECT_EQ (view.getDeviceId(), 0x7f); + EXPECT_EQ (view.getType(), UniversalSysEx::TypeId::sampleDumpHeader); + EXPECT_EQ (view.getSubtype(), 0u); + } +} + +TEST (UMPUniversalSysExTests, SetDeviceId) +{ + UniversalSysEx::Message msg { Manufacturer::universalRealtime, { 0x04, 0x01 } }; + EXPECT_TRUE (isUniversalSysExMessage (msg)); + EXPECT_EQ (msg.getDeviceId(), 0x04); + + msg.setDeviceId (9); + EXPECT_EQ (msg.getDeviceId(), 9); +} + +TEST (UMPUniversalSysExTests, IdentityRequestAndReply) +{ + SysEx7 req { Manufacturer::universalNonRealtime, { 0x7f, 0x06, 0x01 } }; + EXPECT_TRUE (UniversalSysEx::isIdentityRequest (req)); + + auto built = UniversalSysEx::IdentityRequest {}; + EXPECT_TRUE (UniversalSysEx::isIdentityRequest (built)); + + auto reply = UniversalSysEx::IdentityReply { Manufacturer::google, 0x1234, 0x0678, 0x00abcdef }; + EXPECT_TRUE (UniversalSysEx::isIdentityReply (reply)); + + const auto view = UniversalSysEx::IdentityReplyView { reply }; + const auto identity = view.getIdentity(); + EXPECT_EQ (identity.manufacturer, Manufacturer::google); + EXPECT_EQ (identity.family, 0x1234); + EXPECT_EQ (identity.model, 0x0678); + EXPECT_EQ (identity.revision, 0x00abcdef); +} From 4f49a0cbfb8947c24fb0c4735c52523cc71a6b0c Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 22 Dec 2025 17:14:26 +0100 Subject: [PATCH 07/20] Fix tests --- .../midi/ump/yup_UMPMidi1ToBytestreamTranslator.h | 4 ++-- modules/yup_audio_basics/yup_audio_basics.h | 5 ++++- tests/yup_audio_basics/yup_UMPChannelVoice.cpp | 6 ++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToBytestreamTranslator.h b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToBytestreamTranslator.h index 1ca6b0091..870acda24 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToBytestreamTranslator.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToBytestreamTranslator.h @@ -90,7 +90,7 @@ class Midi1ToBytestreamTranslator if (Utils::getMessageType (firstWord) != 0x00) { const auto message = fromUmp (PacketX1 { firstWord }, time); - callback (BytestreamMidiView (&message)); + callback (message); } break; @@ -187,7 +187,7 @@ class Midi1ToBytestreamTranslator void terminateSysExMessage (MessageCallback&& callback) { pendingSysExData.push_back (std::byte { 0xf7 }); - callback (BytestreamMidiView (pendingSysExData, pendingSysExTime)); + callback (BytestreamMidiView (pendingSysExData, pendingSysExTime).getMessage()); pendingSysExData.clear(); } diff --git a/modules/yup_audio_basics/yup_audio_basics.h b/modules/yup_audio_basics/yup_audio_basics.h index c97cadaa5..a18b650c1 100644 --- a/modules/yup_audio_basics/yup_audio_basics.h +++ b/modules/yup_audio_basics/yup_audio_basics.h @@ -133,6 +133,9 @@ #undef YUP_USE_VDSP_FRAMEWORK #endif +//============================================================================== +#include + //============================================================================== #include "buffers/yup_AudioDataConverters.h" YUP_BEGIN_IGNORE_WARNINGS_MSVC (4661) @@ -179,6 +182,6 @@ YUP_END_IGNORE_WARNINGS_MSVC #include "utilities/yup_AudioWorkgroup.h" #include "midi/ump/yup_UMPBytesOnGroup.h" #include "midi/ump/yup_UMPDeviceInfo.h" -#include "midi/ump/yup_UMP.h" #include "midi/ump/yup_UMPPacketBuffer.h" #include "midi/ump/yup_UMPKeyboardState.h" +#include "midi/ump/yup_UMP.h" diff --git a/tests/yup_audio_basics/yup_UMPChannelVoice.cpp b/tests/yup_audio_basics/yup_UMPChannelVoice.cpp index 7be14c64a..7ebfa9956 100644 --- a/tests/yup_audio_basics/yup_UMPChannelVoice.cpp +++ b/tests/yup_audio_basics/yup_UMPChannelVoice.cpp @@ -128,8 +128,10 @@ TEST (UMPChannelVoiceTests, Midi2AttributesAndSensitivity) TEST (UMPChannelVoiceTests, Midi2ToMidi1Translation) { - const auto noteOn = Midi2ChannelVoiceMessageView { makeMidi2NoteOnMessage (4, 7, 99, Velocity { uint16_t { 0x4567 } }) }; - const auto noteOff = Midi2ChannelVoiceMessageView { makeMidi2NoteOffMessage (3, 9, 66, Velocity { uint16_t { 0x1234 } }) }; + const auto noteOnMessage = makeMidi2NoteOnMessage (4, 7, 99, Velocity { uint16_t { 0x4567 } }); + const auto noteOffMessage = makeMidi2NoteOffMessage (3, 9, 66, Velocity { uint16_t { 0x1234 } }); + const auto noteOn = Midi2ChannelVoiceMessageView { noteOnMessage }; + const auto noteOff = Midi2ChannelVoiceMessageView { noteOffMessage }; auto m1 = asMidi1ChannelVoiceMessage (noteOn); ASSERT_TRUE (m1.has_value()); From 57e13e04ca7828dc5c1e780db5cd688c9b6fe349 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 22 Dec 2025 17:19:19 +0100 Subject: [PATCH 08/20] Fix tests --- modules/yup_audio_basics/midi/ump/yup_UMP.h | 4 ++++ .../ump/yup_UMPMidi1ToBytestreamTranslator.h | 23 +++++++++++++++++-- modules/yup_audio_basics/yup_audio_basics.h | 7 ++---- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/modules/yup_audio_basics/midi/ump/yup_UMP.h b/modules/yup_audio_basics/midi/ump/yup_UMP.h index 3cfe78c95..2beb31aa8 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMP.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMP.h @@ -37,12 +37,15 @@ ============================================================================== */ +#include "yup_UMPBytesOnGroup.h" +#include "yup_UMPDeviceInfo.h" #include "yup_UMPProtocols.h" #include "yup_UMPTypes.h" #include "yup_UMPUniversalPacket.h" #include "yup_UMPUtils.h" #include "yup_UMPacket.h" #include "yup_UMPView.h" +#include "yup_UMPPacketBuffer.h" #include "yup_UMPIterator.h" #include "yup_UMPackets.h" #include "yup_UMPMessages.h" @@ -65,3 +68,4 @@ #include "yup_UMPDispatcher.h" #include "yup_UMPReceiver.h" #include "yup_UMPJitterReductionTimestamps.h" +#include "yup_UMPKeyboardState.h" diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToBytestreamTranslator.h b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToBytestreamTranslator.h index 870acda24..a98ada183 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToBytestreamTranslator.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPMidi1ToBytestreamTranslator.h @@ -90,7 +90,7 @@ class Midi1ToBytestreamTranslator if (Utils::getMessageType (firstWord) != 0x00) { const auto message = fromUmp (PacketX1 { firstWord }, time); - callback (message); + dispatchMessage (callback, message); } break; @@ -187,10 +187,29 @@ class Midi1ToBytestreamTranslator void terminateSysExMessage (MessageCallback&& callback) { pendingSysExData.push_back (std::byte { 0xf7 }); - callback (BytestreamMidiView (pendingSysExData, pendingSysExTime).getMessage()); + const BytestreamMidiView view (pendingSysExData, pendingSysExTime); + dispatchMessage (callback, view); pendingSysExData.clear(); } + template + static void dispatchMessage (MessageCallback&& callback, const MidiMessage& message) + { + if constexpr (std::is_invocable_v) + callback (message); + else + callback (BytestreamMidiView (&message)); + } + + template + static void dispatchMessage (MessageCallback&& callback, const BytestreamMidiView& view) + { + if constexpr (std::is_invocable_v) + callback (view.getMessage()); + else + callback (view); + } + static bool shouldPacketTerminateSysExEarly (uint32_t firstWord) { return ! (isSysExContinuation (firstWord) diff --git a/modules/yup_audio_basics/yup_audio_basics.h b/modules/yup_audio_basics/yup_audio_basics.h index a18b650c1..b0fc2e6d9 100644 --- a/modules/yup_audio_basics/yup_audio_basics.h +++ b/modules/yup_audio_basics/yup_audio_basics.h @@ -135,6 +135,7 @@ //============================================================================== #include +#include //============================================================================== #include "buffers/yup_AudioDataConverters.h" @@ -167,6 +168,7 @@ YUP_END_IGNORE_WARNINGS_MSVC #include "mpe/yup_MPESynthesiserVoice.h" #include "mpe/yup_MPESynthesiser.h" #include "mpe/yup_MPEUtils.h" +#include "midi/ump/yup_UMP.h" #include "sources/yup_AudioSource.h" #include "sources/yup_PositionableAudioSource.h" #include "sources/yup_BufferingAudioSource.h" @@ -180,8 +182,3 @@ YUP_END_IGNORE_WARNINGS_MSVC #include "synthesisers/yup_Synthesiser.h" #include "audio_play_head/yup_AudioPlayHead.h" #include "utilities/yup_AudioWorkgroup.h" -#include "midi/ump/yup_UMPBytesOnGroup.h" -#include "midi/ump/yup_UMPDeviceInfo.h" -#include "midi/ump/yup_UMPPacketBuffer.h" -#include "midi/ump/yup_UMPKeyboardState.h" -#include "midi/ump/yup_UMP.h" From 375aa0a1ef10d38723217cf9da87a400f5ebab9f Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 22 Dec 2025 17:33:53 +0100 Subject: [PATCH 09/20] Fix tests --- modules/yup_audio_basics/midi/ump/yup_UMPTypes.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/yup_audio_basics/midi/ump/yup_UMPTypes.h b/modules/yup_audio_basics/midi/ump/yup_UMPTypes.h index 1cfeba33b..3a691a423 100644 --- a/modules/yup_audio_basics/midi/ump/yup_UMPTypes.h +++ b/modules/yup_audio_basics/midi/ump/yup_UMPTypes.h @@ -24,6 +24,10 @@ namespace yup::ump { +#ifdef Status +#undef Status +#endif + using uint2_t = std::uint8_t; using uint4_t = std::uint8_t; using uint7_t = std::uint8_t; From ebb4df1bac7da4a5d812c9283aaafcbd5b5e32f6 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 22 Dec 2025 18:11:05 +0100 Subject: [PATCH 10/20] More missing tests --- .../yup_UMPJitterReductionTimestamps.cpp | 16 ++++ .../yup_audio_basics/yup_UMPKeyboardState.cpp | 79 +++++++++++++++++ .../yup_audio_basics/yup_UMPPacketBuffer.cpp | 87 +++++++++++++++++++ .../yup_UMPPacketCollector.cpp | 57 ++++++++++++ 4 files changed, 239 insertions(+) create mode 100644 tests/yup_audio_basics/yup_UMPKeyboardState.cpp create mode 100644 tests/yup_audio_basics/yup_UMPPacketBuffer.cpp create mode 100644 tests/yup_audio_devices/yup_UMPPacketCollector.cpp diff --git a/tests/yup_audio_basics/yup_UMPJitterReductionTimestamps.cpp b/tests/yup_audio_basics/yup_UMPJitterReductionTimestamps.cpp index b41568211..720e38a68 100644 --- a/tests/yup_audio_basics/yup_UMPJitterReductionTimestamps.cpp +++ b/tests/yup_audio_basics/yup_UMPJitterReductionTimestamps.cpp @@ -95,3 +95,19 @@ TEST (JitterReductionTimestampTests, JitterTimestampMessage) EXPECT_EQ (tsMsg.getByte3(), 0x7bu); EXPECT_EQ (tsMsg.getByte4(), 0x7cu); } + +TEST (JitterReductionTimestampTests, JitterClockFollowerScheduling) +{ + JitterClockFollower follower; + follower.reset(); + + const auto now = JitterClockFollower::SystemClock::now(); + const auto offset = std::chrono::milliseconds (5); + follower.setSecurityOffset (offset); + + follower.processClock (now, JitterTimestamp { 0 }); + const auto scheduled = follower.scheduleMessage (now, JitterTimestamp { 0 }); + + EXPECT_GE (scheduled, now + offset); + EXPECT_EQ (follower.getSecurityOffset(), offset); +} diff --git a/tests/yup_audio_basics/yup_UMPKeyboardState.cpp b/tests/yup_audio_basics/yup_UMPKeyboardState.cpp new file mode 100644 index 000000000..25fae6680 --- /dev/null +++ b/tests/yup_audio_basics/yup_UMPKeyboardState.cpp @@ -0,0 +1,79 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; +using namespace yup::ump; + +TEST (UMPKeyboardStateTests, ProcessesMidi1NoteOnOff) +{ + UMPKeyboardState state (ump::PacketProtocol::MIDI_1_0); + state.setGroup (0); + + const auto noteOn = makeMidi1NoteOnMessage (0, 0, 60, Velocity { uint7_t { 100 } }); + state.processNextUMPPacket (View (noteOn.data)); + EXPECT_TRUE (state.isNoteOn (1, 60)); + + const auto noteOff = makeMidi1NoteOffMessage (0, 0, 60, Velocity { uint7_t { 64 } }); + state.processNextUMPPacket (View (noteOff.data)); + EXPECT_FALSE (state.isNoteOn (1, 60)); +} + +TEST (UMPKeyboardStateTests, ProcessesMidi2NoteOnOff) +{ + UMPKeyboardState state (ump::PacketProtocol::MIDI_2_0); + state.setGroup (0); + + const auto noteOn = makeMidi2NoteOnMessage (0, 1, 64, Velocity { uint16_t { 0x9000 } }); + state.processNextUMPPacket (View (noteOn.data)); + EXPECT_TRUE (state.isNoteOn (2, 64)); + + const auto noteOff = makeMidi2NoteOffMessage (0, 1, 64, Velocity { uint16_t { 0x4000 } }); + state.processNextUMPPacket (View (noteOff.data)); + EXPECT_FALSE (state.isNoteOn (2, 64)); +} + +TEST (UMPKeyboardStateTests, FiltersByGroup) +{ + UMPKeyboardState state; + state.setGroup (1); + + const auto noteOn = makeMidi1NoteOnMessage (0, 0, 60, Velocity { uint7_t { 100 } }); + state.processNextUMPPacket (View (noteOn.data)); + EXPECT_FALSE (state.isNoteOn (1, 60)); +} + +TEST (UMPKeyboardStateTests, AllNotesOffController) +{ + UMPKeyboardState state (ump::PacketProtocol::MIDI_1_0); + state.setGroup (0); + + const auto noteOn = makeMidi1NoteOnMessage (0, 0, 60, Velocity { uint7_t { 100 } }); + state.processNextUMPPacket (View (noteOn.data)); + EXPECT_TRUE (state.isNoteOn (1, 60)); + + const auto allNotesOff = makeMidi1ControlChangeMessage (0, 0, 123, ControllerValue { uint7_t { 0 } }); + state.processNextUMPPacket (View (allNotesOff.data)); + EXPECT_FALSE (state.isNoteOn (1, 60)); +} diff --git a/tests/yup_audio_basics/yup_UMPPacketBuffer.cpp b/tests/yup_audio_basics/yup_UMPPacketBuffer.cpp new file mode 100644 index 000000000..e3ea09abf --- /dev/null +++ b/tests/yup_audio_basics/yup_UMPPacketBuffer.cpp @@ -0,0 +1,87 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; +using namespace yup::ump; + +TEST (UMPPacketBufferTests, AddEventOrdersBySamplePosition) +{ + UMPPacketBuffer buffer; + + const UniversalPacket p1 { 0x40000000u, 0x12345678u }; + const UniversalPacket p2 { 0x20000000u }; + + EXPECT_TRUE (buffer.addEvent (p1.data, p1.getSize(), 10)); + EXPECT_TRUE (buffer.addEvent (p2.data, p2.getSize(), 5)); + + EXPECT_EQ (buffer.getNumEvents(), 2); + EXPECT_EQ (buffer.getFirstEventTime(), 5); + EXPECT_EQ (buffer.getLastEventTime(), 10); + + auto it = buffer.begin(); + const auto first = *it; + EXPECT_EQ (first.samplePosition, 5); + EXPECT_EQ (first.numWords, 1); + EXPECT_EQ (first.getView()[0], p2.data[0]); + + ++it; + const auto second = *it; + EXPECT_EQ (second.samplePosition, 10); + EXPECT_EQ (second.numWords, 2); + EXPECT_EQ (second.getView()[0], p1.data[0]); + EXPECT_EQ (second.getView()[1], p1.data[1]); +} + +TEST (UMPPacketBufferTests, ConstructorAddsSinglePacket) +{ + const UniversalPacket packet { 0x40000000u, 0xabcdef01u }; + UMPPacketBuffer buffer { View (packet.data) }; + + EXPECT_EQ (buffer.getNumEvents(), 1); + EXPECT_EQ (buffer.getFirstEventTime(), 0); + + const auto meta = *buffer.begin(); + EXPECT_EQ (meta.samplePosition, 0); + EXPECT_EQ (meta.numWords, 2); + EXPECT_EQ (meta.getView()[0], packet.data[0]); + EXPECT_EQ (meta.getView()[1], packet.data[1]); +} + +TEST (UMPPacketBufferTests, FindNextSamplePosition) +{ + UMPPacketBuffer buffer; + + const UniversalPacket p1 { 0x20000000u }; + const UniversalPacket p2 { 0x40000000u, 0x11111111u }; + const UniversalPacket p3 { 0x40000000u, 0x22222222u }; + + buffer.addEvent (p1.data, p1.getSize(), 2); + buffer.addEvent (p2.data, p2.getSize(), 8); + buffer.addEvent (p3.data, p3.getSize(), 12); + + auto it = buffer.findNextSamplePosition (9); + ASSERT_NE (it, buffer.end()); + EXPECT_EQ ((*it).samplePosition, 12); +} diff --git a/tests/yup_audio_devices/yup_UMPPacketCollector.cpp b/tests/yup_audio_devices/yup_UMPPacketCollector.cpp new file mode 100644 index 000000000..e49630b18 --- /dev/null +++ b/tests/yup_audio_devices/yup_UMPPacketCollector.cpp @@ -0,0 +1,57 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; +using namespace yup::ump; + +TEST (UMPPacketCollectorTests, CollectsPacketsIntoBlocks) +{ + UMPPacketCollector collector (PacketProtocol::MIDI_1_0, 0); + collector.reset (1000.0); + + const auto packet = makeMidi1NoteOnMessage (0, 0, 60, Velocity { uint7_t { 100 } }); + const auto timeStamp = Time::getMillisecondCounterHiRes() * 0.001 + 0.005; + collector.addPacketToQueue (View (packet.data), timeStamp); + + UMPPacketBuffer buffer; + collector.removeNextBlockOfPackets (buffer, 64); + + EXPECT_FALSE (buffer.isEmpty()); + EXPECT_EQ (buffer.getNumEvents(), 1); +} + +TEST (UMPPacketCollectorTests, HandlesKeyboardStateCallbacks) +{ + UMPPacketCollector collector (PacketProtocol::MIDI_2_0, 1); + collector.reset (1000.0); + + collector.handleNoteOn (nullptr, 1, 64, 0.5f); + + UMPPacketBuffer buffer; + collector.removeNextBlockOfPackets (buffer, 64); + + EXPECT_FALSE (buffer.isEmpty()); + EXPECT_EQ (buffer.getNumEvents(), 1); +} From a97fd628523cf271bf3defdfb6859177739b8114 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Tue, 23 Dec 2025 16:10:10 +0100 Subject: [PATCH 11/20] More tests for UMP --- .../yup_audio_basics/yup_UMPChannelVoice.cpp | 213 ++++++++++++ .../yup_UMPJitterReductionTimestamps.cpp | 186 +++++++++++ .../yup_audio_basics/yup_UMPKeyboardState.cpp | 259 +++++++++++++++ .../yup_UMPMidi1ByteStream.cpp | 308 ++++++++++++++++++ .../yup_audio_basics/yup_UMPPacketBuffer.cpp | 296 +++++++++++++++++ .../yup_UMPPacketCollector.cpp | 219 +++++++++++++ 6 files changed, 1481 insertions(+) diff --git a/tests/yup_audio_basics/yup_UMPChannelVoice.cpp b/tests/yup_audio_basics/yup_UMPChannelVoice.cpp index 7ebfa9956..558019897 100644 --- a/tests/yup_audio_basics/yup_UMPChannelVoice.cpp +++ b/tests/yup_audio_basics/yup_UMPChannelVoice.cpp @@ -141,3 +141,216 @@ TEST (UMPChannelVoiceTests, Midi2ToMidi1Translation) ASSERT_TRUE (m2.has_value()); EXPECT_EQ (*m2, makeMidi1NoteOffMessage (3, 9, 66, Velocity { uint16_t { 0x1234 } })); } + +TEST (UMPChannelVoiceTests, ProgramChangeMessages) +{ + EXPECT_TRUE (isProgramChangeMessage (makeMidi1ProgramChangeMessage (0, 5, 42))); + EXPECT_EQ (getProgramValue (makeMidi1ProgramChangeMessage (0, 5, 42)), 42); + + EXPECT_TRUE (isProgramChangeMessage (makeMidi2ProgramChangeMessage (1, 7, 99))); + EXPECT_EQ (getProgramValue (makeMidi2ProgramChangeMessage (1, 7, 99)), 99); +} + +TEST (UMPChannelVoiceTests, Midi1ToMidi2Translation) +{ + auto midi1NoteOn = makeMidi1NoteOnMessage (2, 3, 60, Velocity { uint7_t { 100 } }); + auto result = asMidi2ChannelVoiceMessage (Midi1ChannelVoiceMessageView { midi1NoteOn }); + ASSERT_TRUE (result.has_value()); + EXPECT_TRUE (isNoteOnMessage (*result)); + + auto midi1NoteOff = makeMidi1NoteOffMessage (1, 4, 72, Velocity { uint7_t { 64 } }); + result = asMidi2ChannelVoiceMessage (Midi1ChannelVoiceMessageView { midi1NoteOff }); + ASSERT_TRUE (result.has_value()); + EXPECT_TRUE (isNoteOffMessage (*result)); +} + +TEST (UMPChannelVoiceTests, Midi1ZeroVelocityNoteOnToMidi2) +{ + auto midi1NoteOn = makeMidi1NoteOnMessage (2, 3, 60, Velocity { uint7_t { 0 } }); + auto result = asMidi2ChannelVoiceMessage (Midi1ChannelVoiceMessageView { midi1NoteOn }); + ASSERT_TRUE (result.has_value()); + EXPECT_TRUE (isNoteOffMessage (*result)); +} + +TEST (UMPChannelVoiceTests, Midi1ControlChangeToMidi2) +{ + auto midi1CC = makeMidi1ControlChangeMessage (3, 7, 10, ControllerValue { uint7_t { 64 } }); + auto result = asMidi2ChannelVoiceMessage (Midi1ChannelVoiceMessageView { midi1CC }); + ASSERT_TRUE (result.has_value()); + EXPECT_TRUE (isControlChangeMessage (*result)); +} + +TEST (UMPChannelVoiceTests, Midi1ProgramChangeToMidi2) +{ + auto midi1PC = makeMidi1ProgramChangeMessage (4, 8, 42); + auto result = asMidi2ChannelVoiceMessage (Midi1ChannelVoiceMessageView { midi1PC }); + ASSERT_TRUE (result.has_value()); + EXPECT_TRUE (isProgramChangeMessage (*result)); +} + +TEST (UMPChannelVoiceTests, Midi1PolyPressureToMidi2) +{ + auto midi1PP = makeMidi1PolyPressureMessage (5, 9, 72, ControllerValue { uint7_t { 80 } }); + auto result = asMidi2ChannelVoiceMessage (Midi1ChannelVoiceMessageView { midi1PP }); + ASSERT_TRUE (result.has_value()); + EXPECT_TRUE (isPolyPressureMessage (*result)); +} + +TEST (UMPChannelVoiceTests, Midi1ChannelPressureToMidi2) +{ + auto midi1CP = makeMidi1ChannelPressureMessage (6, 10, ControllerValue { uint7_t { 70 } }); + auto result = asMidi2ChannelVoiceMessage (Midi1ChannelVoiceMessageView { midi1CP }); + ASSERT_TRUE (result.has_value()); + EXPECT_TRUE (isChannelPressureMessage (*result)); +} + +TEST (UMPChannelVoiceTests, Midi1PitchBendToMidi2) +{ + auto midi1PB = makeMidi1PitchBendMessage (7, 11, PitchBend { uint14_t { 8192 } }); + auto result = asMidi2ChannelVoiceMessage (Midi1ChannelVoiceMessageView { midi1PB }); + ASSERT_TRUE (result.has_value()); + EXPECT_TRUE (isChannelPitchBendMessage (*result)); +} + +TEST (UMPChannelVoiceTests, Midi2PolyPressureToMidi1) +{ + auto midi2PP = makeMidi2PolyPressureMessage (2, 5, 64, ControllerValue { 0x80000000u }); + auto result = asMidi1ChannelVoiceMessage (Midi2ChannelVoiceMessageView { midi2PP }); + ASSERT_TRUE (result.has_value()); + EXPECT_TRUE (isPolyPressureMessage (*result)); +} + +TEST (UMPChannelVoiceTests, Midi2ControlChangeToMidi1) +{ + auto midi2CC = makeMidi2ControlChangeMessage (3, 6, 7, ControllerValue { 0x70000000u }); + auto result = asMidi1ChannelVoiceMessage (Midi2ChannelVoiceMessageView { midi2CC }); + ASSERT_TRUE (result.has_value()); + EXPECT_TRUE (isControlChangeMessage (*result)); +} + +TEST (UMPChannelVoiceTests, Midi2ProgramChangeToMidi1) +{ + auto midi2PC = makeMidi2ProgramChangeMessage (4, 7, 55); + auto result = asMidi1ChannelVoiceMessage (Midi2ChannelVoiceMessageView { midi2PC }); + ASSERT_TRUE (result.has_value()); + EXPECT_TRUE (isProgramChangeMessage (*result)); +} + +TEST (UMPChannelVoiceTests, Midi2ChannelPressureToMidi1) +{ + auto midi2CP = makeMidi2ChannelPressureMessage (5, 8, ControllerValue { 0x60000000u }); + auto result = asMidi1ChannelVoiceMessage (Midi2ChannelVoiceMessageView { midi2CP }); + ASSERT_TRUE (result.has_value()); + EXPECT_TRUE (isChannelPressureMessage (*result)); +} + +TEST (UMPChannelVoiceTests, Midi2PitchBendToMidi1) +{ + auto midi2PB = makeMidi2PitchBendMessage (6, 9, PitchBend { 0x80000000u }); + auto result = asMidi1ChannelVoiceMessage (Midi2ChannelVoiceMessageView { midi2PB }); + ASSERT_TRUE (result.has_value()); + EXPECT_TRUE (isChannelPitchBendMessage (*result)); +} + +TEST (UMPChannelVoiceTests, Midi2NoteOnZeroVelocityToMidi1) +{ + auto midi2NoteOn = makeMidi2NoteOnMessage (2, 3, 60, Velocity { uint16_t { 0 } }); + auto result = asMidi1ChannelVoiceMessage (Midi2ChannelVoiceMessageView { midi2NoteOn }); + ASSERT_TRUE (result.has_value()); + EXPECT_TRUE (isNoteOnMessage (*result)); + EXPECT_EQ (getNoteVelocity (*result).asUInt7(), 1); +} + +TEST (UMPChannelVoiceTests, Midi2WithAttributeNotTranslatable) +{ + auto midi2NoteOn = makeMidi2NoteOnMessage (2, 3, 60, Velocity { uint16_t { 0x8000 } }, Pitch7_9 { 61.5f }); + auto result = asMidi1ChannelVoiceMessage (Midi2ChannelVoiceMessageView { midi2NoteOn }); + EXPECT_FALSE (result.has_value()); +} + +TEST (UMPChannelVoiceTests, Midi1SpecialCCNotTranslated) +{ + auto midi1BankSelect = makeMidi1ControlChangeMessage (3, 7, ControlChange::bankSelectMsb, ControllerValue { uint7_t { 1 } }); + auto result = asMidi2ChannelVoiceMessage (Midi1ChannelVoiceMessageView { midi1BankSelect }); + EXPECT_FALSE (result.has_value()); + + auto midi1RpnLsb = makeMidi1ControlChangeMessage (3, 7, ControlChange::rpnLsb, ControllerValue { uint7_t { 1 } }); + result = asMidi2ChannelVoiceMessage (Midi1ChannelVoiceMessageView { midi1RpnLsb }); + EXPECT_FALSE (result.has_value()); +} + +TEST (UMPChannelVoiceTests, Midi2SpecialCCNotTranslated) +{ + auto midi2BankSelect = makeMidi2ControlChangeMessage (3, 7, ControlChange::bankSelectMsb, ControllerValue { 0x80000000u }); + auto result = asMidi1ChannelVoiceMessage (Midi2ChannelVoiceMessageView { midi2BankSelect }); + EXPECT_FALSE (result.has_value()); + + auto midi2RpnMsb = makeMidi2ControlChangeMessage (3, 7, ControlChange::rpnMsb, ControllerValue { 0x80000000u }); + result = asMidi1ChannelVoiceMessage (Midi2ChannelVoiceMessageView { midi2RpnMsb }); + EXPECT_FALSE (result.has_value()); +} + +TEST (UMPChannelVoiceTests, Midi2ProgramChangeWithBankSelectNotTranslated) +{ + auto packet = makeMidi2ProgramChangeMessage (4, 7, 55); + packet.setByte (3, packet.getByte (3) | 0x01); + auto result = asMidi1ChannelVoiceMessage (Midi2ChannelVoiceMessageView { packet }); + EXPECT_FALSE (result.has_value()); +} + +TEST (UMPChannelVoiceTests, GetProgramValueInvalid) +{ + UniversalPacket invalidPacket { 0x10000000u }; + EXPECT_EQ (getProgramValue (invalidPacket), 0xff); +} + +TEST (UMPChannelVoiceTests, GetNotePitchOnMidi1) +{ + auto midi1NoteOn = makeMidi1NoteOnMessage (2, 3, 60, Velocity { uint7_t { 100 } }); + EXPECT_EQ (getNotePitch (midi1NoteOn), Pitch7_9 { NoteNumber (60) }); +} + +TEST (UMPChannelVoiceTests, GetNoteVelocityOnMidi1NoteOn) +{ + auto midi1NoteOn = makeMidi1NoteOnMessage (2, 3, 60, Velocity { uint7_t { 100 } }); + EXPECT_EQ (getNoteVelocity (midi1NoteOn), Velocity { uint7_t { 100 } }); +} + +TEST (UMPChannelVoiceTests, PerNoteMessages) +{ + auto perNotePB = makePerNotePitchBendMessage (5, 6, 72, PitchBend { 0x80001000u }); + EXPECT_TRUE (isPerNotePitchBendMessage (perNotePB)); + + auto regPerNoteCC = makeRegisteredPerNoteControllerMessage (3, 4, 60, 1, ControllerValue { 0x50000000u }); + EXPECT_TRUE (isRegisteredPerNoteControllerMessage (regPerNoteCC)); + EXPECT_EQ (getPerNoteControllerIndex (regPerNoteCC), 1); + + auto assPerNoteCC = makeAssignablePerNoteControllerMessage (7, 8, 64, 5, ControllerValue { 0x60000000u }); + EXPECT_TRUE (isAssignablePerNoteControllerMessage (assPerNoteCC)); + EXPECT_EQ (getPerNoteControllerIndex (assPerNoteCC), 5); +} + +TEST (UMPChannelVoiceTests, RegisteredAndAssignableControllers) +{ + auto regCC = makeRegisteredControllerMessage (2, 3, 0, 5, ControllerValue { 0x12345678u }); + EXPECT_TRUE (isRegisteredControllerMessage (regCC)); + + auto assCC = makeAssignableControllerMessage (4, 5, 1, 7, ControllerValue { 0x87654321u }); + EXPECT_TRUE (isAssignableControllerMessage (assCC)); +} + +TEST (UMPChannelVoiceTests, PitchBendSensitivity) +{ + auto pbSens = makeRegisteredControllerMessage (1, 2, 0, RegisteredParameterNumber::pitchBendSensitivity, ControllerValue { 0x02000000u }); + EXPECT_TRUE (isPitchBendSensitivityMessage (pbSens)); + EXPECT_EQ (getPitchBendSensitivityValue (pbSens), PitchBendSensitivity { 0x02000000u }); +} + +TEST (UMPChannelVoiceTests, GetMidi2NoteAttribute) +{ + auto noteOn = makeMidi2NoteOnMessage (2, 3, 60, Velocity { uint16_t { 0x8000 } }, Pitch7_9 { 61.5f }); + EXPECT_EQ (getMidi2NoteAttribute (noteOn), NoteAttribute::pitch_7_9); + + auto noteOnNoAttr = makeMidi2NoteOnMessage (2, 3, 60, Velocity { uint16_t { 0x8000 } }); + EXPECT_EQ (getMidi2NoteAttribute (noteOnNoAttr), NoteAttribute::none); +} diff --git a/tests/yup_audio_basics/yup_UMPJitterReductionTimestamps.cpp b/tests/yup_audio_basics/yup_UMPJitterReductionTimestamps.cpp index 720e38a68..a92c3775d 100644 --- a/tests/yup_audio_basics/yup_UMPJitterReductionTimestamps.cpp +++ b/tests/yup_audio_basics/yup_UMPJitterReductionTimestamps.cpp @@ -111,3 +111,189 @@ TEST (JitterReductionTimestampTests, JitterClockFollowerScheduling) EXPECT_GE (scheduled, now + offset); EXPECT_EQ (follower.getSecurityOffset(), offset); } + +TEST (JitterReductionTimestampTests, JitterClockNow) +{ + auto t1 = JitterClock::now(); + std::this_thread::sleep_for (std::chrono::microseconds (100)); + auto t2 = JitterClock::now(); + + EXPECT_NE (t1.value, t2.value); +} + +TEST (JitterReductionTimestampTests, JitterTimestampWraparound) +{ + JitterTimestamp t1 { 0xfffe }; + JitterTimestamp t2 { 0x0002 }; + + auto diff = t2 - t1; + EXPECT_EQ (diff.count(), 4); +} + +TEST (JitterReductionTimestampTests, JitterClockFollowerInitialState) +{ + JitterClockFollower follower; + follower.reset(); + + EXPECT_EQ (follower.getSecurityOffset().count(), 2); +} + +TEST (JitterReductionTimestampTests, JitterClockFollowerFirstClock) +{ + JitterClockFollower follower; + follower.reset(); + + const auto now = JitterClockFollower::SystemClock::now(); + follower.processClock (now, JitterTimestamp { 100 }); + + const auto scheduled = follower.scheduleMessage (now, JitterTimestamp { 100 }); + EXPECT_GE (scheduled, now); +} + +TEST (JitterReductionTimestampTests, JitterClockFollowerSubsequentClocks) +{ + JitterClockFollower follower; + follower.reset(); + + auto now = JitterClockFollower::SystemClock::now(); + follower.processClock (now, JitterTimestamp { 1000 }); + + now += std::chrono::milliseconds (10); + follower.processClock (now, JitterTimestamp { 1100 }); + + const auto scheduled = follower.scheduleMessage (now, JitterTimestamp { 1100 }); + EXPECT_GE (scheduled, now); +} + +TEST (JitterReductionTimestampTests, JitterClockFollowerEarlyClock) +{ + JitterClockFollower follower; + follower.reset(); + + auto now = JitterClockFollower::SystemClock::now(); + follower.processClock (now, JitterTimestamp { 1000 }); + + auto early = now + std::chrono::milliseconds (5); + follower.processClock (early, JitterTimestamp { 1100 }); + + auto laterStill = now + std::chrono::milliseconds (20); + const auto scheduled = follower.scheduleMessage (laterStill, JitterTimestamp { 1200 }); + EXPECT_GE (scheduled, now); +} + +TEST (JitterReductionTimestampTests, JitterClockFollowerLateClock) +{ + JitterClockFollower follower; + follower.reset(); + + auto now = JitterClockFollower::SystemClock::now(); + follower.processClock (now, JitterTimestamp { 1000 }); + + auto late = now + std::chrono::milliseconds (20); + follower.processClock (late, JitterTimestamp { 1100 }); + + const auto scheduled = follower.scheduleMessage (late, JitterTimestamp { 1100 }); + EXPECT_GE (scheduled, now); +} + +TEST (JitterReductionTimestampTests, JitterClockFollowerScheduleWithoutClock) +{ + JitterClockFollower follower; + follower.reset(); + + const auto now = JitterClockFollower::SystemClock::now(); + const auto scheduled = follower.scheduleMessage (now, JitterTimestamp { 100 }); + + EXPECT_GE (scheduled, now); +} + +TEST (JitterReductionTimestampTests, JitterClockFollowerSecurityOffsetAdjustment) +{ + JitterClockFollower follower; + follower.reset(); + + auto now = JitterClockFollower::SystemClock::now(); + follower.processClock (now, JitterTimestamp { 1000 }); + + auto late = now + std::chrono::milliseconds (50); + follower.processClock (late, JitterTimestamp { 1100 }); + + EXPECT_GT (follower.getSecurityOffset().count(), 2); +} + +TEST (JitterReductionTimestampTests, JitterClockFollowerMultipleSchedules) +{ + JitterClockFollower follower; + follower.reset(); + + auto now = JitterClockFollower::SystemClock::now(); + follower.processClock (now, JitterTimestamp { 1000 }); + + auto scheduled1 = follower.scheduleMessage (now, JitterTimestamp { 1000 }); + auto scheduled2 = follower.scheduleMessage (now, JitterTimestamp { 1010 }); + auto scheduled3 = follower.scheduleMessage (now, JitterTimestamp { 1020 }); + + EXPECT_LE (scheduled1, scheduled2); + EXPECT_LE (scheduled2, scheduled3); +} + +TEST (JitterReductionTimestampTests, JitterClockMessageConstruction) +{ + JitterClockMessage msg; + EXPECT_EQ (msg.getType(), PacketType::utility); + EXPECT_EQ (msg.getStatus(), uint8_t (UtilityStatus::jitterClock)); +} + +TEST (JitterReductionTimestampTests, JitterTimestampMessageConstruction) +{ + JitterTimestampMessage msg; + EXPECT_EQ (msg.getType(), PacketType::utility); + EXPECT_EQ (msg.getStatus(), uint8_t (UtilityStatus::jitterTimestamp)); +} + +TEST (JitterReductionTimestampTests, JitterClockMessageSetGetTimestamp) +{ + JitterClockMessage msg; + + for (uint16_t i = 0; i < 1000; i += 100) + { + JitterTimestamp ts { i }; + msg.setTimestamp (ts); + EXPECT_EQ (msg.getTimestamp(), ts); + } +} + +TEST (JitterReductionTimestampTests, JitterTimestampMessageSetGetTimestamp) +{ + JitterTimestampMessage msg; + + for (uint16_t i = 0; i < 1000; i += 100) + { + JitterTimestamp ts { i }; + msg.setTimestamp (ts); + EXPECT_EQ (msg.getTimestamp(), ts); + } +} + +TEST (JitterReductionTimestampTests, JitterTimestampMaxValue) +{ + JitterTimestamp t { 0xffff }; + EXPECT_EQ (t.value, 0xffffu); + + JitterTimestamp t2 { 0 }; + auto diff = t2 - t; + EXPECT_EQ (diff.count(), 1); +} + +TEST (JitterReductionTimestampTests, JitterClockFollowerResetClearsState) +{ + JitterClockFollower follower; + + auto now = JitterClockFollower::SystemClock::now(); + follower.processClock (now, JitterTimestamp { 1000 }); + follower.setSecurityOffset (std::chrono::milliseconds (100)); + + follower.reset(); + + EXPECT_EQ (follower.getSecurityOffset().count(), 2); +} diff --git a/tests/yup_audio_basics/yup_UMPKeyboardState.cpp b/tests/yup_audio_basics/yup_UMPKeyboardState.cpp index 25fae6680..a7dd3932c 100644 --- a/tests/yup_audio_basics/yup_UMPKeyboardState.cpp +++ b/tests/yup_audio_basics/yup_UMPKeyboardState.cpp @@ -77,3 +77,262 @@ TEST (UMPKeyboardStateTests, AllNotesOffController) state.processNextUMPPacket (View (allNotesOff.data)); EXPECT_FALSE (state.isNoteOn (1, 60)); } + +TEST (UMPKeyboardStateTests, DirectNoteOnOff) +{ + UMPKeyboardState state (ump::PacketProtocol::MIDI_1_0); + state.setGroup (0); + + state.noteOn (1, 60, 0.8f); + EXPECT_TRUE (state.isNoteOn (1, 60)); + + state.noteOff (1, 60, 0.5f); + EXPECT_FALSE (state.isNoteOn (1, 60)); +} + +TEST (UMPKeyboardStateTests, DirectNoteOnMidi2Protocol) +{ + UMPKeyboardState state (ump::PacketProtocol::MIDI_2_0); + state.setGroup (0); + + state.noteOn (5, 72, 1.0f); + EXPECT_TRUE (state.isNoteOn (5, 72)); + + state.noteOff (5, 72, 0.0f); + EXPECT_FALSE (state.isNoteOn (5, 72)); +} + +TEST (UMPKeyboardStateTests, IsNoteOnForChannels) +{ + UMPKeyboardState state; + state.setGroup (0); + + state.noteOn (1, 60, 0.8f); + state.noteOn (2, 60, 0.7f); + + EXPECT_TRUE (state.isNoteOnForChannels (0x0003, 60)); + EXPECT_FALSE (state.isNoteOnForChannels (0x0004, 60)); + EXPECT_TRUE (state.isNoteOnForChannels (0x0001, 60)); +} + +TEST (UMPKeyboardStateTests, ResetClearsAllNotes) +{ + UMPKeyboardState state; + state.setGroup (0); + + state.noteOn (1, 60, 0.8f); + state.noteOn (2, 64, 0.7f); + state.noteOn (3, 67, 0.9f); + + EXPECT_TRUE (state.isNoteOn (1, 60)); + EXPECT_TRUE (state.isNoteOn (2, 64)); + EXPECT_TRUE (state.isNoteOn (3, 67)); + + state.reset(); + + EXPECT_FALSE (state.isNoteOn (1, 60)); + EXPECT_FALSE (state.isNoteOn (2, 64)); + EXPECT_FALSE (state.isNoteOn (3, 67)); +} + +TEST (UMPKeyboardStateTests, AllNotesOffMethod) +{ + UMPKeyboardState state; + state.setGroup (0); + + state.noteOn (3, 60, 0.8f); + state.noteOn (3, 64, 0.7f); + state.noteOn (4, 67, 0.9f); + + state.allNotesOff (3); + + EXPECT_FALSE (state.isNoteOn (3, 60)); + EXPECT_FALSE (state.isNoteOn (3, 64)); + EXPECT_TRUE (state.isNoteOn (4, 67)); +} + +TEST (UMPKeyboardStateTests, AllNotesOffAllChannels) +{ + UMPKeyboardState state; + state.setGroup (0); + + state.noteOn (1, 60, 0.8f); + state.noteOn (2, 64, 0.7f); + state.noteOn (3, 67, 0.9f); + + state.allNotesOff (0); + + EXPECT_FALSE (state.isNoteOn (1, 60)); + EXPECT_FALSE (state.isNoteOn (2, 64)); + EXPECT_FALSE (state.isNoteOn (3, 67)); +} + +TEST (UMPKeyboardStateTests, DISABLED_InvalidNoteNumbers) +{ + UMPKeyboardState state; + state.setGroup (0); + + state.noteOn (1, -1, 0.8f); + state.noteOn (1, 128, 0.8f); + state.noteOn (1, 200, 0.8f); + + EXPECT_FALSE (state.isNoteOn (1, -1)); + EXPECT_FALSE (state.isNoteOn (1, 128)); + EXPECT_FALSE (state.isNoteOn (1, 200)); +} + +TEST (UMPKeyboardStateTests, Midi1ZeroVelocityNoteOnBecomesNoteOff) +{ + UMPKeyboardState state (ump::PacketProtocol::MIDI_1_0); + state.setGroup (0); + + const auto noteOn1 = makeMidi1NoteOnMessage (0, 0, 60, Velocity { uint7_t { 100 } }); + state.processNextUMPPacket (View (noteOn1.data)); + EXPECT_TRUE (state.isNoteOn (1, 60)); + + const auto noteOnZero = makeMidi1NoteOnMessage (0, 0, 60, Velocity { uint7_t { 0 } }); + state.processNextUMPPacket (View (noteOnZero.data)); + EXPECT_FALSE (state.isNoteOn (1, 60)); +} + +TEST (UMPKeyboardStateTests, Midi2ZeroVelocityNoteOnBecomesNoteOff) +{ + UMPKeyboardState state (ump::PacketProtocol::MIDI_2_0); + state.setGroup (0); + + const auto noteOn1 = makeMidi2NoteOnMessage (0, 1, 64, Velocity { uint16_t { 0x8000 } }); + state.processNextUMPPacket (View (noteOn1.data)); + EXPECT_TRUE (state.isNoteOn (2, 64)); + + const auto noteOnZero = makeMidi2NoteOnMessage (0, 1, 64, Velocity { uint16_t { 0 } }); + state.processNextUMPPacket (View (noteOnZero.data)); + EXPECT_FALSE (state.isNoteOn (2, 64)); +} + +TEST (UMPKeyboardStateTests, ProcessBuffer) +{ + UMPKeyboardState state; + state.setGroup (0); + + UMPPacketBuffer buffer; + const auto noteOn = makeMidi1NoteOnMessage (0, 0, 60, Velocity { uint7_t { 100 } }); + buffer.addEvent (noteOn.data, 1, 0); + + state.processNextUMPBuffer (buffer, 0, 64, false); + EXPECT_TRUE (state.isNoteOn (1, 60)); +} + +TEST (UMPKeyboardStateTests, ProcessBufferWithIndirectEvents) +{ + UMPKeyboardState state; + state.setGroup (0); + + state.noteOn (1, 60, 0.8f); + + UMPPacketBuffer buffer; + state.processNextUMPBuffer (buffer, 0, 64, true); + + EXPECT_FALSE (buffer.isEmpty()); +} + +class KeyboardStateListener : public UMPKeyboardState::Listener +{ +public: + void handleNoteOn (UMPKeyboardState*, int channel, int note, float velocity) override + { + noteOnCalls.push_back ({ channel, note, velocity }); + } + + void handleNoteOff (UMPKeyboardState*, int channel, int note, float velocity) override + { + noteOffCalls.push_back ({ channel, note, velocity }); + } + + struct Event + { + int channel; + int note; + float velocity; + }; + + std::vector noteOnCalls; + std::vector noteOffCalls; +}; + +TEST (UMPKeyboardStateTests, ListenerReceivesNoteOnOff) +{ + UMPKeyboardState state; + state.setGroup (0); + KeyboardStateListener listener; + + state.addListener (&listener); + + state.noteOn (1, 60, 0.8f); + state.noteOff (1, 60, 0.5f); + + ASSERT_EQ (listener.noteOnCalls.size(), 1u); + EXPECT_EQ (listener.noteOnCalls[0].channel, 1); + EXPECT_EQ (listener.noteOnCalls[0].note, 60); + EXPECT_FLOAT_EQ (listener.noteOnCalls[0].velocity, 0.8f); + + ASSERT_EQ (listener.noteOffCalls.size(), 1u); + EXPECT_EQ (listener.noteOffCalls[0].channel, 1); + EXPECT_EQ (listener.noteOffCalls[0].note, 60); + EXPECT_FLOAT_EQ (listener.noteOffCalls[0].velocity, 0.5f); + + state.removeListener (&listener); +} + +TEST (UMPKeyboardStateTests, RemovedListenerDoesNotReceiveEvents) +{ + UMPKeyboardState state; + state.setGroup (0); + KeyboardStateListener listener; + + state.addListener (&listener); + state.removeListener (&listener); + + state.noteOn (1, 60, 0.8f); + + EXPECT_TRUE (listener.noteOnCalls.empty()); +} + +TEST (UMPKeyboardStateTests, NoteOffOnNonActiveNote) +{ + UMPKeyboardState state; + state.setGroup (0); + + state.noteOff (1, 60, 0.5f); + EXPECT_FALSE (state.isNoteOn (1, 60)); +} + +TEST (UMPKeyboardStateTests, MultipleChannelsSameNote) +{ + UMPKeyboardState state; + state.setGroup (0); + + state.noteOn (1, 60, 0.8f); + state.noteOn (2, 60, 0.7f); + state.noteOn (3, 60, 0.6f); + + EXPECT_TRUE (state.isNoteOn (1, 60)); + EXPECT_TRUE (state.isNoteOn (2, 60)); + EXPECT_TRUE (state.isNoteOn (3, 60)); + + state.noteOff (2, 60, 0.5f); + + EXPECT_TRUE (state.isNoteOn (1, 60)); + EXPECT_FALSE (state.isNoteOn (2, 60)); + EXPECT_TRUE (state.isNoteOn (3, 60)); +} + +TEST (UMPKeyboardStateTests, IgnoresNonNotePackets) +{ + UMPKeyboardState state; + state.setGroup (0); + + const auto cc = makeMidi1ControlChangeMessage (0, 0, 7, ControllerValue { uint7_t { 100 } }); + state.processNextUMPPacket (View (cc.data)); + + EXPECT_FALSE (state.isNoteOn (1, 60)); +} diff --git a/tests/yup_audio_basics/yup_UMPMidi1ByteStream.cpp b/tests/yup_audio_basics/yup_UMPMidi1ByteStream.cpp index 276d8cf1f..15daa114e 100644 --- a/tests/yup_audio_basics/yup_UMPMidi1ByteStream.cpp +++ b/tests/yup_audio_basics/yup_UMPMidi1ByteStream.cpp @@ -136,3 +136,311 @@ TEST (Midi1ByteStreamParserTests, Midi1ByteStreamRoundTrip) ASSERT_EQ (received.size(), 1u); EXPECT_TRUE (packetEquals (packet, received.front())); } + +TEST (Midi1ByteStreamParserTests, RunningStatus) +{ + std::vector received; + Midi1ByteStreamParser parser ([&] (UniversalPacket p) + { + received.push_back (p); + }); + + parser.feed (0x90); + parser.feed (0x3c); + parser.feed (0x7f); + parser.feed (0x40); + parser.feed (0x60); + + ASSERT_EQ (received.size(), 2u); + EXPECT_TRUE (packetEquals (makeMidi1ChannelVoiceMessage (0, Status (Midi1ChannelVoiceStatus::noteOn), 0, 0x3c, 0x7f), received[0])); + EXPECT_TRUE (packetEquals (makeMidi1ChannelVoiceMessage (0, Status (Midi1ChannelVoiceStatus::noteOn), 0, 0x40, 0x60), received[1])); +} + +TEST (Midi1ByteStreamParserTests, ProgramChangeAndChannelPressure) +{ + std::vector received; + Midi1ByteStreamParser parser ([&] (UniversalPacket p) + { + received.push_back (p); + }); + + parser.feed (0xc5); + parser.feed (0x42); + parser.feed (0xd7); + parser.feed (0x55); + + ASSERT_EQ (received.size(), 2u); + EXPECT_TRUE (packetEquals (makeMidi1ChannelVoiceMessage (0, Status (Midi1ChannelVoiceStatus::programChange), 5, 0x42, 0), received[0])); + EXPECT_TRUE (packetEquals (makeMidi1ChannelVoiceMessage (0, Status (Midi1ChannelVoiceStatus::channelPressure), 7, 0x55, 0), received[1])); +} + +TEST (Midi1ByteStreamParserTests, SystemRealtimeInterleaved) +{ + std::vector received; + Midi1ByteStreamParser parser ([&] (UniversalPacket p) + { + received.push_back (p); + }); + + parser.feed (0x90); + parser.feed (0xf8); + parser.feed (0x3c); + parser.feed (0xfa); + parser.feed (0x7f); + + ASSERT_EQ (received.size(), 3u); + EXPECT_TRUE (packetEquals (makeSystemMessage (0, Status (SystemStatus::clock)), received[0])); + EXPECT_TRUE (packetEquals (makeSystemMessage (0, Status (SystemStatus::start)), received[1])); + EXPECT_TRUE (packetEquals (makeMidi1ChannelVoiceMessage (0, Status (Midi1ChannelVoiceStatus::noteOn), 0, 0x3c, 0x7f), received[2])); +} + +TEST (Midi1ByteStreamParserTests, IgnoredRealtimeBytes) +{ + std::vector received; + Midi1ByteStreamParser parser ([&] (UniversalPacket p) + { + received.push_back (p); + }); + + parser.feed (0xf9); + parser.feed (0xfd); + parser.feed (0xf8); + + ASSERT_EQ (received.size(), 1u); + EXPECT_TRUE (packetEquals (makeSystemMessage (0, Status (SystemStatus::clock)), received[0])); +} + +TEST (Midi1ByteStreamParserTests, TuneRequest) +{ + std::vector received; + Midi1ByteStreamParser parser ([&] (UniversalPacket p) + { + received.push_back (p); + }); + + parser.feed (0xf6); + + ASSERT_EQ (received.size(), 1u); + EXPECT_TRUE (packetEquals (makeSystemMessage (0, Status (SystemStatus::tuneRequest)), received[0])); +} + +TEST (Midi1ByteStreamParserTests, SysExCompleteWithPackets) +{ + std::vector received; + Midi1ByteStreamParser parser ([&] (UniversalPacket p) + { + received.push_back (p); + }); + + parser.feed (0xf0); + parser.feed (0x7e); + parser.feed (0xf7); + + ASSERT_EQ (received.size(), 1u); + EXPECT_EQ (received[0].getType(), PacketType::data); + EXPECT_EQ (received[0].getStatus() & 0xf0, uint8_t (DataStatus::sysex7Complete)); +} + +TEST (Midi1ByteStreamParserTests, SysExContinueWithPackets) +{ + std::vector received; + Midi1ByteStreamParser parser ([&] (UniversalPacket p) + { + received.push_back (p); + }); + + parser.feed (0xf0); + for (int i = 0; i < 7; ++i) + parser.feed (0x01 + i); + parser.feed (0xf7); + + ASSERT_GE (received.size(), 1u); + EXPECT_EQ (received[0].getType(), PacketType::data); +} + +TEST (Midi1ByteStreamParserTests, SysExWithCallback) +{ + std::vector receivedSysEx; + std::vector receivedPackets; + + Midi1ByteStreamParser parser ( + [&] (UniversalPacket p) + { + receivedPackets.push_back (p); + }, + [&] (const SysEx7& sysex) + { + receivedSysEx.push_back (sysex); + }); + + parser.feed (0xf0); + parser.feed (0x7e); + parser.feed (0x00); + parser.feed (0x01); + parser.feed (0x02); + parser.feed (0xf7); + + ASSERT_EQ (receivedSysEx.size(), 1u); + EXPECT_EQ (receivedSysEx[0].manufacturerId, 0x7e0000u); + EXPECT_EQ (receivedSysEx[0].data.size(), 2u); +} + +TEST (Midi1ByteStreamParserTests, SysExWithCallbackThreeByteManufacturerId) +{ + std::vector receivedSysEx; + + Midi1ByteStreamParser parser ( + [] (UniversalPacket) {}, + [&] (const SysEx7& sysex) + { + receivedSysEx.push_back (sysex); + }); + + parser.feed (0xf0); + parser.feed (0x00); + parser.feed (0x01); + parser.feed (0x02); + parser.feed (0x03); + parser.feed (0xf7); + + ASSERT_EQ (receivedSysEx.size(), 1u); + EXPECT_EQ (receivedSysEx[0].manufacturerId, 0x000102u); + EXPECT_EQ (receivedSysEx[0].data.size(), 1u); + EXPECT_EQ (receivedSysEx[0].data[0], 0x03); +} + +TEST (Midi1ByteStreamParserTests, SysExInterruptedByStatus) +{ + std::vector received; + Midi1ByteStreamParser parser ([&] (UniversalPacket p) + { + received.push_back (p); + }); + + parser.feed (0xf0); + parser.feed (0x7e); + parser.feed (0x90); + parser.feed (0x3c); + parser.feed (0x7f); + + EXPECT_GT (received.size(), 0u); + auto lastPacket = received.back(); + EXPECT_EQ (lastPacket.getType(), PacketType::midi1ChannelVoice); +} + +TEST (Midi1ByteStreamParserTests, ResetClearsState) +{ + std::vector received; + Midi1ByteStreamParser parser ([&] (UniversalPacket p) + { + received.push_back (p); + }); + + parser.feed (0x90); + parser.feed (0x3c); + parser.reset(); + + parser.feed (0x7f); + + EXPECT_TRUE (received.empty()); +} + +TEST (Midi1ByteStreamParserTests, CallbacksDisabled) +{ + std::vector received; + Midi1ByteStreamParser parser ( + [&] (UniversalPacket p) + { + received.push_back (p); + }, + {}, + false); + + parser.feed (0xf8); + parser.feed (0x90); + parser.feed (0x3c); + parser.feed (0x7f); + + EXPECT_TRUE (received.empty()); +} + +TEST (Midi1ByteStreamParserTests, GetMidi1ByteStreamSizeEdgeCases) +{ + EXPECT_EQ (getMidi1ByteStreamSize (UniversalPacket {}), 0u); + + auto invalidData = UniversalPacket {}; + invalidData.setByte (1, 0x37); + EXPECT_EQ (getMidi1ByteStreamSize (invalidData), 0u); + + auto sysexPacket = makeSysEx7StartPacket (0); + sysexPacket.setByte (1, uint8_t (DataStatus::sysex7Complete) + 5); + EXPECT_EQ (getMidi1ByteStreamSize (sysexPacket), 5u); +} + +TEST (Midi1ByteStreamParserTests, FromMidi1ByteStreamInvalidStatus) +{ + auto packet = fromMidi1ByteStream (0x7f, 0, 0); + EXPECT_EQ (packet.getType(), PacketType::utility); + + packet = fromMidi1ByteStream (0xf0, 0, 0); + EXPECT_EQ (packet.getType(), PacketType::utility); + + packet = fromMidi1ByteStream (0xf7, 0, 0); + EXPECT_EQ (packet.getType(), PacketType::utility); + + packet = fromMidi1ByteStream (0xf9, 0, 0); + EXPECT_EQ (packet.getType(), PacketType::utility); + + packet = fromMidi1ByteStream (0xfd, 0, 0); + EXPECT_EQ (packet.getType(), PacketType::utility); +} + +TEST (Midi1ByteStreamParserTests, FromMidi1ByteStreamValidSystemAndChannel) +{ + auto packet = fromMidi1ByteStream (0xf8, 0, 0); + EXPECT_EQ (packet.getType(), PacketType::system); + EXPECT_EQ (packet.getStatus(), uint8_t (SystemStatus::clock)); + + packet = fromMidi1ByteStream (0x90, 0x3c, 0x7f); + EXPECT_EQ (packet.getType(), PacketType::midi1ChannelVoice); + EXPECT_EQ (packet.getStatus(), 0x90); +} + +TEST (Midi1ByteStreamParserTests, ToMidi1ByteStreamSysExPackets) +{ + uint8_t result[8] = {}; + + auto completePacket = makeSysEx7CompletePacket (0); + completePacket.addPayloadByte (0x7e); + completePacket.addPayloadByte (0x00); + completePacket.addPayloadByte (0x01); + auto size = toMidi1ByteStream (completePacket, result); + ASSERT_EQ (size, 5u); + EXPECT_EQ (result[0], 0xf0); + EXPECT_EQ (result[4], 0xf7); + + std::fill (std::begin (result), std::end (result), 0); + auto startPacket = makeSysEx7StartPacket (0); + startPacket.addPayloadByte (0x7e); + startPacket.addPayloadByte (0x00); + size = toMidi1ByteStream (startPacket, result); + ASSERT_EQ (size, 3u); + EXPECT_EQ (result[0], 0xf0); + + std::fill (std::begin (result), std::end (result), 0); + auto endPacket = makeSysEx7EndPacket (0); + endPacket.addPayloadByte (0x01); + size = toMidi1ByteStream (endPacket, result); + ASSERT_EQ (size, 2u); + EXPECT_EQ (result[1], 0xf7); +} + +TEST (Midi1ByteStreamParserTests, ToMidi1ByteStreamUnsupportedType) +{ + uint8_t result[8] = {}; + + auto utilityPacket = UniversalPacket {}; + utilityPacket.setByte (0, uint8_t (PacketType::utility) << 4); + auto size = toMidi1ByteStream (utilityPacket, result); + EXPECT_EQ (size, 0u); +} diff --git a/tests/yup_audio_basics/yup_UMPPacketBuffer.cpp b/tests/yup_audio_basics/yup_UMPPacketBuffer.cpp index e3ea09abf..287a6bcca 100644 --- a/tests/yup_audio_basics/yup_UMPPacketBuffer.cpp +++ b/tests/yup_audio_basics/yup_UMPPacketBuffer.cpp @@ -85,3 +85,299 @@ TEST (UMPPacketBufferTests, FindNextSamplePosition) ASSERT_NE (it, buffer.end()); EXPECT_EQ ((*it).samplePosition, 12); } + +TEST (UMPPacketBufferTests, ClearEntireBuffer) +{ + UMPPacketBuffer buffer; + + const UniversalPacket p1 { 0x20000000u }; + const UniversalPacket p2 { 0x40000000u, 0x11111111u }; + + buffer.addEvent (p1.data, p1.getSize(), 5); + buffer.addEvent (p2.data, p2.getSize(), 10); + + EXPECT_FALSE (buffer.isEmpty()); + buffer.clear(); + EXPECT_TRUE (buffer.isEmpty()); +} + +TEST (UMPPacketBufferTests, ClearRangeRemovesEvents) +{ + UMPPacketBuffer buffer; + + const UniversalPacket p1 { 0x20000000u }; + const UniversalPacket p2 { 0x40000000u, 0x11111111u }; + const UniversalPacket p3 { 0x40000000u, 0x22222222u }; + + buffer.addEvent (p1.data, p1.getSize(), 2); + buffer.addEvent (p2.data, p2.getSize(), 8); + buffer.addEvent (p3.data, p3.getSize(), 12); + + buffer.clear (7, 6); + + EXPECT_EQ (buffer.getNumEvents(), 1); + EXPECT_EQ (buffer.getFirstEventTime(), 2); + EXPECT_EQ (buffer.getLastEventTime(), 2); +} + +TEST (UMPPacketBufferTests, ClearRangeAtStart) +{ + UMPPacketBuffer buffer; + + const UniversalPacket p1 { 0x20000000u }; + const UniversalPacket p2 { 0x40000000u, 0x11111111u }; + + buffer.addEvent (p1.data, p1.getSize(), 2); + buffer.addEvent (p2.data, p2.getSize(), 8); + + buffer.clear (0, 5); + + EXPECT_EQ (buffer.getNumEvents(), 1); + EXPECT_EQ (buffer.getFirstEventTime(), 8); +} + +TEST (UMPPacketBufferTests, ClearRangeAtEnd) +{ + UMPPacketBuffer buffer; + + const UniversalPacket p1 { 0x20000000u }; + const UniversalPacket p2 { 0x40000000u, 0x11111111u }; + + buffer.addEvent (p1.data, p1.getSize(), 2); + buffer.addEvent (p2.data, p2.getSize(), 8); + + buffer.clear (7, 10); + + EXPECT_EQ (buffer.getNumEvents(), 1); + EXPECT_EQ (buffer.getLastEventTime(), 2); +} + +TEST (UMPPacketBufferTests, SwapWith) +{ + UMPPacketBuffer buffer1; + UMPPacketBuffer buffer2; + + const UniversalPacket p1 { 0x20000000u }; + const UniversalPacket p2 { 0x40000000u, 0x11111111u }; + + buffer1.addEvent (p1.data, p1.getSize(), 5); + buffer2.addEvent (p2.data, p2.getSize(), 10); + + buffer1.swapWith (buffer2); + + EXPECT_EQ (buffer1.getNumEvents(), 1); + EXPECT_EQ (buffer1.getFirstEventTime(), 10); + EXPECT_EQ (buffer2.getNumEvents(), 1); + EXPECT_EQ (buffer2.getFirstEventTime(), 5); +} + +TEST (UMPPacketBufferTests, EnsureSize) +{ + UMPPacketBuffer buffer; + buffer.ensureSize (1024); + + const UniversalPacket p1 { 0x20000000u }; + EXPECT_TRUE (buffer.addEvent (p1.data, p1.getSize(), 0)); +} + +TEST (UMPPacketBufferTests, AddEventWithView) +{ + UMPPacketBuffer buffer; + + const UniversalPacket packet { 0x40000000u, 0x12345678u }; + EXPECT_TRUE (buffer.addEvent (View (packet.data), 5)); + + EXPECT_EQ (buffer.getNumEvents(), 1); + EXPECT_EQ (buffer.getFirstEventTime(), 5); +} + +TEST (UMPPacketBufferTests, AddEventRejectsInvalidData) +{ + UMPPacketBuffer buffer; + + EXPECT_FALSE (buffer.addEvent (nullptr, 1, 0)); + EXPECT_FALSE (buffer.addEvent ((uint32_t*) 0x1, 0, 0)); + EXPECT_FALSE (buffer.addEvent ((uint32_t*) 0x1, 5, 0)); +} + +TEST (UMPPacketBufferTests, AddEventsFromOtherBuffer) +{ + UMPPacketBuffer buffer1; + UMPPacketBuffer buffer2; + + const UniversalPacket p1 { 0x20000000u }; + const UniversalPacket p2 { 0x40000000u, 0x11111111u }; + const UniversalPacket p3 { 0x40000000u, 0x22222222u }; + + buffer1.addEvent (p1.data, p1.getSize(), 2); + buffer1.addEvent (p2.data, p2.getSize(), 8); + buffer1.addEvent (p3.data, p3.getSize(), 12); + + buffer2.addEvents (buffer1, 0, -1, 0); + + EXPECT_EQ (buffer2.getNumEvents(), 3); +} + +TEST (UMPPacketBufferTests, AddEventsWithRange) +{ + UMPPacketBuffer buffer1; + UMPPacketBuffer buffer2; + + const UniversalPacket p1 { 0x20000000u }; + const UniversalPacket p2 { 0x40000000u, 0x11111111u }; + const UniversalPacket p3 { 0x40000000u, 0x22222222u }; + + buffer1.addEvent (p1.data, p1.getSize(), 2); + buffer1.addEvent (p2.data, p2.getSize(), 8); + buffer1.addEvent (p3.data, p3.getSize(), 12); + + buffer2.addEvents (buffer1, 5, 10, 0); + + EXPECT_EQ (buffer2.getNumEvents(), 2); + EXPECT_EQ (buffer2.getFirstEventTime(), 8); + EXPECT_EQ (buffer2.getLastEventTime(), 12); +} + +TEST (UMPPacketBufferTests, AddEventsWithSampleDelta) +{ + UMPPacketBuffer buffer1; + UMPPacketBuffer buffer2; + + const UniversalPacket p1 { 0x20000000u }; + const UniversalPacket p2 { 0x40000000u, 0x11111111u }; + + buffer1.addEvent (p1.data, p1.getSize(), 2); + buffer1.addEvent (p2.data, p2.getSize(), 8); + + buffer2.addEvents (buffer1, 0, -1, 10); + + EXPECT_EQ (buffer2.getNumEvents(), 2); + EXPECT_EQ (buffer2.getFirstEventTime(), 12); + EXPECT_EQ (buffer2.getLastEventTime(), 18); +} + +TEST (UMPPacketBufferTests, IteratorPreIncrement) +{ + UMPPacketBuffer buffer; + + const UniversalPacket p1 { 0x20000000u }; + const UniversalPacket p2 { 0x40000000u, 0x11111111u }; + + buffer.addEvent (p1.data, p1.getSize(), 2); + buffer.addEvent (p2.data, p2.getSize(), 8); + + auto it = buffer.begin(); + EXPECT_EQ ((*it).samplePosition, 2); + + ++it; + EXPECT_EQ ((*it).samplePosition, 8); +} + +TEST (UMPPacketBufferTests, IteratorPostIncrement) +{ + UMPPacketBuffer buffer; + + const UniversalPacket p1 { 0x20000000u }; + const UniversalPacket p2 { 0x40000000u, 0x11111111u }; + + buffer.addEvent (p1.data, p1.getSize(), 2); + buffer.addEvent (p2.data, p2.getSize(), 8); + + auto it = buffer.begin(); + auto oldIt = it++; + + EXPECT_EQ ((*oldIt).samplePosition, 2); + EXPECT_EQ ((*it).samplePosition, 8); +} + +TEST (UMPPacketBufferTests, IteratorDereference) +{ + UMPPacketBuffer buffer; + + const UniversalPacket packet { 0x40000000u, 0x12345678u }; + buffer.addEvent (packet.data, packet.getSize(), 5); + + auto it = buffer.begin(); + auto meta = *it; + + EXPECT_EQ (meta.samplePosition, 5); + EXPECT_EQ (meta.numWords, 2); + EXPECT_EQ (meta.getView()[0], packet.data[0]); + EXPECT_EQ (meta.getView()[1], packet.data[1]); +} + +TEST (UMPPacketBufferTests, GetFirstEventTimeOnEmpty) +{ + UMPPacketBuffer buffer; + EXPECT_EQ (buffer.getFirstEventTime(), 0); +} + +TEST (UMPPacketBufferTests, GetLastEventTimeOnEmpty) +{ + UMPPacketBuffer buffer; + EXPECT_EQ (buffer.getLastEventTime(), 0); +} + +TEST (UMPPacketBufferTests, GetNumEventsOnEmpty) +{ + UMPPacketBuffer buffer; + EXPECT_EQ (buffer.getNumEvents(), 0); +} + +TEST (UMPPacketBufferTests, FindNextSamplePositionAtStart) +{ + UMPPacketBuffer buffer; + + const UniversalPacket p1 { 0x20000000u }; + buffer.addEvent (p1.data, p1.getSize(), 5); + + auto it = buffer.findNextSamplePosition (0); + ASSERT_NE (it, buffer.end()); + EXPECT_EQ ((*it).samplePosition, 5); +} + +TEST (UMPPacketBufferTests, FindNextSamplePositionBeyondEnd) +{ + UMPPacketBuffer buffer; + + const UniversalPacket p1 { 0x20000000u }; + buffer.addEvent (p1.data, p1.getSize(), 5); + + auto it = buffer.findNextSamplePosition (100); + EXPECT_EQ (it, buffer.end()); +} + +TEST (UMPPacketBufferTests, RangeBasedForLoop) +{ + UMPPacketBuffer buffer; + + const UniversalPacket p1 { 0x20000000u }; + const UniversalPacket p2 { 0x40000000u, 0x11111111u }; + + buffer.addEvent (p1.data, p1.getSize(), 2); + buffer.addEvent (p2.data, p2.getSize(), 8); + + int count = 0; + for (const auto& meta : buffer) + { + EXPECT_GE (meta.samplePosition, 0); + ++count; + } + + EXPECT_EQ (count, 2); +} + +TEST (UMPPacketBufferTests, AddMultipleEventsAtSameSample) +{ + UMPPacketBuffer buffer; + + const UniversalPacket p1 { 0x20000000u }; + const UniversalPacket p2 { 0x40000000u, 0x11111111u }; + + buffer.addEvent (p1.data, p1.getSize(), 5); + buffer.addEvent (p2.data, p2.getSize(), 5); + + EXPECT_EQ (buffer.getNumEvents(), 2); + EXPECT_EQ (buffer.getFirstEventTime(), 5); + EXPECT_EQ (buffer.getLastEventTime(), 5); +} diff --git a/tests/yup_audio_devices/yup_UMPPacketCollector.cpp b/tests/yup_audio_devices/yup_UMPPacketCollector.cpp index e49630b18..cba91b93d 100644 --- a/tests/yup_audio_devices/yup_UMPPacketCollector.cpp +++ b/tests/yup_audio_devices/yup_UMPPacketCollector.cpp @@ -55,3 +55,222 @@ TEST (UMPPacketCollectorTests, HandlesKeyboardStateCallbacks) EXPECT_FALSE (buffer.isEmpty()); EXPECT_EQ (buffer.getNumEvents(), 1); } + +TEST (UMPPacketCollectorTests, HandlesNoteOffMidi1) +{ + UMPPacketCollector collector (PacketProtocol::MIDI_1_0, 0); + collector.reset (1000.0); + + collector.handleNoteOff (nullptr, 2, 60, 0.6f); + + UMPPacketBuffer buffer; + collector.removeNextBlockOfPackets (buffer, 64); + + EXPECT_FALSE (buffer.isEmpty()); + EXPECT_EQ (buffer.getNumEvents(), 1); +} + +TEST (UMPPacketCollectorTests, HandlesNoteOffMidi2) +{ + UMPPacketCollector collector (PacketProtocol::MIDI_2_0, 1); + collector.reset (1000.0); + + collector.handleNoteOff (nullptr, 3, 72, 0.3f); + + UMPPacketBuffer buffer; + collector.removeNextBlockOfPackets (buffer, 64); + + EXPECT_FALSE (buffer.isEmpty()); + EXPECT_EQ (buffer.getNumEvents(), 1); +} + +TEST (UMPPacketCollectorTests, PacketReceivedDirectly) +{ + UMPPacketCollector collector (PacketProtocol::MIDI_1_0, 0); + collector.reset (1000.0); + + const auto packet = makeMidi1NoteOnMessage (0, 0, 60, Velocity { uint7_t { 100 } }); + const auto timeStamp = Time::getMillisecondCounterHiRes() * 0.001 + 0.005; + collector.packetReceived (View (packet.data), timeStamp); + + UMPPacketBuffer buffer; + collector.removeNextBlockOfPackets (buffer, 64); + + EXPECT_FALSE (buffer.isEmpty()); +} + +TEST (UMPPacketCollectorTests, MultiplePacketsInBlock) +{ + UMPPacketCollector collector (PacketProtocol::MIDI_1_0, 0); + collector.reset (1000.0); + + const auto packet1 = makeMidi1NoteOnMessage (0, 0, 60, Velocity { uint7_t { 100 } }); + const auto packet2 = makeMidi1NoteOnMessage (0, 0, 64, Velocity { uint7_t { 90 } }); + const auto packet3 = makeMidi1NoteOnMessage (0, 0, 67, Velocity { uint7_t { 80 } }); + + auto baseTime = Time::getMillisecondCounterHiRes() * 0.001; + collector.addPacketToQueue (View (packet1.data), baseTime + 0.001); + collector.addPacketToQueue (View (packet2.data), baseTime + 0.002); + collector.addPacketToQueue (View (packet3.data), baseTime + 0.003); + + UMPPacketBuffer buffer; + collector.removeNextBlockOfPackets (buffer, 128); + + EXPECT_EQ (buffer.getNumEvents(), 3); +} + +TEST (UMPPacketCollectorTests, EnsureStorageAllocated) +{ + UMPPacketCollector collector (PacketProtocol::MIDI_1_0, 0); + collector.reset (1000.0); + + collector.ensureStorageAllocated (1024); + + const auto packet = makeMidi1NoteOnMessage (0, 0, 60, Velocity { uint7_t { 100 } }); + const auto timeStamp = Time::getMillisecondCounterHiRes() * 0.001 + 0.005; + collector.addPacketToQueue (View (packet.data), timeStamp); + + UMPPacketBuffer buffer; + collector.removeNextBlockOfPackets (buffer, 64); + + EXPECT_FALSE (buffer.isEmpty()); +} + +TEST (UMPPacketCollectorTests, ScalesPacketTimingForLargeBlocks) +{ + UMPPacketCollector collector (PacketProtocol::MIDI_1_0, 0); + collector.reset (1000.0); + + auto baseTime = Time::getMillisecondCounterHiRes() * 0.001; + + for (int i = 0; i < 100; ++i) + { + const auto packet = makeMidi1NoteOnMessage (0, 0, 60 + (i % 12), Velocity { uint7_t { 100 } }); + collector.addPacketToQueue (View (packet.data), baseTime + i * 0.001); + } + + std::this_thread::sleep_for (std::chrono::milliseconds (10)); + + UMPPacketBuffer buffer; + collector.removeNextBlockOfPackets (buffer, 64); + + EXPECT_GT (buffer.getNumEvents(), 0); +} + +TEST (UMPPacketCollectorTests, ClearsOldPackets) +{ + UMPPacketCollector collector (PacketProtocol::MIDI_1_0, 0); + collector.reset (1000.0); + + auto baseTime = Time::getMillisecondCounterHiRes() * 0.001; + const auto packet = makeMidi1NoteOnMessage (0, 0, 60, Velocity { uint7_t { 100 } }); + collector.addPacketToQueue (View (packet.data), baseTime - 2.0); + + UMPPacketBuffer buffer; + collector.removeNextBlockOfPackets (buffer, 64); + + EXPECT_GE (buffer.getNumEvents(), 0); +} + +TEST (UMPPacketCollectorTests, EmptyBufferOnNoPackets) +{ + UMPPacketCollector collector (PacketProtocol::MIDI_1_0, 0); + collector.reset (1000.0); + + UMPPacketBuffer buffer; + collector.removeNextBlockOfPackets (buffer, 64); + + EXPECT_TRUE (buffer.isEmpty()); +} + +TEST (UMPPacketCollectorTests, VelocityConversionMidi1) +{ + UMPPacketCollector collector (PacketProtocol::MIDI_1_0, 0); + collector.reset (1000.0); + + collector.handleNoteOn (nullptr, 1, 60, 0.0f); + collector.handleNoteOn (nullptr, 1, 61, 0.5f); + collector.handleNoteOn (nullptr, 1, 62, 1.0f); + + UMPPacketBuffer buffer; + collector.removeNextBlockOfPackets (buffer, 64); + + EXPECT_EQ (buffer.getNumEvents(), 3); +} + +TEST (UMPPacketCollectorTests, VelocityConversionMidi2) +{ + UMPPacketCollector collector (PacketProtocol::MIDI_2_0, 0); + collector.reset (1000.0); + + collector.handleNoteOn (nullptr, 1, 60, 0.0f); + collector.handleNoteOn (nullptr, 1, 61, 0.5f); + collector.handleNoteOn (nullptr, 1, 62, 1.0f); + + UMPPacketBuffer buffer; + collector.removeNextBlockOfPackets (buffer, 64); + + EXPECT_EQ (buffer.getNumEvents(), 3); +} + +TEST (UMPPacketCollectorTests, DifferentGroupsInConstructor) +{ + UMPPacketCollector collector1 (PacketProtocol::MIDI_1_0, 0); + UMPPacketCollector collector2 (PacketProtocol::MIDI_1_0, 5); + + collector1.reset (1000.0); + collector2.reset (1000.0); + + collector1.handleNoteOn (nullptr, 1, 60, 0.8f); + collector2.handleNoteOn (nullptr, 1, 60, 0.8f); + + UMPPacketBuffer buffer1, buffer2; + collector1.removeNextBlockOfPackets (buffer1, 64); + collector2.removeNextBlockOfPackets (buffer2, 64); + + EXPECT_FALSE (buffer1.isEmpty()); + EXPECT_FALSE (buffer2.isEmpty()); +} + +TEST (UMPPacketCollectorTests, ResetClearsIncomingPackets) +{ + UMPPacketCollector collector (PacketProtocol::MIDI_1_0, 0); + collector.reset (1000.0); + + const auto packet = makeMidi1NoteOnMessage (0, 0, 60, Velocity { uint7_t { 100 } }); + const auto timeStamp = Time::getMillisecondCounterHiRes() * 0.001 + 0.005; + collector.addPacketToQueue (View (packet.data), timeStamp); + + collector.reset (2000.0); + + UMPPacketBuffer buffer; + collector.removeNextBlockOfPackets (buffer, 64); + + EXPECT_TRUE (buffer.isEmpty()); +} + +TEST (UMPPacketCollectorTests, HandlesSmallBlockSizes) +{ + UMPPacketCollector collector (PacketProtocol::MIDI_1_0, 0); + collector.reset (1000.0); + + collector.handleNoteOn (nullptr, 1, 60, 0.8f); + + UMPPacketBuffer buffer; + collector.removeNextBlockOfPackets (buffer, 1); + + EXPECT_FALSE (buffer.isEmpty()); +} + +TEST (UMPPacketCollectorTests, HandlesLargeBlockSizes) +{ + UMPPacketCollector collector (PacketProtocol::MIDI_1_0, 0); + collector.reset (1000.0); + + collector.handleNoteOn (nullptr, 1, 60, 0.8f); + + UMPPacketBuffer buffer; + collector.removeNextBlockOfPackets (buffer, 8192); + + EXPECT_FALSE (buffer.isEmpty()); +} From e27c7016876d69ff6b7cbe7d0d62b4b39d22e7ac Mon Sep 17 00:00:00 2001 From: kunitoki Date: Tue, 23 Dec 2025 16:19:04 +0100 Subject: [PATCH 12/20] Midi 2.0 and UMP support for linux using ALSA sequencer and rawmidi --- .../native/yup_Midi_linux.cpp | 2397 +++++++++++------ 1 file changed, 1583 insertions(+), 814 deletions(-) diff --git a/modules/yup_audio_devices/native/yup_Midi_linux.cpp b/modules/yup_audio_devices/native/yup_Midi_linux.cpp index 7621b28bb..1e03a6bf1 100644 --- a/modules/yup_audio_devices/native/yup_Midi_linux.cpp +++ b/modules/yup_audio_devices/native/yup_Midi_linux.cpp @@ -35,817 +35,1586 @@ DISCLAIMED. ============================================================================== -*/ - -namespace yup -{ - -#if YUP_ALSA - -//============================================================================== -class AlsaClient -{ - auto lowerBound (int portId) const - { - const auto comparator = [] (const auto& port, const auto& id) - { - return port->getPortId() < id; - }; - return std::lower_bound (ports.begin(), ports.end(), portId, comparator); - } - - auto findPortIterator (int portId) const - { - const auto iter = lowerBound (portId); - return (iter == ports.end() || (*iter)->getPortId() != portId) ? ports.end() : iter; - } - -public: - ~AlsaClient() - { - inputThread.reset(); - - jassert (activeCallbacks.get() == 0); - - if (handle != nullptr) - { - snd_seq_delete_simple_port (handle, announcementsIn); - snd_seq_close (handle); - } - } - - static String getAlsaMidiName() - { -#ifdef YUP_ALSA_MIDI_NAME - return YUP_ALSA_MIDI_NAME; -#else - if (auto* app = YUPApplicationBase::getInstance()) - return app->getApplicationName(); - - return "YUP"; -#endif - } - - //============================================================================== - // represents an input or output port of the supplied AlsaClient - struct Port - { - explicit Port (bool forInput) noexcept - : isInput (forInput) - { - } - - ~Port() - { - if (isValid()) - { - if (isInput) - enableCallback (false); - else - snd_midi_event_free (midiParser); - - snd_seq_delete_simple_port (client->get(), portId); - } - } - - void connectWith (int sourceClient, int sourcePort) const noexcept - { - if (isInput) - snd_seq_connect_from (client->get(), portId, sourceClient, sourcePort); - else - snd_seq_connect_to (client->get(), portId, sourceClient, sourcePort); - } - - bool isValid() const noexcept - { - return client->get() != nullptr && portId >= 0; - } - - void setupInput (MidiInput* input, MidiInputCallback* cb) - { - jassert (cb != nullptr && input != nullptr); - callback = cb; - midiInput = input; - } - - void setupOutput() - { - jassert (! isInput); - snd_midi_event_new ((size_t) maxEventSize, &midiParser); - } - - void enableCallback (bool enable) - { - callbackEnabled = enable; - } - - bool sendMessageNow (const MidiMessage& message) - { - if (message.getRawDataSize() > maxEventSize) - { - maxEventSize = message.getRawDataSize(); - snd_midi_event_free (midiParser); - snd_midi_event_new ((size_t) maxEventSize, &midiParser); - } - - snd_seq_event_t event; - snd_seq_ev_clear (&event); - - auto numBytes = (long) message.getRawDataSize(); - auto* data = message.getRawData(); - - auto seqHandle = client->get(); - bool success = true; - - while (numBytes > 0) - { - auto numSent = snd_midi_event_encode (midiParser, data, numBytes, &event); - - if (numSent <= 0) - { - success = numSent == 0; - break; - } - - numBytes -= numSent; - data += numSent; - - snd_seq_ev_set_source (&event, (unsigned char) portId); - snd_seq_ev_set_subs (&event); - snd_seq_ev_set_direct (&event); - - if (snd_seq_event_output_direct (seqHandle, &event) < 0) - { - success = false; - break; - } - } - - snd_midi_event_reset_encode (midiParser); - return success; - } - - bool operator== (const Port& lhs) const noexcept - { - return portId != -1 && portId == lhs.portId; - } - - void createPort (const String& name, bool enableSubscription) - { - if (auto seqHandle = client->get()) - { - const unsigned int caps = - isInput ? (SND_SEQ_PORT_CAP_WRITE | (enableSubscription ? SND_SEQ_PORT_CAP_SUBS_WRITE : 0)) - : (SND_SEQ_PORT_CAP_READ | (enableSubscription ? SND_SEQ_PORT_CAP_SUBS_READ : 0)); - - portName = name; - portId = snd_seq_create_simple_port (seqHandle, portName.toUTF8(), caps, SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION); - } - } - - void handleIncomingMidiMessage (const MidiMessage& message) const - { - if (callbackEnabled) - callback->handleIncomingMidiMessage (midiInput, message); - } - - void handlePartialSysexMessage (const uint8* messageData, int numBytesSoFar, double timeStamp) - { - if (callbackEnabled) - callback->handlePartialSysexMessage (midiInput, messageData, numBytesSoFar, timeStamp); - } - - int getPortId() const { return portId; } - - const String& getPortName() const { return portName; } - - private: - const std::shared_ptr client = AlsaClient::getInstance(); - - MidiInputCallback* callback = nullptr; - snd_midi_event_t* midiParser = nullptr; - MidiInput* midiInput = nullptr; - - String portName; - - int maxEventSize = 4096, portId = -1; - std::atomic callbackEnabled { false }; - bool isInput = false; - }; - - static std::shared_ptr getInstance() - { - static std::weak_ptr ptr; - - if (auto locked = ptr.lock()) - return locked; - - std::shared_ptr result (new AlsaClient()); - ptr = result; - return result; - } - - void handleIncomingMidiMessage (snd_seq_event* event, const MidiMessage& message) - { - const ScopedLock sl (callbackLock); - - if (auto* port = findPort (event->dest.port)) - port->handleIncomingMidiMessage (message); - } - - void handlePartialSysexMessage (snd_seq_event* event, const uint8* messageData, int numBytesSoFar, double timeStamp) - { - const ScopedLock sl (callbackLock); - - if (auto* port = findPort (event->dest.port)) - port->handlePartialSysexMessage (messageData, numBytesSoFar, timeStamp); - } - - snd_seq_t* get() const noexcept { return handle; } - - int getId() const noexcept { return clientId; } - - Port* createPort (const String& name, bool forInput, bool enableSubscription) - { - const ScopedLock sl (callbackLock); - - auto port = new Port (forInput); - port->createPort (name, enableSubscription); - - const auto iter = lowerBound (port->getPortId()); - jassert (iter == ports.end() || port->getPortId() < (*iter)->getPortId()); - ports.insert (iter, rawToUniquePtr (port)); - - return port; - } - - void deletePort (Port* port) - { - const ScopedLock sl (callbackLock); - - if (const auto iter = findPortIterator (port->getPortId()); iter != ports.end()) - ports.erase (iter); - } - -private: - AlsaClient() - { - snd_seq_open (&handle, "default", SND_SEQ_OPEN_DUPLEX, 0); - - if (handle != nullptr) - { - snd_seq_nonblock (handle, SND_SEQ_NONBLOCK); - snd_seq_set_client_name (handle, getAlsaMidiName().toRawUTF8()); - clientId = snd_seq_client_id (handle); - - // It's good idea to pre-allocate a good number of elements - ports.reserve (32); - - announcementsIn = snd_seq_create_simple_port (handle, - TRANS ("announcements").toRawUTF8(), - SND_SEQ_PORT_CAP_WRITE, - SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION); - snd_seq_connect_from (handle, announcementsIn, SND_SEQ_CLIENT_SYSTEM, SND_SEQ_PORT_SYSTEM_ANNOUNCE); - - inputThread.emplace (*this); - } - } - - Port* findPort (int portId) - { - if (const auto iter = findPortIterator (portId); iter != ports.end()) - return iter->get(); - - return nullptr; - } - - snd_seq_t* handle = nullptr; - int clientId = 0; - int announcementsIn = 0; - std::vector> ports; - Atomic activeCallbacks; - CriticalSection callbackLock; - - //============================================================================== - class SequencerThread - { - public: - explicit SequencerThread (AlsaClient& c) - : client (c) - { - } - - ~SequencerThread() noexcept - { - shouldStop = true; - thread.join(); - } - - private: - // If we directly call MidiDeviceListConnectionBroadcaster::get() from the background thread, - // there's a possibility that we'll deadlock in the following scenario: - // - The main thread calls MidiDeviceListConnectionBroadcaster::get() for the first time - // (e.g. to register a listener). The static MidiDeviceListConnectionBroadcaster singleton - // begins construction. During the constructor, an AlsaClient is created to iterate midi - // ins/outs. - // - The AlsaClient starts a new SequencerThread. If connections are updated, the - // SequencerThread may call MidiDeviceListConnectionBroadcaster::get().notify() - // while the MidiDeviceListConnectionBroadcaster singleton is still being created. - // - The SequencerThread blocks until the MidiDeviceListConnectionBroadcaster has been - // created on the main thread, but the MidiDeviceListConnectionBroadcaster's constructor - // can't complete until the AlsaClient's destructor has run, which in turn requires the - // SequencerThread to join. - class UpdateNotifier final : private AsyncUpdater - { - public: - ~UpdateNotifier() override { cancelPendingUpdate(); } - - using AsyncUpdater::triggerAsyncUpdate; - - private: - void handleAsyncUpdate() override { MidiDeviceListConnectionBroadcaster::get().notify(); } - }; - - AlsaClient& client; - MidiDataConcatenator concatenator { 2048 }; - std::atomic shouldStop { false }; - UpdateNotifier notifier; - std::thread thread { [this] - { - Thread::setCurrentThreadName ("YUP MIDI Input"); - - auto seqHandle = client.get(); - - const int maxEventSize = 16 * 1024; - snd_midi_event_t* midiParser; - - if (snd_midi_event_new (maxEventSize, &midiParser) >= 0) - { - const ScopeGuard freeMidiEvent { [&] - { - snd_midi_event_free (midiParser); - } }; - - const auto numPfds = snd_seq_poll_descriptors_count (seqHandle, POLLIN); - std::vector pfd (static_cast (numPfds)); - snd_seq_poll_descriptors (seqHandle, pfd.data(), (unsigned int) numPfds, POLLIN); - - std::vector buffer (maxEventSize); - - while (! shouldStop) - { - // This timeout shouldn't be too long, so that the program can exit in a timely manner - if (poll (pfd.data(), (nfds_t) numPfds, 100) > 0) - { - if (shouldStop) - break; - - do - { - snd_seq_event_t* inputEvent = nullptr; - - if (snd_seq_event_input (seqHandle, &inputEvent) >= 0) - { - const ScopeGuard freeInputEvent { [&] - { - snd_seq_free_event (inputEvent); - } }; - - constexpr int systemEvents[] { - SND_SEQ_EVENT_CLIENT_CHANGE, - SND_SEQ_EVENT_CLIENT_START, - SND_SEQ_EVENT_CLIENT_EXIT, - SND_SEQ_EVENT_PORT_CHANGE, - SND_SEQ_EVENT_PORT_START, - SND_SEQ_EVENT_PORT_EXIT, - SND_SEQ_EVENT_PORT_SUBSCRIBED, - SND_SEQ_EVENT_PORT_UNSUBSCRIBED, - }; - - const auto foundEvent = std::find (std::begin (systemEvents), - std::end (systemEvents), - inputEvent->type); - - if (foundEvent != std::end (systemEvents)) - { - notifier.triggerAsyncUpdate(); - continue; - } - - // xxx what about SYSEXes that are too big for the buffer? - const auto numBytes = snd_midi_event_decode (midiParser, - buffer.data(), - maxEventSize, - inputEvent); - - snd_midi_event_reset_decode (midiParser); - - concatenator.pushMidiData (buffer.data(), (int) numBytes, Time::getMillisecondCounter() * 0.001, inputEvent, client); - } - } while (snd_seq_event_input_pending (seqHandle, 0) > 0); - } - } - } - } }; - }; - - std::optional inputThread; -}; - -//============================================================================== -static String getFormattedPortIdentifier (int clientId, int portId) -{ - return String (clientId) + "-" + String (portId); -} - -static AlsaClient::Port* iterateMidiClient (AlsaClient& client, - snd_seq_client_info_t* clientInfo, - bool forInput, - Array& devices, - const String& deviceIdentifierToOpen) -{ - AlsaClient::Port* port = nullptr; - - auto seqHandle = client.get(); - snd_seq_port_info_t* portInfo = nullptr; - - snd_seq_port_info_alloca (&portInfo); - jassert (portInfo != nullptr); - auto numPorts = snd_seq_client_info_get_num_ports (clientInfo); - auto sourceClient = snd_seq_client_info_get_client (clientInfo); - - snd_seq_port_info_set_client (portInfo, sourceClient); - snd_seq_port_info_set_port (portInfo, -1); - - while (--numPorts >= 0) - { - if (snd_seq_query_next_port (seqHandle, portInfo) == 0 - && (snd_seq_port_info_get_capability (portInfo) - & (forInput ? SND_SEQ_PORT_CAP_SUBS_READ : SND_SEQ_PORT_CAP_SUBS_WRITE)) - != 0) - { - String portName (snd_seq_port_info_get_name (portInfo)); - auto portID = snd_seq_port_info_get_port (portInfo); - - MidiDeviceInfo device (portName, getFormattedPortIdentifier (sourceClient, portID)); - devices.add (device); - - if (deviceIdentifierToOpen.isNotEmpty() && deviceIdentifierToOpen == device.identifier) - { - if (portID != -1) - { - port = client.createPort (portName, forInput, false); - jassert (port->isValid()); - port->connectWith (sourceClient, portID); - break; - } - } - } - } - - return port; -} - -static AlsaClient::Port* iterateMidiDevices (bool forInput, - Array& devices, - const String& deviceIdentifierToOpen) -{ - AlsaClient::Port* port = nullptr; - auto client = AlsaClient::getInstance(); - - if (auto seqHandle = client->get()) - { - snd_seq_system_info_t* systemInfo = nullptr; - snd_seq_client_info_t* clientInfo = nullptr; - - snd_seq_system_info_alloca (&systemInfo); - jassert (systemInfo != nullptr); - - if (snd_seq_system_info (seqHandle, systemInfo) == 0) - { - snd_seq_client_info_alloca (&clientInfo); - jassert (clientInfo != nullptr); - - auto numClients = snd_seq_system_info_get_cur_clients (systemInfo); - - while (--numClients >= 0) - { - if (snd_seq_query_next_client (seqHandle, clientInfo) == 0) - { - port = iterateMidiClient (*client, - clientInfo, - forInput, - devices, - deviceIdentifierToOpen); - - if (port != nullptr) - break; - } - } - } - } - - return port; -} - -struct AlsaPortPtr -{ - explicit AlsaPortPtr (AlsaClient::Port* p) - : ptr (p) - { - } - - virtual ~AlsaPortPtr() noexcept { AlsaClient::getInstance()->deletePort (ptr); } - - AlsaClient::Port* ptr = nullptr; -}; - -//============================================================================== -class MidiInput::Pimpl final : public AlsaPortPtr -{ -public: - using AlsaPortPtr::AlsaPortPtr; -}; - -Array MidiInput::getAvailableDevices() -{ - Array devices; - iterateMidiDevices (true, devices, {}); - - return devices; -} - -MidiDeviceInfo MidiInput::getDefaultDevice() -{ - return getAvailableDevices().getFirst(); -} - -std::unique_ptr MidiInput::openDevice (const String& deviceIdentifier, MidiInputCallback* callback) -{ - if (deviceIdentifier.isEmpty()) - return {}; - - Array devices; - auto* port = iterateMidiDevices (true, devices, deviceIdentifier); - - if (port == nullptr || ! port->isValid()) - return {}; - - jassert (port->isValid()); - - std::unique_ptr midiInput (new MidiInput (port->getPortName(), - deviceIdentifier, - ump::PacketProtocol::MIDI_1_0)); - - port->setupInput (midiInput.get(), callback); - midiInput->internal = std::make_unique (port); - - return midiInput; -} - -std::unique_ptr MidiInput::openDevice (const String&, - ump::PacketProtocol, - ump::Receiver*) -{ - jassertfalse; - return {}; -} - -std::unique_ptr MidiInput::createNewDevice (const String& deviceName, MidiInputCallback* callback) -{ - auto client = AlsaClient::getInstance(); - auto* port = client->createPort (deviceName, true, true); - - if (port == nullptr || ! port->isValid()) - return {}; - - std::unique_ptr midiInput (new MidiInput (deviceName, - getFormattedPortIdentifier (client->getId(), port->getPortId()), - ump::PacketProtocol::MIDI_1_0)); - - port->setupInput (midiInput.get(), callback); - midiInput->internal = std::make_unique (port); - - return midiInput; -} - -std::unique_ptr MidiInput::createNewDevice (const String&, - ump::PacketProtocol, - ump::Receiver*) -{ - jassertfalse; - return {}; -} - -MidiInput::MidiInput (const String& deviceName, - const String& deviceIdentifier, - ump::PacketProtocol protocol) - : deviceInfo (deviceName, deviceIdentifier, protocol) -{ -} - -MidiInput::~MidiInput() -{ - stop(); -} - -void MidiInput::start() -{ - internal->ptr->enableCallback (true); -} - -void MidiInput::stop() -{ - internal->ptr->enableCallback (false); -} - -//============================================================================== -class MidiOutput::Pimpl final : public AlsaPortPtr -{ -public: - using AlsaPortPtr::AlsaPortPtr; -}; - -Array MidiOutput::getAvailableDevices() -{ - Array devices; - iterateMidiDevices (false, devices, {}); - - return devices; -} - -MidiDeviceInfo MidiOutput::getDefaultDevice() -{ - return getAvailableDevices().getFirst(); -} - -std::unique_ptr MidiOutput::openDevice (const String& deviceIdentifier) -{ - if (deviceIdentifier.isEmpty()) - return {}; - - Array devices; - auto* port = iterateMidiDevices (false, devices, deviceIdentifier); - - if (port == nullptr || ! port->isValid()) - return {}; - - std::unique_ptr midiOutput (new MidiOutput (port->getPortName(), - deviceIdentifier, - ump::PacketProtocol::MIDI_1_0)); - - port->setupOutput(); - midiOutput->internal = std::make_unique (port); - - return midiOutput; -} - -std::unique_ptr MidiOutput::openDevice (const String& deviceIdentifier, - ump::PacketProtocol protocol) -{ - if (protocol != ump::PacketProtocol::MIDI_1_0) - return {}; - - return openDevice (deviceIdentifier); -} - -std::unique_ptr MidiOutput::createNewDevice (const String& deviceName) -{ - auto client = AlsaClient::getInstance(); - auto* port = client->createPort (deviceName, false, true); - - if (port == nullptr || ! port->isValid()) - return {}; - - std::unique_ptr midiOutput (new MidiOutput (deviceName, - getFormattedPortIdentifier (client->getId(), port->getPortId()), - ump::PacketProtocol::MIDI_1_0)); - - port->setupOutput(); - midiOutput->internal = std::make_unique (port); - - return midiOutput; -} - -std::unique_ptr MidiOutput::createNewDevice (const String& deviceName, - ump::PacketProtocol protocol) -{ - if (protocol != ump::PacketProtocol::MIDI_1_0) - return {}; - - return createNewDevice (deviceName); -} - -MidiOutput::~MidiOutput() -{ - stopBackgroundThread(); -} - -void MidiOutput::sendMessageNow (const MidiMessage& message) -{ - internal->ptr->sendMessageNow (message); -} - -void MidiOutput::sendMessageNow (const ump::View& message) -{ - ump::ToBytestreamConverter converter { 2048 }; - converter.convert (message, 0.0, [&] (const MidiMessage& midiMessage) - { - sendMessageNow (midiMessage); - }); -} - -void MidiOutput::sendMessageNow (const ump::Packets& packets) -{ - for (auto it = packets.cbegin(); it != packets.cend(); ++it) - sendMessageNow (*it); -} - -MidiDeviceListConnection MidiDeviceListConnection::make (std::function cb) -{ - auto& broadcaster = MidiDeviceListConnectionBroadcaster::get(); - // We capture the AlsaClient instance here to ensure that it remains alive for at least as long - // as the MidiDeviceListConnection. This is necessary because system change messages will only - // be processed when the AlsaClient's SequencerThread is running. - return { &broadcaster, broadcaster.add ([fn = std::move (cb), client = AlsaClient::getInstance()] - { - NullCheckedInvocation::invoke (fn); - }) }; -} - -//============================================================================== -#else - -class MidiInput::Pimpl -{ -}; - -// (These are just stub functions if ALSA is unavailable...) -MidiInput::MidiInput (const String& deviceName, - const String& deviceID, - ump::PacketProtocol protocol) - : deviceInfo (deviceName, deviceID, protocol) -{ -} - -MidiInput::~MidiInput() {} - -void MidiInput::start() {} - -void MidiInput::stop() {} - -Array MidiInput::getAvailableDevices() { return {}; } - -MidiDeviceInfo MidiInput::getDefaultDevice() { return {}; } - -std::unique_ptr MidiInput::openDevice (const String&, MidiInputCallback*) { return {}; } - -std::unique_ptr MidiInput::openDevice (const String&, - ump::PacketProtocol, - ump::Receiver*) -{ - return {}; -} - -std::unique_ptr MidiInput::createNewDevice (const String&, MidiInputCallback*) { return {}; } - -std::unique_ptr MidiInput::createNewDevice (const String&, - ump::PacketProtocol, - ump::Receiver*) -{ - return {}; -} - -class MidiOutput::Pimpl -{ -}; - -MidiOutput::~MidiOutput() {} - -void MidiOutput::sendMessageNow (const MidiMessage&) {} - -void MidiOutput::sendMessageNow (const ump::View&) {} - -void MidiOutput::sendMessageNow (const ump::Packets&) {} - -Array MidiOutput::getAvailableDevices() { return {}; } - -MidiDeviceInfo MidiOutput::getDefaultDevice() { return {}; } - -std::unique_ptr MidiOutput::openDevice (const String&) { return {}; } - -std::unique_ptr MidiOutput::openDevice (const String&, ump::PacketProtocol) { return {}; } - -std::unique_ptr MidiOutput::createNewDevice (const String&) { return {}; } - -std::unique_ptr MidiOutput::createNewDevice (const String&, ump::PacketProtocol) { return {}; } - -MidiDeviceListConnection MidiDeviceListConnection::make (std::function cb) -{ - auto& broadcaster = MidiDeviceListConnectionBroadcaster::get(); - return { &broadcaster, broadcaster.add (std::move (cb)) }; -} - -#endif - -} // namespace yup +*/ + +#include +#include +#include +#include +#include + +#if defined(__has_include) +#if __has_include() +#include +#define YUP_HAS_ALSA_UMP 1 +#endif +#endif + +#ifndef YUP_HAS_ALSA_UMP +#define YUP_HAS_ALSA_UMP 0 +#endif + +namespace yup +{ + +#if YUP_ALSA + +namespace +{ +constexpr const char* umpRawmidiPrefix = "ump-rawmidi:"; +constexpr const char* umpSequencerPrefix = "ump-seq:"; + +bool isUmpRawmidiIdentifier (const String& identifier) +{ + return identifier.startsWith (umpRawmidiPrefix); +} + +bool isUmpSequencerIdentifier (const String& identifier) +{ + return identifier.startsWith (umpSequencerPrefix); +} + +String getUmpRawmidiPath (const String& identifier) +{ + return identifier.fromFirstOccurrenceOf (umpRawmidiPrefix, false, false); +} + +String getUmpRawmidiIdentifier (const String& path) +{ + return String (umpRawmidiPrefix) + path; +} + +String getUmpSequencerIdentifier (const String& identifier) +{ + return String (umpSequencerPrefix) + identifier; +} + +Array findUmpRawmidiNodes() +{ + Array results; + File ("/dev/snd").findChildFiles (results, File::findFiles, false, "umpC*D*"); + return results; +} + +bool parseUmpRawmidiPath (const String& path, int& card, int& device) +{ + const auto name = File (path).getFileName(); + if (! name.startsWith ("umpC")) + return false; + + const auto dIndex = name.indexOfChar ('D'); + if (dIndex <= 4) + return false; + + const auto cardString = name.substring (4, dIndex); + const auto deviceString = name.substring (dIndex + 1); + + if (! cardString.containsOnly ("0123456789") || ! deviceString.containsOnly ("0123456789")) + return false; + + card = cardString.getIntValue(); + device = deviceString.getIntValue(); + return true; +} + +String getUmpAlsaDeviceName (const String& path) +{ + int card = 0; + int device = 0; + if (! parseUmpRawmidiPath (path, card, device)) + return {}; + + return "hw:" + String (card) + "," + String (device); +} +} // namespace + +//============================================================================== +class AlsaClient +{ + auto lowerBound (int portId) const + { + const auto comparator = [] (const auto& port, const auto& id) + { + return port->getPortId() < id; + }; + return std::lower_bound (ports.begin(), ports.end(), portId, comparator); + } + + auto findPortIterator (int portId) const + { + const auto iter = lowerBound (portId); + return (iter == ports.end() || (*iter)->getPortId() != portId) ? ports.end() : iter; + } + +public: + ~AlsaClient() + { + inputThread.reset(); + + jassert (activeCallbacks.get() == 0); + + if (handle != nullptr) + { + snd_seq_delete_simple_port (handle, announcementsIn); + snd_seq_close (handle); + } + } + + static String getAlsaMidiName() + { +#ifdef YUP_ALSA_MIDI_NAME + return YUP_ALSA_MIDI_NAME; +#else + if (auto* app = YUPApplicationBase::getInstance()) + return app->getApplicationName(); + + return "YUP"; +#endif + } + + //============================================================================== + // represents an input or output port of the supplied AlsaClient + struct Port + { + explicit Port (bool forInput) noexcept + : isInput (forInput) + { + } + + ~Port() + { + if (isValid()) + { + if (isInput) + enableCallback (false); + else + snd_midi_event_free (midiParser); + + snd_seq_delete_simple_port (client->get(), portId); + } + } + + void connectWith (int sourceClient, int sourcePort) const noexcept + { + if (isInput) + snd_seq_connect_from (client->get(), portId, sourceClient, sourcePort); + else + snd_seq_connect_to (client->get(), portId, sourceClient, sourcePort); + } + + bool isValid() const noexcept + { + return client->get() != nullptr && portId >= 0; + } + + void setupInput (MidiInput* input, MidiInputCallback* cb) + { + jassert (cb != nullptr && input != nullptr); + callback = cb; + midiInput = input; + umpReceiver = nullptr; + umpConverter.reset(); + umpToBytestream = std::make_unique (2048); + } + + void setupInputUMP (MidiInput* input, ump::Receiver* receiverIn, ump::PacketProtocol protocolIn) + { + jassert (receiverIn != nullptr && input != nullptr); + callback = nullptr; + midiInput = input; + umpReceiver = receiverIn; + umpProtocol = protocolIn; + umpConverter = std::make_unique (protocolIn); + } + + void setupOutput() + { + jassert (! isInput); + snd_midi_event_new ((size_t) maxEventSize, &midiParser); + } + + void enableCallback (bool enable) + { + callbackEnabled = enable; + } + + bool sendMessageNow (const MidiMessage& message) + { + if (message.getRawDataSize() > maxEventSize) + { + maxEventSize = message.getRawDataSize(); + snd_midi_event_free (midiParser); + snd_midi_event_new ((size_t) maxEventSize, &midiParser); + } + + snd_seq_event_t event; + snd_seq_ev_clear (&event); + + auto numBytes = (long) message.getRawDataSize(); + auto* data = message.getRawData(); + + auto seqHandle = client->get(); + bool success = true; + + while (numBytes > 0) + { + auto numSent = snd_midi_event_encode (midiParser, data, numBytes, &event); + + if (numSent <= 0) + { + success = numSent == 0; + break; + } + + numBytes -= numSent; + data += numSent; + + snd_seq_ev_set_source (&event, (unsigned char) portId); + snd_seq_ev_set_subs (&event); + snd_seq_ev_set_direct (&event); + + if (snd_seq_event_output_direct (seqHandle, &event) < 0) + { + success = false; + break; + } + } + + snd_midi_event_reset_encode (midiParser); + return success; + } + + bool sendUmpMessageNow (const uint32_t* words, uint32_t numWords) + { +#if defined(SND_SEQ_EVENT_UMP) + if (words == nullptr || numWords == 0 || numWords > 4) + return false; + + snd_seq_event_t event; + snd_seq_ev_clear (&event); + event.type = 0; + event.flags |= SND_SEQ_EVENT_UMP; + + std::array payload {}; + std::memcpy (payload.data(), words, sizeof (uint32_t) * numWords); + snd_seq_ev_set_variable (&event, (unsigned int) (numWords * sizeof (uint32_t)), payload.data()); + + snd_seq_ev_set_source (&event, (unsigned char) portId); + snd_seq_ev_set_subs (&event); + snd_seq_ev_set_direct (&event); + + return snd_seq_event_output_direct (client->get(), &event) >= 0; +#else + (void) words; + (void) numWords; + return false; +#endif + } + + bool operator== (const Port& lhs) const noexcept + { + return portId != -1 && portId == lhs.portId; + } + + void createPort (const String& name, bool enableSubscription) + { + if (auto seqHandle = client->get()) + { + const unsigned int caps = + isInput ? (SND_SEQ_PORT_CAP_WRITE | (enableSubscription ? SND_SEQ_PORT_CAP_SUBS_WRITE : 0)) + : (SND_SEQ_PORT_CAP_READ | (enableSubscription ? SND_SEQ_PORT_CAP_SUBS_READ : 0)); + + portName = name; + portId = snd_seq_create_simple_port (seqHandle, portName.toUTF8(), caps, SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION); + } + } + + void handleIncomingMidiMessage (const MidiMessage& message) const + { + if (! callbackEnabled) + return; + + if (callback != nullptr) + { + callback->handleIncomingMidiMessage (midiInput, message); + return; + } + + if (umpReceiver != nullptr && umpConverter != nullptr) + { + const auto timestamp = message.getTimeStamp(); + umpConverter->convert (ump::BytestreamMidiView (&message), [&] (const ump::View& view) + { + umpReceiver->packetReceived (view, timestamp); + }); + } + } + + void handlePartialSysexMessage (const uint8* messageData, int numBytesSoFar, double timeStamp) + { + if (callbackEnabled) + callback->handlePartialSysexMessage (midiInput, messageData, numBytesSoFar, timeStamp); + } + + void handleIncomingUmpPacket (const uint32_t* words, uint32_t numWords, double timeStamp) const + { + if (! callbackEnabled || words == nullptr || numWords == 0) + return; + + const ump::View view (words); + + if (umpReceiver != nullptr) + { + umpReceiver->packetReceived (view, timeStamp); + return; + } + + if (callback != nullptr && umpToBytestream != nullptr) + { + umpToBytestream->convert (view, timeStamp, [&] (const MidiMessage& message) + { + callback->handleIncomingMidiMessage (midiInput, message); + }); + } + } + + int getPortId() const { return portId; } + + const String& getPortName() const { return portName; } + + private: + const std::shared_ptr client = AlsaClient::getInstance(); + + MidiInputCallback* callback = nullptr; + snd_midi_event_t* midiParser = nullptr; + MidiInput* midiInput = nullptr; + ump::Receiver* umpReceiver = nullptr; + ump::PacketProtocol umpProtocol = ump::PacketProtocol::MIDI_1_0; + std::unique_ptr umpConverter; + std::unique_ptr umpToBytestream; + + String portName; + + int maxEventSize = 4096, portId = -1; + std::atomic callbackEnabled { false }; + bool isInput = false; + }; + + static std::shared_ptr getInstance() + { + static std::weak_ptr ptr; + + if (auto locked = ptr.lock()) + return locked; + + std::shared_ptr result (new AlsaClient()); + ptr = result; + return result; + } + + void handleIncomingMidiMessage (snd_seq_event* event, const MidiMessage& message) + { + const ScopedLock sl (callbackLock); + + if (auto* port = findPort (event->dest.port)) + port->handleIncomingMidiMessage (message); + } + + void handlePartialSysexMessage (snd_seq_event* event, const uint8* messageData, int numBytesSoFar, double timeStamp) + { + const ScopedLock sl (callbackLock); + + if (auto* port = findPort (event->dest.port)) + port->handlePartialSysexMessage (messageData, numBytesSoFar, timeStamp); + } + + snd_seq_t* get() const noexcept { return handle; } + + int getId() const noexcept { return clientId; } + + Port* createPort (const String& name, bool forInput, bool enableSubscription) + { + const ScopedLock sl (callbackLock); + + auto port = new Port (forInput); + port->createPort (name, enableSubscription); + + const auto iter = lowerBound (port->getPortId()); + jassert (iter == ports.end() || port->getPortId() < (*iter)->getPortId()); + ports.insert (iter, rawToUniquePtr (port)); + + return port; + } + + void deletePort (Port* port) + { + const ScopedLock sl (callbackLock); + + if (const auto iter = findPortIterator (port->getPortId()); iter != ports.end()) + ports.erase (iter); + } + +private: + AlsaClient() + { + snd_seq_open (&handle, "default", SND_SEQ_OPEN_DUPLEX, 0); + + if (handle != nullptr) + { + snd_seq_nonblock (handle, SND_SEQ_NONBLOCK); + snd_seq_set_client_name (handle, getAlsaMidiName().toRawUTF8()); + clientId = snd_seq_client_id (handle); + + // It's good idea to pre-allocate a good number of elements + ports.reserve (32); + + announcementsIn = snd_seq_create_simple_port (handle, + TRANS ("announcements").toRawUTF8(), + SND_SEQ_PORT_CAP_WRITE, + SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION); + snd_seq_connect_from (handle, announcementsIn, SND_SEQ_CLIENT_SYSTEM, SND_SEQ_PORT_SYSTEM_ANNOUNCE); + + inputThread.emplace (*this); + } + } + + Port* findPort (int portId) + { + if (const auto iter = findPortIterator (portId); iter != ports.end()) + return iter->get(); + + return nullptr; + } + + snd_seq_t* handle = nullptr; + int clientId = 0; + int announcementsIn = 0; + bool umpConfigured = false; + std::vector> ports; + Atomic activeCallbacks; + CriticalSection callbackLock; + + //============================================================================== + class SequencerThread + { + public: + explicit SequencerThread (AlsaClient& c) + : client (c) + { + } + + ~SequencerThread() noexcept + { + shouldStop = true; + thread.join(); + } + + private: + // If we directly call MidiDeviceListConnectionBroadcaster::get() from the background thread, + // there's a possibility that we'll deadlock in the following scenario: + // - The main thread calls MidiDeviceListConnectionBroadcaster::get() for the first time + // (e.g. to register a listener). The static MidiDeviceListConnectionBroadcaster singleton + // begins construction. During the constructor, an AlsaClient is created to iterate midi + // ins/outs. + // - The AlsaClient starts a new SequencerThread. If connections are updated, the + // SequencerThread may call MidiDeviceListConnectionBroadcaster::get().notify() + // while the MidiDeviceListConnectionBroadcaster singleton is still being created. + // - The SequencerThread blocks until the MidiDeviceListConnectionBroadcaster has been + // created on the main thread, but the MidiDeviceListConnectionBroadcaster's constructor + // can't complete until the AlsaClient's destructor has run, which in turn requires the + // SequencerThread to join. + class UpdateNotifier final : private AsyncUpdater + { + public: + ~UpdateNotifier() override { cancelPendingUpdate(); } + + using AsyncUpdater::triggerAsyncUpdate; + + private: + void handleAsyncUpdate() override { MidiDeviceListConnectionBroadcaster::get().notify(); } + }; + + AlsaClient& client; + MidiDataConcatenator concatenator { 2048 }; + std::atomic shouldStop { false }; + UpdateNotifier notifier; + std::thread thread { [this] + { + Thread::setCurrentThreadName ("YUP MIDI Input"); + + auto seqHandle = client.get(); + + const int maxEventSize = 16 * 1024; + snd_midi_event_t* midiParser; + + if (snd_midi_event_new (maxEventSize, &midiParser) >= 0) + { + const ScopeGuard freeMidiEvent { [&] + { + snd_midi_event_free (midiParser); + } }; + + const auto numPfds = snd_seq_poll_descriptors_count (seqHandle, POLLIN); + std::vector pfd (static_cast (numPfds)); + snd_seq_poll_descriptors (seqHandle, pfd.data(), (unsigned int) numPfds, POLLIN); + + std::vector buffer (maxEventSize); + + while (! shouldStop) + { + // This timeout shouldn't be too long, so that the program can exit in a timely manner + if (poll (pfd.data(), (nfds_t) numPfds, 100) > 0) + { + if (shouldStop) + break; + + do + { + snd_seq_event_t* inputEvent = nullptr; + + if (snd_seq_event_input (seqHandle, &inputEvent) >= 0) + { + const ScopeGuard freeInputEvent { [&] + { + snd_seq_free_event (inputEvent); + } }; + + constexpr int systemEvents[] { + SND_SEQ_EVENT_CLIENT_CHANGE, + SND_SEQ_EVENT_CLIENT_START, + SND_SEQ_EVENT_CLIENT_EXIT, + SND_SEQ_EVENT_PORT_CHANGE, + SND_SEQ_EVENT_PORT_START, + SND_SEQ_EVENT_PORT_EXIT, + SND_SEQ_EVENT_PORT_SUBSCRIBED, + SND_SEQ_EVENT_PORT_UNSUBSCRIBED, + }; + + const auto foundEvent = std::find (std::begin (systemEvents), + std::end (systemEvents), + inputEvent->type); + + if (foundEvent != std::end (systemEvents)) + { + notifier.triggerAsyncUpdate(); + continue; + } + +#if defined(SND_SEQ_EVENT_UMP) + if ((inputEvent->flags & SND_SEQ_EVENT_UMP) != 0) + { + const auto timeStamp = Time::getMillisecondCounter() * 0.001; + const auto* dataPtr = static_cast (inputEvent->data.ext.ptr); + const auto dataLen = inputEvent->data.ext.len; + + if (dataPtr != nullptr && dataLen >= 4) + { + std::array words {}; + const auto bytesToCopy = jmin ((size_t) dataLen, sizeof (words)); + std::memcpy (words.data(), dataPtr, bytesToCopy); + + if (auto* port = client.findPort (inputEvent->dest.port)) + port->handleIncomingUmpPacket (words.data(), + static_cast (bytesToCopy / sizeof (uint32_t)), + timeStamp); + } + + continue; + } +#endif + + // xxx what about SYSEXes that are too big for the buffer? + const auto numBytes = snd_midi_event_decode (midiParser, + buffer.data(), + maxEventSize, + inputEvent); + + snd_midi_event_reset_decode (midiParser); + + concatenator.pushMidiData (buffer.data(), (int) numBytes, Time::getMillisecondCounter() * 0.001, inputEvent, client); + } + } while (snd_seq_event_input_pending (seqHandle, 0) > 0); + } + } + } + } }; + }; + + std::optional inputThread; +}; + +//============================================================================== +static String getFormattedPortIdentifier (int clientId, int portId) +{ + return String (clientId) + "-" + String (portId); +} + +static AlsaClient::Port* iterateMidiClient (AlsaClient& client, + snd_seq_client_info_t* clientInfo, + bool forInput, + Array& devices, + const String& deviceIdentifierToOpen) +{ + AlsaClient::Port* port = nullptr; + + auto seqHandle = client.get(); + snd_seq_port_info_t* portInfo = nullptr; + + snd_seq_port_info_alloca (&portInfo); + jassert (portInfo != nullptr); + auto numPorts = snd_seq_client_info_get_num_ports (clientInfo); + auto sourceClient = snd_seq_client_info_get_client (clientInfo); + + snd_seq_port_info_set_client (portInfo, sourceClient); + snd_seq_port_info_set_port (portInfo, -1); + + while (--numPorts >= 0) + { + if (snd_seq_query_next_port (seqHandle, portInfo) == 0 + && (snd_seq_port_info_get_capability (portInfo) + & (forInput ? SND_SEQ_PORT_CAP_SUBS_READ : SND_SEQ_PORT_CAP_SUBS_WRITE)) + != 0) + { + String portName (snd_seq_port_info_get_name (portInfo)); + auto portID = snd_seq_port_info_get_port (portInfo); + + const auto baseIdentifier = getFormattedPortIdentifier (sourceClient, portID); + MidiDeviceInfo device (portName, baseIdentifier); + devices.add (device); + +#if defined(SND_SEQ_PORT_TYPE_MIDI_UMP) + if ((snd_seq_port_info_get_type (portInfo) & SND_SEQ_PORT_TYPE_MIDI_UMP) != 0) + { + devices.add (MidiDeviceInfo { portName, + getUmpSequencerIdentifier (baseIdentifier), + ump::PacketProtocol::MIDI_2_0, + true }); + } +#endif + + if (deviceIdentifierToOpen.isNotEmpty() && deviceIdentifierToOpen == device.identifier) + { + if (portID != -1) + { + port = client.createPort (portName, forInput, false); + jassert (port->isValid()); + port->connectWith (sourceClient, portID); + break; + } + } + } + } + + return port; +} + +static AlsaClient::Port* iterateMidiDevices (bool forInput, + Array& devices, + const String& deviceIdentifierToOpen) +{ + AlsaClient::Port* port = nullptr; + auto client = AlsaClient::getInstance(); + + if (auto seqHandle = client->get()) + { + snd_seq_system_info_t* systemInfo = nullptr; + snd_seq_client_info_t* clientInfo = nullptr; + + snd_seq_system_info_alloca (&systemInfo); + jassert (systemInfo != nullptr); + + if (snd_seq_system_info (seqHandle, systemInfo) == 0) + { + snd_seq_client_info_alloca (&clientInfo); + jassert (clientInfo != nullptr); + + auto numClients = snd_seq_system_info_get_cur_clients (systemInfo); + + while (--numClients >= 0) + { + if (snd_seq_query_next_client (seqHandle, clientInfo) == 0) + { + port = iterateMidiClient (*client, + clientInfo, + forInput, + devices, + deviceIdentifierToOpen); + + if (port != nullptr) + break; + } + } + } + } + + return port; +} + +struct AlsaPortPtr +{ + explicit AlsaPortPtr (AlsaClient::Port* p) + : ptr (p) + { + } + + virtual ~AlsaPortPtr() noexcept { AlsaClient::getInstance()->deletePort (ptr); } + + AlsaClient::Port* ptr = nullptr; +}; + +//============================================================================== +class MidiInput::Pimpl +{ +public: + void configureForUmp() + { +#if defined(SND_SEQ_EVENT_UMP) + if (! umpConfigured && handle != nullptr) + { + snd_seq_set_client_midi_version (handle, 2); +#if defined(snd_seq_set_client_ump_conversion) + snd_seq_set_client_ump_conversion (handle, 1); +#endif + umpConfigured = true; + } +#endif + } + + virtual ~Pimpl() = default; + virtual void start() = 0; + virtual void stop() = 0; +}; + +class SequencerInputPimpl final : public MidiInput::Pimpl + , public AlsaPortPtr +{ +public: + explicit SequencerInputPimpl (AlsaClient::Port* p, std::unique_ptr cb) + : AlsaPortPtr (p) + , callback (std::move (cb)) + { + } + + void start() override + { + ptr->enableCallback (true); + } + + void stop() override + { + ptr->enableCallback (false); + } + +private: + std::unique_ptr callback; +}; + +class UmpRawmidiInputPimpl final : public MidiInput::Pimpl +{ +public: + UmpRawmidiInputPimpl (int fdIn, std::unique_ptr handlerIn) + : fd (fdIn) + , handler (std::move (handlerIn)) + { + startThread(); + } + +#if YUP_HAS_ALSA_UMP + UmpRawmidiInputPimpl (snd_ump_t* handleIn, std::unique_ptr handlerIn) + : umpHandle (handleIn) + , handler (std::move (handlerIn)) + { + startThread(); + } +#endif + + ~UmpRawmidiInputPimpl() override + { + shouldStop = true; + if (thread.joinable()) + thread.join(); + + if (fd >= 0) + ::close (fd); +#if YUP_HAS_ALSA_UMP + if (umpHandle != nullptr) + snd_ump_close (umpHandle); +#endif + } + + void start() override + { + callbackEnabled = true; + handler->reset(); + } + + void stop() override + { + callbackEnabled = false; + } + +private: + void startThread() + { + thread = std::thread ([this] + { + Thread::setCurrentThreadName ("YUP UMP RawMIDI Input"); + + std::array buffer {}; + +#if YUP_HAS_ALSA_UMP + if (umpHandle != nullptr) + { + const auto numPfds = snd_ump_poll_descriptors_count (umpHandle); + std::vector pfds ((size_t) numPfds); + snd_ump_poll_descriptors (umpHandle, pfds.data(), (unsigned int) numPfds); + + while (! shouldStop) + { + if (poll (pfds.data(), (nfds_t) pfds.size(), 100) <= 0) + continue; + + unsigned short revents = 0; + snd_ump_poll_descriptors_revents (umpHandle, pfds.data(), (unsigned int) pfds.size(), &revents); + if ((revents & POLLIN) == 0) + continue; + + const auto bytesRead = snd_ump_read (umpHandle, buffer.data(), buffer.size() * sizeof (uint32_t)); + if (bytesRead <= 0) + continue; + + const auto words = static_cast (bytesRead / sizeof (uint32_t)); + if (words == 0 || ! callbackEnabled) + continue; + + const auto now = Time::getMillisecondCounterHiRes() * 0.001; + handler->pushMidiData (buffer.data(), buffer.data() + words, now); + } + + return; + } +#endif + + pollfd pfd { fd, POLLIN, 0 }; + + while (! shouldStop) + { + if (poll (&pfd, 1, 100) <= 0) + continue; + + if ((pfd.revents & POLLIN) == 0) + continue; + + const auto bytesRead = ::read (fd, buffer.data(), buffer.size() * sizeof (uint32_t)); + if (bytesRead <= 0) + continue; + + const auto words = static_cast (bytesRead / sizeof (uint32_t)); + if (words == 0 || ! callbackEnabled) + continue; + + const auto now = Time::getMillisecondCounterHiRes() * 0.001; + handler->pushMidiData (buffer.data(), buffer.data() + words, now); + } + }); + } + + int fd = -1; +#if YUP_HAS_ALSA_UMP + snd_ump_t* umpHandle = nullptr; +#endif + std::unique_ptr handler; + std::atomic shouldStop { false }; + std::atomic callbackEnabled { false }; + std::thread thread; +}; + +class UmpReceiverCallback final : public MidiInputCallback +{ +public: + UmpReceiverCallback (ump::PacketProtocol protocolIn, ump::Receiver& receiverIn) + : receiver (receiverIn) + , converter (protocolIn) + { + } + + void handleIncomingMidiMessage (MidiInput*, const MidiMessage& message) override + { + const auto timestamp = message.getTimeStamp(); + converter.convert (ump::BytestreamMidiView (&message), [&] (const ump::View& view) + { + receiver.packetReceived (view, timestamp); + }); + } + + void handlePartialSysexMessage (MidiInput*, const uint8*, int, double) override {} + +private: + ump::Receiver& receiver; + ump::GenericUMPConverter converter; +}; + +class UmpReceiverCallback final : public MidiInputCallback +{ +public: + UmpReceiverCallback (ump::PacketProtocol protocolIn, ump::Receiver& receiverIn) + : receiver (receiverIn) + , converter (protocolIn) + { + } + + void handleIncomingMidiMessage (MidiInput*, const MidiMessage& message) override + { + const auto timestamp = message.getTimeStamp(); + converter.convert (ump::BytestreamMidiView (&message), [&] (const ump::View& view) + { + receiver.packetReceived (view, timestamp); + }); + } + + void handlePartialSysexMessage (MidiInput*, const uint8*, int, double) override {} + +private: + ump::Receiver& receiver; + ump::GenericUMPConverter converter; +}; + +Array MidiInput::getAvailableDevices() +{ + Array devices; + for (const auto& node : findUmpRawmidiNodes()) + { + const auto path = node.getFullPathName(); + devices.add (MidiDeviceInfo { "UMP " + node.getFileName(), + getUmpRawmidiIdentifier (path), + ump::PacketProtocol::MIDI_2_0, + true }); + } + iterateMidiDevices (true, devices, {}); + + return devices; +} + +MidiDeviceInfo MidiInput::getDefaultDevice() +{ + return getAvailableDevices().getFirst(); +} + +std::unique_ptr MidiInput::openDevice (const String& deviceIdentifier, MidiInputCallback* callback) +{ + if (deviceIdentifier.isEmpty()) + return {}; + + String identifierToOpen = deviceIdentifier; + if (isUmpSequencerIdentifier (identifierToOpen)) + identifierToOpen = identifierToOpen.fromFirstOccurrenceOf (umpSequencerPrefix, false, false); + + if (isUmpRawmidiIdentifier (identifierToOpen)) + { + if (callback == nullptr) + return {}; + + const auto path = getUmpRawmidiPath (identifierToOpen); + int fd = -1; +#if YUP_HAS_ALSA_UMP + snd_ump_t* umpHandle = nullptr; + const auto umpName = getUmpAlsaDeviceName (path); + if (umpName.isNotEmpty() + && snd_ump_open (&umpHandle, nullptr, umpName.toRawUTF8(), SND_RAWMIDI_NONBLOCK) >= 0) + { + snd_ump_nonblock (umpHandle, 1); + } + else + { + umpHandle = nullptr; + } +#endif + +#if YUP_HAS_ALSA_UMP + if (umpHandle == nullptr) + fd = ::open (path.toRawUTF8(), O_RDONLY | O_NONBLOCK); +#else + fd = ::open (path.toRawUTF8(), O_RDONLY | O_NONBLOCK); +#endif + + if (fd < 0 +#if YUP_HAS_ALSA_UMP + && umpHandle == nullptr +#endif + ) + return {}; + + std::unique_ptr midiInput (new MidiInput (File (path).getFileName(), + deviceIdentifier, + ump::PacketProtocol::MIDI_1_0)); + + auto handler = std::make_unique (*midiInput, *callback); +#if YUP_HAS_ALSA_UMP + if (umpHandle != nullptr) + midiInput->internal.reset (new UmpRawmidiInputPimpl (umpHandle, std::move (handler))); + else +#endif + midiInput->internal.reset (new UmpRawmidiInputPimpl (fd, std::move (handler))); + return midiInput; + } + + Array devices; + auto* port = iterateMidiDevices (true, devices, identifierToOpen); + + if (port == nullptr || ! port->isValid()) + return {}; + + jassert (port->isValid()); + + std::unique_ptr midiInput (new MidiInput (port->getPortName(), + deviceIdentifier, + ump::PacketProtocol::MIDI_1_0)); + + port->setupInput (midiInput.get(), callback); + midiInput->internal.reset (new SequencerInputPimpl (port, nullptr)); + + return midiInput; +} + +std::unique_ptr MidiInput::openDevice (const String& deviceIdentifier, + ump::PacketProtocol protocol, + ump::Receiver* receiver) +{ + if (deviceIdentifier.isEmpty() || receiver == nullptr) + return {}; + + String identifierToOpen = deviceIdentifier; + if (isUmpSequencerIdentifier (identifierToOpen)) + identifierToOpen = identifierToOpen.fromFirstOccurrenceOf (umpSequencerPrefix, false, false); + + if (isUmpRawmidiIdentifier (identifierToOpen)) + { + const auto path = getUmpRawmidiPath (identifierToOpen); + int fd = -1; +#if YUP_HAS_ALSA_UMP + snd_ump_t* umpHandle = nullptr; + const auto umpName = getUmpAlsaDeviceName (path); + if (umpName.isNotEmpty() + && snd_ump_open (&umpHandle, nullptr, umpName.toRawUTF8(), SND_RAWMIDI_NONBLOCK) >= 0) + { + snd_ump_nonblock (umpHandle, 1); + } + else + { + umpHandle = nullptr; + } +#endif + +#if YUP_HAS_ALSA_UMP + if (umpHandle == nullptr) + fd = ::open (path.toRawUTF8(), O_RDONLY | O_NONBLOCK); +#else + fd = ::open (path.toRawUTF8(), O_RDONLY | O_NONBLOCK); +#endif + + if (fd < 0 +#if YUP_HAS_ALSA_UMP + && umpHandle == nullptr +#endif + ) + return {}; + + std::unique_ptr midiInput (new MidiInput (File (path).getFileName(), + deviceIdentifier, + protocol)); + + auto handler = std::make_unique (protocol, *receiver); +#if YUP_HAS_ALSA_UMP + if (umpHandle != nullptr) + midiInput->internal.reset (new UmpRawmidiInputPimpl (umpHandle, std::move (handler))); + else +#endif + midiInput->internal.reset (new UmpRawmidiInputPimpl (fd, std::move (handler))); + return midiInput; + } + + Array devices; + auto* port = iterateMidiDevices (true, devices, identifierToOpen); + + if (port == nullptr || ! port->isValid()) + return {}; + + if (deviceIdentifier.startsWith (umpSequencerPrefix)) + AlsaClient::getInstance()->configureForUmp(); + + std::unique_ptr midiInput (new MidiInput (port->getPortName(), + deviceIdentifier, + protocol)); + + if (deviceIdentifier.startsWith (umpSequencerPrefix)) + { + port->setupInputUMP (midiInput.get(), receiver, protocol); + midiInput->internal.reset (new SequencerInputPimpl (port, nullptr)); + } + else + { + auto callback = std::make_unique (protocol, *receiver); + port->setupInput (midiInput.get(), callback.get()); + midiInput->internal.reset (new SequencerInputPimpl (port, std::move (callback))); + } + return midiInput; +} + +std::unique_ptr MidiInput::createNewDevice (const String& deviceName, MidiInputCallback* callback) +{ + auto client = AlsaClient::getInstance(); + auto* port = client->createPort (deviceName, true, true); + + if (port == nullptr || ! port->isValid()) + return {}; + + std::unique_ptr midiInput (new MidiInput (deviceName, + getFormattedPortIdentifier (client->getId(), port->getPortId()), + ump::PacketProtocol::MIDI_1_0)); + + port->setupInput (midiInput.get(), callback); + midiInput->internal.reset (new SequencerInputPimpl (port, nullptr)); + + return midiInput; +} + +std::unique_ptr MidiInput::createNewDevice (const String&, + ump::PacketProtocol, + ump::Receiver*) +{ + jassertfalse; + return {}; +} + +MidiInput::MidiInput (const String& deviceName, + const String& deviceIdentifier, + ump::PacketProtocol protocol) + : deviceInfo (deviceName, deviceIdentifier, protocol) +{ +} + +MidiInput::~MidiInput() +{ + stop(); +} + +void MidiInput::start() +{ + internal->start(); +} + +void MidiInput::stop() +{ + internal->stop(); +} + +//============================================================================== +class MidiOutput::Pimpl +{ +public: + virtual ~Pimpl() = default; + virtual void sendMessageNow (const MidiMessage& message) = 0; + virtual void sendMessageNow (const ump::View& message) = 0; +}; + +class SequencerOutputPimpl final : public AlsaPortPtr + , public MidiOutput::Pimpl +{ +public: + explicit SequencerOutputPimpl (AlsaClient::Port* p) + : AlsaPortPtr (p) + { + } + + void sendMessageNow (const MidiMessage& message) override + { + ptr->sendMessageNow (message); + } + + void sendMessageNow (const ump::View& message) override + { + ump::ToBytestreamConverter converter { 2048 }; + converter.convert (message, 0.0, [&] (const MidiMessage& midiMessage) + { + ptr->sendMessageNow (midiMessage); + }); + } +}; + +class UmpSequencerOutputPimpl final : public AlsaPortPtr + , public MidiOutput::Pimpl +{ +public: + explicit UmpSequencerOutputPimpl (AlsaClient::Port* p, ump::PacketProtocol protocolIn) + : AlsaPortPtr (p) + , converter (protocolIn) + { + } + + void sendMessageNow (const MidiMessage& message) override + { + converter.convert (ump::BytestreamMidiView (&message), [&] (const ump::View& view) + { + ptr->sendUmpMessageNow (view.data(), view.size()); + }); + } + + void sendMessageNow (const ump::View& message) override + { + ptr->sendUmpMessageNow (message.data(), message.size()); + } + +private: + ump::GenericUMPConverter converter; +}; + +class UmpRawmidiOutputPimpl final : public MidiOutput::Pimpl +{ +public: + UmpRawmidiOutputPimpl (int fdIn, ump::PacketProtocol protocolIn) + : fd (fdIn) + , converter (protocolIn) + { + } + +#if YUP_HAS_ALSA_UMP + UmpRawmidiOutputPimpl (snd_ump_t* handleIn, ump::PacketProtocol protocolIn) + : umpHandle (handleIn) + , converter (protocolIn) + { + } +#endif + + ~UmpRawmidiOutputPimpl() override + { + if (fd >= 0) + ::close (fd); +#if YUP_HAS_ALSA_UMP + if (umpHandle != nullptr) + snd_ump_close (umpHandle); +#endif + } + + void sendMessageNow (const MidiMessage& message) override + { + converter.convert (ump::BytestreamMidiView (&message), [&] (const ump::View& view) + { + writeWords (view.data(), view.size()); + }); + } + + void sendMessageNow (const ump::View& message) override + { + writeWords (message.data(), message.size()); + } + +private: + void writeWords (const uint32_t* data, uint32_t numWords) const + { + if (fd < 0 || data == nullptr || numWords == 0) + { +#if YUP_HAS_ALSA_UMP + if (umpHandle == nullptr) + return; +#else + return; +#endif + } + +#if YUP_HAS_ALSA_UMP + if (umpHandle != nullptr) + { + const auto totalBytes = static_cast (numWords) * sizeof (uint32_t); + snd_ump_write (umpHandle, data, totalBytes); + return; + } +#endif + + const auto totalBytes = static_cast (numWords) * sizeof (uint32_t); + const auto* bytes = reinterpret_cast (data); + size_t offset = 0; + + while (offset < totalBytes) + { + const auto written = ::write (fd, bytes + offset, totalBytes - offset); + if (written <= 0) + break; + + offset += static_cast (written); + } + } + + int fd = -1; +#if YUP_HAS_ALSA_UMP + snd_ump_t* umpHandle = nullptr; +#endif + ump::GenericUMPConverter converter; +}; + +Array MidiOutput::getAvailableDevices() +{ + Array devices; + for (const auto& node : findUmpRawmidiNodes()) + { + const auto path = node.getFullPathName(); + devices.add (MidiDeviceInfo { "UMP " + node.getFileName(), + getUmpRawmidiIdentifier (path), + ump::PacketProtocol::MIDI_2_0, + true }); + } + iterateMidiDevices (false, devices, {}); + + return devices; +} + +MidiDeviceInfo MidiOutput::getDefaultDevice() +{ + return getAvailableDevices().getFirst(); +} + +std::unique_ptr MidiOutput::openDevice (const String& deviceIdentifier) +{ + if (deviceIdentifier.isEmpty()) + return {}; + + if (isUmpRawmidiIdentifier (deviceIdentifier)) + return openDevice (deviceIdentifier, ump::PacketProtocol::MIDI_2_0); + if (isUmpSequencerIdentifier (deviceIdentifier)) + return openDevice (deviceIdentifier, ump::PacketProtocol::MIDI_2_0); + + Array devices; + auto* port = iterateMidiDevices (false, devices, deviceIdentifier); + + if (port == nullptr || ! port->isValid()) + return {}; + + std::unique_ptr midiOutput (new MidiOutput (port->getPortName(), + deviceIdentifier, + ump::PacketProtocol::MIDI_1_0)); + + port->setupOutput(); + midiOutput->internal.reset (new SequencerOutputPimpl (port)); + + return midiOutput; +} + +std::unique_ptr MidiOutput::openDevice (const String& deviceIdentifier, + ump::PacketProtocol protocol) +{ + if (deviceIdentifier.isEmpty()) + return {}; + + String identifierToOpen = deviceIdentifier; + if (isUmpSequencerIdentifier (identifierToOpen)) + identifierToOpen = identifierToOpen.fromFirstOccurrenceOf (umpSequencerPrefix, false, false); + + if (isUmpRawmidiIdentifier (identifierToOpen)) + { + const auto path = getUmpRawmidiPath (identifierToOpen); + int fd = -1; +#if YUP_HAS_ALSA_UMP + snd_ump_t* umpHandle = nullptr; + const auto umpName = getUmpAlsaDeviceName (path); + if (umpName.isNotEmpty() + && snd_ump_open (nullptr, &umpHandle, umpName.toRawUTF8(), SND_RAWMIDI_NONBLOCK) >= 0) + { + snd_ump_nonblock (umpHandle, 1); + } + else + { + umpHandle = nullptr; + } +#endif + +#if YUP_HAS_ALSA_UMP + if (umpHandle == nullptr) + fd = ::open (path.toRawUTF8(), O_WRONLY | O_NONBLOCK); +#else + fd = ::open (path.toRawUTF8(), O_WRONLY | O_NONBLOCK); +#endif + + if (fd < 0 +#if YUP_HAS_ALSA_UMP + && umpHandle == nullptr +#endif + ) + return {}; + + std::unique_ptr midiOutput (new MidiOutput (File (path).getFileName(), + deviceIdentifier, + protocol)); + +#if YUP_HAS_ALSA_UMP + if (umpHandle != nullptr) + midiOutput->internal.reset (new UmpRawmidiOutputPimpl (umpHandle, protocol)); + else +#endif + midiOutput->internal.reset (new UmpRawmidiOutputPimpl (fd, protocol)); + return midiOutput; + } + + if (deviceIdentifier.startsWith (umpSequencerPrefix)) + { +#if defined(SND_SEQ_EVENT_UMP) + Array devices; + auto* port = iterateMidiDevices (false, devices, identifierToOpen); + + if (port == nullptr || ! port->isValid()) + return {}; + + AlsaClient::getInstance()->configureForUmp(); + + std::unique_ptr midiOutput (new MidiOutput (port->getPortName(), + deviceIdentifier, + protocol)); + + port->setupOutput(); + midiOutput->internal.reset (new UmpSequencerOutputPimpl (port, protocol)); + return midiOutput; +#else + return {}; +#endif + } + + if (protocol != ump::PacketProtocol::MIDI_1_0) + return {}; + + return openDevice (deviceIdentifier); +} + +std::unique_ptr MidiOutput::createNewDevice (const String& deviceName) +{ + auto client = AlsaClient::getInstance(); + auto* port = client->createPort (deviceName, false, true); + + if (port == nullptr || ! port->isValid()) + return {}; + + std::unique_ptr midiOutput (new MidiOutput (deviceName, + getFormattedPortIdentifier (client->getId(), port->getPortId()), + ump::PacketProtocol::MIDI_1_0)); + + port->setupOutput(); + midiOutput->internal.reset (new SequencerOutputPimpl (port)); + + return midiOutput; +} + +std::unique_ptr MidiOutput::createNewDevice (const String& deviceName, + ump::PacketProtocol protocol) +{ + if (protocol != ump::PacketProtocol::MIDI_1_0) + return {}; + + return createNewDevice (deviceName); +} + +MidiOutput::~MidiOutput() +{ + stopBackgroundThread(); +} + +void MidiOutput::sendMessageNow (const MidiMessage& message) +{ + internal->sendMessageNow (message); +} + +void MidiOutput::sendMessageNow (const ump::View& message) +{ + internal->sendMessageNow (message); +} + +void MidiOutput::sendMessageNow (const ump::Packets& packets) +{ + for (auto it = packets.cbegin(); it != packets.cend(); ++it) + sendMessageNow (*it); +} + +MidiDeviceListConnection MidiDeviceListConnection::make (std::function cb) +{ + auto& broadcaster = MidiDeviceListConnectionBroadcaster::get(); + // We capture the AlsaClient instance here to ensure that it remains alive for at least as long + // as the MidiDeviceListConnection. This is necessary because system change messages will only + // be processed when the AlsaClient's SequencerThread is running. + return { &broadcaster, broadcaster.add ([fn = std::move (cb), client = AlsaClient::getInstance()] + { + NullCheckedInvocation::invoke (fn); + }) }; +} + +//============================================================================== +#else + +class MidiInput::Pimpl +{ +}; + +// (These are just stub functions if ALSA is unavailable...) +MidiInput::MidiInput (const String& deviceName, + const String& deviceID, + ump::PacketProtocol protocol) + : deviceInfo (deviceName, deviceID, protocol) +{ +} + +MidiInput::~MidiInput() {} + +void MidiInput::start() {} + +void MidiInput::stop() {} + +Array MidiInput::getAvailableDevices() { return {}; } + +MidiDeviceInfo MidiInput::getDefaultDevice() { return {}; } + +std::unique_ptr MidiInput::openDevice (const String&, MidiInputCallback*) { return {}; } + +std::unique_ptr MidiInput::openDevice (const String&, + ump::PacketProtocol, + ump::Receiver*) +{ + return {}; +} + +std::unique_ptr MidiInput::createNewDevice (const String&, MidiInputCallback*) { return {}; } + +std::unique_ptr MidiInput::createNewDevice (const String&, + ump::PacketProtocol, + ump::Receiver*) +{ + return {}; +} + +class MidiOutput::Pimpl +{ +}; + +MidiOutput::~MidiOutput() {} + +void MidiOutput::sendMessageNow (const MidiMessage&) {} + +void MidiOutput::sendMessageNow (const ump::View&) {} + +void MidiOutput::sendMessageNow (const ump::Packets&) {} + +Array MidiOutput::getAvailableDevices() { return {}; } + +MidiDeviceInfo MidiOutput::getDefaultDevice() { return {}; } + +std::unique_ptr MidiOutput::openDevice (const String&) { return {}; } + +std::unique_ptr MidiOutput::openDevice (const String&, ump::PacketProtocol) { return {}; } + +std::unique_ptr MidiOutput::createNewDevice (const String&) { return {}; } + +std::unique_ptr MidiOutput::createNewDevice (const String&, ump::PacketProtocol) { return {}; } + +MidiDeviceListConnection MidiDeviceListConnection::make (std::function cb) +{ + auto& broadcaster = MidiDeviceListConnectionBroadcaster::get(); + return { &broadcaster, broadcaster.add (std::move (cb)) }; +} + +#endif + +} // namespace yup From 4de9600bafb65dfba2dc2b17b87b32784ad021dc Mon Sep 17 00:00:00 2001 From: kunitoki Date: Tue, 23 Dec 2025 16:24:14 +0100 Subject: [PATCH 13/20] Fix tests --- tests/yup_audio_basics/yup_UMPJitterReductionTimestamps.cpp | 6 ++++-- tests/yup_audio_basics/yup_UMPMidi1ByteStream.cpp | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/yup_audio_basics/yup_UMPJitterReductionTimestamps.cpp b/tests/yup_audio_basics/yup_UMPJitterReductionTimestamps.cpp index a92c3775d..b6bc7c16c 100644 --- a/tests/yup_audio_basics/yup_UMPJitterReductionTimestamps.cpp +++ b/tests/yup_audio_basics/yup_UMPJitterReductionTimestamps.cpp @@ -135,7 +135,8 @@ TEST (JitterReductionTimestampTests, JitterClockFollowerInitialState) JitterClockFollower follower; follower.reset(); - EXPECT_EQ (follower.getSecurityOffset().count(), 2); + auto offsetMs = std::chrono::duration_cast (follower.getSecurityOffset()); + EXPECT_EQ (offsetMs.count(), 2); } TEST (JitterReductionTimestampTests, JitterClockFollowerFirstClock) @@ -295,5 +296,6 @@ TEST (JitterReductionTimestampTests, JitterClockFollowerResetClearsState) follower.reset(); - EXPECT_EQ (follower.getSecurityOffset().count(), 2); + auto offsetMs = std::chrono::duration_cast (follower.getSecurityOffset()); + EXPECT_EQ (offsetMs.count(), 2); } diff --git a/tests/yup_audio_basics/yup_UMPMidi1ByteStream.cpp b/tests/yup_audio_basics/yup_UMPMidi1ByteStream.cpp index 15daa114e..0a5e0f514 100644 --- a/tests/yup_audio_basics/yup_UMPMidi1ByteStream.cpp +++ b/tests/yup_audio_basics/yup_UMPMidi1ByteStream.cpp @@ -282,7 +282,7 @@ TEST (Midi1ByteStreamParserTests, SysExWithCallback) ASSERT_EQ (receivedSysEx.size(), 1u); EXPECT_EQ (receivedSysEx[0].manufacturerId, 0x7e0000u); - EXPECT_EQ (receivedSysEx[0].data.size(), 2u); + EXPECT_EQ (receivedSysEx[0].data.size(), 3u); } TEST (Midi1ByteStreamParserTests, SysExWithCallbackThreeByteManufacturerId) From 86bb910830e045780ca133f0d853310da5de7b5d Mon Sep 17 00:00:00 2001 From: kunitoki Date: Tue, 23 Dec 2025 16:52:33 +0100 Subject: [PATCH 14/20] More fixes --- .../native/yup_Midi_linux.cpp | 140 ++++++------------ .../yup_audio_devices/yup_audio_devices.cpp | 19 +++ thirdparty/rive/rive.cpp | 15 +- 3 files changed, 76 insertions(+), 98 deletions(-) diff --git a/modules/yup_audio_devices/native/yup_Midi_linux.cpp b/modules/yup_audio_devices/native/yup_Midi_linux.cpp index 1e03a6bf1..fd5e04950 100644 --- a/modules/yup_audio_devices/native/yup_Midi_linux.cpp +++ b/modules/yup_audio_devices/native/yup_Midi_linux.cpp @@ -1,58 +1,41 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com - - YUP is an open source library subject to 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. - - YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== - - This file is part of the JUCE library. - Copyright (c) 2022 - Raw Material Software Limited - - JUCE is an open source library subject to commercial or open-source - licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - To use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ +/* + ============================================================================== -#include -#include -#include -#include -#include + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com -#if defined(__has_include) -#if __has_include() -#include -#define YUP_HAS_ALSA_UMP 1 -#endif -#endif + YUP is an open source library subject to open-source licensing. -#ifndef YUP_HAS_ALSA_UMP -#define YUP_HAS_ALSA_UMP 0 -#endif + 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ namespace yup { @@ -432,6 +415,20 @@ class AlsaClient int getId() const noexcept { return clientId; } + void configureForUmp() + { +#if defined(SND_SEQ_EVENT_UMP) + if (! umpConfigured && handle != nullptr) + { + snd_seq_set_client_midi_version (handle, 2); +#if defined(snd_seq_set_client_ump_conversion) + snd_seq_set_client_ump_conversion (handle, 1); +#endif + umpConfigured = true; + } +#endif + } + Port* createPort (const String& name, bool forInput, bool enableSubscription) { const ScopedLock sl (callbackLock); @@ -766,20 +763,6 @@ struct AlsaPortPtr class MidiInput::Pimpl { public: - void configureForUmp() - { -#if defined(SND_SEQ_EVENT_UMP) - if (! umpConfigured && handle != nullptr) - { - snd_seq_set_client_midi_version (handle, 2); -#if defined(snd_seq_set_client_ump_conversion) - snd_seq_set_client_ump_conversion (handle, 1); -#endif - umpConfigured = true; - } -#endif - } - virtual ~Pimpl() = default; virtual void start() = 0; virtual void stop() = 0; @@ -954,31 +937,6 @@ class UmpReceiverCallback final : public MidiInputCallback ump::GenericUMPConverter converter; }; -class UmpReceiverCallback final : public MidiInputCallback -{ -public: - UmpReceiverCallback (ump::PacketProtocol protocolIn, ump::Receiver& receiverIn) - : receiver (receiverIn) - , converter (protocolIn) - { - } - - void handleIncomingMidiMessage (MidiInput*, const MidiMessage& message) override - { - const auto timestamp = message.getTimeStamp(); - converter.convert (ump::BytestreamMidiView (&message), [&] (const ump::View& view) - { - receiver.packetReceived (view, timestamp); - }); - } - - void handlePartialSysexMessage (MidiInput*, const uint8*, int, double) override {} - -private: - ump::Receiver& receiver; - ump::GenericUMPConverter converter; -}; - Array MidiInput::getAvailableDevices() { Array devices; diff --git a/modules/yup_audio_devices/yup_audio_devices.cpp b/modules/yup_audio_devices/yup_audio_devices.cpp index 73a83c92e..9310b98c5 100644 --- a/modules/yup_audio_devices/yup_audio_devices.cpp +++ b/modules/yup_audio_devices/yup_audio_devices.cpp @@ -184,6 +184,15 @@ YUP_END_IGNORE_WARNINGS_GCC_LIKE #include "native/yup_ALSA_linux.cpp" #endif +#if defined(__has_include) && __has_include() +#include +#define YUP_HAS_ALSA_UMP 1 +#endif + +#ifndef YUP_HAS_ALSA_UMP +#define YUP_HAS_ALSA_UMP 0 +#endif + #if (YUP_LINUX && YUP_BELA) /* Got an include error here? If so, you've either not got the bela headers installed, or you've not got your paths set up correctly to find its header @@ -198,6 +207,12 @@ YUP_END_IGNORE_WARNINGS_GCC_LIKE #undef SIZEOF #if ! YUP_BELA +#include +#include +#include +#include +#include + #include "native/yup_Midi_linux.cpp" #endif @@ -249,6 +264,7 @@ RealtimeThreadFactory getAndroidRealtimeThreadFactory() { return nullptr; } } // namespace yup #endif +//============================================================================== #elif YUP_WASM #if YUP_EMSCRIPTEN #include @@ -261,6 +277,7 @@ RealtimeThreadFactory getAndroidRealtimeThreadFactory() { return nullptr; } #endif +//============================================================================== #if (YUP_LINUX || YUP_BSD || YUP_MAC || YUP_WINDOWS) && YUP_JACK /* Got an include error here? If so, you've either not got jack-audio-connection-kit installed, or you've not got your paths set up correctly to find its header files. @@ -277,6 +294,7 @@ RealtimeThreadFactory getAndroidRealtimeThreadFactory() { return nullptr; } #include "native/yup_JackAudio.cpp" #endif +//============================================================================== #if ! YUP_SYSTEMAUDIOVOL_IMPLEMENTED namespace yup { @@ -309,6 +327,7 @@ bool YUP_CALLTYPE SystemAudioVolume::setMuted (bool) } // namespace yup #endif +//============================================================================== #include "audio_io/yup_AudioDeviceManager.cpp" #include "audio_io/yup_AudioIODevice.cpp" #include "audio_io/yup_AudioIODeviceType.cpp" diff --git a/thirdparty/rive/rive.cpp b/thirdparty/rive/rive.cpp index dcb1c66e0..68ae7c807 100644 --- a/thirdparty/rive/rive.cpp +++ b/thirdparty/rive/rive.cpp @@ -21,12 +21,13 @@ #include "rive.h" -#if __clang__ - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Wshorten-64-to-32" -#elif __GNUC__ +#if __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" + #pragma GCC diagnostic ignored "-Wempty-body" +#elif __clang__ + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wshorten-64-to-32" #elif _MSC_VER #pragma warning (push) #pragma warning (disable : 4244) @@ -581,10 +582,10 @@ #include "source/audio_event.cpp" #include "source/nested_artboard_leaf.cpp" -#if __clang__ - #pragma clang diagnostic pop -#elif __GNUC__ +#if __GNUC__ #pragma GCC diagnostic pop +#elif __clang__ + #pragma clang diagnostic pop #elif _MSC_VER #pragma warning (pop) #endif From a8f35e0ea90526a22ef6ea5f0c78faff6e274d12 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Tue, 23 Dec 2025 17:15:02 +0100 Subject: [PATCH 15/20] Fix includes --- modules/yup_audio_devices/yup_audio_devices.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/yup_audio_devices/yup_audio_devices.h b/modules/yup_audio_devices/yup_audio_devices.h index c4ae9e9b8..8628cb964 100644 --- a/modules/yup_audio_devices/yup_audio_devices.h +++ b/modules/yup_audio_devices/yup_audio_devices.h @@ -175,6 +175,8 @@ #include "midi_io/yup_MidiDevices.h" #include "midi_io/yup_MidiMessageCollector.h" #include "midi_io/ump/yup_UMPPacketCollector.h" +#include "midi_io/ump/yup_UMPBytestreamInputHandler.h" +#include "midi_io/ump/yup_UMPU32InputHandler.h" namespace yup { From 952f8b20cd9fefecca7b4c6fecbb4b6cbba63007 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 24 Dec 2025 10:02:07 +0100 Subject: [PATCH 16/20] Fix warnings --- examples/graphics/source/examples/Widgets.h | 2 +- .../native/yup_CoreMidi_apple.mm | 881 +++++++++--------- .../yup_audio_devices/yup_audio_devices.cpp | 6 +- .../pffft_library/pffft_library_double.c | 4 + 4 files changed, 452 insertions(+), 441 deletions(-) diff --git a/examples/graphics/source/examples/Widgets.h b/examples/graphics/source/examples/Widgets.h index 15c5d4633..620c7d59d 100644 --- a/examples/graphics/source/examples/Widgets.h +++ b/examples/graphics/source/examples/Widgets.h @@ -105,7 +105,7 @@ class WidgetsDemo : public yup::Component slider = std::make_unique (yup::Slider::Rotary, "slider"); slider->setRange (yup::Range (0.0, 100.0)); slider->setValue (50.0); - slider->onValueChanged = [this] (float value) + slider->onValueChanged = [this] (double value) { updateStatus ("Slider value: " + yup::String (value, 1)); }; diff --git a/modules/yup_audio_devices/native/yup_CoreMidi_apple.mm b/modules/yup_audio_devices/native/yup_CoreMidi_apple.mm index 04457d6f4..595f1b4c6 100644 --- a/modules/yup_audio_devices/native/yup_CoreMidi_apple.mm +++ b/modules/yup_audio_devices/native/yup_CoreMidi_apple.mm @@ -84,67 +84,67 @@ static bool checkError(OSStatus err, [[maybe_unused]] int lineNum) constexpr auto implementationStrategy = ImplementationStrategy::onlyOld; #endif -struct SenderBase -{ - virtual ~SenderBase() noexcept = default; - - virtual void send(MIDIPortRef port, - MIDIEndpointRef endpoint, - ump::PacketProtocol protocol, - const ump::BytestreamMidiView& m) = 0; - virtual void send(MIDIPortRef port, - MIDIEndpointRef endpoint, - ump::PacketProtocol protocol, - ump::Iterator b, - ump::Iterator e) = 0; -}; +struct SenderBase +{ + virtual ~SenderBase() noexcept = default; + + virtual void send(MIDIPortRef port, + MIDIEndpointRef endpoint, + ump::PacketProtocol protocol, + const ump::BytestreamMidiView& m) = 0; + virtual void send(MIDIPortRef port, + MIDIEndpointRef endpoint, + ump::PacketProtocol protocol, + ump::Iterator b, + ump::Iterator e) = 0; +}; template struct Sender; #if YUP_HAS_NEW_COREMIDI_API template <> -struct API_AVAILABLE(macos(11.0), ios(14.0)) Sender final : public SenderBase -{ - void send(MIDIPortRef port, - MIDIEndpointRef endpoint, - ump::PacketProtocol protocol, - const ump::BytestreamMidiView& m) override - { - newSendImpl(port, endpoint, protocol, m); - } - - void send(MIDIPortRef port, - MIDIEndpointRef endpoint, - ump::PacketProtocol protocol, - ump::Iterator b, - ump::Iterator e) override - { - newSendImpl(port, endpoint, protocol, b, e); - } - - private: - void newSendImpl(MIDIPortRef port, - MIDIEndpointRef endpoint, - ump::PacketProtocol protocol, - const ump::BytestreamMidiView& message) - { -#if YUP_IOS - const MIDITimeStamp timeStamp = mach_absolute_time(); -#else - const MIDITimeStamp timeStamp = AudioGetCurrentHostTime(); -#endif - - MIDIEventList stackList = {}; - MIDIEventPacket* end = nullptr; - - const auto init = [&] - { - const auto protocolId = protocol == ump::PacketProtocol::MIDI_2_0 - ? kMIDIProtocol_2_0 - : kMIDIProtocol_1_0; - end = MIDIEventListInit(&stackList, protocolId); - }; +struct API_AVAILABLE(macos(11.0), ios(14.0)) Sender final : public SenderBase +{ + void send(MIDIPortRef port, + MIDIEndpointRef endpoint, + ump::PacketProtocol protocol, + const ump::BytestreamMidiView& m) override + { + newSendImpl(port, endpoint, protocol, m); + } + + void send(MIDIPortRef port, + MIDIEndpointRef endpoint, + ump::PacketProtocol protocol, + ump::Iterator b, + ump::Iterator e) override + { + newSendImpl(port, endpoint, protocol, b, e); + } + + private: + void newSendImpl(MIDIPortRef port, + MIDIEndpointRef endpoint, + ump::PacketProtocol protocol, + const ump::BytestreamMidiView& message) + { +#if YUP_IOS + const MIDITimeStamp timeStamp = mach_absolute_time(); +#else + const MIDITimeStamp timeStamp = AudioGetCurrentHostTime(); +#endif + + MIDIEventList stackList = {}; + MIDIEventPacket* end = nullptr; + + const auto init = [&] + { + const auto protocolId = protocol == ump::PacketProtocol::MIDI_2_0 + ? kMIDIProtocol_2_0 + : kMIDIProtocol_1_0; + end = MIDIEventListInit(&stackList, protocolId); + }; const auto send = [&] { @@ -164,107 +164,107 @@ void newSendImpl(MIDIPortRef port, reinterpret_cast(view.data())); }; - init(); - - ump::GenericUMPConverter converter { protocol }; - converter.convert (message, [&] (const ump::View& view) - { - add (view); - - if (end != nullptr) - return; - - send(); - init(); - add (view); - }); - - send(); - } - - void newSendImpl(MIDIPortRef port, - MIDIEndpointRef endpoint, - ump::PacketProtocol protocol, - ump::Iterator b, - ump::Iterator e) - { -#if YUP_IOS - const MIDITimeStamp timeStamp = mach_absolute_time(); -#else - const MIDITimeStamp timeStamp = AudioGetCurrentHostTime(); -#endif - - MIDIEventList stackList = {}; - MIDIEventPacket* end = nullptr; - - const auto init = [&] - { - const auto protocolId = protocol == ump::PacketProtocol::MIDI_2_0 - ? kMIDIProtocol_2_0 - : kMIDIProtocol_1_0; - end = MIDIEventListInit(&stackList, protocolId); - }; - - 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(); - - ump::GenericUMPConverter converter { protocol }; - converter.convert (b, e, [&] (const ump::View& view) - { - add (view); - - if (end != nullptr) - return; - - send(); - init(); - add (view); - }); - - send(); - } -}; -#endif + init(); + + ump::GenericUMPConverter converter { protocol }; + converter.convert (message, [&] (const ump::View& view) + { + add (view); + + if (end != nullptr) + return; + + send(); + init(); + add (view); + }); + + send(); + } + + void newSendImpl(MIDIPortRef port, + MIDIEndpointRef endpoint, + ump::PacketProtocol protocol, + ump::Iterator b, + ump::Iterator e) + { +#if YUP_IOS + const MIDITimeStamp timeStamp = mach_absolute_time(); +#else + const MIDITimeStamp timeStamp = AudioGetCurrentHostTime(); +#endif + + MIDIEventList stackList = {}; + MIDIEventPacket* end = nullptr; + + const auto init = [&] + { + const auto protocolId = protocol == ump::PacketProtocol::MIDI_2_0 + ? kMIDIProtocol_2_0 + : kMIDIProtocol_1_0; + end = MIDIEventListInit(&stackList, protocolId); + }; + + 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(); + + ump::GenericUMPConverter converter { protocol }; + converter.convert (b, e, [&] (const ump::View& view) + { + add (view); + + if (end != nullptr) + return; + + send(); + init(); + add (view); + }); + + send(); + } +}; +#endif #if YUP_HAS_OLD_COREMIDI_API template <> -struct Sender final : public SenderBase -{ - void send(MIDIPortRef port, - MIDIEndpointRef endpoint, - [[maybe_unused]] ump::PacketProtocol protocol, - const ump::BytestreamMidiView& m) override - { - oldSendImpl(port, endpoint, m); - } - - void send(MIDIPortRef port, - MIDIEndpointRef endpoint, - [[maybe_unused]] ump::PacketProtocol protocol, - ump::Iterator b, - ump::Iterator e) override - { - std::for_each(b, e, [&](const ump::View& v) - { bytestreamConverter.convert(v, 0.0, [&](const ump::BytestreamMidiView& m) - { send(port, endpoint, protocol, m); }); }); - } +struct Sender final : public SenderBase +{ + void send(MIDIPortRef port, + MIDIEndpointRef endpoint, + [[maybe_unused]] ump::PacketProtocol protocol, + const ump::BytestreamMidiView& m) override + { + oldSendImpl(port, endpoint, m); + } + + void send(MIDIPortRef port, + MIDIEndpointRef endpoint, + [[maybe_unused]] ump::PacketProtocol protocol, + ump::Iterator b, + ump::Iterator e) override + { + std::for_each(b, e, [&](const ump::View& v) + { bytestreamConverter.convert(v, 0.0, [&](const ump::BytestreamMidiView& m) + { send(port, endpoint, protocol, m); }); }); + } private: ump::ToBytestreamConverter bytestreamConverter{2048}; @@ -335,29 +335,29 @@ void oldSendImpl(MIDIPortRef port, MIDIEndpointRef endpoint, const ump::Bytestre #if YUP_HAS_NEW_COREMIDI_API && YUP_HAS_OLD_COREMIDI_API template <> -struct Sender -{ - Sender() - : sender(makeImpl()) - { - } - - void send(MIDIPortRef port, - MIDIEndpointRef endpoint, - ump::PacketProtocol protocol, - const ump::BytestreamMidiView& m) - { - sender->send(port, endpoint, protocol, m); - } - - void send(MIDIPortRef port, - MIDIEndpointRef endpoint, - ump::PacketProtocol protocol, - ump::Iterator b, - ump::Iterator e) - { - sender->send(port, endpoint, protocol, b, e); - } +struct Sender +{ + Sender() + : sender(makeImpl()) + { + } + + void send(MIDIPortRef port, + MIDIEndpointRef endpoint, + ump::PacketProtocol protocol, + const ump::BytestreamMidiView& m) + { + sender->send(port, endpoint, protocol, m); + } + + void send(MIDIPortRef port, + MIDIEndpointRef endpoint, + ump::PacketProtocol protocol, + ump::Iterator b, + ump::Iterator e) + { + sender->send(port, endpoint, protocol, b, e); + } private: static std::unique_ptr makeImpl() @@ -432,14 +432,14 @@ Resource release() noexcept using ScopedEndpointRef = ScopedMidiResource; //============================================================================== -class MidiPortAndEndpoint -{ - public: - MidiPortAndEndpoint(ScopedPortRef p, ScopedEndpointRef ep, ump::PacketProtocol protocolIn) noexcept - : port(std::move(p)), endpoint(std::move(ep)) - , protocol(protocolIn) - { - } +class MidiPortAndEndpoint +{ + public: + MidiPortAndEndpoint(ScopedPortRef p, ScopedEndpointRef ep, ump::PacketProtocol protocolIn) noexcept + : port(std::move(p)), endpoint(std::move(ep)) + , protocol(protocolIn) + { + } ~MidiPortAndEndpoint() noexcept { @@ -448,53 +448,60 @@ Resource release() noexcept endpoint.release(); } - void send(const ump::BytestreamMidiView& m) - { - sender.send(*port, *endpoint, protocol, m); - } - - void send(ump::Iterator b, ump::Iterator e) - { - sender.send(*port, *endpoint, protocol, b, e); - } + void send(const ump::BytestreamMidiView& m) + { + sender.send(*port, *endpoint, protocol, m); + } + + void send(ump::Iterator b, ump::Iterator e) + { + sender.send(*port, *endpoint, protocol, b, e); + } bool canStop() const noexcept { return *port != 0; } void stop() const { CHECK_ERROR(MIDIPortDisconnectSource(*port, *endpoint)); } private: ScopedPortRef port; - ScopedEndpointRef endpoint; - ump::PacketProtocol protocol; - - SenderToUse sender; -}; - -static void updateProtocolInfo(MIDIObjectRef entity, MidiDeviceInfo& info) -{ -#if YUP_HAS_NEW_COREMIDI_API - SInt32 protocol = 0; - - if (MIDIObjectGetIntegerProperty(entity, kMIDIPropertyProtocolID, &protocol) == noErr) - { - if (protocol == kMIDIProtocol_2_0) - { - info.protocol = ump::PacketProtocol::MIDI_2_0; - info.supportsMidi2 = true; - } - else - { - info.protocol = ump::PacketProtocol::MIDI_1_0; - info.supportsMidi2 = false; - } - } -#else - ignoreUnused(entity, info); -#endif -} - -static MidiDeviceInfo getMidiObjectInfo(MIDIObjectRef entity) -{ - MidiDeviceInfo info; + ScopedEndpointRef endpoint; + ump::PacketProtocol protocol; + + SenderToUse sender; +}; + +static void updateProtocolInfo(MIDIObjectRef entity, MidiDeviceInfo& info) +{ +#if YUP_HAS_NEW_COREMIDI_API + if (@available(iOS 14.0, macOS 11.0, *)) + { + SInt32 protocol = 0; + + if (MIDIObjectGetIntegerProperty(entity, kMIDIPropertyProtocolID, &protocol) == noErr) + { + if (protocol == kMIDIProtocol_2_0) + { + info.protocol = ump::PacketProtocol::MIDI_2_0; + info.supportsMidi2 = true; + } + else + { + info.protocol = ump::PacketProtocol::MIDI_1_0; + info.supportsMidi2 = false; + } + } + } + else + { + ignoreUnused(entity, info); + } +#else + ignoreUnused(entity, info); +#endif +} + +static MidiDeviceInfo getMidiObjectInfo(MIDIObjectRef entity) +{ + MidiDeviceInfo info; { CFObjectHolder str; @@ -517,9 +524,9 @@ static MidiDeviceInfo getMidiObjectInfo(MIDIObjectRef entity) info.identifier = String::fromCFString(str.object); } - updateProtocolInfo(entity, info); - return info; -} + updateProtocolInfo(entity, info); + return info; +} static MidiDeviceInfo getEndpointInfo(MIDIEndpointRef endpoint, bool isExternal) { @@ -1093,54 +1100,54 @@ static CreatorFunctionPointers getCreatorFunctionPointers() return std::make_unique(midiInput, CoreMidiHelpers::ReceiverToUse(midiInput, *midiInputCallback)); } - static std::unique_ptr makeInput(const String& name, - const String& identifier, - ump::PacketProtocol protocol, - ump::Receiver& receiver) - { - using namespace CoreMidiHelpers; - - if (auto midiInput = rawToUniquePtr(new MidiInput(name, identifier, protocol))) - { - if ((midiInput->internal = makePimpl(*midiInput, protocol, receiver))) - { - const ScopedLock sl(callbackLock); - activeCallbacks.add(midiInput->internal.get()); - - return midiInput; - } - } - - return {}; - } - - static std::unique_ptr makeInput(const String& name, - const String& identifier, - ump::PacketProtocol protocol, - MidiInputCallback* midiInputCallback) - { - using namespace CoreMidiHelpers; - - if (auto midiInput = rawToUniquePtr(new MidiInput(name, identifier, protocol))) - { - if ((midiInput->internal = makePimpl(*midiInput, midiInputCallback))) - { - 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; + static std::unique_ptr makeInput(const String& name, + const String& identifier, + ump::PacketProtocol protocol, + ump::Receiver& receiver) + { + using namespace CoreMidiHelpers; + + if (auto midiInput = rawToUniquePtr(new MidiInput(name, identifier, protocol))) + { + if ((midiInput->internal = makePimpl(*midiInput, protocol, receiver))) + { + const ScopedLock sl(callbackLock); + activeCallbacks.add(midiInput->internal.get()); + + return midiInput; + } + } + + return {}; + } + + static std::unique_ptr makeInput(const String& name, + const String& identifier, + ump::PacketProtocol protocol, + MidiInputCallback* midiInputCallback) + { + using namespace CoreMidiHelpers; + + if (auto midiInput = rawToUniquePtr(new MidiInput(name, identifier, protocol))) + { + if ((midiInput->internal = makePimpl(*midiInput, midiInputCallback))) + { + 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 {}; @@ -1159,12 +1166,12 @@ static CreatorFunctionPointers getCreatorFunctionPointers() if (!CHECK_ERROR(MIDIObjectGetStringProperty(endpoint, kMIDIPropertyName, &cfName.object))) continue; - if (auto input = makeInput(endpointInfo.name, - endpointInfo.identifier, - protocol, - std::forward(args)...)) - { - MIDIPortRef port; + if (auto input = makeInput(endpointInfo.name, + endpointInfo.identifier, + protocol, + std::forward(args)...)) + { + MIDIPortRef port; if (!CHECK_ERROR(CreatorFunctionsToUse::createInputPort(protocol, client, cfName.object, input->internal.get(), &port))) continue; @@ -1174,34 +1181,34 @@ static CreatorFunctionPointers getCreatorFunctionPointers() if (!CHECK_ERROR(MIDIPortConnectSource(*scopedPort, endpoint, nullptr))) continue; - input->internal->portAndEndpoint = std::make_unique(std::move(scopedPort), - ScopedEndpointRef{endpoint}, - protocol); - return input; - } - } + input->internal->portAndEndpoint = std::make_unique(std::move(scopedPort), + ScopedEndpointRef{endpoint}, + protocol); + return input; + } + } } return {}; } template - static std::unique_ptr createDevice(ump::PacketProtocol protocol, - const String& deviceName, - Args&&... args) - { - using namespace CoreMidiHelpers; + 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), - protocol, - std::forward(args)...)) - { - MIDIEndpointRef endpoint; + if (auto input = makeInput(deviceName, + String(deviceIdentifier), + protocol, + std::forward(args)...)) + { + MIDIEndpointRef endpoint; CFUniquePtr name(deviceName.toCFString()); auto err = CreatorFunctionsToUse::createDestination(protocol, client, name.get(), input->internal.get(), &endpoint); @@ -1223,12 +1230,12 @@ static CreatorFunctionPointers getCreatorFunctionPointers() if (!CHECK_ERROR(MIDIObjectSetIntegerProperty(endpoint, kMIDIPropertyUniqueID, (SInt32)deviceIdentifier))) return {}; - input->internal->portAndEndpoint = std::make_unique(ScopedPortRef{}, - std::move(scopedEndpoint), - protocol); - return input; - } - } + input->internal->portAndEndpoint = std::make_unique(ScopedPortRef{}, + std::move(scopedEndpoint), + protocol); + return input; + } + } return {}; } @@ -1245,51 +1252,51 @@ static CreatorFunctionPointers getCreatorFunctionPointers() 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::openDevice(const String& deviceIdentifier, - ump::PacketProtocol protocol, - ump::Receiver* receiver) -{ - if (receiver == nullptr) - return nullptr; - - return Pimpl::openDevice(protocol, deviceIdentifier, *receiver); -} - -std::unique_ptr MidiInput::createNewDevice(const String& deviceName, MidiInputCallback* callback) -{ - return Pimpl::createDevice(ump::PacketProtocol::MIDI_1_0, - deviceName, - callback); -} - -std::unique_ptr MidiInput::createNewDevice(const String& deviceName, - ump::PacketProtocol protocol, - ump::Receiver* receiver) -{ - if (receiver == nullptr) - return {}; - - return Pimpl::createDevice(protocol, - deviceName, - *receiver); -} - -MidiInput::MidiInput(const String& deviceName, - const String& deviceIdentifier, - ump::PacketProtocol protocol) - : deviceInfo(deviceName, deviceIdentifier, protocol) -{ -} +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::openDevice(const String& deviceIdentifier, + ump::PacketProtocol protocol, + ump::Receiver* receiver) +{ + if (receiver == nullptr) + return nullptr; + + return Pimpl::openDevice(protocol, deviceIdentifier, *receiver); +} + +std::unique_ptr MidiInput::createNewDevice(const String& deviceName, MidiInputCallback* callback) +{ + return Pimpl::createDevice(ump::PacketProtocol::MIDI_1_0, + deviceName, + callback); +} + +std::unique_ptr MidiInput::createNewDevice(const String& deviceName, + ump::PacketProtocol protocol, + ump::Receiver* receiver) +{ + if (receiver == nullptr) + return {}; + + return Pimpl::createDevice(protocol, + deviceName, + *receiver); +} + +MidiInput::MidiInput(const String& deviceName, + const String& deviceIdentifier, + ump::PacketProtocol protocol) + : deviceInfo(deviceName, deviceIdentifier, protocol) +{ +} MidiInput::~MidiInput() = default; @@ -1322,70 +1329,70 @@ static CreatorFunctionPointers getCreatorFunctionPointers() return getAvailableDevices().getFirst(); } -std::unique_ptr MidiOutput::openDevice(const String& deviceIdentifier) -{ - return openDevice(deviceIdentifier, ump::PacketProtocol::MIDI_1_0); -} - -std::unique_ptr MidiOutput::openDevice(const String& deviceIdentifier, - ump::PacketProtocol protocol) -{ - 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; - - CFObjectHolder cfName; - - if (!CHECK_ERROR(MIDIObjectGetStringProperty(endpoint, kMIDIPropertyName, &cfName.object))) - continue; - - MIDIPortRef port; - - if (!CHECK_ERROR(MIDIOutputPortCreate(client, cfName.object, &port))) - continue; - - ScopedPortRef scopedPort{port}; - - auto midiOutput = rawToUniquePtr(new MidiOutput(endpointInfo.name, endpointInfo.identifier, protocol)); - midiOutput->internal = std::make_unique(std::move(scopedPort), - ScopedEndpointRef{endpoint}, - protocol); - - return midiOutput; - } - } - - return {}; -} - -std::unique_ptr MidiOutput::createNewDevice(const String& deviceName) -{ - return createNewDevice(deviceName, ump::PacketProtocol::MIDI_1_0); -} - -std::unique_ptr MidiOutput::createNewDevice(const String& deviceName, - ump::PacketProtocol protocol) -{ - using namespace CoreMidiHelpers; - - if (auto client = getGlobalMidiClient()) - { - MIDIEndpointRef endpoint; - - CFUniquePtr name(deviceName.toCFString()); - - auto err = CreatorFunctionsToUse::createSource(protocol, client, name.get(), &endpoint); - ScopedEndpointRef scopedEndpoint{endpoint}; +std::unique_ptr MidiOutput::openDevice(const String& deviceIdentifier) +{ + return openDevice(deviceIdentifier, ump::PacketProtocol::MIDI_1_0); +} + +std::unique_ptr MidiOutput::openDevice(const String& deviceIdentifier, + ump::PacketProtocol protocol) +{ + 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; + + CFObjectHolder cfName; + + if (!CHECK_ERROR(MIDIObjectGetStringProperty(endpoint, kMIDIPropertyName, &cfName.object))) + continue; + + MIDIPortRef port; + + if (!CHECK_ERROR(MIDIOutputPortCreate(client, cfName.object, &port))) + continue; + + ScopedPortRef scopedPort{port}; + + auto midiOutput = rawToUniquePtr(new MidiOutput(endpointInfo.name, endpointInfo.identifier, protocol)); + midiOutput->internal = std::make_unique(std::move(scopedPort), + ScopedEndpointRef{endpoint}, + protocol); + + return midiOutput; + } + } + + return {}; +} + +std::unique_ptr MidiOutput::createNewDevice(const String& deviceName) +{ + return createNewDevice(deviceName, ump::PacketProtocol::MIDI_1_0); +} + +std::unique_ptr MidiOutput::createNewDevice(const String& deviceName, + ump::PacketProtocol protocol) +{ + using namespace CoreMidiHelpers; + + if (auto client = getGlobalMidiClient()) + { + MIDIEndpointRef endpoint; + + CFUniquePtr name(deviceName.toCFString()); + + auto err = CreatorFunctionsToUse::createSource(protocol, client, name.get(), &endpoint); + ScopedEndpointRef scopedEndpoint{endpoint}; #if YUP_IOS if (err == kMIDINotPermitted) @@ -1405,13 +1412,13 @@ static CreatorFunctionPointers getCreatorFunctionPointers() if (!CHECK_ERROR(MIDIObjectSetIntegerProperty(*scopedEndpoint, kMIDIPropertyUniqueID, (SInt32)deviceIdentifier))) return {}; - auto midiOutput = rawToUniquePtr(new MidiOutput(deviceName, String(deviceIdentifier), protocol)); - midiOutput->internal = std::make_unique(ScopedPortRef{}, - std::move(scopedEndpoint), - protocol); - - return midiOutput; - } + auto midiOutput = rawToUniquePtr(new MidiOutput(deviceName, String(deviceIdentifier), protocol)); + midiOutput->internal = std::make_unique(ScopedPortRef{}, + std::move(scopedEndpoint), + protocol); + + return midiOutput; + } return {}; } @@ -1421,22 +1428,22 @@ static CreatorFunctionPointers getCreatorFunctionPointers() stopBackgroundThread(); } -void MidiOutput::sendMessageNow(const MidiMessage& message) -{ - internal->send(ump::BytestreamMidiView(&message)); -} - -void MidiOutput::sendMessageNow(const ump::View& message) -{ - auto begin = ump::Iterator (message.data(), message.size() * sizeof (uint32_t)); - auto end = ump::Iterator (message.data() + message.size(), 0); - internal->send (begin, end); -} - -void MidiOutput::sendMessageNow (const ump::Packets& packets) -{ - internal->send (packets.cbegin(), packets.cend()); -} +void MidiOutput::sendMessageNow(const MidiMessage& message) +{ + internal->send(ump::BytestreamMidiView(&message)); +} + +void MidiOutput::sendMessageNow(const ump::View& message) +{ + auto begin = ump::Iterator (message.data(), message.size() * sizeof (uint32_t)); + auto end = ump::Iterator (message.data() + message.size(), 0); + internal->send (begin, end); +} + +void MidiOutput::sendMessageNow (const ump::Packets& packets) +{ + internal->send (packets.cbegin(), packets.cend()); +} MidiDeviceListConnection MidiDeviceListConnection::make(std::function cb) { diff --git a/modules/yup_audio_devices/yup_audio_devices.cpp b/modules/yup_audio_devices/yup_audio_devices.cpp index a41411181..a13442172 100644 --- a/modules/yup_audio_devices/yup_audio_devices.cpp +++ b/modules/yup_audio_devices/yup_audio_devices.cpp @@ -62,11 +62,12 @@ #include "yup_audio_devices.h" -#include "audio_io/yup_SampleRateHelpers.cpp" -#include "midi_io/yup_MidiDevices.cpp" #include "midi_io/ump/yup_UMPBytestreamInputHandler.h" #include "midi_io/ump/yup_UMPU32InputHandler.h" +#include "midi_io/yup_MidiDevices.cpp" +#include "audio_io/yup_SampleRateHelpers.cpp" + //============================================================================== #if YUP_MAC || YUP_IOS #include @@ -225,7 +226,6 @@ RealtimeThreadFactory getAndroidRealtimeThreadFactory(); } // namespace yup #include "native/yup_Audio_android.cpp" - #include "native/yup_Midi_android.cpp" #if YUP_USE_ANDROID_OPENSLES || YUP_USE_ANDROID_OBOE diff --git a/thirdparty/pffft_library/pffft_library_double.c b/thirdparty/pffft_library/pffft_library_double.c index 1f9f350db..bd8c1ee5d 100644 --- a/thirdparty/pffft_library/pffft_library_double.c +++ b/thirdparty/pffft_library/pffft_library_double.c @@ -21,4 +21,8 @@ #include "pffft_library.h" +#ifdef _USE_MATH_DEFINES +#undef _USE_MATH_DEFINES +#endif + #include "upstream/pffft_double.c" From 7ccbc2bd6f60819e12be24c0be50768c3cb10802 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 24 Dec 2025 10:20:06 +0100 Subject: [PATCH 17/20] Fix warnings in tests --- modules/yup_audio_devices/yup_audio_devices.cpp | 3 --- modules/yup_python/scripting/yup_ScriptUtilities.h | 2 +- tests/yup_audio_basics/yup_AudioPlayHead.cpp | 3 ++- tests/yup_audio_basics/yup_BufferingAudioSource.cpp | 3 ++- tests/yup_audio_basics/yup_ChannelRemappingAudioSource.cpp | 3 ++- tests/yup_audio_basics/yup_MidiKeyboardState.cpp | 3 ++- tests/yup_audio_basics/yup_ResamplingAudioSource.cpp | 3 ++- tests/yup_audio_basics/yup_ReverbAudioSource.cpp | 3 ++- tests/yup_audio_basics/yup_Synthesiser.cpp | 4 ++-- tests/yup_core/yup_ListenerList.cpp | 1 + tests/yup_gui/yup_Component.cpp | 5 +++++ 11 files changed, 21 insertions(+), 12 deletions(-) diff --git a/modules/yup_audio_devices/yup_audio_devices.cpp b/modules/yup_audio_devices/yup_audio_devices.cpp index a13442172..927453357 100644 --- a/modules/yup_audio_devices/yup_audio_devices.cpp +++ b/modules/yup_audio_devices/yup_audio_devices.cpp @@ -62,9 +62,6 @@ #include "yup_audio_devices.h" -#include "midi_io/ump/yup_UMPBytestreamInputHandler.h" -#include "midi_io/ump/yup_UMPU32InputHandler.h" - #include "midi_io/yup_MidiDevices.cpp" #include "audio_io/yup_SampleRateHelpers.cpp" diff --git a/modules/yup_python/scripting/yup_ScriptUtilities.h b/modules/yup_python/scripting/yup_ScriptUtilities.h index 74e5e81c3..13f7f8171 100644 --- a/modules/yup_python/scripting/yup_ScriptUtilities.h +++ b/modules/yup_python/scripting/yup_ScriptUtilities.h @@ -55,7 +55,7 @@ std::optional python_cast (const pybind11::object& value) @param scriptEngine The script engine to redirect the streams to. */ -struct YUP_API ScriptStreamRedirection +struct ScriptStreamRedirection { ScriptStreamRedirection() noexcept; ~ScriptStreamRedirection() noexcept; diff --git a/tests/yup_audio_basics/yup_AudioPlayHead.cpp b/tests/yup_audio_basics/yup_AudioPlayHead.cpp index e31ff7422..ea27fcbd8 100644 --- a/tests/yup_audio_basics/yup_AudioPlayHead.cpp +++ b/tests/yup_audio_basics/yup_AudioPlayHead.cpp @@ -49,7 +49,6 @@ class TestAudioPlayHead : public AudioPlayHead private: std::optional testPosition; }; -} // namespace class AudioPlayHeadTests : public ::testing::Test { @@ -62,6 +61,8 @@ class AudioPlayHeadTests : public ::testing::Test std::unique_ptr playHead; }; +} // namespace + TEST_F (AudioPlayHeadTests, DefaultTransportControlMethodsExist) { // Test that default implementations exist and don't crash diff --git a/tests/yup_audio_basics/yup_BufferingAudioSource.cpp b/tests/yup_audio_basics/yup_BufferingAudioSource.cpp index 05eb16710..d4f67ac87 100644 --- a/tests/yup_audio_basics/yup_BufferingAudioSource.cpp +++ b/tests/yup_audio_basics/yup_BufferingAudioSource.cpp @@ -104,7 +104,6 @@ class MockPositionableAudioSource : public PositionableAudioSource int64 currentPosition; bool looping; }; -} // namespace //============================================================================== class BufferingAudioSourceTests : public ::testing::Test @@ -131,6 +130,8 @@ class BufferingAudioSourceTests : public ::testing::Test std::unique_ptr buffering; }; +} // namespace + //============================================================================== TEST_F (BufferingAudioSourceTests, Constructor) { diff --git a/tests/yup_audio_basics/yup_ChannelRemappingAudioSource.cpp b/tests/yup_audio_basics/yup_ChannelRemappingAudioSource.cpp index 13620ec9a..43b07a087 100644 --- a/tests/yup_audio_basics/yup_ChannelRemappingAudioSource.cpp +++ b/tests/yup_audio_basics/yup_ChannelRemappingAudioSource.cpp @@ -67,7 +67,6 @@ class MockAudioSource : public AudioSource int lastSamplesPerBlock = 0; double lastSampleRate = 0.0; }; -} // namespace //============================================================================== class ChannelRemappingAudioSourceTests : public ::testing::Test @@ -88,6 +87,8 @@ class ChannelRemappingAudioSourceTests : public ::testing::Test std::unique_ptr remapper; }; +} // namespace + //============================================================================== TEST_F (ChannelRemappingAudioSourceTests, Constructor) { diff --git a/tests/yup_audio_basics/yup_MidiKeyboardState.cpp b/tests/yup_audio_basics/yup_MidiKeyboardState.cpp index 0e45092b1..dfaa70e40 100644 --- a/tests/yup_audio_basics/yup_MidiKeyboardState.cpp +++ b/tests/yup_audio_basics/yup_MidiKeyboardState.cpp @@ -57,7 +57,6 @@ class TestListener : public MidiKeyboardState::Listener std::vector noteOnCalls; std::vector noteOffCalls; }; -} // namespace //============================================================================== class MidiKeyboardStateTests : public ::testing::Test @@ -79,6 +78,8 @@ class MidiKeyboardStateTests : public ::testing::Test std::unique_ptr listener; }; +} // namespace + //============================================================================== // Constructor and Reset Tests //============================================================================== diff --git a/tests/yup_audio_basics/yup_ResamplingAudioSource.cpp b/tests/yup_audio_basics/yup_ResamplingAudioSource.cpp index de2600945..4513c06bb 100644 --- a/tests/yup_audio_basics/yup_ResamplingAudioSource.cpp +++ b/tests/yup_audio_basics/yup_ResamplingAudioSource.cpp @@ -67,7 +67,6 @@ class MockResamplingAudioSource : public AudioSource int lastSamplesPerBlock = 0; double lastSampleRate = 0.0; }; -} // namespace //============================================================================== class ResamplingAudioSourceTests : public ::testing::Test @@ -88,6 +87,8 @@ class ResamplingAudioSourceTests : public ::testing::Test std::unique_ptr resampler; }; +} // namespace + //============================================================================== TEST_F (ResamplingAudioSourceTests, Constructor) { diff --git a/tests/yup_audio_basics/yup_ReverbAudioSource.cpp b/tests/yup_audio_basics/yup_ReverbAudioSource.cpp index 6acfc2dee..cc5fc36d7 100644 --- a/tests/yup_audio_basics/yup_ReverbAudioSource.cpp +++ b/tests/yup_audio_basics/yup_ReverbAudioSource.cpp @@ -67,7 +67,6 @@ class MockReverbAudioSource : public AudioSource double lastSampleRate = 0.0; float fillValue = 0.5f; }; -} // namespace //============================================================================== class ReverbAudioSourceTests : public ::testing::Test @@ -88,6 +87,8 @@ class ReverbAudioSourceTests : public ::testing::Test std::unique_ptr reverbSource; }; +} // namespace + //============================================================================== TEST_F (ReverbAudioSourceTests, ConstructorWithDeleteInput) { diff --git a/tests/yup_audio_basics/yup_Synthesiser.cpp b/tests/yup_audio_basics/yup_Synthesiser.cpp index 61b43c7f9..2e2b96bb6 100644 --- a/tests/yup_audio_basics/yup_Synthesiser.cpp +++ b/tests/yup_audio_basics/yup_Synthesiser.cpp @@ -204,8 +204,6 @@ class TestVoice : public SynthesiserVoice SynthesiserSound* currentSound = nullptr; }; -} // namespace - class SynthesiserTest : public ::testing::Test { protected: @@ -223,6 +221,8 @@ class SynthesiserTest : public ::testing::Test std::unique_ptr synth; }; +} // namespace + TEST_F (SynthesiserTest, DefaultConstruction) { Synthesiser synthesiser; diff --git a/tests/yup_core/yup_ListenerList.cpp b/tests/yup_core/yup_ListenerList.cpp index c0b3fb7a5..9a7328f16 100644 --- a/tests/yup_core/yup_ListenerList.cpp +++ b/tests/yup_core/yup_ListenerList.cpp @@ -46,6 +46,7 @@ using namespace yup; namespace { + class MockListener { public: diff --git a/tests/yup_gui/yup_Component.cpp b/tests/yup_gui/yup_Component.cpp index 374ebb6ea..63a0ae7bc 100644 --- a/tests/yup_gui/yup_Component.cpp +++ b/tests/yup_gui/yup_Component.cpp @@ -996,6 +996,9 @@ TEST_F (ComponentTest, OpaqueStateWithMultipleChildren) // Tests for missing Component methods using ComponentMock // ============================================================================= +namespace +{ + class ComponentMockTest : public ::testing::Test { protected: @@ -1008,6 +1011,8 @@ class ComponentMockTest : public ::testing::Test std::unique_ptr mockComponent; }; +} // namespace + // ============================================================================= TEST_F (ComponentMockTest, VirtualMethodCallbacks) From df1d005022aedb02fd7b6ad742ec6d218d5f0d0c Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 24 Dec 2025 12:16:15 +0100 Subject: [PATCH 18/20] Fix more warnings --- tests/yup_audio_basics.cpp | 11 ++++++++++- tests/yup_core.cpp | 11 ++++++++++- tests/yup_gui.cpp | 11 ++++++++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/tests/yup_audio_basics.cpp b/tests/yup_audio_basics.cpp index 103c8f775..0330633a5 100644 --- a/tests/yup_audio_basics.cpp +++ b/tests/yup_audio_basics.cpp @@ -1,3 +1,8 @@ +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsubobject-linkage" +#endif + #include "yup_audio_basics/yup_ADSR.cpp" #include "yup_audio_basics/yup_AudioChannelSet.cpp" #include "yup_audio_basics/yup_AudioDataConverters.cpp" @@ -49,4 +54,8 @@ #include "yup_audio_basics/yup_UMPSysExCollectors.cpp" #include "yup_audio_basics/yup_UMPTypes.cpp" #include "yup_audio_basics/yup_UMPUniversalPacket.cpp" -#include "yup_audio_basics/yup_UMPUniversalSysEx.cpp" \ No newline at end of file +#include "yup_audio_basics/yup_UMPUniversalSysEx.cpp" + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif \ No newline at end of file diff --git a/tests/yup_core.cpp b/tests/yup_core.cpp index f9d4b5fb3..9a00f7813 100644 --- a/tests/yup_core.cpp +++ b/tests/yup_core.cpp @@ -1,3 +1,8 @@ +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsubobject-linkage" +#endif + #include "yup_core/yup_AbstractFifo.cpp" #include "yup_core/yup_ArrayBase.cpp" #include "yup_core/yup_Atomic.cpp" @@ -83,4 +88,8 @@ #include "yup_core/yup_WebInputStream.cpp" #include "yup_core/yup_XmlDocument.cpp" #include "yup_core/yup_XmlElement.cpp" -#include "yup_core/yup_ZipFile.cpp" \ No newline at end of file +#include "yup_core/yup_ZipFile.cpp" + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif \ No newline at end of file diff --git a/tests/yup_gui.cpp b/tests/yup_gui.cpp index 2903d7301..adc235c01 100644 --- a/tests/yup_gui.cpp +++ b/tests/yup_gui.cpp @@ -1,3 +1,8 @@ +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsubobject-linkage" +#endif + #include "yup_gui/yup_ComboBox.cpp" #include "yup_gui/yup_Component.cpp" #include "yup_gui/yup_FileChooser.cpp" @@ -7,4 +12,8 @@ #include "yup_gui/yup_SwitchButton.cpp" #include "yup_gui/yup_TextButton.cpp" #include "yup_gui/yup_TextEditor.cpp" -#include "yup_gui/yup_ToggleButton.cpp" \ No newline at end of file +#include "yup_gui/yup_ToggleButton.cpp" + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif \ No newline at end of file From 775e5faf6f9f24f45d300128770b94d3dec29cd3 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 24 Dec 2025 12:28:15 +0100 Subject: [PATCH 19/20] More warning disable --- modules/yup_python/yup_python.h | 4 ++++ tests/yup_audio_basics.cpp | 3 ++- tests/yup_core.cpp | 3 ++- tests/yup_gui.cpp | 3 ++- thirdparty/harfbuzz/harfbuzz.h | 11 +++++++++++ 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/modules/yup_python/yup_python.h b/modules/yup_python/yup_python.h index 2ee178c4b..2e0788cf0 100644 --- a/modules/yup_python/yup_python.h +++ b/modules/yup_python/yup_python.h @@ -108,6 +108,8 @@ static inline constexpr const char* const PythonModuleName = YUP_PYTHON_STRINGIF //============================================================================== +YUP_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wattributes") + #include "scripting/yup_ScriptException.h" #include "scripting/yup_ScriptEngine.h" #include "scripting/yup_ScriptBindings.h" @@ -115,3 +117,5 @@ static inline constexpr const char* const PythonModuleName = YUP_PYTHON_STRINGIF #include "utilities/yup_ClassDemangling.h" #include "utilities/yup_CrashHandling.h" #include "utilities/yup_PythonInterop.h" + +YUP_END_IGNORE_WARNINGS_GCC_LIKE diff --git a/tests/yup_audio_basics.cpp b/tests/yup_audio_basics.cpp index 0330633a5..ced4367a8 100644 --- a/tests/yup_audio_basics.cpp +++ b/tests/yup_audio_basics.cpp @@ -1,5 +1,6 @@ #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunknown-warning-option" #pragma GCC diagnostic ignored "-Wsubobject-linkage" #endif @@ -58,4 +59,4 @@ #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic pop -#endif \ No newline at end of file +#endif diff --git a/tests/yup_core.cpp b/tests/yup_core.cpp index 9a00f7813..d17dad03d 100644 --- a/tests/yup_core.cpp +++ b/tests/yup_core.cpp @@ -1,5 +1,6 @@ #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunknown-warning-option" #pragma GCC diagnostic ignored "-Wsubobject-linkage" #endif @@ -92,4 +93,4 @@ #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic pop -#endif \ No newline at end of file +#endif diff --git a/tests/yup_gui.cpp b/tests/yup_gui.cpp index adc235c01..8f9456501 100644 --- a/tests/yup_gui.cpp +++ b/tests/yup_gui.cpp @@ -1,5 +1,6 @@ #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunknown-warning-option" #pragma GCC diagnostic ignored "-Wsubobject-linkage" #endif @@ -16,4 +17,4 @@ #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic pop -#endif \ No newline at end of file +#endif diff --git a/thirdparty/harfbuzz/harfbuzz.h b/thirdparty/harfbuzz/harfbuzz.h index 303c744b6..cdbb47534 100644 --- a/thirdparty/harfbuzz/harfbuzz.h +++ b/thirdparty/harfbuzz/harfbuzz.h @@ -44,5 +44,16 @@ #pragma once +#if defined(__GNUC__) || defined(__clang__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wempty-body" + #pragma GCC diagnostic ignored "-Wunused-function" + #pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + #include "upstream/hb.h" #include "upstream/hb-ot.h" + +#if defined(__GNUC__) || defined(__clang__) + #pragma GCC diagnostic pop +#endif From 06b43fdc1b3e464d8dd9ebd5948422bf2d433a14 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 24 Dec 2025 12:33:29 +0100 Subject: [PATCH 20/20] Fix warning --- cmake/yup_utilities.cmake | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmake/yup_utilities.cmake b/cmake/yup_utilities.cmake index 07cd797cb..4c37ff822 100644 --- a/cmake/yup_utilities.cmake +++ b/cmake/yup_utilities.cmake @@ -290,7 +290,8 @@ function (_yup_setup_coverage_flags target_name) -fprofile-arcs -fprofile-update=atomic -ftest-coverage - -fno-elide-constructors) + $<$:-fno-elide-constructors> + $<$:-fno-elide-constructors>) target_link_options (${target_name} INTERFACE --coverage)