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