diff --git a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt index b37dce3358..28e590cf98 100644 --- a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt +++ b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt @@ -81,8 +81,10 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_basics/midi/ump/juce_UMP_test.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPacket.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPackets.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPBytesOnGroup.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPConversion.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPConverters.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPDeviceInfo.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPDispatcher.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPFactory.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPIterator.cpp" @@ -2153,8 +2155,10 @@ set_source_files_properties( "../../../../../modules/juce_audio_basics/midi/ump/juce_UMP_test.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPacket.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPackets.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPBytesOnGroup.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPConversion.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPConverters.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPDeviceInfo.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPDispatcher.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPFactory.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPIterator.cpp" diff --git a/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj index 740980086b..72ef6ca426 100644 --- a/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj @@ -2810,8 +2810,10 @@ + + diff --git a/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj.filters index 05e8c69bc7..a8c9a94b93 100644 --- a/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj.filters @@ -3624,12 +3624,18 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump diff --git a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj index 5bca301937..7977eb7601 100644 --- a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj @@ -2810,8 +2810,10 @@ + + diff --git a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters index bbe9e4d3bd..01cbaff928 100644 --- a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters @@ -3624,12 +3624,18 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump diff --git a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj index 9a9d85f9df..a9ac21a2a6 100644 --- a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj @@ -2810,8 +2810,10 @@ + + diff --git a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters index 751651b92f..f0f14261ef 100644 --- a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters @@ -3624,12 +3624,18 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump diff --git a/examples/Utilities/UnitTestsDemo.h b/examples/Utilities/UnitTestsDemo.h index d5e61e60db..e1547c2834 100644 --- a/examples/Utilities/UnitTestsDemo.h +++ b/examples/Utilities/UnitTestsDemo.h @@ -33,7 +33,8 @@ juce_audio_formats, juce_audio_processors, juce_audio_utils, juce_core, juce_cryptography, juce_data_structures, juce_dsp, juce_events, juce_graphics, juce_gui_basics, juce_gui_extra, - juce_opengl, juce_osc, juce_product_unlocking, juce_video + juce_opengl, juce_osc, juce_product_unlocking, juce_video, + juce_midi_ci exporters: xcode_mac, vs2022, linux_make, androidstudio, xcode_iphone moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1,JUCE_PLUGINHOST_VST3=1,JUCE_PLUGINHOST_LV2=1 diff --git a/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt b/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt index fadc56f02e..7baaeb0293 100644 --- a/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt +++ b/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt @@ -53,8 +53,10 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_basics/midi/ump/juce_UMP_test.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPacket.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPackets.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPBytesOnGroup.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPConversion.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPConverters.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPDeviceInfo.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPDispatcher.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPFactory.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPIterator.cpp" @@ -1825,8 +1827,10 @@ set_source_files_properties( "../../../../../modules/juce_audio_basics/midi/ump/juce_UMP_test.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPacket.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPackets.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPBytesOnGroup.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPConversion.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPConverters.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPDeviceInfo.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPDispatcher.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPFactory.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPIterator.cpp" diff --git a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj index 2011ffbc94..6d2b8c853a 100644 --- a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj +++ b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj @@ -2414,8 +2414,10 @@ + + diff --git a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters index 4de359f466..d197119a0b 100644 --- a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters +++ b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters @@ -3048,12 +3048,18 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump diff --git a/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt b/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt index d9744fc2ea..6fc432f974 100644 --- a/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt +++ b/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt @@ -86,8 +86,10 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_basics/midi/ump/juce_UMP_test.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPacket.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPackets.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPBytesOnGroup.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPConversion.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPConverters.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPDeviceInfo.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPDispatcher.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPFactory.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPIterator.cpp" @@ -2011,8 +2013,10 @@ set_source_files_properties( "../../../../../modules/juce_audio_basics/midi/ump/juce_UMP_test.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPacket.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPackets.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPBytesOnGroup.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPConversion.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPConverters.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPDeviceInfo.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPDispatcher.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPFactory.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPIterator.cpp" diff --git a/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj index 78bbd48ce2..da90fc4e0b 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj @@ -2594,8 +2594,10 @@ + + diff --git a/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj.filters index 92576361e7..9cf923552d 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj.filters @@ -3324,12 +3324,18 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump diff --git a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj index d440c2415f..f9c46ed0e8 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj @@ -2594,8 +2594,10 @@ + + diff --git a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters index b643e21287..fd4b79c96b 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters @@ -3324,12 +3324,18 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump diff --git a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj index 2de62435dd..c791268b0e 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj @@ -2594,8 +2594,10 @@ + + diff --git a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters index 15ccebaa3d..d25cd68b1d 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters @@ -3324,12 +3324,18 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump diff --git a/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt b/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt index a41825469d..735dd6c0bb 100644 --- a/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt +++ b/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt @@ -57,8 +57,10 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_basics/midi/ump/juce_UMP_test.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPacket.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPackets.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPBytesOnGroup.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPConversion.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPConverters.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPDeviceInfo.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPDispatcher.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPFactory.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPIterator.cpp" @@ -1909,8 +1911,10 @@ set_source_files_properties( "../../../../../modules/juce_audio_basics/midi/ump/juce_UMP_test.cpp" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPacket.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPackets.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPBytesOnGroup.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPConversion.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPConverters.h" + "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPDeviceInfo.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPDispatcher.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPFactory.h" "../../../../../modules/juce_audio_basics/midi/ump/juce_UMPIterator.cpp" diff --git a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj index 1b4a9df8ee..403f606d98 100644 --- a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj +++ b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj @@ -2505,8 +2505,10 @@ + + diff --git a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters index 710abcb7fe..32733727fa 100644 --- a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters +++ b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters @@ -3189,12 +3189,18 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump diff --git a/extras/Projucer/Source/Utility/Helpers/jucer_MiscUtilities.cpp b/extras/Projucer/Source/Utility/Helpers/jucer_MiscUtilities.cpp index e07bc65d46..0c03c0f7cb 100644 --- a/extras/Projucer/Source/Utility/Helpers/jucer_MiscUtilities.cpp +++ b/extras/Projucer/Source/Utility/Helpers/jucer_MiscUtilities.cpp @@ -282,7 +282,8 @@ StringArray getJUCEModules() noexcept "juce_opengl", "juce_osc", "juce_product_unlocking", - "juce_video" + "juce_video", + "juce_midi_ci" }; return juceModuleIds; diff --git a/extras/UnitTestRunner/Builds/LinuxMakefile/Makefile b/extras/UnitTestRunner/Builds/LinuxMakefile/Makefile index 2474404d9b..aec64b0ae3 100644 --- a/extras/UnitTestRunner/Builds/LinuxMakefile/Makefile +++ b/extras/UnitTestRunner/Builds/LinuxMakefile/Makefile @@ -39,7 +39,7 @@ ifeq ($(CONFIG),Debug) TARGET_ARCH := endif - JUCE_CPPFLAGS := $(DEPFLAGS) "-DLINUX=1" "-DDEBUG=1" "-D_DEBUG=1" "-DJUCE_DISPLAY_SPLASH_SCREEN=0" "-DJUCE_USE_DARK_SPLASH_SCREEN=1" "-DJUCE_PROJUCER_VERSION=0x70008" "-DJUCE_MODULE_AVAILABLE_juce_analytics=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_basics=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_devices=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_formats=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_processors=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_utils=1" "-DJUCE_MODULE_AVAILABLE_juce_core=1" "-DJUCE_MODULE_AVAILABLE_juce_cryptography=1" "-DJUCE_MODULE_AVAILABLE_juce_data_structures=1" "-DJUCE_MODULE_AVAILABLE_juce_dsp=1" "-DJUCE_MODULE_AVAILABLE_juce_events=1" "-DJUCE_MODULE_AVAILABLE_juce_graphics=1" "-DJUCE_MODULE_AVAILABLE_juce_gui_basics=1" "-DJUCE_MODULE_AVAILABLE_juce_gui_extra=1" "-DJUCE_MODULE_AVAILABLE_juce_opengl=1" "-DJUCE_MODULE_AVAILABLE_juce_osc=1" "-DJUCE_MODULE_AVAILABLE_juce_product_unlocking=1" "-DJUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1" "-DJUCE_PLUGINHOST_VST3=1" "-DJUCE_PLUGINHOST_LV2=1" "-DJUCE_STRICT_REFCOUNTEDPOINTER=1" "-DJUCE_STANDALONE_APPLICATION=1" "-DJUCE_UNIT_TESTS=1" "-DJUCER_LINUX_MAKE_6D53C8B4=1" "-DJUCE_APP_VERSION=1.0.0" "-DJUCE_APP_VERSION_HEX=0x10000" $(shell $(PKG_CONFIG) --cflags alsa freetype2 gl libcurl webkit2gtk-4.0 gtk+-x11-3.0) -pthread -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/lilv/src -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/lilv -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/sratom -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/sord/src -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/sord -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/serd -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/lv2 -I../../../../modules/juce_audio_processors/format_types/LV2_SDK -I../../../../modules/juce_audio_processors/format_types/VST3_SDK -I../../JuceLibraryCode -I../../../../modules $(CPPFLAGS) + JUCE_CPPFLAGS := $(DEPFLAGS) "-DLINUX=1" "-DDEBUG=1" "-D_DEBUG=1" "-DJUCE_DISPLAY_SPLASH_SCREEN=0" "-DJUCE_USE_DARK_SPLASH_SCREEN=1" "-DJUCE_PROJUCER_VERSION=0x70008" "-DJUCE_MODULE_AVAILABLE_juce_analytics=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_basics=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_devices=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_formats=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_processors=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_utils=1" "-DJUCE_MODULE_AVAILABLE_juce_core=1" "-DJUCE_MODULE_AVAILABLE_juce_cryptography=1" "-DJUCE_MODULE_AVAILABLE_juce_data_structures=1" "-DJUCE_MODULE_AVAILABLE_juce_dsp=1" "-DJUCE_MODULE_AVAILABLE_juce_events=1" "-DJUCE_MODULE_AVAILABLE_juce_graphics=1" "-DJUCE_MODULE_AVAILABLE_juce_gui_basics=1" "-DJUCE_MODULE_AVAILABLE_juce_gui_extra=1" "-DJUCE_MODULE_AVAILABLE_juce_midi_ci=1" "-DJUCE_MODULE_AVAILABLE_juce_opengl=1" "-DJUCE_MODULE_AVAILABLE_juce_osc=1" "-DJUCE_MODULE_AVAILABLE_juce_product_unlocking=1" "-DJUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1" "-DJUCE_PLUGINHOST_VST3=1" "-DJUCE_PLUGINHOST_LV2=1" "-DJUCE_STRICT_REFCOUNTEDPOINTER=1" "-DJUCE_STANDALONE_APPLICATION=1" "-DJUCE_UNIT_TESTS=1" "-DJUCER_LINUX_MAKE_6D53C8B4=1" "-DJUCE_APP_VERSION=1.0.0" "-DJUCE_APP_VERSION_HEX=0x10000" $(shell $(PKG_CONFIG) --cflags alsa freetype2 gl libcurl webkit2gtk-4.0 gtk+-x11-3.0) -pthread -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/lilv/src -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/lilv -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/sratom -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/sord/src -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/sord -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/serd -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/lv2 -I../../../../modules/juce_audio_processors/format_types/LV2_SDK -I../../../../modules/juce_audio_processors/format_types/VST3_SDK -I../../JuceLibraryCode -I../../../../modules $(CPPFLAGS) JUCE_CPPFLAGS_CONSOLEAPP := "-DJucePlugin_Build_VST=0" "-DJucePlugin_Build_VST3=0" "-DJucePlugin_Build_AU=0" "-DJucePlugin_Build_AUv3=0" "-DJucePlugin_Build_AAX=0" "-DJucePlugin_Build_Standalone=0" "-DJucePlugin_Build_Unity=0" "-DJucePlugin_Build_LV2=0" JUCE_TARGET_CONSOLEAPP := UnitTestRunner @@ -60,7 +60,7 @@ ifeq ($(CONFIG),Release) TARGET_ARCH := endif - JUCE_CPPFLAGS := $(DEPFLAGS) "-DLINUX=1" "-DNDEBUG=1" "-DJUCE_DISPLAY_SPLASH_SCREEN=0" "-DJUCE_USE_DARK_SPLASH_SCREEN=1" "-DJUCE_PROJUCER_VERSION=0x70008" "-DJUCE_MODULE_AVAILABLE_juce_analytics=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_basics=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_devices=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_formats=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_processors=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_utils=1" "-DJUCE_MODULE_AVAILABLE_juce_core=1" "-DJUCE_MODULE_AVAILABLE_juce_cryptography=1" "-DJUCE_MODULE_AVAILABLE_juce_data_structures=1" "-DJUCE_MODULE_AVAILABLE_juce_dsp=1" "-DJUCE_MODULE_AVAILABLE_juce_events=1" "-DJUCE_MODULE_AVAILABLE_juce_graphics=1" "-DJUCE_MODULE_AVAILABLE_juce_gui_basics=1" "-DJUCE_MODULE_AVAILABLE_juce_gui_extra=1" "-DJUCE_MODULE_AVAILABLE_juce_opengl=1" "-DJUCE_MODULE_AVAILABLE_juce_osc=1" "-DJUCE_MODULE_AVAILABLE_juce_product_unlocking=1" "-DJUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1" "-DJUCE_PLUGINHOST_VST3=1" "-DJUCE_PLUGINHOST_LV2=1" "-DJUCE_STRICT_REFCOUNTEDPOINTER=1" "-DJUCE_STANDALONE_APPLICATION=1" "-DJUCE_UNIT_TESTS=1" "-DJUCER_LINUX_MAKE_6D53C8B4=1" "-DJUCE_APP_VERSION=1.0.0" "-DJUCE_APP_VERSION_HEX=0x10000" $(shell $(PKG_CONFIG) --cflags alsa freetype2 gl libcurl webkit2gtk-4.0 gtk+-x11-3.0) -pthread -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/lilv/src -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/lilv -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/sratom -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/sord/src -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/sord -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/serd -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/lv2 -I../../../../modules/juce_audio_processors/format_types/LV2_SDK -I../../../../modules/juce_audio_processors/format_types/VST3_SDK -I../../JuceLibraryCode -I../../../../modules $(CPPFLAGS) + JUCE_CPPFLAGS := $(DEPFLAGS) "-DLINUX=1" "-DNDEBUG=1" "-DJUCE_DISPLAY_SPLASH_SCREEN=0" "-DJUCE_USE_DARK_SPLASH_SCREEN=1" "-DJUCE_PROJUCER_VERSION=0x70008" "-DJUCE_MODULE_AVAILABLE_juce_analytics=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_basics=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_devices=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_formats=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_processors=1" "-DJUCE_MODULE_AVAILABLE_juce_audio_utils=1" "-DJUCE_MODULE_AVAILABLE_juce_core=1" "-DJUCE_MODULE_AVAILABLE_juce_cryptography=1" "-DJUCE_MODULE_AVAILABLE_juce_data_structures=1" "-DJUCE_MODULE_AVAILABLE_juce_dsp=1" "-DJUCE_MODULE_AVAILABLE_juce_events=1" "-DJUCE_MODULE_AVAILABLE_juce_graphics=1" "-DJUCE_MODULE_AVAILABLE_juce_gui_basics=1" "-DJUCE_MODULE_AVAILABLE_juce_gui_extra=1" "-DJUCE_MODULE_AVAILABLE_juce_midi_ci=1" "-DJUCE_MODULE_AVAILABLE_juce_opengl=1" "-DJUCE_MODULE_AVAILABLE_juce_osc=1" "-DJUCE_MODULE_AVAILABLE_juce_product_unlocking=1" "-DJUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1" "-DJUCE_PLUGINHOST_VST3=1" "-DJUCE_PLUGINHOST_LV2=1" "-DJUCE_STRICT_REFCOUNTEDPOINTER=1" "-DJUCE_STANDALONE_APPLICATION=1" "-DJUCE_UNIT_TESTS=1" "-DJUCER_LINUX_MAKE_6D53C8B4=1" "-DJUCE_APP_VERSION=1.0.0" "-DJUCE_APP_VERSION_HEX=0x10000" $(shell $(PKG_CONFIG) --cflags alsa freetype2 gl libcurl webkit2gtk-4.0 gtk+-x11-3.0) -pthread -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/lilv/src -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/lilv -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/sratom -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/sord/src -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/sord -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/serd -I../../../../modules/juce_audio_processors/format_types/LV2_SDK/lv2 -I../../../../modules/juce_audio_processors/format_types/LV2_SDK -I../../../../modules/juce_audio_processors/format_types/VST3_SDK -I../../JuceLibraryCode -I../../../../modules $(CPPFLAGS) JUCE_CPPFLAGS_CONSOLEAPP := "-DJucePlugin_Build_VST=0" "-DJucePlugin_Build_VST3=0" "-DJucePlugin_Build_AU=0" "-DJucePlugin_Build_AUv3=0" "-DJucePlugin_Build_AAX=0" "-DJucePlugin_Build_Standalone=0" "-DJucePlugin_Build_Unity=0" "-DJucePlugin_Build_LV2=0" JUCE_TARGET_CONSOLEAPP := UnitTestRunner @@ -89,6 +89,7 @@ OBJECTS_CONSOLEAPP := \ $(JUCE_OBJDIR)/include_juce_graphics_f817e147.o \ $(JUCE_OBJDIR)/include_juce_gui_basics_e3f79785.o \ $(JUCE_OBJDIR)/include_juce_gui_extra_6dee1c1a.o \ + $(JUCE_OBJDIR)/include_juce_midi_ci_1fda4092.o \ $(JUCE_OBJDIR)/include_juce_opengl_a8a032b.o \ $(JUCE_OBJDIR)/include_juce_osc_f3df604d.o \ $(JUCE_OBJDIR)/include_juce_product_unlocking_8278fcdc.o \ @@ -191,6 +192,11 @@ $(JUCE_OBJDIR)/include_juce_gui_extra_6dee1c1a.o: ../../JuceLibraryCode/include_ @echo "Compiling include_juce_gui_extra.cpp" $(V_AT)$(CXX) $(JUCE_CXXFLAGS) $(JUCE_CPPFLAGS_CONSOLEAPP) $(JUCE_CFLAGS_CONSOLEAPP) -o "$@" -c "$<" +$(JUCE_OBJDIR)/include_juce_midi_ci_1fda4092.o: ../../JuceLibraryCode/include_juce_midi_ci.cpp + -$(V_AT)mkdir -p $(@D) + @echo "Compiling include_juce_midi_ci.cpp" + $(V_AT)$(CXX) $(JUCE_CXXFLAGS) $(JUCE_CPPFLAGS_CONSOLEAPP) $(JUCE_CFLAGS_CONSOLEAPP) -o "$@" -c "$<" + $(JUCE_OBJDIR)/include_juce_opengl_a8a032b.o: ../../JuceLibraryCode/include_juce_opengl.cpp -$(V_AT)mkdir -p $(@D) @echo "Compiling include_juce_opengl.cpp" diff --git a/extras/UnitTestRunner/Builds/MacOSX/UnitTestRunner.xcodeproj/project.pbxproj b/extras/UnitTestRunner/Builds/MacOSX/UnitTestRunner.xcodeproj/project.pbxproj index b4dd3cd71e..c4c178dc8e 100644 --- a/extras/UnitTestRunner/Builds/MacOSX/UnitTestRunner.xcodeproj/project.pbxproj +++ b/extras/UnitTestRunner/Builds/MacOSX/UnitTestRunner.xcodeproj/project.pbxproj @@ -37,6 +37,7 @@ A70F7F4891DB1CF67653BE74 /* Accelerate.framework */ = {isa = PBXBuildFile; fileRef = B38A1AC42B002115350C0268; }; AA207299991F85938465BF65 /* Cocoa.framework */ = {isa = PBXBuildFile; fileRef = 2030A589A9355FE6A0F72428; }; AF1FE82A4A20DCB8944B35C7 /* include_juce_gui_extra.mm */ = {isa = PBXBuildFile; fileRef = 4195CB317C364D778AE2ADB1; }; + B407D123F08A9A8C12624ABA /* include_juce_midi_ci.cpp */ = {isa = PBXBuildFile; fileRef = 0EFA505235D959565503D537; }; BFED026CA071070CEB87CFB5 /* include_juce_audio_basics.mm */ = {isa = PBXBuildFile; fileRef = 4BD792956FE7C22CB8FB691D; }; D17BAE3D36BB94FC2C8E2438 /* Main.cpp */ = {isa = PBXBuildFile; fileRef = 88AA2B9840A6792BBAD559EE; }; D43289CF624A7B068237C192 /* include_juce_gui_basics.mm */ = {isa = PBXBuildFile; fileRef = 583EA0E5C4B75A629AEF1157; }; @@ -52,6 +53,7 @@ 05501801BF6C4A47598C59E2 /* juce_cryptography */ /* juce_cryptography */ = {isa = PBXFileReference; lastKnownFileType = folder; name = juce_cryptography; path = ../../../../modules/juce_cryptography; sourceTree = SOURCE_ROOT; }; 080EAB9CF5AB2BD6B2BBB173 /* ConsoleApp */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = UnitTestRunner; sourceTree = BUILT_PRODUCTS_DIR; }; 08ED235CBE02E0FB4BE4653E /* include_juce_cryptography.mm */ /* include_juce_cryptography.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = include_juce_cryptography.mm; path = ../../JuceLibraryCode/include_juce_cryptography.mm; sourceTree = SOURCE_ROOT; }; + 0EFA505235D959565503D537 /* include_juce_midi_ci.cpp */ /* include_juce_midi_ci.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = include_juce_midi_ci.cpp; path = ../../JuceLibraryCode/include_juce_midi_ci.cpp; sourceTree = SOURCE_ROOT; }; 1CA82C74AEC08421812BDCAC /* include_juce_opengl.mm */ /* include_juce_opengl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = include_juce_opengl.mm; path = ../../JuceLibraryCode/include_juce_opengl.mm; sourceTree = SOURCE_ROOT; }; 1DC921E6494548F5E73E1056 /* juce_graphics */ /* juce_graphics */ = {isa = PBXFileReference; lastKnownFileType = folder; name = juce_graphics; path = ../../../../modules/juce_graphics; sourceTree = SOURCE_ROOT; }; 2030A589A9355FE6A0F72428 /* Cocoa.framework */ /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; @@ -82,6 +84,7 @@ 88AA2B9840A6792BBAD559EE /* Main.cpp */ /* Main.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Main.cpp; path = ../../Source/Main.cpp; sourceTree = SOURCE_ROOT; }; 8C449538B266A891147103D6 /* IOKit.framework */ /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; }; 8EBA9CF0874619A8FA0B4E74 /* juce_osc */ /* juce_osc */ = {isa = PBXFileReference; lastKnownFileType = folder; name = juce_osc; path = ../../../../modules/juce_osc; sourceTree = SOURCE_ROOT; }; + 8EC828FBFEC92A64A135467C /* juce_midi_ci */ /* juce_midi_ci */ = {isa = PBXFileReference; lastKnownFileType = folder; name = juce_midi_ci; path = ../../../../modules/juce_midi_ci; sourceTree = SOURCE_ROOT; }; A40A2A0B2841A622C53047CD /* juce_audio_processors */ /* juce_audio_processors */ = {isa = PBXFileReference; lastKnownFileType = folder; name = juce_audio_processors; path = ../../../../modules/juce_audio_processors; sourceTree = SOURCE_ROOT; }; A59D9064C3A2D7EC3DC45420 /* include_juce_osc.cpp */ /* include_juce_osc.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = include_juce_osc.cpp; path = ../../JuceLibraryCode/include_juce_osc.cpp; sourceTree = SOURCE_ROOT; }; A76DD7182C290A9020C96CA7 /* include_juce_audio_formats.mm */ /* include_juce_audio_formats.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = include_juce_audio_formats.mm; path = ../../JuceLibraryCode/include_juce_audio_formats.mm; sourceTree = SOURCE_ROOT; }; @@ -171,6 +174,7 @@ EECBAA403D2D6AEEA8CB05EB, 583EA0E5C4B75A629AEF1157, 4195CB317C364D778AE2ADB1, + 0EFA505235D959565503D537, 1CA82C74AEC08421812BDCAC, A59D9064C3A2D7EC3DC45420, B96EC82EC3D2813B50386198, @@ -241,6 +245,7 @@ 1DC921E6494548F5E73E1056, DD849A04E38279B842EDE213, 2A163F48282EEE95B8A8BA7A, + 8EC828FBFEC92A64A135467C, CC27F53A76BFB2675D2683A1, 8EBA9CF0874619A8FA0B4E74, 748F996DD2778AD1442AECA6, @@ -327,6 +332,7 @@ A1A39E64F9E03EFFA10B0A10, D43289CF624A7B068237C192, AF1FE82A4A20DCB8944B35C7, + B407D123F08A9A8C12624ABA, 1D06F1A254F84A7AE3E90DF2, 7164274FE42C7EC423455E05, 1B09834E81EAF5BCB87FAAF4, @@ -416,6 +422,7 @@ "JUCE_MODULE_AVAILABLE_juce_graphics=1", "JUCE_MODULE_AVAILABLE_juce_gui_basics=1", "JUCE_MODULE_AVAILABLE_juce_gui_extra=1", + "JUCE_MODULE_AVAILABLE_juce_midi_ci=1", "JUCE_MODULE_AVAILABLE_juce_opengl=1", "JUCE_MODULE_AVAILABLE_juce_osc=1", "JUCE_MODULE_AVAILABLE_juce_product_unlocking=1", @@ -548,6 +555,7 @@ "JUCE_MODULE_AVAILABLE_juce_graphics=1", "JUCE_MODULE_AVAILABLE_juce_gui_basics=1", "JUCE_MODULE_AVAILABLE_juce_gui_extra=1", + "JUCE_MODULE_AVAILABLE_juce_midi_ci=1", "JUCE_MODULE_AVAILABLE_juce_opengl=1", "JUCE_MODULE_AVAILABLE_juce_osc=1", "JUCE_MODULE_AVAILABLE_juce_product_unlocking=1", diff --git a/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj b/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj index 8820b91f52..3a6505308e 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj +++ b/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj @@ -64,7 +64,7 @@ Disabled ProgramDatabase ..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sratom;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\serd;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lv2;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK;..\..\..\..\modules\juce_audio_processors\format_types\VST3_SDK;..\..\JuceLibraryCode;..\..\..\..\modules;%(AdditionalIncludeDirectories) - _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;DEBUG;_DEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2017_78A5024=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;DEBUG;_DEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_midi_ci=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2017_78A5024=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) MultiThreadedDebugDLL true NotUsing @@ -80,7 +80,7 @@ ..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sratom;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\serd;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lv2;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK;..\..\..\..\modules\juce_audio_processors\format_types\VST3_SDK;..\..\JuceLibraryCode;..\..\..\..\modules;%(AdditionalIncludeDirectories) - _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;DEBUG;_DEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2017_78A5024=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;DEBUG;_DEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_midi_ci=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2017_78A5024=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) $(OutDir)\UnitTestRunner.exe @@ -108,7 +108,7 @@ Full ..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sratom;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\serd;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lv2;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK;..\..\..\..\modules\juce_audio_processors\format_types\VST3_SDK;..\..\JuceLibraryCode;..\..\..\..\modules;%(AdditionalIncludeDirectories) - _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;NDEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2017_78A5024=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;NDEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_midi_ci=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2017_78A5024=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) MultiThreadedDLL true NotUsing @@ -124,7 +124,7 @@ ..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sratom;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\serd;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lv2;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK;..\..\..\..\modules\juce_audio_processors\format_types\VST3_SDK;..\..\JuceLibraryCode;..\..\..\..\modules;%(AdditionalIncludeDirectories) - _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;NDEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2017_78A5024=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;NDEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_midi_ci=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2017_78A5024=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) $(OutDir)\UnitTestRunner.exe @@ -2525,6 +2525,42 @@ true + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + true @@ -2631,6 +2667,7 @@ /bigobj %(AdditionalOptions) + @@ -2650,8 +2687,10 @@ + + @@ -3634,6 +3673,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj.filters b/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj.filters index 72f760eabd..b34259cec1 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj.filters +++ b/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj.filters @@ -623,6 +623,15 @@ {A4D76113-9EDC-DA60-D89B-5BACF7F1C426} + + {EC0A49B5-F336-1F4D-6C32-40E19BE1426F} + + + {39CDBE58-7B8F-B367-DAE9-BCA326A4C637} + + + {F64F0BED-92DA-A4AE-0A76-9AC4FC01C199} + {1A9221A3-E993-70B2-6EA2-8E1DB5FF646A} @@ -3196,6 +3205,42 @@ JUCE Modules\juce_gui_extra + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci + JUCE Modules\juce_opengl\opengl @@ -3334,6 +3379,9 @@ JUCE Library Code + + JUCE Library Code + JUCE Library Code @@ -3387,12 +3435,18 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump @@ -6339,6 +6393,99 @@ JUCE Modules\juce_gui_extra + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci + JUCE Modules\juce_opengl\geometry diff --git a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj index 3ad17aeea1..5678c08f3c 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj +++ b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj @@ -64,7 +64,7 @@ Disabled ProgramDatabase ..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sratom;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\serd;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lv2;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK;..\..\..\..\modules\juce_audio_processors\format_types\VST3_SDK;..\..\JuceLibraryCode;..\..\..\..\modules;%(AdditionalIncludeDirectories) - _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;DEBUG;_DEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2019_78A5026=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;DEBUG;_DEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_midi_ci=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2019_78A5026=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) MultiThreadedDebugDLL true NotUsing @@ -80,7 +80,7 @@ ..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sratom;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\serd;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lv2;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK;..\..\..\..\modules\juce_audio_processors\format_types\VST3_SDK;..\..\JuceLibraryCode;..\..\..\..\modules;%(AdditionalIncludeDirectories) - _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;DEBUG;_DEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2019_78A5026=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;DEBUG;_DEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_midi_ci=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2019_78A5026=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) $(OutDir)\UnitTestRunner.exe @@ -108,7 +108,7 @@ Full ..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sratom;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\serd;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lv2;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK;..\..\..\..\modules\juce_audio_processors\format_types\VST3_SDK;..\..\JuceLibraryCode;..\..\..\..\modules;%(AdditionalIncludeDirectories) - _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;NDEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2019_78A5026=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;NDEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_midi_ci=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2019_78A5026=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) MultiThreadedDLL true NotUsing @@ -124,7 +124,7 @@ ..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sratom;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\serd;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lv2;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK;..\..\..\..\modules\juce_audio_processors\format_types\VST3_SDK;..\..\JuceLibraryCode;..\..\..\..\modules;%(AdditionalIncludeDirectories) - _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;NDEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2019_78A5026=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;NDEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_midi_ci=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2019_78A5026=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) $(OutDir)\UnitTestRunner.exe @@ -2525,6 +2525,42 @@ true + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + true @@ -2631,6 +2667,7 @@ /bigobj %(AdditionalOptions) + @@ -2650,8 +2687,10 @@ + + @@ -3634,6 +3673,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters index 86023113e1..4a6a45d561 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters +++ b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters @@ -623,6 +623,15 @@ {A4D76113-9EDC-DA60-D89B-5BACF7F1C426} + + {EC0A49B5-F336-1F4D-6C32-40E19BE1426F} + + + {39CDBE58-7B8F-B367-DAE9-BCA326A4C637} + + + {F64F0BED-92DA-A4AE-0A76-9AC4FC01C199} + {1A9221A3-E993-70B2-6EA2-8E1DB5FF646A} @@ -3196,6 +3205,42 @@ JUCE Modules\juce_gui_extra + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci + JUCE Modules\juce_opengl\opengl @@ -3334,6 +3379,9 @@ JUCE Library Code + + JUCE Library Code + JUCE Library Code @@ -3387,12 +3435,18 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump @@ -6339,6 +6393,99 @@ JUCE Modules\juce_gui_extra + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci + JUCE Modules\juce_opengl\geometry diff --git a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj index 69c25cb54f..db19d5929c 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj +++ b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj @@ -64,7 +64,7 @@ Disabled ProgramDatabase ..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sratom;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\serd;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lv2;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK;..\..\..\..\modules\juce_audio_processors\format_types\VST3_SDK;..\..\JuceLibraryCode;..\..\..\..\modules;%(AdditionalIncludeDirectories) - _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;DEBUG;_DEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2022_78A503E=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;DEBUG;_DEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_midi_ci=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2022_78A503E=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) MultiThreadedDebugDLL true NotUsing @@ -80,7 +80,7 @@ ..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sratom;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\serd;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lv2;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK;..\..\..\..\modules\juce_audio_processors\format_types\VST3_SDK;..\..\JuceLibraryCode;..\..\..\..\modules;%(AdditionalIncludeDirectories) - _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;DEBUG;_DEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2022_78A503E=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;DEBUG;_DEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_midi_ci=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2022_78A503E=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) $(OutDir)\UnitTestRunner.exe @@ -108,7 +108,7 @@ Full ..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sratom;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\serd;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lv2;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK;..\..\..\..\modules\juce_audio_processors\format_types\VST3_SDK;..\..\JuceLibraryCode;..\..\..\..\modules;%(AdditionalIncludeDirectories) - _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;NDEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2022_78A503E=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;NDEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_midi_ci=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2022_78A503E=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) MultiThreadedDLL true NotUsing @@ -124,7 +124,7 @@ ..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lilv;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sratom;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord\src;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\sord;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\serd;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK\lv2;..\..\..\..\modules\juce_audio_processors\format_types\LV2_SDK;..\..\..\..\modules\juce_audio_processors\format_types\VST3_SDK;..\..\JuceLibraryCode;..\..\..\..\modules;%(AdditionalIncludeDirectories) - _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;NDEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2022_78A503E=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;_CONSOLE;WIN32;_WINDOWS;NDEBUG;JUCE_DISPLAY_SPLASH_SCREEN=0;JUCE_USE_DARK_SPLASH_SCREEN=1;JUCE_PROJUCER_VERSION=0x70008;JUCE_MODULE_AVAILABLE_juce_analytics=1;JUCE_MODULE_AVAILABLE_juce_audio_basics=1;JUCE_MODULE_AVAILABLE_juce_audio_devices=1;JUCE_MODULE_AVAILABLE_juce_audio_formats=1;JUCE_MODULE_AVAILABLE_juce_audio_processors=1;JUCE_MODULE_AVAILABLE_juce_audio_utils=1;JUCE_MODULE_AVAILABLE_juce_core=1;JUCE_MODULE_AVAILABLE_juce_cryptography=1;JUCE_MODULE_AVAILABLE_juce_data_structures=1;JUCE_MODULE_AVAILABLE_juce_dsp=1;JUCE_MODULE_AVAILABLE_juce_events=1;JUCE_MODULE_AVAILABLE_juce_graphics=1;JUCE_MODULE_AVAILABLE_juce_gui_basics=1;JUCE_MODULE_AVAILABLE_juce_gui_extra=1;JUCE_MODULE_AVAILABLE_juce_midi_ci=1;JUCE_MODULE_AVAILABLE_juce_opengl=1;JUCE_MODULE_AVAILABLE_juce_osc=1;JUCE_MODULE_AVAILABLE_juce_product_unlocking=1;JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1;JUCE_PLUGINHOST_VST3=1;JUCE_PLUGINHOST_LV2=1;JUCE_STRICT_REFCOUNTEDPOINTER=1;JUCE_STANDALONE_APPLICATION=1;JUCE_UNIT_TESTS=1;JUCER_VS2022_78A503E=1;JUCE_APP_VERSION=1.0.0;JUCE_APP_VERSION_HEX=0x10000;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;JucePlugin_Build_LV2=0;%(PreprocessorDefinitions) $(OutDir)\UnitTestRunner.exe @@ -2525,6 +2525,42 @@ true + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + true @@ -2631,6 +2667,7 @@ /bigobj %(AdditionalOptions) + @@ -2650,8 +2687,10 @@ + + @@ -3634,6 +3673,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters index 5db23b33be..9261afe878 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters +++ b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters @@ -623,6 +623,15 @@ {A4D76113-9EDC-DA60-D89B-5BACF7F1C426} + + {EC0A49B5-F336-1F4D-6C32-40E19BE1426F} + + + {39CDBE58-7B8F-B367-DAE9-BCA326A4C637} + + + {F64F0BED-92DA-A4AE-0A76-9AC4FC01C199} + {1A9221A3-E993-70B2-6EA2-8E1DB5FF646A} @@ -3196,6 +3205,42 @@ JUCE Modules\juce_gui_extra + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci + JUCE Modules\juce_opengl\opengl @@ -3334,6 +3379,9 @@ JUCE Library Code + + JUCE Library Code + JUCE Library Code @@ -3387,12 +3435,18 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump @@ -6339,6 +6393,99 @@ JUCE Modules\juce_gui_extra + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\ci + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci\detail + + + JUCE Modules\juce_midi_ci + JUCE Modules\juce_opengl\geometry diff --git a/extras/UnitTestRunner/CMakeLists.txt b/extras/UnitTestRunner/CMakeLists.txt index 1291d6041f..444369adb4 100644 --- a/extras/UnitTestRunner/CMakeLists.txt +++ b/extras/UnitTestRunner/CMakeLists.txt @@ -42,6 +42,7 @@ target_link_libraries(UnitTestRunner PRIVATE juce::juce_analytics juce::juce_audio_utils juce::juce_dsp + juce::juce_midi_ci juce::juce_opengl juce::juce_osc juce::juce_product_unlocking diff --git a/extras/UnitTestRunner/JuceLibraryCode/JuceHeader.h b/extras/UnitTestRunner/JuceLibraryCode/JuceHeader.h index c1862eca8f..1ff2aae7db 100644 --- a/extras/UnitTestRunner/JuceLibraryCode/JuceHeader.h +++ b/extras/UnitTestRunner/JuceLibraryCode/JuceHeader.h @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include diff --git a/extras/UnitTestRunner/JuceLibraryCode/include_juce_midi_ci.cpp b/extras/UnitTestRunner/JuceLibraryCode/include_juce_midi_ci.cpp new file mode 100644 index 0000000000..53e9c79eee --- /dev/null +++ b/extras/UnitTestRunner/JuceLibraryCode/include_juce_midi_ci.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/extras/UnitTestRunner/UnitTestRunner.jucer b/extras/UnitTestRunner/UnitTestRunner.jucer index 2bdf1e1260..65ff0cf5da 100644 --- a/extras/UnitTestRunner/UnitTestRunner.jucer +++ b/extras/UnitTestRunner/UnitTestRunner.jucer @@ -34,6 +34,7 @@ + @@ -59,6 +60,7 @@ + @@ -85,6 +87,7 @@ + @@ -111,6 +114,7 @@ + @@ -137,6 +141,7 @@ + @@ -155,6 +160,7 @@ + + + diff --git a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_StaticLibrary.vcxproj.filters b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_StaticLibrary.vcxproj.filters index 4f71f79c7c..34b48d7964 100644 --- a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_StaticLibrary.vcxproj.filters +++ b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_StaticLibrary.vcxproj.filters @@ -3156,12 +3156,18 @@ JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump JUCE Modules\juce_audio_basics\midi\ump + + JUCE Modules\juce_audio_basics\midi\ump + JUCE Modules\juce_audio_basics\midi\ump diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index 68330694c3..cf267f3966 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -40,6 +40,7 @@ juce_add_modules( juce_graphics juce_gui_basics juce_gui_extra + juce_midi_ci juce_opengl juce_osc juce_product_unlocking diff --git a/modules/juce_audio_basics/juce_audio_basics.h b/modules/juce_audio_basics/juce_audio_basics.h index f2fd112bf6..47cd8df065 100644 --- a/modules/juce_audio_basics/juce_audio_basics.h +++ b/modules/juce_audio_basics/juce_audio_basics.h @@ -124,3 +124,10 @@ JUCE_END_IGNORE_WARNINGS_MSVC #include "synthesisers/juce_Synthesiser.h" #include "audio_play_head/juce_AudioPlayHead.h" #include "utilities/juce_AudioWorkgroup.h" +#include "midi/ump/juce_UMPBytesOnGroup.h" +#include "midi/ump/juce_UMPDeviceInfo.h" + +namespace juce +{ + namespace ump = universal_midi_packets; +} diff --git a/modules/juce_audio_basics/midi/juce_MidiMessage.cpp b/modules/juce_audio_basics/midi/juce_MidiMessage.cpp index 876760c532..2550db26d8 100644 --- a/modules/juce_audio_basics/midi/juce_MidiMessage.cpp +++ b/modules/juce_audio_basics/midi/juce_MidiMessage.cpp @@ -685,6 +685,11 @@ MidiMessage MidiMessage::createSysExMessage (const void* sysexData, const int da return MidiMessage (m, dataSize + 2); } +MidiMessage MidiMessage::createSysExMessage (Span data) +{ + return createSysExMessage (data.data(), (int) data.size()); +} + const uint8* MidiMessage::getSysExData() const noexcept { return isSysEx() ? getRawData() + 1 : nullptr; diff --git a/modules/juce_audio_basics/midi/juce_MidiMessage.h b/modules/juce_audio_basics/midi/juce_MidiMessage.h index 5241d79489..298818df3c 100644 --- a/modules/juce_audio_basics/midi/juce_MidiMessage.h +++ b/modules/juce_audio_basics/midi/juce_MidiMessage.h @@ -218,6 +218,13 @@ public: */ int getSysExDataSize() const noexcept; + /** Returns a span that bounds the sysex body bytes contained in this message. */ + Span getSysExDataSpan() const noexcept + { + return { reinterpret_cast (getSysExData()), + (size_t) getSysExDataSize() }; + } + //============================================================================== /** Returns true if this message is a 'key-down' event. @@ -855,6 +862,10 @@ public: static MidiMessage createSysExMessage (const void* sysexData, int dataSize); + /** Creates a system-exclusive message. + The data passed in is wrapped with header and tail bytes of 0xf0 and 0xf7. + */ + static MidiMessage createSysExMessage (Span data); //============================================================================== #ifndef DOXYGEN diff --git a/modules/juce_audio_basics/midi/ump/juce_UMPBytesOnGroup.h b/modules/juce_audio_basics/midi/ump/juce_UMPBytesOnGroup.h new file mode 100644 index 0000000000..183acb454b --- /dev/null +++ b/modules/juce_audio_basics/midi/ump/juce_UMPBytesOnGroup.h @@ -0,0 +1,38 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +/** + Holds a UMP group, and a span of bytes that were received or are to be + sent on that group. Helpful when working with sysex messages. + + @tags{Audio} +*/ +struct BytesOnGroup +{ + uint8_t group{}; + Span bytes; +}; + +} // namespace juce::universal_midi_packets diff --git a/modules/juce_audio_basics/midi/ump/juce_UMPDeviceInfo.h b/modules/juce_audio_basics/midi/ump/juce_UMPDeviceInfo.h new file mode 100644 index 0000000000..943b7f48a6 --- /dev/null +++ b/modules/juce_audio_basics/midi/ump/juce_UMPDeviceInfo.h @@ -0,0 +1,58 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::universal_midi_packets +{ + +/** + Holds MIDI device info that may be required by certain UMP messages and + MIDI-CI messages. + + @tags{Audio} +*/ +struct DeviceInfo +{ + std::array manufacturer; ///< LSB first + std::array family; ///< LSB first + std::array modelNumber; ///< LSB first + std::array revision; + +private: + auto tie() const { return std::tie (manufacturer, family, modelNumber, revision); } + +public: + bool operator== (const DeviceInfo& other) const { return tie() == other.tie(); } + bool operator!= (const DeviceInfo& other) const { return tie() != other.tie(); } + + static constexpr auto marshallingVersion = std::nullopt; + + template + static auto serialise (Archive& archive, This& t) + { + return archive (named ("manufacturer", t.manufacturer), + named ("family", t.family), + named ("modelNumber", t.modelNumber), + named ("revision", t.revision)); + } +}; + +} // namespace juce::universal_midi_packets diff --git a/modules/juce_midi_ci/ci/juce_CIChannelAddress.h b/modules/juce_midi_ci/ci/juce_CIChannelAddress.h new file mode 100644 index 0000000000..67b6d14ef9 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIChannelAddress.h @@ -0,0 +1,83 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +/** + Identifies a channel or set of channels in a multi-group MIDI endpoint. + + @tags{Audio} +*/ +class ChannelAddress +{ +private: + uint8_t group{}; ///< A group within a MIDI endpoint, where 0 <= group && group < 16 + ChannelInGroup channel{}; ///< A set of channels related to specified group + + auto tie() const { return std::tie (group, channel); } + +public: + /** Returns a copy of this object with the specified group. */ + [[nodiscard]] ChannelAddress withGroup (int g) const + { + jassert (isPositiveAndBelow (g, 16)); + return withMember (*this, &ChannelAddress::group, (uint8_t) g); + } + + /** Returns a copy of this object with the specified channel. */ + [[nodiscard]] ChannelAddress withChannel (ChannelInGroup c) const + { + return withMember (*this, &ChannelAddress::channel, c); + } + + /** Returns the group. */ + [[nodiscard]] uint8_t getGroup() const { return group; } + + /** Returns the channel in the group. */ + [[nodiscard]] ChannelInGroup getChannel() const { return channel; } + + /** Returns true if this address refers to all channels in the function + block containing the specified group. + */ + bool isBlock() const { return channel == ChannelInGroup::wholeBlock; } + + /** Returns true if this address refers to all channels in the specified + group. + */ + bool isGroup() const { return channel == ChannelInGroup::wholeGroup; } + + /** Returns true if this address refers to a single channel. */ + bool isSingleChannel() const { return ! isBlock() && ! isGroup(); } + + bool operator< (const ChannelAddress& other) const { return tie() < other.tie(); } + bool operator<= (const ChannelAddress& other) const { return tie() <= other.tie(); } + bool operator> (const ChannelAddress& other) const { return tie() > other.tie(); } + bool operator>= (const ChannelAddress& other) const { return tie() >= other.tie(); } + bool operator== (const ChannelAddress& other) const { return tie() == other.tie(); } + bool operator!= (const ChannelAddress& other) const { return ! operator== (other); } +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIDevice.cpp b/modules/juce_midi_ci/ci/juce_CIDevice.cpp new file mode 100644 index 0000000000..4f3c4f0db1 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIDevice.cpp @@ -0,0 +1,2369 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +class Device::Impl +{ + template + static auto getProfileHostImpl (This& t) { return t.profileHost.has_value() ? &*t.profileHost : nullptr; } + + template + static auto getPropertyHostImpl (This& t) { return t.propertyHost.has_value() ? &*t.propertyHost : nullptr; } + +public: + explicit Impl (const Options& opt) + : options (getValidated (opt)), + muid (getReallyRandomMuid()) + { + if (options.getFeatures().isProfileConfigurationSupported()) + profileHost.emplace (options.getFunctionBlock(), profileDelegate, concreteBufferOutput); + + if (options.getFeatures().isPropertyExchangeSupported()) + propertyHost.emplace (options.getFunctionBlock(), propertyDelegate, concreteBufferOutput, cacheProvider); + + outgoing.reserve (options.getMaxSysExSize()); + } + + ~Impl() + { + if (concreteBufferOutput.hasSentMuid()) + { + detail::MessageTypeUtils::send (concreteBufferOutput, + options.getFunctionBlock().firstGroup, + MUID::getBroadcast(), + ChannelInGroup::wholeBlock, + Message::InvalidateMUID { muid }); + } + } + + void sendDiscovery() + { + { + const auto aboutToRemove = std::move (discovered); + + for (const auto& pair : aboutToRemove) + listeners.call ([&] (auto& l) { l.deviceRemoved (pair.first); }); + } + + const Message::Header header + { + ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + muid, + MUID::getBroadcast(), + }; + + jassert (options.getOutputs().size() < 128); + + for (size_t i = 0; i < options.getOutputs().size(); ++i) + { + const Message::Discovery discovery + { + options.getDeviceInfo(), + options.getFeatures().getSupportedCapabilities(), + uint32_t (options.getMaxSysExSize()), + std::byte (i % 128), + }; + + outgoing.clear(); + detail::Marshalling::Writer { outgoing } (header, discovery); + options.getOutputs()[i]->processMessage ({ options.getFunctionBlock().firstGroup, outgoing }); + } + } + + void sendEndpointInquiry (MUID destination, Message::EndpointInquiry endpoint) + { + detail::MessageTypeUtils::send (concreteBufferOutput, + options.getFunctionBlock().firstGroup, + destination, + ChannelInGroup::wholeBlock, + endpoint); + } + + void sendProfileInquiry (MUID receiver, ChannelInGroup address) + { + if (! supportsProfiles (receiver)) + return; + + detail::MessageTypeUtils::send (concreteBufferOutput, + options.getFunctionBlock().firstGroup, + receiver, + address, + Message::ProfileInquiry{}); + } + + void sendProfileDetailsInquiry (MUID receiver, ChannelInGroup address, Profile profile, std::byte target) + { + if (! supportsProfiles (receiver)) + return; + + detail::MessageTypeUtils::send (concreteBufferOutput, + options.getFunctionBlock().firstGroup, + receiver, + address, + Message::ProfileDetails { profile, target }); + } + + void sendProfileSpecificData (MUID receiver, ChannelInGroup address, Profile profile, Span data) + { + if (! supportsProfiles (receiver)) + return; + + detail::MessageTypeUtils::send (concreteBufferOutput, + options.getFunctionBlock().firstGroup, + receiver, + address, + Message::ProfileSpecificData { profile, data }); + } + + void sendProfileEnablement (MUID m, ChannelInGroup address, Profile profile, int numChannels) + { + if (! supportsProfiles (m)) + return; + + // There are only 256 channels on a UMP endpoint, so requesting more probably doesn't make sense! + jassert (numChannels <= 256); + + if (numChannels > 0) + { + detail::MessageTypeUtils::send (concreteBufferOutput, + options.getFunctionBlock().firstGroup, + m, + address, + Message::ProfileOn { profile, (uint16_t) numChannels }); + } + else + { + detail::MessageTypeUtils::send (concreteBufferOutput, + options.getFunctionBlock().firstGroup, + m, + address, + Message::ProfileOff { profile }); + } + } + + void sendPropertyCapabilitiesInquiry (MUID m) + { + if (! supportsProperties (m)) + return; + + detail::MessageTypeUtils::send (concreteBufferOutput, + options.getFunctionBlock().firstGroup, + m, + ChannelInGroup::wholeBlock, + Message::PropertyExchangeCapabilities { std::byte { propertyDelegate.getNumSimultaneousRequestsSupported() }, {}, {} }); + } + + ErasedScopeGuard sendPropertyGetInquiry (MUID m, + const PropertyRequestHeader& propertyHeader, + std::function callback) + { + const auto iter = discovered.find (m); + + if (iter == discovered.end() || ! Features { iter->second.discovery.capabilities }.isPropertyExchangeSupported()) + return {}; + + auto primed = iter->second.initiatorPropertyCaches.primeCache (propertyDelegate.getNumSimultaneousRequestsSupported(), + std::move (callback), + detail::PropertyHostUtils::getTerminator (concreteBufferOutput, options.getFunctionBlock(), m)); + + if (! primed.isValid()) + return {}; + + detail::MessageTypeUtils::send (concreteBufferOutput, + options.getFunctionBlock().firstGroup, + m, + ChannelInGroup::wholeBlock, + Message::PropertyGetData { { primed.id, Encodings::jsonTo7BitText (propertyHeader.toVarCondensed()) } }); + + return std::move (primed.token); + } + + void sendPropertySetInquiry (MUID m, + const PropertyRequestHeader& propertyHeader, + Span propertyBody, + std::function callback) + { + const auto iter = discovered.find (m); + + if (iter == discovered.end() || ! Features { iter->second.discovery.capabilities }.isPropertyExchangeSupported()) + return; + + const auto encoded = Encodings::tryEncode (propertyBody, propertyHeader.mutualEncoding); + + if (! encoded.has_value()) + { + NullCheckedInvocation::invoke (callback, PropertyExchangeResult { PropertyExchangeResult::Error::invalidPayload }); + return; + } + + auto primed = iter->second.initiatorPropertyCaches.primeCache (propertyDelegate.getNumSimultaneousRequestsSupported(), + std::move (callback), + detail::PropertyHostUtils::getTerminator (concreteBufferOutput, options.getFunctionBlock(), m)); + + if (! primed.isValid()) + return; + + detail::PropertyHostUtils::send (concreteBufferOutput, + options.getFunctionBlock().firstGroup, + detail::MessageMeta::Meta::subID2, + m, + primed.id, + Encodings::jsonTo7BitText (propertyHeader.toVarCondensed()), + *encoded, + cacheProvider.getMaxSysexSizeForMuid (m)); + } + + void sendPropertySubscriptionStart (MUID m, + const PropertySubscriptionHeader& header, + std::function cb) + { + const auto resource = header.resource; + auto wrappedCallback = [this, m, resource, callback = std::move (cb)] (const PropertyExchangeResult& result) + { + if (! result.getError().has_value()) + { + const auto foundMuid = discovered.find (m); + + if (foundMuid != discovered.end()) + { + const auto parsed = result.getHeaderAsSubscriptionHeader(); + + // The responder should have given us a subscription ID so that we can reference the original subscription + // whenever we get updates in the future, or if we want to end the subscription. + jassert (parsed.subscribeId.isNotEmpty()); + const auto emplaceResult = foundMuid->second.subscriptions.insert ({ parsed.subscribeId, resource }); + + // If this fails, the device gave us a subscribeId that it was already using for another subscription. + jassertquiet (emplaceResult.second); + } + } + + NullCheckedInvocation::invoke (callback, result); + }; + + inquirePropertySubscribe (m, header, std::move (wrappedCallback)); + } + + void sendPropertySubscriptionEnd (MUID m, + const String& subscribeId, + std::function cb) + { + const auto iter = discovered.find (m); + + if (iter == discovered.end() || ! Features { iter->second.discovery.capabilities }.isPropertyExchangeSupported()) + { + // Trying to send a subscription message to a device that doesn't exist (maybe it got removed), or + // that doesn't support property exchange. + jassertfalse; + return; + } + + if (iter->second.subscriptions.count ({ subscribeId, {} }) == 0) + { + // Trying to end a subscription that doesn't exist - perhaps it already ended. + jassertfalse; + return; + } + + auto wrappedCallback = [this, m, subscribeId, callback = std::move (cb)] (const PropertyExchangeResult& result) + { + if (! result.getError().has_value()) + { + const auto foundMuid = discovered.find (m); + + if (foundMuid != discovered.end()) + foundMuid->second.subscriptions.erase ({ subscribeId, {} }); + } + + NullCheckedInvocation::invoke (callback, result); + }; + + PropertySubscriptionHeader header; + header.subscribeId = subscribeId; + header.command = PropertySubscriptionCommand::end; + inquirePropertySubscribe (m, header, std::move (wrappedCallback)); + } + + std::vector getOngoingSubscriptionsForMuid (MUID m) const + { + const auto iter = discovered.find (m); + + if (iter == discovered.end()) + return {}; + + std::vector result; + result.reserve (iter->second.subscriptions.size()); + + for (const auto& [subscribeId, resource] : iter->second.subscriptions) + result.push_back ({ subscribeId, resource }); + + return result; + } + + int countOngoingPropertyTransactions() const + { + return std::accumulate (discovered.begin(), + discovered.end(), + 0, + [] (auto acc, const auto& pair) + { + return acc + pair.second.initiatorPropertyCaches.countOngoingTransactions(); + }); + } + + void processMessage (ump::BytesOnGroup msg) + { + // Queried before the property host to unconditionally register capabilities of property exchange hosts. + FirstListener firstListener { this }; + LastListener lastListener { this }; + + ResponderDelegate* const l[] { &firstListener, + getProfileHostImpl (*this), + getPropertyHostImpl (*this), + &lastListener }; + + const auto status = detail::Responder::processCompleteMessage (concreteBufferOutput, msg, l); + + if (status == Parser::Status::collidingMUID) + { + muid = getReallyRandomMuid(); + concreteBufferOutput.resetSentMuid(); + sendDiscovery(); + } + } + + void addListener (Listener& l) + { + listeners.add (&l); + } + + void removeListener (Listener& l) + { + listeners.remove (&l); + } + + std::vector getDiscoveredMuids() const + { + std::vector result (discovered.size(), MUID::makeUnchecked (0)); + std::transform (discovered.begin(), discovered.end(), result.begin(), [] (const auto& p) { return p.first; }); + return result; + } + + std::optional getDiscoveryInfoForMuid (MUID m) const + { + const auto iter = discovered.find (m); + return iter != discovered.end() + ? std::optional (iter->second.discovery) + : std::nullopt; + } + + std::optional getNumPropertyExchangeRequestsSupportedForMuid (MUID m) const + { + const auto iter = discovered.find (m); + return iter != discovered.end() + ? std::optional ((int) iter->second.propertyExchangeResponse->numSimultaneousRequestsSupported) + : std::nullopt; + } + + const ChannelProfileStates* getProfileStateForMuid (MUID m, ChannelAddress address) const + { + const auto iter = discovered.find (m); + return iter != discovered.end() ? iter->second.profileStates.getStateForDestination (address) : nullptr; + } + + var getResourceListForMuid (MUID x) const + { + const auto iter = discovered.find (x); + return iter != discovered.end() ? iter->second.resourceList : var(); + } + + var getDeviceInfoForMuid (MUID x) const + { + const auto iter = discovered.find (x); + return iter != discovered.end() ? iter->second.deviceInfo : var(); + } + + var getChannelListForMuid (MUID x) const + { + const auto iter = discovered.find (x); + return iter != discovered.end() ? iter->second.channelList : var(); + } + + MUID getMuid() const { return muid; } + + Options getOptions() const { return options; } + + ProfileHost* getProfileHost() { return getProfileHostImpl (*this); } + const ProfileHost* getProfileHost() const { return getProfileHostImpl (*this); } + + PropertyHost* getPropertyHost() { return getPropertyHostImpl (*this); } + const PropertyHost* getPropertyHost() const { return getPropertyHostImpl (*this); } + +private: + class FirstListener : public ResponderDelegate + { + public: + explicit FirstListener (Impl* d) : device (d) {} + + bool tryRespond (ResponderOutput& output, const Message::Parsed& message) override + { + detail::MessageTypeUtils::visit (message, Visitor { device, &output }); + return false; + } + + private: + class Visitor : public detail::MessageTypeUtils::MessageVisitor + { + public: + Visitor (Impl* d, ResponderOutput* o) + : device (d), output (o) {} + + void visit (const Message::PropertyExchangeCapabilities& caps) const override { visitImpl (caps); } + void visit (const Message::PropertyExchangeCapabilitiesResponse& caps) const override { visitImpl (caps); } + using MessageVisitor::visit; + + private: + template + void visitImpl (const Body& t) const + { + const auto responderMUID = output->getIncomingHeader().source; + const auto iter = device->discovered.find (responderMUID); + + if (iter == device->discovered.end()) + return; + + iter->second.propertyExchangeResponse = Message::PropertyExchangeCapabilitiesResponse { t.numSimultaneousRequestsSupported, + t.majorVersion, + t.minorVersion }; + } + + Impl* device = nullptr; + ResponderOutput* output = nullptr; + }; + + Impl* device = nullptr; + }; + + class LastListener : public ResponderDelegate + { + public: + explicit LastListener (Impl* d) : device (d) {} + + bool tryRespond (ResponderOutput& output, const Message::Parsed& message) override + { + bool result = false; + detail::MessageTypeUtils::visit (message, Visitor { device, &output, &result }); + return result; + } + + private: + class Visitor : public detail::MessageTypeUtils::MessageVisitor + { + public: + Visitor (Impl* d, ResponderOutput* o, bool* b) + : device (d), output (o), handled (b) {} + + void visit (const Message::Discovery& x) const override { visitImpl (x); } + void visit (const Message::DiscoveryResponse& x) const override { visitImpl (x); } + void visit (const Message::InvalidateMUID& x) const override { visitImpl (x); } + void visit (const Message::EndpointInquiry& x) const override { visitImpl (x); } + void visit (const Message::EndpointInquiryResponse& x) const override { visitImpl (x); } + void visit (const Message::NAK& x) const override { visitImpl (x); } + void visit (const Message::ProfileInquiryResponse& x) const override { visitImpl (x); } + void visit (const Message::ProfileAdded& x) const override { visitImpl (x); } + void visit (const Message::ProfileRemoved& x) const override { visitImpl (x); } + void visit (const Message::ProfileEnabledReport& x) const override { visitImpl (x); } + void visit (const Message::ProfileDisabledReport& x) const override { visitImpl (x); } + void visit (const Message::ProfileDetailsResponse& x) const override { visitImpl (x); } + void visit (const Message::ProfileSpecificData& x) const override { visitImpl (x); } + void visit (const Message::PropertyExchangeCapabilitiesResponse& x) const override { visitImpl (x); } + void visit (const Message::PropertyGetDataResponse& x) const override { visitImpl (x); } + void visit (const Message::PropertySetDataResponse& x) const override { visitImpl (x); } + void visit (const Message::PropertySubscribe& x) const override { visitImpl (x); } + void visit (const Message::PropertySubscribeResponse& x) const override { visitImpl (x); } + void visit (const Message::PropertyNotify& x) const override { visitImpl (x); } + using MessageVisitor::visit; + + private: + template + void visitImpl (const Body& body) const { *handled = messageReceived (body); } + + bool messageReceived (const Message::Discovery& body) const + { + const auto replyPath = uint8_t (output->getIncomingHeader().version) >= 0x02 ? body.outputPathID : std::byte { 0x00 }; + + detail::MessageTypeUtils::send (*output, Message::DiscoveryResponse + { + device->options.getDeviceInfo(), + device->options.getFeatures().getSupportedCapabilities(), + uint32_t (device->options.getMaxSysExSize()), + replyPath, + device->options.getFunctionBlock().identifier, + }); + + // TODO(reuk) rather than sending a new discovery inquiry, we should store the details from the incoming message + const auto iter = device->discovered.find (output->getIncomingHeader().source); + + if (iter == device->discovered.end()) + { + const auto initiator = output->getIncomingHeader().source; + device->discovered.emplace (initiator, Discovered { body }); + device->listeners.call ([&] (auto& l) { l.deviceAdded (initiator); }); + device->sendEndpointInquiry (initiator, Message::EndpointInquiry { std::byte{} }); + } + + return true; + } + + bool messageReceived (const Message::DiscoveryResponse& response) const + { + const auto responderMUID = output->getIncomingHeader().source; + const auto iter = device->discovered.find (responderMUID); + + if (iter != device->discovered.end()) + { + device->discovered.erase (iter); + device->listeners.call ([&] (auto& l) { l.deviceRemoved (responderMUID); }); + + const Message::Header header + { + ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device->muid, + MUID::getBroadcast(), + }; + + detail::MessageTypeUtils::send (*output, output->getIncomingGroup(), header, Message::InvalidateMUID { responderMUID }); + } + else + { + const Message::Discovery discovery { response.device, + response.capabilities, + response.maximumSysexSize, + response.outputPathID }; + device->discovered.emplace (responderMUID, Discovered { discovery }); + device->listeners.call ([&] (auto& l) { l.deviceAdded (responderMUID); }); + device->sendEndpointInquiry (output->getIncomingHeader().source, Message::EndpointInquiry { std::byte{} }); + } + + return true; + } + + bool messageReceived (const Message::InvalidateMUID& invalidate) const + { + const auto targetMuid = invalidate.target; + const auto iter = device->discovered.find (targetMuid); + + if (iter != device->discovered.end()) + { + device->discovered.erase (iter); + device->listeners.call ([&] (auto& l) { l.deviceRemoved (targetMuid); }); + } + + if (invalidate.target != device->muid) + return false; + + device->muid = getReallyRandomMuid(); + device->concreteBufferOutput.resetSentMuid(); + device->sendDiscovery(); + + return true; + } + + bool messageReceived (const Message::EndpointInquiry& endpoint) const + { + // Only status 0 is defined at time of writing + if (endpoint.status == std::byte{}) + { + const auto& id = device->options.getProductInstanceId(); + const auto length = std::distance (id.begin(), std::find (id.begin(), id.end(), 0)); + + if (length <= 0) + return false; + + Message::EndpointInquiryResponse response; + response.status = endpoint.status; + response.data = Span (reinterpret_cast (id.data()), (size_t) length); + detail::MessageTypeUtils::send (*output, response); + return true; + } + + return false; + } + + bool messageReceived (const Message::EndpointInquiryResponse& endpoint) const + { + const auto responderMUID = output->getIncomingHeader().source; + const auto iter = device->discovered.find (responderMUID); + + if (iter == device->discovered.end()) + return false; // Got an endpoint response for a device we haven't discovered + + device->listeners.call ([&] (auto& l) { l.endpointReceived (responderMUID, endpoint); }); + return true; + } + + bool messageReceived (const Message::NAK& nak) const + { + const auto responderMUID = output->getIncomingHeader().source; + device->listeners.call ([&] (auto& l) { l.messageNotAcknowledged (responderMUID, nak); }); + return true; + } + + bool messageReceived (const Message::ProfileInquiryResponse& response) const + { + const auto responderMUID = output->getIncomingHeader().source; + const auto iter = device->discovered.find (responderMUID); + + if (iter == device->discovered.end()) + return false; + + const auto destination = output->getIncomingHeader().deviceID; + auto* state = iter->second.profileStates.getStateForDestination (output->getChannelAddress()); + + if (state == nullptr) + return false; + + ChannelProfileStates newState; + + for (auto& enabled : response.enabledProfiles) + newState.set (enabled, { 1, 1 }); + + for (auto& disabled : response.disabledProfiles) + newState.set (disabled, { 1, 0 }); + + *state = newState; + device->listeners.call ([&] (auto& l) { l.profileStateReceived (responderMUID, destination); }); + + return true; + } + + bool messageReceived (const Message::ProfileAdded& added) const + { + const auto responderMUID = output->getIncomingHeader().source; + const auto iter = device->discovered.find (responderMUID); + + if (iter == device->discovered.end()) + return false; + + const auto address = output->getChannelAddress(); + auto* state = iter->second.profileStates.getStateForDestination (address); + + if (state == nullptr) + return false; + + state->set (added.profile, { 1, 0 }); + device->listeners.call ([&] (auto& l) { l.profilePresenceChanged (responderMUID, address.getChannel(), added.profile, true); }); + + return true; + } + + bool messageReceived (const Message::ProfileRemoved& removed) const + { + const auto responderMUID = output->getIncomingHeader().source; + const auto iter = device->discovered.find (responderMUID); + + if (iter == device->discovered.end()) + return false; + + const auto address = output->getChannelAddress(); + auto* state = iter->second.profileStates.getStateForDestination (address); + + if (state == nullptr) + return false; + + state->erase (removed.profile); + device->listeners.call ([&] (auto& l) { l.profilePresenceChanged (responderMUID, address.getChannel(), removed.profile, false); }); + + return true; + } + + bool messageReceived (const Message::ProfileEnabledReport& x) const + { + const auto responderMUID = output->getIncomingHeader().source; + const auto iter = device->discovered.find (responderMUID); + + if (iter == device->discovered.end()) + return false; + + const auto address = output->getChannelAddress(); + auto* state = iter->second.profileStates.getStateForDestination (address); + + if (state == nullptr) + return false; + + const auto numChannels = jmax ((uint16_t) 1, x.numChannels); + + state->set (x.profile, { state->get (x.profile).supported, numChannels }); + device->listeners.call ([&] (auto& l) { l.profileEnablementChanged (responderMUID, address.getChannel(), x.profile, numChannels); }); + + return true; + } + + bool messageReceived (const Message::ProfileDisabledReport& x) const + { + const auto responderMUID = output->getIncomingHeader().source; + const auto iter = device->discovered.find (responderMUID); + + if (iter == device->discovered.end()) + return false; + + const auto address = output->getChannelAddress(); + auto* state = iter->second.profileStates.getStateForDestination (address); + + if (state == nullptr) + return false; + + state->set (x.profile, { state->get (x.profile).supported, 0 }); + device->listeners.call ([&] (auto& l) { l.profileEnablementChanged (responderMUID, address.getChannel(), x.profile, 0); }); + + return true; + } + + bool messageReceived (const Message::ProfileDetailsResponse& response) const + { + const auto responderMUID = output->getIncomingHeader().source; + const auto destination = output->getIncomingHeader().deviceID; + device->listeners.call ([&] (auto& l) { l.profileDetailsReceived (responderMUID, destination, response.profile, response.target, response.data); }); + + return true; + } + + bool messageReceived (const Message::ProfileSpecificData& data) const + { + const auto responderMUID = output->getIncomingHeader().source; + const auto destination = output->getIncomingHeader().deviceID; + device->listeners.call ([&] (auto& l) { l.profileSpecificDataReceived (responderMUID, destination, data.profile, data.data); }); + + return true; + } + + bool messageReceived (const Message::PropertyExchangeCapabilitiesResponse&) const + { + const auto source = output->getIncomingHeader().source; + const auto iter = device->discovered.find (source); + + constexpr auto hasResource = [] (var obj, auto resource) + { + if (auto* array = obj.getArray()) + for (const auto& item : *array) + if (item.isObject() && item.getProperty ("resource", {}) == var (resource)) + return true; + + return false; + }; + + const auto transaction = device->ongoingTransactions.emplace (device->ongoingTransactions.end()); + + const auto onResourceListReceived = [this, iter, source, hasResource, transaction] (const PropertyExchangeResult& result) + { + const auto validateResponse = [] (const PropertyExchangeResult& r) + { + const auto parsed = r.getHeaderAsReplyHeader(); + return ! r.getError().has_value() + && parsed.mediaType == PropertySubscriptionHeader().mediaType + && parsed.status == 200; + }; + + const auto allDone = [this, source, transaction] + { + device->ongoingTransactions.erase (transaction); + device->listeners.call ([source] (auto& l) { l.propertyExchangeCapabilitiesReceived (source); }); + }; + + if (! validateResponse (result)) + { + jassertfalse; + allDone(); + return; + } + + const auto bodyAsObj = Encodings::jsonFrom7BitText (result.getBody()); + iter->second.resourceList = bodyAsObj; + + const auto onChannelListReceived = [iter, allDone, validateResponse] (const PropertyExchangeResult& r) + { + if (validateResponse (r)) + iter->second.channelList = Encodings::jsonFrom7BitText (r.getBody()); + + allDone(); + return; + }; + + const auto getChannelList = [this, bodyAsObj, source, allDone, hasResource, onChannelListReceived, transaction] + { + if (hasResource (bodyAsObj, "ChannelList")) + { + PropertyRequestHeader header; + header.resource = "ChannelList"; + *transaction = device->sendPropertyGetInquiry (source, header, onChannelListReceived); + return; + } + + allDone(); + return; + }; + + if (hasResource (bodyAsObj, "DeviceInfo")) + { + PropertyRequestHeader header; + header.resource = "DeviceInfo"; + *transaction = device->sendPropertyGetInquiry (source, + header, + [iter, getChannelList, validateResponse] (const PropertyExchangeResult& r) + { + if (validateResponse (r)) + iter->second.deviceInfo = Encodings::jsonFrom7BitText (r.getBody()); + + getChannelList(); + }); + return; + } + + return getChannelList(); + }; + + PropertyRequestHeader header; + header.resource = "ResourceList"; + *transaction = device->sendPropertyGetInquiry (source, header, onResourceListReceived); + + return true; + } + + bool handlePropertyDataResponse (const Message::DynamicSizePropertyExchange& response) const + { + const auto responderMUID = output->getIncomingHeader().source; + const auto iter = device->discovered.find (responderMUID); + + if (iter == device->discovered.end()) + return false; + + iter->second.initiatorPropertyCaches.addChunk (response.requestID, response); + + return true; + } + + bool messageReceived (const Message::PropertyGetDataResponse& response) const + { + handlePropertyDataResponse (response); + return true; + } + + bool messageReceived (const Message::PropertySetDataResponse& response) const + { + handlePropertyDataResponse (Message::DynamicSizePropertyExchange { response.requestID, + response.header, + 1, + 1, + {} }); + return true; + } + + bool messageReceived (const Message::PropertySubscribe& subscription) const + { + const auto responderMUID = output->getIncomingHeader().source; + const auto iter = device->discovered.find (responderMUID); + + if (iter == device->discovered.end()) + return false; + + const auto request = subscription.requestID; + const auto source = output->getIncomingHeader().source; + + const auto jsonHeader = Encodings::jsonFrom7BitText (subscription.header); + const auto typedHeader = PropertySubscriptionHeader::parseCondensed (jsonHeader); + const auto subscribeId = typedHeader.subscribeId; + + const auto callback = [this, request, source, subscribeId] (const PropertyExchangeResult& result) + { + if (result.getError().has_value()) + return; + + PropertySubscriptionData data; + + data.header = result.getHeaderAsSubscriptionHeader(); + data.body = result.getBody(); + + if (data.header.command == PropertySubscriptionCommand::end) + { + const auto foundMuid = device->discovered.find (source); + + if (foundMuid != device->discovered.end()) + foundMuid->second.subscriptions.erase ({ data.header.subscribeId, {} }); + } + + if (data.header.command != PropertySubscriptionCommand::start) + device->listeners.call ([source, &data] (auto& l) { l.propertySubscriptionDataReceived (source, data); }); + + PropertyReplyHeader header; + header.extended["subscribeId"] = subscribeId; + const auto headerBytes = Encodings::jsonTo7BitText (header.toVarCondensed()); + + detail::MessageTypeUtils::send (device->concreteBufferOutput, + device->options.getFunctionBlock().firstGroup, + source, + ChannelInGroup::wholeBlock, + Message::PropertySubscribeResponse { { request, headerBytes, 1, 1, {} } }); + }; + + // Subscription events may be sent at any time by the responder, so there may not be + // an existing transaction ID for new subscription messages. + iter->second.responderPropertyCaches.primeCache (device->propertyDelegate.getNumSimultaneousRequestsSupported(), + callback, + subscription.requestID); + + iter->second.responderPropertyCaches.addChunk (subscription.requestID, subscription); + + return true; + } + + bool messageReceived (const Message::PropertySubscribeResponse& response) const + { + handlePropertyDataResponse (response); + return true; + } + + bool messageReceived (const Message::PropertyNotify& notify) const + { + const auto responderMUID = output->getIncomingHeader().source; + const auto iter = device->discovered.find (responderMUID); + + if (iter == device->discovered.end()) + return false; + + iter->second.initiatorPropertyCaches.notify (notify.requestID, notify.header); + iter->second.responderPropertyCaches.notify (notify.requestID, notify.header); + + return true; + } + + + Impl* device = nullptr; + ResponderOutput* output = nullptr; + bool* handled = nullptr; + }; + + Impl* device = nullptr; + }; + + struct Discovered + { + explicit Discovered (Message::Discovery r) : discovery (r) {} + + Message::Discovery discovery; + std::optional propertyExchangeResponse; + BlockProfileStates profileStates; + InitiatorPropertyExchangeCache initiatorPropertyCaches; + ResponderPropertyExchangeCache responderPropertyCaches; + var resourceList, deviceInfo, channelList; + std::set subscriptions; ///< subscribeIds of subscriptions that we initiated + }; + + class ConcreteBufferOutput : public BufferOutput + { + public: + explicit ConcreteBufferOutput (Impl& d) : device (d) {} + + MUID getMuid() const override { return device.muid; } + std::vector& getOutputBuffer() override { return device.outgoing; } + + void send (uint8_t group) override + { + sentMuid = true; + + for (auto* o : device.options.getOutputs()) + o->processMessage ({ group, getOutputBuffer() }); + } + + bool hasSentMuid() const { return sentMuid; } + void resetSentMuid() { sentMuid = false; } + + private: + Impl& device; + bool sentMuid = false; + }; + + class CacheProviderImpl : public CacheProvider + { + public: + explicit CacheProviderImpl (Impl& d) : device (d) {} + + std::set getDiscoveredMuids() const override + { + std::set result; + + for (const auto& d : device.discovered) + result.insert (d.first); + + return result; + } + + InitiatorPropertyExchangeCache* getCacheForMuidAsInitiator (MUID m) override + { + const auto iter = device.discovered.find (m); + return iter != device.discovered.end() ? &iter->second.initiatorPropertyCaches : nullptr; + } + + ResponderPropertyExchangeCache* getCacheForMuidAsResponder (MUID m) override + { + const auto iter = device.discovered.find (m); + return iter != device.discovered.end() ? &iter->second.responderPropertyCaches : nullptr; + } + + int getMaxSysexSizeForMuid (MUID m) const override + { + constexpr auto defaultResult = 1 << 16; + + const auto iter = device.discovered.find (m); + return iter != device.discovered.end() ? jmin (defaultResult, (int) iter->second.discovery.maximumSysexSize) : defaultResult; + } + + public: + Impl& device; + }; + + class ProfileDelegateImpl : public ProfileDelegate + { + public: + explicit ProfileDelegateImpl (Impl& d) : device (d) {} + + void profileEnablementRequested (MUID x, ProfileAtAddress profileAtAddress, int numChannels, bool enabled) override + { + if (auto* d = device.options.getProfileDelegate()) + return d->profileEnablementRequested (x, profileAtAddress, numChannels, enabled); + + if (! device.profileHost.has_value()) + return; + + if (enabled) + device.profileHost->enableProfile (profileAtAddress, numChannels); + else + device.profileHost->disableProfile (profileAtAddress); + } + + public: + Impl& device; + }; + + class PropertyDelegateImpl : public PropertyDelegate + { + public: + explicit PropertyDelegateImpl (Impl& d) : device (d) {} + + uint8_t getNumSimultaneousRequestsSupported() const override + { + if (auto* d = device.options.getPropertyDelegate()) + return d->getNumSimultaneousRequestsSupported(); + + return 127; + } + + PropertyReplyData propertyGetDataRequested (MUID m, const PropertyRequestHeader& header) override + { + if (auto* d = device.options.getPropertyDelegate()) + return d->propertyGetDataRequested (m, header); + + PropertyReplyData result; + result.header.status = 404; // Resource not found, do not retry + result.header.message = TRANS ("Handling for \"Inquiry: Get Property Data\" is not implemented."); + return result; + } + + PropertyReplyHeader propertySetDataRequested (MUID m, const PropertyRequestData& data) override + { + if (auto* d = device.options.getPropertyDelegate()) + return d->propertySetDataRequested (m, data); + + PropertyReplyHeader result; + result.status = 404; // Resource not found, do not retry + result.message = TRANS ("Handling for \"Inquiry: Set Property Data\" is not implemented."); + return result; + } + + bool subscriptionStartRequested (MUID m, const PropertySubscriptionHeader& data) override + { + if (auto* d = device.options.getPropertyDelegate()) + return d->subscriptionStartRequested (m, data); + + return false; + } + + void subscriptionDidStart (MUID m, const String& id, const PropertySubscriptionHeader& data) override + { + if (auto* d = device.options.getPropertyDelegate()) + d->subscriptionDidStart (m, id, data); + } + + void subscriptionWillEnd (MUID m, const ci::Subscription& subscription) override + { + if (auto* d = device.options.getPropertyDelegate()) + d->subscriptionWillEnd (m, subscription); + } + + public: + Impl& device; + }; + + static MUID getReallyRandomMuid() + { + Random random; + random.setSeedRandomly(); + return MUID::makeRandom (random); + } + + static DeviceOptions getValidated (DeviceOptions opt) + { + opt = opt.withMaxSysExSize (jmax ((size_t) 128, opt.getMaxSysExSize())); + + if (opt.getFeatures().isPropertyExchangeSupported()) + opt = opt.withMaxSysExSize (jmax ((size_t) 512, opt.getMaxSysExSize())); + + opt = opt.withFeatures (opt.getFeatures().withProcessInquirySupported (false)); + + // You'll need to provide some outputs if you want the device to talk to the outside world! + jassert (! opt.getOutputs().empty()); + + return opt; + } + + template + bool supportsFlag (MUID m, Member member) const + { + const auto iter = discovered.find (m); + return iter != discovered.end() && (Features (iter->second.discovery.capabilities).*member)(); + } + + bool supportsProfiles (MUID m) const + { + return supportsFlag (m, &Features::isProfileConfigurationSupported); + } + + bool supportsProperties (MUID m) const + { + return supportsFlag (m, &Features::isPropertyExchangeSupported); + } + + void inquirePropertySubscribe (MUID m, + const PropertySubscriptionHeader& header, + std::function cb) + { + const auto iter = discovered.find (m); + + if (iter == discovered.end() || ! Features { iter->second.discovery.capabilities }.isPropertyExchangeSupported()) + { + // Trying to send a subscription message to a device that doesn't exist (maybe it got removed), or + // that doesn't support property exchange. + jassertfalse; + return; + } + + auto primed = iter->second.initiatorPropertyCaches.primeCache (propertyDelegate.getNumSimultaneousRequestsSupported(), + std::move (cb), + detail::PropertyHostUtils::getTerminator (concreteBufferOutput, options.getFunctionBlock(), m)); + + if (! primed.isValid()) + return; + + detail::PropertyHostUtils::send (concreteBufferOutput, + options.getFunctionBlock().firstGroup, + detail::MessageMeta::Meta::subID2, + m, + primed.id, + Encodings::jsonTo7BitText (header.toVarCondensed()), + {}, + cacheProvider.getMaxSysexSizeForMuid (m)); + } + + DeviceOptions options; + MUID muid; + std::vector outgoing; + std::map discovered; + ListenerList listeners; + ConcreteBufferOutput concreteBufferOutput { *this }; + CacheProviderImpl cacheProvider { *this }; + ProfileDelegateImpl profileDelegate { *this }; + PropertyDelegateImpl propertyDelegate { *this }; + std::optional profileHost; + std::optional propertyHost; + std::list ongoingTransactions; +}; + +//============================================================================== +Device::Device (const Options& opt) : pimpl (std::make_unique (opt)) {} +Device::~Device() = default; +Device::Device (Device&&) noexcept = default; +Device& Device::operator= (Device&&) noexcept = default; + +void Device::processMessage (ump::BytesOnGroup msg) { pimpl->processMessage (msg); } +void Device::sendDiscovery() { pimpl->sendDiscovery(); } +void Device::sendEndpointInquiry (MUID destination, Message::EndpointInquiry endpoint) { pimpl->sendEndpointInquiry (destination, endpoint); } +void Device::sendProfileInquiry (MUID destination, ChannelInGroup address) { pimpl->sendProfileInquiry (destination, address); } +void Device::sendProfileDetailsInquiry (MUID destination, ChannelInGroup address, Profile profile, std::byte target) +{ + pimpl->sendProfileDetailsInquiry (destination, address, profile, target); +} +void Device::sendProfileSpecificData (MUID destination, ChannelInGroup address, Profile profile, Span data) +{ + pimpl->sendProfileSpecificData (destination, address, profile, data); +} +void Device::sendProfileEnablement (MUID destination, ChannelInGroup address, Profile profile, int numChannels) +{ + pimpl->sendProfileEnablement (destination, address, profile, numChannels); +} +void Device::sendPropertyCapabilitiesInquiry (MUID destination) +{ + pimpl->sendPropertyCapabilitiesInquiry (destination); +} +ErasedScopeGuard Device::sendPropertyGetInquiry (MUID destination, + const PropertyRequestHeader& header, + std::function onResult) +{ + return pimpl->sendPropertyGetInquiry (destination, header, std::move (onResult)); +} +void Device::sendPropertySetInquiry (MUID destination, + const PropertyRequestHeader& header, + Span body, + std::function onResult) +{ + pimpl->sendPropertySetInquiry (destination, header, body, std::move (onResult)); +} +void Device::sendPropertySubscriptionStart (MUID destination, + const PropertySubscriptionHeader& header, + std::function onResult) +{ + pimpl->sendPropertySubscriptionStart (destination, header, std::move (onResult)); +} +void Device::sendPropertySubscriptionEnd (MUID destination, + const String& subscribeId, + std::function onResult) +{ + pimpl->sendPropertySubscriptionEnd (destination, subscribeId, std::move (onResult)); +} +std::vector Device::getOngoingSubscriptionsForMuid (MUID m) const { return pimpl->getOngoingSubscriptionsForMuid (m); } +int Device::countOngoingPropertyTransactions() const { return pimpl->countOngoingPropertyTransactions(); } +void Device::addListener (Listener& l) { pimpl->addListener (l); } +void Device::removeListener (Listener& l) { pimpl->removeListener (l); } +MUID Device::getMuid() const { return pimpl->getMuid(); } +DeviceOptions Device::getOptions() const { return pimpl->getOptions(); } +std::vector Device::getDiscoveredMuids() const { return pimpl->getDiscoveredMuids(); } +const ProfileHost* Device::getProfileHost() const { return pimpl->getProfileHost(); } +ProfileHost* Device::getProfileHost() { return pimpl->getProfileHost(); } +const PropertyHost* Device::getPropertyHost() const { return pimpl->getPropertyHost(); } +PropertyHost* Device::getPropertyHost() { return pimpl->getPropertyHost(); } +std::optional Device::getDiscoveryInfoForMuid (MUID m) const { return pimpl->getDiscoveryInfoForMuid (m); } +const ChannelProfileStates* Device::getProfileStateForMuid (MUID m, ChannelAddress address) const { return pimpl->getProfileStateForMuid (m, address); } +std::optional Device::getNumPropertyExchangeRequestsSupportedForMuid (MUID m) const +{ + return pimpl->getNumPropertyExchangeRequestsSupportedForMuid (m); +} +var Device::getResourceListForMuid (MUID x) const { return pimpl->getResourceListForMuid (x); } +var Device::getDeviceInfoForMuid (MUID x) const { return pimpl->getDeviceInfoForMuid (x); } +var Device::getChannelListForMuid (MUID x) const { return pimpl->getChannelListForMuid (x); } + +//============================================================================== +//============================================================================== +#if JUCE_UNIT_TESTS + +class DeviceTests : public UnitTest +{ +public: + DeviceTests() : UnitTest ("Device", UnitTestCategories::midi) {} + + void runTest() override + { + auto random = getRandom(); + + struct GroupOutput + { + uint8_t group; + std::vector bytes; + + bool operator== (const GroupOutput& other) const + { + const auto tie = [] (const auto& x) { return std::tie (x.group, x.bytes); }; + return tie (*this) == tie (other); + } + + bool operator!= (const GroupOutput& other) const { return ! operator== (other); } + }; + + struct Output : public DeviceMessageHandler + { + void processMessage (ump::BytesOnGroup msg) override + { + messages.push_back ({ msg.group, std::vector (msg.bytes.begin(), msg.bytes.end()) }); + } + + std::vector messages; + }; + + const ump::DeviceInfo deviceInfo { { std::byte { 0x01 }, std::byte { 0x02 }, std::byte { 0x03 } }, + { std::byte { 0x11 }, std::byte { 0x12 } }, + { std::byte { 0x21 }, std::byte { 0x22 } }, + { std::byte { 0x31 }, std::byte { 0x32 }, std::byte { 0x33 }, std::byte { 0x34 } } }; + + FunctionBlock functionBlock; + + beginTest ("When receiving Discovery from a MUID that matches the Device MUID, reply with InvalidateMUID and initiate discovery"); + { + Output output; + const auto options = DeviceOptions().withOutputs ({ &output }) + .withFunctionBlock (functionBlock) + .withDeviceInfo (deviceInfo) + .withMaxSysExSize (512); + Device device { options }; + + const auto commonMUID = device.getMuid(); + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + commonMUID, + MUID::getBroadcast() }, + Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } }, + { std::byte { 0x15 }, std::byte { 0x16 } }, + { std::byte { 0x25 }, std::byte { 0x26 } }, + { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } }, + std::byte{}, + 1024, + std::byte{} }) }); + + expect (device.getMuid() != commonMUID); + const std::vector responses + { + { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + commonMUID, + MUID::getBroadcast() }, + Message::InvalidateMUID { commonMUID }) }, + { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + MUID::getBroadcast() }, + Message::Discovery { deviceInfo, std::byte{}, 512, std::byte{} }) }, + }; + expect (output.messages == responses); + } + + beginTest ("When receiving Discovery from a MUID that does not match the Device MUID, reply with DiscoveryResponse and EndpointInquiry"); + { + Output output; + const auto options = DeviceOptions().withOutputs ({ &output }) + .withFunctionBlock (functionBlock) + .withDeviceInfo (deviceInfo) + .withMaxSysExSize (512); + Device device { options }; + + const auto responderMUID = device.getMuid(); + const auto initiatorMUID = MUID::makeRandom (random); + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + initiatorMUID, + MUID::getBroadcast() }, + Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } }, + { std::byte { 0x15 }, std::byte { 0x16 } }, + { std::byte { 0x25 }, std::byte { 0x26 } }, + { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } }, + std::byte{}, + 1024, + std::byte{} }) }); + + expect (device.getMuid() == responderMUID); + const std::vector responses + { + { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + responderMUID, + initiatorMUID }, + Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) }, + { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + responderMUID, + initiatorMUID }, + Message::EndpointInquiry { std::byte{} }) }, + }; + expect (output.messages == responses); + } + + beginTest ("Sending a V1 discovery message notifies the listener"); + { + Output output; + const auto options = DeviceOptions().withOutputs ({ &output }) + .withFunctionBlock (functionBlock) + .withDeviceInfo (deviceInfo) + .withMaxSysExSize (512); + Device device { options }; + + const auto responderMUID = device.getMuid(); + const auto initiatorMUID = MUID::makeRandom (random); + constexpr uint8_t version = 0x01; + + auto bytes = getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + std::byte { version }, + initiatorMUID, + MUID::getBroadcast() }, + Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } }, + { std::byte { 0x15 }, std::byte { 0x16 } }, + { std::byte { 0x25 }, std::byte { 0x26 } }, + { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } }, + std::byte{}, + 1024, + std::byte{} }); + + // V1 message doesn't have an output path + bytes.pop_back(); + device.processMessage ({ 0, bytes }); + + expect (device.getMuid() == responderMUID); + const std::vector responses + { + { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + responderMUID, + initiatorMUID }, + Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) }, + { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + responderMUID, + initiatorMUID }, + Message::EndpointInquiry { std::byte{} }) }, + }; + expect (output.messages == responses); + } + + beginTest ("Sending a V2 discovery message notifies the input listener"); + { + constexpr std::byte outputPathID { 5 }; + const auto initiatorMUID = MUID::makeRandom (random); + constexpr std::byte version { 0x02 }; + + Output output; + const auto options = DeviceOptions().withOutputs ({ &output }) + .withFunctionBlock (functionBlock) + .withDeviceInfo (deviceInfo) + .withMaxSysExSize (512); + Device device { options }; + + const auto responderMUID = device.getMuid(); + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + version, + initiatorMUID, + MUID::getBroadcast() }, + Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } }, + { std::byte { 0x15 }, std::byte { 0x16 } }, + { std::byte { 0x25 }, std::byte { 0x26 } }, + { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } }, + std::byte{}, + 1024, + outputPathID }) }); + + expect (device.getMuid() == responderMUID); + const std::vector responses + { + { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + responderMUID, + initiatorMUID }, + Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, outputPathID, std::byte { 0x7f } }) }, + { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + responderMUID, + initiatorMUID }, + Message::EndpointInquiry { std::byte{} }) }, + }; + expect (output.messages == responses); + } + + beginTest ("Sending a discovery message with a future version notifies the input listener and ignores trailing fields"); + { + constexpr std::byte outputPathID { 10 }; + const auto initiatorMUID = MUID::makeRandom (random); + constexpr std::byte version { 0x03 }; + + Output output; + const auto options = DeviceOptions().withOutputs ({ &output }) + .withFunctionBlock (functionBlock) + .withDeviceInfo (deviceInfo) + .withMaxSysExSize (512); + Device device { options }; + + const auto responderMUID = device.getMuid(); + + auto bytes = getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + version, + initiatorMUID, + MUID::getBroadcast() }, + Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } }, + { std::byte { 0x15 }, std::byte { 0x16 } }, + { std::byte { 0x25 }, std::byte { 0x26 } }, + { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } }, + std::byte{}, + 1024, + outputPathID }); + + // Future versions might have more trailing bytes + bytes.insert (bytes.end(), { std::byte{}, std::byte{} }); + device.processMessage ({ 0, bytes }); + + expect (device.getMuid() == responderMUID); + const std::vector responses + { + { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + responderMUID, + initiatorMUID }, + Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, outputPathID, std::byte { 0x7f } }) }, + { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + responderMUID, + initiatorMUID }, + Message::EndpointInquiry { std::byte{} }) }, + }; + expect (output.messages == responses); + } + + beginTest ("When receiving an InvalidateMUID that matches the Device MUID, initiate discovery using a new MUID"); + { + Output output; + const auto options = DeviceOptions().withOutputs ({ &output }) + .withFunctionBlock (functionBlock) + .withDeviceInfo (deviceInfo) + .withMaxSysExSize (512); + Device device { options }; + + const auto deviceMUID = device.getMuid(); + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + MUID::makeRandom (random), + MUID::getBroadcast() }, + Message::InvalidateMUID { deviceMUID }) }); + + expect (device.getMuid() != deviceMUID); + + expect (Parser::parse (MUID::makeRandom (random), output.messages.front().bytes) == Message::Parsed { { ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + MUID::getBroadcast() }, + Message::Discovery { deviceInfo, + {}, + 512, + {} } }); + } + + struct Listener : public DeviceListener + { + void deviceAdded (MUID x) override { added .push_back (x); } + void deviceRemoved (MUID x) override { removed.push_back (x); } + + std::vector added, removed; + }; + + beginTest ("When receiving a DiscoveryResponse, update the set of known devices, notify outputs, and request endpoint info"); + { + Listener delegate; + Output output; + const auto options = DeviceOptions().withOutputs ({ &output }) + .withFunctionBlock (functionBlock) + .withDeviceInfo (deviceInfo) + .withMaxSysExSize (512); + Device device { options }; + device.addListener (delegate); + + expect (device.getDiscoveredMuids().empty()); + + const auto deviceMUID = device.getMuid(); + const auto responderMUID = MUID::makeRandom (random); + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + responderMUID, + deviceMUID }, + Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) }); + + expect (device.getDiscoveredMuids() == std::vector { responderMUID }); + expect (delegate.added == std::vector { responderMUID }); + + std::vector responses + { + { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + deviceMUID, + responderMUID }, + Message::EndpointInquiry { std::byte{} }) }, + }; + expect (output.messages == responses); + + beginTest ("When receiving a DiscoveryResponse with a MUID that matches a known device, invalidate that MUID"); + { + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + responderMUID, + deviceMUID }, + Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) }); + + expect (device.getDiscoveredMuids().empty()); + expect (delegate.removed == std::vector { responderMUID }); + + responses.push_back ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + deviceMUID, + MUID::getBroadcast() }, + Message::InvalidateMUID { responderMUID }) }); + expect (output.messages == responses); + } + } + + beginTest ("After receiving an EndpointResponse, the listener is notified"); + { + static constexpr std::byte dataBytes[] { std::byte { 0x01 }, std::byte { 0x7f }, std::byte { 0x41 } }; + + struct EndpointListener : public DeviceListener + { + EndpointListener (UnitTest& t, Device& d) : test (t), device (d) {} + + void endpointReceived (MUID, Message::EndpointInquiryResponse) override { called = true; } + + UnitTest& test; + Device& device; + bool called = false; + }; + + Output output; + const auto options = DeviceOptions().withOutputs ({ &output }) + .withFunctionBlock (functionBlock) + .withDeviceInfo (deviceInfo) + .withMaxSysExSize (512); + Device device { options }; + + EndpointListener delegate { *this, device }; + device.addListener (delegate); + + const auto responderMUID = MUID::makeRandom (random); + const auto deviceMUID = device.getMuid(); + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + responderMUID, + deviceMUID }, + Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) }); + + expect (! delegate.called); + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + responderMUID, + deviceMUID }, + Message::EndpointInquiryResponse { std::byte{}, dataBytes }) }); + + expect (delegate.called); + } + + beginTest ("If a device has not previously acted as a responder, modifying profiles does not emit events"); + { + Output output; + + const auto options = DeviceOptions().withOutputs ({ &output }) + .withFunctionBlock (functionBlock) + .withDeviceInfo (deviceInfo) + .withMaxSysExSize (512) + .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true)); + Device device { options }; + + expect (device.getProfileHost() != nullptr); + + const Profile profile { std::byte { 0x01 }, + std::byte { 0x02 }, + std::byte { 0x03 }, + std::byte { 0x04 }, + std::byte { 0x05 } }; + + device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }); + + expect (output.messages.empty()); + + beginTest ("The device reports profiles accurately"); + { + const auto inquiryMUID = MUID::makeRandom (random); + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + device.getMuid() }, + Message::ProfileInquiry{}) }); + + const Profile disabledProfiles[] { profile }; + expect (output.messages.size() == 1); + expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + inquiryMUID }, + Message::ProfileInquiryResponse { {}, disabledProfiles })); + } + + beginTest ("If a device has previously acted as a responder to profile inquiry, then modifying profiles emits events"); + { + const auto numChannels = 0; + device.getProfileHost()->enableProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }, numChannels); + + expect (output.messages.size() == 2); + expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + MUID::getBroadcast() }, + Message::ProfileEnabledReport { profile, numChannels })); + } + } + + beginTest ("If a device receives a details inquiry message addressed to an unsupported profile, a NAK with a code of 0x04 is emitted"); + { + Output output; + const auto options = DeviceOptions().withOutputs ({ &output }) + .withFunctionBlock (functionBlock) + .withDeviceInfo (deviceInfo) + .withMaxSysExSize (512) + .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true)); + Device device { options }; + + expect (device.getProfileHost() != nullptr); + + const auto inquiryMUID = MUID::makeRandom (random); + + const Profile profile { std::byte { 0x01 }, + std::byte { 0x02 }, + std::byte { 0x03 }, + std::byte { 0x04 }, + std::byte { 0x05 } }; + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + device.getMuid() }, + Message::ProfileDetails { profile, std::byte { 0x02 } }) }); + + expect (output.messages.size() == 1); + expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + inquiryMUID }, + Message::NAK { detail::MessageMeta::Meta::subID2, + std::byte { 0x04 }, + {}, + {}, + {} })); + } + + beginTest ("If a device receives a set profile on and enables the profile, profile enabled report is emitted"); + { + // Note: if there's no explicit profile delegate, the device will toggle profiles as requested. + Output output; + const auto options = DeviceOptions().withOutputs ({ &output }) + .withFunctionBlock (functionBlock) + .withDeviceInfo (deviceInfo) + .withMaxSysExSize (512) + .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true)); + Device device { options }; + expect (device.getProfileHost() != nullptr); + + const Profile profile { std::byte { 0x01 }, + std::byte { 0x02 }, + std::byte { 0x03 }, + std::byte { 0x04 }, + std::byte { 0x05 } }; + + device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }); + + const auto inquiryMUID = MUID::makeRandom (random); + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + device.getMuid() }, + Message::ProfileOn { profile, 1 }) }); + + expect (output.messages.size() == 1); + expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + MUID::getBroadcast() }, + Message::ProfileEnabledReport { profile, 1 })); + } + + struct DoNothingProfileDelegate : public ProfileDelegate + { + void profileEnablementRequested (MUID, ProfileAtAddress, int, bool) override {} + }; + + beginTest ("If a device receives a set profile on but then doesn't enable the profile, profile disabled report is emitted"); + { + DoNothingProfileDelegate delegate; + Output output; + const auto options = DeviceOptions().withOutputs ({ &output }) + .withFunctionBlock (functionBlock) + .withDeviceInfo (deviceInfo) + .withMaxSysExSize (512) + .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true)) + .withProfileDelegate (&delegate); + Device device { options }; + + expect (device.getProfileHost() != nullptr); + + const Profile profile { std::byte { 0x01 }, + std::byte { 0x02 }, + std::byte { 0x03 }, + std::byte { 0x04 }, + std::byte { 0x05 } }; + + device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }); + + const auto inquiryMUID = MUID::makeRandom (random); + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + device.getMuid() }, + Message::ProfileOn { profile, 1 }) }); + + expect (output.messages.size() == 1); + expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + MUID::getBroadcast() }, + Message::ProfileDisabledReport { profile, {} })); + } + + beginTest ("If a device receives a set profile on for an unsupported profile, NAK is emitted"); + { + Output output; + const auto options = DeviceOptions().withOutputs ({ &output }) + .withFunctionBlock (functionBlock) + .withDeviceInfo (deviceInfo) + .withMaxSysExSize (512) + .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true)); + Device device { options }; + + expect (device.getProfileHost() != nullptr); + + const Profile profile { std::byte { 0x01 }, + std::byte { 0x02 }, + std::byte { 0x03 }, + std::byte { 0x04 }, + std::byte { 0x05 } }; + + const auto inquiryMUID = MUID::makeRandom (random); + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + device.getMuid() }, + Message::ProfileOn { profile, 1 }) }); + + expect (output.messages.size() == 1); + expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + inquiryMUID }, + Message::NAK { detail::MessageMeta::Meta::subID2, + {}, + {}, + {}, + {} })); + } + + beginTest ("If a device receives a set profile off and disables the profile, profile disabled report is emitted"); + { + // Note: if there's no explicit profile delegate, the device will toggle profiles as requested. + Output output; + const auto options = DeviceOptions().withOutputs ({ &output }) + .withFunctionBlock (functionBlock) + .withDeviceInfo (deviceInfo) + .withMaxSysExSize (512) + .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true)); + Device device { options }; + + expect (device.getProfileHost() != nullptr); + + const Profile profile { std::byte { 0x01 }, + std::byte { 0x02 }, + std::byte { 0x03 }, + std::byte { 0x04 }, + std::byte { 0x05 } }; + + device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }); + device.getProfileHost()->enableProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }, 0); + + const auto inquiryMUID = MUID::makeRandom (random); + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + device.getMuid() }, + Message::ProfileOff { profile }) }); + + expect (output.messages.size() == 1); + expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + MUID::getBroadcast() }, + Message::ProfileDisabledReport { profile, {} })); + } + + beginTest ("If a device receives a set profile off but then doesn't disable the profile, profile enabled report is emitted"); + { + Output output; + DoNothingProfileDelegate delegate; + const auto options = DeviceOptions().withOutputs ({ &output }) + .withFunctionBlock (functionBlock) + .withDeviceInfo (deviceInfo) + .withMaxSysExSize (512) + .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true)) + .withProfileDelegate (&delegate); + Device device { options }; + + expect (device.getProfileHost() != nullptr); + + const Profile profile { std::byte { 0x01 }, + std::byte { 0x02 }, + std::byte { 0x03 }, + std::byte { 0x04 }, + std::byte { 0x05 } }; + + device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }); + device.getProfileHost()->enableProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }, 1); + + const auto inquiryMUID = MUID::makeRandom (random); + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + device.getMuid() }, + Message::ProfileOff { profile }) }); + + expect (output.messages.size() == 1); + expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + MUID::getBroadcast() }, + Message::ProfileEnabledReport { profile, 1 })); + } + + beginTest ("If a device receives a set profile off for an unsupported profile, NAK is emitted"); + { + Output output; + const auto options = DeviceOptions().withOutputs ({ &output }) + .withFunctionBlock (functionBlock) + .withDeviceInfo (deviceInfo) + .withMaxSysExSize (512) + .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true)); + Device device { options }; + + expect (device.getProfileHost() != nullptr); + + const Profile profile { std::byte { 0x01 }, + std::byte { 0x02 }, + std::byte { 0x03 }, + std::byte { 0x04 }, + std::byte { 0x05 } }; + + const auto inquiryMUID = MUID::makeRandom (random); + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + device.getMuid() }, + Message::ProfileOff { profile }) }); + + expect (output.messages.size() == 1); + expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + inquiryMUID }, + Message::NAK { detail::MessageMeta::Meta::subID2, + {}, + {}, + {}, + {} })); + } + + const FunctionBlock realBlock { std::byte{}, 0, 3 }; + + beginTest ("If a device receives a profile inquiry addressed to a channel, that channel's profiles are emitted"); + { + Output output; + const auto options = DeviceOptions().withOutputs ({ &output }) + .withFunctionBlock (realBlock) + .withDeviceInfo (deviceInfo) + .withMaxSysExSize (512) + .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true)); + Device device { options }; + + auto& profileHost = *device.getProfileHost(); + + Profile channel0Profile { std::byte { 0x01 } }; + Profile channel1Profile { std::byte { 0x02 } }; + + profileHost.addProfile ({ channel0Profile, ChannelAddress{}.withChannel (ChannelInGroup::channel0) }); + profileHost.addProfile ({ channel1Profile, ChannelAddress{}.withChannel (ChannelInGroup::channel1) }); + + const auto inquiryMUID = MUID::makeRandom (random); + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::channel0, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + device.getMuid() }, + Message::ProfileInquiry{}) }); + + const Profile channel0Profiles[] { channel0Profile }; + const Profile channel1Profiles[] { channel1Profile }; + + expect (output.messages.size() == 1); + expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::channel0, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + inquiryMUID }, + Message::ProfileInquiryResponse { {}, channel0Profiles })); + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::channel2, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + device.getMuid() }, + Message::ProfileInquiry{}) }); + + expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::channel2, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + inquiryMUID }, + Message::ProfileInquiryResponse { {}, {} })); + + Profile group0Profile { std::byte { 0x05 } }; + Profile group1Profile { std::byte { 0x06 } }; + const Profile group0Profiles[] { group0Profile }; + const Profile group1Profiles[] { group1Profile }; + + beginTest ("If a device receives a profile inquiry addressed to a group, that group's profiles are emitted"); + { + profileHost.addProfile ({ group0Profile, ChannelAddress{}.withGroup (0).withChannel (ChannelInGroup::wholeGroup) }); + profileHost.addProfile ({ group1Profile, ChannelAddress{}.withGroup (1).withChannel (ChannelInGroup::wholeGroup) }); + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeGroup, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + device.getMuid() }, + Message::ProfileInquiry{}) }); + + expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeGroup, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + inquiryMUID }, + Message::ProfileInquiryResponse { {}, group0Profiles })); + + device.processMessage ({ 2, getMessageBytes ({ ChannelInGroup::wholeGroup, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + device.getMuid() }, + Message::ProfileInquiry{}) }); + + expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeGroup, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + inquiryMUID }, + Message::ProfileInquiryResponse { {}, {} })); + } + + beginTest ("If a device receives a profile inquiry addressed to a block, the profiles for member channels, then member groups, then the block are emitted"); + { + Profile blockProfile { std::byte { 0x0a } }; + + profileHost.addProfile ({ blockProfile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }); + + output.messages.clear(); + + device.processMessage ({ 1, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + device.getMuid() }, + Message::ProfileInquiry{}) }); + + const Profile blockProfiles[] { blockProfile }; + + expect (output.messages == std::vector { { 0, getMessageBytes ({ ChannelInGroup::channel0, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + inquiryMUID }, + Message::ProfileInquiryResponse { {}, channel0Profiles }) }, + { 0, getMessageBytes ({ ChannelInGroup::channel1, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + inquiryMUID }, + Message::ProfileInquiryResponse { {}, channel1Profiles }) }, + { 0, getMessageBytes ({ ChannelInGroup::wholeGroup, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + inquiryMUID }, + Message::ProfileInquiryResponse { {}, group0Profiles }) }, + { 1, getMessageBytes ({ ChannelInGroup::wholeGroup, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + inquiryMUID }, + Message::ProfileInquiryResponse { {}, group1Profiles }) }, + { 1, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + inquiryMUID }, + Message::ProfileInquiryResponse { {}, blockProfiles }) } }); + } + } + + // Property exchange + { + const auto inquiryMUID = MUID::makeRandom (random); + + struct Delegate : public PropertyDelegate + { + uint8_t getNumSimultaneousRequestsSupported() const override { return 1; } + PropertyReplyData propertyGetDataRequested (MUID, const PropertyRequestHeader&) override { return {}; } + PropertyReplyHeader propertySetDataRequested (MUID, const PropertyRequestData&) override { return {}; } + bool subscriptionStartRequested (MUID, const PropertySubscriptionHeader&) override { return true; } + void subscriptionDidStart (MUID, const String&, const PropertySubscriptionHeader&) override {} + void subscriptionWillEnd (MUID, const Subscription&) override {} + }; + + Delegate delegate; + Output output; + const auto options = DeviceOptions().withOutputs ({ &output }) + .withFunctionBlock (realBlock) + .withDeviceInfo (deviceInfo) + .withMaxSysExSize (512) + .withFeatures (DeviceFeatures{}.withPropertyExchangeSupported (true)) + .withPropertyDelegate (&delegate); + Device device { options }; + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + MUID::getBroadcast() }, + Message::Discovery { {}, DeviceFeatures{}.withPropertyExchangeSupported (true).getSupportedCapabilities(), 512, {} }) }); + + expect (output.messages.size() == 2); + output.messages.clear(); + + beginTest ("If a device receives too many concurrent property exchange requests, it responds with a retry status code."); + { + auto obj = std::make_unique(); + obj->setProperty ("resource", "X-CustomProp"); + const auto header = Encodings::jsonTo7BitText (obj.release()); + + for (const auto& requestID : { std::byte { 0 }, std::byte { 1 } }) + { + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + device.getMuid() }, + Message::PropertySetData { { requestID, header, 0, 1, {} } }) }); + } + + expect (output.messages.size() == 1); + const auto parsed = Parser::parse (output.messages.back().bytes); + + expect (parsed.has_value()); + expect (parsed->header == Message::Header { ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + inquiryMUID }); + + auto* body = std::get_if (&parsed->body); + expect (body != nullptr); + expect (body->requestID == std::byte { 1 }); + auto replyHeader = Encodings::jsonFrom7BitText (body->header); + expect (replyHeader.getProperty ("status", "") == var (343)); + } + + // Terminate ongoing message + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + device.getMuid() }, + Message::PropertySetData { { {}, {}, 0, 0, {} } }) }); + output.messages.clear(); + + beginTest ("If a device receives an unexpectedly-terminated request, it responds with an error status code."); + { + auto obj = std::make_unique(); + obj->setProperty ("resource", "X-CustomProp"); + const auto header = Encodings::jsonTo7BitText (obj.release()); + const std::byte requestID { 3 }; + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + device.getMuid() }, + Message::PropertySetData { { requestID, header, 2, 1, {} } }) }); + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + device.getMuid() }, + Message::PropertySetData { { requestID, header, 2, 0, {} } }) }); + + expect (output.messages.size() == 1); + const auto parsed = Parser::parse (output.messages.back().bytes); + + expect (parsed.has_value()); + expect (parsed->header == Message::Header { ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + inquiryMUID }); + + auto* body = std::get_if (&parsed->body); + expect (body != nullptr); + expect (body->requestID == requestID); + auto replyHeader = Encodings::jsonFrom7BitText (body->header); + expect (replyHeader.getProperty ("status", "") == var (400)); + } + + output.messages.clear(); + + beginTest ("If a request is terminated via notify, the device responds with an error status code."); + { + auto obj = std::make_unique(); + obj->setProperty ("resource", "X-CustomProp"); + const auto header = Encodings::jsonTo7BitText (obj.release()); + const std::byte requestID { 100 }; + + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + device.getMuid() }, + Message::PropertySetData { { requestID, header, 2, 1, {} } }) }); + + auto notifyHeader = std::make_unique(); + notifyHeader->setProperty ("status", 144); + device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + inquiryMUID, + device.getMuid() }, + Message::PropertyNotify { { requestID, Encodings::jsonTo7BitText (notifyHeader.release()), 1, 1, {} } }) }); + + expect (output.messages.size() == 1); + const auto parsed = Parser::parse (output.messages.back().bytes); + + expect (parsed.has_value()); + expect (parsed->header == Message::Header { ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + device.getMuid(), + inquiryMUID }); + + auto* body = std::get_if (&parsed->body); + expect (body != nullptr); + expect (body->requestID == requestID); + auto replyHeader = Encodings::jsonFrom7BitText (body->header); + expect (replyHeader.getProperty ("status", "") == var (400)); + } + } + } + +private: + template + static std::vector getMessageBytes (const Message::Header& header, const Msg& body) + { + std::vector bytes; + detail::Marshalling::Writer { bytes } (header, body); + return bytes; + } +}; + +static DeviceTests deviceTests; + +#endif + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIDevice.h b/modules/juce_midi_ci/ci/juce_CIDevice.h new file mode 100644 index 0000000000..c9ac6a4a9c --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIDevice.h @@ -0,0 +1,319 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +//============================================================================== +/** + Instances of this type are responsible for parsing and interpreting incoming + MIDI-CI messages, and for sending MIDI-CI messages to other devices. + + Each Device can act both as a target for messages, and as a source of + messages intended to inspect/configure other devices. + + The member functions of Device are generally used to inspect other + devices. Member functions starting with 'send' are used to send or request + information from other devices; registered DeviceListeners will be notified + when the Device receives a response, and then member functions named + matching 'get.*ForMuid' can be used to retrieve the result of the inquiry. + + If the Device does not have local profiles or properties, then responses + to all incoming messages will be generated automatically using the + information supplied during construction. + + If the Device has profiles or properties, then you should implement a + ProfileDelegate and/or a PropertyDelegate as appropriate, and pass this + delegate during construction. Each Delegate will receive callbacks when a + remote device makes a request of the local device, such as + enabling/disabling a profile, or setting/getting property data. + + Sometimes the local device must send notifications when + updating its profile or property state, for example when profiles are + added, or when a subscribed property is changed. Methods to send these + notifications are found on the ProfileHost and PropertyHost classes. + + @tags{Audio} +*/ +class Device : public DeviceMessageHandler +{ +public: + using Features = DeviceFeatures; + using Listener = DeviceListener; + using Options = DeviceOptions; + + /** Constructs a device using the provided options. */ + explicit Device (const Options& opt); + + Device (Device&&) noexcept; + Device& operator= (Device&&) noexcept; + + JUCE_DECLARE_NON_COPYABLE (Device) + + /** Destructor, sends a message to invalidate this device's MUID. */ + ~Device() override; + + //============================================================================== + /** To be called with any message that should be processed by the device. + This should only be passed complete CI messages - you might find the Extractor + class useful for parsing a stream of Universal MIDI Packets and extracting the + CI messages. + Note that this function does *not* synchronise with any other member function of this + class. This means that you must not call this directly from the MIDI input thread if there's + any chance of other member functions being called on the same instance simultaneously from + other threads. + It's probably easiest to send all messages onto the main thread and to limit interactions + with the Device to that thread. + */ + void processMessage (ump::BytesOnGroup) override; + + //============================================================================== + /** Sends an inquiry message. + + You can use DeviceListener::deviceAdded to be notified when new devices are discovered. + + This will clear the internal cache of discovered devices, and repopulate it as discovery + response messages are received. + */ + void sendDiscovery(); + + /** Sends an endpoint inquiry message. + + Check the MIDI-CI spec for an explanation of the different endpoint message status codes. + + Received responses will be sent to DeviceListener::endpointReceived. Responses are not + cached by the Device; if you need to cache endpoint responses, you can keep your own + map of MUID->response, update it in endpointReceived, and remove entries in + DeviceListener::deviceRemoved. + */ + void sendEndpointInquiry (MUID destination, Message::EndpointInquiry endpoint); + + //============================================================================== + /** Sends a profile inquiry to a particular device. + + DeviceListener::profileStateReceived will be called when the device replies. + */ + void sendProfileInquiry (MUID muid, ChannelInGroup address); + + /** Sends a profile details inquiry to a particular device. + + DeviceListener::profileDetailsReceived will be called when the device replies. + */ + void sendProfileDetailsInquiry (MUID muid, ChannelInGroup address, Profile profile, std::byte target); + + /** Sends profile data to a particular device. */ + void sendProfileSpecificData (MUID muid, ChannelInGroup address, Profile profile, Span); + + /** Sets a profile on or off. Pass 0 or less to disable the profile, or a positive number to enable it. + */ + void sendProfileEnablement (MUID muid, ChannelInGroup address, Profile profile, int numChannels); + + //============================================================================== + /** Sends a property inquiry to a particular device. + If the device supports properties, this will also automatically request the ResourceList + property, and then the ChannelList and DeviceInfo properties if they are present in the + ResourceList. + */ + void sendPropertyCapabilitiesInquiry (MUID destination); + + /** Sends an inquiry to get a property value from another device, invoking a callback once + the full transaction has completed. + + @param destination the device whose property will be set + @param header information about the property data that will be sent + @param onResult this will be called once the result of the transaction is known. + If the transaction cannot start for some reason (e.g. the request is + malformed, or there are too many simultaneous requests) then the + function will be called immediately. Otherwise, the function will be + called once the destination device has confirmed receipt of the + inquiry. + @return a token bounding the lifetime of the request. + If you need to terminate the transaction before it has completed, + you can call reset() on this token, or cause its destructor to run. + */ + ErasedScopeGuard sendPropertyGetInquiry (MUID destination, + const PropertyRequestHeader& header, + std::function onResult); + + /** Sends an inquiry to set a property value on another device, invoking a callback once + the full transaction has completed. + + @param destination the device whose property will be set + @param header information about the property data that will be sent + @param body the property data payload to send. + If the header specifies 'ascii' encoding, then you are responsible + for ensuring that no byte of the payload data has its most + significant bit set. Sending the message will fail if this is not + the case. Otherwise, if another encoding is specified then the + payload data may contain any byte values. You should not attempt to + encode the data yourself; the payload will be automatically encoded + before being sent. + @param onResult this will be called once the result of the transaction is known. + If the transaction cannot start for some reason (e.g. the + destination does not support property exchange, the request is + malformed, or there are too many simultaneous requests) then the + function will be called immediately. Otherwise, the function will be + called once the destination device has confirmed receipt of the + inquiry. + */ + void sendPropertySetInquiry (MUID destination, + const PropertyRequestHeader& header, + Span body, + std::function onResult); + + /** Sends an inquiry to start a subscription to a property on a device. + The provided callback will be called to indicate whether starting the subscription + succeeded or failed. + When the remote device indicates that its property value has changed, + DeviceListener::propertySubscriptionReceived will be called with information about the + update. + */ + void sendPropertySubscriptionStart (MUID, + const PropertySubscriptionHeader& header, + std::function); + + /** Sends an inquiry to end a subscription to a property on a device. + The provided callback will be called to indicate whether the subscriber acknowledged + receipt of the message. + Note that the remote device may also choose to terminate the subscription of its own + accord - in this case, the end request will be sent to + DeviceListener::propertySubscriptionReceived. + */ + void sendPropertySubscriptionEnd (MUID, + const String& subscribeId, + std::function); + + /** Returns all of the subscriptions that we have requested from another device. + + Does *not* include subscriptions that other devices have requested from us. + */ + std::vector getOngoingSubscriptionsForMuid (MUID m) const; + + /** Returns the number of transactions initiated by us that are yet to receive complete replies. + + Does *not* include the count of unfinished requests addressed to us by other devices. + + @see PropertyHost::countOngoingTransactions() + */ + int countOngoingPropertyTransactions() const; + + //============================================================================== + /** Adds a listener that will be notified when particular events occur. + + Check the members of the Listener class to see the kinds of events that are reported. + To receive notifications through Listener::propertySubscriptionReceived(), you must + first request a subscription using sendPropertySubscriptionStart(). + + @see Listener, removeListener() + */ + void addListener (Listener& l); + + /** Removes a listener that was previously added with addListener(). */ + void removeListener (Listener& l); + + //============================================================================== + /** Returns the MUID currently associated with this device. + + This may change, e.g. if another device reports that it shares the same MUID. + */ + MUID getMuid() const; + + /** Returns the configuration of this device. */ + Options getOptions() const; + + /** Returns a list of all MUIDs that have been discovered by this device. */ + std::vector getDiscoveredMuids() const; + + /** If you set withProfileConfigurationSupported when constructing this device, this will return + a pointer to an object that can be used to query the states of the profiles for this device. + */ + const ProfileHost* getProfileHost() const; + + /** If you set withProfileConfigurationSupported when constructing this device, this will return + a pointer to an object that can be used to modify the states of the profiles for this device. + */ + ProfileHost* getProfileHost(); + + /** If you set withPropertyExchangeSupported when constructing this device, this will return + a pointer to an object that can be used to query the states of the properties for this device. + */ + const PropertyHost* getPropertyHost() const; + + /** If you set withPropertyExchangeSupported when constructing this device, this will return + a pointer to an object that can be used to modify the states of the properties for this device. + */ + PropertyHost* getPropertyHost(); + + //============================================================================== + /** Returns basic attributes about another device that was discovered. + + If there's no record of the provided device, this will return nullopt. + */ + std::optional getDiscoveryInfoForMuid (MUID m) const; + + /** Returns the states of the profiles on a particular channel of a device. + + If the state is unknown, returns nullptr. + + Devices don't report profile capabilities unless asked; you can request capabilities + using inquireProfile(). + */ + const ChannelProfileStates* getProfileStateForMuid (MUID m, ChannelAddress address) const; + + /** Returns the number of simultaneous property exchange requests supported by a particular + device. + + If there's no record of this device's property capabilities (including the case where + the device doesn't support property exchange at all) this will return nullopt. + + Devices don't report property capabilities unless asked; you can request capabilities + using inquirePropertyCapabilities(). + */ + std::optional getNumPropertyExchangeRequestsSupportedForMuid (MUID m) const; + + /** After DeviceListener::propertyExchangeCapabilitiesReceived() has been received for a + particular device, this function will return that device's ResourceList if available, or + a null var otherwise. + */ + var getResourceListForMuid (MUID x) const; + + /** After DeviceListener::propertyExchangeCapabilitiesReceived() has been received for a + particular device, this function will return that device's DeviceInfo if available, or + a null var otherwise. + */ + var getDeviceInfoForMuid (MUID x) const; + + /** After DeviceListener::propertyExchangeCapabilitiesReceived() has been received for a + particular device, this function will return that device's ChannelList if available, or + a null var otherwise. + */ + var getChannelListForMuid (MUID x) const; + +private: + class Impl; + std::unique_ptr pimpl; +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIDeviceFeatures.h b/modules/juce_midi_ci/ci/juce_CIDeviceFeatures.h new file mode 100644 index 0000000000..aff1c52d7d --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIDeviceFeatures.h @@ -0,0 +1,90 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +/** + Flags indicating the features that are supported by a given CI device. + + @tags{Audio} +*/ +class DeviceFeatures +{ +public: + /** Constructs a DeviceFeatures object with no flags enabled. */ + DeviceFeatures() = default; + + /** Constructs a DeviceFeatures object, taking flag values from the "Capability Inquiry + Category Supported" byte in a CI Discovery message. + */ + explicit DeviceFeatures (std::byte f) : flags ((uint8_t) f) {} + + /** Returns a new DeviceFeatures instance with profile configuration marked as supported. */ + [[nodiscard]] DeviceFeatures withProfileConfigurationSupported (bool x = true) const { return withFlag (profileConfiguration, x); } + /** Returns a new DeviceFeatures instance with property exchange marked as supported. */ + [[nodiscard]] DeviceFeatures withPropertyExchangeSupported (bool x = true) const { return withFlag (propertyExchange, x); } + /** Returns a new DeviceFeatures instance with process inquiry marked as supported. */ + [[nodiscard]] DeviceFeatures withProcessInquirySupported (bool x = true) const { return withFlag (processInquiry, x); } + + /** @see withProfileConfigurationSupported() */ + [[nodiscard]] bool isProfileConfigurationSupported () const { return getFlag (profileConfiguration); } + /** @see withPropertyExchangeSupported() */ + [[nodiscard]] bool isPropertyExchangeSupported () const { return getFlag (propertyExchange); } + /** @see withProcessInquirySupported() */ + [[nodiscard]] bool isProcessInquirySupported () const { return getFlag (processInquiry); } + + /** Returns the feature flags formatted into a bitfield suitable for use as the "Capability + Inquiry Category Supported" byte in a CI Discovery message. + */ + std::byte getSupportedCapabilities() const { return std::byte { flags }; } + + /** Returns true if this and other both have the same flags set. */ + bool operator== (const DeviceFeatures& other) const { return flags == other.flags; } + /** Returns true if any flags in this and other differ. */ + bool operator!= (const DeviceFeatures& other) const { return ! operator== (other); } + +private: + enum Flags + { + profileConfiguration = 1 << 2, + propertyExchange = 1 << 3, + processInquiry = 1 << 4, + }; + + [[nodiscard]] DeviceFeatures withFlag (Flags f, bool value) const + { + return withMember (*this, &DeviceFeatures::flags, (uint8_t) (value ? (flags | f) : (flags & ~f))); + } + + bool getFlag (Flags f) const + { + return (flags & f) != 0; + } + + uint8_t flags = 0; +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIDeviceListener.h b/modules/juce_midi_ci/ci/juce_CIDeviceListener.h new file mode 100644 index 0000000000..6314ecd4a7 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIDeviceListener.h @@ -0,0 +1,146 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +/** + Contains information relating to a subscription update. Check the header's + subscription kind to find out whether the payload is a full update, a + partial update, or empty (as is the case for a notification or + subscription-end request). + + @tags{Audio} +*/ +struct PropertySubscriptionData +{ + PropertySubscriptionHeader header; + Span body; +}; + +//============================================================================== +/** + An interface that receives callbacks when certain messages are received by a Device. + + @tags{Audio} +*/ +struct DeviceListener +{ + DeviceListener() = default; + virtual ~DeviceListener() = default; + DeviceListener (const DeviceListener&) = default; + DeviceListener (DeviceListener&&) = default; + DeviceListener& operator= (const DeviceListener&) = default; + DeviceListener& operator= (DeviceListener&&) = default; + + //============================================================================== + /** Called to indicate that a device with the provided MUID was discovered. + To find out more about the device, use Device::getDiscoveryInfoForMuid(). + */ + virtual void deviceAdded ([[maybe_unused]] MUID x) {} + + /** Called to indicate that a device's MUID was invalidated. + If you were previously storing your own information about this device, you should forget + that information here. + */ + virtual void deviceRemoved ([[maybe_unused]] MUID x) {} + + /** Called to indicate that endpoint information was received for the given device. + See the MIDI-CI spec for an explanation of the different status codes. + */ + virtual void endpointReceived ([[maybe_unused]] MUID x, + [[maybe_unused]] Message::EndpointInquiryResponse response) {} + + + /** Called to indicate that a NAK message was received. + This is useful e.g. to display a diagnostic to the user, or to cache the failed request + details and retry the request at a later date. + + The message field of the NAK is 7-bit text. You can convert it to a string using + Encodings::stringFrom7BitText(). + */ + virtual void messageNotAcknowledged ([[maybe_unused]] MUID x, + [[maybe_unused]] Message::NAK) {} + + //============================================================================== + /** Called to indicate that another device reported its enabled and disabled profiles on a + particular channel. + + @see Device::getProfileStateForMuid() + */ + virtual void profileStateReceived ([[maybe_unused]] MUID x, + [[maybe_unused]] ChannelInGroup destination) {} + + /** Called to indicate that a profile was added or removed. */ + virtual void profilePresenceChanged ([[maybe_unused]] MUID x, + [[maybe_unused]] ChannelInGroup destination, + [[maybe_unused]] Profile profile, + [[maybe_unused]] bool exists) {} + + /** Called to indicate that a profile was enabled or disabled. + A channel count of 0 indicates that the profile was disabled. + */ + virtual void profileEnablementChanged ([[maybe_unused]] MUID x, + [[maybe_unused]] ChannelInGroup destination, + [[maybe_unused]] Profile profile, + [[maybe_unused]] int numChannels) {} + + /** Called to indicate that details about a profile were received. */ + virtual void profileDetailsReceived ([[maybe_unused]] MUID x, + [[maybe_unused]] ChannelInGroup destination, + [[maybe_unused]] Profile profile, + [[maybe_unused]] std::byte target, + [[maybe_unused]] Span data) {} + + /** Called to indicate that data for a profile were received. + + Note that this function may be called either when a remote device attempts to send data to + one of the local Device's profiles, or when a profile on a remote device produces some data. + + Each profile will specify its own mechanism for distinguishing between the two cases if + necessary. + */ + virtual void profileSpecificDataReceived ([[maybe_unused]] MUID x, + [[maybe_unused]] ChannelInGroup destination, + [[maybe_unused]] Profile profile, + [[maybe_unused]] Span data) {} + + //============================================================================== + /** Called to indicate that another device reported its property exchange capabilities. + + @see Device::getPropertyExchangeCapabilitiesResponseForMuid() + */ + virtual void propertyExchangeCapabilitiesReceived ([[maybe_unused]] MUID x) {} + + /** Called to indicate that a subscription update was received. + This only receives messages with responder commands (partial, full, notify, end). + + To start a subscription, use Device::sendPropertySubscriptionStart(). + */ + virtual void propertySubscriptionDataReceived ([[maybe_unused]] MUID x, + [[maybe_unused]] const PropertySubscriptionData& data) {} +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIDeviceMessageHandler.h b/modules/juce_midi_ci/ci/juce_CIDeviceMessageHandler.h new file mode 100644 index 0000000000..81a18e08af --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIDeviceMessageHandler.h @@ -0,0 +1,53 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +//============================================================================== +/** + An interface that will receive a callback every time a Device wishes to send a new MIDI-CI + message. + + @tags{Audio} +*/ +struct DeviceMessageHandler +{ + DeviceMessageHandler() = default; + virtual ~DeviceMessageHandler() = default; + DeviceMessageHandler (const DeviceMessageHandler&) = default; + DeviceMessageHandler (DeviceMessageHandler&&) = default; + DeviceMessageHandler& operator= (const DeviceMessageHandler&) = default; + DeviceMessageHandler& operator= (DeviceMessageHandler&&) = default; + + /** Called with the bytes of a MIDI-CI message, along with the message's group. + + To send the message on, format the message appropriately (either into bytestream sysex + or into multiple UMP sysex packets). + */ + virtual void processMessage (ump::BytesOnGroup) = 0; +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIDeviceOptions.h b/modules/juce_midi_ci/ci/juce_CIDeviceOptions.h new file mode 100644 index 0000000000..75276fe0f8 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIDeviceOptions.h @@ -0,0 +1,167 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +//============================================================================== +/** + Configuration options for a Device. + + The options set here will remain constant over the lifetime of a Device. + + @tags{Audio} +*/ +class DeviceOptions +{ +public: + static constexpr auto beginValidAscii = 32; // inclusive + static constexpr auto endValidAscii = 127; // exclusive + + /** Creates a random product instance ID. + This isn't really recommended - it's probably better to have a unique ID that remains + persistent after a restart. + */ + static std::array makeProductInstanceId (Random& random) + { + std::array result{}; + + for (auto& c : result) + c = (char) random.nextInt ({ beginValidAscii, endValidAscii }); + + return result; + } + + /** One or more DeviceMessageHandlers that should receive callbacks with any messages that the + device wishes to send. + Referenced DeviceMessageHandlers *must* outlive any Device constructed from these options. + */ + [[nodiscard]] DeviceOptions withOutputs (std::vector x) const + { + return withMember (*this, &DeviceOptions::outputs, x); + } + + /** The function block layout of this device. */ + [[nodiscard]] DeviceOptions withFunctionBlock (FunctionBlock x) const + { + return withMember (*this, &DeviceOptions::functionBlock, x); + } + + /** Basic information about the device used to determine manufacturer, model, etc. + In order to populate this correctly, you'll need to register with the MIDI association - + otherwise you might accidentally end up using IDs that are already assigned to other + companies/individuals. + */ + [[nodiscard]] DeviceOptions withDeviceInfo (const ump::DeviceInfo& x) const + { + return withMember (*this, &DeviceOptions::deviceInfo, x); + } + + /** The features that you want to enable on the device. + + If you enable property exchange, you may wish to supply a PropertyDelegate using + withPropertyDelegate(). + If you enable profile configuration, you may wish to supply a ProfileDelegate using + withProfileDelegate(). + Process inquiry is not currently supported. + */ + [[nodiscard]] DeviceOptions withFeatures (DeviceFeatures x) const + { + return withMember (*this, &DeviceOptions::features, x); + } + + /** The maximum size of sysex messages to accept and to produce. */ + [[nodiscard]] DeviceOptions withMaxSysExSize (size_t x) const + { + return withMember (*this, &DeviceOptions::maxSysExSize, x); + } + + /** Specifies a profile delegate that can be used to respond to particular profile events. + The referenced ProfileDelegate *must* outlive the Device. + */ + [[nodiscard]] DeviceOptions withProfileDelegate (ProfileDelegate* x) const + { + return withMember (*this, &DeviceOptions::profileDelegate, x); + } + + /** Specifies a property delegate that can be used to respond to particular property events. + The referenced PropertyDelegate *must* outlive the Device. + */ + [[nodiscard]] DeviceOptions withPropertyDelegate (PropertyDelegate* x) const + { + return withMember (*this, &DeviceOptions::propertyDelegate, x); + } + + /** Specifies a product instance ID that will be returned in endpoint response messages. */ + [[nodiscard]] DeviceOptions withProductInstanceId (const std::array& x) const + { + const auto null = std::find (x.begin(), x.end(), 0); + + if (! std::all_of (x.begin(), null, [] (char c) { return beginValidAscii <= c && c < endValidAscii; })) + { + // The product instance ID must be made up of ASCII characters + jassertfalse; + return *this; + } + + if (std::any_of (null, x.end(), [] (auto c) { return c != 0; })) + { + // All characters after the null terminator must be 0 + jassertfalse; + return *this; + } + + return withMember (*this, &DeviceOptions::productInstanceId, x); + } + + /** @see withOutputs() */ + [[nodiscard]] const auto& getOutputs() const { return outputs; } + /** @see withFunctionBlock() */ + [[nodiscard]] const auto& getFunctionBlock() const { return functionBlock; } + /** @see withDeviceInfo() */ + [[nodiscard]] const auto& getDeviceInfo() const { return deviceInfo; } + /** @see withFeatures() */ + [[nodiscard]] const auto& getFeatures() const { return features; } + /** @see withMaxSysExSize() */ + [[nodiscard]] const auto& getMaxSysExSize() const { return maxSysExSize; } + /** @see withProductInstanceId() */ + [[nodiscard]] const auto& getProductInstanceId() const { return productInstanceId; } + /** @see withProfileDelegate() */ + [[nodiscard]] const auto& getProfileDelegate() const { return profileDelegate; } + /** @see withPropertyDelegate() */ + [[nodiscard]] const auto& getPropertyDelegate() const { return propertyDelegate; } + +private: + std::vector outputs; + FunctionBlock functionBlock; + ump::DeviceInfo deviceInfo; + DeviceFeatures features; + size_t maxSysExSize = 512; + std::array productInstanceId{}; + ProfileDelegate* profileDelegate = nullptr; + PropertyDelegate* propertyDelegate = nullptr; +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIEncoding.h b/modules/juce_midi_ci/ci/juce_CIEncoding.h new file mode 100644 index 0000000000..6341d41557 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIEncoding.h @@ -0,0 +1,104 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +#define JUCE_ENCODINGS X(ascii, "ASCII") X(mcoded7, "Mcoded7") X(zlibAndMcoded7, "zlib+Mcoded7") + +/** + Identifies different encodings that may be used by property exchange messages. + + @tags{Audio} +*/ +enum class Encoding +{ + #define X(name, unused) name, + JUCE_ENCODINGS + #undef X +}; + +/** + Utility functions for working with the Encoding enum. + + @tags{Audio} +*/ +struct EncodingUtils +{ + EncodingUtils() = delete; + + /** Converts an Encoding to a human-readable string. */ + static const char* toString (Encoding e) + { + switch (e) + { + #define X(name, string) case Encoding::name: return string; + JUCE_ENCODINGS + #undef X + } + + return nullptr; + } + + /** Converts an encoding string from a property exchange JSON header to + an Encoding. + */ + static std::optional toEncoding (const char* str) + { + #define X(name, string) if (std::string_view (str) == std::string_view (string)) return Encoding::name; + JUCE_ENCODINGS + #undef X + + return {}; + } +}; + +#undef JUCE_ENCODINGS + +} // namespace juce::midi_ci + +namespace juce +{ + template <> + struct SerialisationTraits + { + static constexpr auto marshallingVersion = std::nullopt; + + template + void load (Archive& archive, midi_ci::Encoding& t) + { + String encoding; + archive (encoding); + t = midi_ci::EncodingUtils::toEncoding (encoding.toRawUTF8()).value_or (midi_ci::Encoding{}); + } + + template + void save (Archive& archive, const midi_ci::Encoding& t) + { + archive (midi_ci::EncodingUtils::toString (t)); + } + }; + +} // namespace juce diff --git a/modules/juce_midi_ci/ci/juce_CIEncodings.cpp b/modules/juce_midi_ci/ci/juce_CIEncodings.cpp new file mode 100644 index 0000000000..14134f98a2 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIEncodings.cpp @@ -0,0 +1,379 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +String Encodings::stringFrom7BitText (Span bytes) +{ + std::vector chars; + + while (! bytes.empty()) + { + const auto front = (uint8_t) bytes.front(); + + if ((front < 0x20 || 0x80 <= front) && front != 0x0a) + { + jassertfalse; + return {}; + } + + if (front == '\\') + { + bytes = Span (bytes.data() + 1, bytes.size() - 1); + + if (bytes.empty()) + return {}; + + const auto kind = (uint8_t) bytes.front(); + + switch (kind) + { + case '"': chars.push_back ('"'); break; + case '\\': chars.push_back ('\\'); break; + case '/': chars.push_back ('/'); break; + case 'b': chars.push_back ('\b'); break; + case 'f': chars.push_back ('\f'); break; + case 'n': chars.push_back ('\n'); break; + case 'r': chars.push_back ('\r'); break; + case 't': chars.push_back ('\t'); break; + + case 'u': + { + bytes = Span (bytes.data() + 1, bytes.size() - 1); + + if (bytes.size() < 4) + return {}; + + std::string byteStr (reinterpret_cast (bytes.data()), 4); + const auto unit = [&]() -> std::optional + { + try + { + return (CharPointer_UTF16::CharType) std::stoi (byteStr, {}, 16); + } + catch (...) {} + + jassertfalse; + return {}; + }(); + + if (! unit.has_value()) + return {}; + + chars.push_back (*unit); + bytes = Span (bytes.data() + 4, bytes.size() - 4); + continue; + } + + default: + return {}; + } + + bytes = Span (bytes.data() + 1, bytes.size() - 1); + } + else + { + chars.push_back (front); + bytes = Span (bytes.data() + 1, bytes.size() - 1); + } + } + + chars.push_back ({}); + return String { CharPointer_UTF16 { chars.data() } }; +} + +std::vector Encodings::stringTo7BitText (const String& text) +{ + std::vector result; + + for (const auto character : text) + { + if (character == 0x0a || (0x20 <= character && character < 0x80)) + { + result.emplace_back (std::byte (character)); + } + else + { + // Suspiciously low ASCII value encountered! + jassert (character >= 0x80); + + CharPointer_UTF16::CharType points[2]{}; + CharPointer_UTF16 asUTF16 { points }; + asUTF16.write (character); + + std::for_each (points, asUTF16.getAddress(), [&] (CharPointer_UTF16::CharType unit) + { + const auto str = String::toHexString (unit); + + result.insert (result.end(), { std::byte { '\\' }, std::byte { 'u' } }); + + for (const auto c : str) + result.push_back ((std::byte) c); + }); + } + } + + return result; +} + +std::vector Encodings::toMcoded7 (Span bytes) +{ + std::vector result; + result.reserve ((bytes.size() * 8) + 6 / 7); + + for (size_t index = 0; index < bytes.size(); index += 7) + { + std::array slice{}; + const auto sliceSize = std::min ((size_t) 7, bytes.size() - index); + std::copy (bytes.begin() + index, bytes.begin() + index + sliceSize, slice.begin()); + + result.push_back ((slice[0] & std::byte { 0x80 }) >> 1 + | (slice[1] & std::byte { 0x80 }) >> 2 + | (slice[2] & std::byte { 0x80 }) >> 3 + | (slice[3] & std::byte { 0x80 }) >> 4 + | (slice[4] & std::byte { 0x80 }) >> 5 + | (slice[5] & std::byte { 0x80 }) >> 6 + | (slice[6] & std::byte { 0x80 }) >> 7); + std::transform (slice.begin(), + std::next (slice.begin(), (ptrdiff_t) sliceSize), + std::back_inserter (result), + [] (const std::byte b) { return b & std::byte { 0x7f }; }); + } + + return result; +} + +std::vector Encodings::fromMcoded7 (Span bytes) +{ + std::vector result; + result.reserve ((bytes.size() * 7) + 7 / 8); + + for (size_t index = 0; index < bytes.size(); index += 8) + { + const auto sliceSize = std::min ((size_t) 7, bytes.size() - index - 1); + + for (size_t i = 0; i < sliceSize; ++i) + { + const auto highBit = (bytes[index] & std::byte { (uint8_t) (1 << (6 - i)) }) << (i + 1); + result.push_back (highBit | bytes[index + 1 + i]); + } + } + + return result; +} + +std::optional> Encodings::tryEncode (Span bytes, Encoding mutualEncoding) +{ + switch (mutualEncoding) + { + case Encoding::ascii: + { + if (std::all_of (bytes.begin(), bytes.end(), [] (auto b) { return (b & std::byte { 0x80 }) == std::byte{}; })) + return std::vector (bytes.begin(), bytes.end()); + + jassertfalse; + return {}; + } + + case Encoding::mcoded7: + return toMcoded7 (bytes); + + case Encoding::zlibAndMcoded7: + { + MemoryOutputStream memoryStream; + GZIPCompressorOutputStream (memoryStream).write (bytes.data(), bytes.size()); + return toMcoded7 (Span (static_cast (memoryStream.getData()), memoryStream.getDataSize())); + } + } + + // Unknown encoding! + jassertfalse; + return {}; +} + +std::vector Encodings::decode (Span bytes, Encoding mutualEncoding) +{ + if (mutualEncoding == Encoding::ascii) + { + // All values must be 7-bit! + jassert (std::none_of (bytes.begin(), bytes.end(), [] (const auto& b) { return (b & std::byte { 0x80 }) != std::byte{}; })); + return std::vector (bytes.begin(), bytes.end()); + } + + if (mutualEncoding == Encoding::mcoded7) + return fromMcoded7 (bytes); + + if (mutualEncoding == Encoding::zlibAndMcoded7) + { + const auto mcoded = fromMcoded7 (bytes); + MemoryInputStream memoryStream (mcoded.data(), mcoded.size(), false); + + GZIPDecompressorInputStream zipStream (memoryStream); + + const size_t chunkSize = 1 << 8; + + std::vector result; + + for (;;) + { + const auto previousSize = result.size(); + result.resize (previousSize + chunkSize); + const auto read = zipStream.read (result.data() + previousSize, chunkSize); + + if (read < 0) + { + // Decompression failed! + jassertfalse; + return {}; + } + + result.resize (previousSize + (size_t) read); + + if (read == 0) + return result; + } + } + + // Unknown encoding! + jassertfalse; + return {}; +} + +#if JUCE_UNIT_TESTS + +class EncodingsTests : public UnitTest +{ +public: + EncodingsTests() : UnitTest ("Encodings", UnitTestCategories::midi) {} + + void runTest() override + { + beginTest ("7-bit text encoding"); + { + { + const auto converted = Encodings::stringTo7BitText (juce::CharPointer_UTF8 ("Accepted Beat \xe2\x99\xaa")); + const auto expected = makeByteArray ('A', 'c', 'c', 'e', 'p', 't', 'e', 'd', ' ', 'B', 'e', 'a', 't', ' ', '\\', 'u', '2', '6', '6', 'a'); + expect (std::equal (converted.begin(), converted.end(), expected.begin(), expected.end())); + } + + { + const auto converted = Encodings::stringTo7BitText (juce::CharPointer_UTF8 ("\xe6\xae\x8b\xe3\x82\x8a\xe3\x82\x8f\xe3\x81\x9a\xe3\x81\x8b""5\xe3\x83\x90\xe3\x82\xa4\xe3\x83\x88")); + const auto expected = makeByteArray ('\\', 'u', '6', 'b', '8', 'b', + '\\', 'u', '3', '0', '8', 'a', + '\\', 'u', '3', '0', '8', 'f', + '\\', 'u', '3', '0', '5', 'a', + '\\', 'u', '3', '0', '4', 'b', + '5', + '\\', 'u', '3', '0', 'd', '0', + '\\', 'u', '3', '0', 'a', '4', + '\\', 'u', '3', '0', 'c', '8'); + expect (std::equal (converted.begin(), converted.end(), expected.begin(), expected.end())); + } + } + + beginTest ("7-bit text decoding"); + { + { + const auto converted = Encodings::stringFrom7BitText (makeByteArray ('A', 'c', 'c', 'e', 'p', 't', 'e', 'd', ' ', 'B', 'e', 'a', 't', ' ', '\\', 'u', '2', '6', '6', 'a')); + const String expected = juce::CharPointer_UTF8 ("Accepted Beat \xe2\x99\xaa"); + expect (converted == expected); + } + + { + const auto converted = Encodings::stringFrom7BitText (makeByteArray ('\\', 'u', '6', 'b', '8', 'b', + '\\', 'u', '3', '0', '8', 'a', + '\\', 'u', '3', '0', '8', 'f', + '\\', 'u', '3', '0', '5', 'a', + '\\', 'u', '3', '0', '4', 'b', + '5', + '\\', 'u', '3', '0', 'd', '0', + '\\', 'u', '3', '0', 'a', '4', + '\\', 'u', '3', '0', 'c', '8')); + const String expected = juce::CharPointer_UTF8 ("\xe6\xae\x8b\xe3\x82\x8a\xe3\x82\x8f\xe3\x81\x9a\xe3\x81\x8b""5\xe3\x83\x90\xe3\x82\xa4\xe3\x83\x88"); + expect (converted == expected); + } + } + + beginTest ("Mcoded7 encoding"); + { + { + const auto converted = Encodings::toMcoded7 (makeByteArray (0x81, 0x82, 0x83)); + const auto expected = makeByteArray (0x70, 0x01, 0x02, 0x03); + expect (rangesEqual (converted, expected)); + } + + { + const auto converted = Encodings::toMcoded7 (makeByteArray (0x01, 0x82, 0x03, 0x04, 0x85, 0x06, 0x87, 0x08)); + const auto expected = makeByteArray (0x25, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x00, 0x08); + expect (rangesEqual (converted, expected)); + } + } + + beginTest ("Mcoded7 decoding"); + { + { + const auto converted = Encodings::fromMcoded7 (makeByteArray (0x70, 0x01, 0x02, 0x03)); + const auto expected = makeByteArray (0x81, 0x82, 0x83); + expect (rangesEqual (converted, expected)); + } + + { + const auto converted = Encodings::fromMcoded7 (makeByteArray (0x25, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x00, 0x08)); + const auto expected = makeByteArray (0x01, 0x82, 0x03, 0x04, 0x85, 0x06, 0x87, 0x08); + expect (rangesEqual (converted, expected)); + } + } + } + +private: + static bool deepEqual (const std::optional& a, const std::optional& b) + { + if (a.has_value() && b.has_value()) + return JSONUtils::deepEqual (*a, *b); + + return a == b; + } + + template + static bool rangesEqual (A&& a, B&& b) + { + using std::begin, std::end; + return std::equal (begin (a), end (a), begin (b), end (b)); + } + + template + static std::array makeByteArray (Ts&&... ts) + { + jassert (((0 <= (int) ts && (int) ts <= std::numeric_limits::max()) && ...)); + return { std::byte (ts)... }; + } +}; + +static EncodingsTests encodingsTests; + +#endif + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIEncodings.h b/modules/juce_midi_ci/ci/juce_CIEncodings.h new file mode 100644 index 0000000000..0fe6b3ea48 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIEncodings.h @@ -0,0 +1,87 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +/** + Utility functions for working with data formats used by property exchange + messages. + + @tags{Audio} +*/ +struct Encodings +{ + /** Text in ACK and NAK messages can't be utf-8 or ASCII because each byte only has 7 usable bits. + The encoding rules are in section 5.10.4 of the CI spec. + */ + static String stringFrom7BitText (Span bytes); + + /** Text in ACK and NAK messages can't be utf-8 or ASCII because each byte only has 7 usable bits. + The encoding rules are in section 5.10.4 of the CI spec. + */ + static std::vector stringTo7BitText (const String& text); + + /** Converts a list of bytes representing a 7-bit ASCII string to JSON. */ + static var jsonFrom7BitText (Span bytes) + { + return JSON::parse (stringFrom7BitText (bytes)); + } + + /** Converts a JSON object to a list of bytes in 7-bit ASCII format. */ + static std::vector jsonTo7BitText (const var& v) + { + return stringTo7BitText (JSON::toString (v, true)); + } + + /** Each group of seven stored bytes is transmitted as eight bytes. + First, the sign bits of the seven bytes are sent, followed by the low-order 7 bits of each byte. + */ + static std::vector toMcoded7 (Span bytes); + + /** Each group of seven stored bytes is transmitted as eight bytes. + First, the sign bits of the seven bytes are sent, followed by the low-order 7 bits of each byte. + */ + static std::vector fromMcoded7 (Span bytes); + + /** Attempts to encode the provided byte span using the specified encoding. + + The ASCII encoding does not make any changes to the input stream, but + encoding will fail if any byte has its most significant bit set. + */ + static std::optional> tryEncode (Span bytes, + Encoding mutualEncoding); + + /** Decodes the provided byte span using the specified encoding. + + All bytes of the input must be 7-bit values, i.e. all most-significant bits + are unset. + */ + static std::vector decode (Span bytes, Encoding mutualEncoding); + + Encodings() = delete; +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIFunctionBlock.h b/modules/juce_midi_ci/ci/juce_CIFunctionBlock.h new file mode 100644 index 0000000000..793d87afbd --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIFunctionBlock.h @@ -0,0 +1,49 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +/** + Contains information about a MIDI 2.0 function block. + + @tags{Audio} +*/ +struct FunctionBlock +{ + std::byte identifier { 0x7f }; ///< 0x7f == no function block + uint8_t firstGroup = 0; ///< The first group that is part of the block, 0-based + uint8_t numGroups = 1; ///< The number of groups contained in the block + + bool operator== (const FunctionBlock& other) const + { + const auto tie = [] (auto& x) { return std::tie (x.identifier, x.firstGroup, x.numGroups); }; + return tie (*this) == tie (other); + } + + bool operator!= (const FunctionBlock& other) const { return ! operator== (other); } +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIMessages.h b/modules/juce_midi_ci/ci/juce_CIMessages.h new file mode 100644 index 0000000000..ab0236ff21 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIMessages.h @@ -0,0 +1,664 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +//============================================================================== +/** + Byte values representing different addresses within a group. + + @tags{Audio} +*/ +enum class ChannelInGroup : uint8_t +{ + channel0 = 0x0, + channel1 = 0x1, + channel2 = 0x2, + channel3 = 0x3, + channel4 = 0x4, + channel5 = 0x5, + channel6 = 0x6, + channel7 = 0x7, + channel8 = 0x8, + channel9 = 0x9, + channelA = 0xA, + channelB = 0xB, + channelC = 0xC, + channelD = 0xD, + channelE = 0xE, + channelF = 0xF, + wholeGroup = 0x7e, ///< Refers to all channels in the UMP group + wholeBlock = 0x7f, ///< Refers to all channels in the function block that contains the UMP group +}; + +struct ChannelInGroupUtils +{ + ChannelInGroupUtils() = delete; + + /** Converts a ChannelInGroup to a descriptive string. */ + static String toString (ChannelInGroup c) + { + if (c == ChannelInGroup::wholeGroup) + return "Group"; + + if (c == ChannelInGroup::wholeBlock) + return "Function Block"; + + const auto underlying = (std::underlying_type_t) c; + return "Channel " + String (underlying + 1); + } +}; + +using Profile = std::array; + +//============================================================================== +/** + Namespace containing structs representing different kinds of MIDI-CI message. + + @tags{Audio} +*/ +namespace Message +{ + /** Wraps a span, providing equality operators that compare the span + contents elementwise. + */ + template + struct ComparableRange + { + T& data; + + bool operator== (const ComparableRange& other) const + { + return std::equal (data.begin(), data.end(), other.data.begin(), other.data.end()); + } + + bool operator!= (const ComparableRange& other) const { return ! operator== (other); } + }; + + template static constexpr auto makeComparableRange ( T& t) { return ComparableRange< T> { t }; } + template static constexpr auto makeComparableRange (const T& t) { return ComparableRange { t }; } + + //============================================================================== + /** + Holds fields that can be found at the beginning of every MIDI CI message. + */ + struct Header + { + ChannelInGroup deviceID{}; + std::byte category{}; + std::byte version{}; + MUID source = MUID::makeUnchecked (0); + MUID destination = MUID::makeUnchecked (0); + + auto tie() const + { + return std::tuple (deviceID, category, version, source, destination); + } + + bool operator== (const Header& x) const { return tie() == x.tie(); } + bool operator!= (const Header& x) const { return ! operator== (x); } + }; + + /** + Groups together a CI message header, and some number of trailing bytes. + */ + struct Generic + { + Header header; + Span data; + }; + + //============================================================================== + /** See the MIDI-CI specification. */ + struct DiscoveryResponse + { + ump::DeviceInfo device; + std::byte capabilities{}; + uint32_t maximumSysexSize{}; + std::byte outputPathID{}; /**< Only valid if the message header specifies version 0x02 or greater. */ + std::byte functionBlock{}; /**< Only valid if the message header specifies version 0x02 or greater. */ + + auto tie() const + { + return std::tuple (device, capabilities, maximumSysexSize, outputPathID, functionBlock); + } + + bool operator== (const DiscoveryResponse& x) const { return tie() == x.tie(); } + bool operator!= (const DiscoveryResponse& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct Discovery + { + ump::DeviceInfo device; + std::byte capabilities{}; + uint32_t maximumSysexSize{}; + std::byte outputPathID{}; /**< Only valid if the message header specifies version 0x02 or greater. */ + + auto tie() const + { + return std::tuple (device, capabilities, maximumSysexSize, outputPathID); + } + + bool operator== (const Discovery& x) const { return tie() == x.tie(); } + bool operator!= (const Discovery& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct EndpointInquiryResponse + { + std::byte status; + Span data; + + auto tie() const + { + return std::tuple (status, makeComparableRange (data)); + } + + bool operator== (const EndpointInquiryResponse& x) const { return tie() == x.tie(); } + bool operator!= (const EndpointInquiryResponse& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct EndpointInquiry + { + std::byte status; + + auto tie() const + { + return std::tuple (status); + } + + bool operator== (const EndpointInquiry& x) const { return tie() == x.tie(); } + bool operator!= (const EndpointInquiry& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct InvalidateMUID + { + MUID target = MUID::makeUnchecked (0); + + auto tie() const + { + return std::tuple (target); + } + + bool operator== (const InvalidateMUID& x) const { return tie() == x.tie(); } + bool operator!= (const InvalidateMUID& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct ACK + { + std::byte originalCategory{}; + std::byte statusCode{}; + std::byte statusData{}; + std::array details{}; + Span messageText{}; + + /** Convenience function that returns the message's text as a String. */ + String getMessageTextAsString() const + { + return Encodings::stringFrom7BitText (messageText); + } + + auto tie() const + { + return std::tuple (originalCategory, statusCode, statusData, details, makeComparableRange (messageText)); + } + + bool operator== (const ACK& x) const { return tie() == x.tie(); } + bool operator!= (const ACK& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct NAK + { + std::byte originalCategory{}; /**< Only valid if the message header specifies version 0x02 or greater. */ + std::byte statusCode{}; /**< Only valid if the message header specifies version 0x02 or greater. */ + std::byte statusData{}; /**< Only valid if the message header specifies version 0x02 or greater. */ + std::array details{}; /**< Only valid if the message header specifies version 0x02 or greater. */ + Span messageText{}; /**< Only valid if the message header specifies version 0x02 or greater. */ + + /** Convenience function that returns the message's text as a String. */ + String getMessageTextAsString() const + { + return Encodings::stringFrom7BitText (messageText); + } + + auto tie() const + { + return std::tuple (originalCategory, statusCode, statusData, details, makeComparableRange (messageText)); + } + + bool operator== (const NAK& x) const { return tie() == x.tie(); } + bool operator!= (const NAK& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct ProfileInquiryResponse + { + Span enabledProfiles; + Span disabledProfiles; + + auto tie() const + { + return std::tuple (makeComparableRange (enabledProfiles), makeComparableRange (disabledProfiles)); + } + + bool operator== (const ProfileInquiryResponse& x) const { return tie() == x.tie(); } + bool operator!= (const ProfileInquiryResponse& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct ProfileInquiry + { + auto tie() const + { + return std::tuple(); + } + + bool operator== (const ProfileInquiry& x) const { return tie() == x.tie(); } + bool operator!= (const ProfileInquiry& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct ProfileAdded + { + Profile profile{}; + + auto tie() const + { + return std::tuple (profile); + } + + bool operator== (const ProfileAdded& x) const { return tie() == x.tie(); } + bool operator!= (const ProfileAdded& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct ProfileRemoved + { + Profile profile{}; + + auto tie() const + { + return std::tuple (profile); + } + + bool operator== (const ProfileRemoved& x) const { return tie() == x.tie(); } + bool operator!= (const ProfileRemoved& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct ProfileDetailsResponse + { + Profile profile{}; + std::byte target{}; + Span data; + + auto tie() const + { + return std::tuple (profile, target, makeComparableRange (data)); + } + + bool operator== (const ProfileDetailsResponse& x) const { return tie() == x.tie(); } + bool operator!= (const ProfileDetailsResponse& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct ProfileDetails + { + Profile profile{}; + std::byte target{}; + + auto tie() const + { + return std::tuple (profile, target); + } + + bool operator== (const ProfileDetails& x) const { return tie() == x.tie(); } + bool operator!= (const ProfileDetails& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct ProfileOn + { + Profile profile{}; + uint16_t numChannels{}; /**< Only valid if the message header specifies version 0x02 or greater. */ + + auto tie() const + { + return std::tuple (profile, numChannels); + } + + bool operator== (const ProfileOn& x) const { return tie() == x.tie(); } + bool operator!= (const ProfileOn& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct ProfileOff + { + Profile profile{}; + + auto tie() const + { + return std::tuple (profile); + } + + bool operator== (const ProfileOff& x) const { return tie() == x.tie(); } + bool operator!= (const ProfileOff& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct ProfileEnabledReport + { + Profile profile{}; + uint16_t numChannels{}; /**< Only valid if the message header specifies version 0x02 or greater. */ + + auto tie() const + { + return std::tuple (profile, numChannels); + } + + bool operator== (const ProfileEnabledReport& x) const { return tie() == x.tie(); } + bool operator!= (const ProfileEnabledReport& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct ProfileDisabledReport + { + Profile profile{}; + uint16_t numChannels{}; /**< Only valid if the message header specifies version 0x02 or greater. */ + + auto tie() const + { + return std::tuple (profile, numChannels); + } + + bool operator== (const ProfileDisabledReport& x) const { return tie() == x.tie(); } + bool operator!= (const ProfileDisabledReport& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct ProfileSpecificData + { + Profile profile{}; + Span data; + + auto tie() const + { + return std::tuple (profile, makeComparableRange (data)); + } + + bool operator== (const ProfileSpecificData& x) const { return tie() == x.tie(); } + bool operator!= (const ProfileSpecificData& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct PropertyExchangeCapabilitiesResponse + { + std::byte numSimultaneousRequestsSupported{}; + std::byte majorVersion{}; /**< Only valid if the message header specifies version 0x02 or greater. */ + std::byte minorVersion{}; /**< Only valid if the message header specifies version 0x02 or greater. */ + + auto tie() const + { + return std::tuple (numSimultaneousRequestsSupported, majorVersion, minorVersion); + } + + bool operator== (const PropertyExchangeCapabilitiesResponse& x) const { return tie() == x.tie(); } + bool operator!= (const PropertyExchangeCapabilitiesResponse& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct PropertyExchangeCapabilities + { + std::byte numSimultaneousRequestsSupported{}; + std::byte majorVersion{}; /**< Only valid if the message header specifies version 0x02 or greater. */ + std::byte minorVersion{}; /**< Only valid if the message header specifies version 0x02 or greater. */ + + auto tie() const + { + return std::tuple (numSimultaneousRequestsSupported, majorVersion, minorVersion); + } + + bool operator== (const PropertyExchangeCapabilities& x) const { return tie() == x.tie(); } + bool operator!= (const PropertyExchangeCapabilities& x) const { return ! operator== (x); } + }; + + /** A property-exchange message that has no payload, and must therefore + be contained in a single chunk. + */ + struct StaticSizePropertyExchange + { + std::byte requestID{}; + Span header; + + auto tie() const + { + return std::tuple (requestID, makeComparableRange (header)); + } + }; + + /** A property-exchange message that may form part of a multi-chunk + message sequence. + */ + struct DynamicSizePropertyExchange + { + std::byte requestID{}; + Span header; + uint16_t totalNumChunks{}; + uint16_t thisChunkNum{}; + Span data; + + auto tie() const + { + return std::tuple (requestID, + makeComparableRange (header), + totalNumChunks, + thisChunkNum, + makeComparableRange (data)); + } + }; + + /** See the MIDI-CI specification. */ + struct PropertyGetDataResponse : public DynamicSizePropertyExchange + { + bool operator== (const PropertyGetDataResponse& x) const { return tie() == x.tie(); } + bool operator!= (const PropertyGetDataResponse& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct PropertyGetData : public StaticSizePropertyExchange + { + bool operator== (const PropertyGetData& x) const { return tie() == x.tie(); } + bool operator!= (const PropertyGetData& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct PropertySetDataResponse : public StaticSizePropertyExchange + { + bool operator== (const PropertySetDataResponse& x) const { return tie() == x.tie(); } + bool operator!= (const PropertySetDataResponse& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct PropertySetData : public DynamicSizePropertyExchange + { + bool operator== (const PropertySetData& x) const { return tie() == x.tie(); } + bool operator!= (const PropertySetData& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct PropertySubscribeResponse : public DynamicSizePropertyExchange + { + bool operator== (const PropertySubscribeResponse& x) const { return tie() == x.tie(); } + bool operator!= (const PropertySubscribeResponse& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct PropertySubscribe : public DynamicSizePropertyExchange + { + bool operator== (const PropertySubscribe& x) const { return tie() == x.tie(); } + bool operator!= (const PropertySubscribe& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct PropertyNotify : public DynamicSizePropertyExchange + { + bool operator== (const PropertyNotify& x) const { return tie() == x.tie(); } + bool operator!= (const PropertyNotify& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct ProcessInquiryResponse + { + std::byte supportedFeatures{}; + + auto tie() const + { + return std::tuple (supportedFeatures); + } + + bool operator== (const ProcessInquiryResponse& x) const { return tie() == x.tie(); } + bool operator!= (const ProcessInquiryResponse& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct ProcessInquiry + { + auto tie() const + { + return std::tuple(); + } + + bool operator== (const ProcessInquiry& x) const { return tie() == x.tie(); } + bool operator!= (const ProcessInquiry& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct ProcessMidiMessageReportResponse + { + std::byte messageDataControl{}; + std::byte requestedMessages{}; + std::byte channelControllerMessages{}; + std::byte noteDataMessages{}; + + auto tie() const + { + return std::tuple (messageDataControl, requestedMessages, channelControllerMessages, noteDataMessages); + } + + bool operator== (const ProcessMidiMessageReportResponse& x) const { return tie() == x.tie(); } + bool operator!= (const ProcessMidiMessageReportResponse& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct ProcessMidiMessageReport + { + std::byte messageDataControl{}; + std::byte requestedMessages{}; + std::byte channelControllerMessages{}; + std::byte noteDataMessages{}; + + auto tie() const + { + return std::tuple (messageDataControl, requestedMessages, channelControllerMessages, noteDataMessages); + } + + bool operator== (const ProcessMidiMessageReport& x) const { return tie() == x.tie(); } + bool operator!= (const ProcessMidiMessageReport& x) const { return ! operator== (x); } + }; + + /** See the MIDI-CI specification. */ + struct ProcessEndMidiMessageReport + { + auto tie() const + { + return std::tuple(); + } + + bool operator== (const ProcessEndMidiMessageReport& x) const { return tie() == x.tie(); } + bool operator!= (const ProcessEndMidiMessageReport& x) const { return ! operator== (x); } + }; + + /** + A message with a header and optional body. + + The body may be set to std::monostate to indicate some kind of failure, such as a malformed + incoming message. + */ + struct Parsed + { + using Body = std::variant; + + Header header; + Body body; + + bool operator== (const Parsed& other) const + { + const auto tie = [] (const auto& x) { return std::tie (x.header, x.body); }; + return tie (*this) == tie (other); + } + + bool operator!= (const Parsed& other) const { return ! operator== (other); } + }; +} + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIMuid.h b/modules/juce_midi_ci/ci/juce_CIMuid.h new file mode 100644 index 0000000000..84f742827e --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIMuid.h @@ -0,0 +1,83 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +/** + A 28-bit ID that uniquely identifies a device taking part in a series of + MIDI-CI transactions. + + @tags{Audio} +*/ +class MUID +{ + constexpr explicit MUID (uint32_t v) : value (v) {} + + // 0x0fffff00 to 0x0ffffffe are reserved, 0x0fffffff is 'broadcast' + static constexpr uint32_t userMuidEnd = 0x0fffff00; + static constexpr uint32_t mask = 0x0fffffff; + uint32_t value{}; + +public: + /** Returns the ID as a plain integer. */ + constexpr uint32_t get() const { return value; } + + /** Converts the provided integer to a MUID without validation that it + is within the allowed range. + */ + static MUID makeUnchecked (uint32_t v) + { + // If this is hit, the MUID has too many bits set! + jassert ((v & mask) == v); + return MUID (v); + } + + /** Returns a MUID if the provided value is within the valid range for + MUID values; otherwise returns nullopt. + */ + static std::optional make (uint32_t v) + { + if ((v & mask) == v) + return makeUnchecked (v); + + return {}; + } + + /** Makes a random MUID using the provided random engine. */ + static MUID makeRandom (Random& r) + { + return makeUnchecked ((uint32_t) r.nextInt (userMuidEnd)); + } + + bool operator== (const MUID other) const { return value == other.value; } + bool operator!= (const MUID other) const { return value != other.value; } + bool operator< (const MUID other) const { return value < other.value; } + + /** Returns the special MUID representing the broadcast address. */ + static constexpr MUID getBroadcast() { return MUID { mask }; } +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIParser.cpp b/modules/juce_midi_ci/ci/juce_CIParser.cpp new file mode 100644 index 0000000000..03b2eae9f3 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIParser.cpp @@ -0,0 +1,448 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +std::optional Parser::parse (Span message, Status* status) +{ + const auto setStatus = [&] (Status s) + { + if (status != nullptr) + *status = s; + }; + + setStatus (Status::noError); + + Message::Generic generic; + + if (! detail::Marshalling::Reader { message } (generic)) + { + // Got a full sysex message, but it didn't contain a well-formed header. + setStatus (Status::malformed); + return {}; + } + + if ((generic.header.version & std::byte { 0x70 }) != std::byte{}) + { + setStatus (Status::reservedVersion); + return Message::Parsed { generic.header, std::monostate{} }; + } + + const auto index = (uint8_t) generic.header.category; + constexpr auto tables = detail::MessageTypeUtils::getTables(); + const auto processFunction = tables.parsers[index]; + return Message::Parsed { generic.header, processFunction (generic, status) }; +} + +std::optional Parser::parse (const MUID ourMUID, + Span message, + Status* status) +{ + const auto setStatus = [&] (Status s) + { + if (status != nullptr) + *status = s; + }; + + setStatus (Status::noError); + + if (const auto parsed = parse (message, status)) + { + if (parsed->header.destination != MUID::getBroadcast() && parsed->header.destination != ourMUID) + setStatus (Status::mismatchedMUID); + else if (parsed->header.source == ourMUID) + setStatus (Status::collidingMUID); + else if ((parsed->header.version & std::byte { 0x70 }) != std::byte{}) + setStatus (Status::reservedVersion); + + return parsed; + } + + return {}; +} + +class DescriptionVisitor : public detail::MessageTypeUtils::MessageVisitor +{ +public: + DescriptionVisitor (const Message::Parsed* m, String* str) : msg (m), result (str) {} + + void visit (const std::monostate&) const override { *result = "!! Unrecognised !!"; } + void visit (const Message::Discovery& body) const override { visitImpl (body); } + void visit (const Message::DiscoveryResponse& body) const override { visitImpl (body); } + void visit (const Message::InvalidateMUID& body) const override { visitImpl (body); } + void visit (const Message::EndpointInquiry& body) const override { visitImpl (body); } + void visit (const Message::EndpointInquiryResponse& body) const override { visitImpl (body); } + void visit (const Message::ACK& body) const override { visitImpl (body); } + void visit (const Message::NAK& body) const override { visitImpl (body); } + void visit (const Message::ProfileInquiry& body) const override { visitImpl (body); } + void visit (const Message::ProfileInquiryResponse& body) const override { visitImpl (body); } + void visit (const Message::ProfileAdded& body) const override { visitImpl (body); } + void visit (const Message::ProfileRemoved& body) const override { visitImpl (body); } + void visit (const Message::ProfileDetails& body) const override { visitImpl (body); } + void visit (const Message::ProfileDetailsResponse& body) const override { visitImpl (body); } + void visit (const Message::ProfileOn& body) const override { visitImpl (body); } + void visit (const Message::ProfileOff& body) const override { visitImpl (body); } + void visit (const Message::ProfileEnabledReport& body) const override { visitImpl (body); } + void visit (const Message::ProfileDisabledReport& body) const override { visitImpl (body); } + void visit (const Message::ProfileSpecificData& body) const override { visitImpl (body); } + void visit (const Message::PropertyExchangeCapabilities& body) const override { visitImpl (body); } + void visit (const Message::PropertyExchangeCapabilitiesResponse& body) const override { visitImpl (body); } + void visit (const Message::PropertyGetData& body) const override { visitImpl (body); } + void visit (const Message::PropertyGetDataResponse& body) const override { visitImpl (body); } + void visit (const Message::PropertySetData& body) const override { visitImpl (body); } + void visit (const Message::PropertySetDataResponse& body) const override { visitImpl (body); } + void visit (const Message::PropertySubscribe& body) const override { visitImpl (body); } + void visit (const Message::PropertySubscribeResponse& body) const override { visitImpl (body); } + void visit (const Message::PropertyNotify& body) const override { visitImpl (body); } + void visit (const Message::ProcessInquiry& body) const override { visitImpl (body); } + void visit (const Message::ProcessInquiryResponse& body) const override { visitImpl (body); } + void visit (const Message::ProcessMidiMessageReport& body) const override { visitImpl (body); } + void visit (const Message::ProcessMidiMessageReportResponse& body) const override { visitImpl (body); } + void visit (const Message::ProcessEndMidiMessageReport& body) const override { visitImpl (body); } + +private: + static const char* getDescription (const Message::Discovery&) { return "Discovery"; } + static const char* getDescription (const Message::DiscoveryResponse&) { return "Discovery Response"; } + static const char* getDescription (const Message::InvalidateMUID&) { return "Invalidate MUID"; } + static const char* getDescription (const Message::EndpointInquiry&) { return "Endpoint"; } + static const char* getDescription (const Message::EndpointInquiryResponse&) { return "Endpoint Response"; } + static const char* getDescription (const Message::ACK&) { return "ACK"; } + static const char* getDescription (const Message::NAK&) { return "NAK"; } + static const char* getDescription (const Message::ProfileInquiry&) { return "Profile Inquiry"; } + static const char* getDescription (const Message::ProfileInquiryResponse&) { return "Profile Inquiry Response"; } + static const char* getDescription (const Message::ProfileAdded&) { return "Profile Added"; } + static const char* getDescription (const Message::ProfileRemoved&) { return "Profile Removed"; } + static const char* getDescription (const Message::ProfileDetails&) { return "Profile Details"; } + static const char* getDescription (const Message::ProfileDetailsResponse&) { return "Profile Details Response"; } + static const char* getDescription (const Message::ProfileOn&) { return "Profile On"; } + static const char* getDescription (const Message::ProfileOff&) { return "Profile Off"; } + static const char* getDescription (const Message::ProfileEnabledReport&) { return "Profile Enabled Report"; } + static const char* getDescription (const Message::ProfileDisabledReport&) { return "Profile Disabled Report"; } + static const char* getDescription (const Message::ProfileSpecificData&) { return "Profile Specific Data"; } + static const char* getDescription (const Message::PropertyExchangeCapabilities&) { return "Property Exchange Capabilities"; } + static const char* getDescription (const Message::PropertyExchangeCapabilitiesResponse&) { return "Property Exchange Capabilities Response"; } + static const char* getDescription (const Message::PropertyGetData&) { return "Property Get Data"; } + static const char* getDescription (const Message::PropertyGetDataResponse&) { return "Property Get Data Response"; } + static const char* getDescription (const Message::PropertySetData&) { return "Property Set Data"; } + static const char* getDescription (const Message::PropertySetDataResponse&) { return "Property Set Data Response"; } + static const char* getDescription (const Message::PropertySubscribe&) { return "Property Subscribe"; } + static const char* getDescription (const Message::PropertySubscribeResponse&) { return "Property Subscribe Response"; } + static const char* getDescription (const Message::PropertyNotify&) { return "Property Notify"; } + static const char* getDescription (const Message::ProcessInquiry&) { return "Process Inquiry"; } + static const char* getDescription (const Message::ProcessInquiryResponse&) { return "Process Inquiry Response"; } + static const char* getDescription (const Message::ProcessMidiMessageReport&) { return "Process Midi Message Report"; } + static const char* getDescription (const Message::ProcessMidiMessageReportResponse&) { return "Process Midi Message Report Response"; } + static const char* getDescription (const Message::ProcessEndMidiMessageReport&) { return "Process End Midi Message Report"; } + + template + void visitImpl (const Body& body) const + { + const auto opts = ToVarOptions{}.withExplicitVersion ((int) msg->header.version) + .withVersionIncluded (false); + const auto json = ToVar::convert (body, opts); + + if (json.has_value()) + *result = String (getDescription (body)) + ": " + JSON::toString (*json, true); + } + + const Message::Parsed* msg = nullptr; + String* result = nullptr; +}; + +String Parser::getMessageDescription (const Message::Parsed& message) +{ + String result; + detail::MessageTypeUtils::visit (message, DescriptionVisitor { &message, &result }); + return result; +} + +//============================================================================== +//============================================================================== +#if JUCE_UNIT_TESTS + +class ParserTests : public UnitTest +{ +public: + ParserTests() : UnitTest ("Parser", UnitTestCategories::midi) {} + + void runTest() override + { + auto random = getRandom(); + + beginTest ("Sending an empty message does nothing"); + { + const auto parsed = Parser::parse (MUID::makeRandom (random), {}); + expect (parsed == std::nullopt); + } + + beginTest ("Sending a garbage message does nothing"); + { + const std::vector bytes (128, std::byte { 0x70 }); + const auto parsed = Parser::parse (MUID::makeRandom (random), bytes); + expect (parsed == std::nullopt); + } + + beginTest ("Sending a message with truncated body produces a malformed status"); + { + constexpr auto version1 = 0x01; + const auto truncatedV1 = makeByteArray (0x7e, + /* to function block */ 0x7f, + /* midi CI */ 0x0d, + /* discovery message */ 0x70, + /* version */ version1, + /* source MUID */ 0x01, + /* ... */ 0x02, + /* ... */ 0x03, + /* ... */ 0x04, + /* broadcast MUID */ 0x7f, + /* ... */ 0x7f, + /* ... */ 0x7f, + /* ... */ 0x7f, + /* manufacturer */ 0x10, + /* ... */ 0x11, + /* ... */ 0x12, + /* family */ 0x20, + /* ... */ 0x21, + /* model */ 0x30, + /* ... */ 0x31, + /* revision */ 0x40, + /* ... */ 0x41, + /* ... */ 0x42, + /* ... */ 0x43, + /* CI category supported */ 0x7f, + /* max sysex size */ 0x7f, + /* ... */ 0x7f, + /* ... */ 0x7f); + /* Missing final byte for a version 1 message */ + Parser::Status status{}; + const auto parsedV1 = Parser::parse (MUID::makeRandom (random), truncatedV1, &status); + + expect (status == Parser::Status::malformed); + expect (parsedV1 == Message::Parsed { Message::Header { ChannelInGroup::wholeBlock, + std::byte { 0x70 }, + std::byte { version1 }, + MUID::makeUnchecked (0x80c101), + MUID::getBroadcast() }, + std::monostate{} }); + + constexpr auto version2 = 0x02; + const auto truncatedV2 = makeByteArray (0x7e, + /* to function block */ 0x7f, + /* midi CI */ 0x0d, + /* discovery message */ 0x70, + /* version */ version2, + /* source MUID */ 0x01, + /* ... */ 0x02, + /* ... */ 0x03, + /* ... */ 0x04, + /* broadcast MUID */ 0x7f, + /* ... */ 0x7f, + /* ... */ 0x7f, + /* ... */ 0x7f, + /* manufacturer */ 0x10, + /* ... */ 0x11, + /* ... */ 0x12, + /* family */ 0x20, + /* ... */ 0x21, + /* model */ 0x30, + /* ... */ 0x31, + /* revision */ 0x40, + /* ... */ 0x41, + /* ... */ 0x42, + /* ... */ 0x43, + /* CI category supported */ 0x7f, + /* max sysex size */ 0x7f, + /* ... */ 0x7f, + /* ... */ 0x7f, + /* ... */ 0x7f); + /* Missing final byte for a version 2 message */ + const auto parsedV2 = Parser::parse (MUID::makeRandom (random), truncatedV2); + + expect (status == Parser::Status::malformed); + expect (parsedV2 == Message::Parsed { Message::Header { ChannelInGroup::wholeBlock, + std::byte { 0x70 }, + std::byte { version2 }, + MUID::makeUnchecked (0x80c101), + MUID::getBroadcast() }, + std::monostate{} }); + } + + const auto getExpectedDiscoveryInput = [] (uint8_t version, uint8_t outputPathID) + { + return Message::Parsed { Message::Header { ChannelInGroup::wholeBlock, + std::byte { 0x70 }, + std::byte { version }, + MUID::makeUnchecked (0x80c101), + MUID::getBroadcast() }, + Message::Discovery { { { std::byte { 0x10 }, std::byte { 0x11 }, std::byte { 0x12 } }, + { std::byte { 0x20 }, std::byte { 0x21 } }, + { std::byte { 0x30 }, std::byte { 0x31 } }, + { std::byte { 0x40 }, std::byte { 0x41 }, std::byte { 0x42 }, std::byte { 0x43 } } }, + std::byte { 0x7f }, + 0xfffffff, + std::byte { outputPathID } } }; + }; + + beginTest ("Sending a V1 discovery message notifies the input listener"); + { + const auto initialMUID = MUID::makeRandom (random); + constexpr uint8_t version = 0x01; + + const auto bytes = makeByteArray (0x7e, + /* to function block */ 0x7f, + /* midi CI */ 0x0d, + /* discovery message */ 0x70, + /* version */ version, + /* source MUID */ 0x01, + /* ... */ 0x02, + /* ... */ 0x03, + /* ... */ 0x04, + /* broadcast MUID */ 0x7f, + /* ... */ 0x7f, + /* ... */ 0x7f, + /* ... */ 0x7f, + /* manufacturer */ 0x10, + /* ... */ 0x11, + /* ... */ 0x12, + /* family */ 0x20, + /* ... */ 0x21, + /* model */ 0x30, + /* ... */ 0x31, + /* revision */ 0x40, + /* ... */ 0x41, + /* ... */ 0x42, + /* ... */ 0x43, + /* CI category supported */ 0x7f, + /* max sysex size */ 0x7f, + /* ... */ 0x7f, + /* ... */ 0x7f, + /* ... */ 0x7f); + const auto parsed = Parser::parse (initialMUID, bytes); + + expect (parsed == getExpectedDiscoveryInput (version, 0)); + } + + beginTest ("Sending a V2 discovery message notifies the input listener"); + { + constexpr uint8_t outputPathID = 5; + const auto initialMUID = MUID::makeRandom (random); + constexpr uint8_t version = 0x02; + + const auto bytes = makeByteArray (0x7e, + /* to function block */ 0x7f, + /* midi CI */ 0x0d, + /* discovery message */ 0x70, + /* version */ version, + /* source MUID */ 0x01, + /* ... */ 0x02, + /* ... */ 0x03, + /* ... */ 0x04, + /* broadcast MUID */ 0x7f, + /* ... */ 0x7f, + /* ... */ 0x7f, + /* ... */ 0x7f, + /* manufacturer */ 0x10, + /* ... */ 0x11, + /* ... */ 0x12, + /* family */ 0x20, + /* ... */ 0x21, + /* model */ 0x30, + /* ... */ 0x31, + /* revision */ 0x40, + /* ... */ 0x41, + /* ... */ 0x42, + /* ... */ 0x43, + /* CI category supported */ 0x7f, + /* max sysex size */ 0x7f, + /* ... */ 0x7f, + /* ... */ 0x7f, + /* ... */ 0x7f, + /* output path ID */ outputPathID); + const auto parsed = Parser::parse (initialMUID, bytes); + + expect (parsed == getExpectedDiscoveryInput (version, outputPathID)); + } + + beginTest ("Sending a discovery message with a future version notifies the input listener and ignores trailing fields"); + { + constexpr uint8_t outputPathID = 10; + const auto initialMUID = MUID::makeRandom (random); + constexpr auto version = (uint8_t) detail::MessageMeta::implementationVersion + 1; + + const auto bytes = makeByteArray (0x7e, + /* to function block */ 0x7f, + /* midi CI */ 0x0d, + /* discovery message */ 0x70, + /* version */ version, + /* source MUID */ 0x01, + /* ... */ 0x02, + /* ... */ 0x03, + /* ... */ 0x04, + /* broadcast MUID */ 0x7f, + /* ... */ 0x7f, + /* ... */ 0x7f, + /* ... */ 0x7f, + /* manufacturer */ 0x10, + /* ... */ 0x11, + /* ... */ 0x12, + /* family */ 0x20, + /* ... */ 0x21, + /* model */ 0x30, + /* ... */ 0x31, + /* revision */ 0x40, + /* ... */ 0x41, + /* ... */ 0x42, + /* ... */ 0x43, + /* CI category supported */ 0x7f, + /* max sysex size */ 0x7f, + /* ... */ 0x7f, + /* ... */ 0x7f, + /* ... */ 0x7f, + /* output path ID */ outputPathID, + /* extra bytes */ 0x00, + /* ... */ 0x00, + /* ... */ 0x00, + /* ... */ 0x00); + const auto parsed = Parser::parse (initialMUID, bytes); + + expect (parsed == getExpectedDiscoveryInput (version, outputPathID)); + } + } + +private: + template + static std::array makeByteArray (Ts&&... ts) + { + jassert (((0 <= (int) ts && (int) ts <= std::numeric_limits::max()) && ...)); + return { std::byte (ts)... }; + } +}; + +static ParserTests parserTests; + +#endif + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIParser.h b/modules/juce_midi_ci/ci/juce_CIParser.h new file mode 100644 index 0000000000..fec4a49df3 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIParser.h @@ -0,0 +1,80 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +/** + Parses CI messages. + + @tags{Audio} +*/ +class Parser +{ +public: + Parser() = delete; + + enum class Status + { + noError, ///< Parsing was successful + mismatchedMUID, ///< The message destination MUID doesn't match the provided MUID + collidingMUID, ///< The message source MUID matches the provided MUID + unrecognisedMessage, ///< The message ID doesn't correspond to a known message + reservedVersion, ///< The MIDI CI version uses an unrecognised major version + malformed, ///< The message (whole message, or just body) could not be parsed + }; + + /** Parses the provided message; + + Call this with a full CI message. Don't include any "extra" bytes such as + the leading/trailing 0xf0/0xf7 for messages that were originally in bytestream midi format, + or the packet-header bytes from UMP-formatted sysex messages. + + Returns nullopt if the message doesn't need to be acknowledged by the entity with the provided MUID, + or if the message is malformed. + Otherwise, returns a parsed header, and optionally a body. + If the body is std::monostate, then something went wrong while parsing. For example, the body + may be malformed, or the CI version might be unrecognised. + */ + static std::optional parse (MUID ourMUID, Span message, Status* = nullptr); + + /** Parses the provided message; + + Call this with a full CI message. Don't include any "extra" bytes such as + the leading/trailing 0xf0/0xf7 for messages that were originally in bytestream midi format, + or the packet-header bytes from UMP-formatted sysex messages. + + Returns nullopt if the message is malformed. + Otherwise, returns a parsed header, and optionally a body. + If the body is std::monostate, then something went wrong while parsing. For example, the body + may be malformed, or the CI version might be unrecognised. + */ + static std::optional parse (Span message, Status* = nullptr); + + /** Returns a human-readable string describing the message. */ + static String getMessageDescription (const Message::Parsed& message); +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIProfileAtAddress.h b/modules/juce_midi_ci/ci/juce_CIProfileAtAddress.h new file mode 100644 index 0000000000..796be0e432 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIProfileAtAddress.h @@ -0,0 +1,51 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +/** + Holds a profile ID, and the address of a group/channel. + + @tags{Audio} +*/ +class ProfileAtAddress +{ + auto tie() const { return std::tie (profile, address); } + +public: + Profile profile; ///< The id of a MIDI-CI profile + ChannelAddress address; ///< A group and channel + + bool operator== (const ProfileAtAddress& x) const { return tie() == x.tie(); } + bool operator!= (const ProfileAtAddress& x) const { return tie() != x.tie(); } + + bool operator< (const ProfileAtAddress& x) const { return tie() < x.tie(); } + bool operator<= (const ProfileAtAddress& x) const { return tie() <= x.tie(); } + bool operator> (const ProfileAtAddress& x) const { return tie() > x.tie(); } + bool operator>= (const ProfileAtAddress& x) const { return tie() >= x.tie(); } +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIProfileDelegate.h b/modules/juce_midi_ci/ci/juce_CIProfileDelegate.h new file mode 100644 index 0000000000..d33f5246f0 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIProfileDelegate.h @@ -0,0 +1,57 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +/** + An interface with methods that can be overridden to customise how a Device + implementing profiles responds to profile inquiries. + + @tags{Audio} +*/ +struct ProfileDelegate +{ + ProfileDelegate() = default; + virtual ~ProfileDelegate() = default; + ProfileDelegate (const ProfileDelegate&) = default; + ProfileDelegate (ProfileDelegate&&) = default; + ProfileDelegate& operator= (const ProfileDelegate&) = default; + ProfileDelegate& operator= (ProfileDelegate&&) = default; + + /** Called when a remote device requests that a profile is enabled or disabled. + + Old MIDI-CI implementations on remote devices may request that a profile + is enabled with zero channels active - in this situation, it is + recommended that you use ProfileHost::enableProfile to enable the + default number of channels for that profile. + */ + virtual void profileEnablementRequested ([[maybe_unused]] MUID x, + [[maybe_unused]] ProfileAtAddress profileAtAddress, + [[maybe_unused]] int numChannels, + [[maybe_unused]] bool enabled) = 0; +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIProfileHost.cpp b/modules/juce_midi_ci/ci/juce_CIProfileHost.cpp new file mode 100644 index 0000000000..74a2e1dce7 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIProfileHost.cpp @@ -0,0 +1,336 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +class ProfileHost::Visitor : public detail::MessageTypeUtils::MessageVisitor +{ +public: + Visitor (ProfileHost* h, ResponderOutput* o, bool* b) + : host (h), output (o), handled (b) {} + + void visit (const Message::ProfileInquiry& body) const override { visitImpl (body); } + void visit (const Message::ProfileDetails& body) const override { visitImpl (body); } + void visit (const Message::ProfileOn& body) const override { visitImpl (body); } + void visit (const Message::ProfileOff& body) const override { visitImpl (body); } + using MessageVisitor::visit; + + static auto getNumChannels (Message::Header header, Message::ProfileOn p) + { + return (uint8_t) header.version >= 2 ? p.numChannels : 1; + } + + static auto getNumChannels (Message::Header, Message::ProfileOff) { return 0; } + +private: + template + void visitImpl (const Body& body) const { *handled = messageReceived (body); } + + bool messageReceived (const Message::ProfileInquiry&) const + { + host->isResponder = true; + + if ((uint8_t) output->getIncomingHeader().deviceID < 16 + || output->getIncomingHeader().deviceID == ChannelInGroup::wholeGroup) + { + if (const auto* state = host->getProfileStates().groupStates[output->getIncomingGroup()].getStateForDestination (output->getIncomingHeader().deviceID)) + { + const auto active = state->getActive(); + const auto inactive = state->getInactive(); + detail::MessageTypeUtils::send (*output, Message::ProfileInquiryResponse { active, inactive }); + } + } + else if (output->getIncomingHeader().deviceID == ChannelInGroup::wholeBlock) + { + auto header = output->getReplyHeader (detail::MessageMeta::Meta::subID2); + + const auto sendIfNonEmpty = [&] (const auto group, const auto& state) + { + if (! state.empty()) + { + const auto active = state.getActive(); + const auto inactive = state.getInactive(); + detail::MessageTypeUtils::send (*output, (uint8_t) group, header, Message::ProfileInquiryResponse { active, inactive }); + } + }; + + for (auto groupNum = 0; groupNum < host->functionBlock.numGroups; ++groupNum) + { + const auto group = host->functionBlock.firstGroup + groupNum; + const auto& groupState = host->getProfileStates().groupStates[(size_t) group]; + + for (size_t channel = 0; channel < groupState.channelStates.size(); ++channel) + { + header.deviceID = ChannelInGroup (channel); + sendIfNonEmpty (group, groupState.channelStates[channel]); + } + } + + header.deviceID = ChannelInGroup::wholeGroup; + + for (auto i = 0; i < host->functionBlock.numGroups; ++i) + { + const auto group = host->functionBlock.firstGroup + i; + const auto& groupState = host->getProfileStates().groupStates[(size_t) group]; + sendIfNonEmpty (group, groupState.groupState); + } + + // Always send the block response to indicate that no further replies will follow + header.deviceID = ChannelInGroup::wholeBlock; + const auto state = host->getProfileStates().blockState; + const auto active = state.getActive(); + const auto inactive = state.getInactive(); + detail::MessageTypeUtils::send (*output, output->getIncomingGroup(), header, Message::ProfileInquiryResponse { active, inactive }); + } + + return true; + } + + bool messageReceived (const Message::ProfileDetails& body) const + { + if (body.target == std::byte{}) + { + const auto address = ChannelAddress{}.withGroup (output->getIncomingGroup()) + .withChannel (output->getIncomingHeader().deviceID); + const ProfileAtAddress profileAtAddress { body.profile, address }; + const auto state = host->getState (profileAtAddress); + std::vector extraData; + detail::Marshalling::Writer { extraData } (state.active, state.supported); + detail::MessageTypeUtils::send (*output, Message::ProfileDetailsResponse { body.profile, body.target, extraData }); + } + else + { + detail::MessageTypeUtils::sendNAK (*output, std::byte { 0x04 }); + } + + return true; + } + + template + bool profileEnablementReceived (const Body& request) const + { + const auto destination = ChannelAddress{}.withGroup (output->getIncomingGroup()) + .withChannel (output->getIncomingHeader().deviceID); + if (auto* state = host->states.getStateForDestination (destination)) + { + if (state->get (request.profile).isSupported()) + { + const auto address = ChannelAddress{}.withGroup (output->getIncomingGroup()) + .withChannel (output->getIncomingHeader().deviceID); + const ProfileAtAddress profileAtAddress { request.profile, address }; + + { + const ScopedValueSetter scope { host->currentEnablementMessage, + std::optional (profileAtAddress) }; + host->delegate.profileEnablementRequested (output->getIncomingHeader().source, + profileAtAddress, + getNumChannels (output->getIncomingHeader(), request), + std::is_same_v); + } + + const auto currentState = host->getState (profileAtAddress); + + const auto sendResponse = [&] (auto response) + { + const Message::Header header + { + profileAtAddress.address.getChannel(), + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + output->getMuid(), + MUID::getBroadcast(), + }; + + detail::MessageTypeUtils::send (*output, profileAtAddress.address.getGroup(), header, response); + }; + + if (currentState.isActive()) + sendResponse (Message::ProfileEnabledReport { profileAtAddress.profile, currentState.active }); + else + sendResponse (Message::ProfileDisabledReport { profileAtAddress.profile, 0 }); + + host->isResponder = true; + return true; + } + } + + detail::MessageTypeUtils::sendNAK (*output, {}); + return true; + } + + bool messageReceived (const Message::ProfileOn& request) const + { + return profileEnablementReceived (request); + } + + bool messageReceived (const Message::ProfileOff& request) const + { + return profileEnablementReceived (request); + } + + ProfileHost* host = nullptr; + ResponderOutput* output = nullptr; + bool* handled = nullptr; +}; + +void ProfileHost::addProfile (ProfileAtAddress profileAtAddress, int maxNumChannels) +{ + auto* state = states.getStateForDestination (profileAtAddress.address); + + if (state == nullptr || state->get (profileAtAddress.profile).isSupported()) + return; + + // There are only 256 channels on a UMP endpoint, so requesting more probably doesn't make sense! + jassert (maxNumChannels <= 256); + + state->set (profileAtAddress.profile, { (uint16_t) maxNumChannels, 0 }); + + if (! isResponder || profileAtAddress == currentEnablementMessage) + return; + + const Message::Header header + { + profileAtAddress.address.getChannel(), + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + output.getMuid(), + MUID::getBroadcast(), + }; + + detail::MessageTypeUtils::send (output, + profileAtAddress.address.getGroup(), + header, + Message::ProfileAdded { profileAtAddress.profile }); +} + +void ProfileHost::removeProfile (ProfileAtAddress profileAtAddress) +{ + auto* state = states.getStateForDestination (profileAtAddress.address); + + if (state == nullptr) + return; + + disableProfile (profileAtAddress); + + if (! state->get (profileAtAddress.profile).isSupported()) + return; + + state->erase (profileAtAddress.profile); + + if (! isResponder || profileAtAddress == currentEnablementMessage) + return; + + const Message::Header header + { + profileAtAddress.address.getChannel(), + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + output.getMuid(), + MUID::getBroadcast(), + }; + + detail::MessageTypeUtils::send (output, + profileAtAddress.address.getGroup(), + header, + Message::ProfileRemoved { profileAtAddress.profile }); +} + +void ProfileHost::enableProfile (ProfileAtAddress profileAtAddress, int numChannels) +{ + auto* state = states.getStateForDestination (profileAtAddress.address); + + if (state == nullptr) + return; + + const auto old = state->get (profileAtAddress.profile); + + if (! old.isSupported()) + return; + + // There are only 256 channels on a UMP endpoint, so requesting more probably doesn't make sense! + jassert (numChannels <= 256); + + const auto enabledChannels = jmin (old.supported, (uint16_t) numChannels); + state->set (profileAtAddress.profile, { old.supported, enabledChannels }); + + if (! isResponder || profileAtAddress == currentEnablementMessage) + return; + + const Message::Header header + { + profileAtAddress.address.getChannel(), + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + output.getMuid(), + MUID::getBroadcast(), + }; + + detail::MessageTypeUtils::send (output, + profileAtAddress.address.getGroup(), + header, + Message::ProfileEnabledReport { profileAtAddress.profile, enabledChannels }); +} + +void ProfileHost::disableProfile (ProfileAtAddress profileAtAddress) +{ + auto* state = states.getStateForDestination (profileAtAddress.address); + + if (state == nullptr) + return; + + const auto old = state->get (profileAtAddress.profile); + + if (! old.isActive()) + return; + + state->set (profileAtAddress.profile, { old.supported, 0 }); + + if (! isResponder || profileAtAddress == currentEnablementMessage) + return; + + const Message::Header header + { + profileAtAddress.address.getChannel(), + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + output.getMuid(), + MUID::getBroadcast(), + }; + + detail::MessageTypeUtils::send (output, + profileAtAddress.address.getGroup(), + header, + Message::ProfileDisabledReport { profileAtAddress.profile, old.active }); +} + +bool ProfileHost::tryRespond (ResponderOutput& responderOutput, const Message::Parsed& message) +{ + bool result = false; + detail::MessageTypeUtils::visit (message, Visitor { this, &responderOutput, &result }); + return result; +} + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIProfileHost.h b/modules/juce_midi_ci/ci/juce_CIProfileHost.h new file mode 100644 index 0000000000..dffe7412d8 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIProfileHost.h @@ -0,0 +1,112 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +/** + Acting as a ResponderListener, instances of this class can formulate + appropriate replies to profile transactions initiated by remote devices. + + ProfileHost instances also contains methods to inform remote devices about + changes to local profile state. + + Stores the current state of profiles on the local device. + + @tags{Audio} +*/ +class ProfileHost final : public ResponderDelegate +{ +public: + /** @internal + + Rather than constructing one of these objects yourself, you should configure + a Device with profile support, and then use Device::getProfileHost() + to retrieve a profile host that has been set up to work with that device. + */ + ProfileHost (FunctionBlock fb, ProfileDelegate& d, BufferOutput& o) + : functionBlock (fb), delegate (d), output (o) {} + + /** Adds support for a profile on the specified group/channel with a + maximum number of channels that may be activated. + */ + void addProfile (ProfileAtAddress, int maxNumChannels = 1); + + /** Removes support for a profile on the specified group/channel. + */ + void removeProfile (ProfileAtAddress); + + /** Activates a profile on the specified group/channel with the provided + number of channels. + + The profile should previously have been added with addProfile(), and + numChannels should be in the closed range between 1 and the maximum + number of channels allowed for that profile. + */ + void enableProfile (ProfileAtAddress, int numChannels); + + /** Deactivates a profile on the specified group/channel. + */ + void disableProfile (ProfileAtAddress); + + /** Returns the profile states (supported/active) for all groups and channels. + */ + const BlockProfileStates& getProfileStates() const { return states; } + + /** Returns the number of supported and active channels for the given + profile on the specified group/channel. + + If the supported channels is 0, then the profile is not supported + on the group/channel. + + If the active channels is 0, then the profile is inactive on the + group/channel. + */ + SupportedAndActive getState (ProfileAtAddress profileAtAddress) const + { + if (auto* state = states.getStateForDestination (profileAtAddress.address)) + return state->get (profileAtAddress.profile); + + return {}; + } + + /** @internal */ + bool tryRespond (ResponderOutput&, const Message::Parsed&) override; + +private: + class Visitor; + + template + bool profileEnablementReceived (ResponderOutput&, const Body&); + + FunctionBlock functionBlock; + ProfileDelegate& delegate; + BufferOutput& output; + BlockProfileStates states; + bool isResponder = false; + std::optional currentEnablementMessage; +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIProfileStates.cpp b/modules/juce_midi_ci/ci/juce_CIProfileStates.cpp new file mode 100644 index 0000000000..a6c2ff9b0c --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIProfileStates.cpp @@ -0,0 +1,86 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +SupportedAndActive ChannelProfileStates::get (const Profile& profile) const +{ + const auto iter = std::lower_bound (entries.begin(), entries.end(), profile); + + if (iter != entries.end() && iter->profile == profile) + return iter->state; + + return {}; +} + +std::vector ChannelProfileStates::getActive() const +{ + std::vector result; + + for (const auto& item : entries) + if (item.state.isActive()) + result.push_back (item.profile); + + return result; +} + +std::vector ChannelProfileStates::getInactive() const +{ + std::vector result; + + for (const auto& item : entries) + if (item.state.isSupported()) + result.push_back (item.profile); + + return result; +} + +void ChannelProfileStates::set (const Profile& profile, SupportedAndActive state) +{ + const auto iter = std::lower_bound (entries.begin(), entries.end(), profile); + + if (iter != entries.end() && iter->profile == profile) + { + if (state != SupportedAndActive{}) + iter->state = state; + else + entries.erase (iter); + } + else if (state != SupportedAndActive{}) + { + entries.insert (iter, { profile, state }); + } +} + +void ChannelProfileStates::erase (const Profile& profile) +{ + const auto iter = std::lower_bound (entries.begin(), entries.end(), profile); + + if (iter != entries.end() && iter->profile == profile) + entries.erase (iter); +} + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIProfileStates.h b/modules/juce_midi_ci/ci/juce_CIProfileStates.h new file mode 100644 index 0000000000..f9968fd7dc --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIProfileStates.h @@ -0,0 +1,161 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +/** + Holds a profile ID, along with the number of supported and active channels + corresponding to that profile. + + @tags{Audio} +*/ +struct ProfileStateEntry +{ + Profile profile; ///< A MIDI-CI profile ID + SupportedAndActive state; ///< The number of channels corresponding to the profile + + bool operator< (const Profile& other) const { return profile < other; } + bool operator< (const ProfileStateEntry& other) const { return profile < other.profile; } +}; + +//============================================================================== +/** + Holds the number of channels that are supported and activated for all profiles + at a particular channel address. + + @tags{Audio} +*/ +class ChannelProfileStates +{ +public: + using Entry = ProfileStateEntry; + + /** Returns the number of channels that are supported and active for the + given profile. + */ + SupportedAndActive get (const Profile& profile) const; + + /** Returns all profiles that are active at this address. */ + std::vector getActive() const; + + /** Returns all profiles that are supported but inactive at this address. */ + std::vector getInactive() const; + + /** Sets the number of channels that are supported/active for a given profile. */ + void set (const Profile& profile, SupportedAndActive state); + + /** Removes the record of a particular profile, equivalent to removing support. */ + void erase (const Profile& profile); + + /** Gets a const iterator over all profiles, for range-for compatibility. */ + auto begin() const { return entries.begin(); } + + /** Gets a const iterator over all profiles, for range-for compatibility. */ + auto end() const { return entries.end(); } + + /** Returns true if no profiles are supported. */ + auto empty() const { return entries.empty(); } + + /** Returns the number of profiles that are supported at this address. */ + auto size() const { return entries.size(); } + +private: + std::vector entries; +}; + +//============================================================================== +/** + Contains profile states for each channel in a group, along with the state + of profiles that apply to the group itself. + + @tags{Audio} +*/ +class GroupProfileStates +{ + template + static auto getStateForDestinationImpl (This& t, ChannelInGroup destination) -> decltype (&t.groupState) + { + if (destination == ChannelInGroup::wholeGroup) + return &t.groupState; + + if (const auto index = (size_t) destination; index < t.channelStates.size()) + return &t.channelStates[index]; + + return nullptr; + } + +public: + /** Returns the profile state for the group or a contained channel as appropriate. + Returns nullptr if ChannelInGroup refers to a whole function block. + */ + auto* getStateForDestination (ChannelInGroup d) { return getStateForDestinationImpl (*this, d); } + + /** Returns the profile state for the group or a contained channel as appropriate. + Returns nullptr if ChannelInGroup refers to a whole function block. + */ + auto* getStateForDestination (ChannelInGroup d) const { return getStateForDestinationImpl (*this, d); } + + std::array channelStates; ///< Profile states for each channel in the group + ChannelProfileStates groupState; ///< Profile states for the group itself +}; + +//============================================================================== +/** + Contains profile states for each group and channel in a function block, along with the state + of profiles that apply to the function block itself. + + @tags{Audio} +*/ +class BlockProfileStates +{ + template + static auto getStateForDestinationImpl (This& t, ChannelAddress address) -> decltype (&t.blockState) + { + if (address.isBlock()) + return &t.blockState; + + if (const auto index = (size_t) address.getGroup(); index < t.groupStates.size()) + return t.groupStates[index].getStateForDestination (address.getChannel()); + + return nullptr; + } + +public: + /** Returns the profile state for the function block, group, or channel as appropriate. + Returns nullptr if the address refers to a non-existent channel or group. + */ + auto* getStateForDestination (ChannelAddress address) { return getStateForDestinationImpl (*this, address); } + + /** Returns the profile state for the function block, group, or channel as appropriate. + Returns nullptr if the address refers to a non-existent channel or group. + */ + auto* getStateForDestination (ChannelAddress address) const { return getStateForDestinationImpl (*this, address); } + + std::array groupStates; ///< Profile states for each group in the function block + ChannelProfileStates blockState; ///< Profile states for the whole function block +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIPropertyDelegate.cpp b/modules/juce_midi_ci/ci/juce_CIPropertyDelegate.cpp new file mode 100644 index 0000000000..174945216f --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIPropertyDelegate.cpp @@ -0,0 +1,255 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +struct PropertyDelegateDetail +{ + /* + Note: We don't use ToVar and FromVar here, because we want to omit fields that are using + their default values. + */ + + template + static Target parseTargetHeader (const var& v, + const std::map& parsers) + { + Target target; + + if (auto* obj = v.getDynamicObject()) + { + for (const auto& pair : obj->getProperties()) + { + const auto parserIter = parsers.find (pair.name); + + if (parserIter != parsers.end()) + parserIter->second (target, pair.value); + else + target.extended[pair.name] = pair.value; + } + } + + return target; + } + + static auto getParsersForPropertyReplyHeader() + { + using Target = PropertyReplyHeader; + std::map map; + + map.emplace ("status", [] (Target& header, const var& v) { header.status = v; }); + map.emplace ("message", [] (Target& header, const var& v) { header.message = v; }); + map.emplace ("cacheTime", [] (Target& header, const var& v) { header.cacheTime = v; }); + map.emplace ("mediaType", [] (Target& header, const var& v) { header.mediaType = v; }); + map.emplace ("mutualEncoding", [] (Target& header, const var& v) + { + header.mutualEncoding = EncodingUtils::toEncoding (v.toString().toRawUTF8()).value_or (Encoding::ascii); + }); + + return map; + } + + template + static auto getParsersForGenericPropertyRequestHeader() + { + std::map map; + + map.emplace ("resource", [] (Target& header, const var& v) { header.resource = v; }); + map.emplace ("resId", [] (Target& header, const var& v) { header.resId = v; }); + map.emplace ("mediaType", [] (Target& header, const var& v) { header.mediaType = v; }); + map.emplace ("mutualEncoding", [] (Target& header, const var& v) + { + header.mutualEncoding = EncodingUtils::toEncoding (v.toString().toRawUTF8()).value_or (Encoding::ascii); + }); + + return map; + } + + static auto getParsersForPropertyRequestHeader() + { + auto map = getParsersForGenericPropertyRequestHeader(); + map.emplace ("setPartial", [] (PropertyRequestHeader& header, const var& v) { header.setPartial = v; }); + map.emplace ("offset", [] (PropertyRequestHeader& header, const var& v) + { + if (! header.pagination.has_value()) + header.pagination = Pagination{}; + + header.pagination->offset = v; + }); + map.emplace ("limit", [] (PropertyRequestHeader& header, const var& v) + { + if (! header.pagination.has_value()) + header.pagination = Pagination{}; + + header.pagination->limit = v; + }); + return map; + } + + static auto getParsersForPropertySubscriptionHeader() + { + auto map = getParsersForGenericPropertyRequestHeader(); + + map.emplace ("subscribeId", [] (PropertySubscriptionHeader& header, const var& v) { header.subscribeId = v; }); + map.emplace ("command", [] (PropertySubscriptionHeader& header, const var& v) + { + header.command = [&] + { + if (v == "start") + return PropertySubscriptionCommand::start; + + if (v == "partial") + return PropertySubscriptionCommand::partial; + + if (v == "full") + return PropertySubscriptionCommand::full; + + if (v == "notify") + return PropertySubscriptionCommand::notify; + + if (v == "end") + return PropertySubscriptionCommand::end; + + return PropertySubscriptionCommand::notify; + }(); + }); + + return map; + } + + static auto getSetPartial (const PropertySubscriptionHeader&) { return false; } + static auto getSetPartial (const PropertyRequestHeader& h) { return h.setPartial; } + static auto getSetPartial (const PropertyReplyHeader&) { return false; } + + static auto getPagination (const PropertySubscriptionHeader&) { return std::optional{}; } + static auto getPagination (const PropertyRequestHeader& h) { return h.pagination; } + static auto getPagination (const PropertyReplyHeader&) { return std::optional{}; } + + static auto getCacheTime (const PropertySubscriptionHeader&) { return 0; } + static auto getCacheTime (const PropertyRequestHeader&) { return 0; } + static auto getCacheTime (const PropertyReplyHeader& h) { return h.cacheTime; } + + static auto getMessage (const PropertySubscriptionHeader&) { return String{}; } + static auto getMessage (const PropertyRequestHeader&) { return String{}; } + static auto getMessage (const PropertyReplyHeader& h) { return h.message; } + + static auto getResource (const PropertySubscriptionHeader& h) { return h.resource; } + static auto getResource (const PropertyRequestHeader& h) { return h.resource; } + static auto getResource (const PropertyReplyHeader&) { return String{}; } + + static auto getResId (const PropertySubscriptionHeader& h) { return h.resId; } + static auto getResId (const PropertyRequestHeader& h) { return h.resId; } + static auto getResId (const PropertyReplyHeader&) { return String{}; } + + static auto getCommand (const PropertySubscriptionHeader& h) { return h.command; } + static auto getCommand (const PropertyRequestHeader&) { return PropertySubscriptionCommand{}; } + static auto getCommand (const PropertyReplyHeader&) { return PropertySubscriptionCommand{}; } + + static auto getSubscribeId (const PropertySubscriptionHeader& h) { return h.subscribeId; } + static auto getSubscribeId (const PropertyRequestHeader&) { return String{}; } + static auto getSubscribeId (const PropertyReplyHeader&) { return String{}; } + + static auto getStatus (const PropertySubscriptionHeader&) { return 0; } + static auto getStatus (const PropertyRequestHeader&) { return 0; } + static auto getStatus (const PropertyReplyHeader& h) { return h.status; } + + template + static auto toFieldsFromHeader (const T& t) + { + auto fields = t.extended; + + if (getResource (t) != getResource (T())) + fields["resource"] = getResource (t); + + if (getCommand (t) != getCommand (T())) + fields["command"] = PropertySubscriptionCommandUtils::toString (getCommand (t)); + + if (getSubscribeId (t) != getSubscribeId (T())) + fields["subscribeId"] = getSubscribeId (t); + + if (getResId (t) != getResId (T())) + fields["resId"] = getResId (t); + + if (t.mutualEncoding != T().mutualEncoding) + fields["mutualEncoding"] = EncodingUtils::toString (t.mutualEncoding); + + if (t.mediaType != T().mediaType) + fields["mediaType"] = t.mediaType; + + if (getStatus (t) != getStatus (T())) + fields["status"] = getStatus (t); + + if (getSetPartial (t)) + fields["setPartial"] = true; + + if (getCacheTime (t) != getCacheTime (T())) + fields["cacheTime"] = getCacheTime (t); + + if (getMessage (t) != getMessage (T())) + fields["message"] = getMessage (t); + + if (const auto pagination = getPagination (t)) + { + fields["offset"] = pagination->offset; + fields["limit"] = pagination->limit; + } + + return fields; + } +}; + +//============================================================================== +PropertySubscriptionHeader PropertySubscriptionHeader::parseCondensed (const var& v) +{ + return PropertyDelegateDetail::parseTargetHeader (v, PropertyDelegateDetail::getParsersForPropertySubscriptionHeader()); +} + +var PropertySubscriptionHeader::toVarCondensed() const +{ + return JSONUtils::makeObjectWithKeyFirst (PropertyDelegateDetail::toFieldsFromHeader (*this), "command"); +} + +PropertyRequestHeader PropertyRequestHeader::parseCondensed (const var& v) +{ + return PropertyDelegateDetail::parseTargetHeader (v, PropertyDelegateDetail::getParsersForPropertyRequestHeader()); +} + +var PropertyRequestHeader::toVarCondensed() const +{ + return JSONUtils::makeObjectWithKeyFirst (PropertyDelegateDetail::toFieldsFromHeader (*this), "resource"); +} + +PropertyReplyHeader PropertyReplyHeader::parseCondensed (const var& v) +{ + return PropertyDelegateDetail::parseTargetHeader (v, PropertyDelegateDetail::getParsersForPropertyReplyHeader()); +} + +var PropertyReplyHeader::toVarCondensed() const +{ + return JSONUtils::makeObjectWithKeyFirst (PropertyDelegateDetail::toFieldsFromHeader (*this), "status"); +} + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIPropertyDelegate.h b/modules/juce_midi_ci/ci/juce_CIPropertyDelegate.h new file mode 100644 index 0000000000..53f044b6f8 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIPropertyDelegate.h @@ -0,0 +1,270 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +#define JUCE_SUBSCRIPTION_COMMANDS X(start) X(partial) X(full) X(notify) X(end) + +/** + Kinds of command that may be sent as part of a subscription update. + + Check the Property Exchange specification to find the meaning of the + different kinds. + + @tags{Audio} +*/ +enum class PropertySubscriptionCommand +{ + #define X(str) str, + JUCE_SUBSCRIPTION_COMMANDS + #undef X +}; + +struct PropertySubscriptionCommandUtils +{ + PropertySubscriptionCommandUtils() = delete; + + /** Converts a command to a human-readable string. */ + static const char* toString (PropertySubscriptionCommand x) + { + switch (x) + { + #define X(str) case PropertySubscriptionCommand::str: return #str; + JUCE_SUBSCRIPTION_COMMANDS + #undef X + } + + return nullptr; + } + + /** Converts a command string from a property exchange JSON header to + an PropertySubscriptionCommand. + */ + static std::optional toCommand (const char* str) + { + #define X(name) if (std::string_view (str) == std::string_view (#name)) return PropertySubscriptionCommand::name; + JUCE_SUBSCRIPTION_COMMANDS + #undef X + + return {}; + } +}; + +#undef JUCE_SUBSCRIPTION_COMMANDS + +/** + A struct containing data members that correspond to common fields in a + property subscription header. + + Check the Property Exchange specification to find the meaning of the + different fields. + + @tags{Audio} +*/ +struct PropertySubscriptionHeader +{ + String resource; + String resId; + Encoding mutualEncoding = Encoding::ascii; + String mediaType = "application/json"; + PropertySubscriptionCommand command { -1 }; + String subscribeId; + std::map extended; + + /** Converts a JSON object to a PropertyRequestHeader. + + Unspecified fields will use their default values. + */ + static PropertySubscriptionHeader parseCondensed (const var&); + + /** Converts a PropertySubscriptionHeader to a JSON object suitable for use as + a MIDI-CI message header after conversion to 7-bit ASCII. + */ + var toVarCondensed() const; +}; + +/** + Contains information about the pagination of a request. + + Check the Property Exchange specification to find the meaning of the + different fields. + + @tags{Audio} +*/ +struct Pagination +{ + int offset = 0; + int limit = 1; +}; + +/** + A struct containing data members that correspond to common fields in a + property request header. + + Check the Property Exchange specification to find the meaning of the + different fields. + + @tags{Audio} +*/ +struct PropertyRequestHeader +{ + String resource; + String resId; + Encoding mutualEncoding = Encoding::ascii; + String mediaType = "application/json"; + bool setPartial = false; + std::optional pagination; + std::map extended; + + /** Converts a JSON object to a PropertyRequestHeader. + + Unspecified fields will use their default values. + */ + static PropertyRequestHeader parseCondensed (const var&); + + /** Converts a PropertyRequestHeader to a JSON object suitable for use as + a MIDI-CI message header after conversion to 7-bit ASCII. + */ + var toVarCondensed() const; +}; + +/** + Bundles together a property request header and a data payload. + + @tags{Audio} +*/ +struct PropertyRequestData +{ + PropertyRequestHeader header; + Span body; +}; + +/** + A struct containing data members that correspond to common fields in a + reply to a property exchange request. + + Check the Property Exchange specification to find the meaning of the + different fields. + + For extended attributes that don't correspond to any of the defined data + members, use the 'extended' map. + + @tags{Audio} +*/ +struct PropertyReplyHeader +{ + int status = 200; + String message; + Encoding mutualEncoding = Encoding::ascii; + int cacheTime = 0; + String mediaType = "application/json"; + std::map extended; + + /** Converts a JSON object to a PropertyReplyHeader. + + Unspecified fields will use their default values. + */ + static PropertyReplyHeader parseCondensed (const var&); + + /** Converts a PropertyReplyHeader to a JSON object suitable for use as + a MIDI-CI message header after conversion to 7-bit ASCII. + */ + var toVarCondensed() const; +}; + +/** + Bundles together a property reply header and a data payload. + + @tags{Audio} +*/ +struct PropertyReplyData +{ + PropertyReplyHeader header; + std::vector body; +}; + +/** + An interface with methods that can be overridden to customise how a Device + implementing properties responds to property inquiries. + + @tags{Audio} +*/ +struct PropertyDelegate +{ + PropertyDelegate() = default; + virtual ~PropertyDelegate() = default; + PropertyDelegate (const PropertyDelegate&) = default; + PropertyDelegate (PropertyDelegate&&) = default; + PropertyDelegate& operator= (const PropertyDelegate&) = default; + PropertyDelegate& operator= (PropertyDelegate&&) = default; + + /** Returns the max number of simultaneous property exchange messages that can be processed. */ + virtual uint8_t getNumSimultaneousRequestsSupported() const { return 127; } + + /** Returns a header/body containing the requested data. + To report an error, you can return a failure status code in the header and leave the body empty. + */ + virtual PropertyReplyData propertyGetDataRequested (MUID, const PropertyRequestHeader&) = 0; + + /** Returns a header that describes the result of the set operation. */ + virtual PropertyReplyHeader propertySetDataRequested (MUID, const PropertyRequestData&) = 0; + + /** Returns true to allow the subscription, or false otherwise. */ + virtual bool subscriptionStartRequested (MUID, const PropertySubscriptionHeader&) = 0; + + /** Called with the corresponding subscription token after a subscription has started. */ + virtual void subscriptionDidStart (MUID, const String& subId, const PropertySubscriptionHeader&) = 0; + + /** Called when a device requests for an ongoing subscription to end. */ + virtual void subscriptionWillEnd (MUID, const Subscription& sub) = 0; +}; + +} // namespace juce::midi_ci + +namespace juce +{ + +template <> +struct SerialisationTraits +{ + static constexpr auto marshallingVersion = std::nullopt; + + template + void load (Archive& archive, midi_ci::PropertySubscriptionCommand& t) + { + String command; + archive (command); + t = midi_ci::PropertySubscriptionCommandUtils::toCommand (command.toRawUTF8()).value_or (midi_ci::PropertySubscriptionCommand{}); + } + + template + void save (Archive& archive, const midi_ci::PropertySubscriptionCommand& t) + { + archive (midi_ci::PropertySubscriptionCommandUtils::toString (t)); + } +}; + +} // namespace juce diff --git a/modules/juce_midi_ci/ci/juce_CIPropertyExchangeCache.cpp b/modules/juce_midi_ci/ci/juce_CIPropertyExchangeCache.cpp new file mode 100644 index 0000000000..66a17d7a03 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIPropertyExchangeCache.cpp @@ -0,0 +1,291 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +class PropertyExchangeCache +{ +public: + explicit PropertyExchangeCache (std::function term) + : onTerminate (std::move (term)) {} + + struct OwningResult + { + explicit OwningResult (PropertyExchangeResult::Error e) + : result (e) {} + + OwningResult (var header, std::vector body) + : backingStorage (std::move (body)), + result (header, backingStorage) {} + + OwningResult (OwningResult&&) noexcept = default; + OwningResult& operator= (OwningResult&&) noexcept = default; + + JUCE_DECLARE_NON_COPYABLE (OwningResult) + + std::vector backingStorage; + PropertyExchangeResult result; + }; + + std::optional addChunk (Message::DynamicSizePropertyExchange chunk) + { + jassert (chunk.thisChunkNum == lastChunk + 1 || chunk.thisChunkNum == 0); + lastChunk = chunk.thisChunkNum; + headerStorage.reserve (headerStorage.size() + chunk.header.size()); + std::transform (chunk.header.begin(), + chunk.header.end(), + std::back_inserter (headerStorage), + [] (std::byte b) { return char (b); }); + bodyStorage.insert (bodyStorage.end(), chunk.data.begin(), chunk.data.end()); + + if (chunk.thisChunkNum != 0 && chunk.thisChunkNum != chunk.totalNumChunks) + return {}; + + const auto headerJson = JSON::parse (String (headerStorage.data(), headerStorage.size())); + + onTerminate = nullptr; + const auto encodingString = headerJson.getProperty ("mutualEncoding", "ASCII").toString(); + + if (chunk.thisChunkNum != chunk.totalNumChunks) + return std::optional { std::in_place, PropertyExchangeResult::Error::partial }; + + return std::optional { std::in_place, + headerJson, + Encodings::decode (bodyStorage, EncodingUtils::toEncoding (encodingString.toRawUTF8()).value_or (Encoding::ascii)) }; + + } + + std::optional notify (Span header) + { + const auto headerJson = JSON::parse (String (reinterpret_cast (header.data()), header.size())); + + if (! headerJson.isObject()) + return {}; + + const auto status = headerJson.getProperty ("status", {}); + + if (! status.isInt() || (int) status == 100) + return {}; + + onTerminate = nullptr; + return std::optional { std::in_place, PropertyExchangeResult::Error::notify }; + } + + void terminate() + { + if (auto t = std::exchange (onTerminate, nullptr)) + t(); + } + +private: + std::vector headerStorage; + std::vector bodyStorage; + std::function onTerminate; + uint16_t lastChunk = 0; +}; + +//============================================================================== +class PropertyExchangeCacheArray +{ +public: + PropertyExchangeCacheArray() = default; + + ErasedScopeGuard primeCacheForRequestId (std::byte id, + std::function onDone, + std::function onTerminate) + { + auto& entry = caches[(uint8_t) id]; + entry = std::make_shared (std::move (onDone), std::move (onTerminate)); + auto weak = std::weak_ptr (entry); + + return ErasedScopeGuard { [&entry, weak] + { + // If this fails, then the transaction finished before the ErasedScopeGuard was destroyed. + if (auto locked = weak.lock()) + { + entry->cache.terminate(); + entry = nullptr; + } + } }; + } + + void addChunk (std::byte b, const Message::DynamicSizePropertyExchange& chunk) + { + updateCache (b, [&] (PropertyExchangeCache& c) { return c.addChunk (chunk); }); + } + + void notify (std::byte b, Span header) + { + updateCache (b, [&] (PropertyExchangeCache& c) { return c.notify (header); }); + } + + bool hasTransaction (std::byte id) const + { + return caches[(uint8_t) id] != nullptr; + } + + uint8_t countOngoingTransactions() const + { + return (uint8_t) std::count_if (caches.begin(), caches.end(), [] (auto& c) { return c != nullptr; }); + } + + /** MSB of result is set on failure. */ + std::byte findUnusedId (uint8_t maxSimultaneousTransactions) const + { + if (countOngoingTransactions() >= maxSimultaneousTransactions) + return std::byte { 0xff }; + + return (std::byte) std::distance (caches.begin(), std::find (caches.begin(), caches.end(), nullptr)); + } + + // Instances must stay at the same location to ensure that references captured in the + // ErasedScopeGuard returned from primeCacheForRequestId do not dangle. + JUCE_DECLARE_NON_COPYABLE (PropertyExchangeCacheArray) + JUCE_DECLARE_NON_MOVEABLE (PropertyExchangeCacheArray) + +private: + static constexpr auto numCaches = 128; + + class Transaction + { + public: + Transaction (std::function onSuccess, + std::function onTerminate) + : cache (std::move (onTerminate)), onFinish (std::move (onSuccess)) {} + + PropertyExchangeCache cache; + std::function onFinish; + }; + + template + void updateCache (std::byte b, WithCache&& withCache) + { + if (auto& entry = caches[(uint8_t) b]) + { + if (const auto result = withCache (entry->cache)) + { + const auto tmp = std::move (entry->onFinish); + entry = nullptr; + NullCheckedInvocation::invoke (tmp, result->result); + } + } + } + + std::array, numCaches> caches; +}; + +//============================================================================== +class InitiatorPropertyExchangeCache::Impl +{ +public: + TokenAndId primeCache (uint8_t maxSimultaneousRequests, + std::function onDone, + std::function onTerminate) + { + const auto id = array.findUnusedId (maxSimultaneousRequests); + + if ((id & std::byte { 0x80 }) != std::byte{}) + { + NullCheckedInvocation::invoke (onDone, PropertyExchangeResult { PropertyExchangeResult::Error::tooManyTransactions }); + return {}; + } + + auto token = array.primeCacheForRequestId (id, + std::move (onDone), + [id, term = std::move (onTerminate)] { NullCheckedInvocation::invoke (term, id); }); + return { std::move (token), id }; + } + + void addChunk (std::byte b, const Message::DynamicSizePropertyExchange& chunk) { array.addChunk (b, chunk); } + void notify (std::byte b, Span header) { array.notify (b, header); } + int countOngoingTransactions() const { return array.countOngoingTransactions(); } + bool isAwaitingResponse() const { return countOngoingTransactions() != 0; } + +private: + PropertyExchangeCacheArray array; +}; + +//============================================================================== +InitiatorPropertyExchangeCache::InitiatorPropertyExchangeCache() : pimpl (std::make_unique()) {} +InitiatorPropertyExchangeCache::InitiatorPropertyExchangeCache (InitiatorPropertyExchangeCache&&) noexcept = default; +InitiatorPropertyExchangeCache& InitiatorPropertyExchangeCache::operator= (InitiatorPropertyExchangeCache&&) noexcept = default; +InitiatorPropertyExchangeCache::~InitiatorPropertyExchangeCache() = default; + +InitiatorPropertyExchangeCache::TokenAndId InitiatorPropertyExchangeCache::primeCache (uint8_t maxSimultaneousTransactions, + std::function onDone, + std::function onTerminate) +{ + return pimpl->primeCache (maxSimultaneousTransactions, std::move (onDone), std::move (onTerminate)); +} + +void InitiatorPropertyExchangeCache::addChunk (std::byte b, const Message::DynamicSizePropertyExchange& chunk) { pimpl->addChunk (b, chunk); } +void InitiatorPropertyExchangeCache::notify (std::byte b, Span header) { pimpl->notify (b, header); } +int InitiatorPropertyExchangeCache::countOngoingTransactions() const { return pimpl->countOngoingTransactions(); } +bool InitiatorPropertyExchangeCache::isAwaitingResponse() const { return pimpl->isAwaitingResponse(); } + +//============================================================================== +class ResponderPropertyExchangeCache::Impl +{ +public: + void primeCache (uint8_t maxSimultaneousTransactions, + std::function onDone, + std::byte id) + { + if (array.hasTransaction (id)) + return; + + if (array.countOngoingTransactions() >= maxSimultaneousTransactions) + NullCheckedInvocation::invoke (onDone, PropertyExchangeResult { PropertyExchangeResult::Error::tooManyTransactions }); + else + array.primeCacheForRequestId (id, std::move (onDone), nullptr).release(); + } + + void addChunk (std::byte b, const Message::DynamicSizePropertyExchange& chunk) { array.addChunk (b, chunk); } + void notify (std::byte b, Span header) { array.notify (b, header); } + int countOngoingTransactions() const { return array.countOngoingTransactions(); } + +private: + PropertyExchangeCacheArray array; +}; + +//============================================================================== +ResponderPropertyExchangeCache::ResponderPropertyExchangeCache() : pimpl (std::make_unique()) {} +ResponderPropertyExchangeCache::ResponderPropertyExchangeCache (ResponderPropertyExchangeCache&&) noexcept = default; +ResponderPropertyExchangeCache& ResponderPropertyExchangeCache::operator= (ResponderPropertyExchangeCache&&) noexcept = default; +ResponderPropertyExchangeCache::~ResponderPropertyExchangeCache() = default; + +void ResponderPropertyExchangeCache::primeCache (uint8_t maxSimultaneousTransactions, + std::function onDone, + std::byte id) +{ + return pimpl->primeCache (maxSimultaneousTransactions, std::move (onDone), id); +} + +void ResponderPropertyExchangeCache::addChunk (std::byte b, const Message::DynamicSizePropertyExchange& chunk) { pimpl->addChunk (b, chunk); } +void ResponderPropertyExchangeCache::notify (std::byte b, Span header) { pimpl->notify (b, header); } +int ResponderPropertyExchangeCache::countOngoingTransactions() const { return pimpl->countOngoingTransactions(); } + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIPropertyExchangeCache.h b/modules/juce_midi_ci/ci/juce_CIPropertyExchangeCache.h new file mode 100644 index 0000000000..a2c883f3f4 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIPropertyExchangeCache.h @@ -0,0 +1,158 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +/** + Accumulates message chunks that have been sent by another device in response + to a transaction initiated by a local device. + + @tags{Audio} +*/ +class InitiatorPropertyExchangeCache +{ +public: + InitiatorPropertyExchangeCache(); + ~InitiatorPropertyExchangeCache(); + + InitiatorPropertyExchangeCache (InitiatorPropertyExchangeCache&&) noexcept; + InitiatorPropertyExchangeCache& operator= (InitiatorPropertyExchangeCache&&) noexcept; + + JUCE_DECLARE_NON_COPYABLE (InitiatorPropertyExchangeCache) + + /** Holds a token that can be used to stop waiting for a reply, along with + an identifier byte which uniquely identifies an ongoing transaction. + */ + struct TokenAndId + { + TokenAndId() = default; + TokenAndId (ErasedScopeGuard tokenIn, std::byte idIn) + : token (std::move (tokenIn)), id (idIn) {} + + bool isValid() const { return (id & std::byte { 0x80 }) == std::byte{}; } + + ErasedScopeGuard token{}; + std::byte id { 0x80 }; + }; + + /** Picks an unused request ID, and prepares the cache for that ID to accumulate message chunks. + + Incoming chunks added with addChunk are generated by another device acting as a responder. + */ + TokenAndId primeCache (uint8_t maxSimultaneousRequests, + std::function onDone, + std::function onTerminate); + + /** Adds a message chunk for the provided transaction id. */ + void addChunk (std::byte b, const Message::DynamicSizePropertyExchange& chunk); + + /** Updates the transaction state based on the contents of the provided notification. */ + void notify (std::byte b, Span header); + + /** Returns the number of transactions that have been started but not finished. */ + int countOngoingTransactions() const; + + /** Returns true if there are any transactions in progress that + haven't yet received replies. + */ + bool isAwaitingResponse() const; + +private: + class Impl; + std::unique_ptr pimpl; +}; + +//============================================================================== +/** + Accumulates message chunks that form a request initiated by a remote device. + + @tags{Audio} +*/ +class ResponderPropertyExchangeCache +{ +public: + ResponderPropertyExchangeCache(); + ~ResponderPropertyExchangeCache(); + + ResponderPropertyExchangeCache (ResponderPropertyExchangeCache&&) noexcept; + ResponderPropertyExchangeCache& operator= (ResponderPropertyExchangeCache&&) noexcept; + + JUCE_DECLARE_NON_COPYABLE (ResponderPropertyExchangeCache) + + /** Prepares the cache for the given requestId to accumulate message chunks. + + Incoming chunks added with addChunk are generated by another device acting as an initiator. + */ + void primeCache (uint8_t maxSimultaneousTransactions, + std::function onDone, + std::byte id); + + /** Adds a message chunk for the provided transaction id. */ + void addChunk (std::byte b, const Message::DynamicSizePropertyExchange& chunk); + + /** Updates the transaction state based on the contents of the provided notification. */ + void notify (std::byte b, Span header); + + /** Returns the number of transactions that have been started but not finished. */ + int countOngoingTransactions() const; + +private: + class Impl; + std::unique_ptr pimpl; +}; + +//============================================================================== +/** + An interface for objects that provide resources for property exchange + transactions. + + @tags{Audio} +*/ +class CacheProvider +{ +public: + virtual ~CacheProvider() = default; + + /** Returns a set containing all of the MUIDs currently known to the provider. */ + virtual std::set getDiscoveredMuids() const = 0; + + /** Returns a property exchange cache for accumulating replies to transactions + we initiated. + */ + virtual InitiatorPropertyExchangeCache* getCacheForMuidAsInitiator (MUID m) = 0; + + /** Returns a property exchange cache for accumulating requests initiated + by other devices. + */ + virtual ResponderPropertyExchangeCache* getCacheForMuidAsResponder (MUID m) = 0; + + /** Returns the maximum sysex size supported by the device with the + given MUID. + */ + virtual int getMaxSysexSizeForMuid (MUID m) const = 0; +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIPropertyExchangeResult.h b/modules/juce_midi_ci/ci/juce_CIPropertyExchangeResult.h new file mode 100644 index 0000000000..29d19dc463 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIPropertyExchangeResult.h @@ -0,0 +1,118 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +/** + Contains data returned by a responder in response to a request. + + PropertyExchangeResult::kind indicates whether the transaction resulted in + a well-formed message; however, it's possible that the message is a + well-formed message indicating an error in the responder, so it's important + to check the 'status' field of the header before attempting to do anything + with the payload. + + @tags{Audio} +*/ +class PropertyExchangeResult +{ +public: + enum class Error + { + partial, ///< Got a response, but the responder terminated it before + ///< sending a well-formed message. + + notify, ///< Got a notify message terminating the transaction. + tooManyTransactions, ///< Unable to send the request because doing so would + ///< exceed the number of simultaneous inquiries that were declared. + ///< @see PropertyDelegate::getNumSimultaneousRequestsSupported(). + + invalidPayload, ///< The payload couldn't be encoded for transmission. If you're + ///< using the ASCII encoding, maybe some bytes have their most + ///< significant bit set. + }; + + /** Creates a result denoting an error state. */ + explicit PropertyExchangeResult (Error errorIn) + : PropertyExchangeResult (errorIn, {}, {}) {} + + /** Creates a result denoting a successful transmission. */ + PropertyExchangeResult (var headerIn, Span bodyIn) + : PropertyExchangeResult (std::nullopt, headerIn, bodyIn) {} + + /** Returns the result kind, either nullopt for a successful transmission, or + an error code if something went wrong. + */ + std::optional getError() const { return error; } + + /** Parses the header as a subscription header. + + This may only be called for messages of kind 'full'. + */ + PropertySubscriptionHeader getHeaderAsSubscriptionHeader() const + { + jassert (header != var()); + return PropertySubscriptionHeader::parseCondensed (header); + } + + /** Parses the header as a request header. + + This may only be called for messages of kind 'full'. + */ + PropertyRequestHeader getHeaderAsRequestHeader() const + { + jassert (header != var()); + return PropertyRequestHeader::parseCondensed (header); + } + + /** Parses the header as a reply header. + + This may only be called for messages of kind 'full'. + */ + PropertyReplyHeader getHeaderAsReplyHeader() const + { + jassert (header != var()); + return PropertyReplyHeader::parseCondensed (header); + } + + /** When getKind returns 'full', this is the message payload. + + Note that this is not stored internally; if you need to keep this data + around and reference it in the future, you should copy it into a + vector or some other suitable container. + */ + Span getBody() const { return body; } + +private: + PropertyExchangeResult (std::optional errorIn, var headerIn, Span bodyIn) + : error (errorIn), header (headerIn), body (bodyIn) {} + + std::optional error; + var header; + Span body; +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIPropertyHost.cpp b/modules/juce_midi_ci/ci/juce_CIPropertyHost.cpp new file mode 100644 index 0000000000..ce518f443f --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIPropertyHost.cpp @@ -0,0 +1,411 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +class PropertyHost::Visitor : public detail::MessageTypeUtils::MessageVisitor +{ +public: + Visitor (PropertyHost* h, ResponderOutput* o, bool* b) + : host (h), output (o), handled (b) {} + + void visit (const Message::PropertyExchangeCapabilities& body) const override { visitImpl (body); } + void visit (const Message::PropertyGetData& body) const override { visitImpl (body); } + void visit (const Message::PropertySetData& body) const override { visitImpl (body); } + void visit (const Message::PropertySubscribe& body) const override { visitImpl (body); } + void visit (const Message::PropertyNotify& body) const override { visitImpl (body); } + using MessageVisitor::visit; + +private: + template + void visitImpl (const Body& body) const { *handled = messageReceived (body); } + + bool messageReceived (const Message::PropertyExchangeCapabilities&) const + { + detail::MessageTypeUtils::send (*output, Message::PropertyExchangeCapabilitiesResponse { std::byte { host->delegate.getNumSimultaneousRequestsSupported() }, + {}, + {} }); + return true; + } + + bool messageReceived (const Message::PropertyGetData& data) const + { + // This should always be a single message, so no need to accumulate chunks + const auto reply = host->delegate.propertyGetDataRequested (output->getIncomingHeader().source, + PropertyRequestHeader::parseCondensed (Encodings::jsonFrom7BitText (data.header))); + + const auto encoded = Encodings::tryEncode (reply.body, reply.header.mutualEncoding); + + if (! encoded.has_value()) + { + // If this is hit, the data that was supplied isn't valid for the encoding that was specified + jassertfalse; + return false; + } + + detail::PropertyHostUtils::send (*output, + output->getIncomingGroup(), + detail::MessageMeta::Meta::subID2, + output->getIncomingHeader().source, + data.requestID, + Encodings::jsonTo7BitText (reply.header.toVarCondensed()), + *encoded, + host->cacheProvider.getMaxSysexSizeForMuid (output->getIncomingHeader().source)); + + return true; + } + + bool messageReceived (const Message::PropertySetData& data) const + { + auto* caches = host->cacheProvider.getCacheForMuidAsResponder (output->getIncomingHeader().source); + + if (caches == nullptr) + return false; + + const auto source = output->getIncomingHeader().source; + const auto dest = output->getIncomingHeader().destination; + const auto group = output->getIncomingGroup(); + const auto request = data.requestID; + caches->primeCache (host->delegate.getNumSimultaneousRequestsSupported(), [this, source, dest, group, request] (const PropertyExchangeResult& result) + { + const auto send = [&] (const PropertyReplyHeader& header) + { + detail::MessageTypeUtils::send (host->output, + group, + Message::Header { ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + dest, + source }, + Message::PropertySetDataResponse { { request, Encodings::jsonTo7BitText (header.toVarCondensed()) } }); + }; + + if (result.getError() == PropertyExchangeResult::Error::tooManyTransactions) + { + PropertyReplyHeader header; + header.status = 343; + header.message = TRANS ("The device has initiated too many simultaneous requests"); + send (header); + return; + } + + if (result.getError().has_value()) + { + PropertyReplyHeader header; + header.status = 400; + header.message = TRANS ("Request was incomplete"); + send (header); + return; + } + + send (host->delegate.propertySetDataRequested (source, { result.getHeaderAsRequestHeader(), result.getBody() })); + }, request); + + caches->addChunk (data.requestID, data); + + return true; + } + + bool messageReceived (const Message::PropertySubscribe& data) const + { + auto* caches = host->cacheProvider.getCacheForMuidAsResponder (output->getIncomingHeader().source); + + if (caches == nullptr) + return false; + + if (data.header.empty() || data.thisChunkNum != 1 || data.totalNumChunks != 1) + return false; + + const auto subHeader = PropertySubscriptionHeader::parseCondensed (Encodings::jsonFrom7BitText (data.header)); + const auto tryNotifyInitiator = subHeader.command == PropertySubscriptionCommand::start + || subHeader.command == PropertySubscriptionCommand::end; + + if (! tryNotifyInitiator) + return false; + + const auto source = output->getIncomingHeader().source; + + const auto sendResponse = [&] (const PropertyReplyHeader& header) + { + detail::PropertyHostUtils::send (*output, + output->getIncomingGroup(), + detail::MessageMeta::Meta::subID2, + source, + data.requestID, + Encodings::jsonTo7BitText (header.toVarCondensed()), + {}, + host->cacheProvider.getMaxSysexSizeForMuid (source)); + }; + + if (subHeader.command == PropertySubscriptionCommand::start) + { + if (host->delegate.subscriptionStartRequested (source, subHeader)) + { + auto& currentSubscribeIds = host->registry[source]; + const auto newToken = findUnusedSubscribeId (currentSubscribeIds); + [[maybe_unused]] const auto pair = currentSubscribeIds.emplace (newToken, subHeader.resource); + jassert (pair.second); + const auto subscribeId = subscribeIdFromUid (newToken); + host->delegate.subscriptionDidStart (source, subscribeId, subHeader); + + PropertyReplyHeader header; + header.extended["subscribeId"] = subscribeId; + sendResponse (header); + } + else + { + PropertyReplyHeader header; + header.status = 405; + sendResponse (header); + } + + return true; + } + + if (subHeader.command == PropertySubscriptionCommand::end) + { + const auto token = uidFromSubscribeId (subHeader.subscribeId); + auto& currentSubscribeIds = host->registry[source]; + const auto iter = currentSubscribeIds.find (token); + + if (iter != currentSubscribeIds.end()) + { + host->delegate.subscriptionWillEnd (source, { subHeader.subscribeId, iter->second }); + currentSubscribeIds.erase (iter); + + sendResponse ({}); + return true; + } + + return false; + } + + return false; + } + + bool messageReceived (const Message::PropertyNotify& n) const + { + const auto m = output->getIncomingHeader().source; + + if (auto* it = host->cacheProvider.getCacheForMuidAsResponder (m)) + it->notify (n.requestID, n.header); + + if (auto* it = host->cacheProvider.getCacheForMuidAsInitiator (m)) + it->notify (n.requestID, n.header); + + return true; + } + + PropertyHost* host = nullptr; + ResponderOutput* output = nullptr; + bool* handled = nullptr; +}; + +//============================================================================== +std::set PropertyHost::findSubscriptionsForDevice (MUID device) const +{ + const auto iter = registry.find (device); + + if (iter == registry.end()) + return {}; + + std::set result; + + for (const auto& [subId, resource] : iter->second) + { + [[maybe_unused]] const auto pair = result.insert ({ subscribeIdFromUid (subId), resource }); + jassert (pair.second); + } + + return result; +} + +int PropertyHost::countOngoingTransactions() const +{ + const auto muids = cacheProvider.getDiscoveredMuids(); + + return std::accumulate (muids.begin(), muids.end(), 0, [&] (auto acc, const auto& m) + { + if (auto* cache = cacheProvider.getCacheForMuidAsResponder (m)) + return acc + cache->countOngoingTransactions(); + + return acc; + }); +} + +bool PropertyHost::tryRespond (ResponderOutput& responderOutput, const Message::Parsed& message) +{ + bool result = false; + detail::MessageTypeUtils::visit (message, Visitor { this, &responderOutput, &result }); + return result; +} + +ErasedScopeGuard PropertyHost::sendSubscriptionUpdate (MUID device, + const PropertySubscriptionHeader& header, + Span body, + std::function cb) +{ + const auto deviceIter = registry.find (device); + + if (deviceIter == registry.end()) + { + // That device doesn't have any active subscriptions + jassertfalse; + return {}; + } + + const auto uid = uidFromSubscribeId (header.subscribeId); + const auto subIter = deviceIter->second.find (uid); + + if (subIter == deviceIter->second.end()) + { + // That subscribeId isn't currently in use by that device + jassertfalse; + return {}; + } + + const auto resource = subIter->second; + + if (header.resource != resource) + { + // That subscribeId corresponds to a different resource + jassertfalse; + return {}; + } + + if (header.command == PropertySubscriptionCommand::start) + { + // This function is intended to update ongoing subscriptions. To start a new subscription, + // use CIDevice. + jassertfalse; + return {}; + } + + auto* caches = cacheProvider.getCacheForMuidAsInitiator (device); + + if (caches == nullptr) + return {}; + + const auto terminator = detail::PropertyHostUtils::getTerminator (output, functionBlock, device); + auto wrappedCallback = [&]() -> std::function + { + if (header.command != PropertySubscriptionCommand::end) + return cb; + + return [this, device, uid, resource, cb] (const PropertyExchangeResult& result) + { + if (! result.getError().has_value()) + { + delegate.subscriptionWillEnd (device, { subscribeIdFromUid (uid), resource }); + registry[device].erase (uid); + } + + NullCheckedInvocation::invoke (cb, result); + }; + }(); + + const auto encoded = Encodings::tryEncode (body, header.mutualEncoding); + + if (! encoded.has_value()) + { + NullCheckedInvocation::invoke (wrappedCallback, PropertyExchangeResult { PropertyExchangeResult::Error::invalidPayload }); + return {}; + } + + auto primed = caches->primeCache (delegate.getNumSimultaneousRequestsSupported(), + std::move (wrappedCallback), + std::move (terminator)); + + if (! primed.isValid()) + return {}; + + detail::PropertyHostUtils::send (output, + functionBlock.firstGroup, + detail::MessageMeta::Meta::subID2, + device, + primed.id, + Encodings::jsonTo7BitText (header.toVarCondensed()), + *encoded, + cacheProvider.getMaxSysexSizeForMuid (device)); + + return std::move (primed.token); +} + +void PropertyHost::terminateSubscription (MUID device, const String& subscribeId) +{ + const auto deviceIter = registry.find (device); + + if (deviceIter == registry.end()) + { + // That device doesn't have any active subscriptions + jassertfalse; + return; + } + + const auto uid = uidFromSubscribeId (subscribeId); + const auto subIter = deviceIter->second.find (uid); + + if (subIter == deviceIter->second.end()) + { + // That subscribeId isn't currently in use by that device + jassertfalse; + return; + } + + PropertySubscriptionHeader header; + header.command = PropertySubscriptionCommand::end; + header.subscribeId = subscribeId; + header.resource = subIter->second; + + sendSubscriptionUpdate (device, header, {}, nullptr).release(); +} + +PropertyHost::SubscriptionToken PropertyHost::uidFromSubscribeId (String id) +{ + try + { + // from_chars would be better once we no longer need to support older macOS + return { (size_t) std::stoull (id.toStdString(), {}, 36) }; + } + catch (...) {} + + jassertfalse; + return {}; +} + +String PropertyHost::subscribeIdFromUid (SubscriptionToken uid) +{ + const auto str = std::to_string (uid.uid); + jassert (str.size() <= 8); + return str; +} + +PropertyHost::SubscriptionToken PropertyHost::findUnusedSubscribeId (const std::map& used) +{ + return ! used.empty() ? SubscriptionToken { std::prev (used.end())->first.uid + 1 } : SubscriptionToken { 0 }; +} + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIPropertyHost.h b/modules/juce_midi_ci/ci/juce_CIPropertyHost.h new file mode 100644 index 0000000000..f515afa231 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIPropertyHost.h @@ -0,0 +1,125 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +/** + Acting as a ResponderListener, instances of this class can formulate + appropriate replies to property transactions initiated by remote devices. + + PropertyHost instances also contain methods to inform remote devices about + changes to local property state. + + Keeps track of property subscriptions requested by remote devices. + + @tags{Audio} +*/ +class PropertyHost final : public ResponderDelegate +{ +public: + /** @internal + + Rather than constructing one of these objects yourself, you should configure + a Device with property exchange support, and then use Device::getPropertyHost() + to retrieve a property host that has been set up to work with that device. + */ + PropertyHost (FunctionBlock fb, PropertyDelegate& d, BufferOutput& o, CacheProvider& p) + : functionBlock (fb), delegate (d), output (o), cacheProvider (p) {} + + /** Sends a "Subscription" message from a device, when acting as a + subscription responder. You should call this for all registered + subscribers whenever the subscribed property is modified in a way that + remote devices don't know about (if a remote device requests a + property update, there's no need to send a subscription update after + changing the property accordingly). + + You should *not* attempt to start a new subscription on another device + using this function. Valid subscription commands are "full", "partial", + and "notify". Check the property exchange specification for the intended + use of these commands. + + To terminate a subscription that was initiated by a remote device, + use terminateSubscription(). + + The provided callback will be called once the remote device has confirmed + receipt of the subscription update. If the state of your application + changes such that you no longer need to respond/wait for confirmation, + you can allow the returned Guard to fall out of scope, or reset it + manually. + */ + ErasedScopeGuard sendSubscriptionUpdate (MUID device, + const PropertySubscriptionHeader& header, + Span body, + std::function callback); + + /** Terminates a subscription that was started by a remote device. + + This may be useful if your application has properties that can be + added and removed - you can terminate subscriptions to subscribed + properties before removing those properties. + */ + void terminateSubscription (MUID device, const String& subscribeId); + + /** Returns a set of subscribed resources. + + This set contains all active subscriptionIDs for the given device, + along with the resources to which those subscriptionIDs refer. + */ + std::set findSubscriptionsForDevice (MUID device) const; + + /** Returns the number of transactions that have been initiated by other devices, but not yet + completed, normally because the request has been split into several messages. + */ + int countOngoingTransactions() const; + + /** @internal */ + bool tryRespond (ResponderOutput&, const Message::Parsed&) override; + +private: + class Visitor; + + struct SubscriptionToken + { + size_t uid{}; + + bool operator< (const SubscriptionToken& other) const { return uid < other.uid; } + bool operator== (const SubscriptionToken& other) const { return uid == other.uid; } + bool operator!= (const SubscriptionToken& other) const { return uid != other.uid; } + }; + + static SubscriptionToken uidFromSubscribeId (String id); + static String subscribeIdFromUid (SubscriptionToken uid); + static SubscriptionToken findUnusedSubscribeId (const std::map& used); + + FunctionBlock functionBlock; + PropertyDelegate& delegate; + BufferOutput& output; + CacheProvider& cacheProvider; + + std::map> registry; +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIResponderDelegate.h b/modules/juce_midi_ci/ci/juce_CIResponderDelegate.h new file mode 100644 index 0000000000..c3c7da9cca --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIResponderDelegate.h @@ -0,0 +1,50 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +/** + An interface for types that implement responses for certain message types. + + @tags{Audio} +*/ +class ResponderDelegate +{ +public: + ResponderDelegate() = default; + virtual ~ResponderDelegate() = default; + + /** If the message is processed successfully, and a response sent, then + this returns true. Otherwise, returns false, allowing other ResponderDelegates + to attempt to handle the message if necessary. + */ + virtual bool tryRespond (ResponderOutput& output, const Message::Parsed& message) = 0; + + JUCE_DECLARE_NON_COPYABLE (ResponderDelegate) + JUCE_DECLARE_NON_MOVEABLE (ResponderDelegate) +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIResponderOutput.cpp b/modules/juce_midi_ci/ci/juce_CIResponderOutput.cpp new file mode 100644 index 0000000000..bc607f8e6e --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIResponderOutput.cpp @@ -0,0 +1,44 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +ChannelAddress ResponderOutput::getChannelAddress() const +{ + return ChannelAddress{}.withGroup (getIncomingGroup()) + .withChannel (getIncomingHeader().deviceID); +} + +Message::Header ResponderOutput::getReplyHeader (std::byte replySubID) const +{ + return { getIncomingHeader().deviceID, + replySubID, + detail::MessageMeta::implementationVersion, + getMuid(), + getIncomingHeader().source }; +} + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CIResponderOutput.h b/modules/juce_midi_ci/ci/juce_CIResponderOutput.h new file mode 100644 index 0000000000..38d2d56712 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CIResponderOutput.h @@ -0,0 +1,83 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +/** + Represents a destination into which MIDI-CI messages can be written. + + Each message should be written into the output buffer. Then, send() will + send the current contents of the buffer to the specified group. + + @tags{Audio} +*/ +class BufferOutput +{ +public: + BufferOutput() = default; + virtual ~BufferOutput() = default; + + /** Returns the MUID of the responder. */ + virtual MUID getMuid() const = 0; + + /** Returns the buffer into which replies should be written. */ + virtual std::vector& getOutputBuffer() = 0; + + /** Sends the current contents of the buffer to the provided group. */ + virtual void send (uint8_t group) = 0; + + JUCE_DECLARE_NON_COPYABLE (BufferOutput) + JUCE_DECLARE_NON_MOVEABLE (BufferOutput) +}; + +//============================================================================== +/** + A buffer output that additionally provides information about an incoming message, so that + an appropriate reply can be constructed for that message. + + @tags{Audio} +*/ +class ResponderOutput : public BufferOutput +{ +public: + /** Returns the header of the message that was received. */ + virtual Message::Header getIncomingHeader() const = 0; + + /** Returns the group of the message that was received. */ + virtual uint8_t getIncomingGroup() const = 0; + + /** Returns the channel to which the incoming message was addressed. */ + ChannelAddress getChannelAddress() const; + + /** Returns a default header that can be used for outgoing replies. + + This always sets the destination MUID equal to the source MUID of the incoming header, + so it's not suitable for broadcast messages. + */ + Message::Header getReplyHeader (std::byte replySubID) const; +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CISubscription.h b/modules/juce_midi_ci/ci/juce_CISubscription.h new file mode 100644 index 0000000000..fcd7c45a52 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CISubscription.h @@ -0,0 +1,54 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +//============================================================================== +/** + Matches a subscription ID to a resource name. + + @tags{Audio} +*/ +struct Subscription +{ + String subscribeId; + String resource; + + bool operator< (const Subscription& other) const { return subscribeId < other.subscribeId; } + bool operator<= (const Subscription& other) const { return subscribeId <= other.subscribeId; } + bool operator> (const Subscription& other) const { return subscribeId > other.subscribeId; } + bool operator>= (const Subscription& other) const { return subscribeId >= other.subscribeId; } + + bool operator== (const Subscription& other) const + { + const auto tie = [] (const auto& x) { return std::tie (x.subscribeId, x.resource); }; + return tie (*this) == tie (other); + } + + bool operator!= (const Subscription& other) const { return ! operator== (other); } +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/ci/juce_CISupportedAndActive.h b/modules/juce_midi_ci/ci/juce_CISupportedAndActive.h new file mode 100644 index 0000000000..d4c283b751 --- /dev/null +++ b/modules/juce_midi_ci/ci/juce_CISupportedAndActive.h @@ -0,0 +1,55 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci +{ + +/** + Holds the maximum number of channels that may be activated for a MIDI-CI + profile, along with the number of channels that are currently active. + + @tags{Audio} +*/ +struct SupportedAndActive +{ + uint16_t supported{}; ///< The maximum number of member channels for a profile. 0 indicates that the profile is unsupported. + uint16_t active{}; ///< The number of member channels currently active for a profile. 0 indicates that the profile is inactive. + + /** Returns true if supported is non-zero. */ + bool isSupported() const { return supported != 0; } + + /** Returns true if active is non-zero. */ + bool isActive() const { return active != 0; } + + bool operator== (const SupportedAndActive& other) const + { + const auto tie = [] (auto& x) { return std::tie (x.supported, x.active); }; + return tie (*this) == tie (other); + } + + bool operator!= (const SupportedAndActive& other) const { return ! operator== (other); } +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/detail/juce_CIMarshalling.h b/modules/juce_midi_ci/detail/juce_CIMarshalling.h new file mode 100644 index 0000000000..fdaa28c9ce --- /dev/null +++ b/modules/juce_midi_ci/detail/juce_CIMarshalling.h @@ -0,0 +1,376 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +/* + Utilities for converting sequences of bytes to and from + C++ struct types. +*/ +namespace juce::midi_ci::detail::Marshalling +{ + +template struct IntForNumBytes; +template <> struct IntForNumBytes<1> { using Type = uint8_t; }; +template <> struct IntForNumBytes<2> { using Type = uint16_t; }; +template <> struct IntForNumBytes<4> { using Type = uint32_t; }; + +template using IntForNumBytesT = typename IntForNumBytes::Type; + +//============================================================================== +/* + Reads a sequence of bytes representing a MIDI-CI message, and populates + structs with the information contained in the message. +*/ +class Reader +{ +public: + /* Constructs a reader that will parse the provided buffer, using the most + recent known MIDI-CI version. + */ + explicit Reader (Span b) + : Reader (b, static_cast (MessageMeta::implementationVersion)) {} + + /* Constructs a reader for the provided MIDI-CI version that will parse + the provided buffer. Fields introduced in later versions will be ignored, + and so left with their default values. + */ + Reader (Span b, int v) + : bytes (b), version (v) {} + + std::optional getVersion() const { return version; } + + /* Attempts to interpret the byte sequence passed to the constructor + as a sequence of structs 'T'. + + Returns true if parsing succeeds, otherwise returns false. + */ + template + bool operator() (T&&... t) + { + return (doArchiveChecked (std::forward (t)) && ...); + } + +private: + template + bool doArchiveChecked (T&& t) + { + if (failed) + return false; + + doArchive (t); + return ! failed; + } + + void doArchive (ChannelInGroup& x) + { + if (const auto popped = popBytes (1)) + { + const auto p = *popped; + x = ChannelInGroup (p[0] & std::byte { 0x7f }); + return; + } + + failed = true; + } + + // If we're trying to parse into a constant, then we should check that the next byte(s) + // match that constant. + void doArchive (const std::byte& x) + { + std::byte temp{}; + + if (! doArchiveChecked (temp)) + return; + + failed |= x != temp; + } + + void doArchive (std::byte& x) + { + if (const auto popped = popBytes (1)) + { + const auto p = *popped; + x = p[0] & std::byte { 0x7f }; + return; + } + + failed = true; + } + + void doArchive (const uint16_t& x) + { + uint16_t temp{}; + + if (! doArchiveChecked (temp)) + return; + + failed |= temp != x; + } + + void doArchive (uint16_t& x) + { + if (const auto popped = popBytes (2)) + { + const auto p = *popped; + x = (uint16_t) (((uint16_t) p[0] & 0x7f) << 0x00) + | (uint16_t) (((uint16_t) p[1] & 0x7f) << 0x07); + return; + } + + failed = true; + } + + void doArchive (const uint32_t& x) + { + uint32_t temp{}; + + if (! doArchiveChecked (temp)) + return; + + failed |= temp != x; + } + + void doArchive (uint32_t& x) + { + if (const auto popped = popBytes (4)) + { + const auto p = *popped; + x = (((uint32_t) p[0] & 0x7f) << 0x00) + | (((uint32_t) p[1] & 0x7f) << 0x07) + | (((uint32_t) p[2] & 0x7f) << 0x0e) + | (((uint32_t) p[3] & 0x7f) << 0x15); + return; + } + + failed = true; + } + + template + void doArchive (MessageMeta::SpanWithSizeBytes, B> x) + { + IntForNumBytesT numBytes{}; + + // Read the number of bytes in the field + if (! doArchiveChecked (numBytes)) + return; + + // Attempt to pop that many bytes + if (const auto popped = popBytes (numBytes)) + { + x.span = *popped; + return; + } + + failed = true; + } + + template + void doArchive (MessageMeta::SpanWithSizeBytes>> x) + { + IntForNumBytesT numItems{}; + + // Read the number of items in the field + if (! doArchiveChecked (numItems)) + return; + + if (const auto popped = popBytes (numItems * N)) + { + x.span = Span (unalignedPointerCast*> (popped->data()), numItems); + return; + } + + failed = true; + } + + template + void doArchive (Span& x) + { + if (const auto popped = popBytes (bytes.size())) + { + x = *popped; + return; + } + + failed = true; + } + + template + void doArchive (std::array& x) + { + if (const auto popped = popBytes (x.size())) + { + const auto p = *popped; + std::transform (p.begin(), p.end(), x.begin(), [] (std::byte b) + { + return b & std::byte { 0x7f }; + }); + return; + } + + failed = true; + } + + template + void doArchive (T& t) + { + juce::detail::doLoad (*this, t); + } + + template + void doArchive (Named named) + { + doArchiveChecked (named.value); + } + + std::optional> popBytes (size_t num) + { + if (bytes.size() < num) + return {}; + + const Span result { bytes.data(), num }; + bytes = Span { bytes.data() + num, bytes.size() - num }; + return result; + } + + Span bytes; /* Bytes making up a CI message. */ + int version{}; /* The version to assume when parsing the message, specified in the message header. */ + bool failed = false; +}; + +//============================================================================== +/* + Converts one or more structs into a byte sequence suitable for transmission + as a MIDI-CI message. +*/ +class Writer +{ +public: + /* Constructs a writer that will write into the provided buffer. */ + explicit Writer (std::vector& b) + : Writer (b, static_cast (MessageMeta::implementationVersion)) {} + + /* Constructs a writer that will write a MIDI-CI message of the requested + version to the provided buffer. + + Fields introduced in later MIDI-CI versions will be ignored. + */ + Writer (std::vector& b, int v) + : bytes (b), version (v) {} + + std::optional getVersion() const { return version; } + + /* Formats the information contained in the provided structs into a + MIDI-CI message, and returns a bool indicating success or failure. + */ + template + bool operator() (const T&... t) + { + return (doArchiveChecked (t) && ...); + } + +private: + template + bool doArchiveChecked (T&& t) + { + if (failed) + return false; + + doArchive (t); + return ! failed; + } + + void doArchive (ChannelInGroup x) + { + doArchiveChecked (std::byte (x)); + } + + void doArchive (std::byte x) + { + bytes.push_back (x); + } + + void doArchive (uint16_t x) + { + bytes.insert (bytes.end(), { (std::byte) ((x >> 0x00) & 0x7f), + (std::byte) ((x >> 0x07) & 0x7f) }); + } + + void doArchive (uint32_t x) + { + bytes.insert (bytes.end(), { (std::byte) ((x >> 0x00) & 0x7f), + (std::byte) ((x >> 0x07) & 0x7f), + (std::byte) ((x >> 0x0e) & 0x7f), + (std::byte) ((x >> 0x15) & 0x7f) }); + } + + template + void doArchive (MessageMeta::SpanWithSizeBytes x) + { + if (x.span.size() >= (1 << (7 * NumBytes))) + { + // Unable to express the size of the field in the requested number of bytes + jassertfalse; + failed = true; + return; + } + + // Write the number of bytes, followed by the bytes themselves. + const auto numBytes = (IntForNumBytesT) x.span.size(); + doArchiveChecked (numBytes); + doArchiveChecked (x.span); + } + + template + void doArchive (Span x) + { + failed = ! std::all_of (x.begin(), x.end(), [&] (const auto& item) + { + return doArchiveChecked (item); + }); + } + + template + void doArchive (const std::array& x) + { + bytes.insert (bytes.end(), x.begin(), x.end()); + } + + template + void doArchive (const T& t) + { + juce::detail::doSave (*this, t); + } + + template + void doArchive (Named named) + { + doArchiveChecked (named.value); + } + + std::vector& bytes; /* The buffer that will hold the completed message. */ + int version{}; /* The version to assume when writing the message, specified in the message header. */ + bool failed = false; +}; + +} // namespace juce::midi_ci::detail::Marshalling diff --git a/modules/juce_midi_ci/detail/juce_CIMessageMeta.h b/modules/juce_midi_ci/detail/juce_CIMessageMeta.h new file mode 100644 index 0000000000..5ede3a4066 --- /dev/null +++ b/modules/juce_midi_ci/detail/juce_CIMessageMeta.h @@ -0,0 +1,624 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +/* + Namespace containing metadata about MIDI-CI message types, such as + replies corresponding to inquiries, and serialization functions. + + @tags{Audio} +*/ +namespace juce::midi_ci::detail::MessageMeta +{ + +//============================================================================== +/* The maximum CI version that can be parsed and generated by this implementation. */ +static constexpr std::byte implementationVersion { 0x02 }; + +/* @internal + + Wraps a pointer to a Span. Used to indicate to CI readers/writers that a particular field is + of variable length, starting with a 16-bit or 32-bit byte count. +*/ +template +struct SpanWithSizeBytes +{ + T& span; +}; + +/* @internal + + Creates a SpanWithSizeBytes with an appropriate template argument. +*/ +template +static constexpr auto makeSpanWithSizeBytes ( Span& span) { return SpanWithSizeBytes> { span }; } + +template +static constexpr auto makeSpanWithSizeBytes (const Span& span) { return SpanWithSizeBytes> { span }; } + +template +static constexpr auto makeJsonWithSizeBytes ( Span& span) { return SpanWithSizeBytes, true> { span }; } + +template +static constexpr auto makeJsonWithSizeBytes (const Span& span) { return SpanWithSizeBytes, true> { span }; } + +template +struct Metadata +{ + static constexpr std::byte subID2 { SubID2 }; + using Reply = R; +}; + +template +struct Meta; + +template <> +struct Meta : Metadata<0x71> {}; + +template <> +struct Meta : Metadata<0x70, Message::DiscoveryResponse> {}; + +template <> +struct Meta : Metadata<0x73> {}; + +template <> +struct Meta : Metadata<0x72, Message::EndpointInquiryResponse> {}; + +template <> +struct Meta : Metadata<0x7e> {}; + +template <> +struct Meta : Metadata<0x7d> {}; + +template <> +struct Meta : Metadata<0x7f> {}; + +template <> +struct Meta : Metadata<0x21> {}; + +template <> +struct Meta : Metadata<0x20, Message::ProfileInquiryResponse> {}; + +template <> +struct Meta : Metadata<0x26> {}; + +template <> +struct Meta : Metadata<0x27> {}; + +template <> +struct Meta : Metadata<0x29> {}; + +template <> +struct Meta : Metadata<0x28, Message::ProfileDetailsResponse> {}; + +template <> +struct Meta : Metadata<0x22> {}; + +template <> +struct Meta : Metadata<0x23> {}; + +template <> +struct Meta : Metadata<0x24> {}; + +template <> +struct Meta : Metadata<0x25> {}; + +template <> +struct Meta : Metadata<0x2f> {}; + +template <> +struct Meta : Metadata<0x31> {}; + +template <> +struct Meta : Metadata<0x30, Message::PropertyExchangeCapabilitiesResponse> {}; + +template <> +struct Meta {}; + +template <> +struct Meta {}; + +template <> +struct Meta : Meta, Metadata<0x35> {}; + +template <> +struct Meta : Meta, Metadata<0x34, Message::PropertyGetDataResponse> {}; + +template <> +struct Meta : Meta, Metadata<0x37> {}; + +template <> +struct Meta : Meta, Metadata<0x36, Message::PropertySetDataResponse> {}; + +template <> +struct Meta : Meta, Metadata<0x39> {}; + +template <> +struct Meta : Meta, Metadata<0x38, Message::PropertySubscribeResponse> {}; + +template <> +struct Meta : Meta, Metadata<0x3f> {}; + +template <> +struct Meta : Metadata<0x41> {}; + +template <> +struct Meta : Metadata<0x40, Message::ProcessInquiryResponse> {}; + +template <> +struct Meta : Metadata<0x43> {}; + +template <> +struct Meta : Metadata<0x42, Message::ProcessMidiMessageReportResponse> {}; + +template <> +struct Meta : Metadata<0x44> {}; + +} // namespace juce::midi_ci::detail::MessageMeta + +namespace juce +{ + +struct VersionBase +{ + static constexpr auto marshallingVersion = (int) juce::midi_ci::detail::MessageMeta::implementationVersion; +}; + +template +struct SerialisationTraits> +{ + static constexpr auto marshallingVersion = std::nullopt; + + template + static auto getSize (This& t) + { + if constexpr (NumBytes == 1) + return (uint8_t) t.size(); + else if constexpr (NumBytes == 2) + return (uint16_t) t.size(); + else if constexpr (NumBytes == 4) + return (uint32_t) t.size(); + else if constexpr (NumBytes == 8) + return (uint64_t) t.size(); + else + static_assert (detail::delayStaticAssert, "NumBytes is not a power of two"); + } + + template + static auto load (Archive&, This&) + { + } + + template + static auto save (Archive& archive, const This& t) + { + auto size = getSize (t.span); + archive (serialisationSize (size)); + + for (const auto& element : t.span) + archive (element); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto load (Archive& archive, ci::MUID& t) + { + uint32_t muid{}; + auto result = archive (muid); + t = ci::MUID::makeUnchecked (muid); + return result; + } + + template + static auto save (Archive& archive, const ci::MUID& t) + { + return archive (t.get()); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + const std::byte universalSystemExclusive { 0x7e }, subID { 0x0d }; + return archive (universalSystemExclusive, + t.deviceID, + subID, + t.category, + t.version, + t.source, + t.destination); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + return archive (t.header, t.data); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (named ("device", t.device), + named ("capabilities", t.capabilities), + named ("maximumSysexSize", t.maximumSysexSize)); + + if (0x02 <= archive.getVersion()) + archive (named ("outputPathID", t.outputPathID), named ("functionBlock", t.functionBlock)); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (named ("device", t.device), + named ("capabilities", t.capabilities), + named ("maximumSysexSize", t.maximumSysexSize)); + + if (0x02 <= archive.getVersion()) + archive (named ("outputPathID", t.outputPathID)); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (named ("status", t.status), + named ("data", midi_ci::detail::MessageMeta::makeSpanWithSizeBytes<2> (t.data))); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (named ("status", t.status)); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (named ("target", t.target)); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (named ("originalCategory", t.originalCategory), + named ("statusCode", t.statusCode), + named ("statusData", t.statusData), + named ("details", t.details), + named ("messageText", midi_ci::detail::MessageMeta::makeSpanWithSizeBytes<2> (t.messageText))); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + if (0x02 <= archive.getVersion()) + { + archive (named ("originalCategory", t.originalCategory), + named ("statusCode", t.statusCode), + named ("statusData", t.statusData), + named ("details", t.details), + named ("messageText", midi_ci::detail::MessageMeta::makeSpanWithSizeBytes<2> (t.messageText))); + } + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (named ("enabledProfiles", midi_ci::detail::MessageMeta::makeSpanWithSizeBytes<2> (t.enabledProfiles)), + named ("disabledProfiles", midi_ci::detail::MessageMeta::makeSpanWithSizeBytes<2> (t.disabledProfiles))); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive&, This&) + { + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (named ("profile", t.profile)); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (named ("profile", t.profile)); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (named ("profile", t.profile), + named ("target", t.target), + named ("data", midi_ci::detail::MessageMeta::makeSpanWithSizeBytes<2> (t.data))); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (named ("profile", t.profile), + named ("target", t.target)); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (named ("profile", t.profile)); + + if (0x02 <= archive.getVersion()) + archive (named ("numChannels", t.numChannels)); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (named ("profile", t.profile)); + + if (0x02 <= archive.getVersion()) + { + uint16_t reserved{}; + archive (named ("reserved", reserved)); + } + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (named ("profile", t.profile)); + + if (0x02 <= archive.getVersion()) + archive (named ("numChannels", t.numChannels)); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (named ("profile", t.profile)); + + if (0x02 <= archive.getVersion()) + archive (named ("numChannels", t.numChannels)); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (named ("profile", t.profile), named ("data", midi_ci::detail::MessageMeta::makeSpanWithSizeBytes<4> (t.data))); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (named ("numRequests", t.numSimultaneousRequestsSupported)); + + if (0x02 <= archive.getVersion()) + archive (named ("major", t.majorVersion), named ("minor", t.minorVersion)); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (named ("numRequests", t.numSimultaneousRequestsSupported)); + + if (0x02 <= archive.getVersion()) + archive (named ("major", t.majorVersion), named ("minor", t.minorVersion)); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + const uint16_t chunkNum = 1, dataLength = 0; + archive (named ("requestID", t.requestID), + named ("header", midi_ci::detail::MessageMeta::makeJsonWithSizeBytes<2> (t.header)), + named ("numChunks", chunkNum), + named ("thisChunk", chunkNum), + named ("length", dataLength)); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (named ("requestID", t.requestID), + named ("header", midi_ci::detail::MessageMeta::makeJsonWithSizeBytes<2> (t.header)), + named ("numChunks", t.totalNumChunks), + named ("thisChunk", t.thisChunkNum), + named ("data", midi_ci::detail::MessageMeta::makeSpanWithSizeBytes<2> (t.data))); + } +}; + +template <> +struct SerialisationTraits : SerialisationTraits {}; + +template <> +struct SerialisationTraits : SerialisationTraits {}; + +template <> +struct SerialisationTraits : SerialisationTraits {}; + +template <> +struct SerialisationTraits : SerialisationTraits {}; + +template <> +struct SerialisationTraits : SerialisationTraits {}; + +template <> +struct SerialisationTraits : SerialisationTraits {}; + +template <> +struct SerialisationTraits : SerialisationTraits {}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + archive (t.supportedFeatures); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive&, This&) + { + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + std::byte reserved{}; + archive (named ("messageDataControl", t.messageDataControl), + named ("requestedMessages", t.requestedMessages), + named ("reserved", reserved), + named ("channelControllerMessages", t.channelControllerMessages), + named ("noteDataMessages", t.noteDataMessages)); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive& archive, This& t) + { + std::byte reserved{}; + archive (named ("messageDataControl", t.messageDataControl), + named ("requestedMessages", t.requestedMessages), + named ("reserved", reserved), + named ("channelControllerMessages", t.channelControllerMessages), + named ("noteDataMessages", t.noteDataMessages)); + } +}; + +template <> +struct SerialisationTraits : VersionBase +{ + template + static auto serialise (Archive&, This&) + { + } +}; + +} // namespace juce diff --git a/modules/juce_midi_ci/detail/juce_CIMessageTypeUtils.h b/modules/juce_midi_ci/detail/juce_CIMessageTypeUtils.h new file mode 100644 index 0000000000..4e8bfc1c4c --- /dev/null +++ b/modules/juce_midi_ci/detail/juce_CIMessageTypeUtils.h @@ -0,0 +1,245 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci::detail::MessageTypeUtils +{ + +//============================================================================== +/* + An interface used for types that want to operate on parsed MIDI-CI messages. + + @tags{Audio} +*/ +struct MessageVisitor +{ + MessageVisitor() = default; + MessageVisitor (const MessageVisitor&) = default; + MessageVisitor (MessageVisitor&&) = default; + MessageVisitor& operator= (const MessageVisitor&) = default; + MessageVisitor& operator= (MessageVisitor&&) = default; + virtual ~MessageVisitor() = default; + + virtual void visit (const std::monostate&) const {} + virtual void visit (const Message::Discovery&) const {} + virtual void visit (const Message::EndpointInquiry&) const {} + virtual void visit (const Message::ProfileInquiry&) const {} + virtual void visit (const Message::ProfileDetails&) const {} + virtual void visit (const Message::PropertyExchangeCapabilities&) const {} + virtual void visit (const Message::PropertyGetData&) const {} + virtual void visit (const Message::PropertySetData&) const {} + virtual void visit (const Message::PropertySubscribe&) const {} + virtual void visit (const Message::ProcessInquiry&) const {} + virtual void visit (const Message::ProcessMidiMessageReport&) const {} + virtual void visit (const Message::DiscoveryResponse&) const {} + virtual void visit (const Message::EndpointInquiryResponse&) const {} + virtual void visit (const Message::InvalidateMUID&) const {} + virtual void visit (const Message::ACK&) const {} + virtual void visit (const Message::NAK&) const {} + virtual void visit (const Message::ProfileInquiryResponse&) const {} + virtual void visit (const Message::ProfileAdded&) const {} + virtual void visit (const Message::ProfileRemoved&) const {} + virtual void visit (const Message::ProfileDetailsResponse&) const {} + virtual void visit (const Message::ProfileOn&) const {} + virtual void visit (const Message::ProfileOff&) const {} + virtual void visit (const Message::ProfileEnabledReport&) const {} + virtual void visit (const Message::ProfileDisabledReport&) const {} + virtual void visit (const Message::ProfileSpecificData&) const {} + virtual void visit (const Message::PropertyExchangeCapabilitiesResponse&) const {} + virtual void visit (const Message::PropertyGetDataResponse&) const {} + virtual void visit (const Message::PropertySetDataResponse&) const {} + virtual void visit (const Message::PropertySubscribeResponse&) const {} + virtual void visit (const Message::PropertyNotify&) const {} + virtual void visit (const Message::ProcessInquiryResponse&) const {} + virtual void visit (const Message::ProcessMidiMessageReportResponse&) const {} + virtual void visit (const Message::ProcessEndMidiMessageReport&) const {} +}; + +using ParseFn = Message::Parsed::Body (*) (Message::Generic, Parser::Status* status); +using VisitFn = void (*) (const Message::Parsed&, const MessageVisitor&); + +/* These return the Universal System Exclusive Sub-ID#2 for a particular message type. */ +template +static constexpr auto getParserFor (std::in_place_type_t) +{ + return [] (Message::Generic message, Parser::Status* status) -> Message::Parsed::Body + { + // Parse messages using the version specified in the header of the message + if (Specific parsed; Marshalling::Reader { message.data, static_cast (message.header.version) } (parsed)) + { + return parsed; + } + + if (status != nullptr) + *status = Parser::Status::malformed; + + return std::monostate{}; + }; +} + +template +static constexpr auto getVisitorFor (std::in_place_type_t) +{ + return [] (const Message::Parsed& parsed, const MessageVisitor& visitor) + { + if (auto* body = std::get_if (&parsed.body)) + visitor.visit (*body); + }; +} + +template +struct LookupTables +{ + constexpr LookupTables() + { + for (auto& x : parsers) + { + x = [] (Message::Generic, Parser::Status* status) -> Message::Parsed::Body + { + if (status != nullptr) + *status = Parser::Status::unrecognisedMessage; + + return std::monostate{}; + }; + } + + for (auto& x : visitors) + { + x = [] (const Message::Parsed&, const MessageVisitor& visitor) + { + visitor.visit (std::monostate{}); + }; + } + + (registerTag (std::in_place_type), ...); + } + + template + constexpr void registerTag (std::in_place_type_t tag) + { + constexpr auto category = MessageMeta::Meta::subID2; + parsers[uint8_t (category)] = getParserFor (tag); + visitors[uint8_t (category)] = getVisitorFor (tag); + } + + ParseFn parsers[std::numeric_limits::max()]{}; + VisitFn visitors[std::numeric_limits::max()]{}; +}; + +template +static void send (BufferOutput& output, uint8_t group, const Message::Header& header, const Body& body) +{ + output.getOutputBuffer().clear(); + Marshalling::Writer { output.getOutputBuffer() } (header, body); + output.send (group); +} + +template +static void send (BufferOutput& output, uint8_t group, MUID targetMuid, ChannelInGroup cig, const Body& body) +{ + Message::Header header + { + cig, + MessageMeta::Meta::subID2, + MessageMeta::implementationVersion, + output.getMuid(), + targetMuid, + }; + + send (output, group, header, body); +} + + +template +static void send (ResponderOutput& output, const Body& body) +{ + send (output, output.getIncomingGroup(), output.getReplyHeader (MessageMeta::Meta::subID2), body); +} + +static void sendNAK (ResponderOutput& output, std::byte statusCode) +{ + const auto header = output.getReplyHeader (MessageMeta::Meta::subID2); + const Message::NAK body { output.getIncomingHeader().category, + statusCode, + std::byte { 0x00 }, + {}, // No additional details + {} }; // No message text + send (output, output.getIncomingGroup(), header, body); +} + +class BaseCaseDelegate : public ResponderDelegate +{ +public: + bool tryRespond (ResponderOutput& output, const Message::Parsed&) override + { + sendNAK (output, {}); + return true; + } +}; + +static constexpr auto getTables() +{ + return LookupTables{}; + +} + +static void visit (const Message::Parsed& msg, const MessageVisitor& visitor) +{ + constexpr auto tables = getTables(); + const auto fn = tables.visitors[(uint8_t) msg.header.category]; + fn (msg, visitor); +} + +} // namespace juce::midi_ci::detail::MessageTypeUtils diff --git a/modules/juce_midi_ci/detail/juce_CIPropertyDataMessageChunker.cpp b/modules/juce_midi_ci/detail/juce_CIPropertyDataMessageChunker.cpp new file mode 100644 index 0000000000..35f1775011 --- /dev/null +++ b/modules/juce_midi_ci/detail/juce_CIPropertyDataMessageChunker.cpp @@ -0,0 +1,152 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci::detail +{ + +PropertyDataMessageChunker::PropertyDataMessageChunker (std::vector& storageIn, + int chunkSizeIn, + const std::byte messageKindIn, + const std::byte requestIdIn, + Span headerIn, + MUID sourceIn, + MUID destIn, + InputStream& bodyIn) + : header (headerIn), + storage (&storageIn), + body (&bodyIn), + source (sourceIn), + dest (destIn), + chunkSize (chunkSizeIn), + messageKind (messageKindIn), + requestId (requestIdIn) +{ + if (hasRoomForBody()) + { + populateStorage(); + } + else + { + // Header too large! There's no way to fit this message into the requested chunk size. + jassertfalse; + *this = PropertyDataMessageChunker(); + } +} + +PropertyDataMessageChunker& PropertyDataMessageChunker::operator++() noexcept +{ + if (*this != PropertyDataMessageChunker()) + { + if (body->isExhausted()) + { + *this = PropertyDataMessageChunker(); + } + else + { + ++thisChunk; + populateStorage(); + } + } + + return *this; +} + +Span PropertyDataMessageChunker::operator*() const noexcept +{ + // The end of the stream was reached, no point dereferencing the iterator now! + jassert (storage != nullptr && (int) storage->size() <= chunkSize); + return *storage; +} + +Span PropertyDataMessageChunker::getHeaderForBlock() const +{ + return thisChunk == 1 ? header : Span{}; +} + +int PropertyDataMessageChunker::getRoomForBody() const +{ + return chunkSize - (int) (getHeaderForBlock().size() + 22); +} + +bool PropertyDataMessageChunker::hasRoomForBody() const +{ + const auto bodyRoom = getRoomForBody(); + return (0 < bodyRoom) + || (0 == bodyRoom && body->getNumBytesRemaining() == 0); +} + +void PropertyDataMessageChunker::populateStorage() const +{ + storage->clear(); + storage->resize ((size_t) getRoomForBody()); + + // Read body data into buffer + const auto numBytesRead = (uint16_t) jmax (ssize_t (0), body->read (storage->data(), storage->size())); + + const auto [numChunks, thisChunkNum] = [&]() -> std::tuple + { + if (body->isExhausted() || body->getNumBytesRemaining() == 0) + return std::tuple (thisChunk, thisChunk); + + const auto totalLength = body->getTotalLength(); + + if (totalLength < 0) + return std::tuple ((uint16_t) 0, thisChunk); // 0 means "unknown number" + + const auto roomForBody = getRoomForBody(); + + if (roomForBody != 0) + return std::tuple ((uint16_t) ((totalLength + roomForBody - 1) / roomForBody), thisChunk); + + // During construction, the input stream reported that it had no data remaining, so no + // space was reserved for body content. + // Now, the input stream reports that it has data remaining, but there's nowhere + // to fit it in the message! + jassertfalse; + return std::tuple (thisChunk, (uint16_t) 0); // 0 means "data potentially unusable" + }(); + + // Now we know how many bytes we managed to read, write the header at the end of the buffer + const auto headerForBlock = getHeaderForBlock(); + detail::Marshalling::Writer writer { *storage }; + writer (Message::Header { ChannelInGroup::wholeBlock, + messageKind, + detail::MessageMeta::implementationVersion, + source, + dest }, + requestId, + detail::MessageMeta::makeSpanWithSizeBytes<2> (headerForBlock), + numChunks, + thisChunkNum, + numBytesRead); + + // Finally, swap the header to the beginning of the buffer + std::rotate (storage->begin(), storage->begin() + getRoomForBody(), storage->end()); + + // ...and bring the storage buffer down to size, if we didn't manage to fill it + storage->resize (storage->size() + numBytesRead - (size_t) getRoomForBody()); +} + +} // namespace juce::midi_ci::detail diff --git a/modules/juce_midi_ci/detail/juce_CIPropertyDataMessageChunker.h b/modules/juce_midi_ci/detail/juce_CIPropertyDataMessageChunker.h new file mode 100644 index 0000000000..c008171658 --- /dev/null +++ b/modules/juce_midi_ci/detail/juce_CIPropertyDataMessageChunker.h @@ -0,0 +1,97 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci::detail +{ + +/* + Breaks up a large property exchange message into chunks of the requested size. + + Note that the header *must* fit inside the first block, so you must ensure + that the header is small enough to fit inside the requested chunk size. +*/ +class PropertyDataMessageChunker +{ + auto tie() const { return std::tie (storage, body, source, dest, chunkSize, messageKind, requestId); } + +public: + /* Constructs a chunker instance. + + @param storageIn backing storage where each chunk will be written + @param chunkSizeIn the maximum size of each chunk + @param messageKindIn the subID2 byte identifying the type of message in each chunk + @param requestIdIn the id that should be included in all messages that are part of the same property exchange transaction + @param headerIn the header bytes of the message. This is always JSON encoded as 7-bit ASCII text, see the MIDI-CI spec for full details + @param sourceIn the MUID of the device sending the chunked messages + @param destIn the MUID of the recipient of the chunked messages + @param bodyIn a stream that can supply the data payload for this chunk sequence. All payload bytes *must* be 7-bit (MSB not set). + */ + PropertyDataMessageChunker (std::vector& storageIn, + int chunkSizeIn, + const std::byte messageKindIn, + const std::byte requestIdIn, + Span headerIn, + MUID sourceIn, + MUID destIn, + InputStream& bodyIn); + + /* Returns true if this chunker hasn't finished producing chunks. */ + explicit operator bool() const { return *this != PropertyDataMessageChunker(); } + + /* Allowing foreach usage. */ + auto begin() const { return *this; } + + /* Allow foreach usage. */ + auto end() const { return PropertyDataMessageChunker{}; } + + /* Writes the bytes of the next chunk, if any, into the storage buffer. */ + PropertyDataMessageChunker& operator++() noexcept; + + /* Checks whether the state of this chunker matches the state of another chunker, enabling foreach usage. */ + bool operator== (const PropertyDataMessageChunker& other) const noexcept { return tie() == other.tie(); } + bool operator!= (const PropertyDataMessageChunker& other) const noexcept { return tie() != other.tie(); } + + /* Returns a span over the valid bytes in the output buffer. */ + Span operator*() const noexcept; + +private: + PropertyDataMessageChunker() = default; + + Span getHeaderForBlock() const; + int getRoomForBody() const; + bool hasRoomForBody() const; + void populateStorage() const; + + Span header; + std::vector* storage{}; + InputStream* body{}; + MUID source = MUID::makeUnchecked (0), dest = MUID::makeUnchecked (0); + int chunkSize{}; + uint16_t thisChunk { 0x01 }; + std::byte messageKind{}; + std::byte requestId{}; +}; + +} // namespace juce::midi_ci::detail diff --git a/modules/juce_midi_ci/detail/juce_CIPropertyHostUtils.h b/modules/juce_midi_ci/detail/juce_CIPropertyHostUtils.h new file mode 100644 index 0000000000..cc55e939dc --- /dev/null +++ b/modules/juce_midi_ci/detail/juce_CIPropertyHostUtils.h @@ -0,0 +1,78 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci::detail +{ + +struct PropertyHostUtils +{ + PropertyHostUtils() = delete; + + static void send (BufferOutput& output, + uint8_t group, + std::byte subID2, + MUID targetMuid, + std::byte requestID, + Span header, + Span body, + int chunkSize) + { + MemoryInputStream stream (body.data(), body.size(), false); + const detail::PropertyDataMessageChunker chunker { output.getOutputBuffer(), + std::min (chunkSize, 1 << 16), + subID2, + requestID, + header, + output.getMuid(), + targetMuid, + stream }; + + std::for_each (chunker.begin(), chunker.end(), [&] (auto) { output.send (group); }); + } + + static auto getTerminator (BufferOutput& output, FunctionBlock fb, MUID them) + { + const auto us = output.getMuid(); + return [&output, fb, us, them] (std::byte id) + { + const Message::Header notifyHeader + { + ChannelInGroup::wholeBlock, + detail::MessageMeta::Meta::subID2, + detail::MessageMeta::implementationVersion, + us, + them, + }; + + const auto jsonHeader = Encodings::jsonTo7BitText (JSONUtils::makeObjectWithKeyFirst ({ { "status", 104 } }, "status")); + detail::MessageTypeUtils::send (output, + fb.firstGroup, + notifyHeader, + Message::PropertyNotify { { id, jsonHeader, 1, 1, {} } }); + }; + } + +}; +} // namespace juce::midi_ci::detail diff --git a/modules/juce_midi_ci/detail/juce_CIResponder.cpp b/modules/juce_midi_ci/detail/juce_CIResponder.cpp new file mode 100644 index 0000000000..9f3aa8b8c2 --- /dev/null +++ b/modules/juce_midi_ci/detail/juce_CIResponder.cpp @@ -0,0 +1,507 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + + +namespace juce::midi_ci::detail +{ + +Parser::Status Responder::processCompleteMessage (BufferOutput& output, + ump::BytesOnGroup message, + Span listeners) +{ + auto status = Parser::Status::noError; + const auto parsed = Parser::parse (output.getMuid(), message.bytes, &status); + + if (! parsed.has_value()) + return Parser::Status::malformed; + + class Output : public ResponderOutput + { + public: + Output (BufferOutput& o, Message::Header h, uint8_t g) + : innerOutput (o), header (h), group (g) {} + + MUID getMuid() const override { return innerOutput.getMuid(); } + Message::Header getIncomingHeader() const override { return header; } + uint8_t getIncomingGroup() const override { return group; } + std::vector& getOutputBuffer() override { return innerOutput.getOutputBuffer(); } + void send (uint8_t g) override { innerOutput.send (g); } + + private: + BufferOutput& innerOutput; + Message::Header header; + uint8_t group{}; + }; + + Output responderOutput { output, parsed->header, message.group }; + + if (status != Parser::Status::noError) + { + switch (status) + { + case Parser::Status::collidingMUID: + { + const Message::Header header { ChannelInGroup::wholeBlock, + MessageMeta::Meta::subID2, + MessageMeta::implementationVersion, + output.getMuid(), + MUID::getBroadcast() }; + const Message::InvalidateMUID body { output.getMuid() }; + MessageTypeUtils::send (responderOutput, responderOutput.getIncomingGroup(), header, body); + break; + } + + case Parser::Status::unrecognisedMessage: + MessageTypeUtils::sendNAK (responderOutput, std::byte { 0x01 }); + break; + + case Parser::Status::reservedVersion: + MessageTypeUtils::sendNAK (responderOutput, std::byte { 0x02 }); + break; + + case Parser::Status::malformed: + MessageTypeUtils::sendNAK (responderOutput, std::byte { 0x41 }); + break; + + case Parser::Status::mismatchedMUID: + case Parser::Status::noError: + break; + } + + return status; + } + + for (auto* listener : listeners) + if (listener != nullptr && listener->tryRespond (responderOutput, *parsed)) + return Parser::Status::noError; + + MessageTypeUtils::BaseCaseDelegate base; + + if (base.tryRespond (responderOutput, *parsed)) + return Parser::Status::noError; + + return Parser::Status::unrecognisedMessage; +} + +//============================================================================== +//============================================================================== +#if JUCE_UNIT_TESTS + +class ResponderTests : public UnitTest +{ +public: + ResponderTests() : UnitTest ("Responder", UnitTestCategories::midi) {} + + void runTest() override + { + auto random = getRandom(); + std::vector outgoing; + + const auto makeOutput = [&] + { + struct Output : public BufferOutput + { + Output (Random& r, std::vector& b) + : muid (MUID::makeRandom (r)), buf (b) {} + + MUID getMuid() const override { return muid; } + std::vector& getOutputBuffer() override { return buf; } + void send (uint8_t) override { sent.push_back (buf); } + + MUID muid; + std::vector& buf; + std::vector> sent; + }; + + return Output { random, outgoing }; + }; + + beginTest ("An endpoint message with a matching MUID provokes an endpoint response"); + { + constexpr auto version = MessageMeta::implementationVersion; + + auto output = makeOutput(); + const auto initialMUID = output.getMuid(); + + const auto bytes = makeByteArray (0x7e, + /* to function block */ 0x7f, + /* midi CI */ 0x0d, + /* endpoint message */ 0x72, + /* version */ version, + /* source MUID */ 0x01, + /* ... */ 0x02, + /* ... */ 0x03, + /* ... */ 0x04, + /* destination MUID */ (initialMUID.get() >> 0x00) & 0x7f, + /* ... */ (initialMUID.get() >> 0x07) & 0x7f, + /* ... */ (initialMUID.get() >> 0x0e) & 0x7f, + /* ... */ (initialMUID.get() >> 0x15) & 0x7f, + /* status, product instance ID */ 0x00); + + const Message::Parsed expectedInput { Message::Header { ChannelInGroup::wholeBlock, + std::byte { 0x72 }, + version, + MUID::makeUnchecked (0x80c101), + initialMUID }, + Message::EndpointInquiry { std::byte { 0x00 } } }; + EndpointResponderListener listener; + processCompleteMessage (output, { 0, bytes }, listener); + expect (listener == SilentResponderListener (expectedInput)); + + const auto expectedOutputBytes = makeByteArray (0x7e, + /* to function block */ 0x7f, + /* midi CI */ 0x0d, + /* endpoint reply */ 0x73, + /* version */ version, + /* source MUID */ (initialMUID.get() >> 0x00) & 0x7f, + /* ... */ (initialMUID.get() >> 0x07) & 0x7f, + /* ... */ (initialMUID.get() >> 0x0e) & 0x7f, + /* ... */ (initialMUID.get() >> 0x15) & 0x7f, + /* destination MUID */ 0x01, + /* ... */ 0x02, + /* ... */ 0x03, + /* ... */ 0x04, + /* status */ 0x00, + /* 16-bit length of following data */ 0x04, + /* ... */ 0x00, + /* info */ 0x00, + /* ... */ 0x00, + /* ... */ 0x00, + /* ... */ 0x00); + + expect (rangesEqual (output.sent.front(), expectedOutputBytes)); + } + + beginTest ("An endpoint message directed at a different MUID does not provoke a response"); + { + const auto destMUID = MUID::makeRandom (random); + constexpr auto version = MessageMeta::implementationVersion; + + const auto bytes = makeByteArray (0x7e, + /* to function block */ 0x7f, + /* midi CI */ 0x0d, + /* endpoint message */ 0x72, + /* version */ version, + /* source MUID */ 0x01, + /* ... */ 0x02, + /* ... */ 0x03, + /* ... */ 0x04, + /* destination MUID */ (destMUID.get() >> 0x00) & 0x7f, + /* ... */ (destMUID.get() >> 0x07) & 0x7f, + /* ... */ (destMUID.get() >> 0x0e) & 0x7f, + /* ... */ (destMUID.get() >> 0x15) & 0x7f, + /* status, product instance ID */ 0x00); + + auto output = makeOutput(); + EndpointResponderListener listener; + processCompleteMessage (output, { 0, bytes }, listener); + expect (listener == SilentResponderListener()); + expect (output.sent.empty()); + } + + beginTest ("If the listener fails to compose an endpoint response, a NAK is emitted"); + { + auto output = makeOutput(); + const auto initialMUID = output.getMuid(); + + SilentResponderListener listener; + constexpr auto version = MessageMeta::implementationVersion; + + const auto bytes = makeByteArray (0x7e, + /* to function block */ 0x7f, + /* midi CI */ 0x0d, + /* endpoint message */ 0x72, + /* version */ version, + /* source MUID */ 0x01, + /* ... */ 0x02, + /* ... */ 0x03, + /* ... */ 0x04, + /* destination MUID */ (initialMUID.get() >> 0x00) & 0x7f, + /* ... */ (initialMUID.get() >> 0x07) & 0x7f, + /* ... */ (initialMUID.get() >> 0x0e) & 0x7f, + /* ... */ (initialMUID.get() >> 0x15) & 0x7f, + /* status, product instance ID */ 0x00); + + processCompleteMessage (output, { 0, bytes }, listener); + + const auto expectedOutputBytes = makeByteArray (0x7e, + /* to function block */ 0x7f, + /* midi CI */ 0x0d, + /* nak */ 0x7f, + /* version */ version, + /* source MUID */ (initialMUID.get() >> 0x00) & 0x7f, + /* ... */ (initialMUID.get() >> 0x07) & 0x7f, + /* ... */ (initialMUID.get() >> 0x0e) & 0x7f, + /* ... */ (initialMUID.get() >> 0x15) & 0x7f, + /* destination MUID */ 0x01, + /* ... */ 0x02, + /* ... */ 0x03, + /* ... */ 0x04, + /* original transaction sub-id #2 */ 0x72, + /* nak status code */ 0x00, + /* nak status data */ 0x00, + /* details */ 0x00, + /* ... */ 0x00, + /* ... */ 0x00, + /* ... */ 0x00, + /* ... */ 0x00, + /* message text length */ 0x00, + /* ... */ 0x00); + expect (rangesEqual (output.sent.front(), expectedOutputBytes)); + } + + beginTest ("If a message is sent with reserved bits set in the Message Format Version, a NAK is emitted"); + { + auto output = makeOutput(); + const auto initialMUID = output.getMuid(); + + const auto bytes = makeByteArray (0x7e, + /* to function block */ 0x7f, + /* midi CI */ 0x0d, + /* endpoint message */ 0x72, + /* version, reserved bit set */ 0x12, + /* source MUID */ 0x01, + /* ... */ 0x02, + /* ... */ 0x03, + /* ... */ 0x04, + /* destination MUID */ (initialMUID.get() >> 0x00) & 0x7f, + /* ... */ (initialMUID.get() >> 0x07) & 0x7f, + /* ... */ (initialMUID.get() >> 0x0e) & 0x7f, + /* ... */ (initialMUID.get() >> 0x15) & 0x7f, + /* status, product instance ID */ 0x00); + SilentResponderListener listener; + processCompleteMessage (output, { 0, bytes }, listener); + + expect (listener == SilentResponderListener{}); + + const auto expectedOutputBytes = makeByteArray (0x7e, + /* to function block */ 0x7f, + /* midi CI */ 0x0d, + /* nak */ 0x7f, + /* version */ MessageMeta::implementationVersion, + /* source MUID */ (initialMUID.get() >> 0x00) & 0x7f, + /* ... */ (initialMUID.get() >> 0x07) & 0x7f, + /* ... */ (initialMUID.get() >> 0x0e) & 0x7f, + /* ... */ (initialMUID.get() >> 0x15) & 0x7f, + /* destination MUID */ 0x01, + /* ... */ 0x02, + /* ... */ 0x03, + /* ... */ 0x04, + /* original transaction sub-id #2 */ 0x72, + /* nak status code */ 0x02, + /* nak status data */ 0x00, + /* details */ 0x00, + /* ... */ 0x00, + /* ... */ 0x00, + /* ... */ 0x00, + /* ... */ 0x00, + /* message text length */ 0x00, + /* ... */ 0x00); + + expect (rangesEqual (output.sent.front(), expectedOutputBytes)); + } + + beginTest ("If the message body is malformed, a NAK with a status of 0x41 is emitted"); + { + const auto sourceMUID = MUID::makeRandom (random); + + Message::Header header; + header.deviceID = ChannelInGroup::wholeBlock; + header.category = std::byte { 0x7e }; + header.version = MessageMeta::implementationVersion; + header.source = sourceMUID; + header.destination = MUID::getBroadcast(); + + Message::InvalidateMUID invalidate; + invalidate.target = MUID::makeRandom (random); + + std::vector message; + Marshalling::Writer { message } (header, invalidate); + + // Remove a byte from the end of the message + message.pop_back(); + + auto output = makeOutput(); + const auto ourMUID = output.getMuid(); + + SilentResponderListener listener; + processCompleteMessage (output, { 0, message }, listener); + + const auto expectedOutputBytes = makeByteArray (0x7e, + /* to function block */ 0x7f, + /* midi CI */ 0x0d, + /* nak */ 0x7f, + /* version */ MessageMeta::implementationVersion, + /* source MUID */ (ourMUID.get() >> 0x00) & 0x7f, + /* ... */ (ourMUID.get() >> 0x07) & 0x7f, + /* ... */ (ourMUID.get() >> 0x0e) & 0x7f, + /* ... */ (ourMUID.get() >> 0x15) & 0x7f, + /* destination MUID */ (sourceMUID.get() >> 0x00) & 0x7f, + /* ... */ (sourceMUID.get() >> 0x07) & 0x7f, + /* ... */ (sourceMUID.get() >> 0x0e) & 0x7f, + /* ... */ (sourceMUID.get() >> 0x15) & 0x7f, + /* original transaction sub-id #2 */ 0x7e, + /* nak status code */ 0x41, + /* nak status data */ 0x00, + /* details */ 0x00, + /* ... */ 0x00, + /* ... */ 0x00, + /* ... */ 0x00, + /* ... */ 0x00, + /* message text length */ 0x00, + /* ... */ 0x00); + expect (rangesEqual (output.sent.front(), expectedOutputBytes)); + } + + beginTest ("If an unrecognised message is received, a NAK with a status of 0x01 is emitted"); + { + const auto sourceMUID = MUID::makeRandom (random); + + Message::Header header; + header.deviceID = ChannelInGroup::wholeBlock; + header.category = std::byte { 0x50 }; // reserved + header.version = MessageMeta::implementationVersion; + header.source = sourceMUID; + header.destination = MUID::getBroadcast(); + + std::vector message; + Marshalling::Writer { message } (header); + message.emplace_back(); + + auto output = makeOutput(); + const auto ourMUID = output.getMuid(); + + SilentResponderListener listener; + processCompleteMessage (output, { 0, message }, listener); + + const auto expectedOutputBytes = makeByteArray (0x7e, + /* to function block */ 0x7f, + /* midi CI */ 0x0d, + /* nak */ 0x7f, + /* version */ MessageMeta::implementationVersion, + /* source MUID */ (ourMUID.get() >> 0x00) & 0x7f, + /* ... */ (ourMUID.get() >> 0x07) & 0x7f, + /* ... */ (ourMUID.get() >> 0x0e) & 0x7f, + /* ... */ (ourMUID.get() >> 0x15) & 0x7f, + /* destination MUID */ (sourceMUID.get() >> 0x00) & 0x7f, + /* ... */ (sourceMUID.get() >> 0x07) & 0x7f, + /* ... */ (sourceMUID.get() >> 0x0e) & 0x7f, + /* ... */ (sourceMUID.get() >> 0x15) & 0x7f, + /* original transaction sub-id #2 */ 0x50, + /* nak status code */ 0x01, + /* nak status data */ 0x00, + /* details */ 0x00, + /* ... */ 0x00, + /* ... */ 0x00, + /* ... */ 0x00, + /* ... */ 0x00, + /* message text length */ 0x00, + /* ... */ 0x00); + expect (rangesEqual (output.sent.front(), expectedOutputBytes)); + } + } + +private: + template + static std::array makeByteArray (Ts&&... ts) + { + jassert (((0 <= (int) ts && (int) ts <= std::numeric_limits::max()) && ...)); + return { std::byte (ts)... }; + } + + struct SilentResponderListener : public ResponderDelegate + { + SilentResponderListener() = default; + explicit SilentResponderListener (const Message::Parsed& p) : parsed (p) {} + + bool tryRespond (ResponderOutput&, const Message::Parsed& p) override + { + parsed = p; + return false; + } + + // Returning false indicates that the message was not handled + bool operator== (const SilentResponderListener& other) const { return parsed == other.parsed; } + bool operator!= (const SilentResponderListener& other) const { return ! operator== (other); } + + std::optional parsed; + }; + + struct EndpointResponderListener : public SilentResponderListener + { + bool tryRespond (ResponderOutput& output, const Message::Parsed& message) override + { + parsed = message; + + if (std::holds_alternative (message.body)) + { + std::array data{}; + Message::EndpointInquiryResponse response; + response.status = std::byte{}; + response.data = data; + + MessageTypeUtils::send (output, output.getIncomingGroup(), output.getReplyHeader (std::byte { 0x73 }), response); + return true; + } + + return SilentResponderListener::tryRespond (output, message); + + } + + using SilentResponderListener::operator==, SilentResponderListener::operator!=; + }; + + struct OutputCallback + { + void operator() (Span bytes) + { + output = std::vector (bytes.begin(), bytes.end()); + } + + std::vector output; + }; + + template + static bool rangesEqual (A&& a, B&& b) + { + using std::begin, std::end; + return std::equal (begin (a), end (a), begin (b), end (b)); + } + + + static Parser::Status processCompleteMessage (BufferOutput& output, + ump::BytesOnGroup message, + ResponderDelegate& listener) + { + ResponderDelegate* const listeners[] { &listener }; + return Responder::processCompleteMessage (output, message, listeners); + } +}; + +static ResponderTests responderTests; + +#endif + +} // namespace juce::midi_ci::detail diff --git a/modules/juce_midi_ci/detail/juce_CIResponder.h b/modules/juce_midi_ci/detail/juce_CIResponder.h new file mode 100644 index 0000000000..3e0c079daa --- /dev/null +++ b/modules/juce_midi_ci/detail/juce_CIResponder.h @@ -0,0 +1,49 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce::midi_ci::detail +{ + +/* + Parses individual messages, and additionally gives ResponderDelegates a chance to formulate + a response to any message that would normally necessitate a reply. +*/ +struct Responder +{ + Responder() = delete; + + /* Parses the message, then calls tryParse on each ResponderDelegate in + turn until one returns true, indicating that the message has been + handled. Most 'inquiry' messages should emit one or more reply messages. + These replies will be written to the provided BufferOutput. + If none of the provided delegates are able to handle the message, then + a generic NAK will be written to the BufferOutput. + */ + static Parser::Status processCompleteMessage (BufferOutput& output, + ump::BytesOnGroup message, + Span delegates); +}; + +} // namespace juce::midi_ci diff --git a/modules/juce_midi_ci/juce_midi_ci.cpp b/modules/juce_midi_ci/juce_midi_ci.cpp new file mode 100644 index 0000000000..db9a407740 --- /dev/null +++ b/modules/juce_midi_ci/juce_midi_ci.cpp @@ -0,0 +1,55 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifdef JUCE_MIDI_CI_H_INCLUDED + /* When you add this cpp file to your project, you mustn't include it in a file where you've + already included any other headers - just put it inside a file on its own, possibly with your config + flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix + header files that the compiler may be using. + */ + #error "Incorrect use of JUCE cpp file" +#endif + +#include "juce_midi_ci.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/modules/juce_midi_ci/juce_midi_ci.h b/modules/juce_midi_ci/juce_midi_ci.h new file mode 100644 index 0000000000..b706480178 --- /dev/null +++ b/modules/juce_midi_ci/juce_midi_ci.h @@ -0,0 +1,85 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + + +/******************************************************************************* + The block below describes the properties of this module, and is read by + the Projucer to automatically generate project code that uses it. + For details about the syntax and how to create or use a module, see the + JUCE Module Format.md file. + + + BEGIN_JUCE_MODULE_DECLARATION + + ID: juce_midi_ci + vendor: juce + version: 7.0.8 + name: JUCE MIDI CI Classes + description: Classes facilitating communication via MIDI Capability Inquiry + website: http://www.juce.com/juce + license: GPL/Commercial + minimumCppStandard: 17 + + dependencies: juce_audio_basics + + END_JUCE_MODULE_DECLARATION + +*******************************************************************************/ + + +#pragma once +#define JUCE_MIDI_CI_H_INCLUDED + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace juce +{ + namespace ci = midi_ci; +}