diff --git a/examples/Audio/MidiDemo.h b/examples/Audio/MidiDemo.h index 8372bafeeb..b0aaa01e23 100644 --- a/examples/Audio/MidiDemo.h +++ b/examples/Audio/MidiDemo.h @@ -87,7 +87,8 @@ struct MidiDeviceListEntry final : ReferenceCountedObject class MidiDemo final : public Component, private MidiKeyboardState::Listener, private MidiInputCallback, - private AsyncUpdater + private AsyncUpdater, + private ump::EndpointsListener { public: //============================================================================== @@ -134,13 +135,16 @@ public: setSize (732, 520); updateDeviceLists(); + updateVirtualPorts(); - if (virtualIn != nullptr) - virtualIn->start(); + ump::Endpoints::getInstance()->setVirtualMidiBytestreamServiceActive (true); + ump::Endpoints::getInstance()->addListener (*this); } ~MidiDemo() override { + ump::Endpoints::getInstance()->removeListener (*this); + midiInputs .clear(); midiOutputs.clear(); keyboardState.removeListener (this); @@ -478,6 +482,34 @@ private: updateDeviceList (isInput); } + void endpointsChanged() override + { + updateDeviceLists(); + } + + void virtualMidiServiceActiveChanged() override + { + if (ump::Endpoints::getInstance()->isVirtualMidiBytestreamServiceActive()) + { + if (virtualIn == nullptr || virtualOut == nullptr) + updateVirtualPorts(); + } + else + { + virtualIn = nullptr; + virtualOut = nullptr; + } + } + + void updateVirtualPorts() + { + virtualIn = MidiInput::createNewDevice ("MidiDemo Virtual In", this); + virtualOut = MidiOutput::createNewDevice ("MidiDemo Virtual Out"); + + if (virtualIn != nullptr) + virtualIn->start(); + } + //============================================================================== Label midiInputLabel { "Midi Input Label", "MIDI Input:" }; Label midiOutputLabel { "Midi Output Label", "MIDI Output:" }; @@ -494,13 +526,8 @@ private: CriticalSection midiMonitorLock; Array incomingMessages; - std::unique_ptr virtualIn = MidiInput::createNewDevice ("MidiDemo Virtual In", this); - std::unique_ptr virtualOut = MidiOutput::createNewDevice ("MidiDemo Virtual Out"); - - MidiDeviceListConnection connection = MidiDeviceListConnection::make ([this] - { - updateDeviceLists(); - }); + std::unique_ptr virtualIn; + std::unique_ptr virtualOut; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiDemo) diff --git a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt index 2cbfdeb78f..a1db087f16 100644 --- a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt +++ b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt @@ -110,7 +110,8 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToMidi2DefaultTranslator.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToMidi2DefaultTranslator.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPProtocols.h" - "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPReceiver.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPStringUtils.cpp" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPStringUtils.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPSysEx7.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPSysEx7.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPUtils.cpp" @@ -199,11 +200,33 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h" "../../../../../modules/juce_audio_devices/audio_io/juce_SampleRateHelpers.cpp" "../../../../../modules/juce_audio_devices/audio_io/juce_SystemAudioVolume.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPBlock.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPDisconnectionListener.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoint.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpointId.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPInput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPInput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPIOHelpers.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPSession.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPSession.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.h" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDeviceListConnectionBroadcaster.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDevices.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDevices.h" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.h" + "../../../../../modules/juce_audio_devices/midi_io/juce_ScheduledEventThread.h" + "../../../../../modules/juce_audio_devices/midi_io/juce_WaitFreeListeners.cpp" + "../../../../../modules/juce_audio_devices/midi_io/juce_WaitFreeListeners.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStream.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStreamBase.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStreamBuilder.h" @@ -347,6 +370,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_devices/native/oboe/CMakeLists.txt" "../../../../../modules/juce_audio_devices/native/oboe/README.md" "../../../../../modules/juce_audio_devices/native/juce_ALSA_linux.cpp" + "../../../../../modules/juce_audio_devices/native/juce_ALSA_weak_linux.h" "../../../../../modules/juce_audio_devices/native/juce_ASIO_windows.cpp" "../../../../../modules/juce_audio_devices/native/juce_Audio_android.cpp" "../../../../../modules/juce_audio_devices/native/juce_Audio_ios.cpp" @@ -2772,7 +2796,8 @@ set_source_files_properties( "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToMidi2DefaultTranslator.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToMidi2DefaultTranslator.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPProtocols.h" - "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPReceiver.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPStringUtils.cpp" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPStringUtils.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPSysEx7.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPSysEx7.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPUtils.cpp" @@ -2861,11 +2886,33 @@ set_source_files_properties( "../../../../../modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h" "../../../../../modules/juce_audio_devices/audio_io/juce_SampleRateHelpers.cpp" "../../../../../modules/juce_audio_devices/audio_io/juce_SystemAudioVolume.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPBlock.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPDisconnectionListener.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoint.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpointId.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPInput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPInput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPIOHelpers.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPSession.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPSession.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.h" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDeviceListConnectionBroadcaster.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDevices.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDevices.h" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.h" + "../../../../../modules/juce_audio_devices/midi_io/juce_ScheduledEventThread.h" + "../../../../../modules/juce_audio_devices/midi_io/juce_WaitFreeListeners.cpp" + "../../../../../modules/juce_audio_devices/midi_io/juce_WaitFreeListeners.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStream.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStreamBase.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStreamBuilder.h" @@ -3009,6 +3056,7 @@ set_source_files_properties( "../../../../../modules/juce_audio_devices/native/oboe/CMakeLists.txt" "../../../../../modules/juce_audio_devices/native/oboe/README.md" "../../../../../modules/juce_audio_devices/native/juce_ALSA_linux.cpp" + "../../../../../modules/juce_audio_devices/native/juce_ALSA_weak_linux.h" "../../../../../modules/juce_audio_devices/native/juce_ASIO_windows.cpp" "../../../../../modules/juce_audio_devices/native/juce_Audio_android.cpp" "../../../../../modules/juce_audio_devices/native/juce_Audio_ios.cpp" diff --git a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj index 779aec163c..eaddb790e8 100644 --- a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj @@ -213,6 +213,9 @@ true + + true + true @@ -336,6 +339,30 @@ true + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + true @@ -345,6 +372,9 @@ true + + true + true @@ -3346,7 +3376,7 @@ - + @@ -3393,8 +3423,21 @@ + + + + + + + + + + + + + @@ -3471,6 +3514,7 @@ + diff --git a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters index 3a9e49be12..f82b67d680 100644 --- a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters @@ -68,6 +68,9 @@ {BF23FC10-1D57-2A9B-706F-6DD8A7B593D4} + + {386862D5-4DCC-A4B3-5642-60A201E303EF} + {092EFC17-7C95-7E04-0ACA-0D61A462EE81} @@ -895,6 +898,9 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump @@ -1021,6 +1027,30 @@ JUCE Modules\juce_audio_devices\audio_io + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + JUCE Modules\juce_audio_devices\midi_io @@ -1030,6 +1060,9 @@ JUCE Modules\juce_audio_devices\midi_io + + JUCE Modules\juce_audio_devices\midi_io + JUCE Modules\juce_audio_devices\native\oboe\src\aaudio @@ -4311,7 +4344,7 @@ JUCE Modules\juce_audio_basics\midi\ump - + JUCE Modules\juce_audio_basics\midi\ump @@ -4452,12 +4485,51 @@ JUCE Modules\juce_audio_devices\audio_io + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + JUCE Modules\juce_audio_devices\midi_io JUCE Modules\juce_audio_devices\midi_io + + JUCE Modules\juce_audio_devices\midi_io + + + JUCE Modules\juce_audio_devices\midi_io + JUCE Modules\juce_audio_devices\native\oboe\include\oboe @@ -4686,6 +4758,9 @@ JUCE Modules\juce_audio_devices\native\oboe\src\opensles + + JUCE Modules\juce_audio_devices\native + JUCE Modules\juce_audio_devices\native diff --git a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj index 3e383642a1..f4e3773667 100644 --- a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj @@ -213,6 +213,9 @@ true + + true + true @@ -336,6 +339,30 @@ true + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + true @@ -345,6 +372,9 @@ true + + true + true @@ -3346,7 +3376,7 @@ - + @@ -3393,8 +3423,21 @@ + + + + + + + + + + + + + @@ -3471,6 +3514,7 @@ + diff --git a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters index 4d93f7970a..22c2ac6d48 100644 --- a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters @@ -68,6 +68,9 @@ {BF23FC10-1D57-2A9B-706F-6DD8A7B593D4} + + {386862D5-4DCC-A4B3-5642-60A201E303EF} + {092EFC17-7C95-7E04-0ACA-0D61A462EE81} @@ -895,6 +898,9 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump @@ -1021,6 +1027,30 @@ JUCE Modules\juce_audio_devices\audio_io + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + JUCE Modules\juce_audio_devices\midi_io @@ -1030,6 +1060,9 @@ JUCE Modules\juce_audio_devices\midi_io + + JUCE Modules\juce_audio_devices\midi_io + JUCE Modules\juce_audio_devices\native\oboe\src\aaudio @@ -4311,7 +4344,7 @@ JUCE Modules\juce_audio_basics\midi\ump - + JUCE Modules\juce_audio_basics\midi\ump @@ -4452,12 +4485,51 @@ JUCE Modules\juce_audio_devices\audio_io + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + JUCE Modules\juce_audio_devices\midi_io JUCE Modules\juce_audio_devices\midi_io + + JUCE Modules\juce_audio_devices\midi_io + + + JUCE Modules\juce_audio_devices\midi_io + JUCE Modules\juce_audio_devices\native\oboe\include\oboe @@ -4686,6 +4758,9 @@ JUCE Modules\juce_audio_devices\native\oboe\src\opensles + + JUCE Modules\juce_audio_devices\native + JUCE Modules\juce_audio_devices\native diff --git a/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt b/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt index 826ec6a7f5..581dac87d6 100644 --- a/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt +++ b/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt @@ -65,7 +65,8 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToMidi2DefaultTranslator.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToMidi2DefaultTranslator.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPProtocols.h" - "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPReceiver.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPStringUtils.cpp" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPStringUtils.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPSysEx7.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPSysEx7.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPUtils.cpp" @@ -154,11 +155,33 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h" "../../../../../modules/juce_audio_devices/audio_io/juce_SampleRateHelpers.cpp" "../../../../../modules/juce_audio_devices/audio_io/juce_SystemAudioVolume.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPBlock.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPDisconnectionListener.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoint.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpointId.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPInput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPInput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPIOHelpers.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPSession.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPSession.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.h" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDeviceListConnectionBroadcaster.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDevices.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDevices.h" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.h" + "../../../../../modules/juce_audio_devices/midi_io/juce_ScheduledEventThread.h" + "../../../../../modules/juce_audio_devices/midi_io/juce_WaitFreeListeners.cpp" + "../../../../../modules/juce_audio_devices/midi_io/juce_WaitFreeListeners.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStream.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStreamBase.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStreamBuilder.h" @@ -302,6 +325,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_devices/native/oboe/CMakeLists.txt" "../../../../../modules/juce_audio_devices/native/oboe/README.md" "../../../../../modules/juce_audio_devices/native/juce_ALSA_linux.cpp" + "../../../../../modules/juce_audio_devices/native/juce_ALSA_weak_linux.h" "../../../../../modules/juce_audio_devices/native/juce_ASIO_windows.cpp" "../../../../../modules/juce_audio_devices/native/juce_Audio_android.cpp" "../../../../../modules/juce_audio_devices/native/juce_Audio_ios.cpp" @@ -2341,7 +2365,8 @@ set_source_files_properties( "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToMidi2DefaultTranslator.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToMidi2DefaultTranslator.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPProtocols.h" - "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPReceiver.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPStringUtils.cpp" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPStringUtils.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPSysEx7.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPSysEx7.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPUtils.cpp" @@ -2430,11 +2455,33 @@ set_source_files_properties( "../../../../../modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h" "../../../../../modules/juce_audio_devices/audio_io/juce_SampleRateHelpers.cpp" "../../../../../modules/juce_audio_devices/audio_io/juce_SystemAudioVolume.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPBlock.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPDisconnectionListener.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoint.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpointId.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPInput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPInput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPIOHelpers.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPSession.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPSession.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.h" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDeviceListConnectionBroadcaster.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDevices.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDevices.h" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.h" + "../../../../../modules/juce_audio_devices/midi_io/juce_ScheduledEventThread.h" + "../../../../../modules/juce_audio_devices/midi_io/juce_WaitFreeListeners.cpp" + "../../../../../modules/juce_audio_devices/midi_io/juce_WaitFreeListeners.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStream.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStreamBase.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStreamBuilder.h" @@ -2578,6 +2625,7 @@ set_source_files_properties( "../../../../../modules/juce_audio_devices/native/oboe/CMakeLists.txt" "../../../../../modules/juce_audio_devices/native/oboe/README.md" "../../../../../modules/juce_audio_devices/native/juce_ALSA_linux.cpp" + "../../../../../modules/juce_audio_devices/native/juce_ALSA_weak_linux.h" "../../../../../modules/juce_audio_devices/native/juce_ASIO_windows.cpp" "../../../../../modules/juce_audio_devices/native/juce_Audio_android.cpp" "../../../../../modules/juce_audio_devices/native/juce_Audio_ios.cpp" diff --git a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj index 5e0b86bdc7..c634f6a293 100644 --- a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj +++ b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj @@ -171,6 +171,9 @@ true + + true + true @@ -294,6 +297,30 @@ true + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + true @@ -303,6 +330,9 @@ true + + true + true @@ -2860,7 +2890,7 @@ - + @@ -2907,8 +2937,21 @@ + + + + + + + + + + + + + @@ -2985,6 +3028,7 @@ + diff --git a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters index 5624d8b630..e4559b715c 100644 --- a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters +++ b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters @@ -41,6 +41,9 @@ {BF23FC10-1D57-2A9B-706F-6DD8A7B593D4} + + {386862D5-4DCC-A4B3-5642-60A201E303EF} + {092EFC17-7C95-7E04-0ACA-0D61A462EE81} @@ -676,6 +679,9 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump @@ -802,6 +808,30 @@ JUCE Modules\juce_audio_devices\audio_io + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + JUCE Modules\juce_audio_devices\midi_io @@ -811,6 +841,9 @@ JUCE Modules\juce_audio_devices\midi_io + + JUCE Modules\juce_audio_devices\midi_io + JUCE Modules\juce_audio_devices\native\oboe\src\aaudio @@ -3573,7 +3606,7 @@ JUCE Modules\juce_audio_basics\midi\ump - + JUCE Modules\juce_audio_basics\midi\ump @@ -3714,12 +3747,51 @@ JUCE Modules\juce_audio_devices\audio_io + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + JUCE Modules\juce_audio_devices\midi_io JUCE Modules\juce_audio_devices\midi_io + + JUCE Modules\juce_audio_devices\midi_io + + + JUCE Modules\juce_audio_devices\midi_io + JUCE Modules\juce_audio_devices\native\oboe\include\oboe @@ -3948,6 +4020,9 @@ JUCE Modules\juce_audio_devices\native\oboe\src\opensles + + JUCE Modules\juce_audio_devices\native + JUCE Modules\juce_audio_devices\native diff --git a/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt b/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt index c9a6926eec..e1229592f2 100644 --- a/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt +++ b/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt @@ -98,7 +98,8 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToMidi2DefaultTranslator.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToMidi2DefaultTranslator.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPProtocols.h" - "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPReceiver.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPStringUtils.cpp" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPStringUtils.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPSysEx7.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPSysEx7.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPUtils.cpp" @@ -187,11 +188,33 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h" "../../../../../modules/juce_audio_devices/audio_io/juce_SampleRateHelpers.cpp" "../../../../../modules/juce_audio_devices/audio_io/juce_SystemAudioVolume.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPBlock.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPDisconnectionListener.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoint.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpointId.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPInput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPInput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPIOHelpers.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPSession.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPSession.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.h" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDeviceListConnectionBroadcaster.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDevices.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDevices.h" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.h" + "../../../../../modules/juce_audio_devices/midi_io/juce_ScheduledEventThread.h" + "../../../../../modules/juce_audio_devices/midi_io/juce_WaitFreeListeners.cpp" + "../../../../../modules/juce_audio_devices/midi_io/juce_WaitFreeListeners.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStream.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStreamBase.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStreamBuilder.h" @@ -335,6 +358,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_devices/native/oboe/CMakeLists.txt" "../../../../../modules/juce_audio_devices/native/oboe/README.md" "../../../../../modules/juce_audio_devices/native/juce_ALSA_linux.cpp" + "../../../../../modules/juce_audio_devices/native/juce_ALSA_weak_linux.h" "../../../../../modules/juce_audio_devices/native/juce_ASIO_windows.cpp" "../../../../../modules/juce_audio_devices/native/juce_Audio_android.cpp" "../../../../../modules/juce_audio_devices/native/juce_Audio_ios.cpp" @@ -2527,7 +2551,8 @@ set_source_files_properties( "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToMidi2DefaultTranslator.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToMidi2DefaultTranslator.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPProtocols.h" - "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPReceiver.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPStringUtils.cpp" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPStringUtils.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPSysEx7.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPSysEx7.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPUtils.cpp" @@ -2616,11 +2641,33 @@ set_source_files_properties( "../../../../../modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h" "../../../../../modules/juce_audio_devices/audio_io/juce_SampleRateHelpers.cpp" "../../../../../modules/juce_audio_devices/audio_io/juce_SystemAudioVolume.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPBlock.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPDisconnectionListener.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoint.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpointId.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPInput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPInput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPIOHelpers.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPSession.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPSession.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.h" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDeviceListConnectionBroadcaster.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDevices.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDevices.h" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.h" + "../../../../../modules/juce_audio_devices/midi_io/juce_ScheduledEventThread.h" + "../../../../../modules/juce_audio_devices/midi_io/juce_WaitFreeListeners.cpp" + "../../../../../modules/juce_audio_devices/midi_io/juce_WaitFreeListeners.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStream.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStreamBase.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStreamBuilder.h" @@ -2764,6 +2811,7 @@ set_source_files_properties( "../../../../../modules/juce_audio_devices/native/oboe/CMakeLists.txt" "../../../../../modules/juce_audio_devices/native/oboe/README.md" "../../../../../modules/juce_audio_devices/native/juce_ALSA_linux.cpp" + "../../../../../modules/juce_audio_devices/native/juce_ALSA_weak_linux.h" "../../../../../modules/juce_audio_devices/native/juce_ASIO_windows.cpp" "../../../../../modules/juce_audio_devices/native/juce_Audio_android.cpp" "../../../../../modules/juce_audio_devices/native/juce_Audio_ios.cpp" diff --git a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj index 261e491ecd..4e9a345265 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj @@ -179,6 +179,9 @@ true + + true + true @@ -302,6 +305,30 @@ true + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + true @@ -311,6 +338,9 @@ true + + true + true @@ -3040,7 +3070,7 @@ - + @@ -3087,8 +3117,21 @@ + + + + + + + + + + + + + @@ -3165,6 +3208,7 @@ + diff --git a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters index 6b33703eb0..3786b0a7ae 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters @@ -50,6 +50,9 @@ {BF23FC10-1D57-2A9B-706F-6DD8A7B593D4} + + {386862D5-4DCC-A4B3-5642-60A201E303EF} + {092EFC17-7C95-7E04-0ACA-0D61A462EE81} @@ -751,6 +754,9 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump @@ -877,6 +883,30 @@ JUCE Modules\juce_audio_devices\audio_io + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + JUCE Modules\juce_audio_devices\midi_io @@ -886,6 +916,9 @@ JUCE Modules\juce_audio_devices\midi_io + + JUCE Modules\juce_audio_devices\midi_io + JUCE Modules\juce_audio_devices\native\oboe\src\aaudio @@ -3849,7 +3882,7 @@ JUCE Modules\juce_audio_basics\midi\ump - + JUCE Modules\juce_audio_basics\midi\ump @@ -3990,12 +4023,51 @@ JUCE Modules\juce_audio_devices\audio_io + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + JUCE Modules\juce_audio_devices\midi_io JUCE Modules\juce_audio_devices\midi_io + + JUCE Modules\juce_audio_devices\midi_io + + + JUCE Modules\juce_audio_devices\midi_io + JUCE Modules\juce_audio_devices\native\oboe\include\oboe @@ -4224,6 +4296,9 @@ JUCE Modules\juce_audio_devices\native\oboe\src\opensles + + JUCE Modules\juce_audio_devices\native + JUCE Modules\juce_audio_devices\native diff --git a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj index a42d3abc82..d12bbcb1ac 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj @@ -179,6 +179,9 @@ true + + true + true @@ -302,6 +305,30 @@ true + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + true @@ -311,6 +338,9 @@ true + + true + true @@ -3040,7 +3070,7 @@ - + @@ -3087,8 +3117,21 @@ + + + + + + + + + + + + + @@ -3165,6 +3208,7 @@ + diff --git a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters index 426485d009..e455e17612 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters @@ -50,6 +50,9 @@ {BF23FC10-1D57-2A9B-706F-6DD8A7B593D4} + + {386862D5-4DCC-A4B3-5642-60A201E303EF} + {092EFC17-7C95-7E04-0ACA-0D61A462EE81} @@ -751,6 +754,9 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump @@ -877,6 +883,30 @@ JUCE Modules\juce_audio_devices\audio_io + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + JUCE Modules\juce_audio_devices\midi_io @@ -886,6 +916,9 @@ JUCE Modules\juce_audio_devices\midi_io + + JUCE Modules\juce_audio_devices\midi_io + JUCE Modules\juce_audio_devices\native\oboe\src\aaudio @@ -3849,7 +3882,7 @@ JUCE Modules\juce_audio_basics\midi\ump - + JUCE Modules\juce_audio_basics\midi\ump @@ -3990,12 +4023,51 @@ JUCE Modules\juce_audio_devices\audio_io + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + JUCE Modules\juce_audio_devices\midi_io JUCE Modules\juce_audio_devices\midi_io + + JUCE Modules\juce_audio_devices\midi_io + + + JUCE Modules\juce_audio_devices\midi_io + JUCE Modules\juce_audio_devices\native\oboe\include\oboe @@ -4224,6 +4296,9 @@ JUCE Modules\juce_audio_devices\native\oboe\src\opensles + + JUCE Modules\juce_audio_devices\native + JUCE Modules\juce_audio_devices\native diff --git a/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt b/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt index cf2736252b..a3a004a8ea 100644 --- a/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt +++ b/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt @@ -69,7 +69,8 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToMidi2DefaultTranslator.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToMidi2DefaultTranslator.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPProtocols.h" - "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPReceiver.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPStringUtils.cpp" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPStringUtils.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPSysEx7.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPSysEx7.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPUtils.cpp" @@ -158,11 +159,33 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h" "../../../../../modules/juce_audio_devices/audio_io/juce_SampleRateHelpers.cpp" "../../../../../modules/juce_audio_devices/audio_io/juce_SystemAudioVolume.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPBlock.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPDisconnectionListener.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoint.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpointId.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPInput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPInput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPIOHelpers.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPSession.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPSession.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.h" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDeviceListConnectionBroadcaster.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDevices.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDevices.h" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.h" + "../../../../../modules/juce_audio_devices/midi_io/juce_ScheduledEventThread.h" + "../../../../../modules/juce_audio_devices/midi_io/juce_WaitFreeListeners.cpp" + "../../../../../modules/juce_audio_devices/midi_io/juce_WaitFreeListeners.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStream.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStreamBase.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStreamBuilder.h" @@ -306,6 +329,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_devices/native/oboe/CMakeLists.txt" "../../../../../modules/juce_audio_devices/native/oboe/README.md" "../../../../../modules/juce_audio_devices/native/juce_ALSA_linux.cpp" + "../../../../../modules/juce_audio_devices/native/juce_ALSA_weak_linux.h" "../../../../../modules/juce_audio_devices/native/juce_ASIO_windows.cpp" "../../../../../modules/juce_audio_devices/native/juce_Audio_android.cpp" "../../../../../modules/juce_audio_devices/native/juce_Audio_ios.cpp" @@ -2425,7 +2449,8 @@ set_source_files_properties( "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToMidi2DefaultTranslator.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPMidi1ToMidi2DefaultTranslator.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPProtocols.h" - "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPReceiver.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPStringUtils.cpp" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPStringUtils.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPSysEx7.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPSysEx7.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPUtils.cpp" @@ -2514,11 +2539,33 @@ set_source_files_properties( "../../../../../modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h" "../../../../../modules/juce_audio_devices/audio_io/juce_SampleRateHelpers.cpp" "../../../../../modules/juce_audio_devices/audio_io/juce_SystemAudioVolume.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPBlock.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPDisconnectionListener.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoint.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpointId.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPInput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPInput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPIOHelpers.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPSession.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPSession.h" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.cpp" + "../../../../../modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.h" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDeviceListConnectionBroadcaster.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDevices.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiDevices.h" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.cpp" "../../../../../modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.h" + "../../../../../modules/juce_audio_devices/midi_io/juce_ScheduledEventThread.h" + "../../../../../modules/juce_audio_devices/midi_io/juce_WaitFreeListeners.cpp" + "../../../../../modules/juce_audio_devices/midi_io/juce_WaitFreeListeners.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStream.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStreamBase.h" "../../../../../modules/juce_audio_devices/native/oboe/include/oboe/AudioStreamBuilder.h" @@ -2662,6 +2709,7 @@ set_source_files_properties( "../../../../../modules/juce_audio_devices/native/oboe/CMakeLists.txt" "../../../../../modules/juce_audio_devices/native/oboe/README.md" "../../../../../modules/juce_audio_devices/native/juce_ALSA_linux.cpp" + "../../../../../modules/juce_audio_devices/native/juce_ALSA_weak_linux.h" "../../../../../modules/juce_audio_devices/native/juce_ASIO_windows.cpp" "../../../../../modules/juce_audio_devices/native/juce_Audio_android.cpp" "../../../../../modules/juce_audio_devices/native/juce_Audio_ios.cpp" diff --git a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj index 6201fa50f5..49f25aade1 100644 --- a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj +++ b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj @@ -171,6 +171,9 @@ true + + true + true @@ -294,6 +297,30 @@ true + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + true @@ -303,6 +330,9 @@ true + + true + true @@ -2951,7 +2981,7 @@ - + @@ -2998,8 +3028,21 @@ + + + + + + + + + + + + + @@ -3076,6 +3119,7 @@ + diff --git a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters index 7386f54946..bbf625eb3d 100644 --- a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters +++ b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters @@ -41,6 +41,9 @@ {BF23FC10-1D57-2A9B-706F-6DD8A7B593D4} + + {386862D5-4DCC-A4B3-5642-60A201E303EF} + {092EFC17-7C95-7E04-0ACA-0D61A462EE81} @@ -706,6 +709,9 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump @@ -832,6 +838,30 @@ JUCE Modules\juce_audio_devices\audio_io + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + JUCE Modules\juce_audio_devices\midi_io @@ -841,6 +871,9 @@ JUCE Modules\juce_audio_devices\midi_io + + JUCE Modules\juce_audio_devices\midi_io + JUCE Modules\juce_audio_devices\native\oboe\src\aaudio @@ -3714,7 +3747,7 @@ JUCE Modules\juce_audio_basics\midi\ump - + JUCE Modules\juce_audio_basics\midi\ump @@ -3855,12 +3888,51 @@ JUCE Modules\juce_audio_devices\audio_io + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + JUCE Modules\juce_audio_devices\midi_io JUCE Modules\juce_audio_devices\midi_io + + JUCE Modules\juce_audio_devices\midi_io + + + JUCE Modules\juce_audio_devices\midi_io + JUCE Modules\juce_audio_devices\native\oboe\include\oboe @@ -4089,6 +4161,9 @@ JUCE Modules\juce_audio_devices\native\oboe\src\opensles + + JUCE Modules\juce_audio_devices\native + JUCE Modules\juce_audio_devices\native diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h index a26c0d849d..e4d5e48023 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h @@ -2699,7 +2699,7 @@ protected: inline static const NuGetPackage webviewPackage { "Microsoft.Web.WebView2", "1.0.1901.177", false }; inline static const NuGetPackage cppwinrtPackage { "Microsoft.Windows.CppWinRT", "2.0.240405.15", true }; - inline static const NuGetPackage midiPackage { "Microsoft.Windows.Devices.Midi2", "1.0.3-preview-10.250204-1909", false }; + inline static const NuGetPackage midiPackage { "Microsoft.Windows.Devices.Midi2", "1.0.3-preview-11.250228-237", false }; void getPackagesToInclude (std::vector& result) const { diff --git a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj index 096e4df55d..592afd2ca8 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj +++ b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj @@ -187,6 +187,9 @@ true + + true + true @@ -310,6 +313,30 @@ true + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + true @@ -319,6 +346,9 @@ true + + true + true @@ -3152,7 +3182,7 @@ - + @@ -3199,8 +3229,21 @@ + + + + + + + + + + + + + @@ -3277,6 +3320,7 @@ + diff --git a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters index 9617e2e6f6..cd68d73a44 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters +++ b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters @@ -50,6 +50,9 @@ {BF23FC10-1D57-2A9B-706F-6DD8A7B593D4} + + {386862D5-4DCC-A4B3-5642-60A201E303EF} + {092EFC17-7C95-7E04-0ACA-0D61A462EE81} @@ -799,6 +802,9 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump @@ -925,6 +931,30 @@ JUCE Modules\juce_audio_devices\audio_io + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + JUCE Modules\juce_audio_devices\midi_io @@ -934,6 +964,9 @@ JUCE Modules\juce_audio_devices\midi_io + + JUCE Modules\juce_audio_devices\midi_io + JUCE Modules\juce_audio_devices\native\oboe\src\aaudio @@ -4008,7 +4041,7 @@ JUCE Modules\juce_audio_basics\midi\ump - + JUCE Modules\juce_audio_basics\midi\ump @@ -4149,12 +4182,51 @@ JUCE Modules\juce_audio_devices\audio_io + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + JUCE Modules\juce_audio_devices\midi_io JUCE Modules\juce_audio_devices\midi_io + + JUCE Modules\juce_audio_devices\midi_io + + + JUCE Modules\juce_audio_devices\midi_io + JUCE Modules\juce_audio_devices\native\oboe\include\oboe @@ -4383,6 +4455,9 @@ JUCE Modules\juce_audio_devices\native\oboe\src\opensles + + JUCE Modules\juce_audio_devices\native + JUCE Modules\juce_audio_devices\native diff --git a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj index ef88d065fd..18636d335f 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj +++ b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj @@ -187,6 +187,9 @@ true + + true + true @@ -310,6 +313,30 @@ true + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + true @@ -319,6 +346,9 @@ true + + true + true @@ -3152,7 +3182,7 @@ - + @@ -3199,8 +3229,21 @@ + + + + + + + + + + + + + @@ -3277,6 +3320,7 @@ + diff --git a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters index 9981f6c572..c614d58a95 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters +++ b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters @@ -50,6 +50,9 @@ {BF23FC10-1D57-2A9B-706F-6DD8A7B593D4} + + {386862D5-4DCC-A4B3-5642-60A201E303EF} + {092EFC17-7C95-7E04-0ACA-0D61A462EE81} @@ -799,6 +802,9 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump @@ -925,6 +931,30 @@ JUCE Modules\juce_audio_devices\audio_io + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + JUCE Modules\juce_audio_devices\midi_io @@ -934,6 +964,9 @@ JUCE Modules\juce_audio_devices\midi_io + + JUCE Modules\juce_audio_devices\midi_io + JUCE Modules\juce_audio_devices\native\oboe\src\aaudio @@ -4008,7 +4041,7 @@ JUCE Modules\juce_audio_basics\midi\ump - + JUCE Modules\juce_audio_basics\midi\ump @@ -4149,12 +4182,51 @@ JUCE Modules\juce_audio_devices\audio_io + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + JUCE Modules\juce_audio_devices\midi_io JUCE Modules\juce_audio_devices\midi_io + + JUCE Modules\juce_audio_devices\midi_io + + + JUCE Modules\juce_audio_devices\midi_io + JUCE Modules\juce_audio_devices\native\oboe\include\oboe @@ -4383,6 +4455,9 @@ JUCE Modules\juce_audio_devices\native\oboe\src\opensles + + JUCE Modules\juce_audio_devices\native + JUCE Modules\juce_audio_devices\native diff --git a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj index 3eac1a4527..39c42ddf03 100644 --- a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj +++ b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj @@ -170,6 +170,9 @@ true + + true + true @@ -293,6 +296,30 @@ true + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + true @@ -302,6 +329,9 @@ true + + true + true @@ -2927,7 +2957,7 @@ - + @@ -2974,8 +3004,21 @@ + + + + + + + + + + + + + @@ -3052,6 +3095,7 @@ + diff --git a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj.filters b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj.filters index 3c365df13f..318f6384ea 100644 --- a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj.filters +++ b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj.filters @@ -35,6 +35,9 @@ {BF23FC10-1D57-2A9B-706F-6DD8A7B593D4} + + {386862D5-4DCC-A4B3-5642-60A201E303EF} + {092EFC17-7C95-7E04-0ACA-0D61A462EE81} @@ -703,6 +706,9 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump @@ -829,6 +835,30 @@ JUCE Modules\juce_audio_devices\audio_io + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + JUCE Modules\juce_audio_devices\midi_io @@ -838,6 +868,9 @@ JUCE Modules\juce_audio_devices\midi_io + + JUCE Modules\juce_audio_devices\midi_io + JUCE Modules\juce_audio_devices\native\oboe\src\aaudio @@ -3681,7 +3714,7 @@ JUCE Modules\juce_audio_basics\midi\ump - + JUCE Modules\juce_audio_basics\midi\ump @@ -3822,12 +3855,51 @@ JUCE Modules\juce_audio_devices\audio_io + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + + + JUCE Modules\juce_audio_devices\midi_io\ump + JUCE Modules\juce_audio_devices\midi_io JUCE Modules\juce_audio_devices\midi_io + + JUCE Modules\juce_audio_devices\midi_io + + + JUCE Modules\juce_audio_devices\midi_io + JUCE Modules\juce_audio_devices\native\oboe\include\oboe @@ -4056,6 +4128,9 @@ JUCE Modules\juce_audio_devices\native\oboe\src\opensles + + JUCE Modules\juce_audio_devices\native + JUCE Modules\juce_audio_devices\native diff --git a/modules/juce_audio_basics/juce_audio_basics.h b/modules/juce_audio_basics/juce_audio_basics.h index e035f1a47e..c58280f207 100644 --- a/modules/juce_audio_basics/juce_audio_basics.h +++ b/modules/juce_audio_basics/juce_audio_basics.h @@ -110,6 +110,7 @@ JUCE_END_IGNORE_WARNINGS_MSVC #include "midi/juce_MidiFile.h" #include "midi/juce_MidiKeyboardState.h" #include "midi/juce_MidiRPN.h" +#include "midi/juce_MidiDataConcatenator.h" #include "mpe/juce_MPEValue.h" #include "mpe/juce_MPENote.h" #include "mpe/juce_MPEZoneLayout.h" diff --git a/modules/juce_audio_basics/midi/ump/juce_UMPConversion.h b/modules/juce_audio_basics/midi/ump/juce_UMPConversion.h index 56c81e1c7b..453f8847db 100644 --- a/modules/juce_audio_basics/midi/ump/juce_UMPConversion.h +++ b/modules/juce_audio_basics/midi/ump/juce_UMPConversion.h @@ -36,7 +36,7 @@ namespace juce::universal_midi_packets { -/** Represents a MIDI message that happened at a particular time. +/** Represents a MIDI message on bytestream transport that happened at a particular time. Unlike MidiMessage, BytestreamMidiView is non-owning. */ @@ -63,15 +63,18 @@ struct BytestreamMidiView return MidiMessage (bytes.data(), (int) bytes.size(), timestamp); } - bool isSysEx() const + MidiMessageMetadata getMidiMessageMetadata() const { - return ! bytes.empty() && bytes.front() == std::byte { 0xf0 }; + return MidiMessageMetadata { reinterpret_cast (bytes.data()), + (int) bytes.size(), + (int) timestamp }; } Span bytes; double timestamp = 0.0; }; +//============================================================================== /** Functions to assist conversion of UMP messages to/from other formats, especially older 'bytestream' formatted MidiMessages. @@ -80,19 +83,39 @@ struct BytestreamMidiView */ struct Conversion { - /** Converts from a MIDI 1 bytestream to MIDI 1 on Universal MIDI Packets. - - `callback` is a function which accepts a single View argument. + /** Converts 7-bit data (the most significant bit of each byte must be unset) to a series of + Universal MIDI Packets. */ template - static void toMidi1 (const BytesOnGroup& m, PacketCallbackFunction&& callback) + static void umpFrom7BitData (BytesOnGroup msg, PacketCallbackFunction&& callback) { - const auto size = m.bytes.size(); + // If this is hit, non-7-bit data was supplied. + // Maybe you forgot to trim the leading/trailing bytes that delimit a bytestream SysEx message. + jassert (std::all_of (msg.bytes.begin(), msg.bytes.end(), [] (std::byte b) { return (b & std::byte { 0x80 }) == std::byte{}; })); + + Factory::splitIntoPackets (msg.bytes, 6, [&] (SysEx7::Kind kind, Span bytesThisTime) + { + const auto packet = Factory::Detail::makeSysEx (msg.group, kind, bytesThisTime); + callback (View (packet.data())); + }); + } + + /** Converts from a MIDI 1 bytestream to MIDI 1 on Universal MIDI Packets. + + @param bytes the bytes in a single well-formed bytestream MIDI message + @param callback a function that accepts a single View argument. This may be called several + times for each invocation of toMidi1 if the bytestream message converts + to multiple Universal MIDI Packets. + */ + template + static void toMidi1 (const BytesOnGroup& groupBytes, PacketCallbackFunction&& callback) + { + const auto size = groupBytes.bytes.size(); if (size <= 0) return; - const auto* data = m.bytes.data(); + const auto* data = groupBytes.bytes.data(); const auto firstByte = data[0]; if (firstByte != std::byte { 0xf0 }) @@ -107,45 +130,19 @@ struct Conversion case 3: return 0xffffffff; } + // This function can only handle a single bytestream MIDI message at a time! + jassertfalse; return 0x00000000; }(); const auto extraByte = ((((firstByte & std::byte { 0xf0 }) == std::byte { 0xf0 }) ? std::byte { 0x1 } : std::byte { 0x2 }) << 0x4); - const PacketX1 packet { mask & Utils::bytesToWord (extraByte, data[0], data[1], data[2]) }; + const std::byte group { (uint8_t) (groupBytes.group & 0xf) }; + const PacketX1 packet { mask & Utils::bytesToWord (extraByte | group, data[0], data[1], data[2]) }; callback (View (packet.data())); return; } - const auto numSysExBytes = (ssize_t) (size - 2); - const auto numMessages = SysEx7::getNumPacketsRequiredForDataSize ((uint32_t) numSysExBytes); - auto* dataOffset = data + 1; - - if (numMessages <= 1) - { - const auto packet = Factory::makeSysExIn1Packet (0, { dataOffset, (size_t) numSysExBytes }); - callback (View (packet.data())); - return; - } - - constexpr ssize_t byteIncrement = 6; - - for (auto i = static_cast (numSysExBytes); i > 0; i -= byteIncrement, dataOffset += byteIncrement) - { - const auto func = [&] - { - if (i == numSysExBytes) - return Factory::makeSysExStart; - - if (i <= byteIncrement) - return Factory::makeSysExEnd; - - return Factory::makeSysExContinue; - }(); - - const auto bytesNow = std::min (byteIncrement, i); - const auto packet = func (0, { dataOffset, (size_t) bytesNow }); - callback (View (packet.data())); - } + umpFrom7BitData ({ groupBytes.group, Span (data + 1, size - 2) }, std::forward (callback)); } /** Widens a 7-bit MIDI 1.0 value to a 8-bit MIDI 2.0 value. */ diff --git a/modules/juce_audio_basics/midi/ump/juce_UMPDeviceInfo.h b/modules/juce_audio_basics/midi/ump/juce_UMPDeviceInfo.h index 8192316009..2bf5180731 100644 --- a/modules/juce_audio_basics/midi/ump/juce_UMPDeviceInfo.h +++ b/modules/juce_audio_basics/midi/ump/juce_UMPDeviceInfo.h @@ -43,7 +43,7 @@ namespace juce::universal_midi_packets */ struct DeviceInfo { - std::array manufacturer; ///< LSB first + std::array manufacturer; std::array family; ///< LSB first std::array modelNumber; ///< LSB first std::array revision; diff --git a/modules/juce_audio_basics/midi/ump/juce_UMPFactory.h b/modules/juce_audio_basics/midi/ump/juce_UMPFactory.h index 5a33a33cd2..da1a8fff9b 100644 --- a/modules/juce_audio_basics/midi/ump/juce_UMPFactory.h +++ b/modules/juce_audio_basics/midi/ump/juce_UMPFactory.h @@ -36,6 +36,216 @@ namespace juce::universal_midi_packets { +/** + Holds the data from a stream configuration notification message, with strong types. + + @tags{Audio} +*/ +class StreamConfiguration +{ +public: + [[nodiscard]] StreamConfiguration withProtocol (PacketProtocol p) const { return withFlag (isMidi2, p == PacketProtocol::MIDI_2_0); } + [[nodiscard]] StreamConfiguration withTransmitTimestamp (bool b) const { return withFlag (transmitTimestamp, b); } + [[nodiscard]] StreamConfiguration withReceiveTimestamp (bool b) const { return withFlag (receiveTimestamp, b); } + + /** The protocol in use by the endpoint. This protocol will be used for sending and receiving messages. */ + [[nodiscard]] PacketProtocol getProtocol() const { return getFlag (isMidi2) ? PacketProtocol::MIDI_2_0 : PacketProtocol::MIDI_1_0; } + /** True if this endpoint intends to send JR timestamps. */ + [[nodiscard]] bool getTransmitTimestamp() const { return getFlag (transmitTimestamp); } + /** True if this endpoint expects to receive JR timestamps. */ + [[nodiscard]] bool getReceiveTimestamp() const { return getFlag (receiveTimestamp); } + + bool operator== (const StreamConfiguration& other) const { return options == other.options; } + bool operator!= (const StreamConfiguration& other) const { return options != other.options; } + +private: + enum Flags + { + isMidi2 = 1 << 0, + transmitTimestamp = 1 << 1, + receiveTimestamp = 1 << 2, + }; + + StreamConfiguration withFlag (Flags f, bool value) const + { + return withMember (*this, &StreamConfiguration::options, value ? (options | f) : (options & ~f)); + } + + bool getFlag (Flags f) const + { + return (options & f) != 0; + } + + int options = 0; +}; + +/** + Holds the data from an endpoint info notification message, with strong types. + + @tags{Audio} +*/ +class EndpointInfo +{ + auto tie() const { return std::tie (versionMajor, versionMinor, numFunctionBlocks, flags); } + +public: + [[nodiscard]] EndpointInfo withVersion (uint8_t major, uint8_t minor) const + { + return withMember (withMember (*this, &EndpointInfo::versionMinor, minor), &EndpointInfo::versionMajor, major); + } + + [[nodiscard]] EndpointInfo withNumFunctionBlocks (uint8_t x) const + { + return withMember (*this, &EndpointInfo::numFunctionBlocks, x); + } + + [[nodiscard]] EndpointInfo withStaticFunctionBlocks (bool b) const { return withFlag (staticFunctionBlocks, b); } + [[nodiscard]] EndpointInfo withMidi1Support (bool b) const { return withFlag (supportsMidi1, b); } + [[nodiscard]] EndpointInfo withMidi2Support (bool b) const { return withFlag (supportsMidi2, b); } + [[nodiscard]] EndpointInfo withReceiveJRSupport (bool b) const { return withFlag (supportsReceiveJR, b); } + [[nodiscard]] EndpointInfo withTransmitJRSupport (bool b) const { return withFlag (supportsTransmitJR, b); } + + /** The major version byte. */ + [[nodiscard]] uint8_t getVersionMajor() const { return versionMajor; } + /** The minor version byte. */ + [[nodiscard]] uint8_t getVersionMinor() const { return versionMinor; } + /** The number of function blocks declared on this endpoint. */ + [[nodiscard]] uint8_t getNumFunctionBlocks() const { return numFunctionBlocks; } + /** True if the function block configuration cannot change. */ + [[nodiscard]] bool hasStaticFunctionBlocks() const { return getFlag (staticFunctionBlocks); } + /** True if this endpoint is capable of supporting the MIDI 1.0 protocol. */ + [[nodiscard]] bool hasMidi1Support() const { return getFlag (supportsMidi1); } + /** True if this endpoint is capable of supporting the MIDI 2.0 protocol. */ + [[nodiscard]] bool hasMidi2Support() const { return getFlag (supportsMidi2); } + /** True if this endpoint is capable of receiving JR timestamps. */ + [[nodiscard]] bool hasReceiveJRSupport() const { return getFlag (supportsReceiveJR); } + /** True if this endpoint is capable of transmitting JR timestamps. */ + [[nodiscard]] bool hasTransmitJRSupport() const { return getFlag (supportsTransmitJR); } + + bool operator== (const EndpointInfo& other) const { return tie() == other.tie(); } + bool operator!= (const EndpointInfo& other) const { return tie() != other.tie(); } + +private: + enum Flags + { + staticFunctionBlocks = 1 << 0, + supportsMidi1 = 1 << 1, + supportsMidi2 = 1 << 2, + supportsReceiveJR = 1 << 3, + supportsTransmitJR = 1 << 4, + }; + + EndpointInfo withFlag (Flags f, bool value) const + { + return withMember (*this, &EndpointInfo::flags, (uint8_t) (value ? (flags | f) : (flags & ~f))); + } + + bool getFlag (Flags f) const + { + return (flags & f) != 0; + } + + uint8_t versionMajor, versionMinor, numFunctionBlocks, flags; +}; + +/** Directions that can apply to a Function Block or Group Terminal Block. */ +enum class BlockDirection : uint8_t +{ + unknown = 0b00, ///< Block direction is unknown or undeclared + receiver = 0b01, ///< Block is a receiver of messages + sender = 0b10, ///< Block is a sender of messages + bidirectional = 0b11, ///< Block both sends and receives messages +}; + +/** UI hints that can apply to a Function Block or Group Terminal Block. */ +enum class BlockUiHint : uint8_t +{ + unknown = 0b00, ///< Block direction is unknown or undeclared + receiver = 0b01, ///< Block is a receiver of messages + sender = 0b10, ///< Block is a sender of messages + bidirectional = 0b11, ///< Block both sends and receives messages +}; + +/** Describes how a MIDI 1.0 port maps to a given Block, if applicable. */ +enum class BlockMIDI1ProxyKind : uint8_t +{ + inapplicable = 0b00, ///< Block does not represent a MIDI 1.0 port + unrestrictedBandwidth = 0b01, ///< Block represents a MIDI 1.0 port and can handle high bandwidth + restrictedBandwidth = 0b10, ///< Block represents a MIDI 1.0 port that requires restricted bandwidth +}; + +/** + Holds the data from a function block info notification message, with strong types. + + @tags{Audio} +*/ +class BlockInfo +{ +public: + [[nodiscard]] BlockInfo withEnabled (bool x) const { return withMember (*this, &BlockInfo::enabled, x); } + [[nodiscard]] BlockInfo withUiHint (BlockUiHint x) const { return withMember (*this, &BlockInfo::flags, replaceBits<4, 2> (flags, (uint8_t) x)); } + [[nodiscard]] BlockInfo withMIDI1ProxyKind (BlockMIDI1ProxyKind x) const { return withMember (*this, &BlockInfo::flags, replaceBits<2, 2> (flags, (uint8_t) x)); } + [[nodiscard]] BlockInfo withDirection (BlockDirection x) const { return withMember (*this, &BlockInfo::flags, replaceBits<0, 2> (flags, (uint8_t) x)); } + [[nodiscard]] BlockInfo withFirstGroup (uint8_t x) const { return withMember (*this, &BlockInfo::firstGroup, x); } + [[nodiscard]] BlockInfo withNumGroups (uint8_t x) const { return withMember (*this, &BlockInfo::numGroups, x); } + [[nodiscard]] BlockInfo withCiVersion (uint8_t x) const { return withMember (*this, &BlockInfo::ciVersion, x); } + [[nodiscard]] BlockInfo withMaxSysex8Streams (uint8_t x) const { return withMember (*this, &BlockInfo::numSysex8Streams, x); } + + /** True if the block is enabled/active, false otherwise. */ + bool isEnabled() const { return enabled; } + /** The directionality of the block, for display to the user. */ + BlockUiHint getUiHint() const { return (BlockUiHint) getBits<4, 2> (flags); } + /** The kind of MIDI 1.0 proxy represented by this block, if any. */ + BlockMIDI1ProxyKind getMIDI1ProxyKind() const { return (BlockMIDI1ProxyKind) getBits<2, 2> (flags); } + /** The actual directionality of the block. */ + BlockDirection getDirection() const { return (BlockDirection) getBits<0, 2> (flags); } + /** The zero-based index of the first group in the block. */ + uint8_t getFirstGroup() const { return firstGroup; } + /** The number of groups contained in the block, must be one or greater. */ + uint8_t getNumGroups() const { return numGroups; } + /** The CI version supported by this block. Implies a bidirectional block. */ + uint8_t getCiVersion() const { return ciVersion; } + /** The number of simultaneous SysEx8 streams supported on this block. */ + uint8_t getMaxSysex8Streams() const { return numSysex8Streams; } + + bool operator== (const BlockInfo& other) const + { + const auto tie = [] (auto& x) + { + return std::tuple (x.enabled, x.flags, x.firstGroup, x.numGroups, x.ciVersion, x.numSysex8Streams); + }; + return tie (*this) == tie (other); + } + + bool operator!= (const BlockInfo& other) const + { + return ! operator== (other); + } + +private: + template + static Value replaceBits (Value value, Value replacement) + { + constexpr auto mask = ((Value) 1 << numBits) - 1; + const auto maskedValue = value & ~(mask << position); + return (Value) (maskedValue | (replacement << position)); + } + + template + static Value getBits (Value value) + { + constexpr auto mask = ((Value) 1 << numBits) - 1; + return (value >> position) & mask; + } + + uint8_t enabled{}; + uint8_t flags{}; + uint8_t firstGroup{}; + uint8_t numGroups{}; + uint8_t ciVersion{}; + uint8_t numSysex8Streams{}; +}; + /** This struct holds functions that can be used to create different kinds of Universal MIDI Packet. @@ -44,6 +254,36 @@ namespace juce::universal_midi_packets */ struct Factory { + template + static void splitIntoPackets (Span bytes, size_t bytesPerPacket, Callback&& callback) + { + const auto numPackets = (bytes.size() / bytesPerPacket) + ((bytes.size() % bytesPerPacket) != 0); + auto* dataOffset = bytes.data(); + + if (numPackets <= 1) + { + callback (SysEx7::Kind::complete, bytes); + return; + } + + for (auto i = static_cast (bytes.size()); i > 0; i -= (ssize_t) bytesPerPacket, dataOffset += bytesPerPacket) + { + const auto kind = [&] + { + if (i == (ssize_t) bytes.size()) + return SysEx7::Kind::begin; + + if (i <= (ssize_t) bytesPerPacket) + return SysEx7::Kind::end; + + return SysEx7::Kind::continuation; + }(); + + const auto bytesNow = std::min ((ssize_t) bytesPerPacket, i); + callback (kind, Span (dataOffset, (size_t) bytesNow)); + } + } + /** @internal */ struct Detail { @@ -90,6 +330,41 @@ struct Factory return PacketX4 { words }; } + + static PacketX4 makePacketX4 (Span header, + Span data) + { + jassert (data.size() <= 14); + + std::array bytes{{}}; + std::copy (header.begin(), header.end(), bytes.begin()); + std::copy (data.begin(), data.end(), std::next (bytes.begin(), (ptrdiff_t) header.size())); + + std::array words{}; + + size_t index = 0; + + for (auto& word : words) + word = ByteOrder::bigEndianInt (bytes.data() + 4 * index++); + + return PacketX4 { words }; + } + + static PacketX4 makeStreamSubpacket (std::byte status, + SysEx7::Kind kind, + Span data) + { + jassert (data.size() <= 14); + const std::byte header[] { std::byte (0xf0) | std::byte ((uint8_t) kind << 2), status, }; + return makePacketX4 (header, data); + } + + static PacketX4 makeStreamConfiguration (StreamConfiguration options) + { + return Detail::makeStream().withU8<0x2> (options.getProtocol() == PacketProtocol::MIDI_2_0 ? 0x2 : 0x1) + .withU8<0x3> ((options.getReceiveTimestamp() ? 0x2 : 0x0) | (options.getTransmitTimestamp() ? 0x1 : 0x0)); + } + }; static PacketX1 makeNoop (uint8_t group) @@ -520,6 +795,23 @@ struct Factory .withU8<7> ((uint8_t) filterBitmap); } + static PacketX4 makeEndpointInfoNotification (const EndpointInfo& info) + { + return Detail::makeStream().withU8<1> (1) + .withU8<2> (info.getVersionMajor()) + .withU8<3> (info.getVersionMinor()) + .withU8<4> (info.getNumFunctionBlocks() | (info.hasStaticFunctionBlocks() ? 0x80 : 0x00)) + .withU8<6> ((info.hasMidi1Support() ? 0x1 : 0x0) | (info.hasMidi2Support() ? 0x2 : 0x0)) + .withU8<7> ((info.hasTransmitJRSupport() ? 0x1 : 0x0) | (info.hasReceiveJRSupport() ? 0x2 : 0x0)); + } + + static PacketX4 makeFunctionBlockDiscovery (uint8_t block, std::byte filterBitmap) + { + return Detail::makeStream().withU8<1> (0x10) + .withU8<2> (block) + .withU8<3> ((uint8_t) filterBitmap); + } + static PacketX4 makeDeviceIdentityNotification (DeviceInfo info) { return Detail::makeStream().withU8<0x1> (2) @@ -535,6 +827,87 @@ struct Factory .withU8<0xe> ((uint8_t) info.revision[2]) .withU8<0xf> ((uint8_t) info.revision[3]); } + + template + static bool makeEndpointNameNotification (const String& bytes, Fn&& fn) + { + constexpr auto maxSize = 98; + + if (maxSize <= bytes.getNumBytesAsUTF8()) + return false; + + const Span byteSpan { reinterpret_cast (bytes.toRawUTF8()), bytes.getNumBytesAsUTF8() }; + + splitIntoPackets (byteSpan, 14, [&] (SysEx7::Kind kind, Span bytesThisTime) + { + const auto packet = Detail::makeStreamSubpacket (std::byte (3), kind, bytesThisTime); + fn (View (packet.data())); + }); + + return true; + } + + template + static bool makeProductInstanceIdNotification (const String& bytes, Fn&& fn) + { + constexpr auto maxSize = 42; + + if (maxSize < bytes.getNumBytesAsUTF8()) + return false; + + const Span byteSpan { reinterpret_cast (bytes.toRawUTF8()), bytes.getNumBytesAsUTF8() }; + + splitIntoPackets (byteSpan, 14, [&] (SysEx7::Kind kind, Span bytesThisTime) + { + const auto packet = Detail::makeStreamSubpacket (std::byte (4), kind, bytesThisTime); + fn (View (packet.data())); + }); + + return true; + } + + template + static bool makeFunctionBlockNameNotification (uint8_t index, const String& bytes, Fn&& fn) + { + constexpr auto maxSize = 91; + + if (maxSize < bytes.getNumBytesAsUTF8()) + return false; + + const Span byteSpan { reinterpret_cast (bytes.toRawUTF8()), bytes.getNumBytesAsUTF8() }; + + splitIntoPackets (byteSpan, 13, [&] (SysEx7::Kind kind, Span bytesThisTime) + { + const std::byte header[] { std::byte (0xf0) | std::byte ((uint8_t) kind << 2), std::byte (0x12), std::byte (index) }; + fn (View (Detail::makePacketX4 (header, bytesThisTime).data())); + }); + + return true; + } + + static PacketX4 makeFunctionBlockInfoNotification (uint8_t index, const BlockInfo& info) + { + const auto flags = ((uint8_t) info.getDirection() << 0) + | ((uint8_t) info.getMIDI1ProxyKind() << 2) + | ((uint8_t) info.getUiHint() << 4); + return Detail::makeStream().withU8<0x1> (0x11) + .withU8<0x2> ((uint8_t) (index | (info.isEnabled() << 7))) + .withU8<0x3> ((uint8_t) flags) + .withU8<0x4> (info.getFirstGroup()) + .withU8<0x5> (info.getNumGroups()) + .withU8<0x6> (info.getCiVersion()) + .withU8<0x7> (info.getMaxSysex8Streams()); + } + + static PacketX4 makeStreamConfigurationRequest (StreamConfiguration options) + { + return Detail::makeStreamConfiguration (options).withU8<0x1> (5); + } + + static PacketX4 makeStreamConfigurationNotification (StreamConfiguration options) + { + return Detail::makeStreamConfiguration (options).withU8<0x1> (6); + } }; } // namespace juce::universal_midi_packets diff --git a/modules/juce_audio_basics/midi/ump/juce_UMPProtocols.h b/modules/juce_audio_basics/midi/ump/juce_UMPProtocols.h index 1770c0c52e..9f260bf43a 100644 --- a/modules/juce_audio_basics/midi/ump/juce_UMPProtocols.h +++ b/modules/juce_audio_basics/midi/ump/juce_UMPProtocols.h @@ -36,15 +36,25 @@ namespace juce::universal_midi_packets { -/** The kinds of MIDI protocol that can be formatted into Universal MIDI Packets. */ -enum class PacketProtocol +/** Kinds of MIDI message transport. +*/ +enum class Transport : uint8_t +{ + bytestream, ///< A stream of variable-length messages. Suitable for MIDI 1.0. + ump, ///< A stream of 32-bit words. Suitable for MIDI-1UP and MIDI 2.0. +}; + +/** The kinds of MIDI protocol that can be formatted into Universal MIDI Packets. +*/ +enum class PacketProtocol : uint8_t { MIDI_1_0, MIDI_2_0, }; -/** All kinds of MIDI protocol understood by JUCE. */ -enum class MidiProtocol +/** All kinds of MIDI protocol understood by JUCE. +*/ +enum class MidiProtocol : uint8_t { bytestream, UMP_MIDI_1_0, diff --git a/modules/juce_audio_basics/midi/ump/juce_UMPUtils.h b/modules/juce_audio_basics/midi/ump/juce_UMPUtils.h index 2de2159212..c51bb84e22 100644 --- a/modules/juce_audio_basics/midi/ump/juce_UMPUtils.h +++ b/modules/juce_audio_basics/midi/ump/juce_UMPUtils.h @@ -128,10 +128,20 @@ struct Utils stream = 0xf, }; + static constexpr bool hasGroup (MessageKind k) + { + return ! isGroupless (k); + } + + static constexpr bool isGroupless (MessageKind k) + { + return k == MessageKind::utility || k == MessageKind::stream; + } + static constexpr MessageKind getMessageType (uint32_t w) noexcept { return MessageKind { U4<0>::get (w) }; } - static constexpr uint8_t getGroup (uint32_t w) noexcept { return U4<1>::get (w); } static constexpr std::byte getStatus (uint32_t w) noexcept { return std::byte { U4<2>::get (w) }; } static constexpr uint8_t getChannel (uint32_t w) noexcept { return U4<3>::get (w); } + static constexpr uint8_t getGroup (uint32_t w) noexcept { return U4<1>::get (w); } }; } // namespace juce::universal_midi_packets diff --git a/modules/juce_audio_basics/midi/ump/juce_UMPView.h b/modules/juce_audio_basics/midi/ump/juce_UMPView.h index 2fd47306ca..cbc3199c22 100644 --- a/modules/juce_audio_basics/midi/ump/juce_UMPView.h +++ b/modules/juce_audio_basics/midi/ump/juce_UMPView.h @@ -65,7 +65,7 @@ public: */ const uint32_t* data() const noexcept { return ptr; } - /** Get the number of 32-words (between 1 and 4 inclusive) in the Universal + /** Get the number of 32 bit words (between 1 and 4 inclusive) in the Universal MIDI Packet currently pointed-to by this view. */ uint32_t size() const noexcept; diff --git a/modules/juce_audio_basics/midi/ump/juce_UMPacket.h b/modules/juce_audio_basics/midi/ump/juce_UMPacket.h index eb50f106c1..178198e0c9 100644 --- a/modules/juce_audio_basics/midi/ump/juce_UMPacket.h +++ b/modules/juce_audio_basics/midi/ump/juce_UMPacket.h @@ -169,6 +169,16 @@ public: return std::get (contents); } + bool operator== (const Packet& other) const + { + return contents == other.contents; + } + + bool operator!= (const Packet& other) const + { + return contents != other.contents; + } + //============================================================================== using Contents = std::array; diff --git a/modules/juce_audio_devices/juce_audio_devices.cpp b/modules/juce_audio_devices/juce_audio_devices.cpp index 5e5807d61a..2afb980525 100644 --- a/modules/juce_audio_devices/juce_audio_devices.cpp +++ b/modules/juce_audio_devices/juce_audio_devices.cpp @@ -59,44 +59,47 @@ #include "midi_io/juce_WaitFreeListeners.h" #include "midi_io/juce_WaitFreeListeners.cpp" -#include "audio_io/juce_SampleRateHelpers.cpp" #include "midi_io/juce_MidiDeviceListConnectionBroadcaster.cpp" +#include "midi_io/ump/juce_UMPIOHelpers.cpp" +#include "midi_io/ump/juce_UMPInput.cpp" +#include "midi_io/ump/juce_UMPOutput.cpp" +#include "midi_io/ump/juce_UMPLegacyVirtualInput.cpp" +#include "midi_io/ump/juce_UMPLegacyVirtualOutput.cpp" +#include "midi_io/ump/juce_UMPVirtualEndpoint.cpp" +#include "midi_io/ump/juce_UMPSession.cpp" +#include "midi_io/ump/juce_UMPEndpoints.cpp" + +#include "audio_io/juce_SampleRateHelpers.cpp" +#include "midi_io/juce_MidiDevices.cpp" + //============================================================================== #if JUCE_MAC || JUCE_IOS #include #include - #include - #include #endif #if JUCE_MAC #define Point CarbonDummyPointName #define Component CarbonDummyCompName #import - #import + #import #import #undef Point #undef Component #include "native/juce_CoreAudio_mac.cpp" - #include "native/juce_CoreMidi_mac.mm" #elif JUCE_IOS #import #import - #import - - #if TARGET_OS_SIMULATOR - #import - #endif + #import #if JUCE_MODULE_AVAILABLE_juce_graphics #include #endif #include "native/juce_Audio_ios.cpp" - #include "native/juce_CoreMidi_mac.mm" //============================================================================== #elif JUCE_WINDOWS @@ -132,9 +135,6 @@ JUCE_END_IGNORE_WARNINGS_MSVC #endif - #include - #include "native/juce_Midi_windows.cpp" - #if JUCE_ASIO /* This is very frustrating - we only need to use a handful of definitions from a couple of the header files in Steinberg's ASIO SDK, and it'd be easy to copy @@ -173,6 +173,7 @@ JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wzero-length-array") #include JUCE_END_IGNORE_WARNINGS_GCC_LIKE + #include "native/juce_ALSA_weak_linux.h" #include "native/juce_ALSA_linux.cpp" #endif @@ -191,22 +192,19 @@ #undef SIZEOF - #include - #include "native/juce_Midi_linux.cpp" - //============================================================================== #elif JUCE_ANDROID + // Currently we're just using this for enum values + #include + namespace juce { using RealtimeThreadFactory = pthread_t (*) (void* (*) (void*), void*); RealtimeThreadFactory getAndroidRealtimeThreadFactory(); } // namespace juce -#include "native/juce_Audio_android.cpp" - - #include - #include "native/juce_Midi_android.cpp" + #include "native/juce_Audio_android.cpp" #if JUCE_USE_ANDROID_OPENSLES || JUCE_USE_ANDROID_OBOE #include "native/juce_HighPerformanceAudioHelpers_android.h" @@ -264,8 +262,6 @@ namespace juce #include "native/juce_JackAudio.cpp" #endif -#include "midi_io/juce_MidiDevices.cpp" - #if ! JUCE_SYSTEMAUDIOVOL_IMPLEMENTED namespace juce { @@ -283,3 +279,25 @@ namespace juce #include "midi_io/juce_MidiMessageCollector.cpp" #include "sources/juce_AudioSourcePlayer.cpp" #include "sources/juce_AudioTransportSource.cpp" + +#if JUCE_LINUX || JUCE_BSD + #include "native/juce_Midi_linux.cpp" +#elif JUCE_ANDROID + #include "native/juce_Midi_android.cpp" +#elif JUCE_MAC || JUCE_IOS + #include "native/juce_CoreMidi_mac.mm" +#elif JUCE_WINDOWS + #if JUCE_USE_WINDOWS_MIDI_SERVICES + JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4265) + #include + #include + #include + + #include + #include + #include + JUCE_END_IGNORE_WARNINGS_MSVC + #endif + + #include "native/juce_Midi_windows.cpp" +#endif diff --git a/modules/juce_audio_devices/juce_audio_devices.h b/modules/juce_audio_devices/juce_audio_devices.h index 72b64bce7c..7da8286390 100644 --- a/modules/juce_audio_devices/juce_audio_devices.h +++ b/modules/juce_audio_devices/juce_audio_devices.h @@ -87,6 +87,40 @@ #define JUCE_USE_WINRT_MIDI 0 #endif +/** Config: JUCE_USE_WINDOWS_MIDI_SERVICES + Enables the use of Windows MIDI Services, in turn enabling MIDI 2.0 support on + some Windows versions. At time of writing, this only works on the Canary insiders + build of Windows 11, but eventually this will work on the final release of + Windows 10 too. + + In order to enable this functionality, your target and minimum Windows SDK + versions must be set to at least 10.0.20348.0. + + This functionality depends on the CppWinRT tool and the Windows MIDI Services + NuGet package at build-time. If you're using the Projucer, this will be set up + for you automatically when this flag is enabled. If you're using CMake, you'll + need to pass "NEEDS_WINDOWS_MIDI_SERVICES TRUE" to your juce_add_ call. + + Currently, the Windows MIDI Services NuGet package is not hosted in the official + repository. In order to allow NuGet and/or Visual Studio to find the package, + you'll need to set up a local repository pointing at a local folder holding the + package. You can do this from the Visual Studio package management UI. + + Because this API is implemented using code generated by the CppWinRT tool, it + can only be built by compilers that understand the generated code. MSVC is + most likely to work; some versions of Clang may work but this is not guaranteed. + + Even once the Windows MIDI Services are stable and enabled in Windows, + the service won't be usable until the service's SDK package is installed. + Therefore, you will need to ensure that the installer for your software also + installs this SDK package, or informs the user how to install the dependency. + If the SDK is not available, JUCE will fall back to using the older WinRT + API (if enabled) or the Win32 API otherwise. These APIs do not support MIDI 2.0. +*/ +#ifndef JUCE_USE_WINDOWS_MIDI_SERVICES + #define JUCE_USE_WINDOWS_MIDI_SERVICES 0 +#endif + /** Config: JUCE_ASIO Enables ASIO audio devices (MS Windows only). Turning this on means that you'll need to have the Steinberg ASIO SDK installed @@ -164,6 +198,18 @@ #endif //============================================================================== +#include "midi_io/juce_ScheduledEventThread.h" +#include "midi_io/ump/juce_UMPEndpointId.h" +#include "midi_io/ump/juce_UMPBlock.h" +#include "midi_io/ump/juce_UMPEndpoint.h" +#include "midi_io/ump/juce_UMPDisconnectionListener.h" +#include "midi_io/ump/juce_UMPInput.h" +#include "midi_io/ump/juce_UMPOutput.h" +#include "midi_io/ump/juce_UMPLegacyVirtualInput.h" +#include "midi_io/ump/juce_UMPLegacyVirtualOutput.h" +#include "midi_io/ump/juce_UMPVirtualEndpoint.h" +#include "midi_io/ump/juce_UMPSession.h" +#include "midi_io/ump/juce_UMPEndpoints.h" #include "midi_io/juce_MidiDevices.h" #include "midi_io/juce_MidiMessageCollector.h" diff --git a/modules/juce_audio_devices/midi_io/juce_MidiDeviceListConnectionBroadcaster.cpp b/modules/juce_audio_devices/midi_io/juce_MidiDeviceListConnectionBroadcaster.cpp index c80d2168de..2257b27b3b 100644 --- a/modules/juce_audio_devices/midi_io/juce_MidiDeviceListConnectionBroadcaster.cpp +++ b/modules/juce_audio_devices/midi_io/juce_MidiDeviceListConnectionBroadcaster.cpp @@ -35,11 +35,185 @@ namespace juce { -class MidiDeviceListConnectionBroadcaster final : private AsyncUpdater +/* Identifies a single group of an endpoint. */ +struct EndpointGroup { + ump::EndpointId endpointId; + uint8_t group; // 0 to 15 inclusive + + bool operator== (const EndpointGroup& other) const + { + const auto tie = [] (auto& x) { return std::tuple (x.endpointId, x.group); }; + return tie (*this) == tie (other); + } + + bool operator!= (const EndpointGroup& other) const { return ! operator== (other); } +}; + +class MidiDeviceListConnectionBroadcaster : private AsyncUpdater, + private ump::EndpointsListener +{ + struct GroupId + { + String identifier; + ump::IOKind kind; + + auto tie() const { return std::tuple (identifier, kind); } + bool operator== (const GroupId& other) const { return tie() == other.tie(); } + bool operator!= (const GroupId& other) const { return tie() != other.tie(); } + bool operator< (const GroupId& other) const { return tie() < other.tie(); } + }; + + class Endpoints + { + auto tie() const { return std::tie (idBuffer, map, inputs, outputs); } + + public: + void refresh() + { + updateIdBuffer(); + map = computeMap(); + inputs = computeInfo (ump::IOKind::src); + outputs = computeInfo (ump::IOKind::dst); + } + + std::optional getEndpointGroupForId (ump::IOKind direction, const String& id) const + { + JUCE_ASSERT_MESSAGE_THREAD + + const auto iter = map.find ({ id, direction }); + + if (iter == map.end()) + return {}; + + return iter->second; + } + + void getAllMidiDeviceInfo (ump::IOKind direction, Array& buffer) const + { + JUCE_ASSERT_MESSAGE_THREAD + buffer.addArray (direction == ump::IOKind::src ? inputs : outputs); + } + + bool operator== (const Endpoints& other) const + { + return tie() == other.tie(); + } + + bool operator!= (const Endpoints& other) const + { + return ! operator== (other); + } + + private: + void updateIdBuffer() + { + idBuffer.clear(); + + auto* ep = ump::Endpoints::getInstance(); + + if (ep == nullptr) + return; + + ep->getEndpoints (idBuffer); + } + + std::map computeMap() const + { + auto* ep = ump::Endpoints::getInstance(); + + if (ep == nullptr) + return {}; + + std::map result; + + for (const auto& id : idBuffer) + { + const auto info = ep->getStaticDeviceInfo (id); + + if (! info.has_value()) + continue; + + for (const auto kind : ump::ioKinds) + { + const auto groups = info->getLegacyIdentifiers (kind); + + for (const auto [index, groupId] : enumerate (groups, uint8_t{})) + { + if (groupId.isNotEmpty()) + result.emplace (GroupId { groupId, kind }, EndpointGroup { id, index }); + } + } + } + + return result; + } + + Array computeInfo (ump::IOKind dir) const + { + auto* ep = ump::Endpoints::getInstance(); + + if (ep == nullptr) + return {}; + + Array result; + + for (const auto& id : idBuffer) + { + const auto endpoint = ep->getEndpoint (id); + + if (! endpoint.has_value()) + continue; + + const auto info = ep->getStaticDeviceInfo (id); + + if (! info.has_value()) + continue; + + const auto groups = info->getLegacyIdentifiers (dir); + + for (const auto [groupIndex, groupId] : enumerate (groups, uint8_t{})) + { + if (groupId.isEmpty()) + continue; + + const auto blockName = findNameForGroup (groupIndex, dir, endpoint->getBlocks()); + + if (! blockName.has_value()) + continue; + + const auto separator = endpoint->getName().isEmpty() || blockName->isEmpty() ? "" : " : "; + const auto name = endpoint->getName() + separator + *blockName; + + result.add (MidiDeviceInfo { name, groupId }); + } + } + + return result; + } + + std::vector idBuffer = std::invoke ([]() -> std::vector + { + auto* ep = ump::Endpoints::getInstance(); + + if (ep == nullptr) + return {}; + + std::vector result; + ep->getEndpoints (result); + return result; + }); + + std::map map = computeMap(); + Array inputs = computeInfo (ump::IOKind::src), outputs = computeInfo (ump::IOKind::dst); + }; + public: ~MidiDeviceListConnectionBroadcaster() override { + if (auto* instance = ump::Endpoints::getInstanceWithoutCreating()) + instance->removeListener (*this); + cancelPendingUpdate(); } @@ -49,13 +223,66 @@ public: return callbacks.emplace (key++, std::move (callback)).first->first; } + void getAllMidiDeviceInfo (ump::IOKind direction, Array& buffer) + { + JUCE_ASSERT_MESSAGE_THREAD + + if (! endpoints.has_value()) + endpoints = Endpoints{}; + + endpoints->getAllMidiDeviceInfo (direction, buffer); + } + + std::optional getInfoForId (ump::IOKind direction, const String& id) + { + if (const auto eg = getEndpointGroupForId (direction, id)) + return findInfoForDeviceIdentifier ({ id, direction }, *eg); + + return {}; + } + + std::optional getEndpointGroupForId (ump::IOKind direction, const String& id) + { + JUCE_ASSERT_MESSAGE_THREAD + + if (! endpoints.has_value()) + endpoints = Endpoints{}; + + return endpoints->getEndpointGroupForId (direction, id); + } + void remove (const MidiDeviceListConnection::Key k) { JUCE_ASSERT_MESSAGE_THREAD callbacks.erase (k); } - void notify() + static auto& get() + { + static MidiDeviceListConnectionBroadcaster result; + return result; + } + +private: + MidiDeviceListConnectionBroadcaster() + { + ump::Endpoints::getInstance()->addListener (*this); + } + + static std::optional findNameForGroup (uint8_t group, ump::IOKind kind, Span blocks) + { + const auto matchingGroup = std::find_if (blocks.begin(), blocks.end(), [&] (const ump::Block& b) + { + return directionsMatch (b, kind) && group == b.getFirstGroup() && b.getNumGroups() == 1; + }); + + if (matchingGroup != blocks.end()) + return matchingGroup->getName(); + + return {}; + } + + void endpointsChanged() override { auto* mm = MessageManager::getInstanceWithoutCreating(); @@ -70,47 +297,60 @@ public: cancelPendingUpdate(); - if (auto prev = std::exchange (lastNotifiedState, State{}); prev != lastNotifiedState) + if (auto prev = std::exchange (endpoints, Endpoints{}); prev != endpoints) for (auto it = callbacks.begin(); it != callbacks.end();) NullCheckedInvocation::invoke ((it++)->second); } - static auto& get() - { - static MidiDeviceListConnectionBroadcaster result; - return result; - } - -private: - MidiDeviceListConnectionBroadcaster() = default; - - class State - { - Array ins = MidiInput::getAvailableDevices(), - outs = MidiOutput::getAvailableDevices(); - auto tie() const - { - return std::tie (ins, outs); - } - - public: - bool operator== (const State& other) const - { - return tie() == other.tie(); - } - bool operator!= (const State& other) const - { - return tie() != other.tie(); - } - }; - void handleAsyncUpdate() override { - notify(); + endpointsChanged(); } + static bool directionsMatch (ump::BlockDirection d, ump::IOKind k) + { + switch (d) + { + case ump::BlockDirection::unknown: return true; + case ump::BlockDirection::bidirectional: return true; + case ump::BlockDirection::sender: return k == ump::IOKind::src; + case ump::BlockDirection::receiver: return k == ump::IOKind::dst; + } + + jassertfalse; + return false; + } + + static bool directionsMatch (const ump::Block& b, ump::IOKind k) + { + return directionsMatch (b.getDirection(), k); + } + + static std::optional findInfoForDeviceIdentifier (const GroupId& key, const EndpointGroup& value) + { + auto* endpoints = ump::Endpoints::getInstance(); + + if (endpoints == nullptr) + return {}; + + const auto endpoint = endpoints->getEndpoint (value.endpointId); + + if (! endpoint.has_value()) + return {}; + + const auto blockName = findNameForGroup (value.group, key.kind, endpoint->getBlocks()); + + if (! blockName.has_value()) + return {}; + + const auto separator = endpoint->getName().isEmpty() || blockName->isEmpty() ? "" : " : "; + const auto name = endpoint->getName() + separator + *blockName; + + return MidiDeviceInfo { name, key.identifier }; + } + + std::optional endpoints; std::map> callbacks; - std::optional lastNotifiedState; MidiDeviceListConnection::Key key = 0; }; diff --git a/modules/juce_audio_devices/midi_io/juce_MidiDevices.cpp b/modules/juce_audio_devices/midi_io/juce_MidiDevices.cpp index 23b02c6541..4f7ddf8f25 100644 --- a/modules/juce_audio_devices/midi_io/juce_MidiDevices.cpp +++ b/modules/juce_audio_devices/midi_io/juce_MidiDevices.cpp @@ -35,144 +35,368 @@ namespace juce { -MidiDeviceListConnection::~MidiDeviceListConnection() noexcept +MidiDeviceListConnection MidiDeviceListConnection::make (std::function callback) { - if (broadcaster != nullptr) - broadcaster->remove (key); + auto& broadcaster = MidiDeviceListConnectionBroadcaster::get(); + + const auto key = broadcaster.add (std::move (callback)); + + MidiDeviceListConnection result; + result.token = ErasedScopeGuard { [&broadcaster, key] { broadcaster.remove (key); } }; + return result; } //============================================================================== -void MidiInputCallback::handlePartialSysexMessage ([[maybe_unused]] MidiInput* source, - [[maybe_unused]] const uint8* messageData, - [[maybe_unused]] int numBytesSoFar, - [[maybe_unused]] double timestamp) {} +static std::shared_ptr getLegacySession() +{ + static std::weak_ptr weak; + + if (auto strong = weak.lock()) + return strong; + + if (auto session = ump::Endpoints::getInstance()->makeSession (ump::Endpoints::Impl::getGlobalMidiClientName())) + { + auto strong = std::make_shared (std::move (session)); + weak = strong; + return strong; + } + + return nullptr; +} + +class MidiInput::Impl : private ump::Consumer +{ +public: + void start() + { + const SpinLock::ScopedLockType lock { spinLock }; + active = true; + } + + void stop() + { + const SpinLock::ScopedLockType lock { spinLock }; + active = false; + } + + MidiDeviceInfo getDeviceInfo() const noexcept + { + return customName.has_value() ? storedInfo.withName (*customName) : storedInfo; + } + + void setName (String x) + { + customName = std::move (x); + } + + void addCallback (MidiInputCallback& cb) + { + callbacks.add (cb); + } + + void removeCallback (MidiInputCallback& cb) + { + callbacks.remove (cb); + } + + /* session may be null, in which case it's up to the caller to ensure that the session lives + long enough for the connection to be useful. + */ + static std::unique_ptr make (std::shared_ptr session, + ump::Input connection, + uint8_t group, + const MidiDeviceInfo& info, + MidiInputCallback* cb, + ump::LegacyVirtualInput virtualEndpoint) + { + auto result = rawToUniquePtr (new MidiInput); + result->pimpl = rawToUniquePtr (new Impl (session, + std::move (connection), + group, + result.get(), + info, + std::move (virtualEndpoint))); + + if (cb != nullptr) + result->addCallback (*cb); + + return result; + } + + uint8_t getGroup() const + { + return group; + } + + ump::EndpointId getEndpointId() const + { + return connection.getEndpointId(); + } + + ~Impl() override + { + connection.removeConsumer (*this); + } + +private: + Impl (std::shared_ptr s, + ump::Input x, + uint8_t g, + MidiInput* o, + MidiDeviceInfo i, + ump::LegacyVirtualInput v) + : session (s), + virtualEndpoint (std::move (v)), + connection (std::move (x)), + storedInfo (i), + group (g), + owner (o) + { + connection.addConsumer (*this); + } + + void consume (ump::Iterator b, ump::Iterator e, double time) override + { + const SpinLock::ScopedTryLockType lock { spinLock }; + + if (! lock.isLocked() || ! active) + return; + + for (const auto& view : makeRange (b, e)) + { + if (ump::Utils::getGroup (view[0]) != group) + continue; + + converter.convert (view, time, [this] (ump::BytesOnGroup v, double t) + { + const MidiMessage msg { v.bytes.data(), (int) v.bytes.size(), t }; + + callbacks.call ([&] (MidiInputCallback& l) + { + l.handleIncomingMidiMessage (owner, msg); + }); + }); + } + } + + std::shared_ptr session; + ump::LegacyVirtualInput virtualEndpoint; + std::optional customName; + ump::Input connection; + MidiDeviceInfo storedInfo; + ump::ToBytestreamConverter converter { 4096 }; + WaitFreeListeners callbacks; + uint8_t group{}; + MidiInput* owner = nullptr; + SpinLock spinLock; + bool active = false; +}; + +MidiInput::MidiInput() = default; +MidiInput::~MidiInput() = default; + +Array MidiInput::getAvailableDevices() +{ + Array result; + MidiDeviceListConnectionBroadcaster::get().getAllMidiDeviceInfo (ump::IOKind::src, result); + return result; +} + +MidiDeviceInfo MidiInput::getDefaultDevice() +{ + return getAvailableDevices().getFirst(); +} + +std::unique_ptr MidiInput::openDevice (const String& deviceIdentifier, MidiInputCallback* callback) +{ + const auto address = MidiDeviceListConnectionBroadcaster::get().getEndpointGroupForId (ump::IOKind::src, deviceIdentifier); + + if (! address.has_value()) + return {}; + + const auto info = MidiDeviceListConnectionBroadcaster::get().getInfoForId (ump::IOKind::src, deviceIdentifier); + + if (! info.has_value()) + return {}; + + auto session = getLegacySession(); + + if (session == nullptr) + return {}; + + auto connection = session->connectInput (address->endpointId, ump::PacketProtocol::MIDI_1_0); + + if (! connection.isAlive()) + return {}; + + return Impl::make (session, std::move (connection), address->group, *info, callback, {}); +} + +static inline bool isValidMidi1VirtualEndpoint (const std::optional& ep, + ump::BlockDirection dir) +{ + if (! ep.has_value()) + return false; + + if (! ep->hasMidi1Support()) + return false; + + if (ep->getProtocol() != ump::PacketProtocol::MIDI_1_0) + return false; + + if (! ep->hasStaticBlocks()) + return false; + + auto blocks = ep->getBlocks(); + const auto iter = std::find_if (blocks.begin(), blocks.end(), [&] (const ump::Block& b) + { + return b.getDirection() == dir + && b.getNumGroups() == 1 + && b.isEnabled() + && b.getMIDI1ProxyKind() != ump::BlockMIDI1ProxyKind::inapplicable; + }); + + if (iter == blocks.end()) + return false; + + return true; +} + +std::unique_ptr MidiInput::createNewDevice (const String& name, MidiInputCallback* callback) +{ + auto session = getLegacySession(); + + if (! session) + return {}; + + auto port = session->createLegacyVirtualInput (name); + + if (! port) + return {}; + + jassert (isValidMidi1VirtualEndpoint (ump::Endpoints::getInstance()->getEndpoint (port.getId()), + ump::BlockDirection::receiver)); + + auto connection = session->connectInput (port.getId(), ump::PacketProtocol::MIDI_1_0); + + if (! connection) + return {}; + + return Impl::make (session, std::move (connection), 0, { name, {} }, callback, std::move (port)); +} + +void MidiInput::start() +{ + pimpl->start(); +} + +void MidiInput::stop() +{ + pimpl->stop(); +} + +MidiDeviceInfo MidiInput::getDeviceInfo() const noexcept +{ + return pimpl->getDeviceInfo(); +} + +void MidiInput::setName (const String& newName) noexcept +{ + pimpl->setName (newName); +} + +uint8_t MidiInput::getGroup() const +{ + return pimpl->getGroup(); +} + +ump::EndpointId MidiInput::getEndpointId() const +{ + return pimpl->getEndpointId(); +} + +void MidiInput::addCallback (MidiInputCallback& callback) +{ + pimpl->addCallback (callback); +} + +void MidiInput::removeCallback (MidiInputCallback& callback) +{ + pimpl->removeCallback (callback); +} //============================================================================== -MidiOutput::MidiOutput (const String& deviceName, const String& deviceIdentifier) - : Thread (SystemStats::getJUCEVersion() + ": midi out"), deviceInfo (deviceName, deviceIdentifier) +MidiOutput::MidiOutput (std::shared_ptr s, + ump::Output x, + uint8_t g, + const MidiDeviceInfo& i, + ump::LegacyVirtualOutput v) + : session (s), + virtualEndpoint (std::move (v)), + connection (std::move (x)), + storedInfo (i), + group (g) { } -void MidiOutput::sendBlockOfMessagesNow (const MidiBuffer& buffer) +Array MidiOutput::getAvailableDevices() { - for (const auto metadata : buffer) - sendMessageNow (metadata.getMessage()); + Array result; + MidiDeviceListConnectionBroadcaster::get().getAllMidiDeviceInfo (ump::IOKind::dst, result); + return result; } -void MidiOutput::sendBlockOfMessages (const MidiBuffer& buffer, - double millisecondCounterToStartAt, - double samplesPerSecondForBuffer) +std::unique_ptr MidiOutput::openDevice (const String& deviceIdentifier) { - // You've got to call startBackgroundThread() for this to actually work.. - jassert (isThreadRunning()); + const auto address = MidiDeviceListConnectionBroadcaster::get().getEndpointGroupForId (ump::IOKind::dst, deviceIdentifier); - // this needs to be a value in the future - RTFM for this method! - jassert (millisecondCounterToStartAt > 0); + if (! address.has_value()) + return {}; - auto timeScaleFactor = 1000.0 / samplesPerSecondForBuffer; + const auto info = MidiDeviceListConnectionBroadcaster::get().getInfoForId (ump::IOKind::dst, deviceIdentifier); - for (const auto metadata : buffer) - { - auto eventTime = millisecondCounterToStartAt + timeScaleFactor * metadata.samplePosition; - auto* m = new PendingMessage (metadata.data, metadata.numBytes, eventTime); + if (! info.has_value()) + return {}; - const ScopedLock sl (lock); + auto session = getLegacySession(); - if (firstMessage == nullptr || firstMessage->message.getTimeStamp() > eventTime) - { - m->next = firstMessage; - firstMessage = m; - } - else - { - auto* mm = firstMessage; + if (session == nullptr) + return {}; - while (mm->next != nullptr && mm->next->message.getTimeStamp() <= eventTime) - mm = mm->next; + auto connection = session->connectOutput (address->endpointId); - m->next = mm->next; - mm->next = m; - } - } + if (! connection.isAlive()) + return {}; - notify(); + return rawToUniquePtr (new MidiOutput (session, std::move (connection), address->group, *info, {})); } -void MidiOutput::clearAllPendingMessages() +std::unique_ptr MidiOutput::createNewDevice (const String& name) { - const ScopedLock sl (lock); + auto session = getLegacySession(); - while (firstMessage != nullptr) - { - auto* m = firstMessage; - firstMessage = firstMessage->next; - delete m; - } + if (! session) + return {}; + + auto port = session->createLegacyVirtualOutput (name); + + if (! port) + return {}; + + jassert (isValidMidi1VirtualEndpoint (ump::Endpoints::getInstance()->getEndpoint (port.getId()), + ump::BlockDirection::sender)); + + auto connection = session->connectOutput (port.getId()); + + if (! connection) + return {}; + + return rawToUniquePtr (new MidiOutput (session, std::move (connection), 0, { name, {} }, std::move (port))); } -void MidiOutput::startBackgroundThread() +MidiDeviceInfo MidiOutput::getDeviceInfo() const noexcept { - startThread (Priority::high); -} - -void MidiOutput::stopBackgroundThread() -{ - stopThread (5000); -} - -void MidiOutput::run() -{ - while (! threadShouldExit()) - { - auto now = Time::getMillisecondCounter(); - uint32 eventTime = 0; - uint32 timeToWait = 500; - - PendingMessage* message; - - { - const ScopedLock sl (lock); - message = firstMessage; - - if (message != nullptr) - { - eventTime = (uint32) roundToInt (message->message.getTimeStamp()); - - if (eventTime > now + 20) - { - timeToWait = eventTime - (now + 20); - message = nullptr; - } - else - { - firstMessage = message->next; - } - } - } - - if (message != nullptr) - { - std::unique_ptr messageDeleter (message); - - if (eventTime > now) - { - Time::waitForMillisecondCounter (eventTime); - - if (threadShouldExit()) - break; - } - - if (eventTime > now - 200) - sendMessageNow (message->message); - } - else - { - jassert (timeToWait < 1000 * 30); - wait ((int) timeToWait); - } - } - - clearAllPendingMessages(); + return customName.has_value() ? storedInfo.withName (*customName) : storedInfo; } } // namespace juce diff --git a/modules/juce_audio_devices/midi_io/juce_MidiDevices.h b/modules/juce_audio_devices/midi_io/juce_MidiDevices.h index b7ba68d01b..3d48cd64a8 100644 --- a/modules/juce_audio_devices/midi_io/juce_MidiDevices.h +++ b/modules/juce_audio_devices/midi_io/juce_MidiDevices.h @@ -35,8 +35,6 @@ namespace juce { -class MidiDeviceListConnectionBroadcaster; - /** To find out when the available MIDI devices change, call MidiDeviceListConnection::make(), passing a lambda that will be called on each configuration change. @@ -67,22 +65,6 @@ public: */ MidiDeviceListConnection() = default; - MidiDeviceListConnection (const MidiDeviceListConnection&) = delete; - MidiDeviceListConnection (MidiDeviceListConnection&& other) noexcept - : broadcaster (std::exchange (other.broadcaster, nullptr)), - key (std::exchange (other.key, Key{})) - { - } - - MidiDeviceListConnection& operator= (const MidiDeviceListConnection&) = delete; - MidiDeviceListConnection& operator= (MidiDeviceListConnection&& other) noexcept - { - MidiDeviceListConnection (std::move (other)).swap (*this); - return *this; - } - - ~MidiDeviceListConnection() noexcept; - /** Clears this connection. If this object had an active connection, that connection will be deactivated, and the @@ -90,7 +72,7 @@ public: */ void reset() noexcept { - MidiDeviceListConnection().swap (*this); + token.reset(); } /** Registers a function to be called whenever the midi device list changes. @@ -102,22 +84,12 @@ public: static MidiDeviceListConnection make (std::function); private: - MidiDeviceListConnection (MidiDeviceListConnectionBroadcaster* b, const Key k) - : broadcaster (b), key (k) {} - - void swap (MidiDeviceListConnection& other) noexcept - { - std::swap (other.broadcaster, broadcaster); - std::swap (other.key, key); - } - - MidiDeviceListConnectionBroadcaster* broadcaster = nullptr; - Key key = {}; + ErasedScopeGuard token; }; //============================================================================== /** - This struct contains information about a MIDI input or output device. + This struct contains information about a MIDI 1.0 input or output port. You can get one of these structs by calling the static getAvailableDevices() or getDefaultDevice() methods of MidiInput and MidiOutput or by calling getDeviceInfo() @@ -148,22 +120,29 @@ struct MidiDeviceInfo /** The identifier for this device. - This will be provided by the OS and it's format will differ on different systems + This will be provided by the OS and its format will differ on different systems e.g. on macOS it will be a number whereas on Windows it will be a long alphanumeric string. */ String identifier; + [[nodiscard]] MidiDeviceInfo withName (String x) const { return withMember (*this, &MidiDeviceInfo::name, x); } + [[nodiscard]] MidiDeviceInfo withIdentifier (String x) const { return withMember (*this, &MidiDeviceInfo::identifier, x); } + //============================================================================== - auto tie() const { return std::tie (name, identifier); } - bool operator== (const MidiDeviceInfo& other) const noexcept { return tie() == other.tie(); } - bool operator!= (const MidiDeviceInfo& other) const noexcept { return tie() != other.tie(); } + bool operator== (const MidiDeviceInfo& other) const noexcept + { + const auto tie = [] (auto& x) { return std::tuple (x.name, x.identifier); }; + return tie (*this) == tie (other); + } + + bool operator!= (const MidiDeviceInfo& other) const noexcept { return ! operator== (other); } }; class MidiInputCallback; //============================================================================== /** - Represents a midi input device. + Represents a midi input device using the old bytestream format. To create one of these, use the static getAvailableDevices() method to find out what inputs are available, and then use the openDevice() method to try to open one. @@ -175,6 +154,12 @@ class MidiInputCallback; class JUCE_API MidiInput final { public: + MidiInput (MidiInput&&) = delete; + + MidiInput& operator= (MidiInput&&) = delete; + + ~MidiInput(); + //============================================================================== /** Returns a list of the available midi input devices. @@ -196,13 +181,14 @@ public: @param deviceIdentifier the ID of the device to open - use the getAvailableDevices() method to find the available devices that can be opened - @param callback the object that will receive the midi messages from this device + @param callback the object that will receive the midi messages from this device, + you can also add and remove receivers with + addCallback() and removeCallback() @see MidiInputCallback, getDevices */ - static std::unique_ptr openDevice (const String& deviceIdentifier, MidiInputCallback* callback); + static std::unique_ptr openDevice (const String& deviceIdentifier, MidiInputCallback* callback = nullptr); - #if JUCE_LINUX || JUCE_BSD || JUCE_MAC || JUCE_IOS || DOXYGEN /** This will try to create a new midi input device (only available on Linux, macOS and iOS). This will attempt to create a new midi input device with the specified name for other @@ -216,13 +202,9 @@ public: @param deviceName the name of the device to create @param callback the object that will receive the midi messages from this device */ - static std::unique_ptr createNewDevice (const String& deviceName, MidiInputCallback* callback); - #endif + static std::unique_ptr createNewDevice (const String& deviceName, MidiInputCallback* callback = nullptr); //============================================================================== - /** Destructor. */ - ~MidiInput(); - /** Starts the device running. After calling this, the device will start sending midi messages to the MidiInputCallback @@ -239,37 +221,37 @@ public: void stop(); /** Returns the MidiDeviceInfo struct containing some information about this device. */ - MidiDeviceInfo getDeviceInfo() const noexcept { return deviceInfo; } + MidiDeviceInfo getDeviceInfo() const noexcept; /** Returns the identifier of this device. */ - String getIdentifier() const noexcept { return deviceInfo.identifier; } + String getIdentifier() const noexcept { return getDeviceInfo().identifier; } /** Returns the name of this device. */ - String getName() const noexcept { return deviceInfo.name; } + String getName() const noexcept { return getDeviceInfo().name; } /** Sets a custom name for the device. */ - void setName (const String& newName) noexcept { deviceInfo.name = newName; } + void setName (const String& newName) noexcept; - //============================================================================== - /** @cond */ - [[deprecated ("Use getAvailableDevices instead.")]] - static StringArray getDevices(); - [[deprecated ("Use getDefaultDevice instead.")]] - static int getDefaultDeviceIndex(); - [[deprecated ("Use openDevice that takes a device identifier instead.")]] - static std::unique_ptr openDevice (int, MidiInputCallback*); - /** @endcond */ + /** In the case that this input refers to a specific group of a UMP input, this returns the + index of the group. + */ + uint8_t getGroup() const; - /** @internal */ - class Pimpl; + /** Returns the EndpointId that uniquely identifies the UMP endpoint that contains this input. */ + ump::EndpointId getEndpointId() const; + + /** Adds an input listener. */ + void addCallback (MidiInputCallback&); + + /** Removed an input listener. */ + void removeCallback (MidiInputCallback&); private: + class Impl; + MidiInput(); + //============================================================================== - explicit MidiInput (const String&, const String&); - - MidiDeviceInfo deviceInfo; - - std::unique_ptr internal; + std::unique_ptr pimpl; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiInput) }; @@ -316,15 +298,15 @@ public: The message passed in will contain the start of a sysex, but won't be finished with the terminating 0xf7 byte. */ - virtual void handlePartialSysexMessage (MidiInput* source, - const uint8* messageData, - int numBytesSoFar, - double timestamp); + virtual void handlePartialSysexMessage ([[maybe_unused]] MidiInput* source, + [[maybe_unused]] const uint8* messageData, + [[maybe_unused]] int numBytesSoFar, + [[maybe_unused]] double timestamp) {} }; //============================================================================== /** - Represents a midi output device. + Represents a midi output device using the old bytestream format. To create one of these, use the static getAvailableDevices() method to find out what outputs are available, and then use the openDevice() method to try to open one. @@ -333,7 +315,7 @@ public: @tags{Audio} */ -class JUCE_API MidiOutput final : private Thread +class JUCE_API MidiOutput final { public: //============================================================================== @@ -346,7 +328,10 @@ public: static Array getAvailableDevices(); /** Returns the MidiDeviceInfo of the default midi output device to use. */ - static MidiDeviceInfo getDefaultDevice(); + static MidiDeviceInfo getDefaultDevice() + { + return getAvailableDevices().getFirst(); + } /** Tries to open one of the midi output devices. @@ -361,7 +346,6 @@ public: */ static std::unique_ptr openDevice (const String& deviceIdentifier); - #if JUCE_LINUX || JUCE_BSD || JUCE_MAC || JUCE_IOS || DOXYGEN /** This will try to create a new midi output device (only available on Linux, macOS and iOS). This will attempt to create a new midi output device with the specified name that other @@ -375,30 +359,45 @@ public: @param deviceName the name of the device to create */ static std::unique_ptr createNewDevice (const String& deviceName); - #endif - - //============================================================================== - /** Destructor. */ - ~MidiOutput() override; /** Returns the MidiDeviceInfo struct containing some information about this device. */ - MidiDeviceInfo getDeviceInfo() const noexcept { return deviceInfo; } + MidiDeviceInfo getDeviceInfo() const noexcept; /** Returns the identifier of this device. */ - String getIdentifier() const noexcept { return deviceInfo.identifier; } + String getIdentifier() const noexcept { return getDeviceInfo().identifier; } /** Returns the name of this device. */ - String getName() const noexcept { return deviceInfo.name; } + String getName() const noexcept { return getDeviceInfo().name; } /** Sets a custom name for the device. */ - void setName (const String& newName) noexcept { deviceInfo.name = newName; } + void setName (const String& newName) noexcept { customName = newName; } + + /** In the case that this output refers to a specific group of a UMP output, this returns the + index of the group. + */ + uint8_t getGroup() const { return group; } + + /** Returns the EndpointId that uniquely identifies the UMP endpoint that contains this output. */ + ump::EndpointId getEndpointId() const { return connection.getEndpointId(); } //============================================================================== /** Sends out a MIDI message immediately. */ - void sendMessageNow (const MidiMessage& message); + void sendMessageNow (const MidiMessage& message) + { + converter.convert ({ group, message.asSpan() }, [this] (const ump::View& view) + { + ump::Iterator b (view.data(), view.size()); + auto e = std::next (b); + connection.send (b, e); + }); + } /** Sends out a sequence of MIDI messages immediately. */ - void sendBlockOfMessagesNow (const MidiBuffer& buffer); + void sendBlockOfMessagesNow (const MidiBuffer& buffer) + { + for (const auto metadata : buffer) + sendMessageNow (metadata.getMessage()); + } /** This lets you supply a block of messages that will be sent out at some point in the future. @@ -419,62 +418,60 @@ public: */ void sendBlockOfMessages (const MidiBuffer& buffer, double millisecondCounterToStartAt, - double samplesPerSecondForBuffer); + double samplesPerSecondForBuffer) + { + // This needs to be a value in the future - check the documentation for this function! + jassert (millisecondCounterToStartAt > 0); - /** Gets rid of any midi messages that had been added by sendBlockOfMessages(). */ - void clearAllPendingMessages(); + const auto timeScaleFactor = 1000.0 / samplesPerSecondForBuffer; + + for (const auto item : buffer) + { + auto msg = item.getMessage(); + msg.setTimeStamp (millisecondCounterToStartAt + timeScaleFactor * msg.getTimeStamp()); + outputThread.addEvent (msg); + } + } + + /** Gets rid of any midi messages that had been added by sendBlockOfMessages(). + */ + void clearAllPendingMessages() { outputThread.clearAllPendingMessages(); } /** Starts up a background thread so that the device can send blocks of data. Call this to get the device ready, before using sendBlockOfMessages(). */ - void startBackgroundThread(); + void startBackgroundThread() { outputThread.start(); } /** Stops the background thread, and clears any pending midi events. @see startBackgroundThread */ - void stopBackgroundThread(); + void stopBackgroundThread() { outputThread.stop(); } /** Returns true if the background thread used to send blocks of data is running. + @see startBackgroundThread, stopBackgroundThread */ - bool isBackgroundThreadRunning() const noexcept { return isThreadRunning(); } - - //============================================================================== - /** @cond */ - [[deprecated ("Use getAvailableDevices instead.")]] - static StringArray getDevices(); - [[deprecated ("Use getDefaultDevice instead.")]] - static int getDefaultDeviceIndex(); - [[deprecated ("Use openDevice that takes a device identifier instead.")]] - static std::unique_ptr openDevice (int); - /** @endcond */ - - /** @internal */ - class Pimpl; + bool isBackgroundThreadRunning() const { return outputThread.isRunning(); } private: + MidiOutput (std::shared_ptr, + ump::Output, + uint8_t, + const MidiDeviceInfo&, + ump::LegacyVirtualOutput); + //============================================================================== - struct PendingMessage + std::shared_ptr session; + ump::LegacyVirtualOutput virtualEndpoint; + std::optional customName; + ump::Output connection; + MidiDeviceInfo storedInfo; + ump::ToUMP1Converter converter; + uint8_t group{}; + ScheduledEventThread outputThread { [this] (const MidiMessage& message) { - PendingMessage (const void* data, int len, double timeStamp) - : message (data, len, timeStamp) - { - } - - MidiMessage message; - PendingMessage* next; - }; - - //============================================================================== - explicit MidiOutput (const String&, const String&); - void run() override; - - MidiDeviceInfo deviceInfo; - - std::unique_ptr internal; - - CriticalSection lock; - PendingMessage* firstMessage = nullptr; + sendMessageNow (message); + } }; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiOutput) }; diff --git a/modules/juce_audio_devices/midi_io/juce_ScheduledEventThread.h b/modules/juce_audio_devices/midi_io/juce_ScheduledEventThread.h new file mode 100644 index 0000000000..824d2823fc --- /dev/null +++ b/modules/juce_audio_devices/midi_io/juce_ScheduledEventThread.h @@ -0,0 +1,164 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +/** + @internal + + Allows events to be queued up, then for each event calls the OutputCallback at the time + dictated by that event's timestamp. + + Event must have a getTimeStamp() member function that returns the output time of the event. +*/ +template +class ScheduledEventThread : private Thread +{ +public: + using OutputCallback = std::function; + + explicit ScheduledEventThread (OutputCallback&& c) + : Thread (SystemStats::getJUCEVersion() + ": MIDI Out"), + outputCallback (std::move (c)) + { + jassert (outputCallback != nullptr); + } + + ~ScheduledEventThread() override + { + stop(); + } + + void clearAllPendingMessages() + { + { + const std::scoped_lock sl (mutex); + pendingMessages.clear(); + } + + condvar.notify_one(); + } + + void start() + { + { + const std::scoped_lock sl (mutex); + backgroundThreadRunning = true; + } + + startThread (Priority::high); + } + + void stop() + { + { + const std::scoped_lock sl (mutex); + backgroundThreadRunning = false; + } + + condvar.notify_one(); + stopThread (-1); + } + + void addEvent (const Event& event) + { + // You've got to call startBackgroundThread() for this to actually work. + jassert (isThreadRunning()); + + { + const std::scoped_lock sl (mutex); + pendingMessages.insert (event); + } + + condvar.notify_one(); + } + + bool isRunning() const + { + const std::scoped_lock sl (mutex); + return backgroundThreadRunning; + } + +private: + void run() override + { + for (;;) + { + std::unique_lock lock (mutex); + condvar.wait (lock, [&] + { + return ! pendingMessages.empty() || ! backgroundThreadRunning; + }); + + if (! backgroundThreadRunning) + return; + + const auto now = Time::getMillisecondCounter(); + const auto event = *pendingMessages.begin(); + pendingMessages.erase (pendingMessages.begin()); + + const auto timestamp = event.getTimeStamp(); + + if (timestamp > now + 20) + { + const auto millis = static_cast (timestamp - (now + 20)); + condvar.wait_for (lock, std::chrono::milliseconds (millis)); + continue; + } + + if (timestamp > now) + Time::waitForMillisecondCounter ((uint32) timestamp); + + if (timestamp > now - 200) + outputCallback (event); + } + } + + struct Comparator + { + bool operator() (const Event& a, const Event& b) const + { + return a.getTimeStamp() < b.getTimeStamp(); + } + }; + + mutable std::mutex mutex; + std::condition_variable condvar; + std::multiset pendingMessages; + OutputCallback outputCallback; + bool backgroundThreadRunning = false; +}; + +} // namespace juce diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPBlock.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPBlock.h new file mode 100644 index 0000000000..a3d9542888 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPBlock.h @@ -0,0 +1,159 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +//============================================================================== +/** + Represents a Function Block (FB) or Group Terminal Block (GTB). + + GTBs are only available in USB MIDI, so devices on non-USB transports will not expose GTB + information. In the case that a device only exposes GTBs and not FBs, default values will + be used for any fields that are unavailable in the GTB definition. + + @see BlockInfo + + @tags{Audio} +*/ +class Block +{ +public: + Block() = default; + + [[nodiscard]] Block withName (const String& x) const { return withMember (*this, &Block::name, x); } + + [[nodiscard]] Block withFirstGroup (uint8_t x) const { return withMember (*this, &Block::info, info.withFirstGroup (x)); } + [[nodiscard]] Block withNumGroups (uint8_t x) const { return withMember (*this, &Block::info, info.withNumGroups (x)); } + [[nodiscard]] Block withMaxSysex8Streams (uint8_t x) const { return withMember (*this, &Block::info, info.withMaxSysex8Streams (x)); } + [[nodiscard]] Block withEnabled (bool x = true) const { return withMember (*this, &Block::info, info.withEnabled (x)); } + + [[nodiscard]] Block withUiHint (BlockUiHint x) const + { + return withMember (*this, &Block::info, info.withUiHint (x)); + } + + [[nodiscard]] Block withMIDI1ProxyKind (BlockMIDI1ProxyKind x) const + { + return withMember (*this, &Block::info, info.withMIDI1ProxyKind (x)); + } + + [[nodiscard]] Block withDirection (BlockDirection x) const + { + return withMember (*this, &Block::info, info.withDirection (x)); + } + + /** The name of the block, if any. */ + String getName() const { return name; } + + /** All info relating to this block. */ + BlockInfo getInfo() const { return info; } + + /** In the range 0x0 to 0xF inclusive. */ + uint8_t getFirstGroup() const + { + return info.getFirstGroup(); + } + + /** In the range 0x01 to 0x10 inclusive. */ + uint8_t getNumGroups() const + { + return info.getNumGroups(); + } + + /** The number of simultaneous Sysex8 streams that are supported. */ + uint8_t getMaxSysex8Streams() const + { + return info.getMaxSysex8Streams(); + } + + /** Some blocks may support bidirectional communication (e.g. for CI) but function predominantly + as a sender or receiver. The direction returned here is a hint to the user that doesn't + necessarily reflect the hardware capabilities. + */ + BlockUiHint getUiHint() const + { + return info.getUiHint(); + } + + /** If this block is a proxy for a MIDI 1.0 stream, describes the capabilities of that stream. */ + BlockMIDI1ProxyKind getMIDI1ProxyKind() const + { + return info.getMIDI1ProxyKind(); + } + + /** Returns the message transmission directions that are supported by this block. + Use this to determine block capabilities. + For information that will be displayed to the user, prefer getUIHint(). + */ + BlockDirection getDirection() const + { + return info.getDirection(); + } + + /** True if this block is enabled. */ + bool isEnabled() const + { + return info.isEnabled(); + } + + /** True if the names of the two blocks match. */ + bool nameMatches (const Block& other) const + { + return name == other.name; + } + + /** True if the BlockInfo of the two blocks match. */ + bool infoMatches (const Block& other) const + { + return info == other.info; + } + + bool operator== (const Block& other) const + { + const auto tie = [] (auto& x) { return std::tuple (x.info, x.name); }; + return tie (*this) == tie (other); + } + + bool operator!= (const Block& other) const + { + return ! operator== (other); + } + +private: + BlockInfo info; + String name; +}; + +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPDisconnectionListener.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPDisconnectionListener.h new file mode 100644 index 0000000000..e5a4e338f3 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPDisconnectionListener.h @@ -0,0 +1,60 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +//============================================================================== +/** + An interface class for entities that are interested in disconnection notifications. + + @tags{Audio} +*/ +struct DisconnectionListener +{ + DisconnectionListener() = default; + + DisconnectionListener (const DisconnectionListener&) = default; + DisconnectionListener (DisconnectionListener&&) noexcept = default; + + DisconnectionListener& operator= (const DisconnectionListener&) = default; + DisconnectionListener& operator= (DisconnectionListener&&) noexcept = default; + + virtual ~DisconnectionListener() = default; + + /** Called to notify that the system has destroyed/disconnected a resource. */ + virtual void disconnected() = 0; +}; + +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoint.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoint.h new file mode 100644 index 0000000000..e216f4d550 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoint.h @@ -0,0 +1,187 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +//============================================================================== +/** + Represents a single MIDI endpoint, which may have up to one input and up to one output. + + An Endpoint object just holds a snapshot of the physical endpoint's last known state at the + point when the Endpoint instance was created. + Instead of storing Endpoint instances, it's a better idea to store an EndpointId, and to call + Endpoints::getEndpoint() to get an up-to-date snapshot. + + To connect to an endpoint, use Session::connectInput() or Session::connectOutput() to create + a connection in the context of a particular session. + + @tags{Audio} +*/ +class Endpoint +{ +public: + Endpoint() = default; + + [[nodiscard]] Endpoint withName (String x) const { return withMember (*this, &Endpoint::name, x); } + [[nodiscard]] Endpoint withProtocol (std::optional x) const { return withMember (*this, &Endpoint::streamConfig, StreamConfigFlags ((streamConfig & ~maskProtocol) | asFlags (x))); } + [[nodiscard]] Endpoint withDeviceInfo (DeviceInfo x) const { return withMember (*this, &Endpoint::deviceInfo, std::move (x)); } + [[nodiscard]] Endpoint withProductInstanceId (String x) const { return withMember (*this, &Endpoint::productInstanceId, std::move (x)); } + [[nodiscard]] Endpoint withUMPVersion (uint8_t major, uint8_t minor) const { return withMember (*this, &Endpoint::endpointInfo, endpointInfo.withVersion (major, minor)); } + [[nodiscard]] Endpoint withStaticBlocks (bool x = true) const { return withMember (*this, &Endpoint::endpointInfo, endpointInfo.withStaticFunctionBlocks (x)); } + [[nodiscard]] Endpoint withMidi1Support (bool x = true) const { return withMember (*this, &Endpoint::endpointInfo, endpointInfo.withMidi1Support (x)); } + [[nodiscard]] Endpoint withMidi2Support (bool x = true) const { return withMember (*this, &Endpoint::endpointInfo, endpointInfo.withMidi2Support (x)); } + [[nodiscard]] Endpoint withReceiveJRSupport (bool x = true) const { return withMember (*this, &Endpoint::endpointInfo, endpointInfo.withReceiveJRSupport (x)); } + [[nodiscard]] Endpoint withTransmitJRSupport (bool x = true) const { return withMember (*this, &Endpoint::endpointInfo, endpointInfo.withTransmitJRSupport (x)); } + + [[nodiscard]] Endpoint withReceiveJREnabled (bool x = true) const { return withMember (*this, &Endpoint::streamConfig, StreamConfigFlags (x ? (streamConfig | maskRxjr) : (streamConfig & ~maskRxjr))); } + [[nodiscard]] Endpoint withTransmitJREnabled (bool x = true) const { return withMember (*this, &Endpoint::streamConfig, StreamConfigFlags (x ? (streamConfig | maskTxjr) : (streamConfig & ~maskTxjr))); } + + /** The block index is used to uniquely identify the block, so be sure to always declare + blocks in a consistent order. + */ + [[nodiscard]] Endpoint withBlocks (Span x) const + { + auto result = withNumBlocks (x.size()); + std::copy (x.begin(), x.end(), result.blocks.begin()); + return result; + } + + /** The number of blocks on this endpoint. */ + [[nodiscard]] Endpoint withNumBlocks (size_t x) const + { + jassert (x <= blocks.size()); + auto result = *this; + result.endpointInfo = result.endpointInfo.withNumFunctionBlocks ((uint8_t) std::min (blocks.size(), x)); + return result; + } + + /** Returns the name of this endpoint. */ + String getName() const { return name; } + + /** Returns properties of the device that owns the endpoint. */ + DeviceInfo getDeviceInfo() const + { + return deviceInfo; + } + + /** Returns the product instance ID if available, or an empty string otherwise. + The product instance ID should match the device serial number, and should be unique per + manufacturer, family, and model. + + This ID can be used to distinguish between separate devices that have the same DeviceInfo. + It can also be used to determine whether separate endpoints are associated with the same + device. + */ + String getProductInstanceId() const + { + return productInstanceId; + } + + /** The protocol that the endpoint currently expects to send and receive; endpoints are allowed + to switch protocols, so this won't always return the same value. + May return nullopt if the protocol is unknown, perhaps because negotiation has not taken place. + */ + std::optional getProtocol() const + { + switch (streamConfig & maskProtocol) + { + case 1: return ump::PacketProtocol::MIDI_1_0; + case 2: return ump::PacketProtocol::MIDI_2_0; + } + + return {}; + } + + uint8_t getUMPVersionMajor() const { return endpointInfo.getVersionMajor(); } + uint8_t getUMPVersionMinor() const { return endpointInfo.getVersionMinor(); } + bool hasStaticBlocks() const { return endpointInfo.hasStaticFunctionBlocks(); } + bool hasMidi1Support() const { return endpointInfo.hasMidi1Support(); } + bool hasMidi2Support() const { return endpointInfo.hasMidi2Support(); } + bool hasReceiveJRSupport() const { return endpointInfo.hasReceiveJRSupport(); } + bool hasTransmitJRSupport() const { return endpointInfo.hasTransmitJRSupport(); } + + bool isReceiveJREnabled() const { return streamConfig & maskRxjr; } + bool isTransmitJREnabled() const { return streamConfig & maskTxjr; } + + /** There can be a maximum of 32 blocks. + This may return an empty span if the endpoint has neither function blocks nor group terminal + blocks. + */ + Span getBlocks() const& + { + return { blocks.data(), endpointInfo.getNumFunctionBlocks() }; + } + + /** Returns a mutable view over the blocks in this endpoint. */ + Span getBlocks() & + { + return { blocks.data(), endpointInfo.getNumFunctionBlocks() }; + } + + // These are deleted because it's probably not a good idea to create a span over a temporary + Span getBlocks() const&& = delete; + Span getBlocks() && = delete; + +private: + enum StreamConfigFlags : uint16_t + { + maskProtocol = 0x00ff, + maskTxjr = 0x0100, + maskRxjr = 0x0200, + }; + + static StreamConfigFlags asFlags (std::optional p) + { + if (! p.has_value()) + return {}; + + switch (*p) + { + case ump::PacketProtocol::MIDI_1_0: return StreamConfigFlags { 1 }; + case ump::PacketProtocol::MIDI_2_0: return StreamConfigFlags { 2 }; + } + + return {}; + } + + std::array blocks; + String name; + String productInstanceId; + EndpointInfo endpointInfo; + DeviceInfo deviceInfo; + StreamConfigFlags streamConfig{}; +}; + +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPEndpointId.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPEndpointId.h new file mode 100644 index 0000000000..710c90608d --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPEndpointId.h @@ -0,0 +1,96 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +/** Directional properties of a MIDI endpoint. */ +enum class IOKind : uint8_t +{ + src, ///< A source of MIDI events + dst, ///< A destination for MIDI events +}; + +/** All possible MIDI directions. */ +constexpr IOKind ioKinds[] { IOKind::src, IOKind::dst }; + +//============================================================================== +/** + Identifies a MIDI endpoint. This is intended to be an opaque type that can only be compared with + other instances. + + For backwards compatibility, we need to ensure that port identifier strings that used to work + with MidiInput and MidiOutput continue to function in the same way. However, the old identifiers + weren't necessarily unique between inputs and outputs (a MIDI 1.0 input and output could have + the same ID), which means that a single id string isn't enough to uniquely identify an input + or output port. + + @tags{Audio} +*/ +class EndpointId +{ + auto tie() const { return std::tuple (src, dst); } + +public: + /** @internal */ + class Impl; + + EndpointId() = default; + + bool operator== (const EndpointId& x) const { return tie() == x.tie(); } + bool operator!= (const EndpointId& x) const { return tie() != x.tie(); } + bool operator< (const EndpointId& x) const { return tie() < x.tie(); } + bool operator<= (const EndpointId& x) const { return tie() <= x.tie(); } + bool operator> (const EndpointId& x) const { return tie() > x.tie(); } + bool operator>= (const EndpointId& x) const { return tie() >= x.tie(); } + + String get (IOKind k) const { return k == IOKind::src ? src : dst; } + + static EndpointId make (IOKind dir, const String& id) + { + return dir == IOKind::src ? makeSrcDst (id, {}) : makeSrcDst ({}, id); + } + + static EndpointId makeSrcDst (const String& s, const String& d) + { + return { s, d }; + } + + String src, dst; + +private: + EndpointId (const String& s, const String& d) : src (s), dst (d) {} +}; + +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.cpp b/modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.cpp new file mode 100644 index 0000000000..f2ab98beba --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.cpp @@ -0,0 +1,243 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +class Endpoints::Impl : private EndpointsListener +{ +public: + class Native + { + public: + virtual ~Native() = default; + virtual Backend getBackend() const = 0; + virtual bool isVirtualMidiBytestreamServiceActive() const = 0; + virtual bool isVirtualMidiUmpServiceActive() const = 0; + virtual void setVirtualMidiBytestreamServiceActive (bool) {} + virtual void setVirtualMidiUmpServiceActive (bool) {} + virtual void getEndpoints (std::vector&) const = 0; + virtual std::optional getEndpoint (const EndpointId&) const = 0; + virtual std::optional getStaticDeviceInfo (const EndpointId&) const = 0; + virtual std::unique_ptr makeSession (const String&) = 0; + + static std::unique_ptr make (EndpointsListener&); + }; + + Backend getBackend() const + { + return native->getBackend(); + } + + bool isVirtualMidiBytestreamServiceActive() const + { + return native->isVirtualMidiBytestreamServiceActive(); + } + + bool isVirtualMidiUmpServiceActive() const + { + return native->isVirtualMidiUmpServiceActive(); + } + + void setVirtualMidiBytestreamServiceActive (bool x) + { + native->setVirtualMidiBytestreamServiceActive (x); + } + + void setVirtualMidiUmpServiceActive (bool x) + { + native->setVirtualMidiUmpServiceActive (x); + } + + void getEndpoints (std::vector& x) const + { + native->getEndpoints (x); + } + + std::optional getEndpoint (const EndpointId& x) const + { + return native->getEndpoint (x); + } + + std::optional getStaticDeviceInfo (const EndpointId& x) const + { + return native->getStaticDeviceInfo (x); + } + + std::unique_ptr makeSession (const String& x) + { + return native->makeSession (x); + } + + void addListener (EndpointsListener& x) + { + listeners.add (&x); + } + + void removeListener (EndpointsListener& x) + { + listeners.remove (&x); + } + + static std::unique_ptr make() + { + auto result = rawToUniquePtr (new Impl); + result->native = Native::make (*result); + + if (result->native == nullptr) + return {}; + + return result; + } + + static String getGlobalMidiClientName() + { + if (auto* app = JUCEApplicationBase::getInstance()) + return app->getApplicationName(); + + return "JUCE"; + } + +private: + void endpointsChanged() override + { + listeners.call ([] (auto& l) { l.endpointsChanged(); }); + } + + void virtualMidiServiceActiveChanged() override + { + listeners.call ([] (auto& l) { l.virtualMidiServiceActiveChanged(); }); + } + + Impl() = default; + + ListenerList listeners; + std::unique_ptr native; +}; + +void Endpoints::getEndpoints (std::vector& x) const +{ + x.clear(); + + if (impl != nullptr) + impl->getEndpoints (x); +} + +std::vector Endpoints::getEndpoints() const +{ + std::vector result; + getEndpoints (result); + return result; +} + +std::optional Endpoints::getEndpoint (const EndpointId& x) const +{ + if (impl != nullptr) + return impl->getEndpoint (x); + + return {}; +} + +std::optional Endpoints::getStaticDeviceInfo (const EndpointId& x) const +{ + if (impl != nullptr) + return impl->getStaticDeviceInfo (x); + + return {}; +} + +void Endpoints::addListener (EndpointsListener& x) +{ + if (impl != nullptr) + impl->addListener (x); +} + +void Endpoints::removeListener (EndpointsListener& x) +{ + if (impl != nullptr) + impl->addListener (x); +} + +Session Endpoints::makeSession (const String& x) const +{ + return Session::Impl::makeSession (impl != nullptr ? impl->makeSession (x) : nullptr); +} + +std::optional Endpoints::getBackend() const +{ + if (impl != nullptr) + return impl->getBackend(); + + return {}; +} + +bool Endpoints::isVirtualMidiBytestreamServiceActive() const +{ + if (impl != nullptr) + return impl->isVirtualMidiBytestreamServiceActive(); + + return false; +} + +bool Endpoints::isVirtualMidiUmpServiceActive() const +{ + if (impl != nullptr) + return impl->isVirtualMidiUmpServiceActive(); + + return false; +} + +void Endpoints::setVirtualMidiBytestreamServiceActive (bool x) +{ + if (impl != nullptr) + impl->setVirtualMidiBytestreamServiceActive (x); +} + +void Endpoints::setVirtualMidiUmpServiceActive (bool x) +{ + if (impl != nullptr) + impl->setVirtualMidiUmpServiceActive (x); +} + +Endpoints::Endpoints() + : impl (Impl::make()) +{ +} + +Endpoints::~Endpoints() +{ + clearSingletonInstance(); +} + +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.h new file mode 100644 index 0000000000..b321014137 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPEndpoints.h @@ -0,0 +1,277 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +/** + An interface class for types that are interested in receiving updates + about changes to available MIDI endpoints. + + @tags{Audio} +*/ +struct EndpointsListener +{ + EndpointsListener() = default; + + EndpointsListener (const EndpointsListener&) = default; + EndpointsListener (EndpointsListener&&) noexcept = default; + + EndpointsListener& operator= (const EndpointsListener&) = default; + EndpointsListener& operator= (EndpointsListener&&) noexcept = default; + + virtual ~EndpointsListener() = default; + + /** Called on each platform to notify listeners that some aspect of the MIDI configuration + has changed, including device connection, disconnection, and property changes. + */ + virtual void endpointsChanged() = 0; + + /** Called on Android to indicate that the session managing the virtual MIDI ports + was started or stopped. + Creating a virtual endpoint will fail if the service is not running, so you may wish to + listen for this event, and to create the virtual ports after this function has been called. + You can query the current state of the service using Endpoints::isVirtualUmpServiceActive() + and Endpoints::isVirtualBytestreamServiceActive(). + */ + virtual void virtualMidiServiceActiveChanged() {} +}; + +/** + Static information about a particular MIDI device that can be queried without opening a + connection to the device. + + This information differs from the detailed information in the Endpoint struct, in that + the StaticDeviceInformation is communicated out-of-band, whereas Endpoint information is + communicated 'in-band', i.e. it is sent as MIDI messages after establishing a connection to the + device. + + @tags{Audio} +*/ +class StaticDeviceInfo +{ +public: + [[nodiscard]] StaticDeviceInfo withName (const String& x) const { return withMember (*this, &StaticDeviceInfo::name, x); } + [[nodiscard]] StaticDeviceInfo withManufacturer (const String& x) const { return withMember (*this, &StaticDeviceInfo::manufacturer, x); } + [[nodiscard]] StaticDeviceInfo withProduct (const String& x) const { return withMember (*this, &StaticDeviceInfo::product, x); } + [[nodiscard]] StaticDeviceInfo withHasSource (bool x) const { return withMember (*this, &StaticDeviceInfo::source, x); } + [[nodiscard]] StaticDeviceInfo withHasDestination (bool x) const { return withMember (*this, &StaticDeviceInfo::destination, x); } + [[nodiscard]] StaticDeviceInfo withTransport (Transport x) const { return withMember (*this, &StaticDeviceInfo::transport, x); } + + [[nodiscard]] StaticDeviceInfo withLegacyIdentifiersSrc (Span x) const + { + auto copy = *this; + std::copy (x.begin(), x.end(), copy.identifierSrc.begin()); + return copy; + } + + [[nodiscard]] StaticDeviceInfo withLegacyIdentifiersDst (Span x) const + { + auto copy = *this; + std::copy (x.begin(), x.end(), copy.identifierDst.begin()); + return copy; + } + + [[nodiscard]] StaticDeviceInfo withLegacyIdentifiers (IOKind k, Span x) const + { + return k == IOKind::src ? withLegacyIdentifiersSrc (x) : withLegacyIdentifiersDst (x); + } + + String getName() const { return name; } + String getManufacturer() const { return manufacturer; } + String getProduct() const { return product; } + bool hasSource() const { return source; } + bool hasDestination() const { return destination; } + Transport getTransport() const { return transport; } + + /** Returns an identifier to uniquely identify each group, for use with the legacy MIDI API. */ + Span getLegacyIdentifiersSrc() const& { return identifierSrc; } + /** Returns an identifier to uniquely identify each group, for use with the legacy MIDI API. */ + Span getLegacyIdentifiersDst() const& { return identifierDst; } + + Span getLegacyIdentifiers (IOKind k) const& + { + return k == IOKind::src ? getLegacyIdentifiersSrc() : getLegacyIdentifiersDst(); + } + + Span getLegacyIdentifiersSrc() const&& = delete; + Span getLegacyIdentifiersDst() const&& = delete; + Span getLegacyIdentifiers (IOKind) const&& = delete; + +private: + std::array identifierSrc, identifierDst; + String name; ///< The full human-readable name of this device + String manufacturer; ///< The name of the organisation that produced this device + String product; ///< The human-readable product name + Transport transport{}; ///< The format used for MIDI messages in transit + uint8_t source = false; ///< True if the device can send messages + uint8_t destination = false; ///< True if the device can receive messages +}; + +/** MIDI implementation technologies + + @tags{Audio} +*/ +enum class Backend +{ + alsa, ///< linux + android, ///< android + coremidi, ///< macOS, iOS + winmm, ///< classic Windows MIDI + winrt, ///< Windows WinRT MIDI 1.0 (experimental) + wms, ///< Windows MIDI Services (experimental) +}; + +/** + Endpoints known to the system. + + Use this to locate hardware and software devices that are capable of sending and + receiving MIDI messages. + + Call makeSession() to create a session that manages connections to those devices. + + @tags{Audio} +*/ +class Endpoints : public DeletedAtShutdown +{ +public: + /** Fetch endpoint ids. */ + std::vector getEndpoints() const; + + /** Fetch endpoint ids into the provided buffer. */ + void getEndpoints (std::vector&) const; + + /** Fetches information about a particular endpoint, or nullopt if the information + is unavailable. + + Currently, this function may return nullopt on Android for UMP devices that are + connected but not opened. + + If you're displaying endpoint names in a user-facing list, it's probably a good idea to + use the Endpoint name if getEndpoint() returns a value. Otherwise, you can use the + StaticDeviceInformation name if getStaticDeviceInformation() returns a value. If both + functions return nullopt, this implies that the device is unknown/disconnected. + */ + std::optional getEndpoint (const EndpointId&) const; + + /** Fetches static information about a particular endpoint, or nullopt if the endpoint is + unavailable, which might happen if the endpoint has been disconnected. + */ + std::optional getStaticDeviceInfo (const EndpointId&) const; + + /** Adds a listener that will receive notifications when endpoints are added, removed, + or otherwise changed. + */ + void addListener (EndpointsListener& l); + + /** Removes a listener that was previously added with addListener(). + */ + void removeListener (EndpointsListener&); + + /** Creates a session to manage connections to endpoints. + It's possible for this function to fail, in which case Session::isAlive() will return false. + */ + Session makeSession (const String& name) const; + + /** Returns the technology that is being used to communicate with MIDI devices on this platform. + This is mainly relevant on Windows, where there are several different MIDI implementations. + + The intended use of this getter is to allow programs to detect whether the Windows MIDI + Services (WMS) are active and running. If initialisation of WMS failed, the implementation + will fall back to an earlier, more limited implementation. Programs may wish to detect this + situation so that they can direct users to install the WMS SDK. + + If no MIDI technology is available, this returns nullopt. + */ + std::optional getBackend() const; + + /** Creating a virtual legacy port will only succeed if this function returns true. + On platforms that support virtual MIDI connections (macOS, iOS, ALSA, Windows MIDI Services), + this will normally return true. + On platforms that don't support virtual MIDI (older Windows MIDI APIs), this will always + return false. + On Android, this will return false until the virtual MIDI service has started, and will + return true while the service is running. + You can listen for virtualMidiServiceActiveChanged() to take actions (e.g. creating or + destroying virtual ports) when the service status changes. + */ + bool isVirtualMidiBytestreamServiceActive() const; + + /** Creating a virtual ump endpoint will only succeed if this function returns true. + On recent platforms that support virtual MIDI connections (recent macOS and iOS, recent ALSA, + Windows MIDI Services), this will normally return true. + On platforms that don't support virtual MIDI (older Windows MIDI APIs), this will always + return false. + On Android, this will return false until the virtual MIDI service has started, and will + return true while the service is running. + You can listen for virtualMidiServiceActiveChanged() to take actions (e.g. creating or + destroying virtual ports) when the service status changes. + */ + bool isVirtualMidiUmpServiceActive() const; + + /** By default, Android MIDI services are initially disabled, but can be enabled by calling + this function, passing true. + This function is *not synchronous*. + To find out when the service state changes, listen for virtualMidiServiceActiveChanged() + and check the result of isVirtualMidiBytestreamServiceActive(). + + On platforms other than Android, this call does nothing. + */ + void setVirtualMidiBytestreamServiceActive (bool); + + /** By default, Android MIDI services are initially disabled, but can be enabled by calling + this function, passing true. + This function is *not synchronous*. + To find out when the service state changes, listen for virtualMidiServiceActiveChanged() + and check the result of isVirtualMidiUmpServiceActive(). + On Android versions that don't support virtual UMP (API level < 35), this call does nothing. + + On platforms other than Android, this call does nothing. + */ + void setVirtualMidiUmpServiceActive (bool); + + JUCE_DECLARE_SINGLETON_INLINE (Endpoints, true) + + ~Endpoints() override; + + /** @internal */ + class Impl; + +private: + Endpoints(); + + std::unique_ptr impl; +}; + +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPIOHelpers.cpp b/modules/juce_audio_devices/midi_io/ump/juce_UMPIOHelpers.cpp new file mode 100644 index 0000000000..2998724b7f --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPIOHelpers.cpp @@ -0,0 +1,126 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +struct EndpointAndStaticInfo +{ + Endpoint endpoint; + StaticDeviceInfo info; + EndpointId id; +}; + +class IOHelpers +{ +public: + IOHelpers() = delete; + + static Block makeLegacyBlock (bool isInput) + { + const auto direction = isInput ? BlockDirection::receiver : BlockDirection::sender; + const auto hint = isInput ? BlockUiHint::receiver : BlockUiHint::sender; + return Block{}.withName ("Legacy MIDI 1.0") + .withEnabled (true) + .withFirstGroup (0) + .withNumGroups (1) + .withMIDI1ProxyKind (BlockMIDI1ProxyKind::unrestrictedBandwidth) + .withDirection (direction) + .withUiHint (hint); + } + + static EndpointAndStaticInfo makeProxyEndpoint (const MidiDeviceInfo& info, BlockDirection direction) + { + jassert (direction != BlockDirection::unknown); + + const auto hint = std::invoke ([&] + { + switch (direction) + { + case BlockDirection::bidirectional: return BlockUiHint::bidirectional; + case BlockDirection::sender: return BlockUiHint::sender; + case BlockDirection::receiver: return BlockUiHint::receiver; + case BlockDirection::unknown: break; + } + + return BlockUiHint::unknown; + }); + + const auto block = Block{}.withDirection (direction) + .withUiHint (hint) + .withEnabled (true) + .withFirstGroup (0) + .withNumGroups (1) + .withMIDI1ProxyKind (BlockMIDI1ProxyKind::unrestrictedBandwidth); + const auto id = std::invoke ([&] + { + switch (direction) + { + case BlockDirection::bidirectional: + return EndpointId::makeSrcDst (info.identifier, info.identifier); + case BlockDirection::receiver: + return EndpointId::make (IOKind::dst, info.identifier); + case BlockDirection::sender: + return EndpointId::make (IOKind::src, info.identifier); + case BlockDirection::unknown: break; + } + + return EndpointId{}; + }); + + const auto srcId = direction != BlockDirection::receiver ? info.identifier : ""; + const auto dstId = direction != BlockDirection::sender ? info.identifier : ""; + + std::array srcIds { srcId }; + std::array dstIds { dstId }; + + const auto baseEndpoint = Endpoint{}.withName (info.name) + .withProtocol (PacketProtocol::MIDI_1_0) + .withUMPVersion (1, 1) + .withMidi1Support (true) + .withStaticBlocks (true) + .withBlocks (std::array { block }); + + const auto staticInfo = StaticDeviceInfo{}.withLegacyIdentifiersSrc (srcIds) + .withLegacyIdentifiersDst (dstIds) + .withHasSource (direction == BlockDirection::sender) + .withHasDestination (direction == BlockDirection::receiver) + .withName (info.name) + .withTransport (Transport::bytestream); + + return { baseEndpoint, staticInfo, id }; + } +}; + +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPInput.cpp b/modules/juce_audio_devices/midi_io/ump/juce_UMPInput.cpp new file mode 100644 index 0000000000..930d6a530d --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPInput.cpp @@ -0,0 +1,189 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +class Input::Impl final : private Consumer, + private DisconnectionListener +{ +public: + class Native + { + public: + virtual ~Native() = default; + + /* Returns the ID of the endpoint to which this connection is connected. */ + virtual EndpointId getEndpointId() const = 0; + + /* The protocol to which incoming messages are converted. */ + virtual PacketProtocol getProtocol() const = 0; + }; + + EndpointId getEndpointId() const + { + return identifier; + } + + PacketProtocol getProtocol() const + { + return protocol; + } + + void addConsumer (Consumer& x) + { + consumers.add (x); + } + + void removeConsumer (Consumer& x) + { + consumers.remove (x); + } + + void addDisconnectionListener (DisconnectionListener& x) + { + disconnectListeners.add (&x); + } + + void removeDisconnectionListener (DisconnectionListener& x) + { + disconnectListeners.remove (&x); + } + + bool isAlive() const + { + return native != nullptr; + } + + template + static Input makeInput (Callback&& callback) + { + auto impl = rawToUniquePtr (new Impl); + impl->native = callback (static_cast (*impl), static_cast (*impl)); + + if (impl->native == nullptr) + return {}; + + impl->identifier = impl->native->getEndpointId(); + impl->protocol = impl->native->getProtocol(); + return Input { std::move (impl) }; + } + +private: + Impl() = default; + + void consume (Iterator b, Iterator e, double t) override + { + consumers.call ([&] (auto& l) { l.consume (b, e, t); }); + } + + void disconnected() override + { + JUCE_ASSERT_MESSAGE_THREAD + + native = nullptr; + disconnectListeners.call ([] (auto& x) { x.disconnected(); }); + } + + ListenerList disconnectListeners; + WaitFreeListeners consumers; + EndpointId identifier; + PacketProtocol protocol; + std::unique_ptr native; +}; + +Input::Input() = default; +Input::~Input() = default; +Input::Input (std::unique_ptr x) : impl (std::move (x)) {} +Input::Input (Input&&) noexcept = default; +Input& Input::operator= (Input&&) noexcept = default; + +EndpointId Input::getEndpointId() const +{ + if (impl != nullptr) + return impl->getEndpointId(); + + return {}; +} + +PacketProtocol Input::getProtocol() const +{ + // You should ensure that isAlive() returns true before calling other member functions! + jassert (isAlive()); + + if (impl != nullptr) + return impl->getProtocol(); + + return {}; +} + +void Input::addConsumer (Consumer& x) +{ + // You should ensure that isAlive() returns true before calling other member functions! + jassert (isAlive()); + + if (impl != nullptr) + impl->addConsumer (x); +} + +void Input::removeConsumer (Consumer& x) +{ + if (impl != nullptr) + impl->removeConsumer (x); +} + +void Input::addDisconnectionListener (DisconnectionListener& x) +{ + // You should ensure that isAlive() returns true before calling other member functions! + jassert (isAlive()); + + if (impl != nullptr) + impl->addDisconnectionListener (x); +} + +void Input::removeDisconnectionListener (DisconnectionListener& x) +{ + if (impl != nullptr) + impl->removeDisconnectionListener (x); +} + +bool Input::isAlive() const +{ + if (impl != nullptr) + return impl->isAlive(); + + return false; +} + +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPInput.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPInput.h new file mode 100644 index 0000000000..f6bc71379d --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPInput.h @@ -0,0 +1,148 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +//============================================================================== +/** + An interface class for entities that consume Universal MIDI Packets from some producer. + + @tags{Audio} +*/ +struct Consumer +{ + Consumer() = default; + + Consumer (const Consumer&) = default; + Consumer (Consumer&&) noexcept = default; + + Consumer& operator= (const Consumer&) = default; + Consumer& operator= (Consumer&&) noexcept = default; + + virtual ~Consumer() noexcept = default; + + /** This will be called each time a new packet is ready for processing. */ + virtual void consume (Iterator b, Iterator e, double time) = 0; +}; + +//============================================================================== +/** + An input (from the JUCE project's perspective) that receives messages sent by an endpoint. + + An Input is conceptually similar to a unique_ptr, in that it's a nullable move-only type. + You can check the null state of an instance by calling isAlive(). + isAlive() will return true for an Input that's currently connected, or false otherwise. + In particular, isAlive() will return false for a default-constructed Input. + If isAlive() returns false, you should avoid calling other member functions: + although this won't result in undefined behaviour, none of the functions will produce useful + results in this state. + + A particular pitfall to watch out for is calling addConsumer() and removeConsumer() on a + default-constructed Input. This has no effect. Instead, if you want to attach listeners to + an Input, you should use Session::connectedInput() to create an Input, and ensure that isAlive() + returns true on that input before attaching a Consumer. + + @tags{Audio} +*/ +class Input +{ +public: + /** Creates a disconnected input. + A default-constructed input will never receive any messages. + */ + Input(); + ~Input(); + + Input (Input&&) noexcept; + Input& operator= (Input&&) noexcept; + + Input (const Input&) = delete; + Input& operator= (const Input&) = delete; + + /** Returns this connection's endpoint. */ + EndpointId getEndpointId() const; + + /** Returns the protocol that was requested when creating this connection. */ + PacketProtocol getProtocol() const; + + /** Attaches a receiver that will receive MIDI messages from this input. + + Incoming messages will be converted to the protocol that was requested when + opening the Input. + + If isAlive() returns false at the point where this function is called, this function + will have no effect. This can commonly happen when attempting to add listeners to a + default-constructed Input, or if the input device got disconnected. + + It is an error to add or remove a consumer from within the consumer callback. + This will cause deadlocks, so be careful! + */ + void addConsumer (Consumer& r); + + /** Detaches the receiver so that it will no longer receive MIDI messages from this + Input. + + It is an error to add or remove a consumer from within the consumer callback. + This will cause deadlocks, so be careful! + */ + void removeConsumer (Consumer& r); + + /** Attaches a listener that will be notified when this endpoint is disconnected. + + Calling this function on an instance for which isAlive() returns false has no effect. + */ + void addDisconnectionListener (DisconnectionListener& r); + + /** Removes a disconnection listener. */ + void removeDisconnectionListener (DisconnectionListener& r); + + /** True if this connection is currently active. + + This function returns false for a default-constructed instance. + */ + bool isAlive() const; + + explicit operator bool() const { return isAlive(); } + + /** @internal */ + class Impl; + +private: + explicit Input (std::unique_ptr); + + std::unique_ptr impl; +}; + +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.cpp b/modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.cpp new file mode 100644 index 0000000000..7c1bafd073 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.cpp @@ -0,0 +1,100 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +class LegacyVirtualInput::Impl final +{ +public: + class Native + { + public: + virtual ~Native() = default; + + virtual EndpointId getId() const = 0; + }; + + EndpointId getId() const + { + return identifier; + } + + bool isAlive() const + { + return native != nullptr; + } + + static LegacyVirtualInput makeLegacyVirtualInput (std::unique_ptr x) + { + if (x != nullptr) + return LegacyVirtualInput { rawToUniquePtr (new Impl (std::move (x))) }; + + return {}; + } + +private: + explicit Impl (std::unique_ptr n) + : native (std::move (n)), + identifier (native->getId()) + { + } + + std::unique_ptr native; + EndpointId identifier; +}; + +LegacyVirtualInput::LegacyVirtualInput() = default; +LegacyVirtualInput::~LegacyVirtualInput() = default; +LegacyVirtualInput::LegacyVirtualInput (std::unique_ptr x) : impl (std::move (x)) {} +LegacyVirtualInput::LegacyVirtualInput (LegacyVirtualInput&&) noexcept = default; +LegacyVirtualInput& LegacyVirtualInput::operator= (LegacyVirtualInput&&) noexcept = default; + +EndpointId LegacyVirtualInput::getId() const +{ + if (impl != nullptr) + return impl->getId(); + + return {}; +} + +bool LegacyVirtualInput::isAlive() const +{ + if (impl != nullptr) + return impl->isAlive(); + + return false; +} + +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.h new file mode 100644 index 0000000000..377fef4205 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualInput.h @@ -0,0 +1,80 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +/** + Represents a virtual MIDI 1.0 input port. + + @tags{Audio} +*/ +class LegacyVirtualInput +{ +public: + /** Creates an invalid virtual port that doesn't correspond to any virtual device. */ + LegacyVirtualInput(); + ~LegacyVirtualInput(); + + LegacyVirtualInput (LegacyVirtualInput&&) noexcept; + LegacyVirtualInput& operator= (LegacyVirtualInput&&) noexcept; + + LegacyVirtualInput (const LegacyVirtualInput&) = delete; + LegacyVirtualInput& operator= (const LegacyVirtualInput&) = delete; + + /** Retrieves the unique id of this input. + + You can pass this ID to Session::createInput in order to receive messages sent to this input. + + Note that this is *not* guaranteed to be stable - creating the 'same' virtual device + across several program invocations may produce a different ID each time. + + To fetch the current details of this device, you can pass this ID to Endpoints::getEndpoint(). + */ + EndpointId getId() const; + + bool isAlive() const; + + explicit operator bool() const { return isAlive(); } + + /** @internal */ + class Impl; + +private: + explicit LegacyVirtualInput (std::unique_ptr); + + std::unique_ptr impl; +}; + +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.cpp b/modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.cpp new file mode 100644 index 0000000000..7fae7f6400 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.cpp @@ -0,0 +1,100 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +class LegacyVirtualOutput::Impl final +{ +public: + class Native + { + public: + virtual ~Native() = default; + + virtual EndpointId getId() const = 0; + }; + + EndpointId getId() const + { + return identifier; + } + + bool isAlive() const + { + return native != nullptr; + } + + static LegacyVirtualOutput makeLegacyVirtualOutput (std::unique_ptr x) + { + if (x != nullptr) + return LegacyVirtualOutput { rawToUniquePtr (new Impl (std::move (x))) }; + + return {}; + } + +private: + explicit Impl (std::unique_ptr n) + : native (std::move (n)), + identifier (native->getId()) + { + } + + std::unique_ptr native; + EndpointId identifier; +}; + +LegacyVirtualOutput::LegacyVirtualOutput() = default; +LegacyVirtualOutput::~LegacyVirtualOutput() = default; +LegacyVirtualOutput::LegacyVirtualOutput (std::unique_ptr x) : impl (std::move (x)) {} +LegacyVirtualOutput::LegacyVirtualOutput (LegacyVirtualOutput&&) noexcept = default; +LegacyVirtualOutput& LegacyVirtualOutput::operator= (LegacyVirtualOutput&&) noexcept = default; + +EndpointId LegacyVirtualOutput::getId() const +{ + if (impl != nullptr) + return impl->getId(); + + return {}; +} + +bool LegacyVirtualOutput::isAlive() const +{ + if (impl != nullptr) + return impl->isAlive(); + + return false; +} + +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.h new file mode 100644 index 0000000000..849002503f --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPLegacyVirtualOutput.h @@ -0,0 +1,80 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +/** + Represents a virtual MIDI 1.0 output port. + + @tags{Audio} +*/ +class LegacyVirtualOutput +{ +public: + /** Creates an invalid virtual port that doesn't correspond to any virtual device. */ + LegacyVirtualOutput(); + ~LegacyVirtualOutput(); + + LegacyVirtualOutput (LegacyVirtualOutput&&) noexcept; + LegacyVirtualOutput& operator= (LegacyVirtualOutput&&) noexcept; + + LegacyVirtualOutput (const LegacyVirtualOutput&) = delete; + LegacyVirtualOutput& operator= (const LegacyVirtualOutput&) = delete; + + /** Retrieves the unique id of this input. + + You can pass this ID to Session::createOutput in order to send messages to this output. + + Note that this is *not* guaranteed to be stable - creating the 'same' virtual device + across several program invocations may produce a different ID each time. + + To fetch the current details of this device, you can pass this ID to Endpoints::getEndpoint(). + */ + EndpointId getId() const; + + bool isAlive() const; + + explicit operator bool() const { return isAlive(); } + + /** @internal */ + class Impl; + +private: + explicit LegacyVirtualOutput (std::unique_ptr); + + std::unique_ptr impl; +}; + +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.cpp b/modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.cpp new file mode 100644 index 0000000000..9e45910db0 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.cpp @@ -0,0 +1,159 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +class Output::Impl final : private DisconnectionListener +{ +public: + class Native + { + public: + virtual ~Native() = default; + + virtual EndpointId getEndpointId() const = 0; + + virtual bool send (Iterator b, Iterator e) = 0; + }; + + EndpointId getEndpointId() const + { + return identifier; + } + + bool send (Iterator beginIterator, Iterator endIterator) + { + if (native != nullptr) + return native->send (beginIterator, endIterator); + + return false; + } + + void addDisconnectionListener (DisconnectionListener& x) + { + disconnectListeners.add (&x); + } + + void removeDisconnectionListener (DisconnectionListener& x) + { + disconnectListeners.remove (&x); + } + + bool isAlive() const + { + return native != nullptr; + } + + template + static Output makeOutput (Callback&& callback) + { + auto impl = rawToUniquePtr (new Impl); + impl->native = callback (static_cast (*impl)); + + if (impl->native == nullptr) + return {}; + + impl->identifier = impl->native->getEndpointId(); + return Output { std::move (impl) }; + } + +private: + Impl() = default; + + void disconnected() override + { + JUCE_ASSERT_MESSAGE_THREAD + + native = nullptr; + disconnectListeners.call ([] (auto& x) { x.disconnected(); }); + } + + ListenerList disconnectListeners; + std::unique_ptr native; + EndpointId identifier; +}; + +Output::Output() = default; +Output::~Output() = default; +Output::Output (std::unique_ptr x) : impl (std::move (x)) {} +Output::Output (Output&&) noexcept = default; +Output& Output::operator= (Output&&) noexcept = default; + +EndpointId Output::getEndpointId() const +{ + // You should ensure that isAlive() returns true before calling other member functions! + jassert (isAlive()); + + if (impl != nullptr) + return impl->getEndpointId(); + + return {}; +} + +bool Output::send (Iterator beginIterator, Iterator endIterator) +{ + // You should ensure that isAlive() returns true before calling other member functions! + jassert (isAlive()); + + if (impl != nullptr) + return impl->send (beginIterator, endIterator); + + return false; +} + +void Output::addDisconnectionListener (DisconnectionListener& x) +{ + // You should ensure that isAlive() returns true before calling other member functions! + jassert (isAlive()); + + if (impl != nullptr) + impl->addDisconnectionListener (x); +} + +void Output::removeDisconnectionListener (DisconnectionListener& x) +{ + if (impl != nullptr) + impl->removeDisconnectionListener (x); +} + +bool Output::isAlive() const +{ + if (impl != nullptr) + return impl->isAlive(); + + return false; +} + +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.h new file mode 100644 index 0000000000..41567474c3 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPOutput.h @@ -0,0 +1,106 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +/** + An output (from the JUCE project's perspective) that sends messages to an endpoint. + + An Output is conceptually similar to a unique_ptr, in that it's a nullable move-only type. + You can check the null state of an instance by calling isAlive(). + isAlive() will return true for an Output that's currently connected, or false otherwise. + In particular, isAlive() will return false for a default-constructed Output. + If isAlive() returns false, you should avoid calling other member functions: + although this won't result in undefined behaviour, none of the functions will produce useful + results in this state. + + @tags{Audio} +*/ +class Output +{ +public: + /** Creates a disconnected output. + Sending messages to a default-constructed output won't do anything. + */ + Output(); + ~Output(); + + Output (Output&&) noexcept; + Output& operator= (Output&&) noexcept; + + Output (const Output&) = delete; + Output& operator= (const Output&) = delete; + + /** Returns this connection's endpoint. */ + EndpointId getEndpointId() const; + + /** Sends a range of messages to this endpoint. + If isAlive() returns false at the point where this function is called, then this + function has no effect. + + Returns true on success, false on failure. + + You may send messages using any protocol and they will be converted automatically + to the protocol expected by the receiver. + */ + bool send (Iterator beginIterator, Iterator endIterator); + + /** Attaches a listener that will be notified when this endpoint is disconnected. + + Calling this function on an instance for which isAlive() returns false has no effect. + */ + void addDisconnectionListener (DisconnectionListener& r); + + /** Removes a disconnection listener. */ + void removeDisconnectionListener (DisconnectionListener& r); + + /** True if this connection is currently active. + + This function returns false for a default-constructed instance. + */ + bool isAlive() const; + + explicit operator bool() const { return isAlive(); } + + /** @internal */ + class Impl; + +private: + explicit Output (std::unique_ptr); + + std::unique_ptr impl; +}; + +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPSession.cpp b/modules/juce_audio_devices/midi_io/ump/juce_UMPSession.cpp new file mode 100644 index 0000000000..35b3c0d5db --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPSession.cpp @@ -0,0 +1,310 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +class Session::Impl +{ +public: + class Native + { + public: + virtual ~Native() = default; + + virtual String getName() const = 0; + virtual std::unique_ptr connectInput (DisconnectionListener&, + const EndpointId&, + PacketProtocol, + Consumer&) = 0; + virtual std::unique_ptr connectOutput (DisconnectionListener&, + const EndpointId&) = 0; + + /* Creates a full MIDI 2.0 UMP endpoint, or returns nullptr on failure. */ + virtual std::unique_ptr createNativeVirtualEndpoint (const String&, + const DeviceInfo&, + const String&, + PacketProtocol, + Span, + BlocksAreStatic) + { + // If this is hit, you're trying to create a virtual (app-to-app) MIDI 2.0 endpoint, but + // the current MIDI backend does not implement this feature. + jassertfalse; + return {}; + } + + /* Creates a virtual MIDI 1.0 port. This is intended for use on platforms that don't + support virtual MIDI 2.0. + */ + virtual std::unique_ptr createLegacyVirtualInput (const String&) + { + // If this is hit, you're trying to create a virtual (app-to-app) MIDI 1.0 endpoint, but + // the current MIDI backend does not implement this feature. + jassertfalse; + return {}; + } + + /* Creates a virtual MIDI 1.0 port. This is intended for use on platforms that don't + support virtual MIDI 2.0. + */ + virtual std::unique_ptr createLegacyVirtualOutput (const String&) + { + // If this is hit, you're trying to create a virtual (app-to-app) MIDI 1.0 endpoint, but + // the current MIDI backend does not implement this feature. + jassertfalse; + return {}; + } + }; + + String getName() const + { + return native->getName(); + } + + Input makeInput (const EndpointId& endpointId, PacketProtocol protocol) + { + return Input::Impl::makeInput ([&] (DisconnectionListener& d, Consumer& c) + { + return native->connectInput (d, endpointId, protocol, c); + }); + } + + Output makeOutput (const EndpointId& endpointId) + { + return Output::Impl::makeOutput ([&] (DisconnectionListener& d) + { + return native->connectOutput (d, endpointId); + }); + } + + VirtualEndpoint createVirtualEndpoint (const String& name, + const DeviceInfo& info, + const String& productInstance, + PacketProtocol protocol, + Span blocks, + BlocksAreStatic areStatic) + { + if (blocks.size() > 32) + { + // UMP endpoints support a maximum of 32 function blocks + jassertfalse; + return {}; + } + + if (name.getNumBytesAsUTF8() > 98) + { + // Per the spec, there's a length restriction on endpoint names + jassertfalse; + return {}; + } + + const auto isBlockNameTooLong = [] (const Block& b) + { + return b.getName().getNumBytesAsUTF8() > 91; + }; + + if (std::any_of (blocks.begin(), blocks.end(), isBlockNameTooLong)) + { + // Per the spec, there's a length restriction on block names + jassertfalse; + return {}; + } + + const auto isBlockDisabled = [] (const Block& b) { return ! b.isEnabled(); }; + + if (areStatic == BlocksAreStatic::yes && std::any_of (blocks.begin(), blocks.end(), isBlockDisabled)) + { + // You may not request a disabled function block if the function block topology is static + jassertfalse; + return {}; + } + + auto umpEndpointNative = native->createNativeVirtualEndpoint (name, + info, + productInstance, + protocol, + blocks, + areStatic); + + if (umpEndpointNative == nullptr) + return {}; + + auto result = VirtualEndpoint::Impl::makeVirtualEndpoint (std::move (umpEndpointNative)); + + if (! result) + return {}; + + #if JUCE_ASSERTIONS_ENABLED_OR_LOGGED + if (const auto endpoint = Endpoints::getInstance()->getEndpoint (result.getId())) + { + jassert (endpoint->getName() == name); + jassert (endpoint->getProductInstanceId() == productInstance); + jassert (endpoint->getProtocol() == protocol); + jassert (endpoint->hasStaticBlocks() == (areStatic == BlocksAreStatic::yes)); + jassert (std::equal (blocks.begin(), + blocks.end(), + endpoint->getBlocks().begin(), + endpoint->getBlocks().end())); + } + else + { + // Unable to find this endpoint, even though we just created it! + jassertfalse; + } + + if (const auto staticInfo = Endpoints::getInstance()->getStaticDeviceInfo (result.getId())) + { + jassert (staticInfo->getTransport() == Transport::ump); + } + else + { + jassertfalse; + } + #endif + + return result; + } + + LegacyVirtualInput createLegacyVirtualInput (const String& name) + { + if (auto result = native->createLegacyVirtualInput (name)) + { + [[maybe_unused]] const auto id = result->getId(); + jassert (id.dst.isNotEmpty()); + return LegacyVirtualInput::Impl::makeLegacyVirtualInput (std::move (result)); + } + + return {}; + } + + LegacyVirtualOutput createLegacyVirtualOutput (const String& name) + { + if (auto result = native->createLegacyVirtualOutput (name)) + { + [[maybe_unused]] const auto id = result->getId(); + jassert (id.src.isNotEmpty()); + return LegacyVirtualOutput::Impl::makeLegacyVirtualOutput (std::move (result)); + } + + return {}; + } + + static Session makeSession (std::unique_ptr x) + { + if (x == nullptr) + return Session { nullptr }; + + return Session { rawToUniquePtr (new Impl (std::move (x))) }; + } + +private: + explicit Impl (std::unique_ptr n) : native (std::move (n)) {} + + // Order of data members is important to ensure that all inputs+outputs are destroyed before the + // native session + std::unique_ptr native; +}; + +String Session::getName() const +{ + if (impl != nullptr) + return impl->getName(); + + return {}; +} + +Input Session::connectInput (const EndpointId& x, PacketProtocol p) +{ + if (impl != nullptr) + return impl->makeInput (x, p); + + return {}; +} + +Output Session::connectOutput (const EndpointId& x) +{ + if (impl != nullptr) + return impl->makeOutput (x); + + return {}; +} + +VirtualEndpoint Session::createVirtualEndpoint (const String& name, + const DeviceInfo& deviceInfo, + const String& productInstanceID, + PacketProtocol protocol, + Span initialBlocks, + BlocksAreStatic areStatic) +{ + if (impl == nullptr) + return {}; + + return impl->createVirtualEndpoint (name, + deviceInfo, + productInstanceID, + protocol, + initialBlocks, + areStatic); +} + +LegacyVirtualInput Session::createLegacyVirtualInput (const juce::String& name) +{ + if (impl == nullptr) + return {}; + + return impl->createLegacyVirtualInput (name); +} + +LegacyVirtualOutput Session::createLegacyVirtualOutput (const juce::String& name) +{ + if (impl == nullptr) + return {}; + + return impl->createLegacyVirtualOutput (name); +} + +bool Session::isAlive() const +{ + return impl != nullptr; +} + +Session::Session (std::shared_ptr x) : impl (std::move (x)) {} +Session::Session (const Session&) = default; +Session::Session (Session&&) noexcept = default; +Session& Session::operator= (const Session&) = default; +Session& Session::operator= (Session&&) noexcept = default; +Session::~Session() = default; + +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPSession.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPSession.h new file mode 100644 index 0000000000..62b03f74b1 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPSession.h @@ -0,0 +1,185 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +/** This type is passed when creating a virtual endpoint to request static or dynamic blocks. */ +enum class BlocksAreStatic : uint8_t +{ + no, ///< Indicates that block layouts will not change after construction. + yes, ///< Indicates that the block layout may be modified after construction. +}; + +/** + Allows creating new connections to endpoints. + + The session is internally reference counted, meaning that the system resources represented + by the session won't be released until the Session object, along with all Inputs, Outputs, + and VirtualEndpoints that it created, have been destroyed. + + Internally, sessions cache open connections, so that multiple Inputs + or Outputs to the same endpoint will share resources associated with that connection. + Therefore, in order to minimise resource usage, you should create as few sessions as possible, + and re-use them when creating new connections. It may still make sense to have a separate + session for logically discrete components of your project, e.g. a multi-window application + where each window has its own document with an associated MIDI configuration. + + @tags{Audio} +*/ +class Session +{ +public: + /** Returns the name that was provided when creating this session, or an empty string + if the session is not alive. + */ + String getName() const; + + /** Creates a connection to a particular endpoint. + On failure, returns a disconnected connection (i.e. isAlive() returns false). + Passing an EndpointId denoting an endpoint that can only receive messages will fail. + + Incoming messages will be automatically converted to the specified protocol, regardless + of their actual protocol 'on the wire'. + + If the session is not alive, this will always fail and return an Input that is not alive. + */ + Input connectInput (const EndpointId&, PacketProtocol); + + /** Creates a connection to a particular endpoint. + On failure, returns a disconnected connection (i.e. isAlive() returns false). + Passing an EndpointId denoting an endpoint that can only send messages will fail. + + If the session is not alive, this will always fail and return an Output that is not alive. + */ + Output connectOutput (const EndpointId&); + + /** Returns a new VirtualEndpoint if virtual endpoints are supported and the configuration is + valid. If creating the endpoint fails, this will return an invalid VirtualEndpoint. + + To close the endpoint and remove it from the global MIDI configuration, call + destroyVirtualEndpoint(). + + To actually send and receive messages through this endpoint, use connectInput + and connectOutput to create endpoint connections, in the same way you would treat + any other device. + + If the function blocks are static, all blocks must be marked as active. + If the function blocks are not static, then blocks may be initially inactive. + The number of declared function blocks may not change while the device is active, + so if you need a dynamic number of blocks, mark the block layout as non-static + and mark any initially unused blocks as inactive. + + Only a maximum of 32 blocks are allowed on an endpoint. If you pass more than 32 + blocks, this function will fail. + + This function may also fail if virtual devices are not available on the current platform. + + Some platforms (older macOS, older Linux) support virtual MIDI 1.0 devices but not + virtual UMP devices. On such platforms, this function will fail. You may wish to check + for this case, and to call createLegacyVirtualInput() and/or createLegacyVirtualOutput() + as a fall-back. + + If the session is not alive, this will always fail and return a VirtualEndpoint that is not + alive. + + On Android, this function will only ever succeed on newer platforms (API level >= 35). + You may need to call Endpoints::setVirtualMidiUmpServiceActive() to make the virtual UMP + service available. You can listen for state changes in the virtual MIDI service using + EndpointsListener::virtualMidiServiceActiveChanged(). A maximum of one VirtualEndpoint may + be alive at any time on Android, and attempting to create additional virtual endpoints will + fail. + */ + VirtualEndpoint createVirtualEndpoint (const String& name, + const DeviceInfo& deviceInfo, + const String& productInstanceID, + PacketProtocol protocol, + Span initialBlocks, + BlocksAreStatic); + + /** Creates a MIDI 1.0-compatible input port. + + Where supported by platform APIs, this will explicitly create a single-group MIDI 1.0 port. + There are some special cases to keep in mind: + + - Windows MIDI Services only allows creation of UMP endpoints, not MIDI 1.0 ports, so on + that platform we create an endpoint with a single MIDI 1.0 Block. + - Android requires that virtual ports are declared in the app manifest. JUCE declares a + virtual bytestream device with a single input and output, and a virtual UMP device with + a single bidirectional port. By default, these virtual devices are disabled, but they + can be enabled by calling Endpoints::setVirtualMidiBytestreamServiceActive() and + Endpoints::setVirtualMidiUmpServiceActive(). After each service becomes available, you + may create exactly one VirtualEndpoint, one LegacyVirtualInput, and/or one + LegacyVirtualOutput. Additional virtual ports are not supported. + */ + LegacyVirtualInput createLegacyVirtualInput (const String& name); + + /** Creates a MIDI 1.0-compatible output port. + + Where supported by platform APIs, this will explicitly create a single-group MIDI 1.0 port. + There are some special cases to keep in mind: + + - Windows MIDI Services only allows creation of UMP endpoints, not MIDI 1.0 ports, so on + that platform we create an endpoint with a single MIDI 1.0 Block. + - Android requires that virtual ports are declared in the app manifest. JUCE declares a + virtual bytestream device with a single input and output, and a virtual UMP device with + a single bidirectional port. By default, these virtual devices are disabled, but they + can be enabled by calling Endpoints::setVirtualMidiBytestreamServiceActive() and + Endpoints::setVirtualMidiUmpServiceActive(). After each service becomes available, you + may create exactly one VirtualEndpoint, one LegacyVirtualInput, and/or one + LegacyVirtualOutput. Additional virtual ports are not supported. + */ + LegacyVirtualOutput createLegacyVirtualOutput (const String& name); + + /** True if this session was created successfully and is currently alive. */ + bool isAlive() const; + + explicit operator bool() const { return isAlive(); } + + Session (const Session&); + Session (Session&&) noexcept; + Session& operator= (const Session&); + Session& operator= (Session&&) noexcept; + ~Session(); + + /** @internal */ + class Impl; + +private: + explicit Session (std::shared_ptr); + + std::shared_ptr impl; +}; + +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.cpp b/modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.cpp new file mode 100644 index 0000000000..0b9241f99d --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.cpp @@ -0,0 +1,134 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +class VirtualEndpoint::Impl final +{ +public: + class Native + { + public: + virtual ~Native() = default; + + virtual EndpointId getId() const = 0; + virtual bool setBlock (uint8_t, const Block&) = 0; + virtual bool setName (const String&) = 0; + }; + + EndpointId getId() const + { + return identifier; + } + + bool setBlock (uint8_t i, const Block& b) + { + if (native != nullptr) + return native->setBlock (i, b); + + return false; + } + + bool setName (const String& n) + { + if (native != nullptr) + return native->setName (n); + + return false; + } + + bool isAlive() const + { + return native != nullptr; + } + + static VirtualEndpoint makeVirtualEndpoint (std::unique_ptr x) + { + if (x != nullptr) + return VirtualEndpoint { rawToUniquePtr (new Impl (std::move (x))) }; + + return {}; + } + +private: + explicit Impl (std::unique_ptr n) + : native (std::move (n)), + identifier (native->getId()) + { + } + + std::unique_ptr native; + EndpointId identifier; +}; + +VirtualEndpoint::VirtualEndpoint() = default; +VirtualEndpoint::~VirtualEndpoint() = default; +VirtualEndpoint::VirtualEndpoint (std::unique_ptr x) : impl (std::move (x)) {} +VirtualEndpoint::VirtualEndpoint (VirtualEndpoint&&) noexcept = default; +VirtualEndpoint& VirtualEndpoint::operator= (VirtualEndpoint&&) noexcept = default; + +EndpointId VirtualEndpoint::getId() const +{ + if (impl != nullptr) + return impl->getId(); + + return {}; +} + +bool VirtualEndpoint::setBlock (uint8_t i, const Block& b) +{ + if (impl != nullptr) + return impl->setBlock (i, b); + + return false; +} + +bool VirtualEndpoint::setName (const String& x) +{ + if (impl != nullptr) + return impl->setName (x); + + return false; +} + +bool VirtualEndpoint::isAlive() const +{ + if (impl != nullptr) + return impl->isAlive(); + + return false; +} + +} diff --git a/modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.h b/modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.h new file mode 100644 index 0000000000..b60b78ab48 --- /dev/null +++ b/modules/juce_audio_devices/midi_io/ump/juce_UMPVirtualEndpoint.h @@ -0,0 +1,112 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +/** + Represents a virtual device that allows this program to advertise itself to other MIDI-aware + applications on the system. + + Creating a VirtualEndpoint will install a new endpoint on the system. This endpoint will + be visible when enumerating endpoints using the Endpoints singleton. If you're displaying + a list of endpoints in your UI, it's probably a good idea to omit any virtual endpoints + created by the current application in order to avoid confusion. + + After creating a VirtualEndpoint, it can be opened like any other connection, by calling + Session::connectInput() and Session::connectOutput(), passing the EndpointId for the + virtual endpoint. + + @tags{Audio} +*/ +class VirtualEndpoint +{ +public: + /** Creates an invalid virtual endpoint that doesn't correspond to any virtual device. */ + VirtualEndpoint(); + ~VirtualEndpoint(); + + VirtualEndpoint (VirtualEndpoint&&) noexcept; + VirtualEndpoint& operator= (VirtualEndpoint&&) noexcept; + + VirtualEndpoint (const VirtualEndpoint&) = delete; + VirtualEndpoint& operator= (const VirtualEndpoint&) = delete; + + /** Retrieves the unique id of this endpoint. + + Note that this is *not* guaranteed to be stable - creating the 'same' virtual device + across several program invocations may produce a different ID each time. + + To fetch the current details of this device, you can pass this ID to Endpoints::getEndpoint(). + */ + EndpointId getId() const; + + /** Sets new properties for the block at the given zero-based index. The number of function + blocks on an endpoint may not change. + + Returns true on success, or false otherwise. + + This may fail for several reasons, including: + - attempting to modify an endpoint with static function blocks + - attempting to update a block index that doesn't exist on this endpoint + - attempting to set a block with invalid properties, e.g. the sum of the start index and + number of included groups is greater than 16 + - platform-specific reasons, e.g. macOS doesn't currently allow changing the number of + spanned groups in a block + */ + bool setBlock (uint8_t index, const Block& newBlock); + + /** Assigns a new name to this endpoint, and sends a notification to connected endpoints. */ + bool setName (const String&); + + /** Returns true if this object represents an endpoint that is currently alive, or false if + the endpoint is not alive. This can happen because the endpoint failed to open, or + if the session holding the endpoint was closed. + + This function returns false for a default-constructed instance. + */ + bool isAlive() const; + + explicit operator bool() const { return isAlive(); } + + /** @internal */ + class Impl; + +private: + explicit VirtualEndpoint (std::unique_ptr); + + std::unique_ptr impl; +}; + +} diff --git a/modules/juce_audio_devices/native/java/app/com/rmsl/juce/JuceMidiSupport.java b/modules/juce_audio_devices/native/java/app/com/rmsl/juce/JuceMidiSupport.java index 887c0485c9..4d940c781d 100644 --- a/modules/juce_audio_devices/native/java/app/com/rmsl/juce/JuceMidiSupport.java +++ b/modules/juce_audio_devices/native/java/app/com/rmsl/juce/JuceMidiSupport.java @@ -35,71 +35,162 @@ package com.rmsl.juce; +import android.annotation.TargetApi; import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothProfile; import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanResult; import android.bluetooth.le.ScanSettings; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.icu.text.IDNA; import android.media.midi.MidiDevice; import android.media.midi.MidiDeviceInfo; +import android.media.midi.MidiDeviceService; import android.media.midi.MidiDeviceStatus; import android.media.midi.MidiInputPort; import android.media.midi.MidiManager; -import android.bluetooth.le.ScanCallback; -import android.bluetooth.BluetoothDevice; import android.media.midi.MidiOutputPort; import android.media.midi.MidiReceiver; +import android.media.midi.MidiUmpDeviceService; import android.os.Build; import android.os.ParcelUuid; import android.util.Log; import android.util.Pair; import java.io.IOException; +import java.lang.annotation.Native; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.Timer; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.function.Predicate; import static android.content.Context.MIDI_SERVICE; import static android.content.Context.BLUETOOTH_SERVICE; +import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED; +import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED; +import static android.content.pm.PackageManager.DONT_KILL_APP; public class JuceMidiSupport { - //============================================================================== - public interface JuceMidiPort + public static class PortPath { - boolean isInputPort (); + int deviceId; + int portIndex; + int type; + @Override + public boolean equals (Object other) + { + if (other.getClass() != getClass()) + return false; + + PortPath p = (PortPath) other; + + return p.deviceId == deviceId + && p.portIndex == portIndex + && p.type == type; + } + + @Override + public int hashCode() + { + return Objects.hash (deviceId, portIndex, type); + } + } + + static public abstract class JuceMidiPort extends MidiReceiver + { // start, stop does nothing on an output port - void start (); + abstract void start(); - void stop (); + abstract void stop(); - void close (); + abstract void close(); - // send will do nothing on an input port - void sendMidi (byte[] msg, int offset, int count); + abstract boolean isActive(); - String getName (); + abstract PortPath getPortPath(); } static BluetoothAdapter getDefaultBluetoothAdapter (Context ctx) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S_V2) - return BluetoothAdapter.getDefaultAdapter(); + { + @SuppressWarnings ("deprecation") + BluetoothAdapter result = BluetoothAdapter.getDefaultAdapter(); + return result; + } return ((BluetoothManager) ctx.getSystemService (BLUETOOTH_SERVICE)).getAdapter(); } + // These correspond to the constants in MidiManager, but those may not be available depending + // on the current SDK. + private static final int TRANSPORT_BYTESTREAM = 1; + private static final int TRANSPORT_UMP = 2; + + static Set getDevicesForTransport (MidiManager mm, int transport) + { + if (Build.VERSION_CODES.TIRAMISU <= Build.VERSION.SDK_INT) + return mm.getDevicesForTransport (transport); + + if (transport == TRANSPORT_BYTESTREAM) + { + @SuppressWarnings ("deprecation") + HashSet result = new HashSet<> (Arrays.asList (mm.getDevices())); + return result; + } + + return new HashSet<>(); + } + + public static class InfoWithTransport + { + InfoWithTransport (MidiDeviceInfo i, int t) + { + info = i; + transport = t; + } + + public final MidiDeviceInfo info; + public final int transport; + } + + static List getDevicesPreferringUmp (MidiManager mm) + { + ArrayList result = new ArrayList<>(); + + for (int transport : Arrays.asList (TRANSPORT_UMP, TRANSPORT_BYTESTREAM)) + { + for (MidiDeviceInfo info : getDevicesForTransport (mm, transport)) + { + result.add (new InfoWithTransport (info, transport)); + } + } + + return result; + } + //============================================================================== public static class BluetoothMidiManager extends ScanCallback { @@ -116,7 +207,7 @@ public class JuceMidiSupport public String getHumanReadableStringForBluetoothAddress (String address) { BluetoothDevice btDevice = getDefaultBluetoothAdapter (appContext).getRemoteDevice (address); - return btDevice.getName (); + return btDevice.getName(); } public int getBluetoothDeviceStatus (String address) @@ -134,7 +225,7 @@ public class JuceMidiSupport return; } - BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner (); + BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner(); if (bluetoothLeScanner == null) { @@ -144,18 +235,19 @@ public class JuceMidiSupport if (shouldStart) { - ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder (); + ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder(); scanFilterBuilder.setServiceUuid (ParcelUuid.fromString (bluetoothLEMidiServiceUUID)); - ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder (); + ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder(); scanSettingsBuilder.setCallbackType (ScanSettings.CALLBACK_TYPE_ALL_MATCHES) .setScanMode (ScanSettings.SCAN_MODE_LOW_POWER) .setScanMode (ScanSettings.MATCH_MODE_STICKY); - bluetoothLeScanner.startScan (Arrays.asList (scanFilterBuilder.build ()), - scanSettingsBuilder.build (), + bluetoothLeScanner.startScan (Arrays.asList (scanFilterBuilder.build()), + scanSettingsBuilder.build(), this); - } else + } + else { bluetoothLeScanner.stopScan (this); } @@ -167,7 +259,7 @@ public class JuceMidiSupport if (btDevice == null) { - Log.d ("JUCE", "failed to create buletooth device from address"); + Log.d ("JUCE", "failed to create bluetooth device from address"); return false; } @@ -179,34 +271,37 @@ public class JuceMidiSupport getAndroidMidiDeviceManager (appContext).unpairBluetoothDevice (address); } + @Override public void onScanFailed (int errorCode) { } + @Override public void onScanResult (int callbackType, ScanResult result) { if (callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES || callbackType == ScanSettings.CALLBACK_TYPE_FIRST_MATCH) { - BluetoothDevice device = result.getDevice (); + BluetoothDevice device = result.getDevice(); if (device != null) - bluetoothMidiDevices.add (device.getAddress ()); + bluetoothMidiDevices.add (device.getAddress()); } if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) { Log.d ("JUCE", "ScanSettings.CALLBACK_TYPE_MATCH_LOST"); - BluetoothDevice device = result.getDevice (); + BluetoothDevice device = result.getDevice(); if (device != null) { - bluetoothMidiDevices.remove (device.getAddress ()); - unpairBluetoothMidiDevice (device.getAddress ()); + bluetoothMidiDevices.remove (device.getAddress()); + unpairBluetoothMidiDevice (device.getAddress()); } } } + @Override public void onBatchScanResults (List results) { for (ScanResult result : results) @@ -220,143 +315,166 @@ public class JuceMidiSupport private Context appContext = null; } - public static class JuceMidiInputPort extends MidiReceiver implements JuceMidiPort + public static class NativeMidiReceiver extends MidiReceiver { - private native void handleReceive (long host, byte[] msg, int offset, int count, long timestamp); - - public JuceMidiInputPort (MidiDeviceManager mm, MidiOutputPort actualPort, MidiPortPath portPathToUse, long hostToUse) + NativeMidiReceiver (long h) { + host = h; + } + + @Override + public void onSend (byte[] msg, int offset, int count, long timestamp) throws IOException + { + if (host != 0 && count > 0) + handleReceive (host, msg, offset, count, timestamp); + } + + public long host = 0; + private native void handleReceive (long host, byte[] msg, int offset, int count, long timestamp); + } + + private native static void handleDeviceAdded (JuceMidiDeviceInfo info); + private native static void handleDeviceRemoved (int id); + + public static class JuceMidiInputPort extends JuceMidiPort + { + public JuceMidiInputPort (MidiDeviceManager mm, MidiOutputPort actualPort, int d, int t, long hostToUse) + { + receiver = new NativeMidiReceiver (hostToUse); owner = mm; androidPort = actualPort; - portPath = portPathToUse; - juceHost = hostToUse; + deviceId = d; + transport = t; isConnected = false; } @Override - protected void finalize () throws Throwable + protected void finalize() throws Throwable { - close (); - super.finalize (); + close(); + super.finalize(); } @Override - public boolean isInputPort () + public void start() { - return true; + if (owner != null && androidPort != null && ! isConnected) + androidPort.connect (receiver); + + isConnected = true; } @Override - public void start () - { - if (owner != null && androidPort != null && !isConnected) - { - androidPort.connect (this); - isConnected = true; - } - } - - @Override - public void stop () + public void stop() { if (owner != null && androidPort != null && isConnected) - { - androidPort.disconnect (this); - isConnected = false; - } + androidPort.disconnect (receiver); + + isConnected = false; } @Override - public void close () + public boolean isActive() { + return isConnected; + } + + @Override + public void close() + { + PortPath p = null; + if (androidPort != null) { + p = getPortPath(); + try { - androidPort.close (); - } catch (IOException exception) + androidPort.close(); + } + catch (IOException exception) { Log.d ("JUCE", "IO Exception while closing port"); } } - if (owner != null) - owner.removePort (portPath); + if (owner != null && p != null) + owner.removePort (p); owner = null; androidPort = null; } @Override - public void onSend (byte[] msg, int offset, int count, long timestamp) + public PortPath getPortPath() { - if (count > 0) - handleReceive (juceHost, msg, offset, count, timestamp); + PortPath p = new PortPath(); + p.deviceId = deviceId; + p.portIndex = androidPort.getPortNumber(); + p.type = MidiDeviceInfo.PortInfo.TYPE_OUTPUT; + return p; } @Override - public void onFlush () + public void onFlush() {} @Override - public void sendMidi (byte[] msg, int offset, int count) + public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException { } - @Override - public String getName () - { - return owner.getPortName (portPath); - } - + NativeMidiReceiver receiver; MidiDeviceManager owner; MidiOutputPort androidPort; - MidiPortPath portPath; - long juceHost; + int deviceId; + int transport; boolean isConnected; } - public static class JuceMidiOutputPort implements JuceMidiPort + public static class JuceMidiOutputPort extends JuceMidiPort { - public JuceMidiOutputPort (MidiDeviceManager mm, MidiInputPort actualPort, MidiPortPath portPathToUse) + public JuceMidiOutputPort (MidiDeviceManager mm, MidiInputPort actualPort, int d, int t) { owner = mm; androidPort = actualPort; - portPath = portPathToUse; + deviceId = d; + transport = t; } @Override - protected void finalize () throws Throwable + protected void finalize() throws Throwable { - close (); - super.finalize (); + close(); + super.finalize(); } @Override - public boolean isInputPort () + public void start() + { + } + + @Override + public void stop() + { + } + + @Override + public boolean isActive() { return false; } @Override - public void start () - { - } - - @Override - public void stop () - { - } - - @Override - public void sendMidi (byte[] msg, int offset, int count) + public void onSend (byte[] msg, int offset, int count, long timestamp) { if (androidPort != null) { try { - androidPort.send (msg, offset, count); - } catch (IOException exception) + androidPort.send (msg, offset, count, timestamp); + } + catch (IOException exception) { Log.d ("JUCE", "send midi had IO exception"); } @@ -364,80 +482,106 @@ public class JuceMidiSupport } @Override - public void close () + public void close() { + PortPath p = null; + if (androidPort != null) { + p = getPortPath(); + try { - androidPort.close (); - } catch (IOException exception) + androidPort.close(); + } + catch (IOException exception) { Log.d ("JUCE", "IO Exception while closing port"); } } - if (owner != null) - owner.removePort (portPath); + if (owner != null && p != null) + owner.removePort (p); owner = null; androidPort = null; } @Override - public String getName () + public PortPath getPortPath() { - return owner.getPortName (portPath); + PortPath p = new PortPath(); + p.deviceId = deviceId; + p.portIndex = androidPort.getPortNumber(); + p.type = MidiDeviceInfo.PortInfo.TYPE_INPUT; + return p; } MidiDeviceManager owner; MidiInputPort androidPort; - MidiPortPath portPath; - } - - private static class MidiPortPath extends Object - { - public MidiPortPath (int deviceIdToUse, boolean direction, int androidIndex) - { - deviceId = deviceIdToUse; - isInput = direction; - portIndex = androidIndex; - } - - public int deviceId; - public int portIndex; - public boolean isInput; - - @Override - public int hashCode () - { - Integer i = new Integer ((deviceId * 128) + (portIndex < 128 ? portIndex : 127)); - return i.hashCode () * (isInput ? -1 : 1); - } - - @Override - public boolean equals (Object obj) - { - if (obj == null) - return false; - - if (getClass () != obj.getClass ()) - return false; - - MidiPortPath other = (MidiPortPath) obj; - return (portIndex == other.portIndex && isInput == other.isInput && deviceId == other.deviceId); - } + int deviceId; + int transport; } //============================================================================== - public static class MidiDeviceManager extends MidiManager.DeviceCallback implements MidiManager.OnDeviceOpenedListener + public static class JuceMidiDeviceInfo + { + public String name; + public String manufacturer; + public String product; + public String serialNumber; + public int id; + public int defaultProtocol; + public int transport; + public int type; + + public ArrayList dst; // input port names + public ArrayList src; // output port names + } + + public static class TransportDeviceCallback + { + public void onDeviceAdded (InfoWithTransport info) {} + public void onDeviceRemoved (MidiDeviceInfo info) {} + } + + public static class DeviceCallbackImpl extends MidiManager.DeviceCallback + { + public DeviceCallbackImpl (TransportDeviceCallback c, int t) + { + callback = c; + transport = t; + } + + @Override + public void onDeviceAdded (MidiDeviceInfo device) + { + super.onDeviceAdded (device); + callback.onDeviceAdded (new InfoWithTransport (device, transport)); + } + + @Override + public void onDeviceRemoved (MidiDeviceInfo device) + { + super.onDeviceRemoved (device); + callback.onDeviceRemoved (device); + } + + public final TransportDeviceCallback callback; + public final int transport; + } + + //============================================================================== + public static class MidiDeviceManager + extends TransportDeviceCallback + implements MidiManager.OnDeviceOpenedListener { //============================================================================== - private class DummyBluetoothGattCallback extends BluetoothGattCallback + private class InertBluetoothGattCallback extends BluetoothGattCallback { - public DummyBluetoothGattCallback (MidiDeviceManager mm) + public InertBluetoothGattCallback (MidiDeviceManager mm) { - super (); + super(); owner = mm; } @@ -446,85 +590,74 @@ public class JuceMidiSupport if (newState == BluetoothProfile.STATE_CONNECTED) { gatt.requestConnectionPriority (BluetoothGatt.CONNECTION_PRIORITY_HIGH); - owner.pairBluetoothDeviceStepTwo (gatt.getDevice ()); + owner.pairBluetoothDeviceStepTwo (gatt.getDevice()); } } - public void onServicesDiscovered (BluetoothGatt gatt, int status) {} - - public void onCharacteristicRead (BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {} - - public void onCharacteristicWrite (BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {} - - public void onCharacteristicChanged (BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {} - - public void onDescriptorRead (BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {} - - public void onDescriptorWrite (BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {} - - public void onReliableWriteCompleted (BluetoothGatt gatt, int status) {} - - public void onReadRemoteRssi (BluetoothGatt gatt, int rssi, int status) {} - - public void onMtuChanged (BluetoothGatt gatt, int mtu, int status) {} - private MidiDeviceManager owner; } + // get(String) is deprecated but there doesn't seem to be an alternative to fetch a BluetoothDevice + @SuppressWarnings ("deprecation") + private static BluetoothDevice getBluetoothDevice (MidiDeviceInfo info) + { + return (BluetoothDevice) info.getProperties().get (MidiDeviceInfo.PROPERTY_BLUETOOTH_DEVICE); + } + //============================================================================== private class MidiDeviceOpenTask extends java.util.TimerTask { - public MidiDeviceOpenTask (MidiDeviceManager deviceManager, MidiDevice device, BluetoothGatt gattToUse) + public MidiDeviceOpenTask (MidiDeviceManager deviceManager, DiscoveredDevice deviceIn) { owner = deviceManager; - midiDevice = device; - btGatt = gattToUse; + device = deviceIn; } @Override - public boolean cancel () + public boolean cancel() { synchronized (MidiDeviceOpenTask.class) { owner = null; - boolean retval = super.cancel (); + boolean retval = super.cancel(); - if (btGatt != null) + if (device.gatt != null) { - btGatt.disconnect (); - btGatt.close (); + device.gatt.disconnect(); + device.gatt.close(); - btGatt = null; + device.gatt = null; } - if (midiDevice != null) + if (device.device != null) { try { - midiDevice.close (); - } catch (IOException e) + device.device.close(); + } + catch (IOException e) { } - midiDevice = null; + device.device = null; } return retval; } } - public String getBluetoothAddress () + public String getBluetoothAddress() { synchronized (MidiDeviceOpenTask.class) { - if (midiDevice != null) + if (device.device != null) { - MidiDeviceInfo info = midiDevice.getInfo (); - if (info.getType () == MidiDeviceInfo.TYPE_BLUETOOTH) + MidiDeviceInfo info = device.device.getInfo(); + if (info.getType() == MidiDeviceInfo.TYPE_BLUETOOTH) { - BluetoothDevice btDevice = (BluetoothDevice) info.getProperties ().get (info.PROPERTY_BLUETOOTH_DEVICE); + BluetoothDevice btDevice = getBluetoothDevice (info); if (btDevice != null) - return btDevice.getAddress (); + return btDevice.getAddress(); } } } @@ -532,26 +665,28 @@ public class JuceMidiSupport return ""; } - public BluetoothGatt getGatt () { return btGatt; } - - public int getID () + public BluetoothGatt getGatt() { - return midiDevice.getInfo ().getId (); + return device.gatt; + } + + public int getID() + { + return device.device.getInfo().getId(); } @Override - public void run () + public void run() { synchronized (MidiDeviceOpenTask.class) { - if (owner != null && midiDevice != null) - owner.onDeviceOpenedDelayed (midiDevice); + if (owner != null && device != null) + owner.onDeviceOpenedDelayed (device); } } private MidiDeviceManager owner; - private MidiDevice midiDevice; - private BluetoothGatt btGatt; + private DiscoveredDevice device; } //============================================================================== @@ -566,137 +701,122 @@ public class JuceMidiSupport return; } - MidiDeviceInfo[] foundDevices = manager.getDevices (); + for (InfoWithTransport info : getDevicesPreferringUmp (manager)) + { + try + { + onDeviceAdded (info); + } + catch (IllegalArgumentException e) + { + Log.d ("JUCE", "MidiDeviceManager warning: Skipping MIDI UMP device " + info.toString()); + } + } - for (MidiDeviceInfo info : foundDevices) - onDeviceAdded (info); - - manager.registerDeviceCallback (this, null); + registerDeviceCallback(); } - protected void finalize () throws Throwable + @SuppressWarnings ("deprecation") + private void registerDeviceCallback() { - manager.unregisterDeviceCallback (this); + if (Build.VERSION_CODES.TIRAMISU <= Build.VERSION.SDK_INT) + { + for (int transport : Arrays.asList (TRANSPORT_UMP, TRANSPORT_BYTESTREAM)) + deviceCallbacks.add (new DeviceCallbackImpl (this, transport)); + + for (DeviceCallbackImpl callback : deviceCallbacks) + manager.registerDeviceCallback (callback.transport, executor, callback); + } + else + { + deviceCallbacks.add (new DeviceCallbackImpl (this, TRANSPORT_BYTESTREAM)); + + for (DeviceCallbackImpl callback : deviceCallbacks) + manager.registerDeviceCallback (callback, null); + } + } + + protected void finalize() throws Throwable + { + for (DeviceCallbackImpl callback : deviceCallbacks) + manager.unregisterDeviceCallback (callback); + + deviceCallbacks.clear(); synchronized (MidiDeviceManager.class) { - btDevicesPairing.clear (); + btDevicesPairing.clear(); - for (Integer deviceID : openTasks.keySet ()) - openTasks.get (deviceID).cancel (); + for (Integer deviceID : openTasks.keySet()) + openTasks.get (deviceID).cancel(); openTasks = null; } - for (MidiPortPath key : openPorts.keySet ()) - openPorts.get (key).get ().close (); + for (PortPath key : openPorts.keySet()) + openPorts.get (key).get().close(); openPorts = null; - for (Pair device : midiDevices) + for (DiscoveredDevice device : midiDevices) { - if (device.second != null) + if (device.gatt != null) { - device.second.disconnect (); - device.second.close (); + device.gatt.disconnect(); + device.gatt.close(); } - device.first.close (); + device.device.close(); } - midiDevices.clear (); + midiDevices.clear(); - super.finalize (); + super.finalize(); } - public String[] getJuceAndroidMidiOutputDeviceNameAndIDs () - { - return getJuceAndroidMidiDeviceNameAndIDs (MidiDeviceInfo.PortInfo.TYPE_OUTPUT); - } - - public String[] getJuceAndroidMidiInputDeviceNameAndIDs () - { - return getJuceAndroidMidiDeviceNameAndIDs (MidiDeviceInfo.PortInfo.TYPE_INPUT); - } - - private String[] getJuceAndroidMidiDeviceNameAndIDs (int portType) - { - // only update the list when JUCE asks for a new list - synchronized (MidiDeviceManager.class) - { - deviceInfos = getDeviceInfos (); - } - - ArrayList portNameAndIDs = new ArrayList (); - - for (MidiPortPath portInfo : getAllPorts (portType)) - { - portNameAndIDs.add (getPortName (portInfo)); - portNameAndIDs.add (Integer.toString (portInfo.hashCode ())); - } - - String[] names = new String[portNameAndIDs.size ()]; - return portNameAndIDs.toArray (names); - } - - private JuceMidiPort openMidiPortWithID (int deviceID, long host, boolean isInput) + private JuceMidiPort makeMidiPort (int deviceId, java.util.function.Function callback) { synchronized (MidiDeviceManager.class) { - int portTypeToFind = (isInput ? MidiDeviceInfo.PortInfo.TYPE_INPUT : MidiDeviceInfo.PortInfo.TYPE_OUTPUT); - MidiPortPath portInfo = getPortPathForID (portTypeToFind, deviceID); + DiscoveredDevice device = getDiscoveredDeviceForId (deviceId); - if (portInfo != null) - { - // ports must be opened exclusively! - if (openPorts.containsKey (portInfo)) - return null; + if (device == null || device.device == null) + return null; - Pair devicePair = getMidiDevicePairForId (portInfo.deviceId); + JuceMidiPort juceMidiPort = callback.apply (device); - if (devicePair != null) - { - MidiDevice device = devicePair.first; - if (device != null) - { - JuceMidiPort juceMidiPort = null; + if (juceMidiPort == null) + return null; - if (isInput) - { - MidiOutputPort outputPort = device.openOutputPort (portInfo.portIndex); - - if (outputPort != null) - juceMidiPort = new JuceMidiInputPort (this, outputPort, portInfo, host); - } else - { - MidiInputPort inputPort = device.openInputPort (portInfo.portIndex); - - if (inputPort != null) - juceMidiPort = new JuceMidiOutputPort (this, inputPort, portInfo); - } - - if (juceMidiPort != null) - { - openPorts.put (portInfo, new WeakReference (juceMidiPort)); - - return juceMidiPort; - } - } - } - } + openPorts.put (juceMidiPort.getPortPath(), new WeakReference (juceMidiPort)); + return juceMidiPort; } - - return null; } - public JuceMidiPort openMidiInputPortWithID (int deviceID, long host) + public JuceMidiPort openMidiInputPortWithID (int deviceId, int portIndex, long host) { - return openMidiPortWithID (deviceID, host, true); + return makeMidiPort (deviceId, (device) -> + { + MidiOutputPort port = device.device.openOutputPort (portIndex); + + if (port == null) + return null; + + return new JuceMidiInputPort (this, port, device.device.getInfo().getId(), device.transport, host); + }); } - public JuceMidiPort openMidiOutputPortWithID (int deviceID) + public JuceMidiPort openMidiOutputPortWithID (int deviceId, int portIndex) { - return openMidiPortWithID (deviceID, 0, false); + return makeMidiPort (deviceId, (device) -> + { + MidiInputPort port = device.device.openInputPort (portIndex); + + if (port == null) + return null; + + return new JuceMidiOutputPort (this, port, device.device.getInfo().getId(), device.transport); + }); } /* 0: unpaired, 1: paired, 2: pairing */ @@ -704,7 +824,7 @@ public class JuceMidiSupport { synchronized (MidiDeviceManager.class) { - if (!address.isEmpty ()) + if (!address.isEmpty()) { if (findMidiDeviceForBluetoothAddress (address) != null) return 1; @@ -722,8 +842,8 @@ public class JuceMidiSupport public boolean pairBluetoothDevice (BluetoothDevice btDevice) { - String btAddress = btDevice.getAddress (); - if (btAddress.isEmpty ()) + String btAddress = btDevice.getAddress(); + if (btAddress.isEmpty()) return false; synchronized (MidiDeviceManager.class) @@ -731,14 +851,14 @@ public class JuceMidiSupport if (getBluetoothDeviceStatus (btAddress) != 0) return false; - - btDevicesPairing.put (btDevice.getAddress (), null); - BluetoothGatt gatt = btDevice.connectGatt (appContext.getApplicationContext (), true, new DummyBluetoothGattCallback (this)); + btDevicesPairing.put (btDevice.getAddress(), null); + BluetoothGatt gatt = btDevice.connectGatt (appContext.getApplicationContext(), true, new InertBluetoothGattCallback (this)); if (gatt != null) { - btDevicesPairing.put (btDevice.getAddress (), gatt); - } else + btDevicesPairing.put (btDevice.getAddress(), gatt); + } + else { pairBluetoothDeviceStepTwo (btDevice); } @@ -754,7 +874,7 @@ public class JuceMidiSupport public void unpairBluetoothDevice (String address) { - if (address.isEmpty ()) + if (address.isEmpty()) return; synchronized (MidiDeviceManager.class) @@ -764,8 +884,8 @@ public class JuceMidiSupport BluetoothGatt gatt = btDevicesPairing.get (address); if (gatt != null) { - gatt.disconnect (); - gatt.close (); + gatt.disconnect(); + gatt.close(); } btDevicesPairing.remove (address); @@ -774,21 +894,23 @@ public class JuceMidiSupport MidiDeviceOpenTask openTask = findOpenTaskForBluetoothAddress (address); if (openTask != null) { - int deviceID = openTask.getID (); - openTask.cancel (); + int deviceID = openTask.getID(); + openTask.cancel(); openTasks.remove (deviceID); } - Pair midiDevicePair = findMidiDeviceForBluetoothAddress (address); - if (midiDevicePair != null) + DiscoveredDevice discoveredDevice = findMidiDeviceForBluetoothAddress (address); + + if (discoveredDevice != null) { - MidiDevice midiDevice = midiDevicePair.first; - onDeviceRemoved (midiDevice.getInfo ()); + MidiDevice midiDevice = discoveredDevice.device; + onDeviceRemoved (midiDevice.getInfo()); try { - midiDevice.close (); - } catch (IOException exception) + midiDevice.close(); + } + catch (IOException exception) { Log.d ("JUCE", "IOException while closing midi device"); } @@ -796,15 +918,15 @@ public class JuceMidiSupport } } - private Pair findMidiDeviceForBluetoothAddress (String address) + private DiscoveredDevice findMidiDeviceForBluetoothAddress (String address) { - for (Pair midiDevice : midiDevices) + for (DiscoveredDevice midiDevice : midiDevices) { - MidiDeviceInfo info = midiDevice.first.getInfo (); - if (info.getType () == MidiDeviceInfo.TYPE_BLUETOOTH) + MidiDeviceInfo info = midiDevice.device.getInfo(); + if (info.getType() == MidiDeviceInfo.TYPE_BLUETOOTH) { - BluetoothDevice btDevice = (BluetoothDevice) info.getProperties ().get (info.PROPERTY_BLUETOOTH_DEVICE); - if (btDevice != null && btDevice.getAddress ().equals (address)) + BluetoothDevice btDevice = getBluetoothDevice (info); + if (btDevice != null && btDevice.getAddress().equals (address)) return midiDevice; } } @@ -814,269 +936,257 @@ public class JuceMidiSupport private MidiDeviceOpenTask findOpenTaskForBluetoothAddress (String address) { - for (Integer deviceID : openTasks.keySet ()) + for (Integer deviceID : openTasks.keySet()) { MidiDeviceOpenTask openTask = openTasks.get (deviceID); - if (openTask.getBluetoothAddress ().equals (address)) + if (openTask.getBluetoothAddress().equals (address)) return openTask; } return null; } - public void removePort (MidiPortPath path) + public void removePort (PortPath path) { openPorts.remove (path); } @Override - public void onDeviceAdded (MidiDeviceInfo info) + public void onDeviceAdded (InfoWithTransport info) { - // only add standard midi devices - if (info.getType () == info.TYPE_BLUETOOTH) - return; + synchronized (transportForDevice) + { + transportForDevice.put (info.info.getId(), info.transport); + } - manager.openDevice (info, this, null); + try + { + manager.openDevice (info.info, this, null); + } + catch (Exception e) + { + // Something went wrong opening the device + } } @Override public void onDeviceRemoved (MidiDeviceInfo info) { + synchronized (transportForDevice) + { + transportForDevice.remove (info.getId()); + } + + int discoveredId = 0; + synchronized (MidiDeviceManager.class) { - Pair devicePair = getMidiDevicePairForId (info.getId ()); + DiscoveredDevice discovered = getDiscoveredDeviceForId (info.getId()); - if (devicePair != null) + if (discovered == null) + return; + + discoveredId = discovered.device.getInfo().getId(); + + for (;;) { - MidiDevice midiDevice = devicePair.first; - BluetoothGatt gatt = devicePair.second; + boolean removedPort = false; - // close all ports that use this device - boolean removedPort = true; - - while (removedPort == true) + for (PortPath key : openPorts.keySet()) { - removedPort = false; - for (MidiPortPath key : openPorts.keySet ()) + if (key.deviceId == info.getId()) { - if (key.deviceId == info.getId ()) - { - openPorts.get (key).get ().close (); - removedPort = true; - break; - } + openPorts.get (key).get().close(); + removedPort = true; + break; } } - if (gatt != null) - { - gatt.disconnect (); - gatt.close (); - } - - midiDevices.remove (devicePair); + if (! removedPort) + break; } + + if (discovered.gatt != null) + { + discovered.gatt.disconnect(); + discovered.gatt.close(); + } + + midiDevices.remove (discovered); } - handleDevicesChanged(); + handleDeviceRemoved (discoveredId); } - @Override - public void onDeviceStatusChanged (MidiDeviceStatus status) + private int getTransportForDevice (int deviceId) { + synchronized (transportForDevice) + { + try + { + return transportForDevice.getOrDefault (deviceId, 0); + } + catch (NullPointerException e) + { + return 0; + } + } } @Override public void onDeviceOpened (MidiDevice theDevice) { + final int transport = getTransportForDevice (theDevice.getInfo().getId()); + + if (transport == 0) + return; + synchronized (MidiDeviceManager.class) { - MidiDeviceInfo info = theDevice.getInfo (); - int deviceID = info.getId (); - BluetoothGatt gatt = null; + MidiDeviceInfo info = theDevice.getInfo(); + int deviceID = info.getId(); + + // This device is already being opened + if (openTasks.containsKey (deviceID)) + return; + boolean isBluetooth = false; + BluetoothGatt gatt = null; - if (!openTasks.containsKey (deviceID)) + if (info.getType() == MidiDeviceInfo.TYPE_BLUETOOTH) { - if (info.getType () == MidiDeviceInfo.TYPE_BLUETOOTH) + isBluetooth = true; + BluetoothDevice btDevice = getBluetoothDevice (info); + + if (btDevice != null) { - isBluetooth = true; - BluetoothDevice btDevice = (BluetoothDevice) info.getProperties ().get (info.PROPERTY_BLUETOOTH_DEVICE); - if (btDevice != null) + String btAddress = btDevice.getAddress(); + + if (btDevicesPairing.containsKey (btAddress)) { - String btAddress = btDevice.getAddress (); - if (btDevicesPairing.containsKey (btAddress)) - { - gatt = btDevicesPairing.get (btAddress); - btDevicesPairing.remove (btAddress); - } else - { - // unpair was called in the mean time - try - { - Pair midiDevicePair = findMidiDeviceForBluetoothAddress (btDevice.getAddress ()); - if (midiDevicePair != null) - { - gatt = midiDevicePair.second; - - if (gatt != null) - { - gatt.disconnect (); - gatt.close (); - } - } - - theDevice.close (); - } catch (IOException e) - { - } - - return; - } + gatt = btDevicesPairing.get (btAddress); + btDevicesPairing.remove (btAddress); } } - - MidiDeviceOpenTask openTask = new MidiDeviceOpenTask (this, theDevice, gatt); - openTasks.put (deviceID, openTask); - - new java.util.Timer ().schedule (openTask, (isBluetooth ? 2000 : 100)); } + + DiscoveredDevice discovered = new DiscoveredDevice (theDevice, gatt, transport); + MidiDeviceOpenTask openTask = new MidiDeviceOpenTask (this, discovered); + openTasks.put (deviceID, openTask); + new Timer().schedule (openTask, (isBluetooth ? 2000 : 100)); } } - public void onDeviceOpenedDelayed (MidiDevice theDevice) + public void onDeviceOpenedDelayed (DiscoveredDevice theDevice) { synchronized (MidiDeviceManager.class) { - int deviceID = theDevice.getInfo ().getId (); + int deviceID = theDevice.device.getInfo().getId(); if (openTasks.containsKey (deviceID)) { - if (!midiDevices.contains (theDevice)) + if (! midiDevices.contains (theDevice)) { - BluetoothGatt gatt = openTasks.get (deviceID).getGatt (); openTasks.remove (deviceID); - midiDevices.add (new Pair (theDevice, gatt)); - handleDevicesChanged(); + midiDevices.add (theDevice); + + JuceMidiDeviceInfo info = new JuceMidiDeviceInfo(); + info.name = theDevice.device.getInfo().getProperties().getString (MidiDeviceInfo.PROPERTY_NAME); + info.manufacturer = theDevice.device.getInfo().getProperties().getString (MidiDeviceInfo.PROPERTY_MANUFACTURER); + info.product = theDevice.device.getInfo().getProperties().getString (MidiDeviceInfo.PROPERTY_PRODUCT); + info.serialNumber = theDevice.device.getInfo().getProperties().getString (MidiDeviceInfo.PROPERTY_SERIAL_NUMBER); + info.id = theDevice.device.getInfo().getId(); + info.transport = theDevice.transport; + info.type = theDevice.device.getInfo().getType(); + + info.dst = new ArrayList<>(); + info.src = new ArrayList<>(); + + for (int i = 0; i < theDevice.device.getInfo().getInputPortCount(); ++i) + info.dst.add (""); + + for (int i = 0; i < theDevice.device.getInfo().getOutputPortCount(); ++i) + info.src.add (""); + + for (MidiDeviceInfo.PortInfo i : theDevice.device.getInfo().getPorts()) + { + ArrayList target = i.getType() == MidiDeviceInfo.PortInfo.TYPE_INPUT ? info.dst : info.src; + target.set (i.getPortNumber(), i.getName()); + } + + info.defaultProtocol = Build.VERSION_CODES.TIRAMISU <= Build.VERSION.SDK_INT + ? theDevice.device.getInfo().getDefaultProtocol() + : -1; + + handleDeviceAdded (info); } - } else + } + else { - // unpair was called in the mean time - MidiDeviceInfo info = theDevice.getInfo (); - BluetoothDevice btDevice = (BluetoothDevice) info.getProperties ().get (info.PROPERTY_BLUETOOTH_DEVICE); + // unpair was called in the meantime + MidiDeviceInfo info = theDevice.device.getInfo(); + BluetoothDevice btDevice = getBluetoothDevice (info); + if (btDevice != null) { - String btAddress = btDevice.getAddress (); - Pair midiDevicePair = findMidiDeviceForBluetoothAddress (btDevice.getAddress ()); - if (midiDevicePair != null) + DiscoveredDevice discoveredDevice = findMidiDeviceForBluetoothAddress (btDevice.getAddress()); + if (discoveredDevice != null) { - BluetoothGatt gatt = midiDevicePair.second; + BluetoothGatt gatt = discoveredDevice.gatt; if (gatt != null) { - gatt.disconnect (); - gatt.close (); + gatt.disconnect(); + gatt.close(); } } } try { - theDevice.close (); - } catch (IOException e) + theDevice.device.close(); + } + catch (IOException e) { } } } } - public String getPortName (MidiPortPath path) - { - int portTypeToFind = (path.isInput ? MidiDeviceInfo.PortInfo.TYPE_INPUT : MidiDeviceInfo.PortInfo.TYPE_OUTPUT); - - synchronized (MidiDeviceManager.class) - { - for (MidiDeviceInfo info : deviceInfos) - { - if (info.getId () == path.deviceId) - { - for (MidiDeviceInfo.PortInfo portInfo : info.getPorts ()) - { - int portType = portInfo.getType (); - if (portType == portTypeToFind) - { - int portIndex = portInfo.getPortNumber (); - if (portIndex == path.portIndex) - { - String portName = portInfo.getName (); - if (portName.isEmpty ()) - portName = (String) info.getProperties ().get (info.PROPERTY_NAME); - - return portName; - } - } - } - } - } - } - - return ""; - } - - public ArrayList getAllPorts (int portType) - { - ArrayList ports = new ArrayList (); - - for (MidiDeviceInfo info : deviceInfos) - for (MidiDeviceInfo.PortInfo portInfo : info.getPorts ()) - if (portInfo.getType () == portType) - ports.add (new MidiPortPath (info.getId (), (portType == MidiDeviceInfo.PortInfo.TYPE_INPUT), - portInfo.getPortNumber ())); - - return ports; - } - - public MidiPortPath getPortPathForID (int portType, int deviceID) - { - for (MidiPortPath port : getAllPorts (portType)) - if (port.hashCode () == deviceID) - return port; - - return null; - } - - private MidiDeviceInfo[] getDeviceInfos () + private DiscoveredDevice getDiscoveredDeviceForId (int deviceId) { synchronized (MidiDeviceManager.class) { - MidiDeviceInfo[] infos = new MidiDeviceInfo[midiDevices.size ()]; - - int idx = 0; - for (Pair midiDevice : midiDevices) - infos[idx++] = midiDevice.first.getInfo (); - - return infos; - } - } - - private Pair getMidiDevicePairForId (int deviceId) - { - synchronized (MidiDeviceManager.class) - { - for (Pair midiDevice : midiDevices) - if (midiDevice.first.getInfo ().getId () == deviceId) + for (DiscoveredDevice midiDevice : midiDevices) + if (midiDevice.device.getInfo().getId() == deviceId) return midiDevice; } return null; } - private MidiManager manager; - private HashMap btDevicesPairing = new HashMap(); + class DiscoveredDevice + { + DiscoveredDevice (MidiDevice deviceIn, BluetoothGatt gattIn, int transportIn) + { + device = deviceIn; + gatt = gattIn; + transport = transportIn; + } + + MidiDevice device; + BluetoothGatt gatt; + int transport; + } + + private final Executor executor = Executors.newSingleThreadExecutor(); + private final MidiManager manager; + private final HashMap btDevicesPairing = new HashMap(); private HashMap openTasks = new HashMap(); - private ArrayList> midiDevices = new ArrayList>(); - private MidiDeviceInfo[] deviceInfos; - private HashMap> openPorts = new HashMap>(); + private final HashMap transportForDevice = new HashMap<>(); + private final ArrayList midiDevices = new ArrayList<>(); + private final ArrayList deviceCallbacks = new ArrayList<>(); + private HashMap> openPorts = new HashMap>(); private Context appContext = null; } @@ -1101,7 +1211,7 @@ public class JuceMidiSupport if (adapter == null) return null; - if (adapter.getBluetoothLeScanner () == null) + if (adapter.getBluetoothLeScanner() == null) return null; synchronized (JuceMidiSupport.class) @@ -1113,9 +1223,6 @@ public class JuceMidiSupport return bluetoothManager; } - // To be called when devices become (un)available - private native static void handleDevicesChanged(); - private static MidiDeviceManager midiDeviceManager = null; private static BluetoothMidiManager bluetoothManager = null; } diff --git a/modules/juce_audio_devices/native/javaopt/app/com/rmsl/juce/VirtualMidiServices.java b/modules/juce_audio_devices/native/javaopt/app/com/rmsl/juce/VirtualMidiServices.java new file mode 100644 index 0000000000..8146424310 --- /dev/null +++ b/modules/juce_audio_devices/native/javaopt/app/com/rmsl/juce/VirtualMidiServices.java @@ -0,0 +1,181 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +package com.rmsl.juce; + + +import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED; +import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED; +import static android.content.pm.PackageManager.DONT_KILL_APP; + +import android.annotation.TargetApi; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.media.midi.MidiDeviceService; +import android.media.midi.MidiReceiver; +import android.media.midi.MidiUmpDeviceService; +import android.os.Build; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +public class VirtualMidiServices +{ + /** Argument is the receiver that should be sent outgoing messages. + The return value is a newly-created receiver that will receive incoming messages. + */ + private static native MidiReceiver addVirtualPort (MidiReceiver output, boolean isUmp); + + /** Argument is a receiver previously returned from addVirtualPort. */ + private static native void removeVirtualPort (MidiReceiver output); + + public static class VirtualPort extends MidiReceiver + { + @Override + public void onSend (byte[] msg, int offset, int count, long timestamp) throws IOException + { + if (onSend != null) + onSend.send (msg, offset, count, timestamp); + } + + public MidiReceiver onSend; + } + + public static class VirtualBytestreamService extends MidiDeviceService + { + @Override + public MidiReceiver[] onGetInputPortReceivers() + { + return new MidiReceiver[] { port }; + } + + @Override + public void onCreate() + { + super.onCreate(); + MidiReceiver[] receivers = getOutputPortReceivers(); + port.onSend = addVirtualPort (receivers != null && receivers.length > 0 ? receivers[0] : null, false); + } + + @Override + public void onDestroy() + { + removeVirtualPort (port.onSend); + super.onDestroy(); + } + + private final VirtualPort port = new VirtualPort(); + } + + @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + public static class VirtualUmpService extends MidiUmpDeviceService + { + @Override + public List onGetInputPortReceivers() + { + return Collections.singletonList (port); + } + + @Override + public void onCreate() + { + super.onCreate(); + List receivers = getOutputPortReceivers(); + port.onSend = addVirtualPort (! receivers.isEmpty() ? receivers.getFirst() : null, true); + } + + @Override + public void onDestroy() + { + removeVirtualPort (port.onSend); + super.onDestroy(); + } + + private final VirtualPort port = new VirtualPort(); + } + + private static void setVirtualMidiServiceEnabled (Context context, Class klass, boolean x) + { + PackageManager pm = context.getPackageManager(); + + if (pm == null) + return; + + ComponentName componentName = new ComponentName (context, klass); + + int desiredState = x ? COMPONENT_ENABLED_STATE_ENABLED : COMPONENT_ENABLED_STATE_DISABLED; + int actualState = pm.getComponentEnabledSetting (componentName); + + if (desiredState == actualState) + return; + + pm.setComponentEnabledSetting (componentName, desiredState, DONT_KILL_APP); + } + + public static void setVirtualMidiBytestreamEnabled (Context context, boolean x) + { + setVirtualMidiServiceEnabled (context, VirtualBytestreamService.class, x); + } + + public static void setVirtualMidiUmpEnabled (Context context, boolean x) + { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) + setVirtualMidiServiceEnabled (context, VirtualUmpService.class, x); + } + + private static boolean isServiceAvailable (Context context, Class k) + { + PackageManager packageManager = context.getPackageManager(); + Intent intent = new Intent (context, k); + List services = packageManager.queryIntentServices (intent, PackageManager.MATCH_DEFAULT_ONLY); + + return services != null && ! services.isEmpty(); + } + + public static boolean isVirtualBytestreamAvailable (Context context) + { + return isServiceAvailable (context, VirtualBytestreamService.class); + } + + public static boolean isVirtualUmpAvailable (Context context) + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) + return false; + + return isServiceAvailable (context, VirtualUmpService.class); + } +} diff --git a/modules/juce_audio_devices/native/juce_ALSA_weak_linux.h b/modules/juce_audio_devices/native/juce_ALSA_weak_linux.h new file mode 100644 index 0000000000..61f0085761 --- /dev/null +++ b/modules/juce_audio_devices/native/juce_ALSA_weak_linux.h @@ -0,0 +1,161 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +// This header holds definitions for ALSA symbols that are only present in newer library versions. +// It also contains weak definitions for functions that are only present in newer library versions. +// By requesting the functions to weak-link, it's possible to build UMP-aware programs on Linux +// versions that don't have up-to-date copies of ALSA, and it's also possible to run software +// built on older Linux platforms, even if that software was built on a more recent system. + +#if SND_LIB_VERSION < ((1 << 16) | (2 << 8) | 10) + +struct snd_seq_ump_event_t +{ + snd_seq_event_type_t type; + unsigned char flags; + unsigned char tag; + unsigned char queue; + snd_seq_timestamp_t time; + snd_seq_addr_t source; + snd_seq_addr_t dest; + + union + { + decltype (snd_seq_event_t::data) data; // Older headers don't define snd_seq_event_data_t + unsigned int ump[4]; + }; +}; + +struct snd_ump_endpoint_info_t; +struct snd_ump_block_info_t; + +#define snd_ump_block_info_alloca(ptr) __snd_alloca (ptr, snd_ump_block_info) +#define snd_ump_endpoint_info_alloca(ptr) __snd_alloca (ptr, snd_ump_endpoint_info) + +constexpr auto SND_SEQ_CLIENT_LEGACY_MIDI = 0; +constexpr auto SND_SEQ_CLIENT_UMP_MIDI_1_0 = 1; +constexpr auto SND_SEQ_CLIENT_UMP_MIDI_2_0 = 2; + +constexpr auto SND_UMP_EP_INFO_STATIC_BLOCKS = 0x1; +constexpr auto SND_UMP_EP_INFO_PROTO_MIDI1 = 0x100; +constexpr auto SND_UMP_EP_INFO_PROTO_MIDI2 = 0x200; + +constexpr auto SND_SEQ_PORT_CAP_INACTIVE = 1 << 8; + +constexpr auto SND_SEQ_EVENT_UMP = 1 << 5; + +constexpr auto SND_SEQ_PORT_DIR_INPUT = 1; +constexpr auto SND_SEQ_PORT_DIR_OUTPUT = 2; +constexpr auto SND_SEQ_PORT_DIR_BIDIRECTION = 3; + +constexpr auto SND_UMP_BLOCK_UI_HINT_UNKNOWN = 0; +constexpr auto SND_UMP_BLOCK_UI_HINT_RECEIVER = 1; +constexpr auto SND_UMP_BLOCK_UI_HINT_SENDER = 2; +constexpr auto SND_UMP_BLOCK_UI_HINT_BOTH = 3; + +constexpr auto SND_UMP_BLOCK_IS_MIDI1 = 1U << 0; +constexpr auto SND_UMP_BLOCK_IS_LOWSPEED = 1U << 1; + +constexpr auto SND_UMP_DIR_INPUT = 1; +constexpr auto SND_UMP_DIR_OUTPUT = 2; +constexpr auto SND_UMP_DIR_BIDIRECTION = 3; + +constexpr auto SND_UMP_EP_INFO_PROTO_JRTS_TX = 1; +constexpr auto SND_UMP_EP_INFO_PROTO_JRTS_RX = 2; +#endif + +JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wredundant-decls") + +extern "C" +{ + [[gnu::weak]] int snd_seq_client_info_get_midi_version (const snd_seq_client_info_t*); + [[gnu::weak]] int snd_seq_set_client_midi_version (snd_seq_t*, int); + [[gnu::weak]] int snd_seq_set_client_ump_conversion (snd_seq_t*, int); + [[gnu::weak]] int snd_seq_ump_event_input (snd_seq_t*, snd_seq_ump_event_t**); + [[gnu::weak]] int snd_seq_ump_event_output_direct (snd_seq_t*, snd_seq_ump_event_t*); + [[gnu::weak]] int snd_seq_port_info_get_ump_is_midi1 (const snd_seq_port_info_t*); + [[gnu::weak]] int snd_seq_port_info_get_ump_group (const snd_seq_port_info_t*); + [[gnu::weak]] int snd_seq_create_ump_endpoint (snd_seq_t*, const snd_ump_endpoint_info_t*, unsigned int); + [[gnu::weak]] int snd_seq_create_ump_block (snd_seq_t*, int, const snd_ump_block_info_t*); + [[gnu::weak]] int snd_seq_set_ump_block_info (snd_seq_t*, int, const void*); + [[gnu::weak]] int snd_seq_get_ump_block_info (snd_seq_t*, int, int, void*); + [[gnu::weak]] int snd_seq_get_ump_endpoint_info (snd_seq_t*, int, void*); + + [[gnu::weak]] const char* snd_ump_endpoint_info_get_product_id (const snd_ump_endpoint_info_t*); + [[gnu::weak]] const unsigned char* snd_ump_endpoint_info_get_sw_revision (const snd_ump_endpoint_info_t*); + [[gnu::weak]] unsigned int snd_ump_endpoint_info_get_family_id (const snd_ump_endpoint_info_t*); + [[gnu::weak]] unsigned int snd_ump_endpoint_info_get_flags (const snd_ump_endpoint_info_t*); + [[gnu::weak]] unsigned int snd_ump_endpoint_info_get_manufacturer_id (const snd_ump_endpoint_info_t*); + [[gnu::weak]] unsigned int snd_ump_endpoint_info_get_model_id (const snd_ump_endpoint_info_t*); + [[gnu::weak]] unsigned int snd_ump_endpoint_info_get_num_blocks (const snd_ump_endpoint_info_t*); + [[gnu::weak]] unsigned int snd_ump_endpoint_info_get_protocol (const snd_ump_endpoint_info_t*); + [[gnu::weak]] unsigned int snd_ump_endpoint_info_get_protocol_caps (const snd_ump_endpoint_info_t*); + [[gnu::weak]] unsigned int snd_ump_endpoint_info_get_version (const snd_ump_endpoint_info_t*); + + [[gnu::weak]] void snd_ump_endpoint_info_set_product_id (snd_ump_endpoint_info_t*, const char*); + [[gnu::weak]] void snd_ump_endpoint_info_set_sw_revision (snd_ump_endpoint_info_t*, const unsigned char*); + [[gnu::weak]] void snd_ump_endpoint_info_set_family_id (snd_ump_endpoint_info_t*, unsigned int); + [[gnu::weak]] void snd_ump_endpoint_info_set_flags (snd_ump_endpoint_info_t*, unsigned int); + [[gnu::weak]] void snd_ump_endpoint_info_set_manufacturer_id (snd_ump_endpoint_info_t*, unsigned int); + [[gnu::weak]] void snd_ump_endpoint_info_set_model_id (snd_ump_endpoint_info_t*, unsigned int); + [[gnu::weak]] void snd_ump_endpoint_info_set_name (snd_ump_endpoint_info_t*, const char*); + [[gnu::weak]] void snd_ump_endpoint_info_set_num_blocks (snd_ump_endpoint_info_t*, unsigned int); + [[gnu::weak]] void snd_ump_endpoint_info_set_protocol (snd_ump_endpoint_info_t*, unsigned int); + [[gnu::weak]] void snd_ump_endpoint_info_set_protocol_caps (snd_ump_endpoint_info_t*, unsigned int); + [[gnu::weak]] void snd_ump_endpoint_info_set_version (snd_ump_endpoint_info_t*, unsigned int); + + [[gnu::weak]] const char* snd_ump_block_info_get_name (const snd_ump_block_info_t*); + [[gnu::weak]] unsigned int snd_ump_block_info_get_active (const snd_ump_block_info_t*); + [[gnu::weak]] unsigned int snd_ump_block_info_get_direction (const snd_ump_block_info_t*); + [[gnu::weak]] unsigned int snd_ump_block_info_get_first_group (const snd_ump_block_info_t*); + [[gnu::weak]] unsigned int snd_ump_block_info_get_flags (const snd_ump_block_info_t*); + [[gnu::weak]] unsigned int snd_ump_block_info_get_num_groups (const snd_ump_block_info_t*); + [[gnu::weak]] unsigned int snd_ump_block_info_get_sysex8_streams (const snd_ump_block_info_t*); + [[gnu::weak]] unsigned int snd_ump_block_info_get_ui_hint (const snd_ump_block_info_t*); + + [[gnu::weak]] void snd_ump_block_info_set_name (snd_ump_block_info_t*, const char*); + [[gnu::weak]] void snd_ump_block_info_set_active (snd_ump_block_info_t*, unsigned int); + [[gnu::weak]] void snd_ump_block_info_set_direction (snd_ump_block_info_t*, unsigned int); + [[gnu::weak]] void snd_ump_block_info_set_first_group (snd_ump_block_info_t*, unsigned int); + [[gnu::weak]] void snd_ump_block_info_set_flags (snd_ump_block_info_t*, unsigned int); + [[gnu::weak]] void snd_ump_block_info_set_num_groups (snd_ump_block_info_t*, unsigned int); + [[gnu::weak]] void snd_ump_block_info_set_sysex8_streams (snd_ump_block_info_t*, unsigned int); + [[gnu::weak]] void snd_ump_block_info_set_ui_hint (snd_ump_block_info_t*, unsigned int); + [[gnu::weak]] void snd_ump_block_info_set_block_id (snd_ump_block_info_t*, unsigned int); + + [[gnu::weak]] int snd_seq_port_info_get_direction (const snd_seq_port_info_t*); + + [[gnu::weak]] size_t snd_ump_block_info_sizeof (void); + [[gnu::weak]] size_t snd_ump_endpoint_info_sizeof (void); +} diff --git a/modules/juce_audio_devices/native/juce_CoreMidi_mac.mm b/modules/juce_audio_devices/native/juce_CoreMidi_mac.mm index 6d6a441dc3..471c46ddd7 100644 --- a/modules/juce_audio_devices/native/juce_CoreMidi_mac.mm +++ b/modules/juce_audio_devices/native/juce_CoreMidi_mac.mm @@ -35,17 +35,104 @@ namespace juce { -#ifndef JUCE_LOG_COREMIDI_ERRORS - #define JUCE_LOG_COREMIDI_ERRORS 1 +// Annoyingly, the macOS 15.2 SDK still sets MAC_OS_X_VERSION_MAX_ALLOWED to MAC_OS_VERSION_14_0 +// so we can't use this macro to check for availability of very recent symbols. +#if __has_include ("CoreMIDI/MIDIUMPMutableFunctionBlock.h") + #define JUCE_COREMIDI_UMP_ENDPOINT_CAN_BE_BUILT 1 +#else + #define JUCE_COREMIDI_UMP_ENDPOINT_CAN_BE_BUILT 0 #endif -namespace CoreMidiHelpers +struct CoreMidiHelpers { - struct Consumer + #ifndef JUCE_LOG_COREMIDI_ERRORS + #define JUCE_LOG_COREMIDI_ERRORS 1 + #endif + + static ump::Transport getPlatformTransport() { - virtual ~Consumer() = default; - virtual void consume (ump::Iterator, ump::Iterator, double) = 0; - }; + if (@available (macOS 11, iOS 14, *)) + return ump::Transport::ump; + + return ump::Transport::bytestream; + } + + static constexpr MIDIProtocolID convertToPacketProtocol (ump::PacketProtocol p) + { + return p == ump::PacketProtocol::MIDI_2_0 ? kMIDIProtocol_2_0 + : kMIDIProtocol_1_0; + } + + static constexpr auto int14ToBytes (uint16_t x) + { + return std::array { std::byte (x & 0x7f), std::byte ((x >> 7) & 0x7f) }; + } + + static constexpr auto bytesToInt14 (Span bytes) + { + return UInt16 ((UInt16 (bytes[0]) & 0x7f) | ((UInt16 (bytes[1]) & 0x7f) << 7)); + } + + #if JUCE_COREMIDI_UMP_ENDPOINT_CAN_BE_BUILT + API_AVAILABLE (macos (15), ios (18)) + static ump::EndpointId getIdForNativeEndpoint (const MIDIUMPEndpoint* endpoint) + { + const auto src = getConnectedEndpointInfo (endpoint.MIDISource); + const auto dst = getConnectedEndpointInfo (endpoint.MIDIDestination); + return ump::EndpointId::makeSrcDst (src.identifier, dst.identifier); + } + #endif + + static ump::EndpointId convertToVirtualId (ump::EndpointId e) + { + const auto addPrefix = [] (auto str) { return str.isNotEmpty() ? "VIRTUAL" + str : ""; }; + return ump::EndpointId::makeSrcDst (addPrefix (e.src), addPrefix (e.dst)); + } + + static MIDIEntityRef findEntity (MIDIEndpointRef e) + { + if (e == MIDIEndpointRef{}) + return {}; + + MIDIEntityRef result{}; + MIDIEndpointGetEntity (e, &result); + return result; + } + + static MIDIDeviceRef findDevice (MIDIEntityRef e) + { + if (e == MIDIEntityRef{}) + return {}; + + MIDIDeviceRef result{}; + MIDIEntityGetDevice (e, &result); + return result; + } + + static String getStringProperty (MIDIObjectRef obj, const CFStringRef propertyName) + { + CFObjectHolder result; + + if (MIDIObjectGetStringProperty (obj, propertyName, &result.object) == noErr) + return String::fromCFString (result.object); + + return {}; + } + + static String findName (MIDIObjectRef obj) + { + return getStringProperty (obj, kMIDIPropertyName); + } + + static String findManufacturer (MIDIObjectRef obj) + { + return getStringProperty (obj, kMIDIPropertyManufacturer); + } + + static String findProduct (MIDIObjectRef obj) + { + return getStringProperty (obj, kMIDIPropertyModel); + } static bool checkError (OSStatus err, [[maybe_unused]] int lineNum) { @@ -53,14 +140,30 @@ namespace CoreMidiHelpers return true; #if JUCE_LOG_COREMIDI_ERRORS - Logger::writeToLog ("CoreMIDI error: " + String (lineNum) + " - " + String::toHexString ((int) err)); + Logger::writeToLog ("CoreMIDI error, line " + String (lineNum) + " - " + String::toHexString ((int) err)); #endif return false; } - #undef CHECK_ERROR - #define CHECK_ERROR(a) CoreMidiHelpers::checkError (a, __LINE__) + static void logNSError ([[maybe_unused]] NSError* e, [[maybe_unused]] int lineNum) + { + #if JUCE_LOG_COREMIDI_ERRORS + const auto errorText = std::invoke ([&] + { + if (e == nullptr) + return String(); + + return " - " + nsStringToJuce (e.localizedDescription) + + " - " + nsStringToJuce (e.localizedFailureReason); + }); + + Logger::writeToLog ("CoreMIDI error, line " + String (lineNum) + errorText); + #endif + } + + #pragma push_macro ("JUCE_CHECK_ERROR") + #define JUCE_CHECK_ERROR(a) checkError (a, __LINE__) enum class ImplementationStrategy { @@ -71,231 +174,12 @@ namespace CoreMidiHelpers #if JUCE_MAC_API_VERSION_MIN_REQUIRED_AT_LEAST (11, 0) || JUCE_IOS_API_VERSION_MIN_REQUIRED_AT_LEAST (14, 0) #define JUCE_HAS_OLD_COREMIDI_API 0 - constexpr auto implementationStrategy = ImplementationStrategy::onlyNew; + static constexpr auto implementationStrategy = ImplementationStrategy::onlyNew; #else #define JUCE_HAS_OLD_COREMIDI_API 1 - constexpr auto implementationStrategy = ImplementationStrategy::both; + static constexpr auto implementationStrategy = ImplementationStrategy::both; #endif - struct SenderBase - { - virtual ~SenderBase() noexcept = default; - - virtual void send (MIDIPortRef port, MIDIEndpointRef endpoint, const ump::BytesOnGroup& m) = 0; - virtual void send (MIDIPortRef port, MIDIEndpointRef endpoint, ump::Iterator b, ump::Iterator e) = 0; - }; - - template - struct Sender; - - template <> - struct API_AVAILABLE (macos (11.0), ios (14.0)) Sender final : public SenderBase - { - void send (MIDIPortRef port, MIDIEndpointRef endpoint, const ump::BytesOnGroup& 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 JUCE_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); - }; - - 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::convertImpl (umpConverter, params..., [&] (const auto& v) - { - umpConverter.convert (v, [&] (const ump::View& view) - { - add (view); - - if (end != nullptr) - return; - - send(); - init(); - add (view); - }); - }); - - send(); - } - }; - - #if JUCE_HAS_OLD_COREMIDI_API - template <> - struct Sender final : public SenderBase - { - void send (MIDIPortRef port, MIDIEndpointRef endpoint, const ump::BytesOnGroup& 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::BytesOnGroup& m, double) - { - send (port, endpoint, m); - }); - }); - } - - private: - ump::ToBytestreamConverter bytestreamConverter { 2048 }; - - void oldSendImpl (MIDIPortRef port, MIDIEndpointRef endpoint, const ump::BytesOnGroup& message) - { - #if JUCE_IOS - const MIDITimeStamp timeStamp = mach_absolute_time(); - #else - const MIDITimeStamp timeStamp = AudioGetCurrentHostTime(); - #endif - - HeapBlock allocatedPackets; - MIDIPacketList stackPacket; - auto* packetToSend = &stackPacket; - auto dataSize = message.bytes.size(); - - const auto bytes = message.bytes; - const auto isSysEx = ! bytes.empty() && bytes.front() == std::byte { 0xf0 }; - - if (isSysEx) - { - const int maxPacketSize = 256; - int pos = 0, bytesLeft = (int) dataSize; - const int numPackets = (bytesLeft + maxPacketSize - 1) / maxPacketSize; - allocatedPackets.malloc ((size_t) (32 * (size_t) numPackets + dataSize), 1); - packetToSend = allocatedPackets; - packetToSend->numPackets = (UInt32) numPackets; - - auto* p = packetToSend->packet; - - for (int i = 0; i < numPackets; ++i) - { - p->timeStamp = timeStamp; - p->length = (UInt16) jmin (maxPacketSize, bytesLeft); - memcpy (p->data, message.bytes.data() + pos, p->length); - pos += p->length; - bytesLeft -= p->length; - p = MIDIPacketNext (p); - } - } - else if (dataSize < 65536) // max packet size - { - auto stackCapacity = sizeof (stackPacket.packet->data); - - if (dataSize > stackCapacity) - { - allocatedPackets.malloc ((sizeof (MIDIPacketList) - stackCapacity) + dataSize, 1); - packetToSend = allocatedPackets; - } - - packetToSend->numPackets = 1; - auto& p = *(packetToSend->packet); - p.timeStamp = timeStamp; - p.length = (UInt16) dataSize; - memcpy (p.data, message.bytes.data(), dataSize); - } - else - { - jassertfalse; // packet too large to send! - return; - } - - if (port != 0) - MIDISend (port, endpoint, packetToSend); - else - MIDIReceived (endpoint, packetToSend); - } - }; - #endif - - #if JUCE_HAS_OLD_COREMIDI_API - template <> - struct Sender - { - Sender() - : sender (makeImpl()) - {} - - void send (MIDIPortRef port, MIDIEndpointRef endpoint, const ump::BytesOnGroup& m) - { - sender->send (port, endpoint, m); - } - - void send (MIDIPortRef port, MIDIEndpointRef endpoint, ump::Iterator b, ump::Iterator e) - { - sender->send (port, endpoint, b, e); - } - - private: - static std::unique_ptr makeImpl() - { - if (@available (macOS 11, iOS 14, *)) - return std::make_unique>(); - - return std::make_unique>(); - } - - std::unique_ptr sender; - }; - #endif - - using SenderToUse = Sender; - template class ScopedMidiResource { @@ -340,14 +224,20 @@ namespace CoreMidiHelpers struct PortRefDestructor { - void operator() (MIDIPortRef p) const noexcept { MIDIPortDispose (p); } + void operator() (MIDIPortRef p) const noexcept + { + JUCE_CHECK_ERROR (MIDIPortDispose (p)); + } }; using ScopedPortRef = ScopedMidiResource; struct EndpointRefDestructor { - void operator() (MIDIEndpointRef p) const noexcept { MIDIEndpointDispose (p); } + void operator() (MIDIEndpointRef p) const noexcept + { + JUCE_CHECK_ERROR (MIDIEndpointDispose (p)); + } }; using ScopedEndpointRef = ScopedMidiResource; @@ -356,9 +246,13 @@ namespace CoreMidiHelpers class MidiPortAndEndpoint { public: + MidiPortAndEndpoint() = default; + MidiPortAndEndpoint (ScopedPortRef p, ScopedEndpointRef ep) noexcept - : port (std::move (p)), endpoint (std::move (ep)) - {} + : port (std::move (p)), endpoint (std::move (ep)) {} + + MidiPortAndEndpoint (MidiPortAndEndpoint&&) noexcept = default; + MidiPortAndEndpoint& operator= (MidiPortAndEndpoint&&) noexcept = default; ~MidiPortAndEndpoint() noexcept { @@ -367,62 +261,46 @@ namespace CoreMidiHelpers endpoint.release(); } - void send (const ump::BytesOnGroup& m) - { - sender.send (*port, *endpoint, m); - } + MIDIPortRef getPort() const { return *port; } + MIDIEndpointRef getEndpoint() const { return *endpoint; } - void send (ump::Iterator b, ump::Iterator e) + void start() const { - sender.send (*port, *endpoint, b, e); + JUCE_CHECK_ERROR (MIDIPortConnectSource (*port, *endpoint, nullptr)); } - 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; - + const auto name = findName (entity); + const auto identifier = std::invoke ([&] { + SInt32 objectID = 0; + + if (JUCE_CHECK_ERROR (MIDIObjectGetIntegerProperty (entity, kMIDIPropertyUniqueID, &objectID))) + return String (objectID); + CFObjectHolder str; - if (CHECK_ERROR (MIDIObjectGetStringProperty (entity, kMIDIPropertyName, &str.object))) - info.name = String::fromCFString (str.object); - } + if (JUCE_CHECK_ERROR (MIDIObjectGetStringProperty (entity, kMIDIPropertyUniqueID, &str.object))) + return String::fromCFString (str.object); - SInt32 objectID = 0; + return String{}; + }); - if (CHECK_ERROR (MIDIObjectGetIntegerProperty (entity, kMIDIPropertyUniqueID, &objectID))) - { - info.identifier = String (objectID); - } - else - { - CFObjectHolder str; - - if (CHECK_ERROR (MIDIObjectGetStringProperty (entity, kMIDIPropertyUniqueID, &str.object))) - info.identifier = String::fromCFString (str.object); - } - - return info; + return MidiDeviceInfo { name, identifier }; } static MidiDeviceInfo getEndpointInfo (MIDIEndpointRef endpoint, bool isExternal) { - // NB: don't attempt to use nullptr for refs - it fails in some types of build. - MIDIEntityRef entity = 0; - MIDIEndpointGetEntity (endpoint, &entity); + const auto entity = findEntity (endpoint); // probably virtual - if (entity == 0) + if (entity == MIDIEntityRef{}) return getMidiObjectInfo (endpoint); auto result = getMidiObjectInfo (endpoint); @@ -432,28 +310,29 @@ namespace CoreMidiHelpers result = getMidiObjectInfo (entity); // now consider the device - MIDIDeviceRef device = 0; - MIDIEntityGetDevice (entity, &device); + const auto device = findDevice (entity); - if (device != 0) + if (device == MIDIDeviceRef{}) + return result; + + const auto info = getMidiObjectInfo (device); + + if (info == MidiDeviceInfo()) + return result; + + // if an external device has only one entity, throw away + // the endpoint name and just use the device name + if (isExternal && MIDIDeviceGetNumberOfEntities (device) < 2) { - auto info = getMidiObjectInfo (device); + result.name = info.name; + return result; + } - if (info != MidiDeviceInfo()) - { - // if an external device has only one entity, throw away - // the endpoint name and just use the device name - if (isExternal && MIDIDeviceGetNumberOfEntities (device) < 2) - { - result = info; - } - else if (! result.name.startsWithIgnoreCase (info.name)) - { - // prepend the device name and identifier to the entity's - result.name = (info.name + " " + result.name).trimEnd(); - result.identifier = info.identifier + " " + result.identifier; - } - } + if (! result.name.startsWithIgnoreCase (info.name)) + { + // prepend the device name and identifier to the entity's + return { (info.name + " " + result.name).trimEnd(), + info.identifier + " " + result.identifier }; } return result; @@ -465,13 +344,12 @@ namespace CoreMidiHelpers // Does the endpoint have connections? CFObjectHolder connections; - int numConnections = 0; MIDIObjectGetDataProperty (endpoint, kMIDIPropertyConnectionUniqueID, &connections.object); if (connections.object != nullptr) { - numConnections = ((int) CFDataGetLength (connections.object)) / (int) sizeof (MIDIUniqueID); + const auto numConnections = ((int) CFDataGetLength (connections.object)) / (int) sizeof (MIDIUniqueID); if (numConnections > 0) { @@ -479,34 +357,24 @@ namespace CoreMidiHelpers for (int i = 0; i < numConnections; ++i, ++pid) { - auto id = (MIDIUniqueID) ByteOrder::swapIfLittleEndian ((uint32) *pid); + const auto id = (MIDIUniqueID) ByteOrder::swapIfLittleEndian ((uint32) *pid); MIDIObjectRef connObject; MIDIObjectType connObjectType; - auto err = MIDIObjectFindByUniqueID (id, &connObject, &connObjectType); + const auto err = MIDIObjectFindByUniqueID (id, &connObject, &connObjectType); if (err == noErr) { - MidiDeviceInfo deviceInfo; + const auto deviceInfo = connObjectType == kMIDIObjectType_ExternalSource + || connObjectType == kMIDIObjectType_ExternalDestination + // Connected to an external device's endpoint (10.3 and later). + ? getEndpointInfo (static_cast (connObject), true) + // Connected to an external device (10.2) (or something else, catch-all) + : getMidiObjectInfo (connObject); - if (connObjectType == kMIDIObjectType_ExternalSource - || connObjectType == kMIDIObjectType_ExternalDestination) + if (deviceInfo != MidiDeviceInfo{}) { - // Connected to an external device's endpoint (10.3 and later). - deviceInfo = getEndpointInfo (static_cast (connObject), true); - } - else - { - // Connected to an external device (10.2) (or something else, catch-all) - deviceInfo = getMidiObjectInfo (connObject); - } - - if (deviceInfo != MidiDeviceInfo()) - { - if (result.name.isNotEmpty()) result.name += ", "; - if (result.identifier.isNotEmpty()) result.identifier += ", "; - - result.name += deviceInfo.name; - result.identifier += deviceInfo.identifier; + result.name += (result.name.isNotEmpty() ? ", " : "") + deviceInfo.name; + result.identifier += (result.identifier.isNotEmpty() ? ", " : "") + deviceInfo.identifier; } } } @@ -514,312 +382,912 @@ namespace CoreMidiHelpers } // Here, either the endpoint had no connections, or we failed to obtain names for them. - if (result == MidiDeviceInfo()) + if (result == MidiDeviceInfo{}) return getEndpointInfo (endpoint, false); return result; } - static int createUniqueIDForMidiPort (String deviceName, bool isInput) + class EndpointInfo { - String uniqueID; + public: + ump::Endpoint endpoint; + ump::StaticDeviceInfo staticInfo; + MIDIEndpointRef src{}; + MIDIEndpointRef dst{}; - #ifdef JucePlugin_CFBundleIdentifier - uniqueID = JUCE_STRINGIFY (JucePlugin_CFBundleIdentifier); - #else - auto appBundle = File::getSpecialLocation (File::currentApplicationFile); - CFUniquePtr appBundlePath (appBundle.getFullPathName().toCFString()); - - if (auto bundleURL = CFUniquePtr (CFURLCreateWithFileSystemPath (kCFAllocatorDefault, - appBundlePath.get(), - kCFURLPOSIXPathStyle, - true))) - if (auto bundleRef = CFUniquePtr (CFBundleCreate (kCFAllocatorDefault, bundleURL.get()))) - if (auto bundleId = CFBundleGetIdentifier (bundleRef.get())) - uniqueID = String::fromCFString (bundleId); - #endif - - if (uniqueID.isEmpty()) - uniqueID = String (Random::getSystemRandom().nextInt (1024)); - - uniqueID += "." + deviceName + (isInput ? ".input" : ".output"); - return uniqueID.hashCode(); - } - - static void enableSimulatorMidiSession() - { - #if TARGET_OS_SIMULATOR - static bool hasEnabledNetworkSession = false; - - if (! hasEnabledNetworkSession) + static EndpointInfo withEndpoint (ump::Endpoint ep, + ump::IOKind kind, + MIDIEndpointRef s, + Span ids) { - MIDINetworkSession* session = [MIDINetworkSession defaultSession]; - session.enabled = YES; - session.connectionPolicy = MIDINetworkConnectionPolicy_Anyone; + return kind == ump::IOKind::src ? withSrc (ep, s, ids) : withDst (ep, s, ids); + } - hasEnabledNetworkSession = true; + static EndpointInfo withSrcDst (ump::Endpoint ep, + MIDIEndpointRef s, + Span srcIds, + MIDIEndpointRef d, + Span dstIds) + { + return { ep, s, srcIds, d, dstIds }; + } + + static EndpointInfo withSrc (ump::Endpoint ep, MIDIEndpointRef s, Span srcIds) + { + return { ep, s, srcIds, {}, std::array{} }; + } + + static EndpointInfo withDst (ump::Endpoint ep, MIDIEndpointRef d, Span dstIds) + { + return { ep, {}, std::array{}, d, dstIds }; + } + + MIDIEndpointRef get (ump::IOKind k) const + { + return k == ump::IOKind::src ? src : dst; + } + + private: + EndpointInfo() = default; + EndpointInfo (ump::Endpoint ep, + MIDIEndpointRef s, + Span srcIds, + MIDIEndpointRef d, + Span dstIds) + : endpoint (ep), + staticInfo (ump::StaticDeviceInfo{}.withName (endpoint.getName()) + .withManufacturer (findManufacturer (s != MIDIEndpointRef{} ? s : d)) + .withTransport (getPlatformTransport()) + .withProduct (findProduct (s != MIDIEndpointRef{} ? s : d)) + .withHasSource (s != MIDIEndpointRef{}) + .withHasDestination (s != MIDIEndpointRef{}) + .withLegacyIdentifiersSrc (srcIds) + .withLegacyIdentifiersDst (dstIds)), + src (s), + dst (d) {} + }; + + struct EndpointRemovalListener + { + virtual ~EndpointRemovalListener() = default; + virtual void endpointRemoved (MIDIEndpointRef) = 0; + }; + + /* + This is used to implement the global Endpoints object. + As well as providing access to cached ump::Endpoint instances, + it also stores a mapping of ump::EndpointId to MIDIEndpointRef, which is used to open + devices by their ID. + */ + class SharedEndpointsImplNative + { + public: + MIDIClientRef getClient() const { return client; } + + void getEndpoints (std::vector& x) const + { + endpoints.getEndpoints (x); + } + + std::optional getEndpoint (const ump::EndpointId& x) const + { + if (const auto endpointInfo = getCachedInfo (x)) + return endpointInfo->endpoint; + + return {}; + } + + std::optional getStaticDeviceInfo (const ump::EndpointId& x) const + { + if (const auto endpointInfo = getCachedInfo (x)) + return endpointInfo->staticInfo; + + return {}; + } + + MIDIEndpointRef getNativeEndpointForId (ump::IOKind kind, const ump::EndpointId& x) const + { + return endpoints.getNativeEndpointForId (kind, x); + } + + void addEndpointRemovalListener (EndpointRemovalListener& l) + { + endpointRemovalListeners.add (&l); + } + + void removeEndpointRemovalListener (EndpointRemovalListener& l) + { + endpointRemovalListeners.remove (&l); + } + + #if JUCE_COREMIDI_UMP_ENDPOINT_CAN_BE_BUILT + API_AVAILABLE (macos (15.0), ios (18.0)) + void addVirtualEndpoint (const ump::EndpointId& privateId, MIDIUMPEndpoint* endpoint) + { + virtualEndpoints[privateId] = endpoint; + } + + void removeVirtualEndpoint (const ump::EndpointId& privateId) + { + virtualEndpoints.erase (privateId); } #endif - } - static void globalSystemChangeCallback (const MIDINotification* notification, void*) - { - if (notification != nullptr && notification->messageID == kMIDIMsgSetupChanged) - MidiDeviceListConnectionBroadcaster::get().notify(); - } + void addLegacyVirtualPort (const ump::EndpointId& privateId, EndpointInfo ep) + { + legacyPorts.insert_or_assign (privateId, std::move (ep)); + } - static String getGlobalMidiClientName() - { - if (auto* app = JUCEApplicationBase::getInstance()) - return app->getApplicationName(); + void removeLegacyVirtualPort (const ump::EndpointId& privateId) + { + legacyPorts.erase (privateId); + } - return "JUCE"; - } - - static MIDIClientRef getGlobalMidiClient() - { - static const auto globalMidiClient = [&] + static std::shared_ptr make (ump::EndpointsListener& listener) { JUCE_ASSERT_MESSAGE_THREAD enableSimulatorMidiSession(); - CFUniquePtr name (getGlobalMidiClientName().toCFString()); - MIDIClientRef result{}; - CHECK_ERROR (MIDIClientCreate (name.get(), globalSystemChangeCallback, nullptr, &result)); + const auto name = ump::Endpoints::Impl::getGlobalMidiClientName(); + CFUniquePtr cfName (name.toCFString()); + MIDIClientRef clientRef{}; + + if (! JUCE_CHECK_ERROR (MIDIClientCreate (cfName.get(), systemChangeCallback, nullptr, &clientRef))) + return nullptr; + + const std::shared_ptr result { new SharedEndpointsImplNative (clientRef, listener) }; + Listeners::get().add (result); return result; - }(); + } - return globalMidiClient; - } - - static Array findDevices (bool forInput) - { - // It seems that OSX can be a bit picky about the thread that's first used to - // search for devices. It's safest to use the message thread for calling this. - JUCE_ASSERT_MESSAGE_THREAD - - if (getGlobalMidiClient() == 0) + private: + std::optional getCachedInfo (const ump::EndpointId& x) const { - jassertfalse; + #if JUCE_COREMIDI_UMP_ENDPOINT_CAN_BE_BUILT + if (@available (macos 15, ios 18, *)) + if (const auto iter = virtualEndpoints.find (x); iter != virtualEndpoints.end()) + return getInfoForEndpoint (iter->second); + #endif + + if (const auto iter = legacyPorts.find (x); iter != legacyPorts.end()) + return iter->second; + + return endpoints.getEndpointInfo (x); + } + + class Listeners : private AsyncUpdater + { + public: + ~Listeners() override + { + cancelPendingUpdate(); + } + + void add (std::weak_ptr ptr) + { + const std::scoped_lock lock { mutex }; + listeners.push_back (ptr); + } + + void notify (std::optional removed) + { + { + const std::scoped_lock lock { mutex }; + updateQueue.push_back (removed); + } + + triggerMainThreadUpdate(); + } + + static Listeners& get() + { + static Listeners listeners; + return listeners; + } + + private: + Listeners() = default; + + void triggerMainThreadUpdate() + { + if (MessageManager::getInstance()->isThisTheMessageThread()) + handleAsyncUpdate(); + else + triggerAsyncUpdate(); + } + + void handleAsyncUpdate() override + { + const auto [listenersCopy, updatesCopy] = std::invoke ([&] + { + const std::scoped_lock lock { mutex }; + const ScopeGuard scope { [this] { updateQueue.clear(); } }; + return std::tuple (listeners, updateQueue); + }); + + for (const auto& u : updatesCopy) + for (const auto& l : listenersCopy) + if (const auto locked = l.lock()) + locked->notify (u); + + const std::scoped_lock lock { mutex }; + listeners.erase (std::remove_if (listeners.begin(), listeners.end(), [] (auto& l) { return ! l.lock(); }), + listeners.end()); + } + + std::mutex mutex; + std::vector> listeners; + + // Queue of update messages to send, nullopt indicates that the event was not a removal + std::vector> updateQueue; + }; + + SharedEndpointsImplNative (MIDIClientRef c, ump::EndpointsListener& l) + : client (c), listener (l) + { + if (@available (macos 15.0, ios 18.0, *)) + observers.emplace (*this); + } + + static void enableSimulatorMidiSession() + { + #if TARGET_OS_SIMULATOR + static bool hasEnabledNetworkSession = false; + + if (! hasEnabledNetworkSession) + { + MIDINetworkSession* session = [MIDINetworkSession defaultSession]; + session.enabled = YES; + session.connectionPolicy = MIDINetworkConnectionPolicy_Anyone; + + hasEnabledNetworkSession = true; + } + #endif + } + + static void systemChangeCallback (const MIDINotification* notification, void*) + { + if (notification == nullptr) + return; + + switch (notification->messageID) + { + case kMIDIMsgObjectRemoved: + { + const auto& addRemove = *reinterpret_cast (notification); + + if (addRemove.childType == kMIDIObjectType_Source || addRemove.childType == kMIDIObjectType_Destination) + Listeners::get().notify (addRemove.child); + + break; + } + + case kMIDIMsgObjectAdded: + case kMIDIMsgPropertyChanged: + case kMIDIMsgThruConnectionsChanged: + case kMIDIMsgIOError: + case kMIDIMsgSerialPortOwnerChanged: + Listeners::get().notify (std::nullopt); + break; + + case kMIDIMsgSetupChanged: + #if JUCE_IOS + case kMIDIMsgInternalStart: + #endif + + default: + break; + } + } + + template + static auto copyToByteArray (const Numeric (&arr)[N]) + { + std::array result; + std::transform (std::begin (arr), std::end (arr), std::begin (result), [] (auto x) { return std::byte (x); }); + return result; + } + + template + static void forEachEndpointInDirection (ump::IOKind dir, Fn&& callback) + { + const auto end = dir == ump::IOKind::src + ? MIDIGetNumberOfSources() + : MIDIGetNumberOfDestinations(); + + const auto get = dir == ump::IOKind::src + ? MIDIGetSource + : MIDIGetDestination; + + for (auto i = decltype (end){}; i < end; ++i) + { + const MIDIEndpointRef ref = get (i); + + if (ref == MIDIEndpointRef{}) + continue; + + callback (ref); + } + } + + static std::array getGroupIdentifiers (MIDIDeviceRef dev, ump::IOKind dir) + { + std::array result; + + forEachEndpointInDevice (dev, dir, [&] (auto endpoint) + { + const auto identifier = getConnectedEndpointInfo (endpoint).identifier; + const auto bitmap = getUMPActiveGroupBitmap (endpoint); + + if (! bitmap.has_value()) + { + result[0] = identifier; + return; + } + + if (countNumberOfBits ((uint32) *bitmap) > 1) + return; + + const auto index = log2 (*bitmap); + + if (index >= result.size()) + { + // This shouldn't happen, there's a max of 16 groups! + jassertfalse; + return; + } + + result[(size_t) index] = identifier; + }); + + return result; + } + + static size_t getSyntheticBlocks (MIDIDeviceRef dev, Span blocks) + { + size_t result = 0; + + for (const auto dir : ump::ioKinds) + { + forEachEndpointInDevice (dev, dir, [&] (auto endpoint) + { + const auto bitmap = getUMPActiveGroupBitmap (endpoint); + + if (! bitmap.has_value()) + return; + + const auto numGroups = countNumberOfBits ((uint32) *bitmap); + + if (numGroups == 0) + return; + + const auto index = log2 (*bitmap); + + if (result >= blocks.size()) + { + // This shouldn't happen, there's a max of 32 blocks! + jassertfalse; + return; + } + + blocks[result++] = ump::Block{}.withDirection (dir == ump::IOKind::src ? ump::BlockDirection::sender : ump::BlockDirection::receiver) + .withUiHint (dir == ump::IOKind::src ? ump::BlockUiHint::sender : ump::BlockUiHint::receiver) + .withEnabled (true) + .withFirstGroup ((uint8_t) index) + .withNumGroups ((uint8_t) numGroups) + .withMaxSysex8Streams (0) + .withMIDI1ProxyKind (ump::BlockMIDI1ProxyKind::inapplicable) + .withName (findName (endpoint)); + }); + } + + return result; + } + + #if JUCE_COREMIDI_UMP_ENDPOINT_CAN_BE_BUILT + API_AVAILABLE (macos (15.0), ios (18.0)) + static EndpointInfo getInfoForEndpoint (MIDIUMPEndpoint* endpoint) + { + jassert (endpoint != nullptr); + + const auto protocol = endpoint.MIDIProtocol == kMIDIProtocol_2_0 + ? ump::PacketProtocol::MIDI_2_0 + : ump::PacketProtocol::MIDI_1_0; + + const ump::DeviceInfo deviceInfo + { + copyToByteArray (endpoint.deviceInfo.manufacturerID.sysExIDByte), + int14ToBytes (endpoint.deviceInfo.family), + int14ToBytes (endpoint.deviceInfo.modelNumber), + copyToByteArray (endpoint.deviceInfo.revisionLevel.revisionLevel), + }; + + std::array blocks; + size_t blockCount = 0; + + for (MIDIUMPFunctionBlock* block in endpoint.functionBlocks) + { + const auto fb = ump::Block{}.withName (nsStringToJuce (block.name)) + .withFirstGroup (block.firstGroup) + .withNumGroups (block.totalGroupsSpanned) + .withMaxSysex8Streams (block.maxSysEx8Streams) + .withDirection (ump::BlockDirection { (uint8_t) block.direction }) + .withUiHint (ump::BlockUiHint { (uint8_t) block.UIHint }) + .withMIDI1ProxyKind (ump::BlockMIDI1ProxyKind { (uint8_t) block.MIDI1Info }) + .withEnabled (block.isEnabled); + + blocks[blockCount++] = fb; + } + + const auto srcManufacturer = findManufacturer (endpoint.MIDISource); + const auto dstManufacturer = findManufacturer (endpoint.MIDIDestination); + + const auto item = ump::Endpoint{}.withName (nsStringToJuce (endpoint.name)) + .withProtocol (protocol) + .withDeviceInfo (deviceInfo) + .withProductInstanceId (nsStringToJuce (endpoint.productInstanceID)) + .withReceiveJRSupport (endpoint.hasJRTSReceiveCapability) + .withTransmitJRSupport (endpoint.hasJRTSTransmitCapability) + .withStaticBlocks (endpoint.hasStaticFunctionBlocks) + .withMidi1Support ((endpoint.supportedMIDIProtocols & kMIDIUMPProtocolOptionsMIDI1) != 0) + .withMidi2Support ((endpoint.supportedMIDIProtocols & kMIDIUMPProtocolOptionsMIDI2) != 0) + .withBlocks ({ blocks.data(), blockCount }); + + return EndpointInfo::withSrcDst (item, + endpoint.MIDISource, + getGroupIdentifiers (findDevice (findEntity (endpoint.MIDISource)), ump::IOKind::src), + endpoint.MIDIDestination, + getGroupIdentifiers (findDevice (findEntity (endpoint.MIDIDestination)), ump::IOKind::dst)); + } + #endif + + static std::map findNativeUMPEndpoints() + { + #if JUCE_COREMIDI_UMP_ENDPOINT_CAN_BE_BUILT + if (@available (macos 15, ios 18, *)) + { + std::map result; + + for (MIDIUMPEndpoint* endpoint in MIDIUMPEndpointManager.sharedInstance.UMPEndpoints) + { + const auto id = getIdForNativeEndpoint (endpoint); + const auto info = getInfoForEndpoint (endpoint); + result.emplace (id, info); + } + + return result; + } + #endif + return {}; } - enableSimulatorMidiSession(); - - Array devices; - auto numDevices = (forInput ? MIDIGetNumberOfSources() : MIDIGetNumberOfDestinations()); - - for (ItemCount i = 0; i < numDevices; ++i) + static std::optional getUMPActiveGroupBitmap ([[maybe_unused]] MIDIEndpointRef r) { - MidiDeviceInfo deviceInfo; + #if JUCE_MAC_API_VERSION_CAN_BE_BUILT (14, 0) || JUCE_IOS_API_VERSION_CAN_BE_BUILT (17, 0) + if (@available (macos 14, ios 17, *)) + { + SInt32 bitmap{}; - if (auto dest = forInput ? MIDIGetSource (i) : MIDIGetDestination (i)) - deviceInfo = getConnectedEndpointInfo (dest); + // If there's no active group bitmap, then this endpoint might be a native UMP endpoint + // that supports all groups, but we probably should have found that via the UMP endpoint manager. + if (MIDIObjectGetIntegerProperty (r, kMIDIPropertyUMPActiveGroupBitmap, &bitmap) == noErr) + return bitmap; + } + #endif - if (deviceInfo == MidiDeviceInfo()) - deviceInfo.name = deviceInfo.identifier = ""; - - devices.add (deviceInfo); + return {}; } - return devices; - } + static bool canTransmitGroupless ([[maybe_unused]] MIDIEndpointRef endpoint) + { + #if JUCE_MAC_API_VERSION_CAN_BE_BUILT (14, 0) || JUCE_IOS_API_VERSION_CAN_BE_BUILT (17, 0) + if (@available (macos 14, ios 17, *)) + { + SInt32 result; + if (MIDIObjectGetIntegerProperty (endpoint, kMIDIPropertyUMPCanTransmitGroupless, &result) == noErr) + return result; + } + #endif + + return false; + } + + template + static void forEachEndpointInDevice (MIDIDeviceRef dev, ump::IOKind dir, Callback&& cb) + { + const auto entities = MIDIDeviceGetNumberOfEntities (dev); + + for (auto i = decltype (entities){}; i < entities; ++i) + { + const auto entity = MIDIDeviceGetEntity (dev, i); + + const auto endpoints = dir == ump::IOKind::src + ? MIDIEntityGetNumberOfSources (entity) + : MIDIEntityGetNumberOfDestinations (entity); + + for (auto j = decltype (endpoints){}; j < endpoints; ++j) + { + const auto endpoint = dir == ump::IOKind::src + ? MIDIEntityGetSource (entity, j) + : MIDIEntityGetDestination (entity, j); + + cb (endpoint); + } + } + } + + static std::optional findGrouplessEndpoint (MIDIDeviceRef dev, ump::IOKind dir) + { + std::optional result; + + forEachEndpointInDevice (dev, dir, [&] (auto endpoint) + { + if (! canTransmitGroupless (endpoint)) + return; + + // If this is hit, there's more than one groupless endpoint in this device! + // Maybe the device has multiple UMP endpoints, which is a scenario I didn't + // anticipate. + jassert (! result.has_value()); + result = endpoint; + }); + + return result; + } + + static std::map findAllUMPDevices() + { + std::set devices; + + for (const auto dir : ump::ioKinds) + { + forEachEndpointInDirection (dir, [&] (auto endpoint) + { + const auto groupless = canTransmitGroupless (endpoint); + const auto bitmap = getUMPActiveGroupBitmap (endpoint); + + if (! groupless && ! bitmap.has_value()) + return; + + // If any endpoint in the device has the active group bitmap set, or is able + // to transmit groupless messages, we assume the endpoint supports multiple + // groups, i.e. it is a UMP endpoint. + // This assumption does not hold if the device holds a UMP port and completely + // separate non-multiplexed MIDI 1.0 ports. It also does not hold if the + // endpoint has a single GTB spanning all 16 groups, although in that case + // we probably won't see individual ports referring to GTBs multiplexed on the + // endpoint, so I don't think we need to worry about that. + const auto device = findDevice (findEntity (endpoint)); + + if (device == MIDIDeviceRef{}) + return; + + devices.insert (device); + }); + } + + std::array blocks; + std::map result; + + for (const auto& dev : devices) + { + // If this is a UMP endpoint, then in each direction there should be one + // MIDIEndpointRef that 'contains' the other MIDIEndpointRefs. + // We assume this is the endpoint that can transmit groupless messages. + const auto src = findGrouplessEndpoint (dev, ump::IOKind::src); + const auto dst = findGrouplessEndpoint (dev, ump::IOKind::dst); + const auto numBlocks = getSyntheticBlocks (dev, blocks); + + const auto item = ump::Endpoint{}.withName (findName (dev)) + .withProtocol (ump::PacketProtocol::MIDI_1_0) + .withStaticBlocks (true) + .withBlocks (Span { blocks.data(), numBlocks }); + + const auto srcIds = getGroupIdentifiers (dev, ump::IOKind::src); + const auto dstIds = getGroupIdentifiers (dev, ump::IOKind::dst); + + const auto info = std::invoke ([&]() -> std::optional> + { + if (src.has_value() && dst.has_value()) + { + const auto srcInfo = getConnectedEndpointInfo (*src); + const auto dstInfo = getConnectedEndpointInfo (*dst); + const auto id = ump::EndpointId::makeSrcDst (srcInfo.identifier, dstInfo.identifier); + return std::tuple (id, EndpointInfo::withSrcDst (item, *src, srcIds, *dst, dstIds)); + } + + if (src.has_value()) + { + const auto srcInfo = getConnectedEndpointInfo (*src); + const auto id = ump::EndpointId::make (ump::IOKind::src, srcInfo.identifier); + return std::tuple (id, EndpointInfo::withSrc (item, *src, srcIds)); + } + + if (dst.has_value()) + { + const auto dstInfo = getConnectedEndpointInfo (*dst); + const auto id = ump::EndpointId::make (ump::IOKind::dst, dstInfo.identifier); + return std::tuple (id, EndpointInfo::withDst (item, *dst, dstIds)); + } + + return {}; + }); + + if (info.has_value()) + result.insert (*info); + } + + return result; + } + + struct Endpoints + { + public: + void getEndpoints (std::vector& x) const + { + x.insert (x.end(), orderedEndpoints.begin(), orderedEndpoints.end()); + } + + std::optional getEndpointInfo (const ump::EndpointId& x) const + { + const auto iter = endpoints.find (x); + + if (iter == endpoints.end()) + return {}; + + return iter->second; + } + + MIDIEndpointRef getNativeEndpointForId (ump::IOKind kind, const ump::EndpointId& x) const + { + const auto iter = endpoints.find (x); + + if (iter == endpoints.end()) + return {}; + + return iter->second.get (kind); + } + + private: + std::map endpoints = std::invoke ([&] + { + std::map result; + + // In CoreMIDI, each MIDI device may have any number of Entities, each of which may + // hold any number of input and output ports. Ideally, we'd represent each MIDIDevice + // as a separate UMP endpoint, but there are some difficulties with this approach: + // - A UMP endpoint can only hold up to 16 input/output channels, so we'll run into + // problems with message addressing if the total number of endpoints on the device is + // greater than this. + // - e.g. the IAC client allows more than 16 buses, each of which is a single entity + // containing an input and and output endpoint. + // - The mapping between entities and function blocks isn't clear. If an entity has + // only inputs or only outputs, this is fairly straightforward. But, if the entity + // has a mismatched non-zero number of inputs and outputs this can't be represented + // as a function block. + // Instead of grouping ports in this way, we create a separate UMP endpoint for each + // port. This means that, if the device holds more than 16 ins/outs, we can create + // and endpoint for each, and we don't need to worry about addressing messages to + // groups indices that can't be represented in UMP. + + // It seems that OSX can be a bit picky about the thread that's first used to + // search for devices. It's safest to use the message thread for calling this. + JUCE_ASSERT_MESSAGE_THREAD + + const auto bidiUmpEndpoints = findNativeUMPEndpoints(); + + // Might include duplicates of the devices in the UMPEndpointManager + const auto allUmpDevices = findAllUMPDevices(); + + result.insert (bidiUmpEndpoints.begin(), bidiUmpEndpoints.end()); + // Resolve duplicates in favour of the devices that the system reported as UMP-native + result.insert (allUmpDevices.begin(), allUmpDevices.end()); + + // We've found all UMP endpoints; find the devices holding those endpoints + const auto umpDevices = std::invoke ([&] + { + std::set set; + + for (const auto& item : result) + { + set.insert (findDevice (findEntity (item.second.src))); + set.insert (findDevice (findEntity (item.second.dst))); + } + + return set; + }); + + // Now look for endpoints that aren't part of a UMP device + for (const auto dir : ump::ioKinds) + { + forEachEndpointInDirection (dir, [&] (MIDIEndpointRef e) + { + const auto endpointDevice = findDevice (findEntity (e)); + + if (umpDevices.count (endpointDevice) != 0) + return; + + const auto port = getConnectedEndpointInfo (e); + + if (port == MidiDeviceInfo{}) + return; + + const auto direction = dir == ump::IOKind::src ? ump::BlockDirection::sender : ump::BlockDirection::receiver; + const auto endpoint = ump::IOHelpers::makeProxyEndpoint (port, direction); + auto toEmplace = EndpointInfo::withEndpoint (endpoint.endpoint, + dir, + e, + endpoint.info.getLegacyIdentifiers (dir)); + + const auto emplaceResult = result.emplace (endpoint.id, std::move (toEmplace)); + jassertquiet (emplaceResult.second); + }); + } + + return result; + }); + + // Old JUCE versions return MIDI devices in the order they appear when iterating + // endpoints with MIDIGetSource() and MIDIGetDestination(). The default device of each + // type is the result of calling these functions with an argument of 0. + // In order for the new MIDI implementation to return the same default devices and + // a similar device ordering (for display in lists/dropdowns etc.) we attempt to + // sort the devices so that they contine to match the order returned by MIDIGetSource() + // and MIDIGetDestination(). + std::vector orderedEndpoints = std::invoke ([&] + { + const auto idForEndpoint = std::invoke ([&] + { + std::map map; + + for (const auto& item : endpoints) + { + if (item.second.src != MIDIEndpointRef{}) + map[item.second.src] = item.first; + + if (item.second.dst != MIDIEndpointRef{}) + map[item.second.dst] = item.first; + } + + return map; + }); + + std::vector result; + std::set encountered; + + for (const auto dir : ump::ioKinds) + { + forEachEndpointInDirection (dir, [&] (MIDIEndpointRef e) + { + const auto iter = idForEndpoint.find (e); + + if (iter == idForEndpoint.end()) + return; + + const auto id = iter->second; + + if (encountered.count (id) != 0) + return; + + encountered.insert (id); + result.push_back (id); + }); + } + + return result; + }); + }; + + void notify (std::optional removedEndpoint) + { + JUCE_ASSERT_MESSAGE_THREAD; + + endpoints = Endpoints{}; + + if (removedEndpoint.has_value()) + { + endpointRemovalListeners.call ([&] (auto& c) + { + c.endpointRemoved (*removedEndpoint); + }); + } + + listener.endpointsChanged(); + } + + struct Observers + { + API_AVAILABLE (macos (15.0), ios (18.0)) + explicit Observers ([[maybe_unused]] SharedEndpointsImplNative& owner) + #if JUCE_COREMIDI_UMP_ENDPOINT_CAN_BE_BUILT + : added { MIDIUMPEndpointWasAddedNotification, MIDIUMPEndpointManager.sharedInstance, [&owner] { owner.notify (std::nullopt); } }, + // We should get the individual endpoint removal notifications through the global MIDI notification mechanism + removed { MIDIUMPEndpointWasRemovedNotification, MIDIUMPEndpointManager.sharedInstance, [&owner] { owner.notify (std::nullopt); } }, + updated { MIDIUMPEndpointWasUpdatedNotification, MIDIUMPEndpointManager.sharedInstance, [&owner] { owner.notify (std::nullopt); } }, + fbUpdated { MIDIUMPFunctionBlockWasUpdatedNotification, MIDIUMPEndpointManager.sharedInstance, [&owner] { owner.notify (std::nullopt); } } + #endif + { + } + + #if JUCE_COREMIDI_UMP_ENDPOINT_CAN_BE_BUILT + FunctionNotificationCenterObserver added, removed, updated, fbUpdated; + #endif + }; + + #if JUCE_COREMIDI_UMP_ENDPOINT_CAN_BE_BUILT + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wunguarded-availability-new") + std::map virtualEndpoints; + JUCE_END_IGNORE_WARNINGS_GCC_LIKE + #endif + + std::map legacyPorts; + + MIDIClientRef client; + std::optional observers; + ump::EndpointsListener& listener; + ListenerList endpointRemovalListeners; + Endpoints endpoints; + }; //============================================================================== template struct Receiver; + template + struct CreatorFunctions; + + using ReceiverToUse = Receiver; + using CreatorFunctionsToUse = CreatorFunctions; + template <> struct Receiver { - explicit Receiver (Consumer& cb) - : callback (cb) - {} + explicit Receiver (ump::Consumer& cb) + : callback (cb) {} - void dispatch (const MIDIEventList* list, double time) const + void pushUmp (const MIDIEventList* list) { auto* packet = list->packet; for (uint32_t i = 0; i < list->numPackets; ++i) { + const auto elapsedTime = packet->timeStamp - startTimeNative; + const auto juceTimeMillis = startTimeMillis + (1e6 * (double) timeConversions.hostTimeToNanos (elapsedTime)); + static_assert (sizeof (uint32_t) == sizeof (UInt32) && alignof (uint32_t) == alignof (UInt32), "If this fails, the cast below will be broken too!"); callback.consume (ump::Iterator { packet->words, packet->wordCount }, ump::Iterator { packet->words + packet->wordCount, 0 }, - time); + juceTimeMillis * 0.001); packet = MIDIEventPacketNext (packet); } } private: - Consumer& callback; + ump::Consumer& callback; + + #if JUCE_IOS + const MIDITimeStamp startTimeNative = mach_absolute_time(); + #else + const MIDITimeStamp startTimeNative = AudioGetCurrentHostTime(); + #endif + const uint32_t startTimeMillis = Time::getMillisecondCounter(); + CoreAudioTimeConversions timeConversions; }; - #if JUCE_HAS_OLD_COREMIDI_API - template <> - struct Receiver - { - explicit Receiver (Consumer& cb) - : dispatcher (0, ump::PacketProtocol::MIDI_1_0, 4096), callback (cb) {} - - void dispatch (const MIDIPacketList* list, double time) - { - auto* packet = list->packet; - - for (unsigned int i = 0; i < list->numPackets; ++i) - { - const auto* bytes = unalignedPointerCast (packet->data); - const auto len = readUnalignedlength)> (&(packet->length)); - - dispatcher.dispatch (Span (bytes, len), time, [this] (const ump::View& v, double t) - { - ump::Iterator b { v.data(), v.size() }; - auto e = std::next (b); - callback.consume (b, e, t); - }); - - packet = MIDIPacketNext (packet); - } - } - - private: - ump::BytestreamToUMPDispatcher dispatcher; - Consumer& callback; - }; - #endif - - #if JUCE_HAS_OLD_COREMIDI_API - template <> - struct Receiver - { - explicit Receiver (Consumer& cb) - : newReceiver (cb), oldReceiver (cb) - {} - - void dispatch (const MIDIEventList* list, double time) - { - newReceiver.dispatch (list, time); - } - - void dispatch (const MIDIPacketList* list, double time) - { - oldReceiver.dispatch (list, time); - } - - private: - Receiver newReceiver; - Receiver oldReceiver; - }; - #endif - - using ReceiverToUse = Receiver; - - class MidiPortAndCallback; - CriticalSection callbackLock; - Array activeCallbacks; - - class MidiPortAndCallback : private Consumer - { - public: - MidiPortAndCallback (MidiInput& inputIn, MidiInputCallback& cb) - : input (&inputIn), callback (cb), receiver (*this) - {} - - ~MidiPortAndCallback() override - { - active = false; - - { - const ScopedLock sl (callbackLock); - activeCallbacks.removeFirstMatchingValue (this); - } - - if (portAndEndpoint != nullptr && portAndEndpoint->canStop()) - portAndEndpoint->stop(); - } - - template - void handlePackets (const EventList* list) - { - const auto time = Time::getMillisecondCounterHiRes() * 0.001; - - const ScopedLock sl (callbackLock); - - if (activeCallbacks.contains (this) && active) - receiver.dispatch (list, time); - } - - MidiInput* input = nullptr; - MidiInputCallback& callback; - std::atomic active { false }; - - ReceiverToUse receiver; - - std::unique_ptr portAndEndpoint; - - private: - void consume (ump::Iterator b, ump::Iterator e, double t) override - { - for (auto view : makeRange (b, e)) - { - bytestreamConverter.convert (view, t, [this] (ump::BytesOnGroup x, double time) - { - callback.handleIncomingMidiMessage (input, { x.bytes.data(), (int) x.bytes.size(), time }); - }); - } - } - - ump::ToBytestreamConverter bytestreamConverter { 2048 }; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiPortAndCallback) - }; - - //============================================================================== - static Array getEndpoints (bool isInput) - { - Array endpoints; - auto numDevices = (isInput ? MIDIGetNumberOfSources() : MIDIGetNumberOfDestinations()); - - for (ItemCount i = 0; i < numDevices; ++i) - endpoints.add (isInput ? MIDIGetSource (i) : MIDIGetDestination (i)); - - return endpoints; - } - - struct CreatorFunctionPointers - { - OSStatus (*createInputPort) (ump::PacketProtocol protocol, - MIDIClientRef client, - CFStringRef portName, - void* refCon, - MIDIPortRef* outPort); - - OSStatus (*createDestination) (ump::PacketProtocol protocol, - MIDIClientRef client, - CFStringRef name, - void* refCon, - MIDIEndpointRef* outDest); - - OSStatus (*createSource) (ump::PacketProtocol protocol, - MIDIClientRef client, - CFStringRef name, - MIDIEndpointRef* outSrc); - }; - - template - struct CreatorFunctions; - template <> struct API_AVAILABLE (macos (11.0), ios (14.0)) CreatorFunctions { static OSStatus createInputPort (ump::PacketProtocol protocol, MIDIClientRef client, CFStringRef portName, - void* refCon, + ReceiverToUse* refCon, MIDIPortRef* outPort) { return MIDIInputPortCreateWithProtocol (client, @@ -835,7 +1303,7 @@ namespace CoreMidiHelpers static OSStatus createDestination (ump::PacketProtocol protocol, MIDIClientRef client, CFStringRef name, - void* refCon, + ReceiverToUse* refCon, MIDIEndpointRef* outDest) { return MIDIDestinationCreateWithProtocol (client, @@ -859,32 +1327,63 @@ namespace CoreMidiHelpers outSrc); } - static constexpr CreatorFunctionPointers getCreatorFunctionPointers() - { - return { createInputPort, createDestination, createSource }; - } - private: - static constexpr MIDIProtocolID convertToPacketProtocol (ump::PacketProtocol p) - { - return p == ump::PacketProtocol::MIDI_2_0 ? kMIDIProtocol_2_0 - : kMIDIProtocol_1_0; - } - static void newMidiInputProc (const MIDIEventList* list, void* readProcRefCon, void*) { - static_cast (readProcRefCon)->handlePackets (list); + static_cast (readProcRefCon)->pushUmp (list); } }; #if JUCE_HAS_OLD_COREMIDI_API + template <> + struct Receiver + { + explicit Receiver (ump::Consumer& cb) + : dispatcher (0, ump::PacketProtocol::MIDI_1_0, 4096), callback (cb) {} + + void pushBytes (const MIDIPacketList* list) + { + auto* packet = list->packet; + + for (unsigned int i = 0; i < list->numPackets; ++i) + { + const auto elapsedTime = packet->timeStamp - startTimeNative; + const auto juceTimeMillis = startTimeMillis + (1e6 * (double) timeConversions.hostTimeToNanos (elapsedTime)); + + const auto* bytes = unalignedPointerCast (packet->data); + const auto len = readUnalignedlength)> (&(packet->length)); + + dispatcher.dispatch (Span (bytes, len), juceTimeMillis * 0.001, [this] (const ump::View& v, double time) + { + ump::Iterator b { v.data(), v.size() }; + auto e = std::next (b); + callback.consume (b, e, time); + }); + + packet = MIDIPacketNext (packet); + } + } + + private: + ump::BytestreamToUMPDispatcher dispatcher; + ump::Consumer& callback; + + #if JUCE_IOS + const MIDITimeStamp startTimeNative = mach_absolute_time(); + #else + const MIDITimeStamp startTimeNative = AudioGetCurrentHostTime(); + #endif + const uint32_t startTimeMillis = Time::getMillisecondCounter(); + CoreAudioTimeConversions timeConversions; + }; + template <> struct CreatorFunctions { static OSStatus createInputPort (ump::PacketProtocol, MIDIClientRef client, CFStringRef portName, - void* refCon, + ReceiverToUse* refCon, MIDIPortRef* outPort) { return MIDIInputPortCreate (client, portName, oldMidiInputProc, refCon, outPort); @@ -893,7 +1392,7 @@ namespace CoreMidiHelpers static OSStatus createDestination (ump::PacketProtocol, MIDIClientRef client, CFStringRef name, - void* refCon, + ReceiverToUse* refCon, MIDIEndpointRef* outDest) { return MIDIDestinationCreate (client, name, oldMidiInputProc, refCon, outDest); @@ -907,39 +1406,64 @@ namespace CoreMidiHelpers return MIDISourceCreate (client, name, outSrc); } - static constexpr CreatorFunctionPointers getCreatorFunctionPointers() - { - return { createInputPort, createDestination, createSource }; - } - private: static void oldMidiInputProc (const MIDIPacketList* list, void* readProcRefCon, void*) { - static_cast (readProcRefCon)->handlePackets (list); + static_cast (readProcRefCon)->pushBytes (list); } }; - #endif - #if JUCE_HAS_OLD_COREMIDI_API + template <> + struct Receiver + { + explicit Receiver (ump::Consumer& cb) + : newReceiver (cb), oldReceiver (cb) {} + + void pushUmp (const MIDIEventList* list) + { + newReceiver.pushUmp (list); + } + + void pushBytes (const MIDIPacketList* list) + { + oldReceiver.pushBytes (list); + } + + private: + Receiver newReceiver; + Receiver oldReceiver; + }; + template <> struct CreatorFunctions { + private: + template + static auto call (Fn&& fn) + { + if (@available (macOS 11, iOS 14, *)) + return fn (CreatorFunctions{}); + + return fn (CreatorFunctions{}); + } + + public: static OSStatus createInputPort (ump::PacketProtocol protocol, MIDIClientRef client, CFStringRef portName, - void* refCon, + ReceiverToUse* refCon, MIDIPortRef* outPort) { - return getCreatorFunctionPointers().createInputPort (protocol, client, portName, refCon, outPort); + return call ([&] (const auto& fn) { return fn.createInputPort (protocol, client, portName, refCon, outPort); }); } static OSStatus createDestination (ump::PacketProtocol protocol, MIDIClientRef client, CFStringRef name, - void* refCon, + ReceiverToUse* refCon, MIDIEndpointRef* outDest) { - return getCreatorFunctionPointers().createDestination (protocol, client, name, refCon, outDest); + return call ([&] (const auto& fn) { return fn.createDestination (protocol, client, name, refCon, outDest); }); } static OSStatus createSource (ump::PacketProtocol protocol, @@ -947,346 +1471,1065 @@ namespace CoreMidiHelpers CFStringRef name, MIDIEndpointRef* outSrc) { - return getCreatorFunctionPointers().createSource (protocol, client, name, outSrc); - } - - private: - static CreatorFunctionPointers getCreatorFunctionPointers() - { - if (@available (macOS 11, iOS 14, *)) - return CreatorFunctions::getCreatorFunctionPointers(); - - return CreatorFunctions::getCreatorFunctionPointers(); + return call ([&] (const auto& fn) { return fn.createSource (protocol, client, name, outSrc); }); } }; #endif - using CreatorFunctionsToUse = CreatorFunctions; -} - -//============================================================================== -class MidiInput::Pimpl : public CoreMidiHelpers::MidiPortAndCallback -{ -public: - using MidiPortAndCallback::MidiPortAndCallback; - - static std::unique_ptr makePimpl (MidiInput& midiInput, - MidiInputCallback* midiInputCallback) + //============================================================================== + struct OutputInterface { - if (midiInputCallback == nullptr) - return {}; + virtual ~OutputInterface() = default; + virtual bool send (const MidiPortAndEndpoint& portAndEndpoint, ump::Iterator b, ump::Iterator e) = 0; + }; - return std::make_unique (midiInput, *midiInputCallback); - } + template + class Output; - template - static std::unique_ptr makeInput (const String& name, - const String& identifier, - Args&&... args) + using OutputToUse = Output; + + template <> + class API_AVAILABLE (macos (11.0), ios (14.0)) Output final : public OutputInterface { - using namespace CoreMidiHelpers; - - if (auto midiInput = rawToUniquePtr (new MidiInput (name, identifier))) + public: + bool send (const MidiPortAndEndpoint& portAndEndpoint, ump::Iterator b, ump::Iterator e) override { - if ((midiInput->internal = makePimpl (*midiInput, std::forward (args)...))) - { - const ScopedLock sl (callbackLock); - activeCallbacks.add (midiInput->internal.get()); + #if JUCE_IOS + const MIDITimeStamp timeStamp = mach_absolute_time(); + #else + const MIDITimeStamp timeStamp = AudioGetCurrentHostTime(); + #endif - return midiInput; + MIDIEventList stackList = {}; + MIDIEventPacket* end = nullptr; + + const auto isMessageKind = [] (const auto kind) + { + return [kind] (const ump::View& v) { return ump::Utils::getMessageType (v[0]) == kind; }; + }; + + const auto containsChannelVoice2 = std::any_of (b, e, isMessageKind (ump::Utils::MessageKind::channelVoice2)); + + #if JUCE_ASSERTIONS_ENABLED_OR_LOGGED + const auto containsChannelVoice1 = std::any_of (b, e, isMessageKind (ump::Utils::MessageKind::channelVoice1)); + + // If this is hit, you're trying to send a mixture of Channel Voice 1.0 and Channel Voice 2.0 + // messages. This is probably a mistake! + jassert (! (containsChannelVoice1 && containsChannelVoice2)); + #endif + + const auto init = [&] + { + // We rely on the system to convert from the declared packet protocol if necessary. + end = MIDIEventListInit (&stackList, containsChannelVoice2 ? kMIDIProtocol_2_0 : kMIDIProtocol_1_0); + }; + + const auto send = [&] + { + JUCE_CHECK_ERROR (portAndEndpoint.getPort() != 0 ? MIDISendEventList (portAndEndpoint.getPort(), portAndEndpoint.getEndpoint(), &stackList) + : MIDIReceivedEventList (portAndEndpoint.getEndpoint(), &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(); + + std::for_each (b, e, [&] (ump::View view) + { + add (view); + + if (end != nullptr) + return; + + send(); + init(); + add (view); + }); + + send(); + + return true; + } + }; + + #if JUCE_HAS_OLD_COREMIDI_API + template <> + class Output final : public OutputInterface + { + public: + bool send (const MidiPortAndEndpoint& portAndEndpoint, ump::Iterator b, ump::Iterator e) override + { + for (const auto& v : makeRange (b, e)) + { + converter.convert (v, 0.0, [&] (ump::BytesOnGroup msg, double) + { + sendBytes (portAndEndpoint, msg.bytes); + }); } + + return true; } - return {}; - } - - template - static std::unique_ptr openDevice (ump::PacketProtocol protocol, - const String& deviceIdentifier, - Args&&... args) - { - using namespace CoreMidiHelpers; - - if (deviceIdentifier.isEmpty()) - return {}; - - if (auto client = getGlobalMidiClient()) + private: + void sendBytes (const MidiPortAndEndpoint& portAndEndpoint, Span message) { - for (auto& endpoint : getEndpoints (true)) + #if JUCE_IOS + const MIDITimeStamp timeStamp = mach_absolute_time(); + #else + const MIDITimeStamp timeStamp = AudioGetCurrentHostTime(); + #endif + + HeapBlock allocatedPackets; + MIDIPacketList stackPacket; + auto* packetToSend = &stackPacket; + auto dataSize = message.size(); + + const auto isSysEx = ! message.empty() && message.front() == std::byte { 0xf0 }; + + if (isSysEx) { - auto endpointInfo = getConnectedEndpointInfo (endpoint); + const int maxPacketSize = 256; + int pos = 0, bytesLeft = (int) dataSize; + const int numPackets = (bytesLeft + maxPacketSize - 1) / maxPacketSize; + allocatedPackets.malloc ((size_t) (32 * (size_t) numPackets + dataSize), 1); + packetToSend = allocatedPackets; + packetToSend->numPackets = (UInt32) numPackets; - if (deviceIdentifier != endpointInfo.identifier) - continue; + auto* p = packetToSend->packet; - CFObjectHolder cfName; - - if (! CHECK_ERROR (MIDIObjectGetStringProperty (endpoint, kMIDIPropertyName, &cfName.object))) - continue; - - if (auto input = makeInput (endpointInfo.name, endpointInfo.identifier, std::forward (args)...)) + for (int i = 0; i < numPackets; ++i) { - MIDIPortRef port; - - if (! CHECK_ERROR (CreatorFunctionsToUse::createInputPort (protocol, client, cfName.object, input->internal.get(), &port))) - continue; - - ScopedPortRef scopedPort { port }; - - if (! CHECK_ERROR (MIDIPortConnectSource (*scopedPort, endpoint, nullptr))) - continue; - - input->internal->portAndEndpoint = std::make_unique (std::move (scopedPort), ScopedEndpointRef { endpoint }); - return input; + p->timeStamp = timeStamp; + p->length = (UInt16) jmin (maxPacketSize, bytesLeft); + memcpy (p->data, message.data() + pos, p->length); + pos += p->length; + bytesLeft -= p->length; + p = MIDIPacketNext (p); } } - } - - return {}; - } - - template - static std::unique_ptr createDevice (ump::PacketProtocol protocol, - const String& deviceName, - Args&&... args) - { - using namespace CoreMidiHelpers; - - if (auto client = getGlobalMidiClient()) - { - auto deviceIdentifier = createUniqueIDForMidiPort (deviceName, true); - - if (auto input = makeInput (deviceName, String (deviceIdentifier), std::forward (args)...)) + else if (dataSize < 65536) // max packet size { - MIDIEndpointRef endpoint; - CFUniquePtr name (deviceName.toCFString()); + auto stackCapacity = sizeof (stackPacket.packet->data); - auto err = CreatorFunctionsToUse::createDestination (protocol, client, name.get(), input->internal.get(), &endpoint); - ScopedEndpointRef scopedEndpoint { endpoint }; - - #if JUCE_IOS - if (err == kMIDINotPermitted) + if (dataSize > stackCapacity) { - // If you've hit this assertion then you probably haven't enabled the "Audio Background Capability" - // setting in the iOS exporter for your app - this is required if you want to create a MIDI device! - jassertfalse; - return {}; + allocatedPackets.malloc ((sizeof (MIDIPacketList) - stackCapacity) + dataSize, 1); + packetToSend = allocatedPackets; } - #endif - if (! CHECK_ERROR (err)) - return {}; - - if (! CHECK_ERROR (MIDIObjectSetIntegerProperty (endpoint, kMIDIPropertyUniqueID, (SInt32) deviceIdentifier))) - return {}; - - input->internal->portAndEndpoint = std::make_unique (ScopedPortRef{}, std::move (scopedEndpoint)); - return input; + packetToSend->numPackets = 1; + auto& p = *(packetToSend->packet); + p.timeStamp = timeStamp; + p.length = (UInt16) dataSize; + memcpy (p.data, message.data(), dataSize); } + else + { + jassertfalse; // packet too large to send! + return; + } + + if (portAndEndpoint.getPort() != 0) + MIDISend (portAndEndpoint.getPort(), portAndEndpoint.getEndpoint(), packetToSend); + else + MIDIReceived (portAndEndpoint.getEndpoint(), packetToSend); } - return {}; - } -}; + ump::ToBytestreamConverter converter { 4096 }; + }; -//============================================================================== -Array MidiInput::getAvailableDevices() -{ - return CoreMidiHelpers::findDevices (true); -} - -MidiDeviceInfo MidiInput::getDefaultDevice() -{ - return getAvailableDevices().getFirst(); -} - -std::unique_ptr MidiInput::openDevice (const String& deviceIdentifier, MidiInputCallback* callback) -{ - if (callback == nullptr) - return nullptr; - - return Pimpl::openDevice (ump::PacketProtocol::MIDI_1_0, - deviceIdentifier, - callback); -} - -std::unique_ptr MidiInput::createNewDevice (const String& deviceName, MidiInputCallback* callback) -{ - return Pimpl::createDevice (ump::PacketProtocol::MIDI_1_0, - deviceName, - callback); -} - -StringArray MidiInput::getDevices() -{ - StringArray deviceNames; - - for (auto& d : getAvailableDevices()) - deviceNames.add (d.name); - - return deviceNames; -} - -int MidiInput::getDefaultDeviceIndex() -{ - return 0; -} - -std::unique_ptr MidiInput::openDevice (int index, MidiInputCallback* callback) -{ - return openDevice (getAvailableDevices()[index].identifier, callback); -} - -MidiInput::MidiInput (const String& deviceName, const String& deviceIdentifier) - : deviceInfo (deviceName, deviceIdentifier) -{ -} - -MidiInput::~MidiInput() = default; - -void MidiInput::start() -{ - const ScopedLock sl (CoreMidiHelpers::callbackLock); - internal->active = true; -} - -void MidiInput::stop() -{ - const ScopedLock sl (CoreMidiHelpers::callbackLock); - internal->active = false; -} - -//============================================================================== -class MidiOutput::Pimpl : public CoreMidiHelpers::MidiPortAndEndpoint -{ -public: - using MidiPortAndEndpoint::MidiPortAndEndpoint; -}; - -Array MidiOutput::getAvailableDevices() -{ - return CoreMidiHelpers::findDevices (false); -} - -MidiDeviceInfo MidiOutput::getDefaultDevice() -{ - return getAvailableDevices().getFirst(); -} - -std::unique_ptr MidiOutput::openDevice (const String& deviceIdentifier) -{ - if (deviceIdentifier.isEmpty()) - return {}; - - using namespace CoreMidiHelpers; - - if (auto client = getGlobalMidiClient()) + template <> + class Output final { - for (auto& endpoint : getEndpoints (false)) + public: + bool send (const MidiPortAndEndpoint& portAndEndpoint, ump::Iterator b, ump::Iterator e) { - auto endpointInfo = getConnectedEndpointInfo (endpoint); + return outputInterface->send (portAndEndpoint, b, e); + } - if (deviceIdentifier != endpointInfo.identifier) - continue; + private: + std::unique_ptr outputInterface = std::invoke ([&]() -> std::unique_ptr + { + if (@available (macOS 11, iOS 14, *)) + return std::make_unique>(); + + return std::make_unique>(); + }); + }; + #endif + + class Connection : private EndpointRemovalListener + { + public: + explicit Connection (std::shared_ptr k) + : knownEndpoints (k) + { + jassert (knownEndpoints != nullptr); + knownEndpoints->addEndpointRemovalListener (*this); + } + + ~Connection() override + { + knownEndpoints->removeEndpointRemovalListener (*this); + } + + void addDisconnectionListener (ump::DisconnectionListener& l) + { + disconnectListeners.add (&l); + } + + void removeDisconnectionListener (ump::DisconnectionListener& l) + { + disconnectListeners.remove (&l); + } + + std::shared_ptr getKnownEndpoints() const + { + return knownEndpoints; + } + + MidiPortAndEndpoint portAndEndpoint; + + private: + void endpointRemoved (MIDIEndpointRef x) override + { + if (x == portAndEndpoint.getEndpoint()) + disconnectListeners.call ([] (auto& c) { c.disconnected(); }); + } + + std::shared_ptr knownEndpoints; + ListenerList disconnectListeners; + }; + + static int createUniqueIDForMidiPort (String deviceName, ump::IOKind direction) + { + String uniqueID; + + #ifdef JucePlugin_CFBundleIdentifier + uniqueID = JUCE_STRINGIFY (JucePlugin_CFBundleIdentifier); + #else + auto appBundle = File::getSpecialLocation (File::currentApplicationFile); + CFUniquePtr appBundlePath (appBundle.getFullPathName().toCFString()); + + if (auto bundleURL = CFUniquePtr (CFURLCreateWithFileSystemPath (kCFAllocatorDefault, + appBundlePath.get(), + kCFURLPOSIXPathStyle, + true))) + if (auto bundleRef = CFUniquePtr (CFBundleCreate (kCFAllocatorDefault, bundleURL.get()))) + if (auto bundleId = CFBundleGetIdentifier (bundleRef.get())) + uniqueID = String::fromCFString (bundleId); + #endif + + if (uniqueID.isEmpty()) + uniqueID = String (Random::getSystemRandom().nextInt (1024)); + + uniqueID += "." + deviceName + (direction == ump::IOKind::dst ? ".input" : ".output"); + return uniqueID.hashCode(); + } + + class ConnectionToSrc : private ump::Consumer + { + public: + void addConsumer (ump::Consumer& l) + { + consumers.add (l); + } + + void removeConsumer (ump::Consumer& l) + { + consumers.remove (l); + } + + void addDisconnectionListener (ump::DisconnectionListener& l) + { + connection.addDisconnectionListener (l); + } + + void removeDisconnectionListener (ump::DisconnectionListener& l) + { + connection.removeDisconnectionListener (l); + } + + static std::shared_ptr make (std::shared_ptr cachedEndpoints, + ump::EndpointId id) + { + const auto endpoint = cachedEndpoints->getNativeEndpointForId (ump::IOKind::src, id); + + if (endpoint == MIDIEndpointRef{}) + { + jassertfalse; + return {}; + } CFObjectHolder cfName; - if (! CHECK_ERROR (MIDIObjectGetStringProperty (endpoint, kMIDIPropertyName, &cfName.object))) - continue; + if (! JUCE_CHECK_ERROR (MIDIObjectGetStringProperty (endpoint, kMIDIPropertyName, &cfName.object))) + return {}; + + auto receiver = rawToUniquePtr (new ConnectionToSrc (cachedEndpoints)); MIDIPortRef port; + const auto error = CreatorFunctionsToUse::createInputPort (ump::PacketProtocol::MIDI_2_0, + cachedEndpoints->getClient(), + cfName.object, + &receiver->receiver, + &port); - if (! CHECK_ERROR (MIDIOutputPortCreate (client, cfName.object, &port))) - continue; + if (! JUCE_CHECK_ERROR (error)) + return {}; - ScopedPortRef scopedPort { port }; - - auto midiOutput = rawToUniquePtr (new MidiOutput (endpointInfo.name, endpointInfo.identifier)); - midiOutput->internal = std::make_unique (std::move (scopedPort), ScopedEndpointRef { endpoint }); - - return midiOutput; + receiver->connection.portAndEndpoint = { ScopedPortRef { port }, ScopedEndpointRef { endpoint } }; + receiver->connection.portAndEndpoint.start(); + return receiver; } - } - 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 }; - - #if JUCE_IOS - if (err == kMIDINotPermitted) + #if JUCE_COREMIDI_UMP_ENDPOINT_CAN_BE_BUILT + API_AVAILABLE (macos (15), ios (18)) + static std::shared_ptr makeVirtualEndpoint (std::shared_ptr cachedEndpoints, + const String& deviceName, + const ump::DeviceInfo& deviceInfo, + const String& deviceIdentity, + ump::PacketProtocol protocol) { - // If you've hit this assertion then you probably haven't enabled the "Audio Background Capability" - // setting in the iOS exporter for your app - this is required if you want to create a MIDI device! - jassertfalse; - return {}; + std::shared_ptr receiver = rawToUniquePtr (new ConnectionToSrc (cachedEndpoints)); + std::weak_ptr weak = receiver; + + receiver->endpoint.reset ([[MIDIUMPMutableEndpoint alloc] initWithName: juceStringToNS (deviceName) + deviceInfo: makeDeviceInfo (deviceInfo).get() + productInstanceID: juceStringToNS (deviceIdentity) + MIDIProtocol: convertToPacketProtocol (protocol) + destinationCallback: ^(const MIDIEventList* list, void*) + { + if (auto strong = weak.lock()) + strong->receiver.pushUmp (list); + }]); + + if (receiver->endpoint == nullptr) + { + jassertfalse; + return {}; + } + + receiver->connection.portAndEndpoint = { {}, ScopedEndpointRef { receiver->endpoint.get().MIDIDestination } }; + + return receiver; + } + + API_AVAILABLE (macos (15), ios (18)) + MIDIUMPMutableEndpoint* getEndpoint() + { + return endpoint.get(); } #endif - if (! CHECK_ERROR (err)) - return {}; + MIDIEndpointRef getMIDIEndpoint() const + { + return connection.portAndEndpoint.getEndpoint(); + } - auto deviceIdentifier = createUniqueIDForMidiPort (deviceName, false); + std::shared_ptr getKnownEndpoints() const + { + return connection.getKnownEndpoints(); + } - if (! CHECK_ERROR (MIDIObjectSetIntegerProperty (*scopedEndpoint, kMIDIPropertyUniqueID, (SInt32) deviceIdentifier))) - return {}; + static std::tuple, ump::EndpointId> makeVirtual (std::shared_ptr cachedEndpoints, + const String& deviceName) + { + const auto direction = ump::IOKind::dst; + const auto deviceIdentifier = createUniqueIDForMidiPort (deviceName, direction); + const auto endpointId = ump::EndpointId::make (direction, String (deviceIdentifier)); - auto midiOutput = rawToUniquePtr (new MidiOutput (deviceName, String (deviceIdentifier))); - midiOutput->internal = std::make_unique (ScopedPortRef{}, std::move (scopedEndpoint)); + const CFUniquePtr cfName (deviceName.toCFString()); + std::shared_ptr receiver = rawToUniquePtr (new ConnectionToSrc (cachedEndpoints)); - return midiOutput; + MIDIEndpointRef endpoint{}; + const auto err = CreatorFunctionsToUse::createDestination (ump::PacketProtocol::MIDI_1_0, + cachedEndpoints->getClient(), + cfName.get(), + &receiver->receiver, + &endpoint); + ScopedEndpointRef scopedEndpoint { endpoint }; + + #if JUCE_IOS + if (err == kMIDINotPermitted) + { + // If you've hit this assertion then you probably haven't enabled the "Audio Background Capability" + // setting in the iOS exporter for your app - this is required if you want to create a MIDI device! + jassertfalse; + return {}; + } + #endif + + if (err != noErr) + return {}; + + if (MIDIObjectSetIntegerProperty (*scopedEndpoint, kMIDIPropertyUniqueID, (SInt32) deviceIdentifier) != noErr) + return {}; + + receiver->connection.portAndEndpoint = { {}, std::move (scopedEndpoint) }; + return { receiver, endpointId }; + } + + private: + explicit ConnectionToSrc (std::shared_ptr e) + : connection (e) + { + } + + #if JUCE_COREMIDI_UMP_ENDPOINT_CAN_BE_BUILT + API_AVAILABLE (macos (15), ios (18)) + static NSUniquePtr makeDeviceInfo (const ump::DeviceInfo& x) + { + const auto castToByte = [] (auto b) { return Byte (b); }; + + MIDI2DeviceManufacturer manufacturer{}; + std::transform (x.manufacturer.begin(), x.manufacturer.end(), manufacturer.sysExIDByte, castToByte); + + MIDI2DeviceRevisionLevel revision{}; + std::transform (x.revision.begin(), x.revision.end(), revision.revisionLevel, castToByte); + + return NSUniquePtr + { + [[MIDI2DeviceInfo alloc] initWithManufacturerID: manufacturer + family: bytesToInt14 (x.family) + modelNumber: bytesToInt14 (x.modelNumber) + revisionLevel: revision] + }; + } + #endif + + void consume (ump::Iterator b, ump::Iterator e, double time) override + { + consumers.call ([&] (auto& c) { c.consume (b, e, time); }); + } + + Connection connection; + WaitFreeListeners consumers; + ReceiverToUse receiver { *this }; + + #if JUCE_COREMIDI_UMP_ENDPOINT_CAN_BE_BUILT + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wunguarded-availability-new") + NSUniquePtr endpoint; + JUCE_END_IGNORE_WARNINGS_GCC_LIKE + #endif + }; + + class ConnectionToDst + { + public: + void send (ump::Iterator b, ump::Iterator e) + { + output.send (connection.portAndEndpoint, b, e); + } + + void addDisconnectionListener (ump::DisconnectionListener& l) + { + connection.addDisconnectionListener (l); + } + + void removeDisconnectionListener (ump::DisconnectionListener& l) + { + connection.removeDisconnectionListener (l); + } + + MIDIEndpointRef getMIDIEndpoint() const + { + return connection.portAndEndpoint.getEndpoint(); + } + + static std::shared_ptr make (std::shared_ptr cachedEndpoints, + ump::EndpointId id) + { + const auto endpoint = cachedEndpoints->getNativeEndpointForId (ump::IOKind::dst, id); + + if (endpoint == MIDIEndpointRef{}) + { + jassertfalse; + return {}; + } + + CFObjectHolder cfName; + + if (! JUCE_CHECK_ERROR (MIDIObjectGetStringProperty (endpoint, kMIDIPropertyName, &cfName.object))) + return {}; + + MIDIPortRef port; + + if (! JUCE_CHECK_ERROR (MIDIOutputPortCreate (cachedEndpoints->getClient(), cfName.object, &port))) + return {}; + + auto result = rawToUniquePtr (new ConnectionToDst (cachedEndpoints)); + result->connection.portAndEndpoint = { ScopedPortRef { port }, ScopedEndpointRef { endpoint } }; + return result; + } + + static std::shared_ptr fromVirtual (std::shared_ptr cachedEndpoints, + ScopedEndpointRef scopedEndpoint) + { + std::shared_ptr sender = rawToUniquePtr (new ConnectionToDst (cachedEndpoints)); + sender->connection.portAndEndpoint = { {}, std::move (scopedEndpoint) }; + return sender; + } + + static std::tuple, ump::EndpointId> makeVirtual (std::shared_ptr cachedEndpoints, + const String& deviceName) + { + const auto direction = ump::IOKind::src; + const auto deviceIdentifier = createUniqueIDForMidiPort (deviceName, direction); + const auto endpointId = ump::EndpointId::make (direction, String (deviceIdentifier)); + + const CFUniquePtr cfName (deviceName.toCFString()); + + MIDIEndpointRef endpoint{}; + const auto err = CreatorFunctionsToUse::createSource (ump::PacketProtocol::MIDI_1_0, + cachedEndpoints->getClient(), + cfName.get(), + &endpoint); + ScopedEndpointRef scopedEndpoint { endpoint }; + + #if JUCE_IOS + if (err == kMIDINotPermitted) + { + // If you've hit this assertion then you probably haven't enabled the "Audio Background Capability" + // setting in the iOS exporter for your app - this is required if you want to create a MIDI device! + jassertfalse; + return {}; + } + #endif + + if (err != noErr) + return {}; + + if (MIDIObjectSetIntegerProperty (*scopedEndpoint, kMIDIPropertyUniqueID, (SInt32) deviceIdentifier) != noErr) + return {}; + + return { fromVirtual (cachedEndpoints, std::move (scopedEndpoint)), endpointId }; + } + + std::shared_ptr getKnownEndpoints() const + { + return connection.getKnownEndpoints(); + } + + private: + explicit ConnectionToDst (std::shared_ptr e) + : connection (e) + { + } + + Connection connection; + OutputToUse output; + }; + + class InputImplNative : public ump::Input::Impl::Native, + private ump::Consumer + { + public: + InputImplNative (std::shared_ptr k, + ump::EndpointId i, + ump::DisconnectionListener& l, + ump::PacketProtocol p, + ump::Consumer& cb) + : converter (p), + connection (k), + id (std::move (i)), + disconnectListener (l), + consumer (cb) + { + connection->addDisconnectionListener (disconnectListener); + connection->addConsumer (consumer); + } + + ~InputImplNative() override + { + connection->removeConsumer (consumer); + connection->removeDisconnectionListener (disconnectListener); + } + + ump::EndpointId getEndpointId() const override + { + return id; + } + + ump::PacketProtocol getProtocol() const override + { + return converter.getProtocol(); + } + + private: + void consume (ump::Iterator b, ump::Iterator e, double time) override + { + for (const auto& view : makeRange (b, e)) + { + converter.convert (view, [&] (const auto& v) + { + ump::Iterator begin { v.data(), v.size() }; + consumer.consume (begin, std::next (begin), time); + }); + } + } + + ump::GenericUMPConverter converter; + std::shared_ptr connection; + ump::EndpointId id; + ump::DisconnectionListener& disconnectListener; + ump::Consumer& consumer; + }; + + class OutputImplNative final : public ump::Output::Impl::Native + { + public: + OutputImplNative (std::shared_ptr k, + ump::EndpointId i, + ump::DisconnectionListener& l) + : connection (k), id (std::move (i)), disconnectListener (l) + { + connection->addDisconnectionListener (disconnectListener); + } + + ~OutputImplNative() override + { + connection->removeDisconnectionListener (disconnectListener); + } + + bool send (ump::Iterator b, ump::Iterator e) override + { + connection->send (b, e); + return true; + } + + ump::EndpointId getEndpointId() const override + { + return id; + } + + private: + std::shared_ptr connection; + ump::EndpointId id; + ump::DisconnectionListener& disconnectListener; + }; + + #if JUCE_COREMIDI_UMP_ENDPOINT_CAN_BE_BUILT + API_AVAILABLE (macos (15), ios (18)) + static NSUniquePtr makeBlock (const ump::Block& block) + { + const auto direction = std::invoke ([&] + { + switch (block.getDirection()) + { + case ump::BlockDirection::unknown: return kMIDIUMPFunctionBlockDirectionUnknown; + case ump::BlockDirection::receiver: return kMIDIUMPFunctionBlockDirectionInput; + case ump::BlockDirection::sender: return kMIDIUMPFunctionBlockDirectionOutput; + case ump::BlockDirection::bidirectional: return kMIDIUMPFunctionBlockDirectionBidirectional; + } + + return kMIDIUMPFunctionBlockDirectionUnknown; + }); + + const auto midi1Kind = std::invoke ([&] + { + switch (block.getMIDI1ProxyKind()) + { + case ump::BlockMIDI1ProxyKind::inapplicable: return kMIDIUMPFunctionBlockMIDI1InfoNotMIDI1; + case ump::BlockMIDI1ProxyKind::unrestrictedBandwidth: return kMIDIUMPFunctionBlockMIDI1InfoUnrestrictedBandwidth; + case ump::BlockMIDI1ProxyKind::restrictedBandwidth: return kMIDIUMPFunctionBlockMIDI1InfoRestrictedBandwidth; + } + + return kMIDIUMPFunctionBlockMIDI1InfoNotMIDI1; + }); + + const auto uiHint = std::invoke ([&] + { + switch (block.getUiHint()) + { + case ump::BlockUiHint::unknown: return kMIDIUMPFunctionBlockUIHintUnknown; + case ump::BlockUiHint::receiver: return kMIDIUMPFunctionBlockUIHintReceiver; + case ump::BlockUiHint::sender: return kMIDIUMPFunctionBlockUIHintSender; + case ump::BlockUiHint::bidirectional: return kMIDIUMPFunctionBlockUIHintSenderReceiver; + } + + return kMIDIUMPFunctionBlockUIHintUnknown; + }); + + return NSUniquePtr + { + [[MIDIUMPMutableFunctionBlock alloc] initWithName: juceStringToNS (block.getName()) + direction: direction + firstGroup: block.getFirstGroup() + totalGroupsSpanned: block.getNumGroups() + maxSysEx8Streams: block.getMaxSysex8Streams() + MIDI1Info: midi1Kind + UIHint: uiHint + isEnabled: block.isEnabled()] + }; } - return {}; -} + class API_AVAILABLE (macos (15), ios (18)) VirtualEndpointImplNative : public ump::VirtualEndpoint::Impl::Native + { + public: + ~VirtualEndpointImplNative() override + { + auto* endpoint = src->getEndpoint(); + JUCE_CHECK_ERROR ([endpoint setEnabled: false error: nil]); + src->getKnownEndpoints()->removeVirtualEndpoint (getId()); + } -StringArray MidiOutput::getDevices() + ump::EndpointId getId() const override + { + auto* endpoint = src->getEndpoint(); + return convertToVirtualId (getIdForNativeEndpoint (endpoint)); + } + + bool setBlock (uint8_t index, const ump::Block& block) override + { + auto* endpoint = src->getEndpoint(); + NSUniquePtr blocks { endpoint.mutableFunctionBlocks.mutableCopy }; + [blocks.get() replaceObjectAtIndex: index withObject: makeBlock (block).get()]; + [endpoint setMutableFunctionBlocks: blocks.get()]; + return true; + } + + bool setName (const String& name) override + { + auto* endpoint = src->getEndpoint(); + NSError* error{}; + return [endpoint setName: juceStringToNS (name) error: &error]; + } + + static std::unique_ptr make (std::shared_ptr cachedEndpoints, + const String& deviceName, + const ump::DeviceInfo& deviceInfo, + const String& deviceIdentity, + ump::PacketProtocol protocol, + Span blocks, + ump::BlocksAreStatic areStatic) + { + const auto src = ConnectionToSrc::makeVirtualEndpoint (cachedEndpoints, + deviceName, + deviceInfo, + deviceIdentity, + protocol); + + if (src == nullptr || src->getEndpoint() == nullptr) + return {}; + + auto* endpoint = src->getEndpoint(); + + NSUniquePtr mutableBlocks { [[NSMutableArray alloc] initWithCapacity: blocks.size()] }; + + for (const auto& block : blocks) + if (const auto mutableBlock = makeBlock (block)) + [mutableBlocks.get() addObject: mutableBlock.get()]; + + NSError* error = nullptr; + // On macOS 15, passing a non-null NSError* will cause the function to fail. + // Hopefully we can get more detailed error information in future versions. + const auto successAssigningBlocks = [endpoint registerFunctionBlocks: mutableBlocks.get() + markAsStatic: (areStatic == ump::BlocksAreStatic::yes) + error: nil]; + + if (! successAssigningBlocks) + { + logNSError (error, __LINE__); + + // Something is wrong with the function block configuration. + jassertfalse; + return {}; + } + + const auto successSettingEnabled = [endpoint setEnabled: true error: &error]; + + if (! successSettingEnabled) + { + logNSError (error, __LINE__); + + // Something went wrong when enabling the endpoint + jassertfalse; + return {}; + } + + const auto id = getIdForNativeEndpoint (endpoint); + const auto dst = ConnectionToDst::fromVirtual (cachedEndpoints, ScopedEndpointRef { endpoint.MIDISource }); + return rawToUniquePtr (new VirtualEndpointImplNative (src, dst)); + } + + std::shared_ptr getSrc() const { return src; } + std::shared_ptr getDst() const { return dst; } + + private: + VirtualEndpointImplNative (std::shared_ptr s, + std::shared_ptr d) + : src (s), + dst (d) + { + jassert (src != nullptr && src->getEndpoint() != nullptr); + src->getKnownEndpoints()->addVirtualEndpoint (getId(), src->getEndpoint()); + } + + std::shared_ptr src; + std::shared_ptr dst; + }; + #endif + + class LegacyVirtualEndpointImplNative : public ump::LegacyVirtualInput::Impl::Native, + public ump::LegacyVirtualOutput::Impl::Native + { + public: + ~LegacyVirtualEndpointImplNative() override + { + if (src != nullptr) + src->getKnownEndpoints()->removeLegacyVirtualPort (getId()); + + if (dst != nullptr) + dst->getKnownEndpoints()->removeLegacyVirtualPort (getId()); + } + + ump::EndpointId getId() const override + { + return convertToVirtualId (id); + } + + static std::unique_ptr make (std::shared_ptr x, + ump::EndpointId i) + { + return rawToUniquePtr (new LegacyVirtualEndpointImplNative (std::move (x), std::move (i))); + } + + static std::unique_ptr make (std::shared_ptr x, + ump::EndpointId i) + { + return rawToUniquePtr (new LegacyVirtualEndpointImplNative (std::move (x), std::move (i))); + } + + private: + LegacyVirtualEndpointImplNative (std::shared_ptr x, ump::EndpointId i) + : src (std::move (x)), id (i) + { + const auto port = getMidiObjectInfo (src->getMIDIEndpoint()); + auto endpoint = ump::IOHelpers::makeProxyEndpoint (port, ump::BlockDirection::receiver); + src->getKnownEndpoints()->addLegacyVirtualPort (getId(), + EndpointInfo::withSrc (endpoint.endpoint, + src->getMIDIEndpoint(), + std::array{})); + } + + LegacyVirtualEndpointImplNative (std::shared_ptr x, ump::EndpointId i) + : dst (std::move (x)), id (i) + { + const auto port = getMidiObjectInfo (dst->getMIDIEndpoint()); + auto endpoint = ump::IOHelpers::makeProxyEndpoint (port, ump::BlockDirection::sender); + dst->getKnownEndpoints()->addLegacyVirtualPort (getId(), + EndpointInfo::withDst (endpoint.endpoint, + dst->getMIDIEndpoint(), + std::array{})); + } + + std::shared_ptr src; + std::shared_ptr dst; + ump::EndpointId id; + }; + + class SessionImplNative : public ump::Session::Impl::Native + { + public: + String getName() const override + { + return name; + } + + std::unique_ptr connectInput (ump::DisconnectionListener& l, + const ump::EndpointId& id, + ump::PacketProtocol p, + ump::Consumer& c) override + { + if (auto src = findOrMakeSrc (id)) + return rawToUniquePtr (new InputImplNative { src, id, l, p, c }); + + return {}; + } + + std::unique_ptr connectOutput (ump::DisconnectionListener& l, + const ump::EndpointId& id) override + { + if (auto dst = findOrMakeDst (id)) + return rawToUniquePtr (new OutputImplNative { dst, id, l }); + + return {}; + } + + std::unique_ptr createNativeVirtualEndpoint ([[maybe_unused]] const String& deviceName, + [[maybe_unused]] const ump::DeviceInfo& deviceInfo, + [[maybe_unused]] const String& deviceIdentity, + [[maybe_unused]] ump::PacketProtocol protocol, + [[maybe_unused]] Span blocks, + [[maybe_unused]] ump::BlocksAreStatic areStatic) override + { + #if JUCE_COREMIDI_UMP_ENDPOINT_CAN_BE_BUILT + if (@available (macos 15, ios 18, *)) + { + auto connection = VirtualEndpointImplNative::make (cachedEndpoints, + deviceName, + deviceInfo, + deviceIdentity, + protocol, + blocks, + areStatic); + + if (connection != nullptr) + { + connectionsSrc[connection->getId()] = connection->getSrc(); + connectionsDst[connection->getId()] = connection->getDst(); + } + + return connection; + } + #endif + + return {}; + } + + std::unique_ptr createLegacyVirtualInput (const String& deviceName) override + { + const auto [connection, id] = ConnectionToSrc::makeVirtual (cachedEndpoints, deviceName); + + if (connection == nullptr) + return {}; + + auto result = LegacyVirtualEndpointImplNative::make (connection, id); + connectionsSrc[result->getId()] = connection; + return result; + } + + std::unique_ptr createLegacyVirtualOutput (const String& deviceName) override + { + const auto [connection, id] = ConnectionToDst::makeVirtual (cachedEndpoints, deviceName); + + if (connection == nullptr) + return {}; + + auto result = LegacyVirtualEndpointImplNative::make (connection, id); + connectionsDst[result->getId()] = connection; + return result; + } + + static std::unique_ptr make (std::shared_ptr c, const String& n) + { + return rawToUniquePtr (new SessionImplNative (c, n)); + } + + private: + SessionImplNative (std::shared_ptr c, const String& n) + : cachedEndpoints (c), name (n) {} + + std::shared_ptr findOrMakeSrc (const ump::EndpointId& id) + { + auto& weak = connectionsSrc[id]; + + if (auto strong = weak.lock()) + return strong; + + const auto strong = ConnectionToSrc::make (cachedEndpoints, id); + weak = strong; + return strong; + } + + std::shared_ptr findOrMakeDst (const ump::EndpointId& id) + { + auto& weak = connectionsDst[id]; + + if (auto strong = weak.lock()) + return strong; + + const auto strong = ConnectionToDst::make (cachedEndpoints, id); + weak = strong; + return strong; + } + + std::map> connectionsSrc; + std::map> connectionsDst; + + std::shared_ptr cachedEndpoints; + String name; + }; + + class EndpointsImplNative : public ump::Endpoints::Impl::Native + { + public: + ump::Backend getBackend() const override + { + return ump::Backend::coremidi; + } + + bool isVirtualMidiBytestreamServiceActive() const override + { + return true; + } + + bool isVirtualMidiUmpServiceActive() const override + { + if (@available (macos 15, ios 18, *)) + return true; + + return false; + } + + void setVirtualMidiBytestreamServiceActive (bool) override {} + void setVirtualMidiUmpServiceActive (bool) override {} + + void getEndpoints (std::vector& x) const override + { + strong->getEndpoints (x); + } + + std::optional getEndpoint (const ump::EndpointId& x) const override + { + return strong->getEndpoint (x); + } + + std::optional getStaticDeviceInfo (const ump::EndpointId& x) const override + { + return strong->getStaticDeviceInfo (x); + } + + std::unique_ptr makeSession (const String& x) override + { + return SessionImplNative::make (strong, x); + } + + static std::unique_ptr make (ump::EndpointsListener& listener) + { + if (auto inner = SharedEndpointsImplNative::make (listener)) + return rawToUniquePtr (new EndpointsImplNative (inner)); + + return {}; + } + + private: + explicit EndpointsImplNative (std::shared_ptr s) + : strong (s) {} + + std::shared_ptr strong; + }; + + CoreMidiHelpers() = delete; + + #pragma pop_macro ("JUCE_CHECK_ERROR") +}; + +auto ump::Endpoints::Impl::Native::make (EndpointsListener& l) -> std::unique_ptr { - StringArray deviceNames; - - for (auto& d : getAvailableDevices()) - deviceNames.add (d.name); - - return deviceNames; + return CoreMidiHelpers::EndpointsImplNative::make (l); } -int MidiOutput::getDefaultDeviceIndex() -{ - return 0; -} - -std::unique_ptr MidiOutput::openDevice (int index) -{ - return openDevice (getAvailableDevices()[index].identifier); -} - -MidiOutput::~MidiOutput() -{ - stopBackgroundThread(); -} - -void MidiOutput::sendMessageNow (const MidiMessage& message) -{ - internal->send ({ 0, message.asSpan() }); -} - -MidiDeviceListConnection MidiDeviceListConnection::make (std::function cb) -{ - auto& broadcaster = MidiDeviceListConnectionBroadcaster::get(); - return { &broadcaster, broadcaster.add (std::move (cb)) }; -} - -#undef CHECK_ERROR - } // namespace juce diff --git a/modules/juce_audio_devices/native/juce_Midi_android.cpp b/modules/juce_audio_devices/native/juce_Midi_android.cpp index 66767100d0..850fac6a9a 100644 --- a/modules/juce_audio_devices/native/juce_Midi_android.cpp +++ b/modules/juce_audio_devices/native/juce_Midi_android.cpp @@ -35,1131 +35,3603 @@ namespace juce { -//============================================================================== -// This byte-code is generated from native/java/com/rmsl/juce/JuceMidiSupport.java with min sdk version 23 -// See juce_core/native/java/README.txt on how to generate this byte-code. -constexpr unsigned char javaMidiByteCode[] -{ - 0x1f, 0x8b, 0x08, 0x08, 0xa3, 0xf2, 0xc6, 0x63, 0x00, 0x03, 0x63, 0x6c, - 0x61, 0x73, 0x73, 0x65, 0x73, 0x2e, 0x64, 0x65, 0x78, 0x00, 0x95, 0x7c, - 0x09, 0x7c, 0xdb, 0x47, 0x95, 0xff, 0x9b, 0x9f, 0x7e, 0xb2, 0x6c, 0xf9, - 0x92, 0x64, 0x27, 0x76, 0x1c, 0xc7, 0x96, 0x8f, 0xc4, 0xf7, 0x15, 0xc7, - 0xa9, 0x13, 0x3b, 0xa9, 0xef, 0xc4, 0xb1, 0x73, 0xd4, 0x56, 0xd2, 0x36, - 0x2e, 0xdb, 0x2a, 0xb6, 0x12, 0xab, 0xb1, 0x25, 0x45, 0x92, 0xd3, 0x04, - 0x4a, 0x9b, 0x1e, 0x6c, 0x12, 0x08, 0x10, 0x4a, 0x9b, 0x96, 0x12, 0xd8, - 0xd2, 0x1b, 0x28, 0xd0, 0x76, 0x39, 0xca, 0x6e, 0x77, 0x29, 0x6c, 0x97, - 0x2d, 0xf7, 0xd5, 0x85, 0x70, 0x2d, 0xa5, 0x84, 0xa5, 0x94, 0x02, 0x61, - 0xf7, 0xbf, 0x6c, 0xe8, 0xbf, 0x90, 0xfd, 0xbe, 0x99, 0xf9, 0x49, 0x3f, - 0x3b, 0x6e, 0x53, 0x92, 0xcf, 0x57, 0x6f, 0xe6, 0xcd, 0x9b, 0x99, 0x37, - 0x33, 0x6f, 0xde, 0xbc, 0xf9, 0xfd, 0x24, 0x4f, 0x85, 0x0e, 0xb9, 0x5b, - 0xdb, 0x3b, 0xe8, 0xae, 0x83, 0xd9, 0xd5, 0xe5, 0x6f, 0x37, 0xff, 0xb8, - 0x6a, 0xcf, 0xe9, 0xa7, 0xdf, 0xb2, 0xa7, 0xbd, 0xf5, 0xf0, 0x6b, 0xab, - 0x1b, 0x4f, 0x9c, 0xba, 0xff, 0x6c, 0xe9, 0x30, 0x51, 0x8c, 0x88, 0x0e, - 0xed, 0x5a, 0xe3, 0x23, 0xfd, 0x6f, 0x74, 0x33, 0xd1, 0x32, 0xa1, 0xf8, - 0x9b, 0x80, 0xe7, 0x4d, 0xa2, 0xab, 0x40, 0xcf, 0x39, 0x89, 0x2a, 0x40, - 0x3d, 0x6e, 0xa2, 0xbf, 0x07, 0x2d, 0xce, 0x26, 0xca, 0x02, 0x7d, 0xdc, - 0x83, 0x3a, 0x97, 0x11, 0x7d, 0xdb, 0x4b, 0x74, 0xa6, 0x89, 0xe8, 0x47, - 0xc0, 0x4f, 0x81, 0x17, 0x81, 0x5f, 0x02, 0xd9, 0xcd, 0x44, 0x4b, 0x80, - 0x15, 0x40, 0x05, 0x50, 0x03, 0x34, 0x01, 0xfd, 0xc0, 0x6d, 0xc0, 0x73, - 0xc0, 0x59, 0xe0, 0x57, 0xc0, 0x6f, 0x80, 0x73, 0x80, 0xb7, 0x85, 0xa8, - 0x13, 0xe8, 0x02, 0x7a, 0x81, 0x00, 0x30, 0x0d, 0x1c, 0x04, 0x4e, 0x00, - 0xef, 0x05, 0xde, 0x0f, 0xdc, 0x03, 0x7c, 0x08, 0x78, 0x00, 0x78, 0x04, - 0x78, 0x0c, 0x78, 0x12, 0x38, 0x03, 0xfc, 0x19, 0xf0, 0xb7, 0x12, 0x8d, - 0x00, 0x37, 0x02, 0x1f, 0x05, 0xbe, 0x05, 0xbc, 0x0a, 0x94, 0xb7, 0x11, - 0x6d, 0x01, 0x66, 0x81, 0x93, 0xc0, 0x3f, 0x01, 0x2f, 0x02, 0x7f, 0x06, - 0xca, 0x56, 0x63, 0x3c, 0xc0, 0x01, 0xe0, 0x1e, 0xe0, 0x69, 0xe0, 0xb7, - 0xc0, 0xaa, 0x76, 0xa2, 0xcd, 0xc0, 0x2c, 0x70, 0x12, 0x78, 0x0c, 0xf8, - 0x22, 0xf0, 0x03, 0xe0, 0x1c, 0x60, 0xae, 0xc1, 0xbc, 0x01, 0xcd, 0xc0, - 0x14, 0x70, 0x37, 0xf0, 0x35, 0xe0, 0x4f, 0xc0, 0xa6, 0x0e, 0xa2, 0x77, - 0x03, 0xcf, 0x00, 0xbf, 0x03, 0xf2, 0xd7, 0x12, 0xb5, 0x03, 0xe3, 0xc0, - 0x41, 0xe0, 0x2e, 0xe0, 0x09, 0xe0, 0x39, 0xe0, 0xc7, 0xc0, 0x39, 0xc0, - 0xc4, 0x9c, 0x16, 0x03, 0x2d, 0xc0, 0x36, 0x60, 0x16, 0xb8, 0x1d, 0xf8, - 0x20, 0xf0, 0x29, 0xe0, 0x59, 0xe0, 0x87, 0xc0, 0xef, 0x00, 0x47, 0x27, - 0xfa, 0x06, 0x1a, 0x80, 0x0d, 0xc0, 0x36, 0x20, 0x08, 0x1c, 0x02, 0xde, - 0x05, 0x7c, 0x13, 0x78, 0x11, 0x38, 0x07, 0xfc, 0x09, 0xf0, 0xaf, 0x23, - 0xaa, 0x02, 0x6a, 0x80, 0x46, 0x60, 0x35, 0xb0, 0x1e, 0xd8, 0x08, 0xf4, - 0x03, 0xc3, 0xc0, 0x0e, 0x60, 0x27, 0xb0, 0x1b, 0xb8, 0x0e, 0xd8, 0x0b, - 0xcc, 0x02, 0x49, 0xe0, 0x10, 0xf0, 0x36, 0xe0, 0x66, 0xe0, 0x36, 0xe0, - 0x28, 0xf0, 0x71, 0xe0, 0x47, 0x00, 0xad, 0xc7, 0x7a, 0x03, 0xf5, 0xc0, - 0x5a, 0xa0, 0x1f, 0xd8, 0x0c, 0xec, 0x04, 0x82, 0x40, 0x18, 0x48, 0x02, - 0x27, 0x81, 0xfb, 0x81, 0xa7, 0x80, 0xaf, 0x02, 0x3f, 0x01, 0xce, 0x02, - 0xbf, 0x01, 0xfe, 0x1b, 0x78, 0x15, 0x10, 0x5d, 0x44, 0x6e, 0xa0, 0x08, - 0xa8, 0x00, 0x9a, 0x81, 0x36, 0xa0, 0x0b, 0xd8, 0x0c, 0x04, 0x80, 0x6b, - 0x80, 0x13, 0xc0, 0x29, 0xe0, 0xf3, 0xc0, 0x8f, 0x80, 0x9f, 0x03, 0xbf, - 0x05, 0xfe, 0x0b, 0xf8, 0x0b, 0xb7, 0xd1, 0x8d, 0x36, 0x80, 0x76, 0x60, - 0x07, 0xb0, 0x0f, 0xb8, 0x11, 0x38, 0x05, 0x7c, 0x02, 0xf8, 0x1c, 0xf0, - 0x55, 0xe0, 0x3f, 0x81, 0x3f, 0x00, 0x62, 0x03, 0x51, 0x2e, 0xb0, 0x14, - 0xe8, 0x05, 0x36, 0x03, 0xdb, 0x81, 0xab, 0x80, 0x1b, 0x81, 0x87, 0x80, - 0x6f, 0x01, 0x2f, 0x03, 0xc6, 0x46, 0xd8, 0x38, 0x50, 0x0c, 0xd4, 0x00, - 0xeb, 0x80, 0x7e, 0x60, 0x0c, 0x98, 0x04, 0xe6, 0x80, 0x9b, 0x80, 0x3b, - 0x81, 0x07, 0x80, 0xc7, 0x80, 0xcf, 0x02, 0xff, 0x02, 0x7c, 0x1d, 0xf8, - 0x77, 0xe0, 0x45, 0xe0, 0x15, 0xe0, 0x8f, 0xc0, 0x5f, 0x00, 0xd7, 0xe5, - 0xe8, 0x1f, 0x58, 0x06, 0x34, 0x02, 0xeb, 0x80, 0xcb, 0x81, 0x41, 0xe0, - 0x6a, 0x20, 0x0e, 0x1c, 0x03, 0xfe, 0x0e, 0x78, 0x0a, 0xf8, 0x2a, 0xf0, - 0x53, 0xe0, 0x15, 0xe0, 0x35, 0xa0, 0xa0, 0x87, 0xa8, 0x1a, 0xe8, 0x02, - 0x06, 0x80, 0x31, 0x60, 0x0a, 0xb8, 0x09, 0x38, 0x01, 0xdc, 0x0b, 0xdc, - 0x0f, 0x3c, 0x05, 0x7c, 0x03, 0xf8, 0x21, 0xf0, 0x12, 0x20, 0x7a, 0x89, - 0x96, 0x03, 0x2d, 0xc0, 0xe5, 0xc0, 0x16, 0x60, 0x27, 0x70, 0x0d, 0x70, - 0x00, 0x78, 0x37, 0xf0, 0x30, 0xf0, 0x04, 0xf0, 0x8f, 0xc0, 0x33, 0xc0, - 0x19, 0xe0, 0x05, 0xe0, 0x15, 0xc0, 0xd5, 0x47, 0x94, 0x03, 0x2c, 0x01, - 0xca, 0x81, 0x1a, 0xa0, 0x09, 0x18, 0x06, 0xc6, 0x81, 0x29, 0xe0, 0x00, - 0x70, 0x0b, 0x70, 0x14, 0x78, 0x0f, 0xf0, 0x01, 0xe0, 0x51, 0xe0, 0x93, - 0xc0, 0x53, 0xc0, 0x33, 0xc0, 0x57, 0x80, 0xef, 0x01, 0x2f, 0x03, 0xce, - 0x7e, 0xb4, 0x05, 0xac, 0x02, 0x5a, 0x80, 0x3c, 0x98, 0x58, 0x01, 0x29, - 0x5f, 0x54, 0x09, 0x54, 0x01, 0xd5, 0xc0, 0x4a, 0x60, 0x15, 0x50, 0x03, - 0xd4, 0x02, 0x75, 0x40, 0x3d, 0xd0, 0x00, 0x34, 0x02, 0x70, 0x45, 0x04, - 0xd7, 0x42, 0x70, 0x0b, 0x04, 0x17, 0x40, 0xd8, 0xea, 0x84, 0x6d, 0x4d, - 0xd8, 0xba, 0x84, 0x2d, 0x4a, 0xd8, 0x96, 0x84, 0x6d, 0x47, 0xd8, 0x4e, - 0x84, 0x2d, 0x42, 0xda, 0x94, 0x09, 0xe6, 0x43, 0x30, 0x09, 0xc2, 0x52, - 0x13, 0x96, 0x86, 0x30, 0xbd, 0x84, 0xa1, 0x12, 0x54, 0x23, 0xa8, 0x43, - 0x03, 0xc0, 0x20, 0x30, 0xa4, 0xfd, 0x25, 0xdc, 0x27, 0xc1, 0xad, 0xd2, - 0x16, 0x60, 0x84, 0xfd, 0x29, 0xb0, 0x15, 0xd8, 0x06, 0x6c, 0x07, 0x76, - 0x00, 0x57, 0x00, 0x63, 0xc0, 0x38, 0x10, 0x00, 0x76, 0x02, 0x57, 0x02, - 0x57, 0x03, 0xbb, 0x81, 0x6b, 0x80, 0xbf, 0x01, 0xae, 0x05, 0x82, 0xc0, - 0x1e, 0x60, 0x12, 0xb8, 0x1e, 0x78, 0x2b, 0xf0, 0x76, 0xe0, 0x26, 0xe0, - 0x66, 0xe0, 0x08, 0x70, 0x0b, 0x70, 0x2b, 0xa9, 0xb9, 0xb1, 0xfe, 0xe5, - 0x6b, 0xba, 0x03, 0x83, 0xf7, 0xe8, 0xf4, 0x55, 0x48, 0x2f, 0x03, 0x35, - 0x74, 0xbe, 0x4c, 0xa7, 0xaf, 0xd3, 0x7c, 0x87, 0x8d, 0xef, 0xd0, 0x75, - 0x99, 0x6f, 0x6a, 0xbe, 0x5f, 0xa7, 0xa7, 0x35, 0x3f, 0xcb, 0x26, 0x8f, - 0xe3, 0x80, 0x92, 0x9a, 0x9f, 0xa3, 0xf9, 0x25, 0x5a, 0xa7, 0x1b, 0x35, - 0xdf, 0xd2, 0x89, 0xd3, 0x1e, 0x5b, 0xba, 0xc0, 0x26, 0xbf, 0x44, 0xcb, - 0x97, 0xe8, 0x32, 0xab, 0x6e, 0xa9, 0xad, 0xaf, 0x32, 0xad, 0x5b, 0x89, - 0xd6, 0x89, 0x65, 0xca, 0x74, 0x7a, 0x87, 0x4e, 0x57, 0xe8, 0x71, 0xb1, - 0x4c, 0xb5, 0x96, 0x29, 0xd5, 0xe9, 0xdb, 0x91, 0x5e, 0xa1, 0xd3, 0x27, - 0xb4, 0xfc, 0x2a, 0x5b, 0xdd, 0x1a, 0x5d, 0x77, 0x39, 0x29, 0x5b, 0xba, - 0x47, 0xeb, 0xd0, 0x6a, 0xd3, 0xb3, 0xcd, 0xa6, 0xdb, 0x6a, 0x9b, 0x6e, - 0x9c, 0xbe, 0x0f, 0xfc, 0x72, 0x9d, 0x7e, 0xb4, 0x31, 0xcd, 0xb7, 0xe6, - 0xb3, 0xdd, 0xd6, 0x4e, 0xbb, 0x4d, 0x7f, 0x4e, 0x3f, 0x6e, 0x4b, 0x5b, - 0x63, 0x5c, 0x6b, 0xeb, 0x6b, 0xbd, 0xad, 0x2f, 0xb6, 0xcd, 0xa7, 0x34, - 0xbf, 0x5b, 0xf3, 0xd9, 0x46, 0x2e, 0xd7, 0xe9, 0xfd, 0x3a, 0xcd, 0x75, - 0x67, 0x74, 0xfa, 0x19, 0xa4, 0x67, 0x75, 0xfa, 0x39, 0xa4, 0x23, 0x3a, - 0xfd, 0x3c, 0xd2, 0x51, 0x9d, 0x3e, 0x8b, 0xf4, 0x41, 0x9d, 0x7e, 0xa5, - 0x51, 0xc5, 0x02, 0x9c, 0x3e, 0x8f, 0xf4, 0x01, 0xab, 0x7d, 0x6c, 0xa8, - 0xa4, 0x4e, 0xe7, 0x20, 0x3d, 0xa7, 0xd3, 0x85, 0xb6, 0xb4, 0xbf, 0x29, - 0xdd, 0x66, 0xa3, 0x2d, 0x7d, 0x9d, 0xad, 0xaf, 0x35, 0x36, 0x7e, 0x77, - 0x53, 0xba, 0xdf, 0x01, 0x1b, 0x7f, 0x87, 0x2d, 0x7d, 0x95, 0xad, 0xdf, - 0xeb, 0x6c, 0xfc, 0x69, 0x5b, 0xdd, 0x18, 0xd2, 0x37, 0x58, 0x63, 0xb7, - 0xc9, 0x1f, 0x43, 0xfa, 0x90, 0x4e, 0x9f, 0xb4, 0xd5, 0x7d, 0xdc, 0xa6, - 0x0f, 0xaf, 0x9d, 0x25, 0xff, 0xa8, 0x8d, 0xbf, 0xc3, 0x96, 0x3e, 0x6d, - 0xeb, 0xeb, 0x41, 0xa4, 0x13, 0x56, 0x3b, 0x48, 0x1f, 0xd6, 0xe9, 0xa7, - 0x9a, 0xd2, 0x73, 0xf5, 0x0c, 0xd2, 0x71, 0x9d, 0xfe, 0x7a, 0x93, 0xda, - 0xc3, 0x3d, 0x7a, 0x8d, 0xde, 0xa6, 0xd3, 0xbc, 0x46, 0x37, 0xea, 0xf4, - 0x59, 0x5b, 0xfa, 0x3e, 0x5b, 0xda, 0xb2, 0x9f, 0x7e, 0x5d, 0x97, 0xd3, - 0x03, 0x36, 0x7b, 0x18, 0xb4, 0xd9, 0xc3, 0x90, 0xe6, 0x97, 0xe8, 0xf4, - 0x8d, 0xda, 0x9e, 0xa7, 0x79, 0x5d, 0x60, 0x8d, 0x1f, 0xd1, 0xd4, 0x21, - 0x78, 0xaf, 0x78, 0xe8, 0x28, 0x31, 0x6d, 0xa7, 0x77, 0x4a, 0xba, 0x8e, - 0x4e, 0x48, 0x9a, 0x45, 0x42, 0xb0, 0x9f, 0x5d, 0x46, 0x7f, 0x4b, 0x4c, - 0x7b, 0xe8, 0xab, 0x92, 0x0a, 0xfa, 0x96, 0xa4, 0x35, 0xf4, 0x3f, 0x92, - 0xd6, 0xd2, 0xab, 0xc4, 0xbe, 0x78, 0x89, 0x94, 0xab, 0xd2, 0xfc, 0x2a, - 0xcd, 0x5f, 0xa9, 0xf3, 0x4c, 0x3d, 0x82, 0xf7, 0x9a, 0x49, 0xef, 0x25, - 0xa6, 0x5e, 0xfa, 0x9e, 0xa4, 0xaa, 0x7c, 0x95, 0x2e, 0xaf, 0xd1, 0xfa, - 0xd4, 0xc0, 0x13, 0xbf, 0x47, 0xd2, 0x01, 0xba, 0x57, 0xd2, 0x62, 0xfa, - 0x8e, 0xa4, 0x6b, 0xe9, 0xdf, 0x75, 0xf9, 0x7f, 0x6b, 0xfa, 0xff, 0x48, - 0xed, 0xd5, 0x0f, 0x48, 0xda, 0x43, 0x5f, 0xd7, 0xf9, 0x3f, 0x11, 0x9f, - 0x05, 0x15, 0xf4, 0x2e, 0x49, 0x6b, 0xe8, 0x79, 0x62, 0x9f, 0x97, 0x45, - 0x8f, 0x49, 0xea, 0xa0, 0x4f, 0x4a, 0x9a, 0x41, 0xff, 0x42, 0xec, 0xf3, - 0x32, 0xe9, 0x2e, 0x49, 0xab, 0xe8, 0x21, 0x4d, 0xff, 0x89, 0xd8, 0xe7, - 0x35, 0xd0, 0xfb, 0x35, 0xfd, 0xa0, 0xa4, 0x4e, 0x7a, 0x5c, 0xd2, 0xed, - 0x74, 0x01, 0xd4, 0x09, 0x7e, 0x06, 0x68, 0x26, 0x6a, 0x3c, 0x48, 0xec, - 0x17, 0x87, 0x29, 0x47, 0x30, 0xbd, 0x8c, 0xf2, 0x41, 0xdd, 0xba, 0x3c, - 0x3b, 0x45, 0xb3, 0xe9, 0xa4, 0xa4, 0x6e, 0xca, 0x42, 0x79, 0xae, 0x6e, - 0x2f, 0x4f, 0x97, 0xe7, 0x81, 0x73, 0x52, 0xd2, 0x1c, 0x72, 0x09, 0x45, - 0x33, 0x05, 0xfb, 0xcc, 0x3c, 0xfa, 0x28, 0x31, 0xad, 0xa4, 0x67, 0x41, - 0xbd, 0x5a, 0x2f, 0x2f, 0x3c, 0xeb, 0xe7, 0x24, 0xf5, 0xd0, 0xef, 0x24, - 0xf5, 0xd2, 0x7f, 0x81, 0xfa, 0xb4, 0xfe, 0x1c, 0xdc, 0x7f, 0x41, 0xd3, - 0x7f, 0x25, 0xe5, 0x6f, 0x3f, 0x2b, 0xe9, 0x38, 0x7d, 0x51, 0x52, 0x1f, - 0x7d, 0x49, 0xf3, 0xb9, 0x7c, 0x89, 0x6e, 0x77, 0x09, 0x4e, 0x2f, 0x13, - 0xfd, 0x2e, 0xd5, 0x7a, 0x15, 0xe3, 0xb4, 0x7a, 0x52, 0xd2, 0x36, 0x7a, - 0x45, 0xd2, 0x2e, 0xfa, 0xad, 0xa4, 0x1b, 0xe9, 0x35, 0x49, 0x37, 0xd0, - 0x12, 0xc1, 0xf6, 0xa7, 0xea, 0x2f, 0x83, 0xc5, 0xdf, 0xa9, 0xe9, 0x07, - 0xa5, 0x2d, 0xaa, 0x76, 0x4a, 0xa1, 0xff, 0x03, 0xd2, 0x66, 0x0b, 0xe8, - 0x7e, 0x62, 0x5f, 0x69, 0xd0, 0x3d, 0xd2, 0x1e, 0x87, 0x64, 0x79, 0x05, - 0xd6, 0x53, 0x51, 0x41, 0x1f, 0x97, 0x74, 0x15, 0x7d, 0x5a, 0xd2, 0x5d, - 0xf4, 0x8f, 0x92, 0x6e, 0xa6, 0x33, 0x92, 0x36, 0xd2, 0x4b, 0x92, 0x36, - 0xd1, 0xaf, 0x25, 0x1d, 0xa3, 0xf3, 0x92, 0x8e, 0x50, 0xae, 0xb4, 0xeb, - 0x4d, 0x54, 0x28, 0xed, 0xb7, 0x57, 0xb6, 0x57, 0xa9, 0xf5, 0x62, 0xfa, - 0x61, 0x49, 0xd5, 0xfc, 0x54, 0x22, 0x2a, 0xf8, 0x37, 0x49, 0xb7, 0xd1, - 0x37, 0x74, 0xf9, 0x59, 0x49, 0xb7, 0xd2, 0xcb, 0x92, 0x8e, 0x52, 0x86, - 0x50, 0xfc, 0x6c, 0x4d, 0xf3, 0x04, 0xdb, 0x77, 0x8f, 0x6c, 0xb7, 0x4a, - 0xb7, 0x5b, 0xa5, 0xdb, 0xad, 0xd2, 0xed, 0x56, 0xe9, 0xf6, 0xaa, 0x74, - 0xfd, 0x2a, 0x5d, 0xbf, 0x4a, 0xd7, 0xaf, 0xd6, 0xf5, 0xaa, 0xb5, 0x7c, - 0xb5, 0x96, 0xaf, 0xd6, 0xf2, 0xd5, 0x5a, 0xbe, 0x5a, 0xcb, 0xaf, 0x44, - 0xd4, 0x91, 0x21, 0xf7, 0xd1, 0x1a, 0xfa, 0xa1, 0xa4, 0x1d, 0xf4, 0x23, - 0x4d, 0x7f, 0x2c, 0x69, 0x3b, 0xfd, 0x44, 0xd2, 0xb5, 0xf4, 0x53, 0x4d, - 0xff, 0x43, 0xf3, 0x7f, 0xa9, 0xe9, 0x7f, 0x4a, 0xba, 0x9a, 0x7e, 0xa5, - 0xe9, 0x6f, 0xe4, 0xbe, 0xeb, 0x97, 0xed, 0xae, 0x42, 0xff, 0xef, 0x93, - 0xb4, 0x8a, 0x3e, 0x26, 0xa9, 0x4b, 0xde, 0xf5, 0xf8, 0x6c, 0xfc, 0x8c, - 0xa4, 0x88, 0xa6, 0x84, 0xda, 0x6f, 0x19, 0x72, 0xdf, 0xa9, 0xf1, 0xd6, - 0xc0, 0x52, 0xfe, 0x4e, 0xd2, 0x12, 0xba, 0x4f, 0xd2, 0x1a, 0x7a, 0x58, - 0x52, 0xb5, 0x7e, 0x35, 0xb0, 0x9b, 0x27, 0x24, 0xbd, 0x92, 0x9e, 0x92, - 0x74, 0x17, 0x7d, 0x5e, 0xd3, 0x7f, 0x90, 0xb4, 0x90, 0x9e, 0x96, 0x74, - 0x25, 0xfd, 0xb3, 0xa4, 0xa5, 0xf4, 0x8c, 0xa4, 0xeb, 0xe9, 0x67, 0x92, - 0xae, 0xa3, 0x17, 0x34, 0xfd, 0xb9, 0xe6, 0xbf, 0x28, 0x69, 0x37, 0xfd, - 0x42, 0xfb, 0x85, 0xdf, 0x4b, 0x5a, 0x44, 0xe7, 0x24, 0x5d, 0x46, 0x7f, - 0x90, 0x74, 0x07, 0xfd, 0x51, 0xd2, 0x56, 0xfa, 0x5f, 0xed, 0x47, 0xfe, - 0x22, 0xe9, 0x26, 0x2a, 0x10, 0xec, 0x1f, 0x9a, 0xe5, 0x38, 0x6a, 0x11, - 0x91, 0x9d, 0xd6, 0xfe, 0xe2, 0x2b, 0xd2, 0x4f, 0x34, 0x60, 0x47, 0x2a, - 0x9a, 0xa1, 0xe9, 0x6d, 0x92, 0x2e, 0xa5, 0x47, 0x24, 0x5d, 0x4e, 0x8f, - 0x4a, 0x6a, 0xd2, 0xa7, 0x74, 0xf9, 0x97, 0x89, 0x63, 0x82, 0x46, 0x29, - 0xdf, 0xa6, 0xdb, 0x69, 0xc3, 0xca, 0xf9, 0x04, 0xd3, 0x32, 0x2a, 0x12, - 0x1c, 0x03, 0xa8, 0xf6, 0x56, 0xeb, 0x79, 0x5b, 0x8d, 0x28, 0xe4, 0x13, - 0xc4, 0x67, 0xbd, 0xea, 0xbf, 0x1d, 0xf3, 0xff, 0x35, 0xe2, 0x58, 0x74, - 0x50, 0xca, 0x75, 0x60, 0x67, 0xf0, 0x3e, 0x59, 0xab, 0xeb, 0xad, 0x85, - 0xdc, 0x3b, 0x74, 0xfe, 0x0e, 0x9d, 0x3f, 0x25, 0x69, 0x2d, 0x7d, 0x53, - 0xe7, 0xff, 0x4c, 0x2a, 0x5e, 0x70, 0x0b, 0xa6, 0x3b, 0xc9, 0x2b, 0x38, - 0xa6, 0xad, 0xa3, 0x63, 0xc4, 0x71, 0xad, 0x6a, 0xa7, 0x53, 0xd7, 0xef, - 0x84, 0xfc, 0xdd, 0x92, 0xfa, 0x65, 0x3f, 0x9d, 0x88, 0x98, 0xbf, 0x2d, - 0x69, 0x05, 0xfd, 0x7f, 0xcd, 0xe7, 0xf6, 0xd6, 0xe9, 0x7a, 0xeb, 0x74, - 0xff, 0xeb, 0x74, 0x3f, 0xeb, 0x74, 0x3f, 0xeb, 0x74, 0x3f, 0xeb, 0xa1, - 0xff, 0x73, 0xc4, 0xb4, 0x9c, 0x7e, 0x40, 0x1c, 0x9f, 0x28, 0xbd, 0xba, - 0x35, 0xdd, 0xa0, 0xdb, 0xd9, 0x80, 0xe8, 0xd8, 0x10, 0x1c, 0x4f, 0xab, - 0xfc, 0x46, 0x6d, 0x77, 0x1c, 0xb3, 0x09, 0x6e, 0x93, 0xd4, 0xbf, 0x62, - 0xe0, 0x1a, 0x04, 0xd5, 0x5f, 0xc2, 0x21, 0x77, 0x6a, 0x48, 0xc5, 0x6b, - 0x22, 0x23, 0x1d, 0x6f, 0x71, 0xf9, 0x8d, 0x28, 0x3f, 0xaf, 0x0f, 0xc1, - 0x95, 0x9a, 0xef, 0xb4, 0x95, 0x9f, 0x40, 0x79, 0xe1, 0xa0, 0xca, 0xaf, - 0xd2, 0xfc, 0x8d, 0xb6, 0xf2, 0xd3, 0x28, 0x1f, 0xd1, 0xe5, 0x35, 0xba, - 0xff, 0xa5, 0x40, 0x4f, 0xa3, 0x2a, 0x7f, 0x0c, 0xe5, 0x71, 0x5d, 0x5e, - 0xab, 0xeb, 0xd9, 0xfb, 0x7f, 0x0d, 0xe5, 0x2f, 0xeb, 0xf2, 0x3a, 0x5d, - 0xdf, 0x5e, 0x9e, 0x83, 0x0b, 0x81, 0xa9, 0x0f, 0xe3, 0x4a, 0x5d, 0xce, - 0x67, 0xf2, 0x66, 0xdd, 0x7e, 0x29, 0xca, 0x9b, 0x75, 0x79, 0x95, 0xad, - 0xbe, 0x55, 0xbe, 0x06, 0xe5, 0xd7, 0xeb, 0x72, 0x07, 0xfc, 0x24, 0xc7, - 0xeb, 0xf7, 0xd5, 0xa9, 0x58, 0x37, 0xe0, 0x71, 0xd0, 0x01, 0xcf, 0x71, - 0xee, 0x85, 0x22, 0x7e, 0x03, 0xbc, 0x1c, 0x63, 0x9d, 0x91, 0x4d, 0x25, - 0x46, 0x21, 0xfa, 0xf8, 0x10, 0x1d, 0xf0, 0xaf, 0x86, 0xbc, 0xcf, 0xc8, - 0x37, 0x94, 0xe4, 0x31, 0x2d, 0xf9, 0x76, 0x48, 0xba, 0xc1, 0xb5, 0xda, - 0xfb, 0x58, 0x9d, 0x8a, 0xdf, 0xe7, 0x4b, 0xcd, 0x6a, 0xa9, 0x74, 0xbf, - 0x9f, 0xaa, 0x53, 0x31, 0xfc, 0x62, 0xfd, 0x46, 0x3c, 0x4e, 0xc8, 0xe4, - 0x18, 0x5e, 0x29, 0x2f, 0xa4, 0xfc, 0x67, 0x2c, 0x79, 0xbf, 0x93, 0x22, - 0x9e, 0x8f, 0x4a, 0x6b, 0xa9, 0xf2, 0x0e, 0xa1, 0xce, 0xc7, 0x70, 0x52, - 0xe5, 0xc0, 0x4a, 0x86, 0x70, 0x52, 0xa9, 0xf6, 0x79, 0xdc, 0x9f, 0xaf, - 0xe3, 0xb3, 0x10, 0xf1, 0x9b, 0x27, 0x0f, 0xf9, 0x4a, 0xd8, 0x55, 0xcc, - 0xc3, 0x7e, 0x64, 0x02, 0x6d, 0x4f, 0xf8, 0x1c, 0xf2, 0x2e, 0x61, 0xca, - 0x53, 0x9b, 0xe8, 0xcb, 0x75, 0x6a, 0x9d, 0xe2, 0x9e, 0x4f, 0x20, 0x9f, - 0xe3, 0x88, 0x7b, 0x3e, 0x0e, 0xea, 0x86, 0xcd, 0xe6, 0x82, 0xf7, 0x18, - 0xf3, 0xd0, 0x7e, 0x2e, 0xf9, 0xbc, 0x91, 0xd6, 0xf5, 0x38, 0xdf, 0x6a, - 0xcf, 0xe5, 0xe9, 0x51, 0xa8, 0x7f, 0xdc, 0x5f, 0x9e, 0xb4, 0x15, 0x87, - 0xe4, 0x7e, 0xab, 0x4e, 0xed, 0x1b, 0x9f, 0x67, 0xb5, 0xc3, 0x49, 0x3e, - 0x7f, 0xbb, 0xc3, 0x0b, 0x9d, 0x7d, 0xe8, 0x2f, 0x07, 0x6d, 0x66, 0x53, - 0xa0, 0x82, 0xc7, 0x60, 0xea, 0x31, 0x3f, 0x82, 0xfa, 0xbe, 0x9e, 0x76, - 0x47, 0x39, 0x95, 0x38, 0x78, 0xae, 0xc3, 0x72, 0xae, 0x1d, 0x56, 0x0d, - 0x47, 0xa7, 0xc3, 0x47, 0x81, 0x6a, 0x55, 0xc3, 0x21, 0x6b, 0x3c, 0x0a, - 0xbe, 0xce, 0x39, 0x22, 0xfe, 0x0d, 0x58, 0xe3, 0x3c, 0xc4, 0x0b, 0x86, - 0xbc, 0x4f, 0xfd, 0xb4, 0x4e, 0xdd, 0xff, 0x02, 0xd7, 0xa5, 0xe7, 0xb5, - 0x44, 0x14, 0x62, 0xfc, 0x99, 0x54, 0xe2, 0xca, 0x91, 0x6d, 0x1f, 0x80, - 0x7c, 0x44, 0x5e, 0x9a, 0x72, 0x34, 0x3f, 0x3b, 0xc5, 0xef, 0x74, 0x5d, - 0x46, 0x95, 0xc8, 0xc7, 0x3c, 0xf9, 0xf0, 0xd8, 0x25, 0xc2, 0x44, 0x2b, - 0x6d, 0xd0, 0x30, 0x47, 0x44, 0xfc, 0x5e, 0xf8, 0xc4, 0x4a, 0x91, 0x87, - 0xb2, 0x02, 0xd6, 0xd9, 0x17, 0xf1, 0x2f, 0xc1, 0x3e, 0xcb, 0x71, 0xf8, - 0xcc, 0x88, 0x7f, 0x29, 0xfc, 0x3e, 0x52, 0x2b, 0x39, 0x35, 0x4e, 0x55, - 0x95, 0x7d, 0xe8, 0xc1, 0x83, 0x16, 0x72, 0x5c, 0x5b, 0x5d, 0x86, 0x79, - 0xc0, 0xf3, 0x61, 0x5e, 0x51, 0x57, 0xc4, 0x53, 0xa8, 0xda, 0xea, 0xc9, - 0xa1, 0x58, 0xb0, 0x16, 0xf5, 0x72, 0xe9, 0x3a, 0x39, 0x77, 0x96, 0x5d, - 0xfc, 0xbe, 0x4e, 0xed, 0xd9, 0xf9, 0xf6, 0x73, 0x04, 0x76, 0x91, 0xa7, - 0x57, 0x56, 0xfd, 0x33, 0xe4, 0xfc, 0xe6, 0xa7, 0xec, 0xe3, 0x7f, 0xea, - 0xd4, 0x1d, 0x32, 0xe0, 0x77, 0x63, 0x7e, 0xb3, 0x51, 0x27, 0x21, 0xed, - 0x82, 0x6d, 0x22, 0x03, 0xff, 0xb9, 0xe6, 0xab, 0xda, 0x86, 0x62, 0x1e, - 0xbe, 0xb9, 0x4f, 0x08, 0x37, 0x4d, 0x18, 0x2e, 0x9a, 0x70, 0x64, 0xd3, - 0x6e, 0x33, 0x0b, 0xde, 0xf5, 0x1a, 0x91, 0xa9, 0x75, 0x11, 0xb2, 0xaf, - 0xcc, 0x7a, 0x15, 0x0b, 0x07, 0xfc, 0x2e, 0x69, 0x0b, 0x11, 0x0f, 0xdf, - 0xfe, 0x6b, 0x33, 0x4b, 0x70, 0xc6, 0x94, 0x08, 0xaf, 0x9c, 0x33, 0x8f, - 0xec, 0xb1, 0x13, 0x23, 0x55, 0xbd, 0xde, 0x0c, 0x8e, 0x8f, 0x26, 0xc0, - 0x9b, 0x40, 0xad, 0x3c, 0xb9, 0x26, 0xdc, 0x9e, 0x10, 0x2d, 0x08, 0xbb, - 0x84, 0xd4, 0xa3, 0x00, 0xed, 0xba, 0x40, 0x23, 0x1e, 0x8e, 0xe2, 0xa3, - 0x9e, 0x93, 0xda, 0x7e, 0x84, 0x6d, 0x7c, 0x96, 0x4d, 0xe5, 0x41, 0xf7, - 0x4c, 0xd0, 0x65, 0xf5, 0xca, 0x9e, 0x37, 0x64, 0xe7, 0xd0, 0xf8, 0x2d, - 0x59, 0xe4, 0x3a, 0xe2, 0x7a, 0xbf, 0x78, 0x50, 0x7c, 0xc6, 0xfc, 0xf2, - 0xc1, 0xcc, 0x3e, 0x2d, 0x6b, 0xa6, 0x6e, 0xd7, 0xe9, 0xfa, 0xd6, 0xfc, - 0x54, 0xd6, 0xab, 0x98, 0x4c, 0x69, 0xeb, 0xd1, 0x63, 0xca, 0xa1, 0x9d, - 0x15, 0x99, 0xb4, 0x0e, 0xe3, 0x8e, 0xe0, 0x82, 0xe5, 0xc7, 0x8c, 0x5e, - 0xe3, 0xcf, 0x9c, 0x57, 0xaf, 0xee, 0x0d, 0xea, 0x75, 0xca, 0x7a, 0xcd, - 0x5c, 0x8f, 0xac, 0x7a, 0x17, 0xad, 0x11, 0xa5, 0xd7, 0xa8, 0xb5, 0xde, - 0x5a, 0xa3, 0x5c, 0xcc, 0x56, 0xde, 0xbc, 0x35, 0x62, 0xdd, 0xb9, 0x66, - 0x47, 0xbd, 0x5a, 0xff, 0x98, 0xe7, 0xbd, 0x72, 0x8d, 0x72, 0xb1, 0x46, - 0x39, 0x58, 0xa3, 0x3c, 0xb4, 0x6e, 0xad, 0x4b, 0x4f, 0x6a, 0x5d, 0x72, - 0xf4, 0xba, 0x54, 0x2f, 0xba, 0x2e, 0xb9, 0x7a, 0x5d, 0xf2, 0x6c, 0xeb, - 0x82, 0xf6, 0x50, 0x6b, 0xf1, 0x75, 0x19, 0x49, 0xad, 0xcb, 0x96, 0x79, - 0xeb, 0xe2, 0xd4, 0xda, 0x5d, 0x51, 0xaf, 0x9e, 0x35, 0x04, 0x3c, 0xba, - 0xdf, 0x9e, 0x95, 0xe4, 0xef, 0x47, 0xbf, 0xec, 0x23, 0x1d, 0x4e, 0xc1, - 0xfd, 0xae, 0x5e, 0xa4, 0xed, 0x85, 0x6b, 0xb2, 0x18, 0xcf, 0x21, 0x6b, - 0x60, 0xc4, 0xf5, 0x24, 0xd7, 0x7b, 0x42, 0x78, 0x30, 0x07, 0x3c, 0x13, - 0x13, 0x46, 0xbe, 0x1c, 0xbb, 0xe3, 0xa2, 0x3a, 0x8b, 0xad, 0xf7, 0xeb, - 0xf1, 0x78, 0xfe, 0xa7, 0xea, 0x49, 0xce, 0xb5, 0xaf, 0xb2, 0xbd, 0x2a, - 0x5f, 0xee, 0xdf, 0x2c, 0xec, 0xdf, 0x00, 0xda, 0x8f, 0x78, 0x32, 0xf5, - 0x1e, 0xbf, 0x09, 0xb3, 0xf4, 0xd7, 0xb6, 0xfd, 0x66, 0x78, 0x8b, 0xe9, - 0xbf, 0x18, 0xcf, 0xb2, 0x9f, 0xd9, 0x7a, 0xf5, 0xdc, 0x28, 0xe0, 0xf1, - 0x4a, 0x9d, 0x9d, 0x72, 0xd6, 0x1c, 0x14, 0xaf, 0x57, 0x67, 0x6f, 0x29, - 0x3e, 0x57, 0x60, 0xcf, 0x4e, 0xf4, 0x16, 0x60, 0xa5, 0x9e, 0x44, 0x8f, - 0x6e, 0x23, 0xd0, 0xef, 0xa5, 0x4e, 0x07, 0xfc, 0x85, 0xc7, 0x05, 0x49, - 0xce, 0x45, 0x3c, 0x19, 0x48, 0x4d, 0xf4, 0x7a, 0x91, 0xf3, 0xa1, 0xcc, - 0x05, 0xce, 0x32, 0xc9, 0xf1, 0xc1, 0x82, 0xf2, 0x8d, 0x5c, 0x51, 0x46, - 0x35, 0x82, 0xb5, 0x5d, 0x8a, 0xd6, 0x4b, 0xb5, 0x4e, 0xfc, 0xbc, 0x25, - 0x43, 0xda, 0x5a, 0xd5, 0xcd, 0x2d, 0xe5, 0x55, 0x52, 0x27, 0xd6, 0xea, - 0xd6, 0x7a, 0xeb, 0x6c, 0xf3, 0xa1, 0x1d, 0x3e, 0x79, 0xb9, 0x2f, 0x3f, - 0xb1, 0xc5, 0xe6, 0x6b, 0x1d, 0xd9, 0x8b, 0x1d, 0xad, 0x57, 0xcf, 0x08, - 0x95, 0x8e, 0x01, 0xf4, 0xd5, 0x29, 0x2a, 0x64, 0x0d, 0x39, 0xc7, 0x9e, - 0x2a, 0x50, 0xb7, 0xe1, 0x6b, 0x6f, 0x5f, 0xbd, 0x14, 0xdc, 0x4a, 0xc9, - 0x2d, 0x31, 0xde, 0x0d, 0x2b, 0x6d, 0x65, 0x7f, 0x2a, 0xca, 0x85, 0x43, - 0xed, 0x2d, 0x8f, 0x29, 0xcb, 0xca, 0x10, 0x11, 0x95, 0xc9, 0x93, 0xde, - 0x4b, 0x4a, 0x63, 0x35, 0x37, 0xd5, 0x28, 0x6d, 0xd0, 0xf6, 0x24, 0x10, - 0x6f, 0x39, 0xa5, 0x06, 0x44, 0x77, 0xd4, 0xab, 0xe7, 0x66, 0x13, 0xf0, - 0xfb, 0x31, 0xcf, 0x13, 0xd2, 0x86, 0x0a, 0xb0, 0x87, 0x60, 0xff, 0xa6, - 0x57, 0xda, 0xa7, 0xda, 0x47, 0x1f, 0xd6, 0x7b, 0x5b, 0xeb, 0xd9, 0x56, - 0x80, 0x5e, 0xb3, 0x28, 0xb0, 0x1a, 0xfa, 0x1a, 0x38, 0x6f, 0xfc, 0x37, - 0x40, 0x9b, 0x32, 0xbe, 0x41, 0xda, 0xfa, 0xcc, 0x97, 0x7d, 0xb1, 0x67, - 0x57, 0xbd, 0x3e, 0x5c, 0xaf, 0x9e, 0x2f, 0x95, 0x22, 0xe2, 0x41, 0x1b, - 0xc1, 0x65, 0xd0, 0xfa, 0x34, 0x8f, 0x0f, 0xa7, 0xf3, 0x00, 0xe2, 0x85, - 0xc0, 0x24, 0x73, 0xee, 0xb5, 0x9f, 0xab, 0x66, 0xa7, 0xb9, 0xd4, 0x3a, - 0x57, 0xcd, 0x72, 0xd3, 0x4f, 0x67, 0x9c, 0x86, 0x08, 0x0c, 0x08, 0x2a, - 0x37, 0xbd, 0x72, 0x96, 0x70, 0xea, 0x99, 0x5b, 0x4d, 0x61, 0x88, 0xb1, - 0xda, 0x5f, 0xf1, 0xd8, 0xb9, 0xff, 0x5a, 0xa3, 0x46, 0xd4, 0x5e, 0x50, - 0x3a, 0x34, 0xc9, 0x9e, 0x9b, 0xa1, 0x83, 0x3a, 0x33, 0x9e, 0xac, 0x57, - 0xcf, 0xac, 0x02, 0x3b, 0x54, 0x5f, 0x3c, 0x7a, 0xee, 0x8b, 0x63, 0x84, - 0x4e, 0xd1, 0x2a, 0xfb, 0x22, 0x39, 0xaf, 0x7e, 0x0a, 0x20, 0x88, 0x2e, - 0x37, 0x54, 0x3f, 0x86, 0x8c, 0x35, 0x78, 0x7d, 0xdd, 0x0e, 0xdf, 0x9a, - 0xf6, 0xfe, 0x5f, 0x5d, 0xe0, 0xd5, 0x60, 0x6e, 0x89, 0x43, 0xad, 0xc6, - 0x6a, 0xa4, 0xcb, 0x11, 0xdb, 0x74, 0x1a, 0x3f, 0xbc, 0xc0, 0xab, 0x21, - 0x6b, 0xf8, 0xef, 0xa0, 0x3d, 0x58, 0xbf, 0x4e, 0xe3, 0x5b, 0x17, 0xbc, - 0xc2, 0x97, 0xe1, 0xcd, 0xc8, 0xd0, 0x16, 0xf2, 0xcf, 0xa9, 0x75, 0x97, - 0x73, 0xd1, 0xab, 0xb4, 0xe1, 0x15, 0x64, 0x6d, 0x84, 0xac, 0x53, 0x2c, - 0xb5, 0x11, 0xb2, 0x5d, 0x68, 0x53, 0x05, 0x6d, 0x1c, 0x4a, 0x1b, 0x79, - 0x9a, 0xc3, 0x9a, 0x30, 0x43, 0x8e, 0xf6, 0xf1, 0x97, 0x2e, 0x60, 0xe4, - 0x46, 0x19, 0xf9, 0x9c, 0x5e, 0x67, 0xae, 0x53, 0x8e, 0xdf, 0x69, 0x8d, - 0xbf, 0x46, 0xf6, 0x56, 0x4b, 0x19, 0xf2, 0xcc, 0x17, 0xf4, 0xd5, 0x7a, - 0xf5, 0xbc, 0x86, 0x35, 0x74, 0x4a, 0x9b, 0x7c, 0xbf, 0x1c, 0xbb, 0xcf, - 0xe8, 0x84, 0x25, 0xe5, 0x1b, 0xa5, 0x02, 0xfa, 0x08, 0x8e, 0xc2, 0x4c, - 0x19, 0xd5, 0x98, 0x54, 0x26, 0x10, 0xc7, 0xf5, 0x2c, 0xb5, 0x6a, 0x18, - 0x3e, 0x47, 0xa4, 0xf5, 0x41, 0xf2, 0x3b, 0x02, 0x3d, 0x4b, 0xc0, 0x2b, - 0x92, 0x33, 0x58, 0x69, 0xac, 0xa4, 0x58, 0xeb, 0x95, 0xd4, 0x67, 0xfa, - 0x0a, 0x22, 0x3d, 0x0e, 0x72, 0x56, 0xe5, 0x48, 0xcf, 0x1f, 0xe8, 0x4b, - 0xd5, 0x73, 0x72, 0xad, 0x31, 0xe2, 0x73, 0xfc, 0x26, 0xdc, 0xcb, 0xd0, - 0xaa, 0x03, 0xba, 0x8a, 0x1a, 0xa7, 0x3a, 0x67, 0x3b, 0xa4, 0x9e, 0x7d, - 0xd2, 0x7f, 0x72, 0x1c, 0xfd, 0xe3, 0x7a, 0x92, 0x31, 0xba, 0xcf, 0x13, - 0xdb, 0x71, 0x23, 0x55, 0xf4, 0x73, 0x84, 0xc7, 0x91, 0x1e, 0x97, 0xbd, - 0xa8, 0xfd, 0x1e, 0xc7, 0x61, 0x3e, 0x23, 0x36, 0x76, 0x23, 0xf5, 0x7b, - 0x72, 0x4c, 0xaf, 0xc9, 0xcf, 0x33, 0x78, 0x2f, 0xbe, 0x84, 0xf2, 0xb7, - 0xa4, 0xe6, 0xb6, 0x33, 0xc7, 0x44, 0x34, 0x56, 0x6b, 0xf8, 0x2a, 0x22, - 0xad, 0x49, 0x2a, 0xce, 0xca, 0xc9, 0xf2, 0x89, 0xce, 0xac, 0x11, 0x0a, - 0xdc, 0x5a, 0x82, 0x75, 0xfb, 0x08, 0x6e, 0x24, 0x3c, 0xd7, 0x18, 0x27, - 0xac, 0x67, 0xec, 0x8e, 0x52, 0x8a, 0xf9, 0xe3, 0x98, 0xaf, 0x1c, 0xf0, - 0xd6, 0x51, 0xa0, 0x52, 0x59, 0x40, 0xa7, 0xd1, 0x8e, 0x76, 0x0a, 0x69, - 0xec, 0x74, 0x19, 0xea, 0x94, 0xd2, 0x97, 0x60, 0x75, 0x9d, 0x66, 0x09, - 0x55, 0xe6, 0x54, 0x92, 0xeb, 0x4b, 0xae, 0xdb, 0x5c, 0x77, 0x99, 0x0f, - 0x1f, 0xcc, 0xd8, 0x08, 0xfd, 0x6a, 0x3d, 0x63, 0xf7, 0xb2, 0xcc, 0x72, - 0xba, 0x9f, 0x72, 0xdc, 0x9d, 0xee, 0x4c, 0xc8, 0x54, 0x51, 0xac, 0x67, - 0x98, 0x4e, 0xdc, 0x5b, 0x6b, 0xb8, 0x4a, 0x3b, 0x73, 0xf2, 0x28, 0xf0, - 0x4e, 0xd4, 0xcc, 0xee, 0x40, 0x4f, 0x77, 0xe2, 0xde, 0xc5, 0xf3, 0x72, - 0x6f, 0x36, 0x7a, 0xcf, 0x91, 0xf6, 0x9b, 0x85, 0xfd, 0x93, 0xa5, 0xc6, - 0x17, 0x90, 0x73, 0xb2, 0x9b, 0x9c, 0xda, 0x76, 0x5f, 0xab, 0x57, 0xef, - 0x15, 0x02, 0xad, 0xcb, 0x31, 0xab, 0x0f, 0x68, 0xdb, 0xfd, 0xe4, 0x02, - 0x1b, 0x5e, 0x6e, 0xb3, 0xe1, 0x36, 0xec, 0x50, 0xc8, 0xfa, 0xef, 0x87, - 0xa7, 0xe6, 0xfc, 0x2a, 0xd4, 0x9b, 0x92, 0x16, 0xc5, 0x96, 0xd9, 0x27, - 0x47, 0x7e, 0x96, 0x2d, 0x13, 0xb3, 0x97, 0x8e, 0x49, 0x33, 0x1a, 0xd4, - 0x73, 0xea, 0x88, 0x7f, 0x86, 0xad, 0xda, 0xb1, 0x60, 0x4f, 0x62, 0x65, - 0xf3, 0x6d, 0xb1, 0x6e, 0x2d, 0xda, 0xbc, 0x4d, 0xf6, 0xdd, 0xde, 0x7b, - 0xee, 0x82, 0x97, 0x7c, 0x0e, 0xaf, 0x83, 0xb4, 0xce, 0xd8, 0xfd, 0x0d, - 0x4a, 0x67, 0xb5, 0x16, 0x6c, 0x6d, 0xb0, 0x2b, 0xb1, 0x0e, 0xbb, 0x34, - 0xe6, 0xbf, 0x9e, 0xe3, 0x40, 0x68, 0x8c, 0xb8, 0xb7, 0x10, 0x1e, 0xd6, - 0x0c, 0xb4, 0x2d, 0x95, 0x2b, 0xd2, 0xcb, 0xf6, 0x58, 0xd9, 0x29, 0x4c, - 0xe9, 0x77, 0x63, 0xfe, 0xfd, 0x2c, 0x87, 0xf9, 0x56, 0x79, 0xd8, 0xb9, - 0x99, 0x6f, 0xe6, 0x9a, 0x98, 0x27, 0x73, 0xbe, 0x8d, 0xe7, 0xcb, 0x3e, - 0x0d, 0x2a, 0x6b, 0x50, 0xef, 0x33, 0x76, 0xbe, 0xb0, 0x42, 0x5a, 0xb0, - 0xcf, 0x83, 0xd5, 0xf7, 0x2b, 0xcb, 0x0e, 0x7c, 0xb7, 0x88, 0x2a, 0xaa, - 0x7c, 0xa6, 0xcf, 0xd9, 0xd1, 0xd1, 0x47, 0x43, 0x19, 0x86, 0x93, 0xf7, - 0x51, 0x06, 0xb9, 0x5d, 0x63, 0x2f, 0x95, 0x52, 0xfb, 0xd1, 0x0e, 0xe8, - 0x58, 0x81, 0x7c, 0x8e, 0xab, 0xe2, 0x90, 0x2f, 0xab, 0xe3, 0x78, 0x13, - 0x0d, 0xb9, 0x5d, 0x59, 0x11, 0x4f, 0x39, 0xc6, 0xed, 0xce, 0x6e, 0xcf, - 0xae, 0x44, 0x79, 0x99, 0x4c, 0x8f, 0xbd, 0x52, 0x46, 0xed, 0x5f, 0x83, - 0x0d, 0x79, 0x56, 0x20, 0x9f, 0x93, 0xc7, 0x63, 0xcb, 0x93, 0xb3, 0x93, - 0x27, 0x3d, 0x35, 0xda, 0xc8, 0x2b, 0xa1, 0xef, 0x4b, 0xdf, 0x90, 0x87, - 0x74, 0x79, 0x5e, 0x3b, 0xac, 0xde, 0x9b, 0x77, 0x26, 0x2b, 0x4b, 0xd4, - 0x3e, 0x7f, 0xc6, 0xe9, 0x14, 0xb5, 0x5f, 0x28, 0x13, 0x25, 0x38, 0xc8, - 0xbc, 0x79, 0xb9, 0x79, 0x65, 0x02, 0xbb, 0x36, 0xaf, 0xf6, 0x02, 0x9f, - 0xe2, 0x9b, 0x31, 0x9a, 0x5d, 0x29, 0x5f, 0xbd, 0x53, 0xcf, 0xa5, 0x41, - 0xad, 0x0d, 0xea, 0x3d, 0x8f, 0x9a, 0xcb, 0x98, 0x27, 0xc2, 0x2b, 0x20, - 0x26, 0xda, 0x8a, 0x30, 0x2f, 0x95, 0xb8, 0x45, 0xc4, 0x3c, 0xa7, 0xf4, - 0xaa, 0xf1, 0x7c, 0xf1, 0xaa, 0xb1, 0x77, 0xe4, 0x55, 0x33, 0x6d, 0x96, - 0x61, 0x4a, 0x4b, 0xa8, 0x95, 0x11, 0x55, 0x81, 0xb4, 0x84, 0xbb, 0x11, - 0x95, 0xf1, 0x1a, 0xb2, 0x27, 0x3e, 0xe0, 0x79, 0xb7, 0x3e, 0xdd, 0xef, - 0x86, 0xe7, 0xae, 0x7d, 0x91, 0x3d, 0x34, 0xd6, 0xd6, 0xac, 0x1a, 0xe0, - 0xfb, 0xd3, 0x87, 0xa8, 0x47, 0xfa, 0x61, 0xdc, 0x9f, 0xe4, 0x0a, 0x40, - 0x67, 0xb3, 0xf6, 0x02, 0xaf, 0x01, 0xfb, 0xf8, 0x81, 0x94, 0xce, 0xfd, - 0x72, 0xed, 0xd9, 0x96, 0xfa, 0x1b, 0x52, 0xfb, 0xd8, 0x1f, 0xe5, 0x96, - 0x71, 0x2e, 0x59, 0x65, 0x9b, 0xad, 0x32, 0x7f, 0xba, 0x2c, 0x5f, 0xdf, - 0x4d, 0xb6, 0x36, 0xa8, 0x77, 0x0e, 0xe9, 0x71, 0x05, 0xbe, 0x83, 0x95, - 0xf3, 0xf9, 0x70, 0x83, 0xe8, 0xa8, 0x6e, 0xa1, 0x21, 0xa7, 0x30, 0x79, - 0xa5, 0xe0, 0x59, 0x32, 0x2a, 0xf6, 0xf9, 0x32, 0x3b, 0x0e, 0x55, 0xd2, - 0x50, 0x56, 0x46, 0x26, 0xaf, 0x54, 0x16, 0xb9, 0xdd, 0xed, 0x2f, 0xe3, - 0xfe, 0xe1, 0xae, 0x95, 0x67, 0xae, 0x13, 0xab, 0xe5, 0x2b, 0x6d, 0xff, - 0xa6, 0x03, 0xfa, 0xfa, 0x72, 0x78, 0xfd, 0x20, 0x91, 0x15, 0xeb, 0xb9, - 0x85, 0xfe, 0xe1, 0x34, 0x8f, 0xf3, 0x3e, 0x3a, 0x93, 0x99, 0x29, 0x6a, - 0x7f, 0x76, 0xc6, 0x34, 0x45, 0xed, 0x77, 0xbd, 0x72, 0xbe, 0x79, 0x4c, - 0x57, 0x37, 0xa8, 0xf7, 0x68, 0x31, 0xcf, 0x2a, 0xe4, 0x2b, 0xb1, 0xb2, - 0x31, 0x0f, 0x3f, 0x6f, 0x9a, 0x68, 0x5d, 0x3a, 0x2f, 0xb7, 0xdc, 0xa6, - 0xe5, 0x44, 0xeb, 0xb2, 0x79, 0x65, 0x25, 0x34, 0xb1, 0x66, 0x09, 0xce, - 0xe3, 0xef, 0x62, 0xf6, 0x8a, 0xe5, 0x7a, 0x94, 0x9b, 0xc5, 0xe0, 0x15, - 0xd3, 0x3a, 0xd3, 0x4d, 0x25, 0x26, 0xdf, 0xe2, 0x42, 0x32, 0x0a, 0x35, - 0xa5, 0xdd, 0xd4, 0x48, 0x99, 0x8a, 0x1e, 0x9f, 0xe8, 0xc0, 0x9d, 0x63, - 0xc8, 0x30, 0xb1, 0x12, 0x73, 0xb8, 0xa1, 0x9f, 0x11, 0x42, 0xd4, 0xfe, - 0x6f, 0x00, 0xf5, 0x10, 0x4f, 0x20, 0x38, 0x5e, 0x03, 0x69, 0x2b, 0x86, - 0x88, 0x41, 0x4f, 0x7e, 0x9e, 0x12, 0xe8, 0x29, 0x46, 0x2f, 0x0d, 0x58, - 0x23, 0xeb, 0x54, 0x61, 0xef, 0x7b, 0x1f, 0xa4, 0x02, 0xbd, 0xca, 0x63, - 0xa8, 0xf3, 0xe5, 0x93, 0x0b, 0xce, 0x99, 0x42, 0xdb, 0x39, 0x03, 0x8f, - 0xd1, 0xaf, 0x3c, 0x46, 0x95, 0xcc, 0xb3, 0xc7, 0x98, 0x64, 0xbf, 0xf6, - 0x6b, 0x8e, 0x9f, 0x96, 0xc3, 0xd2, 0x02, 0x3d, 0x25, 0xaf, 0xe7, 0x7d, - 0xd0, 0x56, 0x89, 0xe5, 0x7d, 0x50, 0xb7, 0x16, 0x6d, 0x95, 0xd8, 0xda, - 0xe2, 0xbd, 0x75, 0x97, 0x3c, 0x17, 0xcb, 0x8d, 0x6a, 0xc8, 0x5d, 0xc1, - 0xed, 0xfe, 0x62, 0xa2, 0xb7, 0x04, 0x6d, 0x2e, 0x76, 0x1a, 0x57, 0x2e, - 0x38, 0x8d, 0x0d, 0xf8, 0x61, 0x8f, 0xa4, 0xe5, 0x86, 0x29, 0x23, 0x37, - 0xc3, 0x96, 0xe3, 0x13, 0x2e, 0xe0, 0x85, 0xbf, 0x16, 0x5e, 0x19, 0xb9, - 0xc1, 0x23, 0xfc, 0x58, 0xb5, 0xcb, 0xcf, 0x78, 0x39, 0x42, 0x37, 0x75, - 0x7c, 0x62, 0xc5, 0x07, 0xec, 0x63, 0x1b, 0x20, 0xf7, 0xfe, 0x94, 0xdd, - 0xde, 0x21, 0x4c, 0x6d, 0x7f, 0x27, 0x1b, 0xd4, 0xb9, 0xc4, 0x67, 0x3e, - 0xfb, 0x3c, 0x5f, 0x5b, 0xbb, 0x87, 0x9f, 0x04, 0x04, 0xb0, 0x8a, 0x3e, - 0xdc, 0x49, 0xeb, 0xa8, 0xd5, 0xc7, 0x31, 0x68, 0x96, 0x3c, 0x93, 0x1c, - 0x74, 0x37, 0xe4, 0x8f, 0xd9, 0xfc, 0x5c, 0x09, 0x5a, 0xe7, 0xf8, 0xcd, - 0xaf, 0xe2, 0x37, 0x58, 0x6d, 0x60, 0x6e, 0x39, 0x6e, 0xa9, 0xef, 0xe1, - 0xf1, 0x67, 0xb0, 0x97, 0x0b, 0x91, 0xdb, 0xb9, 0xce, 0x39, 0x43, 0x3a, - 0xc6, 0x73, 0xfa, 0xd6, 0xb6, 0x87, 0x36, 0xc1, 0x13, 0xa6, 0xa2, 0x3c, - 0xa7, 0x8a, 0x2b, 0xae, 0x48, 0x45, 0x79, 0x9d, 0x56, 0x94, 0xe7, 0x08, - 0xcc, 0x29, 0x4f, 0xd9, 0x81, 0x7a, 0x9d, 0x4e, 0x44, 0xac, 0x07, 0x96, - 0xca, 0x79, 0x6e, 0x93, 0xb2, 0x26, 0xa9, 0xf2, 0x87, 0x50, 0xee, 0x2a, - 0xa8, 0x2d, 0xb5, 0x62, 0x43, 0xf6, 0xb8, 0x45, 0xd2, 0xe3, 0x62, 0x1c, - 0x5e, 0xcc, 0x1b, 0x24, 0x55, 0xec, 0xc8, 0xbb, 0x97, 0x67, 0x50, 0xc8, - 0x99, 0xcb, 0x94, 0x91, 0x5c, 0x25, 0x4e, 0x8b, 0xd8, 0xf8, 0x1e, 0x3a, - 0x70, 0x34, 0x70, 0x28, 0xa5, 0xb9, 0xc1, 0xa7, 0x55, 0xad, 0xa8, 0xcc, - 0xdc, 0x00, 0x0b, 0xff, 0x14, 0x24, 0x3b, 0x4d, 0x27, 0x15, 0x18, 0xdf, - 0x76, 0xd5, 0x3a, 0x0a, 0x8c, 0x29, 0xba, 0xa5, 0x32, 0xd2, 0xf3, 0x38, - 0x15, 0xaf, 0x96, 0x91, 0x60, 0x26, 0x3c, 0x74, 0x26, 0xef, 0xa3, 0x41, - 0xb4, 0xcb, 0xb7, 0xa4, 0x65, 0x98, 0xd9, 0x49, 0x1d, 0x8b, 0x1a, 0xf4, - 0x0e, 0x71, 0x73, 0xcb, 0xe4, 0x3b, 0x84, 0x8c, 0x9f, 0x0d, 0x19, 0x4b, - 0x3f, 0xd9, 0xa0, 0xde, 0x63, 0xa5, 0xe7, 0xd0, 0x99, 0x9e, 0x43, 0x11, - 0xe8, 0x53, 0x5a, 0x08, 0xf9, 0x24, 0xe3, 0x23, 0x38, 0x99, 0xd8, 0xde, - 0x60, 0xaf, 0x7d, 0x58, 0x63, 0xff, 0x07, 0x10, 0x5b, 0xb8, 0x8d, 0x75, - 0xc6, 0x95, 0x34, 0x5f, 0xee, 0x7e, 0x1d, 0x97, 0xb1, 0x1d, 0x87, 0xe4, - 0x08, 0xd8, 0xbe, 0x75, 0xb9, 0xe0, 0x39, 0x2a, 0x90, 0xd1, 0x57, 0xa5, - 0xc3, 0x8f, 0x08, 0xa6, 0x9d, 0xc6, 0xa5, 0x3f, 0x6c, 0xa3, 0x03, 0xf2, - 0x8d, 0x52, 0x6d, 0x4b, 0x5a, 0x8b, 0xd7, 0x8b, 0xbd, 0x97, 0xeb, 0x55, - 0xb1, 0xcf, 0x72, 0xe1, 0x9b, 0x98, 0x65, 0xa7, 0x8a, 0x97, 0x11, 0xaf, - 0x71, 0x0c, 0xc4, 0xf3, 0x14, 0x41, 0xc9, 0x01, 0x7d, 0xaf, 0xe0, 0x77, - 0x81, 0x2a, 0xf6, 0x3e, 0x78, 0x73, 0x4b, 0xf2, 0x20, 0xee, 0x12, 0xea, - 0x0c, 0x78, 0xae, 0x41, 0xbd, 0x9b, 0xb7, 0xe6, 0xc8, 0xcf, 0x67, 0x91, - 0xe0, 0x98, 0x65, 0x99, 0xec, 0x73, 0x34, 0x15, 0xb5, 0x5a, 0x3b, 0xc3, - 0x57, 0xe0, 0x2b, 0x6c, 0x5f, 0xd3, 0x8e, 0x53, 0x33, 0x70, 0x97, 0xda, - 0xb9, 0x1c, 0x8d, 0xf1, 0xce, 0x55, 0xf4, 0xe3, 0xec, 0x1f, 0x33, 0x3a, - 0x33, 0x7e, 0x7f, 0x81, 0x77, 0x1b, 0xfb, 0xd3, 0xf2, 0x8c, 0x5a, 0x1a, - 0xdb, 0x57, 0x6a, 0xb5, 0x9e, 0xd9, 0x7e, 0xf4, 0x77, 0x17, 0x02, 0x77, - 0xaa, 0x9d, 0x3c, 0x25, 0x3d, 0x98, 0xda, 0xc9, 0xca, 0x9b, 0xa9, 0x9d, - 0x6c, 0x52, 0xed, 0x37, 0x3a, 0x8d, 0x4c, 0xbd, 0x2b, 0xf5, 0x6e, 0xbc, - 0x97, 0x57, 0xe6, 0x83, 0xf0, 0x70, 0x65, 0x7a, 0x3e, 0x31, 0x5e, 0xb7, - 0xdc, 0x7f, 0x6e, 0x2b, 0x3e, 0xdd, 0x89, 0x31, 0xbe, 0x25, 0xb5, 0xff, - 0xae, 0x59, 0xf4, 0x59, 0x99, 0xb5, 0x27, 0xff, 0xa3, 0x41, 0xdd, 0xb9, - 0xad, 0x3d, 0x58, 0xab, 0xf7, 0xa0, 0xf5, 0x3c, 0xe8, 0xac, 0x3e, 0x4f, - 0x02, 0x9e, 0x12, 0x69, 0xfd, 0x7e, 0xe9, 0x23, 0x0d, 0x69, 0x5d, 0x2f, - 0x37, 0xa8, 0xef, 0x36, 0xe8, 0x08, 0x04, 0x1e, 0x9a, 0x77, 0xb3, 0xf6, - 0x93, 0xb6, 0xb8, 0xa3, 0x53, 0x14, 0xe9, 0xfc, 0xfd, 0xc8, 0xab, 0xdd, - 0xb4, 0x70, 0xf5, 0x54, 0xf9, 0x43, 0x28, 0xd7, 0xb1, 0x09, 0x6a, 0xf1, - 0x2d, 0x6f, 0xaf, 0xdc, 0xeb, 0xec, 0x2f, 0x59, 0xc6, 0xb6, 0x67, 0x20, - 0x5b, 0x61, 0xc5, 0x3b, 0x88, 0x63, 0x8a, 0x69, 0xc1, 0x0d, 0x06, 0x76, - 0x78, 0x08, 0x76, 0xc8, 0x56, 0x61, 0xf2, 0xb3, 0x09, 0x3e, 0x15, 0xd0, - 0x22, 0x9f, 0x0a, 0x85, 0xca, 0x4a, 0x64, 0xb4, 0xc3, 0xb1, 0xc1, 0x00, - 0xda, 0xde, 0xa6, 0x6d, 0x84, 0xbf, 0xcb, 0x91, 0x2d, 0x6d, 0x64, 0xe2, - 0xe6, 0x96, 0xb1, 0x09, 0x39, 0x56, 0x79, 0x23, 0x6e, 0x54, 0xcf, 0xa4, - 0x7d, 0x38, 0x63, 0x10, 0xd3, 0xa3, 0xdd, 0xf7, 0xc9, 0x48, 0x81, 0x29, - 0x7a, 0x33, 0x56, 0x57, 0x30, 0xb7, 0xdc, 0xc4, 0x2a, 0xb7, 0x95, 0xd1, - 0x58, 0x1f, 0xe2, 0x98, 0x8a, 0x7c, 0xda, 0xd9, 0xb6, 0x82, 0x76, 0xf6, - 0xad, 0x40, 0x3a, 0x0b, 0xfc, 0x52, 0x1a, 0x1b, 0x40, 0x3c, 0xd4, 0xeb, - 0x90, 0x4f, 0xce, 0xac, 0xfb, 0x2a, 0x7c, 0x66, 0xa3, 0xf5, 0x1d, 0x98, - 0x36, 0x1a, 0xeb, 0x2d, 0xc5, 0xde, 0x3f, 0x42, 0xdf, 0xf5, 0x1e, 0xa1, - 0xb1, 0xfe, 0x32, 0xea, 0xa8, 0xc2, 0x09, 0xeb, 0x28, 0x70, 0xdc, 0x4c, - 0x8f, 0xb7, 0xc5, 0xfc, 0xef, 0x42, 0xfc, 0x10, 0xf1, 0x9c, 0x90, 0x37, - 0xde, 0x9d, 0xbd, 0x88, 0xc4, 0x30, 0x93, 0xbe, 0x3f, 0xe0, 0x04, 0xf6, - 0xde, 0x81, 0xa5, 0xcd, 0xa7, 0xf4, 0x73, 0x1c, 0x7e, 0x54, 0x96, 0x7e, - 0x8e, 0x73, 0xb5, 0x28, 0xa5, 0x6b, 0x8c, 0x15, 0x74, 0xb5, 0xa3, 0x2c, - 0xf5, 0x1c, 0x87, 0x68, 0xb9, 0x7e, 0x2e, 0x7d, 0x1d, 0x50, 0x80, 0xd3, - 0xa0, 0x03, 0xeb, 0x71, 0x40, 0xd6, 0xe6, 0x9b, 0x41, 0x09, 0x1d, 0x97, - 0xe7, 0xab, 0x3a, 0x5b, 0x78, 0xb5, 0xb2, 0xf5, 0x53, 0x61, 0x87, 0x3e, - 0x23, 0x2b, 0x1b, 0xd5, 0x73, 0x7c, 0x7e, 0xce, 0x89, 0x12, 0xd8, 0xcf, - 0x3a, 0xb4, 0xed, 0x15, 0xe9, 0x67, 0x9b, 0x9c, 0x2b, 0xa5, 0x3a, 0xd8, - 0xc3, 0x1e, 0x51, 0x4e, 0xeb, 0x70, 0xbb, 0xad, 0x14, 0xbc, 0x6a, 0x6b, - 0x30, 0xde, 0x30, 0x38, 0x65, 0xb4, 0xc7, 0x28, 0xc7, 0x3d, 0x2b, 0x17, - 0x11, 0x68, 0x8d, 0xe1, 0x93, 0xb5, 0x84, 0x7e, 0xb6, 0xce, 0xff, 0x2b, - 0xc8, 0xea, 0xab, 0x45, 0x7f, 0x0f, 0xc3, 0x3a, 0xf5, 0x0d, 0xd9, 0x3e, - 0x46, 0x0f, 0x7d, 0xac, 0x1e, 0xfc, 0xba, 0x87, 0x1a, 0xf4, 0x30, 0x2d, - 0x7b, 0xf0, 0xcb, 0x1e, 0xfc, 0xa9, 0x1e, 0xd4, 0x33, 0x3c, 0xee, 0x63, - 0x45, 0xaa, 0x0f, 0xeb, 0xdb, 0x28, 0xf3, 0xf7, 0x87, 0xf5, 0x4c, 0xaa, - 0xab, 0x51, 0x7d, 0x4f, 0x46, 0xcd, 0x63, 0xfa, 0x5b, 0x39, 0x16, 0xff, - 0x80, 0x7c, 0xcb, 0x9e, 0x47, 0xf7, 0x0a, 0x41, 0x79, 0xc2, 0xe3, 0x7b, - 0x0b, 0xc5, 0x90, 0xa0, 0xeb, 0xf1, 0xf1, 0x10, 0x8e, 0xa3, 0x3c, 0xba, - 0x16, 0x29, 0xe3, 0x1c, 0x75, 0x19, 0xef, 0xbb, 0xf9, 0x06, 0x7a, 0x4e, - 0x4a, 0x79, 0xf3, 0xe9, 0xef, 0x05, 0x74, 0x99, 0xde, 0x2f, 0x3c, 0x05, - 0xdd, 0x33, 0xa7, 0x68, 0x0e, 0xdc, 0x2f, 0x6f, 0x7b, 0x67, 0xe4, 0xd0, - 0xe1, 0xf0, 0xee, 0xdd, 0x13, 0x0f, 0x0d, 0x6f, 0xa7, 0x47, 0x58, 0x90, - 0x3e, 0x6a, 0xe0, 0xe3, 0x8c, 0x7c, 0x3e, 0x42, 0x79, 0xdd, 0x4d, 0x4d, - 0x4d, 0xdd, 0x74, 0x3b, 0x58, 0xdb, 0x8e, 0x5f, 0x3e, 0x72, 0x7d, 0x13, - 0xfd, 0x02, 0x22, 0xdd, 0x74, 0xaf, 0x61, 0x2a, 0xf3, 0x3c, 0x87, 0xec, - 0xb3, 0x23, 0xf4, 0xaa, 0x26, 0x5c, 0xf5, 0xb3, 0x86, 0x43, 0xd7, 0xa4, - 0x5f, 0xa6, 0xeb, 0x7d, 0xc1, 0xe0, 0x7a, 0xdf, 0x57, 0x65, 0xdb, 0x8e, - 0xd3, 0xc7, 0x1d, 0xd0, 0x25, 0x0f, 0x6d, 0x7f, 0xda, 0xc1, 0xac, 0x2d, - 0xdd, 0xf4, 0x2e, 0x3e, 0xd5, 0x7f, 0x8c, 0xcd, 0x3c, 0xda, 0x34, 0x3a, - 0xda, 0x3d, 0xd1, 0xb2, 0xad, 0xa7, 0xa5, 0x9b, 0xde, 0xcb, 0xcc, 0x57, - 0xc1, 0xdc, 0x30, 0x3a, 0x72, 0xf8, 0xc5, 0xfd, 0xf4, 0x5d, 0x87, 0xd2, - 0x6b, 0x37, 0x5a, 0xbf, 0x0b, 0x65, 0x1b, 0x8e, 0x76, 0xd3, 0x7b, 0x32, - 0x41, 0xef, 0x11, 0x85, 0xf9, 0xcf, 0x76, 0xd3, 0x5d, 0x19, 0x3c, 0x56, - 0x9f, 0xe7, 0xe8, 0xe1, 0x9f, 0xd3, 0xe9, 0x4c, 0xa4, 0x37, 0x08, 0x5f, - 0xfe, 0xcf, 0x9b, 0xe8, 0x2b, 0x4e, 0xa4, 0x47, 0x8e, 0x6f, 0x08, 0xb7, - 0x3c, 0x24, 0x7c, 0xbe, 0x47, 0x36, 0xd0, 0xc7, 0x9c, 0x06, 0xdf, 0x8d, - 0x3e, 0x05, 0x7e, 0x17, 0x9d, 0x37, 0xa5, 0x5a, 0xc7, 0x6f, 0x3c, 0xda, - 0xfc, 0xce, 0x77, 0x1f, 0x7f, 0xe4, 0xf8, 0xbd, 0x0f, 0xb7, 0xd1, 0x43, - 0xb2, 0xa5, 0xa5, 0x9e, 0xa3, 0xf4, 0x8e, 0x4c, 0x56, 0x15, 0xed, 0xd3, - 0x67, 0xb8, 0x95, 0x0d, 0xd7, 0x1f, 0x6a, 0x3e, 0xd5, 0x1c, 0x6e, 0xa6, - 0x1f, 0xb8, 0x90, 0x3b, 0xbc, 0xe1, 0xee, 0x63, 0x0f, 0xef, 0x1f, 0x19, - 0x1d, 0x09, 0x3f, 0x7b, 0x82, 0x7e, 0x69, 0x72, 0x79, 0xb8, 0x7c, 0x42, - 0x14, 0x7a, 0x0e, 0xfd, 0x6b, 0xd8, 0x98, 0xfb, 0x5b, 0xfa, 0x99, 0x89, - 0x5e, 0x7e, 0xc4, 0x1f, 0xe7, 0x59, 0x7c, 0xe2, 0xd4, 0x43, 0xe1, 0x4f, - 0x1f, 0xa4, 0x07, 0x58, 0xd0, 0xb8, 0xdd, 0xec, 0x3a, 0x74, 0xe8, 0xb0, - 0xf1, 0xa7, 0xb7, 0xdd, 0x30, 0x72, 0x76, 0xdb, 0x68, 0xf8, 0x7a, 0xfa, - 0x1c, 0x46, 0x34, 0xb1, 0x61, 0x42, 0x2c, 0xcd, 0xff, 0x5e, 0x53, 0x39, - 0x3e, 0x31, 0x90, 0x66, 0x60, 0xf4, 0xd0, 0x5b, 0x8f, 0x4f, 0x18, 0x33, - 0x57, 0xd3, 0x53, 0xac, 0xd3, 0x71, 0x3a, 0xc7, 0x64, 0xc3, 0xc8, 0xcc, - 0xb3, 0xc7, 0x7f, 0x31, 0x72, 0xec, 0xe8, 0xdb, 0x1f, 0x99, 0xe9, 0xbe, - 0xbc, 0xa7, 0x6d, 0xf7, 0xdd, 0xc2, 0xe3, 0xa1, 0x8f, 0x72, 0x27, 0x1b, - 0x8e, 0x3d, 0x7b, 0xec, 0xc2, 0xa9, 0x87, 0xb6, 0x8f, 0xfc, 0xa2, 0x1b, - 0x65, 0x7d, 0x3d, 0xf4, 0x45, 0x59, 0xe1, 0xd1, 0x91, 0x0f, 0x61, 0x3c, - 0xc7, 0x44, 0x61, 0x51, 0xf7, 0xc6, 0x89, 0xf2, 0x2b, 0xe9, 0x2c, 0x8f, - 0x86, 0x9e, 0xe0, 0x32, 0xfa, 0x35, 0xa7, 0xdf, 0xba, 0xe1, 0xd8, 0x23, - 0xdd, 0x1b, 0xaf, 0xd9, 0x3f, 0xd2, 0x7d, 0xcf, 0xfe, 0x91, 0xb7, 0x1d, - 0xdf, 0x48, 0xa7, 0x1d, 0x38, 0xef, 0x3f, 0xdb, 0x44, 0x77, 0x60, 0x01, - 0x44, 0x41, 0x3e, 0xbd, 0xc3, 0x91, 0x5a, 0xda, 0x9d, 0x90, 0x0f, 0x1f, - 0xa6, 0x2f, 0xf0, 0x1c, 0xbf, 0xeb, 0xad, 0x1b, 0x46, 0x0e, 0x95, 0x6f, - 0xa6, 0x27, 0x90, 0x69, 0x3c, 0xae, 0xd2, 0x97, 0xb3, 0xd9, 0xb2, 0x2d, - 0x17, 0x69, 0xfb, 0xae, 0xd6, 0x94, 0x9f, 0x1b, 0x19, 0x92, 0xb6, 0x2b, - 0x9f, 0x41, 0xea, 0xfc, 0xf0, 0x20, 0xda, 0x75, 0x6a, 0x99, 0x32, 0x4d, - 0x89, 0xac, 0xef, 0x9a, 0x09, 0xfd, 0xd4, 0x50, 0xc8, 0xef, 0xb1, 0x58, - 0x27, 0x8b, 0xf5, 0xae, 0xcb, 0x90, 0x74, 0xb5, 0xcc, 0xd7, 0x6a, 0xfe, - 0x60, 0x4a, 0x2e, 0x57, 0xd7, 0xed, 0x91, 0x9f, 0xaa, 0x8f, 0xde, 0x54, - 0x7d, 0xa1, 0xfb, 0x57, 0x28, 0x92, 0x7b, 0xd4, 0x20, 0xd2, 0xcf, 0x73, - 0xd3, 0xdf, 0x5d, 0x53, 0xbc, 0x0c, 0xcd, 0xcb, 0x90, 0x3c, 0x95, 0x76, - 0xa5, 0xda, 0x72, 0x6b, 0xea, 0xd5, 0xb4, 0x40, 0xcb, 0x14, 0xe8, 0x76, - 0x99, 0x57, 0x48, 0xe9, 0x77, 0x84, 0x86, 0xa4, 0x2b, 0xc8, 0x7a, 0xa7, - 0xa7, 0xee, 0xc0, 0x4b, 0xe4, 0x18, 0x4c, 0x52, 0xef, 0xa1, 0x6b, 0xb5, - 0xdf, 0xe4, 0x77, 0xe2, 0xa6, 0xa6, 0x5e, 0xed, 0x6f, 0xbb, 0x74, 0x9d, - 0x2e, 0x9c, 0x02, 0x6e, 0xdd, 0xde, 0x46, 0xed, 0x43, 0x7a, 0x74, 0x59, - 0xaf, 0xd6, 0xdf, 0x4c, 0xa5, 0xe5, 0xa8, 0x6b, 0xc9, 0x51, 0x3b, 0x5c, - 0x47, 0xa2, 0x8e, 0xaa, 0x5b, 0xdb, 0xfb, 0x3a, 0x5b, 0x07, 0x3b, 0x7a, - 0x9b, 0x06, 0x07, 0x06, 0x3b, 0x9b, 0xd6, 0xf4, 0xb5, 0xb7, 0x37, 0xf5, - 0x5e, 0xd6, 0xd1, 0xd6, 0xb4, 0xb6, 0x7f, 0xb0, 0x7d, 0xcd, 0x60, 0xff, - 0x9a, 0xfe, 0xcb, 0x5a, 0x5b, 0x29, 0xb3, 0x7b, 0x72, 0x26, 0x1c, 0x09, - 0x27, 0x37, 0x52, 0x46, 0xb7, 0xa2, 0xc6, 0xc6, 0x2e, 0x32, 0x37, 0x76, - 0xd5, 0xed, 0xe2, 0x4f, 0xa4, 0x0b, 0xfb, 0x66, 0xe6, 0x42, 0xc9, 0x68, - 0x34, 0x39, 0xbd, 0x35, 0x3c, 0x15, 0xde, 0x1a, 0x8c, 0x04, 0xf7, 0x85, - 0xe2, 0xb4, 0x71, 0x31, 0xae, 0x3f, 0x14, 0x8f, 0x47, 0xe3, 0xeb, 0xfd, - 0x93, 0xd1, 0xb9, 0x99, 0x29, 0x7f, 0x24, 0x9a, 0xf4, 0xef, 0x0b, 0x25, - 0xfd, 0x29, 0x49, 0xff, 0xe8, 0xa0, 0x3f, 0x31, 0x19, 0x8c, 0x44, 0x50, - 0xbf, 0xff, 0xcd, 0xd7, 0x9f, 0x0a, 0xed, 0x0d, 0xce, 0xcd, 0xd8, 0xdb, - 0x09, 0x4e, 0x05, 0x63, 0x49, 0x34, 0x52, 0x32, 0x30, 0x37, 0x3b, 0x7b, - 0x38, 0xc5, 0xdf, 0x14, 0x4c, 0x26, 0xfb, 0x83, 0x33, 0x33, 0x7b, 0x82, - 0x93, 0xfb, 0x49, 0x0c, 0x93, 0x31, 0x3c, 0x4a, 0x8e, 0xe1, 0xd1, 0x51, - 0x2a, 0x1f, 0xde, 0xee, 0x1f, 0x3c, 0x34, 0x19, 0x8a, 0x25, 0xc3, 0xd1, - 0x88, 0xff, 0x86, 0xe9, 0xf0, 0x4c, 0xc8, 0x3f, 0x39, 0x13, 0x4d, 0x84, - 0x23, 0xfb, 0xfc, 0xb1, 0x68, 0x3c, 0x49, 0x2b, 0x87, 0xb7, 0xbf, 0x5e, - 0xf9, 0x2c, 0xd4, 0x83, 0x0a, 0x07, 0xc3, 0x93, 0x21, 0x12, 0x5b, 0xc8, - 0xdc, 0xb2, 0xb3, 0x7f, 0x90, 0xbc, 0x5b, 0xe6, 0x26, 0x43, 0xac, 0xf8, - 0x70, 0x24, 0x36, 0x97, 0xdc, 0xc1, 0x4d, 0xf8, 0x2c, 0xd6, 0xf6, 0xb9, - 0xa4, 0xc5, 0xcb, 0xb1, 0x78, 0x32, 0x57, 0x68, 0xe5, 0xc6, 0xe7, 0x62, - 0xdc, 0x6b, 0xf3, 0xf5, 0xc1, 0x83, 0x41, 0x42, 0x40, 0x68, 0x8c, 0x0e, - 0x93, 0x63, 0x74, 0x58, 0x7e, 0xa0, 0x07, 0x7c, 0xec, 0x06, 0x0f, 0xba, - 0x8f, 0xe2, 0xc3, 0x1c, 0x1d, 0xdd, 0x3d, 0x4a, 0xd5, 0xa3, 0xc1, 0xc8, - 0x54, 0x3c, 0x1a, 0x9e, 0x6a, 0xd9, 0x63, 0x8d, 0xb6, 0x25, 0x35, 0xee, - 0x5e, 0x35, 0x1d, 0x5d, 0x54, 0xf5, 0x46, 0x52, 0x03, 0x72, 0x0c, 0x5d, - 0x54, 0xf1, 0x46, 0x42, 0x3c, 0x85, 0x5d, 0x54, 0x77, 0x29, 0x11, 0x6b, - 0x96, 0xbb, 0xa8, 0xe5, 0x92, 0xa2, 0xd3, 0xc1, 0x78, 0x70, 0x12, 0xea, - 0x85, 0x13, 0xc9, 0xf0, 0x64, 0x17, 0x35, 0x5c, 0xaa, 0xc2, 0x40, 0x28, - 0x31, 0x19, 0x0f, 0xc7, 0x92, 0x51, 0x0c, 0xe8, 0x0d, 0x87, 0xad, 0x8d, - 0x66, 0x71, 0x75, 0x67, 0x42, 0x69, 0xc1, 0xd1, 0xd0, 0xb8, 0xb2, 0xba, - 0xc5, 0x67, 0x08, 0xa2, 0x5c, 0x9e, 0x1e, 0xd3, 0xeb, 0xb4, 0xc7, 0x42, - 0x43, 0xe1, 0x19, 0x0c, 0xa5, 0xba, 0x6f, 0x2e, 0x3c, 0x33, 0xc5, 0xed, - 0x2d, 0x36, 0x99, 0xf3, 0x44, 0xdf, 0x50, 0x64, 0x2c, 0x94, 0x80, 0x61, - 0x2f, 0x3e, 0x27, 0x5a, 0x64, 0x3c, 0x94, 0x4c, 0xc2, 0x0c, 0x13, 0xe9, - 0x2e, 0xdf, 0x60, 0x08, 0x96, 0x70, 0x17, 0x2d, 0x4b, 0x09, 0x4d, 0x46, - 0x23, 0xc9, 0x50, 0x24, 0xd9, 0xd2, 0xcf, 0xf4, 0x10, 0x3a, 0x2b, 0x4f, - 0x15, 0xcd, 0x86, 0xa6, 0xc2, 0xc1, 0x16, 0x36, 0xf0, 0x16, 0x36, 0x4b, - 0xcb, 0x40, 0x1a, 0xdf, 0x58, 0x60, 0x38, 0xb2, 0x37, 0x5a, 0xcd, 0x06, - 0xcd, 0x09, 0xbb, 0x3a, 0xaf, 0x2b, 0xdd, 0x45, 0x2b, 0xdf, 0x58, 0x68, - 0x3c, 0x19, 0x4c, 0xce, 0x41, 0xeb, 0xca, 0xd7, 0x13, 0x4b, 0x6d, 0x33, - 0xbb, 0xc1, 0x2d, 0x90, 0xd1, 0xe6, 0x50, 0xad, 0x9a, 0x4c, 0xaf, 0xe6, - 0x65, 0x97, 0xaa, 0xb0, 0x3d, 0xa2, 0xaa, 0x6c, 0x8f, 0x85, 0x22, 0xa1, - 0xa9, 0x51, 0xd8, 0x69, 0x48, 0xda, 0x8a, 0xff, 0x12, 0x15, 0xdf, 0x60, - 0xec, 0x69, 0x1f, 0x60, 0x5f, 0xff, 0x05, 0x42, 0x63, 0xa1, 0xc9, 0x50, - 0xf8, 0x20, 0xb7, 0x53, 0x92, 0x12, 0x89, 0x26, 0x5a, 0xe4, 0x42, 0x57, - 0xef, 0x1a, 0x1c, 0x1b, 0x1f, 0xde, 0xbe, 0xad, 0x8b, 0x0a, 0xe6, 0x97, - 0x45, 0xa6, 0x66, 0xb0, 0x44, 0x85, 0x76, 0xe6, 0xe6, 0x20, 0x33, 0xd1, - 0x4c, 0x91, 0x9d, 0xbb, 0x23, 0x18, 0x9f, 0x0c, 0xcd, 0xec, 0x9c, 0x0b, - 0x4f, 0x75, 0x91, 0x2f, 0x55, 0x30, 0x97, 0x0c, 0xcf, 0xb4, 0x8c, 0x46, - 0xf7, 0xd9, 0xdb, 0x95, 0xbc, 0x1d, 0xc1, 0x70, 0x7c, 0x51, 0x66, 0x37, - 0xad, 0x19, 0x9d, 0x8c, 0xce, 0xb6, 0xc4, 0x67, 0x13, 0x33, 0x2d, 0xd7, - 0xc3, 0x83, 0xb5, 0x2c, 0x70, 0x63, 0xd5, 0x8b, 0x79, 0xf2, 0x2e, 0x6a, - 0xbb, 0x44, 0xad, 0x8b, 0x3c, 0x68, 0x17, 0xad, 0x7e, 0x93, 0x55, 0xec, - 0xb3, 0xdb, 0xf8, 0x26, 0xeb, 0x28, 0xe9, 0xd1, 0x4b, 0x48, 0xa7, 0x4d, - 0x32, 0x65, 0x4d, 0xaf, 0x7b, 0xc2, 0x74, 0xd1, 0xc0, 0x5f, 0xdd, 0x5a, - 0x9a, 0xc3, 0xc6, 0x16, 0x08, 0x26, 0xf6, 0x5f, 0x7a, 0xa2, 0x2e, 0x6a, - 0xe5, 0xd2, 0x83, 0xb6, 0x06, 0xbc, 0x23, 0x98, 0x9c, 0xe6, 0x0d, 0xff, - 0x86, 0xd2, 0xbc, 0xed, 0xa6, 0x82, 0x33, 0x07, 0xc3, 0xfb, 0x5b, 0xe0, - 0x24, 0xa3, 0xd8, 0x8a, 0x38, 0x04, 0x5b, 0x06, 0x23, 0xfa, 0x00, 0xec, - 0x9f, 0x09, 0x26, 0xb0, 0x35, 0xcb, 0x16, 0x91, 0x19, 0x66, 0x9f, 0xaa, - 0xcb, 0x2b, 0x16, 0x29, 0xdf, 0x1a, 0x9a, 0xdd, 0xa3, 0x05, 0x42, 0x10, - 0x59, 0xb1, 0x88, 0xc8, 0x78, 0x78, 0x5f, 0x04, 0x7b, 0x3f, 0x1e, 0xe2, - 0x4d, 0x70, 0x71, 0x71, 0x60, 0x3a, 0x1e, 0xbd, 0x01, 0x55, 0x97, 0x8c, - 0xf2, 0x59, 0xd9, 0x12, 0x8e, 0xb6, 0xd8, 0x0e, 0xea, 0x2e, 0xf2, 0x2a, - 0xf6, 0x4c, 0x30, 0xb2, 0xaf, 0x45, 0xeb, 0x51, 0x60, 0x63, 0x0d, 0xc3, - 0xe3, 0xc9, 0xf9, 0xf2, 0xd9, 0x98, 0xdb, 0xf7, 0x5c, 0x1f, 0x9a, 0x4c, - 0xce, 0xe7, 0x8d, 0x27, 0xe3, 0x18, 0x69, 0xaa, 0x1b, 0xc9, 0x93, 0x5d, - 0x07, 0xf7, 0xf0, 0x6e, 0x5b, 0x61, 0x63, 0xc7, 0x43, 0x7b, 0x5b, 0xae, - 0x0c, 0x05, 0xf7, 0x8f, 0x85, 0xf6, 0x86, 0xe2, 0xa1, 0xc8, 0xe4, 0xa5, - 0x8a, 0xbb, 0xad, 0x46, 0xe5, 0x86, 0xea, 0x8d, 0xc7, 0x83, 0x87, 0xd9, - 0xc3, 0x74, 0x2d, 0xce, 0xee, 0xb6, 0xd4, 0x4a, 0xb3, 0xd3, 0x63, 0x92, - 0xbc, 0xcd, 0xc1, 0x04, 0x0e, 0xbe, 0xd8, 0xa2, 0xcc, 0xee, 0x8b, 0x98, - 0x38, 0x10, 0x2e, 0x96, 0x04, 0xb3, 0x1b, 0x1e, 0x24, 0xcd, 0x1c, 0xc6, - 0x39, 0x15, 0x94, 0xe7, 0xad, 0xc7, 0xc6, 0x55, 0x6a, 0x2e, 0xe4, 0x74, - 0x53, 0xbe, 0x8d, 0x23, 0xdb, 0xf7, 0xda, 0x18, 0x81, 0xf0, 0x2c, 0x4f, - 0xf8, 0x92, 0x85, 0x2c, 0x65, 0xea, 0xde, 0x8b, 0x6c, 0x99, 0x7a, 0x2f, - 0x62, 0x2d, 0x1e, 0x04, 0xda, 0xa3, 0xc4, 0xc4, 0x61, 0xb8, 0xe8, 0x59, - 0x7f, 0x22, 0x14, 0x97, 0x51, 0x99, 0xef, 0xe2, 0x5d, 0x45, 0x39, 0xf6, - 0x2d, 0x40, 0xae, 0xf1, 0x81, 0x91, 0x6b, 0x87, 0xb7, 0x05, 0x68, 0xa5, - 0xfd, 0x94, 0x6c, 0xee, 0xef, 0x1d, 0x1d, 0xed, 0xeb, 0xed, 0x1f, 0xb9, - 0x36, 0x70, 0xf5, 0x8e, 0xc1, 0x6b, 0xb7, 0xf6, 0x06, 0xfa, 0x37, 0x5f, - 0x3b, 0xba, 0x7d, 0x3c, 0x40, 0x62, 0x17, 0x19, 0xbb, 0x10, 0x8e, 0xed, - 0x42, 0x00, 0x69, 0xee, 0x1a, 0xde, 0x3d, 0x4c, 0x19, 0xbb, 0xb6, 0x20, - 0x40, 0xdb, 0x02, 0x36, 0xc2, 0xb2, 0x5d, 0x88, 0xd7, 0xcc, 0x5d, 0x1c, - 0xb0, 0x39, 0x77, 0x49, 0x2e, 0x38, 0xf2, 0x83, 0xa5, 0x47, 0x55, 0x21, - 0xd2, 0x4e, 0xfe, 0xdc, 0xa2, 0x08, 0x82, 0xbc, 0x5d, 0xbb, 0x49, 0x20, - 0xae, 0x43, 0x63, 0x06, 0x02, 0x3a, 0x63, 0xa2, 0x8f, 0x2a, 0x27, 0x2e, - 0x1d, 0x3d, 0x34, 0x4d, 0xfc, 0x55, 0xa7, 0x71, 0xf5, 0x9b, 0x10, 0x87, - 0x45, 0x4c, 0x2c, 0xb2, 0x21, 0xe6, 0x31, 0xad, 0x1d, 0xe1, 0x0e, 0x4e, - 0x4e, 0x86, 0x12, 0x89, 0xea, 0x56, 0x5c, 0x15, 0xb2, 0x55, 0x7a, 0x68, - 0x26, 0xb8, 0x2f, 0x41, 0x8e, 0xe0, 0xd4, 0x14, 0x38, 0xaa, 0x2f, 0x19, - 0xde, 0xba, 0x83, 0xb1, 0x98, 0x0e, 0x32, 0x28, 0x23, 0x98, 0x60, 0x63, - 0xa1, 0xac, 0xd4, 0xb8, 0xa8, 0x24, 0x95, 0x1c, 0x1d, 0x94, 0xde, 0x47, - 0xad, 0xde, 0xce, 0x9d, 0xc3, 0x03, 0xe4, 0xd9, 0xb3, 0x20, 0xa6, 0xa3, - 0xc2, 0x3d, 0xf6, 0x43, 0x45, 0xe9, 0x9e, 0xb0, 0xc9, 0x5d, 0xab, 0x23, - 0x72, 0xcf, 0x9e, 0xa4, 0x2e, 0xe4, 0xb3, 0x0a, 0x2a, 0x53, 0xc6, 0x9e, - 0x24, 0x7b, 0x69, 0x72, 0xee, 0xe1, 0x73, 0x94, 0x32, 0x30, 0x97, 0x38, - 0x07, 0xc9, 0x39, 0x39, 0x13, 0x0a, 0xc6, 0x99, 0x44, 0x13, 0x21, 0x72, - 0x21, 0x2a, 0x8a, 0x60, 0xd4, 0x94, 0xad, 0x13, 0xb2, 0x4a, 0x26, 0xc7, - 0x4a, 0xc1, 0x70, 0x24, 0x21, 0xd9, 0x32, 0x35, 0x12, 0x3a, 0x4c, 0x62, - 0x8a, 0x32, 0x55, 0x77, 0xc3, 0x18, 0xf2, 0x54, 0x6a, 0x1e, 0x13, 0xe4, - 0x9e, 0x0a, 0x27, 0xac, 0x96, 0x32, 0x42, 0x07, 0xe6, 0x82, 0x33, 0x09, - 0x6a, 0xde, 0x1b, 0xc4, 0xf5, 0x61, 0xca, 0x9f, 0x8c, 0xfa, 0x27, 0xe3, - 0xa1, 0x60, 0x32, 0xe4, 0xdf, 0x33, 0x37, 0xa3, 0xef, 0x2d, 0xaa, 0xae, - 0x7f, 0x6f, 0x3c, 0x3a, 0x8b, 0x3b, 0xcc, 0x54, 0x1c, 0xb3, 0x49, 0x99, - 0x7b, 0xc3, 0x91, 0xe0, 0x4c, 0xf8, 0xad, 0x21, 0xaa, 0x40, 0x6a, 0x2a, - 0x3d, 0xdc, 0xa1, 0x68, 0xdc, 0x16, 0xe1, 0x2b, 0xe1, 0x72, 0x16, 0xb1, - 0x8c, 0x7c, 0x31, 0x01, 0xe7, 0xde, 0x70, 0x1c, 0xf3, 0xee, 0xe6, 0x2e, - 0xd4, 0x1a, 0x92, 0x03, 0xdb, 0x87, 0xdc, 0xf8, 0xd0, 0xd7, 0x04, 0x9d, - 0x56, 0xf2, 0xd9, 0x9c, 0x9e, 0x99, 0xe1, 0x05, 0x4c, 0x50, 0x09, 0x67, - 0xd4, 0x8a, 0x2e, 0x0c, 0xb2, 0x69, 0x79, 0xba, 0xec, 0xe2, 0x4d, 0xbc, - 0x84, 0x0b, 0x63, 0xb1, 0x99, 0xf0, 0xa4, 0x74, 0xdb, 0x96, 0x15, 0x14, - 0x80, 0x7d, 0x91, 0x86, 0xc5, 0x76, 0xa6, 0x3d, 0xfc, 0x93, 0xad, 0x5c, - 0x1c, 0xb3, 0x53, 0x26, 0xd8, 0xd2, 0xb9, 0x93, 0x17, 0xa9, 0x01, 0x75, - 0x19, 0xb4, 0xc6, 0x52, 0x92, 0x66, 0x2d, 0xbc, 0x0e, 0x51, 0x96, 0x2c, - 0x93, 0x66, 0x92, 0x97, 0x4a, 0xea, 0x85, 0x4b, 0xe5, 0x13, 0xe4, 0x42, - 0x5a, 0x2e, 0x7f, 0x1d, 0x12, 0x9b, 0xe7, 0x66, 0x39, 0x2c, 0xc7, 0x05, - 0x13, 0x9e, 0x5f, 0x4d, 0xe0, 0xa2, 0xd3, 0x0c, 0x51, 0xd8, 0xad, 0x24, - 0x53, 0xb2, 0x05, 0x6e, 0x97, 0x2a, 0x91, 0xe0, 0xd3, 0xf5, 0xa2, 0x89, - 0xda, 0x16, 0x9c, 0x65, 0xe6, 0xf0, 0x40, 0x82, 0x6a, 0x2e, 0x96, 0x91, - 0x21, 0xd0, 0x45, 0x82, 0xb5, 0x17, 0x0b, 0xaa, 0xc0, 0xe7, 0x22, 0xc9, - 0x65, 0x90, 0xe4, 0xe2, 0x85, 0x6a, 0x62, 0x70, 0x4b, 0x75, 0x91, 0xaa, - 0xc3, 0x7b, 0x04, 0xc3, 0xd1, 0x2a, 0x73, 0x0b, 0xd2, 0x00, 0x78, 0xf5, - 0x65, 0x26, 0xd7, 0xca, 0xcc, 0xf1, 0xc1, 0x4d, 0x1e, 0x9d, 0x65, 0x1f, - 0xca, 0xd5, 0x06, 0xe4, 0x5a, 0x28, 0x5b, 0x91, 0xa2, 0xf1, 0x68, 0x2c, - 0x14, 0x4f, 0x86, 0xd1, 0x4f, 0x3e, 0xb2, 0x63, 0xa1, 0xd9, 0x68, 0x32, - 0xa4, 0x67, 0x9c, 0xeb, 0x8e, 0x4b, 0x3f, 0xad, 0x37, 0xba, 0xec, 0x32, - 0x70, 0x38, 0x16, 0xa2, 0xc2, 0x69, 0x19, 0xb0, 0xea, 0xf9, 0xc7, 0xfd, - 0x30, 0xb2, 0x2f, 0x34, 0x45, 0xb9, 0x8a, 0xab, 0x83, 0x62, 0x72, 0x4d, - 0x07, 0x13, 0xdb, 0xd8, 0x88, 0x32, 0x91, 0x98, 0xee, 0x8f, 0x4e, 0x41, - 0xd5, 0x70, 0xa2, 0x5f, 0x6d, 0x36, 0x88, 0xbb, 0xc2, 0x89, 0xc1, 0xd9, - 0x58, 0xf2, 0x30, 0x27, 0xe4, 0xfc, 0x71, 0x71, 0xfa, 0x36, 0x9e, 0x19, - 0xd6, 0x67, 0x1a, 0x65, 0x72, 0xcc, 0xb3, 0x39, 0x8a, 0x4d, 0x91, 0xb1, - 0x3f, 0x74, 0x18, 0xbe, 0x9f, 0x5c, 0xb3, 0xda, 0x6c, 0x4d, 0x76, 0x8e, - 0xe4, 0x9e, 0x4d, 0xcd, 0x0f, 0x79, 0x67, 0x2f, 0xb2, 0xed, 0xec, 0x59, - 0x9b, 0x03, 0x32, 0x23, 0x3c, 0x4d, 0x66, 0x84, 0x15, 0xf3, 0x45, 0x23, - 0x7d, 0xc1, 0xe4, 0xe4, 0x74, 0xfa, 0x22, 0x97, 0xa0, 0x22, 0x18, 0xff, - 0xbc, 0xfb, 0xae, 0x35, 0xba, 0xc2, 0x85, 0x05, 0x6c, 0x63, 0xb4, 0x64, - 0x21, 0xf7, 0xca, 0x38, 0xf4, 0x96, 0xad, 0xa8, 0x81, 0x62, 0x33, 0xf1, - 0xe6, 0x08, 0xa9, 0x66, 0xc8, 0x13, 0x8d, 0xa4, 0x6f, 0xc7, 0xb2, 0x05, - 0xaf, 0x9d, 0xa3, 0x6a, 0xe7, 0x46, 0xf5, 0x7d, 0x06, 0x56, 0x80, 0x9e, - 0xf3, 0xa2, 0xf3, 0xae, 0x37, 0xdc, 0xa7, 0x3d, 0x3f, 0x10, 0x9a, 0x09, - 0x1e, 0x06, 0x3b, 0xdf, 0x62, 0xf3, 0x2a, 0x1e, 0xb4, 0xcb, 0xa9, 0xdd, - 0x69, 0x0d, 0xc4, 0x15, 0x8d, 0x0c, 0xcd, 0xcc, 0x25, 0xa6, 0x29, 0x27, - 0x1a, 0xd9, 0x9a, 0x9c, 0xb3, 0xd8, 0xd0, 0x8c, 0xf5, 0x51, 0x26, 0x30, - 0x96, 0x48, 0x84, 0xa9, 0x98, 0x39, 0x33, 0x61, 0xde, 0x47, 0x52, 0xaf, - 0xfe, 0xe8, 0x6c, 0x0c, 0x7e, 0x10, 0xb2, 0xa8, 0x29, 0x4f, 0x38, 0xe9, - 0x27, 0xad, 0x9c, 0x9a, 0x41, 0xca, 0x40, 0x2e, 0x14, 0x91, 0xf3, 0xa5, - 0xed, 0x26, 0x31, 0xc0, 0x3e, 0x16, 0xd7, 0x24, 0xc8, 0x16, 0xc0, 0xde, - 0x22, 0x0b, 0x9c, 0x07, 0xb9, 0x99, 0xa9, 0xd3, 0xb9, 0x9c, 0x4e, 0x5b, - 0x41, 0x11, 0x67, 0xe7, 0x5d, 0x32, 0xae, 0x0c, 0x27, 0xa7, 0x61, 0xc7, - 0xc5, 0x56, 0x41, 0xfa, 0x2a, 0xa1, 0x4b, 0x7c, 0x56, 0x89, 0x8d, 0x97, - 0xc7, 0x3c, 0xdb, 0x63, 0x9d, 0x2c, 0xce, 0xab, 0x6d, 0x20, 0x93, 0xec, - 0x8c, 0xe1, 0x13, 0xa2, 0x37, 0xb0, 0xbb, 0x2a, 0x88, 0x61, 0x93, 0x2d, - 0xd4, 0xb1, 0x64, 0x11, 0xe6, 0x78, 0x32, 0x14, 0x0b, 0xdc, 0x10, 0xa5, - 0xa2, 0x79, 0x65, 0xe9, 0xcd, 0x4a, 0x59, 0x31, 0x79, 0xac, 0x4f, 0x85, - 0x0e, 0x51, 0x66, 0xcc, 0x0a, 0x65, 0x1c, 0x6c, 0xea, 0x4b, 0xe3, 0xa1, - 0x7d, 0x7c, 0x4d, 0x8d, 0xcf, 0xbf, 0xeb, 0x52, 0x46, 0x5c, 0x2e, 0x1e, - 0xb9, 0x15, 0x95, 0xca, 0x2e, 0x8b, 0xe3, 0x60, 0x0a, 0x25, 0x92, 0x69, - 0x8b, 0xda, 0x11, 0x0f, 0x47, 0xb1, 0x22, 0x87, 0xc9, 0x11, 0x9f, 0x8b, - 0x90, 0xcb, 0x7a, 0x22, 0x97, 0x99, 0x98, 0x9c, 0x0e, 0x4d, 0xe1, 0xac, - 0xa2, 0x8c, 0x44, 0x08, 0xa7, 0xda, 0x14, 0x99, 0x09, 0x5e, 0x8a, 0x12, - 0xfe, 0x54, 0x8f, 0xc1, 0xa6, 0x83, 0x53, 0xfe, 0xe1, 0xed, 0xfe, 0x90, - 0x15, 0x81, 0xa3, 0x4e, 0x48, 0x1d, 0x59, 0x94, 0x9f, 0x08, 0xa5, 0x6e, - 0x46, 0x72, 0x97, 0x67, 0x83, 0xc1, 0x0b, 0xbb, 0x95, 0xb7, 0x6e, 0x1e, - 0x67, 0xf4, 0x81, 0x8f, 0x1b, 0x29, 0x5a, 0xe6, 0x03, 0xcf, 0x99, 0x48, - 0x06, 0x79, 0x3a, 0x25, 0x61, 0x59, 0xca, 0x55, 0xc9, 0x64, 0x34, 0x26, - 0xb3, 0x66, 0x02, 0x29, 0x74, 0x62, 0xe5, 0x33, 0x92, 0xd3, 0x61, 0xc4, - 0x22, 0xe4, 0x4a, 0x46, 0x65, 0x6c, 0x4c, 0x99, 0xc9, 0xa8, 0x3e, 0xe5, - 0x96, 0xcc, 0x45, 0x16, 0x9b, 0xf8, 0x65, 0x0b, 0xd8, 0xb6, 0xe9, 0x2d, - 0x9e, 0x8b, 0xbc, 0xce, 0x34, 0x3a, 0x0f, 0x06, 0x21, 0x4f, 0x2e, 0x49, - 0xb6, 0xef, 0xa5, 0xf8, 0x4d, 0x37, 0x0d, 0x74, 0xbe, 0xad, 0x92, 0xcb, - 0x30, 0xdc, 0xca, 0xf5, 0x95, 0x58, 0x92, 0xca, 0xc6, 0x4a, 0xdc, 0xaa, - 0x62, 0xe1, 0x19, 0x79, 0xe2, 0x35, 0xcd, 0x62, 0x94, 0x28, 0x88, 0x87, - 0x10, 0x65, 0x24, 0x42, 0x28, 0x84, 0xd7, 0x6a, 0xc2, 0x7c, 0x4e, 0xee, - 0x4f, 0xcc, 0xcd, 0x26, 0x2a, 0xd7, 0xef, 0x45, 0x68, 0x10, 0x6a, 0xac, - 0x9c, 0x0d, 0x47, 0x9a, 0x82, 0xb1, 0x70, 0xe5, 0xfa, 0xb6, 0xb5, 0x8d, - 0x95, 0xb0, 0xe9, 0x04, 0xea, 0xa2, 0x5a, 0x7b, 0x73, 0x7b, 0xf3, 0xea, - 0xd6, 0x26, 0x84, 0x07, 0x0d, 0xc1, 0x68, 0x22, 0xd6, 0x51, 0xf9, 0x76, - 0x32, 0x9a, 0xc4, 0x29, 0xa3, 0xd4, 0x2c, 0x12, 0x45, 0xd7, 0x15, 0x35, - 0x16, 0x65, 0xaa, 0xac, 0xa3, 0xe8, 0x2d, 0x45, 0xe3, 0x45, 0x2e, 0xa3, - 0x1e, 0x99, 0xe2, 0x3a, 0xa3, 0xc1, 0xb8, 0x5d, 0x98, 0x59, 0xdf, 0x17, - 0x45, 0x6e, 0x99, 0xac, 0xce, 0xca, 0x40, 0xba, 0x48, 0x95, 0xd6, 0xa8, - 0x52, 0x03, 0x9c, 0xdc, 0x74, 0x72, 0xaf, 0x6a, 0xc7, 0x85, 0x66, 0xc7, - 0x8b, 0x1c, 0x45, 0x3d, 0x45, 0xad, 0x45, 0x55, 0x68, 0x4f, 0x32, 0x9d, - 0x45, 0x46, 0x8a, 0xd1, 0xcc, 0x0c, 0x51, 0xbc, 0xc6, 0xd2, 0xc2, 0x28, - 0xba, 0xaa, 0x68, 0xb3, 0x25, 0x68, 0x16, 0x4d, 0xa0, 0x76, 0x95, 0x3d, - 0x7b, 0x45, 0xd1, 0x50, 0xba, 0x99, 0xab, 0x74, 0x33, 0x59, 0x16, 0x63, - 0x02, 0x75, 0x77, 0x15, 0x0d, 0x80, 0x91, 0xd2, 0x78, 0xca, 0x68, 0xe4, - 0x32, 0xa3, 0x78, 0x55, 0xf1, 0x4a, 0xc5, 0x75, 0x83, 0xbb, 0x4f, 0x71, - 0x33, 0x8a, 0x6b, 0x8a, 0x2b, 0x8a, 0x6b, 0x8b, 0xab, 0x8a, 0x2b, 0x8b, - 0xab, 0xd3, 0x75, 0x96, 0xa4, 0x93, 0x4b, 0x85, 0x43, 0x64, 0x99, 0x25, - 0x0e, 0xc3, 0x30, 0x84, 0xb1, 0xe6, 0xc8, 0x11, 0xf3, 0xd1, 0x95, 0x1d, - 0xe2, 0xbe, 0x6a, 0x21, 0x9e, 0x03, 0xce, 0x03, 0xcf, 0xaf, 0x14, 0xe2, - 0xe4, 0x2a, 0x21, 0x1e, 0x04, 0xbe, 0x5d, 0x25, 0xc4, 0x7d, 0x35, 0x42, - 0xbc, 0xc0, 0x8f, 0xdf, 0x33, 0xae, 0xb8, 0x25, 0x93, 0xc4, 0x22, 0x20, - 0x43, 0xb8, 0xf3, 0x0d, 0x71, 0xd2, 0x7f, 0xe5, 0x2d, 0x47, 0xcc, 0x17, - 0x9a, 0xaf, 0x12, 0x47, 0x5a, 0x84, 0xb8, 0x13, 0x78, 0x14, 0x78, 0x1a, - 0x38, 0x07, 0xdc, 0xde, 0x2a, 0xc4, 0x3d, 0xc0, 0x63, 0xc0, 0x33, 0xc0, - 0xf3, 0xad, 0x64, 0x0a, 0xa7, 0x17, 0x4a, 0x08, 0xae, 0xba, 0x07, 0x55, - 0x1f, 0x6c, 0x9f, 0x14, 0x47, 0xda, 0xd0, 0xf3, 0x6a, 0x21, 0x5e, 0x82, - 0xc8, 0x6b, 0x48, 0x7f, 0xbd, 0x9d, 0x5c, 0x2e, 0xdf, 0x12, 0x25, 0xa6, - 0xff, 0x4f, 0x43, 0xf6, 0x99, 0x5e, 0xc3, 0xb8, 0xb3, 0x43, 0x18, 0xdf, - 0x5e, 0xef, 0x30, 0x4e, 0xae, 0x01, 0xdd, 0xe8, 0x30, 0x4e, 0xaf, 0x75, - 0x1b, 0xe7, 0x3b, 0xc3, 0xe6, 0x4b, 0x7d, 0x0e, 0xf1, 0x74, 0x0f, 0xba, - 0xec, 0x76, 0x88, 0xdb, 0x41, 0xef, 0xeb, 0x31, 0xc4, 0xf9, 0x6e, 0x21, - 0x9e, 0xea, 0x82, 0x3a, 0x83, 0x42, 0xbc, 0x02, 0x3c, 0xbe, 0x19, 0x2a, - 0x6c, 0x41, 0x7e, 0x14, 0xe8, 0x14, 0xe2, 0x4c, 0xa7, 0x21, 0xee, 0xbc, - 0x0c, 0x23, 0x46, 0xfe, 0x1c, 0x70, 0x62, 0x2b, 0x39, 0x04, 0x16, 0x92, - 0xff, 0xdd, 0x22, 0xd0, 0xe1, 0xa3, 0x3b, 0x6e, 0xc5, 0xfc, 0x6c, 0x43, - 0xad, 0xed, 0x06, 0xb9, 0xa8, 0xdc, 0x2d, 0xdc, 0xb7, 0x8b, 0x63, 0x47, - 0xcc, 0x9f, 0x8c, 0x71, 0xe9, 0xb9, 0x31, 0x71, 0xcc, 0x7f, 0x6c, 0x5c, - 0x64, 0x9d, 0xb8, 0x42, 0x64, 0x9d, 0xbf, 0x42, 0x64, 0x7e, 0x7d, 0x87, - 0x78, 0xa7, 0x41, 0xe4, 0x34, 0xb2, 0xac, 0x6f, 0x08, 0x0b, 0xda, 0x08, - 0xc1, 0xa7, 0xeb, 0xcc, 0x5b, 0x0d, 0xba, 0x5c, 0xbc, 0x52, 0x27, 0xcc, - 0x67, 0xeb, 0x31, 0x25, 0x75, 0x86, 0xb8, 0xbd, 0x4e, 0x88, 0xb3, 0x48, - 0x9f, 0x07, 0x1e, 0x6c, 0x80, 0x9e, 0xc0, 0xb9, 0x06, 0xf4, 0xef, 0xca, - 0x91, 0xf5, 0x86, 0x79, 0x5a, 0x1b, 0xb7, 0x88, 0xdb, 0x9b, 0x84, 0xf9, - 0x93, 0x26, 0xe8, 0xdb, 0x88, 0xa9, 0x05, 0x5e, 0x43, 0xfa, 0xa9, 0x66, - 0xcc, 0x51, 0xb3, 0x28, 0xb2, 0xde, 0xc9, 0xf0, 0x9b, 0x8c, 0x07, 0xfa, - 0x15, 0x7d, 0xb2, 0x5f, 0xbd, 0xff, 0xf8, 0x02, 0xe8, 0x97, 0x6d, 0xe9, - 0xef, 0xe8, 0xf4, 0x4f, 0x41, 0xcf, 0xda, 0xd2, 0xbf, 0xd7, 0xf5, 0x5e, - 0xd5, 0x34, 0x7f, 0x40, 0x51, 0xbf, 0xa6, 0x75, 0x9a, 0x76, 0x6a, 0xba, - 0x49, 0xd3, 0x5d, 0x9a, 0xee, 0x1b, 0x50, 0xef, 0x57, 0xb8, 0x8f, 0x43, - 0x48, 0xdf, 0x36, 0x90, 0xee, 0xf3, 0x3d, 0x5a, 0xe6, 0x03, 0x36, 0xde, - 0x63, 0xb6, 0xf4, 0xe7, 0x90, 0x7e, 0x7c, 0x28, 0xfd, 0xfe, 0xc9, 0x7a, - 0x2f, 0xf5, 0xe8, 0x90, 0xfa, 0xdb, 0x11, 0xf7, 0x81, 0x3e, 0x6d, 0xfd, - 0x00, 0x5b, 0xff, 0x7b, 0x6e, 0x41, 0xfe, 0xf9, 0x05, 0x79, 0xff, 0x26, - 0xf5, 0x8e, 0xc7, 0x9a, 0x1b, 0x7e, 0xbf, 0xc4, 0x7f, 0x50, 0x42, 0xfe, - 0xad, 0x84, 0x4d, 0xea, 0xfd, 0x52, 0xf1, 0x26, 0xf5, 0x1b, 0xee, 0x4c, - 0xd0, 0x30, 0xe8, 0xb9, 0x21, 0xf5, 0x7b, 0xff, 0x17, 0x86, 0xd4, 0xef, - 0xfb, 0xcf, 0x0f, 0xa9, 0xdf, 0x99, 0xbf, 0x04, 0xda, 0xba, 0x69, 0x7e, - 0xfb, 0xdd, 0x0b, 0xf2, 0x03, 0x9b, 0xd2, 0x6b, 0xc0, 0xff, 0x7a, 0x74, - 0x7b, 0x3b, 0x16, 0xf0, 0x47, 0x34, 0xdf, 0x4b, 0xf3, 0xf9, 0x4c, 0xad, - 0xbf, 0x13, 0x64, 0x50, 0xfa, 0x6f, 0x05, 0xf1, 0x9c, 0x5a, 0x7f, 0x2f, - 0x88, 0xc7, 0xc2, 0xdf, 0x51, 0xe0, 0x9f, 0x44, 0xf0, 0xfc, 0x58, 0x7f, - 0x37, 0x88, 0xdf, 0xbe, 0x59, 0x7f, 0x3b, 0x48, 0xf8, 0xd5, 0xdf, 0xc5, - 0xe0, 0xbf, 0x1f, 0xe4, 0xf0, 0xab, 0xef, 0x84, 0xf3, 0xef, 0xdb, 0x84, - 0x47, 0x7d, 0x5f, 0x95, 0x7f, 0x87, 0x67, 0xf8, 0x55, 0x5f, 0xfc, 0xf7, - 0x85, 0x4c, 0xbf, 0x9a, 0x1b, 0xb6, 0x1b, 0xfe, 0x11, 0x1c, 0xb7, 0xc3, - 0xbf, 0x0f, 0x74, 0xfa, 0x95, 0x4e, 0xfc, 0x3b, 0x41, 0x87, 0x47, 0xbd, - 0xa7, 0x3b, 0x8d, 0x74, 0x86, 0x96, 0xe1, 0xdf, 0x11, 0xf2, 0xcb, 0x4a, - 0x96, 0xe1, 0xbf, 0x71, 0xf4, 0x7f, 0x27, 0xec, 0x58, 0xd2, 0x1c, 0x49, - 0x00, 0x00 +constexpr unsigned char javaMidiByteCode[] { + 0x1f, 0x8b, 0x08, 0x08, 0x90, 0xda, 0xee, 0x67, 0x00, 0x03, 0x63, 0x6c, + 0x61, 0x73, 0x73, 0x65, 0x73, 0x2e, 0x64, 0x65, 0x78, 0x00, 0xad, 0x7c, + 0x09, 0x7c, 0x94, 0xd5, 0xd5, 0xf7, 0xb9, 0xcf, 0xcc, 0x64, 0x87, 0x4c, + 0x26, 0x21, 0x09, 0x01, 0x92, 0xc9, 0x24, 0x40, 0x02, 0x64, 0x4f, 0x20, + 0x21, 0x01, 0xb3, 0x22, 0x81, 0x00, 0x31, 0x99, 0x44, 0x21, 0x52, 0x1c, + 0x92, 0x81, 0x0c, 0x4c, 0x66, 0xc6, 0x99, 0x09, 0x8b, 0xb5, 0x15, 0x97, + 0x56, 0xac, 0x4b, 0xdd, 0x5e, 0x97, 0x56, 0x2d, 0x56, 0x45, 0xdc, 0x5a, + 0x5a, 0x97, 0x5a, 0xeb, 0x82, 0xc5, 0x56, 0xdb, 0xda, 0xd6, 0xf6, 0xed, + 0x6b, 0x37, 0xdf, 0x7e, 0xb5, 0xa5, 0xad, 0x76, 0x7b, 0xb5, 0xf5, 0x7d, + 0x5f, 0xfb, 0x75, 0xf1, 0xfb, 0x9f, 0x7b, 0xef, 0x33, 0xf3, 0x64, 0x01, + 0xda, 0xdf, 0xef, 0x8b, 0xfe, 0xe7, 0x9c, 0x7b, 0xee, 0xb9, 0xfb, 0xb9, + 0xe7, 0x9e, 0xfb, 0xcc, 0xc3, 0x8c, 0xf9, 0x0f, 0x64, 0xd4, 0x36, 0xac, + 0xa2, 0x3b, 0x1e, 0xf0, 0x6c, 0xbe, 0xfe, 0xe0, 0xfd, 0x83, 0x8d, 0xef, + 0x45, 0xdf, 0x7b, 0xe8, 0xca, 0xbd, 0xfb, 0x9e, 0xef, 0x2e, 0x7a, 0xb7, + 0xec, 0xce, 0xde, 0xcf, 0xbd, 0x7f, 0x01, 0x51, 0x84, 0x88, 0x0e, 0x0c, + 0x37, 0xba, 0x48, 0xff, 0x55, 0x40, 0xb6, 0x51, 0x28, 0xf9, 0x30, 0x70, + 0x97, 0x83, 0xc8, 0x07, 0xfa, 0x5e, 0x0a, 0x51, 0x1b, 0xe8, 0xa1, 0x4c, + 0xa2, 0x57, 0x40, 0x2f, 0x9a, 0x43, 0x94, 0x03, 0x7a, 0x24, 0x97, 0xa8, + 0xb9, 0x8b, 0xe8, 0xc4, 0x3c, 0xa2, 0xf8, 0x4a, 0xa2, 0x7d, 0xc0, 0x41, + 0xe0, 0x23, 0xc0, 0xe5, 0xc0, 0x55, 0xc0, 0xc3, 0xc0, 0x13, 0xc0, 0xb3, + 0xc0, 0x09, 0xe0, 0x6b, 0xc0, 0xb7, 0x80, 0xef, 0x01, 0xbf, 0x01, 0x96, + 0xae, 0x22, 0x0a, 0x02, 0x87, 0x80, 0x5b, 0x81, 0xa3, 0xc0, 0x43, 0xc0, + 0x63, 0xc0, 0x17, 0x80, 0x27, 0x81, 0x67, 0x80, 0x9f, 0x02, 0x46, 0x33, + 0x51, 0x31, 0x50, 0x07, 0x34, 0x02, 0xab, 0x81, 0xcd, 0x80, 0x0f, 0x98, + 0x04, 0xae, 0x04, 0xee, 0x04, 0xee, 0x02, 0x8e, 0x00, 0x0f, 0x00, 0x8f, + 0x00, 0x5f, 0x00, 0x9e, 0x02, 0x9e, 0x01, 0x5e, 0x00, 0x5e, 0x02, 0xbe, + 0x09, 0xbc, 0x03, 0xcc, 0x6b, 0xc1, 0x18, 0x80, 0x51, 0xe0, 0x1a, 0xe0, + 0x09, 0xe0, 0xc7, 0x80, 0x7d, 0x35, 0xfa, 0x06, 0x6c, 0x00, 0xf6, 0x03, + 0x77, 0x01, 0x5f, 0x02, 0x7e, 0x04, 0xa4, 0xb4, 0x12, 0x2d, 0x07, 0xb6, + 0x00, 0x87, 0x80, 0x2f, 0x03, 0x3f, 0x05, 0x78, 0x92, 0xca, 0x80, 0x73, + 0x00, 0x2f, 0xb0, 0x07, 0xb8, 0x12, 0xb8, 0x1d, 0xf8, 0x36, 0x20, 0xd6, + 0xa0, 0xdf, 0xc0, 0x38, 0xf0, 0x69, 0xe0, 0xbb, 0xc0, 0xdf, 0x81, 0x0d, + 0x6b, 0xa1, 0x03, 0xfc, 0x14, 0xa8, 0x3c, 0x87, 0x28, 0x0c, 0x7c, 0x16, + 0x78, 0x1d, 0xa0, 0x76, 0xa2, 0x16, 0x60, 0x1b, 0xf0, 0x51, 0xe0, 0x1e, + 0xe0, 0x45, 0xe0, 0x37, 0x40, 0x4a, 0x07, 0x91, 0x07, 0x68, 0x01, 0xfa, + 0x80, 0x51, 0xe0, 0x1a, 0xe0, 0x1e, 0xe0, 0x04, 0xf0, 0x03, 0xe0, 0x2d, + 0xc0, 0xd6, 0x49, 0x54, 0x04, 0xb4, 0x00, 0x17, 0x00, 0x61, 0xe0, 0xe3, + 0xc0, 0xa7, 0x80, 0x2f, 0x02, 0x2f, 0x03, 0x6f, 0x00, 0xef, 0x02, 0x29, + 0x58, 0xcf, 0x85, 0x40, 0x35, 0xd0, 0x0e, 0x0c, 0x01, 0xbb, 0x80, 0xfd, + 0xc0, 0x27, 0x80, 0x7b, 0x81, 0xaf, 0x02, 0xbf, 0x05, 0xb2, 0xba, 0x31, + 0x0f, 0xc0, 0x06, 0x60, 0x3b, 0x70, 0x04, 0x78, 0x1d, 0xf8, 0x2d, 0xf0, + 0x0f, 0x20, 0xad, 0x07, 0x76, 0x02, 0x34, 0x03, 0x9b, 0x81, 0xed, 0xc0, + 0x3e, 0xe0, 0x20, 0xf0, 0x11, 0xe0, 0x0a, 0xe0, 0x30, 0x70, 0x1d, 0x70, + 0x2b, 0x70, 0x07, 0x70, 0x37, 0x70, 0x1f, 0xf0, 0x30, 0x70, 0x1c, 0x78, + 0x12, 0x78, 0x06, 0x78, 0x11, 0xf8, 0x06, 0xf0, 0x5d, 0xe0, 0x75, 0xe0, + 0x27, 0xc0, 0x7f, 0x02, 0x6f, 0x02, 0xbf, 0x02, 0xde, 0x06, 0xe6, 0xac, + 0xc3, 0x12, 0x00, 0x3b, 0x81, 0x03, 0xc0, 0xf5, 0xc0, 0x9d, 0xc0, 0x03, + 0xc0, 0x17, 0x80, 0xe7, 0x81, 0x93, 0xc0, 0x6b, 0xc0, 0x1b, 0xc0, 0x29, + 0xe0, 0x77, 0xc0, 0x9f, 0x81, 0x7f, 0x00, 0x45, 0xe7, 0x12, 0xad, 0x00, + 0x3a, 0x01, 0x2f, 0xb0, 0x0b, 0x98, 0x00, 0xf6, 0x03, 0x97, 0x01, 0x1f, + 0x07, 0xae, 0x07, 0x6e, 0x03, 0xee, 0x03, 0x1e, 0x03, 0x9e, 0x01, 0x9e, + 0x03, 0xbe, 0x03, 0xbc, 0x0e, 0xfc, 0x06, 0x78, 0x17, 0xf8, 0x3b, 0x60, + 0xac, 0x27, 0xca, 0x00, 0xf2, 0x80, 0xf9, 0x40, 0x31, 0x50, 0x06, 0x0c, + 0x02, 0xdb, 0x81, 0x43, 0xc0, 0xbd, 0xc0, 0x63, 0xc0, 0xe3, 0xc0, 0xd3, + 0xc0, 0x4b, 0xc0, 0x6b, 0xc0, 0xef, 0x81, 0xf4, 0x5e, 0x22, 0x37, 0xd0, + 0x04, 0xf4, 0x02, 0xa3, 0xc0, 0x25, 0xc0, 0x55, 0xc0, 0xed, 0xc0, 0xe3, + 0xc0, 0xcb, 0xc0, 0xf7, 0x81, 0x9f, 0x01, 0xff, 0x03, 0x64, 0x6f, 0x80, + 0x0d, 0x01, 0xf5, 0xc0, 0x76, 0x60, 0x0c, 0xd8, 0x0b, 0xc4, 0x81, 0xab, + 0x81, 0xcf, 0x02, 0x8f, 0x00, 0x5f, 0x06, 0xbe, 0x0d, 0xbc, 0x01, 0xbc, + 0x0d, 0xfc, 0x19, 0x30, 0x36, 0x12, 0xe5, 0x02, 0xc5, 0x40, 0x2d, 0x70, + 0x2e, 0xd0, 0x0f, 0x8c, 0x02, 0x07, 0x80, 0xab, 0x81, 0x1b, 0x81, 0xdb, + 0x80, 0x23, 0xc0, 0x51, 0xe0, 0x61, 0xe0, 0x38, 0xf0, 0x0c, 0xf0, 0x12, + 0xf0, 0x3d, 0xe0, 0x27, 0xc0, 0x29, 0xe0, 0xf7, 0xc0, 0xda, 0x3e, 0xa2, + 0x9b, 0x80, 0xcf, 0x00, 0xc7, 0x80, 0xa7, 0x80, 0x67, 0x81, 0xef, 0x02, + 0x6f, 0x00, 0xbf, 0x00, 0xde, 0x06, 0x8c, 0x4d, 0xb0, 0x17, 0x60, 0x21, + 0xd0, 0x04, 0x74, 0x03, 0x5e, 0x60, 0x2f, 0x70, 0x29, 0xf0, 0x31, 0xe0, + 0x16, 0xe0, 0x5e, 0xe0, 0x21, 0xe0, 0x79, 0xe0, 0x9b, 0xc0, 0x8f, 0x81, + 0x3f, 0x01, 0x59, 0x9b, 0x51, 0x16, 0x58, 0x0a, 0xd4, 0x03, 0x2d, 0xc0, + 0x66, 0x60, 0x0f, 0x70, 0x25, 0x70, 0x03, 0x70, 0x3b, 0x70, 0x17, 0x70, + 0x14, 0x78, 0x1e, 0xf8, 0x3a, 0xf0, 0x7d, 0xe0, 0x0f, 0xc0, 0x9f, 0x80, + 0xbf, 0x02, 0x8e, 0x2d, 0x44, 0x99, 0xc0, 0x52, 0xa0, 0x09, 0xe8, 0x06, + 0x7a, 0x81, 0x6d, 0xc0, 0x6e, 0x60, 0x3f, 0x70, 0x29, 0x70, 0x25, 0x70, + 0x03, 0x70, 0x37, 0x70, 0x1f, 0xf0, 0x18, 0xf0, 0x24, 0xf0, 0x02, 0xf0, + 0x4d, 0xe0, 0x67, 0xc0, 0x29, 0xe0, 0xaf, 0x40, 0x7e, 0x3f, 0xe6, 0x13, + 0x58, 0x05, 0xb4, 0x03, 0xec, 0x70, 0x17, 0x00, 0x2b, 0x80, 0x2a, 0xa0, + 0x1a, 0xa8, 0x01, 0x6a, 0x81, 0x3a, 0xa0, 0x1e, 0x68, 0x00, 0x1a, 0x81, + 0x26, 0x00, 0x6e, 0x92, 0xe0, 0xfe, 0x08, 0xee, 0x8a, 0xe0, 0x9a, 0x08, + 0x2e, 0x88, 0xb4, 0xbb, 0x21, 0xb8, 0x0f, 0x82, 0xdb, 0x20, 0xed, 0x2a, + 0x08, 0xdb, 0x9d, 0xb0, 0x8d, 0x09, 0xdb, 0x93, 0xb0, 0x05, 0x09, 0xdb, + 0x82, 0x60, 0xda, 0x04, 0x73, 0x24, 0x98, 0x14, 0xc1, 0x44, 0x08, 0x4b, + 0x4d, 0x58, 0x1a, 0xc2, 0xb4, 0x13, 0xa6, 0x8e, 0x30, 0x6c, 0x42, 0x37, + 0x09, 0x5d, 0xa3, 0xf3, 0x80, 0x01, 0x60, 0x10, 0xf0, 0x02, 0x43, 0xfa, + 0x2c, 0x38, 0x1f, 0xc0, 0xf1, 0x40, 0x5b, 0x81, 0x6d, 0xc0, 0x08, 0x70, + 0x21, 0xb0, 0x1d, 0xf8, 0x10, 0xb0, 0x83, 0xcf, 0x05, 0x52, 0xe7, 0xc5, + 0x4e, 0x60, 0x14, 0x18, 0x03, 0xfc, 0xc0, 0x2e, 0x60, 0x37, 0x30, 0x0e, + 0xec, 0x01, 0x82, 0xc0, 0x04, 0x10, 0x22, 0x75, 0xd6, 0x44, 0x81, 0x18, + 0x30, 0x09, 0xec, 0xe3, 0x33, 0x09, 0x38, 0x08, 0x5c, 0x02, 0x7c, 0x18, + 0xb8, 0x14, 0xf8, 0x08, 0xf0, 0x09, 0x3e, 0x93, 0x80, 0xcf, 0xf0, 0xd9, + 0x03, 0xdc, 0x0b, 0x7c, 0x16, 0xb8, 0x0f, 0xb8, 0x9f, 0xd4, 0xbc, 0x9a, + 0x7f, 0x79, 0x9a, 0x1e, 0xc1, 0x24, 0xce, 0xd3, 0xfc, 0x31, 0xf0, 0xf9, + 0x9a, 0x3f, 0x6e, 0x91, 0x3f, 0x0d, 0xde, 0x03, 0x6a, 0xe8, 0x74, 0x85, + 0xe6, 0x4f, 0x68, 0xb9, 0xcd, 0x22, 0x67, 0xfe, 0x95, 0xc6, 0x24, 0x7f, + 0x4c, 0xeb, 0xd8, 0xb5, 0xce, 0x72, 0xcd, 0xbf, 0xa6, 0xe5, 0xa9, 0x96, + 0xb2, 0xe9, 0xc0, 0x1b, 0x5a, 0x9e, 0xa1, 0xe5, 0x65, 0x40, 0x96, 0xee, + 0x27, 0xcb, 0xe7, 0x68, 0x39, 0xf3, 0x73, 0x2d, 0x7c, 0x8e, 0x45, 0xdf, + 0xa5, 0xf5, 0x99, 0xcf, 0xb7, 0x94, 0x9d, 0x6f, 0x69, 0x6b, 0x81, 0xee, + 0x1b, 0xf3, 0x8b, 0xf4, 0x58, 0xca, 0x75, 0xf9, 0x53, 0xe0, 0x17, 0x6b, + 0xfe, 0xf7, 0xe0, 0x97, 0x68, 0xfe, 0x7d, 0xf0, 0x4b, 0x35, 0x6f, 0x6f, + 0x4a, 0xf2, 0x59, 0x4d, 0x8a, 0x2e, 0xd1, 0x6d, 0x55, 0x68, 0xde, 0xac, + 0xbf, 0xc2, 0xc2, 0x57, 0x5a, 0xe6, 0x6d, 0xb9, 0xa5, 0xff, 0x4d, 0x96, + 0xfe, 0x37, 0x5b, 0xfa, 0xbc, 0x5a, 0xcb, 0x97, 0x6a, 0x3e, 0xaf, 0x49, + 0xd5, 0xc3, 0xfc, 0x42, 0xf0, 0xcb, 0x34, 0x5f, 0x6e, 0x91, 0x9b, 0x73, + 0xde, 0x6a, 0x19, 0x6f, 0xab, 0xa5, 0x0f, 0x6d, 0x16, 0x9d, 0x6e, 0x4b, + 0x1f, 0x7a, 0x2c, 0xfa, 0xcc, 0xaf, 0x68, 0x4a, 0xf2, 0xa6, 0x0d, 0x9c, + 0x6b, 0xe9, 0xe7, 0xb9, 0xba, 0x9f, 0x95, 0x9a, 0x6f, 0x6c, 0x52, 0x3a, + 0x1b, 0xb5, 0x0e, 0xdb, 0xe3, 0x66, 0xcd, 0x5f, 0xab, 0x79, 0xd6, 0xbf, + 0x4e, 0xf3, 0x6d, 0xd0, 0xbf, 0x5e, 0xf3, 0xdd, 0xe0, 0x6f, 0xd0, 0x7c, + 0x3f, 0xf8, 0x4f, 0x6a, 0xfe, 0x02, 0xf0, 0x37, 0x6a, 0x7e, 0x1c, 0xfc, + 0x9d, 0x9a, 0x8f, 0x80, 0xbf, 0x59, 0xf3, 0x97, 0x5a, 0x74, 0x0e, 0x5b, + 0x78, 0x9e, 0x67, 0x93, 0xbf, 0xd1, 0x22, 0x67, 0xfb, 0xbc, 0x49, 0xf3, + 0x77, 0x58, 0xea, 0x3c, 0x62, 0xd1, 0x79, 0x14, 0xfc, 0x6d, 0x9a, 0x7f, + 0xc2, 0x22, 0x7f, 0xd6, 0xc2, 0xbf, 0x64, 0xe1, 0x5f, 0xb5, 0xf0, 0x3f, + 0x68, 0x4a, 0xd6, 0xff, 0x06, 0xf8, 0x5b, 0x35, 0x7f, 0x0a, 0xfc, 0xa7, + 0x34, 0xff, 0x0e, 0xf8, 0xdb, 0x35, 0xff, 0x37, 0xf0, 0x77, 0x68, 0x3e, + 0x6d, 0x65, 0xb2, 0xdd, 0xc2, 0x95, 0xc9, 0xbe, 0xb9, 0x57, 0x26, 0xeb, + 0x5f, 0xb1, 0x32, 0x59, 0x7f, 0xa3, 0x45, 0xbe, 0xd0, 0xd2, 0x87, 0x63, + 0x96, 0xb1, 0xb7, 0x59, 0x74, 0xba, 0x2d, 0x75, 0xf6, 0x81, 0xff, 0x37, + 0x73, 0x9e, 0xc1, 0x7f, 0x5a, 0xf3, 0x17, 0x81, 0xbf, 0xc5, 0x9c, 0xf3, + 0x95, 0xca, 0xa7, 0x6c, 0xd1, 0xeb, 0x78, 0xb7, 0xe6, 0x79, 0x1d, 0xef, + 0xd1, 0x3c, 0xaf, 0x8b, 0xc9, 0x2f, 0xb4, 0xf0, 0xa6, 0x8d, 0x0d, 0x58, + 0x6c, 0x6c, 0x50, 0xf3, 0x85, 0xc0, 0xc7, 0x88, 0xf7, 0xde, 0x5c, 0x7a, + 0x44, 0xd2, 0x56, 0x3a, 0x2e, 0x69, 0x17, 0x7d, 0x51, 0xd2, 0x54, 0xaa, + 0x45, 0xac, 0x5d, 0x8c, 0x53, 0xe0, 0x29, 0x62, 0x0a, 0xbf, 0x8d, 0x74, + 0x09, 0x76, 0xfa, 0x3f, 0x88, 0xa9, 0x4a, 0xbb, 0x41, 0x5f, 0xe0, 0xf9, + 0x81, 0x87, 0x7f, 0x89, 0x54, 0xfa, 0xef, 0x92, 0xb6, 0x52, 0xba, 0x50, + 0x74, 0x8e, 0xa6, 0x15, 0x9a, 0x36, 0x0a, 0xa5, 0xbf, 0x5a, 0x97, 0x37, + 0xeb, 0xe9, 0x06, 0x2d, 0x85, 0xc7, 0x78, 0x98, 0x98, 0x22, 0x8e, 0x93, + 0x74, 0x0b, 0x09, 0x29, 0xaf, 0xa4, 0x32, 0x49, 0x97, 0xd1, 0x32, 0xa1, + 0xf2, 0xb9, 0x9c, 0x07, 0xde, 0xe6, 0x61, 0x3d, 0xb6, 0xaf, 0x4a, 0xaa, + 0xf4, 0x3c, 0x3a, 0xbf, 0x1c, 0xe9, 0x97, 0x49, 0xf9, 0x96, 0x57, 0x24, + 0x15, 0xf4, 0x0d, 0x62, 0xff, 0xa2, 0xe4, 0x8b, 0xb5, 0x7c, 0x09, 0x65, + 0xd3, 0x09, 0x49, 0xed, 0xf4, 0x5d, 0x49, 0x2b, 0xa9, 0x5d, 0x28, 0x3f, + 0xc2, 0xf5, 0x2c, 0xd5, 0xf5, 0x2e, 0xd5, 0xf2, 0x0a, 0xe4, 0xb0, 0x7e, + 0x85, 0x96, 0x57, 0x68, 0x79, 0xa5, 0x9e, 0xcf, 0x4a, 0x9c, 0x72, 0x4f, + 0x48, 0x7a, 0x0e, 0xbd, 0x28, 0xe9, 0x06, 0xfa, 0xba, 0xa4, 0xf3, 0x28, + 0x4d, 0x28, 0x79, 0x96, 0x50, 0x7a, 0xa5, 0x9a, 0x7a, 0x34, 0xed, 0x12, + 0xec, 0x57, 0x04, 0xfd, 0x8d, 0x94, 0x8f, 0xfa, 0xaa, 0xa6, 0x4b, 0x85, + 0xa2, 0x3c, 0x4f, 0x7c, 0x2e, 0x5f, 0x4d, 0x8a, 0x1e, 0x96, 0x74, 0x11, + 0x7d, 0x41, 0xd2, 0x4a, 0xca, 0x14, 0x7c, 0x3e, 0x64, 0xd1, 0x8f, 0x89, + 0xa9, 0x83, 0x7e, 0x2a, 0x69, 0x3a, 0xbd, 0x4b, 0x7c, 0x26, 0x64, 0xd2, + 0x57, 0x24, 0xad, 0xa4, 0xef, 0x6b, 0xfa, 0x5b, 0xe2, 0x33, 0xa1, 0x91, + 0xbe, 0xac, 0xe9, 0x49, 0x49, 0x53, 0xe9, 0x67, 0x92, 0x8e, 0x50, 0x35, + 0xea, 0x73, 0x40, 0xce, 0x67, 0x45, 0x0a, 0xb8, 0xef, 0x11, 0x9f, 0x1b, + 0xc3, 0xd4, 0x26, 0x98, 0xae, 0xa1, 0x73, 0x40, 0xd3, 0x74, 0x7e, 0x7a, + 0x82, 0xce, 0xa5, 0x27, 0x25, 0x9d, 0x43, 0x2d, 0xc8, 0xcf, 0xd4, 0xf5, + 0x65, 0xe9, 0xfc, 0x2c, 0x72, 0xca, 0xfc, 0x2c, 0xcc, 0xfb, 0x2a, 0xa1, + 0x68, 0xb3, 0xe0, 0x33, 0x25, 0x87, 0xfe, 0x83, 0x98, 0x56, 0xd0, 0x9f, + 0x41, 0xb3, 0x75, 0xbf, 0xb2, 0xb1, 0xce, 0xbf, 0x91, 0x34, 0x97, 0x16, + 0x09, 0xa6, 0x79, 0xe4, 0x06, 0x75, 0xea, 0xfe, 0x3b, 0x81, 0x3f, 0x68, + 0xfa, 0xdf, 0xa4, 0xce, 0xa3, 0xff, 0xd4, 0xf4, 0xd7, 0x9a, 0xbe, 0xa5, + 0xe9, 0xef, 0x25, 0xdd, 0x41, 0xff, 0x25, 0xe9, 0x3c, 0x7a, 0x47, 0xcb, + 0xb9, 0x9c, 0x4b, 0xb7, 0xe7, 0xd2, 0xf5, 0xb9, 0x10, 0x51, 0xd4, 0xa3, + 0x9d, 0x5c, 0xdd, 0xef, 0x5c, 0x44, 0x31, 0xb9, 0x42, 0xd1, 0x7c, 0xc1, + 0x67, 0xf4, 0x45, 0xf4, 0x7f, 0x88, 0x69, 0x1d, 0xfd, 0x5c, 0xd2, 0x56, + 0x5a, 0x20, 0xe5, 0x9d, 0xb4, 0x50, 0xd2, 0x55, 0xb4, 0x5c, 0xd2, 0x6e, + 0x4d, 0xbb, 0xe8, 0x5c, 0xc1, 0xe7, 0xa2, 0x6a, 0x27, 0x1f, 0xde, 0xfd, + 0x19, 0x4d, 0x4f, 0x92, 0x3a, 0x2f, 0xb9, 0xdd, 0x02, 0xdd, 0xde, 0x7c, + 0xcc, 0xc3, 0x6b, 0x92, 0x2e, 0xa3, 0x3f, 0x11, 0x9f, 0x9b, 0xf9, 0xf4, + 0x1d, 0xde, 0xe7, 0x58, 0xb9, 0xe7, 0xe5, 0x3e, 0x5d, 0x2b, 0xf5, 0x16, + 0x61, 0x85, 0x7f, 0x24, 0xe9, 0x32, 0x3a, 0x25, 0x69, 0x15, 0xbd, 0x2d, + 0xe9, 0x10, 0xe5, 0x08, 0xa6, 0x4d, 0x54, 0x28, 0xe9, 0x4a, 0x9a, 0x2f, + 0xe9, 0x87, 0x68, 0x89, 0xa4, 0x17, 0xd0, 0x1a, 0x49, 0x07, 0x69, 0x9d, + 0xdc, 0xef, 0xe7, 0xc9, 0xfa, 0x8a, 0xf5, 0x38, 0x8b, 0xf5, 0x38, 0x4b, + 0x10, 0xb9, 0xa5, 0xca, 0x7d, 0xad, 0xfa, 0x55, 0x8a, 0xc8, 0x4c, 0x51, + 0x35, 0x0e, 0xa6, 0xdf, 0x92, 0xb4, 0x94, 0xfe, 0x28, 0x29, 0xa2, 0x30, + 0xa1, 0xe4, 0x05, 0x92, 0x6e, 0xa5, 0x22, 0x9d, 0x6e, 0xd5, 0x74, 0xad, + 0xdc, 0xa7, 0x7d, 0xb2, 0x1e, 0x8f, 0xae, 0xc7, 0xa3, 0xeb, 0xf1, 0xe8, + 0x7a, 0x3c, 0xba, 0x1e, 0x8f, 0x2e, 0xef, 0xd1, 0xe5, 0x3d, 0xba, 0x7c, + 0x99, 0xee, 0x4f, 0x99, 0x2e, 0x5f, 0xa6, 0xcb, 0x95, 0xe9, 0x72, 0x65, + 0x5a, 0xbf, 0x4c, 0xeb, 0x97, 0x63, 0x1f, 0xa6, 0x4a, 0x3f, 0xb0, 0x84, + 0x1e, 0x93, 0xfb, 0xbf, 0x57, 0xa6, 0x17, 0xeb, 0xf4, 0x12, 0x9c, 0x9e, + 0x9c, 0x5e, 0x0a, 0xef, 0xab, 0x68, 0x1b, 0xb9, 0xe4, 0xfe, 0xde, 0x22, + 0xd3, 0x15, 0xa8, 0xf7, 0x4b, 0x7a, 0xdf, 0xbf, 0x2e, 0x69, 0x06, 0xfd, + 0x92, 0xd4, 0x39, 0xfd, 0x2b, 0x49, 0x1b, 0xa9, 0x46, 0xee, 0x63, 0xb5, + 0x2e, 0x95, 0xd8, 0x19, 0x0f, 0x48, 0xaa, 0xc6, 0x55, 0x89, 0x19, 0x7d, + 0x55, 0xd2, 0x12, 0xfa, 0xb6, 0xce, 0xff, 0xa1, 0xa4, 0x6a, 0xfd, 0x2a, + 0xb1, 0x12, 0xbf, 0x90, 0x54, 0xd0, 0x7b, 0x92, 0x16, 0x91, 0x43, 0xd6, + 0xb7, 0x80, 0x52, 0x24, 0x5d, 0x48, 0xa9, 0x92, 0xae, 0x93, 0xeb, 0xc3, + 0x7e, 0x24, 0x4f, 0xd2, 0xf5, 0x34, 0x4f, 0x52, 0xb5, 0x5e, 0x95, 0xb0, + 0x98, 0x62, 0x49, 0x0b, 0xa9, 0x44, 0xd2, 0x0b, 0xa9, 0x5c, 0xd2, 0xd5, + 0xb4, 0x58, 0xa8, 0xfe, 0x2c, 0x97, 0xb4, 0x9f, 0xaa, 0x24, 0x1d, 0xa4, + 0x1e, 0xe9, 0x77, 0x9a, 0x65, 0xbf, 0x97, 0x21, 0x92, 0x7f, 0x5f, 0x52, + 0x35, 0xef, 0xcb, 0xf5, 0x3c, 0x2f, 0x47, 0x44, 0xfd, 0x35, 0xed, 0x8f, + 0xfe, 0x4a, 0x7c, 0x47, 0x50, 0xf2, 0x15, 0xba, 0x3f, 0x2b, 0x74, 0xfb, + 0x55, 0x90, 0xa7, 0x90, 0xa2, 0xa9, 0x92, 0x36, 0xd1, 0x51, 0x49, 0xcf, + 0xa5, 0x07, 0x25, 0x2d, 0xa0, 0x7f, 0x97, 0xd4, 0x4d, 0x3f, 0x90, 0x34, + 0x85, 0xde, 0x90, 0xb4, 0x5e, 0xee, 0xa3, 0x2a, 0xdc, 0x3a, 0xde, 0xd4, + 0xfa, 0xff, 0xa3, 0xcb, 0xff, 0x2f, 0xa9, 0xd8, 0xec, 0x03, 0x49, 0xcb, + 0xa8, 0x57, 0x70, 0xbc, 0xa5, 0xea, 0x5f, 0xad, 0xe7, 0x77, 0x35, 0x2c, + 0xe3, 0x27, 0x92, 0x56, 0x52, 0x87, 0xe0, 0xb8, 0x4b, 0xf5, 0xb7, 0x15, + 0xeb, 0x66, 0x08, 0x8e, 0xbd, 0x06, 0xa5, 0x7e, 0x1b, 0x46, 0xf0, 0xa8, + 0xa4, 0x4a, 0x6f, 0x2d, 0x4e, 0x49, 0x96, 0xaf, 0x85, 0x65, 0xf0, 0x7e, + 0x3b, 0x47, 0xd7, 0x7b, 0x0e, 0x6e, 0x19, 0x8a, 0x6e, 0xa7, 0x87, 0xb4, + 0xfc, 0x69, 0x9d, 0x7e, 0x56, 0xd2, 0x15, 0x64, 0x13, 0x2a, 0xbd, 0x42, + 0xd2, 0xc5, 0xb4, 0x52, 0xf0, 0x5d, 0xa7, 0x86, 0x3e, 0x47, 0x7c, 0xd7, + 0x51, 0xf5, 0x74, 0xea, 0x72, 0x9d, 0xd0, 0x7b, 0x4e, 0xd2, 0x25, 0xb2, + 0x9d, 0x4e, 0x58, 0xd8, 0xef, 0x24, 0xad, 0x25, 0xbb, 0x50, 0xe9, 0x4a, + 0xa1, 0xf2, 0xb9, 0xbe, 0x2e, 0x5d, 0xbe, 0x4b, 0xf7, 0xa3, 0x4b, 0xf7, + 0xa3, 0x4b, 0xb7, 0xd7, 0x8d, 0x71, 0xfd, 0x85, 0x98, 0x96, 0x93, 0x53, + 0x70, 0xac, 0xa8, 0xfa, 0xb3, 0x0e, 0x9e, 0xfb, 0xff, 0x12, 0xc7, 0x85, + 0x2a, 0xbd, 0x5e, 0xd7, 0xb3, 0x1e, 0x37, 0xa5, 0x3a, 0xc1, 0xf7, 0x2c, + 0x95, 0xee, 0xd5, 0xf6, 0xdc, 0x87, 0x1b, 0x5d, 0xb6, 0xe0, 0xfb, 0x96, + 0xda, 0x07, 0x1c, 0x93, 0x23, 0x09, 0xeb, 0x54, 0x7f, 0x7c, 0xde, 0x1e, + 0xc6, 0x45, 0xeb, 0x35, 0x04, 0x13, 0xab, 0x87, 0x54, 0x3c, 0x20, 0xa4, + 0x3f, 0x4c, 0xe6, 0x1f, 0x47, 0x7e, 0x8a, 0x0e, 0x30, 0x4a, 0x74, 0xfe, + 0x6a, 0x4b, 0xfe, 0x09, 0xe4, 0x2f, 0xd4, 0xf9, 0xee, 0x59, 0xf2, 0x5f, + 0x43, 0xfe, 0x32, 0x9d, 0xcf, 0xf1, 0xad, 0xb0, 0xb3, 0x1f, 0x4c, 0xe6, + 0xbf, 0x83, 0xfc, 0xf5, 0x83, 0xc9, 0xb4, 0xd0, 0x7a, 0x66, 0x9a, 0x2f, + 0x81, 0xbb, 0x74, 0xbe, 0x67, 0x96, 0x7c, 0x37, 0xf2, 0xef, 0xd2, 0xf9, + 0x1c, 0x0f, 0xe4, 0x38, 0x55, 0xfb, 0x37, 0x36, 0xf2, 0x79, 0xa2, 0xfe, + 0xbe, 0xa9, 0xf3, 0x17, 0x9f, 0x26, 0xff, 0x4d, 0x9d, 0xbf, 0x44, 0xa7, + 0xad, 0xfd, 0x6f, 0x46, 0xfd, 0xff, 0xad, 0xf3, 0x97, 0x6a, 0xb9, 0xc3, + 0x92, 0xdf, 0x8f, 0xfc, 0x34, 0xaf, 0x4a, 0x57, 0x68, 0x79, 0xaf, 0x25, + 0x7f, 0x1c, 0xf9, 0x0b, 0x74, 0xfe, 0x8a, 0x59, 0xe6, 0xe7, 0x10, 0xf2, + 0xdb, 0x75, 0x7e, 0xa5, 0xce, 0x67, 0xbd, 0x3b, 0x1a, 0x55, 0xfe, 0x8d, + 0xc8, 0xf7, 0xea, 0xfc, 0x65, 0x3a, 0xdf, 0x3a, 0x7f, 0x3f, 0x47, 0xfe, + 0xb7, 0x74, 0xfe, 0xf2, 0x59, 0xea, 0x7f, 0x1f, 0xf9, 0x6f, 0xea, 0xfc, + 0xaa, 0x59, 0xf2, 0xd3, 0x70, 0xc1, 0xfe, 0x2b, 0xf2, 0xd7, 0x0c, 0x71, + 0xdc, 0x60, 0xc8, 0x3b, 0xe8, 0x45, 0x75, 0x6a, 0x2d, 0xbd, 0x30, 0xbe, + 0x8b, 0x9d, 0x1c, 0x39, 0x67, 0x51, 0xc8, 0x6d, 0x40, 0x96, 0x65, 0xb4, + 0x18, 0x99, 0x54, 0x64, 0x2c, 0x84, 0x07, 0xfb, 0x26, 0x5d, 0xec, 0x6e, + 0x83, 0xbe, 0xcb, 0xc8, 0x36, 0x94, 0xe6, 0xbf, 0x69, 0xcd, 0x2b, 0xa0, + 0x99, 0x01, 0xa9, 0x59, 0xdf, 0x78, 0x1d, 0xc7, 0x0a, 0x66, 0x7d, 0xa6, + 0xd6, 0x3e, 0xad, 0x95, 0x6c, 0x37, 0x54, 0xa7, 0xd6, 0x64, 0xb6, 0x76, + 0x43, 0x4e, 0x07, 0xc7, 0x3f, 0x46, 0x8e, 0xd4, 0x17, 0x52, 0x7f, 0xb2, + 0x4e, 0xd9, 0xb2, 0x87, 0x77, 0x74, 0xa9, 0x8d, 0x22, 0xee, 0x87, 0x10, + 0x2b, 0xe4, 0xc8, 0xfa, 0x78, 0x9c, 0x97, 0xd4, 0xa9, 0xbb, 0x6d, 0xc4, + 0x39, 0x17, 0x69, 0x0f, 0xf6, 0x57, 0xc4, 0xc9, 0xe7, 0xc9, 0x88, 0xd3, + 0x46, 0x23, 0x38, 0x00, 0xf8, 0xae, 0x6b, 0x87, 0x2e, 0xdf, 0xa9, 0x0f, + 0xd7, 0xa9, 0x73, 0x3a, 0xea, 0x7c, 0x01, 0xe9, 0x2c, 0x5b, 0xd4, 0xf9, + 0x1c, 0x68, 0x06, 0x7c, 0xd2, 0x1c, 0xc8, 0x9e, 0x67, 0x19, 0xec, 0x3f, + 0x93, 0x5c, 0x39, 0xa1, 0xda, 0x2e, 0x44, 0x10, 0x15, 0xef, 0xcc, 0xd5, + 0xbd, 0x56, 0x7f, 0xdc, 0xde, 0x5c, 0x69, 0x1b, 0x36, 0x29, 0xbd, 0xa9, + 0x4e, 0x3d, 0x43, 0x71, 0x39, 0xeb, 0x6d, 0x0e, 0x72, 0xb9, 0x1b, 0x6c, + 0x39, 0x14, 0x72, 0xba, 0xd0, 0x5e, 0x16, 0xea, 0xcc, 0x94, 0xfd, 0x0d, + 0x39, 0xed, 0x7a, 0x8c, 0x5f, 0x41, 0x79, 0x57, 0x7b, 0x83, 0xad, 0x84, + 0x8a, 0x6c, 0x3c, 0xb7, 0x1f, 0x97, 0x73, 0x6b, 0x33, 0x4b, 0xd8, 0x9a, + 0x6d, 0x2e, 0xf2, 0x96, 0xab, 0x12, 0x36, 0x59, 0xe2, 0x59, 0xc8, 0x75, + 0xca, 0x16, 0x72, 0xaf, 0x83, 0x87, 0x9d, 0x0b, 0x6f, 0x6b, 0xc8, 0xe7, + 0x00, 0x77, 0xd7, 0xa9, 0x67, 0x27, 0xde, 0xf6, 0xe4, 0x3c, 0x16, 0x89, + 0x85, 0xf0, 0xfe, 0x69, 0x54, 0xe4, 0x98, 0x2b, 0xeb, 0x3e, 0x0f, 0xfa, + 0x21, 0x0e, 0xac, 0x90, 0xa7, 0xe4, 0x73, 0x12, 0xf2, 0x66, 0xc7, 0x2a, + 0xf2, 0x38, 0xd2, 0x31, 0x5b, 0xd9, 0x18, 0x4f, 0x91, 0x70, 0xa0, 0x96, + 0x56, 0xf4, 0x30, 0x4b, 0x84, 0xdc, 0x1c, 0x5b, 0x79, 0x44, 0x16, 0xf2, + 0x72, 0xb9, 0xcf, 0xae, 0x90, 0x7b, 0x1e, 0xec, 0x28, 0xcb, 0x70, 0xa1, + 0x17, 0xf9, 0xe8, 0x05, 0xb8, 0x32, 0xc5, 0x85, 0x9c, 0x4e, 0x94, 0xce, + 0x72, 0x94, 0x3b, 0xcf, 0x93, 0xf4, 0x62, 0xe7, 0x71, 0x49, 0x43, 0xce, + 0x3c, 0x55, 0x57, 0x7b, 0x16, 0xf5, 0x77, 0xa0, 0x17, 0xee, 0x39, 0xf0, + 0xac, 0x3c, 0x77, 0xa6, 0x1d, 0x7c, 0xa1, 0x4e, 0x3d, 0xab, 0x98, 0x6a, + 0x2f, 0x1f, 0x87, 0x1d, 0xcc, 0x85, 0x8e, 0x4d, 0xce, 0xf5, 0x97, 0xf4, + 0xda, 0x47, 0x9c, 0xbc, 0x2b, 0x46, 0xd0, 0xc7, 0xad, 0x46, 0x0a, 0xf2, + 0x1d, 0x7a, 0x3d, 0x4f, 0xd4, 0xa9, 0xb8, 0x31, 0xec, 0x5e, 0xce, 0x7e, + 0xb6, 0xd6, 0x81, 0x5e, 0x97, 0xd0, 0x40, 0x7d, 0x0a, 0x45, 0x6a, 0x37, + 0x50, 0x87, 0x11, 0x72, 0xdf, 0x80, 0xfc, 0xb9, 0x89, 0x36, 0x5f, 0xae, + 0x53, 0xcf, 0x4a, 0xc2, 0xee, 0x15, 0x18, 0x8f, 0x17, 0xd6, 0x16, 0x72, + 0x7f, 0x52, 0xb6, 0x48, 0x89, 0x36, 0xbf, 0x93, 0x68, 0xf3, 0x6e, 0xd9, + 0x66, 0x2a, 0xda, 0x4c, 0x23, 0xb6, 0x2d, 0x21, 0xf3, 0x7f, 0x58, 0xa7, + 0x9e, 0xc3, 0xa8, 0xfc, 0xb9, 0xda, 0x0a, 0x71, 0x5f, 0xad, 0x53, 0xcf, + 0x6d, 0x86, 0x9c, 0xf3, 0xa4, 0x7d, 0xdb, 0xb4, 0xfe, 0x9b, 0x75, 0xea, + 0x79, 0x90, 0x07, 0x3b, 0x37, 0xe2, 0xe4, 0x9b, 0xfd, 0x40, 0x69, 0x1e, + 0x6d, 0x15, 0x4d, 0xb0, 0x8e, 0x5c, 0xcc, 0xdf, 0x2a, 0x68, 0x65, 0x88, + 0xad, 0x62, 0x25, 0xb9, 0x4a, 0xb7, 0x8a, 0x55, 0x32, 0x9e, 0x4d, 0x83, + 0x87, 0xe7, 0xba, 0xde, 0xd6, 0xf6, 0x1a, 0x71, 0x6e, 0x97, 0xb6, 0xbd, + 0x0c, 0xe3, 0xba, 0x9a, 0x2e, 0x4a, 0x1d, 0x71, 0x16, 0xc0, 0xb6, 0xf3, + 0x69, 0x24, 0x37, 0x97, 0xb6, 0xe6, 0xa1, 0xb6, 0x79, 0x85, 0xd8, 0xa1, + 0x17, 0xba, 0xe6, 0xc9, 0xb9, 0x11, 0xb2, 0x4f, 0x7f, 0xa9, 0x53, 0xbe, + 0xce, 0xdb, 0x9e, 0x8b, 0x38, 0xa8, 0x19, 0x51, 0x77, 0xc8, 0xb9, 0x45, + 0x5a, 0xa6, 0xb7, 0x93, 0xdb, 0x6d, 0x84, 0x56, 0x45, 0x86, 0xdc, 0xe5, + 0xb6, 0x02, 0x69, 0x15, 0xf5, 0x54, 0x61, 0xa4, 0x3a, 0xbd, 0x9d, 0xf9, + 0xd4, 0x6c, 0xa4, 0xc2, 0x7e, 0x79, 0x76, 0x3e, 0x06, 0xad, 0x91, 0x0e, + 0xb4, 0xd4, 0x91, 0x8b, 0xba, 0xd3, 0xa5, 0xed, 0x63, 0x5c, 0xa2, 0x31, + 0xdb, 0x9c, 0x0f, 0x47, 0xbd, 0x7a, 0x16, 0x15, 0x72, 0xf2, 0x13, 0xbf, + 0xb0, 0xf3, 0x1e, 0xbd, 0x47, 0x84, 0x9e, 0x17, 0xeb, 0xbe, 0x49, 0x91, + 0x91, 0xc5, 0x54, 0x99, 0xb9, 0xcf, 0x33, 0xeb, 0xd5, 0x3c, 0x79, 0x61, + 0x57, 0xcd, 0x98, 0x41, 0xaf, 0x3b, 0x57, 0xee, 0xa1, 0xa1, 0xd2, 0x79, + 0xb8, 0x87, 0xa4, 0x62, 0xb6, 0x0a, 0xd0, 0x9f, 0x26, 0xac, 0xa7, 0xcb, + 0x79, 0xa1, 0x7b, 0xde, 0x94, 0xb2, 0xae, 0x33, 0x94, 0x6d, 0x4e, 0x94, + 0x5d, 0xc9, 0x65, 0xc9, 0x2c, 0x6b, 0x58, 0xfa, 0x27, 0xeb, 0x40, 0x39, + 0x73, 0xcd, 0xe6, 0xd7, 0xcf, 0x5c, 0xb3, 0x22, 0xbd, 0x66, 0xf3, 0x31, + 0xd2, 0x8a, 0xe4, 0x9a, 0xe5, 0x98, 0x6b, 0xe6, 0x90, 0x33, 0x03, 0x7f, + 0x5b, 0xaf, 0x9e, 0xbb, 0xf1, 0x9a, 0xb1, 0xfd, 0x2c, 0xa0, 0x11, 0x63, + 0x3e, 0x6d, 0xb5, 0xa1, 0xbc, 0x7d, 0xa1, 0x65, 0x7d, 0xaa, 0xeb, 0xcd, + 0xf5, 0x99, 0x9f, 0x58, 0x9f, 0x0b, 0xf4, 0xfa, 0x70, 0x1b, 0x4b, 0x4f, + 0xbb, 0x3e, 0x0b, 0x66, 0xac, 0x0f, 0xda, 0xe8, 0x98, 0x7f, 0x9a, 0xf5, + 0x59, 0x9d, 0x58, 0x9f, 0xe1, 0x29, 0xeb, 0x93, 0x25, 0x57, 0x43, 0x50, + 0x7b, 0xbd, 0x7a, 0xa6, 0xe9, 0xbd, 0x68, 0x3e, 0xea, 0xcc, 0xa1, 0xd4, + 0x8b, 0xc5, 0x15, 0xe2, 0x16, 0xfb, 0xfd, 0xf1, 0x94, 0x4a, 0xa9, 0x59, + 0x94, 0x8a, 0x1e, 0xa4, 0x35, 0x08, 0xee, 0xc1, 0xd5, 0x48, 0xa7, 0x99, + 0xb6, 0x2d, 0x1a, 0xb3, 0x66, 0x5b, 0xe3, 0xd3, 0xc9, 0x98, 0xdf, 0x58, + 0x6f, 0xee, 0x9d, 0x66, 0x99, 0x93, 0x22, 0x6f, 0xb9, 0x44, 0x5b, 0xea, + 0x95, 0x0f, 0xf7, 0xb6, 0x2f, 0xa2, 0x81, 0x8e, 0x62, 0x1a, 0xec, 0x2c, + 0xa1, 0x12, 0xc7, 0x12, 0x0a, 0x0d, 0x1d, 0x24, 0x67, 0x7d, 0x96, 0x23, + 0xc7, 0x91, 0x9c, 0x5f, 0x6f, 0xbd, 0xf2, 0x1b, 0xe6, 0xfe, 0x5c, 0x84, + 0xfd, 0x59, 0x4c, 0xdb, 0xe0, 0x55, 0xe7, 0x26, 0xfc, 0x3c, 0xd7, 0x27, + 0xcf, 0x22, 0xb7, 0x1b, 0x2b, 0x57, 0x4a, 0x25, 0x36, 0xd4, 0x55, 0x7b, + 0x09, 0x39, 0x6d, 0x59, 0xb6, 0x1c, 0x5b, 0x72, 0xaf, 0x8f, 0xd4, 0x4f, + 0xdd, 0xeb, 0x6e, 0xd4, 0x55, 0x2a, 0xd7, 0x48, 0xb5, 0xe5, 0xd3, 0x6b, + 0x39, 0x22, 0xca, 0x4d, 0x1d, 0xc3, 0x43, 0x23, 0xb6, 0x32, 0xac, 0xe5, + 0x62, 0x8b, 0x9f, 0x9a, 0xd0, 0xf5, 0x8c, 0x88, 0xa5, 0xd0, 0xcb, 0x90, + 0x7a, 0x4b, 0x64, 0x7f, 0xec, 0xd2, 0xb6, 0xf6, 0xd5, 0xab, 0x7b, 0xaa, + 0xcb, 0xd3, 0x50, 0x96, 0x2d, 0xfd, 0x69, 0x3a, 0xfb, 0x1f, 0xe8, 0x84, + 0x9c, 0x69, 0xda, 0xe7, 0x5e, 0x09, 0x0b, 0xb4, 0xda, 0xe5, 0x65, 0xf5, + 0x2a, 0x06, 0xf1, 0xc2, 0xda, 0xbc, 0x88, 0x86, 0x72, 0xcc, 0xb5, 0x45, + 0x9b, 0x57, 0xd5, 0xab, 0x78, 0x7a, 0x21, 0xa2, 0x91, 0x45, 0xb0, 0x9e, + 0x91, 0x8e, 0x4a, 0xac, 0xec, 0xd7, 0xd0, 0x5a, 0x86, 0xe1, 0xed, 0x82, + 0x7e, 0x43, 0x19, 0x35, 0xdb, 0xf2, 0xc8, 0xe4, 0x43, 0x4e, 0xf6, 0x27, + 0xc9, 0x54, 0x8a, 0x4e, 0x8d, 0xd4, 0x95, 0x69, 0xa9, 0x07, 0xfa, 0x73, + 0x12, 0x7c, 0xc8, 0x39, 0x3f, 0xa1, 0xe1, 0x41, 0xc4, 0x98, 0x6d, 0xcc, + 0x11, 0xc5, 0xb4, 0x94, 0xc3, 0x3a, 0xec, 0x09, 0x21, 0x63, 0x20, 0xae, + 0xb1, 0x46, 0xfa, 0x2a, 0xb6, 0xe7, 0xa6, 0xcb, 0x1a, 0x6b, 0x9a, 0x64, + 0xdf, 0xb9, 0xf7, 0xb7, 0x9a, 0xeb, 0x29, 0xfb, 0xce, 0xf5, 0x15, 0x49, + 0x8b, 0x0b, 0x39, 0x39, 0x1a, 0xc9, 0x90, 0xfe, 0x51, 0x8d, 0xc5, 0xa0, + 0x3b, 0xeb, 0x55, 0x4c, 0xa3, 0xc6, 0xe2, 0xed, 0x40, 0x89, 0x1c, 0xf4, + 0x46, 0x24, 0x79, 0x2e, 0x2d, 0xe7, 0xc8, 0xb9, 0x84, 0xf7, 0x9d, 0xe1, + 0x6a, 0x68, 0xa8, 0xc7, 0xf9, 0xe6, 0xbc, 0x58, 0x4a, 0x9b, 0x45, 0x9a, + 0x3c, 0x23, 0x99, 0x2f, 0xc6, 0x8d, 0xb9, 0x98, 0xb8, 0xad, 0x1c, 0x52, + 0x3d, 0x56, 0x3e, 0x99, 0xfb, 0x5c, 0x99, 0xb0, 0xc3, 0x0a, 0xb9, 0x2e, + 0xdc, 0xcf, 0xa3, 0xda, 0x96, 0x46, 0x04, 0x3c, 0xab, 0xf3, 0x25, 0xb9, + 0x6e, 0x95, 0x58, 0xdf, 0x0a, 0xb9, 0x76, 0x6a, 0x9f, 0x1e, 0xaf, 0x57, + 0xb1, 0xb2, 0xee, 0x5f, 0x5d, 0x25, 0xfa, 0x96, 0x0e, 0x2b, 0xac, 0xc0, + 0x2e, 0xe4, 0x1d, 0xf8, 0x51, 0xac, 0x64, 0x31, 0xf4, 0xad, 0xed, 0x65, + 0xcb, 0x76, 0x9c, 0x09, 0xbb, 0x7f, 0x5a, 0xaf, 0xe5, 0xc5, 0xce, 0x49, + 0x39, 0x0f, 0x39, 0x7a, 0x9d, 0xd9, 0x36, 0x9e, 0xd7, 0x73, 0x15, 0x72, + 0x2e, 0x96, 0x63, 0x28, 0xa2, 0xc7, 0x51, 0x6b, 0x8b, 0xe4, 0x4b, 0x84, + 0x4d, 0x3e, 0x03, 0x50, 0x67, 0xbe, 0xa0, 0x97, 0xea, 0xd5, 0x33, 0x65, + 0x1e, 0xaf, 0x43, 0xce, 0x27, 0x7f, 0x4b, 0x91, 0x21, 0x5c, 0x46, 0x33, + 0x34, 0xb3, 0x8d, 0x85, 0x02, 0xb7, 0x50, 0xc1, 0x51, 0x97, 0x5d, 0x46, + 0x35, 0x76, 0xdc, 0x24, 0x11, 0xb7, 0xb5, 0xaf, 0x30, 0x4b, 0xf0, 0x09, + 0x5e, 0xfb, 0x25, 0x72, 0xdb, 0xbc, 0xed, 0xcb, 0x21, 0x2b, 0x90, 0xbd, + 0xf1, 0x18, 0xb0, 0xda, 0xda, 0x71, 0xea, 0xb4, 0xbb, 0x72, 0x43, 0xed, + 0x36, 0x72, 0x94, 0x71, 0xf4, 0xc2, 0x7e, 0x28, 0x51, 0xce, 0xc1, 0xa5, + 0x06, 0xa8, 0xc2, 0xce, 0x76, 0xea, 0xe5, 0x5a, 0x6d, 0x73, 0x1c, 0xc5, + 0x62, 0xa9, 0x43, 0xc5, 0x72, 0x4d, 0x72, 0x9c, 0x9d, 0x18, 0x87, 0x43, + 0xfa, 0x9a, 0xff, 0xa8, 0x57, 0xf7, 0x0c, 0xef, 0x09, 0xac, 0x9e, 0x7b, + 0x21, 0xdd, 0x88, 0xf8, 0xa5, 0xc5, 0x66, 0x27, 0x57, 0x7a, 0x4e, 0xba, + 0x47, 0xc0, 0xf2, 0xbf, 0xa1, 0x56, 0x35, 0x9d, 0xb2, 0xd2, 0xd9, 0x26, + 0xd2, 0x29, 0xc3, 0x3e, 0xf0, 0xb5, 0xc5, 0x94, 0x7a, 0x85, 0xfd, 0xe8, + 0xbe, 0xd4, 0x3e, 0xd4, 0x95, 0x23, 0x38, 0xc6, 0xe2, 0x39, 0xfb, 0x59, + 0x3d, 0xcf, 0x3d, 0xfb, 0x06, 0xae, 0x6b, 0x01, 0x6e, 0x5b, 0xba, 0x2e, + 0xec, 0x64, 0x0f, 0x56, 0xd6, 0xdb, 0x61, 0xb5, 0x90, 0x52, 0xe9, 0x99, + 0x07, 0xba, 0x17, 0x53, 0xc4, 0x3b, 0x44, 0xee, 0xdc, 0x1c, 0x4a, 0xd1, + 0xbb, 0xf4, 0xd7, 0xf5, 0xea, 0xfb, 0xb0, 0x85, 0xf2, 0xa6, 0x1e, 0x71, + 0xef, 0x47, 0xc4, 0x90, 0x65, 0x87, 0xe7, 0xb5, 0xa3, 0x3f, 0x9d, 0x1e, + 0x6a, 0x41, 0x24, 0x5b, 0xb1, 0x20, 0xea, 0x7e, 0x19, 0x33, 0x9c, 0x65, + 0x2f, 0xb1, 0x97, 0x51, 0x8b, 0x1d, 0xf3, 0x87, 0x9e, 0x78, 0x9b, 0xea, + 0xd0, 0xc2, 0x0e, 0xf6, 0xcd, 0xc2, 0x63, 0xac, 0x45, 0xd9, 0xa3, 0x18, + 0x2b, 0xcf, 0xc8, 0x3c, 0x78, 0x9e, 0x1c, 0x3b, 0xeb, 0xcc, 0xc1, 0xe7, + 0x52, 0x3b, 0xaf, 0xfd, 0x32, 0x39, 0x17, 0xcb, 0xe5, 0xf3, 0x36, 0xee, + 0xff, 0xff, 0x6a, 0x1b, 0xf3, 0xe0, 0xe6, 0x12, 0x19, 0x1a, 0x25, 0x67, + 0x57, 0xa4, 0xf6, 0xc3, 0x32, 0x9a, 0xca, 0x31, 0xec, 0xb2, 0x6f, 0xb8, + 0x17, 0x27, 0x74, 0xd0, 0xef, 0x5a, 0x3f, 0xfc, 0x54, 0x52, 0x27, 0x55, + 0xc7, 0x2e, 0x29, 0x0d, 0xea, 0xfb, 0x3b, 0x6f, 0x7f, 0x2d, 0x7a, 0xf3, + 0x98, 0x5c, 0x39, 0x8e, 0x5f, 0x79, 0xfd, 0x9b, 0xc5, 0x12, 0x19, 0xbf, + 0x92, 0xb4, 0x99, 0x25, 0xe4, 0x75, 0xa9, 0x39, 0x31, 0x64, 0x4c, 0xbd, + 0x04, 0x34, 0xc3, 0xe6, 0x6a, 0x6c, 0xe8, 0x7a, 0xfb, 0x03, 0xb6, 0x40, + 0x96, 0x36, 0x1b, 0xbf, 0xfc, 0x80, 0xd7, 0x56, 0x6a, 0xb8, 0xef, 0xa5, + 0x9d, 0xd8, 0x57, 0xcd, 0xc6, 0x8f, 0x3f, 0xc8, 0x11, 0xae, 0x94, 0x9c, + 0x94, 0x14, 0xbd, 0x93, 0x5d, 0x0d, 0xea, 0xbe, 0xa2, 0xe6, 0xcc, 0xdb, + 0xa1, 0x5a, 0xe6, 0x99, 0xe6, 0x96, 0x85, 0x2c, 0x93, 0x2f, 0x5b, 0x86, + 0xcc, 0x28, 0x81, 0x3f, 0xf3, 0x96, 0xa9, 0x96, 0x65, 0xc4, 0x8a, 0xd5, + 0x40, 0x74, 0x6d, 0x6b, 0x18, 0xfc, 0xed, 0x07, 0x98, 0x23, 0xcc, 0x95, + 0x0b, 0x9e, 0x1c, 0x76, 0x43, 0x6c, 0x37, 0x3c, 0xee, 0xc5, 0xb2, 0x95, + 0x25, 0x32, 0x9e, 0xe3, 0x31, 0x16, 0x37, 0x28, 0x5f, 0xe0, 0xad, 0xe5, + 0xc8, 0xf2, 0x29, 0x3d, 0xc6, 0x17, 0xa7, 0x8d, 0x75, 0x81, 0x65, 0xac, + 0xcd, 0xd8, 0x89, 0xd0, 0x75, 0x3f, 0x01, 0xef, 0xc9, 0xe9, 0x0a, 0x94, + 0x0b, 0xca, 0xde, 0xf0, 0xa8, 0x3a, 0x65, 0x0f, 0x4f, 0xf1, 0xa8, 0xec, + 0x39, 0x76, 0xd2, 0xed, 0xc0, 0x03, 0x34, 0x98, 0x3e, 0x87, 0xc7, 0xc5, + 0xbb, 0x08, 0xfb, 0x45, 0xb4, 0x60, 0x07, 0x47, 0xdc, 0x31, 0xb6, 0x0b, + 0xb4, 0x82, 0x78, 0x3e, 0x0f, 0x9e, 0xcf, 0xee, 0xad, 0xc3, 0x2e, 0x70, + 0x3f, 0x4e, 0x1d, 0xbc, 0xcf, 0x3c, 0xcd, 0xc2, 0x2e, 0xfd, 0x61, 0xc4, + 0x1d, 0x97, 0xf6, 0xd3, 0x6c, 0x57, 0x69, 0x8c, 0xcd, 0x9e, 0x6d, 0x57, + 0x56, 0xa0, 0xc6, 0xb6, 0x54, 0xfb, 0x9a, 0x14, 0x79, 0x26, 0x19, 0xb4, + 0xb2, 0x41, 0xdf, 0xb7, 0xda, 0x1b, 0xa4, 0xcf, 0xf6, 0x76, 0x36, 0xc0, + 0x1f, 0xdc, 0xa5, 0xa3, 0xe6, 0xbb, 0xb8, 0xcf, 0xd8, 0x9b, 0x4f, 0xd2, + 0x80, 0x2d, 0xcb, 0x51, 0xe2, 0x68, 0x46, 0xaf, 0x3e, 0x8d, 0xbc, 0x0c, + 0xcc, 0x56, 0x36, 0xe6, 0xac, 0xc2, 0x06, 0xca, 0x1e, 0xc6, 0xc1, 0x3e, + 0x98, 0x63, 0xeb, 0x42, 0xe9, 0x83, 0x31, 0xa2, 0xcb, 0x5a, 0x16, 0x16, + 0xc1, 0xe7, 0xa5, 0xe8, 0xb1, 0x75, 0x34, 0xa8, 0xef, 0x36, 0x23, 0xce, + 0xeb, 0x31, 0xaa, 0x8b, 0xe5, 0xb3, 0xeb, 0x2c, 0x1a, 0x69, 0xaf, 0x86, + 0x75, 0x75, 0x42, 0xca, 0xcf, 0x9a, 0x47, 0xe0, 0x11, 0xac, 0xa9, 0xfa, + 0x29, 0xa9, 0x06, 0x79, 0xd7, 0x8a, 0x38, 0x8f, 0xc9, 0x54, 0xed, 0x94, + 0x54, 0xd5, 0x14, 0xcd, 0x3a, 0x1a, 0xe9, 0x59, 0x8e, 0xb6, 0x33, 0xe0, + 0x73, 0x0a, 0xe5, 0x48, 0x4a, 0x1c, 0xf3, 0x20, 0xab, 0x81, 0x0c, 0x37, + 0x11, 0x07, 0xdf, 0x38, 0x0e, 0xc9, 0xd8, 0xa5, 0x9f, 0xd8, 0x63, 0xdf, + 0x21, 0x75, 0xf8, 0xf6, 0xa5, 0xe8, 0x73, 0x3c, 0x42, 0xcc, 0xb5, 0x47, + 0xae, 0xa8, 0x43, 0xae, 0x60, 0x09, 0x66, 0xfb, 0x52, 0x8c, 0xb0, 0xe2, + 0x5d, 0x8f, 0xd1, 0x86, 0xd8, 0xe7, 0x72, 0xac, 0xc9, 0x7d, 0xf2, 0xa6, + 0x71, 0x44, 0x9f, 0x94, 0xf7, 0x63, 0xad, 0x43, 0xce, 0x07, 0xd8, 0x76, + 0x65, 0x54, 0xe2, 0xa4, 0x8a, 0x9f, 0x46, 0x9c, 0x57, 0x61, 0xb4, 0x7c, + 0x7e, 0x6e, 0x4f, 0xc4, 0x27, 0xab, 0x7c, 0x2a, 0xd6, 0x32, 0xe8, 0x22, + 0xcc, 0xc9, 0x83, 0x72, 0xee, 0xab, 0x66, 0xd9, 0x3b, 0x73, 0x2d, 0xf6, + 0x54, 0x0c, 0x5f, 0x50, 0x83, 0x1e, 0x34, 0xf0, 0xed, 0xef, 0xbf, 0x94, + 0xfe, 0x23, 0x96, 0x1d, 0xc0, 0x1e, 0xf1, 0x8b, 0xa8, 0xd5, 0xdb, 0xa1, + 0x2c, 0x54, 0xed, 0x85, 0x17, 0xa7, 0xed, 0x89, 0x3c, 0xcb, 0x9e, 0x80, + 0x85, 0x76, 0x29, 0x0b, 0x2d, 0x93, 0x69, 0xb6, 0xd0, 0xbd, 0x1c, 0xdb, + 0xbd, 0xcd, 0xe7, 0x76, 0x3d, 0xac, 0xc7, 0xdb, 0x5e, 0x77, 0x3a, 0x6b, + 0x47, 0x5d, 0x45, 0x66, 0xef, 0x50, 0x76, 0x39, 0xea, 0xaa, 0xb3, 0xd4, + 0xb5, 0x16, 0xe5, 0x1e, 0x94, 0x7b, 0xb8, 0xc4, 0xe0, 0x73, 0xfd, 0x43, + 0x5c, 0xef, 0x2f, 0x47, 0x3a, 0xea, 0x50, 0xe7, 0x6c, 0x5e, 0x62, 0xe1, + 0x34, 0x2f, 0x81, 0x28, 0xc1, 0xc8, 0x92, 0x94, 0x23, 0x04, 0x43, 0x73, + 0x6c, 0x49, 0xea, 0xdc, 0x9d, 0xcf, 0x16, 0xfc, 0xa6, 0xaa, 0x8b, 0x67, + 0x81, 0xa3, 0x47, 0xbb, 0xe5, 0xac, 0x63, 0x7b, 0x5e, 0x0e, 0x9d, 0x07, + 0x12, 0x67, 0xeb, 0xfd, 0x42, 0x9d, 0x55, 0x88, 0x51, 0x1a, 0xd4, 0xb3, + 0x18, 0x65, 0xef, 0x5e, 0x98, 0xa5, 0x77, 0x20, 0x55, 0x46, 0x00, 0xf0, + 0x46, 0x06, 0x5b, 0x3c, 0xf7, 0x7b, 0x60, 0x30, 0x4d, 0xee, 0x02, 0xe9, + 0x2f, 0xe0, 0x51, 0x4b, 0x6d, 0x3c, 0x1f, 0x35, 0xe4, 0x1d, 0x4a, 0xc5, + 0x1e, 0x09, 0xa1, 0xe5, 0xfe, 0x3c, 0xb4, 0x67, 0xf1, 0x19, 0xf9, 0x68, + 0x65, 0xbe, 0x8c, 0x3b, 0x6c, 0xea, 0x59, 0x93, 0x5c, 0x63, 0x8f, 0x58, + 0x59, 0xca, 0x77, 0x3b, 0x43, 0xc6, 0x15, 0x37, 0xea, 0x3d, 0xc0, 0x9e, + 0x28, 0xcd, 0x12, 0x77, 0x44, 0xdc, 0x07, 0xb0, 0x67, 0x32, 0x70, 0x17, + 0xb6, 0x61, 0x14, 0xfa, 0xb4, 0x54, 0x3a, 0x86, 0xee, 0x99, 0xcd, 0x1b, + 0xaf, 0x37, 0x7b, 0xe4, 0xe0, 0x7d, 0xef, 0xc5, 0xd9, 0x24, 0x77, 0xb9, + 0xe0, 0xbb, 0xb4, 0xf4, 0xa5, 0x76, 0x57, 0x93, 0x2b, 0xa5, 0xc1, 0x5b, + 0x4e, 0xa6, 0x37, 0x75, 0xe5, 0x35, 0xc3, 0x03, 0x98, 0xfe, 0xd4, 0x3b, + 0xa9, 0x3c, 0xc6, 0x62, 0xec, 0xde, 0x66, 0x07, 0xee, 0xf4, 0x32, 0xfd, + 0x04, 0xd2, 0x59, 0x8e, 0xd4, 0xe1, 0x92, 0x14, 0xbb, 0x96, 0x3c, 0x0d, + 0x49, 0x85, 0xe1, 0xb2, 0x7b, 0xe0, 0x37, 0x23, 0xfd, 0xbb, 0x29, 0x3a, + 0xee, 0x49, 0xab, 0xa0, 0x48, 0xfb, 0x1e, 0x3a, 0xb0, 0xd4, 0x1b, 0x49, + 0xf4, 0xc2, 0x90, 0x27, 0x76, 0x9a, 0x87, 0xd6, 0x63, 0xdf, 0xf1, 0x37, + 0x4a, 0xcd, 0x76, 0x07, 0xe5, 0x1a, 0xaf, 0xa5, 0x56, 0xd8, 0x72, 0x8d, + 0x31, 0xba, 0xdc, 0x13, 0x6a, 0x3f, 0x49, 0x87, 0xea, 0xb9, 0x7f, 0x73, + 0xd2, 0x70, 0x26, 0xa7, 0xa9, 0xef, 0x40, 0xc6, 0xe5, 0xcc, 0x1c, 0xc0, + 0x68, 0x0d, 0x79, 0xca, 0x1c, 0xc5, 0x9c, 0xb4, 0x8b, 0x84, 0x25, 0xdf, + 0x34, 0xf3, 0xd4, 0xf4, 0x5e, 0xa1, 0xda, 0x94, 0x9e, 0x08, 0xfd, 0xaf, + 0x97, 0xf6, 0xf7, 0x01, 0x79, 0xaf, 0xc0, 0xfa, 0xbb, 0x1f, 0xa5, 0x9b, + 0x91, 0x6e, 0x41, 0x8c, 0x61, 0xd1, 0x13, 0x3c, 0x0a, 0x8c, 0xf0, 0x72, + 0xd6, 0x78, 0x98, 0x6e, 0xc2, 0x3a, 0xb8, 0xd1, 0xcb, 0x8d, 0xbc, 0x43, + 0x6e, 0xb6, 0x9e, 0x41, 0x8b, 0x25, 0x2d, 0xb2, 0xcd, 0x41, 0x89, 0xd5, + 0xf2, 0x09, 0xc2, 0x88, 0x6b, 0xce, 0x69, 0x74, 0xd2, 0x2d, 0x3a, 0x59, + 0xa7, 0xd1, 0xa9, 0xb0, 0xe8, 0xcc, 0x3d, 0x8d, 0x4e, 0x93, 0x45, 0x27, + 0x7b, 0x9a, 0x8e, 0xb2, 0xc2, 0xad, 0xae, 0x4c, 0x1a, 0xb8, 0x79, 0x31, + 0x6d, 0x75, 0xe5, 0x4c, 0xcb, 0x5f, 0xa2, 0xf3, 0x5d, 0x88, 0x8e, 0x94, + 0xf7, 0xc3, 0x3d, 0xcb, 0x95, 0x31, 0x25, 0x85, 0xdb, 0xa4, 0x21, 0xca, + 0xbc, 0xb7, 0xaa, 0x72, 0x88, 0x0e, 0xec, 0x21, 0xa7, 0x87, 0x4f, 0x12, + 0x7b, 0x53, 0x17, 0xfa, 0x9d, 0x97, 0x01, 0xef, 0x47, 0x72, 0x5e, 0xbc, + 0xf4, 0x23, 0x9b, 0x4d, 0x54, 0xfc, 0x6e, 0xba, 0x76, 0x99, 0x45, 0xdb, + 0x39, 0x43, 0xdb, 0x7b, 0x8b, 0xf5, 0x2c, 0x2d, 0x97, 0xb4, 0xb4, 0xb1, + 0xa9, 0xb3, 0x98, 0xd6, 0x39, 0x6c, 0xe8, 0x63, 0x09, 0x7b, 0xcf, 0x14, + 0x57, 0x41, 0xc3, 0x3e, 0x07, 0x79, 0xf3, 0x33, 0xa8, 0xc2, 0xe6, 0xcd, + 0x77, 0xa2, 0x44, 0x31, 0xcb, 0x53, 0x43, 0xce, 0x45, 0x3a, 0x72, 0xfb, + 0x3c, 0xed, 0x73, 0xfc, 0xc8, 0x30, 0x44, 0xc5, 0x2f, 0x2f, 0xc2, 0x21, + 0x9b, 0x6b, 0x2b, 0xa5, 0x95, 0x65, 0x98, 0xfb, 0x3b, 0xad, 0x31, 0x98, + 0x9b, 0x63, 0xb0, 0x74, 0x58, 0xe3, 0x5f, 0xb6, 0xce, 0x4f, 0xc7, 0xea, + 0xde, 0xc2, 0x3b, 0x7f, 0x89, 0xd5, 0x4a, 0xd8, 0xd6, 0x55, 0xbc, 0x9d, + 0x9f, 0x88, 0xb7, 0xf9, 0xb4, 0x2c, 0x94, 0x32, 0x8c, 0x20, 0x07, 0xde, + 0x44, 0xc6, 0xe2, 0xa9, 0xc8, 0x63, 0x2f, 0x22, 0x74, 0x1b, 0xf3, 0x51, + 0xb7, 0x8c, 0x90, 0xd3, 0xb1, 0x8b, 0xd3, 0x79, 0x0f, 0x37, 0x08, 0x21, + 0x9f, 0xb5, 0x3b, 0x30, 0x7e, 0xfe, 0x8e, 0x47, 0xc5, 0xda, 0xcf, 0x19, + 0x97, 0x35, 0x7e, 0xc5, 0x78, 0xce, 0x50, 0xfb, 0x98, 0xbd, 0xc8, 0xeb, + 0x0d, 0xea, 0x9d, 0x1d, 0x6f, 0x44, 0xfa, 0x90, 0x8b, 0x1b, 0xe4, 0x4e, + 0x4e, 0xb3, 0xf8, 0x0f, 0xb6, 0x3f, 0x8e, 0xbf, 0xcd, 0xbd, 0x2c, 0x73, + 0x65, 0x74, 0x97, 0x2a, 0x9f, 0x79, 0xa9, 0x7d, 0xeb, 0x35, 0x66, 0xae, + 0xbd, 0x37, 0xa6, 0xbc, 0xae, 0x7a, 0x0e, 0xf8, 0xa2, 0xf5, 0x79, 0x20, + 0xf6, 0xbb, 0xc7, 0x7c, 0x1e, 0x88, 0x48, 0x70, 0x39, 0x0d, 0xf4, 0x34, + 0x99, 0x2d, 0xa7, 0x34, 0xf8, 0xff, 0xf8, 0x81, 0x2c, 0x8b, 0x1d, 0x8d, + 0x08, 0xc0, 0x56, 0x62, 0x53, 0x5e, 0xd8, 0x26, 0x79, 0xe5, 0x85, 0x11, + 0x49, 0x7e, 0xcf, 0x9b, 0x86, 0xf9, 0x48, 0xc3, 0xbc, 0xa4, 0x29, 0xef, + 0x9a, 0xa6, 0xb9, 0x14, 0xe6, 0x0e, 0xf0, 0x7e, 0xf9, 0x1c, 0xb8, 0x62, + 0xcc, 0xeb, 0xcd, 0xe8, 0x8f, 0xb9, 0x77, 0xf1, 0x49, 0x4b, 0xd3, 0x6c, + 0xf2, 0x76, 0x2c, 0xe4, 0xbd, 0x7c, 0x1b, 0x6a, 0x8b, 0x49, 0x3f, 0x67, + 0x93, 0xef, 0x30, 0x29, 0x7f, 0x37, 0x29, 0xdf, 0x6d, 0xb2, 0xeb, 0x67, + 0x81, 0x7f, 0x6e, 0x50, 0x77, 0x7c, 0xaf, 0xbb, 0x46, 0x7a, 0xcd, 0x1a, + 0xaa, 0x75, 0xa9, 0xe7, 0x2d, 0x42, 0xe6, 0xff, 0x05, 0xf9, 0xfb, 0x89, + 0xef, 0x31, 0xa5, 0x74, 0x11, 0xe6, 0xdb, 0xe5, 0x5a, 0xe9, 0xdc, 0x4c, + 0x2e, 0x37, 0xcf, 0x22, 0x9f, 0x10, 0x7a, 0x36, 0x45, 0xb9, 0xdb, 0x8b, + 0xd3, 0x94, 0xd3, 0xc7, 0xf5, 0xc9, 0xf1, 0xc2, 0xb4, 0x13, 0xa4, 0x60, + 0x4a, 0xec, 0xc5, 0x31, 0x8a, 0xf4, 0x1c, 0x03, 0x88, 0x0b, 0xe0, 0xb9, + 0x23, 0xb5, 0xe7, 0xd2, 0xa0, 0x60, 0x1b, 0xae, 0xa7, 0x8a, 0x5f, 0x7b, + 0xfb, 0x67, 0x3b, 0x6d, 0x9b, 0xa6, 0x9e, 0xb6, 0x03, 0x35, 0x34, 0x90, + 0x9b, 0x42, 0x5e, 0x6f, 0x35, 0x85, 0x70, 0x0e, 0xd6, 0xe7, 0xc1, 0xee, + 0x51, 0xce, 0x23, 0x54, 0x6d, 0xe7, 0x19, 0x5c, 0x9b, 0x93, 0x66, 0xaf, + 0x2b, 0x7b, 0x46, 0x5d, 0x7c, 0xbf, 0xa9, 0xc7, 0x67, 0xc5, 0x1f, 0xad, + 0xcf, 0x1a, 0x33, 0x1b, 0x49, 0x3e, 0x79, 0xf2, 0x3a, 0xeb, 0xa4, 0xdf, + 0x75, 0xeb, 0x67, 0x88, 0xec, 0x19, 0x73, 0x1a, 0xd5, 0xfb, 0x66, 0x3a, + 0x0a, 0xc4, 0xcd, 0x07, 0xab, 0x37, 0xa0, 0xbd, 0xa4, 0x25, 0xf6, 0xe3, + 0xb1, 0xab, 0xf4, 0x13, 0x48, 0x73, 0x8b, 0xf6, 0x99, 0xf6, 0x5e, 0xa7, + 0xfc, 0x7a, 0x07, 0xe9, 0xf8, 0x10, 0xa5, 0x38, 0x56, 0x09, 0xc9, 0xb8, + 0x80, 0xcf, 0x7d, 0xd6, 0xa9, 0xa7, 0xa9, 0xd6, 0xab, 0x63, 0x4e, 0x58, + 0x5d, 0x3e, 0x79, 0xbb, 0x13, 0xfe, 0x01, 0xf3, 0x78, 0x19, 0xe5, 0xca, + 0xfd, 0x63, 0xa7, 0x8a, 0xb4, 0x22, 0xfb, 0x42, 0x2a, 0x12, 0x85, 0x32, + 0x9a, 0xca, 0x53, 0xfb, 0x49, 0x46, 0x9b, 0x7c, 0x6f, 0xe8, 0x44, 0xbd, + 0x7d, 0xa4, 0x9e, 0x6b, 0xf0, 0xbb, 0x76, 0x99, 0xd2, 0x46, 0xb6, 0x5e, + 0xd6, 0xd8, 0xbf, 0x35, 0xf1, 0x8c, 0x62, 0xa9, 0x9e, 0x03, 0xf5, 0xec, + 0x65, 0x9b, 0x68, 0x44, 0x0d, 0xfc, 0x04, 0x86, 0x9f, 0x5b, 0x54, 0x37, + 0xaa, 0x3b, 0xee, 0xe0, 0xe5, 0x8d, 0x94, 0x6f, 0xe3, 0x77, 0x05, 0x85, + 0x8d, 0x6f, 0x80, 0x6b, 0x32, 0x33, 0x28, 0xf5, 0x50, 0xea, 0x2d, 0xe2, + 0x7e, 0xf1, 0xa4, 0xfd, 0xeb, 0xfb, 0xd2, 0x0e, 0x93, 0xf9, 0xfc, 0x5b, + 0x3d, 0xa5, 0x68, 0xd6, 0xef, 0x8a, 0x85, 0x9c, 0x9f, 0x91, 0x4f, 0xae, + 0x98, 0x62, 0x1f, 0xe0, 0x3e, 0x5a, 0xef, 0xe4, 0xfb, 0xa8, 0xdc, 0x3b, + 0xed, 0x4d, 0x34, 0x50, 0xd7, 0x44, 0x0d, 0x4e, 0x27, 0xf8, 0x95, 0xe0, + 0x57, 0x82, 0xcf, 0xa0, 0x81, 0xee, 0x55, 0x34, 0x50, 0xbb, 0x8a, 0x1a, + 0x70, 0xb6, 0xba, 0xf2, 0xb2, 0xed, 0xfc, 0x1d, 0x81, 0x5d, 0x5a, 0x39, + 0xd1, 0x39, 0x8d, 0xea, 0x3d, 0xb3, 0x81, 0xda, 0x26, 0x32, 0x6d, 0x94, + 0xcb, 0x99, 0xa7, 0xce, 0x40, 0xfd, 0xaa, 0xc4, 0x1c, 0x96, 0xd7, 0x0e, + 0x91, 0xd3, 0x60, 0x9b, 0x3d, 0x91, 0x78, 0xb6, 0x60, 0xde, 0xbb, 0xbb, + 0x1a, 0xa7, 0x3e, 0xab, 0x35, 0xe5, 0xe7, 0x36, 0x4e, 0x7f, 0x86, 0x3b, + 0xf3, 0x19, 0xff, 0x6c, 0x32, 0xf3, 0x79, 0x63, 0x5f, 0xa3, 0x7a, 0x0f, + 0xe7, 0x22, 0xde, 0x53, 0x38, 0xf1, 0x9a, 0x60, 0x03, 0x17, 0x4b, 0x2d, + 0xbe, 0xb1, 0x15, 0xd1, 0xe7, 0x89, 0xe3, 0x61, 0x15, 0x97, 0xa5, 0x60, + 0x56, 0x32, 0xf5, 0xb7, 0x1a, 0x36, 0x1d, 0x8f, 0x7a, 0xf5, 0x3b, 0x73, + 0xfc, 0xdc, 0x9e, 0xdf, 0xf7, 0x70, 0x09, 0x8e, 0x48, 0x72, 0x44, 0xf2, + 0x59, 0x3d, 0xa7, 0x16, 0xca, 0x77, 0x43, 0x76, 0x8a, 0xd5, 0xd4, 0x22, + 0xd2, 0xb1, 0x23, 0xf8, 0xd6, 0xba, 0x06, 0xf6, 0x12, 0x80, 0xa4, 0x98, + 0x76, 0x1a, 0xab, 0x71, 0xa7, 0x9a, 0x83, 0xb8, 0x63, 0xa9, 0xe1, 0x92, + 0xa5, 0x84, 0xfe, 0xee, 0x8c, 0xff, 0x2b, 0x25, 0xb3, 0x2d, 0x5f, 0xa3, + 0xfa, 0x4e, 0xcf, 0x8c, 0xd2, 0x0d, 0xed, 0x17, 0x5d, 0xe8, 0x8f, 0xd9, + 0x42, 0xab, 0x6e, 0xa1, 0x12, 0x2d, 0x44, 0x64, 0x0b, 0xad, 0xb2, 0x85, + 0xd6, 0x44, 0x0b, 0xea, 0x19, 0x05, 0xb7, 0xb1, 0x28, 0xd1, 0xc6, 0x42, + 0x4a, 0xd5, 0xeb, 0x35, 0xd1, 0xa8, 0xbe, 0x0f, 0x4b, 0xde, 0x14, 0x5c, + 0xa5, 0xe6, 0x5a, 0xb9, 0x5c, 0x89, 0x95, 0x82, 0x77, 0x29, 0x95, 0x27, + 0xc9, 0x71, 0x1d, 0x2f, 0xbf, 0x30, 0x2d, 0x6e, 0xae, 0x9c, 0x12, 0x37, + 0xb3, 0x77, 0x91, 0xb1, 0xa2, 0xfb, 0x76, 0xdc, 0x11, 0x67, 0xf3, 0xd2, + 0xa7, 0x3e, 0x48, 0x7a, 0xe9, 0x1c, 0xf2, 0x38, 0x4a, 0xe4, 0x73, 0xff, + 0x1e, 0xe9, 0x33, 0xfa, 0xa9, 0xe2, 0xf7, 0x39, 0xda, 0x47, 0xf2, 0x9a, + 0x5d, 0xa2, 0xdf, 0xa9, 0x32, 0x7d, 0xe0, 0x4a, 0xac, 0x59, 0xc8, 0x5d, + 0x2d, 0xe3, 0x83, 0x1c, 0xc3, 0xe5, 0x6c, 0xb0, 0x39, 0xe1, 0xc3, 0xba, + 0xd0, 0x6a, 0x95, 0xec, 0x2d, 0xf7, 0x92, 0x69, 0xc4, 0xfd, 0x0c, 0x3f, + 0x7d, 0xb3, 0x79, 0x0c, 0xf5, 0xcd, 0x92, 0x41, 0x39, 0x49, 0xc3, 0xa0, + 0x99, 0xcf, 0x38, 0xaf, 0x3a, 0x8d, 0xcd, 0x1d, 0xd6, 0xf2, 0x8b, 0x9d, + 0x9f, 0x3a, 0x8d, 0xfc, 0x4e, 0x29, 0xbf, 0xc2, 0x10, 0x34, 0xf7, 0xeb, + 0x77, 0xd2, 0x0f, 0x05, 0x7f, 0x77, 0xf5, 0x3d, 0xfe, 0x7c, 0x80, 0x5e, + 0x65, 0xfe, 0xeb, 0xcc, 0x1b, 0xef, 0x51, 0xab, 0x71, 0xfd, 0x65, 0xfb, + 0xe9, 0x71, 0xd6, 0x13, 0x39, 0xd9, 0x74, 0xb7, 0x81, 0xfe, 0x8c, 0xef, + 0x7d, 0xf9, 0x40, 0xf0, 0x36, 0x7a, 0x83, 0x55, 0xf6, 0xf4, 0x05, 0x83, + 0xd7, 0x6d, 0xbb, 0x3d, 0xb0, 0x6d, 0xdb, 0x9a, 0x93, 0xbd, 0xfd, 0xf4, + 0x49, 0x56, 0xa4, 0x07, 0xed, 0xd0, 0x9a, 0xdb, 0x56, 0x45, 0x9f, 0xb7, + 0x23, 0xd9, 0x46, 0x4f, 0x29, 0xf2, 0xb0, 0xd0, 0xe2, 0x4f, 0x72, 0x5c, + 0xff, 0x57, 0x3e, 0x88, 0x3e, 0x8d, 0xd8, 0x78, 0xdb, 0xc6, 0xdb, 0xe8, + 0x75, 0x43, 0x7d, 0x05, 0x3a, 0xb7, 0xed, 0x40, 0x55, 0x55, 0x55, 0x1b, + 0x5d, 0x09, 0xf9, 0xc8, 0xe6, 0xe0, 0x9d, 0x81, 0x35, 0x55, 0xf4, 0x0b, + 0x83, 0x0b, 0xbf, 0x8d, 0xcf, 0x93, 0x7b, 0xe8, 0x4f, 0x8a, 0x5c, 0x6e, + 0x57, 0xe5, 0x9e, 0xb2, 0xd9, 0x55, 0x31, 0x94, 0xa2, 0xb7, 0x2d, 0x85, + 0x4e, 0xd8, 0xb8, 0xd0, 0x0f, 0x75, 0xf6, 0x27, 0x36, 0xd3, 0x41, 0x9e, + 0x38, 0xf9, 0x0d, 0x9d, 0x3a, 0xf0, 0x78, 0x9d, 0xe6, 0xd2, 0x2d, 0xe9, + 0x4a, 0x61, 0x1b, 0x8a, 0xbf, 0x2c, 0x7b, 0x8d, 0xc6, 0x5f, 0x93, 0xcf, + 0x56, 0x36, 0xb4, 0xd1, 0x3d, 0x0e, 0xa8, 0xfc, 0x0e, 0x4d, 0xf5, 0x55, + 0xf5, 0xed, 0x39, 0x70, 0x70, 0x53, 0xf0, 0x23, 0x9b, 0xda, 0xe8, 0x3e, + 0x16, 0x5e, 0x8b, 0x8f, 0x35, 0x7b, 0xae, 0x3e, 0xb8, 0x67, 0x6f, 0xc9, + 0x7a, 0xfa, 0xa5, 0xd4, 0x47, 0x1d, 0xf4, 0x10, 0x8b, 0xaf, 0x6e, 0xa3, + 0x17, 0x78, 0xc0, 0xf4, 0x86, 0xfc, 0xfc, 0x76, 0x0a, 0x3e, 0x37, 0x5e, + 0xb3, 0x26, 0x50, 0xfd, 0x00, 0xec, 0xf3, 0xd8, 0x1a, 0x3a, 0x92, 0x22, + 0xd5, 0x1f, 0xa2, 0xc7, 0x52, 0x0c, 0x49, 0xaf, 0x95, 0x74, 0xcd, 0xc1, + 0x0b, 0xaf, 0xa9, 0x7e, 0xd3, 0x38, 0xb0, 0xa2, 0x9b, 0xee, 0x51, 0x1a, + 0xf4, 0x88, 0xcc, 0xa0, 0x4f, 0xa7, 0xf2, 0xec, 0xbb, 0x9c, 0x81, 0x83, + 0x01, 0xfa, 0x78, 0x3a, 0xf8, 0x35, 0xc2, 0x95, 0x7d, 0xb2, 0x8a, 0x1e, + 0x91, 0xf2, 0x7c, 0xe7, 0xd5, 0xf4, 0x65, 0x6e, 0x63, 0xcd, 0x9e, 0x03, + 0xd5, 0xb7, 0x55, 0x07, 0xaa, 0xe9, 0x03, 0x96, 0x6f, 0x16, 0x85, 0xae, + 0x6a, 0x7a, 0xca, 0xc1, 0xcb, 0xf8, 0x96, 0xad, 0x75, 0xcf, 0x01, 0xfe, + 0x33, 0x6e, 0xbe, 0x74, 0x7f, 0xd5, 0xd1, 0xbe, 0xa0, 0xc8, 0x73, 0x6d, + 0x16, 0xf9, 0x2e, 0xfa, 0x95, 0x83, 0x2b, 0xce, 0xde, 0x33, 0xb2, 0x66, + 0x44, 0xe4, 0x67, 0xff, 0x7b, 0x55, 0x09, 0x3e, 0xd1, 0x50, 0x35, 0xd0, + 0xb7, 0xed, 0xc2, 0xbd, 0x23, 0x46, 0x70, 0x2b, 0x3d, 0x97, 0x2a, 0x5b, + 0x9c, 0x97, 0x5d, 0x7a, 0x84, 0x3e, 0x99, 0x06, 0xfe, 0xbd, 0x35, 0x1b, + 0x37, 0x9d, 0xac, 0xbb, 0xe6, 0xc3, 0x7d, 0x87, 0x6f, 0xbb, 0x74, 0xdb, + 0xb6, 0xdb, 0x3e, 0xa0, 0x67, 0x58, 0xba, 0xe6, 0xe8, 0x4b, 0x87, 0x6f, + 0x1b, 0xd9, 0x86, 0xda, 0x12, 0xff, 0x9f, 0xdc, 0xf8, 0xd2, 0x81, 0xdb, + 0x4f, 0x3e, 0x79, 0xf2, 0xe8, 0xbb, 0x27, 0x7f, 0x19, 0x38, 0x59, 0xdd, + 0x1f, 0x08, 0x3e, 0xb8, 0xb9, 0xad, 0x73, 0x07, 0x7d, 0x57, 0xd6, 0xf8, + 0xfd, 0xd2, 0x35, 0x0f, 0x56, 0x3f, 0x8c, 0xee, 0x1f, 0x16, 0xae, 0x42, + 0xb4, 0xb6, 0xad, 0xe4, 0x7c, 0x63, 0x5b, 0x2b, 0xbd, 0xc5, 0x43, 0xa1, + 0xef, 0x3b, 0x68, 0xc5, 0x1e, 0x51, 0x9e, 0xfd, 0x24, 0xaa, 0x39, 0x76, + 0x14, 0x1f, 0xf4, 0x25, 0x2e, 0x44, 0x7f, 0xe0, 0xdc, 0x4b, 0xd6, 0x1c, + 0x7e, 0xb0, 0x6d, 0xed, 0x85, 0x7b, 0x37, 0xb6, 0xdd, 0x11, 0xac, 0xfa, + 0xf0, 0xe6, 0x4b, 0xe8, 0x45, 0x43, 0xda, 0xd7, 0x2b, 0x86, 0x5a, 0xcc, + 0x77, 0x68, 0x1c, 0xe9, 0xaf, 0x55, 0x23, 0x70, 0x99, 0x4b, 0x3b, 0x80, + 0x7b, 0xd8, 0xde, 0xae, 0x61, 0x73, 0xdd, 0xbb, 0x9d, 0xbe, 0xcc, 0x33, + 0x78, 0xed, 0x25, 0x6b, 0x36, 0x1e, 0xc0, 0xd2, 0x3d, 0xc2, 0x89, 0x07, + 0x15, 0xff, 0xa4, 0xb4, 0x67, 0xb1, 0xd8, 0x29, 0x10, 0x1f, 0x7f, 0x8a, + 0x6d, 0x75, 0xc5, 0xc6, 0x6b, 0x4f, 0xe1, 0x8e, 0x3f, 0x97, 0xb6, 0xab, + 0xaf, 0x76, 0xe5, 0x1e, 0xda, 0xa4, 0x69, 0x9e, 0xa6, 0xa6, 0xbc, 0x95, + 0xd4, 0x77, 0x3b, 0xad, 0xd4, 0x2a, 0xd3, 0x43, 0x53, 0xf6, 0xab, 0x7a, + 0x26, 0xaf, 0x96, 0x75, 0x8b, 0x7c, 0x56, 0xc7, 0xb2, 0x05, 0x94, 0x7c, + 0x5f, 0xdb, 0x66, 0xa9, 0xcb, 0xa6, 0xb9, 0x25, 0x89, 0xfc, 0x25, 0x9a, + 0x6e, 0xd2, 0x74, 0x35, 0x99, 0xdf, 0xbb, 0x1b, 0x92, 0xaa, 0xf4, 0x3c, + 0x9d, 0x9e, 0x97, 0x28, 0x97, 0xa9, 0xeb, 0xda, 0x20, 0xcf, 0x5c, 0xa1, + 0xa1, 0xda, 0xef, 0x4f, 0xb4, 0x67, 0xb6, 0xa9, 0x90, 0x27, 0x9f, 0x6a, + 0xdb, 0x28, 0xf9, 0x3e, 0xb8, 0xf9, 0x8e, 0x07, 0xd3, 0x6c, 0x5d, 0xb7, + 0x39, 0x9e, 0x1c, 0xad, 0x6f, 0xbe, 0xe7, 0x61, 0x48, 0x5a, 0x44, 0xe6, + 0x7b, 0x05, 0x4c, 0xcd, 0xf7, 0x3c, 0xdc, 0x9a, 0x9a, 0xe3, 0xaa, 0xd4, + 0x75, 0x98, 0xef, 0x29, 0xd8, 0x25, 0x9f, 0x2d, 0xa9, 0xe2, 0xd5, 0xdb, + 0xf0, 0x0e, 0xc9, 0xe7, 0x27, 0xfa, 0xa2, 0xca, 0x56, 0xea, 0xb2, 0x95, + 0xb2, 0x3e, 0x96, 0x2f, 0xd7, 0x79, 0xe6, 0x7b, 0x11, 0x1d, 0x9a, 0xf6, + 0xe8, 0x76, 0x7a, 0x70, 0xb2, 0xa4, 0x69, 0x9d, 0x5e, 0xed, 0x13, 0xb7, + 0xe8, 0xb6, 0x2c, 0xb3, 0x51, 0x41, 0x46, 0x45, 0x25, 0x00, 0x95, 0x4a, + 0x2a, 0xaf, 0x6d, 0xe8, 0x6c, 0xae, 0xed, 0x69, 0xea, 0xa8, 0xea, 0xe9, + 0xee, 0x69, 0xae, 0x6a, 0xec, 0x6c, 0x68, 0xa8, 0xea, 0x58, 0xd5, 0x54, + 0x57, 0xb5, 0xb2, 0xab, 0xa7, 0xa1, 0xb1, 0xa7, 0xab, 0xb1, 0x6b, 0x55, + 0x6d, 0x2d, 0xa5, 0xb5, 0x8d, 0x06, 0x03, 0xa1, 0x40, 0x7c, 0x2d, 0xa5, + 0xb4, 0x29, 0x6a, 0xac, 0x6d, 0x25, 0xdb, 0xda, 0xd6, 0x4a, 0xb2, 0xe3, + 0x63, 0x98, 0x3f, 0x21, 0xc8, 0xeb, 0x0c, 0x4e, 0xfa, 0xe3, 0xe1, 0x70, + 0x7c, 0x7c, 0x53, 0x60, 0x2c, 0xb0, 0xc9, 0x17, 0xf2, 0xed, 0xf6, 0x47, + 0x69, 0xed, 0x6c, 0x52, 0xb7, 0x3f, 0x1a, 0x0d, 0x47, 0x57, 0xbb, 0x47, + 0xc3, 0x93, 0xc1, 0x31, 0x77, 0x28, 0x1c, 0x77, 0xef, 0xf6, 0xc7, 0xdd, + 0x09, 0x4d, 0x77, 0x5f, 0x8f, 0x3b, 0x36, 0xea, 0x0b, 0x85, 0x50, 0xbe, + 0xeb, 0x9f, 0x2f, 0x3f, 0xe6, 0xdf, 0xe5, 0x9b, 0x0c, 0x5a, 0xeb, 0xf1, + 0x8d, 0xf9, 0x22, 0x71, 0x54, 0xe2, 0xea, 0x6e, 0x2e, 0x2f, 0x1f, 0x3c, + 0x18, 0x8a, 0x8f, 0xfb, 0xe3, 0x81, 0xd1, 0xae, 0xa0, 0x2f, 0x16, 0x83, + 0xcc, 0xbf, 0x2f, 0x30, 0xea, 0xef, 0xf2, 0x05, 0x83, 0x3b, 0x7d, 0xa3, + 0x7b, 0x7b, 0x27, 0x22, 0x41, 0x72, 0x76, 0x07, 0x62, 0xa3, 0xe1, 0x7d, + 0xfe, 0xa8, 0x7f, 0x4c, 0xe5, 0x92, 0xe8, 0x25, 0xa3, 0x17, 0xf3, 0xd5, + 0xcb, 0xb4, 0x8f, 0x6c, 0xbd, 0x7d, 0x7d, 0x54, 0xd2, 0xbb, 0xc5, 0xdd, + 0x73, 0x60, 0xd4, 0x1f, 0x89, 0x07, 0xc2, 0x21, 0xf7, 0xfe, 0xf1, 0x40, + 0xd0, 0xef, 0x1e, 0x0d, 0x86, 0x63, 0x81, 0xd0, 0x6e, 0x77, 0x24, 0x1c, + 0x8d, 0xd3, 0xe2, 0xde, 0x2d, 0xa7, 0xcb, 0x9f, 0xc0, 0x20, 0xd0, 0x51, + 0x59, 0x77, 0x51, 0x2f, 0x06, 0x18, 0x4f, 0xf4, 0xf6, 0x5c, 0x5f, 0x3c, + 0x6e, 0xf6, 0x86, 0x72, 0x7a, 0x43, 0xbb, 0xc2, 0xe7, 0x07, 0xe2, 0xe3, + 0xde, 0xa8, 0x2f, 0x14, 0x93, 0xb5, 0x8a, 0x0d, 0x64, 0xdf, 0x30, 0xd4, + 0xd5, 0x43, 0xae, 0x0d, 0x93, 0xa3, 0x7e, 0x9e, 0x0d, 0xd5, 0x47, 0xd6, + 0xa4, 0x1c, 0x53, 0xd6, 0x1b, 0x8a, 0x4c, 0xc6, 0xfb, 0x59, 0x3f, 0xa1, + 0xb6, 0x65, 0x32, 0x6e, 0xca, 0xb2, 0x4c, 0x99, 0x4c, 0xe5, 0x99, 0xa9, + 0xc1, 0xc9, 0x08, 0x37, 0x51, 0xbd, 0xc7, 0xb7, 0xcf, 0x47, 0xa2, 0x8f, + 0x8c, 0xbe, 0x5e, 0xb2, 0xf5, 0x61, 0xcc, 0x76, 0x7c, 0x6c, 0xe0, 0xcf, + 0x0d, 0x7d, 0x2c, 0xe0, 0x1c, 0x66, 0x64, 0x36, 0x38, 0x7b, 0x5f, 0xdf, + 0xb6, 0x3e, 0x2a, 0xef, 0xf3, 0x85, 0xc6, 0xa2, 0xe1, 0xc0, 0x58, 0xcd, + 0x4e, 0x73, 0x2c, 0x35, 0x89, 0x51, 0x75, 0xa8, 0x25, 0x68, 0xa5, 0xb2, + 0x33, 0x69, 0xa9, 0x91, 0xb4, 0x52, 0xe9, 0x99, 0x94, 0x78, 0x82, 0x5a, + 0xa9, 0xf2, 0x6c, 0x2a, 0xe6, 0x1c, 0xb6, 0x9e, 0xb9, 0x63, 0xda, 0x94, + 0x66, 0xaf, 0x30, 0xe8, 0x4f, 0x2a, 0xf6, 0xf9, 0x07, 0x95, 0x2d, 0xce, + 0x3e, 0x06, 0xa8, 0x72, 0x7e, 0xb2, 0xd5, 0xd3, 0xd4, 0xc7, 0x4a, 0xeb, + 0x02, 0x41, 0xcc, 0x45, 0x79, 0xe7, 0x64, 0x20, 0x38, 0xc6, 0xf5, 0xcd, + 0x36, 0xdc, 0x29, 0xaa, 0x67, 0x54, 0x19, 0xf0, 0xc7, 0x60, 0xee, 0xad, + 0xb4, 0xfc, 0xf4, 0x2a, 0x83, 0xfe, 0x78, 0x1c, 0x66, 0x17, 0x4b, 0x36, + 0x79, 0x86, 0x21, 0x98, 0xca, 0xad, 0x34, 0x3f, 0xa1, 0x34, 0x1a, 0x0e, + 0xc5, 0xfd, 0xa1, 0x78, 0x4d, 0x17, 0xd3, 0x03, 0x68, 0xac, 0x24, 0x91, + 0x35, 0xe1, 0x1f, 0x0b, 0xf8, 0x6a, 0xd8, 0xa0, 0x6b, 0x92, 0xc6, 0xd8, + 0x4a, 0x2b, 0xce, 0xac, 0xc0, 0xd6, 0x5a, 0xce, 0xd6, 0xc7, 0x8c, 0xb5, + 0x3b, 0xa7, 0xd5, 0x6e, 0x25, 0xcf, 0xe9, 0x94, 0x12, 0xc6, 0xde, 0x4a, + 0x35, 0xa7, 0xd3, 0xd1, 0xeb, 0x5c, 0x3e, 0x75, 0xbb, 0xb7, 0xd2, 0xaa, + 0xb3, 0x15, 0xd8, 0x12, 0x52, 0x45, 0xb6, 0x44, 0xfc, 0x21, 0xff, 0x58, + 0x5f, 0x20, 0x86, 0x89, 0xe0, 0x19, 0x74, 0x9f, 0xa5, 0xe0, 0x19, 0x06, + 0x95, 0xdc, 0x89, 0xd6, 0x85, 0x9d, 0xa6, 0x34, 0xe0, 0x1f, 0xf5, 0x07, + 0xf6, 0x71, 0x3d, 0x45, 0x09, 0x95, 0x70, 0xac, 0x46, 0xae, 0x60, 0xf9, + 0x70, 0xcf, 0xc0, 0x60, 0xef, 0x96, 0xcd, 0xad, 0x94, 0x3b, 0x35, 0x2f, + 0x34, 0x16, 0xc4, 0xdc, 0xe7, 0x59, 0x85, 0xeb, 0x7d, 0x2c, 0x44, 0x35, + 0x05, 0x56, 0x69, 0xbf, 0x2f, 0x3a, 0xea, 0x0f, 0x0e, 0x4d, 0x06, 0xc6, + 0x5a, 0xc9, 0x95, 0xc8, 0x98, 0x8c, 0x07, 0x82, 0x35, 0x7d, 0xe1, 0xdd, + 0xad, 0xd4, 0xd8, 0x37, 0x1a, 0x9e, 0xa8, 0x89, 0x4e, 0xc4, 0x82, 0x35, + 0x7b, 0xe0, 0x1d, 0x6a, 0xa6, 0xb9, 0x88, 0xf2, 0xd9, 0xdc, 0x71, 0x2b, + 0xd5, 0x9f, 0xa5, 0xd4, 0x4c, 0x5f, 0xdb, 0x4a, 0x75, 0x67, 0x29, 0x33, + 0xc3, 0x03, 0x9e, 0xbd, 0x99, 0x99, 0x8e, 0xf1, 0xec, 0xcd, 0xcc, 0x70, + 0x9c, 0xff, 0x7c, 0x33, 0xd6, 0xe5, 0x5c, 0xf1, 0x4f, 0x96, 0x51, 0xda, + 0x1b, 0xcf, 0xa2, 0x9d, 0x1c, 0x84, 0x69, 0x8d, 0xe5, 0x3d, 0x07, 0xe0, + 0x0f, 0x42, 0xbe, 0x60, 0xe2, 0x18, 0xeb, 0xf3, 0x4d, 0xec, 0x1c, 0xf3, + 0xd5, 0xfe, 0xff, 0xac, 0xac, 0xae, 0x95, 0x3a, 0xff, 0xe5, 0xca, 0xa6, + 0x9f, 0x99, 0xad, 0xd4, 0xf7, 0x2f, 0xd7, 0x71, 0xfa, 0x93, 0xb0, 0x95, + 0xba, 0xff, 0xe5, 0xda, 0x92, 0x12, 0xde, 0xbb, 0x5e, 0x5f, 0x6c, 0xef, + 0xd9, 0xcd, 0x60, 0x46, 0x2d, 0x67, 0x37, 0x83, 0xcd, 0xbe, 0x38, 0xf6, + 0xe9, 0xd4, 0x2d, 0x5b, 0x71, 0x96, 0x32, 0xbc, 0xfc, 0xfd, 0xbe, 0xf8, + 0x38, 0xfb, 0x9f, 0x33, 0x6b, 0x26, 0xcc, 0x7e, 0xba, 0xe3, 0x2a, 0x39, + 0x73, 0x41, 0x76, 0x97, 0x63, 0xbe, 0xe0, 0xbe, 0xc0, 0xde, 0x1a, 0x9c, + 0x5a, 0xe1, 0xb8, 0x8f, 0xa3, 0x90, 0x9a, 0x9e, 0x90, 0x8e, 0x40, 0x64, + 0xf0, 0xd3, 0x4a, 0xc5, 0xb3, 0xe8, 0xf4, 0xf2, 0x21, 0xa7, 0xf3, 0x4b, + 0x67, 0xc9, 0xdf, 0xe4, 0x9f, 0xd8, 0xa9, 0x15, 0xfc, 0x31, 0x3e, 0x5d, + 0x67, 0x53, 0x89, 0x8f, 0x87, 0xc7, 0xe0, 0x63, 0x7c, 0x13, 0x7e, 0x58, + 0x18, 0xb4, 0x16, 0xcd, 0xa2, 0x35, 0x18, 0xd8, 0x1d, 0xf2, 0xc5, 0x27, + 0xa3, 0x7e, 0x76, 0x71, 0x33, 0xb3, 0xbd, 0xe3, 0xd1, 0xf0, 0x7e, 0x14, + 0x9d, 0xd7, 0xc7, 0xf1, 0x48, 0x4d, 0x20, 0x5c, 0x63, 0x89, 0xa7, 0x5a, + 0x29, 0x47, 0x89, 0x83, 0xbe, 0xd0, 0xee, 0x1a, 0xdd, 0xdb, 0x79, 0x16, + 0x91, 0x45, 0xb3, 0xdc, 0x22, 0xee, 0x0d, 0x06, 0xfd, 0xbb, 0x7d, 0xc1, + 0x8e, 0xe8, 0xee, 0xc9, 0x09, 0x1c, 0x68, 0x16, 0xad, 0x5c, 0xab, 0x16, + 0x4e, 0xb9, 0xdd, 0xca, 0xc9, 0x27, 0x85, 0x9b, 0x27, 0x83, 0xc1, 0xfe, + 0x70, 0x00, 0x59, 0x51, 0x4b, 0x31, 0x97, 0x45, 0x63, 0xcb, 0xce, 0x3d, + 0xfe, 0xd1, 0xf8, 0x54, 0xd9, 0x60, 0x3c, 0x8a, 0xe9, 0xe6, 0xb3, 0x74, + 0xba, 0x2c, 0x71, 0x16, 0x5b, 0xbb, 0x2d, 0x07, 0xed, 0xdb, 0xc9, 0x5e, + 0x7c, 0x91, 0x45, 0x1c, 0xf5, 0xef, 0xaa, 0x39, 0xdf, 0xef, 0xdb, 0x3b, + 0xe0, 0xdf, 0x85, 0x1d, 0x16, 0x1a, 0x3d, 0x5b, 0x76, 0x9b, 0x59, 0xa9, + 0xf4, 0xe8, 0x1d, 0xd1, 0xa8, 0xef, 0x20, 0x9f, 0x5c, 0xad, 0xb3, 0x8b, + 0xdb, 0xcc, 0x1e, 0x27, 0xc5, 0x98, 0xcd, 0x7c, 0x8b, 0xac, 0x2b, 0x8c, + 0x79, 0x1b, 0x9d, 0x32, 0x51, 0x52, 0xbe, 0xde, 0x17, 0x43, 0x04, 0x15, + 0x99, 0x55, 0xd8, 0x36, 0x43, 0x88, 0xc8, 0x62, 0xa6, 0x26, 0x84, 0x6d, + 0x38, 0xb1, 0x92, 0xc2, 0x5e, 0x4c, 0xb0, 0x2f, 0x1e, 0xc6, 0xc4, 0x38, + 0x2d, 0x52, 0xd5, 0xfd, 0xe9, 0x92, 0xa9, 0x6d, 0xa8, 0xf9, 0x47, 0xd7, + 0xb3, 0x2d, 0x42, 0xd9, 0xe8, 0x34, 0x41, 0x9b, 0x69, 0x3d, 0x52, 0xe0, + 0x0d, 0x4c, 0x58, 0x96, 0x21, 0x29, 0x52, 0x1e, 0xa3, 0xc4, 0x22, 0x46, + 0x18, 0x34, 0x3a, 0x19, 0x8d, 0x72, 0x24, 0xd4, 0x73, 0xc0, 0x3f, 0x3a, + 0x29, 0x7b, 0xb9, 0xe4, 0xcc, 0x0a, 0x83, 0xfe, 0xa8, 0x72, 0x87, 0xee, + 0x33, 0xeb, 0xc5, 0x12, 0x4b, 0x2a, 0x35, 0x76, 0x4d, 0x86, 0xe4, 0x7c, + 0xd7, 0xac, 0xd3, 0xcc, 0x59, 0xb2, 0x31, 0xa4, 0x19, 0xbe, 0x8b, 0x3a, + 0x66, 0x88, 0x66, 0xbf, 0x32, 0x59, 0xef, 0x54, 0xb1, 0x83, 0x88, 0x70, + 0x26, 0xdc, 0x31, 0xd5, 0x6d, 0x6a, 0x9c, 0x59, 0xc5, 0x7e, 0x5f, 0x34, + 0x04, 0xfb, 0x5d, 0xed, 0x1e, 0xdc, 0x1b, 0x88, 0x44, 0xf8, 0x3a, 0xb3, + 0xa9, 0xb7, 0xbb, 0xd7, 0x3d, 0xb4, 0xa9, 0x5f, 0x5f, 0x69, 0xdc, 0xe4, + 0x9a, 0xe9, 0x7b, 0xc9, 0x35, 0xd3, 0x51, 0x52, 0x9a, 0xe9, 0x08, 0x29, + 0x75, 0xb0, 0x7b, 0xe3, 0x8e, 0xde, 0xcd, 0x5e, 0x5a, 0x6c, 0x8d, 0x41, + 0xab, 0xbb, 0x3a, 0xfa, 0xfa, 0x3a, 0x3b, 0xba, 0x36, 0xee, 0xf0, 0x6e, + 0xed, 0xef, 0xd9, 0xb1, 0xa9, 0xc3, 0xdb, 0xb5, 0x7e, 0x47, 0xdf, 0x96, + 0x41, 0x2f, 0xe5, 0x79, 0x07, 0x3a, 0x36, 0x0f, 0xf6, 0x6f, 0x19, 0xf0, + 0xee, 0xe8, 0xdc, 0xea, 0xed, 0x19, 0xf4, 0x0e, 0xf4, 0x74, 0x6c, 0xa2, + 0x39, 0x49, 0x29, 0xba, 0x43, 0x05, 0xa7, 0xf1, 0x9e, 0x24, 0x86, 0xc9, + 0x18, 0xc6, 0xed, 0x65, 0x18, 0x77, 0x19, 0xfb, 0x30, 0xdf, 0xe6, 0x8c, + 0xe1, 0x0d, 0x94, 0x32, 0xbc, 0x41, 0x5e, 0x74, 0x8c, 0x61, 0xdc, 0x6e, + 0x86, 0x71, 0xbb, 0xb1, 0x0f, 0xf3, 0xed, 0xc7, 0x31, 0xac, 0xae, 0x3f, + 0x20, 0x1b, 0x58, 0x2e, 0x3f, 0xfa, 0x58, 0xcc, 0xb9, 0x29, 0x92, 0xc8, + 0x6c, 0x2d, 0x44, 0x41, 0x63, 0x78, 0x1b, 0x89, 0x6d, 0x64, 0x6c, 0x03, + 0x8b, 0xab, 0x91, 0x31, 0xd2, 0x49, 0x9e, 0x91, 0xb3, 0x47, 0xf9, 0x55, + 0x23, 0xff, 0x52, 0xd4, 0x5c, 0xfe, 0x4f, 0xa8, 0x23, 0x1a, 0x1c, 0x99, + 0xcd, 0xb3, 0xe5, 0x8e, 0xcc, 0xe2, 0xb8, 0x32, 0x7c, 0xa3, 0xa3, 0xfe, + 0x58, 0xac, 0xbc, 0x16, 0xd7, 0x7d, 0x93, 0xaf, 0xb3, 0xf0, 0xf5, 0xe0, + 0x33, 0x15, 0xbf, 0x2e, 0xe8, 0xdb, 0x1d, 0x23, 0x9b, 0x6f, 0x6c, 0x0c, + 0x12, 0xd5, 0x09, 0x79, 0x99, 0xcc, 0xf0, 0x45, 0x22, 0xfa, 0x96, 0x40, + 0x29, 0xe0, 0xfd, 0xa1, 0x31, 0x72, 0x80, 0x06, 0x0f, 0x22, 0x19, 0xe3, + 0x3d, 0x4b, 0xe9, 0x89, 0xf1, 0x53, 0x51, 0x82, 0xed, 0xeb, 0x91, 0x87, + 0x97, 0xb2, 0xba, 0xa1, 0xa1, 0xde, 0x6e, 0x72, 0xee, 0x9c, 0x76, 0x47, + 0xa3, 0xbc, 0x9d, 0xd6, 0xa8, 0x53, 0x8d, 0x31, 0x66, 0xd1, 0xdb, 0xa1, + 0x6f, 0xd4, 0xce, 0x9d, 0x7a, 0xbd, 0x63, 0xfd, 0xbe, 0x00, 0xbb, 0x5a, + 0x72, 0xec, 0x64, 0x67, 0x4b, 0x69, 0xa3, 0xa6, 0x01, 0xa4, 0x60, 0xd6, + 0x11, 0x00, 0x93, 0x63, 0x34, 0xe8, 0xf7, 0x45, 0x99, 0x84, 0x63, 0x7e, + 0x4a, 0xc5, 0xbe, 0x0c, 0x61, 0x26, 0x28, 0x53, 0x33, 0x1c, 0x82, 0xa0, + 0x14, 0xc6, 0xe3, 0x0b, 0x84, 0x62, 0x52, 0x2c, 0xb9, 0x8d, 0xfe, 0x83, + 0x24, 0xc6, 0x28, 0x5b, 0x3f, 0x6b, 0xe8, 0x8f, 0x86, 0xe3, 0xe1, 0xd1, + 0x70, 0x90, 0x52, 0x74, 0x0f, 0xb2, 0xc7, 0xa6, 0xd8, 0x5b, 0x8c, 0xd2, + 0x94, 0xa0, 0x77, 0x8c, 0x32, 0xc6, 0x38, 0x50, 0x52, 0xcd, 0xd8, 0xc6, + 0x30, 0x1d, 0x29, 0xfe, 0x8b, 0x27, 0x7d, 0x41, 0xa8, 0xf8, 0xb5, 0x27, + 0x20, 0xdb, 0xae, 0xf2, 0x5a, 0xfe, 0xa8, 0xe3, 0x8f, 0x7a, 0xaa, 0xde, + 0xe5, 0x0b, 0x04, 0xfd, 0x63, 0xee, 0x78, 0xd8, 0x3d, 0x1a, 0xf5, 0xfb, + 0xe2, 0x7e, 0x77, 0x62, 0xc4, 0xe6, 0x86, 0xdb, 0x15, 0x0d, 0x4f, 0xb8, + 0xb1, 0x16, 0x51, 0xac, 0x0c, 0xa5, 0xed, 0x0a, 0x20, 0xa6, 0x0b, 0x5c, + 0xe2, 0xa7, 0x52, 0x70, 0x63, 0xc9, 0xb9, 0x5a, 0x17, 0x8e, 0x5a, 0x2e, + 0xe4, 0x4a, 0xb9, 0x84, 0x55, 0xcc, 0x3d, 0x3a, 0x9b, 0x42, 0x06, 0xd7, + 0xad, 0x4e, 0x2c, 0xb2, 0xef, 0xe6, 0xf9, 0xb0, 0xc1, 0x65, 0x50, 0x06, + 0x3e, 0xf4, 0xa5, 0x5e, 0xf3, 0x4a, 0xbd, 0x88, 0x79, 0x65, 0x0f, 0xd3, + 0xef, 0xd8, 0xb4, 0x20, 0x99, 0x37, 0xd3, 0x57, 0xcd, 0xe3, 0x4c, 0xd8, + 0x49, 0x60, 0x54, 0x1e, 0xff, 0xa6, 0x0d, 0xe5, 0x42, 0x3c, 0xa3, 0x4f, + 0x2e, 0xab, 0x50, 0x3f, 0xa2, 0x29, 0x9c, 0x29, 0x1b, 0x44, 0x24, 0x31, + 0x19, 0x93, 0x35, 0xcf, 0xbc, 0xc6, 0x53, 0x1a, 0xc4, 0xea, 0x19, 0x50, + 0x0e, 0xb8, 0x6e, 0xb5, 0x92, 0xe6, 0x90, 0x8a, 0x92, 0xa2, 0xe9, 0xcf, + 0x30, 0x64, 0xeb, 0xdd, 0xd3, 0x16, 0x3e, 0x5d, 0xca, 0x64, 0x47, 0x32, + 0x12, 0x6c, 0x8c, 0xf2, 0x93, 0x3c, 0xe6, 0x36, 0xf9, 0x10, 0xa7, 0x20, + 0x29, 0xef, 0x8f, 0xf2, 0xa1, 0xcd, 0xd3, 0x3b, 0x34, 0x11, 0x91, 0xa3, + 0x98, 0x1e, 0x49, 0xa3, 0x24, 0xec, 0x26, 0x15, 0x39, 0xd2, 0x1c, 0x2b, + 0xc1, 0xac, 0x9f, 0x9c, 0xe0, 0x8b, 0xbf, 0x6f, 0x8c, 0x43, 0x05, 0xb5, + 0x38, 0xb3, 0xad, 0x9d, 0x03, 0xaa, 0xd8, 0x49, 0x92, 0xa8, 0x1a, 0xd4, + 0x53, 0x22, 0xc9, 0xe8, 0x3b, 0x4b, 0x57, 0x78, 0x32, 0x14, 0xa7, 0xf9, + 0x10, 0xf1, 0x9a, 0x4c, 0xaf, 0x02, 0x63, 0xe0, 0x62, 0x9b, 0x11, 0xc8, + 0x51, 0x16, 0x98, 0x2d, 0x51, 0x3d, 0x72, 0x39, 0x0b, 0xc9, 0x9b, 0x8f, + 0xaa, 0x65, 0x0e, 0x64, 0x9c, 0xda, 0x3c, 0xc9, 0x21, 0x22, 0x65, 0xea, + 0xa4, 0x74, 0xf0, 0x69, 0x3a, 0x11, 0x53, 0x5a, 0xd1, 0x70, 0x04, 0xb1, + 0x7e, 0x00, 0xf5, 0x67, 0x23, 0x39, 0xe0, 0x9f, 0x08, 0xc7, 0xfd, 0x7a, + 0x02, 0x79, 0x2e, 0xb5, 0xc1, 0x39, 0x99, 0x95, 0x07, 0x92, 0xf6, 0x0c, + 0x72, 0x31, 0x13, 0xf3, 0x88, 0x31, 0xeb, 0x32, 0xdc, 0x49, 0xef, 0xc1, + 0x88, 0x9f, 0x72, 0xc6, 0xe5, 0x7d, 0x57, 0x89, 0x31, 0x08, 0xff, 0x18, + 0xe5, 0x5a, 0x45, 0xdc, 0xd2, 0x3e, 0x08, 0xe7, 0x28, 0xa1, 0x3e, 0x89, + 0x28, 0x75, 0xdc, 0x17, 0xdb, 0xcc, 0xf6, 0x66, 0x07, 0x83, 0xce, 0xf2, + 0x67, 0x57, 0x78, 0xcc, 0x8f, 0x74, 0x18, 0x7b, 0xd4, 0x08, 0x8c, 0x91, + 0x3d, 0xc0, 0x73, 0x97, 0x86, 0x00, 0x70, 0xd8, 0x87, 0x59, 0x02, 0x17, + 0xeb, 0x18, 0xe5, 0x13, 0x8d, 0x32, 0x03, 0xb1, 0x2e, 0xb5, 0xa3, 0x51, + 0x71, 0x6a, 0x20, 0xd6, 0x33, 0x11, 0x89, 0x1f, 0x44, 0xbe, 0x0e, 0x64, + 0x28, 0x65, 0xaf, 0xff, 0x20, 0x4e, 0x34, 0xea, 0x0f, 0xca, 0xeb, 0x55, + 0x39, 0x86, 0x1e, 0x9a, 0x72, 0xd7, 0xe4, 0xfb, 0x6d, 0x6f, 0x77, 0x79, + 0x6d, 0x39, 0x42, 0xfa, 0x2a, 0x0e, 0xe9, 0xab, 0x38, 0xa4, 0xaf, 0x3a, + 0xeb, 0xe5, 0x84, 0xce, 0x9b, 0x56, 0x63, 0x72, 0x41, 0x74, 0x95, 0x75, + 0xff, 0x72, 0x95, 0x59, 0x13, 0xbe, 0xbd, 0xc9, 0x67, 0x83, 0xa9, 0x13, + 0x49, 0x71, 0x68, 0x72, 0x97, 0x6f, 0x94, 0xe3, 0xf5, 0x28, 0xd9, 0xf9, + 0xa4, 0xa1, 0x9c, 0x89, 0x19, 0xa5, 0x33, 0x27, 0x2c, 0x9e, 0xd9, 0x1e, + 0x62, 0xb3, 0x71, 0xf0, 0x67, 0x8c, 0x0a, 0x42, 0xfe, 0xfd, 0x83, 0x58, + 0xd3, 0xa0, 0x1f, 0x31, 0x2e, 0x2c, 0xd7, 0x8c, 0x7b, 0xa0, 0xc5, 0xf3, + 0xee, 0x0a, 0x87, 0x3a, 0x7d, 0xf1, 0xd1, 0xf1, 0xe4, 0x13, 0x2d, 0x14, + 0x91, 0x6e, 0x20, 0xa4, 0x02, 0x4f, 0xde, 0xcc, 0xfe, 0x2e, 0xac, 0xda, + 0x6e, 0x3f, 0xcd, 0x09, 0x87, 0xac, 0x0b, 0x3c, 0x37, 0x3c, 0xe5, 0x19, + 0x0d, 0xcd, 0x9b, 0x9a, 0xee, 0xf6, 0x07, 0x7d, 0x07, 0x21, 0xce, 0x36, + 0xc5, 0xa6, 0x0d, 0xa4, 0x86, 0x43, 0xeb, 0x82, 0x93, 0x58, 0xee, 0x2c, + 0x54, 0xcf, 0xc7, 0xb0, 0xf4, 0xb3, 0x66, 0x4a, 0xf5, 0x82, 0x52, 0x90, + 0xe2, 0x43, 0x2c, 0x97, 0xe7, 0x78, 0xba, 0xd3, 0xc9, 0x60, 0xa1, 0xe6, + 0xe7, 0x30, 0x9f, 0x7c, 0xee, 0x5a, 0x70, 0x9a, 0x55, 0xa6, 0xc2, 0xd3, + 0x2d, 0x16, 0xc6, 0x81, 0x1c, 0xcb, 0x63, 0xda, 0x74, 0x4e, 0xab, 0x3d, + 0x23, 0x59, 0x76, 0xd1, 0xd8, 0xd4, 0xe1, 0xfd, 0xec, 0xbe, 0x72, 0x23, + 0x38, 0xdb, 0xa6, 0xf7, 0xa7, 0x68, 0x16, 0xe1, 0x60, 0xdc, 0x1f, 0xf1, + 0xee, 0x0f, 0x53, 0xc1, 0x94, 0xbc, 0xe4, 0xc2, 0x53, 0x7a, 0x44, 0x46, + 0x14, 0x63, 0xfe, 0x03, 0x94, 0x1a, 0x89, 0x86, 0xc7, 0x26, 0xf9, 0x64, + 0x42, 0x1f, 0x28, 0x2d, 0x6a, 0xc6, 0x69, 0xf9, 0x51, 0xff, 0x6e, 0x7e, + 0xf2, 0x15, 0x9d, 0x16, 0x47, 0xa5, 0x44, 0xe5, 0x54, 0x52, 0x86, 0xa2, + 0xb2, 0xd7, 0xf3, 0xa3, 0x38, 0xd0, 0xfc, 0xb1, 0x78, 0x72, 0xe9, 0xfa, + 0xa3, 0x81, 0x70, 0x34, 0x80, 0x2d, 0x61, 0x8b, 0x4e, 0x86, 0x28, 0xd5, + 0x7c, 0xa6, 0x9f, 0x16, 0x1b, 0x1d, 0xf7, 0x8f, 0x4d, 0x06, 0xb1, 0xc9, + 0x62, 0x3c, 0xc3, 0x45, 0xfc, 0xa9, 0x1e, 0x8d, 0x8f, 0xfb, 0xc6, 0xdc, + 0xbd, 0x5b, 0xdc, 0x7e, 0xf3, 0x9e, 0x45, 0x59, 0x88, 0x47, 0x03, 0xbe, + 0xa0, 0x76, 0x2c, 0x73, 0x54, 0x6a, 0x47, 0x48, 0x25, 0x6d, 0x31, 0x6c, + 0xae, 0x6c, 0x7c, 0x98, 0xfd, 0x92, 0x9e, 0x20, 0x13, 0x02, 0x5e, 0xc7, + 0x4d, 0xbc, 0x8d, 0xe7, 0x72, 0x42, 0x07, 0x17, 0x93, 0xd8, 0xcc, 0xb6, + 0x58, 0x74, 0x94, 0x1c, 0xb1, 0xb8, 0x8f, 0xa7, 0x59, 0x12, 0x56, 0x45, + 0xc5, 0x92, 0x8d, 0x87, 0x23, 0x32, 0x69, 0x8f, 0x81, 0x43, 0x3f, 0xcd, + 0x74, 0x4a, 0x7c, 0x3c, 0x80, 0xe8, 0x88, 0xd2, 0xe2, 0x61, 0xed, 0xa0, + 0xd2, 0xe3, 0x09, 0x9f, 0xee, 0x8a, 0xcf, 0x74, 0x4b, 0xf6, 0x38, 0xf7, + 0x64, 0xde, 0x64, 0x68, 0xb6, 0xc5, 0x9a, 0x3f, 0x4d, 0x6c, 0x59, 0x92, + 0xc2, 0xc9, 0xd0, 0x69, 0x66, 0xdc, 0xb1, 0x4f, 0x3a, 0x9f, 0x54, 0x49, + 0xb6, 0xec, 0xa2, 0xcf, 0x88, 0x8f, 0x7e, 0xb4, 0xbb, 0xf9, 0xc3, 0x1e, + 0xce, 0xc4, 0xf4, 0x79, 0x56, 0x7b, 0xb0, 0x8e, 0x9e, 0x15, 0x1e, 0x6c, + 0xfb, 0x48, 0x20, 0x28, 0x8f, 0xd2, 0xaa, 0x09, 0xcc, 0x00, 0x32, 0xa2, + 0x7e, 0xc4, 0x3a, 0x31, 0x3f, 0x32, 0xe1, 0xdd, 0xaa, 0x30, 0xf7, 0x08, + 0x4c, 0x26, 0x27, 0x62, 0x9e, 0xd5, 0xbb, 0x10, 0x7e, 0xf8, 0x57, 0x78, + 0x26, 0x02, 0xa1, 0x2a, 0x5f, 0x24, 0xe0, 0x59, 0x5d, 0xdf, 0xb8, 0xc2, + 0x13, 0x1b, 0xf7, 0x55, 0xd5, 0xa1, 0x90, 0x6f, 0x95, 0x6f, 0xac, 0xae, + 0xd9, 0xb7, 0xaa, 0xb6, 0x71, 0x65, 0xed, 0xce, 0x55, 0x2d, 0x2d, 0x63, + 0xb5, 0x8d, 0xcd, 0xf5, 0xfe, 0xc6, 0x96, 0x55, 0xa3, 0x75, 0xb5, 0x2d, + 0xbe, 0x55, 0x4d, 0x3b, 0x77, 0xad, 0xda, 0xd5, 0x52, 0x37, 0xc6, 0xb5, + 0xc2, 0x50, 0x62, 0x68, 0x0e, 0x85, 0x9a, 0xab, 0xeb, 0x6a, 0xab, 0x5b, + 0xaa, 0x10, 0x9c, 0x78, 0x3e, 0x42, 0x46, 0xbd, 0x78, 0xc5, 0x58, 0x68, + 0x2f, 0x30, 0x0a, 0xe2, 0x05, 0xfe, 0x82, 0x34, 0x33, 0x29, 0x90, 0x6c, + 0x2a, 0xc8, 0x50, 0x49, 0x5b, 0xc1, 0xc5, 0x32, 0xaf, 0x1a, 0x89, 0xc2, + 0x2a, 0xa3, 0xc6, 0x38, 0x26, 0xec, 0xe9, 0xd7, 0x1b, 0x05, 0x59, 0x49, + 0xd6, 0x99, 0x64, 0x8b, 0xcc, 0x42, 0x7b, 0x65, 0xa1, 0x84, 0x7c, 0x91, + 0x64, 0xcb, 0xd3, 0xed, 0xe0, 0xdd, 0x46, 0x03, 0xeb, 0x88, 0xc2, 0x35, + 0x26, 0xd3, 0x98, 0x54, 0x2c, 0x4e, 0xb2, 0x25, 0x46, 0x1d, 0xd8, 0x85, + 0xf6, 0x6e, 0xe7, 0x21, 0xf9, 0xef, 0x58, 0xec, 0x74, 0x03, 0x7a, 0x57, + 0x8c, 0x3f, 0xd5, 0x99, 0x4a, 0xa5, 0x4a, 0x50, 0xcd, 0x51, 0xaa, 0x46, + 0xb9, 0xd3, 0x29, 0xb5, 0x8c, 0x62, 0x5d, 0x8f, 0x81, 0xcc, 0xf9, 0x2a, + 0xd3, 0x26, 0x33, 0x65, 0xb6, 0xad, 0xd8, 0x9a, 0x7f, 0x45, 0xb2, 0xc9, + 0x6b, 0x54, 0xf7, 0x53, 0x0a, 0x6c, 0x05, 0x1f, 0x2d, 0xe8, 0x2f, 0xd8, + 0x54, 0x90, 0x5e, 0xb0, 0xc9, 0x9c, 0x95, 0x30, 0x46, 0x54, 0x9d, 0x9c, + 0xa4, 0x70, 0xc1, 0xce, 0x82, 0x81, 0xa9, 0xc9, 0x9d, 0x66, 0x92, 0x87, + 0xdf, 0x6f, 0x4d, 0xac, 0x37, 0x13, 0x0e, 0x28, 0x0e, 0x15, 0x04, 0x50, + 0x73, 0x66, 0xb2, 0xd5, 0xcb, 0x8c, 0x5a, 0xa9, 0x58, 0x58, 0x51, 0xb8, + 0xa4, 0x70, 0x69, 0x52, 0x7e, 0x65, 0x92, 0xbd, 0xca, 0xec, 0x98, 0x28, + 0x38, 0xa7, 0x20, 0xb7, 0x60, 0x7f, 0x41, 0xab, 0x59, 0xa3, 0x12, 0xd9, + 0xb1, 0x64, 0xbd, 0x10, 0xc9, 0x8a, 0x32, 0x0b, 0xdd, 0x85, 0x25, 0x85, + 0x95, 0x85, 0x8b, 0x0a, 0x8b, 0x0b, 0x57, 0x14, 0x7a, 0x0a, 0x4b, 0x0b, + 0x97, 0x15, 0x96, 0x15, 0x2e, 0x17, 0x36, 0x91, 0x6e, 0x14, 0x09, 0xc3, + 0x30, 0x84, 0xb1, 0xe6, 0xd0, 0x21, 0xfb, 0xe1, 0xaa, 0xb5, 0xe2, 0xc6, + 0x15, 0x42, 0x3c, 0x0d, 0xbc, 0x05, 0x3c, 0x51, 0x25, 0xc4, 0x3b, 0xc0, + 0x55, 0xd5, 0x42, 0x9c, 0x58, 0x2e, 0xc4, 0xa1, 0x1a, 0xd0, 0x5a, 0x82, + 0xb6, 0x23, 0x47, 0xe4, 0x9c, 0x7b, 0xf9, 0x21, 0xfb, 0xa9, 0xda, 0xf5, + 0xe2, 0xaa, 0x3a, 0x68, 0xd6, 0xf1, 0x0f, 0x15, 0xa4, 0x42, 0xba, 0x01, + 0xf5, 0xfc, 0xa8, 0x8e, 0x32, 0x04, 0xa5, 0x8b, 0x99, 0x7f, 0x1b, 0x51, + 0xe6, 0xfd, 0x3a, 0x22, 0xe1, 0xd8, 0x0e, 0xee, 0x89, 0xe6, 0x0f, 0x1d, + 0x4a, 0x23, 0x31, 0x0d, 0x94, 0x22, 0xd2, 0x72, 0xf5, 0x3b, 0x22, 0xfc, + 0x5f, 0x1f, 0xb7, 0x53, 0xbf, 0x49, 0x1c, 0x69, 0x10, 0xf6, 0xc3, 0x8d, + 0x42, 0x1c, 0xab, 0x17, 0xe2, 0x46, 0xe0, 0x18, 0xf8, 0x67, 0x81, 0xd7, + 0x80, 0xeb, 0x9a, 0xc8, 0x2e, 0x52, 0xe7, 0xeb, 0x12, 0x43, 0x28, 0x71, + 0xc7, 0xca, 0x61, 0x71, 0x62, 0xa5, 0xb0, 0x1f, 0x5b, 0x25, 0xc4, 0x1b, + 0x4d, 0x42, 0xbc, 0xc2, 0x00, 0x7f, 0x5d, 0xb3, 0x10, 0x47, 0x9a, 0xc9, + 0x26, 0xc4, 0xa2, 0x9b, 0xdc, 0x42, 0xfe, 0x3f, 0x7a, 0xf9, 0x11, 0xfb, + 0xfb, 0xcd, 0x63, 0x39, 0xaf, 0x36, 0x63, 0x10, 0xc2, 0x2d, 0x65, 0x7e, + 0xc8, 0x5e, 0x6a, 0xd9, 0x95, 0x73, 0x57, 0x0b, 0xff, 0xc2, 0x01, 0xff, + 0x23, 0x2a, 0x71, 0xa3, 0x5b, 0xd0, 0x6e, 0x8c, 0xee, 0x54, 0x0b, 0xab, + 0x2d, 0x31, 0x20, 0x18, 0x47, 0x43, 0xd7, 0xad, 0x0e, 0x88, 0x47, 0x57, + 0xa3, 0x42, 0x47, 0x05, 0x26, 0x06, 0xc2, 0x3d, 0x10, 0xbe, 0xb4, 0x66, + 0xaf, 0xf8, 0xdb, 0x6a, 0xb4, 0xd8, 0x26, 0xc4, 0xcf, 0x41, 0xef, 0x02, + 0x7d, 0x6b, 0x0d, 0xc6, 0x6d, 0x5c, 0x2f, 0x90, 0xfd, 0xec, 0xf6, 0x1b, + 0x84, 0xf8, 0xc1, 0x76, 0xc8, 0xb6, 0x53, 0x7a, 0xfa, 0xdc, 0xe5, 0x86, + 0x70, 0x25, 0xfe, 0xe3, 0x4a, 0x5c, 0x11, 0x1e, 0xc1, 0xb9, 0xe2, 0xb0, + 0xfb, 0xd1, 0xb5, 0x86, 0xf1, 0x4e, 0xa7, 0x30, 0x7e, 0xd0, 0x2d, 0x32, + 0x5e, 0x01, 0x7f, 0xaa, 0x4b, 0x18, 0xc7, 0xd7, 0xd9, 0x8c, 0xe3, 0x1d, + 0xe9, 0xc6, 0xa3, 0xe7, 0x47, 0xed, 0x8f, 0xf6, 0xda, 0xc5, 0xb3, 0x3d, + 0xb6, 0x43, 0xee, 0xbb, 0xda, 0xc5, 0x21, 0xf7, 0x5b, 0xed, 0x86, 0x38, + 0xd2, 0x87, 0xe6, 0x36, 0x09, 0x71, 0x7c, 0x8b, 0x10, 0xa7, 0xbc, 0x98, + 0xa6, 0x4e, 0x21, 0x5e, 0x05, 0xde, 0x5b, 0x8b, 0xc1, 0x9f, 0x6f, 0x88, + 0x47, 0xb7, 0xa2, 0x5b, 0x5b, 0xd1, 0x13, 0xd1, 0x28, 0xae, 0x16, 0x18, + 0xcf, 0xf1, 0x11, 0x71, 0x85, 0xc1, 0xef, 0x23, 0xbc, 0x3a, 0x82, 0x51, + 0x18, 0xea, 0x1f, 0x34, 0x7d, 0x82, 0xbb, 0x79, 0xff, 0xf6, 0x6b, 0xb1, + 0xf8, 0x17, 0x62, 0x02, 0x2f, 0xb4, 0x53, 0x26, 0x35, 0x17, 0x89, 0x22, + 0x91, 0x21, 0x32, 0x6e, 0x14, 0x87, 0x0f, 0xd9, 0xaf, 0xda, 0xc9, 0x1a, + 0x77, 0xec, 0x44, 0x1f, 0x8f, 0xf3, 0xc7, 0x2b, 0x3b, 0x45, 0xfa, 0xb3, + 0x1f, 0x12, 0xe9, 0xc7, 0x76, 0x88, 0xb4, 0xbf, 0x6d, 0x17, 0x69, 0xc7, + 0x7d, 0x22, 0xed, 0xba, 0x8b, 0xc4, 0x27, 0x0c, 0x92, 0x28, 0x70, 0x18, + 0x76, 0xc1, 0x6f, 0xb4, 0xf0, 0x37, 0xda, 0xbf, 0xe8, 0x57, 0xf4, 0x5d, + 0x4d, 0xf9, 0x07, 0xf2, 0xf8, 0x3b, 0xf0, 0x39, 0xa0, 0x79, 0x16, 0xbe, + 0xc4, 0xc2, 0x2f, 0x3b, 0x4f, 0xe9, 0x36, 0x59, 0x64, 0x9d, 0x16, 0x7e, + 0x93, 0xce, 0xdf, 0xaa, 0xe9, 0x4e, 0x4b, 0xde, 0x5e, 0x0b, 0xbf, 0x4f, + 0xe7, 0x5f, 0xae, 0x65, 0x9f, 0x02, 0xbd, 0x57, 0xcb, 0x1e, 0xb5, 0xc8, + 0x9e, 0xd7, 0xb2, 0x6f, 0x58, 0x64, 0x3f, 0xb3, 0xd4, 0xf3, 0xb6, 0xce, + 0x7f, 0x4f, 0xd3, 0xd4, 0x01, 0x45, 0xf3, 0x35, 0x5d, 0xac, 0x69, 0xa3, + 0xa6, 0x1d, 0x9a, 0x6e, 0x1e, 0x50, 0xdf, 0xfd, 0x73, 0x1d, 0x3b, 0xc0, + 0xef, 0x19, 0x48, 0xd6, 0xb9, 0xdf, 0xc2, 0x5f, 0xa1, 0xf5, 0xaf, 0xd7, + 0xf4, 0x88, 0xa6, 0x8f, 0x83, 0x5e, 0x38, 0x94, 0x7c, 0xef, 0x80, 0xf4, + 0xbb, 0x04, 0x5e, 0xc8, 0xda, 0x41, 0xbb, 0x87, 0xd4, 0x7b, 0x06, 0x7d, + 0xa0, 0xe3, 0x43, 0x34, 0xe5, 0x2f, 0x3e, 0x2d, 0x7d, 0x78, 0x28, 0x59, + 0x07, 0xff, 0xf1, 0xfb, 0x94, 0x87, 0x86, 0xd4, 0xbf, 0x2b, 0x67, 0x7a, + 0xeb, 0x34, 0xfd, 0xe3, 0x3a, 0x6d, 0x96, 0x91, 0xbf, 0x7f, 0x36, 0xa4, + 0x7e, 0xab, 0xeb, 0x18, 0xe8, 0xb3, 0x3a, 0xdf, 0x7c, 0x0f, 0xe5, 0x7c, + 0x9d, 0xff, 0xda, 0x34, 0x39, 0xff, 0x6e, 0xe1, 0x2b, 0x90, 0x9d, 0x9a, + 0x26, 0xe7, 0xdf, 0x32, 0x7c, 0x03, 0xb2, 0xf7, 0xa7, 0xc9, 0xf9, 0xf7, + 0x0d, 0xdf, 0x81, 0xcc, 0x3e, 0x3c, 0xb5, 0x3f, 0xeb, 0x87, 0xd5, 0x7b, + 0x9d, 0x66, 0x7f, 0xf8, 0x3d, 0x8b, 0xc2, 0x61, 0xf5, 0x3b, 0x10, 0xcd, + 0xc3, 0xea, 0xb7, 0x1f, 0x6b, 0x87, 0xd5, 0xef, 0x3f, 0xb6, 0x0f, 0xab, + 0x7f, 0x43, 0xe6, 0x1e, 0x56, 0xbf, 0x03, 0x59, 0x31, 0xac, 0x7e, 0x23, + 0x91, 0xfb, 0xc7, 0xff, 0x5e, 0x82, 0xff, 0x89, 0xf0, 0x05, 0xc3, 0x53, + 0xdb, 0xbd, 0x46, 0x8f, 0x6b, 0x6c, 0x5a, 0xbb, 0x87, 0x86, 0xa7, 0xce, + 0x03, 0xff, 0x26, 0x5b, 0x64, 0x58, 0xfd, 0x1e, 0xdb, 0x81, 0xe1, 0xe4, + 0xef, 0x0a, 0x5a, 0xdf, 0xc7, 0x31, 0x7f, 0x03, 0x98, 0xcb, 0x98, 0xbf, + 0x03, 0xcc, 0xeb, 0xe6, 0x23, 0xf5, 0x5b, 0xc0, 0xfc, 0xce, 0x87, 0xf9, + 0x7b, 0xc0, 0xfc, 0x7e, 0x89, 0xf9, 0x9b, 0xc0, 0x3c, 0x3e, 0xf3, 0x77, + 0x81, 0x85, 0x5b, 0x95, 0xe5, 0xdf, 0x06, 0xb6, 0xb9, 0xd5, 0x6f, 0xe6, + 0xf1, 0x6f, 0x59, 0x08, 0xa7, 0xfa, 0x5d, 0x4b, 0xfe, 0xcd, 0x0f, 0xc3, + 0xad, 0xda, 0xe2, 0xdf, 0x0e, 0xb6, 0xbb, 0xd5, 0xbb, 0x96, 0xbc, 0xdf, + 0xf8, 0x65, 0x17, 0xae, 0x87, 0x7f, 0x0b, 0xc5, 0xe1, 0xd6, 0xef, 0xe9, + 0xf0, 0x6f, 0x62, 0x38, 0xd5, 0xef, 0x64, 0xb0, 0xdd, 0xa4, 0xb8, 0xd5, + 0xfb, 0x34, 0xfc, 0x9b, 0x29, 0x6c, 0x04, 0xdc, 0x6f, 0xfe, 0xfd, 0xe2, + 0xff, 0x07, 0xf7, 0x0e, 0xe8, 0x14, 0xf8, 0x58, 0x00, 0x00 }; #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ - METHOD (getJuceAndroidMidiInputDeviceNameAndIDs, "getJuceAndroidMidiInputDeviceNameAndIDs", "()[Ljava/lang/String;") \ - METHOD (getJuceAndroidMidiOutputDeviceNameAndIDs, "getJuceAndroidMidiOutputDeviceNameAndIDs", "()[Ljava/lang/String;") \ - METHOD (openMidiInputPortWithID, "openMidiInputPortWithID", "(IJ)Lcom/rmsl/juce/JuceMidiSupport$JuceMidiPort;") \ - METHOD (openMidiOutputPortWithID, "openMidiOutputPortWithID", "(I)Lcom/rmsl/juce/JuceMidiSupport$JuceMidiPort;") + STATICMETHOD (nanoTime, "nanoTime", "()J") -DECLARE_JNI_CLASS (MidiDeviceManager, "com/rmsl/juce/JuceMidiSupport$MidiDeviceManager") +DECLARE_JNI_CLASS (JavaLangSystem, "java/lang/System") #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ - METHOD (start, "start", "()V") \ - METHOD (stop, "stop", "()V") \ - METHOD (close, "close", "()V") \ - METHOD (sendMidi, "sendMidi", "([BII)V") \ - METHOD (getName, "getName", "()Ljava/lang/String;") + METHOD (openMidiInputPortWithID, "openMidiInputPortWithID", "(IIJ)Lcom/rmsl/juce/JuceMidiSupport$JuceMidiPort;") \ + METHOD (openMidiOutputPortWithID, "openMidiOutputPortWithID", "(II)Lcom/rmsl/juce/JuceMidiSupport$JuceMidiPort;") -DECLARE_JNI_CLASS (JuceMidiPort, "com/rmsl/juce/JuceMidiSupport$JuceMidiPort") +DECLARE_JNI_CLASS_WITH_BYTECODE (MidiDeviceManager, "com/rmsl/juce/JuceMidiSupport$MidiDeviceManager", 24, javaMidiByteCode) #undef JNI_CLASS_MEMBERS -//============================================================================== -class MidiInput::Pimpl -{ -public: - Pimpl (MidiInput* midiInput, int deviceID, juce::MidiInputCallback* midiInputCallback, jobject deviceManager) - : juceMidiInput (midiInput), - callback (midiInputCallback), - midiConcatenator (2048), - javaMidiDevice (LocalRef (getEnv()->CallObjectMethod (deviceManager, - MidiDeviceManager.openMidiInputPortWithID, - (jint) deviceID, - (jlong) this))) - { - } - - ~Pimpl() - { - if (jobject d = javaMidiDevice.get()) - { - getEnv()->CallVoidMethod (d, JuceMidiPort.close); - javaMidiDevice.clear(); - } - } - - bool isOpen() const noexcept - { - return javaMidiDevice != nullptr; - } - - void start() - { - if (jobject d = javaMidiDevice.get()) - getEnv()->CallVoidMethod (d, JuceMidiPort.start); - } - - void stop() - { - if (jobject d = javaMidiDevice.get()) - getEnv()->CallVoidMethod (d, JuceMidiPort.stop); - - callback = nullptr; - } - - String getName() const noexcept - { - if (jobject d = javaMidiDevice.get()) - return juceString (LocalRef ((jstring) getEnv()->CallObjectMethod (d, JuceMidiPort.getName))); - - return {}; - } - - static void handleReceive (JNIEnv* env, Pimpl& myself, jbyteArray byteArray, jint offset, jint len, jlong timestamp) - { - jassert (byteArray != nullptr); - auto* data = env->GetByteArrayElements (byteArray, nullptr); - - std::vector buffer (static_cast (len)); - std::memcpy (buffer.data(), data + offset, static_cast (len)); - - myself.midiConcatenator.pushMidiData (buffer.data(), - len, - static_cast (timestamp) * 1.0e-9, - myself.juceMidiInput, - *myself.callback); - - env->ReleaseByteArrayElements (byteArray, data, 0); - } - -private: - MidiInput* juceMidiInput; - MidiInputCallback* callback; - MidiDataConcatenator midiConcatenator; - GlobalRef javaMidiDevice; -}; - -//============================================================================== -class MidiOutput::Pimpl -{ -public: - Pimpl (const LocalRef& midiDevice) - : javaMidiDevice (midiDevice) - { - } - - ~Pimpl() - { - if (jobject d = javaMidiDevice.get()) - { - getEnv()->CallVoidMethod (d, JuceMidiPort.close); - javaMidiDevice.clear(); - } - } - - void send (jbyteArray byteArray, jint offset, jint len) - { - if (jobject d = javaMidiDevice.get()) - getEnv()->CallVoidMethod (d, - JuceMidiPort.sendMidi, - byteArray, offset, len); - } - - String getName() const noexcept - { - if (jobject d = javaMidiDevice.get()) - return juceString (LocalRef ((jstring) getEnv()->CallObjectMethod (d, JuceMidiPort.getName))); - - return {}; - } - -private: - GlobalRef javaMidiDevice; -}; - -//============================================================================== #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ - CALLBACK (generatedCallback<&MidiInput::Pimpl::handleReceive>, "handleReceive", "(J[BIIJ)V" ) + FIELD (deviceId, "deviceId", "I") \ + FIELD (portIndex, "portIndex", "I") \ + FIELD (type, "type", "I") \ -DECLARE_JNI_CLASS (JuceMidiInputPort, "com/rmsl/juce/JuceMidiSupport$JuceMidiInputPort") +DECLARE_JNI_CLASS_WITH_BYTECODE (JavaPortPath, "com/rmsl/juce/JuceMidiSupport$PortPath", 24, javaMidiByteCode) #undef JNI_CLASS_MEMBERS -//============================================================================== -class AndroidMidiDeviceManager +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + METHOD (constructor, "", "(J)V") \ + +DECLARE_JNI_CLASS_WITH_BYTECODE (NativeMidiReceiver, "com/rmsl/juce/JuceMidiSupport$NativeMidiReceiver", 24, javaMidiByteCode) +#undef JNI_CLASS_MEMBERS + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + METHOD (send, "send", "([BIIJ)V") \ + +DECLARE_JNI_CLASS (JavaMidiReceiver, "android/media/midi/MidiReceiver") +#undef JNI_CLASS_MEMBERS + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + FIELD (name, "name", "Ljava/lang/String;") \ + FIELD (manufacturer, "manufacturer", "Ljava/lang/String;") \ + FIELD (product, "product", "Ljava/lang/String;") \ + FIELD (serialNumber, "serialNumber", "Ljava/lang/String;") \ + FIELD (id, "id", "I") \ + FIELD (transport, "transport", "I") \ + FIELD (type, "type", "I") \ + FIELD (dst, "dst", "Ljava/util/ArrayList;") \ + FIELD (src, "src", "Ljava/util/ArrayList;") + +DECLARE_JNI_CLASS_WITH_BYTECODE (JuceMidiDeviceInfo, "com/rmsl/juce/JuceMidiSupport$JuceMidiDeviceInfo", 24, javaMidiByteCode) +#undef JNI_CLASS_MEMBERS + +struct AndroidMidiHelpers { -public: - AndroidMidiDeviceManager() = default; + static constexpr int TRANSPORT_BYTESTREAM = 1; + static constexpr int TRANSPORT_UMP = 2; - Array getDevices (bool input) + static jlong midiClockMillis (JNIEnv* env) { - if (jobject dm = deviceManager.get()) - { - jobjectArray jDeviceNameAndIDs - = (jobjectArray) getEnv()->CallObjectMethod (dm, input ? MidiDeviceManager.getJuceAndroidMidiInputDeviceNameAndIDs - : MidiDeviceManager.getJuceAndroidMidiOutputDeviceNameAndIDs); - - // Create a local reference as converting this to a JUCE string will call into JNI - LocalRef localDeviceNameAndIDs (jDeviceNameAndIDs); - - auto deviceNameAndIDs = javaStringArrayToJuce (localDeviceNameAndIDs); - deviceNameAndIDs.appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 ("")); - - Array devices; - - for (int i = 0; i < deviceNameAndIDs.size(); i += 2) - devices.add ({ deviceNameAndIDs[i], deviceNameAndIDs[i + 1] }); - - return devices; - } - - return {}; + const auto nanos = env->CallStaticLongMethod (JavaLangSystem, JavaLangSystem.nanoTime); + return nanos / (jlong) 1e6; } - MidiInput::Pimpl* openMidiInputPortWithID (int deviceID, MidiInput* juceMidiInput, juce::MidiInputCallback* callback) + /* When dealing in UMP, the system always sends/receives ump packets in network order, i.e. + big-endian. + + This function will ensure the correct byte ordering for a range of bytes that are about + to cross the API boundary to/from android. + */ + static void swapByteOrderOfWordsIfNecessary (Span bytes) { - if (auto dm = deviceManager.get()) + if ((bytes.size() % 4) != 0) { - auto androidMidiInput = std::make_unique (juceMidiInput, deviceID, callback, dm); - - if (androidMidiInput->isOpen()) - return androidMidiInput.release(); - - // Perhaps the device is already open + // We're expecting to receive 32-bit words, but the system hasn't given us a whole + // number of words. jassertfalse; + return; } - return nullptr; + #if JUCE_LITTLE_ENDIAN + // Words are in network order (big endian) + for (auto i = decltype (bytes.size()) {}; i < bytes.size(); i += sizeof (uint32_t)) + std::reverse (bytes.data() + i, bytes.data() + i + sizeof (uint32_t)); + #endif } - MidiOutput::Pimpl* openMidiOutputPortWithID (int deviceID) + /* The device info is all out-of-band. These are properties of the device that we can fetch + without needing to open a connection to the device. + This differs from a ump::Endpoint, which requires talking to the device in order to + retrieve some of the information. + We can still use this info to create a stand-in endpoint for a device that's only aware of + MIDI 1.0. + */ + struct StaticDeviceInfo { - if (auto dm = deviceManager.get()) + String name; ///< PROPERTY_NAME + String manufacturer; ///< PROPERTY_MANUFACTURER + String product; ///< PROPERTY_PRODUCT + String serialNumber; ///< PROPERTY_SERIAL_NUMBER + std::vector dst; ///< input port names + port count + std::vector src; ///< output port names + port count + jint id; ///< device id + jint transport; ///< transport, bytestream or ump + jint type; ///< type, virtual/usb/bluetooth + + /* This returns the name used by old JUCE versions to identify a specific port */ + String getLegacyId (size_t portIndex, ump::IOKind direction) const { - if (auto javaMidiPort = getEnv()->CallObjectMethod (dm, MidiDeviceManager.openMidiOutputPortWithID, (jint) deviceID)) - return new MidiOutput::Pimpl (LocalRef (javaMidiPort)); - - // Perhaps the port is already open - jassertfalse; + jassert (portIndex < (direction == ump::IOKind::dst ? dst.size() : src.size())); + return String { ((id * 128) + jmin ((int) portIndex, 127)) * (direction == ump::IOKind::dst ? -1 : 1) }; } - return nullptr; - } + std::array getIds (ump::IOKind dir) const + { + auto& names = dir == ump::IOKind::src ? src : dst; + jassert (names.size() <= 16); -private: - static void handleDevicesChanged (JNIEnv*, jclass) + std::array result; + + for (size_t i = 0; i < names.size(); ++i) + result[i] = getLegacyId (i, dir); + + return result; + } + + /* Result does *not* have an EndpointId set */ + ump::Endpoint getEquivalentBytestreamEndpoint() const + { + // This isn't a bytestream device! + jassert (transport == TRANSPORT_BYTESTREAM); + + size_t numBlocks{}; + std::array blocks; + + for (auto [index, portName] : enumerate (dst, uint8_t{})) + { + blocks[numBlocks++] = ump::Block{}.withEnabled (true) + .withDirection (ump::BlockDirection::receiver) + .withUiHint (ump::BlockUiHint::receiver) + .withFirstGroup (index) + .withNumGroups (1) + .withMIDI1ProxyKind (ump::BlockMIDI1ProxyKind::unrestrictedBandwidth) + .withName (portName.isNotEmpty() ? portName : "Input Port " + String (index + 1)); + } + + for (auto [index, portName] : enumerate (src, uint8_t{})) + { + blocks[numBlocks++] = ump::Block{}.withEnabled (true) + .withDirection (ump::BlockDirection::sender) + .withUiHint (ump::BlockUiHint::sender) + .withFirstGroup (index) + .withNumGroups (1) + .withMIDI1ProxyKind (ump::BlockMIDI1ProxyKind::unrestrictedBandwidth) + .withName (portName.isNotEmpty() ? portName : "Output Port " + String (index + 1)); + } + + return ump::Endpoint{}.withName (name) + .withMidi1Support (true) + .withStaticBlocks (true) + .withProductInstanceId (serialNumber) + .withProtocol (ump::PacketProtocol::MIDI_1_0) + .withBlocks ({ blocks.data(), numBlocks }); + } + + ump::StaticDeviceInfo getUmpStaticDeviceInfo() const + { + return ump::StaticDeviceInfo{}.withName (name) + .withManufacturer (manufacturer) + .withProduct (product) + .withTransport (transport == TRANSPORT_UMP ? ump::Transport::ump + : ump::Transport::bytestream) + .withHasSource (! src.empty()) + .withHasDestination (! dst.empty()) + .withLegacyIdentifiersSrc (getIds (ump::IOKind::src)) + .withLegacyIdentifiersDst (getIds (ump::IOKind::dst)); + } + }; + + struct PortPath { - MidiDeviceListConnectionBroadcaster::get().notify(); - } + int deviceID; + int portIndex; + int type; + }; - #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ - CALLBACK (handleDevicesChanged, "handleDevicesChanged", "()V" ) \ - STATICMETHOD (getAndroidMidiDeviceManager, "getAndroidMidiDeviceManager", "(Landroid/content/Context;)Lcom/rmsl/juce/JuceMidiSupport$MidiDeviceManager;") \ - STATICMETHOD (getAndroidBluetoothManager, "getAndroidBluetoothManager", "(Landroid/content/Context;)Lcom/rmsl/juce/JuceMidiSupport$BluetoothMidiManager;") + class MidiReceiver + { + public: + MidiReceiver() = default; + explicit MidiReceiver (GlobalRef g) : port (g) {} - DECLARE_JNI_CLASS_WITH_BYTECODE (JuceMidiSupport, "com/rmsl/juce/JuceMidiSupport", 23, javaMidiByteCode) - #undef JNI_CLASS_MEMBERS + void send (jbyteArray byteArray, jint len) + { + getEnv()->CallVoidMethod (port, JavaMidiReceiver.send, byteArray, (jint) 0, len, (jlong) 0); + } - GlobalRef deviceManager { LocalRef (getEnv()->CallStaticObjectMethod (JuceMidiSupport, - JuceMidiSupport.getAndroidMidiDeviceManager, - getAppContext().get())) }; + bool operator== (const MidiReceiver& other) const + { + return port == other.port; + } + + bool operator!= (const MidiReceiver& other) const + { + return ! operator== (other); + } + + bool operator< (const MidiReceiver& other) const + { + return port < other.port; + } + + GlobalRef get() const { return port; } + + private: + GlobalRef port; + }; + + /** Very low-level RAII wrapper over JUCE's Java MIDI port. + For simplicity we use the same interface for sources and destinations, so some functions + won't work depending on the actual type of the backing port. + This may deal in either bytestream or UMP messages depending on the result of isUMP(). + */ + class MidiPort + { + public: + MidiPort() = default; + explicit MidiPort (GlobalRef p) : port (p) {} + + MidiPort (MidiPort&& other) noexcept + : port (std::exchange (other.port, {})) + { + } + + MidiPort& operator= (MidiPort&& other) noexcept + { + std::swap (other.port, port); + return *this; + } + + ~MidiPort() noexcept + { + if (port != nullptr) + getEnv()->CallVoidMethod (port, JuceMidiPort.close); + } + + void start() + { + jassert (port != nullptr); + getEnv()->CallVoidMethod (port, JuceMidiPort.start); + } + + void stop() + { + jassert (port != nullptr); + getEnv()->CallVoidMethod (port, JuceMidiPort.stop); + } + + bool isActive() const + { + jassert (port != nullptr); + return getEnv()->CallBooleanMethod (port, JuceMidiPort.isActive); + } + + void sendMidi (jbyteArray byteArray, jint len) + { + jassert (port != nullptr); + getEnv()->CallVoidMethod (port, JavaMidiReceiver.send, byteArray, (jint) 0, len, (jlong) 0); + } + + std::optional getPortPath() const + { + auto* env = getEnv(); + const LocalRef result { env->CallObjectMethod (port, JuceMidiPort.getPortPath) }; + + if (result == nullptr) + return {}; + + return PortPath + { + env->GetIntField (result, JavaPortPath.deviceId), + env->GetIntField (result, JavaPortPath.portIndex), + env->GetIntField (result, JavaPortPath.type), + }; + } + + bool operator== (std::nullptr_t) const { return port == nullptr; } + bool operator!= (std::nullptr_t other) const { return ! operator== (other); } + + bool operator== (const MidiPort& other) const { return port == other.port; } + bool operator!= (const MidiPort& other) const { return port != other.port; } + + bool operator< (const MidiPort& other) const + { + return port.get() < other.port.get(); + } + + explicit operator bool() const { return operator!= (nullptr); } + + GlobalRef get() const { return port; } + + private: + GlobalRef port; + + #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + METHOD (start, "start", "()V") \ + METHOD (stop, "stop", "()V") \ + METHOD (close, "close", "()V") \ + METHOD (isActive, "isActive", "()Z") \ + METHOD (send, "send", "([BIIJ)V") \ + METHOD (getPortPath, "getPortPath", "()Lcom/rmsl/juce/JuceMidiSupport$PortPath;") \ + + DECLARE_JNI_CLASS_WITH_BYTECODE (JuceMidiPort, "com/rmsl/juce/JuceMidiSupport$JuceMidiPort", 24, javaMidiByteCode) + #undef JNI_CLASS_MEMBERS + }; + + struct DeviceManagerListener + { + virtual ~DeviceManagerListener() = default; + virtual void deviceAdded (const StaticDeviceInfo&) = 0; + virtual void deviceRemoved (int) = 0; + }; + + /* Dispatches device-change events onto the main thread */ + class DeviceListener : private AsyncUpdater + { + public: + ~DeviceListener() override + { + cancelPendingUpdate(); + } + + void addListener (DeviceManagerListener& l) + { + listeners.add (&l); + } + + void removeListener (DeviceManagerListener& l) + { + listeners.remove (&l); + } + + void deviceAdded (const StaticDeviceInfo& info) + { + { + const std::scoped_lock lock { mutex }; + events.push_back (info); + } + + triggerAsyncUpdate(); + } + + void deviceRemoved (int id) + { + { + const std::scoped_lock lock { mutex }; + events.push_back (id); + } + + triggerAsyncUpdate(); + } + + private: + void handleAsyncUpdate() override + { + const auto copy = std::invoke ([&] + { + const std::scoped_lock lock { mutex }; + auto result = events; + events.clear(); + return result; + }); + + for (const auto& item : copy) + { + if (auto* x = std::get_if (&item)) + listeners.call ([&] (auto& l) { l.deviceAdded (*x); }); + + if (auto* x = std::get_if (&item)) + listeners.call ([&] (auto& l) { l.deviceRemoved (*x); }); + } + } + + using Event = std::variant; + std::vector events; + std::mutex mutex; + + ListenerList listeners; + }; + + /** Provides notifications when a MIDI device is added/removed, and allows creating MidiPorts + referring to known device ports. + */ + class DeviceManager + { + public: + static std::shared_ptr getSingleton() + { + static std::weak_ptr weak; + + if (auto strong = weak.lock()) + return strong; + + LocalRef manager { getEnv()->CallStaticObjectMethod (JuceMidiSupport, + JuceMidiSupport.getAndroidMidiDeviceManager, + getAppContext().get()) }; + + if (manager == nullptr) + return {}; + + std::shared_ptr strong (new DeviceManager { GlobalRef { std::move (manager) } }); + weak = strong; + return strong; + } + + MidiPort openMidiInputPortWithID (jint deviceID, jint portIndex, jlong host) const + { + return MidiPort { GlobalRef { LocalRef (getEnv()->CallObjectMethod (deviceManager, + MidiDeviceManager.openMidiInputPortWithID, + deviceID, + portIndex, + host)) } }; + } + + MidiPort openMidiOutputPortWithID (jint deviceID, jint portIndex) const + { + return MidiPort { GlobalRef { LocalRef (getEnv()->CallObjectMethod (deviceManager, + MidiDeviceManager.openMidiOutputPortWithID, + deviceID, + portIndex)) } }; + } + + void addListener (DeviceManagerListener& l) + { + listener.addListener (l); + } + + void removeListener (DeviceManagerListener& l) + { + listener.removeListener (l); + } + + private: + explicit DeviceManager (GlobalRef g) + : deviceManager (g) + { + } + + static std::vector arrayListStringToVector (JNIEnv* env, jobject obj) + { + const auto size = env->CallIntMethod (obj, JavaArrayList.size); + std::vector result; + result.reserve ((size_t) size); + + for (jint i = 0; i < size; ++i) + result.push_back (juceString ((jstring) env->CallObjectMethod (obj, JavaArrayList.get, i))); + + return result; + } + + static void handleDeviceAdded (JNIEnv* env, jclass, jobject obj) + { + if (obj == nullptr) + return; + + StaticDeviceInfo info; + info.name = juceString ((jstring) env->GetObjectField (obj, JuceMidiDeviceInfo.name)); + info.manufacturer = juceString ((jstring) env->GetObjectField (obj, JuceMidiDeviceInfo.manufacturer)); + info.serialNumber = juceString ((jstring) env->GetObjectField (obj, JuceMidiDeviceInfo.serialNumber)); + info.product = juceString ((jstring) env->GetObjectField (obj, JuceMidiDeviceInfo.product)); + + info.id = env->GetIntField (obj, JuceMidiDeviceInfo.id); + info.transport = env->GetIntField (obj, JuceMidiDeviceInfo.transport); + info.type = env->GetIntField (obj, JuceMidiDeviceInfo.type); + + info.src = arrayListStringToVector (env, env->GetObjectField (obj, JuceMidiDeviceInfo.src)); + info.dst = arrayListStringToVector (env, env->GetObjectField (obj, JuceMidiDeviceInfo.dst)); + + if (auto strong = getSingleton()) + strong->listener.deviceAdded (info); + } + + static void handleDeviceRemoved (JNIEnv*, jclass, jint id) + { + if (auto strong = getSingleton()) + strong->listener.deviceRemoved (id); + } + + DeviceListener listener; + GlobalRef deviceManager; + + #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + CALLBACK (handleDeviceAdded, "handleDeviceAdded", "(Lcom/rmsl/juce/JuceMidiSupport$JuceMidiDeviceInfo;)V") \ + CALLBACK (handleDeviceRemoved, "handleDeviceRemoved", "(I)V" ) \ + STATICMETHOD (getAndroidMidiDeviceManager, "getAndroidMidiDeviceManager", "(Landroid/content/Context;)Lcom/rmsl/juce/JuceMidiSupport$MidiDeviceManager;") \ + STATICMETHOD (getAndroidBluetoothManager, "getAndroidBluetoothManager", "(Landroid/content/Context;)Lcom/rmsl/juce/JuceMidiSupport$BluetoothMidiManager;") \ + + DECLARE_JNI_CLASS_WITH_BYTECODE (JuceMidiSupport, "com/rmsl/juce/JuceMidiSupport", 24, javaMidiByteCode) + #undef JNI_CLASS_MEMBERS + }; + + /* A source of MIDI messages. + This converts bytestream to UMP if necessary, but performs no other conversions. + */ + class MidiSource + { + public: + MidiSource (ump::Consumer& c, uint8_t portIndex, bool isUmpIn) + : consumer (c), + bytestreamDispatcher { portIndex, ump::PacketProtocol::MIDI_1_0, 4096 }, + isUmp (isUmpIn) + { + } + + private: + void handleInput (JNIEnv* env, jbyteArray byteArray, jint offset, jint len, jlong timestamp) + { + jassert (byteArray != nullptr); + thread_local std::vector buffer (4096); + + { + auto* data = env->GetByteArrayElements (byteArray, nullptr); + auto* bytes = reinterpret_cast (data + offset); + + buffer.resize ((size_t) len); + std::copy (bytes, bytes + len, buffer.begin()); + + env->ReleaseByteArrayElements (byteArray, data, 0); + } + + const auto time = convertNativeTimestamp (timestamp); + + const auto send = [this] (const ump::View& view, double t) + { + const ump::Iterator b { view.data(), view.size() }; + consumer.consume (b, std::next (b), t); + }; + + if (isUmp) + { + swapByteOrderOfWordsIfNecessary (buffer); + + const auto words = reinterpret_cast (buffer.data()); + const auto numWords = buffer.size() / sizeof (uint32_t); + + umpDispatcher.dispatch ({ words, numWords }, time, send); + } + else + { + bytestreamDispatcher.dispatch (buffer, time, send); + } + } + + /* Native timestamps are based on System.nanoTime + JUCE timestamps use Time::getMillisecondCounter + */ + double convertNativeTimestamp (jlong absoluteNanos) const + { + const auto millis = std::invoke ([&] + { + // A timestamp of 0 means ASAP + if (absoluteNanos == 0) + return (double) Time::getMillisecondCounter(); + + const auto elapsedNanos = absoluteNanos - startTimeNative; + const auto elapsedMillis = elapsedNanos / (jlong) 1e6; + return (double) (startTimeMillis + elapsedMillis); + }); + + return millis * 0.001; + } + + static void handleReceive (JNIEnv* env, + MidiSource& myself, + jbyteArray byteArray, + jint offset, + jint len, + jlong timestamp) + { + myself.handleInput (env, byteArray, offset, len, timestamp); + } + + ump::Consumer& consumer; + ump::Dispatcher umpDispatcher; + ump::BytestreamToUMPDispatcher bytestreamDispatcher; + + const jlong startTimeNative = midiClockMillis (getEnv()); + const uint32 startTimeMillis = Time::getMillisecondCounter(); + + bool isUmp; + + #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + CALLBACK (generatedCallback<&MidiSource::handleReceive>, "handleReceive", "(J[BIIJ)V") + + DECLARE_JNI_CLASS (JuceMidiInputPort, "com/rmsl/juce/JuceMidiSupport$NativeMidiReceiver") + #undef JNI_CLASS_MEMBERS + }; + + /* A destination for MIDI messages. + This converts UMP to bytestream if necessary, but performs no other conversions. + If the destination is using UMP transport, this won't convert to the target's preferred protocol. + The connection is exclusive, so this should be shared between all entities that want to + concurrently send MIDI to this destination. + */ + class MidiDestination + { + public: + static std::unique_ptr make (MidiReceiver outputReceiver, bool isUmp) + { + if (outputReceiver == MidiReceiver{}) + return {}; + + auto result = rawToUniquePtr (new MidiDestination); + result->outputReceiver = std::move (outputReceiver); + result->isUMP = isUmp; + return result; + } + + bool send (ump::View v) + { + ump::Iterator b { v.data(), v.size() }; + return send (b, std::next (b)); + } + + bool send (ump::Iterator b, ump::Iterator e) + { + if (outputReceiver == MidiReceiver{}) + return false; + + if (isUMP) + { + auto* env = getEnv(); + const auto messageSize = size_t (e->data() - b->data()) * sizeof (uint32_t); + + LocalRef messageContent (env->NewByteArray ((jsize) messageSize)); + + auto* rawBytes = env->GetByteArrayElements (messageContent.get(), nullptr); + std::memcpy (rawBytes, b->data(), static_cast (messageSize)); + + swapByteOrderOfWordsIfNecessary (Span (unalignedPointerCast (rawBytes), messageSize)); + + env->ReleaseByteArrayElements (messageContent.get(), rawBytes, 0); + + outputReceiver.send (messageContent.get(), (jint) messageSize); + } + else + { + for (auto v : makeRange (b, e)) + { + toBytestream.convert (v, 0.0, [&] (ump::BytesOnGroup bytesView, double) + { + auto *env = getEnv(); + const auto messageSize = (jsize) bytesView.bytes.size(); + + LocalRef messageContent (env->NewByteArray (messageSize)); + + auto *rawBytes = env->GetByteArrayElements (messageContent.get(), nullptr); + std::memcpy (rawBytes, bytesView.bytes.data(), static_cast (messageSize)); + env->ReleaseByteArrayElements (messageContent.get(), rawBytes, 0); + + outputReceiver.send (messageContent.get(), messageSize); + }); + } + } + + return true; + } + + private: + ump::ToBytestreamConverter toBytestream { 4096 }; + MidiReceiver outputReceiver; + bool isUMP; + }; + + class ConverterWithOptionalProtocol + { + public: + void setProtocol (std::optional newProtocol) + { + const auto previousProtocol = std::invoke ([&] + { + const SpinLock::ScopedLockType lock { mutex }; + return converter.has_value() ? std::make_optional (converter->getProtocol()) : std::nullopt; + }); + + if (newProtocol == previousProtocol) + return; + + const SpinLock::ScopedLockType lock { mutex }; + + if (newProtocol.has_value()) + converter.emplace (*newProtocol); + else + converter.reset(); + } + + template + bool convert (ump::Iterator b, ump::Iterator e, Callback&& callback) + { + SpinLock::ScopedTryLockType lock { mutex }; + + if (! lock.isLocked()) + { + // There's contention on the output lock! + jassertfalse; + return false; + } + + if (! converter.has_value()) + { + bool allSent = true; + + // filter out channel voice messages + for (const auto& v : makeRange (b, e)) + { + const auto kind = ump::Utils::getMessageType (v[0]); + + if (kind == ump::Utils::MessageKind::channelVoice1 || kind == ump::Utils::MessageKind::channelVoice2) + { + // Dropping this packet because we don't know whether the endpoint is prepared to receive it + allSent = false; + continue; + } + + const ump::Iterator it { v.data(), v.size() }; + allSent &= callback (it, std::next (it)); + } + + return allSent; + } + + bool allSent = true; + + converter->convert (b, e, [&] (ump::View v) + { + const ump::Iterator it { v.data(), v.size() }; + allSent &= callback (it, std::next (it)); + }); + + return allSent; + } + + private: + SpinLock mutex; + std::optional converter; + }; + + struct Connection + { + virtual ~Connection() = default; + + virtual ump::EndpointId getEndpointId() const = 0; + virtual std::optional getEndpoint() const = 0; + virtual ump::StaticDeviceInfo getStaticDeviceInfo() const = 0; + virtual bool send (ump::Iterator, ump::Iterator) = 0; + + virtual void addConsumer (ump::Consumer&) = 0; + virtual void removeConsumer (ump::Consumer&) = 0; + + virtual void disconnected() = 0; + virtual void addDisconnectionListener (ump::DisconnectionListener&) = 0; + virtual void removeDisconnectionListener (ump::DisconnectionListener&) = 0; + }; + + class VirtualPortConnection : public Connection, + private ump::Consumer + { + public: + static std::unique_ptr make (MidiReceiver outputReceiver, bool isUmp) + { + auto result = rawToUniquePtr (new VirtualPortConnection); + + result->src = std::make_unique (static_cast (*result), 0, isUmp); + result->dst = MidiDestination::make (std::move (outputReceiver), isUmp); + result->isUmp = isUmp; + + if (result->src == nullptr && result->dst == nullptr) + return {}; + + return result; + } + + ump::EndpointId getEndpointId() const override + { + const String v = "VIRTUAL " + String (isUmp ? "UMP" : "BYTESTREAM"); + return ump::EndpointId::makeSrcDst (v, v); + } + + std::optional getEndpoint() const override + { + return endpoint; + } + + ump::StaticDeviceInfo getStaticDeviceInfo() const override + { + return ump::StaticDeviceInfo{}.withTransport (isUmp ? ump::Transport::ump : ump::Transport::bytestream) + .withHasSource (true) + .withHasDestination (true); + } + + void setEndpoint (std::optional ep) + { + endpoint = std::move (ep); + converter.setProtocol (endpoint.has_value() ? endpoint->getProtocol() : std::nullopt); + } + + jlong getHost() const + { + if (src != nullptr) + return (jlong) src.get(); + + return 0; + } + + void addConsumer (ump::Consumer& x) override + { + consumers.add (x); + } + + void removeConsumer (ump::Consumer& x) override + { + consumers.remove (x); + } + + void disconnected() override + { + disconnectListeners.call ([] (auto& x) { x.disconnected(); }); + } + + void addDisconnectionListener (ump::DisconnectionListener& x) override + { + disconnectListeners.add (&x); + } + + void removeDisconnectionListener (ump::DisconnectionListener& x) override + { + disconnectListeners.remove (&x); + } + + bool send (ump::Iterator b, ump::Iterator e) override + { + if (dst != nullptr) + return converter.convert (b, e, [&] (auto&&... args) { return dst->send (args...); }); + + return false; + } + + private: + void consume (ump::Iterator b, ump::Iterator e, double time) override + { + consumers.call ([&] (auto& x) { x.consume (b, e, time); }); + } + + std::optional endpoint; + WaitFreeListeners consumers; + ConverterWithOptionalProtocol converter; + ListenerList disconnectListeners; + std::unique_ptr src; + std::unique_ptr dst; + bool isUmp = false; + }; + + class VirtualEndpointImplNative : public ump::DisconnectionListener, + private ump::Consumer + { + public: + static std::unique_ptr make (const String& name, + const ump::DeviceInfo& info, + const String& instanceId, + ump::PacketProtocol protocol, + Span blocks, + ump::BlocksAreStatic blocksAreStatic, + std::shared_ptr connection) + { + if (connection == nullptr) + return {}; + + return rawToUniquePtr (new VirtualEndpointImplNative (name, + info, + instanceId, + protocol, + blocks, + blocksAreStatic, + connection)); + } + + ~VirtualEndpointImplNative() override + { + if (port != nullptr) + { + port->setEndpoint ({}); + port->removeConsumer (*this); + } + } + + ump::EndpointId getId() const + { + return port->getEndpointId(); + } + + bool setBlock (uint8_t index, const ump::Block& b) + { + if (port == nullptr) + return false; + + const auto ep = port->getEndpoint(); + + if (! ep.has_value()) + return false; + + auto endpoint = *ep; + + if (index >= endpoint.getBlocks().size()) + return false; + + const auto old = std::exchange (endpoint.getBlocks()[index], b); + + if (! b.nameMatches (old)) + sendFunctionBlockNameNotification (index); + + if (! b.infoMatches (old)) + sendFunctionBlockInfoNotification (index); + + port->setEndpoint (std::move (endpoint)); + + return true; + } + + bool setName (const String& name) + { + if (port == nullptr) + return false; + + // Per the spec, there's a max length of 98 bytes for endpoint names + if (name.getNumBytesAsUTF8() > 98) + return false; + + const auto ep = port->getEndpoint(); + + if (! ep.has_value()) + return false; + + auto endpoint = *ep; + + if (name == endpoint.getName()) + return true; + + port->setEndpoint (endpoint.withName (name)); + sendEndpointNameNotification(); + + return true; + } + + void disconnected() override + { + port = nullptr; + } + + private: + VirtualEndpointImplNative (const String& name, + const ump::DeviceInfo& info, + const String& instanceId, + ump::PacketProtocol protocol, + Span blocks, + ump::BlocksAreStatic blocksAreStatic, + std::shared_ptr c) + : port (std::move (c)) + { + port->addConsumer (*this); + port->setEndpoint (makeEndpoint (name, info, instanceId, protocol, blocks, blocksAreStatic)); + + processEndpointDiscovery (ump::View { ump::Factory::makeEndpointDiscovery (1, 1, std::byte { 0x1f }).data() }); + processFunctionBlockDiscovery (ump::View { ump::Factory::makeFunctionBlockDiscovery (0xff, std::byte { 0x03 }).data() }); + } + + void consume (ump::Iterator b, ump::Iterator e, double) override + { + for (auto v : makeRange (b, e)) + processPacket (v); + } + + void processPacket (ump::View v) + { + const auto kind = ump::Utils::getMessageType (v[0]); + + if (kind != ump::Utils::MessageKind::stream) + return; + + const auto status = ump::Utils::U8<1>::get (v[0]); + + switch (status) + { + case 0x0: + processEndpointDiscovery (v); + break; + + case 0x5: + processStreamConfigRequest (v); + break; + + case 0x10: + processFunctionBlockDiscovery (v); + break; + } + } + + void processEndpointDiscovery (ump::View v) + { + const auto filterBitmap = ump::Utils::U8<3>::get (v[1]); + + if ((filterBitmap & 0x01) != 0) + sendEndpointInfoNotification(); + + if ((filterBitmap & 0x02) != 0) + sendDeviceIdentityNotification(); + + if ((filterBitmap & 0x04) != 0) + sendEndpointNameNotification(); + + if ((filterBitmap & 0x08) != 0) + sendProductInstanceIdNotification(); + + if ((filterBitmap & 0x10) != 0) + sendStreamConfigurationNotification(); + } + + void processStreamConfigRequest (ump::View) + { + // Currently we don't support changing stream configuration, so just reply with + // the current configuration. + sendStreamConfigurationNotification(); + } + + void processFunctionBlockDiscovery (ump::View v) + { + const auto blockNumber = ump::Utils::U8<2>::get (v[0]); + const auto filterBitmap = ump::Utils::U8<3>::get (v[0]); + + const auto sendFunctionBlockInfo = [this, filterBitmap] (auto index) + { + if ((filterBitmap & 0x01) != 0) + sendFunctionBlockInfoNotification (index); + + if ((filterBitmap & 0x02) != 0) + sendFunctionBlockNameNotification (index); + }; + + const auto endpoint = port->getEndpoint(); + + if (! endpoint.has_value()) + return; + + if (blockNumber < endpoint->getBlocks().size()) + { + sendFunctionBlockInfo (blockNumber); + } + else if (blockNumber == 0xff) + { + for (uint8_t i = 0; i < endpoint->getBlocks().size(); ++i) + sendFunctionBlockInfo (i); + } + } + + void sendFunctionBlockNameNotification (uint8_t index) + { + const auto endpoint = port->getEndpoint(); + + if (! endpoint.has_value()) + return; + + if (index >= endpoint->getBlocks().size()) + { + jassertfalse; + return; + } + + ump::Factory::makeFunctionBlockNameNotification (index, endpoint->getBlocks()[index].getName(), [this] (auto v) + { + const ump::Iterator iterator { v.data(), v.size() }; + port->send (iterator, std::next (iterator)); + }); + } + + void sendFunctionBlockInfoNotification (uint8_t index) + { + const auto endpoint = port->getEndpoint(); + + if (! endpoint.has_value()) + return; + + if (index >= endpoint->getBlocks().size()) + { + jassertfalse; + return; + } + + const auto packet = ump::Factory::makeFunctionBlockInfoNotification (index, endpoint->getBlocks()[index].getInfo()); + const ump::Iterator iterator { packet.data(), packet.size() }; + port->send (iterator, std::next (iterator)); + } + + void sendEndpointNameNotification() + { + const auto endpoint = port->getEndpoint(); + + if (! endpoint.has_value()) + return; + + ump::Factory::makeEndpointNameNotification (endpoint->getName(), [this] (auto v) + { + const ump::Iterator iterator { v.data(), v.size() }; + port->send (iterator, std::next (iterator)); + }); + } + + void sendDeviceIdentityNotification() + { + const auto endpoint = port->getEndpoint(); + + if (! endpoint.has_value()) + return; + + const auto packet = ump::Factory::makeDeviceIdentityNotification (endpoint->getDeviceInfo()); + const ump::Iterator iterator { packet.data(), packet.size() }; + port->send (iterator, std::next (iterator)); + } + + void sendEndpointInfoNotification() + { + const auto ep = port->getEndpoint(); + + if (! ep.has_value()) + return; + + auto& endpoint = *ep; + + const auto info = ump::EndpointInfo{}.withMidi1Support (endpoint.hasMidi1Support()) + .withMidi2Support (endpoint.hasMidi2Support()) + .withNumFunctionBlocks ((uint8_t) endpoint.getBlocks().size()) + .withReceiveJRSupport (endpoint.hasReceiveJRSupport()) + .withTransmitJRSupport (endpoint.hasTransmitJRSupport()) + .withStaticFunctionBlocks (endpoint.hasStaticBlocks()) + .withVersion (endpoint.getUMPVersionMajor(), endpoint.getUMPVersionMinor()); + const auto packet = ump::Factory::makeEndpointInfoNotification (info); + const ump::Iterator iterator { packet.data(), packet.size() }; + port->send (iterator, std::next (iterator)); + } + + void sendProductInstanceIdNotification() + { + const auto endpoint = port->getEndpoint(); + + if (! endpoint.has_value()) + return; + + ump::Factory::makeProductInstanceIdNotification (endpoint->getProductInstanceId(), [this] (auto v) + { + const ump::Iterator iterator { v.data(), v.size() }; + port->send (iterator, std::next (iterator)); + }); + } + + void sendStreamConfigurationNotification() + { + const auto ep = port->getEndpoint(); + + if (! ep.has_value()) + return; + + auto& endpoint = *ep; + const auto protocol = endpoint.getProtocol(); + + if (! protocol.has_value()) + { + jassertfalse; + return; + } + + const auto info = ump::StreamConfiguration{}.withProtocol (*protocol) + .withReceiveTimestamp (endpoint.isReceiveJREnabled()) + .withTransmitTimestamp (endpoint.isTransmitJREnabled()); + const auto packet = ump::Factory::makeStreamConfigurationNotification (info); + const ump::Iterator iterator { packet.data(), packet.size() }; + port->send (iterator, std::next (iterator)); + } + + static ump::Endpoint makeEndpoint (const String& name, + const ump::DeviceInfo& info, + const String& instanceId, + ump::PacketProtocol protocol, + Span blocks, + ump::BlocksAreStatic blocksAreStatic) + { + return ump::Endpoint{}.withName (name) + .withDeviceInfo (info) + .withProductInstanceId (instanceId) + .withProtocol (protocol) + .withMidi1Support (protocol == ump::PacketProtocol::MIDI_1_0) + .withMidi2Support (protocol == ump::PacketProtocol::MIDI_2_0) + .withUMPVersion (1, 1) + .withBlocks (blocks) + .withStaticBlocks (blocksAreStatic == ump::BlocksAreStatic::yes); + } + + std::shared_ptr port; + }; + + class LegacyVirtualPortImplNative : public ump::DisconnectionListener + { + public: + static std::unique_ptr make (std::shared_ptr x) + { + if (x == nullptr) + return {}; + + return rawToUniquePtr (new LegacyVirtualPortImplNative { x }); + } + + ump::EndpointId getId() const + { + return port->getEndpointId(); + } + + void disconnected() override + { + port = nullptr; + } + + private: + explicit LegacyVirtualPortImplNative (std::shared_ptr x) + : port (std::move (x)) {} + + std::shared_ptr port; + }; + + struct VirtualMidiPortManagerListener + { + virtual ~VirtualMidiPortManagerListener() = default; + virtual void virtualConnectionAdded() = 0; + virtual void virtualConnectionRemoved (std::shared_ptr) = 0; + }; + + class VirtualMidiPortManager + { + public: + static VirtualMidiPortManager& getSingleton() + { + static VirtualMidiPortManager result; + return result; + } + + std::shared_ptr createNativeVirtualEndpoint (const String& name, + const ump::DeviceInfo& info, + const String& productInstance, + ump::PacketProtocol protocol, + Span blocks, + ump::BlocksAreStatic blocksAreStatic) + { + if (! isVirtualUmpAvailable()) + return {}; + + if (umpPort == nullptr) + { + // The virtual port is not available, maybe the service hasn't started yet + return {}; + } + + if (umpConnection.lock() != nullptr) + { + // The virtual port was already claimed, perhaps you called createNativeVirtualEndpoint several times + jassertfalse; + return {}; + } + + std::shared_ptr result = VirtualEndpointImplNative::make (name, + info, + productInstance, + protocol, + blocks, + blocksAreStatic, + umpPort); + + umpConnection = result; + return result; + } + + std::shared_ptr createLegacyVirtualInput() + { + if (! isVirtualBytestreamAvailable()) + return {}; + + if (bytestreamPort == nullptr) + { + // The virtual port is not available, maybe the service hasn't started yet + return {}; + } + + if (srcConnection.lock() != nullptr) + { + // The virtual port was already claimed, perhaps you called createNativeVirtualEndpoint several times + jassertfalse; + return {}; + } + + std::shared_ptr result = LegacyVirtualPortImplNative::make (bytestreamPort); + srcConnection = result; + return result; + } + + std::shared_ptr createLegacyVirtualOutput() + { + if (! isVirtualBytestreamAvailable()) + return {}; + + if (bytestreamPort == nullptr) + { + // The virtual port is not available, maybe the service hasn't started yet + return {}; + } + + if (dstConnection.lock() != nullptr) + { + // The virtual port was already claimed, perhaps you called createNativeVirtualEndpoint several times + jassertfalse; + return {}; + } + + std::shared_ptr result = LegacyVirtualPortImplNative::make (bytestreamPort); + dstConnection = result; + return result; + } + + void addListener (VirtualMidiPortManagerListener& x) + { + listeners.add (&x); + } + + void removeListener (VirtualMidiPortManagerListener& x) + { + listeners.remove (&x); + } + + bool isBytestreamPortAvailable() const + { + return bytestreamPort != nullptr; + } + + bool isUmpPortAvailable() const + { + return umpPort != nullptr; + } + + std::shared_ptr getBytestreamConnection() const + { + return bytestreamPort; + } + + std::shared_ptr getUmpConnection() const + { + return umpPort; + } + + static void setVirtualMidiBytestreamEnabled (bool x) + { + if (VirtualMidiServices != nullptr) + { + getEnv()->CallStaticVoidMethod (VirtualMidiServices, + VirtualMidiServices.setVirtualMidiBytestreamEnabled, + getAppContext().get(), + (jboolean) x); + } + } + + static void setVirtualMidiUmpEnabled (bool x) + { + if (VirtualMidiServices != nullptr) + { + getEnv()->CallStaticVoidMethod (VirtualMidiServices, + VirtualMidiServices.setVirtualMidiUmpEnabled, + getAppContext().get(), + (jboolean) x); + } + } + + private: + VirtualMidiPortManager() = default; + + static bool isVirtualBytestreamAvailable() + { + if (VirtualMidiServices != nullptr) + { + return getEnv()->CallStaticBooleanMethod (VirtualMidiServices, + VirtualMidiServices.isVirtualBytestreamAvailable, + getAppContext().get()); + } + + return false; + } + + static bool isVirtualUmpAvailable() + { + if (VirtualMidiServices != nullptr) + { + return getEnv()->CallStaticBooleanMethod (VirtualMidiServices, + VirtualMidiServices.isVirtualUmpAvailable, + getAppContext().get()); + } + + return false; + } + + jobject addPort (JNIEnv* env, jobject newReceiver, jboolean isUmp) + { + std::shared_ptr port = VirtualPortConnection::make (MidiReceiver { GlobalRef { LocalRef { newReceiver } } }, isUmp); + + if (port == nullptr) + return {}; + + auto* result = env->NewObject (NativeMidiReceiver, NativeMidiReceiver.constructor, port->getHost()); + MidiReceiver key { GlobalRef { LocalRef { env->NewLocalRef (result) } } }; + (isUmp ? umpPort : bytestreamPort) = port; + (isUmp ? umpReceiver : bytestreamReceiver) = key; + + if (! isUmp) + { + StaticDeviceInfo info; + info.dst = { "" }; + info.src = { "" }; + info.transport = TRANSPORT_BYTESTREAM; + port->setEndpoint (info.getEquivalentBytestreamEndpoint()); + } + + listeners.call ([&] (auto& x) { x.virtualConnectionAdded(); }); + return result; + } + + void removePort (JNIEnv* env, jobject previouslyAdded) + { + MidiReceiver key { GlobalRef { LocalRef { env->NewLocalRef (previouslyAdded) } } }; + + const auto connection = std::invoke ([&]() -> std::shared_ptr + { + if (key == umpReceiver) + return umpPort; + + if (key == bytestreamReceiver) + return bytestreamPort; + + return {}; + }); + + if (connection == nullptr) + { + jassertfalse; + return; + } + + listeners.call ([&] (auto& x) { x.virtualConnectionRemoved (connection); }); + + if (key == umpReceiver) + if (auto strong = umpConnection.lock()) + strong->disconnected(); + + if (key == bytestreamReceiver) + { + if (auto strong = srcConnection.lock()) + strong->disconnected(); + + if (auto strong = dstConnection.lock()) + strong->disconnected(); + } + + for (auto* ptr : { &bytestreamPort, &umpPort }) + if (*ptr == connection) + ptr->reset(); + } + + std::shared_ptr bytestreamPort, umpPort; + MidiReceiver bytestreamReceiver, umpReceiver; + + std::weak_ptr umpConnection; + std::weak_ptr srcConnection; + std::weak_ptr dstConnection; + ListenerList listeners; + + static jobject addVirtualPort (JNIEnv* env, jclass, jobject newReceiver, jboolean isUmp) + { + return getSingleton().addPort (env, newReceiver, isUmp); + } + + static void removeVirtualPort (JNIEnv* env, jclass, jobject previouslyAdded) + { + getSingleton().removePort (env, previouslyAdded); + } + + #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + CALLBACK (addVirtualPort, "addVirtualPort", "(Landroid/media/midi/MidiReceiver;Z)Landroid/media/midi/MidiReceiver;") \ + CALLBACK (removeVirtualPort, "removeVirtualPort", "(Landroid/media/midi/MidiReceiver;)V") \ + STATICMETHOD (isVirtualBytestreamAvailable, "isVirtualBytestreamAvailable", "(Landroid/content/Context;)Z") \ + STATICMETHOD (isVirtualUmpAvailable, "isVirtualUmpAvailable", "(Landroid/content/Context;)Z") \ + STATICMETHOD (setVirtualMidiBytestreamEnabled, "setVirtualMidiBytestreamEnabled", "(Landroid/content/Context;Z)V") \ + STATICMETHOD (setVirtualMidiUmpEnabled, "setVirtualMidiUmpEnabled", "(Landroid/content/Context;Z)V") \ + + DECLARE_OPTIONAL_JNI_CLASS_WITH_BYTECODE (VirtualMidiServices, "com/rmsl/juce/VirtualMidiServices", 24, nullptr, true) + #undef JNI_CLASS_MEMBERS + }; + + /* This type holds the bytes of a Universal MIDI Packet in 'string order' */ + struct StreamPacketBytes + { + std::array data; + size_t size = 0; + + static StreamPacketBytes asPlainBytes (ump::View v) + { + StreamPacketBytes result; + + for (const auto word : v) + { + result.data[result.size + 0] = std::byte { ump::Utils::U8<0>::get (word) }; + result.data[result.size + 1] = std::byte { ump::Utils::U8<1>::get (word) }; + result.data[result.size + 2] = std::byte { ump::Utils::U8<2>::get (word) }; + result.data[result.size + 3] = std::byte { ump::Utils::U8<3>::get (word) }; + result.size += 4; + } + + return result; + } + }; + + /* Endpoint name, product instance ID, and function block name may all be spread across + several packets, and this type can reconstitute a UTF-8 string from these packets. + */ + class StringBuilder + { + public: + StringBuilder() + { + buffer.reserve (7 * 16); + } + + void add (ump::View v) + { + if (ump::Utils::getMessageType (v[0]) != ump::Utils::MessageKind::stream) + { + jassertfalse; + return; + } + + const auto status = ump::Utils::U8<1>::get (v[0]); + + if (status != 0x03 && status != 0x04 && status != 0x12) + { + jassertfalse; + return; + } + + const auto format = (ump::Utils::U8<0>::get (v[0]) >> 2) & 0x3; + + switch (format) + { + case 0: + { + beginString(); + append (v); + endString(); + break; + } + + case 1: + { + beginString(); + append (v); + break; + } + + case 2: + { + jassert (! buffer.empty()); + append (v); + break; + } + + case 3: + { + jassert (! buffer.empty()); + append (v); + endString(); + break; + } + } + } + + std::optional get() const + { + if (buffer.empty() || buffer.back() == std::byte{}) + return String::fromUTF8 (reinterpret_cast (buffer.data()), (int) buffer.size()); + + return std::nullopt; + } + + private: + void beginString() + { + buffer.clear(); + } + + void append (ump::View v) + { + const auto status = ump::Utils::U8<1>::get (v[0]); + const auto offset = status == 0x12 ? 3 : 2; + const auto packetAsBytes = StreamPacketBytes::asPlainBytes (v); + + buffer.insert (buffer.end(), packetAsBytes.data.begin() + offset, packetAsBytes.data.end()); + } + + void endString() + { + buffer.push_back ({}); // add null terminator just in case + } + + std::vector buffer; + }; + + struct StreamProcessorQueueDelegate + { + virtual ~StreamProcessorQueueDelegate() = default; + virtual void processStreamPacket (ump::View) = 0; + }; + + /* Filters out stream messages and dispatches them to the main thread, + where they are passed to the delegate for further processing. + */ + class StreamProcessorQueue final : private Timer + { + public: + explicit StreamProcessorQueue (StreamProcessorQueueDelegate& d) + : delegate (d) + { + startTimer (20); + } + + ~StreamProcessorQueue() override + { + stopTimer(); + } + + /** To be called from the MIDI input thread */ + void consume (ump::Iterator b, ump::Iterator e) + { + for (auto v : makeRange (b, e)) + { + if (ump::Utils::getMessageType (v[0]) != ump::Utils::MessageKind::stream) + continue; + + size_t i = 0; + fifo.write ((int) v.size()).forEach ([&] (auto index) + { + buffer[(size_t) index] = v[i++]; + }); + } + } + + private: + void timerCallback() override + { + const auto callback = [this] (ump::View v, double) + { + delegate.processStreamPacket (v); + }; + + const auto scope = fifo.read (fifo.getNumReady()); + dispatcher.dispatch ({ buffer.data() + scope.startIndex1, (size_t) scope.blockSize1 }, 0.0, callback); + dispatcher.dispatch ({ buffer.data() + scope.startIndex2, (size_t) scope.blockSize2 }, 0.0, callback); + } + + static constexpr auto queueSize = 4096; + StreamProcessorQueueDelegate& delegate; + std::vector buffer = std::vector (queueSize); + AbstractFifo fifo { queueSize }; + ump::Dispatcher dispatcher; + }; + + struct EndpointInfoBuilderDelegate + { + virtual ~EndpointInfoBuilderDelegate() = default; + virtual void sendFunctionBlockDiscovery() = 0; + }; + + /* Parses incoming stream messages to build up a cache of all the information known about + a connected endpoint. + Waits until all function block information has been received before notifying all + EndpointListeners, and then notifies listeners on each subsequent configuration change. + */ + class EndpointInfoBuilder : public StreamProcessorQueueDelegate + { + public: + EndpointInfoBuilder (const ump::EndpointId& idToUse, EndpointInfoBuilderDelegate& d) + : id (idToUse), delegate (d) + { + } + + std::optional getEndpoint() const + { + return endpoint; + } + + void processStreamPacket (ump::View v) override + { + JUCE_ASSERT_MESSAGE_THREAD + + const auto status = ump::Utils::U8<1>::get (v[0]); + + switch (status) + { + case 0x1: + processEndpointInfo (v); + break; + + case 0x2: + processDeviceIdentity (v); + break; + + case 0x3: + processEndpointName (v); + break; + + case 0x4: + processProductInstanceID (v); + break; + + case 0x6: + processStreamConfigNotification (v); + break; + + case 0x11: + processFunctionBlockInfo (v); + break; + + case 0x12: + processFunctionBlockName (v); + break; + } + + sendNotificationIfValid(); + } + + void addListener (ump::EndpointsListener& l) + { + changeListeners.add (&l); + } + + void removeListener (ump::EndpointsListener& l) + { + changeListeners.remove (&l); + } + + private: + void sendNotificationIfValid() + { + const auto numSpecifiedBlocks = endpoint.has_value() ? endpoint->getBlocks().size() : 0; + const auto requiredBitmap = (uint32_t) (((uint64_t) 1 << numSpecifiedBlocks) - 1); + + if ((specifiedBlocks & requiredBitmap) != requiredBitmap) + return; + + if ((specifiedBlockNames & requiredBitmap) != requiredBitmap) + return; + + changeListeners.call ([] (auto& x) { x.endpointsChanged(); }); + } + + void processEndpointInfo (ump::View v) + { + const auto prevNumFunctionBlocks = endpoint.has_value() ? endpoint->getBlocks().size() : 0; + + endpoint = endpoint.value_or (ump::Endpoint{}) + .withUMPVersion (ump::Utils::U8<2>::get (v[0]), ump::Utils::U8<3>::get (v[0])) + .withMidi1Support (ump::Utils::U8<2>::get (v[1]) & 0x1) + .withMidi2Support (ump::Utils::U8<2>::get (v[1]) & 0x2) + .withTransmitJRSupport (ump::Utils::U8<3>::get (v[1]) & 0x1) + .withReceiveJRSupport (ump::Utils::U8<3>::get (v[1]) & 0x2) + .withStaticBlocks (ump::Utils::U8<0>::get (v[1]) & 0x80) + .withNumBlocks (ump::Utils::U8<0>::get (v[1]) & 0x7f); + + if (endpoint->getBlocks().size() > prevNumFunctionBlocks) + delegate.sendFunctionBlockDiscovery(); + } + + void processDeviceIdentity (ump::View v) + { + const auto bytes = StreamPacketBytes::asPlainBytes (v); + ump::DeviceInfo info; + std::copy (bytes.data.data() + 0x5, bytes.data.data() + 0x8, info.manufacturer.begin()); + std::copy (bytes.data.data() + 0x8, bytes.data.data() + 0xa, info.family.begin()); + std::copy (bytes.data.data() + 0xa, bytes.data.data() + 0xc, info.modelNumber.begin()); + std::copy (bytes.data.data() + 0xc, bytes.data.data() + 0x10, info.revision.begin()); + endpoint = endpoint.value_or (ump::Endpoint{}).withDeviceInfo (info); + } + + void processEndpointName (ump::View v) + { + endpointNameBuilder.add (v); + + if (auto x = endpointNameBuilder.get()) + endpoint = endpoint.value_or (ump::Endpoint{}).withName (*x); + } + + void processProductInstanceID (ump::View v) + { + productIdBuilder.add (v); + + if (auto x = productIdBuilder.get()) + endpoint = endpoint.value_or (ump::Endpoint{}).withProductInstanceId (*x); + } + + void processStreamConfigNotification (ump::View v) + { + const auto packetProtocol = std::invoke ([&]() -> std::optional + { + switch (ump::Utils::U8<2>::get (v[0])) + { + case 1: + return ump::PacketProtocol::MIDI_1_0; + case 2: + return ump::PacketProtocol::MIDI_2_0; + } + + return {}; + }); + + const auto flags = ump::Utils::U8<3>::get (v[0]); + const auto rxjr = flags & 0x2; + const auto txjr = flags & 0x1; + + endpoint = endpoint.value_or (ump::Endpoint{}) + .withProtocol (packetProtocol) + .withReceiveJREnabled (rxjr) + .withTransmitJREnabled (txjr); + } + + void processFunctionBlockInfo (ump::View v) + { + const auto index = (size_t) (ump::Utils::U8<2>::get (v[0]) & 0x7f); + + if (! endpoint.has_value() || index >= endpoint->getBlocks().size()) + { + // Got a block notification for a block that doesn't exist! + jassertfalse; + return; + } + + endpoint->getBlocks()[index] = ump::Block{}.withEnabled (ump::Utils::U8<2>::get (v[0]) & 0x80) + .withUiHint ((ump::BlockUiHint) ((ump::Utils::U8<3>::get (v[0]) >> 4) & 0x3)) + .withMIDI1ProxyKind ((ump::BlockMIDI1ProxyKind) ((ump::Utils::U8<3>::get (v[0]) >> 2) & 0x3)) + .withDirection ((ump::BlockDirection) ((ump::Utils::U8<3>::get (v[0]) >> 0) & 0x3)) + .withFirstGroup (ump::Utils::U8<0>::get (v[1])) + .withNumGroups (ump::Utils::U8<1>::get (v[1])) + .withMaxSysex8Streams (ump::Utils::U8<3>::get (v[1])); + + specifiedBlocks |= (1 << index); + } + + void processFunctionBlockName (ump::View v) + { + const auto blockIndex = ump::Utils::U8<2>::get (v[0]); + + if (blockIndex >= functionBlockNameBuilders.size()) + { + // block with this index cannot exist + jassertfalse; + return; + } + + if (! endpoint.has_value() || blockIndex >= endpoint->getBlocks().size()) + { + // setting a name for a block that doesn't exist on this endpoint + jassertfalse; + return; + } + + auto& builder = functionBlockNameBuilders[blockIndex]; + + builder.add (v); + + if (auto x = builder.get()) + { + auto& block = endpoint->getBlocks()[blockIndex]; + block = block.withName (*x); + + specifiedBlockNames |= (1 << blockIndex); + } + } + + ump::EndpointId id; + EndpointInfoBuilderDelegate& delegate; + + ListenerList changeListeners; + + std::optional endpoint; + + std::array functionBlockNameBuilders; + StringBuilder endpointNameBuilder, productIdBuilder; + + // Toggle each bit on to indicate that we've received information about the block at that index + uint32_t specifiedBlocks = 0; + uint32_t specifiedBlockNames = 0; + }; + + /* A connection to a UMP device, which is assumed to always have a single input and output port. */ + class UmpConnection : public Connection, + private ump::EndpointsListener, + private EndpointInfoBuilderDelegate, + private ump::Consumer + { + public: + ~UmpConnection() override + { + for (auto& port : ports) + port.stop(); + } + + static std::shared_ptr make (const DeviceManager& manager, + const StaticDeviceInfo& info, + const ump::EndpointId& e) + { + auto result = rawToUniquePtr (new UmpConnection { e }); + + result->src = std::make_unique (static_cast (*result), 0, true); + + if (result->src != nullptr) + { + if (auto port = manager.openMidiInputPortWithID (info.id, (jint) 0, (jlong) result->src.get())) + { + port.start(); + result->ports.push_back (std::move (port)); + } + } + + if (auto port = manager.openMidiOutputPortWithID (info.id, (jint) 0)) + { + result->dst = MidiDestination::make (MidiReceiver { port.get() }, true); + result->ports.push_back (std::move (port)); + } + + if (result->src == nullptr && result->dst == nullptr) + { + // Unable to open a connection in either direction to this device! + jassertfalse; + return {}; + } + + result->staticInfo = info; + result->sendEndpointDiscovery(); + return result; + } + + ump::EndpointId getEndpointId() const override + { + return endpointId; + } + + std::optional getEndpoint() const override + { + return infoBuilder.getEndpoint(); + } + + ump::StaticDeviceInfo getStaticDeviceInfo() const override + { + return staticInfo.getUmpStaticDeviceInfo(); + } + + bool send (ump::Iterator b, ump::Iterator e) override + { + if (dst == nullptr) + return false; + + return converter.convert (b, e, [&] (auto&&... args) { return dst->send (args...); }); + } + + void addConsumer (ump::Consumer& c) override + { + consumers.add (c); + } + + void removeConsumer (ump::Consumer& c) override + { + consumers.remove (c); + } + + void disconnected() override + { + disconnectListeners.call ([] (auto& x) { x.disconnected(); }); + } + + void addDisconnectionListener (ump::DisconnectionListener& l) override + { + disconnectListeners.add (&l); + } + + void removeDisconnectionListener (ump::DisconnectionListener& l) override + { + disconnectListeners.remove (&l); + } + + void addEndpointListener (ump::EndpointsListener& l) + { + infoBuilder.addListener (l); + } + + void removeEndpointListener (ump::EndpointsListener& l) + { + infoBuilder.removeListener (l); + } + + private: + explicit UmpConnection (const ump::EndpointId& idToUse) + : endpointId (idToUse) + { + infoBuilder.addListener (*this); + } + + /* Called when 'our' endpoint is changed. */ + void endpointsChanged() override + { + const auto newProtocol = std::invoke ([&]() -> std::optional + { + if (const auto& e = infoBuilder.getEndpoint()) + return e->getProtocol(); + + return std::nullopt; + }); + + converter.setProtocol (newProtocol); + } + + void sendEndpointDiscovery() + { + // The 'filter bitmap' requests all fields that are defined in the current UMP spec + if (dst != nullptr) + dst->send (ump::View { ump::Factory::makeEndpointDiscovery (1, 1, std::byte { 0x1f }).data() }); + } + + void sendFunctionBlockDiscovery() override + { + if (dst != nullptr) + dst->send (ump::View { ump::Factory::makeFunctionBlockDiscovery (0xff, std::byte { 0x3 }).data() }); + } + + void consume (ump::Iterator b, ump::Iterator e, double t) override + { + // Forward to all interested listeners + consumers.call ([&] (auto& x) { x.consume (b, e, t); }); + + // Filter out stream messages for processing on the main thread + queue.consume (b, e); + } + + ListenerList disconnectListeners; + WaitFreeListeners consumers; + + StaticDeviceInfo staticInfo; + ump::EndpointId endpointId; + EndpointInfoBuilder infoBuilder { endpointId, *this }; + StreamProcessorQueue queue { infoBuilder }; + + std::unique_ptr src; + std::unique_ptr dst; + std::vector ports; + + ConverterWithOptionalProtocol converter; + }; + + class BytestreamConnection : public Connection, + private ump::Consumer + { + public: + ~BytestreamConnection() override + { + for (auto& port : ports) + port.stop(); + } + + static std::shared_ptr make (const DeviceManager& manager, + const StaticDeviceInfo& info, + const ump::EndpointId& e) + { + auto result = rawToUniquePtr (new BytestreamConnection); + + for (size_t i = 0; i < info.src.size(); ++i) + { + auto item = std::make_unique (static_cast (*result), (int) i, false); + auto port = manager.openMidiInputPortWithID (info.id, (jint) i, (jlong) item.get()); + + if (port == nullptr) + continue; + + result->src.push_back (std::move (item)); + port.start(); + result->ports.push_back (std::move (port)); + } + + for (size_t i = 0; i < info.dst.size(); ++i) + { + if (auto port = manager.openMidiOutputPortWithID (info.id, (int) i)) + { + if (auto dst = MidiDestination::make (MidiReceiver { port.get() }, false)) + { + result->dst.push_back (std::move (dst)); + result->ports.push_back (std::move (port)); + } + } + } + + result->endpointId = e; + result->endpoint = info.getEquivalentBytestreamEndpoint(); + result->staticInfo = info; + + return result; + } + + ump::EndpointId getEndpointId() const override + { + return endpointId; + } + + std::optional getEndpoint() const override + { + return endpoint; + } + + ump::StaticDeviceInfo getStaticDeviceInfo() const override + { + return staticInfo.getUmpStaticDeviceInfo(); + } + + bool send (ump::Iterator b, ump::Iterator e) override + { + bool success = true; + + for (const auto v : makeRange (b, e)) + { + const auto kind = ump::Utils::getMessageType (v[0]); + + if (ump::Utils::isGroupless (kind)) + { + success = false; + continue; + } + + const auto groupIndex = ump::Utils::getGroup (v[0]); + + if (groupIndex >= dst.size()) + { + success = false; + continue; + } + + if (auto& x = dst[(size_t) groupIndex]) + x->send (v); + } + + return success; + } + + void addConsumer (ump::Consumer& c) override + { + consumers.add (c); + } + + void removeConsumer (ump::Consumer& c) override + { + consumers.remove (c); + } + + void disconnected() override + { + disconnectListeners.call ([] (auto& x) { x.disconnected(); }); + } + + void addDisconnectionListener (ump::DisconnectionListener& l) override + { + disconnectListeners.add (&l); + } + + void removeDisconnectionListener (ump::DisconnectionListener& l) override + { + disconnectListeners.remove (&l); + } + + private: + void consume (ump::Iterator b, ump::Iterator e, double t) override + { + consumers.call ([&] (auto& x) { x.consume (b, e, t); }); + } + + ListenerList disconnectListeners; + WaitFreeListeners consumers; + + StaticDeviceInfo staticInfo; + ump::EndpointId endpointId; + std::optional endpoint; + + std::vector> src; + std::vector> dst; + std::vector ports; + }; + + class Client : private DeviceManagerListener, + private VirtualMidiPortManagerListener + { + public: + void getEndpoints (std::vector& buffer) const + { + std::transform (infoForEndpoint.begin(), + infoForEndpoint.end(), + std::back_inserter (buffer), + [] (auto& pair) { return pair.first; }); + } + + std::optional getEndpoint (const ump::EndpointId& id) const + { + if (const auto iter = infoForEndpoint.find (id); iter != infoForEndpoint.end()) + { + if (iter->second.transport == TRANSPORT_BYTESTREAM) + return iter->second.getEquivalentBytestreamEndpoint(); + } + + if (const auto iter = connections.find (id); iter != connections.end()) + { + if (auto connection = iter->second.lock()) + return connection->getEndpoint(); + } + + return {}; + } + + std::optional getStaticDeviceInfo (const ump::EndpointId& id) const + { + if (const auto iter = infoForEndpoint.find (id); iter != infoForEndpoint.end()) + return iter->second.getUmpStaticDeviceInfo(); + + if (const auto iter = connections.find (id); iter != connections.end()) + { + if (auto connection = iter->second.lock()) + return connection->getStaticDeviceInfo(); + } + + return {}; + } + + std::shared_ptr findOrMakeConnection (const ump::EndpointId& e) + { + const auto connection = connections.find (e); + + if (connection != connections.end()) + if (auto strong = connection->second.lock()) + return strong; + + const auto info = infoForEndpoint.find (e); + + // We don't know anything about this endpoint, maybe it got disconnected. + // In any case, we can't connect to it. + if (info == infoForEndpoint.end()) + return {}; + + const auto& staticDeviceInfo = info->second; + + switch (staticDeviceInfo.transport) + { + case TRANSPORT_BYTESTREAM: + { + const auto result = BytestreamConnection::make (*deviceManager, staticDeviceInfo, e); + connections[e] = result; + return result; + } + + case TRANSPORT_UMP: + { + const auto result = UmpConnection::make (*deviceManager, staticDeviceInfo, e); + result->addEndpointListener (listener); + connections[e] = result; + return result; + } + } + + // Unknown transport! + jassertfalse; + return {}; + } + + static std::unique_ptr make (ump::EndpointsListener& l) + { + auto result = rawToUniquePtr (new Client { l }); + result->deviceManager = DeviceManager::getSingleton(); + + if (result->deviceManager == nullptr) + return {}; + + result->deviceManager->addListener (*result); + + return result; + } + + ~Client() override + { + if (deviceManager != nullptr) + deviceManager->removeListener (*this); + + VirtualMidiPortManager::getSingleton().removeListener (*this); + } + + private: + explicit Client (ump::EndpointsListener& l) + : listener (l) + { + // It's possible for the VirtualMidiPortManager to be created early on in the app's + // lifecycle, even before the app's main window component peer is constructed. In the + // case that the virtual port manager is constructed first, we still need to make a + // record of these pre-existing ports in order to allow new connections to those ports. + fetchVirtualConnections(); + VirtualMidiPortManager::getSingleton().addListener (*this); + } + + void fetchVirtualConnections() + { + auto& manager = VirtualMidiPortManager::getSingleton(); + + for (const auto& [port, transport] : { std::tuple (manager.getBytestreamConnection(), TRANSPORT_BYTESTREAM), + std::tuple (manager.getUmpConnection(), TRANSPORT_UMP) }) + { + if (port != nullptr) + connections.insert_or_assign (port->getEndpointId(), port); + } + } + + void virtualConnectionAdded() override + { + fetchVirtualConnections(); + listener.virtualMidiServiceActiveChanged(); + } + + void virtualConnectionRemoved (std::shared_ptr x) override + { + listener.virtualMidiServiceActiveChanged(); + const auto iter = connections.find (x->getEndpointId()); + + if (iter != connections.end()) + { + jassert (iter->second.lock() == x); + connections.erase (iter); + } + } + + void deviceAdded (const StaticDeviceInfo& info) override + { + const auto endpointId = findUniqueId (info); + endpointIdForDevice.insert_or_assign (info.id, endpointId); + infoForEndpoint.insert_or_assign (endpointId, info); + listener.endpointsChanged(); + } + + void deviceRemoved (int id) override + { + const auto iter = endpointIdForDevice.find (id); + + if (iter == endpointIdForDevice.end()) + { + // That's weird, a device disconnected, but we had no record of it connecting in the first place! + jassertfalse; + return; + } + + const auto connection = connections.find (iter->second); + + if (connection != connections.end()) + { + if (const auto strong = connection->second.lock()) + strong->disconnected(); + + connections.erase (connection); + } + + infoForEndpoint.erase (iter->second); + endpointIdForDevice.erase (iter); + listener.endpointsChanged(); + } + + ump::EndpointId findUniqueId (const StaticDeviceInfo& info) const + { + const auto base = std::invoke ([&] + { + if (info.serialNumber.isNotEmpty()) + return info.serialNumber; + + if (info.name.isNotEmpty()) + return info.name; + + // The device doesn't have any immediately-available useful info, + // so we're giving up on creating a stable ID + return String (info.id); + }); + + for (int suffix = 1;; ++suffix) + { + const auto potential = base + (suffix == 1 ? "" : " (" + String (suffix) + ')'); + const auto id = ump::EndpointId::makeSrcDst (potential, potential); + + if (infoForEndpoint.count (id) == 0) + return id; + } + } + + ump::EndpointsListener& listener; + std::map endpointIdForDevice; + std::map infoForEndpoint; + std::map> connections; + std::shared_ptr deviceManager; + }; + + class InputImplNative : public ump::Input::Impl::Native, + private ump::Consumer + { + public: + ~InputImplNative() override + { + connection->removeDisconnectionListener (disconnectionListener); + connection->removeConsumer (*this); + } + + ump::EndpointId getEndpointId() const override + { + return connection->getEndpointId(); + } + + ump::PacketProtocol getProtocol() const override + { + return converter.getProtocol(); + } + + static std::unique_ptr make (ump::DisconnectionListener& dl, + std::shared_ptr d, + ump::PacketProtocol p, + ump::Consumer& c) + { + if (d == nullptr) + return {}; + + return rawToUniquePtr (new InputImplNative (dl, d, p, c)); + } + + private: + InputImplNative (ump::DisconnectionListener& dl, + std::shared_ptr d, + ump::PacketProtocol p, + ump::Consumer& c) + : disconnectionListener (dl), + consumer (c), + converter (p), + connection (std::move (d)) + { + connection->addConsumer (*this); + connection->addDisconnectionListener (disconnectionListener); + } + + void consume (ump::Iterator b, ump::Iterator e, double time) override + { + converter.convert (b, e, [this, time] (auto view) + { + const ump::Iterator begin { view.data(), view.size() }; + consumer.consume (begin, std::next (begin), time); + }); + } + + ump::DisconnectionListener& disconnectionListener; + ump::Consumer& consumer; + ump::GenericUMPConverter converter; + std::shared_ptr connection; + }; + + class OutputImplNative : public ump::Output::Impl::Native + { + public: + ~OutputImplNative() override + { + connection->removeDisconnectionListener (disconnectionListener); + } + + ump::EndpointId getEndpointId() const override + { + return connection->getEndpointId(); + } + + bool send (ump::Iterator b, ump::Iterator e) override + { + return connection->send (b, e); + } + + static std::unique_ptr make (ump::DisconnectionListener& dl, + std::shared_ptr d) + { + if (d == nullptr) + return {}; + + return rawToUniquePtr (new OutputImplNative (dl, d)); + } + + private: + OutputImplNative (ump::DisconnectionListener& dl, std::shared_ptr d) + : disconnectionListener (dl), connection (std::move (d)) + { + connection->addDisconnectionListener (disconnectionListener); + } + + ump::DisconnectionListener& disconnectionListener; + std::shared_ptr connection; + }; + + class SessionImplNative : public ump::Session::Impl::Native + { + public: + String getName() const override + { + return name; + } + + std::unique_ptr connectInput (ump::DisconnectionListener& d, + const ump::EndpointId& e, + ump::PacketProtocol p, + ump::Consumer& c) override + { + return InputImplNative::make (d, client->findOrMakeConnection (e), p, c); + } + + std::unique_ptr connectOutput (ump::DisconnectionListener& d, + const ump::EndpointId& e) override + { + return OutputImplNative::make (d, client->findOrMakeConnection (e)); + } + + std::unique_ptr createNativeVirtualEndpoint (const String& endpointName, + const ump::DeviceInfo& info, + const String& productInstance, + ump::PacketProtocol protocol, + Span blocks, + ump::BlocksAreStatic blocksAreStatic) override + { + const auto impl = VirtualMidiPortManager::getSingleton().createNativeVirtualEndpoint (endpointName, + info, + productInstance, + protocol, + blocks, + blocksAreStatic); + + if (impl == nullptr) + return {}; + + struct Result : public ump::VirtualEndpoint::Impl::Native + { + explicit Result (std::shared_ptr x) : impl (std::move (x)) {} + + ump::EndpointId getId() const override + { + return impl->getId(); + } + + bool setBlock (uint8_t index, const juce::universal_midi_packets::Block& block) override + { + return impl->setBlock (index, block); + } + + bool setName (const juce::String& nameIn) override + { + return impl->setName (nameIn); + } + + std::shared_ptr impl; + }; + + return rawToUniquePtr (new Result { impl }); + } + + std::unique_ptr createLegacyVirtualInput (const String&) override + { + const auto impl = VirtualMidiPortManager::getSingleton().createLegacyVirtualInput(); + + if (impl == nullptr) + return {}; + + struct Result : public ump::LegacyVirtualInput::Impl::Native + { + explicit Result (std::shared_ptr x) : impl (std::move (x)) {} + + ump::EndpointId getId() const override + { + return impl->getId(); + } + + std::shared_ptr impl; + }; + + return rawToUniquePtr (new Result { impl }); + } + + std::unique_ptr createLegacyVirtualOutput (const String&) override + { + const auto impl = VirtualMidiPortManager::getSingleton().createLegacyVirtualOutput(); + + if (impl == nullptr) + return {}; + + struct Result : public ump::LegacyVirtualOutput::Impl::Native + { + explicit Result (std::shared_ptr x) : impl (std::move (x)) {} + + ump::EndpointId getId() const override + { + return impl->getId(); + } + + std::shared_ptr impl; + }; + + return rawToUniquePtr (new Result { impl }); + } + + static std::unique_ptr make (std::shared_ptr c, const String& n) + { + return rawToUniquePtr (new SessionImplNative { std::move (c), n }); + } + + private: + explicit SessionImplNative (std::shared_ptr c, const String& n) + : client (std::move (c)), name (n) + { + } + + std::shared_ptr client; + String name; + }; + + class EndpointsImplNative : public ump::Endpoints::Impl::Native + { + public: + ump::Backend getBackend() const override + { + return ump::Backend::android; + } + + void getEndpoints (std::vector& buffer) const override + { + client->getEndpoints (buffer); + } + + std::optional getEndpoint (const ump::EndpointId& x) const override + { + return client->getEndpoint (x); + } + + std::optional getStaticDeviceInfo (const ump::EndpointId& x) const override + { + return client->getStaticDeviceInfo (x); + } + + std::unique_ptr makeSession (const String& name) override + { + return SessionImplNative::make (client, name); + } + + bool isVirtualMidiBytestreamServiceActive() const override + { + return VirtualMidiPortManager::getSingleton().isBytestreamPortAvailable(); + } + + bool isVirtualMidiUmpServiceActive() const override + { + return VirtualMidiPortManager::getSingleton().isUmpPortAvailable(); + } + + void setVirtualMidiBytestreamServiceActive (bool x) override + { + VirtualMidiPortManager::setVirtualMidiBytestreamEnabled (x); + } + + void setVirtualMidiUmpServiceActive (bool x) override + { + VirtualMidiPortManager::setVirtualMidiUmpEnabled (x); + } + + static std::unique_ptr make (ump::EndpointsListener& l) + { + if (auto c = Client::make (l)) + return rawToUniquePtr (new EndpointsImplNative { std::move (c) }); + + return {}; + } + + private: + explicit EndpointsImplNative (std::shared_ptr c) + : client (std::move (c)) + { + } + + std::shared_ptr client; + }; + + AndroidMidiHelpers() = delete; }; -//============================================================================== -Array MidiInput::getAvailableDevices() +auto ump::Endpoints::Impl::Native::make (EndpointsListener& l) -> std::unique_ptr { - AndroidMidiDeviceManager manager; - return manager.getDevices (true); -} - -MidiDeviceInfo MidiInput::getDefaultDevice() -{ - return getAvailableDevices().getFirst(); -} - -std::unique_ptr MidiInput::openDevice (const String& deviceIdentifier, MidiInputCallback* callback) -{ - if (deviceIdentifier.isEmpty()) - return {}; - - AndroidMidiDeviceManager manager; - - std::unique_ptr midiInput (new MidiInput ({}, deviceIdentifier)); - - if (auto* port = manager.openMidiInputPortWithID (deviceIdentifier.getIntValue(), midiInput.get(), callback)) - { - midiInput->internal.reset (port); - midiInput->setName (port->getName()); - - return midiInput; - } - - return {}; -} - -StringArray MidiInput::getDevices() -{ - StringArray deviceNames; - - for (auto& d : getAvailableDevices()) - deviceNames.add (d.name); - - return deviceNames; -} - -int MidiInput::getDefaultDeviceIndex() -{ - return 0; -} - -std::unique_ptr MidiInput::openDevice (int index, MidiInputCallback* callback) -{ - return openDevice (getAvailableDevices()[index].identifier, callback); -} - -MidiInput::MidiInput (const String& deviceName, const String& deviceIdentifier) - : deviceInfo (deviceName, deviceIdentifier) -{ -} - -MidiInput::~MidiInput() = default; - -void MidiInput::start() -{ - if (auto* mi = internal.get()) - mi->start(); -} - -void MidiInput::stop() -{ - if (auto* mi = internal.get()) - mi->stop(); -} - -//============================================================================== -Array MidiOutput::getAvailableDevices() -{ - AndroidMidiDeviceManager manager; - return manager.getDevices (false); -} - -MidiDeviceInfo MidiOutput::getDefaultDevice() -{ - return getAvailableDevices().getFirst(); -} - -std::unique_ptr MidiOutput::openDevice (const String& deviceIdentifier) -{ - if (deviceIdentifier.isEmpty()) - return {}; - - AndroidMidiDeviceManager manager; - - if (auto* port = manager.openMidiOutputPortWithID (deviceIdentifier.getIntValue())) - { - std::unique_ptr midiOutput (new MidiOutput ({}, deviceIdentifier)); - midiOutput->internal.reset (port); - midiOutput->setName (port->getName()); - - return midiOutput; - } - - return {}; -} - -StringArray MidiOutput::getDevices() -{ - StringArray deviceNames; - - for (auto& d : getAvailableDevices()) - deviceNames.add (d.name); - - return deviceNames; -} - -int MidiOutput::getDefaultDeviceIndex() -{ - return 0; -} - -std::unique_ptr MidiOutput::openDevice (int index) -{ - return openDevice (getAvailableDevices()[index].identifier); -} - -MidiOutput::~MidiOutput() -{ - stopBackgroundThread(); -} - -void MidiOutput::sendMessageNow (const MidiMessage& message) -{ - if (auto* androidMidi = internal.get()) - { - auto* env = getEnv(); - auto messageSize = message.getRawDataSize(); - - LocalRef messageContent (env->NewByteArray (messageSize)); - auto content = messageContent.get(); - - auto* rawBytes = env->GetByteArrayElements (content, nullptr); - std::memcpy (rawBytes, message.getRawData(), static_cast (messageSize)); - env->ReleaseByteArrayElements (content, rawBytes, 0); - - androidMidi->send (content, (jint) 0, (jint) messageSize); - } -} - -MidiDeviceListConnection MidiDeviceListConnection::make (std::function callback) -{ - auto& broadcaster = MidiDeviceListConnectionBroadcaster::get(); - return { &broadcaster, broadcaster.add (std::move (callback)) }; + return AndroidMidiHelpers::EndpointsImplNative::make (l); } } // namespace juce diff --git a/modules/juce_audio_devices/native/juce_Midi_linux.cpp b/modules/juce_audio_devices/native/juce_Midi_linux.cpp index 0243d6c6bc..6f8764d485 100644 --- a/modules/juce_audio_devices/native/juce_Midi_linux.cpp +++ b/modules/juce_audio_devices/native/juce_Midi_linux.cpp @@ -37,750 +37,1531 @@ namespace juce #if JUCE_ALSA -//============================================================================== -class AlsaClient +struct AlsaMidiHelpers { - auto lowerBound (int portId) const + struct InputCallback { - 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 JUCE_ALSA_MIDI_NAME - return JUCE_ALSA_MIDI_NAME; - #else - if (auto* app = JUCEApplicationBase::getInstance()) - return app->getApplicationName(); - - return "JUCE"; - #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; + virtual ~InputCallback() = default; + virtual void pushUmp (int port, ump::View, double time) = 0; + virtual void pushBytes (int port, ump::BytestreamMidiView) = 0; }; - static std::shared_ptr getInstance() + struct PortExitCallback { - static std::weak_ptr ptr; + virtual ~PortExitCallback() = default; + virtual void portExit (snd_seq_addr_t port) = 0; + }; - 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) + struct PortsChangedCallback { - const ScopedLock sl (callbackLock); + virtual ~PortsChangedCallback() = default; + virtual void notifyPortsChanged() = 0; + }; - if (auto* port = findPort (event->dest.port)) - port->handleIncomingMidiMessage (message); - } - - void handlePartialSysexMessage (snd_seq_event* event, const uint8* messageData, int numBytesSoFar, double timeStamp) + class UpdateNotifier : private AsyncUpdater { - const ScopedLock sl (callbackLock); + public: + explicit UpdateNotifier (PortsChangedCallback& c) : cb (c) {} + ~UpdateNotifier() override { cancelPendingUpdate(); } + using AsyncUpdater::triggerAsyncUpdate; - if (auto* port = findPort (event->dest.port)) - port->handlePartialSysexMessage (messageData, numBytesSoFar, timeStamp); - } + private: + void handleAsyncUpdate() override { cb.notifyPortsChanged(); } - snd_seq_t* get() const noexcept { return handle; } - int getId() const noexcept { return clientId; } + PortsChangedCallback& cb; + }; - 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 (snd_seq_t* h, InputCallback& cb, PortsChangedCallback& pc, PortExitCallback& pe) + : seqHandle (h), inputCallback (cb), portExit (pe), notifier (pc) {} ~SequencerThread() noexcept { shouldStop = true; thread.join(); + + if (0 <= queueId) + { + snd_seq_stop_queue (seqHandle, queueId, nullptr); + snd_seq_free_queue (seqHandle, queueId); + } + } + + int getQueueId() const + { + return queueId; } 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 + static bool isUmp (const snd_seq_ump_event_t* ev) { return (ev->flags & (1 << 5)) != 0; } + + template + || std::is_same_v, int> = 0> + double computeTimestampWithConvertedBase (const Event* ev) const { - public: - ~UpdateNotifier() override { cancelPendingUpdate(); } - using AsyncUpdater::triggerAsyncUpdate; + // We asked for wallclock timestamps - if the incoming event doesn't comply, then + // we'll have to approximate a timestamp ourselves. + if (snd_seq_ev_is_direct (ev) || ! snd_seq_ev_is_real (ev)) + return Time::getMillisecondCounter() * 0.001; - private: - void handleAsyncUpdate() override { MidiDeviceListConnectionBroadcaster::get().notify(); } - }; + const auto nativeTime = ev->time.time; + const auto initialNanos = (double) startTimeNative.tv_sec * 1e9 + (double) startTimeNative.tv_nsec; + const auto currentNanos = (double) nativeTime.tv_sec * 1e9 + (double) nativeTime.tv_nsec; + const auto elapsedNanos = currentNanos - initialNanos; + const auto elapsedMillis = elapsedNanos / 1e6; - AlsaClient& client; - MidiDataConcatenator concatenator { 2048 }; + // Perhaps this could happen if creating the queue failed, or if the event timestamp isn't + // populated for some other reason. + if (elapsedMillis <= 0) + return Time::getMillisecondCounter() * 0.001; + + return ((double) startTimeMillis + elapsedMillis) * 0.001; + } + + void processEvent (Span buffer, snd_midi_event_t* midiParser) + { + 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 umpEvent = std::invoke ([&] + { + snd_seq_ump_event_t* ev = nullptr; + return snd_seq_ump_event_input != nullptr && snd_seq_ump_event_input (seqHandle, &ev) >= 0 ? ev : nullptr; + }); + + if (umpEvent != nullptr && isUmp (umpEvent)) + { + inputCallback.pushUmp (umpEvent->dest.port, + ump::View { umpEvent->ump }, + computeTimestampWithConvertedBase (umpEvent)); + return; + } + + const auto seqEvent = std::invoke ([&] + { + if (umpEvent != nullptr) + return reinterpret_cast (umpEvent); + + snd_seq_event_t* ev = nullptr; + return snd_seq_event_input (seqHandle, &ev) >= 0 ? ev : nullptr; + }); + + if (seqEvent == nullptr) + return; + + const ScopeGuard freeInputEvent { [&] { snd_seq_free_event (seqEvent); } }; + + const auto foundEvent = std::find (std::begin (systemEvents), + std::end (systemEvents), + seqEvent->type); + + if (foundEvent != std::end (systemEvents)) + { + notifier.triggerAsyncUpdate(); + + if (seqEvent->type == SND_SEQ_EVENT_PORT_EXIT) + portExit.portExit (seqEvent->data.addr); + + return; + } + + // Disable running status for decoded MIDI messages + snd_midi_event_no_status (midiParser, 1); + + // xxx what about SYSEXes that are too big for the buffer? + const auto numBytes = snd_midi_event_decode (midiParser, + unalignedPointerCast (buffer.data()), + (long) buffer.size(), + seqEvent); + + snd_midi_event_reset_decode (midiParser); + + if (numBytes < 0) + { + // UMP messages may not convert to MIDI 1.0 events, in which case decoding will + // return -ENOENT. This is permissible, but other failures probably indicate a real + // problem. + jassert (numBytes == -ENOENT); + return; + } + + const Span bytes (buffer.data(), (size_t) numBytes); + inputCallback.pushBytes (seqEvent->dest.port, + { bytes, computeTimestampWithConvertedBase (seqEvent) }); + } + + static constexpr auto maxEventSize = 16 * 1024; + snd_seq_t* seqHandle = nullptr; + int queueId = seqHandle != nullptr ? snd_seq_alloc_queue (seqHandle) : -1; + InputCallback& inputCallback; + PortExitCallback& portExit; std::atomic shouldStop { false }; UpdateNotifier notifier; + + const snd_seq_real_time_t startTimeNative = std::invoke ([&] + { + if (queueId < 0) + return snd_seq_real_time_t{}; + + snd_seq_start_queue (seqHandle, queueId, nullptr); + snd_seq_drain_output (seqHandle); + + snd_seq_queue_status_t* queueStatus{}; + snd_seq_queue_status_alloca (&queueStatus); + + if (snd_seq_get_queue_status (seqHandle, queueId, queueStatus) != 0) + return snd_seq_real_time_t{}; + + return *snd_seq_queue_status_get_real_time (queueStatus); + }); + + const uint32_t startTimeMillis = Time::getMillisecondCounter(); + std::thread thread { [this] { - Thread::setCurrentThreadName ("JUCE MIDI Input"); + Thread::setCurrentThreadName (SystemStats::getJUCEVersion() + ": ALSA MIDI Input"); - auto seqHandle = client.get(); - - const int maxEventSize = 16 * 1024; snd_midi_event_t* midiParser; - if (snd_midi_event_new (maxEventSize, &midiParser) >= 0) + if (snd_midi_event_new (maxEventSize, &midiParser) < 0) + return; + + 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) { - const ScopeGuard freeMidiEvent { [&] { snd_midi_event_free (midiParser); } }; + // 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) + continue; - 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); + if (shouldStop) + break; - std::vector buffer (maxEventSize); - - while (! shouldStop) + do { - // 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); - } + processEvent (buffer, midiParser); } + 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) + static String getFormattedPortIdentifier (int clientId, int portId) { - 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 String (clientId) + "-" + String (portId); } - 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()) + template + static constexpr std::array makeBytesLittleEndian (unsigned int b) { - snd_seq_system_info_t* systemInfo = nullptr; - snd_seq_client_info_t* clientInfo = nullptr; + std::array result; - snd_seq_system_info_alloca (&systemInfo); - jassert (systemInfo != nullptr); - - if (snd_seq_system_info (seqHandle, systemInfo) == 0) + for (size_t i = 0; i != N; ++i) { - snd_seq_client_info_alloca (&clientInfo); - jassert (clientInfo != nullptr); + result[i] = std::byte { (uint8_t) b }; + b >>= 8; + } - auto numClients = snd_seq_system_info_get_cur_clients (systemInfo); + return result; + } - while (--numClients >= 0) + static constexpr unsigned int fromBytesLittleEndian (Span bytes) + { + unsigned int result{}; + + for (auto& byte : bytes) + { + result <<= 8; + result |= (uint8_t) byte; + } + + return result; + } + + struct AlsaClientInfo + { + static AlsaClientInfo makeUmpEndpoint (snd_seq_t* seq, + snd_seq_client_info_t* client, + const snd_ump_endpoint_info_t* endpoint) + { + const auto clientId = snd_seq_client_info_get_client (client); + const auto portId = 0; + + const auto protocol = snd_ump_endpoint_info_get_protocol (endpoint) == SND_UMP_EP_INFO_PROTO_MIDI2 + ? ump::PacketProtocol::MIDI_2_0 + : ump::PacketProtocol::MIDI_1_0; + + const auto legacyId = getFormattedPortIdentifier (clientId, portId); + const auto manufacturer = snd_ump_endpoint_info_get_manufacturer_id (endpoint); + const auto family = snd_ump_endpoint_info_get_family_id (endpoint); + const auto model = snd_ump_endpoint_info_get_model_id (endpoint); + const auto revisionPtr = snd_ump_endpoint_info_get_sw_revision (endpoint); + + std::array revision; + std::transform (revisionPtr, revisionPtr + 4, revision.data(), [] (auto x) { return std::byte { x }; }); + + const ump::DeviceInfo deviceInfo { makeBytesLittleEndian<3> (manufacturer), + makeBytesLittleEndian<2> (family), + makeBytesLittleEndian<2> (model), + revision }; + + const auto version = makeBytesLittleEndian<2> (snd_ump_endpoint_info_get_version (endpoint)); + const auto flags = snd_ump_endpoint_info_get_flags (endpoint); + const auto caps = snd_ump_endpoint_info_get_protocol_caps (endpoint); + + std::array blocks; + size_t numPushedBlocks = 0; + const auto pushBlock = [&] (ump::Block x) { blocks[numPushedBlocks++] = x; }; + + const auto numBlocks = snd_ump_endpoint_info_get_num_blocks (endpoint); + snd_ump_block_info_t* block = nullptr; + snd_ump_block_info_alloca (&block); + + for (auto i = decltype (numBlocks){}; i < numBlocks; ++i) { - if (snd_seq_query_next_client (seqHandle, clientInfo) == 0) - { - port = iterateMidiClient (*client, - clientInfo, - forInput, - devices, - deviceIdentifierToOpen); + if (snd_seq_get_ump_block_info (seq, clientId, (int) i, block) != 0) + continue; - if (port != nullptr) + const auto uiHint = std::invoke ([&] + { + switch (snd_ump_block_info_get_ui_hint (block)) + { + case SND_UMP_BLOCK_UI_HINT_BOTH: return ump::BlockUiHint::bidirectional; + case SND_UMP_BLOCK_UI_HINT_SENDER: return ump::BlockUiHint::sender; + case SND_UMP_BLOCK_UI_HINT_RECEIVER: return ump::BlockUiHint::receiver; + case SND_UMP_BLOCK_UI_HINT_UNKNOWN: break; + } + + return ump::BlockUiHint::unknown; + }); + + const auto blockFlags = snd_ump_block_info_get_flags (block); + + const auto proxy = std::invoke ([&] + { + if (blockFlags & SND_UMP_BLOCK_IS_MIDI1) + { + if (blockFlags & SND_UMP_BLOCK_IS_LOWSPEED) + return ump::BlockMIDI1ProxyKind::restrictedBandwidth; + + return ump::BlockMIDI1ProxyKind::unrestrictedBandwidth; + } + + return ump::BlockMIDI1ProxyKind::inapplicable; + }); + + const auto direction = std::invoke ([&] + { + switch (snd_ump_block_info_get_direction (block)) + { + case SND_UMP_DIR_INPUT: return ump::BlockDirection::receiver; + case SND_UMP_DIR_OUTPUT: return ump::BlockDirection::sender; + case SND_UMP_DIR_BIDIRECTION: return ump::BlockDirection::bidirectional; + } + + return ump::BlockDirection::unknown; + }); + + const auto b = ump::Block{}.withName (snd_ump_block_info_get_name (block)) + .withFirstGroup ((uint8_t) snd_ump_block_info_get_first_group (block)) + .withNumGroups ((uint8_t) snd_ump_block_info_get_num_groups (block)) + .withMaxSysex8Streams ((uint8_t) snd_ump_block_info_get_sysex8_streams (block)) + .withEnabled (snd_ump_block_info_get_active (block)) + .withUiHint (uiHint) + .withMIDI1ProxyKind (proxy) + .withDirection (direction); + pushBlock (b); + } + + const auto ep = ump::Endpoint{}.withName (snd_seq_client_info_get_name (client)) + .withProtocol (protocol) + .withDeviceInfo (deviceInfo) + .withProductInstanceId (snd_ump_endpoint_info_get_product_id (endpoint)) + .withUMPVersion ((uint8_t) version[1], (uint8_t) version[0]) + .withStaticBlocks (flags & SND_UMP_EP_INFO_STATIC_BLOCKS) + .withMidi1Support (caps & SND_UMP_EP_INFO_PROTO_MIDI1) + .withMidi2Support (caps & SND_UMP_EP_INFO_PROTO_MIDI2) + .withReceiveJRSupport (caps & SND_UMP_EP_INFO_PROTO_JRTS_RX) + .withTransmitJRSupport (caps & SND_UMP_EP_INFO_PROTO_JRTS_TX) + .withBlocks (Span { blocks.data(), numPushedBlocks }); + + return { clientId, + portId, + { ep, getStaticDeviceInfo (seq, client), ump::EndpointId::makeSrcDst (legacyId, legacyId) } }; + } + + static ump::StaticDeviceInfo getStaticDeviceInfo (snd_seq_t* seq, snd_seq_client_info_t* client) + { + const auto clientId = snd_seq_client_info_get_client (client); + + const auto numPorts = snd_seq_client_info_get_num_ports (client); + snd_seq_port_info_t* port = nullptr; + snd_seq_port_info_alloca (&port); + + std::array identifiersSrc, identifiersDst; + + for (auto i = decltype (numPorts){}; i < numPorts; ++i) + { + if (snd_seq_get_any_port_info (seq, clientId, i, port) != 0) + continue; + + const auto groupIndexFrom1 = snd_seq_port_info_get_ump_group (port); + + if (groupIndexFrom1 == 0) + continue; + + const auto identifier = getFormattedPortIdentifier (clientId, i); + const auto d = snd_seq_port_info_get_direction (port); + + const auto portCaps = snd_seq_port_info_get_capability (port); + + // Avoid giving identifiers to inactive ports, because we don't want disabled + // ports to show up when listing MIDI 1.0 ports + if ((portCaps & SND_SEQ_PORT_CAP_INACTIVE) != 0) + continue; + + if (d == SND_SEQ_PORT_DIR_BIDIRECTION || d == SND_SEQ_PORT_DIR_INPUT) + identifiersDst[(size_t) (groupIndexFrom1 - 1)] = identifier; + + if (d == SND_SEQ_PORT_DIR_BIDIRECTION || d == SND_SEQ_PORT_DIR_OUTPUT) + identifiersSrc[(size_t) (groupIndexFrom1 - 1)] = identifier; + } + + const auto version = snd_seq_client_info_get_midi_version (client); + + const auto si = ump::StaticDeviceInfo{}.withName (snd_seq_client_info_get_name (client)) + .withManufacturer ("") + .withProduct ("") + .withTransport (version == SND_SEQ_CLIENT_LEGACY_MIDI + ? ump::Transport::bytestream + : ump::Transport::ump) + .withLegacyIdentifiersSrc (identifiersSrc) + .withLegacyIdentifiersDst (identifiersDst); + + if (snd_seq_get_any_port_info (seq, clientId, 0, port) == 0) + { + return si.withHasSource (snd_seq_port_info_get_direction (port) != SND_SEQ_PORT_DIR_INPUT) + .withHasDestination (snd_seq_port_info_get_direction (port) != SND_SEQ_PORT_DIR_OUTPUT); + } + + return si; + } + + static AlsaClientInfo makeProxy (snd_seq_client_info_t* client, + const snd_seq_port_info_t* port) + { + const auto clientId = snd_seq_client_info_get_client (client); + const auto portId = snd_seq_port_info_get_port (port); + const auto identifier = getFormattedPortIdentifier (clientId, portId); + + jassert (snd_seq_port_info_get_ump_group == nullptr || snd_seq_port_info_get_ump_group (port) == 0); + + const auto kind = std::invoke ([&] + { + if (snd_seq_port_info_get_direction == nullptr) + { + const auto caps = snd_seq_port_info_get_capability (port); + constexpr auto mask = SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_WRITE; + + if ((caps & mask) == mask) + return ump::BlockDirection::bidirectional; + + if ((caps & mask) == SND_SEQ_PORT_CAP_READ) + return ump::BlockDirection::sender; + + if ((caps & mask) == SND_SEQ_PORT_CAP_WRITE) + return ump::BlockDirection::receiver; + + jassertfalse; + return ump::BlockDirection::unknown; + } + + switch (snd_seq_port_info_get_direction (port)) + { + case SND_SEQ_PORT_DIR_INPUT: return ump::BlockDirection::sender; + case SND_SEQ_PORT_DIR_OUTPUT: return ump::BlockDirection::receiver; + case SND_SEQ_PORT_DIR_BIDIRECTION: return ump::BlockDirection::bidirectional; + } + + jassertfalse; + return ump::BlockDirection::unknown; + }); + + const auto fullInfo = ump::IOHelpers::makeProxyEndpoint ({ snd_seq_port_info_get_name (port), identifier }, kind); + + return { clientId, portId, fullInfo }; + } + + int clientId; + int portId; + ump::EndpointAndStaticInfo fullInfo; + }; + + struct SeqDestructor + { + void operator() (snd_seq_t* ptr) const + { + if (ptr != nullptr) + snd_seq_close (ptr); + } + }; + + class Client : private InputCallback, + private PortExitCallback, + private PortsChangedCallback + { + public: + static std::unique_ptr make (ump::EndpointsListener* l) + { + snd_seq_t* handle{}; + const auto error = snd_seq_open (&handle, "default", SND_SEQ_OPEN_DUPLEX, 0); + + if (error != 0 || handle == nullptr) + { + jassertfalse; + return {}; + } + + snd_seq_nonblock (handle, SND_SEQ_NONBLOCK); + snd_seq_set_client_name (handle, getAlsaMidiName().toRawUTF8()); + + if (snd_seq_set_client_midi_version != nullptr) + snd_seq_set_client_midi_version (handle, SND_SEQ_CLIENT_UMP_MIDI_2_0); + + return rawToUniquePtr (new Client (l, handle)); + } + + static String getAlsaMidiName() + { + #ifdef JUCE_ALSA_MIDI_NAME + return JUCE_ALSA_MIDI_NAME + #else + return ump::Endpoints::Impl::getGlobalMidiClientName(); + #endif + } + + snd_seq_t* getSequencer() const { return handle.get(); } + + void addInputCallback (InputCallback& c) + { + inputCallbacks.add (c); + } + + void removeInputCallback (InputCallback& c) + { + inputCallbacks.remove (c); + } + + void addPortExitCallback (PortExitCallback& c) + { + portExitCallbacks.add (c); + } + + void removePortExitCallback (PortExitCallback& c) + { + portExitCallbacks.remove (c); + } + + int getClientId() const { return clientId; } + int getQueueId() const { return inputThread.getQueueId(); } + + String getName() const + { + snd_seq_client_info_t* info{}; + snd_seq_client_info_alloca (&info); + snd_seq_get_client_info (handle.get(), info); + return CharPointer_UTF8 { snd_seq_client_info_get_name (info) }; + } + + void getEndpoints (std::vector& result) const + { + std::transform (cachedEndpoints.begin(), + cachedEndpoints.end(), + std::back_inserter (result), + [] (auto& item) { return item.first; }); + } + + std::optional getClientInfo (const ump::EndpointId& id) const + { + const auto iter = cachedEndpoints.find (id); + + if (iter == cachedEndpoints.end()) + return {}; + + return iter->second; + } + + private: + Client (ump::EndpointsListener* l, snd_seq_t* h) + : listener (l), handle (h) + { + jassert (handle != nullptr); + } + + void pushUmp (int port, ump::View view, double time) override + { + inputCallbacks.call ([&] (auto& l) { l.pushUmp (port, view, time); }); + } + + void pushBytes (int port, ump::BytestreamMidiView view) override + { + inputCallbacks.call ([&] (auto& l) { l.pushBytes (port, view); }); + } + + void portExit (snd_seq_addr_t port) override + { + portExitCallbacks.call ([&] (auto& l) { l.portExit (port); }); + } + + void notifyPortsChanged() override + { + cachedEndpoints = findEndpoints (handle.get()); + + if (listener != nullptr) + listener->endpointsChanged(); + } + + static std::map findEndpoints (snd_seq_t* seq) + { + std::map result; + + snd_seq_client_info_t* clientInfo = nullptr; + snd_seq_client_info_alloca (&clientInfo); + + snd_seq_port_info_t* portInfo = nullptr; + snd_seq_port_info_alloca (&portInfo); + + while (snd_seq_query_next_client (seq, clientInfo) == 0) + { + const auto clientId = snd_seq_client_info_get_client (clientInfo); + + if (snd_seq_get_ump_endpoint_info != nullptr && snd_ump_endpoint_info_sizeof != nullptr) + { + snd_ump_endpoint_info_t* endpointInfo = nullptr; + snd_ump_endpoint_info_alloca (&endpointInfo); + + if (snd_seq_get_ump_endpoint_info (seq, clientId, endpointInfo) == 0) + { + // This is a UMP client, so treat this as a UMP endpoint + const auto info = AlsaClientInfo::makeUmpEndpoint (seq, clientInfo, endpointInfo); + result.emplace (info.fullInfo.id, info); + continue; + } + } + + // This isn't a UMP endpoint, so iterate each port, creating a proxy endpoint for each + snd_seq_port_info_set_client (portInfo, clientId); + snd_seq_port_info_set_port (portInfo, -1); + + while (snd_seq_query_next_port (seq, portInfo) == 0) + { + constexpr auto mask = SND_SEQ_PORT_CAP_SUBS_READ | SND_SEQ_PORT_CAP_SUBS_WRITE; + + if ((snd_seq_port_info_get_capability (portInfo) & mask) == 0) + continue; + + const auto info = AlsaClientInfo::makeProxy (clientInfo, portInfo); + result.emplace (info.fullInfo.id, info); + } + } + + return result; + } + + ump::EndpointsListener* listener{}; + std::unique_ptr handle; + int clientId = snd_seq_client_id (handle.get()); + WaitFreeListeners inputCallbacks; + WaitFreeListeners portExitCallbacks; + std::map cachedEndpoints; + SequencerThread inputThread { handle.get(), *this, *this, *this }; + }; + + class Port : private InputCallback, + private PortExitCallback, + private AsyncUpdater + { + public: + ~Port() override + { + client->removePortExitCallback (*this); + client->removeInputCallback (*this); + + cancelPendingUpdate(); + + if (portId >= 0) + { + if (connected.has_value()) + { + if (direction == ump::IOKind::src) + snd_seq_disconnect_from (client->getSequencer(), portId, connected->client, connected->port); + else + snd_seq_disconnect_to (client->getSequencer(), portId, connected->client, connected->port); + } + + snd_seq_delete_simple_port (client->getSequencer(), portId); + } + } + + std::optional getConnected() const { return connected; } + + int getPortId() const { return jmax (0, portId); } + + ump::EndpointId getId() const + { + const auto id = connected.has_value() + ? getFormattedPortIdentifier (connected->client, connected->port) + : ("VIRTUAL" + getFormattedPortIdentifier (client->getClientId(), getPortId())); + + return ump::EndpointId::makeSrcDst (id, id); + } + + ump::StaticDeviceInfo getStaticDeviceInfo() const + { + snd_seq_client_info_t* clientInfo{}; + snd_seq_client_info_alloca (&clientInfo); + snd_seq_get_any_client_info (client->getSequencer(), client->getClientId(), clientInfo); + return AlsaClientInfo::getStaticDeviceInfo (client->getSequencer(), clientInfo); + } + + void addInputCallback (InputCallback& c) + { + inputCallbacks.add (c); + } + + void removeInputCallback (InputCallback& c) + { + inputCallbacks.remove (c); + } + + /* Disconnection listener is called on the main thread. */ + void addDisconnectionListener (ump::DisconnectionListener& c) + { + disconnectCallbacks.add (&c); + } + + void removeDisconnectionListener (ump::DisconnectionListener& c) + { + disconnectCallbacks.remove (&c); + } + + std::shared_ptr getClient() const + { + return client; + } + + bool isSrc() const { return ! direction.has_value() || *direction == ump::IOKind::src; } + bool isDst() const { return ! direction.has_value() || *direction == ump::IOKind::dst; } + + static std::unique_ptr makeUmpEndpoint (const String& name, + const ump::DeviceInfo& info, + const String& productInstance, + ump::PacketProtocol protocol, + Span blocks, + ump::BlocksAreStatic areStatic) + { + if (snd_seq_create_ump_endpoint == nullptr) + return {}; + + auto virtualClient = Client::make (nullptr); + + if (virtualClient == nullptr) + return {}; + + const auto findMaxGroup = [] (auto acc, const auto& block) + { + return std::max (acc, block.getFirstGroup() + block.getNumGroups()); + }; + + const auto numGroupsRequired = (unsigned int) std::accumulate (blocks.begin(), + blocks.end(), + 0, + findMaxGroup); + + const unsigned int caps = protocol == ump::PacketProtocol::MIDI_2_0 + ? SND_UMP_EP_INFO_PROTO_MIDI2 + : SND_UMP_EP_INFO_PROTO_MIDI1; + + snd_ump_endpoint_info_t* e{}; + snd_ump_endpoint_info_alloca (&e); + + snd_ump_endpoint_info_set_protocol (e, caps); + snd_ump_endpoint_info_set_protocol_caps (e, caps); + snd_ump_endpoint_info_set_name (e, name.toRawUTF8()); + snd_ump_endpoint_info_set_manufacturer_id (e, fromBytesLittleEndian (info.manufacturer)); + snd_ump_endpoint_info_set_family_id (e, fromBytesLittleEndian (info.family)); + snd_ump_endpoint_info_set_model_id (e, fromBytesLittleEndian (info.modelNumber)); + snd_ump_endpoint_info_set_sw_revision (e, reinterpret_cast (info.revision.data())); + snd_ump_endpoint_info_set_flags (e, areStatic == ump::BlocksAreStatic::yes ? SND_UMP_EP_INFO_STATIC_BLOCKS : 0); + snd_ump_endpoint_info_set_num_blocks (e, (unsigned int) blocks.size()); + snd_ump_endpoint_info_set_product_id (e, productInstance.toRawUTF8()); + + auto* sequencer = virtualClient->getSequencer(); + + if (const auto code = snd_seq_create_ump_endpoint (sequencer, e, numGroupsRequired); code != 0) + return {}; + + for (const auto [index, block] : enumerate (blocks, uint8_t{})) + { + snd_ump_block_info_t* b{}; + snd_ump_block_info_alloca (&b); + + copyToBlock (*b, index, block); + + if (const auto code = snd_seq_create_ump_block (sequencer, index, b); code != 0) + return {}; + } + + return rawToUniquePtr (new Port { std::move (virtualClient), -1, std::nullopt, std::nullopt }); + } + + static std::unique_ptr make (std::shared_ptr c, + ump::IOKind d, + std::optional connected, + String name) + { + if (c == nullptr || c->getSequencer() == nullptr) + return {}; + + const auto virtualFlags = ! connected.has_value() + ? (d == ump::IOKind::src ? SND_SEQ_PORT_CAP_SUBS_WRITE : SND_SEQ_PORT_CAP_SUBS_READ) + : SND_SEQ_PORT_CAP_NO_EXPORT; + const auto readWriteFlags = d == ump::IOKind::src ? SND_SEQ_PORT_CAP_WRITE : SND_SEQ_PORT_CAP_READ; + + const auto caps = (unsigned int) (virtualFlags | readWriteFlags); + + const auto portId = snd_seq_create_simple_port (c->getSequencer(), + name.toRawUTF8(), + caps, + SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION); + + if (portId < 0) + { + jassertfalse; + return {}; + } + + if (connected.has_value()) + { + snd_seq_port_subscribe_t* subs{}; + snd_seq_port_subscribe_alloca (&subs); + + // Setting a queue for the subscription is necessary in order to receive timestamps + snd_seq_port_subscribe_set_time_real (subs, 1); + snd_seq_port_subscribe_set_time_update (subs, 1); + snd_seq_port_subscribe_set_queue (subs, c->getQueueId()); + + const snd_seq_addr_t selfPort { (unsigned char) c->getClientId(), + (unsigned char) portId }; + + snd_seq_port_subscribe_set_sender (subs, d == ump::IOKind::src ? &*connected : &selfPort); + snd_seq_port_subscribe_set_dest (subs, d == ump::IOKind::src ? &selfPort : &*connected); + + [[maybe_unused]] const auto code = snd_seq_subscribe_port (c->getSequencer(), subs); + jassert (code == 0); + } + + return rawToUniquePtr (new Port { c, portId, d, connected }); + } + + bool isUmpEndpoint() const + { + return portId == -1; + } + + private: + Port (std::shared_ptr c, int p, std::optional dir, std::optional dst) + : client (c), + portId (p), + direction (dir), + connected (dst) + { + jassert (client != nullptr); + + client->addInputCallback (*this); + client->addPortExitCallback (*this); + } + + void pushUmp (int port, ump::View view, double time) override + { + if (portId == -1 || port == portId) + inputCallbacks.call ([&] (auto& c) { c.pushUmp (port, view, time); }); + } + + void pushBytes (int port, ump::BytestreamMidiView view) override + { + // If this is hit, we've ended up pushing bytestream MIDI to a UMP endpoint, which + // won't work. This is a JUCE bug, please let the JUCE maintainers know! + jassert (portId != -1); + + if (port == portId) + inputCallbacks.call ([&] (auto& c) { c.pushBytes (port, view); }); + } + + void portExit (snd_seq_addr_t port) override + { + const auto notify = connected.has_value() + ? (port.client == connected->client && port.port == connected->port) + : (port.client == client->getClientId() && port.port == portId); + + if (notify) + triggerAsyncUpdate(); + } + + void handleAsyncUpdate() override + { + disconnectCallbacks.call ([&] (auto& c) { c.disconnected(); }); + } + + std::shared_ptr client; + int portId = -1; // A negative portId indicates this is a special UMP virtual port + std::optional direction; // nullopt == bidirectional + std::optional connected; + WaitFreeListeners inputCallbacks; + // Disconnect listeners are called on the main thread + ListenerList disconnectCallbacks; + + JUCE_DECLARE_NON_COPYABLE (Port) + }; + + //============================================================================== + class InputImplNative : public ump::Input::Impl::Native, + private InputCallback + { + auto getInputCallback (double time) + { + return [this, time] (ump::View v) + { + ump::Iterator b (v.data(), v.size()); + auto e = std::next (b); + consumer.consume (b, e, time); + }; + } + + public: + InputImplNative (ump::DisconnectionListener& l, + std::shared_ptr p, + ump::PacketProtocol protocol, + ump::Consumer& c) + : listener (l), + port (std::move (p)), + converter (protocol), + consumer (c) + { + port->addInputCallback (*this); + port->addDisconnectionListener (listener); + } + + ~InputImplNative() override + { + port->removeDisconnectionListener (listener); + port->removeInputCallback (*this); + } + + ump::EndpointId getEndpointId() const override + { + return port->getId(); + } + + ump::PacketProtocol getProtocol() const override + { + return converter.getProtocol(); + } + + private: + void pushUmp (int, ump::View view, double time) override + { + converter.convert (view, getInputCallback (time)); + } + + void pushBytes (int, ump::BytestreamMidiView view) override + { + // Bytestream messages that we're sent don't have a built-in group. + // We currently make a separate endpoint for each bytestream port, rather than combining + // ports into endpoints, so we can stick with group 0 here. + converter.convert ({ 0, view.bytes }, getInputCallback (view.timestamp)); + } + + ump::DisconnectionListener& listener; + std::shared_ptr port; + ump::GenericUMPConverter converter; + ump::Consumer& consumer; + }; + + class OutputImplNative : public ump::Output::Impl::Native + { + public: + OutputImplNative (ump::DisconnectionListener& l, std::shared_ptr portIn) + : listener (l), port (std::move (portIn)) + { + snd_midi_event_t* ptr = nullptr; + [[maybe_unused]] const auto code = snd_midi_event_new (maxEventSize, &ptr); + jassert (code == 0); + midiParser.reset (ptr); + + port->addDisconnectionListener (listener); + } + + ~OutputImplNative() override + { + port->removeDisconnectionListener (listener); + } + + bool send (ump::Iterator b, ump::Iterator e) override + { + if (snd_seq_ump_event_output_direct != nullptr) + { + for (const auto& v : makeRange (b, e)) + sendUmp (v); + } + else + { + for (const auto& v : makeRange (b, e)) + sendBytestream (v); + } + + return true; + } + + ump::EndpointId getEndpointId() const override + { + return port->getId(); + } + + private: + struct SndMidiEventDeleter + { + void operator() (snd_midi_event_t* ptr) const + { + if (ptr != nullptr) + snd_midi_event_free (ptr); + } + }; + + void sendUmp (ump::View v) + { + if (snd_seq_ump_event_output_direct == nullptr) + return; + + snd_seq_ump_event_t event{}; + + snd_seq_ev_set_source (&event, port->getPortId()); + snd_seq_ev_set_subs (&event); + snd_seq_ev_set_direct (&event); + + event.flags |= SND_SEQ_EVENT_UMP; + event.type = 0; + memcpy (event.ump, v.data(), sizeof (uint32_t) * v.size()); + + [[maybe_unused]] const auto code = snd_seq_ump_event_output_direct (port->getClient()->getSequencer(), &event); + jassert (code >= 0); + } + + void sendBytestream (ump::View v) + { + toBytestream.convert (v, 0.0, [this] (ump::BytesOnGroup message, double) + { + if (message.bytes.size() > maxEventSize) + { + maxEventSize = message.bytes.size(); + + snd_midi_event_t* ptr = nullptr; + const auto code = snd_midi_event_new (maxEventSize, &ptr); + jassertquiet (code == 0); + midiParser.reset (ptr); + } + + snd_seq_event_t event; + snd_seq_ev_clear (&event); + + auto numBytes = (long) message.bytes.size(); + auto* data = unalignedPointerCast (message.bytes.data()); + + const auto client = port->getClient(); + auto seqHandle = client->getSequencer(); + + while (numBytes > 0) + { + auto numSent = snd_midi_event_encode (midiParser.get(), data, numBytes, &event); + + if (numSent <= 0) + break; + + numBytes -= numSent; + data += numSent; + + snd_seq_ev_set_source (&event, (unsigned char) port->getPortId()); + snd_seq_ev_set_subs (&event); + snd_seq_ev_set_direct (&event); + + if (snd_seq_event_output_direct (seqHandle, &event) < 0) break; } - } + + snd_midi_event_reset_encode (midiParser.get()); + }); } + + ump::DisconnectionListener& listener; + std::shared_ptr port; + ump::ToBytestreamConverter toBytestream { 4096 }; + + std::unique_ptr midiParser; + size_t maxEventSize = 4096; + }; + + static void copyToBlock (snd_ump_block_info_t& dst, uint8_t index, const ump::Block& src) + { + const auto direction = std::invoke ([&] + { + switch (src.getDirection()) + { + case ump::BlockDirection::bidirectional: return SND_UMP_DIR_BIDIRECTION; + case ump::BlockDirection::sender: return SND_UMP_DIR_OUTPUT; + case ump::BlockDirection::receiver: return SND_UMP_DIR_INPUT; + case ump::BlockDirection::unknown: break; + } + + return decltype (SND_UMP_DIR_BIDIRECTION){}; + }); + + const auto hint = std::invoke ([&] + { + switch (src.getUiHint()) + { + case ump::BlockUiHint::bidirectional: return SND_UMP_BLOCK_UI_HINT_BOTH; + case ump::BlockUiHint::sender: return SND_UMP_BLOCK_UI_HINT_SENDER; + case ump::BlockUiHint::receiver: return SND_UMP_BLOCK_UI_HINT_RECEIVER; + case ump::BlockUiHint::unknown: break; + } + + return SND_UMP_BLOCK_UI_HINT_UNKNOWN; + }); + + snd_ump_block_info_set_block_id (&dst, index); + snd_ump_block_info_set_active (&dst, src.isEnabled()); + snd_ump_block_info_set_direction (&dst, (unsigned int) direction); + snd_ump_block_info_set_ui_hint (&dst, (unsigned int) hint); + snd_ump_block_info_set_first_group (&dst, src.getFirstGroup()); + snd_ump_block_info_set_num_groups (&dst, src.getNumGroups()); + snd_ump_block_info_set_name (&dst, src.getName().toRawUTF8()); + snd_ump_block_info_set_sysex8_streams (&dst, src.getMaxSysex8Streams()); } - return port; -} + class PortWithInfo + { + public: + ump::EndpointId getId() const + { + return port->getId(); + } -struct AlsaPortPtr -{ - explicit AlsaPortPtr (AlsaClient::Port* p) - : ptr (p) {} + ump::Endpoint getEndpoint() const + { + return endpoint; + } - virtual ~AlsaPortPtr() noexcept { AlsaClient::getInstance()->deletePort (ptr); } + ump::StaticDeviceInfo getStaticDeviceInfo() const + { + return port->getStaticDeviceInfo(); + } - AlsaClient::Port* ptr = nullptr; + bool setBlock (uint8_t index, const ump::Block& block) + { + if (snd_seq_set_ump_block_info == nullptr) + return false; + + snd_ump_block_info_t* b{}; + snd_ump_block_info_alloca (&b); + + copyToBlock (*b, index, block); + + if (0 != snd_seq_set_ump_block_info (port->getClient()->getSequencer(), index, b)) + return false; + + endpoint.getBlocks()[index] = block; + return true; + } + + bool setName (const String& x) + { + if (0 != snd_seq_set_client_name (port->getClient()->getSequencer(), x.toRawUTF8())) + return false; + + endpoint = endpoint.withName (x); + return true; + } + + static std::unique_ptr make (std::shared_ptr p, ump::Endpoint e) + { + if (p == nullptr) + return {}; + + return rawToUniquePtr (new PortWithInfo { std::move (p), std::move (e) }); + } + + std::shared_ptr getPort() const { return port; } + + private: + PortWithInfo (std::shared_ptr p, ump::Endpoint e) + : port (std::move (p)), endpoint (e) {} + + std::shared_ptr port; + ump::Endpoint endpoint; + }; + + class VirtualEndpointImplNative : public ump::VirtualEndpoint::Impl::Native, + public ump::LegacyVirtualInput::Impl::Native, + public ump::LegacyVirtualOutput::Impl::Native + { + public: + ump::EndpointId getId() const override + { + return port->getId(); + } + + bool setBlock (uint8_t index, const ump::Block& block) override + { + return port->setBlock (index, block); + } + + bool setName (const String& x) override + { + return port->setName (x); + } + + static std::unique_ptr make (std::shared_ptr p) + { + if (p == nullptr) + return {}; + + return rawToUniquePtr (new VirtualEndpointImplNative { std::move (p) }); + } + + private: + explicit VirtualEndpointImplNative (std::shared_ptr p) + : port (std::move (p)) + { + } + + std::shared_ptr port; + }; + + struct VirtualEndpointRegistry + { + virtual ~VirtualEndpointRegistry() = default; + virtual void virtualEndpointAdded (std::shared_ptr) = 0; + }; + + class SessionImplNative : public ump::Session::Impl::Native + { + public: + explicit SessionImplNative (VirtualEndpointRegistry& r, std::shared_ptr c, String n) + : registry (r), + client (c), + name (n) + { + } + + String getName() const override + { + return name; + } + + std::unique_ptr connectInput (ump::DisconnectionListener& listener, + const ump::EndpointId& identifier, + ump::PacketProtocol protocol, + ump::Consumer& callback) override + { + if (auto port = findOrCreatePort ({ identifier, ump::IOKind::src })) + return rawToUniquePtr (new InputImplNative (listener, std::move (port), protocol, callback)); + + return {}; + } + + std::unique_ptr connectOutput (ump::DisconnectionListener& listener, + const ump::EndpointId& identifier) override + { + if (auto port = findOrCreatePort ({ identifier, ump::IOKind::dst })) + return rawToUniquePtr (new OutputImplNative (listener, std::move (port))); + + return {}; + } + + std::unique_ptr createNativeVirtualEndpoint (const String& n, + const ump::DeviceInfo& info, + const String& productInstance, + ump::PacketProtocol protocol, + Span blocks, + ump::BlocksAreStatic areStatic) override + { + return wrapPortAsEndpoint (createVirtualEndpointPort (n, info, productInstance, protocol, blocks, areStatic)); + } + + std::unique_ptr createLegacyVirtualInput (const String& deviceName) override + { + return wrapPortAsEndpoint (createLegacyEndpointPort (deviceName, ump::IOKind::dst)); + } + + std::unique_ptr createLegacyVirtualOutput (const String& deviceName) override + { + return wrapPortAsEndpoint (createLegacyEndpointPort (deviceName, ump::IOKind::src)); + } + + private: + struct Key + { + ump::EndpointId id; + ump::IOKind direction; + + bool operator< (const Key& other) const + { + const auto tie = [] (auto& x) { return std::tie (x.id, x.direction); }; + return tie (*this) < tie (other); + } + }; + + std::optional getClientInfo (const ump::EndpointId& identifier) const + { + return client->getClientInfo (identifier); + } + + std::shared_ptr findOrCreatePort (const Key& key) + { + auto& weak = weakPorts[key]; + + if (auto strong = weak.lock()) + return strong; + + const auto info = getClientInfo (key.id); + + if (! info.has_value()) + return {}; + + const std::shared_ptr strong = Port::make (client, + key.direction, + snd_seq_addr_t { (unsigned char) info->clientId, + (unsigned char) info->portId }, + client->getName()); + weak = strong; + return strong; + } + + static std::shared_ptr createVirtualEndpointPort (const String& n, + const ump::DeviceInfo& info, + const String& productInstance, + ump::PacketProtocol protocol, + Span blocks, + ump::BlocksAreStatic areStatic) + { + return PortWithInfo::make (Port::makeUmpEndpoint (n, info, productInstance, protocol, blocks, areStatic), + ump::Endpoint{}.withName (n) + .withDeviceInfo (info) + .withProductInstanceId (productInstance) + .withProtocol (protocol) + .withMidi1Support (protocol == ump::PacketProtocol::MIDI_1_0) + .withMidi2Support (protocol == ump::PacketProtocol::MIDI_2_0) + .withStaticBlocks (areStatic == ump::BlocksAreStatic::yes) + .withBlocks (blocks)); + } + + std::shared_ptr createLegacyEndpointPort (const String& portName, ump::IOKind direction) + { + const ump::Block blocks[] { ump::IOHelpers::makeLegacyBlock (direction == ump::IOKind::dst) }; + return PortWithInfo::make (Port::make (client, + direction == ump::IOKind::src ? ump::IOKind::dst : ump::IOKind::src, + std::nullopt, + portName), + ump::Endpoint{}.withName (portName) + .withProtocol (ump::PacketProtocol::MIDI_1_0) + .withMidi1Support (true) + .withStaticBlocks (true) + .withBlocks (blocks)); + } + + std::unique_ptr wrapPortAsEndpoint (std::shared_ptr port) + { + if (port == nullptr) + return {}; + + auto inner = port->getPort(); + + if (inner == nullptr) + return {}; + + if (inner->isSrc()) + weakPorts[{ port->getId(), ump::IOKind::src }] = inner; + + if (inner->isDst()) + weakPorts[{ port->getId(), ump::IOKind::dst }] = inner; + + registry.virtualEndpointAdded (port); + + return VirtualEndpointImplNative::make (port); + } + + VirtualEndpointRegistry& registry; + std::shared_ptr client; + std::map> weakPorts; + String name; + }; + + class AnnouncementsPort + { + public: + AnnouncementsPort() = default; + + explicit AnnouncementsPort (snd_seq_t* s) + : seq (s) + { + if (seq != nullptr) + snd_seq_connect_from (seq, portId, SND_SEQ_CLIENT_SYSTEM, SND_SEQ_PORT_SYSTEM_ANNOUNCE); + } + + AnnouncementsPort (AnnouncementsPort&& other) noexcept + : seq (std::exchange (other.seq, {})), + portId (std::exchange (other.portId, {})) + { + } + + AnnouncementsPort& operator= (AnnouncementsPort&& other) noexcept + { + AnnouncementsPort tmp { std::move (other) }; + std::swap (tmp.seq, seq); + std::swap (tmp.portId, portId); + return *this; + } + + AnnouncementsPort (const AnnouncementsPort&) = delete; + AnnouncementsPort& operator= (const AnnouncementsPort&) = delete; + + ~AnnouncementsPort() noexcept + { + if (seq == nullptr) + return; + + snd_seq_disconnect_from (seq, portId, SND_SEQ_CLIENT_SYSTEM, SND_SEQ_PORT_SYSTEM_ANNOUNCE); + snd_seq_delete_simple_port (seq, portId); + } + + private: + snd_seq_t* seq = nullptr; + int portId = seq != nullptr + ? snd_seq_create_simple_port (seq, + TRANS ("announcements").toRawUTF8(), + SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_NO_EXPORT, + SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION) + : 0; + }; + + class EndpointsImplNative : public ump::Endpoints::Impl::Native, + private VirtualEndpointRegistry + { + public: + explicit EndpointsImplNative (std::shared_ptr c) + : client (std::move (c)) + { + } + + void getEndpoints (std::vector& x) const override + { + client->getEndpoints (x); + } + + std::optional getEndpoint (const ump::EndpointId& x) const override + { + if (const auto iter = virtualPorts.find (x); iter != virtualPorts.end()) + if (auto strong = iter->second.lock()) + return strong->getEndpoint(); + + if (const auto& c = client->getClientInfo (x)) + return c->fullInfo.endpoint; + + return {}; + } + + std::optional getStaticDeviceInfo (const ump::EndpointId& x) const override + { + if (const auto iter = virtualPorts.find (x); iter != virtualPorts.end()) + if (auto strong = iter->second.lock()) + return strong->getStaticDeviceInfo(); + + if (const auto& c = client->getClientInfo (x)) + return c->fullInfo.info; + + return {}; + } + + std::unique_ptr makeSession (const String& n) override + { + return rawToUniquePtr (new SessionImplNative { *this, client, n }); + } + + ump::Backend getBackend() const override + { + return ump::Backend::alsa; + } + + bool isVirtualMidiBytestreamServiceActive() const override + { + return true; + } + + bool isVirtualMidiUmpServiceActive() const override + { + return snd_seq_create_ump_endpoint != nullptr; + } + + void setVirtualMidiBytestreamServiceActive (bool) override {} + void setVirtualMidiUmpServiceActive (bool) override {} + + private: + void virtualEndpointAdded (std::shared_ptr p) override + { + if (p != nullptr) + virtualPorts[p->getId()] = p; + } + + std::map> virtualPorts; + std::shared_ptr client; + AnnouncementsPort announcementsPort { client->getSequencer() }; + }; + + //============================================================================== + AlsaMidiHelpers() = delete; }; -//============================================================================== -class MidiInput::Pimpl final : public AlsaPortPtr +auto ump::Endpoints::Impl::Native::make (EndpointsListener& l) -> std::unique_ptr { -public: - using AlsaPortPtr::AlsaPortPtr; -}; + if (auto client = AlsaMidiHelpers::Client::make (&l)) + return std::make_unique (std::move (client)); -Array MidiInput::getAvailableDevices() -{ - Array devices; - iterateMidiDevices (true, devices, {}); - - return devices; + return {}; } -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)); - - port->setupInput (midiInput.get(), callback); - midiInput->internal = std::make_unique (port); - - 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()))); - - port->setupInput (midiInput.get(), callback); - midiInput->internal = std::make_unique (port); - - return midiInput; -} - -StringArray MidiInput::getDevices() -{ - StringArray deviceNames; - - for (auto& d : getAvailableDevices()) - deviceNames.add (d.name); - - deviceNames.appendNumbersToDuplicates (true, true); - - return deviceNames; -} - -int MidiInput::getDefaultDeviceIndex() -{ - return 0; -} - -std::unique_ptr MidiInput::openDevice (int index, MidiInputCallback* callback) -{ - return openDevice (getAvailableDevices()[index].identifier, callback); -} - -MidiInput::MidiInput (const String& deviceName, const String& deviceIdentifier) - : deviceInfo (deviceName, deviceIdentifier) -{ -} - -MidiInput::~MidiInput() -{ - 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)); - - port->setupOutput(); - midiOutput->internal = std::make_unique (port); - - return midiOutput; -} - -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()))); - - port->setupOutput(); - midiOutput->internal = std::make_unique (port); - - return midiOutput; -} - -StringArray MidiOutput::getDevices() -{ - StringArray deviceNames; - - for (auto& d : getAvailableDevices()) - deviceNames.add (d.name); - - deviceNames.appendNumbersToDuplicates (true, true); - - return deviceNames; -} - -int MidiOutput::getDefaultDeviceIndex() -{ - return 0; -} - -std::unique_ptr MidiOutput::openDevice (int index) -{ - return openDevice (getAvailableDevices()[index].identifier); -} - -MidiOutput::~MidiOutput() -{ - stopBackgroundThread(); -} - -void MidiOutput::sendMessageNow (const MidiMessage& message) -{ - internal->ptr->sendMessageNow (message); -} - -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) - : deviceInfo (deviceName, deviceID) +auto ump::Endpoints::Impl::Native::make (EndpointsListener& l) -> std::unique_ptr { -} - -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::createNewDevice (const String&, MidiInputCallback*) { return {}; } -StringArray MidiInput::getDevices() { return {}; } -int MidiInput::getDefaultDeviceIndex() { return 0;} -std::unique_ptr MidiInput::openDevice (int, MidiInputCallback*) { return {}; } - -class MidiOutput::Pimpl {}; - -MidiOutput::~MidiOutput() {} -void MidiOutput::sendMessageNow (const MidiMessage&) {} -Array MidiOutput::getAvailableDevices() { return {}; } -MidiDeviceInfo MidiOutput::getDefaultDevice() { return {}; } -std::unique_ptr MidiOutput::openDevice (const String&) { return {}; } -std::unique_ptr MidiOutput::createNewDevice (const String&) { return {}; } -StringArray MidiOutput::getDevices() { return {}; } -int MidiOutput::getDefaultDeviceIndex() { return 0;} -std::unique_ptr MidiOutput::openDevice (int) { return {}; } - -MidiDeviceListConnection MidiDeviceListConnection::make (std::function cb) -{ - auto& broadcaster = MidiDeviceListConnectionBroadcaster::get(); - return { &broadcaster, broadcaster.add (std::move (cb)) }; + return nullptr; } #endif diff --git a/modules/juce_audio_devices/native/juce_Midi_windows.cpp b/modules/juce_audio_devices/native/juce_Midi_windows.cpp index 241070341e..6685c1640b 100644 --- a/modules/juce_audio_devices/native/juce_Midi_windows.cpp +++ b/modules/juce_audio_devices/native/juce_Midi_windows.cpp @@ -41,275 +41,2449 @@ namespace juce { -template -class CheckedReference +#if JUCE_USE_WINDOWS_MIDI_SERVICES + +namespace wm2 = winrt::Microsoft::Windows::Devices::Midi2; +namespace wm2v = wm2::Endpoints::Virtual; +namespace mwdmi = Microsoft::Windows::Devices::Midi2::Initialization; + +class MidiServices { public: - template - friend auto createCheckedReference (Ptr*); - - void clear() + [[nodiscard]] static std::unique_ptr makeEndpoints (ump::EndpointsListener& l) { - std::lock_guard lock { mutex }; - ptr = nullptr; - } - - template - void access (Callback&& callback) - { - std::lock_guard lock { mutex }; - callback (ptr); + return EndpointsImplNative::make (l); } private: - explicit CheckedReference (T* ptrIn) : ptr (ptrIn) {} - - T* ptr; - std::mutex mutex; -}; - -template -auto createCheckedReference (Ptr* ptrIn) -{ - return std::shared_ptr> { new CheckedReference (ptrIn) }; -} - -class MidiInput::Pimpl -{ -public: - virtual ~Pimpl() noexcept = default; - - virtual String getDeviceIdentifier() = 0; - virtual String getDeviceName() = 0; - - virtual void start() = 0; - virtual void stop() = 0; -}; - -class MidiOutput::Pimpl -{ -public: - virtual ~Pimpl() noexcept = default; - - virtual String getDeviceIdentifier() = 0; - virtual String getDeviceName() = 0; - - virtual void sendMessageNow (const MidiMessage&) = 0; -}; - -struct MidiServiceType -{ - MidiServiceType() = default; - virtual ~MidiServiceType() noexcept = default; - - virtual Array getAvailableDevices (bool) = 0; - virtual MidiDeviceInfo getDefaultDevice (bool) = 0; - - virtual MidiInput::Pimpl* createInputWrapper (MidiInput&, const String&, MidiInputCallback&) = 0; - virtual MidiOutput::Pimpl* createOutputWrapper (const String&) = 0; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiServiceType) -}; - -//============================================================================== -struct Win32MidiService final : public MidiServiceType, - private Timer -{ - Win32MidiService() = default; - - Array getAvailableDevices (bool isInput) override + /* For both input and output. + It's most resource-efficient to have only one connection to each endpoint. + Therefore, we keep track of endpoints we've opened, and share the endpoints between + Input and Output instances when possible. + */ + class SharedConnection : private AsyncUpdater { - return isInput ? Win32InputWrapper::getAvailableDevices() - : Win32OutputWrapper::getAvailableDevices(); - } - - MidiDeviceInfo getDefaultDevice (bool isInput) override - { - return isInput ? Win32InputWrapper::getDefaultDevice() - : Win32OutputWrapper::getDefaultDevice(); - } - - MidiInput::Pimpl* createInputWrapper (MidiInput& input, const String& deviceIdentifier, MidiInputCallback& callback) override - { - return new Win32InputWrapper (*this, input, deviceIdentifier, callback); - } - - MidiOutput::Pimpl* createOutputWrapper (const String& deviceIdentifier) override - { - return new Win32OutputWrapper (*this, deviceIdentifier); - } - -private: - struct Win32InputWrapper; - - //============================================================================== - struct MidiInCollector final : public ReferenceCountedObject - { - MidiInCollector (Win32MidiService& s, MidiDeviceInfo d) - : deviceInfo (d), midiService (s) + public: + template + static std::unique_ptr make (const wm2::MidiSession& session, + const winrt::param::hstring& id, + Args&&... args) { - } + auto connection = session.CreateEndpointConnection (id); - ~MidiInCollector() - { - stop(); + if (connection == nullptr) + return {}; - if (deviceHandle != nullptr) + setUpConnection (connection, args...); + + auto result = rawToUniquePtr (new SharedConnection (session, std::move (connection))); + result->inputToken = result->connection.MessageReceived ([self = result.get()] (const auto&, const wm2::MidiMessageReceivedEventArgs& args) { - for (int count = 5; --count >= 0;) - { - if (midiInClose (deviceHandle) == MMSYSERR_NOERROR) - break; + std::array words{}; + args.FillWordArray (0, words); - Sleep (20); - } - } - } + const ump::Iterator begin { words.data(), words.size() }; + const auto end = std::next (begin); - using Ptr = ReferenceCountedObjectPtr; + const auto elapsedTime = args.Timestamp() - self->startTimeNative; + const auto juceTimeMillis = self->startTimeMillis + wm2::MidiClock::ConvertTimestampTicksToMilliseconds (elapsedTime); - void addClient (Win32InputWrapper* c) - { - const ScopedLock sl (clientLock); - jassert (! clients.contains (c)); - clients.add (c); - } + self->consumers.call ([&] (auto& c) { c.consume (begin, end, juceTimeMillis * 0.001); }); + }); - void removeClient (Win32InputWrapper* c) - { - const ScopedLock sl (clientLock); - clients.removeFirstMatchingValue (c); - startOrStop(); - midiService.asyncCheckForUnusedCollectors(); - } - - void handleMessage (const uint8* bytes, uint32 timeStamp) - { - if (bytes[0] >= 0x80 && isStarted.load()) + result->disconnectToken = result->connection.EndpointDeviceDisconnected ([self = result.get()] (auto&&...) { - { - auto len = MidiMessage::getMessageLengthFromFirstByte (bytes[0]); - auto time = convertTimeStamp (timeStamp); - const ScopedLock sl (clientLock); + self->triggerAsyncUpdate(); + }); - for (auto* c : clients) - c->pushMidiData (bytes, len, time); - } + if (! result->connection.Open()) + return {}; - writeFinishedBlocks(); - } + return result; } - void handleSysEx (MIDIHDR* hdr, uint32 timeStamp) + ~SharedConnection() { - if (isStarted.load() && hdr->dwBytesRecorded > 0) - { - { - auto time = convertTimeStamp (timeStamp); - const ScopedLock sl (clientLock); + connection.MessageReceived (inputToken); + connection.EndpointDeviceDisconnected (disconnectToken); - for (auto* c : clients) - c->pushMidiData (hdr->lpData, (int) hdr->dwBytesRecorded, time); - } + cancelPendingUpdate(); - writeFinishedBlocks(); - } + if (session != nullptr && connection != nullptr) + session.DisconnectEndpointConnection (connection.ConnectionId()); } - void startOrStop() + ump::EndpointId getEndpointId() const { - const ScopedLock sl (clientLock); - - if (countRunningClients() == 0) - stop(); - else - start(); + const auto id = toString (connection.ConnectedEndpointDeviceId()); + return ump::EndpointId::makeSrcDst (id, id); } - void start() + void addConsumer (ump::Consumer& c) { - if (deviceHandle != nullptr && ! isStarted.load()) - { - activeMidiCollectors.addIfNotAlreadyThere (this); - - for (int i = 0; i < (int) numHeaders; ++i) - { - headers[i].prepare (deviceHandle); - headers[i].write (deviceHandle); - } - - startTime = Time::getMillisecondCounterHiRes(); - auto res = midiInStart (deviceHandle); - - if (res == MMSYSERR_NOERROR) - isStarted = true; - else - unprepareAllHeaders(); - } + consumers.add (c); } - void stop() + void removeConsumer (ump::Consumer& c) { - if (isStarted.load()) - { - isStarted = false; - midiInReset (deviceHandle); - midiInStop (deviceHandle); - activeMidiCollectors.removeFirstMatchingValue (this); - unprepareAllHeaders(); - } + consumers.remove (c); } - static void CALLBACK midiInCallback (HMIDIIN, UINT uMsg, DWORD_PTR dwInstance, - DWORD_PTR midiMessage, DWORD_PTR timeStamp) + void addDisconnectListener (ump::DisconnectionListener& l) { - auto* collector = reinterpret_cast (dwInstance); - - // This is primarily a check for the collector being a dangling - // pointer, as the callback can sometimes be delayed - if (activeMidiCollectors.contains (collector)) - { - if (uMsg == MIM_DATA) - collector->handleMessage ((const uint8*) &midiMessage, (uint32) timeStamp); - else if (uMsg == MIM_LONGDATA) - collector->handleSysEx ((MIDIHDR*) midiMessage, (uint32) timeStamp); - } + disconnectListeners.add (&l); } - MidiDeviceInfo deviceInfo; - HMIDIIN deviceHandle = nullptr; + void removeDisconnectListener (ump::DisconnectionListener& l) + { + disconnectListeners.remove (&l); + } + + bool send (ump::Iterator b, ump::Iterator e) + { + const auto result = connection.SendMultipleMessagesWordArray (0, + 0, + (uint32_t) std::distance (b->data(), e->data()), + { b->data(), e->data() }); + return wm2::MidiSendMessageResults::Succeeded == result; + } private: - Win32MidiService& midiService; - CriticalSection clientLock; - Array clients; - std::atomic isStarted { false }; - double startTime = 0; + SharedConnection (wm2::MidiSession s, wm2::MidiEndpointConnection c) + : session (std::move (s)), connection (std::move (c)) {} - // This static array is used to prevent occasional callbacks to objects that are - // in the process of being deleted - static Array activeMidiCollectors; - - int countRunningClients() const + void handleAsyncUpdate() override { - int num = 0; - - for (auto* c : clients) - if (c->started) - ++num; - - return num; + disconnectListeners.call ([] (auto& c) { c.disconnected(); }); } - struct MidiHeader + static void setUpConnection (const wm2::MidiEndpointConnection&) { - MidiHeader() = default; + // Do nothing + } + static void setUpConnection (const wm2::MidiEndpointConnection& connection, + const wm2::IMidiEndpointMessageProcessingPlugin& plugin) + { + connection.AddMessageProcessingPlugin (plugin); + } + + const uint64_t startTimeNative = wm2::MidiClock::Now(); + const uint32_t startTimeMillis = Time::getMillisecondCounter(); + + wm2::MidiSession session; + wm2::MidiEndpointConnection connection; + WaitFreeListeners consumers; + ListenerList disconnectListeners; + winrt::event_token inputToken, disconnectToken; + }; + + class InputImplNative : public ump::Input::Impl::Native, + private ump::Consumer + { + public: + ~InputImplNative() override + { + + shared->removeDisconnectListener (listener); + shared->removeConsumer (*this); + } + + ump::EndpointId getEndpointId() const override + { + return shared->getEndpointId(); + } + + ump::PacketProtocol getProtocol() const override + { + return converter.getProtocol(); + } + + static std::unique_ptr make (std::shared_ptr x, + ump::DisconnectionListener& l, + ump::PacketProtocol protocol, + Consumer& consumer) + { + if (x == nullptr) + return {}; + + return rawToUniquePtr (new InputImplNative { x, l, protocol, consumer }); + } + + private: + InputImplNative (std::shared_ptr s, + ump::DisconnectionListener& l, + ump::PacketProtocol p, + Consumer& c) + : listener (l), + consumer (c), + shared (s), + converter (p) + { + shared->addConsumer (*this); + shared->addDisconnectListener (listener); + } + + void consume (ump::Iterator b, ump::Iterator e, double time) override + { + converter.convert (b, e, [&] (ump::View v) + { + const ump::Iterator b { v.data(), v.size() }; + consumer.consume (b, std::next (b), time); + }); + } + + ump::DisconnectionListener& listener; + Consumer& consumer; + std::shared_ptr shared; + ump::GenericUMPConverter converter; + }; + + class OutputImplNative : public ump::Output::Impl::Native + { + public: + ~OutputImplNative() override + { + shared->removeDisconnectListener (listener); + } + + ump::EndpointId getEndpointId() const override + { + return shared->getEndpointId(); + } + + bool send (ump::Iterator b, ump::Iterator e) override + { + return shared->send (b, e); + } + + static std::unique_ptr make (std::shared_ptr c, + ump::DisconnectionListener& l) + { + if (c == nullptr) + return {}; + + return rawToUniquePtr (new OutputImplNative { std::move (c), l }); + } + + private: + OutputImplNative (std::shared_ptr c, ump::DisconnectionListener& l) + : listener (l), shared (std::move (c)) + { + shared->addDisconnectListener (listener); + } + + ump::DisconnectionListener& listener; + std::shared_ptr shared; + }; + + class VirtualEndpoint + { + public: + ump::EndpointId getId() const + { + const auto id = toString (device.DeviceEndpointDeviceId()); + return ump::EndpointId::makeSrcDst (id, id); + } + + ump::Endpoint getEndpoint() const + { + return endpoint; + } + + ump::StaticDeviceInfo getStaticDeviceInfo() const + { + return staticInfo; + } + + bool setBlock (uint8_t i, const ump::Block& b) + { + if (! device.UpdateFunctionBlock (makeBlock (i, b))) + return false; + + endpoint.getBlocks()[i] = b; + return true; + } + + bool setName (const String& x) + { + if (! device.UpdateEndpointName (x.toWideCharPointer())) + return false; + + endpoint = endpoint.withName (x); + return true; + } + + static std::unique_ptr make (std::shared_ptr c, + wm2v::MidiVirtualDevice d, + ump::Endpoint ep, + ump::StaticDeviceInfo si) + { + if (d == nullptr) + return {}; + + return rawToUniquePtr (new VirtualEndpoint { std::move (c), + std::move (d), + std::move (ep), + std::move (si) }); + } + + private: + VirtualEndpoint (std::shared_ptr s, + wm2v::MidiVirtualDevice d, + ump::Endpoint ep, + ump::StaticDeviceInfo si) + : shared (std::move (s)), + device (std::move (d)), + endpoint (ep), + staticInfo (std::move (si)) + { + } + + std::shared_ptr shared; + wm2v::MidiVirtualDevice device; + ump::Endpoint endpoint; + ump::StaticDeviceInfo staticInfo; + }; + + struct VirtualEndpointRegistry + { + virtual ~VirtualEndpointRegistry() = default; + virtual void virtualEndpointAdded (std::shared_ptr) = 0; + }; + + class VirtualEndpointImplNative : public ump::VirtualEndpoint::Impl::Native, + public ump::LegacyVirtualInput::Impl::Native, + public ump::LegacyVirtualOutput::Impl::Native + { + public: + ump::EndpointId getId() const override + { + return endpoint->getId(); + } + + bool setBlock (uint8_t i, const ump::Block& b) override + { + return endpoint->setBlock (i, b); + } + + bool setName (const String& x) override + { + return endpoint->setName (x); + } + + static std::unique_ptr make (std::shared_ptr ep) + { + if (ep == nullptr) + return {}; + + return rawToUniquePtr (new VirtualEndpointImplNative { std::move (ep) }); + } + + private: + explicit VirtualEndpointImplNative (std::shared_ptr ep) + : endpoint (std::move (ep)) {} + + std::shared_ptr endpoint; + }; + + class SessionImplNative : public ump::Session::Impl::Native + { + public: + ~SessionImplNative() override + { + session.Close(); + } + + String getName() const override + { + return toString (session.Name()); + } + + std::unique_ptr connectInput (ump::DisconnectionListener& listener, + const ump::EndpointId& id, + ump::PacketProtocol p, + ump::Consumer& consumer) override + { + const auto strong = findOrOpenConnection (id.src.toWideCharPointer()); + return InputImplNative::make (strong, listener, p, consumer); + } + + std::unique_ptr connectOutput (ump::DisconnectionListener& listener, + const ump::EndpointId& id) override + { + const auto strong = findOrOpenConnection (id.dst.toWideCharPointer()); + return OutputImplNative::make (strong, listener); + } + + std::unique_ptr createNativeVirtualEndpoint (const String& name, + const ump::DeviceInfo& info, + const String& productInstance, + ump::PacketProtocol protocol, + Span blocks, + ump::BlocksAreStatic areStatic) override + { + return createNativeVirtualEndpointImpl (name, info, productInstance, protocol, blocks, areStatic); + } + + std::unique_ptr createLegacyVirtualInput (const String& name) override + { + const ump::Block blocks[] { ump::IOHelpers::makeLegacyBlock (true) }; + return createNativeVirtualEndpointImpl (name, {}, {}, ump::PacketProtocol::MIDI_1_0, blocks, ump::BlocksAreStatic::yes); + } + + std::unique_ptr createLegacyVirtualOutput (const String& name) override + { + const ump::Block blocks[] { ump::IOHelpers::makeLegacyBlock (false) }; + return createNativeVirtualEndpointImpl (name, {}, {}, ump::PacketProtocol::MIDI_1_0, blocks, ump::BlocksAreStatic::yes); + } + + static std::unique_ptr make (VirtualEndpointRegistry& r, const String& name) + { + if (auto s = wm2::MidiSession::Create (name.toWideCharPointer())) + return rawToUniquePtr (new SessionImplNative (r, s)); + + return {}; + } + + private: + SessionImplNative (VirtualEndpointRegistry& r, const wm2::MidiSession& s) + : registry (r), session (s) {} + + std::unique_ptr createNativeVirtualEndpointImpl (const String& name, + const ump::DeviceInfo& info, + const String& productInstance, + ump::PacketProtocol protocol, + Span blocks, + ump::BlocksAreStatic areStatic) + { + wm2::MidiDeclaredEndpointInfo e; + e.Name = name.toWideCharPointer(); + e.HasStaticFunctionBlocks = areStatic == ump::BlocksAreStatic::yes; + e.DeclaredFunctionBlockCount = (uint8_t) blocks.size(); + e.ProductInstanceId = productInstance.toWideCharPointer(); + e.SupportsMidi10Protocol = protocol == ump::PacketProtocol::MIDI_1_0; + e.SupportsMidi20Protocol = protocol == ump::PacketProtocol::MIDI_2_0; + e.SpecificationVersionMajor = 1; + e.SpecificationVersionMinor = 1; + e.SupportsReceivingJitterReductionTimestamps = false; + e.SupportsSendingJitterReductionTimestamps = false; + + wm2v::MidiVirtualDeviceCreationConfig config { e.Name, + ump::Endpoints::Impl::getGlobalMidiClientName().toWideCharPointer(), + L"", + e, + makeDeviceInfo (info) }; + + for (const auto [index, value] : enumerate (blocks, uint8_t{})) + config.FunctionBlocks().Append (makeBlock (index, value)); + + auto device = wm2v::MidiVirtualDeviceManager::CreateVirtualDevice (config); + + if (device == nullptr) + return {}; + + // In order to function, the device needs a client plugin installed, which in turn + // requires opening a connection to the endpoint. + auto connection = findOrOpenConnection (device.DeviceEndpointDeviceId(), device); + + if (connection == nullptr) + return {}; + + auto endpoint = ump::Endpoint{}.withName (name) + .withDeviceInfo (info) + .withProductInstanceId (productInstance) + .withProtocol (protocol) + .withMidi1Support (protocol == ump::PacketProtocol::MIDI_1_0) + .withMidi2Support (protocol == ump::PacketProtocol::MIDI_2_0) + .withStaticBlocks (areStatic == ump::BlocksAreStatic::yes) + .withBlocks (blocks); + + auto staticInfo = ump::StaticDeviceInfo{}.withName (name) + .withManufacturer ("") + .withProduct ("") + .withTransport (ump::Transport::ump) + .withHasSource (true) + .withHasDestination (true); + + std::shared_ptr virtualEndpoint = VirtualEndpoint::make (std::move (connection), + std::move (device), + endpoint, + staticInfo); + + if (virtualEndpoint == nullptr) + return {}; + + registry.virtualEndpointAdded (virtualEndpoint); + + return VirtualEndpointImplNative::make (std::move (virtualEndpoint)); + } + + template + std::shared_ptr findOrOpenConnection (const winrt::hstring& id, + Args&&... args) + { + auto& weak = weakConnections[toString (id)]; + + if (const auto strong = weak.lock()) + return strong; + + const std::shared_ptr strong = SharedConnection::make (session, id, args...); + weak = strong; + return strong; + } + + VirtualEndpointRegistry& registry; + std::map> weakConnections; + wm2::MidiSession session; + }; + + static wm2::MidiFunctionBlock makeBlock (uint8_t index, const ump::Block& b) + { + const auto direction = std::invoke ([&] + { + switch (b.getDirection()) + { + case ump::BlockDirection::bidirectional: return wm2::MidiFunctionBlockDirection::Bidirectional; + case ump::BlockDirection::sender: return wm2::MidiFunctionBlockDirection::BlockOutput; + case ump::BlockDirection::receiver: return wm2::MidiFunctionBlockDirection::BlockInput; + case ump::BlockDirection::unknown: return wm2::MidiFunctionBlockDirection::Undefined; + } + + return wm2::MidiFunctionBlockDirection{}; + }); + + const auto hint = std::invoke ([&] + { + switch (b.getUiHint()) + { + case ump::BlockUiHint::bidirectional: return wm2::MidiFunctionBlockUIHint::Bidirectional; + case ump::BlockUiHint::sender: return wm2::MidiFunctionBlockUIHint::Sender; + case ump::BlockUiHint::receiver: return wm2::MidiFunctionBlockUIHint::Receiver; + case ump::BlockUiHint::unknown: return wm2::MidiFunctionBlockUIHint::Unknown; + } + + return wm2::MidiFunctionBlockUIHint{}; + }); + + const auto proxy = std::invoke ([&] + { + switch (b.getMIDI1ProxyKind()) + { + case ump::BlockMIDI1ProxyKind::inapplicable: return wm2::MidiFunctionBlockRepresentsMidi10Connection::Not10; + case ump::BlockMIDI1ProxyKind::restrictedBandwidth: return wm2::MidiFunctionBlockRepresentsMidi10Connection::YesBandwidthRestricted; + case ump::BlockMIDI1ProxyKind::unrestrictedBandwidth: return wm2::MidiFunctionBlockRepresentsMidi10Connection::YesBandwidthUnrestricted; + } + + return wm2::MidiFunctionBlockRepresentsMidi10Connection{}; + }); + + wm2::MidiFunctionBlock result; + result.Name (b.getName().toWideCharPointer()); + result.Number (index); + result.IsActive (b.isEnabled()); + result.FirstGroup (wm2::MidiGroup { b.getFirstGroup() }); + result.GroupCount (b.getNumGroups()); + result.MaxSystemExclusive8Streams (b.getMaxSysex8Streams()); + result.Direction (direction); + result.UIHint (hint); + result.RepresentsMidi10Connection (proxy); + + return result; + } + + struct IndexedBlock + { + uint8_t index; + ump::Block block; + }; + + static IndexedBlock makeBlock (const wm2::MidiFunctionBlock& b) + { + const auto index = b.Number(); + + const auto direction = std::invoke ([&] + { + switch (b.Direction()) + { + case wm2::MidiFunctionBlockDirection::Bidirectional: return ump::BlockDirection::bidirectional; + case wm2::MidiFunctionBlockDirection::BlockOutput: return ump::BlockDirection::sender; + case wm2::MidiFunctionBlockDirection::BlockInput: return ump::BlockDirection::receiver; + case wm2::MidiFunctionBlockDirection::Undefined: return ump::BlockDirection::unknown; + } + + return ump::BlockDirection{}; + }); + + const auto hint = std::invoke ([&] + { + switch (b.UIHint()) + { + case wm2::MidiFunctionBlockUIHint::Bidirectional: return ump::BlockUiHint::bidirectional; + case wm2::MidiFunctionBlockUIHint::Sender: return ump::BlockUiHint::sender; + case wm2::MidiFunctionBlockUIHint::Receiver: return ump::BlockUiHint::receiver; + case wm2::MidiFunctionBlockUIHint::Unknown: return ump::BlockUiHint::unknown; + } + + return ump::BlockUiHint{}; + }); + + const auto proxy = std::invoke ([&] + { + switch (b.RepresentsMidi10Connection()) + { + case wm2::MidiFunctionBlockRepresentsMidi10Connection::Not10: return ump::BlockMIDI1ProxyKind::inapplicable; + case wm2::MidiFunctionBlockRepresentsMidi10Connection::YesBandwidthRestricted: return ump::BlockMIDI1ProxyKind::restrictedBandwidth; + case wm2::MidiFunctionBlockRepresentsMidi10Connection::YesBandwidthUnrestricted: return ump::BlockMIDI1ProxyKind::unrestrictedBandwidth; + case wm2::MidiFunctionBlockRepresentsMidi10Connection::Reserved: break; + } + + return ump::BlockMIDI1ProxyKind{}; + }); + + const auto block = ump::Block{}.withDirection (direction) + .withUiHint (hint) + .withMIDI1ProxyKind (proxy) + .withFirstGroup (b.FirstGroup().Index()) + .withNumGroups (b.GroupCount()) + .withEnabled (b.IsActive()) + .withName (toString (b.Name())) + .withMaxSysex8Streams (b.MaxSystemExclusive8Streams()); + + return { index, block }; + } + + static wm2::MidiDeclaredDeviceIdentity makeDeviceInfo (ump::DeviceInfo x) + { + wm2::MidiDeclaredDeviceIdentity result{}; + + result.SystemExclusiveIdByte1 = (uint8_t) x.manufacturer[0]; + result.SystemExclusiveIdByte2 = (uint8_t) x.manufacturer[1]; + result.SystemExclusiveIdByte3 = (uint8_t) x.manufacturer[2]; + + result.DeviceFamilyLsb = (uint8_t) x.family[0]; + result.DeviceFamilyMsb = (uint8_t) x.family[1]; + + result.DeviceFamilyModelNumberLsb = (uint8_t) x.modelNumber[0]; + result.DeviceFamilyModelNumberMsb = (uint8_t) x.modelNumber[1]; + + result.SoftwareRevisionLevelByte1 = (uint8_t) x.revision[0]; + result.SoftwareRevisionLevelByte2 = (uint8_t) x.revision[1]; + result.SoftwareRevisionLevelByte3 = (uint8_t) x.revision[2]; + result.SoftwareRevisionLevelByte4 = (uint8_t) x.revision[3]; + + return result; + } + + static ump::DeviceInfo makeDeviceInfo (const wm2::MidiDeclaredDeviceIdentity& x) + { + return ump::DeviceInfo + { + { std::byte (x.SystemExclusiveIdByte1), + std::byte (x.SystemExclusiveIdByte2), + std::byte (x.SystemExclusiveIdByte3) }, + + { std::byte (x.DeviceFamilyLsb), + std::byte (x.DeviceFamilyMsb) }, + + { std::byte (x.DeviceFamilyModelNumberLsb), + std::byte (x.DeviceFamilyModelNumberMsb) }, + + { std::byte (x.SoftwareRevisionLevelByte1), + std::byte (x.SoftwareRevisionLevelByte2), + std::byte (x.SoftwareRevisionLevelByte3), + std::byte (x.SoftwareRevisionLevelByte4) }, + }; + } + + class EndpointsImplNative : public ump::Endpoints::Impl::Native, + private VirtualEndpointRegistry, + private AsyncUpdater + { + public: + ~EndpointsImplNative() override + { + watcher.Stop(); + cancelPendingUpdate(); + } + + ump::Backend getBackend() const + { + return ump::Backend::wms; + } + + bool isVirtualMidiUmpServiceActive() const override + { + return wm2v::MidiVirtualDeviceManager::IsTransportAvailable(); + } + + bool isVirtualMidiBytestreamServiceActive() const override + { + return wm2v::MidiVirtualDeviceManager::IsTransportAvailable(); + } + + void getEndpoints (std::vector& buffer) const override + { + std::transform (cachedEndpoints.begin(), + cachedEndpoints.end(), + std::back_inserter (buffer), + [] (auto& pair) { return pair.first; }); + } + + std::optional getEndpoint (const ump::EndpointId& id) const override + { + if (const auto iter = virtualEndpoints.find (id); iter != virtualEndpoints.end()) + if (auto strong = iter->second.lock()) + return strong->getEndpoint(); + + if (const auto iter = cachedEndpoints.find (id); iter != cachedEndpoints.end()) + return iter->second.endpoint; + + return {}; + } + + std::optional getStaticDeviceInfo (const ump::EndpointId& id) const override + { + if (const auto iter = virtualEndpoints.find (id); iter != virtualEndpoints.end()) + if (auto strong = iter->second.lock()) + return strong->getStaticDeviceInfo(); + + if (const auto iter = cachedEndpoints.find (id); iter != cachedEndpoints.end()) + return iter->second.info; + + return {}; + } + + std::unique_ptr makeSession (const String& name) override + { + return SessionImplNative::make (*this, name); + } + + static std::unique_ptr make (ump::EndpointsListener& l) + { + SharedResourcePointer initialiser; + + if (! initialiser->isValid()) + { + // If you hit this, you've tried to initialise Windows MIDI Services but the + // initialisation failed. Did you forget to install the Windows MIDI Services SDK? + jassertfalse; + return {}; + } + + auto watcher = wm2::MidiEndpointDeviceWatcher::Create(); + + if (! watcher) + return {}; + + return rawToUniquePtr (new EndpointsImplNative { watcher, l }); + } + + private: + EndpointsImplNative (wm2::MidiEndpointDeviceWatcher w, ump::EndpointsListener& l) + : listener (l), watcher (w) + { + watcher.Added ([this] (auto&, const wm2::MidiEndpointDeviceInformationAddedEventArgs& args) + { + const auto device = args.AddedDevice(); + const auto id = toString (device.EndpointDeviceId()); + const auto endpoint = makeEndpoint (device); + + const std::scoped_lock lock { mutex }; + pendingWork.push_back ([this, id, endpoint] + { + cachedEndpoints.insert_or_assign (ump::EndpointId::makeSrcDst (id, id), endpoint); + }); + + triggerAsyncUpdate(); + }); + + watcher.Updated ([this] (auto&, const wm2::MidiEndpointDeviceInformationUpdatedEventArgs& args) + { + const auto id = toString (args.EndpointDeviceId()); + + if (const auto info = wm2::MidiEndpointDeviceInformation::CreateFromEndpointDeviceId (args.EndpointDeviceId())) + { + const auto endpoint = makeEndpoint (info); + + const std::scoped_lock lock { mutex }; + pendingWork.push_back ([this, id, endpoint] + { + cachedEndpoints.insert_or_assign (ump::EndpointId::makeSrcDst (id, id), endpoint); + }); + + triggerAsyncUpdate(); + } + }); + + watcher.Removed ([this] (auto&, const wm2::MidiEndpointDeviceInformationRemovedEventArgs& args) + { + const auto id = toString (args.EndpointDeviceId()); + + const std::scoped_lock lock { mutex }; + pendingWork.push_back ([this, id] + { + cachedEndpoints.erase (ump::EndpointId::makeSrcDst (id, id)); + }); + + triggerAsyncUpdate(); + }); + + watcher.EnumerationCompleted ([this] (auto&, auto&) + { + const std::scoped_lock lock { mutex }; + pendingWork.push_back ([this] + { + const auto devices = watcher.EnumeratedEndpointDevices(); + + for (const auto& [id, device] : devices) + { + auto endpoint = makeEndpoint (device); + cachedEndpoints.insert_or_assign (endpoint.id, endpoint); + } + }); + + triggerAsyncUpdate(); + }); + + watcher.Start(); + } + + void virtualEndpointAdded (std::shared_ptr ep) override + { + if (ep != nullptr) + virtualEndpoints[ep->getId()] = ep; + } + + std::function popWork() + { + const std::scoped_lock lock { mutex }; + + if (pendingWork.empty()) + return {}; + + auto result = pendingWork.front(); + pendingWork.pop_front(); + return result; + } + + void handleAsyncUpdate() override + { + while (auto fn = popWork()) + fn(); + + listener.endpointsChanged(); + } + + static ump::EndpointAndStaticInfo makeEndpoint (const wm2::MidiEndpointDeviceInformation& info) + { + const auto transport = std::invoke ([&] + { + const auto t = info.GetTransportSuppliedInfo().NativeDataFormat; + + if (t == wm2::MidiEndpointNativeDataFormat::Midi1ByteFormat) + return ump::Transport::bytestream; + + return ump::Transport::ump; + }); + + const auto itemProtocol = std::invoke ([&] + { + const auto p = info.GetDeclaredStreamConfiguration().Protocol; + + if (p == wm2::MidiProtocol::Midi1 || transport == ump::Transport::bytestream) + return ump::PacketProtocol::MIDI_1_0; + + return ump::PacketProtocol::MIDI_2_0; + }); + + const auto deviceInfo = makeDeviceInfo (info.GetDeclaredDeviceIdentity()); + + const auto id = toString (info.EndpointDeviceId()); + + std::array legacyIds; + + for (auto [index, value] : enumerate (legacyIds)) + { + auto obj = new DynamicObject; + obj->setProperty ("endpoint", id); + obj->setProperty ("group", index); + value = JSON::toString (obj, true); + } + + std::vector blocks; + + for (const auto& fb : info.GetDeclaredFunctionBlocks()) + blocks.push_back (makeBlock (fb).block); + + if (blocks.empty()) + { + for (const auto& gtb : info.GetGroupTerminalBlocks()) + blocks.push_back (makeBlock (gtb.AsEquivalentFunctionBlock()).block); + } + + const auto e = info.GetDeclaredEndpointInfo(); + const auto manufacturer = info.GetContainerDeviceInformation() + .Properties() + .TryLookup (L"System.Devices.Manufacturer"); + const auto product = info.GetContainerDeviceInformation() + .Properties() + .TryLookup (L"System.Devices.ModelName"); + + const auto endpoint = ump::Endpoint{}.withName (toString (info.Name())) + .withProtocol (itemProtocol) + .withBlocks (blocks) + .withDeviceInfo (deviceInfo) + .withProductInstanceId (toString (info.GetDeclaredEndpointInfo().ProductInstanceId)) + .withUMPVersion (e.SpecificationVersionMajor, e.SpecificationVersionMinor) + .withMidi1Support (e.SupportsMidi10Protocol) + .withMidi2Support (e.SupportsMidi20Protocol) + .withStaticBlocks (e.HasStaticFunctionBlocks) + .withReceiveJRSupport (e.SupportsReceivingJitterReductionTimestamps) + .withTransmitJRSupport (e.SupportsSendingJitterReductionTimestamps); + + const auto hasBlockDirection = [&] (auto direction) + { + const auto blockCanUseDirection = [&] (const wm2::MidiFunctionBlock& x) + { + const auto d = x.Direction(); + return d == wm2::MidiFunctionBlockDirection::Bidirectional || d == direction; + }; + + const auto fb = info.GetDeclaredFunctionBlocks(); + const auto gt = info.GetGroupTerminalBlocks(); + + return std::any_of (fb.begin(), fb.end(), blockCanUseDirection) + || std::any_of (gt.begin(), gt.end(), [&] (const wm2::MidiGroupTerminalBlock& x) + { + return blockCanUseDirection (x.AsEquivalentFunctionBlock()); + }); + }; + + const auto staticInfo = ump::StaticDeviceInfo{}.withName (toString (info.Name())) + .withManufacturer (toString (winrt::unbox_value_or (manufacturer, L""))) + .withProduct (toString (winrt::unbox_value_or (product, L""))) + .withHasSource (hasBlockDirection (wm2::MidiFunctionBlockDirection::BlockOutput)) + .withHasDestination (hasBlockDirection (wm2::MidiFunctionBlockDirection::BlockInput)) + .withLegacyIdentifiersSrc (legacyIds) + .withLegacyIdentifiersDst (legacyIds) + .withTransport (transport); + + return { endpoint, staticInfo, ump::EndpointId::makeSrcDst (id, id) }; + } + + class SdkInitialiser + { + public: + SdkInitialiser() = default; + + bool isValid() const { return ptr != nullptr; } + + private: + ComSmartPtr ptr = std::invoke ([]() -> ComSmartPtr + { + try + { + winrt::init_apartment (winrt::apartment_type::single_threaded); + } + catch (...) + { + // We tried... + } + + ComSmartPtr result; + + if (FAILED (CoCreateInstance (__uuidof (mwdmi::MidiClientInitializerUuid), + nullptr, + CLSCTX::CLSCTX_INPROC_SERVER | CLSCTX::CLSCTX_FROM_DEFAULT_CONTEXT, + __uuidof (mwdmi::IMidiClientInitializer), + (void**) result.resetAndGetPointerAddress()))) + return {}; + + if (result == nullptr) + return {}; + + if (FAILED (result->EnsureServiceAvailable())) + return {}; + + return result; + }); + }; + + SharedResourcePointer initialiser; + + std::mutex mutex; + std::deque> pendingWork; + + ump::EndpointsListener& listener; + std::map cachedEndpoints; + std::map> virtualEndpoints; + wm2::MidiEndpointDeviceWatcher watcher; + }; + + static String toString (const winrt::hstring& str) + { + return CharPointer_UTF16 { str.data() }; + } + + MidiServices() = delete; +}; + +#endif + +RTL_OSVERSIONINFOW getWindowsVersionInfo(); + +struct WindowsMidiHelpers +{ + #if JUCE_USE_WINRT_MIDI + #ifndef JUCE_FORCE_WINRT_MIDI + #define JUCE_FORCE_WINRT_MIDI 0 + #endif + + #ifndef JUCE_WINRT_MIDI_LOGGING + #define JUCE_WINRT_MIDI_LOGGING 0 + #endif + + #if JUCE_WINRT_MIDI_LOGGING + #define JUCE_WINRT_MIDI_LOG(x) DBG(x) + #else + #define JUCE_WINRT_MIDI_LOG(x) + #endif + + struct Winrt + { + using DeviceInformation = ABI::Windows::Devices::Enumeration::DeviceInformation; + using DeviceInformationKind = ABI::Windows::Devices::Enumeration::DeviceInformationKind; + using DeviceInformationUpdate = ABI::Windows::Devices::Enumeration::DeviceInformationUpdate; + using DeviceWatcher = ABI::Windows::Devices::Enumeration::DeviceWatcher; + using IBuffer = ABI::Windows::Storage::Streams::IBuffer; + using IBufferFactory = ABI::Windows::Storage::Streams::IBufferFactory; + using IBufferByteAccess = Windows::Storage::Streams::IBufferByteAccess; + using IDeviceInformation = ABI::Windows::Devices::Enumeration::IDeviceInformation; + using IDeviceInformationStatics2 = ABI::Windows::Devices::Enumeration::IDeviceInformationStatics2; + using IDeviceInformationUpdate = ABI::Windows::Devices::Enumeration::IDeviceInformationUpdate; + using IDevicePicker = ABI::Windows::Devices::Enumeration::IDevicePicker; + using IDeviceWatcher = ABI::Windows::Devices::Enumeration::IDeviceWatcher; + using IMidiInPort = ABI::Windows::Devices::Midi::IMidiInPort; + using IMidiInPortStatics = ABI::Windows::Devices::Midi::IMidiInPortStatics; + using IMidiMessage = ABI::Windows::Devices::Midi::IMidiMessage; + using IMidiMessageReceivedEventArgs = ABI::Windows::Devices::Midi::IMidiMessageReceivedEventArgs; + using IMidiOutPort = ABI::Windows::Devices::Midi::IMidiOutPort; + using IMidiOutPortStatics = ABI::Windows::Devices::Midi::IMidiOutPortStatics; + using MidiInPort = ABI::Windows::Devices::Midi::MidiInPort; + using MidiMessageReceivedEventArgs = ABI::Windows::Devices::Midi::MidiMessageReceivedEventArgs; + + template using ITypedEventHandler = ABI::Windows::Foundation::ITypedEventHandler; + template using IAsyncOperation = ABI::Windows::Foundation::IAsyncOperation; + template using IAsyncOperationCompletedHandler = ABI::Windows::Foundation::IAsyncOperationCompletedHandler; + template using IIterable = ABI::Windows::Foundation::Collections::IIterable; + template using IReference = ABI::Windows::Foundation::IReference; + template using IVector = ABI::Windows::Foundation::Collections::IVector; + + class DeviceCallbackHandler + { + public: + struct Delegate + { + virtual ~Delegate() = default; + virtual HRESULT addDevice (IDeviceInformation*) = 0; + virtual HRESULT removeDevice (IDeviceInformationUpdate*) = 0; + virtual HRESULT updateDevice (IDeviceInformationUpdate*) = 0; + }; + + static std::unique_ptr make (Delegate* delegate, + HSTRING deviceSelector, + DeviceInformationKind infoKind) + { + auto* wrtWrapper = WinRTWrapper::getInstanceWithoutCreating(); + + if (wrtWrapper == nullptr) + { + JUCE_WINRT_MIDI_LOG ("Failed to get the WinRTWrapper singleton!"); + return {}; + } + + auto deviceInfoFactory = wrtWrapper->getWRLFactory (&RuntimeClass_Windows_Devices_Enumeration_DeviceInformation[0]); + + if (deviceInfoFactory == nullptr) + return {}; + + // A quick way of getting an IVector... + auto requestedProperties = [wrtWrapper] + { + auto devicePicker = wrtWrapper->activateInstance (&RuntimeClass_Windows_Devices_Enumeration_DevicePicker[0], + __uuidof (IDevicePicker)); + jassert (devicePicker != nullptr); + + IVector* result; + auto hr = devicePicker->get_RequestedProperties (&result); + jassert (SUCCEEDED (hr)); + + hr = result->Clear(); + jassert (SUCCEEDED (hr)); + + return result; + }(); + + StringArray propertyKeys ("System.Devices.ContainerId", + "System.Devices.Aep.ContainerId", + "System.Devices.Aep.IsConnected"); + + for (auto& key : propertyKeys) + { + WinRTWrapper::ScopedHString hstr (key); + auto hr = requestedProperties->Append (hstr.get()); + + if (FAILED (hr)) + { + jassertfalse; + return {}; + } + } + + ComSmartPtr> iter; + auto hr = requestedProperties->QueryInterface (__uuidof (IIterable), (void**) iter.resetAndGetPointerAddress()); + + if (FAILED (hr)) + { + jassertfalse; + return {}; + } + + auto result = rawToUniquePtr (new DeviceCallbackHandler); + result->delegate = delegate; + + hr = deviceInfoFactory->CreateWatcherWithKindAqsFilterAndAdditionalProperties (deviceSelector, iter, infoKind, + result->watcher.resetAndGetPointerAddress()); + + if (FAILED (hr)) + { + jassertfalse; + return {}; + } + + result->thread = std::thread { [r = result.get()] + { + Thread::setCurrentThreadName (SystemStats::getJUCEVersion() + ": WinRT Device Enumeration Thread"); + + r->watcher->add_Added ( + Microsoft::WRL::Callback> ( + [r] (IDeviceWatcher*, IDeviceInformation* info) { return r->delegate->addDevice (info); } + ).Get(), + &r->deviceAddedToken); + + r->watcher->add_Removed ( + Microsoft::WRL::Callback> ( + [r] (IDeviceWatcher*, IDeviceInformationUpdate* infoUpdate) { return r->delegate->removeDevice (infoUpdate); } + ).Get(), + &r->deviceRemovedToken); + + r->watcher->add_Updated ( + Microsoft::WRL::Callback> ( + [r] (IDeviceWatcher*, IDeviceInformationUpdate* infoUpdate) { return r->delegate->updateDevice (infoUpdate); } + ).Get(), + &r->deviceUpdatedToken); + + r->watcher->Start(); + } }; + + return result; + } + + ~DeviceCallbackHandler() + { + if (thread.joinable()) + thread.join(); + + if (watcher == nullptr) + return; + + auto hr = watcher->Stop(); + jassert (SUCCEEDED (hr)); + + if (deviceAddedToken.value != 0) + { + hr = watcher->remove_Added (deviceAddedToken); + jassert (SUCCEEDED (hr)); + deviceAddedToken.value = 0; + } + + if (deviceUpdatedToken.value != 0) + { + hr = watcher->remove_Updated (deviceUpdatedToken); + jassert (SUCCEEDED (hr)); + deviceUpdatedToken.value = 0; + } + + if (deviceRemovedToken.value != 0) + { + hr = watcher->remove_Removed (deviceRemovedToken); + jassert (SUCCEEDED (hr)); + deviceRemovedToken.value = 0; + } + + watcher = nullptr; + } + + template + IInspectable* getValueFromDeviceInfo (String key, InfoType* info) + { + __FIMapView_2_HSTRING_IInspectable* properties; + info->get_Properties (&properties); + + boolean found = false; + WinRTWrapper::ScopedHString keyHstr (key); + auto hr = properties->HasKey (keyHstr.get(), &found); + + if (FAILED (hr)) + { + jassertfalse; + return nullptr; + } + + if (! found) + return nullptr; + + IInspectable* inspectable; + hr = properties->Lookup (keyHstr.get(), &inspectable); + + if (FAILED (hr)) + { + jassertfalse; + return nullptr; + } + + return inspectable; + } + + String getGUIDFromInspectable (IInspectable& inspectable) + { + ComSmartPtr> guidRef; + auto hr = inspectable.QueryInterface (__uuidof (IReference), + (void**) guidRef.resetAndGetPointerAddress()); + + if (FAILED (hr)) + { + jassertfalse; + return {}; + } + + GUID result; + hr = guidRef->get_Value (&result); + + if (FAILED (hr)) + { + jassertfalse; + return {}; + } + + OLECHAR* resultString; + StringFromCLSID (result, &resultString); + + return resultString; + } + + bool getBoolFromInspectable (IInspectable& inspectable) + { + ComSmartPtr> boolRef; + auto hr = inspectable.QueryInterface (__uuidof (IReference), + (void**) boolRef.resetAndGetPointerAddress()); + + if (FAILED (hr)) + { + jassertfalse; + return false; + } + + boolean result; + hr = boolRef->get_Value (&result); + + if (FAILED (hr)) + { + jassertfalse; + return false; + } + + return result; + } + + private: + DeviceCallbackHandler() = default; + + Delegate* delegate = nullptr; + ComSmartPtr watcher; + EventRegistrationToken deviceAddedToken { 0 }, deviceRemovedToken { 0 }, deviceUpdatedToken { 0 }; + std::thread thread; + }; + + struct BLEDeviceWatcherListener + { + virtual ~BLEDeviceWatcherListener() = default; + virtual void bleDeviceAdded (const String& containerID) = 0; + virtual void bleDeviceDisconnected (const String& containerID) = 0; + }; + + class BLEDeviceWatcher : private DeviceCallbackHandler::Delegate + { + public: + static std::unique_ptr make() + { + WinRTWrapper::ScopedHString deviceSelector ("System.Devices.Aep.ProtocolId:=\"{bb7bb05e-5972-42b5-94fc-76eaa7084d49}\"" + " AND System.Devices.Aep.IsPaired:=System.StructuredQueryType.Boolean#True"); + + auto result = rawToUniquePtr (new BLEDeviceWatcher); + + if (auto watcher = DeviceCallbackHandler::make (result.get(), deviceSelector.get(), DeviceInformationKind::DeviceInformationKind_AssociationEndpoint)) + { + result->watcher = std::move (watcher); + return result; + } + + return {}; + } + + bool isBleDevice (const String& containerId) const + { + const ScopedLock lock (deviceChanges); + return bleContainerIds.find (containerId) != bleContainerIds.end(); + } + + void addListener (BLEDeviceWatcherListener& l) + { + listeners.add (l); + } + + void removeListener (BLEDeviceWatcherListener& l) + { + listeners.remove (l); + } + + private: + HRESULT addDevice (IDeviceInformation* addedDeviceInfo) override + { + HSTRING deviceIDHst; + auto hr = addedDeviceInfo->get_Id (&deviceIDHst); + + if (FAILED (hr)) + { + JUCE_WINRT_MIDI_LOG ("Failed to query added BLE device ID!"); + return S_OK; + } + + auto* wrtWrapper = WinRTWrapper::getInstanceWithoutCreating(); + + if (wrtWrapper == nullptr) + { + JUCE_WINRT_MIDI_LOG ("Failed to get the WinRTWrapper singleton!"); + return false; + } + + auto deviceID = wrtWrapper->hStringToString (deviceIDHst); + JUCE_WINRT_MIDI_LOG ("Detected paired BLE device: " << deviceID); + + if (auto* containerIDValue = watcher->getValueFromDeviceInfo ("System.Devices.Aep.ContainerId", addedDeviceInfo)) + { + auto containerID = watcher->getGUIDFromInspectable (*containerIDValue); + + if (containerID.isNotEmpty()) + { + listeners.call ([&containerID] (auto& l) { l.bleDeviceAdded (containerID); }); + + const ScopedLock lock (deviceChanges); + bleContainerIds.insert (containerID); + return S_OK; + } + } + + JUCE_WINRT_MIDI_LOG ("Failed to get a container ID for BLE device: " << deviceID); + return S_OK; + } + + HRESULT removeDevice (IDeviceInformationUpdate* removedDeviceInfo) override + { + if (auto* containerIDValue = watcher->getValueFromDeviceInfo ("System.Devices.Aep.ContainerId", removedDeviceInfo)) + { + auto containerID = watcher->getGUIDFromInspectable (*containerIDValue); + + if (containerID.isNotEmpty()) + { + listeners.call ([&containerID] (auto& l) { l.bleDeviceDisconnected (containerID); }); + + const ScopedLock lock (deviceChanges); + bleContainerIds.erase (containerID); + return S_OK; + } + } + + return E_FAIL; + } + + HRESULT updateDevice (IDeviceInformationUpdate*) override + { + // This shouldn't change the device container + return S_OK; + } + + BLEDeviceWatcher() = default; + + WaitFreeListeners listeners; + std::set bleContainerIds; + CriticalSection deviceChanges; + std::unique_ptr watcher; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BLEDeviceWatcher) + }; + + struct WinRTMIDIDeviceInfo + { + String deviceID, containerID, name; + bool isDefault = false; + }; + + struct MidiIODeviceWatcherListener + { + virtual ~MidiIODeviceWatcherListener() = default; + virtual void deviceAdded (const String&) = 0; + virtual void deviceRemoved (const String&) = 0; + virtual void deviceUpdated (const String&) = 0; + }; + + template + class MidiIODeviceWatcher : private DeviceCallbackHandler::Delegate + { + public: + void getAvailableDevices (std::vector& storage) + { + { + const ScopedLock lock (deviceChanges); + lastQueriedConnectedDevices = connectedDevices; + } + + StringArray deviceNames, deviceIDs; + + for (auto info : lastQueriedConnectedDevices.get()) + { + deviceNames.add (info.name); + deviceIDs .add (info.containerID); + } + + deviceNames.appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 ("")); + deviceIDs .appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 ("")); + + constexpr auto direction = std::invoke ([] + { + if constexpr (std::is_same_v) + return ump::BlockDirection::sender; + else if constexpr (std::is_same_v) + return ump::BlockDirection::receiver; + }); + + for (int i = 0; i < deviceNames.size(); ++i) + { + const auto fullInfo = ump::IOHelpers::makeProxyEndpoint (MidiDeviceInfo { deviceNames[i], deviceIDs[i] }, direction); + storage.push_back (fullInfo); + } + } + + WinRTMIDIDeviceInfo getWinRTDeviceInfoForDevice (const ump::EndpointId& deviceIdentifier) + { + std::vector endpoints; + getAvailableDevices (endpoints); + + for (const auto [index, value] : enumerate (endpoints)) + if (value.id == deviceIdentifier) + return lastQueriedConnectedDevices.get()[(int) index]; + + return {}; + } + + static std::unique_ptr make (ComSmartPtr f) + { + HSTRING deviceSelector; + auto hr = f->GetDeviceSelector (&deviceSelector); + + if (FAILED (hr)) + { + JUCE_WINRT_MIDI_LOG ("Failed to get MIDI device selector!"); + return {}; + } + + auto result = rawToUniquePtr (new MidiIODeviceWatcher); + result->factory = f; + + if (auto watcher = DeviceCallbackHandler::make (result.get(), deviceSelector, DeviceInformationKind::DeviceInformationKind_DeviceInterface)) + { + result->watcher = std::move (watcher); + return result; + } + + return {}; + } + + void addListener (MidiIODeviceWatcherListener& l) + { + listeners.add (l); + } + + void removeListener (MidiIODeviceWatcherListener& l) + { + listeners.remove (l); + } + + private: + MidiIODeviceWatcher() = default; + + HRESULT addDevice (IDeviceInformation* addedDeviceInfo) override + { + WinRTMIDIDeviceInfo info; + + HSTRING deviceID; + auto hr = addedDeviceInfo->get_Id (&deviceID); + + if (FAILED (hr)) + { + JUCE_WINRT_MIDI_LOG ("Failed to query added MIDI device ID!"); + return S_OK; + } + + auto* wrtWrapper = WinRTWrapper::getInstanceWithoutCreating(); + + if (wrtWrapper == nullptr) + { + JUCE_WINRT_MIDI_LOG ("Failed to get the WinRTWrapper singleton!"); + return false; + } + + info.deviceID = wrtWrapper->hStringToString (deviceID); + + listeners.call ([&] (auto& l) { l.deviceAdded (info.deviceID); }); + + JUCE_WINRT_MIDI_LOG ("Detected MIDI device: " << info.deviceID); + + boolean isEnabled = false; + hr = addedDeviceInfo->get_IsEnabled (&isEnabled); + + if (FAILED (hr) || ! isEnabled) + { + JUCE_WINRT_MIDI_LOG ("MIDI device not enabled: " << info.deviceID); + return S_OK; + } + + // We use the container ID to match a MIDI device with a generic BLE device, if possible + if (auto* containerIDValue = watcher->getValueFromDeviceInfo ("System.Devices.ContainerId", addedDeviceInfo)) + info.containerID = watcher->getGUIDFromInspectable (*containerIDValue); + + HSTRING name; + hr = addedDeviceInfo->get_Name (&name); + + if (FAILED (hr)) + { + JUCE_WINRT_MIDI_LOG ("Failed to query detected MIDI device name for " << info.deviceID); + return S_OK; + } + + info.name = wrtWrapper->hStringToString (name); + + boolean isDefault = false; + hr = addedDeviceInfo->get_IsDefault (&isDefault); + + if (FAILED (hr)) + { + JUCE_WINRT_MIDI_LOG ("Failed to query detected MIDI device defaultness for " << info.deviceID << " " << info.name); + return S_OK; + } + + info.isDefault = isDefault; + + JUCE_WINRT_MIDI_LOG ("Adding MIDI device: " << info.deviceID << " " << info.containerID << " " << info.name); + + { + const ScopedLock lock (deviceChanges); + connectedDevices.add (info); + } + + return S_OK; + } + + HRESULT removeDevice (IDeviceInformationUpdate* removedDeviceInfo) override + { + HSTRING removedDeviceIdHstr; + auto hr = removedDeviceInfo->get_Id (&removedDeviceIdHstr); + + if (FAILED (hr)) + { + JUCE_WINRT_MIDI_LOG ("Failed to query removed MIDI device ID!"); + return S_OK; + } + + auto* wrtWrapper = WinRTWrapper::getInstanceWithoutCreating(); + + if (wrtWrapper == nullptr) + { + JUCE_WINRT_MIDI_LOG ("Failed to get the WinRTWrapper singleton!"); + return S_OK; + } + + auto removedDeviceId = wrtWrapper->hStringToString (removedDeviceIdHstr); + + listeners.call ([&] (auto& l) { l.deviceRemoved (removedDeviceId); }); + + JUCE_WINRT_MIDI_LOG ("Removing MIDI device: " << removedDeviceId); + + { + const ScopedLock lock (deviceChanges); + + for (int i = 0; i < connectedDevices.size(); ++i) + { + if (connectedDevices[i].deviceID == removedDeviceId) + { + connectedDevices.remove (i); + JUCE_WINRT_MIDI_LOG ("Removed MIDI device: " << removedDeviceId); + break; + } + } + } + + return S_OK; + } + + // This is never called + HRESULT updateDevice (IDeviceInformationUpdate* c) override + { + HSTRING removedDeviceIdHstr; + auto hr = c->get_Id (&removedDeviceIdHstr); + + if (FAILED (hr)) + { + JUCE_WINRT_MIDI_LOG ("Failed to query removed MIDI device ID!"); + return S_OK; + } + + auto* wrtWrapper = WinRTWrapper::getInstanceWithoutCreating(); + + if (wrtWrapper == nullptr) + { + JUCE_WINRT_MIDI_LOG ("Failed to get the WinRTWrapper singleton!"); + return S_OK; + } + + auto id = wrtWrapper->hStringToString (removedDeviceIdHstr); + + listeners.call ([&] (auto& l) { l.deviceUpdated (id); }); + return S_OK; + } + + ComSmartPtr factory; + + Array connectedDevices; + CriticalSection deviceChanges; + ThreadLocalValue> lastQueriedConnectedDevices; + WaitFreeListeners listeners; + + std::unique_ptr watcher; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiIODeviceWatcher) + }; + + struct DeviceRemovalListener + { + virtual ~DeviceRemovalListener() = default; + virtual void deviceRemoved (const String&) = 0; + }; + + class Client : private MidiIODeviceWatcherListener, + private BLEDeviceWatcherListener, + private AsyncUpdater + { + public: + ~Client() override + { + inputDeviceWatcher->removeListener (*this); + outputDeviceWatcher->removeListener (*this); + bleDeviceWatcher->removeListener (*this); + } + + WinRTMIDIDeviceInfo getWinRTDeviceInfoForInput (const ump::EndpointId& deviceIdentifier) const + { + return inputDeviceWatcher->getWinRTDeviceInfoForDevice (deviceIdentifier); + } + + WinRTMIDIDeviceInfo getWinRTDeviceInfoForOutput (const ump::EndpointId& deviceIdentifier) const + { + return outputDeviceWatcher->getWinRTDeviceInfoForDevice (deviceIdentifier); + } + + bool isBleDevice (const String& containerId) const + { + return bleDeviceWatcher->isBleDevice (containerId); + } + + void addBleListener (BLEDeviceWatcherListener& l) + { + bleDeviceWatcher->addListener (l); + } + + void removeBleListener (BLEDeviceWatcherListener& l) + { + bleDeviceWatcher->removeListener (l); + } + + void getEndpoints (std::vector& x) + { + std::transform (cachedEndpoints.begin(), + cachedEndpoints.end(), + std::back_inserter (x), + [] (const auto& p) { return p.first; }); + } + + std::optional getEndpoint (const ump::EndpointId& x) const + { + const auto iter = cachedEndpoints.find (x); + + if (iter == cachedEndpoints.end()) + return {}; + + return iter->second.endpoint; + } + + std::optional getStaticDeviceInfo (const ump::EndpointId& x) const + { + const auto iter = cachedEndpoints.find (x); + + if (iter == cachedEndpoints.end()) + return {}; + + return iter->second.info; + } + + static std::unique_ptr make (ump::EndpointsListener& l) + { + const auto windowsVersionInfo = getWindowsVersionInfo(); + + if (windowsVersionInfo.dwMajorVersion < 10 || windowsVersionInfo.dwBuildNumber < 17763) + return {}; + + auto* wrtWrapper = WinRTWrapper::getInstance(); + + if (! wrtWrapper->isInitialised()) + return {}; + + auto midiInFactory = wrtWrapper->getWRLFactory (&RuntimeClass_Windows_Devices_Midi_MidiInPort[0]); + + if (midiInFactory == nullptr) + return {}; + + auto midiOutFactory = wrtWrapper->getWRLFactory (&RuntimeClass_Windows_Devices_Midi_MidiOutPort[0]); + + if (midiOutFactory == nullptr) + return {}; + + // The WinRT BLE MIDI API doesn't provide callbacks when devices become disconnected, + // but it does require a disconnection via the API before a device will reconnect again. + // We can monitor the BLE connection state of paired devices to get callbacks when + // connections are broken. + auto bleDeviceWatcher = BLEDeviceWatcher::make(); + + if (bleDeviceWatcher == nullptr) + return {}; + + auto inputDeviceWatcher = MidiIODeviceWatcher::make (midiInFactory); + + if (inputDeviceWatcher == nullptr) + return {}; + + auto outputDeviceWatcher = MidiIODeviceWatcher::make (midiOutFactory); + + if (outputDeviceWatcher == nullptr) + return {}; + + auto result = rawToUniquePtr (new Client { l }); + result->midiInFactory = std::move (midiInFactory); + result->midiOutFactory = std::move (midiOutFactory); + + result->inputDeviceWatcher = std::move (inputDeviceWatcher); + result->outputDeviceWatcher = std::move (outputDeviceWatcher); + result->bleDeviceWatcher = std::move (bleDeviceWatcher); + + result->inputDeviceWatcher->addListener (*result); + result->outputDeviceWatcher->addListener (*result); + result->bleDeviceWatcher->addListener (*result); + + result->updateCachedEndpoints(); + + return result; + } + + ComSmartPtr getMidiInFactory() const { return midiInFactory; } + ComSmartPtr getMidiOutFactory() const { return midiOutFactory; } + + void addDeviceRemovalListener (DeviceRemovalListener& l) + { + listeners.add (l); + } + + void removeDeviceRemovalListener (DeviceRemovalListener& l) + { + listeners.remove (l); + } + + private: + explicit Client (ump::EndpointsListener& l) + : listener (l) {} + + void deviceAdded (const String&) override + { + triggerAsyncUpdate(); + } + + void deviceRemoved (const String& id) override + { + listeners.call ([&] (auto& c) { c.deviceRemoved (id); }); + triggerAsyncUpdate(); + } + + void deviceUpdated (const String&) override + { + triggerAsyncUpdate(); + } + + void bleDeviceAdded (const String&) override + { + triggerAsyncUpdate(); + } + + void bleDeviceDisconnected (const String&) override + { + triggerAsyncUpdate(); + } + + void handleAsyncUpdate() override + { + updateCachedEndpoints(); + listener.endpointsChanged(); + } + + void updateCachedEndpoints() + { + std::vector buffer; + inputDeviceWatcher->getAvailableDevices (buffer); + outputDeviceWatcher->getAvailableDevices (buffer); + + cachedEndpoints.clear(); + + for (auto& item : buffer) + cachedEndpoints.emplace (item.id, item); + + // If this is hit, we got an identical ID for an input and output device + jassert (cachedEndpoints.size() == buffer.size()); + } + + ump::EndpointsListener& listener; + std::map cachedEndpoints; + + ComSmartPtr midiInFactory; + ComSmartPtr midiOutFactory; + std::unique_ptr> inputDeviceWatcher; + std::unique_ptr> outputDeviceWatcher; + std::unique_ptr bleDeviceWatcher; + WaitFreeListeners listeners; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Client) + }; + + template + class WinRTIOWrapper : private BLEDeviceWatcherListener, + private DeviceRemovalListener, + private AsyncUpdater + { + public: + static std::unique_ptr make (std::shared_ptr c, + const ump::EndpointId& deviceIdentifier, + ump::DisconnectionListener& listener) + { + auto result = rawToUniquePtr (new WinRTIOWrapper); + result->client = c; + result->onDisconnect = &listener; + result->deviceInfo = isInput ? c->getWinRTDeviceInfoForInput (deviceIdentifier) + : c->getWinRTDeviceInfoForOutput (deviceIdentifier); + + if (result->deviceInfo.deviceID.isEmpty()) + return {}; + + JUCE_WINRT_MIDI_LOG ("Creating JUCE MIDI IO: " << deviceInfo.deviceID); + + result->client->addDeviceRemovalListener (*result); + + if (result->deviceInfo.containerID.isNotEmpty()) + { + result->isBLEDevice = result->client->isBleDevice (result->deviceInfo.containerID); + result->client->addBleListener (*result); + } + + return result; + } + + WinRTMIDIDeviceInfo getDeviceInfo() const { return deviceInfo; } + bool isBLE() const { return isBLEDevice; } + + ump::EndpointId getEndpointId() const + { + return isInput ? ump::EndpointId::make (ump::IOKind::src, deviceInfo.containerID) + : ump::EndpointId::make (ump::IOKind::dst, deviceInfo.containerID); + } + + ~WinRTIOWrapper() override + { + client->removeDeviceRemovalListener (*this); + client->removeBleListener (*this); + + cancelPendingUpdate(); + } + + private: + WinRTIOWrapper() = default; + + void bleDeviceAdded (const String& containerID) override + { + if (containerID == deviceInfo.containerID) + isBLEDevice = true; + } + + void bleDeviceDisconnected (const String& containerID) override + { + if (containerID != deviceInfo.containerID) + triggerAsyncUpdate(); + + JUCE_WINRT_MIDI_LOG ("Disconnecting MIDI port from BLE disconnection: " << deviceInfo.deviceID + << " " << deviceInfo.containerID << " " << deviceInfo.name); + } + + void deviceRemoved (const String& deviceId) override + { + if (deviceId == deviceInfo.deviceID) + triggerAsyncUpdate(); + } + + void handleAsyncUpdate() override + { + if (onDisconnect != nullptr) + onDisconnect->disconnected(); + } + + std::shared_ptr client; + WinRTMIDIDeviceInfo deviceInfo; + bool isBLEDevice = false; + ump::DisconnectionListener* onDisconnect = nullptr; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WinRTIOWrapper) + }; + + template + static void openMidiPortThread (String threadName, + String midiDeviceID, + const ComSmartPtr& comFactory, + ComSmartPtr& comPort) + { + std::thread { [&] + { + Thread::setCurrentThreadName (threadName); + + const WinRTWrapper::ScopedHString hDeviceId { midiDeviceID }; + ComSmartPtr> asyncOp; + const auto hr = comFactory->FromIdAsync (hDeviceId.get(), asyncOp.resetAndGetPointerAddress()); + + if (FAILED (hr)) + return; + + std::promise> promise; + auto future = promise.get_future(); + + auto callback = [p = std::move (promise)] (IAsyncOperation* asyncOpPtr, AsyncStatus) mutable + { + if (asyncOpPtr == nullptr) + { + p.set_value (nullptr); + return E_ABORT; + } + + ComSmartPtr result; + const auto hr = asyncOpPtr->GetResults (result.resetAndGetPointerAddress()); + + if (FAILED (hr)) + { + p.set_value (nullptr); + return hr; + } + + p.set_value (std::move (result)); + return S_OK; + }; + + const auto ir = asyncOp->put_Completed (Microsoft::WRL::Callback> (std::move (callback)).Get()); + + if (FAILED (ir)) + return; + + if (future.wait_for (std::chrono::milliseconds (2000)) == std::future_status::ready) + comPort = future.get(); + } }.join(); + } + + class InputImplNative : public ump::Input::Impl::Native + { + public: + ~InputImplNative() + { + disconnect(); + } + + ump::EndpointId getEndpointId() const override + { + return wrapper->getEndpointId(); + } + + ump::PacketProtocol getProtocol() const override + { + return dispatcher.getProtocol(); + } + + static std::unique_ptr open (std::shared_ptr c, + ump::DisconnectionListener& listener, + const ump::EndpointId& identifier, + ump::PacketProtocol protocol, + ump::Consumer& consumer) + { + auto result = rawToUniquePtr (new InputImplNative (c, protocol, consumer)); + + auto w = WinRTIOWrapper::make (c, identifier, listener); + + if (w == nullptr) + return {}; + + result->wrapper = std::move (w); + + openMidiPortThread ("Open WinRT MIDI input port", + result->wrapper->getDeviceInfo().deviceID, + result->client->getMidiInFactory(), + result->port); + + if (result->port == nullptr) + { + JUCE_WINRT_MIDI_LOG ("Timed out waiting for midi input port creation"); + return {}; + } + + result->startTime = Time::getMillisecondCounterHiRes(); + result->start(); + return result; + } + + private: + InputImplNative (std::shared_ptr c, ump::PacketProtocol p, ump::Consumer& cb) + : client (c), consumer (cb), dispatcher { 0, p, 4096 } + { + } + + void start() + { + if (midiInMessageToken.value != 0) + { + JUCE_WINRT_MIDI_LOG ("Input already started"); + return; + } + + const auto hr = port->add_MessageReceived ( + Microsoft::WRL::Callback> ( + [this] (IMidiInPort*, IMidiMessageReceivedEventArgs* args) + { + return midiInMessageReceived (args); + } + ).Get(), + &midiInMessageToken); + + if (FAILED (hr)) + { + JUCE_WINRT_MIDI_LOG ("Failed to set MIDI input callback"); + jassertfalse; + } + } + + void stop() + { + if (port != nullptr && midiInMessageToken.value != 0) + port->remove_MessageReceived (midiInMessageToken); + + midiInMessageToken = {}; + } + + void disconnect() + { + stop(); + + if (port != nullptr && wrapper->isBLE()) + port->Release(); + + port = nullptr; + } + + HRESULT midiInMessageReceived (IMidiMessageReceivedEventArgs* args) + { + ComSmartPtr message; + auto hr = args->get_Message (message.resetAndGetPointerAddress()); + + if (FAILED (hr)) + return hr; + + ComSmartPtr buffer; + hr = message->get_RawData (buffer.resetAndGetPointerAddress()); + + if (FAILED (hr)) + return hr; + + ComSmartPtr bufferByteAccess; + hr = buffer->QueryInterface (bufferByteAccess.resetAndGetPointerAddress()); + + if (FAILED (hr)) + return hr; + + uint8_t* bufferData = nullptr; + hr = bufferByteAccess->Buffer (&bufferData); + + if (FAILED (hr)) + return hr; + + uint32_t numBytes = 0; + hr = buffer->get_Length (&numBytes); + + if (FAILED (hr)) + return hr; + + ABI::Windows::Foundation::TimeSpan timespan; + hr = message->get_Timestamp (×pan); + + if (FAILED (hr)) + return hr; + + const Span bytes { unalignedPointerCast (bufferData), numBytes }; + const auto time = convertTimeStamp (timespan.Duration); + dispatcher.dispatch (bytes, time, [this] (const ump::View& view, double timestamp) + { + const ump::Iterator b { view.data(), view.size() }; + const auto e = std::next (b); + consumer.consume (b, e, timestamp); + }); + + return S_OK; + } + + double convertTimeStamp (int64 timestamp) + { + auto millisecondsSinceStart = static_cast (timestamp) / 10000.0; + auto t = startTime + millisecondsSinceStart; + auto now = Time::getMillisecondCounterHiRes(); + + if (t > now) + { + if (t > now + 2.0) + startTime -= 1.0; + + t = now; + } + + return t * 0.001; + } + + std::shared_ptr client; + ComSmartPtr port; + ump::Consumer& consumer; + EventRegistrationToken midiInMessageToken { 0 }; + double startTime = 0; + ump::BytestreamToUMPDispatcher dispatcher; + std::unique_ptr> wrapper; + }; + + class OutputImplNative : public ump::Output::Impl::Native + { + public: + ~OutputImplNative() + { + disconnect(); + } + + ump::EndpointId getEndpointId() const override + { + return wrapper->getEndpointId(); + } + + bool send (ump::Iterator b, ump::Iterator e) override + { + for (const auto& view : makeRange (b, e)) + { + toBytestream.convert (view, 0, [&] (ump::BytesOnGroup bytesView, double) + { + sendBytestream (bytesView.bytes); + }); + } + + return true; + } + + static std::unique_ptr open (std::shared_ptr c, + ump::DisconnectionListener& listener, + const ump::EndpointId& identifier) + { + auto result = rawToUniquePtr (new OutputImplNative { c }); + + auto w = WinRTIOWrapper::make (c, identifier, listener); + + if (w == nullptr) + return {}; + + result->wrapper = std::move (w); + + openMidiPortThread ("Open WinRT MIDI output port", + result->wrapper->getDeviceInfo().deviceID, + result->client->getMidiOutFactory(), + result->port); + + if (result->port == nullptr) + return {}; + + auto* wrtWrapper = WinRTWrapper::getInstanceWithoutCreating(); + + if (wrtWrapper == nullptr) + return {}; + + auto bufferFactory = wrtWrapper->getWRLFactory (&RuntimeClass_Windows_Storage_Streams_Buffer[0]); + + if (bufferFactory == nullptr) + return {}; + + auto hr = bufferFactory->Create (static_cast (65536), result->buffer.resetAndGetPointerAddress()); + + if (FAILED (hr)) + return {}; + + hr = result->buffer->QueryInterface (result->bufferByteAccess.resetAndGetPointerAddress()); + + if (FAILED (hr)) + return {}; + + hr = result->bufferByteAccess->Buffer (&result->bufferData); + + if (FAILED (hr)) + return {}; + + return result; + } + + private: + explicit OutputImplNative (std::shared_ptr c) + : client (std::move (c)) + { + } + + void disconnect() + { + if (port != nullptr && wrapper->isBLE()) + port->Release(); + + port = nullptr; + } + + void sendBytestream (Span message) + { + if (port == nullptr) + return; + + auto numBytes = message.size(); + auto hr = buffer->put_Length ((UINT32) numBytes); + + if (FAILED (hr)) + { + jassertfalse; + return; + } + + memcpy_s (bufferData, numBytes, message.data(), numBytes); + port->SendBuffer (buffer); + } + + std::shared_ptr client; + ComSmartPtr port; + ump::ToBytestreamConverter toBytestream { 4096 }; + ComSmartPtr buffer; + ComSmartPtr bufferByteAccess; + uint8_t* bufferData = nullptr; + std::unique_ptr> wrapper; + }; + + class SessionImplNative : public ump::Session::Impl::Native + { + public: + SessionImplNative (std::shared_ptr c, const String& x) : client (c), name (x) {} + + String getName() const override + { + return name; + } + + std::unique_ptr connectInput (ump::DisconnectionListener& listener, + const ump::EndpointId& id, + ump::PacketProtocol protocol, + ump::Consumer& consumer) override + { + return InputImplNative::open (client, listener, id, protocol, consumer); + } + + std::unique_ptr connectOutput (ump::DisconnectionListener& listener, + const ump::EndpointId& id) override + { + return OutputImplNative::open (client, listener, id); + } + + private: + std::shared_ptr client; + String name; + }; + + class EndpointsImplNative : public ump::Endpoints::Impl::Native + { + public: + ump::Backend getBackend() const override + { + return ump::Backend::winrt; + } + + bool isVirtualMidiUmpServiceActive() const override + { + return false; + } + + bool isVirtualMidiBytestreamServiceActive() const override + { + return false; + } + + void getEndpoints (std::vector& x) const override + { + client->getEndpoints (x); + } + + std::optional getEndpoint (const ump::EndpointId& x) const override + { + return client->getEndpoint (x); + } + + std::optional getStaticDeviceInfo (const ump::EndpointId& x) const override + { + return client->getStaticDeviceInfo (x); + } + + std::unique_ptr makeSession (const String& x) override + { + return std::make_unique (client, x); + } + + static std::unique_ptr make (ump::EndpointsListener& l) + { + return rawToUniquePtr (new EndpointsImplNative { l }); + } + + private: + explicit EndpointsImplNative (ump::EndpointsListener& l) + : client (Client::make (l)) {} + + std::shared_ptr client; + }; + }; + #endif + + struct Win32 + { + struct ITraits + { + using Ptr = HMIDIIN; + using Caps = MIDIINCAPS; + static inline const auto getNum = midiInGetNumDevs; + static inline const auto getCaps = midiInGetDevCaps; + static inline const auto message = midiInMessage; + static inline const auto isInput = true; + }; + + struct OTraits + { + using Ptr = HMIDIOUT; + using Caps = MIDIOUTCAPS; + static inline const auto getNum = midiOutGetNumDevs; + static inline const auto getCaps = midiOutGetDevCaps; + static inline const auto message = midiOutMessage; + static inline const auto isInput = false; + }; + + template + static void getAvailableDevices (const Traits&, std::vector& result) + { + const auto deviceCaps = [&] + { + std::vector devices; + + const auto end = Traits::getNum(); + + for (auto i = (decltype (end)) 0; i < end; ++i) + if (typename Traits::Caps mc{}; Traits::getCaps (i, &mc, sizeof (mc)) == MMSYSERR_NOERROR) + devices.push_back (mc); + + return devices; + }(); + + StringArray deviceIDs, deviceNames; + + for (const auto [i, device] : enumerate (deviceCaps)) + { + const auto name = device.szPname; + const auto identifier = std::invoke ([index = i]() -> String + { + ULONG size = 0; + + if (Traits::message ((typename Traits::Ptr) index, DRV_QUERYDEVICEINTERFACESIZE, (DWORD_PTR) &size, 0) != MMSYSERR_NOERROR) + return {}; + + WCHAR interfaceName[512]{}; + + if (! isPositiveAndBelow (size, std::size (interfaceName)) + || Traits::message ((typename Traits::Ptr) index, DRV_QUERYDEVICEINTERFACE, (DWORD_PTR) interfaceName, sizeof (interfaceName)) != MMSYSERR_NOERROR) + { + return {}; + } + + return interfaceName; + }); + + deviceNames.add (name); + deviceIDs.add (identifier.isNotEmpty() ? identifier : name); + } + + for (auto* list : { &deviceIDs, &deviceNames }) + list->appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 ("")); + + constexpr auto direction = Traits::isInput ? ump::BlockDirection::sender : ump::BlockDirection::receiver; + + for (auto [index, id] : enumerate (deviceIDs, int{})) + { + const auto fullInfo = ump::IOHelpers::makeProxyEndpoint ({ deviceNames[index], id }, direction); + result.push_back (fullInfo); + } + } + + class MidiHeader + { + public: void prepare (HMIDIIN device) { - zerostruct (hdr); - hdr.lpData = data; - hdr.dwBufferLength = (DWORD) numElementsInArray (data); + hdr = {}; + hdr.lpData = data.data(); + hdr.dwBufferLength = (DWORD) data.size(); midiInPrepareHeader (device, &hdr, sizeof (hdr)); } @@ -338,1701 +2512,665 @@ private: write (device); } - MIDIHDR hdr; - char data[256]; - - JUCE_DECLARE_NON_COPYABLE (MidiHeader) + private: + MIDIHDR hdr{}; + std::array data{}; }; - enum { numHeaders = 32 }; - MidiHeader headers[numHeaders]; - - void writeFinishedBlocks() + template + class DeviceCache { - for (int i = 0; i < (int) numHeaders; ++i) - headers[i].writeIfFinished (deviceHandle); - } - - void unprepareAllHeaders() - { - for (int i = 0; i < (int) numHeaders; ++i) - headers[i].unprepare (deviceHandle); - } - - double convertTimeStamp (uint32 timeStamp) - { - auto t = startTime + timeStamp; - auto now = Time::getMillisecondCounterHiRes(); - - if (t > now) + public: + std::shared_ptr open (const ump::EndpointId& id) { - if (t > now + 2.0) - startTime -= 1.0; + const std::scoped_lock lock { mutex }; - t = now; + if (const auto iter = devices.find (id); iter != devices.end()) + if (auto strong = iter->second.lock()) + return strong; + + std::unique_ptr uniqueDevice = openInternal (id); + std::shared_ptr sharedDevice { std::move (uniqueDevice) }; + devices.emplace (id, sharedDevice); + return sharedDevice; } - return t * 0.001; - } + private: + std::mutex mutex; + std::map> devices; + }; - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiInCollector) - }; - - //============================================================================== - template - struct Win32MidiDeviceQuery - { - virtual ~Win32MidiDeviceQuery() = default; - - static Array getAvailableDevices() + // A device can only be opened by one client at a time. In order to allow multiple inputs + // to open the same device in JUCE, we share the device between all inputs that are + // currently using it. + class InputDevice : private AsyncUpdater { - StringArray deviceNames, deviceIDs; - auto deviceCaps = WrapperType::getDeviceCaps(); - - for (int i = 0; i < deviceCaps.size(); ++i) + public: + static std::shared_ptr open (const ump::EndpointId& id) { - deviceNames.add (deviceCaps[i].szPname); - - auto identifier = getInterfaceIDForDevice ((UINT) i); - - if (identifier.isNotEmpty()) - deviceIDs.add (identifier); - else - deviceIDs.add (deviceNames[i]); + static DeviceCache devices; + return devices.open (id); } - deviceNames.appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 ("")); - deviceIDs .appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 ("")); - - Array devices; - - for (int i = 0; i < deviceNames.size(); ++i) - devices.add ({ deviceNames[i], deviceIDs[i] }); - - return devices; - } - - private: - static String getInterfaceIDForDevice (UINT id) - { - ULONG size = 0; - - if (WrapperType::sendMidiMessage ((UINT_PTR) id, DRV_QUERYDEVICEINTERFACESIZE, (DWORD_PTR) &size, 0) == MMSYSERR_NOERROR) + ~InputDevice() override { - WCHAR interfaceName[512] = {}; + allInputs().remove (*this); - if (isPositiveAndBelow (size, sizeof (interfaceName)) - && WrapperType::sendMidiMessage ((UINT_PTR) id, DRV_QUERYDEVICEINTERFACE, - (DWORD_PTR) interfaceName, sizeof (interfaceName)) == MMSYSERR_NOERROR) - { - return interfaceName; - } - } - - return {}; - } - }; - - struct Win32InputWrapper final : public MidiInput::Pimpl, - public Win32MidiDeviceQuery - { - Win32InputWrapper (Win32MidiService& parentService, MidiInput& midiInput, const String& deviceIdentifier, MidiInputCallback& c) - : input (midiInput), callback (c) - { - collector = getOrCreateCollector (parentService, deviceIdentifier); - collector->addClient (this); - } - - ~Win32InputWrapper() override - { - collector->removeClient (this); - } - - static MidiInCollector::Ptr getOrCreateCollector (Win32MidiService& parentService, const String& deviceIdentifier) - { - UINT deviceID = MIDI_MAPPER; - String deviceName; - auto devices = getAvailableDevices(); - - for (int i = 0; i < devices.size(); ++i) - { - auto d = devices.getUnchecked (i); - - if (d.identifier == deviceIdentifier) - { - deviceID = (UINT) i; - deviceName = d.name; - break; - } - } - - const ScopedLock sl (parentService.activeCollectorLock); - - for (auto& c : parentService.activeCollectors) - if (c->deviceInfo.identifier == deviceIdentifier) - return c; - - MidiInCollector::Ptr c (new MidiInCollector (parentService, { deviceName, deviceIdentifier })); - - HMIDIIN h; - auto err = midiInOpen (&h, deviceID, - (DWORD_PTR) &MidiInCollector::midiInCallback, - (DWORD_PTR) (MidiInCollector*) c.get(), - CALLBACK_FUNCTION); - - if (err != MMSYSERR_NOERROR) - throw std::runtime_error ("Failed to create Windows input device wrapper"); - - c->deviceHandle = h; - parentService.activeCollectors.add (c); - return c; - } - - static DWORD sendMidiMessage (UINT_PTR deviceID, UINT msg, DWORD_PTR arg1, DWORD_PTR arg2) - { - return midiInMessage ((HMIDIIN) deviceID, msg, arg1, arg2); - } - - static Array getDeviceCaps() - { - Array devices; - - for (UINT i = 0; i < midiInGetNumDevs(); ++i) - { - MIDIINCAPS mc = {}; - - if (midiInGetDevCaps (i, &mc, sizeof (mc)) == MMSYSERR_NOERROR) - devices.add (mc); - } - - return devices; - } - - static MidiDeviceInfo getDefaultDevice() { return getAvailableDevices().getFirst(); } - - void start() override { started = true; concatenator.reset(); collector->startOrStop(); } - void stop() override { started = false; collector->startOrStop(); concatenator.reset(); } - - String getDeviceIdentifier() override { return collector->deviceInfo.identifier; } - String getDeviceName() override { return collector->deviceInfo.name; } - - void pushMidiData (const void* inputData, int numBytes, double time) - { - concatenator.pushMidiData (inputData, numBytes, time, &input, callback); - } - - MidiInput& input; - MidiInputCallback& callback; - MidiDataConcatenator concatenator { 4096 }; - MidiInCollector::Ptr collector; - bool started = false; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Win32InputWrapper) - }; - - //============================================================================== - struct MidiOutHandle final : public ReferenceCountedObject - { - using Ptr = ReferenceCountedObjectPtr; - - MidiOutHandle (Win32MidiService& parent, MidiDeviceInfo d, HMIDIOUT h) - : owner (parent), deviceInfo (d), handle (h) - { - owner.activeOutputHandles.add (this); - } - - ~MidiOutHandle() - { - if (handle != nullptr) - midiOutClose (handle); - - owner.activeOutputHandles.removeFirstMatchingValue (this); - } - - Win32MidiService& owner; - MidiDeviceInfo deviceInfo; - HMIDIOUT handle; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiOutHandle) - }; - - //============================================================================== - struct Win32OutputWrapper final : public MidiOutput::Pimpl, - public Win32MidiDeviceQuery - { - Win32OutputWrapper (Win32MidiService& p, const String& deviceIdentifier) - : parent (p) - { - auto devices = getAvailableDevices(); - UINT deviceID = MIDI_MAPPER; - String deviceName; - - for (int i = 0; i < devices.size(); ++i) - { - auto d = devices.getUnchecked (i); - - if (d.identifier == deviceIdentifier) - { - deviceID = (UINT) i; - deviceName = d.name; - break; - } - } - - if (deviceID == MIDI_MAPPER) - { - // use the microsoft sw synth as a default - best not to allow deviceID - // to be MIDI_MAPPER, or else device sharing breaks - for (int i = 0; i < devices.size(); ++i) - if (devices[i].name.containsIgnoreCase ("microsoft")) - deviceID = (UINT) i; - } - - for (int i = parent.activeOutputHandles.size(); --i >= 0;) - { - auto* activeHandle = parent.activeOutputHandles.getUnchecked (i); - - if (activeHandle->deviceInfo.identifier == deviceIdentifier) - { - han = activeHandle; + if (deviceHandle == nullptr) return; + + unprepareAllHeaders(); + + midiInReset (deviceHandle); + midiInStop (deviceHandle); + + for (int count = 5; --count >= 0;) + { + if (midiInClose (deviceHandle) == MMSYSERR_NOERROR) + break; + + Sleep (20); } } - for (int i = 4; --i >= 0;) + ump::EndpointId getEndpointId() const { - HMIDIOUT h = nullptr; - auto res = midiOutOpen (&h, deviceID, 0, 0, CALLBACK_NULL); - - if (res == MMSYSERR_NOERROR) - { - han = new MidiOutHandle (parent, { deviceName, deviceIdentifier }, h); - return; - } - - if (res == MMSYSERR_ALLOCATED) - Sleep (100); - else - break; + return endpointId; } - throw std::runtime_error ("Failed to create Windows output device wrapper"); - } - - void sendMessageNow (const MidiMessage& message) override - { - if (message.getRawDataSize() > 3 || message.isSysEx()) + void addConsumer (ump::Consumer& c) { - MIDIHDR h = {}; + consumers.add (c); + } - h.lpData = (char*) message.getRawData(); - h.dwBytesRecorded = h.dwBufferLength = (DWORD) message.getRawDataSize(); + void removeConsumer (ump::Consumer& c) + { + consumers.add (c); + } - if (midiOutPrepareHeader (han->handle, &h, sizeof (MIDIHDR)) == MMSYSERR_NOERROR) + void addDisconnectListener (ump::DisconnectionListener& l) + { + disconnectListeners.add (&l); + } + + void removeDisconnectListener (ump::DisconnectionListener& l) + { + disconnectListeners.remove (&l); + } + + private: + static std::unique_ptr openInternal (const ump::EndpointId& id) + { + std::vector endpoints; + getAvailableDevices (ITraits{}, endpoints); + const auto iter = std::find_if (endpoints.begin(), + endpoints.end(), + [&] (const auto& x) { return x.id == id; }); + + if (iter == endpoints.end()) + return {}; + + const auto deviceID = std::distance (endpoints.begin(), iter); + auto result = rawToUniquePtr (new InputDevice { id }); + + HMIDIIN handle{}; + const auto err = midiInOpen (&handle, + (UINT) deviceID, + (DWORD_PTR) &midiInCallback, + (DWORD_PTR) result.get(), + CALLBACK_FUNCTION); + + if (err != MMSYSERR_NOERROR) + return {}; + + result->deviceHandle = handle; + + for (auto& header : result->headers) { - auto res = midiOutLongMsg (han->handle, &h, sizeof (MIDIHDR)); + header.prepare (handle); + header.write (handle); + } - if (res == MMSYSERR_NOERROR) + if (midiInStart (handle) != MMSYSERR_NOERROR) + return {}; + + return result; + } + + explicit InputDevice (const ump::EndpointId& x) + : endpointId (x) + { + allInputs().add (*this); + } + + void handleMessage (const uint8* bytes, uint32 timeStamp) + { + if (bytes[0] >= 0x80) + { + auto len = MidiMessage::getMessageLengthFromFirstByte (bytes[0]); + auto time = convertTimeStamp (timeStamp); + dispatcher.dispatch ({ unalignedPointerCast (bytes), (size_t) len }, time, [this] (const ump::View& view, double timestamp) { - while ((h.dwFlags & MHDR_DONE) == 0) - Sleep (1); + const ump::Iterator b { view.data(), view.size() }; + const auto e = std::next (b); + consumers.call ([&] (ump::Consumer& c) { c.consume (b, e, timestamp); }); + }); - int count = 500; // 1 sec timeout + writeFinishedBlocks(); + } + } - while (--count >= 0) + void handleSysEx (MIDIHDR* hdr, uint32 timeStamp) + { + if (hdr->dwBytesRecorded > 0) + { + auto time = convertTimeStamp (timeStamp); + dispatcher.dispatch ({ unalignedPointerCast (hdr->lpData), (size_t) hdr->dwBytesRecorded }, time, [this] (const ump::View& view, double timestamp) + { + const ump::Iterator b { view.data(), view.size() }; + const auto e = std::next (b); + consumers.call ([&] (ump::Consumer& c) { c.consume (b, e, timestamp); }); + }); + + writeFinishedBlocks(); + } + } + + void disconnected() + { + triggerAsyncUpdate(); + } + + void handleAsyncUpdate() override + { + disconnectListeners.call ([] (auto& x) { x.disconnected(); }); + } + + void writeFinishedBlocks() + { + for (auto& header : headers) + header.writeIfFinished (deviceHandle); + } + + void unprepareAllHeaders() + { + for (auto& header : headers) + header.unprepare (deviceHandle); + } + + double convertTimeStamp (uint32 timeStamp) + { + auto t = startTime + timeStamp; + auto now = Time::getMillisecondCounterHiRes(); + + if (t > now) + { + if (t > now + 2.0) + startTime -= 1.0; + + t = now; + } + + return t * 0.001; + } + + static void CALLBACK midiInCallback (HMIDIIN, + UINT uMsg, + DWORD_PTR dwInstance, + DWORD_PTR midiMessage, + DWORD_PTR timeStamp) + { + auto* collector = reinterpret_cast (dwInstance); + + allInputs().call ([&] (auto& l) + { + if (collector != &l) + return; + + switch (uMsg) + { + case MIM_DATA: + l.handleMessage ((const uint8*) &midiMessage, (uint32) timeStamp); + break; + + case MIM_LONGDATA: + l.handleSysEx ((MIDIHDR*) midiMessage, (uint32) timeStamp); + break; + + case MIM_CLOSE: + l.disconnected(); + break; + } + }); + } + + static WaitFreeListeners& allInputs() + { + static WaitFreeListeners result; + return result; + } + + ump::EndpointId endpointId; + HMIDIIN deviceHandle = nullptr; + std::array headers; + double startTime = Time::getMillisecondCounterHiRes(); + WaitFreeListeners consumers; + ListenerList disconnectListeners; + // The shared input always converts to plain MIDI 1.0. Clients that want MIDI 2.0 have + // their own converters. + ump::BytestreamToUMPDispatcher dispatcher { 0, ump::PacketProtocol::MIDI_1_0, 4096 }; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InputDevice) + }; + + class OutputDevice : private AsyncUpdater + { + public: + ~OutputDevice() override + { + allOutputs().remove (*this); + + cancelPendingUpdate(); + + if (handle != nullptr) + midiOutClose (handle); + } + + static std::shared_ptr open (const ump::EndpointId& id) + { + static DeviceCache devices; + return devices.open (id); + } + + ump::EndpointId getEndpointId() const + { + return endpointId; + } + + bool send (ump::Iterator b, ump::Iterator e) + { + for (const auto& view : makeRange (b, e)) + { + toBytestream.convert (view, 0, [&] (ump::BytesOnGroup bytesView, double) + { + sendBytestream (bytesView.bytes); + }); + } + + return true; + } + + void addDisconnectListener (ump::DisconnectionListener& l) + { + disconnectListeners.add (&l); + } + + void removeDisconnectListener (ump::DisconnectionListener& l) + { + disconnectListeners.remove (&l); + } + + private: + static std::unique_ptr openInternal (const ump::EndpointId& id) + { + std::vector endpoints; + getAvailableDevices (OTraits{}, endpoints); + const auto iter = std::find_if (endpoints.begin(), + endpoints.end(), + [&] (const auto& x) { return x.id == id; }); + + if (iter == endpoints.end()) + return {}; + + const auto deviceID = std::distance (endpoints.begin(), iter); + + for (auto i = 0; i < 4; ++i) + { + auto result = rawToUniquePtr (new OutputDevice (id)); + + HMIDIOUT h = nullptr; + auto res = midiOutOpen (&h, + (UINT) deviceID, + (DWORD_PTR) &midiOutCallback, + (DWORD_PTR) result.get(), + CALLBACK_FUNCTION); + + switch (res) + { + case MMSYSERR_NOERROR: + result->handle = h; + return result; + + case MMSYSERR_ALLOCATED: + Sleep (100); + break; + + default: + return {}; + } + } + + return {}; + } + + explicit OutputDevice (const ump::EndpointId& x) + : endpointId (x) + { + allOutputs().add (*this); + } + + void sendBytestream (Span message) + { + if (message.empty()) + return; + + if (message.size() > 3 || message[0] == std::byte { 0xf0 }) + { + MIDIHDR h = {}; + + h.lpData = (char*) message.data(); + h.dwBytesRecorded = h.dwBufferLength = (DWORD) message.size(); + + if (midiOutPrepareHeader (handle, &h, sizeof (MIDIHDR)) == MMSYSERR_NOERROR) + { + auto res = midiOutLongMsg (handle, &h, sizeof (MIDIHDR)); + + if (res == MMSYSERR_NOERROR) { - res = midiOutUnprepareHeader (han->handle, &h, sizeof (MIDIHDR)); + while ((h.dwFlags & MHDR_DONE) == 0) + Sleep (1); - if (res == MIDIERR_STILLPLAYING) - Sleep (2); - else - break; + int count = 500; // 1 sec timeout + + while (--count >= 0) + { + res = midiOutUnprepareHeader (handle, &h, sizeof (MIDIHDR)); + + if (res == MIDIERR_STILLPLAYING) + Sleep (2); + else + break; + } } } } - } - else - { - for (int i = 0; i < 50; ++i) + else { - if (midiOutShortMsg (han->handle, *unalignedPointerCast (message.getRawData())) != MIDIERR_NOTREADY) - break; + const auto msg = ByteOrder::makeInt (0 < message.size() ? (uint8_t) message[0] : 0, + 1 < message.size() ? (uint8_t) message[1] : 0, + 2 < message.size() ? (uint8_t) message[2] : 0, + 0); - Sleep (1); + for (int i = 0; i < 50; ++i) + { + if (midiOutShortMsg (handle, msg) != MIDIERR_NOTREADY) + break; + + Sleep (1); + } } } - } - static DWORD sendMidiMessage (UINT_PTR deviceID, UINT msg, DWORD_PTR arg1, DWORD_PTR arg2) - { - return midiOutMessage ((HMIDIOUT) deviceID, msg, arg1, arg2); - } - - static Array getDeviceCaps() - { - Array devices; - - for (UINT i = 0; i < midiOutGetNumDevs(); ++i) + void disconnected() { - MIDIOUTCAPS mc = {}; - - if (midiOutGetDevCaps (i, &mc, sizeof (mc)) == MMSYSERR_NOERROR) - devices.add (mc); + triggerAsyncUpdate(); } - return devices; - } - - static MidiDeviceInfo getDefaultDevice() - { - auto defaultIndex = []() + void handleAsyncUpdate() override { - auto deviceCaps = getDeviceCaps(); - - for (int i = 0; i < deviceCaps.size(); ++i) - if ((deviceCaps[i].wTechnology & MOD_MAPPER) != 0) - return i; - - return 0; - }(); - - return getAvailableDevices()[defaultIndex]; - } - - String getDeviceIdentifier() override { return han->deviceInfo.identifier; } - String getDeviceName() override { return han->deviceInfo.name; } - - Win32MidiService& parent; - MidiOutHandle::Ptr han; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Win32OutputWrapper) - }; - - //============================================================================== - void asyncCheckForUnusedCollectors() - { - startTimer (10); - } - - void timerCallback() override - { - stopTimer(); - - const ScopedLock sl (activeCollectorLock); - - for (int i = activeCollectors.size(); --i >= 0;) - if (activeCollectors.getObjectPointer (i)->getReferenceCount() == 1) - activeCollectors.remove (i); - } - - CriticalSection activeCollectorLock; - ReferenceCountedArray activeCollectors; - Array activeOutputHandles; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Win32MidiService) -}; - -Array Win32MidiService::MidiInCollector::activeMidiCollectors; - -//============================================================================== -//============================================================================== -#if JUCE_USE_WINRT_MIDI - -#ifndef JUCE_FORCE_WINRT_MIDI - #define JUCE_FORCE_WINRT_MIDI 0 -#endif - -#ifndef JUCE_WINRT_MIDI_LOGGING - #define JUCE_WINRT_MIDI_LOGGING 0 -#endif - -#if JUCE_WINRT_MIDI_LOGGING - #define JUCE_WINRT_MIDI_LOG(x) DBG(x) -#else - #define JUCE_WINRT_MIDI_LOG(x) -#endif - -using namespace Microsoft::WRL; - -using namespace ABI::Windows::Foundation; -using namespace ABI::Windows::Foundation::Collections; -using namespace ABI::Windows::Devices::Midi; -using namespace ABI::Windows::Devices::Enumeration; -using namespace ABI::Windows::Storage::Streams; - -//============================================================================== -struct WinRTMidiService final : public MidiServiceType -{ -public: - //============================================================================== - WinRTMidiService() - { - auto* wrtWrapper = WinRTWrapper::getInstance(); - - if (! wrtWrapper->isInitialised()) - throw std::runtime_error ("Failed to initialise the WinRT wrapper"); - - midiInFactory = wrtWrapper->getWRLFactory (&RuntimeClass_Windows_Devices_Midi_MidiInPort[0]); - - if (midiInFactory == nullptr) - throw std::runtime_error ("Failed to create midi in factory"); - - midiOutFactory = wrtWrapper->getWRLFactory (&RuntimeClass_Windows_Devices_Midi_MidiOutPort[0]); - - if (midiOutFactory == nullptr) - throw std::runtime_error ("Failed to create midi out factory"); - - // The WinRT BLE MIDI API doesn't provide callbacks when devices become disconnected, - // but it does require a disconnection via the API before a device will reconnect again. - // We can monitor the BLE connection state of paired devices to get callbacks when - // connections are broken. - bleDeviceWatcher.reset (new BLEDeviceWatcher()); - - if (! bleDeviceWatcher->start()) - throw std::runtime_error ("Failed to start the BLE device watcher"); - - inputDeviceWatcher.reset (new MidiIODeviceWatcher (midiInFactory)); - - if (! inputDeviceWatcher->start()) - throw std::runtime_error ("Failed to start the midi input device watcher"); - - outputDeviceWatcher.reset (new MidiIODeviceWatcher (midiOutFactory)); - - if (! outputDeviceWatcher->start()) - throw std::runtime_error ("Failed to start the midi output device watcher"); - } - - Array getAvailableDevices (bool isInput) override - { - return isInput ? inputDeviceWatcher ->getAvailableDevices() - : outputDeviceWatcher->getAvailableDevices(); - } - - MidiDeviceInfo getDefaultDevice (bool isInput) override - { - return isInput ? inputDeviceWatcher ->getDefaultDevice() - : outputDeviceWatcher->getDefaultDevice(); - } - - MidiInput::Pimpl* createInputWrapper (MidiInput& input, const String& deviceIdentifier, MidiInputCallback& callback) override - { - return new WinRTInputWrapper (*this, input, deviceIdentifier, callback); - } - - MidiOutput::Pimpl* createOutputWrapper (const String& deviceIdentifier) override - { - return new WinRTOutputWrapper (*this, deviceIdentifier); - } - -private: - //============================================================================== - class DeviceCallbackHandler - { - public: - virtual ~DeviceCallbackHandler() {}; - - JUCE_COMCALL addDevice (IDeviceInformation*) = 0; - JUCE_COMCALL removeDevice (IDeviceInformationUpdate*) = 0; - JUCE_COMCALL updateDevice (IDeviceInformationUpdate*) = 0; - - bool attach (HSTRING deviceSelector, DeviceInformationKind infoKind) - { - auto* wrtWrapper = WinRTWrapper::getInstanceWithoutCreating(); - - if (wrtWrapper == nullptr) - { - JUCE_WINRT_MIDI_LOG ("Failed to get the WinRTWrapper singleton!"); - return false; + disconnectListeners.call ([] (auto& x) { x.disconnected(); }); } - auto deviceInfoFactory = wrtWrapper->getWRLFactory (&RuntimeClass_Windows_Devices_Enumeration_DeviceInformation[0]); - - if (deviceInfoFactory == nullptr) - return false; - - // A quick way of getting an IVector... - auto requestedProperties = [wrtWrapper] + static void CALLBACK midiOutCallback (HMIDIOUT, + UINT wMsg, + DWORD_PTR dwInstance, + DWORD_PTR, + DWORD_PTR) { - auto devicePicker = wrtWrapper->activateInstance (&RuntimeClass_Windows_Devices_Enumeration_DevicePicker[0], - __uuidof (IDevicePicker)); - jassert (devicePicker != nullptr); + auto* collector = reinterpret_cast (dwInstance); - IVector* result; - auto hr = devicePicker->get_RequestedProperties (&result); - jassert (SUCCEEDED (hr)); + allOutputs().call ([&] (auto& l) + { + if (collector != &l) + return; - hr = result->Clear(); - jassert (SUCCEEDED (hr)); + switch (wMsg) + { + case MOM_CLOSE: + l.disconnected(); + break; + } + }); + } + static WaitFreeListeners& allOutputs() + { + static WaitFreeListeners result; return result; - }(); - - StringArray propertyKeys ("System.Devices.ContainerId", - "System.Devices.Aep.ContainerId", - "System.Devices.Aep.IsConnected"); - - for (auto& key : propertyKeys) - { - WinRTWrapper::ScopedHString hstr (key); - auto hr = requestedProperties->Append (hstr.get()); - - if (FAILED (hr)) - { - jassertfalse; - return false; - } } - ComSmartPtr> iter; - auto hr = requestedProperties->QueryInterface (__uuidof (IIterable), (void**) iter.resetAndGetPointerAddress()); + ump::EndpointId endpointId; + HMIDIOUT handle = nullptr; + ListenerList disconnectListeners; + ump::ToBytestreamConverter toBytestream { 4096 }; - if (FAILED (hr)) - { - jassertfalse; - return false; - } - - hr = deviceInfoFactory->CreateWatcherWithKindAqsFilterAndAdditionalProperties (deviceSelector, iter, infoKind, - watcher.resetAndGetPointerAddress()); - - if (FAILED (hr)) - { - jassertfalse; - return false; - } - - enumerationThread.startThread(); - - return true; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OutputDevice) }; - void detach() + class InputImplNative : public ump::Input::Impl::Native, + private ump::Consumer { - enumerationThread.stopThread (2000); - - if (watcher == nullptr) - return; - - auto hr = watcher->Stop(); - jassert (SUCCEEDED (hr)); - - if (deviceAddedToken.value != 0) + public: + InputImplNative (std::shared_ptr d, + ump::DisconnectionListener& l, + ump::PacketProtocol p, + Consumer& c) + : device (d), listener (l), consumer (c), converter (p) { - hr = watcher->remove_Added (deviceAddedToken); - jassert (SUCCEEDED (hr)); - deviceAddedToken.value = 0; + device->addConsumer (*this); + device->addDisconnectListener (listener); } - if (deviceUpdatedToken.value != 0) + ~InputImplNative() override { - hr = watcher->remove_Updated (deviceUpdatedToken); - jassert (SUCCEEDED (hr)); - deviceUpdatedToken.value = 0; + device->removeDisconnectListener (listener); + device->removeConsumer (*this); } - if (deviceRemovedToken.value != 0) + ump::EndpointId getEndpointId() const override { - hr = watcher->remove_Removed (deviceRemovedToken); - jassert (SUCCEEDED (hr)); - deviceRemovedToken.value = 0; + return device->getEndpointId(); } - watcher = nullptr; - } + ump::PacketProtocol getProtocol() const override + { + return converter.getProtocol(); + } - template - IInspectable* getValueFromDeviceInfo (String key, InfoType* info) + private: + void consume (ump::Iterator b, ump::Iterator e, double time) override + { + converter.convert (b, e, [&] (ump::View v) + { + const ump::Iterator iter { v.data(), v.size() }; + consumer.consume (iter, std::next (iter), time); + }); + } + + std::shared_ptr device; + ump::DisconnectionListener& listener; + Consumer& consumer; + ump::GenericUMPConverter converter; + }; + + class SessionImplNative : public ump::Session::Impl::Native { - __FIMapView_2_HSTRING_IInspectable* properties; - info->get_Properties (&properties); + public: + explicit SessionImplNative (const String& x) : name (x) {} - boolean found = false; - WinRTWrapper::ScopedHString keyHstr (key); - auto hr = properties->HasKey (keyHstr.get(), &found); - - if (FAILED (hr)) + String getName() const override { - jassertfalse; - return nullptr; + return name; } - if (! found) - return nullptr; - - IInspectable* inspectable; - hr = properties->Lookup (keyHstr.get(), &inspectable); - - if (FAILED (hr)) + std::unique_ptr connectInput (ump::DisconnectionListener& listener, + const ump::EndpointId& endpoint, + ump::PacketProtocol protocol, + ump::Consumer& consumer) override { - jassertfalse; - return nullptr; + auto device = InputDevice::open (endpoint); + + if (device == nullptr) + return {}; + + return std::make_unique (device, listener, protocol, consumer); } - return inspectable; - } + std::unique_ptr connectOutput (ump::DisconnectionListener& listener, + const ump::EndpointId& endpoint) override + { + class Result : public ump::Output::Impl::Native + { + public: + Result (std::shared_ptr d, ump::DisconnectionListener& l) + : device (d), listener (l) + { + device->addDisconnectListener (listener); + } - String getGUIDFromInspectable (IInspectable& inspectable) + ~Result() override + { + device->removeDisconnectListener (listener); + } + + ump::EndpointId getEndpointId() const override { return device->getEndpointId(); } + bool send (ump::Iterator b, ump::Iterator e) override { return device->send (b, e); } + + private: + std::shared_ptr device; + ump::DisconnectionListener& listener; + }; + + auto device = OutputDevice::open (endpoint); + + if (device == nullptr) + return {}; + + return std::make_unique (device, listener); + } + + private: + String name; + }; + + class EndpointsImplNative : public ump::Endpoints::Impl::Native { - ComSmartPtr> guidRef; - auto hr = inspectable.QueryInterface (__uuidof (IReference), - (void**) guidRef.resetAndGetPointerAddress()); - - if (FAILED (hr)) + public: + ump::Backend getBackend() const override { - jassertfalse; + return ump::Backend::winmm; + } + + bool isVirtualMidiUmpServiceActive() const override + { + return false; + } + + bool isVirtualMidiBytestreamServiceActive() const override + { + return false; + } + + void getEndpoints (std::vector& storage) const override + { + std::transform (cachedEndpoints.begin(), + cachedEndpoints.end(), + std::back_inserter (storage), + [] (auto& p) { return p.first; }); + } + + std::optional getEndpoint (const ump::EndpointId& x) const override + { + const auto iter = cachedEndpoints.find (x); + + if (iter != cachedEndpoints.end()) + return iter->second.endpoint; + return {}; } - GUID result; - hr = guidRef->get_Value (&result); - - if (FAILED (hr)) + std::optional getStaticDeviceInfo (const ump::EndpointId& x) const override { - jassertfalse; + const auto iter = cachedEndpoints.find (x); + + if (iter != cachedEndpoints.end()) + return iter->second.info; + return {}; } - OLECHAR* resultString; - StringFromCLSID (result, &resultString); - - return resultString; - } - - bool getBoolFromInspectable (IInspectable& inspectable) - { - ComSmartPtr> boolRef; - auto hr = inspectable.QueryInterface (__uuidof (IReference), - (void**) boolRef.resetAndGetPointerAddress()); - - if (FAILED (hr)) + std::unique_ptr makeSession (const String& x) override { - jassertfalse; - return false; + return std::make_unique (x); } - boolean result; - hr = boolRef->get_Value (&result); - - if (FAILED (hr)) + static std::unique_ptr make (ump::EndpointsListener& listener) { - jassertfalse; - return false; + return rawToUniquePtr (new EndpointsImplNative { listener }); } - return result; - } - - private: - //============================================================================== - struct DeviceEnumerationThread final : public Thread - { - DeviceEnumerationThread (DeviceCallbackHandler& h, - ComSmartPtr& w, - EventRegistrationToken& added, - EventRegistrationToken& removed, - EventRegistrationToken& updated) - : Thread (SystemStats::getJUCEVersion() + ": WinRT Device Enumeration Thread"), handler (h), watcher (w), - deviceAddedToken (added), deviceRemovedToken (removed), deviceUpdatedToken (updated) - {} - - void run() override + private: + explicit EndpointsImplNative (ump::EndpointsListener& l) + : listener (l) { - auto handlerPtr = std::addressof (handler); - - watcher->add_Added ( - Callback> ( - [handlerPtr] (IDeviceWatcher*, IDeviceInformation* info) { return handlerPtr->addDevice (info); } - ).Get(), - &deviceAddedToken); - - watcher->add_Removed ( - Callback> ( - [handlerPtr] (IDeviceWatcher*, IDeviceInformationUpdate* infoUpdate) { return handlerPtr->removeDevice (infoUpdate); } - ).Get(), - &deviceRemovedToken); - - watcher->add_Updated ( - Callback> ( - [handlerPtr] (IDeviceWatcher*, IDeviceInformationUpdate* infoUpdate) { return handlerPtr->updateDevice (infoUpdate); } - ).Get(), - &deviceUpdatedToken); - - watcher->Start(); + updateCachedEndpoints(); } - DeviceCallbackHandler& handler; - ComSmartPtr& watcher; - EventRegistrationToken& deviceAddedToken, deviceRemovedToken, deviceUpdatedToken; + void updateCachedEndpoints() + { + JUCE_ASSERT_MESSAGE_THREAD; + + std::vector buffer; + getAvailableDevices (ITraits{}, buffer); + getAvailableDevices (OTraits{}, buffer); + + cachedEndpoints.clear(); + + for (auto& item : buffer) + cachedEndpoints.emplace (item.id, item); + + // If this is hit, we got an identical ID for an input and output device + jassert (cachedEndpoints.size() == buffer.size()); + } + + ump::EndpointsListener& listener; + std::map cachedEndpoints; + DeviceChangeDetector detector { L"JuceMidiDeviceDetector_", [&] + { + updateCachedEndpoints(); + listener.endpointsChanged(); + } }; }; - - //============================================================================== - ComSmartPtr watcher; - - EventRegistrationToken deviceAddedToken { 0 }, - deviceRemovedToken { 0 }, - deviceUpdatedToken { 0 }; - - DeviceEnumerationThread enumerationThread { *this, watcher, - deviceAddedToken, - deviceRemovedToken, - deviceUpdatedToken }; }; - - //============================================================================== - struct BLEDeviceWatcher final : private DeviceCallbackHandler - { - struct DeviceInfo - { - String containerID; - bool isConnected = false; - }; - - BLEDeviceWatcher() = default; - - ~BLEDeviceWatcher() - { - detach(); - } - - //============================================================================== - HRESULT addDevice (IDeviceInformation* addedDeviceInfo) override - { - HSTRING deviceIDHst; - auto hr = addedDeviceInfo->get_Id (&deviceIDHst); - - if (FAILED (hr)) - { - JUCE_WINRT_MIDI_LOG ("Failed to query added BLE device ID!"); - return S_OK; - } - - auto* wrtWrapper = WinRTWrapper::getInstanceWithoutCreating(); - - if (wrtWrapper == nullptr) - { - JUCE_WINRT_MIDI_LOG ("Failed to get the WinRTWrapper singleton!"); - return false; - } - - auto deviceID = wrtWrapper->hStringToString (deviceIDHst); - JUCE_WINRT_MIDI_LOG ("Detected paired BLE device: " << deviceID); - - if (auto* containerIDValue = getValueFromDeviceInfo ("System.Devices.Aep.ContainerId", addedDeviceInfo)) - { - auto containerID = getGUIDFromInspectable (*containerIDValue); - - if (containerID.isNotEmpty()) - { - DeviceInfo info = { containerID }; - - if (auto* connectedValue = getValueFromDeviceInfo ("System.Devices.Aep.IsConnected", addedDeviceInfo)) - info.isConnected = getBoolFromInspectable (*connectedValue); - - JUCE_WINRT_MIDI_LOG ("Adding BLE device: " << deviceID << " " << info.containerID - << " " << (info.isConnected ? "connected" : "disconnected")); - devices.set (deviceID, info); - - return S_OK; - } - } - - JUCE_WINRT_MIDI_LOG ("Failed to get a container ID for BLE device: " << deviceID); - return S_OK; - } - - HRESULT removeDevice (IDeviceInformationUpdate* removedDeviceInfo) override - { - HSTRING removedDeviceIdHstr; - auto hr = removedDeviceInfo->get_Id (&removedDeviceIdHstr); - - if (FAILED (hr)) - { - JUCE_WINRT_MIDI_LOG ("Failed to query removed BLE device ID!"); - return S_OK; - } - - auto* wrtWrapper = WinRTWrapper::getInstanceWithoutCreating(); - - if (wrtWrapper == nullptr) - { - JUCE_WINRT_MIDI_LOG ("Failed to get the WinRTWrapper singleton!"); - return false; - } - - auto removedDeviceId = wrtWrapper->hStringToString (removedDeviceIdHstr); - - JUCE_WINRT_MIDI_LOG ("Removing BLE device: " << removedDeviceId); - - { - const ScopedLock lock (deviceChanges); - - if (devices.contains (removedDeviceId)) - { - auto& info = devices.getReference (removedDeviceId); - listeners.call ([&info] (Listener& l) { l.bleDeviceDisconnected (info.containerID); }); - devices.remove (removedDeviceId); - JUCE_WINRT_MIDI_LOG ("Removed BLE device: " << removedDeviceId); - } - } - - return S_OK; - } - - HRESULT updateDevice (IDeviceInformationUpdate* updatedDeviceInfo) override - { - HSTRING updatedDeviceIdHstr; - auto hr = updatedDeviceInfo->get_Id (&updatedDeviceIdHstr); - - if (FAILED (hr)) - { - JUCE_WINRT_MIDI_LOG ("Failed to query updated BLE device ID!"); - return S_OK; - } - - auto* wrtWrapper = WinRTWrapper::getInstanceWithoutCreating(); - - if (wrtWrapper == nullptr) - { - JUCE_WINRT_MIDI_LOG ("Failed to get the WinRTWrapper singleton!"); - return false; - } - - auto updatedDeviceId = wrtWrapper->hStringToString (updatedDeviceIdHstr); - - JUCE_WINRT_MIDI_LOG ("Updating BLE device: " << updatedDeviceId); - - if (auto* connectedValue = getValueFromDeviceInfo ("System.Devices.Aep.IsConnected", updatedDeviceInfo)) - { - auto isConnected = getBoolFromInspectable (*connectedValue); - - { - const ScopedLock lock (deviceChanges); - - if (! devices.contains (updatedDeviceId)) - return S_OK; - - auto& info = devices.getReference (updatedDeviceId); - - if (info.isConnected && ! isConnected) - { - JUCE_WINRT_MIDI_LOG ("BLE device connection status change: " << updatedDeviceId << " " << info.containerID << " " << (isConnected ? "connected" : "disconnected")); - listeners.call ([&info] (Listener& l) { l.bleDeviceDisconnected (info.containerID); }); - } - - info.isConnected = isConnected; - } - } - - return S_OK; - } - - //============================================================================== - bool start() - { - WinRTWrapper::ScopedHString deviceSelector ("System.Devices.Aep.ProtocolId:=\"{bb7bb05e-5972-42b5-94fc-76eaa7084d49}\"" - " AND System.Devices.Aep.IsPaired:=System.StructuredQueryType.Boolean#True"); - return attach (deviceSelector.get(), DeviceInformationKind::DeviceInformationKind_AssociationEndpoint); - } - - //============================================================================== - struct Listener - { - virtual ~Listener() = default; - virtual void bleDeviceAdded (const String& containerID) = 0; - virtual void bleDeviceDisconnected (const String& containerID) = 0; - }; - - void addListener (Listener* l) - { - listeners.add (l); - } - - void removeListener (Listener* l) - { - listeners.remove (l); - } - - //============================================================================== - ThreadSafeListenerList listeners; - HashMap devices; - CriticalSection deviceChanges; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BLEDeviceWatcher); - }; - - //============================================================================== - struct WinRTMIDIDeviceInfo - { - String deviceID, containerID, name; - bool isDefault = false; - }; - - //============================================================================== - template - struct MidiIODeviceWatcher final : private DeviceCallbackHandler - { - MidiIODeviceWatcher (ComSmartPtr& comFactory) - : factory (comFactory) - { - } - - ~MidiIODeviceWatcher() - { - detach(); - } - - HRESULT addDevice (IDeviceInformation* addedDeviceInfo) override - { - WinRTMIDIDeviceInfo info; - - HSTRING deviceID; - auto hr = addedDeviceInfo->get_Id (&deviceID); - - if (FAILED (hr)) - { - JUCE_WINRT_MIDI_LOG ("Failed to query added MIDI device ID!"); - return S_OK; - } - - auto* wrtWrapper = WinRTWrapper::getInstanceWithoutCreating(); - - if (wrtWrapper == nullptr) - { - JUCE_WINRT_MIDI_LOG ("Failed to get the WinRTWrapper singleton!"); - return false; - } - - info.deviceID = wrtWrapper->hStringToString (deviceID); - - JUCE_WINRT_MIDI_LOG ("Detected MIDI device: " << info.deviceID); - - boolean isEnabled = false; - hr = addedDeviceInfo->get_IsEnabled (&isEnabled); - - if (FAILED (hr) || ! isEnabled) - { - JUCE_WINRT_MIDI_LOG ("MIDI device not enabled: " << info.deviceID); - return S_OK; - } - - // We use the container ID to match a MIDI device with a generic BLE device, if possible - if (auto* containerIDValue = getValueFromDeviceInfo ("System.Devices.ContainerId", addedDeviceInfo)) - info.containerID = getGUIDFromInspectable (*containerIDValue); - - HSTRING name; - hr = addedDeviceInfo->get_Name (&name); - - if (FAILED (hr)) - { - JUCE_WINRT_MIDI_LOG ("Failed to query detected MIDI device name for " << info.deviceID); - return S_OK; - } - - info.name = wrtWrapper->hStringToString (name); - - boolean isDefault = false; - hr = addedDeviceInfo->get_IsDefault (&isDefault); - - if (FAILED (hr)) - { - JUCE_WINRT_MIDI_LOG ("Failed to query detected MIDI device defaultness for " << info.deviceID << " " << info.name); - return S_OK; - } - - info.isDefault = isDefault; - - JUCE_WINRT_MIDI_LOG ("Adding MIDI device: " << info.deviceID << " " << info.containerID << " " << info.name); - - { - const ScopedLock lock (deviceChanges); - connectedDevices.add (info); - } - - return S_OK; - } - - HRESULT removeDevice (IDeviceInformationUpdate* removedDeviceInfo) override - { - HSTRING removedDeviceIdHstr; - auto hr = removedDeviceInfo->get_Id (&removedDeviceIdHstr); - - if (FAILED (hr)) - { - JUCE_WINRT_MIDI_LOG ("Failed to query removed MIDI device ID!"); - return S_OK; - } - - auto* wrtWrapper = WinRTWrapper::getInstanceWithoutCreating(); - - if (wrtWrapper == nullptr) - { - JUCE_WINRT_MIDI_LOG ("Failed to get the WinRTWrapper singleton!"); - return false; - } - - auto removedDeviceId = wrtWrapper->hStringToString (removedDeviceIdHstr); - - JUCE_WINRT_MIDI_LOG ("Removing MIDI device: " << removedDeviceId); - - { - const ScopedLock lock (deviceChanges); - - for (int i = 0; i < connectedDevices.size(); ++i) - { - if (connectedDevices[i].deviceID == removedDeviceId) - { - connectedDevices.remove (i); - JUCE_WINRT_MIDI_LOG ("Removed MIDI device: " << removedDeviceId); - break; - } - } - } - - return S_OK; - } - - // This is never called - HRESULT updateDevice (IDeviceInformationUpdate*) override { return S_OK; } - - bool start() - { - HSTRING deviceSelector; - auto hr = factory->GetDeviceSelector (&deviceSelector); - - if (FAILED (hr)) - { - JUCE_WINRT_MIDI_LOG ("Failed to get MIDI device selector!"); - return false; - } - - return attach (deviceSelector, DeviceInformationKind::DeviceInformationKind_DeviceInterface); - } - - Array getAvailableDevices() - { - { - const ScopedLock lock (deviceChanges); - lastQueriedConnectedDevices = connectedDevices; - } - - StringArray deviceNames, deviceIDs; - - for (auto info : lastQueriedConnectedDevices.get()) - { - deviceNames.add (info.name); - deviceIDs .add (info.containerID); - } - - deviceNames.appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 ("")); - deviceIDs .appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 ("")); - - Array devices; - - for (int i = 0; i < deviceNames.size(); ++i) - devices.add ({ deviceNames[i], deviceIDs[i] }); - - return devices; - } - - MidiDeviceInfo getDefaultDevice() - { - auto& lastDevices = lastQueriedConnectedDevices.get(); - - for (auto& d : lastDevices) - if (d.isDefault) - return { d.name, d.containerID }; - - return {}; - } - - WinRTMIDIDeviceInfo getWinRTDeviceInfoForDevice (const String& deviceIdentifier) - { - auto devices = getAvailableDevices(); - - for (int i = 0; i < devices.size(); ++i) - if (devices.getUnchecked (i).identifier == deviceIdentifier) - return lastQueriedConnectedDevices.get()[i]; - - return {}; - } - - ComSmartPtr& factory; - - Array connectedDevices; - CriticalSection deviceChanges; - ThreadLocalValue> lastQueriedConnectedDevices; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiIODeviceWatcher); - }; - - //============================================================================== - template - static void openMidiPortThread (String threadName, - String midiDeviceID, - ComSmartPtr& comFactory, - ComSmartPtr& comPort) - { - std::thread { [&] - { - Thread::setCurrentThreadName (threadName); - - const WinRTWrapper::ScopedHString hDeviceId { midiDeviceID }; - ComSmartPtr> asyncOp; - const auto hr = comFactory->FromIdAsync (hDeviceId.get(), asyncOp.resetAndGetPointerAddress()); - - if (FAILED (hr)) - return; - - std::promise> promise; - auto future = promise.get_future(); - - auto callback = [p = std::move (promise)] (IAsyncOperation* asyncOpPtr, AsyncStatus) mutable - { - if (asyncOpPtr == nullptr) - { - p.set_value (nullptr); - return E_ABORT; - } - - ComSmartPtr result; - const auto hr = asyncOpPtr->GetResults (result.resetAndGetPointerAddress()); - - if (FAILED (hr)) - { - p.set_value (nullptr); - return hr; - } - - p.set_value (std::move (result)); - return S_OK; - }; - - const auto ir = asyncOp->put_Completed (Callback> (std::move (callback)).Get()); - - if (FAILED (ir)) - return; - - if (future.wait_for (std::chrono::milliseconds (2000)) == std::future_status::ready) - comPort = future.get(); - } }.join(); - } - - //============================================================================== - template - class WinRTIOWrapper : private BLEDeviceWatcher::Listener - { - public: - WinRTIOWrapper (BLEDeviceWatcher& bleWatcher, - MidiIODeviceWatcher& midiDeviceWatcher, - const String& deviceIdentifier) - : bleDeviceWatcher (bleWatcher) - { - { - const ScopedLock lock (midiDeviceWatcher.deviceChanges); - deviceInfo = midiDeviceWatcher.getWinRTDeviceInfoForDevice (deviceIdentifier); - } - - if (deviceInfo.deviceID.isEmpty()) - throw std::runtime_error ("Invalid device index"); - - JUCE_WINRT_MIDI_LOG ("Creating JUCE MIDI IO: " << deviceInfo.deviceID); - - if (deviceInfo.containerID.isNotEmpty()) - { - bleDeviceWatcher.addListener (this); - - const ScopedLock lock (bleDeviceWatcher.deviceChanges); - - HashMap::Iterator iter (bleDeviceWatcher.devices); - - while (iter.next()) - { - if (iter.getValue().containerID == deviceInfo.containerID) - { - isBLEDevice = true; - break; - } - } - } - } - - virtual ~WinRTIOWrapper() - { - bleDeviceWatcher.removeListener (this); - - disconnect(); - } - - //============================================================================== - virtual void disconnect() - { - if (midiPort != nullptr) - { - if (isBLEDevice) - midiPort->Release(); - } - - midiPort = nullptr; - } - - private: - //============================================================================== - void bleDeviceAdded (const String& containerID) override - { - if (containerID == deviceInfo.containerID) - isBLEDevice = true; - } - - void bleDeviceDisconnected (const String& containerID) override - { - if (containerID == deviceInfo.containerID) - { - JUCE_WINRT_MIDI_LOG ("Disconnecting MIDI port from BLE disconnection: " << deviceInfo.deviceID - << " " << deviceInfo.containerID << " " << deviceInfo.name); - disconnect(); - } - } - - protected: - //============================================================================== - BLEDeviceWatcher& bleDeviceWatcher; - WinRTMIDIDeviceInfo deviceInfo; - bool isBLEDevice = false; - ComSmartPtr midiPort; - }; - - //============================================================================== - struct WinRTInputWrapper final : public MidiInput::Pimpl, - private WinRTIOWrapper - - { - WinRTInputWrapper (WinRTMidiService& service, MidiInput& input, const String& deviceIdentifier, MidiInputCallback& cb) - : WinRTIOWrapper (*service.bleDeviceWatcher, *service.inputDeviceWatcher, deviceIdentifier), - inputDevice (input), - callback (cb) - { - openMidiPortThread ("Open WinRT MIDI input port", deviceInfo.deviceID, service.midiInFactory, midiPort); - - if (midiPort == nullptr) - { - JUCE_WINRT_MIDI_LOG ("Timed out waiting for midi input port creation"); - return; - } - - startTime = Time::getMillisecondCounterHiRes(); - - auto hr = midiPort->add_MessageReceived ( - Callback> ( - [self = checkedReference] (IMidiInPort*, IMidiMessageReceivedEventArgs* args) - { - HRESULT hr = S_OK; - - self->access ([&hr, args] (auto* ptr) - { - if (ptr != nullptr) - hr = ptr->midiInMessageReceived (args); - }); - - return hr; - } - ).Get(), - &midiInMessageToken); - - if (FAILED (hr)) - { - JUCE_WINRT_MIDI_LOG ("Failed to set MIDI input callback"); - jassertfalse; - } - } - - ~WinRTInputWrapper() - { - checkedReference->clear(); - disconnect(); - } - - //============================================================================== - void start() override - { - if (! isStarted) - { - concatenator.reset(); - isStarted = true; - } - } - - void stop() override - { - if (isStarted) - { - isStarted = false; - concatenator.reset(); - } - } - - String getDeviceIdentifier() override { return deviceInfo.containerID; } - String getDeviceName() override { return deviceInfo.name; } - - //============================================================================== - void disconnect() override - { - stop(); - - if (midiPort != nullptr && midiInMessageToken.value != 0) - midiPort->remove_MessageReceived (midiInMessageToken); - - WinRTIOWrapper::disconnect(); - } - - //============================================================================== - HRESULT midiInMessageReceived (IMidiMessageReceivedEventArgs* args) - { - if (! isStarted) - return S_OK; - - ComSmartPtr message; - auto hr = args->get_Message (message.resetAndGetPointerAddress()); - - if (FAILED (hr)) - return hr; - - ComSmartPtr buffer; - hr = message->get_RawData (buffer.resetAndGetPointerAddress()); - - if (FAILED (hr)) - return hr; - - ComSmartPtr bufferByteAccess; - hr = buffer->QueryInterface (bufferByteAccess.resetAndGetPointerAddress()); - - if (FAILED (hr)) - return hr; - - uint8_t* bufferData = nullptr; - hr = bufferByteAccess->Buffer (&bufferData); - - if (FAILED (hr)) - return hr; - - uint32_t numBytes = 0; - hr = buffer->get_Length (&numBytes); - - if (FAILED (hr)) - return hr; - - ABI::Windows::Foundation::TimeSpan timespan; - hr = message->get_Timestamp (×pan); - - if (FAILED (hr)) - return hr; - - concatenator.pushMidiData (bufferData, numBytes, - convertTimeStamp (timespan.Duration), - &inputDevice, callback); - return S_OK; - } - - double convertTimeStamp (int64 timestamp) - { - auto millisecondsSinceStart = static_cast (timestamp) / 10000.0; - auto t = startTime + millisecondsSinceStart; - auto now = Time::getMillisecondCounterHiRes(); - - if (t > now) - { - if (t > now + 2.0) - startTime -= 1.0; - - t = now; - } - - return t * 0.001; - } - - //============================================================================== - MidiInput& inputDevice; - MidiInputCallback& callback; - - MidiDataConcatenator concatenator { 4096 }; - EventRegistrationToken midiInMessageToken { 0 }; - - double startTime = 0; - bool isStarted = false; - - std::shared_ptr> checkedReference = createCheckedReference (this); - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WinRTInputWrapper); - }; - - //============================================================================== - struct WinRTOutputWrapper final : public MidiOutput::Pimpl, - private WinRTIOWrapper - { - WinRTOutputWrapper (WinRTMidiService& service, const String& deviceIdentifier) - : WinRTIOWrapper (*service.bleDeviceWatcher, *service.outputDeviceWatcher, deviceIdentifier) - { - openMidiPortThread ("Open WinRT MIDI output port", deviceInfo.deviceID, service.midiOutFactory, midiPort); - - if (midiPort == nullptr) - throw std::runtime_error ("Timed out waiting for midi output port creation"); - - auto* wrtWrapper = WinRTWrapper::getInstanceWithoutCreating(); - - if (wrtWrapper == nullptr) - throw std::runtime_error ("Failed to get the WinRTWrapper singleton!"); - - auto bufferFactory = wrtWrapper->getWRLFactory (&RuntimeClass_Windows_Storage_Streams_Buffer[0]); - - if (bufferFactory == nullptr) - throw std::runtime_error ("Failed to create output buffer factory"); - - auto hr = bufferFactory->Create (static_cast (65536), buffer.resetAndGetPointerAddress()); - - if (FAILED (hr)) - throw std::runtime_error ("Failed to create output buffer"); - - hr = buffer->QueryInterface (bufferByteAccess.resetAndGetPointerAddress()); - - if (FAILED (hr)) - throw std::runtime_error ("Failed to get buffer byte access"); - - hr = bufferByteAccess->Buffer (&bufferData); - - if (FAILED (hr)) - throw std::runtime_error ("Failed to get buffer data pointer"); - } - - //============================================================================== - void sendMessageNow (const MidiMessage& message) override - { - if (midiPort == nullptr) - return; - - auto numBytes = message.getRawDataSize(); - auto hr = buffer->put_Length (numBytes); - - if (FAILED (hr)) - { - jassertfalse; - return; - } - - memcpy_s (bufferData, numBytes, message.getRawData(), numBytes); - midiPort->SendBuffer (buffer); - } - - String getDeviceIdentifier() override { return deviceInfo.containerID; } - String getDeviceName() override { return deviceInfo.name; } - - //============================================================================== - ComSmartPtr buffer; - ComSmartPtr bufferByteAccess; - uint8_t* bufferData = nullptr; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WinRTOutputWrapper); - }; - - ComSmartPtr midiInFactory; - ComSmartPtr midiOutFactory; - - std::unique_ptr> inputDeviceWatcher; - std::unique_ptr> outputDeviceWatcher; - std::unique_ptr bleDeviceWatcher; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WinRTMidiService) }; -#endif // JUCE_USE_WINRT_MIDI - -//============================================================================== -//============================================================================== -RTL_OSVERSIONINFOW getWindowsVersionInfo(); - -struct MidiService final : public DeletedAtShutdown +auto ump::Endpoints::Impl::Native::make (EndpointsListener& l) -> std::unique_ptr { - MidiService() - { - #if JUCE_USE_WINRT_MIDI - #if ! JUCE_FORCE_WINRT_MIDI - auto windowsVersionInfo = getWindowsVersionInfo(); - if (windowsVersionInfo.dwMajorVersion >= 10 && windowsVersionInfo.dwBuildNumber >= 17763) - #endif - { - try - { - internal.reset (new WinRTMidiService()); - return; - } - catch (std::runtime_error&) {} - } - #endif + #if JUCE_USE_WINDOWS_MIDI_SERVICES + if (auto ptr = MidiServices::makeEndpoints (l)) + return ptr; + #endif - internal.reset (new Win32MidiService()); - } + #if JUCE_USE_WINRT_MIDI + if (auto winrtSession = WindowsMidiHelpers::Winrt::EndpointsImplNative::make (l)) + return winrtSession; + #endif - ~MidiService() - { - clearSingletonInstance(); - } - - static MidiServiceType& getService() - { - jassert (getInstance()->internal != nullptr); - return *getInstance()->internal.get(); - } - - JUCE_DECLARE_SINGLETON_INLINE (MidiService, false) - -private: - std::unique_ptr internal; - DeviceChangeDetector detector { L"JuceMidiDeviceDetector_", [] - { - MidiDeviceListConnectionBroadcaster::get().notify(); - } }; -}; - -//============================================================================== -static int findDefaultDeviceIndex (const Array& available, const MidiDeviceInfo& defaultDevice) -{ - for (int i = 0; i < available.size(); ++i) - if (available.getUnchecked (i) == defaultDevice) - return i; - - return 0; -} - -Array MidiInput::getAvailableDevices() -{ - return MidiService::getService().getAvailableDevices (true); -} - -MidiDeviceInfo MidiInput::getDefaultDevice() -{ - return MidiService::getService().getDefaultDevice (true); -} - -std::unique_ptr MidiInput::openDevice (const String& deviceIdentifier, MidiInputCallback* callback) -{ - if (deviceIdentifier.isEmpty() || callback == nullptr) - return {}; - - std::unique_ptr in (new MidiInput ({}, deviceIdentifier)); - std::unique_ptr wrapper; - - try - { - wrapper.reset (MidiService::getService().createInputWrapper (*in, deviceIdentifier, *callback)); - } - catch (std::runtime_error&) - { - return {}; - } - - in->setName (wrapper->getDeviceName()); - in->internal = std::move (wrapper); - - return in; -} - -StringArray MidiInput::getDevices() -{ - StringArray deviceNames; - - for (auto& d : getAvailableDevices()) - deviceNames.add (d.name); - - return deviceNames; -} - -int MidiInput::getDefaultDeviceIndex() -{ - return findDefaultDeviceIndex (getAvailableDevices(), getDefaultDevice()); -} - -std::unique_ptr MidiInput::openDevice (int index, MidiInputCallback* callback) -{ - return openDevice (getAvailableDevices()[index].identifier, callback); -} - -MidiInput::MidiInput (const String& deviceName, const String& deviceIdentifier) - : deviceInfo (deviceName, deviceIdentifier) -{ -} - -MidiInput::~MidiInput() = default; - -void MidiInput::start() { internal->start(); } -void MidiInput::stop() { internal->stop(); } - -//============================================================================== -Array MidiOutput::getAvailableDevices() -{ - return MidiService::getService().getAvailableDevices (false); -} - -MidiDeviceInfo MidiOutput::getDefaultDevice() -{ - return MidiService::getService().getDefaultDevice (false); -} - -std::unique_ptr MidiOutput::openDevice (const String& deviceIdentifier) -{ - if (deviceIdentifier.isEmpty()) - return {}; - - std::unique_ptr wrapper; - - try - { - wrapper.reset (MidiService::getService().createOutputWrapper (deviceIdentifier)); - } - catch (std::runtime_error&) - { - return {}; - } - - std::unique_ptr out; - out.reset (new MidiOutput (wrapper->getDeviceName(), deviceIdentifier)); - - out->internal = std::move (wrapper); - - return out; -} - -StringArray MidiOutput::getDevices() -{ - StringArray deviceNames; - - for (auto& d : getAvailableDevices()) - deviceNames.add (d.name); - - return deviceNames; -} - -int MidiOutput::getDefaultDeviceIndex() -{ - return findDefaultDeviceIndex (getAvailableDevices(), getDefaultDevice()); -} - -std::unique_ptr MidiOutput::openDevice (int index) -{ - return openDevice (getAvailableDevices()[index].identifier); -} - -MidiOutput::~MidiOutput() -{ - stopBackgroundThread(); -} - -void MidiOutput::sendMessageNow (const MidiMessage& message) -{ - internal->sendMessageNow (message); -} - -MidiDeviceListConnection MidiDeviceListConnection::make (std::function cb) -{ - auto& broadcaster = MidiDeviceListConnectionBroadcaster::get(); - return { &broadcaster, broadcaster.add (std::move (cb)) }; + #if JUCE_FORCE_WINRT_MIDI + return nullptr; + #else + return WindowsMidiHelpers::Win32::EndpointsImplNative::make (l); + #endif } } // namespace juce diff --git a/modules/juce_audio_processors/format_types/juce_AU_Shared.h b/modules/juce_audio_processors/format_types/juce_AU_Shared.h index 63e3afd44c..28b3cee3dd 100644 --- a/modules/juce_audio_processors/format_types/juce_AU_Shared.h +++ b/modules/juce_audio_processors/format_types/juce_AU_Shared.h @@ -48,12 +48,6 @@ #define JUCE_APPLE_MIDI_EVENT_LIST_SUPPORTED 0 #endif -#include - -#if JUCE_APPLE_MIDI_EVENT_LIST_SUPPORTED - #include -#endif - namespace juce {