From 140f32134f53224948340b636a683e5556cf9553 Mon Sep 17 00:00:00 2001 From: Nikhil Dabas Date: Mon, 19 Oct 2026 14:10:22 +0100 Subject: [PATCH 1/5] Update JUCE to 7.0.13-security --- CMakeLists.txt | 11 +- JUCE | 2 +- README.md | 2 +- .../juce_PatchedFLACAudioFormat.cpp | 68 +- .../juce_PatchedFLACAudioFormat.h | 8 +- .../juce_PatchedMP3AudioFormat.cpp | 14 +- .../juce_PatchedMP3AudioFormat.h | 8 +- .../juce_PatchedVST3PluginFormat.cpp | 1577 ++++++++++------- .../juce_PatchedVST3PluginFormat.h | 11 +- .../juce_PatchedWavAudioFormat.h | 4 +- .../juce_mac_PatchedWindowing.mm | 420 ++--- .../juce_patched_gui_basics.cpp | 567 ++---- setup.py | 7 +- vendors/libFLAC/metadata_object.c | 13 +- vendors/libFLAC/private/metadata.h | 47 + 15 files changed, 1379 insertions(+), 1380 deletions(-) create mode 100644 vendors/libFLAC/private/metadata.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d3150d5ff..55e2b6789 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -142,6 +142,7 @@ if(APPLE) find_library(FOUNDATION_FRAMEWORK Foundation REQUIRED) find_library(IOKIT_FRAMEWORK IOKit REQUIRED) find_library(QUARTZ_FRAMEWORK QuartzCore REQUIRED) + find_library(SECURITY_FRAMEWORK Security REQUIRED) find_library(WEBKIT_FRAMEWORK WebKit REQUIRED) set(MACOS_FRAMEWORKS @@ -155,6 +156,7 @@ if(APPLE) ${FOUNDATION_FRAMEWORK} ${IOKIT_FRAMEWORK} ${QUARTZ_FRAMEWORK} + ${SECURITY_FRAMEWORK} ${WEBKIT_FRAMEWORK} ) @@ -169,6 +171,9 @@ elseif(WIN32) # Windows specific flags add_compile_definitions(WINDOWS=1 USE_BUILTIN_FFT NO_THREADING) add_compile_definitions(JUCE_DLL_BUILD=1) + # JUCE 7's DLL allocators (injected via JUCE_LEAK_DETECTOR) cause ambiguous + # operator new/delete errors in classes with multiple JUCE base classes: + add_compile_definitions(JUCE_DISABLE_DLL_ALLOCATORS=1) # python_bindings.cpp generates a very large number of COFF sections (from # the pybind11 template instantiations); /bigobj raises the per-object @@ -269,7 +274,6 @@ endif() # Rubber Band library flags list(APPEND RUBBERBAND_COMPILE_DEFINITIONS USE_BQRESAMPLER=1 - _HAS_STD_BYTE=0 NOMINMAX ALREADY_CONFIGURED ) @@ -375,6 +379,11 @@ endif() # Link libraries if(APPLE) target_link_libraries(pedalboard_native PRIVATE ${MACOS_FRAMEWORKS}) + # JUCE 7's juce_gui_basics weakly links against these: + target_link_options(pedalboard_native PRIVATE + "SHELL:-weak_framework Metal" + "SHELL:-weak_framework MetalKit" + ) elseif(WIN32) target_link_libraries(pedalboard_native PRIVATE ${PLATFORM_LIBRARIES}) # Link Python libraries diff --git a/JUCE b/JUCE index ddaa09110..db7028f41 160000 --- a/JUCE +++ b/JUCE @@ -1 +1 @@ -Subproject commit ddaa09110392a4419fecbb6d3022bede89b7e841 +Subproject commit db7028f413b909ec9fd35f73c98b025c361a1c27 diff --git a/README.md b/README.md index 4206a88df..184a6ec25 100644 --- a/README.md +++ b/README.md @@ -310,7 +310,7 @@ To cite via BibTeX: `pedalboard` is licensed under the [GNU General Public License v3](https://www.gnu.org/licenses/gpl-3.0.en.html). `pedalboard` includes a number of libraries that are statically compiled, and which carry the following licenses: - - The core audio processing code is pulled from [JUCE 6](https://juce.com/), which is [dual-licensed under a commercial license and the GPLv3](https://juce.com/juce-6-licence). + - The core audio processing code is pulled from [JUCE 7](https://juce.com/), which is [dual-licensed under a commercial license and the GPLv3](https://juce.com/juce-7-licence). - The [VST3 SDK](https://github.com/steinbergmedia/vst3sdk), bundled with JUCE, is owned by [SteinbergĀ® Media Technologies GmbH](https://www.steinberg.net/en/home.html) and licensed under the GPLv3. - The `PitchShift` plugin and `time_stretch` functions use [the Rubber Band Library](https://github.com/breakfastquay/rubberband), which is [dual-licensed under a commercial license](https://breakfastquay.com/technology/license.html) and the GPLv2 (or newer). [FFTW](https://www.fftw.org/) is also included to speed up Rubber Band, and [is licensed under the GPLv2 (or newer)](https://www.fftw.org/doc/License-and-Copyright.html). - The `MP3Compressor` plugin uses [libmp3lame from the LAME project](https://lame.sourceforge.io/), which is [licensed under the LGPLv2](https://github.com/lameproject/lame/blob/master/README) and [upgraded to the GPLv3 for inclusion in this project (as permitted by the LGPLv2)](https://www.gnu.org/licenses/gpl-faq.html#AllCompatibility). diff --git a/pedalboard/juce_overrides/juce_PatchedFLACAudioFormat.cpp b/pedalboard/juce_overrides/juce_PatchedFLACAudioFormat.cpp index f649feffe..44235adb2 100644 --- a/pedalboard/juce_overrides/juce_PatchedFLACAudioFormat.cpp +++ b/pedalboard/juce_overrides/juce_PatchedFLACAudioFormat.cpp @@ -2,15 +2,15 @@ ============================================================================== This file is part of the JUCE library. - Copyright (c) 2020 - Raw Material Software Limited + 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 6 End-User License - Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + 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-6-licence + 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 @@ -86,8 +86,8 @@ namespace juce { namespace PatchedFlacNamespace { #if JUCE_INCLUDE_FLAC_CODE || !defined(JUCE_INCLUDE_FLAC_CODE) -#undef VERSION -#define VERSION "1.3.1" +#undef PACKAGE_VERSION +#define PACKAGE_VERSION "1.4.3" #define FLAC__NO_DLL 1 @@ -105,13 +105,13 @@ JUCE_BEGIN_IGNORE_WARNINGS_MSVC( #define SIZE_MAX 0xffffffff #endif -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wconversion", "-Wshadow", - "-Wdeprecated-register", "-Wswitch-enum", - "-Wswitch-default", - "-Wimplicit-fallthrough", - "-Wzero-as-null-pointer-constant", - "-Wsign-conversion", "-Wredundant-decls", - "-Wlanguage-extension-token") +JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wconversion", "-Wdeprecated-register", + "-Wfloat-equal", "-Wimplicit-fallthrough", + "-Wlanguage-extension-token", + "-Wredundant-decls", "-Wshadow", + "-Wsign-conversion", "-Wswitch-default", + "-Wswitch-enum", + "-Wzero-as-null-pointer-constant") #if JUCE_INTEL #if JUCE_32BIT @@ -123,11 +123,26 @@ JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wconversion", "-Wshadow", #define FLAC__HAS_X86INTRIN 1 #endif -#undef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS 1 +#if JUCE_ARM && JUCE_64BIT +#define FLAC__CPU_ARM64 1 + +#if JUCE_USE_ARM_NEON +#define FLAC__HAS_NEONINTRIN 1 +#define FLAC__HAS_A64NEONINTRIN 1 +#endif +#endif + #define flac_max jmax #define flac_min jmin + +#pragma push_macro("DEBUG") +#pragma push_macro("NDEBUG") #undef DEBUG // (some flac code dumps debug trace if the app defines this macro) + +#ifndef NDEBUG +#define NDEBUG // (some flac code prints cpu info if this isn't defined) +#endif + #include "../../JUCE/modules/juce_audio_formats/codecs/flac/all.h" #include "../../JUCE/modules/juce_audio_formats/codecs/flac/libFLAC/bitmath.c" #include "../../JUCE/modules/juce_audio_formats/codecs/flac/libFLAC/bitreader.c" @@ -138,6 +153,7 @@ JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wconversion", "-Wshadow", #include "../../JUCE/modules/juce_audio_formats/codecs/flac/libFLAC/float.c" #include "../../JUCE/modules/juce_audio_formats/codecs/flac/libFLAC/format.c" #include "../../JUCE/modules/juce_audio_formats/codecs/flac/libFLAC/lpc_flac.c" +#include "../../JUCE/modules/juce_audio_formats/codecs/flac/libFLAC/lpc_intrin_neon.c" #include "../../JUCE/modules/juce_audio_formats/codecs/flac/libFLAC/md5.c" #include "../../JUCE/modules/juce_audio_formats/codecs/flac/libFLAC/memory.c" #include "../../JUCE/modules/juce_audio_formats/codecs/flac/libFLAC/stream_decoder.c" @@ -145,13 +161,18 @@ JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wconversion", "-Wshadow", #include "../../JUCE/modules/juce_audio_formats/codecs/flac/libFLAC/stream_encoder_framing.c" #include "../../JUCE/modules/juce_audio_formats/codecs/flac/libFLAC/window_flac.c" #include "../../vendors/libFLAC/metadata_object.c" -#undef VERSION -#else -#include -#endif + +#pragma pop_macro("DEBUG") +#pragma pop_macro("NDEBUG") + +#undef PACKAGE_VERSION JUCE_END_IGNORE_WARNINGS_GCC_LIKE JUCE_END_IGNORE_WARNINGS_MSVC + +#else +#include +#endif } // namespace PatchedFlacNamespace #undef max @@ -165,7 +186,7 @@ template auto emptyRange(Item item) { } //============================================================================== -class PatchedFlacReader : public AudioFormatReader { +class PatchedFlacReader final : public AudioFormatReader { public: PatchedFlacReader(InputStream *in) : AudioFormatReader(in, flacFormatName) { lengthInSamples = 0; @@ -210,7 +231,7 @@ class PatchedFlacReader : public AudioFormatReader { false, true); } - bool readSamples(int **destSamples, int numDestChannels, + bool readSamples(int *const *destSamples, int numDestChannels, int startOffsetInDestBuffer, int64 startSampleInFile, int numSamples) override { if (!ok) @@ -261,7 +282,8 @@ class PatchedFlacReader : public AudioFormatReader { if (!remainingSamples.isEmpty()) for (int i = numDestChannels; --i >= 0;) if (destSamples[i] != nullptr) - zeromem(destSamples[i] + startOffsetInDestBuffer, + zeromem(destSamples[i] + startOffsetInDestBuffer + + (remainingSamples.getStart() - startSampleInFile), (size_t)remainingSamples.getLength() * sizeof(int)); return true; @@ -375,7 +397,7 @@ class PatchedFlacReader : public AudioFormatReader { }; //============================================================================== -class PatchedFlacWriter : public AudioFormatWriter { +class PatchedFlacWriter final : public AudioFormatWriter { public: PatchedFlacWriter(OutputStream *out, double rate, uint32 numChans, uint32 bits, int qualityOptionIndex) diff --git a/pedalboard/juce_overrides/juce_PatchedFLACAudioFormat.h b/pedalboard/juce_overrides/juce_PatchedFLACAudioFormat.h index 5d483322c..331e4b169 100644 --- a/pedalboard/juce_overrides/juce_PatchedFLACAudioFormat.h +++ b/pedalboard/juce_overrides/juce_PatchedFLACAudioFormat.h @@ -2,15 +2,15 @@ ============================================================================== This file is part of the JUCE library. - Copyright (c) 2020 - Raw Material Software Limited + 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 6 End-User License - Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + 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-6-licence + 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 diff --git a/pedalboard/juce_overrides/juce_PatchedMP3AudioFormat.cpp b/pedalboard/juce_overrides/juce_PatchedMP3AudioFormat.cpp index b9f2fc466..72cff7a01 100644 --- a/pedalboard/juce_overrides/juce_PatchedMP3AudioFormat.cpp +++ b/pedalboard/juce_overrides/juce_PatchedMP3AudioFormat.cpp @@ -2,15 +2,15 @@ ============================================================================== This file is part of the JUCE library. - Copyright (c) 2020 - Raw Material Software Limited + 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 6 End-User License - Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + 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-6-licence + 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 @@ -3297,7 +3297,7 @@ struct PatchedMP3Stream { static const char *const mp3FormatName = "MP3 file"; //============================================================================== -class PatchedMP3Reader : public AudioFormatReaderWithPosition { +class PatchedMP3Reader final : public AudioFormatReaderWithPosition { public: PatchedMP3Reader(InputStream *const in) : AudioFormatReaderWithPosition(in, mp3FormatName), stream(*in), @@ -3315,7 +3315,7 @@ class PatchedMP3Reader : public AudioFormatReaderWithPosition { } } - bool readSamples(int **destSamples, int numDestChannels, + bool readSamples(int *const *destSamples, int numDestChannels, int startOffsetInDestBuffer, int64 startSampleInFile, int numSamples) override { if (destSamples == nullptr) { @@ -3368,7 +3368,7 @@ class PatchedMP3Reader : public AudioFormatReaderWithPosition { } const int numToCopy = jmin(decodedEnd - decodedStart, numSamples); - float *const *const dst = reinterpret_cast(destSamples); + float *const *const dst = reinterpret_cast(destSamples); memcpy(dst[0] + startOffsetInDestBuffer, decoded0 + decodedStart, (size_t)numToCopy * sizeof(float)); diff --git a/pedalboard/juce_overrides/juce_PatchedMP3AudioFormat.h b/pedalboard/juce_overrides/juce_PatchedMP3AudioFormat.h index c1713e382..c040e4542 100644 --- a/pedalboard/juce_overrides/juce_PatchedMP3AudioFormat.h +++ b/pedalboard/juce_overrides/juce_PatchedMP3AudioFormat.h @@ -4,15 +4,15 @@ ============================================================================== This file is part of the JUCE library. - Copyright (c) 2020 - Raw Material Software Limited + 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 6 End-User License - Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + 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-6-licence + 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 diff --git a/pedalboard/juce_overrides/juce_PatchedVST3PluginFormat.cpp b/pedalboard/juce_overrides/juce_PatchedVST3PluginFormat.cpp index d03b844e1..1c7b94273 100644 --- a/pedalboard/juce_overrides/juce_PatchedVST3PluginFormat.cpp +++ b/pedalboard/juce_overrides/juce_PatchedVST3PluginFormat.cpp @@ -2,15 +2,15 @@ ============================================================================== This file is part of the JUCE library. - Copyright (c) 2020 - Raw Material Software Limited + 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 6 End-User License - Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + 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-6-licence + 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 @@ -23,35 +23,45 @@ ============================================================================== */ +#define JUCE_CORE_INCLUDE_NATIVE_HEADERS 1 +#define JUCE_CORE_INCLUDE_OBJC_HELPERS 1 #define JUCE_GUI_BASICS_INCLUDE_XHEADERS 1 #define JUCE_GUI_BASICS_INCLUDE_SCOPED_THREAD_DPI_AWARENESS_SETTER 1 +#define JUCE_GRAPHICS_INCLUDE_COREGRAPHICS_HELPERS 1 #include "juce_PatchedVST3PluginFormat.h" #include "../../JUCE/modules/juce_audio_processors/juce_audio_processors.h" - -#include "../../JUCE/modules/juce_core/native/juce_BasicNativeHeaders.h" - -#include "../../JUCE/modules/juce_core/juce_core.h" -#include "../../JUCE/modules/juce_gui_basics/juce_gui_basics.h" #include "../../JUCE/modules/juce_gui_extra/juce_gui_extra.h" -#if JUCE_MAC -#include "../../JUCE/modules/juce_core/native/juce_mac_ObjCHelpers.h" -#endif - #if (JUCE_LINUX || JUCE_BSD) +JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wvariadic-macros") #include +JUCE_END_IGNORE_WARNINGS_GCC_LIKE #include #include #undef KeyPress #endif -#include "../../JUCE/modules/juce_audio_processors/format_types/juce_VST3Headers.h" +namespace juce { -#include "../../JUCE/modules/juce_audio_processors/format_types/juce_VST3Common.h" +// The helpers below are copied from juce_audio_processors.cpp, which defines +// them before including the (unpatched) juce_VST3PluginFormat.cpp. +template void callOnMessageThread(Callback &&callback) { + if (MessageManager::getInstance()->existsAndIsLockedByCurrentThread()) { + callback(); + return; + } -namespace juce { + WaitableEvent completionEvent; + + MessageManager::callAsync([&callback, &completionEvent] { + callback(); + completionEvent.signal(); + }); + + completionEvent.wait(); +} #if JUCE_MAC @@ -107,40 +117,58 @@ struct NSViewComponentWithParent : public NSViewComponent, } } - struct FlippedNSView : public ObjCClass { - FlippedNSView() : ObjCClass("JuceFlippedNSView_") { + struct InnerNSView final : public ObjCClass { + InnerNSView() : ObjCClass("JuceInnerNSView_") { addIvar("owner"); - addMethod(@selector(isFlipped), isFlipped); - addMethod(@selector(isOpaque), isOpaque); - addMethod(@selector(didAddSubview:), didAddSubview); + addMethod(@selector(isOpaque), [](id, SEL) { return YES; }); - registerClass(); - } + addMethod(@selector(didAddSubview:), [](id self, SEL, NSView *) { + if (auto *owner = getIvar(self, "owner")) + if (owner->wantsNudge == WantsNudge::yes) + owner->triggerAsyncUpdate(); + }); - static BOOL isFlipped(id, SEL) { return YES; } - static BOOL isOpaque(id, SEL) { return YES; } + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wundeclared-selector") + addMethod(@selector(clipsToBounds), [](id, SEL) { return YES; }); + JUCE_END_IGNORE_WARNINGS_GCC_LIKE - static void nudge(id self) { - if (auto *owner = getIvar(self, "owner")) - if (owner->wantsNudge == WantsNudge::yes) - owner->triggerAsyncUpdate(); + registerClass(); } - - static void viewDidUnhide(id self, SEL) { nudge(self); } - static void didAddSubview(id self, SEL, NSView *) { nudge(self); } - static void viewDidMoveToSuperview(id self, SEL) { nudge(self); } - static void viewDidMoveToWindow(id self, SEL) { nudge(self); } }; - static FlippedNSView &getViewClass() { - static FlippedNSView result; + static InnerNSView &getViewClass() { + static InnerNSView result; return result; } }; #endif +} // namespace juce + +#include "../../JUCE/modules/juce_audio_processors/utilities/juce_FlagCache.h" + +// These must be included in this order (separated by blank lines so that +// clang-format doesn't re-sort them): +#include "../../JUCE/modules/juce_audio_processors/format_types/juce_VST3Headers.h" + +#include "../../JUCE/modules/juce_audio_processors/format_types/juce_VST3Common.h" + +#include "../../JUCE/modules/juce_audio_processors/format_types/juce_ARACommon.h" + +#if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX) +#include + +namespace ARA { +DEF_CLASS_IID(IMainFactory) +DEF_CLASS_IID(IPlugInEntryPoint) +DEF_CLASS_IID(IPlugInEntryPoint2) +} // namespace ARA +#endif + +namespace juce { + // UB Sanitizer doesn't necessarily have instrumentation for loaded plugins, so // it won't recognize the dynamic types of pointers to the plugin's interfaces. JUCE_BEGIN_NO_SANITIZE("vptr") @@ -205,6 +233,15 @@ static int warnOnFailureIfImplemented(int result) noexcept { #define warnOnFailureIfImplemented(x) x #endif +enum class MediaKind { audio, event }; + +static Vst::MediaType toVstType(MediaKind x) { + return x == MediaKind::audio ? Vst::kAudio : Vst::kEvent; +} +static Vst::BusDirection toVstType(Direction x) { + return x == Direction::input ? Vst::kInput : Vst::kOutput; +} + static std::vector getAllParamIDs(Vst::IEditController &controller) { std::vector result; @@ -224,7 +261,7 @@ getAllParamIDs(Vst::IEditController &controller) { /* Allows parameter updates to be queued up without blocking, and automatically dispatches these updates on the main thread. */ -class EditControllerParameterDispatcher : private Timer { +class EditControllerParameterDispatcher final : private Timer { public: ~EditControllerParameterDispatcher() override { stopTimer(); } @@ -258,7 +295,7 @@ class EditControllerParameterDispatcher : private Timer { }; //============================================================================== -std::array getNormalisedTUID(const TUID &tuid) noexcept { +static std::array getNormalisedTUID(const TUID &tuid) noexcept { const FUID fuid{tuid}; return {{fuid.getLong1(), fuid.getLong2(), fuid.getLong3(), fuid.getLong4()}}; } @@ -282,6 +319,69 @@ static void fillDescriptionWith(PluginDescription &description, description.manufacturerName = toString(object.vendor).trim(); } +static std::vector +createPluginDescriptions(const File &pluginFile, + const Steinberg::ModuleInfo &info) { + std::vector result; + + const auto araMainFactoryClassNames = [&] { + std::unordered_set factories; + +#if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX) + for (const auto &c : info.classes) + if (c.category == kARAMainFactoryClass) + factories.insert(CharPointer_UTF8(c.name.c_str())); +#endif + + return factories; + }(); + + for (const auto &c : info.classes) { + if (c.category != kVstAudioEffectClass) + continue; + + PluginDescription description; + + description.fileOrIdentifier = pluginFile.getFullPathName(); + description.lastFileModTime = pluginFile.getLastModificationTime(); + description.lastInfoUpdateTime = Time::getCurrentTime(); + description.manufacturerName = + CharPointer_UTF8(info.factoryInfo.vendor.c_str()); + description.name = CharPointer_UTF8(c.name.c_str()); + description.descriptiveName = CharPointer_UTF8(c.name.c_str()); + description.pluginFormatName = "VST3"; + description.numInputChannels = 0; + description.numOutputChannels = 0; + description.hasARAExtension = + araMainFactoryClassNames.find(description.name) != + araMainFactoryClassNames.end(); + description.version = CharPointer_UTF8(c.version.c_str()); + + const auto uid = VST3::UID::fromString(c.cid); + + if (!uid) + continue; + + description.deprecatedUid = getHashForRange(uid->data()); + description.uniqueId = getHashForRange(getNormalisedTUID(uid->data())); + + StringArray categories; + + for (const auto &category : c.subCategories) + categories.add(CharPointer_UTF8(category.c_str())); + + description.category = categories.joinIntoString("|"); + + description.isInstrument = std::any_of( + c.subCategories.begin(), c.subCategories.end(), + [](const auto &subcategory) { return subcategory == "Instrument"; }); + + result.push_back(description); + } + + return result; +} + static void createPluginDescription(PluginDescription &description, const File &pluginFile, const String &company, const String &name, @@ -314,33 +414,28 @@ static void createPluginDescription(PluginDescription &description, } static int getNumSingleDirectionBusesFor(Vst::IComponent *component, - bool checkInputs, - bool checkAudioChannels) { + MediaKind kind, Direction direction) { jassert(component != nullptr); - - return (int)component->getBusCount(checkAudioChannels ? Vst::kAudio - : Vst::kEvent, - checkInputs ? Vst::kInput : Vst::kOutput); + JUCE_ASSERT_MESSAGE_THREAD + return (int)component->getBusCount(toVstType(kind), toVstType(direction)); } /** Gives the total number of channels for a particular type of bus direction * and media type */ static int getNumSingleDirectionChannelsFor(Vst::IComponent *component, - bool checkInputs, - bool checkAudioChannels) { + Direction busDirection) { jassert(component != nullptr); + JUCE_ASSERT_MESSAGE_THREAD - const Vst::BusDirections direction = checkInputs ? Vst::kInput : Vst::kOutput; - const Vst::MediaTypes mediaType = - checkAudioChannels ? Vst::kAudio : Vst::kEvent; + const auto direction = toVstType(busDirection); const Steinberg::int32 numBuses = - component->getBusCount(mediaType, direction); + component->getBusCount(Vst::kAudio, direction); int numChannels = 0; for (Steinberg::int32 i = numBuses; --i >= 0;) { Vst::BusInfo busInfo; - warnOnFailure(component->getBusInfo(mediaType, direction, i, busInfo)); + warnOnFailure(component->getBusInfo(Vst::kAudio, direction, i, busInfo)); numChannels += ((busInfo.flags & Vst::BusInfo::kDefaultActive) != 0 ? (int)busInfo.channelCount : 0); @@ -349,20 +444,17 @@ static int getNumSingleDirectionChannelsFor(Vst::IComponent *component, return numChannels; } -static void setStateForAllBusesOfType(Vst::IComponent *component, bool state, - bool activateInputs, - bool activateAudioChannels) { +static void setStateForAllEventBuses(Vst::IComponent *component, bool state, + Direction busDirection) { jassert(component != nullptr); + JUCE_ASSERT_MESSAGE_THREAD - const Vst::BusDirections direction = - activateInputs ? Vst::kInput : Vst::kOutput; - const Vst::MediaTypes mediaType = - activateAudioChannels ? Vst::kAudio : Vst::kEvent; + const auto direction = toVstType(busDirection); const Steinberg::int32 numBuses = - component->getBusCount(mediaType, direction); + component->getBusCount(Vst::kEvent, direction); for (Steinberg::int32 i = numBuses; --i >= 0;) - warnOnFailure(component->activateBus(mediaType, direction, i, state)); + warnOnFailure(component->activateBus(Vst::kEvent, direction, i, state)); } //============================================================================== @@ -374,66 +466,74 @@ static void toProcessContext(Vst::ProcessContext &context, zerostruct(context); context.sampleRate = sampleRate; - auto &fr = context.frameRate; - - if (playHead != nullptr) { - AudioPlayHead::CurrentPositionInfo position; - playHead->getCurrentPosition(position); - - context.projectTimeSamples = - position - .timeInSamples; // Must always be valid, as stated by the VST3 SDK - context.projectTimeMusic = - position.ppqPosition; // Does not always need to be valid... - context.tempo = position.bpm; - context.timeSigNumerator = position.timeSigNumerator; - context.timeSigDenominator = position.timeSigDenominator; - context.barPositionMusic = position.ppqPositionOfLastBarStart; - context.cycleStartMusic = position.ppqLoopStart; - context.cycleEndMusic = position.ppqLoopEnd; - - context.frameRate.framesPerSecond = - (Steinberg::uint32)position.frameRate.getBaseRate(); - context.frameRate.flags = (Steinberg::uint32)( - (position.frameRate.isDrop() ? FrameRate::kDropRate : 0) | - (position.frameRate.isPullDown() ? FrameRate::kPullDownRate : 0)); - - if (position.isPlaying) + + const auto position = playHead != nullptr ? playHead->getPosition() : nullopt; + + if (position.hasValue()) { + if (const auto timeInSamples = position->getTimeInSamples()) + context.projectTimeSamples = *timeInSamples; + else + jassertfalse; // The time in samples *must* be valid. + + if (const auto tempo = position->getBpm()) { + context.state |= ProcessContext::kTempoValid; + context.tempo = *tempo; + } + + if (const auto loop = position->getLoopPoints()) { + context.state |= ProcessContext::kCycleValid; + context.cycleStartMusic = loop->ppqStart; + context.cycleEndMusic = loop->ppqEnd; + } + + if (const auto sig = position->getTimeSignature()) { + context.state |= ProcessContext::kTimeSigValid; + context.timeSigNumerator = sig->numerator; + context.timeSigDenominator = sig->denominator; + } + + if (const auto pos = position->getPpqPosition()) { + context.state |= ProcessContext::kProjectTimeMusicValid; + context.projectTimeMusic = *pos; + } + + if (const auto barStart = position->getPpqPositionOfLastBarStart()) { + context.state |= ProcessContext::kBarPositionValid; + context.barPositionMusic = *barStart; + } + + if (const auto frameRate = position->getFrameRate()) { + if (const auto offset = position->getEditOriginTime()) { + context.state |= ProcessContext::kSmpteValid; + context.smpteOffsetSubframes = + (Steinberg::int32)(80.0 * *offset * frameRate->getEffectiveRate()); + context.frameRate.framesPerSecond = + (Steinberg::uint32)frameRate->getBaseRate(); + context.frameRate.flags = (Steinberg::uint32)( + (frameRate->isDrop() ? FrameRate::kDropRate : 0) | + (frameRate->isPullDown() ? FrameRate::kPullDownRate : 0)); + } + } + + if (const auto hostTime = position->getHostTimeNs()) { + context.state |= ProcessContext::kSystemTimeValid; + context.systemTime = (int64_t)*hostTime; + jassert(context.systemTime >= 0); + } + + if (position->getIsPlaying()) context.state |= ProcessContext::kPlaying; - if (position.isRecording) + if (position->getIsRecording()) context.state |= ProcessContext::kRecording; - if (position.isLooping) + if (position->getIsLooping()) context.state |= ProcessContext::kCycleActive; - } else { - context.tempo = 120.0; - context.timeSigNumerator = 4; - context.timeSigDenominator = 4; - fr.framesPerSecond = 30; - fr.flags = 0; - } - - if (context.projectTimeMusic >= 0.0) - context.state |= ProcessContext::kProjectTimeMusicValid; - if (context.barPositionMusic >= 0.0) - context.state |= ProcessContext::kBarPositionValid; - if (context.tempo > 0.0) - context.state |= ProcessContext::kTempoValid; - if (context.frameRate.framesPerSecond > 0) - context.state |= ProcessContext::kSmpteValid; - - if (context.cycleStartMusic >= 0.0 && context.cycleEndMusic > 0.0 && - context.cycleEndMusic > context.cycleStartMusic) { - context.state |= ProcessContext::kCycleValid; - } - - if (context.timeSigNumerator > 0 && context.timeSigDenominator > 0) - context.state |= ProcessContext::kTimeSigValid; + } } //============================================================================== class PatchedVST3PluginInstance; -struct PatchedVST3HostContext +struct PatchedVST3HostContext final : public Vst::IComponentHandler, // From VST V3.0.0 public Vst::IComponentHandler2, // From VST V3.1.0 (a very well named // class, of course!) @@ -469,9 +569,9 @@ struct PatchedVST3HostContext tresult PLUGIN_API setDirty(TBool) override; //============================================================================== - tresult PLUGIN_API requestOpenEditor(FIDString name) override { - ignoreUnused(name); - jassertfalse; + tresult PLUGIN_API + requestOpenEditor([[maybe_unused]] FIDString name) override { + // This request cannot currently be surfaced in the JUCE public API return kResultFalse; } @@ -491,7 +591,7 @@ struct PatchedVST3HostContext } //============================================================================== - struct ContextMenu : public Vst::IContextMenu { + struct ContextMenu final : public Vst::IContextMenu { ContextMenu(PatchedVST3PluginInstance &pluginInstance) : owner(pluginInstance) {} virtual ~ContextMenu() {} @@ -509,7 +609,7 @@ struct PatchedVST3HostContext ItemAndTarget newItem; newItem.item = item; - newItem.target = target; + newItem.target = addVSTComSmartPtrOwner(target); items.add(newItem); return kResultOk; @@ -520,7 +620,7 @@ struct PatchedVST3HostContext for (int i = items.size(); --i >= 0;) { auto &item = items.getReference(i); - if (item.item.tag == toRemove.tag && item.target == target) + if (item.item.tag == toRemove.tag && item.target.get() == target) items.remove(i); } @@ -536,7 +636,7 @@ struct PatchedVST3HostContext result = item.item; if (target != nullptr) - *target = item.target; + *target = item.target.get(); return kResultTrue; } @@ -592,10 +692,12 @@ struct PatchedVST3HostContext Vst::IContextMenu *PLUGIN_API createContextMenu(IPlugView *, const Vst::ParamID *) override { - if (plugin != nullptr) - return new ContextMenu(*plugin); + if (plugin == nullptr) + return nullptr; - return nullptr; + auto *result = new ContextMenu(*plugin); + result->addRef(); + return result; } tresult PLUGIN_API executeMenuItem(Steinberg::int32) override { @@ -689,7 +791,7 @@ struct PatchedVST3HostContext ~Attribute() noexcept { reset(); } tresult getInt(Steinberg::int64 &result) const { - if (kind != Kind::Int) + if (kind != Kind::tagInt) return kResultFalse; result = storage.storedInt; @@ -697,7 +799,7 @@ struct PatchedVST3HostContext } tresult getFloat(double &result) const { - if (kind != Kind::Float) + if (kind != Kind::tagFloat) return kResultFalse; result = storage.storedFloat; @@ -705,7 +807,7 @@ struct PatchedVST3HostContext } tresult getString(Vst::TChar *data, Steinberg::uint32 numBytes) const { - if (kind != Kind::String) + if (kind != Kind::tagString) return kResultFalse; std::memcpy(data, storage.storedString.data(), @@ -715,7 +817,7 @@ struct PatchedVST3HostContext } tresult getBinary(const void *&data, Steinberg::uint32 &numBytes) const { - if (kind != Kind::Binary) + if (kind != Kind::tagBinary) return kResultFalse; data = storage.storedBinary.data(); @@ -725,32 +827,32 @@ struct PatchedVST3HostContext private: void constructFrom(Int x) noexcept { - kind = Kind::Int; + kind = Kind::tagInt; new (&storage.storedInt) Int(std::move(x)); } void constructFrom(Float x) noexcept { - kind = Kind::Float; + kind = Kind::tagFloat; new (&storage.storedFloat) Float(std::move(x)); } void constructFrom(String x) noexcept { - kind = Kind::String; + kind = Kind::tagString; new (&storage.storedString) String(std::move(x)); } void constructFrom(Binary x) noexcept { - kind = Kind::Binary; + kind = Kind::tagBinary; new (&storage.storedBinary) Binary(std::move(x)); } void reset() noexcept { switch (kind) { - case Kind::Int: + case Kind::tagInt: break; - case Kind::Float: + case Kind::tagFloat: break; - case Kind::String: + case Kind::tagString: storage.storedString.~vector(); break; - case Kind::Binary: + case Kind::tagBinary: storage.storedBinary.~vector(); break; } @@ -758,22 +860,22 @@ struct PatchedVST3HostContext void moveFrom(Attribute &&other) noexcept { switch (other.kind) { - case Kind::Int: + case Kind::tagInt: constructFrom(std::move(other.storage.storedInt)); break; - case Kind::Float: + case Kind::tagFloat: constructFrom(std::move(other.storage.storedFloat)); break; - case Kind::String: + case Kind::tagString: constructFrom(std::move(other.storage.storedString)); break; - case Kind::Binary: + case Kind::tagBinary: constructFrom(std::move(other.storage.storedBinary)); break; } } - enum class Kind { Int, Float, String, Binary }; + enum class Kind { tagInt, tagFloat, tagString, tagBinary }; union Storage { Storage() {} @@ -792,7 +894,7 @@ struct PatchedVST3HostContext }; //============================================================================== - class AttributeList : public Vst::IAttributeList { + class AttributeList final : public Vst::IAttributeList { public: AttributeList() = default; virtual ~AttributeList() = default; @@ -848,9 +950,9 @@ struct PatchedVST3HostContext const auto iter = attributes.find(attr); if (iter != attributes.end()) - iter->second = Attribute(std::move(value)); + iter->second = Attribute(std::forward(value)); else - attributes.emplace(attr, Attribute(std::move(value))); + attributes.emplace(attr, Attribute(std::forward(value))); return kResultTrue; } @@ -873,7 +975,7 @@ struct PatchedVST3HostContext JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AttributeList) }; - struct Message : public Vst::IMessage { + struct Message final : public Vst::IMessage { Message() = default; virtual ~Message() = default; @@ -904,28 +1006,69 @@ struct PatchedVST3HostContext }; //============================================================================== -struct DescriptionFactory { - DescriptionFactory(PatchedVST3HostContext *host, - IPluginFactory *pluginFactory) - : Patchedvst3HostContext(host), factory(pluginFactory) { - jassert(pluginFactory != nullptr); +struct DescriptionLister { + static std::vector tryLoadFast(const File &file, + const File &moduleinfo) { + if (!moduleinfo.existsAsFile()) + return {}; + + MemoryBlock mb; + + if (!moduleinfo.loadFileAsData(mb)) + return {}; + + const std::string_view blockAsStringView( + static_cast(mb.getData()), mb.getSize()); + const auto parsed = + Steinberg::ModuleInfoLib::parseJson(blockAsStringView, nullptr); + + if (!parsed) + return {}; + + return createPluginDescriptions(file, *parsed); + } + + static std::vector findDescriptionsFast(const File &file) { + const auto moduleinfoNewLocation = file.getChildFile("Contents") + .getChildFile("Resources") + .getChildFile("moduleinfo.json"); + + if (const auto loaded = tryLoadFast(file, moduleinfoNewLocation); + !loaded.empty()) + return loaded; + + return tryLoadFast( + file, file.getChildFile("Contents").getChildFile("moduleinfo.json")); } - virtual ~DescriptionFactory() {} + static std::vector + findDescriptionsSlow(PatchedVST3HostContext &host, IPluginFactory &factory, + const File &file) { + std::vector result; - Result findDescriptionsAndPerform(const File &file) { StringArray foundNames; PFactoryInfo factoryInfo; - factory->getFactoryInfo(&factoryInfo); + factory.getFactoryInfo(&factoryInfo); auto companyName = toString(factoryInfo.vendor).trim(); - Result result(Result::ok()); + auto numClasses = factory.countClasses(); + + // Every ARA::IMainFactory must have a matching Steinberg::IComponent. + // The match is determined by the two classes having the same name. + std::unordered_set araMainFactoryClassNames; - auto numClasses = factory->countClasses(); +#if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX) + for (Steinberg::int32 i = 0; i < numClasses; ++i) { + PClassInfo info; + factory.getClassInfo(i, &info); + if (std::strcmp(info.category, kARAMainFactoryClass) == 0) + araMainFactoryClassNames.insert(info.name); + } +#endif for (Steinberg::int32 i = 0; i < numClasses; ++i) { PClassInfo info; - factory->getClassInfo(i, &info); + factory.getClassInfo(i, &info); if (std::strcmp(info.category, kVstAudioEffectClass) != 0) continue; @@ -942,12 +1085,12 @@ struct DescriptionFactory { VSTComSmartPtr pf2; VSTComSmartPtr pf3; - if (pf2.loadFrom(factory)) { + if (pf2.loadFrom(&factory)) { info2.reset(new PClassInfo2()); pf2->getClassInfo2(i, info2.get()); } - if (pf3.loadFrom(factory)) { + if (pf3.loadFrom(&factory)) { infoW.reset(new PClassInfoW()); pf3->getClassInfoUnicode(i, infoW.get()); } @@ -960,13 +1103,12 @@ struct DescriptionFactory { { VSTComSmartPtr component; - if (component.loadFrom(factory, info.cid)) { - if (component->initialize(Patchedvst3HostContext->getFUnknown()) == - kResultOk) { - auto numInputs = - getNumSingleDirectionChannelsFor(component, true, true); - auto numOutputs = - getNumSingleDirectionChannelsFor(component, false, true); + if (component.loadFrom(&factory, info.cid)) { + if (component->initialize(host.getFUnknown()) == kResultOk) { + auto numInputs = getNumSingleDirectionChannelsFor(component.get(), + Direction::input); + auto numOutputs = getNumSingleDirectionChannelsFor( + component.get(), Direction::output); createPluginDescription(desc, file, companyName, name, info, info2.get(), infoW.get(), numInputs, @@ -981,38 +1123,15 @@ struct DescriptionFactory { } } - if (desc.uniqueId != 0) - result = performOnDescription(desc); + if (araMainFactoryClassNames.find(name) != araMainFactoryClassNames.end()) + desc.hasARAExtension = true; - if (result.failed()) - break; + if (desc.uniqueId != 0) + result.push_back(desc); } return result; } - - virtual Result performOnDescription(PluginDescription &) = 0; - -private: - VSTComSmartPtr Patchedvst3HostContext; - VSTComSmartPtr factory; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DescriptionFactory) -}; - -struct DescriptionLister : public DescriptionFactory { - DescriptionLister(PatchedVST3HostContext *host, IPluginFactory *pluginFactory) - : DescriptionFactory(host, pluginFactory) {} - - Result performOnDescription(PluginDescription &desc) { - list.add(new PluginDescription(desc)); - return Result::ok(); - } - - OwnedArray list; - -private: - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DescriptionLister) }; //============================================================================== @@ -1154,7 +1273,7 @@ struct DLLHandle { JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DLLHandle) }; -struct DLLHandleCache : public DeletedAtShutdown { +struct DLLHandleCache final : public DeletedAtShutdown { DLLHandleCache() = default; ~DLLHandleCache() override { clearSingletonInstance(); } @@ -1299,7 +1418,7 @@ class RunLoop final : public Steinberg::Linux::IRunLoop { private: //============================================================================== - struct TimerCaller : private Timer { + struct TimerCaller final : private Timer { TimerCaller(Linux::ITimerHandler *h, int interval) : handler(h) { startTimer(interval); } @@ -1326,7 +1445,7 @@ class RunLoop final : public Steinberg::Linux::IRunLoop { #endif //============================================================================== -struct VST3ModuleHandle : public ReferenceCountedObject { +struct VST3ModuleHandle final : public ReferenceCountedObject { explicit VST3ModuleHandle(const File &pluginFile, const PluginDescription &pluginDesc) : file(pluginFile) { @@ -1380,10 +1499,10 @@ struct VST3ModuleHandle : public ReferenceCountedObject { //============================================================================== bool open(const PluginDescription &description) { - VSTComSmartPtr pluginFactory( - DLLHandleCache::getInstance() - ->findOrCreateHandle(file.getFullPathName()) - .getPluginFactory()); + auto pluginFactory = + addVSTComSmartPtrOwner(DLLHandleCache::getInstance() + ->findOrCreateHandle(file.getFullPathName()) + .getPluginFactory()); if (pluginFactory != nullptr) { auto numClasses = pluginFactory->countClasses(); @@ -1395,10 +1514,12 @@ struct VST3ModuleHandle : public ReferenceCountedObject { if (std::strcmp(info.category, kVstAudioEffectClass) != 0) continue; + const auto uniqueId = getHashForRange(getNormalisedTUID(info.cid)); + const auto deprecatedUid = getHashForRange(info.cid); + if (toString(info.name).trim() == description.name && - (getHashForRange(getNormalisedTUID(info.cid)) == - description.uniqueId || - getHashForRange(info.cid) == description.deprecatedUid)) { + (uniqueId == description.uniqueId || + deprecatedUid == description.deprecatedUid)) { name = description.name; return true; } @@ -1416,14 +1537,73 @@ struct VST3ModuleHandle : public ReferenceCountedObject { JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(VST3ModuleHandle) }; +template +static int compareWithString(Type (&charArray)[N], const String &str) { + return std::strncmp( + str.toRawUTF8(), charArray, + std::min(str.getNumBytesAsUTF8(), (size_t)numElementsInArray(charArray))); +} + +template +static void forEachARAFactory([[maybe_unused]] IPluginFactory *pluginFactory, + [[maybe_unused]] Callback &&cb) { +#if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX) + const auto numClasses = pluginFactory->countClasses(); + for (Steinberg::int32 i = 0; i < numClasses; ++i) { + PClassInfo info; + pluginFactory->getClassInfo(i, &info); + + if (std::strcmp(info.category, kARAMainFactoryClass) == 0) { + const bool keepGoing = cb(info); + if (!keepGoing) + break; + } + } +#endif +} + +static std::shared_ptr +getARAFactory([[maybe_unused]] Steinberg::IPluginFactory *pluginFactory, + [[maybe_unused]] const String &pluginName) { + std::shared_ptr factory; + +#if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX) + forEachARAFactory(pluginFactory, [&pluginFactory, &pluginName, + &factory](const auto &pcClassInfo) { + if (compareWithString(pcClassInfo.name, pluginName) == 0) { + ARA::IMainFactory *source; + if (pluginFactory->createInstance(pcClassInfo.cid, ARA::IMainFactory::iid, + (void **)&source) == + Steinberg::kResultOk) { + factory = getOrCreateARAFactory(source->getFactory(), + [source]() { source->release(); }); + return false; + } + jassert(source == nullptr); + } + + return true; + }); +#endif + + return factory; +} + +static std::shared_ptr +getARAFactory(VST3ModuleHandle &module) { + auto *pluginFactory = module.getPluginFactory(); + return getARAFactory(pluginFactory, module.getName()); +} + //============================================================================== -struct VST3PluginWindow : public AudioProcessorEditor, - private ComponentMovementWatcher, - private ComponentPeer::ScaleFactorListener, - private IPlugFrame { - VST3PluginWindow(AudioPluginInstance *owner, IPlugView *pluginView) +struct VST3PluginWindow final : public AudioProcessorEditor, + private ComponentMovementWatcher, + private ComponentBoundsConstrainer, + private IPlugFrame { + VST3PluginWindow(AudioPluginInstance *owner, + VSTComSmartPtr pluginView) : AudioProcessorEditor(owner), ComponentMovementWatcher(this), - view(pluginView, false) + view(pluginView) #if JUCE_MAC , embeddedComponent(*owner) @@ -1432,30 +1612,29 @@ struct VST3PluginWindow : public AudioProcessorEditor, setSize(10, 10); setOpaque(true); setVisible(true); + setConstrainer(this); warnOnFailure(view->setFrame(this)); view->queryInterface(Steinberg::IPlugViewContentScaleSupport::iid, (void **)&scaleInterface); - if (scaleInterface != nullptr) - warnOnFailure(scaleInterface->setContentScaleFactor( - (Steinberg::IPlugViewContentScaleSupport::ScaleFactor) - nativeScaleFactor)); - + setContentScaleFactor(); resizeToFit(); + + setResizable(view->canResize() == kResultTrue, false); } ~VST3PluginWindow() override { if (scaleInterface != nullptr) scaleInterface->release(); - removeScaleFactorListener(); - #if JUCE_LINUX || JUCE_BSD embeddedComponent.removeClient(); #endif - warnOnFailure(view->removed()); + if (attachedCalled) + warnOnFailure(view->removed()); + warnOnFailure(view->setFrame(nullptr)); processor.editorBeingDeleted(this); @@ -1503,15 +1682,30 @@ struct VST3PluginWindow : public AudioProcessorEditor, bool keyPressed(const KeyPress & /*key*/) override { return true; } private: + void checkBounds(Rectangle &bounds, const Rectangle &, + const Rectangle &, bool, bool, bool, bool) override { + auto rect = componentToVST3Rect(bounds); + view->checkSizeConstraint(&rect); + bounds = vst3ToComponentRect(rect); + } + //============================================================================== - void componentPeerChanged() override { - removeScaleFactorListener(); - currentPeer = getTopLevelComponent()->getPeer(); + void componentPeerChanged() override {} - if (currentPeer != nullptr) { - currentPeer->addScaleFactorListener(this); - nativeScaleFactor = (float)currentPeer->getPlatformScaleFactor(); - } + /* Convert from the component's coordinate system to the hosted VST3's + * coordinate system. */ + ViewRect componentToVST3Rect(Rectangle r) const { + const auto physical = + localAreaToGlobal(r) * nativeScaleFactor * getDesktopScaleFactor(); + return {0, 0, physical.getWidth(), physical.getHeight()}; + } + + /* Convert from the hosted VST3's coordinate system to the component's + * coordinate system. */ + Rectangle vst3ToComponentRect(const ViewRect &vr) const { + return getLocalArea(nullptr, + Rectangle{vr.right, vr.bottom} / + (nativeScaleFactor * getDesktopScaleFactor())); } void componentMovedOrResized(bool, bool wasResized) override { @@ -1519,45 +1713,33 @@ struct VST3PluginWindow : public AudioProcessorEditor, getTopLevelComponent()->getPeer() == nullptr) return; - ViewRect rect; - if (view->canResize() == kResultTrue) { - rect.right = - (Steinberg::int32)roundToInt((float)getWidth() * nativeScaleFactor); - rect.bottom = - (Steinberg::int32)roundToInt((float)getHeight() * nativeScaleFactor); - + auto rect = componentToVST3Rect(getLocalBounds()); view->checkSizeConstraint(&rect); { const ScopedValueSetter recursiveResizeSetter(recursiveResize, true); - setSize(roundToInt((float)rect.getWidth() / nativeScaleFactor), - roundToInt((float)rect.getHeight() / nativeScaleFactor)); + const auto logicalSize = vst3ToComponentRect(rect); + setSize(logicalSize.getWidth(), logicalSize.getHeight()); } -#if JUCE_WINDOWS - setPluginWindowPos(rect); -#else embeddedComponent.setBounds(getLocalBounds()); -#endif view->onSize(&rect); } else { + ViewRect rect; warnOnFailure(view->getSize(&rect)); -#if JUCE_WINDOWS - setPluginWindowPos(rect); -#else - resizeWithRect(embeddedComponent, rect, nativeScaleFactor); -#endif + resizeWithRect(embeddedComponent, rect); } // Some plugins don't update their cursor correctly when mousing out the // window Desktop::getInstance().getMainMouseSource().forceMouseCursorUpdate(); } + using ComponentMovementWatcher::componentMovedOrResized; void componentVisibilityChanged() override { @@ -1565,52 +1747,45 @@ struct VST3PluginWindow : public AudioProcessorEditor, resizeToFit(); componentMovedOrResized(true, true); } - using ComponentMovementWatcher::componentVisibilityChanged; - void nativeScaleFactorChanged(double newScaleFactor) override { - nativeScaleFactor = (float)newScaleFactor; - updatePluginScale(); - componentMovedOrResized(false, true); - } + using ComponentMovementWatcher::componentVisibilityChanged; void resizeToFit() { ViewRect rect; warnOnFailure(view->getSize(&rect)); - resizeWithRect(*this, rect, nativeScaleFactor); + resizeWithRect(*this, rect); } tresult PLUGIN_API resizeView(IPlugView *incomingView, ViewRect *newSize) override { const ScopedValueSetter recursiveResizeSetter(recursiveResize, true); - if (incomingView != nullptr && newSize != nullptr && incomingView == view) { - auto scaleToViewRect = [this](int dimension) { - return (Steinberg::int32)roundToInt((float)dimension * - nativeScaleFactor); - }; - - auto oldWidth = scaleToViewRect(getWidth()); - auto oldHeight = scaleToViewRect(getHeight()); - - resizeWithRect(embeddedComponent, *newSize, nativeScaleFactor); + if (incomingView != nullptr && newSize != nullptr && + incomingView == view.get()) { + const auto oldPhysicalSize = componentToVST3Rect(getLocalBounds()); + const auto logicalSize = vst3ToComponentRect(*newSize); + setSize(logicalSize.getWidth(), logicalSize.getHeight()); + embeddedComponent.setSize(logicalSize.getWidth(), + logicalSize.getHeight()); #if JUCE_WINDOWS - setPluginWindowPos(*newSize); + embeddedComponent.updateHWNDBounds(); +#elif JUCE_LINUX || JUCE_BSD + embeddedComponent.updateEmbeddedBounds(); #endif - setSize(embeddedComponent.getWidth(), embeddedComponent.getHeight()); - // According to the VST3 Workflow Diagrams, a resizeView from the plugin // should always trigger a response from the host which confirms the new // size. - ViewRect rect{0, 0, scaleToViewRect(getWidth()), - scaleToViewRect(getHeight())}; + auto currentPhysicalSize = componentToVST3Rect(getLocalBounds()); - if (rect.right != oldWidth || rect.bottom != oldHeight || !isInOnSize) { + if (currentPhysicalSize.getWidth() != oldPhysicalSize.getWidth() || + currentPhysicalSize.getHeight() != oldPhysicalSize.getHeight() || + !isInOnSize) { // Guard against plug-ins immediately calling resizeView() with the same // size const ScopedValueSetter inOnSizeSetter(isInOnSize, true); - view->onSize(&rect); + view->onSize(¤tPhysicalSize); } return kResultTrue; @@ -1621,29 +1796,25 @@ struct VST3PluginWindow : public AudioProcessorEditor, } //============================================================================== - static void resizeWithRect(Component &comp, const ViewRect &rect, - float scaleFactor) { - comp.setSize( - jmax(10, std::abs(roundToInt((float)rect.getWidth() / scaleFactor))), - jmax(10, std::abs(roundToInt((float)rect.getHeight() / scaleFactor)))); + void resizeWithRect(Component &comp, const ViewRect &rect) const { + const auto logicalSize = vst3ToComponentRect(rect); + comp.setSize(jmax(10, logicalSize.getWidth()), + jmax(10, logicalSize.getHeight())); } void attachPluginWindow() { if (pluginHandle == HandleFormat{}) { #if JUCE_WINDOWS - if (auto *topComp = getTopLevelComponent()) { - peer.reset( - embeddedComponent.createNewPeer(0, topComp->getWindowHandle())); - pluginHandle = (HandleFormat)peer->getNativeHandle(); - } -#else + pluginHandle = static_cast(embeddedComponent.getHWND()); +#endif + embeddedComponent.setBounds(getLocalBounds()); addAndMakeVisible(embeddedComponent); + #if JUCE_MAC pluginHandle = (HandleFormat)embeddedComponent.getView(); #elif JUCE_LINUX || JUCE_BSD pluginHandle = (HandleFormat)embeddedComponent.getHostWindowID(); -#endif #endif if (pluginHandle == HandleFormat{}) { @@ -1651,60 +1822,75 @@ struct VST3PluginWindow : public AudioProcessorEditor, return; } - warnOnFailure( - view->attached((void *)pluginHandle, defaultVST3WindowType)); - updatePluginScale(); - } - } + [[maybe_unused]] const auto attachedResult = + view->attached((void *)pluginHandle, defaultVST3WindowType); + [[maybe_unused]] const auto warning = warnOnFailure(attachedResult); - void removeScaleFactorListener() { - if (currentPeer == nullptr) - return; + if (attachedResult == kResultOk) + attachedCalled = true; - for (int i = 0; i < ComponentPeer::getNumPeers(); ++i) - if (ComponentPeer::getPeer(i) == currentPeer) - currentPeer->removeScaleFactorListener(this); + updatePluginScale(); + } } void updatePluginScale() { if (scaleInterface != nullptr) - warnOnFailure(scaleInterface->setContentScaleFactor( - (Steinberg::IPlugViewContentScaleSupport::ScaleFactor) - nativeScaleFactor)); + setContentScaleFactor(); else resizeToFit(); } + void setContentScaleFactor() { + if (scaleInterface != nullptr) { + [[maybe_unused]] const auto result = + scaleInterface->setContentScaleFactor( + (Steinberg::IPlugViewContentScaleSupport::ScaleFactor) + getEffectiveScale()); + +#if !JUCE_MAC + [[maybe_unused]] const auto warning = warnOnFailure(result); +#endif + } + } + + void setScaleFactor(float s) override { + userScaleFactor = s; + setContentScaleFactor(); + resizeToFit(); + } + + float getEffectiveScale() const { + return nativeScaleFactor * userScaleFactor; + } + //============================================================================== Atomic refCount{1}; VSTComSmartPtr view; #if JUCE_WINDOWS - struct ChildComponent : public Component { - ChildComponent() { setOpaque(true); } - void paint(Graphics &g) override { g.fillAll(Colours::cornflowerblue); } - using Component::createNewPeer; + using HandleFormat = HWND; - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ChildComponent) - }; + struct ViewComponent final : public HWNDComponent { + ViewComponent() { + setOpaque(true); + inner.addToDesktop(0); - void setPluginWindowPos(ViewRect rect) { - if (auto *topComp = getTopLevelComponent()) { - auto pos = - (topComp->getLocalPoint(this, Point()) * nativeScaleFactor) - .roundToInt(); + if (auto *peer = inner.getPeer()) + setHWND(peer->getNativeHandle()); + } - ScopedThreadDPIAwarenessSetter threadDpiAwarenessSetter{pluginHandle}; + void paint(Graphics &g) override { g.fillAll(Colours::black); } - SetWindowPos(pluginHandle, nullptr, pos.x, pos.y, rect.getWidth(), - rect.getHeight(), - isVisible() ? SWP_SHOWWINDOW : SWP_HIDEWINDOW); - } - } + private: + struct Inner final : public Component { + Inner() { setOpaque(true); } + void paint(Graphics &g) override { g.fillAll(Colours::black); } + }; - ChildComponent embeddedComponent; - std::unique_ptr peer; - using HandleFormat = HWND; + Inner inner; + }; + + ViewComponent embeddedComponent; #elif JUCE_MAC NSViewComponentWithParent embeddedComponent; using HandleFormat = NSView *; @@ -1718,11 +1904,35 @@ struct VST3PluginWindow : public AudioProcessorEditor, #endif HandleFormat pluginHandle = {}; - bool recursiveResize = false, isInOnSize = false; + bool recursiveResize = false, isInOnSize = false, attachedCalled = false; - ComponentPeer *currentPeer = nullptr; Steinberg::IPlugViewContentScaleSupport *scaleInterface = nullptr; float nativeScaleFactor = 1.0f; + float userScaleFactor = 1.0f; + + struct ScaleNotifierCallback { + VST3PluginWindow &window; + + void operator()(float platformScale) const { + MessageManager::callAsync( + [ref = Component::SafePointer(&window), + platformScale] { + if (auto *r = ref.getComponent()) { + r->nativeScaleFactor = platformScale; + r->setContentScaleFactor(); + r->resizeToFit(); + +#if JUCE_WINDOWS + r->embeddedComponent.updateHWNDBounds(); +#elif JUCE_LINUX || JUCE_BSD + r->embeddedComponent.updateEmbeddedBounds(); +#endif + } + }); + } + }; + + NativeScaleFactorNotifier scaleNotifier{this, ScaleNotifierCallback{*this}}; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(VST3PluginWindow) @@ -1731,27 +1941,56 @@ struct VST3PluginWindow : public AudioProcessorEditor, JUCE_BEGIN_IGNORE_WARNINGS_MSVC( 4996) // warning about overriding deprecated methods +//============================================================================== +static bool hasARAExtension(IPluginFactory *pluginFactory, + const String &pluginClassName) { + bool result = false; + + forEachARAFactory( + pluginFactory, [&pluginClassName, &result](const auto &pcClassInfo) { + if (compareWithString(pcClassInfo.name, pluginClassName) == 0) { + result = true; + + return false; + } + + return true; + }); + + return result; +} + //============================================================================== struct VST3ComponentHolder { VST3ComponentHolder(const VST3ModuleHandle::Ptr &m) : module(m) { - host = new PatchedVST3HostContext(); + host = addVSTComSmartPtrOwner(new PatchedVST3HostContext()); } ~VST3ComponentHolder() { terminate(); } - // transfers ownership to the plugin instance! - AudioPluginInstance *createPluginInstance(); + bool isIComponentAlsoIEditController() const { + if (component == nullptr) { + jassertfalse; + return false; + } + + return VSTComSmartPtr().loadFrom(component.get()); + } bool fetchController(VSTComSmartPtr &editController) { if (!isComponentInitialised && !initialise()) return false; + editController.loadFrom(component.get()); + // Get the IEditController: TUID controllerCID = {0}; - if (component->getControllerClassId(controllerCID) == kResultTrue && - FUID(controllerCID).isValid()) - editController.loadFrom(factory, controllerCID); + if (editController == nullptr && + component->getControllerClassId(controllerCID) == kResultTrue && + FUID(controllerCID).isValid()) { + editController.loadFrom(factory.get(), controllerCID); + } if (editController == nullptr) { // Try finding the IEditController the long way around: @@ -1762,13 +2001,10 @@ struct VST3ComponentHolder { factory->getClassInfo(i, &classInfo); if (std::strcmp(classInfo.category, kVstComponentControllerClass) == 0) - editController.loadFrom(factory, classInfo.cid); + editController.loadFrom(factory.get(), classInfo.cid); } } - if (editController == nullptr) - editController.loadFrom(component); - return (editController != nullptr); } @@ -1783,8 +2019,8 @@ struct VST3ComponentHolder { if (classIdx >= 0) { PClassInfo info; - bool success = (factory->getClassInfo(classIdx, &info) == kResultOk); - ignoreUnused(success); + [[maybe_unused]] bool success = + (factory->getClassInfo(classIdx, &info) == kResultOk); jassert(success); VSTComSmartPtr pf2; @@ -1793,14 +2029,14 @@ struct VST3ComponentHolder { std::unique_ptr info2; std::unique_ptr infoW; - if (pf2.loadFrom(factory)) { + if (pf2.loadFrom(factory.get())) { info2.reset(new PClassInfo2()); pf2->getClassInfo2(classIdx, info2.get()); } else { info2.reset(); } - if (pf3.loadFrom(factory)) { + if (pf3.loadFrom(factory.get())) { pf3->setHostContext(host->getFUnknown()); infoW.reset(new PClassInfoW()); pf3->getClassInfoUnicode(classIdx, infoW.get()); @@ -1834,6 +2070,9 @@ struct VST3ComponentHolder { info2.get(), infoW.get(), totalNumInputChannels, totalNumOutputChannels); + description.hasARAExtension = + hasARAExtension(factory.get(), description.name); + return; } @@ -1850,7 +2089,7 @@ struct VST3ComponentHolder { // initialisation are only called from the message thread. JUCE_ASSERT_MESSAGE_THREAD - factory = VSTComSmartPtr(module->getPluginFactory()); + factory = addVSTComSmartPtrOwner(module->getPluginFactory()); int classIdx; if ((classIdx = getClassIndex(module->getName())) < 0) @@ -1860,7 +2099,7 @@ struct VST3ComponentHolder { if (factory->getClassInfo(classIdx, &info) != kResultOk) return false; - if (!component.loadFrom(factory, info.cid) || component == nullptr) + if (!component.loadFrom(factory.get(), info.cid) || component == nullptr) return false; cidOfComponent = FUID(info.cid); @@ -1912,7 +2151,7 @@ struct VST3ComponentHolder { This is more memory-efficient than storing large vectors of parameter changes that we'll just throw away. */ -class ParamValueQueue : public Vst::IParamValueQueue { +class ParamValueQueue final : public Vst::IParamValueQueue { public: ParamValueQueue(Vst::ParamID idIn, Steinberg::int32 parameterIndexIn) : paramId(idIn), parameterIndex(parameterIndexIn) {} @@ -1975,12 +2214,12 @@ class ParamValueQueue : public Vst::IParamValueQueue { - addParameterData never allocates, as long you pass a paramID already passed to initialise */ -class ParameterChanges : public Vst::IParameterChanges { +class ParameterChanges final : public Vst::IParameterChanges { static constexpr Steinberg::int32 notInVector = -1; struct Entry { explicit Entry(std::unique_ptr queue) - : ptr(queue.release()) {} + : ptr(addVSTComSmartPtrOwner(queue.release())) {} VSTComSmartPtr ptr; Steinberg::int32 index = notInVector; @@ -2044,11 +2283,9 @@ class ParameterChanges : public Vst::IParameterChanges { } void initialise(const std::vector &idsIn) { - Steinberg::int32 index = 0; - - for (const auto &id : idsIn) + for (const auto [index, id] : enumerate(idsIn)) map.emplace(id, Entry{std::make_unique( - id, Steinberg::int32{index++})}); + id, (Steinberg::int32)index)}); queues.reserve(map.size()); queues.clear(); @@ -2057,7 +2294,7 @@ class ParameterChanges : public Vst::IParameterChanges { template void forEach(Callback &&callback) const { for (const auto *item : queues) { auto *ptr = item->ptr.get(); - callback(ptr->getParameterIndex(), ptr->get()); + callback(ptr->getParameterIndex(), ptr->getParameterId(), ptr->get()); } } @@ -2091,24 +2328,16 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { pluginInstance.parameterDispatcher.push(vstParamIndex, newValue); } - /* If the editor set the value, there's no need to notify it that the - parameter value changed. Instead, we set the cachedValue (which will be - read by the processor during the next processBlock) and notify listeners - that the parameter has changed. - */ - void setValueFromEditor(float newValue) { - pluginInstance.cachedParamValues.set(vstParamIndex, newValue); - sendValueChangedMessageToListeners(newValue); - } - /* If we're syncing the editor to the processor, the processor won't need to be notified about the parameter updates, so we can avoid flagging the change when updating the float cache. */ void setValueWithoutUpdatingProcessor(float newValue) { - pluginInstance.cachedParamValues.setWithoutNotifying(vstParamIndex, - newValue); - sendValueChangedMessageToListeners(newValue); + if (!exactlyEqual( + pluginInstance.cachedParamValues.exchangeWithoutNotifying( + vstParamIndex, newValue), + newValue)) + sendValueChangedMessageToListeners(newValue); } String getText(float value, int maximumLength) const override { @@ -2172,44 +2401,29 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { const Steinberg::int32 vstParamIndex; const Steinberg::Vst::ParamID paramID; const bool automatable; - const bool discrete = - getNumSteps() != AudioProcessor::getDefaultNumParameterSteps(); const int numSteps = [&] { auto stepCount = getParameterInfo().stepCount; return stepCount == 0 ? AudioProcessor::getDefaultNumParameterSteps() : stepCount + 1; }(); + const bool discrete = + getNumSteps() != AudioProcessor::getDefaultNumParameterSteps(); }; //============================================================================== - PatchedVST3PluginInstance(VST3ComponentHolder *componentHolder) + explicit PatchedVST3PluginInstance( + std::unique_ptr componentHolder) : AudioPluginInstance(getBusProperties(componentHolder->component)), - holder(componentHolder), midiInputs(new MidiEventList()), - midiOutputs(new MidiEventList()) { + holder(std::move(componentHolder)) { + jassert(holder->isComponentInitialised); holder->host->setPlugin(this); } ~PatchedVST3PluginInstance() override { - struct VST3Deleter : public CallbackMessage { - VST3Deleter(PatchedVST3PluginInstance &inInstance, WaitableEvent &inEvent) - : vst3Instance(inInstance), completionSignal(inEvent) {} - - void messageCallback() override { - vst3Instance.cleanup(); - completionSignal.signal(); - } - - PatchedVST3PluginInstance &vst3Instance; - WaitableEvent &completionSignal; - }; - - /* if (MessageManager::getInstance()->isThisTheMessageThread()) { */ + // NOTE(pedalboard): Pedalboard doesn't run a message thread, so + // clean up on the current thread instead of calling + // callOnMessageThread([this] { cleanup(); }); cleanup(); - /* } else { - WaitableEvent completionEvent; - (new VST3Deleter(*this, completionEvent))->post(); - completionEvent.wait(); - } */ } void cleanup() { @@ -2219,13 +2433,13 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { releaseResources(); if (editControllerConnection != nullptr && componentConnection != nullptr) { - editControllerConnection->disconnect(componentConnection); - componentConnection->disconnect(editControllerConnection); + editControllerConnection->disconnect(componentConnection.get()); + componentConnection->disconnect(editControllerConnection.get()); } editController->setComponentHandler(nullptr); - if (isControllerInitialised) + if (isControllerInitialised && !holder->isIComponentAlsoIEditController()) editController->terminate(); holder->terminate(); @@ -2256,17 +2470,19 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { if (!(isControllerInitialised || holder->fetchController(editController))) return false; - // (May return an error if the plugin combines the IComponent and - // IEditController implementations) - editController->initialize(holder->host->getFUnknown()); + // If the IComponent and IEditController are the same, we will have + // already initialized the object at this point and should avoid doing so + // again. + if (!holder->isIComponentAlsoIEditController()) + editController->initialize(holder->host->getFUnknown()); isControllerInitialised = true; - editController->setComponentHandler(holder->host); + editController->setComponentHandler(holder->host.get()); grabInformationObjects(); interconnectComponentAndController(); auto configureParameters = [this] { - refreshParameterList(); + initialiseParameterList(); synchroniseStates(); syncProgramNames(); }; @@ -2287,12 +2503,13 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { } void getExtensions(ExtensionsVisitor &visitor) const override { - struct Extensions : public ExtensionsVisitor::VST3Client { + struct Extensions final : public ExtensionsVisitor::VST3Client, + public ExtensionsVisitor::ARAClient { explicit Extensions(const PatchedVST3PluginInstance *instanceIn) : instance(instanceIn) {} Steinberg::Vst::IComponent *getIComponentPtr() const noexcept override { - return instance->holder->component; + return instance->holder->component.get(); } MemoryBlock getPreset() const override { @@ -2303,13 +2520,24 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { return instance->setStateFromPresetFile(rawData); } + void createARAFactoryAsync( + std::function cb) const noexcept override { + cb(ARAFactoryWrapper{ + ::juce::getARAFactory(*(instance->holder->module))}); + } + const PatchedVST3PluginInstance *instance = nullptr; }; - visitor.visitVST3Client(Extensions{this}); + Extensions extensions{this}; + visitor.visitVST3Client(extensions); + + if (::juce::getARAFactory(*(holder->module))) { + visitor.visitARAClient(extensions); + } } - void *getPlatformSpecificData() override { return holder->component; } + void *getPlatformSpecificData() override { return holder->component.get(); } void updateMidiMappings() { // MIDI mappings will always be updated on the main thread, but we need to @@ -2326,38 +2554,33 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { return module != nullptr ? module->getName() : String(); } - void repopulateArrangements( - Array &inputArrangements, - Array &outputArrangements) const { - inputArrangements.clearQuick(); - outputArrangements.clearQuick(); + std::vector + getActualArrangements(bool isInput) const { + std::vector result; - auto numInputAudioBuses = getBusCount(true); - auto numOutputAudioBuses = getBusCount(false); + const auto numBuses = getBusCount(isInput); - for (int i = 0; i < numInputAudioBuses; ++i) - inputArrangements.add(getArrangementForBus(processor, true, i)); + for (auto i = 0; i < numBuses; ++i) + result.push_back(getArrangementForBus(processor.get(), isInput, i)); - for (int i = 0; i < numOutputAudioBuses; ++i) - outputArrangements.add(getArrangementForBus(processor, false, i)); + return result; } - void processorLayoutsToArrangements( - Array &inputArrangements, - Array &outputArrangements) { - inputArrangements.clearQuick(); - outputArrangements.clearQuick(); + std::optional> + busLayoutsToArrangements(bool isInput) const { + std::vector result; - auto numInputBuses = getBusCount(true); - auto numOutputBuses = getBusCount(false); + const auto numBuses = getBusCount(isInput); - for (int i = 0; i < numInputBuses; ++i) - inputArrangements.add( - getVst3SpeakerArrangement(getBus(true, i)->getLastEnabledLayout())); + for (auto i = 0; i < numBuses; ++i) { + if (const auto arr = getVst3SpeakerArrangement( + getBus(isInput, i)->getLastEnabledLayout())) + result.push_back(*arr); + else + return {}; + } - for (int i = 0; i < numOutputBuses; ++i) - outputArrangements.add( - getVst3SpeakerArrangement(getBus(false, i)->getLastEnabledLayout())); + return result; } void prepareToPlay(double newSampleRate, @@ -2378,12 +2601,17 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { // Avoid redundantly calling things like setActive, which can be a // heavy-duty call for some plugins: - if (isActive && getSampleRate() == newSampleRate && + if (isActive && approximatelyEqual(getSampleRate(), newSampleRate) && getBlockSize() == estimatedSamplesPerBlock) return; using namespace Vst; + // If the plugin has already been activated (prepareToPlay has been called + // twice without a matching releaseResources call) deactivate it so that the + // speaker layout and bus activation can be updated safely. + deactivate(); + ProcessSetup setup; setup.symbolicSampleSize = isUsingDoublePrecision() ? kSample64 : kSample32; setup.maxSamplesPerBlock = estimatedSamplesPerBlock; @@ -2394,27 +2622,26 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { holder->initialise(); - Array inputArrangements, outputArrangements; - processorLayoutsToArrangements(inputArrangements, outputArrangements); + auto inArrangements = busLayoutsToArrangements(true).value_or( + std::vector{}); + auto outArrangements = busLayoutsToArrangements(false).value_or( + std::vector{}); // Some plug-ins will crash if you pass a nullptr to setBusArrangements! SpeakerArrangement nullArrangement = {}; - auto *inputArrangementData = inputArrangements.isEmpty() - ? &nullArrangement - : inputArrangements.getRawDataPointer(); - auto *outputArrangementData = outputArrangements.isEmpty() - ? &nullArrangement - : outputArrangements.getRawDataPointer(); + auto *inData = + inArrangements.empty() ? &nullArrangement : inArrangements.data(); + auto *outData = + outArrangements.empty() ? &nullArrangement : outArrangements.data(); warnOnFailure(processor->setBusArrangements( - inputArrangementData, inputArrangements.size(), outputArrangementData, - outputArrangements.size())); + inData, static_cast(inArrangements.size()), outData, + static_cast(outArrangements.size()))); - Array actualInArr, actualOutArr; - repopulateArrangements(actualInArr, actualOutArr); + const auto inArrActual = getActualArrangements(true); + const auto outArrActual = getActualArrangements(false); - jassert(actualInArr == inputArrangements && - actualOutArr == outputArrangements); + jassert(inArrActual == inArrangements && outArrActual == outArrangements); // Needed for having the same sample rate in processBlock(); some plugins // need this! @@ -2432,7 +2659,9 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { Vst::kAudio, Vst::kOutput, i, getBus(false, i)->isEnabled() ? 1 : 0)); setLatencySamples(jmax(0, (int)processor->getLatencySamples())); - cachedBusLayouts = getBusesLayout(); + + inputBusMap.prepare(createChannelMappings(true)); + outputBusMap.prepare(createChannelMappings(false)); setStateForAllMidiBuses(true); @@ -2444,19 +2673,9 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { void releaseResources() override { const SpinLock::ScopedLockType lock(processMutex); - - // if (!isActive) - // return; // Avoids redundantly calling things like setActive - - isActive = false; - - setStateForAllMidiBuses(false); - - if (processor != nullptr) - warnOnFailureIfImplemented(processor->setProcessing(false)); - - if (holder->component != nullptr) - warnOnFailure(holder->component->setActive(false)); + // NOTE(pedalboard): Always deactivate the plugin here, even if we + // think it's already inactive. (See spotify/pedalboard#285.) + deactivate(/* force */ true); } bool supportsDoublePrecisionProcessing() const override { @@ -2544,8 +2763,8 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { data.symbolicSampleSize = sampleSize; data.numInputs = numInputAudioBuses; data.numOutputs = numOutputAudioBuses; - data.inputParameterChanges = inputParameterChanges; - data.outputParameterChanges = outputParameterChanges; + data.inputParameterChanges = inputParameterChanges.get(); + data.outputParameterChanges = outputParameterChanges.get(); data.numSamples = (Steinberg::int32)numSamples; updateTimingInformation(data, getSampleRate()); @@ -2565,9 +2784,15 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { processor->process(data); - outputParameterChanges->forEach([&](Steinberg::int32 index, float value) { - parameterDispatcher.push(index, value); - }); + outputParameterChanges->forEach( + [&](Steinberg::int32 vstParamIndex, Vst::ParamID id, float value) { + // Send the parameter value from the processor to the editor + parameterDispatcher.push(vstParamIndex, value); + + // Update the host's parameter value + if (auto *param = getParameterForID(id)) + param->setValueWithoutUpdatingProcessor(value); + }); midiMessages.clear(); MidiEventList::toMidiBuffer(midiMessages, *midiOutputs); @@ -2587,8 +2812,7 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { // not much we can do to check the layout while the audio processor is // running Let's at least check if it is a VST3 compatible layout - for (int dir = 0; dir < 2; ++dir) { - bool isInput = (dir == 0); + for (const auto isInput : {true, false}) { auto n = getBusCount(isInput); for (int i = 0; i < n; ++i) @@ -2600,8 +2824,7 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { } bool syncBusLayouts(const BusesLayout &layouts) const { - for (int dir = 0; dir < 2; ++dir) { - bool isInput = (dir == 0); + for (const auto isInput : {true, false}) { auto n = getBusCount(isInput); const Vst::BusDirection vstDir = (isInput ? Vst::kInput : Vst::kOutput); @@ -2615,41 +2838,56 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { } } - Array inputArrangements, outputArrangements; + const auto getPotentialArrangements = [&](bool isInput) + -> std::optional> { + std::vector result; - for (int i = 0; i < layouts.inputBuses.size(); ++i) { - const auto &requested = layouts.getChannelSet(true, i); - inputArrangements.add(getVst3SpeakerArrangement( - requested.isDisabled() ? getBus(true, i)->getLastEnabledLayout() - : requested)); - } + for (int i = 0; i < layouts.getBuses(isInput).size(); ++i) { + const auto &requested = layouts.getChannelSet(isInput, i); + + if (const auto arr = getVst3SpeakerArrangement( + requested.isDisabled() + ? getBus(isInput, i)->getLastEnabledLayout() + : requested)) + result.push_back(*arr); + else + return {}; + } - for (int i = 0; i < layouts.outputBuses.size(); ++i) { - const auto &requested = layouts.getChannelSet(false, i); - outputArrangements.add(getVst3SpeakerArrangement( - requested.isDisabled() ? getBus(false, i)->getLastEnabledLayout() - : requested)); + return result; + }; + + auto inArrangements = getPotentialArrangements(true); + auto outArrangements = getPotentialArrangements(false); + + if (!inArrangements.has_value() || !outArrangements.has_value()) { + // This bus layout can't be represented as a VST3 speaker arrangement + return false; } + auto &inputArrangements = *inArrangements; + auto &outputArrangements = *outArrangements; + // Some plug-ins will crash if you pass a nullptr to setBusArrangements! Vst::SpeakerArrangement nullArrangement = {}; - auto *inputArrangementData = inputArrangements.isEmpty() - ? &nullArrangement - : inputArrangements.getRawDataPointer(); - auto *outputArrangementData = outputArrangements.isEmpty() + auto *inputArrangementData = + inputArrangements.empty() ? &nullArrangement : inputArrangements.data(); + auto *outputArrangementData = outputArrangements.empty() ? &nullArrangement - : outputArrangements.getRawDataPointer(); + : outputArrangements.data(); if (processor->setBusArrangements( - inputArrangementData, inputArrangements.size(), - outputArrangementData, outputArrangements.size()) != kResultTrue) + inputArrangementData, static_cast(inputArrangements.size()), + outputArrangementData, + static_cast(outputArrangements.size())) != kResultTrue) return false; // check if the layout matches the request - Array actualIn, actualOut; - repopulateArrangements(actualIn, actualOut); + const auto inArrActual = getActualArrangements(true); + const auto outArrActual = getActualArrangements(false); - return (actualIn == inputArrangements && actualOut == outputArrangements); + return (inArrActual == inputArrangements && + outArrActual == outputArrangements); } bool canApplyBusesLayout(const BusesLayout &layouts) const override { @@ -2657,25 +2895,22 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { // call releaseResources first! jassert(!isActive); - bool result = syncBusLayouts(layouts); - - // didn't succeed? Make sure it's back in it's original state - if (!result) - syncBusLayouts(getBusesLayout()); - + const auto previousLayout = getBusesLayout(); + const auto result = syncBusLayouts(layouts); + syncBusLayouts(previousLayout); return result; } //============================================================================== void updateTrackProperties(const TrackProperties &properties) override { if (trackInfoListener != nullptr) { - VSTComSmartPtr l( - new TrackPropertiesAttributeList(properties)); - trackInfoListener->setChannelContextInfos(l); + auto l = + addVSTComSmartPtrOwner(new TrackPropertiesAttributeList(properties)); + trackInfoListener->setChannelContextInfos(l.get()); } } - struct TrackPropertiesAttributeList : public Vst::IAttributeList { + struct TrackPropertiesAttributeList final : public Vst::IAttributeList { TrackPropertiesAttributeList(const TrackProperties &properties) : props(properties) {} virtual ~TrackPropertiesAttributeList() {} @@ -2743,14 +2978,14 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { }; //============================================================================== - String getChannelName(int channelIndex, bool forInput, - bool forAudioChannel) const { - auto numBuses = getNumSingleDirectionBusesFor(holder->component, forInput, - forAudioChannel); + String getChannelName(int channelIndex, Direction direction) const { + auto numBuses = getNumSingleDirectionBusesFor(holder->component.get(), + MediaKind::audio, direction); + int numCountedChannels = 0; for (int i = 0; i < numBuses; ++i) { - auto busInfo = getBusInfo(forInput, forAudioChannel, i); + auto busInfo = getBusInfo(MediaKind::audio, direction, i); numCountedChannels += busInfo.channelCount; @@ -2762,32 +2997,30 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { } const String getInputChannelName(int channelIndex) const override { - return getChannelName(channelIndex, true, true); + return getChannelName(channelIndex, Direction::input); } const String getOutputChannelName(int channelIndex) const override { - return getChannelName(channelIndex, false, true); + return getChannelName(channelIndex, Direction::output); } bool isInputChannelStereoPair(int channelIndex) const override { int busIdx; return getOffsetInBusBufferForAbsoluteChannelIndex(true, channelIndex, busIdx) >= 0 && - getBusInfo(true, true, busIdx).channelCount == 2; + getBusInfo(MediaKind::audio, Direction::input, busIdx) + .channelCount == 2; } bool isOutputChannelStereoPair(int channelIndex) const override { int busIdx; return getOffsetInBusBufferForAbsoluteChannelIndex(false, channelIndex, busIdx) >= 0 && - getBusInfo(false, true, busIdx).channelCount == 2; + getBusInfo(MediaKind::audio, Direction::output, busIdx) + .channelCount == 2; } - bool acceptsMidi() const override { - return getNumSingleDirectionBusesFor(holder->component, true, false) > 0; - } - bool producesMidi() const override { - return getNumSingleDirectionBusesFor(holder->component, false, false) > 0; - } + bool acceptsMidi() const override { return hasMidiInput; } + bool producesMidi() const override { return hasMidiOutput; } //============================================================================== AudioProcessorParameter *getBypassParameter() const override { @@ -2817,7 +3050,7 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { //============================================================================== AudioProcessorEditor *createEditor() override { - if (auto *view = tryCreatingView()) + if (auto view = becomeVSTComSmartPtrOwner(tryCreatingView())) return new VST3PluginWindow(this, view); return nullptr; @@ -2829,7 +3062,7 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { if (getActiveEditor() != nullptr) return true; - VSTComSmartPtr view(tryCreatingView(), false); + auto view = becomeVSTComSmartPtrOwner(tryCreatingView()); return view != nullptr; } @@ -2908,11 +3141,11 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { auto componentStream(createMemoryStreamForState(*head, "IComponent")); if (componentStream != nullptr && holder->component != nullptr) - holder->component->setState(componentStream); + holder->component->setState(componentStream.get()); if (editController != nullptr) { if (componentStream != nullptr) { - int64 result; + Steinberg::int64 result; componentStream->seek(0, IBStream::kIBSeekSet, &result); setComponentStateAndResetParameters(*componentStream); } @@ -2921,7 +3154,7 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { createMemoryStreamForState(*head, "IEditController")); if (controllerStream != nullptr) - editController->setState(controllerStream); + editController->setState(controllerStream.get()); } } } @@ -2943,15 +3176,15 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { } MemoryBlock getStateForPresetFile() const { - VSTComSmartPtr memoryStream = - new Steinberg::MemoryStream(); + auto memoryStream = + becomeVSTComSmartPtrOwner(new Steinberg::MemoryStream()); if (memoryStream == nullptr || holder->component == nullptr) return {}; const auto saved = Steinberg::Vst::PresetFile::savePreset( - memoryStream, holder->cidOfComponent, holder->component, - editController); + memoryStream.get(), holder->cidOfComponent, holder->component.get(), + editController.get()); if (saved) return {memoryStream->getData(), @@ -2961,17 +3194,16 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { } bool setStateFromPresetFile(const MemoryBlock &rawData) const { - MemoryBlock rawDataCopy(rawData); - VSTComSmartPtr memoryStream = - new Steinberg::MemoryStream(rawDataCopy.getData(), - (int)rawDataCopy.getSize()); + auto rawDataCopy = rawData; + auto memoryStream = becomeVSTComSmartPtrOwner(new Steinberg::MemoryStream( + rawDataCopy.getData(), (int)rawDataCopy.getSize())); if (memoryStream == nullptr || holder->component == nullptr) return false; return Steinberg::Vst::PresetFile::loadPreset( - memoryStream, holder->cidOfComponent, holder->component, editController, - nullptr); + memoryStream.get(), holder->cidOfComponent, holder->component.get(), + editController.get(), nullptr); } //============================================================================== @@ -2985,12 +3217,27 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { } /** @note Not applicable to VST3 */ - void setCurrentProgramStateInformation(const void *data, - int sizeInBytes) override { - ignoreUnused(data, sizeInBytes); + void + setCurrentProgramStateInformation([[maybe_unused]] const void *data, + [[maybe_unused]] int sizeInBytes) override { } private: + void deactivate(bool force = false) { + if (!isActive && !force) + return; + + isActive = false; + + if (processor != nullptr) + warnOnFailureIfImplemented(processor->setProcessing(false)); + + if (holder->component != nullptr) + warnOnFailure(holder->component->setActive(false)); + + setStateForAllMidiBuses(false); + } + //============================================================================== #if JUCE_LINUX || JUCE_BSD SharedResourcePointer runLoop; @@ -3024,9 +3271,7 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { even if there aren't enough channels to process, as very poorly specified by the Steinberg SDK */ - VST3FloatAndDoubleBusMapComposite inputBusMap, outputBusMap; - Array inputBuses, outputBuses; - AudioProcessor::BusesLayout cachedBusLayouts; + HostBufferMapper inputBusMap, outputBusMap; StringArray programNames; Vst::ParamID programParameterID = (Vst::ParamID)-1; @@ -3050,7 +3295,9 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { if (object != nullptr) { Steinberg::MemoryStream stream; - if (object->getState(&stream) == kResultTrue) { + const auto result = object->getState(&stream); + + if (result == kResultTrue) { MemoryBlock info(stream.getData(), (size_t)stream.getSize()); head.createNewChildElement(identifier) ->addTextElement(info.toBase64Encoding()); @@ -3064,40 +3311,52 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { MemoryBlock mem; if (mem.fromBase64Encoding(state->getAllSubText())) { - VSTComSmartPtr stream( - new Steinberg::MemoryStream(), false); + auto stream = becomeVSTComSmartPtrOwner(new Steinberg::MemoryStream()); stream->setSize((TSize)mem.getSize()); mem.copyTo(stream->getData(), 0, mem.getSize()); return stream; } } - return nullptr; + return {}; } CachedParamValues cachedParamValues; - VSTComSmartPtr inputParameterChanges{new ParameterChanges}; - VSTComSmartPtr outputParameterChanges{new ParameterChanges}; - VSTComSmartPtr midiInputs, midiOutputs; + VSTComSmartPtr inputParameterChanges = + addVSTComSmartPtrOwner(new ParameterChanges); + VSTComSmartPtr outputParameterChanges = + addVSTComSmartPtrOwner(new ParameterChanges); + VSTComSmartPtr midiInputs = + addVSTComSmartPtrOwner(new MidiEventList); + VSTComSmartPtr midiOutputs = + addVSTComSmartPtrOwner(new MidiEventList); Vst::ProcessContext timingInfo; //< Only use this in processBlock()! bool isControllerInitialised = false, isActive = false, lastProcessBlockCallWasBypass = false; + const bool hasMidiInput = getNumSingleDirectionBusesFor( + holder->component.get(), MediaKind::event, + Direction::input) > 0, + hasMidiOutput = getNumSingleDirectionBusesFor( + holder->component.get(), MediaKind::event, + Direction::output) > 0; VST3Parameter *bypassParam = nullptr; //============================================================================== /** Some plugins need to be "connected" to intercommunicate between their * implemented classes */ void interconnectComponentAndController() { - componentConnection.loadFrom(holder->component); - editControllerConnection.loadFrom(editController); + componentConnection.loadFrom(holder->component.get()); + editControllerConnection.loadFrom(editController.get()); if (componentConnection != nullptr && editControllerConnection != nullptr) { - warnOnFailure(componentConnection->connect(editControllerConnection)); - warnOnFailure(editControllerConnection->connect(componentConnection)); + warnOnFailure( + componentConnection->connect(editControllerConnection.get())); + warnOnFailure( + editControllerConnection->connect(componentConnection.get())); } } - void refreshParameterList() override { + void initialiseParameterList() { AudioProcessorParameterGroup newParameterTree; // We're going to add parameter groups to the tree recursively in the same @@ -3185,41 +3444,51 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { } void grabInformationObjects() { - processor.loadFrom(holder->component); - unitInfo.loadFrom(holder->component); - programListData.loadFrom(holder->component); - unitData.loadFrom(holder->component); - editController2.loadFrom(holder->component); - midiMapping.loadFrom(holder->component); - componentHandler.loadFrom(holder->component); - componentHandler2.loadFrom(holder->component); - trackInfoListener.loadFrom(holder->component); + processor.loadFrom(holder->component.get()); + unitInfo.loadFrom(holder->component.get()); + programListData.loadFrom(holder->component.get()); + unitData.loadFrom(holder->component.get()); + editController2.loadFrom(holder->component.get()); + midiMapping.loadFrom(holder->component.get()); + componentHandler.loadFrom(holder->component.get()); + componentHandler2.loadFrom(holder->component.get()); + trackInfoListener.loadFrom(holder->component.get()); if (processor == nullptr) - processor.loadFrom(editController); + processor.loadFrom(editController.get()); if (unitInfo == nullptr) - unitInfo.loadFrom(editController); + unitInfo.loadFrom(editController.get()); if (programListData == nullptr) - programListData.loadFrom(editController); + programListData.loadFrom(editController.get()); if (unitData == nullptr) - unitData.loadFrom(editController); + unitData.loadFrom(editController.get()); if (editController2 == nullptr) - editController2.loadFrom(editController); + editController2.loadFrom(editController.get()); if (midiMapping == nullptr) - midiMapping.loadFrom(editController); + midiMapping.loadFrom(editController.get()); if (componentHandler == nullptr) - componentHandler.loadFrom(editController); + componentHandler.loadFrom(editController.get()); if (componentHandler2 == nullptr) - componentHandler2.loadFrom(editController); + componentHandler2.loadFrom(editController.get()); if (trackInfoListener == nullptr) - trackInfoListener.loadFrom(editController); + trackInfoListener.loadFrom(editController.get()); } void setStateForAllMidiBuses(bool newState) { - setStateForAllBusesOfType(holder->component, newState, true, - false); // Activate/deactivate MIDI inputs - setStateForAllBusesOfType(holder->component, newState, false, - false); // Activate/deactivate MIDI outputs + setStateForAllEventBuses(holder->component.get(), newState, + Direction::input); + setStateForAllEventBuses(holder->component.get(), newState, + Direction::output); + } + + std::vector createChannelMappings(bool isInput) const { + std::vector result; + result.reserve((size_t)getBusCount(isInput)); + + for (auto i = 0; i < getBusCount(isInput); ++i) + result.emplace_back(*getBus(isInput, i)); + + return result; } void setupIO() { @@ -3233,7 +3502,8 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { warnOnFailure(processor->setupProcessing(setup)); - cachedBusLayouts = getBusesLayout(); + inputBusMap.prepare(createChannelMappings(true)); + outputBusMap.prepare(createChannelMappings(false)); setRateAndBufferSizeDetails(setup.sampleRate, (int)setup.maxSamplesPerBlock); } @@ -3244,8 +3514,7 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { VSTComSmartPtr processor; processor.loadFrom(component.get()); - for (int dirIdx = 0; dirIdx < 2; ++dirIdx) { - const bool isInput = (dirIdx == 0); + for (const auto isInput : {true, false}) { const Vst::BusDirection dir = (isInput ? Vst::kInput : Vst::kOutput); const int numBuses = component->getBusCount(Vst::kAudio, dir); @@ -3264,7 +3533,8 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { Vst::SpeakerArrangement arr; if (processor != nullptr && processor->getBusArrangement(dir, i, arr) == kResultOk) - layout = getChannelSetForSpeakerArrangement(arr); + if (const auto set = getChannelSetForSpeakerArrangement(arr)) + layout = *set; busProperties.addBus(isInput, toString(info.name), layout, (info.flags & Vst::BusInfo::kDefaultActive) != 0); @@ -3275,10 +3545,11 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { } //============================================================================== - Vst::BusInfo getBusInfo(bool forInput, bool forAudio, int index = 0) const { + Vst::BusInfo getBusInfo(MediaKind kind, Direction direction, + int index = 0) const { Vst::BusInfo busInfo; - busInfo.mediaType = forAudio ? Vst::kAudio : Vst::kEvent; - busInfo.direction = forInput ? Vst::kInput : Vst::kOutput; + busInfo.mediaType = toVstType(kind); + busInfo.direction = toVstType(direction); busInfo.channelCount = 0; holder->component->getBusInfo(busInfo.mediaType, busInfo.direction, @@ -3294,7 +3565,8 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { // call was processBlockBypassed, otherwise do nothing if (processBlockBypassedCalled) { if (bypassParam != nullptr && - (bypassParam->getValue() == 0.0f || !lastProcessBlockCallWasBypass)) + (approximatelyEqual(bypassParam->getValue(), 0.0f) || + !lastProcessBlockCallWasBypass)) bypassParam->setValue(1.0f); } else { if (lastProcessBlockCallWasBypass && bypassParam != nullptr) @@ -3308,7 +3580,7 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { /** @note An IPlugView, when first created, should start with a ref-count of * 1! */ IPlugView *tryCreatingView() const { - JUCE_ASSERT_MESSAGE_THREAD + JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED IPlugView *v = editController->createView(Vst::ViewType::kEditor); @@ -3324,15 +3596,8 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { template void associateWith(Vst::ProcessData &destination, AudioBuffer &buffer) { - VST3BufferExchange::mapBufferToBuses( - inputBuses, inputBusMap.get(), cachedBusLayouts.inputBuses, - buffer); - VST3BufferExchange::mapBufferToBuses( - outputBuses, outputBusMap.get(), - cachedBusLayouts.outputBuses, buffer); - - destination.inputs = inputBuses.getRawDataPointer(); - destination.outputs = outputBuses.getRawDataPointer(); + destination.inputs = inputBusMap.getVst3LayoutForJuceBuffer(buffer); + destination.outputs = outputBusMap.getVst3LayoutForJuceBuffer(buffer); } void associateWith(Vst::ProcessData &destination, MidiBuffer &midiBuffer) { @@ -3340,13 +3605,16 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { midiOutputs->clear(); if (acceptsMidi()) { - MidiEventList::hostToPluginEventList(*midiInputs, midiBuffer, - destination.inputParameterChanges, - storedMidiMapping); + MidiEventList::hostToPluginEventList( + *midiInputs, midiBuffer, storedMidiMapping, + [this](const auto controlID, const auto paramValue) { + if (auto *param = this->getParameterForID(controlID)) + param->setValueNotifyingHost((float)paramValue); + }); } - destination.inputEvents = midiInputs; - destination.outputEvents = midiOutputs; + destination.inputEvents = midiInputs.get(); + destination.outputEvents = midiOutputs.get(); } void updateTimingInformation(Vst::ProcessData &destination, @@ -3449,16 +3717,6 @@ class PatchedVST3PluginInstance final : public AudioPluginInstance { JUCE_END_IGNORE_WARNINGS_MSVC -//============================================================================== -AudioPluginInstance *VST3ComponentHolder::createPluginInstance() { - if (!initialise()) - return nullptr; - - auto *plugin = new PatchedVST3PluginInstance(this); - host->setPlugin(plugin); - return plugin; -} - //============================================================================== tresult PatchedVST3HostContext::beginEdit(Vst::ParamID paramID) { if (plugin == nullptr) @@ -3478,11 +3736,11 @@ tresult PatchedVST3HostContext::performEdit(Vst::ParamID paramID, return kResultTrue; if (auto *param = plugin->getParameterForID(paramID)) { - param->setValueFromEditor((float)valueNormalised); + param->setValueNotifyingHost((float)valueNormalised); // did the plug-in already update the parameter internally - if (plugin->editController->getParamNormalized(paramID) != - (float)valueNormalised) + if (!approximatelyEqual(plugin->editController->getParamNormalized(paramID), + valueNormalised)) return plugin->editController->setParamNormalized(paramID, valueNormalised); @@ -3614,8 +3872,8 @@ tresult PatchedVST3HostContext::ContextMenu::popup(Steinberg::UCoord x, handleResult(topLevelMenu->showMenu(options)); #else topLevelMenu->showMenuAsync( - options, ModalCallbackFunction::create( - menuFinished, VSTComSmartPtr(this))); + options, ModalCallbackFunction::create(menuFinished, + addVSTComSmartPtrOwner(this))); #endif return kResultOk; @@ -3645,63 +3903,120 @@ bool PatchedVST3PluginFormat::setStateFromVSTPresetFile( void PatchedVST3PluginFormat::findAllTypesForFile( OwnedArray &results, const String &fileOrIdentifier) { - if (fileMightContainThisPluginType(fileOrIdentifier)) { + if (!fileMightContainThisPluginType(fileOrIdentifier)) + return; + + if (const auto fast = + DescriptionLister::findDescriptionsFast(File(fileOrIdentifier)); + !fast.empty()) { + for (const auto &d : fast) + results.add(new PluginDescription(d)); + + return; + } + + for (const auto &file : getLibraryPaths(fileOrIdentifier)) { /** Since there is no apparent indication if a VST3 plugin is a shell or not, we're stuck iterating through a VST3's factory, creating a description for every housed plugin. */ - VSTComSmartPtr pluginFactory( - DLLHandleCache::getInstance() - ->findOrCreateHandle(fileOrIdentifier) - .getPluginFactory()); + auto pluginFactory = addVSTComSmartPtrOwner(DLLHandleCache::getInstance() + ->findOrCreateHandle(file) + .getPluginFactory()); - if (pluginFactory != nullptr) { - VSTComSmartPtr host(new PatchedVST3HostContext()); - DescriptionLister lister(host, pluginFactory); - lister.findDescriptionsAndPerform(File(fileOrIdentifier)); + if (pluginFactory == nullptr) + continue; - results.addCopiesOf(lister.list); - } else { - jassertfalse; - } + auto host = addVSTComSmartPtrOwner(new PatchedVST3HostContext()); + + for (const auto &d : DescriptionLister::findDescriptionsSlow( + *host, *pluginFactory, File(file))) + results.add(new PluginDescription(d)); } } -void PatchedVST3PluginFormat::createPluginInstance( - const PluginDescription &description, double, int, - PluginCreationCallback callback) { - std::unique_ptr result; - - if (fileMightContainThisPluginType(description.fileOrIdentifier)) { - File file(description.fileOrIdentifier); +void PatchedVST3PluginFormat::createARAFactoryAsync( + const PluginDescription &description, ARAFactoryCreationCallback callback) { + if (!description.hasARAExtension) { + jassertfalse; + callback({{}, "The provided plugin does not support ARA features"}); + } - auto previousWorkingDirectory = File::getCurrentWorkingDirectory(); - file.getParentDirectory().setAsCurrentWorkingDirectory(); + File file(description.fileOrIdentifier); + auto pluginFactory = + addVSTComSmartPtrOwner(DLLHandleCache::getInstance() + ->findOrCreateHandle(file.getFullPathName()) + .getPluginFactory()); + const auto *pluginName = description.name.toRawUTF8(); - if (const VST3ModuleHandle::Ptr module = - VST3ModuleHandle::findOrCreateModule(file, description)) { - std::unique_ptr holder( - new VST3ComponentHolder(module)); + callback({ARAFactoryWrapper{ + ::juce::getARAFactory(pluginFactory.get(), pluginName)}, + {}}); +} - if (holder->initialise()) { - result.reset(new PatchedVST3PluginInstance(holder.release())); +static std::unique_ptr +createVST3Instance(PatchedVST3PluginFormat &format, + const PluginDescription &description, const File &file) { + if (!format.fileMightContainThisPluginType(description.fileOrIdentifier)) + return nullptr; - if (!result->initialise()) - result.reset(); - } + struct ScopedWorkingDirectory { + ~ScopedWorkingDirectory() { + previousWorkingDirectory.setAsCurrentWorkingDirectory(); } + File previousWorkingDirectory = File::getCurrentWorkingDirectory(); + }; + + const ScopedWorkingDirectory scope; + file.getParentDirectory().setAsCurrentWorkingDirectory(); - previousWorkingDirectory.setAsCurrentWorkingDirectory(); + const VST3ModuleHandle::Ptr module{ + VST3ModuleHandle::findOrCreateModule(file, description)}; + + if (module == nullptr) + return nullptr; + + auto holder = std::make_unique(module); + + if (!holder->initialise()) + return nullptr; + + auto instance = + std::make_unique(std::move(holder)); + + if (!instance->initialise()) + return nullptr; + + return instance; +} + +StringArray +PatchedVST3PluginFormat::getLibraryPaths(const String &fileOrIdentifier) { +#if JUCE_WINDOWS + if (!File(fileOrIdentifier).existsAsFile()) { + StringArray files; + recursiveFileSearch(files, fileOrIdentifier, true); + return files; } +#endif - String errorMsg; + return {fileOrIdentifier}; +} - if (result == nullptr) - errorMsg = TRANS("Unable to load XXX plug-in file").replace("XXX", "VST-3"); +void PatchedVST3PluginFormat::createPluginInstance( + const PluginDescription &description, double, int, + PluginCreationCallback callback) { + for (const auto &file : getLibraryPaths(description.fileOrIdentifier)) { + if (auto result = createVST3Instance(*this, description, file)) { + callback(std::move(result), {}); + return; + } + } - callback(std::move(result), errorMsg); + callback(nullptr, + TRANS("Unable to load XXX plug-in file").replace("XXX", "VST-3")); } bool PatchedVST3PluginFormat::requiresUnblockedMessageThreadDuringCreation( @@ -3713,12 +4028,7 @@ bool PatchedVST3PluginFormat::fileMightContainThisPluginType( const String &fileOrIdentifier) { auto f = File::createFileWithoutCheckingPath(fileOrIdentifier); - return f.hasFileExtension(".vst3") -#if JUCE_MAC || JUCE_LINUX || JUCE_BSD - && f.exists(); -#else - && f.existsAsFile(); -#endif + return f.hasFileExtension(".vst3") && f.exists(); } String PatchedVST3PluginFormat::getNameOfPluginFromIdentifier( @@ -3768,15 +4078,18 @@ void PatchedVST3PluginFormat::recursiveFileSearch(StringArray &results, FileSearchPath PatchedVST3PluginFormat::getDefaultLocationsToSearch() { #if JUCE_WINDOWS - auto programFiles = + const auto localAppData = + File::getSpecialLocation(File::windowsLocalAppData).getFullPathName(); + const auto programFiles = File::getSpecialLocation(File::globalApplicationsDirectory) .getFullPathName(); - return FileSearchPath(programFiles + "\\Common Files\\VST3"); + return FileSearchPath(localAppData + "\\Programs\\Common\\VST3;" + + programFiles + "\\Common Files\\VST3"); #elif JUCE_MAC return FileSearchPath( - "/Library/Audio/Plug-Ins/VST3;~/Library/Audio/Plug-Ins/VST3"); + "~/Library/Audio/Plug-Ins/VST3;/Library/Audio/Plug-Ins/VST3"); #else - return FileSearchPath("/usr/lib/vst3/;/usr/local/lib/vst3/;~/.vst3/"); + return FileSearchPath("~/.vst3/;/usr/lib/vst3/;/usr/local/lib/vst3/"); #endif } diff --git a/pedalboard/juce_overrides/juce_PatchedVST3PluginFormat.h b/pedalboard/juce_overrides/juce_PatchedVST3PluginFormat.h index c6d78c492..7e4f694e0 100644 --- a/pedalboard/juce_overrides/juce_PatchedVST3PluginFormat.h +++ b/pedalboard/juce_overrides/juce_PatchedVST3PluginFormat.h @@ -2,15 +2,15 @@ ============================================================================== This file is part of the JUCE library. - Copyright (c) 2020 - Raw Material Software Limited + 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 6 End-User License - Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + 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-6-licence + 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 @@ -84,6 +84,8 @@ class PatchedVST3PluginFormat : public AudioPluginFormat { bool) override; bool doesPluginStillExist(const PluginDescription &) override; FileSearchPath getDefaultLocationsToSearch() override; + void createARAFactoryAsync(const PluginDescription &, + ARAFactoryCreationCallback callback) override; private: //============================================================================== @@ -93,6 +95,7 @@ class PatchedVST3PluginFormat : public AudioPluginFormat { bool requiresUnblockedMessageThreadDuringCreation( const PluginDescription &) const override; void recursiveFileSearch(StringArray &, const File &, bool recursive); + StringArray getLibraryPaths(const String &); JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PatchedVST3PluginFormat) }; diff --git a/pedalboard/juce_overrides/juce_PatchedWavAudioFormat.h b/pedalboard/juce_overrides/juce_PatchedWavAudioFormat.h index 9133bb81f..cf9a8d068 100644 --- a/pedalboard/juce_overrides/juce_PatchedWavAudioFormat.h +++ b/pedalboard/juce_overrides/juce_PatchedWavAudioFormat.h @@ -90,7 +90,7 @@ class DrWavAudioFormatReader : public AudioFormatReader { // and will be deleted in its destructor } - bool readSamples(int **destChannels, int numDestChannels, + bool readSamples(int *const *destChannels, int numDestChannels, int startOffsetInDestBuffer, int64 startSampleInFile, int numSamples) override { if (!wavInitialized || numSamples <= 0) { @@ -169,7 +169,7 @@ class DrWavAudioFormatReader : public AudioFormatReader { InputStream *inputStream; // Borrowed reference (owned by base class) int64 currentPosition = 0; - void clearSamplesBeyondFile(int **destChannels, int numDestChannels, + void clearSamplesBeyondFile(int *const *destChannels, int numDestChannels, int startOffset, int numSamples, int samplesRead) { for (int ch = 0; ch < numDestChannels; ++ch) { diff --git a/pedalboard/juce_overrides/juce_mac_PatchedWindowing.mm b/pedalboard/juce_overrides/juce_mac_PatchedWindowing.mm index 573977f3b..8cf759612 100644 --- a/pedalboard/juce_overrides/juce_mac_PatchedWindowing.mm +++ b/pedalboard/juce_overrides/juce_mac_PatchedWindowing.mm @@ -2,15 +2,15 @@ ============================================================================== This file is part of the JUCE library. - Copyright (c) 2020 - Raw Material Software Limited + 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 6 End-User License - Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + 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-6-licence + 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 @@ -27,173 +27,6 @@ Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). void LookAndFeel::playAlertSound() { NSBeep(); } -//============================================================================== -class OSXMessageBox : private AsyncUpdater { -public: - OSXMessageBox(const MessageBoxOptions &opts, - std::unique_ptr &&c) - : options(opts), callback(std::move(c)) {} - - int getResult() const { - switch (getRawResult()) { - case NSAlertFirstButtonReturn: - return 0; - case NSAlertSecondButtonReturn: - return 1; - case NSAlertThirdButtonReturn: - return 2; - default: - break; - } - - jassertfalse; - return 0; - } - - using AsyncUpdater::triggerAsyncUpdate; - -private: - void handleAsyncUpdate() override { - auto result = getResult(); - - if (callback != nullptr) - callback->modalStateFinished(result); - - delete this; - } - - static void addButton(NSAlert *alert, const String &button) { - if (!button.isEmpty()) - [alert addButtonWithTitle:juceStringToNS(button)]; - } - - NSInteger getRawResult() const { - NSAlert *alert = [[[NSAlert alloc] init] autorelease]; - - [alert setMessageText:juceStringToNS(options.getTitle())]; - [alert setInformativeText:juceStringToNS(options.getMessage())]; - - [alert - setAlertStyle:options.getIconType() == MessageBoxIconType::WarningIcon - ? NSAlertStyleCritical - : NSAlertStyleInformational]; - - const auto button1Text = options.getButtonText(0); - - addButton(alert, button1Text.isEmpty() ? "OK" : button1Text); - addButton(alert, options.getButtonText(1)); - addButton(alert, options.getButtonText(2)); - - return [alert runModal]; - } - - MessageBoxOptions options; - std::unique_ptr callback; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(OSXMessageBox) -}; - -static int showDialog(const MessageBoxOptions &options, - ModalComponentManager::Callback *callbackIn, - AlertWindowMappings::MapFn mapFn) { -#if JUCE_MODAL_LOOPS_PERMITTED - if (callbackIn == nullptr) { - jassert(mapFn != nullptr); - - OSXMessageBox messageBox(options, nullptr); - return mapFn(messageBox.getResult()); - } -#endif - - auto messageBox = std::make_unique( - options, AlertWindowMappings::getWrappedCallback(callbackIn, mapFn)); - - messageBox->triggerAsyncUpdate(); - messageBox.release(); - - return 0; -} - -#if JUCE_MODAL_LOOPS_PERMITTED -void JUCE_CALLTYPE NativeMessageBox::showMessageBox( - MessageBoxIconType iconType, const String &title, const String &message, - Component * /*associatedComponent*/) { - showDialog(MessageBoxOptions() - .withIconType(iconType) - .withTitle(title) - .withMessage(message) - .withButton(TRANS("OK")), - nullptr, AlertWindowMappings::messageBox); -} - -int JUCE_CALLTYPE NativeMessageBox::show(const MessageBoxOptions &options) { - return showDialog(options, nullptr, AlertWindowMappings::noMapping); -} -#endif - -void JUCE_CALLTYPE NativeMessageBox::showMessageBoxAsync( - MessageBoxIconType iconType, const String &title, const String &message, - Component * /*associatedComponent*/, - ModalComponentManager::Callback *callback) { - showDialog(MessageBoxOptions() - .withIconType(iconType) - .withTitle(title) - .withMessage(message) - .withButton(TRANS("OK")), - callback, AlertWindowMappings::messageBox); -} - -bool JUCE_CALLTYPE NativeMessageBox::showOkCancelBox( - MessageBoxIconType iconType, const String &title, const String &message, - Component * /*associatedComponent*/, - ModalComponentManager::Callback *callback) { - return showDialog(MessageBoxOptions() - .withIconType(iconType) - .withTitle(title) - .withMessage(message) - .withButton(TRANS("OK")) - .withButton(TRANS("Cancel")), - callback, AlertWindowMappings::okCancel) != 0; -} - -int JUCE_CALLTYPE NativeMessageBox::showYesNoCancelBox( - MessageBoxIconType iconType, const String &title, const String &message, - Component * /*associatedComponent*/, - ModalComponentManager::Callback *callback) { - return showDialog(MessageBoxOptions() - .withIconType(iconType) - .withTitle(title) - .withMessage(message) - .withButton(TRANS("Yes")) - .withButton(TRANS("No")) - .withButton(TRANS("Cancel")), - callback, AlertWindowMappings::yesNoCancel); -} - -int JUCE_CALLTYPE NativeMessageBox::showYesNoBox( - MessageBoxIconType iconType, const String &title, const String &message, - Component * /*associatedComponent*/, - ModalComponentManager::Callback *callback) { - return showDialog(MessageBoxOptions() - .withIconType(iconType) - .withTitle(title) - .withMessage(message) - .withButton(TRANS("Yes")) - .withButton(TRANS("No")), - callback, AlertWindowMappings::okCancel); -} - -void JUCE_CALLTYPE -NativeMessageBox::showAsync(const MessageBoxOptions &options, - ModalComponentManager::Callback *callback) { - showDialog(options, callback, AlertWindowMappings::noMapping); -} - -void JUCE_CALLTYPE NativeMessageBox::showAsync( - const MessageBoxOptions &options, std::function callback) { - showAsync(options, ModalCallbackFunction::create(callback)); -} - //============================================================================== static NSRect getDragRect(NSView *view, NSEvent *event) { auto eventPos = [event locationInWindow]; @@ -216,27 +49,9 @@ static NSRect getDragRect(NSView *view, NSEvent *event) { return nil; } -struct NSDraggingSourceHelper : public ObjCClass> { - NSDraggingSourceHelper() - : ObjCClass>("JUCENSDraggingSourceHelper_") { - addIvar *>("callback"); - addIvar("text"); - addIvar("operation"); - - addMethod(@selector(dealloc), dealloc); - addMethod(@selector(pasteboard:item:provideDataForType:), - provideDataForType); - - addMethod(@selector(draggingSession:sourceOperationMaskForDraggingContext:), - sourceOperationMaskForDraggingContext); - addMethod(@selector(draggingSession:endedAtPoint:operation:), - draggingSessionEnded); - - addProtocol(@protocol(NSPasteboardItemDataProvider)); - - registerClass(); - } - +class NSDraggingSourceHelper final + : public ObjCClass> { +public: static void setText(id self, const String &text) { object_setInstanceVariable(self, "text", new String(text)); } @@ -249,48 +64,62 @@ static void setDragOperation(id self, NSDragOperation op) { object_setInstanceVariable(self, "operation", new NSDragOperation(op)); } + static NSDraggingSourceHelper &get() { + static NSDraggingSourceHelper draggingSourceHelper; + return draggingSourceHelper; + } + private: - static void dealloc(id self, SEL) { - delete getIvar(self, "text"); - delete getIvar *>(self, "callback"); - delete getIvar(self, "operation"); + NSDraggingSourceHelper() : ObjCClass("JUCENSDraggingSourceHelper_") { + addIvar *>("callback"); + addIvar("text"); + addIvar("operation"); - sendSuperclassMessage(self, @selector(dealloc)); - } + addMethod(@selector(dealloc), [](id self, SEL) { + delete getIvar(self, "text"); + delete getIvar *>(self, "callback"); + delete getIvar(self, "operation"); - static void provideDataForType(id self, SEL, NSPasteboard *sender, - NSPasteboardItem *, NSString *type) { - if ([type compare:NSPasteboardTypeString] == NSOrderedSame) - if (auto *text = getIvar(self, "text")) - [sender setData:[juceStringToNS(*text) - dataUsingEncoding:NSUTF8StringEncoding] - forType:NSPasteboardTypeString]; - } + sendSuperclassMessage(self, @selector(dealloc)); + }); - static NSDragOperation - sourceOperationMaskForDraggingContext(id self, SEL, NSDraggingSession *, - NSDraggingContext) { - return *getIvar(self, "operation"); - } + addMethod(@selector(pasteboard:item:provideDataForType:), + [](id self, SEL, NSPasteboard *sender, NSPasteboardItem *, + NSString *type) { + if ([type compare:NSPasteboardTypeString] == NSOrderedSame) + if (auto *text = getIvar(self, "text")) + [sender setData:[juceStringToNS(*text) + dataUsingEncoding:NSUTF8StringEncoding] + forType:NSPasteboardTypeString]; + }); - static void draggingSessionEnded(id self, SEL, NSDraggingSession *, NSPoint p, - NSDragOperation) { - // Our view doesn't receive a mouse up when the drag ends so we need to - // generate one here and send it... - if (auto *view = getNSViewForDragEvent(nullptr)) - if (auto *cgEvent = CGEventCreateMouseEvent(nullptr, kCGEventLeftMouseUp, - CGPointMake(p.x, p.y), - kCGMouseButtonLeft)) - if (id e = [NSEvent eventWithCGEvent:cgEvent]) - [view mouseUp:e]; - - if (auto *cb = getIvar *>(self, "callback")) - cb->operator()(); + addMethod(@selector(draggingSession:sourceOperationMaskForDraggingContext:), + [](id self, SEL, NSDraggingSession *, NSDraggingContext) { + return *getIvar(self, "operation"); + }); + + addMethod( + @selector(draggingSession:endedAtPoint:operation:), + [](id self, SEL, NSDraggingSession *, NSPoint p, NSDragOperation) { + // Our view doesn't receive a mouse up when the drag ends so we need + // to generate one here and send it... + if (auto *view = getNSViewForDragEvent(nullptr)) + if (auto *cgEvent = CGEventCreateMouseEvent( + nullptr, kCGEventLeftMouseUp, CGPointMake(p.x, p.y), + kCGMouseButtonLeft)) + if (id e = [NSEvent eventWithCGEvent:cgEvent]) + [view mouseUp:e]; + + if (auto *cb = getIvar *>(self, "callback")) + cb->operator()(); + }); + + addProtocol(@protocol(NSPasteboardItemDataProvider)); + + registerClass(); } }; -static NSDraggingSourceHelper draggingSourceHelper; - bool DragAndDropContainer::performExternalDragDropOfText( const String &text, Component *sourceComponent, std::function callback) { @@ -300,7 +129,7 @@ static void draggingSessionEnded(id self, SEL, NSDraggingSession *, NSPoint p, if (auto *view = getNSViewForDragEvent(sourceComponent)) { JUCE_AUTORELEASEPOOL { if (auto event = [[view window] currentEvent]) { - id helper = [draggingSourceHelper.createInstance() init]; + id helper = [NSDraggingSourceHelper::get().createInstance() init]; NSDraggingSourceHelper::setText(helper, text); NSDraggingSourceHelper::setDragOperation(helper, NSDragOperationCopy); @@ -320,15 +149,17 @@ static void draggingSessionEnded(id self, SEL, NSDraggingSession *, NSPoint p, [[NSWorkspace sharedWorkspace] iconForFile:nsEmptyString()]; [dragItem setDraggingFrame:getDragRect(view, event) contents:image]; + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wnullable-to-nonnull-conversion") if (auto session = [view beginDraggingSessionWithItems:[NSArray arrayWithObject:dragItem] event:event - source:helper]) { - session.animatesToStartingPositionsOnCancelOrFail = YES; - session.draggingFormation = NSDraggingFormationNone; + source:helper]) + JUCE_END_IGNORE_WARNINGS_GCC_LIKE { + session.animatesToStartingPositionsOnCancelOrFail = YES; + session.draggingFormation = NSDraggingFormationNone; - return true; - } + return true; + } } } } @@ -366,7 +197,8 @@ static void draggingSessionEnded(id self, SEL, NSDraggingSession *, NSPoint p, [dragItem release]; } - auto helper = [draggingSourceHelper.createInstance() autorelease]; + auto helper = + [NSDraggingSourceHelper::get().createInstance() autorelease]; if (callback != nullptr) NSDraggingSourceHelper::setCompletionCallback(helper, callback); @@ -374,9 +206,11 @@ static void draggingSessionEnded(id self, SEL, NSDraggingSession *, NSPoint p, NSDraggingSourceHelper::setDragOperation( helper, canMoveFiles ? NSDragOperationMove : NSDragOperationCopy); + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wnullable-to-nonnull-conversion") return [view beginDraggingSessionWithItems:dragItems event:event source:helper] != nullptr; + JUCE_END_IGNORE_WARNINGS_GCC_LIKE } } } @@ -394,12 +228,39 @@ static void draggingSessionEnded(id self, SEL, NSDraggingSession *, NSPoint p, } } +static ComponentPeer *findPeerContainingPoint(Point globalPos) { + for (int i = 0; i < juce::ComponentPeer::getNumPeers(); ++i) { + auto *peer = juce::ComponentPeer::getPeer(i); + + if (peer->contains(peer->globalToLocal(globalPos).toInt(), false)) + return peer; + } + + return nullptr; +} + void MouseInputSource::setRawMousePosition(Point newPosition) { + const auto oldPosition = + Desktop::getInstance().getMainMouseSource().getRawScreenPosition(); + // this rubbish needs to be done around the warp call, to avoid causing a // bizarre glitch.. CGAssociateMouseAndMouseCursorPosition(false); CGWarpMouseCursorPosition(convertToCGPoint(newPosition)); CGAssociateMouseAndMouseCursorPosition(true); + + // Mouse enter and exit events seem to be always generated as a consequence of + // programmatically moving the mouse. However, when the mouse stays within the + // same peer no mouse move event is generated, and we lose track of the + // correct Component under the mouse. Hence, we need to generate this missing + // event here. + if (auto *peer = findPeerContainingPoint(newPosition); + peer != nullptr && peer == findPeerContainingPoint(oldPosition)) { + peer->handleMouseEvent(MouseInputSource::InputSourceType::mouse, + peer->globalToLocal(newPosition), + ModifierKeys::currentModifiers, 0.0f, 0.0f, + Time::currentTimeMillis()); + } } double Desktop::getDefaultMasterScale() { return 1.0; } @@ -414,51 +275,34 @@ static void draggingSessionEnded(id self, SEL, NSDraggingSession *, NSPoint p, isEqualToString:nsStringLiteral("Dark")]; } +JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wundeclared-selector") +static const auto darkModeSelector = @selector(darkModeChanged:); +static const auto keyboardVisibilitySelector = + @selector(keyboardVisiblityChanged:); +JUCE_END_IGNORE_WARNINGS_GCC_LIKE + class Desktop::NativeDarkModeChangeDetectorImpl { public: NativeDarkModeChangeDetectorImpl() { static DelegateClass delegateClass; - - delegate = [delegateClass.createInstance() init]; - object_setInstanceVariable(delegate, "owner", this); - - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wundeclared-selector") - [[NSDistributedNotificationCenter defaultCenter] - addObserver:delegate - selector:@selector(darkModeChanged:) - name:@"AppleInterfaceThemeChangedNotification" - object:nil]; - JUCE_END_IGNORE_WARNINGS_GCC_LIKE - } - - ~NativeDarkModeChangeDetectorImpl() { - object_setInstanceVariable(delegate, "owner", nullptr); - [[NSDistributedNotificationCenter defaultCenter] removeObserver:delegate]; - [delegate release]; + delegate.reset([delegateClass.createInstance() init]); + observer.emplace(delegate.get(), darkModeSelector, + @"AppleInterfaceThemeChangedNotification", nil, + [NSDistributedNotificationCenter class]); } - void darkModeChanged() { Desktop::getInstance().darkModeChanged(); } - private: - struct DelegateClass : public ObjCClass { + struct DelegateClass final : public ObjCClass { DelegateClass() : ObjCClass("JUCEDelegate_") { - addIvar("owner"); - - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wundeclared-selector") - addMethod(@selector(darkModeChanged:), darkModeChanged); - JUCE_END_IGNORE_WARNINGS_GCC_LIKE - + addMethod(darkModeSelector, [](id, SEL, NSNotification *) { + Desktop::getInstance().darkModeChanged(); + }); registerClass(); } - - static void darkModeChanged(id self, SEL, NSNotification *) { - if (auto *owner = - getIvar(self, "owner")) - owner->darkModeChanged(); - } }; - id delegate = nil; + NSUniquePtr delegate; + Optional observer; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(NativeDarkModeChangeDetectorImpl) }; @@ -469,7 +313,7 @@ static void darkModeChanged(id self, SEL, NSNotification *) { } //============================================================================== -class ScreenSaverDefeater : public Timer { +class ScreenSaverDefeater final : public Timer { public: ScreenSaverDefeater() { startTimer(5000); @@ -487,11 +331,10 @@ void timerCallback() override { struct PMAssertion { PMAssertion() : assertionID(kIOPMNullAssertionID) { - IOReturn res = IOPMAssertionCreateWithName( + [[maybe_unused]] IOReturn res = IOPMAssertionCreateWithName( kIOPMAssertionTypePreventUserIdleDisplaySleep, kIOPMAssertionLevelOn, CFSTR("JUCE Playback"), &assertionID); jassert(res == kIOReturnSuccess); - ignoreUnused(res); } ~PMAssertion() { @@ -517,7 +360,7 @@ void timerCallback() override { bool Desktop::isScreenSaverEnabled() { return screenSaverDefeater == nullptr; } //============================================================================== -struct DisplaySettingsChangeCallback : private DeletedAtShutdown { +struct DisplaySettingsChangeCallback final : private DeletedAtShutdown { DisplaySettingsChangeCallback() { CGDisplayRegisterReconfigurationCallback(displayReconfigurationCallback, this); @@ -533,8 +376,7 @@ static void displayReconfigurationCallback(CGDirectDisplayID, CGDisplayChangeSummaryFlags, void *userInfo) { if (auto *thisPtr = static_cast(userInfo)) - if (thisPtr->forceDisplayUpdate != nullptr) - thisPtr->forceDisplayUpdate(); + NullCheckedInvocation::invoke(thisPtr->forceDisplayUpdate); } std::function forceDisplayUpdate; @@ -555,7 +397,7 @@ static void displayReconfigurationCallback(CGDirectDisplayID, const float masterScale) { Displays::Display d; - d.isMain = (mainScreenBottom == 0); + d.isMain = (approximatelyEqual(mainScreenBottom, 0.0)); if (d.isMain) mainScreenBottom = [s frame].size.height; @@ -572,6 +414,17 @@ static void displayReconfigurationCallback(CGDirectDisplayID, [[[s deviceDescription] objectForKey:NSDeviceResolution] sizeValue]; d.dpi = (dpi.width + dpi.height) / 2.0; +#if defined(MAC_OS_VERSION_12_0) && \ + MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_12_0 + if (@available(macOS 12.0, *)) { + const auto safeInsets = [s safeAreaInsets]; + d.safeAreaInsets = detail::WindowingHelpers::roundToInt( + BorderSize{safeInsets.top, safeInsets.left, safeInsets.bottom, + safeInsets.right} + .multipliedBy(1.0 / (double)masterScale)); + } +#endif + return d; } @@ -588,15 +441,6 @@ static void displayReconfigurationCallback(CGDirectDisplayID, } } -//============================================================================== -bool juce_areThereAnyAlwaysOnTopWindows() { - for (NSWindow *window in [NSApp windows]) - if ([window level] > NSNormalWindowLevel) - return true; - - return false; -} - //============================================================================== static void selectImageForDrawing(const Image &image) { [NSGraphicsContext saveGraphicsState]; @@ -624,7 +468,7 @@ static void releaseImageAfterDrawing() { [NSGraphicsContext restoreGraphicsState]; } -Image juce_createIconForFile(const File &file) { +Image detail::WindowingHelpers::createIconForFile(const File &file) { JUCE_AUTORELEASEPOOL { NSImage *image = [[NSWorkspace sharedWorkspace] iconForFile:juceStringToNS(file.getFullPathName())]; @@ -672,14 +516,4 @@ Image createSnapshotOfNativeWindow(void *nativeWindowHandle) { [[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]); } -void Process::setDockIconVisible(bool isVisible) { - ProcessSerialNumber psn{0, kCurrentProcess}; - - OSStatus err = TransformProcessType( - &psn, isVisible ? kProcessTransformToForegroundApplication - : kProcessTransformToUIElementApplication); - jassert(err == 0); - ignoreUnused(err); -} - } // namespace juce diff --git a/pedalboard/juce_overrides/juce_patched_gui_basics.cpp b/pedalboard/juce_overrides/juce_patched_gui_basics.cpp index de62e0935..ca8704c3e 100644 --- a/pedalboard/juce_overrides/juce_patched_gui_basics.cpp +++ b/pedalboard/juce_overrides/juce_patched_gui_basics.cpp @@ -2,15 +2,15 @@ ============================================================================== This file is part of the JUCE library. - Copyright (c) 2020 - Raw Material Software Limited + 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 6 End-User License - Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + 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-6-licence + 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 @@ -23,6 +23,8 @@ ============================================================================== */ +// clang-format off +// (JUCE's include order below is significant; don't let clang-format sort it.) #ifdef JUCE_GUI_BASICS_H_INCLUDED /* When you add this cpp file to your project, you mustn't include it in a file where you've already included any other headers - just put it inside a file @@ -44,40 +46,46 @@ #define JUCE_GUI_BASICS_INCLUDE_XHEADERS 1 #define JUCE_GUI_BASICS_INCLUDE_SCOPED_THREAD_DPI_AWARENESS_SETTER 1 -// Avoid error: ā€˜exchange’ is not a member of ā€˜std’: +// Avoid error: 'exchange' is not a member of 'std': #include #include "../JUCE/modules/juce_gui_basics/juce_gui_basics.h" +#include + //============================================================================== #if JUCE_MAC #import -#import - -#if JUCE_SUPPORT_CARBON -#import // still needed for SetSystemUIMode() -#endif +#import #elif JUCE_IOS -#if JUCE_PUSH_NOTIFICATIONS && defined(__IPHONE_10_0) && \ - __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 +#if JUCE_PUSH_NOTIFICATIONS #import #endif +#import #import //============================================================================== #elif JUCE_WINDOWS -#include -#include -#include #include - -#if !JUCE_MINGW -#include +#include +#include +#include #include +#include + +#if JUCE_MINGW +// Some MinGW headers use 'new' as a parameter name +JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wkeyword-macro") +#define new new_ +JUCE_END_IGNORE_WARNINGS_GCC_LIKE #endif +#include + +#undef new + #if JUCE_WEB_BROWSER #include #include @@ -89,6 +97,7 @@ #pragma comment(lib, "vfw32.lib") #pragma comment(lib, "imm32.lib") #pragma comment(lib, "comctl32.lib") +#pragma comment(lib, "dxgi.lib") #if JUCE_OPENGL #pragma comment(lib, "OpenGL32.Lib") @@ -102,489 +111,255 @@ #endif #endif -#include - //============================================================================== -#define JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED_OR_OFFSCREEN \ - jassert((MessageManager::getInstanceWithoutCreating() != nullptr && \ - MessageManager::getInstanceWithoutCreating() \ - ->currentThreadHasLockedMessageManager()) || \ - getPeer() == nullptr); - -namespace juce { -bool juce_areThereAnyAlwaysOnTopWindows(); - -bool isEmbeddedInForegroundProcess(Component *c); +#include "../JUCE/modules/juce_gui_basics/detail/juce_AccessibilityHelpers.h" +#include "../JUCE/modules/juce_gui_basics/detail/juce_ButtonAccessibilityHandler.h" +#include "../JUCE/modules/juce_gui_basics/detail/juce_ScalingHelpers.h" +#include "../JUCE/modules/juce_gui_basics/detail/juce_ComponentHelpers.h" +#include "../JUCE/modules/juce_gui_basics/detail/juce_FocusHelpers.h" +#include "../JUCE/modules/juce_gui_basics/detail/juce_FocusRestorer.h" +#include "../JUCE/modules/juce_gui_basics/detail/juce_ViewportHelpers.h" +#include "../JUCE/modules/juce_gui_basics/detail/juce_LookAndFeelHelpers.h" +#include "../JUCE/modules/juce_gui_basics/detail/juce_PointerState.h" +#include "../JUCE/modules/juce_gui_basics/detail/juce_CustomMouseCursorInfo.h" +#include "../JUCE/modules/juce_gui_basics/detail/juce_MouseInputSourceImpl.h" +#include "../JUCE/modules/juce_gui_basics/detail/juce_MouseInputSourceList.h" +#include "../JUCE/modules/juce_gui_basics/detail/juce_ToolbarItemDragAndDropOverlayComponent.h" +#include "../JUCE/modules/juce_gui_basics/detail/juce_ScopedMessageBoxInterface.h" +#include "../JUCE/modules/juce_gui_basics/detail/juce_ScopedMessageBoxImpl.h" +#include "../JUCE/modules/juce_gui_basics/detail/juce_ScopedContentSharerInterface.h" +#include "../JUCE/modules/juce_gui_basics/detail/juce_ScopedContentSharerImpl.h" +#include "../JUCE/modules/juce_gui_basics/detail/juce_WindowingHelpers.h" +#include "../JUCE/modules/juce_gui_basics/detail/juce_AlertWindowHelpers.h" +#include "../JUCE/modules/juce_gui_basics/detail/juce_TopLevelWindowManager.h" -#if !JUCE_WINDOWS -bool isEmbeddedInForegroundProcess(Component *) { return false; } +//============================================================================== +#if JUCE_IOS || JUCE_WINDOWS +#include "../JUCE/modules/juce_gui_basics/native/juce_MultiTouchMapper.h" #endif -/* Returns true if this process is in the foreground, or if the viewComponent - is embedded into a window owned by the foreground process. -*/ -static bool isForegroundOrEmbeddedProcess(Component *viewComponent) { - return Process::isForegroundProcess() || - isEmbeddedInForegroundProcess(viewComponent); -} - -bool isWindowOnCurrentVirtualDesktop(void *); - -struct CustomMouseCursorInfo { - ScaledImage image; - Point hotspot; -}; -} // namespace juce - -#include "../JUCE/modules/juce_gui_basics/accessibility/juce_AccessibilityHandler.cpp" +#if JUCE_ANDROID || JUCE_WINDOWS || JUCE_IOS || JUCE_UNIT_TESTS +#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_AccessibilityTextHelpers.h" +#endif -#include "../JUCE/modules/juce_gui_basics/components/juce_Component.cpp" +#if JUCE_MAC || JUCE_IOS +#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_AccessibilitySharedCode_mac.mm" +#include "../JUCE/modules/juce_gui_basics/native/juce_CGMetalLayerRenderer_mac.h" -#include "../JUCE/modules/juce_gui_basics/components/juce_ComponentListener.cpp" +#if JUCE_IOS +#include "../JUCE/modules/juce_gui_basics/native/juce_UIViewComponentPeer_ios.mm" +#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_Accessibility_ios.mm" +#include "../JUCE/modules/juce_gui_basics/native/juce_WindowUtils_ios.mm" +#include "../JUCE/modules/juce_gui_basics/native/juce_Windowing_ios.mm" +#include "../JUCE/modules/juce_gui_basics/native/juce_NativeMessageBox_ios.mm" +#include "../JUCE/modules/juce_gui_basics/native/juce_NativeModalWrapperComponent_ios.h" +#include "../JUCE/modules/juce_gui_basics/native/juce_FileChooser_ios.mm" -#include "../JUCE/modules/juce_gui_basics/components/juce_FocusTraverser.cpp" +#if JUCE_CONTENT_SHARING +#include "../JUCE/modules/juce_gui_basics/native/juce_ContentSharer_ios.cpp" +#endif -#include "../JUCE/modules/juce_gui_basics/mouse/juce_MouseInputSource.cpp" +#else +#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_Accessibility_mac.mm" +#include "../JUCE/modules/juce_gui_basics/native/juce_PerScreenDisplayLinks_mac.h" +#include "../JUCE/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm" +#include "../JUCE/modules/juce_gui_basics/native/juce_WindowUtils_mac.mm" +#include "juce_mac_PatchedWindowing.mm" +#include "../JUCE/modules/juce_gui_basics/native/juce_NativeMessageBox_mac.mm" +#include "../JUCE/modules/juce_gui_basics/native/juce_MainMenu_mac.mm" +#include "../JUCE/modules/juce_gui_basics/native/juce_FileChooser_mac.mm" +#endif -#include "../JUCE/modules/juce_gui_basics/desktop/juce_Displays.cpp" +#include "../JUCE/modules/juce_gui_basics/native/juce_MouseCursor_mac.mm" -#include "../JUCE/modules/juce_gui_basics/desktop/juce_Desktop.cpp" +#elif JUCE_WINDOWS +#if JUCE_MODULE_AVAILABLE_juce_audio_plugin_client +#include +#endif +#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_ComInterfaces_windows.h" +#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_WindowsUIAWrapper_windows.h" +#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_AccessibilityElement_windows.h" +#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_UIAHelpers_windows.h" +#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_UIAProviders_windows.h" +#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_AccessibilityElement_windows.cpp" +#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_Accessibility_windows.cpp" +#include "../JUCE/modules/juce_gui_basics/native/juce_WindowsHooks_windows.h" +#include "../JUCE/modules/juce_gui_basics/native/juce_WindowUtils_windows.cpp" +#include "../JUCE/modules/juce_gui_basics/native/juce_Windowing_windows.cpp" +#include "../JUCE/modules/juce_gui_basics/native/juce_WindowsHooks_windows.cpp" +#include "../JUCE/modules/juce_gui_basics/native/juce_NativeMessageBox_windows.cpp" +#include "../JUCE/modules/juce_gui_basics/native/juce_DragAndDrop_windows.cpp" +#include "../JUCE/modules/juce_gui_basics/native/juce_FileChooser_windows.cpp" -#include "../JUCE/modules/juce_gui_basics/components/juce_ModalComponentManager.cpp" +#elif JUCE_LINUX || JUCE_BSD +#include "../JUCE/modules/juce_gui_basics/native/juce_XSymbols_linux.cpp" +#include "../JUCE/modules/juce_gui_basics/native/juce_DragAndDrop_linux.cpp" -#include "../JUCE/modules/juce_gui_basics/mouse/juce_ComponentDragger.cpp" +JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wzero-as-null-pointer-constant") -#include "../JUCE/modules/juce_gui_basics/mouse/juce_DragAndDropContainer.cpp" +#include "../JUCE/modules/juce_gui_basics/native/juce_ScopedWindowAssociation_linux.h" +#include "../JUCE/modules/juce_gui_basics/native/juce_WindowUtils_linux.cpp" +#include "../JUCE/modules/juce_gui_basics/native/juce_Windowing_linux.cpp" +#include "../JUCE/modules/juce_gui_basics/native/juce_NativeMessageBox_linux.cpp" +#include "../JUCE/modules/juce_gui_basics/native/juce_XWindowSystem_linux.cpp" -#include "../JUCE/modules/juce_gui_basics/mouse/juce_MouseEvent.cpp" +JUCE_END_IGNORE_WARNINGS_GCC_LIKE -#include "../JUCE/modules/juce_gui_basics/mouse/juce_MouseInactivityDetector.cpp" +#include "../JUCE/modules/juce_gui_basics/native/juce_FileChooser_linux.cpp" -#include "../JUCE/modules/juce_gui_basics/mouse/juce_MouseListener.cpp" +#elif JUCE_ANDROID -#include "../JUCE/modules/juce_gui_basics/keyboard/juce_CaretComponent.cpp" +#include "../JUCE/modules/juce_gui_basics/juce_core/files/juce_common_MimeTypes.h" +#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_Accessibility_android.cpp" +#include "../JUCE/modules/juce_gui_basics/native/juce_WindowUtils_android.cpp" +#include "../JUCE/modules/juce_gui_basics/native/juce_Windowing_android.cpp" +#include "../JUCE/modules/juce_gui_basics/native/juce_NativeMessageBox_android.cpp" +#include "../JUCE/modules/juce_gui_basics/native/juce_FileChooser_android.cpp" -#include "../JUCE/modules/juce_gui_basics/keyboard/juce_KeyboardFocusTraverser.cpp" +#if JUCE_CONTENT_SHARING +#include "../JUCE/modules/juce_gui_basics/native/juce_ContentSharer_android.cpp" +#endif -#include "../JUCE/modules/juce_gui_basics/keyboard/juce_KeyListener.cpp" +#endif -#include "../JUCE/modules/juce_gui_basics/keyboard/juce_KeyPress.cpp" +//============================================================================== +// Depends on types defined in platform-specific windowing files +#include "../JUCE/modules/juce_gui_basics/mouse/juce_MouseCursor.cpp" -#include "../JUCE/modules/juce_gui_basics/keyboard/juce_ModifierKeys.cpp" +#if JUCE_UNIT_TESTS +#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_AccessibilityTextHelpers_test.cpp" +#endif +//============================================================================== +#include "../JUCE/modules/juce_gui_basics/accessibility/juce_AccessibilityHandler.cpp" +#include "../JUCE/modules/juce_gui_basics/application/juce_Application.cpp" #include "../JUCE/modules/juce_gui_basics/buttons/juce_ArrowButton.cpp" - #include "../JUCE/modules/juce_gui_basics/buttons/juce_Button.cpp" - #include "../JUCE/modules/juce_gui_basics/buttons/juce_DrawableButton.cpp" - #include "../JUCE/modules/juce_gui_basics/buttons/juce_HyperlinkButton.cpp" - #include "../JUCE/modules/juce_gui_basics/buttons/juce_ImageButton.cpp" - #include "../JUCE/modules/juce_gui_basics/buttons/juce_ShapeButton.cpp" - #include "../JUCE/modules/juce_gui_basics/buttons/juce_TextButton.cpp" - #include "../JUCE/modules/juce_gui_basics/buttons/juce_ToggleButton.cpp" - #include "../JUCE/modules/juce_gui_basics/buttons/juce_ToolbarButton.cpp" - +#include "../JUCE/modules/juce_gui_basics/commands/juce_ApplicationCommandInfo.cpp" +#include "../JUCE/modules/juce_gui_basics/commands/juce_ApplicationCommandManager.cpp" +#include "../JUCE/modules/juce_gui_basics/commands/juce_ApplicationCommandTarget.cpp" +#include "../JUCE/modules/juce_gui_basics/commands/juce_KeyPressMappingSet.cpp" +#include "../JUCE/modules/juce_gui_basics/components/juce_Component.cpp" +#include "../JUCE/modules/juce_gui_basics/components/juce_ComponentListener.cpp" +#include "../JUCE/modules/juce_gui_basics/components/juce_FocusTraverser.cpp" +#include "../JUCE/modules/juce_gui_basics/components/juce_ModalComponentManager.cpp" +#include "../JUCE/modules/juce_gui_basics/desktop/juce_Desktop.cpp" +#include "../JUCE/modules/juce_gui_basics/desktop/juce_Displays.cpp" +#include "../JUCE/modules/juce_gui_basics/detail/juce_AccessibilityHelpers.cpp" #include "../JUCE/modules/juce_gui_basics/drawables/juce_Drawable.cpp" - #include "../JUCE/modules/juce_gui_basics/drawables/juce_DrawableComposite.cpp" - #include "../JUCE/modules/juce_gui_basics/drawables/juce_DrawableImage.cpp" - #include "../JUCE/modules/juce_gui_basics/drawables/juce_DrawablePath.cpp" - #include "../JUCE/modules/juce_gui_basics/drawables/juce_DrawableRectangle.cpp" - #include "../JUCE/modules/juce_gui_basics/drawables/juce_DrawableShape.cpp" - #include "../JUCE/modules/juce_gui_basics/drawables/juce_DrawableText.cpp" - #include "../JUCE/modules/juce_gui_basics/drawables/juce_SVGParser.cpp" - +#include "../JUCE/modules/juce_gui_basics/filebrowser/juce_ContentSharer.cpp" #include "../JUCE/modules/juce_gui_basics/filebrowser/juce_DirectoryContentsDisplayComponent.cpp" - #include "../JUCE/modules/juce_gui_basics/filebrowser/juce_DirectoryContentsList.cpp" - #include "../JUCE/modules/juce_gui_basics/filebrowser/juce_FileBrowserComponent.cpp" - #include "../JUCE/modules/juce_gui_basics/filebrowser/juce_FileChooser.cpp" - #include "../JUCE/modules/juce_gui_basics/filebrowser/juce_FileChooserDialogBox.cpp" - #include "../JUCE/modules/juce_gui_basics/filebrowser/juce_FileListComponent.cpp" - -#include "../JUCE/modules/juce_gui_basics/filebrowser/juce_FilenameComponent.cpp" - #include "../JUCE/modules/juce_gui_basics/filebrowser/juce_FileSearchPathListComponent.cpp" - #include "../JUCE/modules/juce_gui_basics/filebrowser/juce_FileTreeComponent.cpp" - +#include "../JUCE/modules/juce_gui_basics/filebrowser/juce_FilenameComponent.cpp" #include "../JUCE/modules/juce_gui_basics/filebrowser/juce_ImagePreviewComponent.cpp" - -#include "../JUCE/modules/juce_gui_basics/filebrowser/juce_ContentSharer.cpp" - +#include "../JUCE/modules/juce_gui_basics/keyboard/juce_CaretComponent.cpp" +#include "../JUCE/modules/juce_gui_basics/keyboard/juce_KeyListener.cpp" +#include "../JUCE/modules/juce_gui_basics/keyboard/juce_KeyPress.cpp" +#include "../JUCE/modules/juce_gui_basics/keyboard/juce_KeyboardFocusTraverser.cpp" +#include "../JUCE/modules/juce_gui_basics/keyboard/juce_ModifierKeys.cpp" #include "../JUCE/modules/juce_gui_basics/layout/juce_ComponentAnimator.cpp" - #include "../JUCE/modules/juce_gui_basics/layout/juce_ComponentBoundsConstrainer.cpp" - +#include "../JUCE/modules/juce_gui_basics/layout/juce_BorderedComponentBoundsConstrainer.cpp" #include "../JUCE/modules/juce_gui_basics/layout/juce_ComponentBuilder.cpp" - #include "../JUCE/modules/juce_gui_basics/layout/juce_ComponentMovementWatcher.cpp" - #include "../JUCE/modules/juce_gui_basics/layout/juce_ConcertinaPanel.cpp" - +#include "../JUCE/modules/juce_gui_basics/layout/juce_FlexBox.cpp" +#include "../JUCE/modules/juce_gui_basics/layout/juce_Grid.cpp" +#include "../JUCE/modules/juce_gui_basics/layout/juce_GridItem.cpp" #include "../JUCE/modules/juce_gui_basics/layout/juce_GroupComponent.cpp" - #include "../JUCE/modules/juce_gui_basics/layout/juce_MultiDocumentPanel.cpp" - #include "../JUCE/modules/juce_gui_basics/layout/juce_ResizableBorderComponent.cpp" - #include "../JUCE/modules/juce_gui_basics/layout/juce_ResizableCornerComponent.cpp" - #include "../JUCE/modules/juce_gui_basics/layout/juce_ResizableEdgeComponent.cpp" - #include "../JUCE/modules/juce_gui_basics/layout/juce_ScrollBar.cpp" - #include "../JUCE/modules/juce_gui_basics/layout/juce_SidePanel.cpp" - #include "../JUCE/modules/juce_gui_basics/layout/juce_StretchableLayoutManager.cpp" - #include "../JUCE/modules/juce_gui_basics/layout/juce_StretchableLayoutResizerBar.cpp" - #include "../JUCE/modules/juce_gui_basics/layout/juce_StretchableObjectResizer.cpp" - #include "../JUCE/modules/juce_gui_basics/layout/juce_TabbedButtonBar.cpp" - #include "../JUCE/modules/juce_gui_basics/layout/juce_TabbedComponent.cpp" - #include "../JUCE/modules/juce_gui_basics/layout/juce_Viewport.cpp" - #include "../JUCE/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel.cpp" - -#include "../JUCE/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V2.cpp" - #include "../JUCE/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V1.cpp" - +#include "../JUCE/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V2.cpp" #include "../JUCE/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V3.cpp" - #include "../JUCE/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V4.cpp" - -#include "../JUCE/modules/juce_gui_basics/menus/juce_MenuBarComponent.cpp" - #include "../JUCE/modules/juce_gui_basics/menus/juce_BurgerMenuComponent.cpp" - +#include "../JUCE/modules/juce_gui_basics/menus/juce_MenuBarComponent.cpp" #include "../JUCE/modules/juce_gui_basics/menus/juce_MenuBarModel.cpp" - #include "../JUCE/modules/juce_gui_basics/menus/juce_PopupMenu.cpp" - +#include "../JUCE/modules/juce_gui_basics/misc/juce_BubbleComponent.cpp" +#include "../JUCE/modules/juce_gui_basics/misc/juce_DropShadower.cpp" +#include "../JUCE/modules/juce_gui_basics/misc/juce_FocusOutline.cpp" +#include "../JUCE/modules/juce_gui_basics/misc/juce_JUCESplashScreen.cpp" +#include "../JUCE/modules/juce_gui_basics/mouse/juce_ComponentDragger.cpp" +#include "../JUCE/modules/juce_gui_basics/mouse/juce_DragAndDropContainer.cpp" +#include "../JUCE/modules/juce_gui_basics/mouse/juce_MouseEvent.cpp" +#include "../JUCE/modules/juce_gui_basics/mouse/juce_MouseInactivityDetector.cpp" +#include "../JUCE/modules/juce_gui_basics/mouse/juce_MouseInputSource.cpp" +#include "../JUCE/modules/juce_gui_basics/mouse/juce_MouseListener.cpp" +#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_Accessibility.cpp" +#include "../JUCE/modules/juce_gui_basics/native/juce_ScopedDPIAwarenessDisabler.cpp" #include "../JUCE/modules/juce_gui_basics/positioning/juce_MarkerList.cpp" - #include "../JUCE/modules/juce_gui_basics/positioning/juce_RelativeCoordinate.cpp" - #include "../JUCE/modules/juce_gui_basics/positioning/juce_RelativeCoordinatePositioner.cpp" - #include "../JUCE/modules/juce_gui_basics/positioning/juce_RelativeParallelogram.cpp" - #include "../JUCE/modules/juce_gui_basics/positioning/juce_RelativePoint.cpp" - #include "../JUCE/modules/juce_gui_basics/positioning/juce_RelativePointPath.cpp" - #include "../JUCE/modules/juce_gui_basics/positioning/juce_RelativeRectangle.cpp" - #include "../JUCE/modules/juce_gui_basics/properties/juce_BooleanPropertyComponent.cpp" - #include "../JUCE/modules/juce_gui_basics/properties/juce_ButtonPropertyComponent.cpp" - #include "../JUCE/modules/juce_gui_basics/properties/juce_ChoicePropertyComponent.cpp" - +#include "../JUCE/modules/juce_gui_basics/properties/juce_MultiChoicePropertyComponent.cpp" #include "../JUCE/modules/juce_gui_basics/properties/juce_PropertyComponent.cpp" - #include "../JUCE/modules/juce_gui_basics/properties/juce_PropertyPanel.cpp" - #include "../JUCE/modules/juce_gui_basics/properties/juce_SliderPropertyComponent.cpp" - #include "../JUCE/modules/juce_gui_basics/properties/juce_TextPropertyComponent.cpp" - -#include "../JUCE/modules/juce_gui_basics/properties/juce_MultiChoicePropertyComponent.cpp" - #include "../JUCE/modules/juce_gui_basics/widgets/juce_ComboBox.cpp" - #include "../JUCE/modules/juce_gui_basics/widgets/juce_ImageComponent.cpp" - #include "../JUCE/modules/juce_gui_basics/widgets/juce_Label.cpp" - #include "../JUCE/modules/juce_gui_basics/widgets/juce_ListBox.cpp" - #include "../JUCE/modules/juce_gui_basics/widgets/juce_ProgressBar.cpp" - #include "../JUCE/modules/juce_gui_basics/widgets/juce_Slider.cpp" - #include "../JUCE/modules/juce_gui_basics/widgets/juce_TableHeaderComponent.cpp" - #include "../JUCE/modules/juce_gui_basics/widgets/juce_TableListBox.cpp" - #include "../JUCE/modules/juce_gui_basics/widgets/juce_TextEditor.cpp" - -#include "../JUCE/modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.cpp" - #include "../JUCE/modules/juce_gui_basics/widgets/juce_Toolbar.cpp" - +#include "../JUCE/modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.cpp" #include "../JUCE/modules/juce_gui_basics/widgets/juce_ToolbarItemPalette.cpp" - #include "../JUCE/modules/juce_gui_basics/widgets/juce_TreeView.cpp" - +#include "../JUCE/modules/juce_gui_basics/windows/juce_NativeMessageBox.cpp" #include "../JUCE/modules/juce_gui_basics/windows/juce_AlertWindow.cpp" - #include "../JUCE/modules/juce_gui_basics/windows/juce_CallOutBox.cpp" - #include "../JUCE/modules/juce_gui_basics/windows/juce_ComponentPeer.cpp" - #include "../JUCE/modules/juce_gui_basics/windows/juce_DialogWindow.cpp" - #include "../JUCE/modules/juce_gui_basics/windows/juce_DocumentWindow.cpp" - +#include "../JUCE/modules/juce_gui_basics/windows/juce_MessageBoxOptions.cpp" #include "../JUCE/modules/juce_gui_basics/windows/juce_ResizableWindow.cpp" - +#include "../JUCE/modules/juce_gui_basics/windows/juce_ScopedMessageBox.cpp" #include "../JUCE/modules/juce_gui_basics/windows/juce_ThreadWithProgressWindow.cpp" - #include "../JUCE/modules/juce_gui_basics/windows/juce_TooltipWindow.cpp" - #include "../JUCE/modules/juce_gui_basics/windows/juce_TopLevelWindow.cpp" - -#include "../JUCE/modules/juce_gui_basics/commands/juce_ApplicationCommandInfo.cpp" - -#include "../JUCE/modules/juce_gui_basics/commands/juce_ApplicationCommandManager.cpp" - -#include "../JUCE/modules/juce_gui_basics/commands/juce_ApplicationCommandTarget.cpp" - -#include "../JUCE/modules/juce_gui_basics/commands/juce_KeyPressMappingSet.cpp" - -#include "../JUCE/modules/juce_gui_basics/application/juce_Application.cpp" - -#include "../JUCE/modules/juce_gui_basics/misc/juce_BubbleComponent.cpp" - -#include "../JUCE/modules/juce_gui_basics/misc/juce_DropShadower.cpp" - -#include "../JUCE/modules/juce_gui_basics/misc/juce_JUCESplashScreen.cpp" - -#include "../JUCE/modules/juce_gui_basics/layout/juce_FlexBox.cpp" -#include "../JUCE/modules/juce_gui_basics/layout/juce_Grid.cpp" -#include "../JUCE/modules/juce_gui_basics/layout/juce_GridItem.cpp" - -#if JUCE_IOS || JUCE_WINDOWS -#include "../JUCE/modules/juce_gui_basics/native/juce_MultiTouchMapper.h" -#endif - -#if JUCE_ANDROID || JUCE_WINDOWS -#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_AccessibilityTextHelpers.h" -#endif - -namespace juce { - -static const juce::Identifier disableAsyncLayerBackedViewIdentifier{ - "disableAsyncLayerBackedView"}; - -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wmissing-prototypes") - -/** Used by the macOS and iOS peers. */ -void setComponentAsyncLayerBackedViewDisabled( - juce::Component &comp, bool shouldDisableAsyncLayerBackedView) { - comp.getProperties().set(disableAsyncLayerBackedViewIdentifier, - shouldDisableAsyncLayerBackedView); -} - -/** Used by the macOS and iOS peers. */ -bool getComponentAsyncLayerBackedViewDisabled(juce::Component &comp) { - return comp.getProperties()[disableAsyncLayerBackedViewIdentifier]; -} - -JUCE_END_IGNORE_WARNINGS_GCC_LIKE - -} // namespace juce - -#if JUCE_MAC || JUCE_IOS -#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_mac_AccessibilitySharedCode.mm" - -#if JUCE_IOS -#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_ios_Accessibility.mm" - -#include "../JUCE/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm" - -#include "../JUCE/modules/juce_gui_basics/native/juce_ios_Windowing.mm" - -#include "../JUCE/modules/juce_gui_basics/native/juce_ios_FileChooser.mm" - -#if JUCE_CONTENT_SHARING -#include "../JUCE/modules/juce_gui_basics/native/juce_ios_ContentSharer.cpp" -#endif - -#else -#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_mac_Accessibility.mm" - -#include "../JUCE/modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm" - -#include "juce_mac_PatchedWindowing.mm" - -#include "../JUCE/modules/juce_gui_basics/native/juce_mac_MainMenu.mm" - -#include "../JUCE/modules/juce_gui_basics/native/juce_mac_FileChooser.mm" - -#endif - -#include "../JUCE/modules/juce_gui_basics/native/juce_mac_MouseCursor.mm" - -#elif JUCE_WINDOWS - -#if !JUCE_MINGW -#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_win32_WindowsUIAWrapper.h" - -#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_win32_AccessibilityElement.h" - -#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_win32_UIAHelpers.h" - -#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_win32_UIAProviders.h" - -#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_win32_AccessibilityElement.cpp" - -#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_win32_Accessibility.cpp" - -#else -namespace juce { -namespace WindowsAccessibility { -long getUiaRootObjectId() { return -1; } -bool handleWmGetObject(AccessibilityHandler *, WPARAM, LPARAM, LRESULT *) { - return false; -} -void revokeUIAMapEntriesForWindow(HWND) {} -} // namespace WindowsAccessibility -} // namespace juce -#endif - -#include "../JUCE/modules/juce_gui_basics/native/juce_win32_Windowing.cpp" - -#include "../JUCE/modules/juce_gui_basics/native/juce_win32_DragAndDrop.cpp" - -#include "../JUCE/modules/juce_gui_basics/native/juce_win32_FileChooser.cpp" - -#elif JUCE_LINUX || JUCE_BSD -#include "../JUCE/modules/juce_gui_basics/native/x11/juce_linux_X11_DragAndDrop.cpp" - -#include "../JUCE/modules/juce_gui_basics/native/x11/juce_linux_X11_Symbols.cpp" - -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wzero-as-null-pointer-constant") - -#include "../JUCE/modules/juce_gui_basics/native/juce_linux_Windowing.cpp" - -#include "../JUCE/modules/juce_gui_basics/native/x11/juce_linux_XWindowSystem.cpp" - -JUCE_END_IGNORE_WARNINGS_GCC_LIKE - -#include "../JUCE/modules/juce_gui_basics/native/juce_linux_FileChooser.cpp" - -#elif JUCE_ANDROID -#include "../JUCE/modules/juce_gui_basics/native/accessibility/juce_android_Accessibility.cpp" - -#include "../JUCE/modules/juce_gui_basics/native/juce_android_Windowing.cpp" - -#include "../JUCE/modules/juce_gui_basics/native/juce_common_MimeTypes.cpp" - -#include "../JUCE/modules/juce_gui_basics/native/juce_android_FileChooser.cpp" - -#if JUCE_CONTENT_SHARING -#include "../JUCE/modules/juce_gui_basics/native/juce_android_ContentSharer.cpp" -#endif - -#endif - -namespace juce { -#if !JUCE_NATIVE_ACCESSIBILITY_INCLUDED -class AccessibilityHandler::AccessibilityNativeImpl { -public: - AccessibilityNativeImpl(AccessibilityHandler &) {} -}; -void AccessibilityHandler::notifyAccessibilityEvent(AccessibilityEvent) const {} -void AccessibilityHandler::postAnnouncement(const String &, - AnnouncementPriority) {} -AccessibilityNativeHandle * -AccessibilityHandler::getNativeImplementation() const { - return nullptr; -} -void notifyAccessibilityEventInternal(const AccessibilityHandler &, - InternalAccessibilityEvent) {} -std::unique_ptr -AccessibilityHandler::createNativeImpl(AccessibilityHandler &) { - return nullptr; -} -#else -std::unique_ptr -AccessibilityHandler::createNativeImpl(AccessibilityHandler &handler) { - return std::make_unique(handler); -} -#endif -} // namespace juce - -//============================================================================== -#if JUCE_WINDOWS -bool juce::isWindowOnCurrentVirtualDesktop(void *x) { - if (x == nullptr) - return false; - - static auto *desktopManager = [] { - // IVirtualDesktopManager Copied from ShObjdl_core.h, because it may not be - // defined - MIDL_INTERFACE("a5cd92ff-29be-454c-8d04-d82879fb3f1b") - juce_IVirtualDesktopManager: - public - IUnknown { - public: - virtual HRESULT STDMETHODCALLTYPE IsWindowOnCurrentVirtualDesktop( - __RPC__in HWND topLevelWindow, - __RPC__out BOOL * onCurrentDesktop) = 0; - - virtual HRESULT STDMETHODCALLTYPE GetWindowDesktopId( - __RPC__in HWND topLevelWindow, __RPC__out GUID * desktopId) = 0; - - virtual HRESULT STDMETHODCALLTYPE MoveWindowToDesktop( - __RPC__in HWND topLevelWindow, __RPC__in REFGUID desktopId) = 0; - }; - - juce_IVirtualDesktopManager *result = nullptr; - - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wlanguage-extension-token") - - class DECLSPEC_UUID("aa509086-5ca9-4c25-8f95-589d3c07b48a") - juce_VirtualDesktopManager; - - if (SUCCEEDED(CoCreateInstance(__uuidof(juce_VirtualDesktopManager), - nullptr, CLSCTX_ALL, IID_PPV_ARGS(&result)))) - return result; - - JUCE_END_IGNORE_WARNINGS_GCC_LIKE - - return static_cast(nullptr); - }(); - - BOOL current = false; - - if (auto *dm = desktopManager) - if (SUCCEEDED(dm->IsWindowOnCurrentVirtualDesktop(static_cast(x), - ¤t))) - return current != false; - - return true; -} -#else -bool juce::isWindowOnCurrentVirtualDesktop(void *) { return true; } -juce::ScopedDPIAwarenessDisabler::ScopedDPIAwarenessDisabler() { - ignoreUnused(previousContext); -} -juce::ScopedDPIAwarenessDisabler::~ScopedDPIAwarenessDisabler() {} -#endif - -// Depends on types defined in platform-specific windowing files -#include "../JUCE/modules/juce_gui_basics/mouse/juce_MouseCursor.cpp" +#include "../JUCE/modules/juce_gui_basics/windows/juce_VBlankAttachment.cpp" +#include "../JUCE/modules/juce_gui_basics/windows/juce_NativeScaleFactorNotifier.cpp" +// clang-format on diff --git a/setup.py b/setup.py index 7e735d41b..af34f5c56 100644 --- a/setup.py +++ b/setup.py @@ -101,7 +101,6 @@ ALL_CPPFLAGS.extend( [ "-DUSE_BQRESAMPLER=1", - "-D_HAS_STD_BYTE=0", "-DNOMINMAX", "-DALREADY_CONFIGURED", ] @@ -320,6 +319,7 @@ def ignore_files_matching(files, *matches): "Foundation", "IOKit", "QuartzCore", + "Security", "WebKit", ] @@ -327,6 +327,9 @@ def ignore_files_matching(files, *matches): # for Objective-C++ files instead of C++ files. for f in MACOS_FRAMEWORKS: ALL_LINK_ARGS += ["-framework", f] + # JUCE 7's juce_gui_basics weakly links against these: + for f in ("Metal", "MetalKit"): + ALL_LINK_ARGS += ["-weak_framework", f] ALL_CPPFLAGS.append("-DJUCE_PLUGINHOST_AU=1") ALL_CPPFLAGS.append("-xobjective-c++") @@ -363,7 +366,7 @@ def ignore_files_matching(files, *matches): ALL_RESOLVED_SOURCE_PATHS = [str(p.resolve()) for p in ALL_SOURCE_PATHS] elif platform.system() == "Windows": - ALL_CPPFLAGS += ["-DJUCE_DLL_BUILD=1"] + ALL_CPPFLAGS += ["-DJUCE_DLL_BUILD=1", "-DJUCE_DISABLE_DLL_ALLOCATORS=1"] # https://forum.juce.com/t/statically-linked-exe-in-win-10-not-working/25574/3 ALL_LIBRARIES.extend( [ diff --git a/vendors/libFLAC/metadata_object.c b/vendors/libFLAC/metadata_object.c index 8e9df72ba..0ce452610 100644 --- a/vendors/libFLAC/metadata_object.c +++ b/vendors/libFLAC/metadata_object.c @@ -38,7 +38,7 @@ #include #include "../../JUCE/modules/juce_audio_formats/codecs/flac/libFLAC/include/private/memory.h" -#include "../../JUCE/modules/juce_audio_formats/codecs/flac/libFLAC/include/private/metadata.h" +#include "private/metadata.h" #include "../../JUCE/modules/juce_audio_formats/codecs/flac/libFLAC/include/private/stream_encoder_framing.h" #include "../../JUCE/modules/juce_audio_formats/codecs/flac/alloc.h" @@ -50,14 +50,6 @@ */ #define safe_malloc_mul_2op_ safe_malloc_mul_2op_p -static inline void *safe_realloc_(void *ptr, size_t size) { - void *oldptr = ptr; - void *newptr = realloc(ptr, size); - if (size > 0 && newptr == 0) - free(oldptr); - return newptr; -} - /**************************************************************************** * * Local routines @@ -106,7 +98,8 @@ static FLAC__bool free_copy_bytes_(FLAC__byte **to, const FLAC__byte *from, uint /* reallocate entry to 1 byte larger and add a terminating NUL */ /* realloc() failure leaves entry unchanged */ static FLAC__bool ensure_null_terminated_(FLAC__byte **entry, uint32_t length) { - FLAC__byte *x = (FLAC__byte *)safe_realloc_add_2op_(*entry, length, /*+*/ 1); + FLAC__byte *x = + (FLAC__byte *)safe_realloc_nofree_add_2op_(*entry, length, /*+*/ 1); if (x != NULL) { x[length] = '\0'; *entry = x; diff --git a/vendors/libFLAC/private/metadata.h b/vendors/libFLAC/private/metadata.h new file mode 100644 index 000000000..29ceef381 --- /dev/null +++ b/vendors/libFLAC/private/metadata.h @@ -0,0 +1,47 @@ +/* libFLAC - Free Lossless Audio Codec library + * Copyright (C) 2002-2009 Josh Coalson + * Copyright (C) 2011-2023 Xiph.Org Foundation + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of the Xiph.org Foundation nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef FLAC__PRIVATE__METADATA_H +#define FLAC__PRIVATE__METADATA_H + +/* pedalboard: JUCE 7 bundles FLAC 1.4.3 without this private header. */ +#include "../../../JUCE/modules/juce_audio_formats/codecs/flac/metadata.h" + +/* WATCHOUT: all malloc()ed data in the block is free()ed; this may not + * be a consistent state (e.g. PICTURE) or equivalent to the initial + * state after FLAC__metadata_object_new() + */ +void FLAC__metadata_object_delete_data(FLAC__StreamMetadata *object); + +void FLAC__metadata_object_cuesheet_track_delete_data(FLAC__StreamMetadata_CueSheet_Track *object); + +#endif From 80b0b9e413541cc549edcc559bd91d9649a07854 Mon Sep 17 00:00:00 2001 From: Nikhil Dabas Date: Mon, 19 Oct 2026 15:23:22 +0100 Subject: [PATCH 2/5] Fix JUCE 7 audio streams --- pedalboard/io/AudioStream.h | 12 ++++++------ pedalboard/io/LameMP3AudioFormat.h | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pedalboard/io/AudioStream.h b/pedalboard/io/AudioStream.h index b2f760637..e220d68bf 100644 --- a/pedalboard/io/AudioStream.h +++ b/pedalboard/io/AudioStream.h @@ -292,10 +292,10 @@ class AudioStream : public std::enable_shared_from_this } #ifdef JUCE_MODULE_AVAILABLE_juce_audio_devices - virtual void audioDeviceIOCallback(const float **inputChannelData, - int numInputChannels, - float **outputChannelData, - int numOutputChannels, int numSamples) { + void audioDeviceIOCallbackWithContext( + const float *const *inputChannelData, int numInputChannels, + float *const *outputChannelData, int numOutputChannels, int numSamples, + const juce::AudioIODeviceCallbackContext &) override { // Live processing mode: run the input audio through a Pedalboard object. if (playBufferFifo && recordBufferFifo) { for (int i = 0; i < numOutputChannels; i++) { @@ -390,7 +390,7 @@ class AudioStream : public std::enable_shared_from_this } } - virtual void audioDeviceAboutToStart(juce::AudioIODevice *device) { + void audioDeviceAboutToStart(juce::AudioIODevice *device) override { spec.sampleRate = deviceManager.getAudioDeviceSetup().sampleRate; spec.maximumBlockSize = static_cast( deviceManager.getAudioDeviceSetup().bufferSize); @@ -403,7 +403,7 @@ class AudioStream : public std::enable_shared_from_this } } - virtual void audioDeviceStopped() { + void audioDeviceStopped() override { juce::SpinLock::ScopedLockType lock(livePedalboardMutex); for (auto plugin : livePedalboard.getPlugins()) { diff --git a/pedalboard/io/LameMP3AudioFormat.h b/pedalboard/io/LameMP3AudioFormat.h index 3a9cdaa6f..d8e19d03f 100644 --- a/pedalboard/io/LameMP3AudioFormat.h +++ b/pedalboard/io/LameMP3AudioFormat.h @@ -200,7 +200,7 @@ class LameMP3AudioFormat : public juce::AudioFormat { flush(); }; - virtual bool write(const int **samplesToWrite, int numSamples) { + bool write(const int **samplesToWrite, int numSamples) override { std::vector encodedMp3Buffer(1.25 * numSamples + MAXIMUM_MP3_FRAME_SIZE); @@ -229,7 +229,7 @@ class LameMP3AudioFormat : public juce::AudioFormat { return true; } - virtual bool flush() { + bool flush() override { if (!output) return false; output->flush(); From fc83163ffe33107803fb8a3c7db41b2c6bee3195 Mon Sep 17 00:00:00 2001 From: Nikhil Dabas Date: Mon, 19 Oct 2026 22:09:34 +0100 Subject: [PATCH 3/5] Fix JUCE 7 musllinux compatibility --- CMakeLists.txt | 7 ++++--- setup.py | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 55e2b6789..f997980c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -261,10 +261,11 @@ list(APPEND JUCE_COMPILE_DEFINITIONS ) if(DEFINED ENV{CIBW_BUILD} AND "$ENV{CIBW_BUILD}" MATCHES "musllinux") - # For Alpine/musllinux compatibility: + # For Alpine/musllinux compatibility. musl doesn't define these glibc-only + # nl_langinfo() items, for which it returns an empty string: add_compile_definitions( - _NL_IDENTIFICATION_LANGUAGE=0x42 - _NL_IDENTIFICATION_TERRITORY=0x43 + _NL_ADDRESS_LANG_AB=0x42 + _NL_ADDRESS_COUNTRY_AB2=0x43 _LARGEFILE64_SOURCE ) # musl doesn't provide execinfo.h; use our stub in vendors/ diff --git a/setup.py b/setup.py index af34f5c56..1b5bea5b4 100644 --- a/setup.py +++ b/setup.py @@ -89,11 +89,12 @@ ) if "musllinux" in os.getenv("CIBW_BUILD", ""): - # For Alpine/musllinux compatibility: + # For Alpine/musllinux compatibility. musl doesn't define these glibc-only + # nl_langinfo() items, for which it returns an empty string: ALL_CPPFLAGS.extend( [ - "-D_NL_IDENTIFICATION_LANGUAGE=0x42", - "-D_NL_IDENTIFICATION_TERRITORY=0x43", + "-D_NL_ADDRESS_LANG_AB=0x42", + "-D_NL_ADDRESS_COUNTRY_AB2=0x43", ] ) From 715b5ceb95f9891a714245ef3dc830be4d6a1a22 Mon Sep 17 00:00:00 2001 From: Nikhil Dabas Date: Mon, 19 Oct 2026 14:43:04 +0100 Subject: [PATCH 4/5] Add Windows ARM64 build --- .github/workflows/all.yml | 41 +++ .../workflows/build-windows-test-plugins.yml | 99 +++++++ CMakeLists.txt | 12 +- README.md | 3 +- .../build_windows_test_plugins/CMakeLists.txt | 45 ++++ scripts/build_windows_test_plugins/build.ps1 | 242 ++++++++++++++++++ setup.py | 2 +- .../Contents/x86_64-win}/CHOWTapeModel.vst3 | Bin .../x86_64-win}/Magical8bitPlug2.vst3 | Bin tests/test_external_plugins.py | 61 +++++ 10 files changed, 501 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/build-windows-test-plugins.yml create mode 100644 scripts/build_windows_test_plugins/CMakeLists.txt create mode 100644 scripts/build_windows_test_plugins/build.ps1 rename tests/plugins/effect/Windows/{ => CHOWTapeModel.vst3/Contents/x86_64-win}/CHOWTapeModel.vst3 (100%) rename tests/plugins/instrument/Windows/{ => Magical8bitPlug2.vst3/Contents/x86_64-win}/Magical8bitPlug2.vst3 (100%) diff --git a/.github/workflows/all.yml b/.github/workflows/all.yml index e9c5bdfee..476a3d54d 100644 --- a/.github/workflows/all.yml +++ b/.github/workflows/all.yml @@ -641,6 +641,42 @@ jobs: cc: "msvc", cxx: "msvc", } + # Python 3.10 isn't available for Windows on ARM64. + - { + os: windows-11-arm, + python-version: "3.11", + compiler: "msvc", + cc: "msvc", + cxx: "msvc", + } + - { + os: windows-11-arm, + python-version: "3.12", + compiler: "msvc", + cc: "msvc", + cxx: "msvc", + } + - { + os: windows-11-arm, + python-version: "3.13", + compiler: "msvc", + cc: "msvc", + cxx: "msvc", + } + - { + os: windows-11-arm, + python-version: "3.14", + compiler: "msvc", + cc: "msvc", + cxx: "msvc", + } + - { + os: windows-11-arm, + python-version: "3.15", + compiler: "msvc", + cc: "msvc", + cxx: "msvc", + } name: Test with Python ${{ matrix.python-version }} on ${{ matrix.os }} steps: - name: Set up Python ${{ matrix.python-version }} @@ -890,6 +926,11 @@ jobs: - { os: windows-latest, build: cp313-win_amd64 } - { os: windows-latest, build: cp314-win_amd64 } - { os: windows-latest, build: cp315-win_amd64 } + - { os: windows-11-arm, build: cp311-win_arm64 } + - { os: windows-11-arm, build: cp312-win_arm64 } + - { os: windows-11-arm, build: cp313-win_arm64 } + - { os: windows-11-arm, build: cp314-win_arm64 } + - { os: windows-11-arm, build: cp315-win_arm64 } # - { os: windows-latest, build: cp312-win32 } - { os: "ubuntu-24.04", build: cp310-manylinux_x86_64, python: "3.10" } - { os: "ubuntu-24.04-arm", build: cp310-manylinux_aarch64 } diff --git a/.github/workflows/build-windows-test-plugins.yml b/.github/workflows/build-windows-test-plugins.yml new file mode 100644 index 000000000..d0f9e47ad --- /dev/null +++ b/.github/workflows/build-windows-test-plugins.yml @@ -0,0 +1,99 @@ +name: Build Windows Test Plugins + +# Builds the VST3 plugins in tests/plugins/*/Windows from pinned source commits +# (see scripts/build_windows_test_plugins/build.ps1), as prebuilt plugin binaries +# can't be accepted in pull requests. The built plugins are always uploaded as +# workflow artifacts, and are optionally committed to the branch that this +# workflow was run on. +on: + workflow_dispatch: + inputs: + architectures: + description: Which architectures to build plugins for + type: choice + options: + - arm64 + - x64 + - arm64 and x64 + default: arm64 + commit: + description: Commit the built plugins to the branch this workflow is run on + type: boolean + default: false + +permissions: + contents: read + +jobs: + select-architectures: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + steps: + - id: matrix + env: + ARCHITECTURES: ${{ inputs.architectures }} + run: | + arm64='{"architecture": "ARM64", "bundle-folder": "arm64-win", "os": "windows-11-arm"}' + x64='{"architecture": "x64", "bundle-folder": "x86_64-win", "os": "windows-latest"}' + case "$ARCHITECTURES" in + arm64) include="$arm64" ;; + x64) include="$x64" ;; + *) include="$arm64, $x64" ;; + esac + echo "matrix={\"include\": [$include]}" >> "$GITHUB_OUTPUT" + + build: + needs: select-architectures + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.select-architectures.outputs.matrix) }} + runs-on: ${{ matrix.os }} + name: Build ${{ matrix.architecture }} plugins + steps: + - uses: actions/checkout@v4 + - name: Check out JUCE + run: git submodule update --init --depth 1 JUCE + - name: Build plugins + shell: pwsh + run: ./scripts/build_windows_test_plugins/build.ps1 -Architecture ${{ matrix.architecture }} + - uses: actions/upload-artifact@v4 + with: + name: windows-test-plugins-${{ matrix.bundle-folder }} + path: tests/plugins/*/Windows/*.vst3/Contents/${{ matrix.bundle-folder }}/ + if-no-files-found: error + + commit: + needs: build + if: inputs.commit + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Ensure this workflow was run on a branch + if: github.ref_type != 'branch' + run: | + echo "::error::Plugins can only be committed when this workflow is run on a branch." + exit 1 + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + pattern: windows-test-plugins-* + path: tests/plugins + merge-multiple: true + - name: Commit and push plugins + env: + ARCHITECTURES: ${{ inputs.architectures }} + BRANCH: ${{ github.ref_name }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add tests/plugins + if git diff --cached --quiet; then + echo "The built plugins are identical to the committed ones; nothing to commit." + exit 0 + fi + git status --short + git commit -m "Update Windows test plugins ($ARCHITECTURES)" -m "Built by $RUN_URL" + git push origin "HEAD:refs/heads/$BRANCH" diff --git a/CMakeLists.txt b/CMakeLists.txt index f997980c3..b4c06b286 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,7 +55,15 @@ if(WIN32 AND NOT DEFINED Python_LIBRARY) set(Python_VERSION_PATCH ${CMAKE_MATCH_3}) set(DYNAMIC_PYTHON_LIB_FILENAME "python${Python_VERSION_MAJOR}${Python_VERSION_MINOR}.lib") - set(EXPECTED_PYTHON_VERSION_DIR "C:/hostedtoolcache/windows/Python/${Python_VERSION}/x64/libs") + # Ask the interpreter where it lives rather than assuming an x64 hostedtoolcache layout, + # so that this also works for ARM64 and virtual environments. + execute_process( + COMMAND ${Python_EXECUTABLE} -c "import sys; print(sys.base_prefix)" + OUTPUT_VARIABLE PYTHON_BASE_PREFIX + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + file(TO_CMAKE_PATH "${PYTHON_BASE_PREFIX}" PYTHON_BASE_PREFIX) + set(EXPECTED_PYTHON_VERSION_DIR "${PYTHON_BASE_PREFIX}/libs") set(Python_LIBRARY "${EXPECTED_PYTHON_VERSION_DIR}/${DYNAMIC_PYTHON_LIB_FILENAME}") message(STATUS "DEBUG: On Windows, constructed DYNAMIC_PYTHON_LIB_FILENAME: ${DYNAMIC_PYTHON_LIB_FILENAME}") @@ -361,9 +369,9 @@ target_compile_definitions(pedalboard_native PRIVATE ${RUBBERBAND_COMPILE_DEFINI # LAME config if(WIN32) + # lame_config.h defines HAVE_XMMINTRIN_H on x64 only, as SSE isn't available on ARM64. target_compile_definitions(pedalboard_native PRIVATE HAVE_MPGLIB - HAVE_XMMINTRIN_H ) target_compile_options(pedalboard_native PRIVATE "/FI${CMAKE_CURRENT_SOURCE_DIR}/vendors/lame_config.h" diff --git a/README.md b/README.md index 184a6ec25..c4bec6b7a 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,8 @@ If you are new to Python, follow [INSTALLATION.md](https://github.com/spotify/pe - Compatible with a wide range of VSTs and Audio Units - Windows - Tested automatically on GitHub with VSTs - - Platform wheels available for `amd64` (x86-64, Intel/AMD) + - Platform wheels available for `amd64` (x86-64, Intel/AMD) and `arm64` (Windows on ARM, Python 3.11+) + - VST3 plugins must match the architecture of the Python interpreter (i.e.: ARM64 Python can't load x64-only plugins) ## Examples diff --git a/scripts/build_windows_test_plugins/CMakeLists.txt b/scripts/build_windows_test_plugins/CMakeLists.txt new file mode 100644 index 000000000..d6996648c --- /dev/null +++ b/scripts/build_windows_test_plugins/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.22) +project(PedalboardWindowsTestPlugins VERSION 1.0.0) + +set(PEDALBOARD_JUCE_DIR "" CACHE PATH "Path to pedalboard's JUCE submodule") +set(PLUGINS_FILE "" CACHE FILEPATH "Plugin definitions, generated by build.ps1 from each plugin's .jucer file") + +add_subdirectory("${PEDALBOARD_JUCE_DIR}" JUCE) + +# Adds a VST3 plugin target. PLUGIN_OPTIONS are passed to juce_add_plugin, and +# MODULES are JUCE modules (juce_*) or paths to a plugin's own modules. +function(pedalboard_add_test_plugin target) + cmake_parse_arguments(PARSE_ARGV 1 ARG "" "" + "PLUGIN_OPTIONS;MODULES;SOURCES;RESOURCES;DEFINITIONS;COMPILE_OPTIONS") + + juce_add_plugin(${target} ${ARG_PLUGIN_OPTIONS} FORMATS VST3 VST3_AUTO_MANIFEST FALSE) + target_sources(${target} PRIVATE ${ARG_SOURCES}) + + if(ARG_RESOURCES) + juce_add_binary_data(${target}Data SOURCES ${ARG_RESOURCES}) + target_link_libraries(${target} PRIVATE ${target}Data) + endif() + + foreach(module IN LISTS ARG_MODULES) + if(module MATCHES "^juce_") + target_link_libraries(${target} PRIVATE juce::${module}) + else() + juce_add_module("${module}") + get_filename_component(module_name "${module}" NAME) + target_link_libraries(${target} PRIVATE ${module_name}) + endif() + endforeach() + + target_compile_definitions(${target} PUBLIC + # Features that aren't needed by any of the test plugins: + JUCE_WEB_BROWSER=0 + JUCE_USE_CURL=0 + ${ARG_DEFINITIONS}) + target_compile_options(${target} PRIVATE ${ARG_COMPILE_OPTIONS}) + target_link_libraries(${target} PUBLIC + juce::juce_recommended_config_flags + juce::juce_recommended_lto_flags) + juce_generate_juce_header(${target}) +endfunction() + +include("${PLUGINS_FILE}") diff --git a/scripts/build_windows_test_plugins/build.ps1 b/scripts/build_windows_test_plugins/build.ps1 new file mode 100644 index 000000000..3e07fa866 --- /dev/null +++ b/scripts/build_windows_test_plugins/build.ps1 @@ -0,0 +1,242 @@ +<# +.SYNOPSIS +Builds pedalboard's Windows test plugins from source. + +.DESCRIPTION +Builds the VST3 plugins used by tests/test_external_plugins.py from pinned +commits of their source code, against pedalboard's JUCE submodule: + + - CHOWTapeModel 2.7.0 (https://github.com/jatinchowdhury18/AnalogTapeModel, GPLv3) + - Magical8bitPlug2 1.0.1 (https://github.com/yokemura/Magical8bitPlug2, MIT) + +Each plugin is copied into its VST3 bundle in tests/plugins/*/Windows, at +.vst3/Contents/-win/.vst3, replacing any existing build for +that architecture. Each plugin's CMake target (its settings, modules, and source +and resource files) is generated from its Projucer (.jucer) project. + +Run this on a Windows machine with Visual Studio 2022 (or newer), CMake 3.22+ +and Git, after initializing pedalboard's JUCE submodule. This script is run by +the "Build Windows Test Plugins" GitHub Actions workflow, which builds each +architecture on a native runner. + +.PARAMETER Architecture +The CPU architecture to build for: ARM64 or x64. Defaults to the current machine's. + +.PARAMETER BuildDir +Where to check out the plugin sources and build them. +#> +#Requires -Version 7.4 + +param( + [ValidateSet("ARM64", "x64")] + [string]$Architecture = $(if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { "ARM64" } else { "x64" }), + [string]$BuildDir = (Join-Path $PSScriptRoot "build") +) + +$ErrorActionPreference = "Stop" +# Throw if a native command (git, cmake) exits with a non-zero exit code: +$PSNativeCommandUseErrorActionPreference = $true +$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path +$SourceDir = Join-Path $BuildDir "src" +$CMakeBuildDir = Join-Path $BuildDir $Architecture + +# Folder names from https://steinbergmedia.github.io/vst3_dev_portal/pages/Technical+Documentation/Locations+Format/Plugin+Format.html +$BundleArchitecture = @{ "ARM64" = "arm64-win"; "x64" = "x86_64-win" }[$Architecture] + +$Plugins = @( + @{ + Name = "CHOWTapeModel" + Type = "effect" + Url = "https://github.com/jatinchowdhury18/AnalogTapeModel.git" + Commit = "6e98855db14759d7952ec7298b661853679fb03f" # v2.7.0 + Jucer = "Plugin/CHOWTapeModel.jucer" + # Originally built with JUCE 6.0, which allowed modal loops by default: + Definitions = @("JUCE_MODAL_LOOPS_PERMITTED=1") + Submodules = @("Plugin/foleys_gui_magic") + # Source changes needed to build against JUCE 7: + Patches = @( + @{ + File = "Plugin/foleys_gui_magic/General/foleys_MagicGUIState.cpp" + Find = "visualiserThread.startThread (3);" + Replace = "visualiserThread.startThread (juce::Thread::Priority::low);" + } + ) + }, + @{ + Name = "Magical8bitPlug2" + Type = "instrument" + Url = "https://github.com/yokemura/Magical8bitPlug2.git" + Commit = "10be0196618732750aba95b35c31624b1a146ec2" # v1.0.1 + Jucer = "Magical8bitPlug2.jucer" + Definitions = @() + Submodules = @() + Patches = @( + @{ + # The synth adds to the output buffer, which (unlike older JUCE + # versions) JUCE 7's VST3 wrapper doesn't clear first: + File = "Source/PluginProcessor.cpp" + Find = "synth.renderNextBlock (buffer, midiMessages, 0, buffer.getNumSamples()); // [5]" + Replace = "buffer.clear(); synth.renderNextBlock (buffer, midiMessages, 0, buffer.getNumSamples()); // [5]" + } + ) + } +) + +foreach ($Plugin in $Plugins) { + $Checkout = Join-Path $SourceDir $Plugin.Name + if (-not (Test-Path (Join-Path $Checkout ".git"))) { + git init --quiet $Checkout + git -C $Checkout remote add origin $Plugin.Url + } + git -C $Checkout fetch --quiet --depth 1 origin $Plugin.Commit + git -C $Checkout checkout --quiet --force $Plugin.Commit + foreach ($Submodule in $Plugin.Submodules) { + git -C $Checkout submodule update --init --force --depth 1 $Submodule + } + foreach ($Patch in $Plugin.Patches) { + $Path = Join-Path $Checkout $Patch.File + $Source = [IO.File]::ReadAllText($Path) + if (-not $Source.Contains($Patch.Find)) { + throw "Couldn't find '$($Patch.Find)' in $Path" + } + [IO.File]::WriteAllText($Path, $Source.Replace($Patch.Find, $Patch.Replace)) + } + $Plugin.Checkout = $Checkout +} + +# Generate a pedalboard_add_test_plugin() call (see CMakeLists.txt) for each plugin +# from its Projucer project, using Projucer's defaults for any missing settings: +function ConvertTo-CMakeArgument([string]$Value) { + '"' + ($Value -replace '([\\"$;])', '\$1') + '"' +} + +function Get-JucerPluginDefinition($Plugin) { + $JucerPath = Join-Path $Plugin.Checkout $Plugin.Jucer + $JucerDir = Split-Path $JucerPath + $Jucer = [xml](Get-Content -Raw $JucerPath) + $Project = $Jucer.JUCERPROJECT + if ($Project.projectType -ne "audioplug") { + throw "$JucerPath isn't an audio plugin project" + } + $Attribute = { param($Name, $Default) $Value = $Project.GetAttribute($Name); if ($Value) { $Value } else { $Default } } + $ToCMakePath = { param($Path) [IO.Path]::GetFullPath((Join-Path $JucerDir $Path)).Replace('\', '/') } + + # From Project::initialiseAudioPluginValues: the default plugin code is derived + # from the project ID, and must match for the VST3 class ID (and presets) to match. + $Identifier = (($Project.id + $Project.id) -replace '[.,;:/@]', '_') -replace '[^A-Za-z0-9_]', '' + if ($Identifier -match '^\d') { $Identifier = "_$Identifier" } + $Identifier += "xxxx" + $DefaultPluginCode = $Identifier.Substring(0, 1).ToUpper() + $Identifier.Substring(1, 3).ToLower() + + $Options = [ordered]@{ + PRODUCT_NAME = $Project.name + VERSION = & $Attribute version "1.0.0" + COMPANY_NAME = & $Attribute pluginManufacturer (& $Attribute companyName "yourcompany") + COMPANY_EMAIL = & $Attribute companyEmail $null + COMPANY_WEBSITE = & $Attribute companyWebsite $null + PLUGIN_MANUFACTURER_CODE = & $Attribute pluginManufacturerCode "Manu" + PLUGIN_CODE = & $Attribute pluginCode $DefaultPluginCode + PLUGIN_NAME = & $Attribute pluginName $Project.name + DESCRIPTION = & $Attribute pluginDesc $Project.name + BUNDLE_ID = & $Attribute bundleIdentifier $null + } + $Characteristics = (& $Attribute pluginCharacteristicsValue "") -split "," + $Flags = [ordered]@{ + pluginIsSynth = "IS_SYNTH" + pluginWantsMidiIn = "NEEDS_MIDI_INPUT" + pluginProducesMidiOut = "NEEDS_MIDI_OUTPUT" + pluginIsMidiEffectPlugin = "IS_MIDI_EFFECT" + pluginEditorRequiresKeys = "EDITOR_WANTS_KEYBOARD_FOCUS" + } + foreach ($Flag in $Flags.Keys) { + if ($Characteristics -contains $Flag) { + $Options[$Flags[$Flag]] = "TRUE" + } + } + + $ChannelConfigs = & $Attribute pluginChannelConfigs $null + $Definitions = @( + foreach ($Option in $Jucer.SelectNodes("/JUCERPROJECT/JUCEOPTIONS/@*")) { + switch ($Option.Value) { + { $_ -in "1", "enabled" } { "$($Option.Name)=1" } + { $_ -in "0", "disabled" } { "$($Option.Name)=0" } + } + } + if ($ChannelConfigs) { + "JucePlugin_PreferredChannelConfigurations=$($ChannelConfigs -replace '\s', '')" + } + $Plugin.Definitions + ) + + # Use the Visual Studio exporter's compiler flags, and its paths to the plugin's own modules: + $Exporter = $Jucer.SelectNodes("/JUCERPROJECT/EXPORTFORMATS/*[starts-with(local-name(), 'VS')]") | + Sort-Object LocalName | Select-Object -Last 1 + if (-not $Exporter) { + throw "No Visual Studio exporter found in $JucerPath" + } + $CompileOptions = @($Exporter.GetAttribute("extraCompilerFlags") -split '\s+' | Where-Object { $_ }) + $Modules = foreach ($Module in $Jucer.SelectNodes("/JUCERPROJECT/MODULES/MODULE")) { + if ($Module.id -eq "juce_audio_plugin_client") { + continue # Added by juce_add_plugin + } + if ($Module.id -like "juce_*") { + $Module.id + continue + } + $ModulePath = $Exporter.SelectSingleNode("MODULEPATHS/MODULEPATH[@id='$($Module.id)']") + if (-not $ModulePath) { + throw "No path for module $($Module.id) in the $($Exporter.LocalName) exporter in $JucerPath" + } + & $ToCMakePath (Join-Path $ModulePath.path $Module.id) + } + + $Sources = $Jucer.SelectNodes("//FILE[@compile='1']") | ForEach-Object { & $ToCMakePath $_.file } + $Resources = $Jucer.SelectNodes("//FILE[@resource='1']") | ForEach-Object { & $ToCMakePath $_.file } + if (-not $Sources) { + throw "No source files found in $JucerPath" + } + + "pedalboard_add_test_plugin($($Plugin.Name)" + " PLUGIN_OPTIONS" + foreach ($Name in $Options.Keys) { + if ($null -ne $Options[$Name]) { + " $Name $(ConvertTo-CMakeArgument $Options[$Name])" + } + } + foreach ($List in @( + @{ Name = "MODULES"; Values = $Modules }, + @{ Name = "SOURCES"; Values = $Sources }, + @{ Name = "RESOURCES"; Values = $Resources }, + @{ Name = "DEFINITIONS"; Values = $Definitions }, + @{ Name = "COMPILE_OPTIONS"; Values = $CompileOptions })) { + if ($List.Values) { + " $($List.Name)" + $List.Values | ForEach-Object { " $(ConvertTo-CMakeArgument $_)" } + } + } + ")" +} + +$PluginsFile = Join-Path $BuildDir "plugins.cmake" +$PluginDefinitions = foreach ($Plugin in $Plugins) { Get-JucerPluginDefinition $Plugin } +Set-Content -Path $PluginsFile -Value $PluginDefinitions -Encoding utf8NoBOM + +cmake -S $PSScriptRoot -B $CMakeBuildDir -A $Architecture ` + "-DPEDALBOARD_JUCE_DIR=$RepoRoot\JUCE" ` + "-DPLUGINS_FILE=$PluginsFile" +$Targets = $Plugins | ForEach-Object { "$($_.Name)_VST3" } +cmake --build $CMakeBuildDir --config Release --parallel --target @Targets + +foreach ($Plugin in $Plugins) { + $Name = $Plugin.Name + $BuiltBundle = Join-Path $CMakeBuildDir "${Name}_artefacts\Release\VST3\$Name.vst3" + $Built = Get-ChildItem -Path (Join-Path $BuiltBundle "Contents\*-win\$Name.vst3") -File + if (@($Built).Count -ne 1) { + throw "Expected exactly one built plugin in $BuiltBundle, found: $($Built -join ', ')" + } + + $Destination = Join-Path $RepoRoot "tests\plugins\$($Plugin.Type)\Windows\$Name.vst3\Contents\$BundleArchitecture" + New-Item -ItemType Directory -Force $Destination | Out-Null + Copy-Item $Built.FullName (Join-Path $Destination "$Name.vst3") -Force + Write-Host "Updated $Destination\$Name.vst3" +} diff --git a/setup.py b/setup.py index 1b5bea5b4..3cb44a405 100644 --- a/setup.py +++ b/setup.py @@ -239,7 +239,7 @@ def ignore_files_matching(files, *matches): LAME_CONFIG_FILE = str(Path("vendors/lame_config.h").resolve()) if platform.system() == "Windows": LAME_FLAGS.append(f"/FI{LAME_CONFIG_FILE}") - LAME_FLAGS.append("-DHAVE_XMMINTRIN_H") + # lame_config.h defines HAVE_XMMINTRIN_H on x64 only, as SSE isn't available on ARM64. else: LAME_FLAGS.append(f"-include{LAME_CONFIG_FILE}") ALL_CFLAGS.extend(LAME_FLAGS) diff --git a/tests/plugins/effect/Windows/CHOWTapeModel.vst3 b/tests/plugins/effect/Windows/CHOWTapeModel.vst3/Contents/x86_64-win/CHOWTapeModel.vst3 similarity index 100% rename from tests/plugins/effect/Windows/CHOWTapeModel.vst3 rename to tests/plugins/effect/Windows/CHOWTapeModel.vst3/Contents/x86_64-win/CHOWTapeModel.vst3 diff --git a/tests/plugins/instrument/Windows/Magical8bitPlug2.vst3 b/tests/plugins/instrument/Windows/Magical8bitPlug2.vst3/Contents/x86_64-win/Magical8bitPlug2.vst3 similarity index 100% rename from tests/plugins/instrument/Windows/Magical8bitPlug2.vst3 rename to tests/plugins/instrument/Windows/Magical8bitPlug2.vst3/Contents/x86_64-win/Magical8bitPlug2.vst3 diff --git a/tests/test_external_plugins.py b/tests/test_external_plugins.py index 1c570e535..38997a65d 100644 --- a/tests/test_external_plugins.py +++ b/tests/test_external_plugins.py @@ -33,8 +33,10 @@ import random import shutil import signal +import struct import subprocess import sys +import sysconfig import threading import time import typing @@ -84,6 +86,59 @@ f for f in AVAILABLE_INSTRUMENT_PLUGINS_IN_TEST_ENVIRONMENT if "component" not in f ] + +def _windows_plugin_matches_process_architecture(plugin_path: str) -> bool: + """ + Windows can't load a DLL built for a different CPU architecture than the current + process (i.e.: x64 plugins can't be loaded by ARM64 Python), so check the PE header. + """ + expected = { + "win-amd64": (0x8664, "x86_64-win"), + "win-arm64": (0xAA64, "arm64-win"), + "win32": (0x14C, "x86-win"), + }.get(sysconfig.get_platform()) + if expected is None: + return True + machine, bundle_architecture = expected + + if os.path.isdir(plugin_path): + plugin_path = os.path.join( + plugin_path, "Contents", bundle_architecture, os.path.basename(plugin_path) + ) + if not os.path.isfile(plugin_path): + return False + + try: + with open(plugin_path, "rb") as f: + header = f.read(4096) + pe_header_offset = struct.unpack_from(" 2**32 if IS_TESTING_MUSL_LIBC_ON_CI or not IS_64BIT: @@ -267,6 +322,12 @@ def max_volume_of(x: np.ndarray) -> float: not IS_64BIT, reason="External plugins are not officially supported on 32-bit platforms.", ) +@pytest.mark.skipif( + not AVAILABLE_EFFECT_PLUGINS_IN_TEST_ENVIRONMENT + and bool(EFFECT_PLUGINS_FOR_OTHER_ARCHITECTURES), + reason="Test plugins are only available for other CPU architectures: " + + ", ".join(EFFECT_PLUGINS_FOR_OTHER_ARCHITECTURES), +) def test_at_least_one_plugin_is_available_for_testing(): assert AVAILABLE_EFFECT_PLUGINS_IN_TEST_ENVIRONMENT From 7a832e63b6b6af2a0b25114560b1a03c519327a3 Mon Sep 17 00:00:00 2001 From: Nikhil Dabas Date: Mon, 19 Oct 2026 22:08:12 +0100 Subject: [PATCH 5/5] Lower Windows ARM64 coverage minimum until plugins are committed --- .github/workflows/all.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/all.yml b/.github/workflows/all.yml index 476a3d54d..0842c8491 100644 --- a/.github/workflows/all.yml +++ b/.github/workflows/all.yml @@ -788,6 +788,17 @@ jobs: # Inline install here on purpose, as this is only required for test _setup_ and not running pip install google-cloud-storage python ./tests/download_test_plugins.py + # Most of pedalboard's external plugin code is only covered by tests that load plugins, so + # don't enforce the coverage minimum on ARM64 until ARM64 test plugins have been committed + # (by the "Build Windows Test Plugins" workflow): + - name: Relax coverage minimum if no ARM64 test plugins are available + if: matrix.os == 'windows-11-arm' + shell: bash + run: | + if ! compgen -G "tests/plugins/*/Windows/*.vst3/Contents/arm64-win/*.vst3" > /dev/null; then + echo "::warning::No Windows ARM64 test plugins found; external plugin tests will be skipped and coverage won't be enforced." + echo "MINIMUM_COVERAGE_PERCENTAGE=0" >> "$GITHUB_ENV" + fi - name: Run tests if: matrix.os != 'ubuntu-24.04' || matrix.python-version != '3.10' run: pytest --maxfail=4 --cov-report term --cov-fail-under=${{ env.MINIMUM_COVERAGE_PERCENTAGE }} --cov=pedalboard --durations=100