From 5d5fdaf0083b70cd48aa7553f90908edcc176514 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 21 Dec 2021 10:00:51 +0000 Subject: [PATCH 01/76] Projucer: Fix relative paths for Android resource files --- .../Source/ProjectSaving/jucer_ProjectExport_Android.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h index 65d56b87fce0..b36320e81df3 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h @@ -244,7 +244,7 @@ class AndroidProjectExporter final : public ProjectExporter if (androidExtraAssetsFolderValue.isNotEmpty()) { - auto extraAssets = getProject().getFile().getParentDirectory().getChildFile (androidExtraAssetsFolderValue); + auto extraAssets = getProject().getFile().getSiblingFile (androidExtraAssetsFolderValue); if (extraAssets.exists() && extraAssets.isDirectory()) { @@ -1284,7 +1284,7 @@ class AndroidProjectExporter final : public ProjectExporter if (remoteNotifsConfigFilePath.isEmpty()) remoteNotifsConfigFilePath = androidRemoteNotificationsConfigFile.get().toString(); - File file (getProject().getFile().getChildFile (remoteNotifsConfigFilePath)); + File file (getProject().getFile().getSiblingFile (remoteNotifsConfigFilePath)); // Settings file must be present for remote notifications to work and it must be called google-services.json. jassert (file.existsAsFile() && file.getFileName() == "google-services.json"); @@ -1303,7 +1303,7 @@ class AndroidProjectExporter final : public ProjectExporter for (auto& path : resourcePaths) { - auto file = getProject().getFile().getChildFile (path); + auto file = getProject().getFile().getSiblingFile (path); jassert (file.exists()); From 5737c42ccf0a5c29abea4f650c34b53f8ae37c32 Mon Sep 17 00:00:00 2001 From: Tom Poole Date: Wed, 20 Nov 2024 10:10:35 +0000 Subject: [PATCH 02/76] Use getSiblingFile in more places --- examples/Assets/DemoUtilities.h | 6 +++--- .../Builds/Android/app/src/main/assets/DemoUtilities.h | 6 +++--- examples/DemoRunner/Source/Demos/JUCEDemos.cpp | 4 ++-- .../Builds/Android/app/src/main/assets/DemoUtilities.h | 6 +++--- .../Projucer/Source/Utility/PIPs/jucer_PIPGenerator.cpp | 2 +- modules/juce_core/files/juce_File.cpp | 8 ++++---- modules/juce_gui_basics/drawables/juce_SVGParser.cpp | 2 +- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/examples/Assets/DemoUtilities.h b/examples/Assets/DemoUtilities.h index 1c5534bff731..4f1f0f26319b 100644 --- a/examples/Assets/DemoUtilities.h +++ b/examples/Assets/DemoUtilities.h @@ -63,7 +63,7 @@ inline File getExamplesDirectory() noexcept return File { CharPointer_UTF8 { PIP_JUCE_EXAMPLES_DIRECTORY_STRING } }; #else auto currentFile = File::getSpecialLocation (File::SpecialLocationType::currentApplicationFile); - auto exampleDir = currentFile.getParentDirectory().getChildFile ("examples"); + auto exampleDir = currentFile.getSiblingFile ("examples"); if (exampleDir.exists()) return exampleDir; @@ -109,10 +109,10 @@ inline std::unique_ptr createAssetInputStream (const char* resource #else #if JUCE_IOS auto assetsDir = File::getSpecialLocation (File::currentExecutableFile) - .getParentDirectory().getChildFile ("Assets"); + .getSiblingFile ("Assets"); #elif JUCE_MAC auto assetsDir = File::getSpecialLocation (File::currentExecutableFile) - .getParentDirectory().getParentDirectory().getChildFile ("Resources").getChildFile ("Assets"); + .getParentDirectory().getSiblingFile ("Resources").getChildFile ("Assets"); if (! assetsDir.exists()) assetsDir = getExamplesDirectory().getChildFile ("Assets"); diff --git a/examples/DemoRunner/Builds/Android/app/src/main/assets/DemoUtilities.h b/examples/DemoRunner/Builds/Android/app/src/main/assets/DemoUtilities.h index 1c5534bff731..4f1f0f26319b 100644 --- a/examples/DemoRunner/Builds/Android/app/src/main/assets/DemoUtilities.h +++ b/examples/DemoRunner/Builds/Android/app/src/main/assets/DemoUtilities.h @@ -63,7 +63,7 @@ inline File getExamplesDirectory() noexcept return File { CharPointer_UTF8 { PIP_JUCE_EXAMPLES_DIRECTORY_STRING } }; #else auto currentFile = File::getSpecialLocation (File::SpecialLocationType::currentApplicationFile); - auto exampleDir = currentFile.getParentDirectory().getChildFile ("examples"); + auto exampleDir = currentFile.getSiblingFile ("examples"); if (exampleDir.exists()) return exampleDir; @@ -109,10 +109,10 @@ inline std::unique_ptr createAssetInputStream (const char* resource #else #if JUCE_IOS auto assetsDir = File::getSpecialLocation (File::currentExecutableFile) - .getParentDirectory().getChildFile ("Assets"); + .getSiblingFile ("Assets"); #elif JUCE_MAC auto assetsDir = File::getSpecialLocation (File::currentExecutableFile) - .getParentDirectory().getParentDirectory().getChildFile ("Resources").getChildFile ("Assets"); + .getParentDirectory().getSiblingFile ("Resources").getChildFile ("Assets"); if (! assetsDir.exists()) assetsDir = getExamplesDirectory().getChildFile ("Assets"); diff --git a/examples/DemoRunner/Source/Demos/JUCEDemos.cpp b/examples/DemoRunner/Source/Demos/JUCEDemos.cpp index e6b42fad4d9d..506c27da8ce3 100644 --- a/examples/DemoRunner/Source/Demos/JUCEDemos.cpp +++ b/examples/DemoRunner/Source/Demos/JUCEDemos.cpp @@ -50,7 +50,7 @@ void JUCEDemos::registerDemo (std::function constructorCallback, c { #if JUCE_MAC auto f = File::getSpecialLocation (File::currentExecutableFile) - .getParentDirectory().getParentDirectory().getChildFile ("Resources"); + .getParentDirectory().getSiblingFile ("Resources"); #else auto f = findExamplesDirectoryFromExecutable (File::getSpecialLocation (File::currentApplicationFile)); #endif @@ -69,7 +69,7 @@ void JUCEDemos::registerDemo (std::function constructorCallback, c File JUCEDemos::findExamplesDirectoryFromExecutable (File exec) { int numTries = 15; - auto exampleDir = exec.getParentDirectory().getChildFile ("examples"); + auto exampleDir = exec.getSiblingFile ("examples"); if (exampleDir.exists()) return exampleDir; diff --git a/extras/AudioPluginHost/Builds/Android/app/src/main/assets/DemoUtilities.h b/extras/AudioPluginHost/Builds/Android/app/src/main/assets/DemoUtilities.h index 1c5534bff731..4f1f0f26319b 100644 --- a/extras/AudioPluginHost/Builds/Android/app/src/main/assets/DemoUtilities.h +++ b/extras/AudioPluginHost/Builds/Android/app/src/main/assets/DemoUtilities.h @@ -63,7 +63,7 @@ inline File getExamplesDirectory() noexcept return File { CharPointer_UTF8 { PIP_JUCE_EXAMPLES_DIRECTORY_STRING } }; #else auto currentFile = File::getSpecialLocation (File::SpecialLocationType::currentApplicationFile); - auto exampleDir = currentFile.getParentDirectory().getChildFile ("examples"); + auto exampleDir = currentFile.getSiblingFile ("examples"); if (exampleDir.exists()) return exampleDir; @@ -109,10 +109,10 @@ inline std::unique_ptr createAssetInputStream (const char* resource #else #if JUCE_IOS auto assetsDir = File::getSpecialLocation (File::currentExecutableFile) - .getParentDirectory().getChildFile ("Assets"); + .getSiblingFile ("Assets"); #elif JUCE_MAC auto assetsDir = File::getSpecialLocation (File::currentExecutableFile) - .getParentDirectory().getParentDirectory().getChildFile ("Resources").getChildFile ("Assets"); + .getParentDirectory().getSiblingFile ("Resources").getChildFile ("Assets"); if (! assetsDir.exists()) assetsDir = getExamplesDirectory().getChildFile ("Assets"); diff --git a/extras/Projucer/Source/Utility/PIPs/jucer_PIPGenerator.cpp b/extras/Projucer/Source/Utility/PIPs/jucer_PIPGenerator.cpp index 11f7698caed4..4e66e8afedb1 100644 --- a/extras/Projucer/Source/Utility/PIPs/jucer_PIPGenerator.cpp +++ b/extras/Projucer/Source/Utility/PIPs/jucer_PIPGenerator.cpp @@ -507,7 +507,7 @@ Array PIPGenerator::replaceRelativeIncludesAndGetFilesToMove() if (path.startsWith ("<") && path.endsWith (">")) continue; - auto file = pipFile.getParentDirectory().getChildFile (path); + auto file = pipFile.getSiblingFile (path); files.add (file); line = line.replace (path, file.getFileName()); diff --git a/modules/juce_core/files/juce_File.cpp b/modules/juce_core/files/juce_File.cpp index 073ee8e92505..ebc5a95796c3 100644 --- a/modules/juce_core/files/juce_File.cpp +++ b/modules/juce_core/files/juce_File.cpp @@ -1158,10 +1158,10 @@ class FileTests final : public UnitTest expect (home.getChildFile ("...xyz").getFileName() == "...xyz"); expect (home.getChildFile ("./xyz") == home.getChildFile ("xyz")); expect (home.getChildFile ("././xyz") == home.getChildFile ("xyz")); - expect (home.getChildFile ("../xyz") == home.getParentDirectory().getChildFile ("xyz")); - expect (home.getChildFile (".././xyz") == home.getParentDirectory().getChildFile ("xyz")); - expect (home.getChildFile (".././xyz/./abc") == home.getParentDirectory().getChildFile ("xyz/abc")); - expect (home.getChildFile ("./../xyz") == home.getParentDirectory().getChildFile ("xyz")); + expect (home.getChildFile ("../xyz") == home.getSiblingFile ("xyz")); + expect (home.getChildFile (".././xyz") == home.getSiblingFile ("xyz")); + expect (home.getChildFile (".././xyz/./abc") == home.getSiblingFile ("xyz/abc")); + expect (home.getChildFile ("./../xyz") == home.getSiblingFile ("xyz")); expect (home.getChildFile ("a1/a2/a3/./../../a4") == home.getChildFile ("a1/a4")); expect (! File().hasReadAccess()); diff --git a/modules/juce_gui_basics/drawables/juce_SVGParser.cpp b/modules/juce_gui_basics/drawables/juce_SVGParser.cpp index 04175c6e5c6e..56ce6b2bbd3f 100644 --- a/modules/juce_gui_basics/drawables/juce_SVGParser.cpp +++ b/modules/juce_gui_basics/drawables/juce_SVGParser.cpp @@ -1299,7 +1299,7 @@ class SVGState } else { - auto linkedFile = originalFile.getParentDirectory().getChildFile (link); + auto linkedFile = originalFile.getSiblingFile (link); if (linkedFile.existsAsFile()) inputStream = linkedFile.createInputStream(); From 01bfa988277dd3968ce352c5ba54492879ea0945 Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 18 Nov 2024 12:18:24 +0000 Subject: [PATCH 03/76] CoreGraphics: Fix incorrect behaviour of non-solid-colour text fills Previously, filling a string containing a space or other non-rendered character with a gradient would end up filling the entire clip region. The correct behaviour is to completely skip filling any empty paths. --- modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm b/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm index 1caaf7e7b28e..aa18f90ac156 100644 --- a/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm +++ b/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm @@ -867,6 +867,10 @@ void setFill (const FillType& newFill) Path p; auto& f = state->font; f.getTypefacePtr()->getOutlineForGlyph (f.getMetricsKind(), glyph, p); + + if (p.isEmpty()) + continue; + const auto scale = f.getHeight(); fillPath (p, AffineTransform::scale (scale * f.getHorizontalScale(), scale).translated (positions[index]).followedBy (transform)); } From 0aaaea265a1015ec76c6bedb38476063ffb237e6 Mon Sep 17 00:00:00 2001 From: reuk Date: Wed, 20 Nov 2024 20:21:51 +0000 Subject: [PATCH 04/76] AU Client: Ignore availability warnings for MIDIEventList functions Xcode 13.2.1 warns on these functions, despite the lambdas being declared inside an @availability-checked block. --- .../juce_audio_plugin_client/juce_audio_plugin_client_AU_1.mm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/juce_audio_plugin_client/juce_audio_plugin_client_AU_1.mm b/modules/juce_audio_plugin_client/juce_audio_plugin_client_AU_1.mm index 8e1375263b74..7057a0f7f923 100644 --- a/modules/juce_audio_plugin_client/juce_audio_plugin_client_AU_1.mm +++ b/modules/juce_audio_plugin_client/juce_audio_plugin_client_AU_1.mm @@ -2193,7 +2193,9 @@ void pushMidiOutput ([[maybe_unused]] UInt32 nFrames) noexcept const auto init = [&] { + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wunguarded-availability-new") end = MIDIEventListInit (&stackList, kMIDIProtocol_1_0); + JUCE_END_IGNORE_WARNINGS_GCC_LIKE }; const auto send = [&] @@ -2206,6 +2208,7 @@ void pushMidiOutput ([[maybe_unused]] UInt32 nFrames) noexcept static_assert (sizeof (uint32_t) == sizeof (UInt32) && alignof (uint32_t) == alignof (UInt32), "If this fails, the cast below will be broken too!"); + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wunguarded-availability-new") using List = struct MIDIEventList; end = MIDIEventListAdd (&stackList, sizeof (List::packet), @@ -2213,6 +2216,7 @@ void pushMidiOutput ([[maybe_unused]] UInt32 nFrames) noexcept (MIDITimeStamp) timeStamp, view.size(), reinterpret_cast (view.data())); + JUCE_END_IGNORE_WARNINGS_GCC_LIKE }; init(); From 4e6440c3d7e7ceb9d49c9e4f9347cde609308603 Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 18 Nov 2024 13:59:32 +0000 Subject: [PATCH 05/76] AAX Client: Remove channel layout compatibility checks It's important that the plugin always returns the full set of available components. The plugin may be scanned by a separate process from the 'main' DAW process, and these processes may report different compatibility levels. If the scanner has more restricted compatibility than the 'main' DAW process, then some channel layouts may not be registered, and will be hidden in the DAW. --- .../juce_audio_plugin_client_AAX.cpp | 42 ++++--------------- 1 file changed, 7 insertions(+), 35 deletions(-) diff --git a/modules/juce_audio_plugin_client/juce_audio_plugin_client_AAX.cpp b/modules/juce_audio_plugin_client/juce_audio_plugin_client_AAX.cpp index 416423fd15bb..abeba58e6555 100644 --- a/modules/juce_audio_plugin_client/juce_audio_plugin_client_AAX.cpp +++ b/modules/juce_audio_plugin_client/juce_audio_plugin_client_AAX.cpp @@ -2546,30 +2546,6 @@ namespace AAXClasses check (desc.AddProcessProc_Native (algorithmProcessCallback, properties)); } - static inline bool hostSupportsStemFormat (AAX_EStemFormat stemFormat, const AAX_IFeatureInfo* featureInfo) - { - if (featureInfo != nullptr) - { - AAX_ESupportLevel supportLevel; - - if (featureInfo->SupportLevel (supportLevel) == AAX_SUCCESS && supportLevel == AAX_eSupportLevel_ByProperty) - { - std::unique_ptr props (featureInfo->AcquireProperties()); - - // Due to a bug in ProTools 12.8, ProTools thinks that AAX_eStemFormat_Ambi_1_ACN is not supported - // To workaround this bug, check if ProTools supports AAX_eStemFormat_Ambi_2_ACN, and, if yes, - // we can safely assume that it will also support AAX_eStemFormat_Ambi_1_ACN - if (stemFormat == AAX_eStemFormat_Ambi_1_ACN) - stemFormat = AAX_eStemFormat_Ambi_2_ACN; - - if (props != nullptr && props->GetProperty ((AAX_EProperty) stemFormat, (AAX_CPropertyValue*) &supportLevel) != 0) - return (supportLevel == AAX_eSupportLevel_Supported); - } - } - - return (AAX_STEM_FORMAT_INDEX (stemFormat) <= 12); - } - static void getPlugInDescription (AAX_IEffectDescriptor& descriptor, [[maybe_unused]] const AAX_IFeatureInfo* featureInfo) { auto plugin = createPluginFilterOfType (AudioProcessor::wrapperType_AAX); @@ -2628,19 +2604,15 @@ namespace AAXClasses auto aaxOutFormat = numOuts > 0 ? aaxFormats[outIdx] : AAX_eStemFormat_None; auto outLayout = channelSetFromStemFormat (aaxOutFormat, false); - if (hostSupportsStemFormat (aaxInFormat, featureInfo) - && hostSupportsStemFormat (aaxOutFormat, featureInfo)) - { - AudioProcessor::BusesLayout fullLayout; + AudioProcessor::BusesLayout fullLayout; - if (! JuceAAX_Processor::fullBusesLayoutFromMainLayout (*plugin, inLayout, outLayout, fullLayout)) - continue; + if (! JuceAAX_Processor::fullBusesLayoutFromMainLayout (*plugin, inLayout, outLayout, fullLayout)) + continue; - if (auto* desc = descriptor.NewComponentDescriptor()) - { - createDescriptor (*desc, fullLayout, *plugin, pluginIds, numMeters); - check (descriptor.AddComponent (desc)); - } + if (auto* desc = descriptor.NewComponentDescriptor()) + { + createDescriptor (*desc, fullLayout, *plugin, pluginIds, numMeters); + check (descriptor.AddComponent (desc)); } } } From 04fa895f385ad6de7fe1838bfb1d88922412f1f7 Mon Sep 17 00:00:00 2001 From: reuk Date: Thu, 21 Nov 2024 13:26:44 +0000 Subject: [PATCH 06/76] AAX Client: Enable host-provided editor when plugin does not supply an editor --- .../juce_audio_plugin_client/juce_audio_plugin_client_AAX.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/juce_audio_plugin_client/juce_audio_plugin_client_AAX.cpp b/modules/juce_audio_plugin_client/juce_audio_plugin_client_AAX.cpp index abeba58e6555..ade6f87bd9a9 100644 --- a/modules/juce_audio_plugin_client/juce_audio_plugin_client_AAX.cpp +++ b/modules/juce_audio_plugin_client/juce_audio_plugin_client_AAX.cpp @@ -2466,6 +2466,9 @@ namespace AAXClasses properties->AddProperty (AAX_eProperty_InputStemFormat, static_cast (aaxInputFormat)); properties->AddProperty (AAX_eProperty_OutputStemFormat, static_cast (aaxOutputFormat)); + // If the plugin doesn't have an editor, ask the host to provide one + properties->AddProperty (AAX_eProperty_UsesClientGUI, static_cast (! processor.hasEditor())); + const auto& extensions = processor.getAAXClientExtensions(); // This value needs to match the RTAS wrapper's Type ID, so that From 6e910d801050e017a590a7ac2d0d2f3ea0bd187f Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 18 Nov 2024 13:32:00 +0000 Subject: [PATCH 07/76] VST2 Client: Avoid C-style casts of function pointers --- .../juce_audio_plugin_client_VST2.cpp | 87 ++++++++++--------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/modules/juce_audio_plugin_client/juce_audio_plugin_client_VST2.cpp b/modules/juce_audio_plugin_client/juce_audio_plugin_client_VST2.cpp index f3702c6c95c6..629185e1b6c6 100644 --- a/modules/juce_audio_plugin_client/juce_audio_plugin_client_VST2.cpp +++ b/modules/juce_audio_plugin_client/juce_audio_plugin_client_VST2.cpp @@ -274,10 +274,38 @@ class JuceVSTWrapper final : public AudioProcessorListener, memset (&vstEffect, 0, sizeof (vstEffect)); vstEffect.magic = 0x56737450 /* 'VstP' */; - vstEffect.dispatcher = (Vst2::AEffectDispatcherProc) dispatcherCB; + vstEffect.dispatcher = [] (Vst2::AEffect* vstInterface, + Vst2::VstInt32 opCode, + Vst2::VstInt32 index, + Vst2::VstIntPtr value, + void* ptr, + float opt) -> Vst2::VstIntPtr + { + auto* wrapper = getWrapper (vstInterface); + VstOpCodeArguments args = { index, value, ptr, opt }; + + if (opCode == Vst2::effClose) + { + wrapper->dispatcher (opCode, args); + delete wrapper; + return 1; + } + + return wrapper->dispatcher (opCode, args); + }; + vstEffect.process = nullptr; - vstEffect.setParameter = (Vst2::AEffectSetParameterProc) setParameterCB; - vstEffect.getParameter = (Vst2::AEffectGetParameterProc) getParameterCB; + + vstEffect.setParameter = [] (Vst2::AEffect* vstInterface, Vst2::VstInt32 index, float value) + { + getWrapper (vstInterface)->setParameter (index, value); + }; + + vstEffect.getParameter = [] (Vst2::AEffect* vstInterface, Vst2::VstInt32 index) -> float + { + return getWrapper (vstInterface)->getParameter (index); + }; + vstEffect.numPrograms = jmax (1, processor->getNumPrograms()); vstEffect.numParams = juceParameters.getNumParameters(); vstEffect.numInputs = maxNumInChannels; @@ -292,8 +320,21 @@ class JuceVSTWrapper final : public AudioProcessorListener, vstEffect.version = JucePlugin_VersionCode; #endif - vstEffect.processReplacing = (Vst2::AEffectProcessProc) processReplacingCB; - vstEffect.processDoubleReplacing = (Vst2::AEffectProcessDoubleProc) processDoubleReplacingCB; + vstEffect.processReplacing = [] (Vst2::AEffect* vstInterface, + float** inputs, + float** outputs, + Vst2::VstInt32 sampleFrames) + { + getWrapper (vstInterface)->processReplacing (inputs, outputs, sampleFrames); + }; + + vstEffect.processDoubleReplacing = [] (Vst2::AEffect* vstInterface, + double** inputs, + double** outputs, + Vst2::VstInt32 sampleFrames) + { + getWrapper (vstInterface)->processDoubleReplacing (inputs, outputs, sampleFrames); + }; vstEffect.flags |= Vst2::effFlagsHasEditor; @@ -505,22 +546,12 @@ class JuceVSTWrapper final : public AudioProcessorListener, internalProcessReplacing (inputs, outputs, sampleFrames, floatTempBuffers); } - static void processReplacingCB (Vst2::AEffect* vstInterface, float** inputs, float** outputs, int32 sampleFrames) - { - getWrapper (vstInterface)->processReplacing (inputs, outputs, sampleFrames); - } - void processDoubleReplacing (double** inputs, double** outputs, int32 sampleFrames) { jassert (processor->isUsingDoublePrecision()); internalProcessReplacing (inputs, outputs, sampleFrames, doubleTempBuffers); } - static void processDoubleReplacingCB (Vst2::AEffect* vstInterface, double** inputs, double** outputs, int32 sampleFrames) - { - getWrapper (vstInterface)->processDoubleReplacing (inputs, outputs, sampleFrames); - } - //============================================================================== void resume() { @@ -678,22 +709,12 @@ class JuceVSTWrapper final : public AudioProcessorListener, return 0.0f; } - static float getParameterCB (Vst2::AEffect* vstInterface, int32 index) - { - return getWrapper (vstInterface)->getParameter (index); - } - void setParameter (int32 index, float value) { if (auto* param = juceParameters.getParamForIndex (index)) setValueAndNotifyIfChanged (*param, value); } - static void setParameterCB (Vst2::AEffect* vstInterface, int32 index, float value) - { - getWrapper (vstInterface)->setParameter (index, value); - } - void audioProcessorParameterChanged (AudioProcessor*, int index, float newValue) override { if (inParameterChangedCallback.get()) @@ -902,22 +923,6 @@ class JuceVSTWrapper final : public AudioProcessorListener, } } - static pointer_sized_int dispatcherCB (Vst2::AEffect* vstInterface, int32 opCode, int32 index, - pointer_sized_int value, void* ptr, float opt) - { - auto* wrapper = getWrapper (vstInterface); - VstOpCodeArguments args = { index, value, ptr, opt }; - - if (opCode == Vst2::effClose) - { - wrapper->dispatcher (opCode, args); - delete wrapper; - return 1; - } - - return wrapper->dispatcher (opCode, args); - } - //============================================================================== // A component to hold the AudioProcessorEditor, and cope with some housekeeping // chores when it changes or repaints. From 73cb3b88adbce84abd30ed2bef765278241d2ca1 Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 18 Nov 2024 13:32:07 +0000 Subject: [PATCH 08/76] VST2 Host: Avoid C-style casts of function pointers --- .../format_types/juce_VSTPluginFormat.cpp | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp b/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp index e1c214c3cc56..08b48f1f32de 100644 --- a/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp +++ b/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp @@ -81,7 +81,7 @@ JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4355) #endif #ifndef JUCE_VST_WRAPPER_INVOKE_MAIN -#define JUCE_VST_WRAPPER_INVOKE_MAIN effect = module->moduleMain ((Vst2::audioMasterCallback) &audioMaster); +#define JUCE_VST_WRAPPER_INVOKE_MAIN effect = module->moduleMain (audioMaster); #endif #ifndef JUCE_VST_FALLBACK_HOST_NAME @@ -222,8 +222,7 @@ namespace } //============================================================================== -typedef Vst2::AEffect* (VSTCALLBACK *MainCall) (Vst2::audioMasterCallback); -static pointer_sized_int VSTCALLBACK audioMaster (Vst2::AEffect*, int32, int32, pointer_sized_int, void*, float); +using MainCall = Vst2::AEffect* (VSTCALLBACK*) (Vst2::audioMasterCallback); //============================================================================== // Change this to disable logging of various VST activities @@ -2156,6 +2155,20 @@ struct VSTPluginInstance final : public AudioPluginInstance, UseResFile (module->resFileId); #endif + constexpr Vst2::audioMasterCallback audioMaster = [] (Vst2::AEffect* eff, + Vst2::VstInt32 opcode, + Vst2::VstInt32 index, + Vst2::VstIntPtr value, + void* ptr, + float opt) -> Vst2::VstIntPtr + { + if (eff != nullptr) + if (auto* instance = (VSTPluginInstance*) (eff->resvd2)) + return instance->handleCallback (opcode, index, value, ptr, opt); + + return VSTPluginInstance::handleGeneralCallback (opcode, index, value, ptr, opt); + }; + { JUCE_VST_WRAPPER_INVOKE_MAIN } @@ -3458,15 +3471,6 @@ bool VSTPluginInstance::updateSizeFromEditor ([[maybe_unused]] int w, [[maybe_un //============================================================================== // entry point for all callbacks from the plugin -static pointer_sized_int VSTCALLBACK audioMaster (Vst2::AEffect* effect, int32 opcode, int32 index, pointer_sized_int value, void* ptr, float opt) -{ - if (effect != nullptr) - if (auto* instance = (VSTPluginInstance*) (effect->resvd2)) - return instance->handleCallback (opcode, index, value, ptr, opt); - - return VSTPluginInstance::handleGeneralCallback (opcode, index, value, ptr, opt); -} - //============================================================================== VSTPluginFormat::VSTPluginFormat() {} VSTPluginFormat::~VSTPluginFormat() {} From d4107836cd8041d77641580fd00bdff465a88e07 Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 18 Nov 2024 21:01:52 +0000 Subject: [PATCH 09/76] Grid: Convert nonstatic member function to static --- modules/juce_gui_basics/layout/juce_Grid.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/juce_gui_basics/layout/juce_Grid.cpp b/modules/juce_gui_basics/layout/juce_Grid.cpp index 2268cb11b634..650dcf9d9e20 100644 --- a/modules/juce_gui_basics/layout/juce_Grid.cpp +++ b/modules/juce_gui_basics/layout/juce_Grid.cpp @@ -633,6 +633,8 @@ struct Grid::Helpers //============================================================================== struct AutoPlacement { + AutoPlacement() = delete; + using ItemPlacementArray = Array>; //============================================================================== @@ -841,7 +843,7 @@ struct Grid::Helpers } //============================================================================== - ItemPlacementArray deduceAllItems (Grid& grid) const + static ItemPlacementArray deduceAllItems (Grid& grid) { const auto namedAreas = PlacementHelpers::deduceNamedAreas (grid.templateAreas); @@ -1112,7 +1114,7 @@ float Grid::TrackInfo::getAbsoluteSize (float relativeFractionalUnit) const //============================================================================== void Grid::performLayout (Rectangle targetArea) { - const auto itemsAndAreas = Helpers::AutoPlacement().deduceAllItems (*this); + const auto itemsAndAreas = Helpers::AutoPlacement::deduceAllItems (*this); auto implicitTracks = Helpers::AutoPlacement::createImplicitTracks (*this, itemsAndAreas); From 8fa09ae8ab826c6e6d5d7657b3ce9dcd5203362a Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 18 Nov 2024 21:08:38 +0000 Subject: [PATCH 10/76] Grid: Refactor to move columnFirst data member to set Comparator --- modules/juce_gui_basics/layout/juce_Grid.cpp | 37 +++++++++----------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/modules/juce_gui_basics/layout/juce_Grid.cpp b/modules/juce_gui_basics/layout/juce_Grid.cpp index 650dcf9d9e20..4664dcc10202 100644 --- a/modules/juce_gui_basics/layout/juce_Grid.cpp +++ b/modules/juce_gui_basics/layout/juce_Grid.cpp @@ -651,7 +651,7 @@ struct Grid::Helpers { for (int i = 0; i < columnSpan; i++) for (int j = 0; j < rowSpan; j++) - setCell (cell.column + i, cell.row + j); + occupiedCells.insert ({ cell.column + i, cell.row + j }); return { { cell.column, cell.column + columnSpan }, { cell.row, cell.row + rowSpan } }; } @@ -700,36 +700,30 @@ struct Grid::Helpers } private: - struct SortableCell + struct Comparator { - int column, row; - bool columnFirst; - - bool operator< (const SortableCell& other) const + bool operator() (const Cell& a, const Cell& b) const { if (columnFirst) { - if (row == other.row) - return column < other.column; + if (a.row == b.row) + return a.column < b.column; - return row < other.row; + return a.row < b.row; } - if (row == other.row) - return column < other.column; + if (a.row == b.row) + return a.column < b.column; - return row < other.row; + return a.row < b.row; } - }; - void setCell (int column, int row) - { - occupiedCells.insert ({ column, row, columnFirst }); - } + const bool columnFirst; + }; bool isOccupied (Cell cell) const { - return occupiedCells.count ({ cell.column, cell.row, columnFirst }) > 0; + return occupiedCells.count (cell) > 0; } bool isOccupied (Cell cell, int columnSpan, int rowSpan) const @@ -780,8 +774,11 @@ struct Grid::Helpers } int highestCrossDimension; - bool columnFirst; - std::set occupiedCells; + const bool columnFirst; + std::set occupiedCells { Comparator { columnFirst } }; + + JUCE_DECLARE_NON_COPYABLE (OccupancyPlane) + JUCE_DECLARE_NON_MOVEABLE (OccupancyPlane) }; //============================================================================== From b72e43620ccb63a5b39fa6982a92eca2bc189dcc Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 18 Nov 2024 21:14:44 +0000 Subject: [PATCH 11/76] Grid: Fix cell ordering comparison Previously, the set Comparator behaved the same way, regardless of the value of columnFirst. This is incorrect; the set should be sorted such that the final item in the set has the greatest cross-dimension. There was also an off-by-one error in the result of getHighestCrossDimension(). The highestCrossDimension data member is exclusive, but the cell values in the occupiedCells set are inclusive. We need to add 1 to the maximum cell cross-dimension in order to convert it to an exclusive value. --- modules/juce_gui_basics/layout/juce_Grid.cpp | 115 +++++++++++++++---- 1 file changed, 95 insertions(+), 20 deletions(-) diff --git a/modules/juce_gui_basics/layout/juce_Grid.cpp b/modules/juce_gui_basics/layout/juce_Grid.cpp index 4664dcc10202..5d78080d7eae 100644 --- a/modules/juce_gui_basics/layout/juce_Grid.cpp +++ b/modules/juce_gui_basics/layout/juce_Grid.cpp @@ -702,23 +702,19 @@ struct Grid::Helpers private: struct Comparator { - bool operator() (const Cell& a, const Cell& b) const - { - if (columnFirst) - { - if (a.row == b.row) - return a.column < b.column; - - return a.row < b.row; - } + using Tie = std::tuple (*) (const Cell&); - if (a.row == b.row) - return a.column < b.column; + explicit Comparator (bool columnFirstIn) + : tie (columnFirstIn ? Tie { [] (const Cell& x) { return std::tuple (x.row, x.column); } } + : Tie { [] (const Cell& x) { return std::tuple (x.column, x.row); } }) + {} - return a.row < b.row; + bool operator() (const Cell& a, const Cell& b) const + { + return tie (a) < tie (b); } - const bool columnFirst; + const Tie tie; }; bool isOccupied (Cell cell) const @@ -746,20 +742,20 @@ struct Grid::Helpers int getHighestCrossDimension() const { - Cell cell { 1, 1 }; - - if (occupiedCells.size() > 0) - cell = { occupiedCells.crbegin()->column, occupiedCells.crbegin()->row }; + const auto cell = occupiedCells.empty() ? Cell { 1, 1 } + : *std::prev (occupiedCells.end()); - return std::max (getCrossDimension (cell), highestCrossDimension); + return std::max (getCrossDimension (cell) + 1, highestCrossDimension); } Cell advance (Cell cell) const { - if ((getCrossDimension (cell) + 1) >= getHighestCrossDimension()) + const auto next = getCrossDimension (cell) + 1; + + if (next >= getHighestCrossDimension()) return fromDimensions (getMainDimension (cell) + 1, 1); - return fromDimensions (getMainDimension (cell), getCrossDimension (cell) + 1); + return fromDimensions (getMainDimension (cell), next); } int getMainDimension (Cell cell) const { return columnFirst ? cell.column : cell.row; } @@ -1640,6 +1636,85 @@ struct GridTests final : public UnitTest evaluateInvariants (randomSolution); } } + + { + beginTest ("Cell orderings for row and columns work correctly"); + + const Rectangle bounds { 0, 0, 200, 200 }; + + Grid grid; + grid.autoColumns = Grid::TrackInfo { Grid::Fr { 1 } }; + grid.autoRows = Grid::TrackInfo { Grid::Fr { 1 } }; + + grid.items = { GridItem{}.withArea (1, 20), + GridItem{}.withArea (2, 10), + GridItem{}.withArea (GridItem::Span { 1 }, GridItem::Span { 15 }), + GridItem{} }; + + grid.autoFlow = Grid::AutoFlow::row; + grid.performLayout (bounds); + expect (grid.items.getLast().currentBounds == Rect { 150, 0, 10, 100 }); + + grid.autoFlow = Grid::AutoFlow::column; + grid.performLayout (bounds); + expect (grid.items.getLast().currentBounds == Rect { 0, 100, 10, 100 }); + + grid.items = { GridItem{}.withArea (20, 1), + GridItem{}.withArea (10, 2), + GridItem{}.withArea (GridItem::Span { 15 }, GridItem::Span { 1 }), + GridItem{} }; + + grid.autoFlow = Grid::AutoFlow::row; + grid.performLayout (bounds); + expect (grid.items.getLast().currentBounds == Rect { 100, 0, 100, 10 }); + + grid.autoFlow = Grid::AutoFlow::column; + grid.performLayout (bounds); + expect (grid.items.getLast().currentBounds == Rect { 0, 150, 100, 10 }); + } + + beginTest ("Complex grid layout"); + { + Grid grid; + + using Track = Grid::TrackInfo; + + grid.templateRows = { Track (1_fr), Track (1_fr), Track (1_fr) }; + grid.templateColumns = { Track (1_fr), Track (1_fr), Track (1_fr) }; + + grid.autoColumns = Track (1_fr); + grid.autoRows = Track (1_fr); + + grid.autoFlow = Grid::AutoFlow::column; + + grid.items.addArray ({ GridItem().withArea (2, 2, 4, 4), + GridItem(), + GridItem().withArea ({}, 3), + GridItem(), + GridItem().withArea (GridItem::Span (2), {}), + GridItem(), + GridItem(), + GridItem(), + GridItem(), + GridItem(), + GridItem(), + GridItem() }); + + grid.performLayout ({ 60, 30 }); + + expect (grid.items[0] .currentBounds == Rect { 10, 10, 20, 20 }); + expect (grid.items[1] .currentBounds == Rect { 0, 0, 10, 10 }); + expect (grid.items[2] .currentBounds == Rect { 20, 0, 10, 10 }); + expect (grid.items[3] .currentBounds == Rect { 0, 10, 10, 10 }); + expect (grid.items[4] .currentBounds == Rect { 30, 0, 10, 20 }); + expect (grid.items[5] .currentBounds == Rect { 30, 20, 10, 10 }); + expect (grid.items[6] .currentBounds == Rect { 40, 0, 10, 10 }); + expect (grid.items[7] .currentBounds == Rect { 40, 10, 10, 10 }); + expect (grid.items[8] .currentBounds == Rect { 40, 20, 10, 10 }); + expect (grid.items[9] .currentBounds == Rect { 50, 0, 10, 10 }); + expect (grid.items[10].currentBounds == Rect { 50, 10, 10, 10 }); + expect (grid.items[11].currentBounds == Rect { 50, 20, 10, 10 }); + } } }; From 9de56d0aabbe8a471a790c691c3942dd3b7548df Mon Sep 17 00:00:00 2001 From: Christian Haase Date: Fri, 15 Nov 2024 10:09:06 +0100 Subject: [PATCH 12/76] PluginListComponent: Add missing `TRANS` statements --- .../scanning/juce_PluginListComponent.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/juce_audio_processors/scanning/juce_PluginListComponent.cpp b/modules/juce_audio_processors/scanning/juce_PluginListComponent.cpp index 3c748685d9ec..e1484c151877 100644 --- a/modules/juce_audio_processors/scanning/juce_PluginListComponent.cpp +++ b/modules/juce_audio_processors/scanning/juce_PluginListComponent.cpp @@ -404,7 +404,7 @@ PluginListComponent::PluginListComponent (AudioPluginFormatManager& manager, Kno : formatManager (manager), list (listToEdit), deadMansPedalFile (deadMansPedal), - optionsButton ("Options..."), + optionsButton (TRANS ("Options...")), propertiesToUse (props), allowAsync (allowPluginsWhichRequireAsynchronousInstantiation), numThreads (allowAsync ? 1 : 0) @@ -554,7 +554,7 @@ PopupMenu PluginListComponent::createOptionsMenu() for (auto format : formatManager.getFormats()) if (format->canScanForPlugins()) - menu.addItem (PopupMenu::Item ("Remove all " + format->getName() + " plug-ins") + menu.addItem (PopupMenu::Item (TRANS ("Remove all XFMTX plug-ins").replace ("XFMTX", format->getName())) .setEnabled (! list.getTypesForFormat (*format).isEmpty()) .setAction ([this, format] { @@ -583,7 +583,7 @@ PopupMenu PluginListComponent::createOptionsMenu() for (auto format : formatManager.getFormats()) if (format->canScanForPlugins()) - menu.addItem (PopupMenu::Item ("Scan for new or updated " + format->getName() + " plug-ins") + menu.addItem (PopupMenu::Item (TRANS ("Scan for new or updated XFMTX plug-ins").replace ("XFMTX", format->getName())) .setAction ([this, format] { scanFor (*format); })); return menu; From 55fb6dbe621a1855f2b4900c357c9fb99f98afed Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 18 Nov 2024 21:54:08 +0000 Subject: [PATCH 13/76] iOS Audio: Ensure current sampleRate and bufferSize are always updated after querying sample rates The code added in 6f20de54349470aff16453052a82aa7e8e0aea26 was only executed when no explicit sample rates were set. Now, the sample rate is always updated after querying available sample rates and before querying available buffer sizes, so that the buffer size check is guaranteed to use an up-to-date samplerate value. --- modules/juce_audio_devices/native/juce_Audio_ios.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/modules/juce_audio_devices/native/juce_Audio_ios.cpp b/modules/juce_audio_devices/native/juce_Audio_ios.cpp index 709777cec0f2..e7b1c5fbe12f 100644 --- a/modules/juce_audio_devices/native/juce_Audio_ios.cpp +++ b/modules/juce_audio_devices/native/juce_Audio_ios.cpp @@ -518,10 +518,6 @@ struct iOSAudioIODevice::Pimpl final : public AsyncUpdater availableSampleRates.addIfNotAlreadyThere (highestRate); - // Reset sample rate back to the original, so that we don't end up stuck on the highest rate - sampleRate = trySampleRate (sampleRate); - bufferSize = getBufferSize (sampleRate); - AudioUnitAddPropertyListener (audioUnit, kAudioUnitProperty_StreamFormat, dispatchAudioUnitPropertyChange, @@ -553,6 +549,13 @@ struct iOSAudioIODevice::Pimpl final : public AsyncUpdater JUCE_IOS_AUDIO_LOG ("Updating hardware info"); updateAvailableSampleRates(); + + // The sample rate and buffer size may have been affected by + // updateAvailableSampleRates(), so try restoring the last good + // sample rate + sampleRate = trySampleRate (sampleRate); + bufferSize = getBufferSize (sampleRate); + updateAvailableBufferSizes(); if (deviceType != nullptr) From 49477bf0e50990eb7e78d49f656c8d73c4eea02b Mon Sep 17 00:00:00 2001 From: reuk Date: Wed, 20 Nov 2024 13:04:06 +0000 Subject: [PATCH 14/76] LICENSE: Update path to choc --- LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index fa9877036504..39e3c57e1725 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -42,7 +42,7 @@ The JUCE modules contain the following dependencies: - [GLEW](modules/juce_opengl/opengl/juce_gl.h) ([BSD](modules/juce_opengl/opengl/juce_gl.h)), including [Mesa](modules/juce_opengl/opengl/juce_gl.h) ([MIT](modules/juce_opengl/opengl/juce_gl.h)) and [Khronos](modules/juce_opengl/opengl/juce_gl.h) ([MIT](modules/juce_opengl/opengl/juce_gl.h)) - [Ogg Vorbis](modules/juce_audio_formats/codecs/oggvorbis/) ([BSD](modules/juce_audio_formats/codecs/oggvorbis/Ogg%20Vorbis%20Licence.txt)) - [jpeglib](modules/juce_graphics/image_formats/jpglib/) ([Independent JPEG Group License](modules/juce_graphics/image_formats/jpglib/README)) -- [CHOC](modules/juce_core/javascript/choc/) ([ISC](modules/juce_core/javascript/choc/LICENSE.md)), including [QuickJS](modules/juce_core/javascript/choc/javascript/choc_javascript_QuickJS.h) ([MIT](modules/juce_core/javascript/choc/javascript/choc_javascript_QuickJS.h)) +- [CHOC](modules/juce_javascript/choc/) ([ISC](modules/juce_javascript/choc/LICENSE.md)), including [QuickJS](modules/juce_javascript/choc/javascript/choc_javascript_QuickJS.h) ([MIT](modules/juce_javascript/choc/javascript/choc_javascript_QuickJS.h)) - [LV2](modules/juce_audio_processors/format_types/LV2_SDK/) ([ISC](modules/juce_audio_processors/format_types/LV2_SDK/lv2/COPYING)) - [pslextensions](modules/juce_audio_processors/format_types/pslextensions/ipslcontextinfo.h) ([Public domain](modules/juce_audio_processors/format_types/pslextensions/ipslcontextinfo.h)) - [AAX](modules/juce_audio_plugin_client/AAX/SDK/) ([Proprietary Avid AAX License/GPLv3](modules/juce_audio_plugin_client/AAX/SDK/LICENSE.txt)) From f0928ebd6e3ff1303785abbc1aadf134b628336f Mon Sep 17 00:00:00 2001 From: reuk Date: Thu, 21 Nov 2024 22:19:29 +0000 Subject: [PATCH 15/76] Windowing: Fix link of UserNotifications framework on iOS Since eb6ebaf5, juce_gui_basics always depends on UserNotifications when building for iOS. --- modules/juce_gui_basics/juce_gui_basics.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/juce_gui_basics/juce_gui_basics.h b/modules/juce_gui_basics/juce_gui_basics.h index d15687204fd1..387927ed0a94 100644 --- a/modules/juce_gui_basics/juce_gui_basics.h +++ b/modules/juce_gui_basics/juce_gui_basics.h @@ -55,7 +55,7 @@ OSXFrameworks: Cocoa QuartzCore WeakOSXFrameworks: Metal MetalKit iOSFrameworks: CoreServices UIKit - WeakiOSFrameworks: Metal MetalKit UniformTypeIdentifiers + WeakiOSFrameworks: Metal MetalKit UniformTypeIdentifiers UserNotifications END_JUCE_MODULE_DECLARATION From 39b335ccef6c545efa71649e009d9cce6633496d Mon Sep 17 00:00:00 2001 From: tpoole Date: Fri, 22 Nov 2024 11:14:10 +0000 Subject: [PATCH 16/76] Make building with MinGW a compiler error --- extras/Build/CMake/JUCEHelperTargets.cmake | 6 +++--- extras/Build/CMake/JUCEModuleSupport.cmake | 10 +--------- extras/Build/CMake/JUCEUtils.cmake | 9 --------- modules/juce_core/native/juce_Threads_windows.cpp | 2 +- modules/juce_core/system/juce_TargetPlatform.h | 8 +------- 5 files changed, 6 insertions(+), 29 deletions(-) diff --git a/extras/Build/CMake/JUCEHelperTargets.cmake b/extras/Build/CMake/JUCEHelperTargets.cmake index 244cccec7101..051296daea7c 100644 --- a/extras/Build/CMake/JUCEHelperTargets.cmake +++ b/extras/Build/CMake/JUCEHelperTargets.cmake @@ -140,9 +140,9 @@ if((CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") OR (CMAKE_CXX_COMPILER_FRONTEND_VARIA $<$:$,-GL,-flto>>) target_link_libraries(juce_recommended_lto_flags INTERFACE $<$:$<$:-LTCG>>) -elseif((NOT MINGW) AND ((CMAKE_CXX_COMPILER_ID STREQUAL "Clang") - OR (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") - OR (CMAKE_CXX_COMPILER_ID STREQUAL "GNU"))) +elseif((CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + OR (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") + OR (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")) target_compile_options(juce_recommended_lto_flags INTERFACE $<$:-flto>) target_link_libraries(juce_recommended_lto_flags INTERFACE $<$:-flto>) # Xcode 15.0 requires this flag to avoid a compiler bug diff --git a/extras/Build/CMake/JUCEModuleSupport.cmake b/extras/Build/CMake/JUCEModuleSupport.cmake index 0d93d36033a6..03378a2090bf 100644 --- a/extras/Build/CMake/JUCEModuleSupport.cmake +++ b/extras/Build/CMake/JUCEModuleSupport.cmake @@ -68,9 +68,7 @@ endfunction() if((CMAKE_SYSTEM_NAME STREQUAL "Windows") OR (CMAKE_SYSTEM_NAME STREQUAL "Linux") - OR (CMAKE_SYSTEM_NAME MATCHES ".*BSD") - OR MSYS - OR MINGW) + OR (CMAKE_SYSTEM_NAME MATCHES ".*BSD")) # If you really need to override the detected arch for some reason, # you can configure the build with -DJUCE_TARGET_ARCHITECTURE= if(NOT DEFINED JUCE_TARGET_ARCHITECTURE) @@ -654,12 +652,6 @@ function(juce_add_module module_path) endif() _juce_link_libs_from_metadata("${module_name}" "${metadata_dict}" windowsLibs) - elseif(MSYS OR MINGW) - if(module_name STREQUAL "juce_gui_basics") - target_compile_options(${module_name} INTERFACE "-Wa,-mbig-obj") - endif() - - _juce_link_libs_from_metadata("${module_name}" "${metadata_dict}" mingwLibs) endif() endif() diff --git a/extras/Build/CMake/JUCEUtils.cmake b/extras/Build/CMake/JUCEUtils.cmake index 8a6b0d6e1528..8daccbaa5f16 100644 --- a/extras/Build/CMake/JUCEUtils.cmake +++ b/extras/Build/CMake/JUCEUtils.cmake @@ -1051,10 +1051,6 @@ function(_juce_add_vst3_manifest_helper_target) target_compile_options(juce_vst3_helper PRIVATE -fobjc-arc) endif() - if(MSYS OR MINGW) - target_link_options(juce_vst3_helper PRIVATE -municode) - endif() - set_target_properties(juce_vst3_helper PROPERTIES BUILD_WITH_INSTALL_RPATH ON) set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) @@ -1083,11 +1079,6 @@ function(juce_enable_vst3_manifest_step shared_code_target) "juce_enable_copy_plugin_step too.") endif() - if((MSYS OR MINGW) AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9) - message(WARNING "VST3 manifest generation is disabled for ${shared_code_target} because the compiler is not supported.") - return() - endif() - if(CMAKE_SYSTEM_NAME STREQUAL "Windows" AND NOT JUCE_WINDOWS_HELPERS_CAN_RUN) message(WARNING "VST3 manifest generation is disabled for ${shared_code_target} because a " "${JUCE_TARGET_ARCHITECTURE} manifest helper cannot run on a host system processor detected to be " diff --git a/modules/juce_core/native/juce_Threads_windows.cpp b/modules/juce_core/native/juce_Threads_windows.cpp index be21a44c637a..a459a7800e21 100644 --- a/modules/juce_core/native/juce_Threads_windows.cpp +++ b/modules/juce_core/native/juce_Threads_windows.cpp @@ -325,7 +325,7 @@ void DynamicLibrary::close() void* DynamicLibrary::getFunction (const String& functionName) noexcept { - return handle != nullptr ? (void*) GetProcAddress ((HMODULE) handle, functionName.toUTF8()) // (void* cast is required for mingw) + return handle != nullptr ? (void*) GetProcAddress ((HMODULE) handle, functionName.toUTF8()) : nullptr; } diff --git a/modules/juce_core/system/juce_TargetPlatform.h b/modules/juce_core/system/juce_TargetPlatform.h index 0c55ad7c586a..ee433d33873c 100644 --- a/modules/juce_core/system/juce_TargetPlatform.h +++ b/modules/juce_core/system/juce_TargetPlatform.h @@ -110,13 +110,7 @@ #endif #ifdef __MINGW32__ - #define JUCE_MINGW 1 - #warning Support for MinGW has been removed. Please use an alternative compiler. - #ifdef __MINGW64__ - #define JUCE_64BIT 1 - #else - #define JUCE_32BIT 1 - #endif + #error "MinGW is not supported. Please use an alternative compiler." #endif /** If defined, this indicates that the processor is little-endian. */ From ccdd857662a2f0eccdbc5f473896d0409faa9766 Mon Sep 17 00:00:00 2001 From: tpoole Date: Fri, 22 Nov 2024 16:14:11 +0000 Subject: [PATCH 17/76] GitHub: Add more logging to the CLA check --- .github/workflows/check-cla.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/check-cla.yml b/.github/workflows/check-cla.yml index 70d082413d39..c657a4cff283 100644 --- a/.github/workflows/check-cla.yml +++ b/.github/workflows/check-cla.yml @@ -18,6 +18,7 @@ jobs: with urllib.request.urlopen(req) as response: return json.loads(response.read().decode('utf-8')) prCommits = jsonRequest('https://api.github.com/repos/juce-framework/JUCE/pulls/${{ github.event.number }}/commits') + print(f'Commit info:\n{json.dumps(prCommits, indent=4)}') allAuthors = [commit[authorType]['login'] for authorType in ['author', 'committer'] for commit in prCommits if commit[authorType]] uniqueAuthors = [name for name in list(set(allAuthors)) if name != 'web-flow'] if (len(uniqueAuthors) == 0): From af51cb46eb394ceca381bcc9190b42274b292fb1 Mon Sep 17 00:00:00 2001 From: Oliver James Date: Tue, 1 Oct 2024 10:55:12 +0100 Subject: [PATCH 18/76] Projucer: Remove ARM32 support on Windows --- BREAKING_CHANGES.md | 17 +++++++++++++ .../Projucer/Source/Project/jucer_Project.h | 5 +++- .../ProjectSaving/jucer_ProjectExport_MSVC.h | 24 +++++++++++++++---- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 2c1cee87ee5d..0fddbf769406 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -2,6 +2,23 @@ # Version 8.0.4 +## Change + +Support for Arm32 in Projucer has been removed for Windows targets. + +**Possible Issues** + +Projucer projects targeting Arm32 on Windows will no longer be able to select that option. + +**Workaround** + +Select Arm64 or Arm64EC instead of Arm32, and port any 32-bit specific code to 64-bit. + +**Rationale** + +32-bit Arm support has been deprecated in current versions of Windows 11. + + ## Change The Javascript implementation has been moved into a independent juce module. diff --git a/extras/Projucer/Source/Project/jucer_Project.h b/extras/Projucer/Source/Project/jucer_Project.h index 84fc56c56370..a3ce2124bcbe 100644 --- a/extras/Projucer/Source/Project/jucer_Project.h +++ b/extras/Projucer/Source/Project/jucer_Project.h @@ -59,6 +59,7 @@ namespace ProjectMessages DECLARE_ID (pluginCodeInvalid); DECLARE_ID (manufacturerCodeInvalid); DECLARE_ID (deprecatedExporter); + DECLARE_ID (unsupportedArm32Config); DECLARE_ID (notification); DECLARE_ID (warning); @@ -73,7 +74,7 @@ namespace ProjectMessages static Identifier warnings[] = { Ids::cppStandard, Ids::moduleNotFound, Ids::jucePath, Ids::jucerFileModified, Ids::missingModuleDependencies, Ids::oldProjucer, Ids::pluginCodeInvalid, Ids::manufacturerCodeInvalid, - Ids::deprecatedExporter }; + Ids::deprecatedExporter, Ids::unsupportedArm32Config }; if (std::find (std::begin (warnings), std::end (warnings), message) != std::end (warnings)) return Ids::warning; @@ -99,6 +100,7 @@ namespace ProjectMessages if (message == Ids::pluginCodeInvalid) return "Invalid Plugin Code"; if (message == Ids::manufacturerCodeInvalid) return "Invalid Manufacturer Code"; if (message == Ids::deprecatedExporter) return "Deprecated Exporter"; + if (message == Ids::unsupportedArm32Config) return "Unsupported Architecture"; jassertfalse; return {}; @@ -116,6 +118,7 @@ namespace ProjectMessages if (message == Ids::pluginCodeInvalid) return "The plugin code should be exactly four characters in length."; if (message == Ids::manufacturerCodeInvalid) return "The manufacturer code should be exactly four characters in length."; if (message == Ids::deprecatedExporter) return "The project includes a deprecated exporter."; + if (message == Ids::unsupportedArm32Config) return "The project includes a Visual Studio configuration that uses the 32-bit Arm architecture, which is no longer supported. This configuration has been hidden, and will be removed on save."; jassertfalse; return {}; diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h index 626df529081b..455d8d113e5a 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h @@ -148,6 +148,24 @@ class MSVCProjectExporterBase : public ProjectExporter config->getValue (Ids::targetName) = oldStyleLibName; } + { + std::vector toErase; + + for (const auto& config : getConfigurations()) + { + if (config.getProperty (Ids::winArchitecture) == "ARM") + toErase.push_back (config); + } + + if (! toErase.empty()) + { + for (const auto& e : toErase) + e.getParent().removeChild (e, nullptr); + + getProject().addProjectMessage (ProjectMessages::Ids::unsupportedArm32Config, {}); + } + } + for (ConfigIterator i (*this); i.next();) dynamic_cast (*i).updateOldLTOSetting(); } @@ -214,7 +232,6 @@ class MSVCProjectExporterBase : public ProjectExporter String getIntel64BitArchName() const { return "x64"; } String getIntel32BitArchName() const { return "Win32"; } String getArm64BitArchName() const { return "ARM64"; } - String getArm32BitArchName() const { return "ARM"; } String getArchitectureString() const { return architectureTypeValue.get(); } String getDebugInformationFormatString() const { return debugInformationFormatValue.get(); } @@ -258,8 +275,8 @@ class MSVCProjectExporterBase : public ProjectExporter addVisualStudioPluginInstallPathProperties (props); props.add (new ChoicePropertyComponent (architectureTypeValue, "Architecture", - { getIntel32BitArchName(), getIntel64BitArchName(), getArm32BitArchName(), getArm64BitArchName() }, - { getIntel32BitArchName(), getIntel64BitArchName(), getArm32BitArchName(), getArm64BitArchName() }), + { getIntel32BitArchName(), getIntel64BitArchName(), getArm64BitArchName() }, + { getIntel32BitArchName(), getIntel64BitArchName(), getArm64BitArchName() }), "Which Windows architecture to use."); props.add (new ChoicePropertyComponentWithEnablement (debugInformationFormatValue, @@ -399,7 +416,6 @@ class MSVCProjectExporterBase : public ProjectExporter { { "Win32", { "%programfiles(x86)%", "%CommonProgramFiles(x86)%" } }, { "x64", { "%ProgramW6432%", "%CommonProgramW6432%" } }, - { "ARM", { "%programfiles(arm)%", "%CommonProgramFiles(arm)%" } }, { "ARM64", { "%ProgramW6432%", "%CommonProgramW6432%" } } }; From ac0ebe5797a3dcc51d6c04374aa562690fc802f1 Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 28 Oct 2024 13:26:44 +0000 Subject: [PATCH 19/76] Projucer: Add Arm64(EC) support on Windows --- .../ProjectSaving/jucer_ProjectExport_MSVC.h | 922 +++++++++++------- 1 file changed, 575 insertions(+), 347 deletions(-) diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h index 455d8d113e5a..18fc4b4974a0 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h @@ -53,6 +53,45 @@ inline StringArray msBuildEscape (StringArray range) return range; } +enum class Architecture +{ + win32, + win64, + arm64, + arm64ec +}; + +static String toString (Architecture arch) +{ + switch (arch) + { + case Architecture::win32: return "Win32"; + case Architecture::win64: return "x64"; + case Architecture::arm64: return "ARM64"; + case Architecture::arm64ec: return "ARM64EC"; + } + + jassertfalse; + return ""; +} + +static std::optional architectureTypeFromString (const String& string) +{ + constexpr Architecture values[] { Architecture::win32, + Architecture::win64, + Architecture::arm64, + Architecture::arm64ec }; + + for (auto value : values) + { + if (toString (value) == string) + return value; + } + + jassertfalse; + return {}; +} + //============================================================================== class MSVCProjectExporterBase : public ProjectExporter { @@ -125,7 +164,7 @@ class MSVCProjectExporterBase : public ProjectExporter if (oldStylePrebuildCommand.isNotEmpty()) for (ConfigIterator config (*this); config.next();) - dynamic_cast (*config).getValue (Ids::prebuildCommand) = oldStylePrebuildCommand; + static_cast (&*config)->getValue (Ids::prebuildCommand) = oldStylePrebuildCommand; } { @@ -167,7 +206,28 @@ class MSVCProjectExporterBase : public ProjectExporter } for (ConfigIterator i (*this); i.next();) - dynamic_cast (*i).updateOldLTOSetting(); + { + auto& config = *static_cast (&*i); + config.updateOldLTOSetting(); + config.updateOldArchSetting(); + } + } + + Array getAllActiveArchitectures() const + { + Array archs; + + for (ConstConfigIterator i (*this); i.next();) + { + const auto& config = *static_cast (&*i); + const auto configArchs = config.getArchitectures(); + + for (const auto& arch : configArchs) + if (! archs.contains (arch)) + archs.add (arch); + } + + return archs; } void initialiseDependencyPathValues() override @@ -183,8 +243,7 @@ class MSVCProjectExporterBase : public ProjectExporter } //============================================================================== - class MSVCBuildConfiguration final : public BuildConfiguration, - private Value::Listener + class MSVCBuildConfiguration final : public BuildConfiguration { public: MSVCBuildConfiguration (Project& p, const ValueTree& settings, const ProjectExporter& e) @@ -199,7 +258,7 @@ class MSVCProjectExporterBase : public ProjectExporter multiProcessorCompilationValue (config, Ids::multiProcessorCompilation, getUndoManager(), true), intermediatesPathValue (config, Ids::intermediatesPath, getUndoManager()), characterSetValue (config, Ids::characterSet, getUndoManager()), - architectureTypeValue (config, Ids::winArchitecture, getUndoManager(), getIntel64BitArchName()), + architectureTypeValue (config, Ids::winArchitecture, getUndoManager(), Array { toString (Architecture::win64) }, ","), fastMathValue (config, Ids::fastMath, getUndoManager()), debugInformationFormatValue (config, Ids::debugInformationFormat, getUndoManager(), isDebug() ? "ProgramDatabase" : "None"), pluginBinaryCopyStepValue (config, Ids::enablePluginBinaryCopyStep, getUndoManager(), false), @@ -207,32 +266,68 @@ class MSVCProjectExporterBase : public ProjectExporter vst3BinaryLocation (config, Ids::vst3BinaryLocation, getUndoManager()), aaxBinaryLocation (config, Ids::aaxBinaryLocation, getUndoManager()), lv2BinaryLocation (config, Ids::lv2BinaryLocation, getUndoManager()), - unityPluginBinaryLocation (config, Ids::unityPluginBinaryLocation, getUndoManager(), {}) + unityPluginBinaryLocation (config, Ids::unityPluginBinaryLocation, getUndoManager()) { - setPluginBinaryCopyLocationDefaults(); + constexpr std::tuple paths[] + { + { Architecture::win32, "%programfiles(x86)%", "%CommonProgramFiles(x86)%" }, + { Architecture::win64, "%ProgramW6432%", "%CommonProgramW6432%" }, + { Architecture::arm64, "%ProgramW6432%", "%CommonProgramW6432%" }, + { Architecture::arm64ec, "%ProgramW6432%", "%CommonProgramW6432%" } + }; + + for (const auto& [arch, programFolderPath, commonFolderPath] : paths) + { + setBinaryPathDefault (Ids::vstBinaryLocation, arch, programFolderPath + String ("\\Steinberg\\Vstplugins")); + setBinaryPathDefault (Ids::vst3BinaryLocation, arch, commonFolderPath + String ("\\VST3")); + setBinaryPathDefault (Ids::aaxBinaryLocation, arch, commonFolderPath + String ("\\Avid\\Audio\\Plug-Ins")); + setBinaryPathDefault (Ids::lv2BinaryLocation, arch, "%APPDATA%\\LV2"); + } + optimisationLevelValue.setDefault (isDebug() ? optimisationOff : optimiseFull); + } - architectureValueToListenTo = architectureTypeValue.getPropertyAsValue(); - architectureValueToListenTo.addListener (this); + String getBinaryPath (const Identifier& id, Architecture arch) const + { + if (auto* location = getLocationForArchitecture (id, arch)) + return location->get().toString(); + + return ""; + } + + void setBinaryPathDefault (const Identifier& id, Architecture arch, const String& path) + { + if (auto* location = getLocationForArchitecture (id, arch)) + location->setDefault (path); } //============================================================================== int getWarningLevel() const { return warningLevelValue.get(); } bool areWarningsTreatedAsErrors() const { return warningsAreErrorsValue.get(); } + Array getArchitectures() const + { + auto value = architectureTypeValue.get(); + auto* array = value.getArray(); + + if (array == nullptr) + return {}; + + Array result; + result.resize (array->size()); + + std::transform (array->begin(), array->end(), result.begin(), [] (const var& archVar) + { + return *architectureTypeFromString (archVar.toString()); + }); + + return result; + } + String getPrebuildCommandString() const { return prebuildCommandValue.get(); } String getPostbuildCommandString() const { return postbuildCommandValue.get(); } - String getVSTBinaryLocationString() const { return vstBinaryLocation.get(); } - String getVST3BinaryLocationString() const { return vst3BinaryLocation.get(); } - String getAAXBinaryLocationString() const { return aaxBinaryLocation.get();} - String getLV2BinaryLocationString() const { return lv2BinaryLocation.get();} - String getUnityPluginBinaryLocationString() const { return unityPluginBinaryLocation.get(); } String getIntermediatesPathString() const { return intermediatesPathValue.get(); } String getCharacterSetString() const { return characterSetValue.get(); } - String getIntel64BitArchName() const { return "x64"; } - String getIntel32BitArchName() const { return "Win32"; } - String getArm64BitArchName() const { return "ARM64"; } - String getArchitectureString() const { return architectureTypeValue.get(); } String getDebugInformationFormatString() const { return debugInformationFormatValue.get(); } bool shouldGenerateDebugSymbols() const { return generateDebugSymbolsValue.get(); } @@ -243,9 +338,9 @@ class MSVCProjectExporterBase : public ProjectExporter bool isPluginBinaryCopyStepEnabled() const { return pluginBinaryCopyStepValue.get(); } //============================================================================== - String createMSVCConfigName() const + String createMSVCConfigName (Architecture arch) const { - return getName() + "|" + getArchitectureString(); + return getName() + "|" + toString (arch); } String getOutputFilename (const String& suffix, @@ -274,9 +369,20 @@ class MSVCProjectExporterBase : public ProjectExporter if (project.isAudioPluginProject()) addVisualStudioPluginInstallPathProperties (props); - props.add (new ChoicePropertyComponent (architectureTypeValue, "Architecture", - { getIntel32BitArchName(), getIntel64BitArchName(), getArm64BitArchName() }, - { getIntel32BitArchName(), getIntel64BitArchName(), getArm64BitArchName() }), + const auto architectureList = exporter.getExporterIdentifier() == Identifier { "VS2022" } + ? std::vector { Architecture::win32, Architecture::win64, Architecture::arm64, Architecture::arm64ec } + : std::vector { Architecture::win32, Architecture::win64, Architecture::arm64 }; + + Array architectureListAsStrings; + Array architectureListAsVars; + + for (const auto& arch : architectureList) + { + architectureListAsStrings.add (toString (arch)); + architectureListAsVars.add (toString (arch)); + } + + props.add (new MultiChoicePropertyComponent (architectureTypeValue, "Architecture", architectureListAsStrings, architectureListAsVars), "Which Windows architecture to use."); props.add (new ChoicePropertyComponentWithEnablement (debugInformationFormatValue, @@ -361,15 +467,111 @@ class MSVCProjectExporterBase : public ProjectExporter linkTimeOptimisationValue = (static_cast (config ["wholeProgramOptimisation"]) == 0); } + void updateOldArchSetting() + { + if (architectureTypeValue.get().isArray()) + return; + + const auto archString = architectureTypeValue.get().toString(); + const auto archType = architectureTypeFromString (archString); + + if (! archType) + return; + + const auto pluginBinaryPathLocationIds = + { + Ids::vstBinaryLocation, + Ids::vst3BinaryLocation, + Ids::lv2BinaryLocation, + Ids::aaxBinaryLocation, + Ids::unityPluginBinaryLocation + }; + + for (const auto& location : pluginBinaryPathLocationIds) + { + if (auto* prop = config.getPropertyPointer (location)) + { + setBinaryPathDefault (location, *archType, prop->toString()); + } + } + + architectureTypeValue = Array { archString }; + } + private: ValueTreePropertyWithDefault warningLevelValue, warningsAreErrorsValue, prebuildCommandValue, postbuildCommandValue, generateDebugSymbolsValue, enableIncrementalLinkingValue, useRuntimeLibDLLValue, multiProcessorCompilationValue, intermediatesPathValue, characterSetValue, architectureTypeValue, fastMathValue, debugInformationFormatValue, pluginBinaryCopyStepValue; - ValueTreePropertyWithDefault vstBinaryLocation, vst3BinaryLocation, aaxBinaryLocation, lv2BinaryLocation, unityPluginBinaryLocation; + struct LocationProperties + { + LocationProperties (ValueTree& tree, const Identifier& propertyID, UndoManager* um) + : win32 (tree, propertyID + "_Win32", um), + x64 (tree, propertyID + "_x64", um), + arm64 (tree, propertyID + "_arm64", um), + arm64ec (tree, propertyID + "_arm64ec", um) + { + } + + const ValueTreePropertyWithDefault* get (Architecture arch) const + { + return get (*this, arch); + } + + ValueTreePropertyWithDefault* get (Architecture arch) + { + return get (*this, arch); + } + + ValueTreePropertyWithDefault win32, x64, arm64, arm64ec; + + private: + template + static auto get (This& t, Architecture arch) -> decltype (t.get (arch)) + { + switch (arch) + { + case Architecture::win32: return &t.win32; + case Architecture::win64: return &t.x64; + case Architecture::arm64: return &t.arm64; + case Architecture::arm64ec: return &t.arm64ec; + } + + jassertfalse; + return nullptr; + } + }; + + template + static auto getLocationForArchitecture (This& t, const Identifier& id, Architecture arch) + { + const auto properties = + { + std::pair { Ids::vstBinaryLocation, &t.vstBinaryLocation }, + std::pair { Ids::vst3BinaryLocation, &t.vst3BinaryLocation }, + std::pair { Ids::aaxBinaryLocation, &t.aaxBinaryLocation }, + std::pair { Ids::lv2BinaryLocation, &t.lv2BinaryLocation } + }; + + const auto iter = std::find_if (properties.begin(), + properties.end(), + [id] (auto pair) { return id == pair.first; }); - Value architectureValueToListenTo; + return iter != properties.end() ? iter->second->get (arch) : nullptr; + } + + ValueTreePropertyWithDefault* getLocationForArchitecture (const Identifier& id, Architecture arch) + { + return getLocationForArchitecture (*this, id, arch); + } + + const ValueTreePropertyWithDefault* getLocationForArchitecture (const Identifier& id, Architecture arch) const + { + return getLocationForArchitecture (*this, id, arch); + } + + LocationProperties vstBinaryLocation, vst3BinaryLocation, aaxBinaryLocation, lv2BinaryLocation, unityPluginBinaryLocation; //============================================================================== void addVisualStudioPluginInstallPathProperties (PropertyListBuilder& props) @@ -381,60 +583,33 @@ class MSVCProjectExporterBase : public ProjectExporter props.add (new ChoicePropertyComponent (pluginBinaryCopyStepValue, "Enable Plugin Copy Step"), "Enable this to copy plugin binaries to a specified folder after building."); + const auto addLocationProperties = [&] (auto& locationProps, const String& format) + { + for (const auto& [member, arch] : { std::tuple (&LocationProperties::win32, Architecture::win32), + std::tuple (&LocationProperties::x64, Architecture::win64), + std::tuple (&LocationProperties::arm64, Architecture::arm64), + std::tuple (&LocationProperties::arm64ec, Architecture::arm64ec) }) + { + const auto archAndFormat = toString (arch) + " " + format; + props.add (new TextPropertyComponentWithEnablement (locationProps.*member, pluginBinaryCopyStepValue, archAndFormat + " Binary Location", 1024, false), + "The folder in which the compiled " + archAndFormat + " binary should be placed."); + } + }; + if (project.shouldBuildVST3()) - props.add (new TextPropertyComponentWithEnablement (vst3BinaryLocation, pluginBinaryCopyStepValue, "VST3 Binary Location", - 1024, false), - "The folder in which the compiled VST3 binary should be placed."); + addLocationProperties (vst3BinaryLocation, "VST3"); if (project.shouldBuildAAX()) - props.add (new TextPropertyComponentWithEnablement (aaxBinaryLocation, pluginBinaryCopyStepValue, "AAX Binary Location", - 1024, false), - "The folder in which the compiled AAX binary should be placed."); + addLocationProperties (aaxBinaryLocation, "AAX"); if (project.shouldBuildLV2()) - props.add (new TextPropertyComponentWithEnablement (lv2BinaryLocation, pluginBinaryCopyStepValue, "LV2 Binary Location", - 1024, false), - "The folder in which the compiled LV2 binary should be placed."); + addLocationProperties (lv2BinaryLocation, "LV2"); if (project.shouldBuildUnityPlugin()) - props.add (new TextPropertyComponentWithEnablement (unityPluginBinaryLocation, pluginBinaryCopyStepValue, "Unity Binary Location", - 1024, false), - "The folder in which the compiled Unity plugin binary and associated C# GUI script should be placed."); + addLocationProperties (unityPluginBinaryLocation, "Unity"); if (project.shouldBuildVST()) - props.add (new TextPropertyComponentWithEnablement (vstBinaryLocation, pluginBinaryCopyStepValue, "VST (Legacy) Binary Location", - 1024, false), - "The folder in which the compiled legacy VST binary should be placed."); - - } - - void setPluginBinaryCopyLocationDefaults() - { - const auto [programsFolderPath, commonsFolderPath] = [&]() -> std::tuple - { - static const std::map> options - { - { "Win32", { "%programfiles(x86)%", "%CommonProgramFiles(x86)%" } }, - { "x64", { "%ProgramW6432%", "%CommonProgramW6432%" } }, - { "ARM64", { "%ProgramW6432%", "%CommonProgramW6432%" } } - }; - - if (const auto iter = options.find (getArchitectureString()); iter != options.cend()) - return iter->second; - - jassertfalse; - return { "%programfiles%", "%CommonProgramFiles%" }; - }(); - - vstBinaryLocation.setDefault (programsFolderPath + String ("\\Steinberg\\Vstplugins")); - vst3BinaryLocation.setDefault (commonsFolderPath + String ("\\VST3")); - aaxBinaryLocation.setDefault (commonsFolderPath + String ("\\Avid\\Audio\\Plug-Ins")); - lv2BinaryLocation.setDefault ("%APPDATA%\\LV2"); - } - - void valueChanged (Value&) override - { - setPluginBinaryCopyLocationDefaults(); + addLocationProperties (vstBinaryLocation, "VST (Legacy)"); } }; @@ -448,7 +623,7 @@ class MSVCProjectExporterBase : public ProjectExporter projectGuid = createGUID (owner.getProject().getProjectUIDString() + getName()); } - virtual ~MSVCTarget() {} + virtual ~MSVCTarget() = default; String getProjectVersionString() const { return "10.00"; } String getProjectFileSuffix() const { return ".vcxproj"; } @@ -462,17 +637,22 @@ class MSVCProjectExporterBase : public ProjectExporter projectXml.setAttribute ("ToolsVersion", getOwner().getToolsVersion()); projectXml.setAttribute ("xmlns", "http://schemas.microsoft.com/developer/msbuild/2003"); + const auto allArchitectures = owner.getAllActiveArchitectures(); + { auto* configsGroup = projectXml.createNewChildElement ("ItemGroup"); configsGroup->setAttribute ("Label", "ProjectConfigurations"); for (ConstConfigIterator i (owner); i.next();) { - auto& config = dynamic_cast (*i); - auto* e = configsGroup->createNewChildElement ("ProjectConfiguration"); - e->setAttribute ("Include", config.createMSVCConfigName()); - e->createNewChildElement ("Configuration")->addTextElement (config.getName()); - e->createNewChildElement ("Platform")->addTextElement (config.getArchitectureString()); + auto& config = *static_cast (&*i); + for (const auto& arch : allArchitectures) + { + auto* e = configsGroup->createNewChildElement ("ProjectConfiguration"); + e->setAttribute ("Include", config.createMSVCConfigName (arch)); + e->createNewChildElement ("Configuration")->addTextElement (config.getName()); + e->createNewChildElement ("Platform")->addTextElement (toString (arch)); + } } } @@ -489,40 +669,43 @@ class MSVCProjectExporterBase : public ProjectExporter for (ConstConfigIterator i (owner); i.next();) { - auto& config = dynamic_cast (*i); + auto& config = *static_cast (&*i); - auto* e = projectXml.createNewChildElement ("PropertyGroup"); - setConditionAttribute (*e, config); - e->setAttribute ("Label", "Configuration"); - e->createNewChildElement ("ConfigurationType")->addTextElement (getProjectType()); - e->createNewChildElement ("UseOfMfc")->addTextElement ("false"); - e->createNewChildElement ("WholeProgramOptimization")->addTextElement (config.isLinkTimeOptimisationEnabled() ? "true" - : "false"); + for (const auto& arch : allArchitectures) + { + auto* e = projectXml.createNewChildElement ("PropertyGroup"); + setConditionAttribute (*e, config, arch); + e->setAttribute ("Label", "Configuration"); + e->createNewChildElement ("ConfigurationType")->addTextElement (getProjectType()); + e->createNewChildElement ("UseOfMfc")->addTextElement ("false"); + e->createNewChildElement ("WholeProgramOptimization")->addTextElement (config.isLinkTimeOptimisationEnabled() ? "true" + : "false"); - auto charSet = config.getCharacterSetString(); + auto charSet = config.getCharacterSetString(); - if (charSet.isNotEmpty()) - e->createNewChildElement ("CharacterSet")->addTextElement (charSet); + if (charSet.isNotEmpty()) + e->createNewChildElement ("CharacterSet")->addTextElement (charSet); - if (config.shouldLinkIncremental()) - e->createNewChildElement ("LinkIncremental")->addTextElement ("true"); + if (config.shouldLinkIncremental()) + e->createNewChildElement ("LinkIncremental")->addTextElement ("true"); - e->createNewChildElement ("PlatformToolset")->addTextElement (owner.getPlatformToolset()); + e->createNewChildElement ("PlatformToolset")->addTextElement (owner.getPlatformToolset()); - addWindowsTargetPlatformToConfig (*e); + addWindowsTargetPlatformToConfig (*e); - struct IntelLibraryInfo - { - String libraryKind; - String configString; - }; + struct IntelLibraryInfo + { + String libraryKind; + String configString; + }; - for (const auto& info : { IntelLibraryInfo { owner.getIPPLibrary(), "UseIntelIPP" }, - IntelLibraryInfo { owner.getIPP1ALibrary(), "UseIntelIPP1A" }, - IntelLibraryInfo { owner.getMKL1ALibrary(), "UseInteloneMKL" } }) - { - if (info.libraryKind.isNotEmpty()) - e->createNewChildElement (info.configString)->addTextElement (info.libraryKind); + for (const auto& info : { IntelLibraryInfo { owner.getIPPLibrary(), "UseIntelIPP" }, + IntelLibraryInfo { owner.getIPP1ALibrary(), "UseIntelIPP1A" }, + IntelLibraryInfo { owner.getMKL1ALibrary(), "UseInteloneMKL" } }) + { + if (info.libraryKind.isNotEmpty()) + e->createNewChildElement (info.configString)->addTextElement (info.libraryKind); + } } } @@ -552,47 +735,50 @@ class MSVCProjectExporterBase : public ProjectExporter for (ConstConfigIterator i (owner); i.next();) { - auto& config = dynamic_cast (*i); + auto& config = *static_cast (&*i); - if (getConfigTargetPath (config).isNotEmpty()) + for (const auto& arch : allArchitectures) { - auto* outdir = props->createNewChildElement ("OutDir"); - setConditionAttribute (*outdir, config); - outdir->addTextElement (build_tools::windowsStylePath (getConfigTargetPath (config)) + "\\"); - } - - { - auto* intdir = props->createNewChildElement ("IntDir"); - setConditionAttribute (*intdir, config); + if (getConfigTargetPath (config).isNotEmpty()) + { + auto* outdir = props->createNewChildElement ("OutDir"); + setConditionAttribute (*outdir, config, arch); + outdir->addTextElement (build_tools::windowsStylePath (getConfigTargetPath (config)) + "\\"); + } - auto intermediatesPath = getIntermediatesPath (config); - if (! intermediatesPath.endsWithChar (L'\\')) - intermediatesPath += L'\\'; + { + auto* intdir = props->createNewChildElement ("IntDir"); + setConditionAttribute (*intdir, config, arch); - intdir->addTextElement (build_tools::windowsStylePath (intermediatesPath)); - } + auto intermediatesPath = getIntermediatesPath (config); + if (! intermediatesPath.endsWithChar (L'\\')) + intermediatesPath += L'\\'; - { - auto* targetName = props->createNewChildElement ("TargetName"); - setConditionAttribute (*targetName, config); - targetName->addTextElement (msBuildEscape (config.getOutputFilename ("", false, type))); - } + intdir->addTextElement (build_tools::windowsStylePath (intermediatesPath)); + } - { - auto* manifest = props->createNewChildElement ("GenerateManifest"); - setConditionAttribute (*manifest, config); - manifest->addTextElement ("true"); - } + { + auto* targetName = props->createNewChildElement ("TargetName"); + setConditionAttribute (*targetName, config, arch); + targetName->addTextElement (msBuildEscape (config.getOutputFilename ("", false, type))); + } - if (type != SharedCodeTarget) - { - auto librarySearchPaths = getLibrarySearchPaths (config); + { + auto* manifest = props->createNewChildElement ("GenerateManifest"); + setConditionAttribute (*manifest, config, arch); + manifest->addTextElement ("true"); + } - if (! librarySearchPaths.isEmpty()) + if (type != SharedCodeTarget) { - auto* libPath = props->createNewChildElement ("LibraryPath"); - setConditionAttribute (*libPath, config); - libPath->addTextElement ("$(LibraryPath);" + librarySearchPaths.joinIntoString (";")); + auto librarySearchPaths = getLibrarySearchPaths (config); + + if (! librarySearchPaths.isEmpty()) + { + auto* libPath = props->createNewChildElement ("LibraryPath"); + setConditionAttribute (*libPath, config, arch); + libPath->addTextElement ("$(LibraryPath);" + librarySearchPaths.joinIntoString (";")); + } } } } @@ -600,191 +786,194 @@ class MSVCProjectExporterBase : public ProjectExporter for (ConstConfigIterator i (owner); i.next();) { - auto& config = dynamic_cast (*i); - - enum class EscapeQuotes { no, yes }; + auto& config = *static_cast (&*i); - // VS doesn't correctly escape double quotes in preprocessor definitions, so we have - // to add our own layer of escapes - const auto addIncludePathsAndPreprocessorDefinitions = [this, &config] (XmlElement& xml, EscapeQuotes escapeQuotes) + for (const auto& arch : allArchitectures) { - auto includePaths = getOwner().getHeaderSearchPaths (config); - includePaths.add ("%(AdditionalIncludeDirectories)"); - xml.createNewChildElement ("AdditionalIncludeDirectories")->addTextElement (includePaths.joinIntoString (";")); - - const auto preprocessorDefs = getPreprocessorDefs (config, ";") + ";%(PreprocessorDefinitions)"; - const auto preprocessorDefsEscaped = escapeQuotes == EscapeQuotes::yes ? preprocessorDefs.replace ("\"", "\\\"") - : preprocessorDefs; - xml.createNewChildElement ("PreprocessorDefinitions")->addTextElement (preprocessorDefsEscaped); - }; - - bool isDebug = config.isDebug(); - - auto* group = projectXml.createNewChildElement ("ItemDefinitionGroup"); - setConditionAttribute (*group, config); + enum class EscapeQuotes { no, yes }; - { - auto* midl = group->createNewChildElement ("Midl"); - midl->createNewChildElement ("PreprocessorDefinitions")->addTextElement (isDebug ? "_DEBUG;%(PreprocessorDefinitions)" - : "NDEBUG;%(PreprocessorDefinitions)"); - midl->createNewChildElement ("MkTypLibCompatible")->addTextElement ("true"); - midl->createNewChildElement ("SuppressStartupBanner")->addTextElement ("true"); - midl->createNewChildElement ("TargetEnvironment")->addTextElement ("Win32"); - midl->createNewChildElement ("HeaderFileName"); - } + // VS doesn't correctly escape double quotes in preprocessor definitions, so we have + // to add our own layer of escapes + const auto addIncludePathsAndPreprocessorDefinitions = [this, &config] (XmlElement& xml, EscapeQuotes escapeQuotes) + { + auto includePaths = getOwner().getHeaderSearchPaths (config); + includePaths.add ("%(AdditionalIncludeDirectories)"); + xml.createNewChildElement ("AdditionalIncludeDirectories")->addTextElement (includePaths.joinIntoString (";")); - bool isUsingEditAndContinue = false; - const auto pdbFilename = getOwner().getIntDirFile (config, config.getOutputFilename (".pdb", true, type)); + const auto preprocessorDefs = getPreprocessorDefs (config, ";") + ";%(PreprocessorDefinitions)"; + const auto preprocessorDefsEscaped = escapeQuotes == EscapeQuotes::yes ? preprocessorDefs.replace ("\"", "\\\"") + : preprocessorDefs; + xml.createNewChildElement ("PreprocessorDefinitions")->addTextElement (preprocessorDefsEscaped); + }; - { - auto* cl = group->createNewChildElement ("ClCompile"); + bool isDebug = config.isDebug(); - cl->createNewChildElement ("Optimization")->addTextElement (getOptimisationLevelString (config.getOptimisationLevelInt())); + auto* group = projectXml.createNewChildElement ("ItemDefinitionGroup"); + setConditionAttribute (*group, config, arch); - if (isDebug || config.shouldGenerateDebugSymbols()) { - cl->createNewChildElement ("DebugInformationFormat") - ->addTextElement (config.getDebugInformationFormatString()); + auto* midl = group->createNewChildElement ("Midl"); + midl->createNewChildElement ("PreprocessorDefinitions")->addTextElement (isDebug ? "_DEBUG;%(PreprocessorDefinitions)" + : "NDEBUG;%(PreprocessorDefinitions)"); + midl->createNewChildElement ("MkTypLibCompatible")->addTextElement ("true"); + midl->createNewChildElement ("SuppressStartupBanner")->addTextElement ("true"); + midl->createNewChildElement ("TargetEnvironment")->addTextElement ("Win32"); + midl->createNewChildElement ("HeaderFileName"); } - addIncludePathsAndPreprocessorDefinitions (*cl, EscapeQuotes::no); + bool isUsingEditAndContinue = false; + const auto pdbFilename = getOwner().getIntDirFile (config, config.getOutputFilename (".pdb", true, type)); - cl->createNewChildElement ("RuntimeLibrary")->addTextElement (config.isUsingRuntimeLibDLL() ? (isDebug ? "MultiThreadedDebugDLL" : "MultiThreadedDLL") - : (isDebug ? "MultiThreadedDebug" : "MultiThreaded")); - cl->createNewChildElement ("RuntimeTypeInfo")->addTextElement ("true"); - cl->createNewChildElement ("PrecompiledHeader")->addTextElement ("NotUsing"); - cl->createNewChildElement ("AssemblerListingLocation")->addTextElement ("$(IntDir)\\"); - cl->createNewChildElement ("ObjectFileName")->addTextElement ("$(IntDir)\\"); - cl->createNewChildElement ("ProgramDataBaseFileName")->addTextElement (pdbFilename); - cl->createNewChildElement ("WarningLevel")->addTextElement ("Level" + String (config.getWarningLevel())); - cl->createNewChildElement ("SuppressStartupBanner")->addTextElement ("true"); - cl->createNewChildElement ("MultiProcessorCompilation")->addTextElement (config.shouldUseMultiProcessorCompilation() ? "true" : "false"); + { + auto* cl = group->createNewChildElement ("ClCompile"); - if (config.isFastMathEnabled()) - cl->createNewChildElement ("FloatingPointModel")->addTextElement ("Fast"); + cl->createNewChildElement ("Optimization")->addTextElement (getOptimisationLevelString (config.getOptimisationLevelInt())); - auto extraFlags = getOwner().replacePreprocessorTokens (config, config.getAllCompilerFlagsString()).trim(); + if (isDebug || config.shouldGenerateDebugSymbols()) + { + cl->createNewChildElement ("DebugInformationFormat") + ->addTextElement (config.getDebugInformationFormatString()); + } - if (extraFlags.isNotEmpty()) - cl->createNewChildElement ("AdditionalOptions")->addTextElement (extraFlags + " %(AdditionalOptions)"); + addIncludePathsAndPreprocessorDefinitions (*cl, EscapeQuotes::no); - if (config.areWarningsTreatedAsErrors()) - cl->createNewChildElement ("TreatWarningAsError")->addTextElement ("true"); + cl->createNewChildElement ("RuntimeLibrary")->addTextElement (config.isUsingRuntimeLibDLL() ? (isDebug ? "MultiThreadedDebugDLL" : "MultiThreadedDLL") + : (isDebug ? "MultiThreadedDebug" : "MultiThreaded")); + cl->createNewChildElement ("RuntimeTypeInfo")->addTextElement ("true"); + cl->createNewChildElement ("PrecompiledHeader")->addTextElement ("NotUsing"); + cl->createNewChildElement ("AssemblerListingLocation")->addTextElement ("$(IntDir)\\"); + cl->createNewChildElement ("ObjectFileName")->addTextElement ("$(IntDir)\\"); + cl->createNewChildElement ("ProgramDataBaseFileName")->addTextElement (pdbFilename); + cl->createNewChildElement ("WarningLevel")->addTextElement ("Level" + String (config.getWarningLevel())); + cl->createNewChildElement ("SuppressStartupBanner")->addTextElement ("true"); + cl->createNewChildElement ("MultiProcessorCompilation")->addTextElement (config.shouldUseMultiProcessorCompilation() ? "true" : "false"); - auto cppStandard = owner.project.getCppStandardString(); - cl->createNewChildElement ("LanguageStandard")->addTextElement ("stdcpp" + cppStandard); - } + if (config.isFastMathEnabled()) + cl->createNewChildElement ("FloatingPointModel")->addTextElement ("Fast"); - { - auto* res = group->createNewChildElement ("ResourceCompile"); - addIncludePathsAndPreprocessorDefinitions (*res, EscapeQuotes::yes); - } + auto extraFlags = getOwner().replacePreprocessorTokens (config, config.getAllCompilerFlagsString()).trim(); - auto externalLibraries = getExternalLibraries (config, getOwner().getExternalLibrariesStringArray()); - auto additionalDependencies = type != SharedCodeTarget && type != LV2Helper && type != VST3Helper && ! externalLibraries.isEmpty() - ? externalLibraries.joinIntoString (";") + ";%(AdditionalDependencies)" - : String(); + if (extraFlags.isNotEmpty()) + cl->createNewChildElement ("AdditionalOptions")->addTextElement (extraFlags + " %(AdditionalOptions)"); - auto librarySearchPaths = config.getLibrarySearchPaths(); - auto additionalLibraryDirs = type != SharedCodeTarget && type != LV2Helper && type != VST3Helper && librarySearchPaths.size() > 0 - ? getOwner().replacePreprocessorTokens (config, librarySearchPaths.joinIntoString (";")) + ";%(AdditionalLibraryDirectories)" - : String(); + if (config.areWarningsTreatedAsErrors()) + cl->createNewChildElement ("TreatWarningAsError")->addTextElement ("true"); + + auto cppStandard = owner.project.getCppStandardString(); + cl->createNewChildElement ("LanguageStandard")->addTextElement ("stdcpp" + cppStandard); + } - { - auto* link = group->createNewChildElement ("Link"); - link->createNewChildElement ("OutputFile")->addTextElement (getOutputFilePath (config)); - link->createNewChildElement ("SuppressStartupBanner")->addTextElement ("true"); - link->createNewChildElement ("IgnoreSpecificDefaultLibraries")->addTextElement (isDebug ? "libcmt.lib; msvcrt.lib;;%(IgnoreSpecificDefaultLibraries)" - : "%(IgnoreSpecificDefaultLibraries)"); - link->createNewChildElement ("GenerateDebugInformation")->addTextElement ((isDebug || config.shouldGenerateDebugSymbols()) ? "true" : "false"); - link->createNewChildElement ("ProgramDatabaseFile")->addTextElement (pdbFilename); - link->createNewChildElement ("SubSystem")->addTextElement (type == ConsoleApp || type == LV2Helper || type == VST3Helper ? "Console" : "Windows"); - - if (config.getArchitectureString() == "Win32") - link->createNewChildElement ("TargetMachine")->addTextElement ("MachineX86"); - - if (isUsingEditAndContinue) - link->createNewChildElement ("ImageHasSafeExceptionHandlers")->addTextElement ("false"); - - if (! isDebug) { - link->createNewChildElement ("OptimizeReferences")->addTextElement ("true"); - link->createNewChildElement ("EnableCOMDATFolding")->addTextElement ("true"); + auto* res = group->createNewChildElement ("ResourceCompile"); + addIncludePathsAndPreprocessorDefinitions (*res, EscapeQuotes::yes); } - if (additionalLibraryDirs.isNotEmpty()) - link->createNewChildElement ("AdditionalLibraryDirectories")->addTextElement (additionalLibraryDirs); + auto externalLibraries = getExternalLibraries (config, getOwner().getExternalLibrariesStringArray()); + auto additionalDependencies = type != SharedCodeTarget && type != LV2Helper && type != VST3Helper && ! externalLibraries.isEmpty() + ? externalLibraries.joinIntoString (";") + ";%(AdditionalDependencies)" + : String(); - link->createNewChildElement ("LargeAddressAware")->addTextElement ("true"); + auto librarySearchPaths = config.getLibrarySearchPaths(); + auto additionalLibraryDirs = type != SharedCodeTarget && type != LV2Helper && type != VST3Helper && librarySearchPaths.size() > 0 + ? getOwner().replacePreprocessorTokens (config, librarySearchPaths.joinIntoString (";")) + ";%(AdditionalLibraryDirectories)" + : String(); - if (config.isLinkTimeOptimisationEnabled()) - link->createNewChildElement ("LinkTimeCodeGeneration")->addTextElement ("UseLinkTimeCodeGeneration"); + { + auto* link = group->createNewChildElement ("Link"); + link->createNewChildElement ("OutputFile")->addTextElement (getOutputFilePath (config)); + link->createNewChildElement ("SuppressStartupBanner")->addTextElement ("true"); + link->createNewChildElement ("IgnoreSpecificDefaultLibraries")->addTextElement (isDebug ? "libcmt.lib; msvcrt.lib;;%(IgnoreSpecificDefaultLibraries)" + : "%(IgnoreSpecificDefaultLibraries)"); + link->createNewChildElement ("GenerateDebugInformation")->addTextElement ((isDebug || config.shouldGenerateDebugSymbols()) ? "true" : "false"); + link->createNewChildElement ("ProgramDatabaseFile")->addTextElement (pdbFilename); + link->createNewChildElement ("SubSystem")->addTextElement (type == ConsoleApp || type == LV2Helper || type == VST3Helper ? "Console" : "Windows"); + + if (arch == Architecture::win32) + link->createNewChildElement ("TargetMachine")->addTextElement ("MachineX86"); + + if (isUsingEditAndContinue) + link->createNewChildElement ("ImageHasSafeExceptionHandlers")->addTextElement ("false"); + + if (! isDebug) + { + link->createNewChildElement ("OptimizeReferences")->addTextElement ("true"); + link->createNewChildElement ("EnableCOMDATFolding")->addTextElement ("true"); + } - if (additionalDependencies.isNotEmpty()) - link->createNewChildElement ("AdditionalDependencies")->addTextElement (additionalDependencies); + if (additionalLibraryDirs.isNotEmpty()) + link->createNewChildElement ("AdditionalLibraryDirectories")->addTextElement (additionalLibraryDirs); - auto extraLinkerOptions = config.getAllLinkerFlagsString(); - if (extraLinkerOptions.isNotEmpty()) - link->createNewChildElement ("AdditionalOptions")->addTextElement (getOwner().replacePreprocessorTokens (config, extraLinkerOptions).trim() - + " %(AdditionalOptions)"); + link->createNewChildElement ("LargeAddressAware")->addTextElement ("true"); - auto delayLoadedDLLs = getOwner().msvcDelayLoadedDLLs; - if (delayLoadedDLLs.isNotEmpty()) - link->createNewChildElement ("DelayLoadDLLs")->addTextElement (delayLoadedDLLs); + if (config.isLinkTimeOptimisationEnabled()) + link->createNewChildElement ("LinkTimeCodeGeneration")->addTextElement ("UseLinkTimeCodeGeneration"); - auto moduleDefinitionsFile = getModuleDefinitions (config); - if (moduleDefinitionsFile.isNotEmpty()) - link->createNewChildElement ("ModuleDefinitionFile") - ->addTextElement (moduleDefinitionsFile); - } + if (additionalDependencies.isNotEmpty()) + link->createNewChildElement ("AdditionalDependencies")->addTextElement (additionalDependencies); - { - auto* bsc = group->createNewChildElement ("Bscmake"); - bsc->createNewChildElement ("SuppressStartupBanner")->addTextElement ("true"); - bsc->createNewChildElement ("OutputFile")->addTextElement (getOwner().getIntDirFile (config, config.getOutputFilename (".bsc", true, type))); - } + auto extraLinkerOptions = config.getAllLinkerFlagsString(); + if (extraLinkerOptions.isNotEmpty()) + link->createNewChildElement ("AdditionalOptions")->addTextElement (getOwner().replacePreprocessorTokens (config, extraLinkerOptions).trim() + + " %(AdditionalOptions)"); - if (type != SharedCodeTarget && type != LV2Helper && type != VST3Helper) - { - auto* lib = group->createNewChildElement ("Lib"); + auto delayLoadedDLLs = getOwner().msvcDelayLoadedDLLs; + if (delayLoadedDLLs.isNotEmpty()) + link->createNewChildElement ("DelayLoadDLLs")->addTextElement (delayLoadedDLLs); - if (additionalDependencies.isNotEmpty()) - lib->createNewChildElement ("AdditionalDependencies")->addTextElement (additionalDependencies); + auto moduleDefinitionsFile = getModuleDefinitions (config); + if (moduleDefinitionsFile.isNotEmpty()) + link->createNewChildElement ("ModuleDefinitionFile") + ->addTextElement (moduleDefinitionsFile); + } - if (additionalLibraryDirs.isNotEmpty()) - lib->createNewChildElement ("AdditionalLibraryDirectories")->addTextElement (additionalLibraryDirs); - } + { + auto* bsc = group->createNewChildElement ("Bscmake"); + bsc->createNewChildElement ("SuppressStartupBanner")->addTextElement ("true"); + bsc->createNewChildElement ("OutputFile")->addTextElement (getOwner().getIntDirFile (config, config.getOutputFilename (".bsc", true, type))); + } - if (auto manifestFile = getOwner().getManifestPath(); manifestFile.getRoot() != build_tools::RelativePath::unknown || type == VST3Helper) - { - auto* bsc = group->createNewChildElement ("Manifest"); - auto* additional = bsc->createNewChildElement ("AdditionalManifestFiles"); + if (type != SharedCodeTarget && type != LV2Helper && type != VST3Helper) + { + auto* lib = group->createNewChildElement ("Lib"); + + if (additionalDependencies.isNotEmpty()) + lib->createNewChildElement ("AdditionalDependencies")->addTextElement (additionalDependencies); - if (manifestFile.getRoot() != build_tools::RelativePath::unknown) + if (additionalLibraryDirs.isNotEmpty()) + lib->createNewChildElement ("AdditionalLibraryDirectories")->addTextElement (additionalLibraryDirs); + } + + if (auto manifestFile = getOwner().getManifestPath(); manifestFile.getRoot() != build_tools::RelativePath::unknown || type == VST3Helper) { - additional->addTextElement (manifestFile.rebased (getOwner().getProject().getFile().getParentDirectory(), - getOwner().getTargetFolder(), - build_tools::RelativePath::buildTargetFolder).toWindowsStyle()); + auto* bsc = group->createNewChildElement ("Manifest"); + auto* additional = bsc->createNewChildElement ("AdditionalManifestFiles"); + + if (manifestFile.getRoot() != build_tools::RelativePath::unknown) + { + additional->addTextElement (manifestFile.rebased (getOwner().getProject().getFile().getParentDirectory(), + getOwner().getTargetFolder(), + build_tools::RelativePath::buildTargetFolder).toWindowsStyle()); + } } - } - if (getTargetFileType() == staticLibrary && config.getArchitectureString() == "Win32") - { - auto* lib = group->createNewChildElement ("Lib"); - lib->createNewChildElement ("TargetMachine")->addTextElement ("MachineX86"); - } + if (getTargetFileType() == staticLibrary && arch == Architecture::win32) + { + auto* lib = group->createNewChildElement ("Lib"); + lib->createNewChildElement ("TargetMachine")->addTextElement ("MachineX86"); + } - auto preBuild = getPreBuildSteps (config); - if (preBuild.isNotEmpty()) - group->createNewChildElement ("PreBuildEvent") - ->createNewChildElement ("Command") - ->addTextElement (preBuild); - - auto postBuild = getPostBuildSteps (config); - if (postBuild.isNotEmpty()) - group->createNewChildElement ("PostBuildEvent") - ->createNewChildElement ("Command") - ->addTextElement (postBuild); + auto preBuild = getPreBuildSteps (config, arch); + if (preBuild.isNotEmpty()) + group->createNewChildElement ("PreBuildEvent") + ->createNewChildElement ("Command") + ->addTextElement (preBuild); + + auto postBuild = getPostBuildSteps (config, arch); + if (postBuild.isNotEmpty()) + group->createNewChildElement ("PostBuildEvent") + ->createNewChildElement ("Command") + ->addTextElement (postBuild); + } } std::unique_ptr otherFilesGroup (new XmlElement ("ItemGroup")); @@ -878,11 +1067,15 @@ class MSVCProjectExporterBase : public ProjectExporter } //============================================================================== - static void setSourceFilePCHSettings (XmlElement& element, const File& pchFile, const String& option, const BuildConfiguration& config) + static void setSourceFilePCHSettings (XmlElement& element, + const File& pchFile, + const String& option, + const BuildConfiguration& config, + Architecture arch) { - auto setConfigConditionAttribute = [&config] (XmlElement* elementToSet) -> XmlElement* + auto setConfigConditionAttribute = [&config, arch] (XmlElement* elementToSet) -> XmlElement* { - setConditionAttribute (*elementToSet, config); + setConditionAttribute (*elementToSet, config, arch); return elementToSet; }; @@ -894,39 +1087,44 @@ class MSVCProjectExporterBase : public ProjectExporter void writePrecompiledHeaderFiles (XmlElement& cpps) const { - for (ConstConfigIterator config (owner); config.next();) + for (ConstConfigIterator i (owner); i.next();) { - if (config->shouldUsePrecompiledHeaderFile()) + if (! i->shouldUsePrecompiledHeaderFile()) + continue; + + auto& config = *static_cast (&*i); + + for (const auto& arch : config.getArchitectures()) { - auto pchFileContent = config->getPrecompiledHeaderFileContent(); + auto pchFileContent = config.getPrecompiledHeaderFileContent(); - if (pchFileContent.isNotEmpty()) - { - auto pchFile = owner.getTargetFolder().getChildFile (config->getPrecompiledHeaderFilename()).withFileExtension (".h"); + if (pchFileContent.isEmpty()) + continue; - build_tools::writeStreamToFile (pchFile, [&] (MemoryOutputStream& mo) - { - mo << pchFileContent; - }); + auto pchFile = owner.getTargetFolder().getChildFile (config.getPrecompiledHeaderFilename()).withFileExtension (".h"); - auto pchSourceFile = pchFile.withFileExtension (".cpp"); + build_tools::writeStreamToFile (pchFile, [&] (MemoryOutputStream& mo) + { + mo << pchFileContent; + }); - build_tools::writeStreamToFile (pchSourceFile, [this] (MemoryOutputStream& mo) - { - mo.setNewLineString (owner.getNewLineString()); + auto pchSourceFile = pchFile.withFileExtension (".cpp"); - writeAutoGenWarningComment (mo); + build_tools::writeStreamToFile (pchSourceFile, [this] (MemoryOutputStream& mo) + { + mo.setNewLineString (owner.getNewLineString()); - mo << " This is an empty source file generated by JUCE required for Visual Studio PCH." << newLine - << newLine - << "*/" << newLine - << newLine; - }); + writeAutoGenWarningComment (mo); - auto* pchSourceElement = cpps.createNewChildElement ("ClCompile"); - pchSourceElement->setAttribute ("Include", prependDot (pchSourceFile.getFileName())); - setSourceFilePCHSettings (*pchSourceElement, pchFile, "Create", *config); - } + mo << " This is an empty source file generated by JUCE required for Visual Studio PCH." << newLine + << newLine + << "*/" << newLine + << newLine; + }); + + auto* pchSourceElement = cpps.createNewChildElement ("ClCompile"); + pchSourceElement->setAttribute ("Include", prependDot (pchSourceFile.getFileName())); + setSourceFilePCHSettings (*pchSourceElement, pchFile, "Create", config, arch); } } } @@ -974,12 +1172,17 @@ class MSVCProjectExporterBase : public ProjectExporter { for (ConstConfigIterator i (owner); i.next();) { - if (i->shouldUsePrecompiledHeaderFile()) + auto& config = *static_cast (&*i); + + if (config.shouldUsePrecompiledHeaderFile()) { - auto pchFile = owner.getTargetFolder().getChildFile (i->getPrecompiledHeaderFilename()).withFileExtension (".h"); + for (const auto& arch : config.getArchitectures()) + { + auto pchFile = owner.getTargetFolder().getChildFile (i->getPrecompiledHeaderFilename()).withFileExtension (".h"); - if (pchFile.existsAsFile()) - setSourceFilePCHSettings (*e, pchFile, "Use", *i); + if (pchFile.existsAsFile()) + setSourceFilePCHSettings (*e, pchFile, "Use", *i, arch); + } } } } @@ -1000,10 +1203,10 @@ class MSVCProjectExporterBase : public ProjectExporter } } - static void setConditionAttribute (XmlElement& xml, const BuildConfiguration& config) + static void setConditionAttribute (XmlElement& xml, const BuildConfiguration& config, Architecture arch) { - auto& msvcConfig = dynamic_cast (config); - xml.setAttribute ("Condition", "'$(Configuration)|$(Platform)'=='" + msvcConfig.createMSVCConfigName() + "'"); + auto& msvcConfig = *static_cast (&config); + xml.setAttribute ("Condition", "'$(Configuration)|$(Platform)'=='" + msvcConfig.createMSVCConfigName (arch) + "'"); } //============================================================================== @@ -1080,7 +1283,7 @@ class MSVCProjectExporterBase : public ProjectExporter for (int i = 0; i < getOwner().getAllGroups().size(); ++i) { - auto& group = getOwner().getAllGroups().getReference(i); + auto& group = getOwner().getAllGroups().getReference (i); if (group.getNumChildren() > 0) addFilesToFilter (group, group.getName(), *cpps, *headers, *otherFilesGroup, *groupsXml); @@ -1255,7 +1458,7 @@ class MSVCProjectExporterBase : public ProjectExporter return aaxSdk.getChildFile ("Utilities").getChildFile ("PlugIn.ico"); } - String getExtraPostBuildSteps (const MSVCBuildConfiguration& config) const + String getExtraPostBuildSteps (const MSVCBuildConfiguration& config, Architecture arch) const { const auto copyBuildOutputIntoBundle = [&] (const StringArray& segments) { @@ -1282,7 +1485,7 @@ class MSVCProjectExporterBase : public ProjectExporter const auto bundleScript = aaxSdk.getChildFile ("Utilities").getChildFile ("CreatePackage.bat"); const auto iconFilePath = getAAXIconFile(); - const auto segments = getAaxBundleStructure (config); + const auto segments = getAaxBundleStructure (config, arch); const auto pkgScript = copyBuildOutputIntoBundle (segments); @@ -1295,7 +1498,7 @@ class MSVCProjectExporterBase : public ProjectExporter + String (" ") + createRebasedPath (iconFilePath); - const auto copyScript = copyBundleToInstallDirectory (segments, config.getAAXBinaryLocationString()); + const auto copyScript = copyBundleToInstallDirectory (segments, config.getBinaryPath (Ids::aaxBinaryLocation, arch)); return pkgScript + fixScript + copyScript; } @@ -1310,7 +1513,7 @@ class MSVCProjectExporterBase : public ProjectExporter if (config.isPluginBinaryCopyStepEnabled()) { - auto copyLocation = config.getUnityPluginBinaryLocationString(); + auto copyLocation = config.getBinaryPath (Ids::unityPluginBinaryLocation, arch); pkgScript += "\r\ncopy /Y \"$(OutDir)$(TargetFileName)\" " + String (copyLocation + "\\$(TargetFileName)").quoted(); pkgScript += "\r\ncopy /Y " + String ("$(OutDir)" + config.project.getUnityScriptName()).quoted() + " " + String (copyLocation + "\\" + config.project.getUnityScriptName()).quoted(); @@ -1335,7 +1538,7 @@ class MSVCProjectExporterBase : public ProjectExporter + writerTarget->getBinaryNameWithSuffix (config); const auto copyStep = "xcopy /E /H /I /K /R /Y \"$(OutDir)\" \"" - + config.getLV2BinaryLocationString() + + config.getBinaryPath (Ids::lv2BinaryLocation, arch) + '\\' + config.getTargetBinaryNameString() + ".lv2\"\r\n"; @@ -1347,7 +1550,7 @@ class MSVCProjectExporterBase : public ProjectExporter if (type == VST3PlugIn) { - const auto segments = getVst3BundleStructure (config); + const auto segments = getVst3BundleStructure (config, arch); const auto manifestScript = [&]() -> String { @@ -1384,18 +1587,18 @@ class MSVCProjectExporterBase : public ProjectExporter }(); const auto pkgScript = copyBuildOutputIntoBundle (segments); - const auto copyScript = copyBundleToInstallDirectory (segments, config.getVST3BinaryLocationString()); + const auto copyScript = copyBundleToInstallDirectory (segments, config.getBinaryPath (Ids::vst3BinaryLocation, arch)); return pkgScript + manifestScript + copyScript; } if (type == VSTPlugIn && config.isPluginBinaryCopyStepEnabled()) - return "copy /Y \"$(OutDir)$(TargetFileName)\" \"" + config.getVSTBinaryLocationString() + "\\$(TargetFileName)\""; + return "copy /Y \"$(OutDir)$(TargetFileName)\" \"" + config.getBinaryPath (Ids::vstBinaryLocation, arch) + "\\$(TargetFileName)\""; return {}; } - String getExtraPreBuildSteps (const MSVCBuildConfiguration& config) const + String getExtraPreBuildSteps (const MSVCBuildConfiguration& config, Architecture arch) const { const auto createBundleStructure = [&] (const StringArray& segments) { @@ -1412,26 +1615,26 @@ class MSVCProjectExporterBase : public ProjectExporter }; if (type == AAXPlugIn) - return createBundleStructure (getAaxBundleStructure (config)); + return createBundleStructure (getAaxBundleStructure (config, arch)); if (type == VST3PlugIn) - return createBundleStructure (getVst3BundleStructure (config)); + return createBundleStructure (getVst3BundleStructure (config, arch)); return {}; } - String getPostBuildSteps (const MSVCBuildConfiguration& config) const + String getPostBuildSteps (const MSVCBuildConfiguration& config, Architecture arch) const { auto postBuild = config.getPostbuildCommandString().replace ("\n", "\r\n");; - auto extraPostBuild = getExtraPostBuildSteps (config); + auto extraPostBuild = getExtraPostBuildSteps (config, arch); return postBuild + String (postBuild.isNotEmpty() && extraPostBuild.isNotEmpty() ? "\r\n" : "") + extraPostBuild; } - String getPreBuildSteps (const MSVCBuildConfiguration& config) const + String getPreBuildSteps (const MSVCBuildConfiguration& config, Architecture arch) const { auto preBuild = config.getPrebuildCommandString().replace ("\n", "\r\n");; - auto extraPreBuild = getExtraPreBuildSteps (config); + auto extraPreBuild = getExtraPreBuildSteps (config, arch); return preBuild + String (preBuild.isNotEmpty() && extraPreBuild.isNotEmpty() ? "\r\n" : "") + extraPreBuild; } @@ -1513,24 +1716,31 @@ class MSVCProjectExporterBase : public ProjectExporter } protected: - StringArray getAaxBundleStructure (const MSVCBuildConfiguration& config) const + StringArray getAaxBundleStructure (const MSVCBuildConfiguration& config, Architecture arch) const { const auto dllName = config.getOutputFilename (".aaxplugin", false, type); - return { dllName, "Contents", config.getArchitectureString(), dllName }; + return { dllName, "Contents", toString (arch), dllName }; } - StringArray getVst3BundleStructure (const MSVCBuildConfiguration& config) const + StringArray getVst3BundleStructure (const MSVCBuildConfiguration& config, Architecture arch) const { - static const std::map suffixes + // These suffixes are defined in the VST3 SDK docs + const auto suffix = std::invoke ([&]() -> String { - { "Win32", "x86" }, - { "x64", "x86_64" }, - }; + switch (arch) + { + case Architecture::win32: return "x86"; + case Architecture::win64: return "x86_64"; + case Architecture::arm64: return "arm64"; + case Architecture::arm64ec: return "arm64ec"; + } - const auto iter = suffixes.find (config.getArchitectureString()); + jassertfalse; + return {}; + }); const auto dllName = config.getOutputFilename (".vst3", false, type); - return { dllName, "Contents", iter != suffixes.cend() ? iter->second + "-win" : "win", dllName }; + return { dllName, "Contents", suffix + "-win", dllName }; } const MSVCProjectExporterBase& owner; @@ -1667,7 +1877,7 @@ class MSVCProjectExporterBase : public ProjectExporter msvcExtraPreprocessorDefs.set ("_CRT_SECURE_NO_WARNINGS", ""); if (type.isCommandLineApp()) - msvcExtraPreprocessorDefs.set("_CONSOLE", ""); + msvcExtraPreprocessorDefs.set ("_CONSOLE", ""); callForAllSupportedTargets ([this] (build_tools::ProjectType::Target::Type targetType) { @@ -1849,23 +2059,41 @@ class MSVCProjectExporterBase : public ProjectExporter for (ConstConfigIterator i (*this); i.next();) { - auto& config = dynamic_cast (*i); - auto configName = config.createMSVCConfigName(); - out << "\t\t" << configName << " = " << configName << newLine; + auto& config = *static_cast (&*i); + + for (const auto& arch : config.getArchitectures()) + { + auto configName = config.createMSVCConfigName (arch); + out << "\t\t" << configName << " = " << configName << newLine; + } } out << "\tEndGlobalSection" << newLine << "\tGlobalSection(ProjectConfigurationPlatforms) = postSolution" << newLine; + const auto allArchitectures = getAllActiveArchitectures(); + for (auto& target : targets) + { for (ConstConfigIterator i (*this); i.next();) { - auto& config = dynamic_cast (*i); - auto configName = config.createMSVCConfigName(); + auto& config = *static_cast (&*i); - for (auto& suffix : { "ActiveCfg", "Build.0" }) - out << "\t\t" << target->getProjectGuid() << "." << configName << "." << suffix << " = " << configName << newLine; + // Add a configuration for all projects but only mark the desired to be built. + // We have to do this as VS will automatically add the entry anyway. + for (const auto& arch : allArchitectures) + { + auto configName = config.createMSVCConfigName (arch); + + out << "\t\t" << target->getProjectGuid() << "." << configName << "." << "ActiveCfg" << " = " << configName << newLine; + + const auto shouldBuild = config.getArchitectures().contains (arch); + + if (shouldBuild) + out << "\t\t" << target->getProjectGuid() << "." << configName << "." << "Build.0" << " = " << configName << newLine; + } } + } out << "\tEndGlobalSection" << newLine << "\tGlobalSection(SolutionProperties) = preSolution" << newLine From 59bd0702912beb5ec1032b99590b0ebc0bfae134 Mon Sep 17 00:00:00 2001 From: Oliver James Date: Wed, 16 Oct 2024 14:06:28 +0100 Subject: [PATCH 20/76] Projucer: Disable AAX builds for Windows ARM --- .../Source/ProjectSaving/jucer_ProjectExport_MSVC.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h index 18fc4b4974a0..c371155ea664 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h @@ -337,6 +337,12 @@ class MSVCProjectExporterBase : public ProjectExporter bool isFastMathEnabled() const { return fastMathValue.get(); } bool isPluginBinaryCopyStepEnabled() const { return pluginBinaryCopyStepValue.get(); } + static bool shouldBuildTarget (build_tools::ProjectType::Target::Type targetType, Architecture arch) + { + return targetType != build_tools::ProjectType::Target::AAXPlugIn + || (arch != Architecture::arm64 && arch != Architecture::arm64ec); + } + //============================================================================== String createMSVCConfigName (Architecture arch) const { @@ -2087,7 +2093,7 @@ class MSVCProjectExporterBase : public ProjectExporter out << "\t\t" << target->getProjectGuid() << "." << configName << "." << "ActiveCfg" << " = " << configName << newLine; - const auto shouldBuild = config.getArchitectures().contains (arch); + const auto shouldBuild = config.shouldBuildTarget (target->type, arch) && config.getArchitectures().contains (arch); if (shouldBuild) out << "\t\t" << target->getProjectGuid() << "." << configName << "." << "Build.0" << " = " << configName << newLine; From 2f3dd44f3366e259356454d13bb0bc1db9e90377 Mon Sep 17 00:00:00 2001 From: Anthony Nicholls Date: Tue, 22 Oct 2024 15:46:58 +0100 Subject: [PATCH 21/76] Projucer: Add arm64 warning on Windows --- .../Projucer/Source/Project/jucer_Project.h | 5 +++- .../ProjectSaving/jucer_ProjectExport_MSVC.h | 28 ++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/extras/Projucer/Source/Project/jucer_Project.h b/extras/Projucer/Source/Project/jucer_Project.h index a3ce2124bcbe..96d60a31de21 100644 --- a/extras/Projucer/Source/Project/jucer_Project.h +++ b/extras/Projucer/Source/Project/jucer_Project.h @@ -60,6 +60,7 @@ namespace ProjectMessages DECLARE_ID (manufacturerCodeInvalid); DECLARE_ID (deprecatedExporter); DECLARE_ID (unsupportedArm32Config); + DECLARE_ID (arm64Warning); DECLARE_ID (notification); DECLARE_ID (warning); @@ -74,7 +75,7 @@ namespace ProjectMessages static Identifier warnings[] = { Ids::cppStandard, Ids::moduleNotFound, Ids::jucePath, Ids::jucerFileModified, Ids::missingModuleDependencies, Ids::oldProjucer, Ids::pluginCodeInvalid, Ids::manufacturerCodeInvalid, - Ids::deprecatedExporter, Ids::unsupportedArm32Config }; + Ids::deprecatedExporter, Ids::unsupportedArm32Config, Ids::arm64Warning }; if (std::find (std::begin (warnings), std::end (warnings), message) != std::end (warnings)) return Ids::warning; @@ -101,6 +102,7 @@ namespace ProjectMessages if (message == Ids::manufacturerCodeInvalid) return "Invalid Manufacturer Code"; if (message == Ids::deprecatedExporter) return "Deprecated Exporter"; if (message == Ids::unsupportedArm32Config) return "Unsupported Architecture"; + if (message == Ids::arm64Warning) return "Prefer arm64ec over arm64"; jassertfalse; return {}; @@ -119,6 +121,7 @@ namespace ProjectMessages if (message == Ids::manufacturerCodeInvalid) return "The manufacturer code should be exactly four characters in length."; if (message == Ids::deprecatedExporter) return "The project includes a deprecated exporter."; if (message == Ids::unsupportedArm32Config) return "The project includes a Visual Studio configuration that uses the 32-bit Arm architecture, which is no longer supported. This configuration has been hidden, and will be removed on save."; + if (message == Ids::arm64Warning) return "For software where interoperability is a concern (such as plugins and hosts), arm64ec will provide the best compatibility with existing x64 software"; jassertfalse; return {}; diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h index c371155ea664..eff607f49aef 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h @@ -243,7 +243,8 @@ class MSVCProjectExporterBase : public ProjectExporter } //============================================================================== - class MSVCBuildConfiguration final : public BuildConfiguration + class MSVCBuildConfiguration final : public BuildConfiguration, + private ValueTree::Listener { public: MSVCBuildConfiguration (Project& p, const ValueTree& settings, const ProjectExporter& e) @@ -285,6 +286,31 @@ class MSVCProjectExporterBase : public ProjectExporter } optimisationLevelValue.setDefault (isDebug() ? optimisationOff : optimiseFull); + + config.addListener (this); + } + + ~MSVCBuildConfiguration() override + { + config.removeListener (this); + } + + void valueTreePropertyChanged (ValueTree&, const Identifier& property) override + { + if (property != Ids::winArchitecture) + return; + + project.removeProjectMessage (ProjectMessages::Ids::arm64Warning); + + const auto selectedArchs = architectureTypeValue.get(); + + if (! selectedArchs.getArray()->contains (toString (Architecture::arm64))) + return; + + if (selectedArchs.getArray()->contains (toString (Architecture::arm64ec))) + return; + + project.addProjectMessage (ProjectMessages::Ids::arm64Warning, {}); } String getBinaryPath (const Identifier& id, Architecture arch) const From b9c6f7833bce19f5f915c82a5eb5b1853ab6665c Mon Sep 17 00:00:00 2001 From: Oliver James Date: Thu, 24 Oct 2024 15:03:06 +0100 Subject: [PATCH 22/76] Projucer: Implement VST3 cross-platform manifest generation support This enables the generation of VST3 manifests across platforms that support it. For instance, Windows ARM64 systems can now generate x64 manifests. --- .../ProjectSaving/jucer_ProjectExport_MSVC.h | 390 ++++++++++++++++-- 1 file changed, 348 insertions(+), 42 deletions(-) diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h index eff607f49aef..9dbc75521ae0 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h @@ -53,6 +53,193 @@ inline StringArray msBuildEscape (StringArray range) return range; } +class MSVCScriptBuilder +{ +public: + struct StringOrBuilder + { + StringOrBuilder() = default; + StringOrBuilder (const String& s) : value (s) {} + StringOrBuilder (const char* s) : StringOrBuilder (String { s }) {} + StringOrBuilder (const MSVCScriptBuilder& sb) : StringOrBuilder (sb.build()) {} + + bool isNotEmpty() const { return value.isNotEmpty(); } + + String value; + }; + + MSVCScriptBuilder& exit (int code) + { + script << "exit /b " << code; + script << newLine; + return *this; + } + + MSVCScriptBuilder& deleteFile (const StringOrBuilder& path) + { + script << "del /s /q " << path.value; + script << newLine; + return *this; + } + + MSVCScriptBuilder& mkdir (const StringOrBuilder& path) + { + script << "mkdir " << path.value; + script << newLine; + return *this; + } + + MSVCScriptBuilder& delay (int timeSeconds) + { + script << "timeout /t " << String { timeSeconds } << " /nobreak"; + script << newLine; + return *this; + } + + MSVCScriptBuilder& warning (const String& message) + { + script << "echo : Warning: " + message; + script << newLine; + return *this; + } + + MSVCScriptBuilder& info (const String& message) + { + script << "echo : Info: " << message; + script << newLine; + return *this; + } + + MSVCScriptBuilder& error (const String& message) + { + script << "echo : Error: " << message; + script << newLine; + return *this; + } + + MSVCScriptBuilder& run (const StringOrBuilder& command, bool echo = false) + { + if (echo) + script << "echo \"running " << command.value << "\"" << newLine; + + script << command.value; + script << newLine; + return *this; + } + + template + MSVCScriptBuilder& set (const String& name, T value) + { + script << "set " << name << "=" << value; + script << newLine; + return *this; + } + + MSVCScriptBuilder& runAndCheck (const StringOrBuilder& command, + const StringOrBuilder& success, + const StringOrBuilder& failed = {}) + { + run (command.value); + ifelse ("%ERRORLEVEL% equ 0", success.value, failed.value); + return *this; + } + + MSVCScriptBuilder& ifAllConditionsTrue (const StringArray& conditions, const StringOrBuilder& left) + { + jassert (left.isNotEmpty()); + + for (const auto& string : conditions) + script << "if " << string << " "; + + script << "(" << newLine << left.value << ")"; + script << newLine; + return *this; + } + + MSVCScriptBuilder& ifelse (const String& expr, + const StringOrBuilder& left, + const StringOrBuilder& right = {}) + { + jassert (left.isNotEmpty()); + + script << "if " << expr << " (" << newLine << left.value; + + if (right.isNotEmpty()) + script << ") else (" << newLine << right.value << ")"; + else + script << ")"; + + script << newLine; + return *this; + } + + MSVCScriptBuilder& append (const StringOrBuilder& string) + { + script << string.value << newLine; + return *this; + } + + MSVCScriptBuilder& labelledSection (const String& name, const StringOrBuilder& body) + { + script << ":" << name << newLine << body.value << newLine; + return *this; + } + + MSVCScriptBuilder& call (const String& label) + { + script << "call :" << label << newLine; + return *this; + } + + MSVCScriptBuilder& jump (const String& label) + { + script << "goto :" << label << newLine; + return *this; + } + + String build() const + { + MemoryOutputStream stream; + build (stream); + return stream.toUTF8(); + } + +private: + void build (OutputStream& stream) const + { + const auto genTab = [] (int depth, const int tabWidth = 4) + { + return String{}.paddedLeft (' ', depth * tabWidth); + }; + + int depth = 0; + auto lines = StringArray::fromLines (script); + + for (auto [lineIndex, line] : enumerate (lines)) + { + const auto trimmed = line.trim(); + const auto enter = trimmed.endsWith ("("); + const auto leave = trimmed.startsWith (")"); + + if (leave && depth > 0) + depth--; + + if (trimmed.isNotEmpty()) + { + stream << genTab (depth) << trimmed; + + if (lineIndex < lines.size() - 1) + stream << newLine; + } + + if (enter) + depth++; + } + } + + String script; +}; + enum class Architecture { win32, @@ -61,7 +248,7 @@ enum class Architecture arm64ec }; -static String toString (Architecture arch) +static String getArchitectureValueString (Architecture arch) { switch (arch) { @@ -84,7 +271,7 @@ static std::optional architectureTypeFromString (const String& str for (auto value : values) { - if (toString (value) == string) + if (getArchitectureValueString (value) == string) return value; } @@ -92,6 +279,34 @@ static std::optional architectureTypeFromString (const String& str return {}; } +static String getVisualStudioArchitectureId (Architecture architecture) +{ + switch (architecture) + { + case Architecture::win32: return "x86"; + case Architecture::win64: return "AMD64"; + case Architecture::arm64: return "ARM64"; + case Architecture::arm64ec: return "ARM64"; + } + + jassertfalse; + return ""; +} + +static String getVisualStudioPlatformId (Architecture architecture) +{ + switch (architecture) + { + case Architecture::win32: return "x86"; + case Architecture::win64: return "x64"; + case Architecture::arm64: return "ARM64"; + case Architecture::arm64ec: return "ARM64EC"; + } + + jassertfalse; + return ""; +} + //============================================================================== class MSVCProjectExporterBase : public ProjectExporter { @@ -259,7 +474,7 @@ class MSVCProjectExporterBase : public ProjectExporter multiProcessorCompilationValue (config, Ids::multiProcessorCompilation, getUndoManager(), true), intermediatesPathValue (config, Ids::intermediatesPath, getUndoManager()), characterSetValue (config, Ids::characterSet, getUndoManager()), - architectureTypeValue (config, Ids::winArchitecture, getUndoManager(), Array { toString (Architecture::win64) }, ","), + architectureTypeValue (config, Ids::winArchitecture, getUndoManager(), Array { getArchitectureValueString (Architecture::win64) }, ","), fastMathValue (config, Ids::fastMath, getUndoManager()), debugInformationFormatValue (config, Ids::debugInformationFormat, getUndoManager(), isDebug() ? "ProgramDatabase" : "None"), pluginBinaryCopyStepValue (config, Ids::enablePluginBinaryCopyStep, getUndoManager(), false), @@ -304,10 +519,10 @@ class MSVCProjectExporterBase : public ProjectExporter const auto selectedArchs = architectureTypeValue.get(); - if (! selectedArchs.getArray()->contains (toString (Architecture::arm64))) + if (! selectedArchs.getArray()->contains (getArchitectureValueString (Architecture::arm64))) return; - if (selectedArchs.getArray()->contains (toString (Architecture::arm64ec))) + if (selectedArchs.getArray()->contains (getArchitectureValueString (Architecture::arm64ec))) return; project.addProjectMessage (ProjectMessages::Ids::arm64Warning, {}); @@ -372,7 +587,7 @@ class MSVCProjectExporterBase : public ProjectExporter //============================================================================== String createMSVCConfigName (Architecture arch) const { - return getName() + "|" + toString (arch); + return getName() + "|" + getArchitectureValueString (arch); } String getOutputFilename (const String& suffix, @@ -410,8 +625,8 @@ class MSVCProjectExporterBase : public ProjectExporter for (const auto& arch : architectureList) { - architectureListAsStrings.add (toString (arch)); - architectureListAsVars.add (toString (arch)); + architectureListAsStrings.add (getArchitectureValueString (arch)); + architectureListAsVars.add (getArchitectureValueString (arch)); } props.add (new MultiChoicePropertyComponent (architectureTypeValue, "Architecture", architectureListAsStrings, architectureListAsVars), @@ -622,7 +837,7 @@ class MSVCProjectExporterBase : public ProjectExporter std::tuple (&LocationProperties::arm64, Architecture::arm64), std::tuple (&LocationProperties::arm64ec, Architecture::arm64ec) }) { - const auto archAndFormat = toString (arch) + " " + format; + const auto archAndFormat = getArchitectureValueString (arch) + " " + format; props.add (new TextPropertyComponentWithEnablement (locationProps.*member, pluginBinaryCopyStepValue, archAndFormat + " Binary Location", 1024, false), "The folder in which the compiled " + archAndFormat + " binary should be placed."); } @@ -683,7 +898,7 @@ class MSVCProjectExporterBase : public ProjectExporter auto* e = configsGroup->createNewChildElement ("ProjectConfiguration"); e->setAttribute ("Include", config.createMSVCConfigName (arch)); e->createNewChildElement ("Configuration")->addTextElement (config.getName()); - e->createNewChildElement ("Platform")->addTextElement (toString (arch)); + e->createNewChildElement ("Platform")->addTextElement (getArchitectureValueString (arch)); } } } @@ -1492,6 +1707,8 @@ class MSVCProjectExporterBase : public ProjectExporter String getExtraPostBuildSteps (const MSVCBuildConfiguration& config, Architecture arch) const { + using Builder = MSVCScriptBuilder; + const auto copyBuildOutputIntoBundle = [&] (const StringArray& segments) { return "copy /Y " @@ -1598,35 +1815,101 @@ class MSVCProjectExporterBase : public ProjectExporter if (writerTarget == nullptr) return ""; - const auto writer = writerTarget->getExpandedConfigTargetPath (config) - + "\\" - + writerTarget->getBinaryNameWithSuffix (config); - - // moduleinfotool doesn't handle Windows-style path separators properly when computing the bundle name - const auto normalisedBundlePath = getOwner().getOutDirFile (config, segments[0]).replace ("\\", "/"); - const auto contentsDir = normalisedBundlePath + "\\Contents"; - const auto resourceDir = contentsDir + "\\Resources"; - - return "\r\ndel /s /q " + (contentsDir + "\\moduleinfo.json").quoted() + "\r\n" - "if not exist \"" + resourceDir + "\\\" del /s /q " + resourceDir.quoted() + " && mkdir " + resourceDir.quoted() + "\r\n" - + writer.quoted() - + " -create -version " - + getOwner().project.getVersionString().quoted() - + " -path " - + normalisedBundlePath.quoted() - + " -output " - + (resourceDir + "\\moduleinfo.json").quoted(); + const auto helperExecutablePath = writerTarget->getExpandedConfigTargetPath (config) + + "\\" + + writerTarget->getBinaryNameWithSuffix (config); + + { + // moduleinfotool doesn't handle Windows-style path separators properly when computing the bundle name + const auto normalisedBundlePath = getOwner().getOutDirFile (config, segments[0]).replace ("\\", "/"); + const auto contentsDir = normalisedBundlePath + "\\Contents"; + const auto resourceDir = contentsDir + "\\Resources"; + const auto manifestPath = (resourceDir + "\\moduleinfo.json"); + const auto resourceDirPath = resourceDir + "\\"; + const auto pluginName = getOwner().project.getPluginNameString(); + + const auto manifestInvocationString = StringArray + { + helperExecutablePath.quoted(), + "-create", + "-version", getOwner().project.getVersionString().quoted(), + "-path", normalisedBundlePath.quoted(), + "-output", manifestPath.quoted() + }.joinIntoString (" "); + + const auto crossCompilationPairs = + { + // This catches ARM64 and EC for x64 manifest generation + std::pair { Architecture::arm64, Architecture::win64 }, + std::pair { arch, arch } + }; + + Builder builder; + + builder.set ("manifest_generated", 0); + + for (auto [hostArch, targetArch] : crossCompilationPairs) + { + const StringArray expr + { + "\"$(PROCESSOR_ARCHITECTURE)\" == " + getVisualStudioArchitectureId (hostArch).quoted(), + "\"$(Platform)\"" " == " + getVisualStudioPlatformId (targetArch).quoted() + }; + + builder.ifAllConditionsTrue (expr, Builder{}.call ("_generate_manifest") + .set ("manifest_generated", 1)); + } + + const auto archMismatchErrorString = StringArray + { + "VST3 manifest generation is disabled for", + pluginName, + "because a", + getVisualStudioArchitectureId (arch), + "manifest helper cannot run on a host system", + "processor detected to be $(PROCESSOR_ARCHITECTURE)." + }.joinIntoString (" "); + + const auto architectureMatched = Builder{} + .ifelse ("exist " + manifestPath.quoted(), + Builder{}.deleteFile (manifestPath.quoted())) + .ifelse ("not exist " + resourceDirPath.quoted(), + Builder{}.mkdir (resourceDirPath.quoted())) + .runAndCheck (manifestInvocationString, + Builder{}.info ("Successfully generated a manifest for " + pluginName) + .jump ("_continue"), + Builder{}.info ("The manifest helper failed") + .jump ("_continue")); + + builder.ifelse ("%manifest_generated% equ 0", + Builder{}.jump ("_arch_mismatch")); + + builder.jump ("_continue"); + builder.labelledSection ("_generate_manifest", architectureMatched); + builder.labelledSection ("_arch_mismatch", Builder{}.info (archMismatchErrorString)); + builder.labelledSection ("_continue", ""); + + return builder.build(); + } }(); const auto pkgScript = copyBuildOutputIntoBundle (segments); const auto copyScript = copyBundleToInstallDirectory (segments, config.getBinaryPath (Ids::vst3BinaryLocation, arch)); - return pkgScript + manifestScript + copyScript; + const auto binCopyScript = config.isPluginBinaryCopyStepEnabled() + ? MSVCScriptBuilder{}.append ("copy /Y \"$(OutDir)$(TargetFileName)\" \"" + + config.getBinaryPath (Ids::vstBinaryLocation, arch) + + "\\$(TargetFileName)\"") + : MSVCScriptBuilder{}; + + return MSVCScriptBuilder{} + .append (pkgScript) + .append (manifestScript) + .append (copyScript) + .append (binCopyScript) + .build(); } - if (type == VSTPlugIn && config.isPluginBinaryCopyStepEnabled()) - return "copy /Y \"$(OutDir)$(TargetFileName)\" \"" + config.getBinaryPath (Ids::vstBinaryLocation, arch) + "\\$(TargetFileName)\""; - return {}; } @@ -1635,15 +1918,18 @@ class MSVCProjectExporterBase : public ProjectExporter const auto createBundleStructure = [&] (const StringArray& segments) { auto directory = getOwner().getOutDirFile (config, ""); - String script; + MSVCScriptBuilder script; std::for_each (segments.begin(), std::prev (segments.end()), [&] (const auto& s) { directory += (directory.isEmpty() ? "" : "\\") + s; - script += "if not exist \"" + directory + "\\\" del /s /q " + directory.quoted() + " && mkdir " + directory.quoted() + "\r\n"; + + script.ifelse ("not exist " + (directory + "\\").quoted(), + MSVCScriptBuilder{}.deleteFile (directory.quoted()) + .mkdir (directory.quoted()).build()); }); - return script; + return script.build(); }; if (type == AAXPlugIn) @@ -1657,18 +1943,38 @@ class MSVCProjectExporterBase : public ProjectExporter String getPostBuildSteps (const MSVCBuildConfiguration& config, Architecture arch) const { - auto postBuild = config.getPostbuildCommandString().replace ("\n", "\r\n");; - auto extraPostBuild = getExtraPostBuildSteps (config, arch); + const auto post = config.getPostbuildCommandString(); + const auto extra = getExtraPostBuildSteps (config, arch); - return postBuild + String (postBuild.isNotEmpty() && extraPostBuild.isNotEmpty() ? "\r\n" : "") + extraPostBuild; + if (post.isNotEmpty() || extra.isNotEmpty()) + { + return MSVCScriptBuilder{} + .append ("cmd /c (") + .append (post.replace ("\n", "\r\n")) + .append (extra) + .append (")") + .build(); + } + + return ""; } String getPreBuildSteps (const MSVCBuildConfiguration& config, Architecture arch) const { - auto preBuild = config.getPrebuildCommandString().replace ("\n", "\r\n");; - auto extraPreBuild = getExtraPreBuildSteps (config, arch); + const auto pre = config.getPrebuildCommandString(); + const auto extra = getExtraPreBuildSteps (config, arch); - return preBuild + String (preBuild.isNotEmpty() && extraPreBuild.isNotEmpty() ? "\r\n" : "") + extraPreBuild; + if (pre.isNotEmpty() || extra.isNotEmpty()) + { + return MSVCScriptBuilder{} + .append ("cmd /c (") + .append (pre.replace ("\n", "\r\n")) + .append (extra) + .append (")") + .build(); + } + + return ""; } String getBinaryNameWithSuffix (const MSVCBuildConfiguration& config) const @@ -1751,7 +2057,7 @@ class MSVCProjectExporterBase : public ProjectExporter StringArray getAaxBundleStructure (const MSVCBuildConfiguration& config, Architecture arch) const { const auto dllName = config.getOutputFilename (".aaxplugin", false, type); - return { dllName, "Contents", toString (arch), dllName }; + return { dllName, "Contents", getArchitectureValueString (arch), dllName }; } StringArray getVst3BundleStructure (const MSVCBuildConfiguration& config, Architecture arch) const From 3ec470721764f187c8b1ac28de121904e930c311 Mon Sep 17 00:00:00 2001 From: Oliver James Date: Mon, 28 Oct 2024 01:20:27 +0000 Subject: [PATCH 23/76] Projucer: Don't run scripts on non-built targets --- .../Source/ProjectSaving/jucer_ProjectExport_MSVC.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h index 9dbc75521ae0..ad943f07f24d 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h @@ -1027,6 +1027,15 @@ class MSVCProjectExporterBase : public ProjectExporter libPath->addTextElement ("$(LibraryPath);" + librarySearchPaths.joinIntoString (";")); } } + + const auto enabled = config.getArchitectures().contains (arch) ? "true" : "false"; + + for (const auto optionName : { "PreBuildEventUseInBuild", "PostBuildEventUseInBuild" }) + { + auto* tag = props->createNewChildElement (optionName); + setConditionAttribute (*tag, config, arch); + tag->addTextElement (enabled); + } } } } From 0aaba52527649477fe366e06c8b1808e1ec9d06b Mon Sep 17 00:00:00 2001 From: Oliver James Date: Mon, 25 Nov 2024 16:16:22 +0000 Subject: [PATCH 24/76] Resave all projects --- .../DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj | 4 ++++ .../DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj | 4 ++++ .../Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj | 4 ++++ .../Builds/VisualStudio2019/AudioPluginHost_App.vcxproj | 4 ++++ .../Builds/VisualStudio2022/AudioPluginHost_App.vcxproj | 4 ++++ .../Builds/VisualStudio2022/BinaryBuilder_ConsoleApp.vcxproj | 4 ++++ .../Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj | 4 ++++ extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj | 4 ++++ extras/Projucer/Builds/VisualStudio2022/Projucer_App.vcxproj | 4 ++++ .../Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj | 4 ++++ .../Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj | 4 ++++ .../Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj | 4 ++++ 12 files changed, 48 insertions(+) diff --git a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj index b3c028a00893..377388730cd9 100644 --- a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj @@ -47,10 +47,14 @@ $(Platform)\$(Configuration)\App\ DemoRunner true + true + true $(SolutionDir)$(Platform)\$(Configuration)\App\ $(Platform)\$(Configuration)\App\ DemoRunner true + true + true diff --git a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj index 1145aa1b20e4..0aa583c9d398 100644 --- a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj @@ -47,10 +47,14 @@ $(Platform)\$(Configuration)\App\ DemoRunner true + true + true $(SolutionDir)$(Platform)\$(Configuration)\App\ $(Platform)\$(Configuration)\App\ DemoRunner true + true + true diff --git a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj index 1b6285e6c5bc..435dc4331ade 100644 --- a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj +++ b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj @@ -47,10 +47,14 @@ $(Platform)\$(Configuration)\App\ AudioPerformanceTest true + true + true $(SolutionDir)$(Platform)\$(Configuration)\App\ $(Platform)\$(Configuration)\App\ AudioPerformanceTest true + true + true diff --git a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj index 6c59173e0218..f44f8ff9dbd0 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj @@ -47,10 +47,14 @@ $(Platform)\$(Configuration)\App\ AudioPluginHost true + true + true $(SolutionDir)$(Platform)\$(Configuration)\App\ $(Platform)\$(Configuration)\App\ AudioPluginHost true + true + true diff --git a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj index d739b7def32b..e68c5fab1870 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj @@ -47,10 +47,14 @@ $(Platform)\$(Configuration)\App\ AudioPluginHost true + true + true $(SolutionDir)$(Platform)\$(Configuration)\App\ $(Platform)\$(Configuration)\App\ AudioPluginHost true + true + true diff --git a/extras/BinaryBuilder/Builds/VisualStudio2022/BinaryBuilder_ConsoleApp.vcxproj b/extras/BinaryBuilder/Builds/VisualStudio2022/BinaryBuilder_ConsoleApp.vcxproj index 93da43c13a24..366bc40fbe8a 100644 --- a/extras/BinaryBuilder/Builds/VisualStudio2022/BinaryBuilder_ConsoleApp.vcxproj +++ b/extras/BinaryBuilder/Builds/VisualStudio2022/BinaryBuilder_ConsoleApp.vcxproj @@ -47,10 +47,14 @@ $(Platform)\$(Configuration)\ConsoleApp\ BinaryBuilder true + true + true $(SolutionDir)$(Platform)\$(Configuration)\ConsoleApp\ $(Platform)\$(Configuration)\ConsoleApp\ BinaryBuilder true + true + true diff --git a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj index 8e795be0f107..761d9fbd5373 100644 --- a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj +++ b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj @@ -47,10 +47,14 @@ $(Platform)\$(Configuration)\App\ JUCE Network Graphics Demo true + true + true $(SolutionDir)$(Platform)\$(Configuration)\App\ $(Platform)\$(Configuration)\App\ JUCE Network Graphics Demo true + true + true diff --git a/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj b/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj index fa6b37df6492..f044782ebf69 100644 --- a/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj +++ b/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj @@ -47,10 +47,14 @@ $(Platform)\$(Configuration)\App\ Projucer true + true + true $(SolutionDir)$(Platform)\$(Configuration)\App\ $(Platform)\$(Configuration)\App\ Projucer true + true + true diff --git a/extras/Projucer/Builds/VisualStudio2022/Projucer_App.vcxproj b/extras/Projucer/Builds/VisualStudio2022/Projucer_App.vcxproj index 874aefdc5be2..04670029a0ec 100644 --- a/extras/Projucer/Builds/VisualStudio2022/Projucer_App.vcxproj +++ b/extras/Projucer/Builds/VisualStudio2022/Projucer_App.vcxproj @@ -47,10 +47,14 @@ $(Platform)\$(Configuration)\App\ Projucer true + true + true $(SolutionDir)$(Platform)\$(Configuration)\App\ $(Platform)\$(Configuration)\App\ Projucer true + true + true diff --git a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj index b603fc4129d4..d7db6fd365b2 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj +++ b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj @@ -47,10 +47,14 @@ $(Platform)\$(Configuration)\ConsoleApp\ UnitTestRunner true + true + true $(SolutionDir)$(Platform)\$(Configuration)\ConsoleApp\ $(Platform)\$(Configuration)\ConsoleApp\ UnitTestRunner true + true + true diff --git a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj index 013f1f84f9da..84f1c9922d06 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj +++ b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj @@ -47,10 +47,14 @@ $(Platform)\$(Configuration)\ConsoleApp\ UnitTestRunner true + true + true $(SolutionDir)$(Platform)\$(Configuration)\ConsoleApp\ $(Platform)\$(Configuration)\ConsoleApp\ UnitTestRunner true + true + true diff --git a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj index dc4478d9200c..0ce5ac00ffe8 100644 --- a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj +++ b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj @@ -47,10 +47,14 @@ $(Platform)\$(Configuration)\Dynamic Library\ juce_dll true + true + true $(SolutionDir)$(Platform)\$(Configuration)\Dynamic Library\ $(Platform)\$(Configuration)\Dynamic Library\ juce_dll true + true + true From 2b958c0416be4ae738fb43bfbd751595acf20636 Mon Sep 17 00:00:00 2001 From: Oliver James Date: Mon, 25 Nov 2024 16:18:07 +0000 Subject: [PATCH 25/76] Projucer: Add checks for incompatible LV2 architecture configurations --- .../ProjectSaving/jucer_ProjectExport_MSVC.h | 49 +++++++++++++++++-- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h index ad943f07f24d..805e17977ee9 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_MSVC.h @@ -1791,9 +1791,11 @@ class MSVCProjectExporterBase : public ProjectExporter return nullptr; }(); - const auto writer = writerTarget->getExpandedConfigTargetPath (config) + Builder builder; + const auto writer = (writerTarget->getExpandedConfigTargetPath (config) + "\\" - + writerTarget->getBinaryNameWithSuffix (config); + + writerTarget->getBinaryNameWithSuffix (config)).quoted() + + " \"$(OutDir)$(TargetFileName)\"\r\n"; const auto copyStep = "xcopy /E /H /I /K /R /Y \"$(OutDir)\" \"" + config.getBinaryPath (Ids::lv2BinaryLocation, arch) @@ -1801,9 +1803,12 @@ class MSVCProjectExporterBase : public ProjectExporter + config.getTargetBinaryNameString() + ".lv2\"\r\n"; - return writer.quoted() - + " \"$(OutDir)$(TargetFileName)\"\r\n" - + (config.isPluginBinaryCopyStepEnabled() ? copyStep : ""); + builder.runAndCheck (writer, + config.isPluginBinaryCopyStepEnabled() ? copyStep : Builder{}.info ("Sucessfully generated LV2 manifest").build(), + Builder{}.error ("Failed to generate LV2 manifest.") + .exit (-1)); + + return builder.build(); } if (type == VST3PlugIn) @@ -1941,6 +1946,40 @@ class MSVCProjectExporterBase : public ProjectExporter return script.build(); }; + if (type == LV2PlugIn) + { + const auto crossCompilationPairs = + { + // This catches ARM64 and EC for x64 manifest generation + std::pair { Architecture::arm64, Architecture::win64 }, + std::pair { arch, arch } + }; + + MSVCScriptBuilder builder; + + for (auto [hostArch, targetArch] : crossCompilationPairs) + { + const StringArray expr + { + "\"$(PROCESSOR_ARCHITECTURE)\" == " + getVisualStudioArchitectureId (hostArch).quoted(), + "\"$(Platform)\"" " == " + getVisualStudioPlatformId (targetArch).quoted() + }; + + builder.ifAllConditionsTrue (expr, MSVCScriptBuilder{}.jump ("_continue")); + } + + builder.error (StringArray + { + "\"$(Platform)\"", + "LV2 cross-compilation is not available on", + "\"$(PROCESSOR_ARCHITECTURE)\" hosts." + }.joinIntoString (" ")); + builder.exit (-1); + builder.labelledSection ("_continue", ""); + + return builder.build(); + } + if (type == AAXPlugIn) return createBundleStructure (getAaxBundleStructure (config, arch)); From 5023fc69d5680f4d8c1cd9c9140956fa99dd8d9c Mon Sep 17 00:00:00 2001 From: Oliver James Date: Mon, 18 Nov 2024 14:20:40 +0000 Subject: [PATCH 26/76] CMake: Passthrough OSX_DEPLOYMENT_TARGET when configuring juceaide --- extras/Build/juceaide/CMakeLists.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/extras/Build/juceaide/CMakeLists.txt b/extras/Build/juceaide/CMakeLists.txt index 7ef20eddf165..644744be4888 100644 --- a/extras/Build/juceaide/CMakeLists.txt +++ b/extras/Build/juceaide/CMakeLists.txt @@ -111,6 +111,12 @@ else() set(ENV{CMAKE_GENERATOR_PLATFORM} "${CMAKE_HOST_SYSTEM_PROCESSOR}") endif() + set(PASSTHROUGH_ARGS "") + + if(CMAKE_OSX_DEPLOYMENT_TARGET) + list(APPEND PASSTHROUGH_ARGS "-DCMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}") + endif() + # Looks like we're bootstrapping, reinvoke CMake execute_process(COMMAND "${CMAKE_COMMAND}" "." @@ -120,6 +126,7 @@ else() "-DCMAKE_BUILD_TYPE=Debug" "-DJUCE_BUILD_HELPER_TOOLS=ON" "-DCMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX}" + "${PASSTHROUGH_ARGS}" WORKING_DIRECTORY "${JUCE_SOURCE_DIR}" OUTPUT_VARIABLE command_output ERROR_VARIABLE command_output From e04cc9abe257b87b5b89b73a7359371ec25f6904 Mon Sep 17 00:00:00 2001 From: Oliver James Date: Mon, 18 Nov 2024 14:20:53 +0000 Subject: [PATCH 27/76] CMake: Passthrough CMAKE_XXX_COMPILER_LAUNCHER when configuring juceaide --- extras/Build/juceaide/CMakeLists.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/extras/Build/juceaide/CMakeLists.txt b/extras/Build/juceaide/CMakeLists.txt index 644744be4888..630681ca15ad 100644 --- a/extras/Build/juceaide/CMakeLists.txt +++ b/extras/Build/juceaide/CMakeLists.txt @@ -117,6 +117,14 @@ else() list(APPEND PASSTHROUGH_ARGS "-DCMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}") endif() + if(CMAKE_C_COMPILER_LAUNCHER) + list(APPEND PASSTHROUGH_ARGS "-DCMAKE_C_COMPILER_LAUNCHER=${CMAKE_C_COMPILER_LAUNCHER}") + endif() + + if(CMAKE_CXX_COMPILER_LAUNCHER) + list(APPEND PASSTHROUGH_ARGS "-DCMAKE_CXX_COMPILER_LAUNCHER=${CMAKE_CXX_COMPILER_LAUNCHER}") + endif() + # Looks like we're bootstrapping, reinvoke CMake execute_process(COMMAND "${CMAKE_COMMAND}" "." From bd322d0f784c46e61fea59289d964bd59c632cc3 Mon Sep 17 00:00:00 2001 From: Anthony Nicholls Date: Mon, 23 Sep 2024 12:31:51 +0100 Subject: [PATCH 28/76] String: Refactor a test function to be more generic --- modules/juce_core/text/juce_String.cpp | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/modules/juce_core/text/juce_String.cpp b/modules/juce_core/text/juce_String.cpp index 47cd5a64a62a..ff18f600e470 100644 --- a/modules/juce_core/text/juce_String.cpp +++ b/modules/juce_core/text/juce_String.cpp @@ -2371,7 +2371,8 @@ class StringTests final : public UnitTest { static void test (UnitTest& test, Random& r) { - String s (createRandomWideCharString (r)); + constexpr auto stringLength = 50; + const String s (createRandomWideCharString (r, stringLength)); using CharType = typename CharPointerType::CharType; CharType buffer[300]; @@ -2395,25 +2396,29 @@ class StringTests final : public UnitTest } }; - static String createRandomWideCharString (Random& r) + static String createRandomWideCharString (Random& r, size_t length) { - juce_wchar buffer[50] = { 0 }; + std::vector characters (length, 0); - for (int i = 0; i < numElementsInArray (buffer) - 1; ++i) + for (auto& character : characters) { if (r.nextBool()) { do { - buffer[i] = (juce_wchar) (1 + r.nextInt (0x10ffff - 1)); + character = (juce_wchar) (1 + r.nextInt (0x10ffff - 1)); } - while (! CharPointer_UTF16::canRepresent (buffer[i])); + while (! CharPointer_UTF16::canRepresent (character)); } else - buffer[i] = (juce_wchar) (1 + r.nextInt (0xff)); + { + character = (juce_wchar) (1 + r.nextInt (0xff)); + } } - return CharPointer_UTF32 (buffer); + characters.push_back (0); + + return CharPointer_UTF32 (characters.data()); } void runTest() override From 0823ee6aed9c18f178aa85236d3c254b76b5eed5 Mon Sep 17 00:00:00 2001 From: Anthony Nicholls Date: Mon, 25 Nov 2024 13:37:51 +0000 Subject: [PATCH 29/76] String: Fix the string length being passed in a UTF conversion test --- modules/juce_core/text/juce_String.cpp | 27 +++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/modules/juce_core/text/juce_String.cpp b/modules/juce_core/text/juce_String.cpp index ff18f600e470..ed06d7c93ae2 100644 --- a/modules/juce_core/text/juce_String.cpp +++ b/modules/juce_core/text/juce_String.cpp @@ -2375,24 +2375,25 @@ class StringTests final : public UnitTest const String s (createRandomWideCharString (r, stringLength)); using CharType = typename CharPointerType::CharType; - CharType buffer[300]; + constexpr auto bytesPerCodeUnit = sizeof (CharType); + constexpr auto maxCodeUnitsPerCodePoint = 4 / bytesPerCodeUnit; - memset (buffer, 0xff, sizeof (buffer)); - CharPointerType (buffer).writeAll (s.toUTF32()); - test.expectEquals (String (CharPointerType (buffer)), s); + std::array codeUnits{}; + const auto codeUnitsSizeInBytes = codeUnits.size() * bytesPerCodeUnit; - memset (buffer, 0xff, sizeof (buffer)); - CharPointerType (buffer).writeAll (s.toUTF16()); - test.expectEquals (String (CharPointerType (buffer)), s); + std::memset (codeUnits.data(), 0xff, codeUnitsSizeInBytes); + CharPointerType (codeUnits.data()).writeAll (s.toUTF32()); + test.expectEquals (String (CharPointerType (codeUnits.data())), s); - memset (buffer, 0xff, sizeof (buffer)); - CharPointerType (buffer).writeAll (s.toUTF8()); - test.expectEquals (String (CharPointerType (buffer)), s); + std::memset (codeUnits.data(), 0xff, codeUnitsSizeInBytes); + CharPointerType (codeUnits.data()).writeAll (s.toUTF16()); + test.expectEquals (String (CharPointerType (codeUnits.data())), s); - const auto nullTerminator = std::find (buffer, buffer + std::size (buffer), (CharType) 0); - const auto numValidBytes = (int) std::distance (buffer, nullTerminator) * (int) sizeof (CharType); + std::memset (codeUnits.data(), 0xff, codeUnitsSizeInBytes); + CharPointerType (codeUnits.data()).writeAll (s.toUTF8()); + test.expectEquals (String (CharPointerType (codeUnits.data())), s); - test.expect (CharPointerType::isValidString (buffer, numValidBytes)); + test.expect (CharPointerType::isValidString (codeUnits.data(), codeUnitsSizeInBytes)); } }; From 6b08ced201245209c411046c07c24fd451a4541e Mon Sep 17 00:00:00 2001 From: Anthony Nicholls Date: Fri, 13 Sep 2024 14:22:35 +0100 Subject: [PATCH 30/76] VST3: Add support for parameter migration --- BREAKING_CHANGES.md | 21 ++ .../Builds/Android/app/CMakeLists.txt | 2 + .../VisualStudio2019/DemoRunner_App.vcxproj | 3 + .../DemoRunner_App.vcxproj.filters | 3 + .../VisualStudio2022/DemoRunner_App.vcxproj | 3 + .../DemoRunner_App.vcxproj.filters | 3 + .../Builds/Android/app/CMakeLists.txt | 2 + .../AudioPerformanceTest_App.vcxproj | 3 + .../AudioPerformanceTest_App.vcxproj.filters | 3 + .../Builds/Android/app/CMakeLists.txt | 2 + .../AudioPluginHost_App.vcxproj | 3 + .../AudioPluginHost_App.vcxproj.filters | 3 + .../AudioPluginHost_App.vcxproj | 3 + .../AudioPluginHost_App.vcxproj.filters | 3 + .../Builds/Android/app/CMakeLists.txt | 2 + .../NetworkGraphicsDemo_App.vcxproj | 3 + .../NetworkGraphicsDemo_App.vcxproj.filters | 3 + .../UnitTestRunner_ConsoleApp.vcxproj | 3 + .../UnitTestRunner_ConsoleApp.vcxproj.filters | 3 + .../UnitTestRunner_ConsoleApp.vcxproj | 3 + .../UnitTestRunner_ConsoleApp.vcxproj.filters | 3 + .../WindowsDLL_DynamicLibrary.vcxproj | 3 + .../WindowsDLL_DynamicLibrary.vcxproj.filters | 3 + .../detail/juce_PluginUtilities.h | 89 +------- .../juce_audio_plugin_client_VST3.cpp | 213 +++++++++++++----- .../juce_audio_processors.cpp | 1 + .../processors/juce_AudioProcessor.h | 6 +- .../utilities/juce_VST3ClientExtensions.cpp | 167 ++++++++++++++ .../utilities/juce_VST3ClientExtensions.h | 148 +++++++++++- 29 files changed, 564 insertions(+), 143 deletions(-) create mode 100644 modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.cpp diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 0fddbf769406..88f5a1bda30b 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -37,6 +37,27 @@ The Javascript implementation increases compilation times while being required by only a select number of projects. +## Change + +The return type for VST3ClientExtensions::getCompatibleClasses() has changed +from a String to an array of 16 bytes. + +**Possible Issues** + +Any inherited classes overriding this method might fail to compile. + +**Workaround** + +Either explicitly switch to creating a 16-byte std::array or use +VST3ClientExtensions::toInterfaceId() to convert a string to a 16-byte array. + +**Rationale** + +As part of adding functionality to support migrating parameter IDs from +compatible plugins it was useful to switch to a safer type for representing +VST3 interface IDs that closer matches the VST3 SDK types. + + ## Change The VBlankAttachment class' inheritance from the ComponentPeer::VBlankListener diff --git a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt index 62f2831a24a7..23f6137865d5 100644 --- a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt +++ b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt @@ -860,6 +860,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_processors/utilities/juce_RangedAudioParameter.h" "../../../../../modules/juce_audio_processors/utilities/juce_VST2ClientExtensions.cpp" "../../../../../modules/juce_audio_processors/utilities/juce_VST2ClientExtensions.h" + "../../../../../modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.cpp" "../../../../../modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.h" "../../../../../modules/juce_audio_processors/juce_audio_processors.cpp" "../../../../../modules/juce_audio_processors/juce_audio_processors.mm" @@ -3455,6 +3456,7 @@ set_source_files_properties( "../../../../../modules/juce_audio_processors/utilities/juce_RangedAudioParameter.h" "../../../../../modules/juce_audio_processors/utilities/juce_VST2ClientExtensions.cpp" "../../../../../modules/juce_audio_processors/utilities/juce_VST2ClientExtensions.h" + "../../../../../modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.cpp" "../../../../../modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.h" "../../../../../modules/juce_audio_processors/juce_audio_processors.cpp" "../../../../../modules/juce_audio_processors/juce_audio_processors.mm" diff --git a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj index 377388730cd9..9247d2cf1cd6 100644 --- a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj @@ -1095,6 +1095,9 @@ true + + true + true diff --git a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters index 6f25f4f2bbfb..25877052c308 100644 --- a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters @@ -1804,6 +1804,9 @@ JUCE Modules\juce_audio_processors\utilities + + JUCE Modules\juce_audio_processors\utilities + JUCE Modules\juce_audio_processors diff --git a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj index 0aa583c9d398..d97ebc46391d 100644 --- a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj @@ -1095,6 +1095,9 @@ true + + true + true diff --git a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters index 730cd91e51a3..c1ba09e9889f 100644 --- a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters @@ -1804,6 +1804,9 @@ JUCE Modules\juce_audio_processors\utilities + + JUCE Modules\juce_audio_processors\utilities + JUCE Modules\juce_audio_processors diff --git a/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt b/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt index 34171f6197db..f9d6992c0349 100644 --- a/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt +++ b/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt @@ -815,6 +815,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_processors/utilities/juce_RangedAudioParameter.h" "../../../../../modules/juce_audio_processors/utilities/juce_VST2ClientExtensions.cpp" "../../../../../modules/juce_audio_processors/utilities/juce_VST2ClientExtensions.h" + "../../../../../modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.cpp" "../../../../../modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.h" "../../../../../modules/juce_audio_processors/juce_audio_processors.cpp" "../../../../../modules/juce_audio_processors/juce_audio_processors.mm" @@ -3070,6 +3071,7 @@ set_source_files_properties( "../../../../../modules/juce_audio_processors/utilities/juce_RangedAudioParameter.h" "../../../../../modules/juce_audio_processors/utilities/juce_VST2ClientExtensions.cpp" "../../../../../modules/juce_audio_processors/utilities/juce_VST2ClientExtensions.h" + "../../../../../modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.cpp" "../../../../../modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.h" "../../../../../modules/juce_audio_processors/juce_audio_processors.cpp" "../../../../../modules/juce_audio_processors/juce_audio_processors.mm" diff --git a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj index 435dc4331ade..e64a688c9cc1 100644 --- a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj +++ b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj @@ -1055,6 +1055,9 @@ true + + true + true diff --git a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters index 7a475a3df1e6..b05a019507d4 100644 --- a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters +++ b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters @@ -1594,6 +1594,9 @@ JUCE Modules\juce_audio_processors\utilities + + JUCE Modules\juce_audio_processors\utilities + JUCE Modules\juce_audio_processors diff --git a/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt b/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt index 35e7501e7e20..a88bfc6b68f1 100644 --- a/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt +++ b/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt @@ -848,6 +848,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_processors/utilities/juce_RangedAudioParameter.h" "../../../../../modules/juce_audio_processors/utilities/juce_VST2ClientExtensions.cpp" "../../../../../modules/juce_audio_processors/utilities/juce_VST2ClientExtensions.h" + "../../../../../modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.cpp" "../../../../../modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.h" "../../../../../modules/juce_audio_processors/juce_audio_processors.cpp" "../../../../../modules/juce_audio_processors/juce_audio_processors.mm" @@ -3256,6 +3257,7 @@ set_source_files_properties( "../../../../../modules/juce_audio_processors/utilities/juce_RangedAudioParameter.h" "../../../../../modules/juce_audio_processors/utilities/juce_VST2ClientExtensions.cpp" "../../../../../modules/juce_audio_processors/utilities/juce_VST2ClientExtensions.h" + "../../../../../modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.cpp" "../../../../../modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.h" "../../../../../modules/juce_audio_processors/juce_audio_processors.cpp" "../../../../../modules/juce_audio_processors/juce_audio_processors.mm" diff --git a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj index f44f8ff9dbd0..c37d716ce5a0 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj @@ -1063,6 +1063,9 @@ true + + true + true diff --git a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters index c5886faeb6d0..957d187b47bf 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters @@ -1669,6 +1669,9 @@ JUCE Modules\juce_audio_processors\utilities + + JUCE Modules\juce_audio_processors\utilities + JUCE Modules\juce_audio_processors diff --git a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj index e68c5fab1870..b28adb205918 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj @@ -1063,6 +1063,9 @@ true + + true + true diff --git a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters index aad4602684ea..5e4d2e64e9e1 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters @@ -1669,6 +1669,9 @@ JUCE Modules\juce_audio_processors\utilities + + JUCE Modules\juce_audio_processors\utilities + JUCE Modules\juce_audio_processors diff --git a/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt b/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt index e46ad7a010e2..be29f69e1791 100644 --- a/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt +++ b/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt @@ -819,6 +819,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_audio_processors/utilities/juce_RangedAudioParameter.h" "../../../../../modules/juce_audio_processors/utilities/juce_VST2ClientExtensions.cpp" "../../../../../modules/juce_audio_processors/utilities/juce_VST2ClientExtensions.h" + "../../../../../modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.cpp" "../../../../../modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.h" "../../../../../modules/juce_audio_processors/juce_audio_processors.cpp" "../../../../../modules/juce_audio_processors/juce_audio_processors.mm" @@ -3154,6 +3155,7 @@ set_source_files_properties( "../../../../../modules/juce_audio_processors/utilities/juce_RangedAudioParameter.h" "../../../../../modules/juce_audio_processors/utilities/juce_VST2ClientExtensions.cpp" "../../../../../modules/juce_audio_processors/utilities/juce_VST2ClientExtensions.h" + "../../../../../modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.cpp" "../../../../../modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.h" "../../../../../modules/juce_audio_processors/juce_audio_processors.cpp" "../../../../../modules/juce_audio_processors/juce_audio_processors.mm" diff --git a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj index 761d9fbd5373..56d8b7707e2a 100644 --- a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj +++ b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj @@ -1055,6 +1055,9 @@ true + + true + true diff --git a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters index f3ddc3d53629..b954c7a5de13 100644 --- a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters +++ b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters @@ -1624,6 +1624,9 @@ JUCE Modules\juce_audio_processors\utilities + + JUCE Modules\juce_audio_processors\utilities + JUCE Modules\juce_audio_processors diff --git a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj index d7db6fd365b2..6451d6a7fcdb 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj +++ b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj @@ -1071,6 +1071,9 @@ true + + true + true diff --git a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters index 399197e763d2..308240036510 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters +++ b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters @@ -1717,6 +1717,9 @@ JUCE Modules\juce_audio_processors\utilities + + JUCE Modules\juce_audio_processors\utilities + JUCE Modules\juce_audio_processors diff --git a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj index 84f1c9922d06..7a0e63f8eb72 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj +++ b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj @@ -1071,6 +1071,9 @@ true + + true + true diff --git a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters index 97053ec167f9..182b38de9c95 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters +++ b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters @@ -1717,6 +1717,9 @@ JUCE Modules\juce_audio_processors\utilities + + JUCE Modules\juce_audio_processors\utilities + JUCE Modules\juce_audio_processors diff --git a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj index 0ce5ac00ffe8..28d7d579de2e 100644 --- a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj +++ b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj @@ -1054,6 +1054,9 @@ true + + true + true diff --git a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj.filters b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj.filters index b6dcc7091bf0..343fe5911fac 100644 --- a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj.filters +++ b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj.filters @@ -1621,6 +1621,9 @@ JUCE Modules\juce_audio_processors\utilities + + JUCE Modules\juce_audio_processors\utilities + JUCE Modules\juce_audio_processors diff --git a/modules/juce_audio_plugin_client/detail/juce_PluginUtilities.h b/modules/juce_audio_plugin_client/detail/juce_PluginUtilities.h index 1e7744f94c1a..ca2193dd961b 100644 --- a/modules/juce_audio_plugin_client/detail/juce_PluginUtilities.h +++ b/modules/juce_audio_plugin_client/detail/juce_PluginUtilities.h @@ -66,88 +66,6 @@ struct PluginUtilities return hostType; } - // NB: Nasty old-fashioned code in here because it's copied from the Steinberg example code. - static void getUUIDForVST2ID (bool forControllerUID, uint8 uuid[16]) - { - #if JUCE_WINDOWS - const auto juce_sprintf = [] (auto&& head, auto&&... tail) { sprintf_s (head, (size_t) numElementsInArray (head), tail...); }; - const auto juce_strcpy = [] (auto&& head, auto&&... tail) { strcpy_s (head, (size_t) numElementsInArray (head), tail...); }; - const auto juce_strcat = [] (auto&& head, auto&&... tail) { strcat_s (head, (size_t) numElementsInArray (head), tail...); }; - const auto juce_sscanf = [] (auto&&... args) { sscanf_s (args...); }; - #else - const auto juce_sprintf = [] (auto&& head, auto&&... tail) { snprintf (head, (size_t) numElementsInArray (head), tail...); }; - const auto juce_strcpy = [] (auto&&... args) { strcpy (args...); }; - const auto juce_strcat = [] (auto&&... args) { strcat (args...); }; - const auto juce_sscanf = [] (auto&&... args) { sscanf (args...); }; - #endif - - char uidString[33]; - - const int vstfxid = (('V' << 16) | ('S' << 8) | (forControllerUID ? 'E' : 'T')); - char vstfxidStr[7] = { 0 }; - juce_sprintf (vstfxidStr, "%06X", vstfxid); - - juce_strcpy (uidString, vstfxidStr); - - char uidStr[9] = { 0 }; - juce_sprintf (uidStr, "%08X", JucePlugin_VSTUniqueID); - juce_strcat (uidString, uidStr); - - char nameidStr[3] = { 0 }; - const size_t len = strlen (JucePlugin_Name); - - for (size_t i = 0; i <= 8; ++i) - { - juce::uint8 c = i < len ? static_cast (JucePlugin_Name[i]) : 0; - - if (c >= 'A' && c <= 'Z') - c += 'a' - 'A'; - - juce_sprintf (nameidStr, "%02X", c); - juce_strcat (uidString, nameidStr); - } - - unsigned long p0; - unsigned int p1, p2; - unsigned int p3[8]; - - juce_sscanf (uidString, "%08lX%04X%04X%02X%02X%02X%02X%02X%02X%02X%02X", - &p0, &p1, &p2, &p3[0], &p3[1], &p3[2], &p3[3], &p3[4], &p3[5], &p3[6], &p3[7]); - - union q0_u { - uint32 word; - uint8 bytes[4]; - } q0; - - union q1_u { - uint16 half; - uint8 bytes[2]; - } q1, q2; - - q0.word = static_cast (p0); - q1.half = static_cast (p1); - q2.half = static_cast (p2); - - // VST3 doesn't use COM compatible UUIDs on non windows platforms - #if ! JUCE_WINDOWS - q0.word = ByteOrder::swap (q0.word); - q1.half = ByteOrder::swap (q1.half); - q2.half = ByteOrder::swap (q2.half); - #endif - - for (int i = 0; i < 4; ++i) - uuid[i+0] = q0.bytes[i]; - - for (int i = 0; i < 2; ++i) - uuid[i+4] = q1.bytes[i]; - - for (int i = 0; i < 2; ++i) - uuid[i+6] = q2.bytes[i]; - - for (int i = 0; i < 8; ++i) - uuid[i+8] = static_cast (p3[i]); - } - #if JucePlugin_Build_VST static bool handleManufacturerSpecificVST2Opcode ([[maybe_unused]] int32 index, [[maybe_unused]] pointer_sized_int value, @@ -158,9 +76,10 @@ struct PluginUtilities if ((index == (int32) ByteOrder::bigEndianInt ("stCA") || index == (int32) ByteOrder::bigEndianInt ("stCa")) && value == (int32) ByteOrder::bigEndianInt ("FUID") && ptr != nullptr) { - uint8 fuid[16]; - getUUIDForVST2ID (false, fuid); - ::memcpy (ptr, fuid, 16); + const auto uidString = VST3ClientExtensions::convertVST2PluginId (JucePlugin_VSTUniqueID, JucePlugin_Name, VST3ClientExtensions::InterfaceType::component); + MemoryBlock uidValue; + uidValue.loadFromHexString (uidString); + uidValue.copyTo (ptr, 0, uidValue.getSize()); return true; } #endif diff --git a/modules/juce_audio_plugin_client/juce_audio_plugin_client_VST3.cpp b/modules/juce_audio_plugin_client/juce_audio_plugin_client_VST3.cpp index aa6b9800bec7..63045e3dbd3d 100644 --- a/modules/juce_audio_plugin_client/juce_audio_plugin_client_VST3.cpp +++ b/modules/juce_audio_plugin_client/juce_audio_plugin_client_VST3.cpp @@ -38,6 +38,38 @@ //============================================================================== #if JucePlugin_Build_VST3 +#if JUCE_VST3_CAN_REPLACE_VST2 && ! JUCE_FORCE_USE_LEGACY_PARAM_IDS && ! JUCE_IGNORE_VST3_MISMATCHED_PARAMETER_ID_WARNING + + // If you encounter this error there may be an issue migrating parameter + // automation between sessions saved using the VST2 and VST3 versions of this + // plugin. + // + // If you have released neither a VST2 or VST3 version of the plugin, + // consider only releasing a VST3 version and disabling JUCE_VST3_CAN_REPLACE_VST2. + // + // If you have released a VST2 version of the plugin but have not yet released + // a VST3 version of the plugin, consider enabling JUCE_FORCE_USE_LEGACY_PARAM_IDS. + // This will ensure that the parameter IDs remain compatible between both the + // VST2 and VST3 versions of the plugin in all hosts. + // + // If you have released a VST3 version of the plugin but have not released a + // VST2 version of the plugin, enable JUCE_IGNORE_VST3_MISMATCHED_PARAMETER_ID_WARNING. + // DO NOT change the JUCE_VST3_CAN_REPLACE_VST2 or JUCE_FORCE_USE_LEGACY_PARAM_IDS + // values as this will break compatibility with currently released VST3 + // versions of the plugin. + // + // If you have already released a VST2 and VST3 version of the plugin you may + // find in some hosts when a session containing automation data is saved using + // the VST2 or VST3 version, and is later loaded using the other version, the + // automation data will fail to control any of the parameters in the plugin as + // the IDs for these parameters are different. To fix parameter automation for + // the VST3 plugin when a session was saved with the VST2 plugin, implement + // VST3ClientExtensions::getCompatibleParameterIds() and enable + // JUCE_IGNORE_VST3_MISMATCHED_PARAMETER_ID_WARNING. + + #error You may have a conflict with parameter automation between VST2 and VST3 versions of your plugin. See the comment above for more details. +#endif + JUCE_BEGIN_NO_SANITIZE ("vptr") #if JUCE_PLUGINHOST_VST3 @@ -97,22 +129,32 @@ JUCE_BEGIN_NO_SANITIZE ("vptr") namespace juce { -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4310) -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wall") +using VST3InterfaceType = VST3ClientExtensions::InterfaceType; +using VST3InterfaceId = VST3ClientExtensions::InterfaceId; -#if JUCE_VST3_CAN_REPLACE_VST2 - static Steinberg::FUID getFUIDForVST2ID (bool forControllerUID) - { - Steinberg::TUID uuid; - detail::PluginUtilities::getUUIDForVST2ID (forControllerUID, (uint8*) uuid); - return Steinberg::FUID (uuid); - } -#endif +using namespace Steinberg; -JUCE_END_IGNORE_WARNINGS_MSVC -JUCE_END_IGNORE_WARNINGS_GCC_LIKE +static FUID toSteinbergUID (const VST3InterfaceId& uid) +{ + return FUID::fromTUID ((const char*) (uid.data())); +} -using namespace Steinberg; +static VST3InterfaceId toVST3InterfaceId (const TUID uid) +{ + VST3InterfaceId iid; + std::memcpy (iid.data(), uid, iid.size()); + return iid; +} + +static VST3InterfaceId getInterfaceId (VST3InterfaceType interfaceType) +{ + #if JUCE_VST3_CAN_REPLACE_VST2 + if (interfaceType == VST3InterfaceType::controller || interfaceType == VST3InterfaceType::component) + return VST3ClientExtensions::convertVST2PluginId (JucePlugin_VSTUniqueID, JucePlugin_Name, interfaceType); + #endif + + return VST3ClientExtensions::convertJucePluginId (JucePlugin_ManufacturerCode, JucePlugin_PluginCode, interfaceType); +} //============================================================================== #if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE @@ -502,13 +544,15 @@ class JuceAudioProcessor final : public Vst::IUnitInfo #if JUCE_FORCE_USE_LEGACY_PARAM_IDS return static_cast (paramIndex); #else + jassert (paramIndex < vstParamIDs.size()); return vstParamIDs.getReference (paramIndex); #endif } AudioProcessorParameter* getParamForVSTParamID (Vst::ParamID paramID) const noexcept { - return paramMap[static_cast (paramID)]; + const auto iter = paramMap.find (paramID); + return iter != paramMap.end() ? iter->second : nullptr; } AudioProcessorParameter* getBypassParameter() const noexcept @@ -561,8 +605,61 @@ class JuceAudioProcessor final : public Vst::IUnitInfo bool isUsingManagedParameters() const noexcept { return juceParameters.isUsingManagedParameters(); } + std::map getParameterMap (const VST3InterfaceId& pluginId) const + { + const auto iter = compatibleParameterIdMap.find (pluginId); + return iter != compatibleParameterIdMap.end() ? iter->second + : std::map{}; + } + + AudioProcessorParameter* getParameter (const String& juceParamId) const + { + const auto iter = juceIdParameterMap.find (juceParamId); + return iter != juceIdParameterMap.end() ? iter->second : nullptr; + } + + void updateParameterMapping() + { + static const auto currentPluginId = getInterfaceId (VST3InterfaceType::component); + + compatibleParameterIdMap = {}; + compatibleParameterIdMap[currentPluginId] = paramMap; + + if (const auto* ext = audioProcessor->getVST3ClientExtensions()) + { + for (auto& compatibleClass : ext->getCompatibleClasses()) + { + auto& parameterIdMap = compatibleParameterIdMap[compatibleClass]; + + for (auto [oldParamId, newParamId] : ext->getCompatibleParameterIds (compatibleClass)) + { + auto* parameter = getParameter (newParamId); + parameterIdMap[oldParamId] = parameter; + + // This means a parameter ID returned by getCompatibleParameterIds() + // does not match any parameters declared in the plugin. All IDs must + // match an existing parameter, or return an empty string to indicate + // there is no parameter to map to. + jassert (parameter != nullptr || newParamId.isEmpty()); + + // This means getCompatibleParameterIds() returned a parameter mapping + // that will hide a parameter in the current plugin! If this is due to + // an ID collision between plugin versions, you may be able to determine + // the mapping to report based on setStateInformation(). If you've + // already done this you can safely ignore this warning. If there is no + // way to determine the difference between the two plugin versions in + // setStateInformation() the best course of action is to remove the + // problematic parameter from the mapping. + jassert (compatibleClass != currentPluginId + || getParamForVSTParamID (oldParamId) == nullptr + || parameter == getParamForVSTParamID (oldParamId)); + } + } + } + } + //============================================================================== - inline static const FUID iid { TUID INLINE_UID (0x0101ABAB, 0xABCDEF01, JucePlugin_ManufacturerCode, JucePlugin_PluginCode) }; + inline static const FUID iid = toSteinbergUID (getInterfaceId (VST3InterfaceType::processor)); private: //============================================================================== @@ -570,7 +667,7 @@ class JuceAudioProcessor final : public Vst::IUnitInfo { parameterGroups = audioProcessor->getParameterTree().getSubgroups (true); - #if JUCE_DEBUG + #if JUCE_ASSERTIONS_ENABLED_OR_LOGGED auto allGroups = parameterGroups; allGroups.add (&audioProcessor->getParameterTree()); std::unordered_set unitIDs; @@ -633,7 +730,8 @@ class JuceAudioProcessor final : public Vst::IUnitInfo } vstParamIDs.add (vstParamID); - paramMap.set (static_cast (vstParamID), juceParam); + paramMap[vstParamID] = juceParam; + juceIdParameterMap[LegacyAudioParameter::getParamID (juceParam, false)] = juceParam; } auto numPrograms = audioProcessor->getNumPrograms(); @@ -650,7 +748,7 @@ class JuceAudioProcessor final : public Vst::IUnitInfo programParamID = static_cast (i++); vstParamIDs.add (programParamID); - paramMap.set (static_cast (programParamID), ownedProgramParameter.get()); + paramMap[programParamID] = ownedProgramParameter.get(); } cachedParamValues = CachedParamValues { { vstParamIDs.begin(), vstParamIDs.end() } }; @@ -658,20 +756,13 @@ class JuceAudioProcessor final : public Vst::IUnitInfo Vst::ParamID generateVSTParamIDForParam (const AudioProcessorParameter* param) { - auto juceParamID = LegacyAudioParameter::getParamID (param, false); + const auto juceParamID = LegacyAudioParameter::getParamID (param, false); - #if JUCE_FORCE_USE_LEGACY_PARAM_IDS + #if JUCE_FORCE_USE_LEGACY_PARAM_IDS return static_cast (juceParamID.getIntValue()); - #else - auto paramHash = static_cast (juceParamID.hashCode()); - - #if JUCE_USE_STUDIO_ONE_COMPATIBLE_PARAMETERS - // studio one doesn't like negative parameters - paramHash &= ~(((Vst::ParamID) 1) << (sizeof (Vst::ParamID) * 8 - 1)); + #else + return VST3ClientExtensions::convertJuceParameterId (juceParamID, JUCE_USE_STUDIO_ONE_COMPATIBLE_PARAMETERS); #endif - - return paramHash; - #endif } //============================================================================== @@ -679,6 +770,8 @@ class JuceAudioProcessor final : public Vst::IUnitInfo CachedParamValues cachedParamValues; Vst::ParamID bypassParamID = 0, programParamID = static_cast (paramPreset); bool bypassIsRegularParameter = false; + std::map> compatibleParameterIdMap; + std::map juceIdParameterMap; //============================================================================== std::atomic refCount { 0 }; @@ -686,7 +779,7 @@ class JuceAudioProcessor final : public Vst::IUnitInfo //============================================================================== LegacyAudioParametersWrapper juceParameters; - HashMap paramMap; + std::map paramMap; std::unique_ptr ownedBypassParameter, ownedProgramParameter; Array parameterGroups; @@ -764,6 +857,7 @@ static void setValueAndNotifyIfChanged (AudioProcessorParameter& param, float ne class JuceVST3EditController final : public Vst::EditController, public Vst::IMidiMapping, public Vst::IUnitInfo, + public Vst::IRemapParamID, public Vst::ChannelContext::IInfoListener, #if JucePlugin_Enable_ARA public Presonus::IPlugInViewEmbedding, @@ -783,12 +877,7 @@ class JuceVST3EditController final : public Vst::EditController, } //============================================================================== - - #if JUCE_VST3_CAN_REPLACE_VST2 - inline static const FUID iid = getFUIDForVST2ID (true); - #else - inline static const FUID iid { TUID INLINE_UID (0xABCDEF01, 0x1234ABCD, JucePlugin_ManufacturerCode, JucePlugin_PluginCode) }; - #endif + inline static const FUID iid = toSteinbergUID (getInterfaceId (VST3InterfaceType::controller)); //============================================================================== JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Winconsistent-missing-override") @@ -1028,6 +1117,30 @@ class JuceVST3EditController final : public Vst::EditController, JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProgramChangeParameter) }; + //============================================================================== + tresult PLUGIN_API getCompatibleParamID (const TUID pluginToReplaceUID, + Vst::ParamID oldParamID, + Vst::ParamID& newParamID) override + { + const auto parameterMap = audioProcessor->getParameterMap (toVST3InterfaceId (pluginToReplaceUID)); + const auto iter = parameterMap.find (oldParamID); + + if (iter == parameterMap.end()) + { + // This suggests a host is trying to load a plugin and parameter ID + // combination that hasn't been accounted for in getCompatibleParameterIds(). + // Override this method in VST3ClientExtensions and return a suitable + // parameter mapping to silence this warning. + jassertfalse; + return kResultFalse; + } + + const auto* parameter = iter->second; + newParamID = parameter != nullptr ? audioProcessor->getVSTParamIDForIndex (parameter->getParameterIndex()) + : 0xffffffff; + return kResultTrue; + } + //============================================================================== tresult PLUGIN_API setChannelContextInfos (Vst::IAttributeList* list) override { @@ -1101,8 +1214,10 @@ class JuceVST3EditController final : public Vst::EditController, } } + audioProcessor->updateParameterMapping(); + if (auto* handler = getComponentHandler()) - handler->restartComponent (Vst::kParamValuesChanged); + handler->restartComponent (Vst::kParamValuesChanged | Vst::kParamIDMappingChanged); return kResultOk; } @@ -1549,6 +1664,7 @@ class JuceVST3EditController final : public Vst::EditController, UniqueBase{}, UniqueBase{}, UniqueBase{}, + UniqueBase{}, UniqueBase{}, SharedBase{}, UniqueBase{}, @@ -1557,6 +1673,9 @@ class JuceVST3EditController final : public Vst::EditController, #endif SharedBase{}); + if (targetIID == Vst::IRemapParamID::iid) + jassertfalse; + if (result.isOk()) return result; @@ -2508,7 +2627,7 @@ class JuceVST3EditController final : public Vst::EditController, return createARAFactory(); } - inline static const FUID iid { TUID INLINE_UID (0xABCDEF01, 0xA1B2C3D4, JucePlugin_ManufacturerCode, JucePlugin_PluginCode) }; + inline static const FUID iid = toSteinbergUID (getInterfaceId (VST3InterfaceType::ara)); private: //============================================================================== @@ -2581,11 +2700,7 @@ class JuceVST3Component final : public Vst::IComponent, AudioProcessor& getPluginInstance() const noexcept { return *pluginInstance; } //============================================================================== - #if JUCE_VST3_CAN_REPLACE_VST2 - inline static const FUID iid = getFUIDForVST2ID (false); - #else - inline static const FUID iid { TUID INLINE_UID (0xABCDEF01, 0x9182FAEB, JucePlugin_ManufacturerCode, JucePlugin_PluginCode) }; - #endif + inline static const FUID iid = toSteinbergUID (getInterfaceId (VST3InterfaceType::component)); JUCE_DECLARE_VST3_COM_REF_METHODS @@ -3998,15 +4113,7 @@ class JucePluginCompatibility final : public IPluginCompatibility Array oldArray; for (const auto& uid : extensions->getCompatibleClasses()) - { - // All UIDs returned from getCompatibleClasses should be 32 characters long - jassert (uid.length() == 32); - - // All UIDs returned from getCompatibleClasses should be in hex notation - jassert (uid.containsOnly ("ABCDEF0123456789")); - - oldArray.add (uid); - } + oldArray.add (String::toHexString (uid.data(), (int) uid.size(), 0)); return oldArray; }()); @@ -4034,7 +4141,7 @@ class JucePluginCompatibility final : public IPluginCompatibility return kNotImplemented; } - inline static const FUID iid { TUID INLINE_UID (0xABCDEF01, 0xC0DEF00D, JucePlugin_ManufacturerCode, JucePlugin_PluginCode) }; + inline static const FUID iid = toSteinbergUID (getInterfaceId (VST3InterfaceType::compatibility)); private: std::atomic refCount { 1 }; diff --git a/modules/juce_audio_processors/juce_audio_processors.cpp b/modules/juce_audio_processors/juce_audio_processors.cpp index 873ca3ade0bf..a3d5d7ed9e08 100644 --- a/modules/juce_audio_processors/juce_audio_processors.cpp +++ b/modules/juce_audio_processors/juce_audio_processors.cpp @@ -231,6 +231,7 @@ struct NSViewComponentWithParent : public NSViewComponent, #include "utilities/juce_PluginHostType.cpp" #include "utilities/juce_AAXClientExtensions.cpp" #include "utilities/juce_VST2ClientExtensions.cpp" +#include "utilities/juce_VST3ClientExtensions.cpp" #include "utilities/ARA/juce_ARA_utils.cpp" #include "format_types/juce_LV2PluginFormat.cpp" diff --git a/modules/juce_audio_processors/processors/juce_AudioProcessor.h b/modules/juce_audio_processors/processors/juce_AudioProcessor.h index 74376b509664..103d303c1251 100644 --- a/modules/juce_audio_processors/processors/juce_AudioProcessor.h +++ b/modules/juce_audio_processors/processors/juce_AudioProcessor.h @@ -1154,7 +1154,11 @@ class JUCE_API AudioProcessor : private AAXClientExtensions See also the helper function getXmlFromBinary() for loading settings as XML. - @see setCurrentProgramStateInformation + VST3ClientExtensions::getCompatibleParameterIds() will always be called after + setStateInformation() therefore you can use information from the plugin state + to determine which parameter mapping to use if necessary. + + @see setCurrentProgramStateInformation, VST3ClientExtensions::getCompatibleParameterIds */ virtual void setStateInformation (const void* data, int sizeInBytes) = 0; diff --git a/modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.cpp b/modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.cpp new file mode 100644 index 000000000000..b3cd133bb843 --- /dev/null +++ b/modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.cpp @@ -0,0 +1,167 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +std::map VST3ClientExtensions::getCompatibleParameterIds (const InterfaceId&) const +{ + return {}; +} + +VST3ClientExtensions::InterfaceId VST3ClientExtensions::convertJucePluginId (uint32_t manufacturerCode, + uint32_t pluginCode, + InterfaceType interfaceType) +{ + const auto word0 = std::invoke ([&]() -> uint32_t + { + switch (interfaceType) + { + case InterfaceType::ara: [[fallthrough]]; + case InterfaceType::controller: [[fallthrough]]; + case InterfaceType::compatibility: [[fallthrough]]; + case InterfaceType::component: return 0xABCDEF01; + case InterfaceType::processor: return 0x0101ABAB; + } + + jassertfalse; + return 0; + }); + + const auto word1 = std::invoke ([&]() -> uint32_t + { + switch (interfaceType) + { + case InterfaceType::ara: return 0xA1B2C3D4; + case InterfaceType::controller: return 0x1234ABCD; + case InterfaceType::compatibility: return 0xC0DEF00D; + case InterfaceType::component: return 0x9182FAEB; + case InterfaceType::processor: return 0xABCDEF01; + } + + jassertfalse; + return 0; + }); + + std::array data; + std::memcpy (&data[0], &word0, sizeof (word0)); + std::memcpy (&data[4], &word1, sizeof (word1)); + std::memcpy (&data[8], &manufacturerCode, sizeof (manufacturerCode)); + std::memcpy (&data[12], &pluginCode, sizeof (pluginCode)); + + return data; +} + +VST3ClientExtensions::InterfaceId VST3ClientExtensions::convertVST2PluginId (uint32_t pluginCode, + const String& pluginName, + InterfaceType interfaceType) +{ + VST3ClientExtensions::InterfaceId iid{}; + + iid[0] = (std::byte) 'V'; + iid[1] = (std::byte) 'S'; + iid[2] = (std::byte) std::invoke ([&] + { + switch (interfaceType) + { + case InterfaceType::controller: return 'E'; + case InterfaceType::component: return 'T'; + case InterfaceType::ara: [[fallthrough]]; + case InterfaceType::compatibility: [[fallthrough]]; + case InterfaceType::processor: break; + } + + // A VST2 plugin only has two interfaces + // - component (the audio effect) + // - controller (the editor/UI) + jassertfalse; + return '\0'; + }); + iid[3] = (std::byte) (pluginCode >> 24); + iid[4] = (std::byte) (pluginCode >> 16); + iid[5] = (std::byte) (pluginCode >> 8); + iid[6] = (std::byte) pluginCode; + + for (const auto [index, character] : enumerate (pluginName, (size_t) 7)) + { + if (index >= iid.size()) + break; + + iid[index] = (std::byte) CharacterFunctions::toLowerCase (character); + } + + #if JUCE_WINDOWS + std::swap (iid[0], iid[3]); + std::swap (iid[1], iid[2]); + std::swap (iid[4], iid[5]); + std::swap (iid[6], iid[7]); + #endif + + return iid; +} + +uint32_t VST3ClientExtensions::convertJuceParameterId (const String& parameterId, bool studioOneCompatible) +{ + auto hash = (uint32_t) (parameterId.hashCode()); + + if (studioOneCompatible) + hash &= 0x7fffffff; + + return hash; +} + +VST3ClientExtensions::InterfaceId VST3ClientExtensions::toInterfaceId (const String& interfaceIdString) +{ + jassert (interfaceIdString.length() == 32); + jassert (interfaceIdString.containsOnly ("0123456789abcdefABCDEF")); + + return { (std::byte) interfaceIdString.substring ( 0, 2).getHexValue32(), + (std::byte) interfaceIdString.substring ( 2, 4).getHexValue32(), + (std::byte) interfaceIdString.substring ( 4, 6).getHexValue32(), + (std::byte) interfaceIdString.substring ( 6, 8).getHexValue32(), + (std::byte) interfaceIdString.substring ( 8, 10).getHexValue32(), + (std::byte) interfaceIdString.substring (10, 12).getHexValue32(), + (std::byte) interfaceIdString.substring (12, 14).getHexValue32(), + (std::byte) interfaceIdString.substring (14, 16).getHexValue32(), + (std::byte) interfaceIdString.substring (16, 18).getHexValue32(), + (std::byte) interfaceIdString.substring (18, 20).getHexValue32(), + (std::byte) interfaceIdString.substring (20, 22).getHexValue32(), + (std::byte) interfaceIdString.substring (22, 24).getHexValue32(), + (std::byte) interfaceIdString.substring (24, 26).getHexValue32(), + (std::byte) interfaceIdString.substring (26, 28).getHexValue32(), + (std::byte) interfaceIdString.substring (28, 30).getHexValue32(), + (std::byte) interfaceIdString.substring (30, 32).getHexValue32() }; +} + +} // namespace juce diff --git a/modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.h b/modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.h index 09c8cdb73aa3..cee4e6a30554 100644 --- a/modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.h +++ b/modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.h @@ -104,16 +104,156 @@ struct VST3ClientExtensions */ virtual bool getPluginHasMainInput() const { return true; } - /** This function should return the UIDs of any compatible VST2 plug-ins. + using InterfaceId = std::array; - Each item in the vector should be a 32-character string consisting only - of the characters 0-9 and A-F. + /** This function should return the UIDs of any compatible VST2 or VST3 + plug-ins. This information will be used to implement the IPluginCompatibility interface. Hosts can use this interface to determine whether this VST3 is capable of replacing a given VST2. + + Each compatible class is a 16-byte array that corresponds to the VST3 + interface ID for the class implementing the IComponent interface. + For VST2 or JUCE plugins these IDs can be determined in the following + ways: + - Use convertVST2PluginId() for VST2 plugins or JUCE VST3 plugins with + JUCE_VST3_CAN_REPLACE_VST3 enabled + - Use convertJucePluginId() for any other JUCE VST3 plugins + + If JUCE_VST3_CAN_REPLACE_VST2 is enabled the VST3 plugin will have the + same identifier as the VST2 plugin and therefore there will be no need + to implement this function. + + If the parameter IDs between compatible versions differ + getCompatibleParameterIds() should also be overridden. However, unlike + getCompatibleParameterIds() this function should remain constant and + always return the same IDs. + + @see getCompatibleParameterIds() */ - virtual std::vector getCompatibleClasses() const { return {}; } + virtual std::vector getCompatibleClasses() const { return {}; } + + /** This function should return a map of VST3 parameter IDs and the JUCE + parameters they map to. + + This information is used to implement the IRemapParameter interface. + Hosts can use this to preserve automation data when a session was saved + using a compatible plugin that has different parameter IDs. + + Not all hosts will take this information into account. Therefore, + parameter IDs should be maintained between plugin versions. For JUCE + plugins migrating from VST2 to VST3 the best method for achieving this + is enabling JUCE_FORCE_LEGACY_PARAM_IDS. However, if a plugin has + already been released without enabling this flag, this method offers an + alternative approach that won't cause any further compatibility issues. + + The key in the map is a VST3 parameter identifier or Vst::ParamID. For + VST2 or JUCE plugins these IDs can be determined in the following ways + - Use the parameter index for + - VST2 plugins + - JUCE VST3 plugins with JUCE_FORCE_LEGACY_PARAM_IDS enabled + - Any parameter that doesn't inherit from HostedAudioProcessorParameter + - Use convertJuceParameterId() for JUCE VST3 plugins where + JUCE_FORCE_LEGACY_PARAM_IDS is disabled + + The value in the map is the JUCE parameter ID for the parameter to map + to, or an empty string to indicate that there is no parameter to map to. + If a parameter doesn't inherit from HostedAudioProcessorParameter its ID + will be the parameter index as a string, for example "1". Otherwise + always use the actual parameter ID (even if JUCE_FORCE_LEGACY_PARAM_IDS + is enabled). + + In the unlikely event that two plugins share the same plugin ID, and + both have a different parameters that share the same parameter ID, it + may be possible to determine which version of the plugin is being loaded + during setStateInformation(). This method will always be called after + setStateInformation(), so that the map with the correct mapping can be + provided when queried. + + Below is an example of how you might implement this function for a JUCE + VST3 plugin where JUCE_VST3_CAN_REPLACE_VST2 is enabled, but + JUCE_FORCE_LEGACY_PARAM_IDS is disabled. + + @code + std::map getCompatibleParameterIds (const String&) const override + { + return { { 0, "Frequency" }, + { 1, "CutOff" }, + { 2, "Gain" }, + { 3, "Bypass" } }; + } + @endcode + + @param compatibleClass A plugin identifier, either for the current + plugin or one listed in getCompatibleClasses(). + This parameter allows the implementation to + return a different parameter map for each + compatible class. Use convertJucePluginId() and + convertVST2PluginId() to determine the class IDs + used by JUCE plugins. + + @returns A map where each key is a VST3 parameter ID in the compatible + plugin, and the value is the unique JUCE parameter ID in the + current plugin that it should be mapped to. + + @see getCompatibleClasses, convertJucePluginId, convertVST2PluginId, convertJuceParameterId + */ + virtual std::map getCompatibleParameterIds (const InterfaceId& compatibleClass) const; + + /** An enum indicating the various VST3 interface types. + + In most cases users shouldn't need to concern themselves with any + interfaces other than the component, which is used to report the actual + audio effect. + */ + enum class InterfaceType + { + ara, + controller, + compatibility, + component, + processor + }; + + /** Returns a 16-byte array indicating the VST3 interface ID used for a + given JUCE VST3 plugin. + + Internally this is what JUCE will use to assign an ID to each VST3 + interface, unless JUCE_VST3_CAN_REPLACE_VST2 is enabled. + + @see convertVST2PluginId, getCompatibleClasses, getCompatibleParameterIds + */ + static InterfaceId convertJucePluginId (uint32_t manufacturerCode, + uint32_t pluginCode, + InterfaceType interfaceType = InterfaceType::component); + + /** Returns a 16-byte array indicating the VST3 interface ID used for a + given VST2 plugin. + + Internally JUCE will use this method to assign an ID for the component + and controller interfaces when JUCE_VST3_CAN_REPLACE_VST2 is enabled. + + @convertJucePluginId, getCompatibleClasses, getCompatibleParameterIds + */ + static InterfaceId convertVST2PluginId (uint32_t pluginCode, + const String& pluginName, + InterfaceType interfaceType = InterfaceType::component); + + /** Returns the VST3 compatible parameter ID reported for a given JUCE + parameter. + + Internally JUCE will use this method to determine the Vst::ParamID for + a HostedAudioProcessorParameter, unless JUCE_FORCE_LEGACY_PARAM_IDS is + enabled, in which case it will use the parameter index. + + @see getCompatibleParameterIds + */ + static uint32_t convertJuceParameterId (const String& parameterId, + bool studioOneCompatible = true); + + /** Converts a 32-character hex notation string to a VST3 interface ID. */ + static InterfaceId toInterfaceId (const String& interfaceIdString); }; } // namespace juce From 5e803ded5fd6344d56dde772330a4a7785e9d14a Mon Sep 17 00:00:00 2001 From: Anthony Nicholls Date: Fri, 27 Sep 2024 16:26:59 +0100 Subject: [PATCH 31/76] Deprecations: Add ignore deprecation warning macros --- .../buffers/juce_AudioDataConverters.cpp | 6 ++---- modules/juce_audio_basics/midi/juce_MidiBuffer.cpp | 6 ++---- modules/juce_audio_basics/midi/juce_MidiMessage.cpp | 6 ++---- modules/juce_audio_devices/native/juce_Audio_ios.cpp | 4 ++-- .../codecs/juce_OggVorbisAudioFormat.cpp | 7 +++++-- .../format_types/juce_LV2SupportLibs.cpp | 5 +++-- .../format_types/juce_LegacyAudioParameter.cpp | 6 ++---- .../format_types/juce_VST3PluginFormat.cpp | 4 ++-- .../format_types/juce_VSTPluginFormat.cpp | 10 +++------- .../processors/juce_AudioProcessor.cpp | 6 ++---- modules/juce_core/containers/juce_Variant.cpp | 6 ++---- modules/juce_core/files/juce_DirectoryIterator.cpp | 6 ++---- modules/juce_core/files/juce_File.cpp | 6 ++---- .../juce_core/files/juce_RangedDirectoryIterator.cpp | 6 ++---- .../juce_core/files/juce_RangedDirectoryIterator.h | 6 ++---- modules/juce_core/memory/juce_ScopedPointer.h | 12 ++++-------- .../native/juce_AndroidDocument_android.cpp | 6 ++---- modules/juce_core/native/juce_Files_mac.mm | 4 ++-- modules/juce_core/native/juce_Files_windows.cpp | 4 ++-- modules/juce_core/native/juce_SharedCode_posix.h | 4 ++-- modules/juce_core/system/juce_CompilerWarnings.h | 8 ++++++++ modules/juce_core/text/juce_String.cpp | 10 ++++------ .../juce_data_structures/values/juce_ValueTree.cpp | 6 ++---- .../interprocess/juce_ConnectedChildProcess.cpp | 12 ++++-------- .../juce_events/native/juce_MessageManager_mac.mm | 4 ++-- modules/juce_graphics/fonts/juce_Font.cpp | 12 ++++-------- modules/juce_graphics/images/juce_Image.cpp | 6 ++---- modules/juce_graphics/juce_graphics_Harfbuzz.cpp | 8 +++++--- .../juce_gui_basics/native/juce_FileChooser_mac.mm | 4 ++-- .../native/juce_NSViewComponentPeer_mac.mm | 4 ++-- .../native/juce_PerScreenDisplayLinks_mac.h | 4 ++-- .../native/juce_UIViewComponentPeer_ios.mm | 4 ++-- modules/juce_gui_basics/native/juce_Windowing_mac.mm | 4 ++-- .../code_editor/juce_CodeEditorComponent.cpp | 6 ++---- .../juce_gui_extra/native/juce_AppleRemote_mac.mm | 4 ++-- .../native/juce_PushNotifications_mac.cpp | 4 ++-- .../native/juce_SystemTrayIcon_mac.cpp | 4 ++-- .../native/juce_WebBrowserComponent_linux.cpp | 4 ++-- .../native/juce_WebBrowserComponent_mac.mm | 12 ++++++------ modules/juce_opengl/native/juce_OpenGL_mac.h | 4 ++-- .../marketplace/juce_OnlineUnlockStatus.cpp | 6 ++---- modules/juce_video/native/juce_CameraDevice_mac.h | 12 ++++++------ 42 files changed, 114 insertions(+), 148 deletions(-) diff --git a/modules/juce_audio_basics/buffers/juce_AudioDataConverters.cpp b/modules/juce_audio_basics/buffers/juce_AudioDataConverters.cpp index 15450baa5b82..192019ac774d 100644 --- a/modules/juce_audio_basics/buffers/juce_AudioDataConverters.cpp +++ b/modules/juce_audio_basics/buffers/juce_AudioDataConverters.cpp @@ -35,8 +35,7 @@ namespace juce { -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS void AudioDataConverters::convertFloatToInt16LE (const float* source, void* dest, int numSamples, int destBytesPerSample) { @@ -641,7 +640,6 @@ static AudioConversionTests audioConversionUnitTests; #endif -JUCE_END_IGNORE_WARNINGS_MSVC -JUCE_END_IGNORE_WARNINGS_GCC_LIKE +JUCE_END_IGNORE_DEPRECATION_WARNINGS } // namespace juce diff --git a/modules/juce_audio_basics/midi/juce_MidiBuffer.cpp b/modules/juce_audio_basics/midi/juce_MidiBuffer.cpp index fa1dafc6bf3b..b089840e8bff 100644 --- a/modules/juce_audio_basics/midi/juce_MidiBuffer.cpp +++ b/modules/juce_audio_basics/midi/juce_MidiBuffer.cpp @@ -221,8 +221,7 @@ MidiBufferIterator MidiBuffer::findNextSamplePosition (int samplePosition) const } //============================================================================== -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS MidiBuffer::Iterator::Iterator (const MidiBuffer& b) noexcept : buffer (b), iterator (b.data.begin()) @@ -257,8 +256,7 @@ bool MidiBuffer::Iterator::getNextEvent (MidiMessage& result, int& samplePositio return true; } -JUCE_END_IGNORE_WARNINGS_MSVC -JUCE_END_IGNORE_WARNINGS_GCC_LIKE +JUCE_END_IGNORE_DEPRECATION_WARNINGS //============================================================================== //============================================================================== diff --git a/modules/juce_audio_basics/midi/juce_MidiMessage.cpp b/modules/juce_audio_basics/midi/juce_MidiMessage.cpp index 940cb001116c..530a880f3add 100644 --- a/modules/juce_audio_basics/midi/juce_MidiMessage.cpp +++ b/modules/juce_audio_basics/midi/juce_MidiMessage.cpp @@ -1231,8 +1231,7 @@ struct MidiMessageTest final : public UnitTest size_t index = 0; - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") - JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS for (const auto& input : inputs) { @@ -1258,8 +1257,7 @@ struct MidiMessageTest final : public UnitTest ++index; } - JUCE_END_IGNORE_WARNINGS_GCC_LIKE - JUCE_END_IGNORE_WARNINGS_MSVC + JUCE_END_IGNORE_DEPRECATION_WARNINGS } beginTest ("ReadVariableLengthVal should return 0 if input is truncated"); diff --git a/modules/juce_audio_devices/native/juce_Audio_ios.cpp b/modules/juce_audio_devices/native/juce_Audio_ios.cpp index e7b1c5fbe12f..36f2449dfd46 100644 --- a/modules/juce_audio_devices/native/juce_Audio_ios.cpp +++ b/modules/juce_audio_devices/native/juce_Audio_ios.cpp @@ -818,7 +818,7 @@ struct iOSAudioIODevice::Pimpl final : public AsyncUpdater //============================================================================== #if JUCE_MODULE_AVAILABLE_juce_graphics - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS Image getIcon (int size) { #if TARGET_OS_MACCATALYST @@ -834,7 +834,7 @@ struct iOSAudioIODevice::Pimpl final : public AsyncUpdater return {}; } - JUCE_END_IGNORE_WARNINGS_GCC_LIKE + JUCE_END_IGNORE_DEPRECATION_WARNINGS #endif void switchApplication() diff --git a/modules/juce_audio_formats/codecs/juce_OggVorbisAudioFormat.cpp b/modules/juce_audio_formats/codecs/juce_OggVorbisAudioFormat.cpp index e45016b49328..15f744ac16e9 100644 --- a/modules/juce_audio_formats/codecs/juce_OggVorbisAudioFormat.cpp +++ b/modules/juce_audio_formats/codecs/juce_OggVorbisAudioFormat.cpp @@ -44,11 +44,10 @@ namespace juce namespace OggVorbisNamespace { #if JUCE_INCLUDE_OGGVORBIS_CODE || ! defined (JUCE_INCLUDE_OGGVORBIS_CODE) - JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4267 4127 4244 4996 4100 4701 4702 4013 4133 4206 4305 4189 4706 4995 4365 4456 4457 4459 6297 6011 6001 6308 6255 6386 6385 6246 6387 6263 6262 28182) + JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4267 4127 4244 4100 4701 4702 4013 4133 4206 4305 4189 4706 4995 4365 4456 4457 4459 6297 6011 6001 6308 6255 6386 6385 6246 6387 6263 6262 28182) JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wcast-align", "-Wconversion", - "-Wdeprecated-declarations", "-Wdeprecated-register", "-Wfloat-conversion", "-Wfloat-equal", @@ -61,6 +60,9 @@ namespace OggVorbisNamespace "-Wswitch-default", "-Wswitch-enum", "-Wzero-as-null-pointer-constant") + + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS + JUCE_BEGIN_NO_SANITIZE ("undefined") #include "oggvorbis/vorbisenc.h" @@ -92,6 +94,7 @@ namespace OggVorbisNamespace #include "oggvorbis/libvorbis-1.3.7/lib/window.c" JUCE_END_NO_SANITIZE + JUCE_END_IGNORE_DEPRECATION_WARNINGS JUCE_END_IGNORE_WARNINGS_MSVC JUCE_END_IGNORE_WARNINGS_GCC_LIKE #else diff --git a/modules/juce_audio_processors/format_types/juce_LV2SupportLibs.cpp b/modules/juce_audio_processors/format_types/juce_LV2SupportLibs.cpp index d55c302fbfe9..ac718f197ea1 100644 --- a/modules/juce_audio_processors/format_types/juce_LV2SupportLibs.cpp +++ b/modules/juce_audio_processors/format_types/juce_LV2SupportLibs.cpp @@ -37,7 +37,6 @@ JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wc99-extensions", "-Wcast-align", "-Wconversion", - "-Wdeprecated-declarations", "-Wextra-semi", "-Wfloat-conversion", "-Wfloat-equal", @@ -56,7 +55,8 @@ JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wc99-extensions", "-Wswitch-enum", "-Wunused-parameter", "-Wzero-as-null-pointer-constant") -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4100 4200 4244 4267 4389 4702 4706 4800 4996 6308 28182 28183 6385 6386 6387 6011 6282 6323 6330 6001 6031) +JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4100 4200 4244 4267 4389 4702 4706 4800 6308 28182 28183 6385 6386 6387 6011 6282 6323 6330 6001 6031) +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS extern "C" { @@ -113,5 +113,6 @@ extern "C" } // extern "C" +JUCE_END_IGNORE_DEPRECATION_WARNINGS JUCE_END_IGNORE_WARNINGS_MSVC JUCE_END_IGNORE_WARNINGS_GCC_LIKE diff --git a/modules/juce_audio_processors/format_types/juce_LegacyAudioParameter.cpp b/modules/juce_audio_processors/format_types/juce_LegacyAudioParameter.cpp index 380abce17b35..977206dc0724 100644 --- a/modules/juce_audio_processors/format_types/juce_LegacyAudioParameter.cpp +++ b/modules/juce_audio_processors/format_types/juce_LegacyAudioParameter.cpp @@ -35,8 +35,7 @@ namespace juce { -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS class LegacyAudioParameter final : public HostedAudioProcessorParameter { @@ -219,7 +218,6 @@ class LegacyAudioParametersWrapper bool legacyParamIDs = false, usingManagedParameters = false; }; -JUCE_END_IGNORE_WARNINGS_GCC_LIKE -JUCE_END_IGNORE_WARNINGS_MSVC +JUCE_END_IGNORE_DEPRECATION_WARNINGS } // namespace juce diff --git a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp index 7afd89b9e62f..41cddcc03cc6 100644 --- a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp +++ b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp @@ -1932,7 +1932,7 @@ struct VST3PluginWindow final : public AudioProcessorEditor, JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VST3PluginWindow) }; -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) // warning about overriding deprecated methods +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS //============================================================================== static bool hasARAExtension (IPluginFactory* pluginFactory, const String& pluginClassName) @@ -3891,7 +3891,7 @@ class VST3PluginInstance final : public AudioPluginInstance JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VST3PluginInstance) }; -JUCE_END_IGNORE_WARNINGS_MSVC +JUCE_END_IGNORE_DEPRECATION_WARNINGS //============================================================================== tresult VST3HostContext::beginEdit (Vst::ParamID paramID) diff --git a/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp b/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp index 08b48f1f32de..0a8f4856425c 100644 --- a/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp +++ b/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp @@ -63,11 +63,11 @@ struct AEffect; #include "juce_VSTCommon.h" -JUCE_END_IGNORE_WARNINGS_GCC_LIKE JUCE_END_IGNORE_WARNINGS_MSVC +JUCE_END_IGNORE_WARNINGS_GCC_LIKE -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4355) +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS #include "juce_VSTMidiEventList.h" @@ -828,8 +828,6 @@ struct ModuleHandle final : public ReferenceCountedObject static const int defaultVSTSampleRateValue = 44100; static const int defaultVSTBlockSizeValue = 512; -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) - class TempChannelPointers { public: @@ -3446,8 +3444,6 @@ struct VSTPluginWindow final : public AudioProcessorEditor, }; #endif -JUCE_END_IGNORE_WARNINGS_MSVC - //============================================================================== AudioProcessorEditor* VSTPluginInstance::createEditor() { @@ -3762,7 +3758,7 @@ void VSTPluginFormat::aboutToScanVSTShellPlugin (const PluginDescription&) {} } // namespace juce -JUCE_END_IGNORE_WARNINGS_GCC_LIKE +JUCE_END_IGNORE_DEPRECATION_WARNINGS JUCE_END_IGNORE_WARNINGS_MSVC #endif diff --git a/modules/juce_audio_processors/processors/juce_AudioProcessor.cpp b/modules/juce_audio_processors/processors/juce_AudioProcessor.cpp index b9fe2031c107..740eb051cf21 100644 --- a/modules/juce_audio_processors/processors/juce_AudioProcessor.cpp +++ b/modules/juce_audio_processors/processors/juce_AudioProcessor.cpp @@ -1271,8 +1271,7 @@ VST3ClientExtensions* AudioProcessor::getVST3ClientExtensions() } //============================================================================== -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS void AudioProcessor::setParameterNotifyingHost (int parameterIndex, float newValue) { @@ -1510,8 +1509,7 @@ AudioProcessorParameter* AudioProcessor::getParamChecked (int index) const bool AudioProcessor::canAddBus ([[maybe_unused]] bool isInput) const { return false; } bool AudioProcessor::canRemoveBus ([[maybe_unused]] bool isInput) const { return false; } -JUCE_END_IGNORE_WARNINGS_GCC_LIKE -JUCE_END_IGNORE_WARNINGS_MSVC +JUCE_END_IGNORE_DEPRECATION_WARNINGS //============================================================================== void AudioProcessorListener::audioProcessorParameterChangeGestureBegin (AudioProcessor*, int) {} diff --git a/modules/juce_core/containers/juce_Variant.cpp b/modules/juce_core/containers/juce_Variant.cpp index 0c6b6728d0fd..0b45553bef70 100644 --- a/modules/juce_core/containers/juce_Variant.cpp +++ b/modules/juce_core/containers/juce_Variant.cpp @@ -896,13 +896,11 @@ var::NativeFunctionArgs::NativeFunctionArgs (const var& t, const var* args, int //============================================================================== #if JUCE_ALLOW_STATIC_NULL_VARIABLES -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS const var var::null; -JUCE_END_IGNORE_WARNINGS_GCC_LIKE -JUCE_END_IGNORE_WARNINGS_MSVC +JUCE_END_IGNORE_DEPRECATION_WARNINGS #endif diff --git a/modules/juce_core/files/juce_DirectoryIterator.cpp b/modules/juce_core/files/juce_DirectoryIterator.cpp index 4ea33f5377f2..782636472cee 100644 --- a/modules/juce_core/files/juce_DirectoryIterator.cpp +++ b/modules/juce_core/files/juce_DirectoryIterator.cpp @@ -58,8 +58,7 @@ bool DirectoryIterator::next() return next (nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); } -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS bool DirectoryIterator::next (bool* isDirResult, bool* isHiddenResult, int64* fileSize, Time* modTime, Time* creationTime, bool* isReadOnly) @@ -144,8 +143,7 @@ bool DirectoryIterator::next (bool* isDirResult, bool* isHiddenResult, int64* fi } } -JUCE_END_IGNORE_WARNINGS_GCC_LIKE -JUCE_END_IGNORE_WARNINGS_MSVC +JUCE_END_IGNORE_DEPRECATION_WARNINGS const File& DirectoryIterator::getFile() const { diff --git a/modules/juce_core/files/juce_File.cpp b/modules/juce_core/files/juce_File.cpp index ebc5a95796c3..4fd9e4288caa 100644 --- a/modules/juce_core/files/juce_File.cpp +++ b/modules/juce_core/files/juce_File.cpp @@ -1029,13 +1029,11 @@ File File::getLinkedTarget() const //============================================================================== #if JUCE_ALLOW_STATIC_NULL_VARIABLES -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS const File File::nonexistent{}; -JUCE_END_IGNORE_WARNINGS_GCC_LIKE -JUCE_END_IGNORE_WARNINGS_MSVC +JUCE_END_IGNORE_DEPRECATION_WARNINGS #endif diff --git a/modules/juce_core/files/juce_RangedDirectoryIterator.cpp b/modules/juce_core/files/juce_RangedDirectoryIterator.cpp index cc4b7b4a9848..181bd01a774e 100644 --- a/modules/juce_core/files/juce_RangedDirectoryIterator.cpp +++ b/modules/juce_core/files/juce_RangedDirectoryIterator.cpp @@ -35,8 +35,7 @@ namespace juce { -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS float DirectoryEntry::getEstimatedProgress() const { @@ -85,7 +84,6 @@ void RangedDirectoryIterator::increment() iterator = nullptr; } -JUCE_END_IGNORE_WARNINGS_GCC_LIKE -JUCE_END_IGNORE_WARNINGS_MSVC +JUCE_END_IGNORE_DEPRECATION_WARNINGS } // namespace juce diff --git a/modules/juce_core/files/juce_RangedDirectoryIterator.h b/modules/juce_core/files/juce_RangedDirectoryIterator.h index 1f6ca5448384..ef9b6d2234d4 100644 --- a/modules/juce_core/files/juce_RangedDirectoryIterator.h +++ b/modules/juce_core/files/juce_RangedDirectoryIterator.h @@ -36,8 +36,7 @@ namespace juce { //============================================================================== -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS /** Describes the attributes of a file or folder. @@ -196,7 +195,6 @@ inline RangedDirectoryIterator begin (const RangedDirectoryIterator& it) { retur inline RangedDirectoryIterator end (const RangedDirectoryIterator&) { return {}; } -JUCE_END_IGNORE_WARNINGS_MSVC -JUCE_END_IGNORE_WARNINGS_GCC_LIKE +JUCE_END_IGNORE_DEPRECATION_WARNINGS } // namespace juce diff --git a/modules/juce_core/memory/juce_ScopedPointer.h b/modules/juce_core/memory/juce_ScopedPointer.h index f16afbb522a1..107190bff1b4 100644 --- a/modules/juce_core/memory/juce_ScopedPointer.h +++ b/modules/juce_core/memory/juce_ScopedPointer.h @@ -46,8 +46,7 @@ class [[deprecated]] ScopedPointer { public: //============================================================================== - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") - JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS inline ScopedPointer() {} @@ -154,13 +153,11 @@ class [[deprecated]] ScopedPointer ScopedPointer& operator= (const ScopedPointer&) = delete; #endif - JUCE_END_IGNORE_WARNINGS_MSVC - JUCE_END_IGNORE_WARNINGS_GCC_LIKE + JUCE_END_IGNORE_DEPRECATION_WARNINGS }; //============================================================================== -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS template bool operator== (ObjectType1* pointer1, const ScopedPointer& pointer2) noexcept @@ -228,8 +225,7 @@ template void deleteAndZero (ScopedPointer&) { static_assert (sizeof (Type) == 12345, "Attempt to call deleteAndZero() on a ScopedPointer"); } -JUCE_END_IGNORE_WARNINGS_GCC_LIKE -JUCE_END_IGNORE_WARNINGS_MSVC +JUCE_END_IGNORE_DEPRECATION_WARNINGS } // namespace juce diff --git a/modules/juce_core/native/juce_AndroidDocument_android.cpp b/modules/juce_core/native/juce_AndroidDocument_android.cpp index 6282ea6a67a5..fd8e7cabb46d 100644 --- a/modules/juce_core/native/juce_AndroidDocument_android.cpp +++ b/modules/juce_core/native/juce_AndroidDocument_android.cpp @@ -231,12 +231,10 @@ struct AndroidDocumentDetail struct DirectoryIteratorEngine { - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") - JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS DirectoryIteratorEngine (const File& dir, bool recursive) : iterator (dir, recursive, "*", File::findFilesAndDirectories) {} - JUCE_END_IGNORE_WARNINGS_MSVC - JUCE_END_IGNORE_WARNINGS_GCC_LIKE + JUCE_END_IGNORE_DEPRECATION_WARNINGS auto read() const { return AndroidDocument::fromFile (iterator.getFile()); } bool increment() { return iterator.next(); } diff --git a/modules/juce_core/native/juce_Files_mac.mm b/modules/juce_core/native/juce_Files_mac.mm index dc39f8c54e76..3e81d843bf94 100644 --- a/modules/juce_core/native/juce_Files_mac.mm +++ b/modules/juce_core/native/juce_Files_mac.mm @@ -431,7 +431,7 @@ bool next (String& filenameFound, return true; } - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS NSMutableDictionary* dict = [[NSMutableDictionary new] autorelease]; @@ -443,7 +443,7 @@ bool next (String& filenameFound, configuration: dict error: nil]; - JUCE_END_IGNORE_WARNINGS_GCC_LIKE + JUCE_END_IGNORE_DEPRECATION_WARNINGS } if (file.exists()) diff --git a/modules/juce_core/native/juce_Files_windows.cpp b/modules/juce_core/native/juce_Files_windows.cpp index bbfe611c291f..8db0a1ff9190 100644 --- a/modules/juce_core/native/juce_Files_windows.cpp +++ b/modules/juce_core/native/juce_Files_windows.cpp @@ -253,12 +253,12 @@ namespace WindowsFileHelpers //============================================================================== #if JUCE_ALLOW_STATIC_NULL_VARIABLES -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS const juce_wchar File::separator = '\\'; const StringRef File::separatorString ("\\"); -JUCE_END_IGNORE_WARNINGS_MSVC +JUCE_END_IGNORE_DEPRECATION_WARNINGS #endif diff --git a/modules/juce_core/native/juce_SharedCode_posix.h b/modules/juce_core/native/juce_SharedCode_posix.h index c53ef0fff6f3..3f505f09c037 100644 --- a/modules/juce_core/native/juce_SharedCode_posix.h +++ b/modules/juce_core/native/juce_SharedCode_posix.h @@ -113,12 +113,12 @@ static MaxNumFileHandlesInitialiser maxNumFileHandlesInitialiser; //============================================================================== #if JUCE_ALLOW_STATIC_NULL_VARIABLES -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS const juce_wchar File::separator = '/'; const StringRef File::separatorString ("/"); -JUCE_END_IGNORE_WARNINGS_GCC_LIKE +JUCE_END_IGNORE_DEPRECATION_WARNINGS #endif diff --git a/modules/juce_core/system/juce_CompilerWarnings.h b/modules/juce_core/system/juce_CompilerWarnings.h index 9bd8f5abea1b..1f4905479723 100644 --- a/modules/juce_core/system/juce_CompilerWarnings.h +++ b/modules/juce_core/system/juce_CompilerWarnings.h @@ -238,6 +238,14 @@ #define JUCE_SANITIZER_ATTRIBUTE_MINIMUM_CLANG_VERSION 9 #endif +#define JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS \ + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") \ + JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) + +#define JUCE_END_IGNORE_DEPRECATION_WARNINGS \ + JUCE_END_IGNORE_WARNINGS_MSVC \ + JUCE_END_IGNORE_WARNINGS_GCC_LIKE + /** Disable sanitizers for a range of functions. This functionality doesn't seem to exist on GCC yet, so at the moment this only works for clang. diff --git a/modules/juce_core/text/juce_String.cpp b/modules/juce_core/text/juce_String.cpp index ed06d7c93ae2..d85cfd3009ef 100644 --- a/modules/juce_core/text/juce_String.cpp +++ b/modules/juce_core/text/juce_String.cpp @@ -1860,7 +1860,7 @@ String String::formattedRaw (const char* pf, ...) va_start (args, pf); #if JUCE_WINDOWS - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS #endif #if JUCE_ANDROID @@ -1881,7 +1881,7 @@ String String::formattedRaw (const char* pf, ...) #endif #if JUCE_WINDOWS - JUCE_END_IGNORE_WARNINGS_GCC_LIKE + JUCE_END_IGNORE_DEPRECATION_WARNINGS #endif va_end (args); @@ -2342,13 +2342,11 @@ static String serialiseDouble (double input, int maxDecimalPlaces = 0) //============================================================================== #if JUCE_ALLOW_STATIC_NULL_VARIABLES -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS const String String::empty; -JUCE_END_IGNORE_WARNINGS_GCC_LIKE -JUCE_END_IGNORE_WARNINGS_MSVC +JUCE_END_IGNORE_DEPRECATION_WARNINGS #endif diff --git a/modules/juce_data_structures/values/juce_ValueTree.cpp b/modules/juce_data_structures/values/juce_ValueTree.cpp index e534747f140e..b0df154664d0 100644 --- a/modules/juce_data_structures/values/juce_ValueTree.cpp +++ b/modules/juce_data_structures/values/juce_ValueTree.cpp @@ -1111,13 +1111,11 @@ void ValueTree::Listener::valueTreeRedirected (ValueTree&) //============================================================================== #if JUCE_ALLOW_STATIC_NULL_VARIABLES -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS const ValueTree ValueTree::invalid; -JUCE_END_IGNORE_WARNINGS_GCC_LIKE -JUCE_END_IGNORE_WARNINGS_MSVC +JUCE_END_IGNORE_DEPRECATION_WARNINGS #endif diff --git a/modules/juce_events/interprocess/juce_ConnectedChildProcess.cpp b/modules/juce_events/interprocess/juce_ConnectedChildProcess.cpp index a322b739e4e5..533a75e3080a 100644 --- a/modules/juce_events/interprocess/juce_ConnectedChildProcess.cpp +++ b/modules/juce_events/interprocess/juce_ConnectedChildProcess.cpp @@ -149,11 +149,9 @@ void ChildProcessCoordinator::handleConnectionLost() {} void ChildProcessCoordinator::handleMessageFromWorker (const MemoryBlock& mb) { - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") - JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS handleMessageFromSlave (mb); - JUCE_END_IGNORE_WARNINGS_GCC_LIKE - JUCE_END_IGNORE_WARNINGS_MSVC + JUCE_END_IGNORE_DEPRECATION_WARNINGS } bool ChildProcessCoordinator::sendMessageToWorker (const MemoryBlock& mb) @@ -276,11 +274,9 @@ void ChildProcessWorker::handleConnectionLost() {} void ChildProcessWorker::handleMessageFromCoordinator (const MemoryBlock& mb) { - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") - JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS handleMessageFromMaster (mb); - JUCE_END_IGNORE_WARNINGS_GCC_LIKE - JUCE_END_IGNORE_WARNINGS_MSVC + JUCE_END_IGNORE_DEPRECATION_WARNINGS } bool ChildProcessWorker::sendMessageToCoordinator (const MemoryBlock& mb) diff --git a/modules/juce_events/native/juce_MessageManager_mac.mm b/modules/juce_events/native/juce_MessageManager_mac.mm index f727616ae974..07779664332b 100644 --- a/modules/juce_events/native/juce_MessageManager_mac.mm +++ b/modules/juce_events/native/juce_MessageManager_mac.mm @@ -142,11 +142,11 @@ { if (notification.userInfo != nil) { - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS // NSUserNotification is deprecated from macOS 11, but there doesn't seem to be a // replacement for NSApplicationLaunchUserNotificationKey returning a non-deprecated type NSUserNotification* userNotification = notification.userInfo[NSApplicationLaunchUserNotificationKey]; - JUCE_END_IGNORE_WARNINGS_GCC_LIKE + JUCE_END_IGNORE_DEPRECATION_WARNINGS JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wnullable-to-nonnull-conversion") if (userNotification != nil && userNotification.userInfo != nil) diff --git a/modules/juce_graphics/fonts/juce_Font.cpp b/modules/juce_graphics/fonts/juce_Font.cpp index 5641606f1b79..b6553ed19e97 100644 --- a/modules/juce_graphics/fonts/juce_Font.cpp +++ b/modules/juce_graphics/fonts/juce_Font.cpp @@ -754,11 +754,9 @@ float Font::getDescentInPoints() const { return getDescent() * getHeightToP int Font::getStringWidth (const String& text) const { - JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS return (int) std::ceil (getStringWidthFloat (text)); - JUCE_END_IGNORE_WARNINGS_GCC_LIKE - JUCE_END_IGNORE_WARNINGS_MSVC + JUCE_END_IGNORE_DEPRECATION_WARNINGS } float Font::getStringWidthFloat (const String& text) const @@ -941,11 +939,9 @@ class FontTests : public UnitTest beginTest ("Old constructor from Typeface"); { - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") - JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS Font f { face }; - JUCE_END_IGNORE_WARNINGS_MSVC - JUCE_END_IGNORE_WARNINGS_GCC_LIKE + JUCE_END_IGNORE_DEPRECATION_WARNINGS expect (f.getTypefaceName() == face->getName()); expect (f.getTypefaceStyle() == face->getStyle()); diff --git a/modules/juce_graphics/images/juce_Image.cpp b/modules/juce_graphics/images/juce_Image.cpp index 112bcce29e32..277ebd6bf0ed 100644 --- a/modules/juce_graphics/images/juce_Image.cpp +++ b/modules/juce_graphics/images/juce_Image.cpp @@ -841,13 +841,11 @@ void ImageEffects::applySingleChannelBoxBlurEffect (int radius, const Image& inp //============================================================================== #if JUCE_ALLOW_STATIC_NULL_VARIABLES -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS const Image Image::null; -JUCE_END_IGNORE_WARNINGS_GCC_LIKE -JUCE_END_IGNORE_WARNINGS_MSVC +JUCE_END_IGNORE_DEPRECATION_WARNINGS #endif diff --git a/modules/juce_graphics/juce_graphics_Harfbuzz.cpp b/modules/juce_graphics/juce_graphics_Harfbuzz.cpp index bc464f9fb83e..2653786c7e4e 100644 --- a/modules/juce_graphics/juce_graphics_Harfbuzz.cpp +++ b/modules/juce_graphics/juce_graphics_Harfbuzz.cpp @@ -34,10 +34,9 @@ #include -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4100 4127 4189 4244 4245 4265 4267 4309 4310 4312 4456 4457 4458 4459 4701 4702 4706 4996 6001 6011 6239 6244 6246 6262 6297 6313 6319 6326 6336 6385 6386 28251) +JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4100 4127 4189 4244 4245 4265 4267 4309 4310 4312 4456 4457 4458 4459 4701 4702 4706 6001 6011 6239 6244 6246 6262 6297 6313 6319 6326 6336 6385 6386 28251) -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations", - "-Wcast-function-type", +JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wcast-function-type", "-Wsign-conversion", "-Wzero-as-null-pointer-constant", "-Wformat-pedantic", @@ -56,6 +55,8 @@ JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations", "-Woverflow", "-Wimplicit-fallthrough") +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS + #define HAVE_ATEXIT 1 #if JUCE_LINUX || JUCE_BSD @@ -95,5 +96,6 @@ JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations", #undef HAVE_FREETYPE #undef HAVE_CORETEXT +JUCE_END_IGNORE_DEPRECATION_WARNINGS JUCE_END_IGNORE_WARNINGS_GCC_LIKE JUCE_END_IGNORE_WARNINGS_MSVC diff --git a/modules/juce_gui_basics/native/juce_FileChooser_mac.mm b/modules/juce_gui_basics/native/juce_FileChooser_mac.mm index 4987e5f2bbe2..24c84c812920 100644 --- a/modules/juce_gui_basics/native/juce_FileChooser_mac.mm +++ b/modules/juce_gui_basics/native/juce_FileChooser_mac.mm @@ -103,9 +103,9 @@ [panel setTitle: nsTitle]; [panel setReleasedWhenClosed: YES]; - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS [panel setAllowedFileTypes: createAllowedTypesArray (filters)]; - JUCE_END_IGNORE_WARNINGS_GCC_LIKE + JUCE_END_IGNORE_DEPRECATION_WARNINGS if (! isSave) { diff --git a/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm b/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm index 86ea738548bf..664243a03a5e 100644 --- a/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm +++ b/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm @@ -1460,9 +1460,9 @@ static unsigned int getNSWindowStyleMask (const int flags) noexcept if (@available (macOS 10.13, *)) return NSPasteboardTypeFileURL; - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS return (NSString*) kUTTypeFileURL; - JUCE_END_IGNORE_WARNINGS_GCC_LIKE + JUCE_END_IGNORE_DEPRECATION_WARNINGS }(); return [NSArray arrayWithObjects: type, (NSString*) kPasteboardTypeFileURLPromise, NSPasteboardTypeString, nil]; diff --git a/modules/juce_gui_basics/native/juce_PerScreenDisplayLinks_mac.h b/modules/juce_gui_basics/native/juce_PerScreenDisplayLinks_mac.h index 4e412beb1393..dcea884cf935 100644 --- a/modules/juce_gui_basics/native/juce_PerScreenDisplayLinks_mac.h +++ b/modules/juce_gui_basics/native/juce_PerScreenDisplayLinks_mac.h @@ -109,7 +109,7 @@ class FunctionNotificationCenterObserver // NSScreen.displayLink(target:selector:) all of which were only introduced in macOS 14+ however, // it's not clear how these methods can be used to replace all use cases -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS class ScopedDisplayLink { @@ -188,7 +188,7 @@ class ScopedDisplayLink JUCE_DECLARE_NON_MOVEABLE (ScopedDisplayLink) }; -JUCE_END_IGNORE_WARNINGS_GCC_LIKE +JUCE_END_IGNORE_DEPRECATION_WARNINGS //============================================================================== /* diff --git a/modules/juce_gui_basics/native/juce_UIViewComponentPeer_ios.mm b/modules/juce_gui_basics/native/juce_UIViewComponentPeer_ios.mm index e2d229f2a28f..2d7835d82b87 100644 --- a/modules/juce_gui_basics/native/juce_UIViewComponentPeer_ios.mm +++ b/modules/juce_gui_basics/native/juce_UIViewComponentPeer_ios.mm @@ -115,9 +115,9 @@ static UIInterfaceOrientation getWindowOrientation() return [(UIWindowScene*) scene interfaceOrientation]; } - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS return [sharedApplication statusBarOrientation]; - JUCE_END_IGNORE_WARNINGS_GCC_LIKE + JUCE_END_IGNORE_DEPRECATION_WARNINGS } struct Orientations diff --git a/modules/juce_gui_basics/native/juce_Windowing_mac.mm b/modules/juce_gui_basics/native/juce_Windowing_mac.mm index 6b18e9de8239..9839e5dd8fe2 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_mac.mm +++ b/modules/juce_gui_basics/native/juce_Windowing_mac.mm @@ -603,12 +603,12 @@ static Image createNSWindowSnapshot (NSWindow* nsWindow) #else - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS return createImageFromCGImage ((CGImageRef) CFAutorelease (CGWindowListCreateImage (CGRectNull, kCGWindowListOptionIncludingWindow, (CGWindowID) [nsWindow windowNumber], kCGWindowImageBoundsIgnoreFraming))); - JUCE_END_IGNORE_WARNINGS_GCC_LIKE + JUCE_END_IGNORE_DEPRECATION_WARNINGS #endif } diff --git a/modules/juce_gui_extra/code_editor/juce_CodeEditorComponent.cpp b/modules/juce_gui_extra/code_editor/juce_CodeEditorComponent.cpp index 4a83998c5041..f9aa7a9b5644 100644 --- a/modules/juce_gui_extra/code_editor/juce_CodeEditorComponent.cpp +++ b/modules/juce_gui_extra/code_editor/juce_CodeEditorComponent.cpp @@ -1697,11 +1697,9 @@ void CodeEditorComponent::setFont (const Font& newFont) { font = newFont; - JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS charWidth = font.getStringWidthFloat ("0"); - JUCE_END_IGNORE_WARNINGS_GCC_LIKE - JUCE_END_IGNORE_WARNINGS_MSVC + JUCE_END_IGNORE_DEPRECATION_WARNINGS lineHeight = roundToInt (font.getHeight()); resized(); diff --git a/modules/juce_gui_extra/native/juce_AppleRemote_mac.mm b/modules/juce_gui_extra/native/juce_AppleRemote_mac.mm index 0b2f5493d79d..77d3f43a7eda 100644 --- a/modules/juce_gui_extra/native/juce_AppleRemote_mac.mm +++ b/modules/juce_gui_extra/native/juce_AppleRemote_mac.mm @@ -63,9 +63,9 @@ io_object_t getAppleRemoteDevice() return kIOMainPortDefault; #endif - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS return kIOMasterPortDefault; - JUCE_END_IGNORE_WARNINGS_GCC_LIKE + JUCE_END_IGNORE_DEPRECATION_WARNINGS }(); if (IOServiceGetMatchingServices (defaultPort, dict, &iter) == kIOReturnSuccess diff --git a/modules/juce_gui_extra/native/juce_PushNotifications_mac.cpp b/modules/juce_gui_extra/native/juce_PushNotifications_mac.cpp index b1a3c81e8328..c994beb3bd4c 100644 --- a/modules/juce_gui_extra/native/juce_PushNotifications_mac.cpp +++ b/modules/juce_gui_extra/native/juce_PushNotifications_mac.cpp @@ -35,7 +35,7 @@ namespace juce { -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS namespace PushNotificationsDelegateDetailsOsx { @@ -534,6 +534,6 @@ struct PushNotifications::Pimpl : private PushNotificationsDelegate PushNotifications::Settings settings; }; -JUCE_END_IGNORE_WARNINGS_GCC_LIKE +JUCE_END_IGNORE_DEPRECATION_WARNINGS } // namespace juce diff --git a/modules/juce_gui_extra/native/juce_SystemTrayIcon_mac.cpp b/modules/juce_gui_extra/native/juce_SystemTrayIcon_mac.cpp index cad7a62fa1e2..624282de74a7 100644 --- a/modules/juce_gui_extra/native/juce_SystemTrayIcon_mac.cpp +++ b/modules/juce_gui_extra/native/juce_SystemTrayIcon_mac.cpp @@ -35,7 +35,7 @@ namespace juce { -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS extern NSMenu* createNSMenu (const PopupMenu&, const String& name, int topLevelMenuId, int topLevelIndex, bool addDelegate); @@ -446,6 +446,6 @@ void SystemTrayIconComponent::showDropdownMenu (const PopupMenu& menu) pimpl->statusItemHolder->showMenu (menu); } -JUCE_END_IGNORE_WARNINGS_GCC_LIKE +JUCE_END_IGNORE_DEPRECATION_WARNINGS } // namespace juce diff --git a/modules/juce_gui_extra/native/juce_WebBrowserComponent_linux.cpp b/modules/juce_gui_extra/native/juce_WebBrowserComponent_linux.cpp index f92dfbe4e683..ffea19caa4d3 100644 --- a/modules/juce_gui_extra/native/juce_WebBrowserComponent_linux.cpp +++ b/modules/juce_gui_extra/native/juce_WebBrowserComponent_linux.cpp @@ -1137,11 +1137,11 @@ class GtkChildProcess final : private CommandReceiver::Responder // Using the non-deprecated webkit_javascript_result_get_js_value() functions seems easier // but returned values fail the JS_IS_VALUE() internal assertion. The example code from the // documentation doesn't seem to work either. - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS WebKitJavascriptResultUniquePtr jsResult { wk.juce_webkit_web_view_run_javascript_finish (owner->webview, result, &error) }; - JUCE_END_IGNORE_WARNINGS_GCC_LIKE + JUCE_END_IGNORE_DEPRECATION_WARNINGS if (jsResult == nullptr) { diff --git a/modules/juce_gui_extra/native/juce_WebBrowserComponent_mac.mm b/modules/juce_gui_extra/native/juce_WebBrowserComponent_mac.mm index e00ebde53482..fcff49f51bcd 100644 --- a/modules/juce_gui_extra/native/juce_WebBrowserComponent_mac.mm +++ b/modules/juce_gui_extra/native/juce_WebBrowserComponent_mac.mm @@ -197,9 +197,9 @@ explicit WebViewKeyEquivalentResponder (bool acceptsFirstMouse) if (@available (macOS 10.12, *)) return (modifierFlags & NSEventModifierFlagDeviceIndependentFlagsMask) == NSEventModifierFlagCommand; - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS return (modifierFlags & NSDeviceIndependentModifierFlagsMask) == NSCommandKeyMask; - JUCE_END_IGNORE_WARNINGS_GCC_LIKE + JUCE_END_IGNORE_DEPRECATION_WARNINGS }(); if (isCommandDown) @@ -270,7 +270,7 @@ explicit WebViewKeyEquivalentResponder (bool acceptsFirstMouse) } }; -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS struct DownloadClickDetectorClass final : public ObjCClass { DownloadClickDetectorClass() : ObjCClass ("JUCEWebClickDetector_") @@ -372,7 +372,7 @@ static void didFailLoadWithError (id self, SEL, WebView* sender, NSError* error, } } }; -JUCE_END_IGNORE_WARNINGS_GCC_LIKE +JUCE_END_IGNORE_DEPRECATION_WARNINGS #endif // Connects the delegate to the rest of the implementation without making WebViewDelegateClass @@ -652,7 +652,7 @@ static void displayError (WebBrowserComponent* owner, NSError* error) //============================================================================== #if JUCE_MAC -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS class WebBrowserComponent::Impl::Platform::WebViewImpl : public WebBrowserComponent::Impl::PlatformInterface, #if JUCE_MAC public NSViewComponent @@ -783,7 +783,7 @@ void evaluateJavascript (const String&, WebBrowserComponent::EvaluationCallback) ObjCObjectHandle webView; ObjCObjectHandle clickListener; }; -JUCE_END_IGNORE_WARNINGS_GCC_LIKE +JUCE_END_IGNORE_DEPRECATION_WARNINGS #endif class WebBrowserComponent::Impl::Platform::WKWebViewImpl : public WebBrowserComponent::Impl::PlatformInterface, diff --git a/modules/juce_opengl/native/juce_OpenGL_mac.h b/modules/juce_opengl/native/juce_OpenGL_mac.h index badc06f207ec..c5efcc0e878c 100644 --- a/modules/juce_opengl/native/juce_OpenGL_mac.h +++ b/modules/juce_opengl/native/juce_OpenGL_mac.h @@ -35,7 +35,7 @@ namespace juce { -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS class OpenGLContext::NativeContext { @@ -326,6 +326,6 @@ bool OpenGLHelpers::isContextActive() return CGLGetCurrentContext() != CGLContextObj(); } -JUCE_END_IGNORE_WARNINGS_GCC_LIKE +JUCE_END_IGNORE_DEPRECATION_WARNINGS } // namespace juce diff --git a/modules/juce_product_unlocking/marketplace/juce_OnlineUnlockStatus.cpp b/modules/juce_product_unlocking/marketplace/juce_OnlineUnlockStatus.cpp index 76f44dcd5043..478db67fb28a 100644 --- a/modules/juce_product_unlocking/marketplace/juce_OnlineUnlockStatus.cpp +++ b/modules/juce_product_unlocking/marketplace/juce_OnlineUnlockStatus.cpp @@ -328,8 +328,7 @@ String OnlineUnlockStatus::MachineIDUtilities::getUniqueMachineID() return getEncodedIDString (SystemStats::getUniqueDeviceID()); } -JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") -JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) +JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS StringArray OnlineUnlockStatus::MachineIDUtilities::getLocalMachineIDs() { @@ -350,8 +349,7 @@ StringArray OnlineUnlockStatus::getLocalMachineIDs() return MachineIDUtilities::getLocalMachineIDs(); } -JUCE_END_IGNORE_WARNINGS_GCC_LIKE -JUCE_END_IGNORE_WARNINGS_MSVC +JUCE_END_IGNORE_DEPRECATION_WARNINGS void OnlineUnlockStatus::userCancelled() { diff --git a/modules/juce_video/native/juce_CameraDevice_mac.h b/modules/juce_video/native/juce_CameraDevice_mac.h index 4a5dc509656e..df409217db4e 100644 --- a/modules/juce_video/native/juce_CameraDevice_mac.h +++ b/modules/juce_video/native/juce_CameraDevice_mac.h @@ -148,9 +148,9 @@ struct CameraDevice::Pimpl { if (@available (macOS 10.15, *)) { - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS const auto deviceType = AVCaptureDeviceTypeExternalUnknown; - JUCE_END_IGNORE_WARNINGS_GCC_LIKE + JUCE_END_IGNORE_DEPRECATION_WARNINGS auto* discovery = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes: @[AVCaptureDeviceTypeBuiltInWideAngleCamera, deviceType] mediaType: AVMediaTypeVideo @@ -159,9 +159,9 @@ struct CameraDevice::Pimpl return [discovery devices]; } - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS return [AVCaptureDevice devicesWithMediaType: AVMediaTypeVideo]; - JUCE_END_IGNORE_WARNINGS_GCC_LIKE + JUCE_END_IGNORE_DEPRECATION_WARNINGS } static StringArray getAvailableDevices() @@ -331,7 +331,7 @@ struct CameraDevice::Pimpl NSUniquePtr> delegate; }; - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS class PreCatalinaStillImageOutput : public ImageOutputBase { public: @@ -397,7 +397,7 @@ struct CameraDevice::Pimpl private: AVCaptureStillImageOutput* imageOutput = nil; }; - JUCE_END_IGNORE_WARNINGS_GCC_LIKE + JUCE_END_IGNORE_DEPRECATION_WARNINGS //============================================================================== void addImageCapture() From 04188c0e09edf182d7e208cfcf1c9ba61cbd8e82 Mon Sep 17 00:00:00 2001 From: Anthony Nicholls Date: Fri, 13 Oct 2023 18:24:42 +0100 Subject: [PATCH 32/76] AudioBuffer: Remove approximatelyEqual --- .../buffers/juce_AudioSampleBuffer.h | 151 ++++++------------ 1 file changed, 46 insertions(+), 105 deletions(-) diff --git a/modules/juce_audio_basics/buffers/juce_AudioSampleBuffer.h b/modules/juce_audio_basics/buffers/juce_AudioSampleBuffer.h index cfc841150f23..ddd5889673e8 100644 --- a/modules/juce_audio_basics/buffers/juce_AudioSampleBuffer.h +++ b/modules/juce_audio_basics/buffers/juce_AudioSampleBuffer.h @@ -700,14 +700,10 @@ class AudioBuffer jassert (isPositiveAndBelow (channel, numChannels)); jassert (startSample >= 0 && numSamples >= 0 && startSample + numSamples <= size); - if (! approximatelyEqual (gain, Type (1)) && ! isClear) + if (! isClear) { auto* d = channels[channel] + startSample; - - if (approximatelyEqual (gain, Type())) - FloatVectorOperations::clear (d, numSamples); - else - FloatVectorOperations::multiply (d, gain, numSamples); + FloatVectorOperations::multiply (d, gain, numSamples); } } @@ -742,23 +738,16 @@ class AudioBuffer { if (! isClear) { - if (approximatelyEqual (startGain, endGain)) - { - applyGain (channel, startSample, numSamples, startGain); - } - else - { - jassert (isPositiveAndBelow (channel, numChannels)); - jassert (startSample >= 0 && numSamples >= 0 && startSample + numSamples <= size); + jassert (isPositiveAndBelow (channel, numChannels)); + jassert (startSample >= 0 && numSamples >= 0 && startSample + numSamples <= size); - const auto increment = (endGain - startGain) / (float) numSamples; - auto* d = channels[channel] + startSample; + const auto increment = (endGain - startGain) / (float) numSamples; + auto* d = channels[channel] + startSample; - while (--numSamples >= 0) - { - *d++ *= startGain; - startGain += increment; - } + while (--numSamples >= 0) + { + *d++ *= startGain; + startGain += increment; } } } @@ -812,7 +801,7 @@ class AudioBuffer jassert (isPositiveAndBelow (sourceChannel, source.numChannels)); jassert (sourceStartSample >= 0 && sourceStartSample + numSamples <= source.size); - if (! approximatelyEqual (gainToApplyToSource, (Type) 0) && numSamples > 0 && ! source.isClear) + if (numSamples > 0 && ! source.isClear) { auto* d = channels[destChannel] + destStartSample; auto* s = source.channels[sourceChannel] + sourceStartSample; @@ -822,18 +811,11 @@ class AudioBuffer if (isClear) { isClear = false; - - if (! approximatelyEqual (gainToApplyToSource, Type (1))) - FloatVectorOperations::copyWithMultiply (d, s, gainToApplyToSource, numSamples); - else - FloatVectorOperations::copy (d, s, numSamples); + FloatVectorOperations::copyWithMultiply (d, s, gainToApplyToSource, numSamples); } else { - if (! approximatelyEqual (gainToApplyToSource, Type (1))) - FloatVectorOperations::addWithMultiply (d, s, gainToApplyToSource, numSamples); - else - FloatVectorOperations::add (d, s, numSamples); + FloatVectorOperations::addWithMultiply (d, s, gainToApplyToSource, numSamples); } JUCE_END_IGNORE_WARNINGS_MSVC @@ -864,26 +846,16 @@ class AudioBuffer jassert (destStartSample >= 0 && numSamples >= 0 && destStartSample + numSamples <= size); jassert (source != nullptr); - if (! approximatelyEqual (gainToApplyToSource, Type()) && numSamples > 0) - { - auto* d = channels[destChannel] + destStartSample; + auto* d = channels[destChannel] + destStartSample; - if (isClear) - { - isClear = false; - - if (! approximatelyEqual (gainToApplyToSource, Type (1))) - FloatVectorOperations::copyWithMultiply (d, source, gainToApplyToSource, numSamples); - else - FloatVectorOperations::copy (d, source, numSamples); - } - else - { - if (! approximatelyEqual (gainToApplyToSource, Type (1))) - FloatVectorOperations::addWithMultiply (d, source, gainToApplyToSource, numSamples); - else - FloatVectorOperations::add (d, source, numSamples); - } + if (isClear) + { + isClear = false; + FloatVectorOperations::copyWithMultiply (d, source, gainToApplyToSource, numSamples); + } + else + { + FloatVectorOperations::addWithMultiply (d, source, gainToApplyToSource, numSamples); } } @@ -913,27 +885,20 @@ class AudioBuffer Type startGain, Type endGain) noexcept { - if (approximatelyEqual (startGain, endGain)) - { - addFrom (destChannel, destStartSample, source, numSamples, startGain); - } - else + jassert (isPositiveAndBelow (destChannel, numChannels)); + jassert (destStartSample >= 0 && numSamples >= 0 && destStartSample + numSamples <= size); + jassert (source != nullptr); + + if (numSamples > 0) { - jassert (isPositiveAndBelow (destChannel, numChannels)); - jassert (destStartSample >= 0 && numSamples >= 0 && destStartSample + numSamples <= size); - jassert (source != nullptr); + isClear = false; + const auto increment = (endGain - startGain) / (Type) numSamples; + auto* d = channels[destChannel] + destStartSample; - if (numSamples > 0) + while (--numSamples >= 0) { - isClear = false; - const auto increment = (endGain - startGain) / (Type) numSamples; - auto* d = channels[destChannel] + destStartSample; - - while (--numSamples >= 0) - { - *d++ += startGain * *source++; - startGain += increment; - } + *d++ += startGain * *source++; + startGain += increment; } } } @@ -1036,25 +1001,8 @@ class AudioBuffer if (numSamples > 0) { auto* d = channels[destChannel] + destStartSample; - - if (! approximatelyEqual (gain, Type (1))) - { - if (approximatelyEqual (gain, Type())) - { - if (! isClear) - FloatVectorOperations::clear (d, numSamples); - } - else - { - isClear = false; - FloatVectorOperations::copyWithMultiply (d, source, gain, numSamples); - } - } - else - { - isClear = false; - FloatVectorOperations::copy (d, source, numSamples); - } + isClear = false; + FloatVectorOperations::copyWithMultiply (d, source, gain, numSamples); } } @@ -1085,27 +1033,20 @@ class AudioBuffer Type startGain, Type endGain) noexcept { - if (approximatelyEqual (startGain, endGain)) - { - copyFrom (destChannel, destStartSample, source, numSamples, startGain); - } - else + jassert (isPositiveAndBelow (destChannel, numChannels)); + jassert (destStartSample >= 0 && numSamples >= 0 && destStartSample + numSamples <= size); + jassert (source != nullptr); + + if (numSamples > 0) { - jassert (isPositiveAndBelow (destChannel, numChannels)); - jassert (destStartSample >= 0 && numSamples >= 0 && destStartSample + numSamples <= size); - jassert (source != nullptr); + isClear = false; + const auto increment = (endGain - startGain) / (Type) numSamples; + auto* d = channels[destChannel] + destStartSample; - if (numSamples > 0) + while (--numSamples >= 0) { - isClear = false; - const auto increment = (endGain - startGain) / (Type) numSamples; - auto* d = channels[destChannel] + destStartSample; - - while (--numSamples >= 0) - { - *d++ = startGain * *source++; - startGain += increment; - } + *d++ = startGain * *source++; + startGain += increment; } } } From a50292f50d1f64c085b18cb3b078a85a9d52650c Mon Sep 17 00:00:00 2001 From: Anthony Nicholls Date: Thu, 21 Nov 2024 15:57:56 +0000 Subject: [PATCH 33/76] AudioBuffer: Prefer early returns to nested if statements --- .../buffers/juce_AudioSampleBuffer.h | 211 +++++++++--------- 1 file changed, 111 insertions(+), 100 deletions(-) diff --git a/modules/juce_audio_basics/buffers/juce_AudioSampleBuffer.h b/modules/juce_audio_basics/buffers/juce_AudioSampleBuffer.h index ddd5889673e8..5fbad7aac56f 100644 --- a/modules/juce_audio_basics/buffers/juce_AudioSampleBuffer.h +++ b/modules/juce_audio_basics/buffers/juce_AudioSampleBuffer.h @@ -569,17 +569,17 @@ class AudioBuffer */ void clear() noexcept { - if (! isClear) - { - for (int i = 0; i < numChannels; ++i) - { - JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4661) - FloatVectorOperations::clear (channels[i], size); - JUCE_END_IGNORE_WARNINGS_MSVC - } + if (isClear) + return; - isClear = true; + for (int i = 0; i < numChannels; ++i) + { + JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4661) + FloatVectorOperations::clear (channels[i], size); + JUCE_END_IGNORE_WARNINGS_MSVC } + + isClear = true; } /** Clears a specified region of all the channels. @@ -598,13 +598,13 @@ class AudioBuffer { jassert (startSample >= 0 && numSamples >= 0 && startSample + numSamples <= size); - if (! isClear) - { - for (int i = 0; i < numChannels; ++i) - FloatVectorOperations::clear (channels[i] + startSample, numSamples); + if (isClear) + return; - isClear = (startSample == 0 && numSamples == size); - } + for (int i = 0; i < numChannels; ++i) + FloatVectorOperations::clear (channels[i] + startSample, numSamples); + + isClear = (startSample == 0 && numSamples == size); } /** Clears a specified region of just one channel. @@ -700,11 +700,11 @@ class AudioBuffer jassert (isPositiveAndBelow (channel, numChannels)); jassert (startSample >= 0 && numSamples >= 0 && startSample + numSamples <= size); - if (! isClear) - { - auto* d = channels[channel] + startSample; - FloatVectorOperations::multiply (d, gain, numSamples); - } + if (isClear) + return; + + auto* d = channels[channel] + startSample; + FloatVectorOperations::multiply (d, gain, numSamples); } /** Applies a gain multiple to a region of all the channels. @@ -736,19 +736,19 @@ class AudioBuffer void applyGainRamp (int channel, int startSample, int numSamples, Type startGain, Type endGain) noexcept { - if (! isClear) - { - jassert (isPositiveAndBelow (channel, numChannels)); - jassert (startSample >= 0 && numSamples >= 0 && startSample + numSamples <= size); + jassert (isPositiveAndBelow (channel, numChannels)); + jassert (startSample >= 0 && numSamples >= 0 && startSample + numSamples <= size); - const auto increment = (endGain - startGain) / (float) numSamples; - auto* d = channels[channel] + startSample; + if (isClear) + return; - while (--numSamples >= 0) - { - *d++ *= startGain; - startGain += increment; - } + const auto increment = (endGain - startGain) / (float) numSamples; + auto* d = channels[channel] + startSample; + + while (--numSamples >= 0) + { + *d++ *= startGain; + startGain += increment; } } @@ -801,25 +801,25 @@ class AudioBuffer jassert (isPositiveAndBelow (sourceChannel, source.numChannels)); jassert (sourceStartSample >= 0 && sourceStartSample + numSamples <= source.size); - if (numSamples > 0 && ! source.isClear) - { - auto* d = channels[destChannel] + destStartSample; - auto* s = source.channels[sourceChannel] + sourceStartSample; + if (numSamples <= 0 || source.isClear) + return; - JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4661) + auto* d = channels[destChannel] + destStartSample; + auto* s = source.channels[sourceChannel] + sourceStartSample; - if (isClear) - { - isClear = false; - FloatVectorOperations::copyWithMultiply (d, s, gainToApplyToSource, numSamples); - } - else - { - FloatVectorOperations::addWithMultiply (d, s, gainToApplyToSource, numSamples); - } + JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4661) - JUCE_END_IGNORE_WARNINGS_MSVC + if (isClear) + { + isClear = false; + FloatVectorOperations::copyWithMultiply (d, s, gainToApplyToSource, numSamples); } + else + { + FloatVectorOperations::addWithMultiply (d, s, gainToApplyToSource, numSamples); + } + + JUCE_END_IGNORE_WARNINGS_MSVC } /** Adds samples from an array of floats to one of the channels. @@ -846,6 +846,9 @@ class AudioBuffer jassert (destStartSample >= 0 && numSamples >= 0 && destStartSample + numSamples <= size); jassert (source != nullptr); + if (numSamples <= 0) + return; + auto* d = channels[destChannel] + destStartSample; if (isClear) @@ -889,19 +892,19 @@ class AudioBuffer jassert (destStartSample >= 0 && numSamples >= 0 && destStartSample + numSamples <= size); jassert (source != nullptr); - if (numSamples > 0) - { - isClear = false; - const auto increment = (endGain - startGain) / (Type) numSamples; - auto* d = channels[destChannel] + destStartSample; + if (numSamples <= 0) + return; - while (--numSamples >= 0) - { - *d++ += startGain * *source++; - startGain += increment; - } + isClear = false; + const auto increment = (endGain - startGain) / (Type) numSamples; + auto* d = channels[destChannel] + destStartSample; + + while (--numSamples >= 0) + { + *d++ += startGain * *source++; + startGain += increment; } - } +} /** Copies samples from another buffer to this one. @@ -930,20 +933,20 @@ class AudioBuffer jassert (isPositiveAndBelow (sourceChannel, source.numChannels)); jassert (sourceStartSample >= 0 && numSamples >= 0 && sourceStartSample + numSamples <= source.size); - if (numSamples > 0) + if (numSamples <= 0) + return; + + if (source.isClear) { - if (source.isClear) - { - if (! isClear) - FloatVectorOperations::clear (channels[destChannel] + destStartSample, numSamples); - } - else - { - isClear = false; - FloatVectorOperations::copy (channels[destChannel] + destStartSample, - source.channels[sourceChannel] + sourceStartSample, - numSamples); - } + if (! isClear) + FloatVectorOperations::clear (channels[destChannel] + destStartSample, numSamples); + } + else + { + isClear = false; + FloatVectorOperations::copy (channels[destChannel] + destStartSample, + source.channels[sourceChannel] + sourceStartSample, + numSamples); } } @@ -968,11 +971,11 @@ class AudioBuffer jassert (destStartSample >= 0 && numSamples >= 0 && destStartSample + numSamples <= size); jassert (source != nullptr); - if (numSamples > 0) - { - isClear = false; - FloatVectorOperations::copy (channels[destChannel] + destStartSample, source, numSamples); - } + if (numSamples <= 0) + return; + + isClear = false; + FloatVectorOperations::copy (channels[destChannel] + destStartSample, source, numSamples); } /** Copies samples from an array of floats into one of the channels, applying a gain to it. @@ -998,12 +1001,12 @@ class AudioBuffer jassert (destStartSample >= 0 && numSamples >= 0 && destStartSample + numSamples <= size); jassert (source != nullptr); - if (numSamples > 0) - { - auto* d = channels[destChannel] + destStartSample; - isClear = false; - FloatVectorOperations::copyWithMultiply (d, source, gain, numSamples); - } + if (numSamples <= 0) + return; + + auto* d = channels[destChannel] + destStartSample; + isClear = false; + FloatVectorOperations::copyWithMultiply (d, source, gain, numSamples); } /** Copies samples from an array of floats into one of the channels, applying a gain ramp. @@ -1037,17 +1040,17 @@ class AudioBuffer jassert (destStartSample >= 0 && numSamples >= 0 && destStartSample + numSamples <= size); jassert (source != nullptr); - if (numSamples > 0) - { - isClear = false; - const auto increment = (endGain - startGain) / (Type) numSamples; - auto* d = channels[destChannel] + destStartSample; + if (numSamples <= 0) + return; - while (--numSamples >= 0) - { - *d++ = startGain * *source++; - startGain += increment; - } + isClear = false; + const auto increment = (endGain - startGain) / (Type) numSamples; + auto* d = channels[destChannel] + destStartSample; + + while (--numSamples >= 0) + { + *d++ = startGain * *source++; + startGain += increment; } } @@ -1077,8 +1080,7 @@ class AudioBuffer if (isClear) return Type (0); - auto r = findMinMax (channel, startSample, numSamples); - + const auto r = findMinMax (channel, startSample, numSamples); return jmax (r.getStart(), -r.getStart(), r.getEnd(), -r.getEnd()); } @@ -1087,9 +1089,11 @@ class AudioBuffer { Type mag (0); - if (! isClear) - for (int i = 0; i < numChannels; ++i) - mag = jmax (mag, getMagnitude (i, startSample, numSamples)); + if (isClear) + return mag; + + for (int i = 0; i < numChannels; ++i) + mag = jmax (mag, getMagnitude (i, startSample, numSamples)); return mag; } @@ -1100,7 +1104,7 @@ class AudioBuffer jassert (isPositiveAndBelow (channel, numChannels)); jassert (startSample >= 0 && numSamples >= 0 && startSample + numSamples <= size); - if (numSamples <= 0 || channel < 0 || channel >= numChannels || isClear) + if (numSamples <= 0 || isClear || ! isPositiveAndBelow (channel, numChannels)) return Type (0); auto* data = channels[channel] + startSample; @@ -1121,14 +1125,21 @@ class AudioBuffer jassert (isPositiveAndBelow (channel, numChannels)); jassert (startSample >= 0 && numSamples >= 0 && startSample + numSamples <= size); - if (! isClear) - std::reverse (channels[channel] + startSample, - channels[channel] + startSample + numSamples); + if (isClear) + return; + + std::reverse (channels[channel] + startSample, + channels[channel] + startSample + numSamples); } /** Reverses a part of the buffer. */ void reverse (int startSample, int numSamples) const noexcept { + jassert (startSample >= 0 && numSamples >= 0 && startSample + numSamples <= size); + + if (isClear) + return; + for (int i = 0; i < numChannels; ++i) reverse (i, startSample, numSamples); } From 8fe8717ebc9ba0f3c6632f86b17044d4c936b338 Mon Sep 17 00:00:00 2001 From: Anthony Nicholls Date: Fri, 22 Nov 2024 10:24:37 +0000 Subject: [PATCH 34/76] Random: Make the system random object safer to use from multiple threads --- modules/juce_core/maths/juce_Random.cpp | 38 ++++++++++++++++++------- modules/juce_core/maths/juce_Random.h | 13 ++++++--- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/modules/juce_core/maths/juce_Random.cpp b/modules/juce_core/maths/juce_Random.cpp index a11bd8693fe6..32a4d6af7b66 100644 --- a/modules/juce_core/maths/juce_Random.cpp +++ b/modules/juce_core/maths/juce_Random.cpp @@ -46,26 +46,29 @@ Random::Random() : seed (1) void Random::setSeed (const int64 newSeed) noexcept { - if (this == &getSystemRandom()) - { - // Resetting the system Random risks messing up - // JUCE's internal state. If you need a predictable - // stream of random numbers you should use a local - // Random object. - jassertfalse; - return; - } + // Resetting the system Random risks messing up JUCE's internal state. + // If you need a predictable stream of random numbers you should use a + // local Random object. + jassert (! isSystemRandom); seed = newSeed; } void Random::combineSeed (const int64 seedValue) noexcept { + // Resetting the system Random risks messing up JUCE's internal state. + // Consider using a local Random object instead. + jassert (! isSystemRandom); + seed ^= nextInt64() ^ seedValue; } void Random::setSeedRandomly() { + // Resetting the system Random risks messing up JUCE's internal state. + // Consider using a local Random object instead. + jassert (! isSystemRandom); + static std::atomic globalSeed { 0 }; combineSeed (globalSeed ^ (int64) (pointer_sized_int) this); @@ -78,13 +81,28 @@ void Random::setSeedRandomly() Random& Random::getSystemRandom() noexcept { - static Random sysRand; + thread_local Random sysRand = std::invoke ([] + { + Random r; + #if JUCE_ASSERTIONS_ENABLED_OR_LOGGED + r.isSystemRandom = true; + #endif + return r; + }); + return sysRand; } //============================================================================== int Random::nextInt() noexcept { + // If you encounter this assertion you've likely stored a reference to the + // system random object and are accessing it from a thread other than the + // one it was first created on. This may lead to race conditions on the + // random object. To avoid this assertion call Random::getSystemRandom() + // directly instead of storing a reference. + jassert (! isSystemRandom || this == &getSystemRandom()); + seed = (int64) (((((uint64) seed) * 0x5deece66dLL) + 11) & 0xffffffffffffLL); return (int) (seed >> 16); diff --git a/modules/juce_core/maths/juce_Random.h b/modules/juce_core/maths/juce_Random.h index d991bea03674..e49592392154 100644 --- a/modules/juce_core/maths/juce_Random.h +++ b/modules/juce_core/maths/juce_Random.h @@ -128,11 +128,12 @@ class JUCE_API Random final */ void setSeedRandomly(); - /** The overhead of creating a new Random object is fairly small, but if you want to avoid - it, you can call this method to get a global shared Random object. + /** The overhead of creating a new Random object is fairly small, but if you want + to avoid it, you can call this method to get a global shared Random object. - It's not thread-safe though, so threads should use their own Random object, otherwise - you run the risk of your random numbers becoming.. erm.. randomly corrupted.. + Note this will return a different object per thread it's accessed from, + making it thread safe. However, it's therefore important not store a reference + to this object that will later be accessed from other threads. */ static Random& getSystemRandom() noexcept; @@ -140,6 +141,10 @@ class JUCE_API Random final //============================================================================== int64 seed; + #if JUCE_ASSERTIONS_ENABLED_OR_LOGGED + bool isSystemRandom = false; + #endif + JUCE_LEAK_DETECTOR (Random) }; From 655d18b7210335ad4d6d3fb94d96a4520c24fde8 Mon Sep 17 00:00:00 2001 From: Anthony Nicholls Date: Fri, 22 Nov 2024 14:40:06 +0000 Subject: [PATCH 35/76] Random: Add some extra data race tests --- modules/juce_core/maths/juce_Random.cpp | 92 ++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 11 deletions(-) diff --git a/modules/juce_core/maths/juce_Random.cpp b/modules/juce_core/maths/juce_Random.cpp index 32a4d6af7b66..ac5a82c943db 100644 --- a/modules/juce_core/maths/juce_Random.cpp +++ b/modules/juce_core/maths/juce_Random.cpp @@ -204,23 +204,93 @@ class RandomTests final : public UnitTest void runTest() override { beginTest ("Random"); + { + Random r = getRandom(); - Random r = getRandom(); + for (int i = 2000; --i >= 0;) + { + expect (r.nextDouble() >= 0.0 && r.nextDouble() < 1.0); + expect (r.nextFloat() >= 0.0f && r.nextFloat() < 1.0f); + expect (r.nextInt (5) >= 0 && r.nextInt (5) < 5); + expect (r.nextInt (1) == 0); - for (int i = 2000; --i >= 0;) - { - expect (r.nextDouble() >= 0.0 && r.nextDouble() < 1.0); - expect (r.nextFloat() >= 0.0f && r.nextFloat() < 1.0f); - expect (r.nextInt (5) >= 0 && r.nextInt (5) < 5); - expect (r.nextInt (1) == 0); + int n = r.nextInt (50) + 1; + expect (r.nextInt (n) >= 0 && r.nextInt (n) < n); - int n = r.nextInt (50) + 1; - expect (r.nextInt (n) >= 0 && r.nextInt (n) < n); + n = r.nextInt (0x7ffffffe) + 1; + expect (r.nextInt (n) >= 0 && r.nextInt (n) < n); + } + } - n = r.nextInt (0x7ffffffe) + 1; - expect (r.nextInt (n) >= 0 && r.nextInt (n) < n); + beginTest ("System random stress test"); + { + // Run this with thread-sanitizer to detect race conditions + runOnMultipleThreadsConcurrently ([] { Random::getSystemRandom().nextInt(); }); } } + +private: + static void runOnMultipleThreadsConcurrently (std::function functionToInvoke, + int numberOfInvocationsPerThread = 10'000, + int numberOfThreads = 100) + { + class FastWaitableEvent + { + public: + void notify() { notified = true; } + void wait() const { while (! notified){} } + + private: + std::atomic notified = false; + }; + + class InvokerThread final : private Thread + { + public: + InvokerThread (std::function fn, FastWaitableEvent& notificationEvent, int numInvocationsToTrigger) + : Thread ("InvokerThread"), + invokable (fn), + notified (¬ificationEvent), + numInvocations (numInvocationsToTrigger) + { + startThread(); + } + + ~InvokerThread() { stopThread (-1); } + + void waitUntilReady() const { ready.wait(); } + + private: + void run() final + { + ready.notify(); + notified->wait(); + + for (int i = numInvocations; --i >= 0;) + invokable(); + } + + std::function invokable; + FastWaitableEvent* notified; + FastWaitableEvent ready; + int numInvocations; + }; + + std::vector> threads; + threads.reserve ((size_t) numberOfThreads); + FastWaitableEvent start; + + for (int i = numberOfThreads; --i >= 0;) + threads.push_back (std::make_unique (functionToInvoke, start, numberOfInvocationsPerThread)); + + for (auto& thread : threads) + thread->waitUntilReady(); + + // just to increase the odds that all the threads are now at the same point + // ready to be notified + Thread::sleep (1); + start.notify(); + } }; static RandomTests randomTests; From f98bf8434a1762b81d9add84d85dc34ae3ed694a Mon Sep 17 00:00:00 2001 From: Anthony Nicholls Date: Fri, 22 Nov 2024 10:26:04 +0000 Subject: [PATCH 36/76] TemporaryFile: Stopping use a LockedRandom now that system random is thread safe --- .../juce_core/files/juce_TemporaryFile.cpp | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/modules/juce_core/files/juce_TemporaryFile.cpp b/modules/juce_core/files/juce_TemporaryFile.cpp index 52fbc5883909..6614054b6287 100644 --- a/modules/juce_core/files/juce_TemporaryFile.cpp +++ b/modules/juce_core/files/juce_TemporaryFile.cpp @@ -35,23 +35,6 @@ namespace juce { -// Using Random::getSystemRandom() can be a bit dangerous in multithreaded contexts! -class LockedRandom -{ -public: - int nextInt() - { - const ScopedLock lock (mutex); - return random.nextInt(); - } - -private: - CriticalSection mutex; - Random random; -}; - -static LockedRandom lockedRandom; - static File createTempFile (const File& parentDirectory, String name, const String& suffix, int optionFlags) { @@ -63,7 +46,7 @@ static File createTempFile (const File& parentDirectory, String name, TemporaryFile::TemporaryFile (const String& suffix, const int optionFlags) : temporaryFile (createTempFile (File::getSpecialLocation (File::tempDirectory), - "temp_" + String::toHexString (lockedRandom.nextInt()), + "temp_" + String::toHexString (Random::getSystemRandom().nextInt()), suffix, optionFlags)), targetFile() { @@ -72,7 +55,7 @@ TemporaryFile::TemporaryFile (const String& suffix, const int optionFlags) TemporaryFile::TemporaryFile (const File& target, const int optionFlags) : temporaryFile (createTempFile (target.getParentDirectory(), target.getFileNameWithoutExtension() - + "_temp" + String::toHexString (lockedRandom.nextInt()), + + "_temp" + String::toHexString (Random::getSystemRandom().nextInt()), target.getFileExtension(), optionFlags)), targetFile (target) { From 48375432be49d1c605d8d9db7f7cf574f9ee84a0 Mon Sep 17 00:00:00 2001 From: Anthony Nicholls Date: Fri, 13 Oct 2023 17:23:08 +0100 Subject: [PATCH 37/76] TemporaryFile: Make single argument constructors explicit --- .../juce_core/files/juce_TemporaryFile.cpp | 6 ++++ modules/juce_core/files/juce_TemporaryFile.h | 32 +++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/modules/juce_core/files/juce_TemporaryFile.cpp b/modules/juce_core/files/juce_TemporaryFile.cpp index 6614054b6287..91a049661ba6 100644 --- a/modules/juce_core/files/juce_TemporaryFile.cpp +++ b/modules/juce_core/files/juce_TemporaryFile.cpp @@ -44,6 +44,12 @@ static File createTempFile (const File& parentDirectory, String name, return parentDirectory.getNonexistentChildFile (name, suffix, (optionFlags & TemporaryFile::putNumbersInBrackets) != 0); } +TemporaryFile::TemporaryFile() : TemporaryFile { String{} } {} + +TemporaryFile::TemporaryFile (const String& suffix) : TemporaryFile { suffix, 0 } {} + +TemporaryFile::TemporaryFile (const File& target) : TemporaryFile { target, 0 } {} + TemporaryFile::TemporaryFile (const String& suffix, const int optionFlags) : temporaryFile (createTempFile (File::getSpecialLocation (File::tempDirectory), "temp_" + String::toHexString (Random::getSystemRandom().nextInt()), diff --git a/modules/juce_core/files/juce_TemporaryFile.h b/modules/juce_core/files/juce_TemporaryFile.h index 33377f7c129c..dffe03dbd74d 100644 --- a/modules/juce_core/files/juce_TemporaryFile.h +++ b/modules/juce_core/files/juce_TemporaryFile.h @@ -87,6 +87,16 @@ class JUCE_API TemporaryFile }; //============================================================================== + /** Creates a randomly-named temporary file in the default temp directory. + */ + TemporaryFile(); + + /** Creates a randomly-named temporary file in the default temp directory. + + @param suffix a file suffix to use for the file + */ + explicit TemporaryFile (const String& suffix); + /** Creates a randomly-named temporary file in the default temp directory. @param suffix a file suffix to use for the file @@ -94,8 +104,24 @@ class JUCE_API TemporaryFile The file will not be created until you write to it. And remember that when this object is deleted, the file will also be deleted! */ - TemporaryFile (const String& suffix = String(), - int optionFlags = 0); + TemporaryFile (const String& suffix, + int optionFlags); + + /** Creates a temporary file in the same directory as a specified file. + + This is useful if you have a file that you want to overwrite, but don't + want to harm the original file if the write operation fails. You can + use this to create a temporary file next to the target file, then + write to the temporary file, and finally use overwriteTargetFileWithTemporary() + to replace the target file with the one you've just written. + + This class won't create any files until you actually write to them. And remember + that when this object is deleted, the temporary file will also be deleted! + + @param targetFile the file that you intend to overwrite - the temporary + file will be created in the same directory as this + */ + explicit TemporaryFile (const File& targetFile); /** Creates a temporary file in the same directory as a specified file. @@ -113,7 +139,7 @@ class JUCE_API TemporaryFile @param optionFlags a combination of the values listed in the OptionFlags enum */ TemporaryFile (const File& targetFile, - int optionFlags = 0); + int optionFlags); /** Creates a temporary file using an explicit filename. The other constructors are a better choice than this one, unless for some reason From c12ab11ee47327aecb9ce55f340c8b5101b03753 Mon Sep 17 00:00:00 2001 From: Anthony Nicholls Date: Fri, 22 Nov 2024 10:27:34 +0000 Subject: [PATCH 38/76] MIDI CI: Remove unnecessary call to setSeedRandomly --- modules/juce_midi_ci/ci/juce_CIDevice.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/juce_midi_ci/ci/juce_CIDevice.cpp b/modules/juce_midi_ci/ci/juce_CIDevice.cpp index 6c2d55a64216..ae43eabc6a5c 100644 --- a/modules/juce_midi_ci/ci/juce_CIDevice.cpp +++ b/modules/juce_midi_ci/ci/juce_CIDevice.cpp @@ -1168,7 +1168,6 @@ class Device::Impl : private SubscriptionManagerDelegate static MUID getReallyRandomMuid() { Random random; - random.setSeedRandomly(); return MUID::makeRandom (random); } From 7ab382d357fe92f347792fa51d0f1a2e1381ea89 Mon Sep 17 00:00:00 2001 From: Anthony Nicholls Date: Thu, 28 Nov 2024 11:09:03 +0000 Subject: [PATCH 39/76] VST3 Client: Remove unhelpful jassert --- .../juce_audio_plugin_client/juce_audio_plugin_client_VST3.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/juce_audio_plugin_client/juce_audio_plugin_client_VST3.cpp b/modules/juce_audio_plugin_client/juce_audio_plugin_client_VST3.cpp index 63045e3dbd3d..2e62ed015ca1 100644 --- a/modules/juce_audio_plugin_client/juce_audio_plugin_client_VST3.cpp +++ b/modules/juce_audio_plugin_client/juce_audio_plugin_client_VST3.cpp @@ -1673,9 +1673,6 @@ class JuceVST3EditController final : public Vst::EditController, #endif SharedBase{}); - if (targetIID == Vst::IRemapParamID::iid) - jassertfalse; - if (result.isOk()) return result; From c2f567f3ee39060e86c4dfc94199aae8601909e9 Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 2 Dec 2024 14:14:27 +0000 Subject: [PATCH 40/76] Fix unused variable warnings --- .../utilities/ARA/juce_AudioProcessor_ARAExtensions.cpp | 2 +- .../juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm | 2 +- .../juce_gui_basics/native/juce_UIViewComponentPeer_ios.mm | 4 ++-- modules/juce_video/native/juce_CameraDevice_ios.h | 4 ++-- modules/juce_video/native/juce_CameraDevice_mac.h | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/juce_audio_processors/utilities/ARA/juce_AudioProcessor_ARAExtensions.cpp b/modules/juce_audio_processors/utilities/ARA/juce_AudioProcessor_ARAExtensions.cpp index 588eb554e1a2..c34061e15378 100644 --- a/modules/juce_audio_processors/utilities/ARA/juce_AudioProcessor_ARAExtensions.cpp +++ b/modules/juce_audio_processors/utilities/ARA/juce_AudioProcessor_ARAExtensions.cpp @@ -134,7 +134,7 @@ void AudioProcessorARAExtension::didBindToARA() noexcept playbackRenderer->araExtension = this; #endif -#if (! JUCE_DISABLE_ASSERTIONS) +#if JUCE_ASSERTIONS_ENABLED_OR_LOGGED // validate proper subclassing of the instance role classes if (auto playbackRenderer = getPlaybackRenderer()) jassert (dynamic_cast (playbackRenderer) != nullptr); diff --git a/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm b/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm index 664243a03a5e..0f5ff074f36e 100644 --- a/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm +++ b/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm @@ -1633,7 +1633,7 @@ bool sendEventToInputContextOrComponent (NSEvent* ev) const auto handled = [&]() -> bool { - if (auto* target = findCurrentTextInputTarget()) + if (findCurrentTextInputTarget() != nullptr) if (const auto* inputContext = [view inputContext]) return [inputContext handleEvent: ev] && ! viewCannotHandleEvent; diff --git a/modules/juce_gui_basics/native/juce_UIViewComponentPeer_ios.mm b/modules/juce_gui_basics/native/juce_UIViewComponentPeer_ios.mm index 2d7835d82b87..c3f6341ba6d6 100644 --- a/modules/juce_gui_basics/native/juce_UIViewComponentPeer_ios.mm +++ b/modules/juce_gui_basics/native/juce_UIViewComponentPeer_ios.mm @@ -1107,7 +1107,7 @@ - (instancetype) initWithOwner: (UIViewComponentPeer*) ownerIn - (BOOL) canPerformAction: (SEL) action withSender: (id) sender { - if (auto* target = [self getTextInputTarget]) + if ([self getTextInputTarget] != nullptr) { if (action == @selector (paste:)) return [[UIPasteboard generalPasteboard] hasStrings]; @@ -1266,7 +1266,7 @@ - (void) setSelectedTextRange: (JuceUITextRange*) range - (UITextRange*) markedTextRange { if (owner != nullptr && owner->stringBeingComposed.isNotEmpty()) - if (auto* target = owner->findCurrentTextInputTarget()) + if (owner->findCurrentTextInputTarget() != nullptr) return [JuceUITextRange withRange: owner->getMarkedTextRange()]; return nil; diff --git a/modules/juce_video/native/juce_CameraDevice_ios.h b/modules/juce_video/native/juce_CameraDevice_ios.h index 395385fa7074..d0812c71d8bd 100644 --- a/modules/juce_video/native/juce_CameraDevice_ios.h +++ b/modules/juce_video/native/juce_CameraDevice_ios.h @@ -646,7 +646,7 @@ struct CameraDevice::Pimpl printImageOutputDebugInfo (captureOutput); - if (auto* connection = findVideoConnection (captureOutput)) + if (findVideoConnection (captureOutput) != nullptr) { auto* photoOutput = (AVCapturePhotoOutput*) captureOutput; auto outputConnection = [photoOutput connectionWithMediaType: AVMediaTypeVideo]; @@ -1176,7 +1176,7 @@ struct CameraDevice::ViewerComponent : public UIViewComponent { if ([keyPath isEqualToString: @"videoRotationAngleForHorizonLevelPreview"]) { - if (auto* previewLayer = getPreviewLayer (self)) + if (getPreviewLayer (self) != nullptr) { auto* viewer = static_cast (context); auto& session = viewer->cameraDevice.pimpl->captureSession; diff --git a/modules/juce_video/native/juce_CameraDevice_mac.h b/modules/juce_video/native/juce_CameraDevice_mac.h index df409217db4e..f87dcd689364 100644 --- a/modules/juce_video/native/juce_CameraDevice_mac.h +++ b/modules/juce_video/native/juce_CameraDevice_mac.h @@ -542,7 +542,7 @@ struct CameraDevice::Pimpl startSession(); - if (auto* videoConnection = getVideoConnection()) + if (getVideoConnection() != nullptr) imageOutput->triggerImageCapture (*this); } From 2583b0648132e6ff3df839e7f2caf4388d31080d Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 2 Dec 2024 15:20:49 +0000 Subject: [PATCH 41/76] NSViewComponentPeer: Guard API availability --- .../native/juce_NSViewComponentPeer_mac.mm | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm b/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm index 0f5ff074f36e..58c33abafabc 100644 --- a/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm +++ b/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm @@ -129,14 +129,16 @@ static constexpr int translateVirtualToAsciiKeyCode (int keyCode) noexcept constexpr int extendedKeyModifier = 0x30000; //============================================================================== -class JuceCALayerDelegate final : public ObjCClass> +struct JuceCALayerDelegateCallback +{ + virtual ~JuceCALayerDelegateCallback() = default; + virtual void displayLayer (CALayer*) = 0; +}; + +class API_AVAILABLE (macos (10.12)) JuceCALayerDelegate final : public ObjCClass> { public: - struct Callback - { - virtual ~Callback() = default; - virtual void displayLayer (CALayer*) = 0; - }; + using Callback = JuceCALayerDelegateCallback; static NSObject* construct (Callback* owner) { @@ -176,7 +178,7 @@ static void setOwner (id self, Callback* newOwner) //============================================================================== class NSViewComponentPeer final : public ComponentPeer, - private JuceCALayerDelegate::Callback + private JuceCALayerDelegateCallback { public: NSViewComponentPeer (Component& comp, const int windowStyleFlags, NSView* viewToAttachTo) From dfe4858e555656b2a8a2735e5967c8cb4cbe731c Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 25 Nov 2024 15:33:04 +0000 Subject: [PATCH 42/76] AudioTransportSource: Avoid nullptr dereference in hasStreamFinished() --- .../juce_audio_devices/sources/juce_AudioTransportSource.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/juce_audio_devices/sources/juce_AudioTransportSource.cpp b/modules/juce_audio_devices/sources/juce_AudioTransportSource.cpp index 69cbd1b30ce6..8bf92ba0aa72 100644 --- a/modules/juce_audio_devices/sources/juce_AudioTransportSource.cpp +++ b/modules/juce_audio_devices/sources/juce_AudioTransportSource.cpp @@ -168,8 +168,9 @@ double AudioTransportSource::getLengthInSeconds() const bool AudioTransportSource::hasStreamFinished() const noexcept { - return positionableSource->getNextReadPosition() > positionableSource->getTotalLength() + 1 - && ! positionableSource->isLooping(); + return positionableSource == nullptr + || (positionableSource->getNextReadPosition() > positionableSource->getTotalLength() + 1 + && ! positionableSource->isLooping()); } void AudioTransportSource::setNextReadPosition (int64 newPosition) From 3186522b0bd8801b6fe50f917e96dca3f9d500bf Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 25 Nov 2024 16:45:01 +0000 Subject: [PATCH 43/76] VST3 Host: Fix bug where MIDI CCs mapped to parameters would fail to update the host and editcontroller --- .../format_types/juce_VST3Common.h | 2 +- .../format_types/juce_VST3PluginFormat.cpp | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/modules/juce_audio_processors/format_types/juce_VST3Common.h b/modules/juce_audio_processors/format_types/juce_VST3Common.h index 8b1171ebbb4b..6b93925de9de 100644 --- a/modules/juce_audio_processors/format_types/juce_VST3Common.h +++ b/modules/juce_audio_processors/format_types/juce_VST3Common.h @@ -1430,7 +1430,7 @@ class MidiEventList : public Steinberg::Vst::IEventList controlEvent->controllerNumber); if (controlParamID != Steinberg::Vst::kNoParamId) - callback (controlParamID, controlEvent->paramValue, msg.getTimeStamp()); + callback (controlParamID, (float) controlEvent->paramValue, msg.getTimeStamp()); return true; } diff --git a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp index 41cddcc03cc6..fb122ff5d068 100644 --- a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp +++ b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp @@ -2593,6 +2593,11 @@ class VST3PluginInstance final : public AudioPluginInstance return cachedInfo; } + Steinberg::int32 getVstParamIndex() const + { + return vstParamIndex; + } + private: Vst::ParameterInfo fetchParameterInfo() const { @@ -3770,12 +3775,21 @@ class VST3PluginInstance final : public AudioPluginInstance if (acceptsMidi()) { - const auto midiMessageCallback = [&] (auto controlID, auto paramValue, auto time) + const auto midiMessageCallback = [&] (auto controlID, float paramValue, auto time) { Steinberg::int32 queueIndex{}; if (auto* queue = inputParameterChanges->addParameterData (controlID, queueIndex)) - queue->append ({ (Steinberg::int32) time, (float) paramValue }); + queue->append ({ (Steinberg::int32) time, paramValue }); + + if (auto* param = getParameterForID (controlID)) + { + // Send the parameter value to the editor + parameterDispatcher.push (param->getVstParamIndex(), paramValue); + + // Update the host's view of the parameter value + param->setValueWithoutUpdatingProcessor (paramValue); + } }; MidiEventList::hostToPluginEventList (*midiInputs, From 89a2407debe74ea5317010d64de3188cb084b093 Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 25 Nov 2024 17:06:56 +0000 Subject: [PATCH 44/76] AudioFormatReader: Update searchForLevel to work for more than two channels --- .../format/juce_AudioFormatReader.cpp | 53 +++++++------------ 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/modules/juce_audio_formats/format/juce_AudioFormatReader.cpp b/modules/juce_audio_formats/format/juce_AudioFormatReader.cpp index 57efbccf7dd1..5d61027bc502 100644 --- a/modules/juce_audio_formats/format/juce_AudioFormatReader.cpp +++ b/modules/juce_audio_formats/format/juce_AudioFormatReader.cpp @@ -299,11 +299,12 @@ int64 AudioFormatReader::searchForLevel (int64 startSample, return -1; const int bufferSize = 4096; - HeapBlock tempSpace (bufferSize * 2 + 64); + const size_t channels = numChannels; + HeapBlock tempSpace (bufferSize * channels + 64); + std::vector channelPointers (channels); - int* tempBuffer[3] = { tempSpace.get(), - tempSpace.get() + bufferSize, - nullptr }; + for (auto [index, ptr] : enumerate (channelPointers, size_t{})) + ptr = tempSpace + (bufferSize * index); int consecutive = 0; int64 firstMatchPos = -1; @@ -326,7 +327,7 @@ int64 AudioFormatReader::searchForLevel (int64 startSample, if (bufferStart >= lengthInSamples) break; - read (tempBuffer, 2, bufferStart, numThisTime, false); + read (channelPointers.data(), (int) channels, bufferStart, numThisTime, false); auto num = numThisTime; while (--num >= 0) @@ -334,43 +335,25 @@ int64 AudioFormatReader::searchForLevel (int64 startSample, if (numSamplesToSearch < 0) --startSample; - bool matches = false; auto index = (int) (startSample - bufferStart); - if (usesFloatingPointData) + const auto matches = std::invoke ([&] { - const float sample1 = std::abs (((float*) tempBuffer[0]) [index]); - - if (sample1 >= magnitudeRangeMinimum - && sample1 <= magnitudeRangeMaximum) + if (usesFloatingPointData) { - matches = true; + return std::any_of (channelPointers.begin(), channelPointers.end(), [&] (const auto& ptr) + { + const float sample = std::abs (((float*) ptr) [index]); + return magnitudeRangeMinimum <= sample && sample <= magnitudeRangeMaximum; + }); } - else if (numChannels > 1) - { - const float sample2 = std::abs (((float*) tempBuffer[1]) [index]); - matches = (sample2 >= magnitudeRangeMinimum - && sample2 <= magnitudeRangeMaximum); - } - } - else - { - const int sample1 = std::abs (tempBuffer[0] [index]); - - if (sample1 >= intMagnitudeRangeMinimum - && sample1 <= intMagnitudeRangeMaximum) - { - matches = true; - } - else if (numChannels > 1) + return std::any_of (channelPointers.begin(), channelPointers.end(), [&] (const auto& ptr) { - const int sample2 = std::abs (tempBuffer[1][index]); - - matches = (sample2 >= intMagnitudeRangeMinimum - && sample2 <= intMagnitudeRangeMaximum); - } - } + const int sample = std::abs (ptr[index]); + return intMagnitudeRangeMinimum <= sample && sample <= intMagnitudeRangeMaximum; + }); + }); if (matches) { From 6f85c2c8622aecfd831c02a92c779c4898317c3d Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 22 Nov 2024 21:42:51 +0100 Subject: [PATCH 45/76] CMake: Add missing modules to package script --- extras/Build/CMake/JUCEConfig.cmake.in | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extras/Build/CMake/JUCEConfig.cmake.in b/extras/Build/CMake/JUCEConfig.cmake.in index c0c1eef2ddef..03a09efb4af7 100644 --- a/extras/Build/CMake/JUCEConfig.cmake.in +++ b/extras/Build/CMake/JUCEConfig.cmake.in @@ -39,6 +39,7 @@ include("@PACKAGE_UTILS_INSTALL_DIR@/JUCEUtils.cmake") set(_juce_modules juce_analytics + juce_animation juce_audio_basics juce_audio_devices juce_audio_formats @@ -55,6 +56,7 @@ set(_juce_modules juce_gui_basics juce_gui_extra juce_javascript + juce_midi_ci juce_opengl juce_osc juce_product_unlocking From 3c75e7eeeb3b4d3304544f8954cc0a28427c81a9 Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 25 Nov 2024 19:16:34 +0000 Subject: [PATCH 46/76] GuiApp: Remove redundant qualifications from identifiers --- examples/CMake/GuiApp/Main.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/CMake/GuiApp/Main.cpp b/examples/CMake/GuiApp/Main.cpp index 6930f3e0db12..702aaa4dc60b 100644 --- a/examples/CMake/GuiApp/Main.cpp +++ b/examples/CMake/GuiApp/Main.cpp @@ -57,8 +57,8 @@ class GuiAppApplication final : public juce::JUCEApplication explicit MainWindow (juce::String name) : DocumentWindow (name, juce::Desktop::getInstance().getDefaultLookAndFeel() - .findColour (ResizableWindow::backgroundColourId), - DocumentWindow::allButtons) + .findColour (backgroundColourId), + allButtons) { setUsingNativeTitleBar (true); setContentOwned (new MainComponent(), true); @@ -78,7 +78,7 @@ class GuiAppApplication final : public juce::JUCEApplication // This is called when the user tries to close this window. Here, we'll just // ask the app to quit when this happens, but you can change this to do // whatever you need. - JUCEApplication::getInstance()->systemRequestedQuit(); + getInstance()->systemRequestedQuit(); } /* Note: Be careful if you override any DocumentWindow methods - the base From 4b9253dc762c841cb2f64f7f167e498700ec7f6f Mon Sep 17 00:00:00 2001 From: reuk Date: Wed, 27 Nov 2024 16:54:41 +0000 Subject: [PATCH 47/76] DirectX: Make it easier to enable debug logging output --- modules/juce_graphics/native/juce_DirectX_windows.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/juce_graphics/native/juce_DirectX_windows.h b/modules/juce_graphics/native/juce_DirectX_windows.h index 2e911b9c59e6..516d20248ee9 100644 --- a/modules/juce_graphics/native/juce_DirectX_windows.h +++ b/modules/juce_graphics/native/juce_DirectX_windows.h @@ -35,6 +35,8 @@ namespace juce { +constexpr auto enableDirectXDebugLayer = false; + struct DxgiAdapter : public ReferenceCountedObject { using Ptr = ReferenceCountedObjectPtr; @@ -60,7 +62,8 @@ struct DxgiAdapter : public ReferenceCountedObject // This flag adds support for surfaces with a different color channel ordering // than the API default. It is required for compatibility with Direct2D. - const auto creationFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; + const auto creationFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT + | (enableDirectXDebugLayer ? D3D11_CREATE_DEVICE_DEBUG : 0); if (const auto hr = D3D11CreateDevice (result->dxgiAdapter, D3D_DRIVER_TYPE_UNKNOWN, @@ -266,7 +269,7 @@ class DirectX ComSmartPtr d2dSharedFactory = [&] { D2D1_FACTORY_OPTIONS options; - options.debugLevel = D2D1_DEBUG_LEVEL_NONE; + options.debugLevel = enableDirectXDebugLayer ? D2D1_DEBUG_LEVEL_INFORMATION : D2D1_DEBUG_LEVEL_NONE; JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token") ComSmartPtr result; auto hr = D2D1CreateFactory (D2D1_FACTORY_TYPE_MULTI_THREADED, From 543ae06632a571920da44ddfe6c18bdcd1d9ea6b Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 25 Nov 2024 18:29:35 +0000 Subject: [PATCH 48/76] Windowing: Fix issue where edge resizers could incorrectly be displayed for non-resizable windows The buggy behaviour could be seen in a blank GUI app project by setting a native titlebar and calling setResizable (false, false). The resulting window would still display a resize cursor when hovering the window border. --- modules/juce_gui_basics/native/juce_Windowing_windows.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp index bf82c01d32c4..4dc75bc04f7e 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -2390,6 +2390,7 @@ class HWNDComponentPeer final : public ComponentPeer const auto hasMax = (styleFlags & windowHasMaximiseButton) != 0; const auto appearsOnTaskbar = (styleFlags & windowAppearsOnTaskbar) != 0; const auto resizable = (styleFlags & windowIsResizable) != 0; + const auto usesDropShadow = windowUsesNativeShadow(); if (parentToAddTo != nullptr) { @@ -2397,13 +2398,14 @@ class HWNDComponentPeer final : public ComponentPeer } else { - if (titled || windowUsesNativeShadow()) + if (titled || usesDropShadow) { + type |= usesDropShadow ? WS_CAPTION : 0; type |= titled ? (WS_OVERLAPPED | WS_CAPTION) : WS_POPUP; type |= hasClose ? (WS_SYSMENU | WS_CAPTION) : 0; type |= hasMin ? (WS_MINIMIZEBOX | WS_CAPTION | WS_SYSMENU) : 0; type |= hasMax ? (WS_MAXIMIZEBOX | WS_CAPTION | WS_SYSMENU) : 0; - type |= resizable || windowUsesNativeShadow() ? WS_THICKFRAME : 0; + type |= resizable ? WS_THICKFRAME : 0; } else { @@ -2420,7 +2422,7 @@ class HWNDComponentPeer final : public ComponentPeer L"", type, 0, 0, 0, 0, parentToAddTo, nullptr, (HINSTANCE) Process::getCurrentModuleInstanceHandle(), nullptr); - if (! titled && windowUsesNativeShadow()) + if (! titled && usesDropShadow) { // The choice of margins is very particular. // - Using 0 for all values disables the system decoration (shadow etc.) completely. From 90d9f573c299519735400849614e9c8c2b0ee163 Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 25 Nov 2024 21:12:44 +0000 Subject: [PATCH 49/76] Windowing: Avoid using bogus window-border sizes when computing constrained window sizes Previously, for windows with a native titlebar and a constrainer, the window could be restored at the wrong size. This happened because findPhysicalBorderSize() may return nonsensical values when called during a SC_RESTORE, which in turn produces an unexpected window size when adding the bogus border size to the constrained client area size. We now avoid trying to constrain the window if we're unable to determine the correct border size. I think this is only likely to happen during SC_RESTORE, in which case the system should have a pretty good idea of where the window should go, and constraining should not be necessary. --- .../native/juce_Windowing_windows.cpp | 53 +++++++++++++------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp index 4dc75bc04f7e..d092112be3b1 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -1530,7 +1530,7 @@ class HWNDComponentPeer final : public ComponentPeer return BorderSize { 0, 0, 0, 0 }; } - BorderSize findPhysicalBorderSize() const + std::optional> findPhysicalBorderSize() const { if (const auto custom = getCustomBorderSize()) return *custom; @@ -1543,10 +1543,15 @@ class HWNDComponentPeer final : public ComponentPeer if (! GetWindowInfo (hwnd, &info)) return {}; - return { info.rcClient.top - info.rcWindow.top, - info.rcClient.left - info.rcWindow.left, - info.rcWindow.bottom - info.rcClient.bottom, - info.rcWindow.right - info.rcClient.right }; + // Sometimes GetWindowInfo returns bogus information when called in the middle of restoring + // the window + if (info.rcWindow.left <= -32000 && info.rcWindow.top <= -32000) + return {}; + + return BorderSize { info.rcClient.top - info.rcWindow.top, + info.rcClient.left - info.rcWindow.left, + info.rcWindow.bottom - info.rcClient.bottom, + info.rcWindow.right - info.rcClient.right }; } void setBounds (const Rectangle& bounds, bool isNowFullScreen) override @@ -1563,7 +1568,7 @@ class HWNDComponentPeer final : public ComponentPeer const ScopedValueSetter scope (shouldIgnoreModalDismiss, true); - const auto borderSize = findPhysicalBorderSize(); + const auto borderSize = findPhysicalBorderSize().value_or (BorderSize{}); auto newBounds = borderSize.addedTo ([&] { ScopedThreadDPIAwarenessSetter setter { hwnd }; @@ -1630,7 +1635,7 @@ class HWNDComponentPeer final : public ComponentPeer // This means that we may end up clipping off up to one logical pixel under the physical // window border, but this is preferable to displaying an uninitialised/unpainted // region of the client area. - const auto physicalBorder = findPhysicalBorderSize(); + const auto physicalBorder = findPhysicalBorderSize().value_or (BorderSize{}); const auto physicalBounds = D2DUtilities::toRectangle (getWindowScreenRect (hwnd)); const auto physicalClient = physicalBorder.subtractedFrom (physicalBounds); @@ -1781,7 +1786,7 @@ class HWNDComponentPeer final : public ComponentPeer BorderSize getFrameSize() const override { - return findPhysicalBorderSize().multipliedBy (1.0 / scaleFactor); + return findPhysicalBorderSize().value_or (BorderSize{}).multipliedBy (1.0 / scaleFactor); } bool setAlwaysOnTop (bool alwaysOnTop) override @@ -3363,7 +3368,11 @@ class HWNDComponentPeer final : public ComponentPeer movingLeft, movingBottom, movingRight); - r = D2DUtilities::toRECT (modifiedPhysicalBounds); + + if (! modifiedPhysicalBounds.has_value()) + return TRUE; + + r = D2DUtilities::toRECT (*modifiedPhysicalBounds); } return TRUE; @@ -3378,12 +3387,14 @@ class HWNDComponentPeer final : public ComponentPeer && ! Component::isMouseButtonDownAnywhere()) { const auto requestedPhysicalBounds = D2DUtilities::toRectangle ({ wp.x, wp.y, wp.x + wp.cx, wp.y + wp.cy }); - const auto modifiedPhysicalBounds = getConstrainedBounds (requestedPhysicalBounds, false, false, false, false); - wp.x = modifiedPhysicalBounds.getX(); - wp.y = modifiedPhysicalBounds.getY(); - wp.cx = modifiedPhysicalBounds.getWidth(); - wp.cy = modifiedPhysicalBounds.getHeight(); + if (const auto modifiedPhysicalBounds = getConstrainedBounds (requestedPhysicalBounds, false, false, false, false)) + { + wp.x = modifiedPhysicalBounds->getX(); + wp.y = modifiedPhysicalBounds->getY(); + wp.cx = modifiedPhysicalBounds->getWidth(); + wp.cy = modifiedPhysicalBounds->getHeight(); + } } } @@ -3395,9 +3406,17 @@ class HWNDComponentPeer final : public ComponentPeer return 0; } - Rectangle getConstrainedBounds (Rectangle proposed, bool top, bool left, bool bottom, bool right) const + std::optional> getConstrainedBounds (Rectangle proposed, + bool top, + bool left, + bool bottom, + bool right) const { const auto physicalBorder = findPhysicalBorderSize(); + + if (! physicalBorder.has_value()) + return {}; + const auto logicalBorder = getFrameSize(); // The constrainer expects to operate in logical coordinate space. @@ -3409,7 +3428,7 @@ class HWNDComponentPeer final : public ComponentPeer // After the constrainer returns, we substitute in the other direction, replacing logical // borders with physical. const auto requestedPhysicalBounds = proposed; - const auto requestedPhysicalClient = physicalBorder.subtractedFrom (requestedPhysicalBounds); + const auto requestedPhysicalClient = physicalBorder->subtractedFrom (requestedPhysicalBounds); const auto requestedLogicalClient = detail::ScalingHelpers::unscaledScreenPosToScaled ( component, convertPhysicalScreenRectangleToLogical (requestedPhysicalClient, hwnd)); @@ -3455,7 +3474,7 @@ class HWNDComponentPeer final : public ComponentPeer return modified; }(); - return physicalBorder.addedTo (withSnappedPosition); + return physicalBorder->addedTo (withSnappedPosition); } enum class ForceRefreshDispatcher From 81a95abb3c8049fe5208f3f7c309f015aebfc7a2 Mon Sep 17 00:00:00 2001 From: reuk Date: Tue, 26 Nov 2024 13:20:29 +0000 Subject: [PATCH 50/76] Windowing: Use local coordinate space of component in call to findControlAtPoint --- .../native/juce_Windowing_windows.cpp | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp index d092112be3b1..8cb548ee43c0 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -2032,7 +2032,7 @@ class HWNDComponentPeer final : public ComponentPeer } HWNDComponentPeer& peer; - ComponentPeer::DragInfo dragInfo; + DragInfo dragInfo; bool peerIsDeleted = false; private: @@ -3740,12 +3740,12 @@ class HWNDComponentPeer final : public ComponentPeer static ModifierKeys getMouseModifiers() { - HWNDComponentPeer::updateKeyModifiers(); + updateKeyModifiers(); int mouseMods = 0; - if (HWNDComponentPeer::isKeyDown (VK_LBUTTON)) mouseMods |= ModifierKeys::leftButtonModifier; - if (HWNDComponentPeer::isKeyDown (VK_RBUTTON)) mouseMods |= ModifierKeys::rightButtonModifier; - if (HWNDComponentPeer::isKeyDown (VK_MBUTTON)) mouseMods |= ModifierKeys::middleButtonModifier; + if (isKeyDown (VK_LBUTTON)) mouseMods |= ModifierKeys::leftButtonModifier; + if (isKeyDown (VK_RBUTTON)) mouseMods |= ModifierKeys::rightButtonModifier; + if (isKeyDown (VK_MBUTTON)) mouseMods |= ModifierKeys::middleButtonModifier; ModifierKeys::currentModifiers = ModifierKeys::currentModifiers.withoutMouseButtons().withFlags (mouseMods); @@ -3796,8 +3796,12 @@ class HWNDComponentPeer final : public ComponentPeer if (const auto result = DefWindowProc (h, message, wParam, lParam); HTSIZEFIRST <= result && result <= HTSIZELAST) return result; - const auto localPoint = getLocalPointFromScreenLParam (lParam).toFloat(); - const auto kind = component.findControlAtPoint (localPoint); + const auto physicalPoint = D2DUtilities::toPoint (getPOINTFromLParam (lParam)); + const auto logicalPoint = convertPhysicalScreenPointToLogical (physicalPoint, hwnd); + const auto localPoint = globalToLocal (logicalPoint.toFloat()); + const auto componentPoint = detail::ScalingHelpers::unscaledScreenPosToScaled (component, localPoint); + + const auto kind = component.findControlAtPoint (componentPoint); using Kind = Component::WindowControlKind; switch (kind) @@ -4093,7 +4097,7 @@ class HWNDComponentPeer final : public ComponentPeer if (auto* modal = Component::getCurrentlyModalComponent()) if (auto* peer = modal->getPeer()) - if ((peer->getStyleFlags() & ComponentPeer::windowIsTemporary) != 0) + if ((peer->getStyleFlags() & windowIsTemporary) != 0) sendInputAttemptWhenModalMessage(); break; From c24d1a17a72be472d559d19cfb399ffbaf40601b Mon Sep 17 00:00:00 2001 From: reuk Date: Wed, 27 Nov 2024 16:55:49 +0000 Subject: [PATCH 51/76] Windowing: Avoid changing window bounds in WM_WINDOWPOSCHANGING This change intends to address a bug observed only on Windows 10 with a display scale factor of 125%. When the native titlebar is enabled, and the window's border-resizer is used to resize the window slowly the with mouse, the client area of the window may move to the wrong location, or be drawn with some areas obscured/clipped. This is especially observable when resizing the WidgetsDemo to its smallest size, and then dragging the right border a single pixel to the right. On my computer, this consistently causes the client area to display at the wrong location. I haven't been able to find any obvious bug in JUCE that might cause this behaviour. In particular, it seems that the window begins displaying incorrectly *before* the window ever actually resizes. During the resize, the system sends events (WM_SIZING and WM_WINDOWPOSCHANGING) to the window, and according to the documentation, the window may modify the message parameters in order to constrain the new window size. When running on a scaled display, JUCE attempts to map the logical client area size to a sensible size in physical pixels, and uses the sizing messages to enforce this size requirement. In the case of the broken window rendering, the system requests a new window size, which JUCE rejects. The window's display state doesn't change, so the swap chain does not resize, and the swap chain does not present. Put another way, the broken rendering happens *independently* of JUCE modifying the swap chain in any way. Therefore, I believe that the bug is introduced elsewhere, potentially by Windows itself. I also checked to see whether the issue could be caused by mishandling of the NCCALCSIZE message, which is normally used to configure the relative positions of the client and nonclient areas. However, in the buggy case, NCCALCSIZE is not sent until *after* the first 'broken' frame is painted - and even then, the implementation immediately falls back to DefWindowProc. Given that the issue appears to be a bug in Windows, the proposed change is a workaround, rather than a true fix. It appears as though the problem goes away when WM_WINDOWPOSCHANGING does not modify the requested bounds. Therefore, for windows with native titlebars, we rely on the constraints to be applied in WM_SIZING only, when sizing the window in a sizemove gesture. --- .../native/juce_Windowing_windows.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp index 8cb548ee43c0..8af733c51136 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -2231,7 +2231,7 @@ class HWNDComponentPeer final : public ComponentPeer uint32 lastPaintTime = 0; ULONGLONG lastMagnifySize = 0; bool isDragging = false, isMouseOver = false, - hasCreatedCaret = false, constrainerIsResizing = false; + hasCreatedCaret = false, constrainerIsResizing = false, sizing = false; IconConverters::IconPtr currentWindowIcon; FileDropTarget* dropTarget = nullptr; UWPUIViewSettings uwpViewSettings; @@ -4010,13 +4010,25 @@ class HWNDComponentPeer final : public ComponentPeer break; //============================================================================== + case WM_ENTERSIZEMOVE: + sizing = true; + break; + + case WM_EXITSIZEMOVE: + sizing = false; + break; + case WM_SIZING: + sizing = true; return handleSizeConstraining (*(RECT*) lParam, wParam); case WM_MOVING: return handleSizeConstraining (*(RECT*) lParam, 0); case WM_WINDOWPOSCHANGING: + if (hasTitleBar() && sizing) + break; + return handlePositionChanging (*(WINDOWPOS*) lParam); case 0x2e0: /* WM_DPICHANGED */ return handleDPIChanging ((int) HIWORD (wParam), *(RECT*) lParam); From 2fcf9ebf38982c9487b334367a8b7c05c4ccb330 Mon Sep 17 00:00:00 2001 From: reuk Date: Wed, 27 Nov 2024 20:10:35 +0000 Subject: [PATCH 52/76] Windowing: Fix issue where components dragged between monitors with different scalings could detach from the mouse This bug could be observed by running the WidgetsDemo Drag+Drop pane on Windows 10, and dragging an item between two displays at different scale factors. This is issue is a regression introduced in 9817a2bb66408ba44cdf365f90f91fd33afedcc9. The regression was caused by the change in mouse position calculation. The incorrect version switched to using ClientToScreen, but the correct version used getPointFromLocalLParam. The function getPointFromLocalLParam was replaced by clientLParamToPoint in 24ab3cb6a3aa926b963614669a0ac9ab609333cf, and is restored by this commit. --- .../native/juce_Windowing_windows.cpp | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp index 8af733c51136..6ad1d16dd160 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -2683,22 +2683,15 @@ class HWNDComponentPeer final : public ComponentPeer client, }; - std::optional doMouseMove (LPARAM lParam, bool isMouseDownEvent, WindowArea area) + std::optional doMouseMove (const LPARAM lParam, bool isMouseDownEvent, WindowArea area) { - auto point = getPOINTFromLParam (lParam); - - if (area == WindowArea::client) - ClientToScreen (hwnd, &point); - - const auto adjustedLParam = MAKELPARAM (point.x, point.y); - // Check if the mouse has moved since being pressed in the caption area. // If it has, then we defer to DefWindowProc to handle the mouse movement. // Allowing DefWindowProc to handle WM_NCLBUTTONDOWN directly will pause message // processing (and therefore painting) when the mouse is clicked in the caption area, // which is why we wait until the mouse is *moved* before asking the system to take over. // Letting the system handle the move is important for things like Aero Snap to work. - if (captionMouseDown.has_value() && *captionMouseDown != adjustedLParam) + if (area == WindowArea::nonclient && captionMouseDown.has_value() && *captionMouseDown != lParam) { captionMouseDown.reset(); @@ -2710,20 +2703,21 @@ class HWNDComponentPeer final : public ComponentPeer // ModifierKeys::currentModifiers gets left in the wrong state. As a workaround, we // manually update the modifier keys after DefWindowProc exits, and update the // capture state if necessary. - const auto result = DefWindowProc (hwnd, WM_NCLBUTTONDOWN, HTCAPTION, adjustedLParam); + const auto result = DefWindowProc (hwnd, WM_NCLBUTTONDOWN, HTCAPTION, lParam); getMouseModifiers(); releaseCaptureIfNecessary(); return result; } - const auto position = getLocalPointFromScreenLParam (adjustedLParam); - ModifierKeys modsToSend (ModifierKeys::currentModifiers); // this will be handled by WM_TOUCH if (isTouchEvent() || areOtherTouchSourcesActive()) return {}; + const auto position = area == WindowArea::client ? getPointFromLocalLParam (lParam) + : getLocalPointFromScreenLParam (lParam); + if (! isMouseOver) { isMouseOver = true; @@ -2808,7 +2802,7 @@ class HWNDComponentPeer final : public ComponentPeer isDragging = true; - doMouseEvent (clientLparamToPoint (lParam), MouseInputSource::defaultPressure); + doMouseEvent (getPointFromLocalLParam (lParam), MouseInputSource::defaultPressure); } } @@ -3726,11 +3720,21 @@ class HWNDComponentPeer final : public ComponentPeer return globalToLocal (convertPhysicalScreenPointToLogical (globalPos, hwnd).toFloat()); } - Point clientLparamToPoint (LPARAM lParam) + Point getPointFromLocalLParam (LPARAM lParam) noexcept { - auto point = getPOINTFromLParam (lParam); - ClientToScreen (hwnd, &point); - return getLocalPointFromScreenLParam (MAKELPARAM (point.x, point.y)); + const auto p = D2DUtilities::toPoint (getPOINTFromLParam (lParam)); + + if (! isPerMonitorDPIAwareWindow (hwnd)) + return p.toFloat(); + + // LPARAM is relative to this window's top-left but may be on a different monitor so we need to calculate the + // physical screen position and then convert this to local logical coordinates + auto r = getWindowScreenRect (hwnd); + const auto windowBorder = findPhysicalBorderSize().value_or (BorderSize{}); + const auto offset = p + + Point { (int) r.left, (int) r.top } + + Point { windowBorder.getLeft(), windowBorder.getTop() }; + return globalToLocal (Desktop::getInstance().getDisplays().physicalToLogical (offset).toFloat()); } Point getCurrentMousePos() noexcept @@ -3978,7 +3982,7 @@ class HWNDComponentPeer final : public ComponentPeer case WM_LBUTTONUP: case WM_MBUTTONUP: case WM_RBUTTONUP: - doMouseUp (clientLparamToPoint (lParam), wParam); + doMouseUp (getPointFromLocalLParam (lParam), wParam); return 0; case WM_POINTERWHEEL: From fc76e936d3bcd79642b9244abd2b5f45698c1124 Mon Sep 17 00:00:00 2001 From: reuk Date: Thu, 28 Nov 2024 16:19:12 +0000 Subject: [PATCH 53/76] DragImageComponent: Avoid case where image may detach from mouse when dragging between screens When two monitors are available, both with different scale factors, then the drag-image may 'detach' from the mouse while the image's top-left coordinate was on one display, and the mouse cursor was on the other display. This happened because, on Windows, the mouse cursor moves continuously in physical (not logical!) space. In other words, the mouse may not move continuously in logical space, and the discontinuity becomes visible when components are positioned relative to the mouse in logical space. In order to display consistently, the top-left position of the image must be set relative to the physical position of the mouse. --- .../mouse/juce_DragAndDropContainer.cpp | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/modules/juce_gui_basics/mouse/juce_DragAndDropContainer.cpp b/modules/juce_gui_basics/mouse/juce_DragAndDropContainer.cpp index 88fe008996e7..3ec2a0b18dd3 100644 --- a/modules/juce_gui_basics/mouse/juce_DragAndDropContainer.cpp +++ b/modules/juce_gui_basics/mouse/juce_DragAndDropContainer.cpp @@ -324,12 +324,35 @@ class DragAndDropContainer::DragImageComponent final : public Component, void setNewScreenPos (Point screenPos) { - auto newPos = screenPos - imageOffset; + setTopLeftPosition (std::invoke ([&] + { + if (auto* p = getParentComponent()) + return p->getLocalPoint (nullptr, screenPos - imageOffset); + + #if JUCE_WINDOWS + // On Windows, the mouse position is continuous in physical pixels across screen boundaries. + // i.e. if two screens are set to different scale factors, when the mouse moves horizontally + // between those screens, the mouse's physical y coordinate will be preserved, and if + // the mouse moves vertically between screens its physical x coordinate will be preserved. + + // To avoid the dragged image detaching from the mouse, compute the new top left position + // in physical coords and then convert back to logical. + // If we were to stay in logical coordinates the whole time, the image may detach from the + // mouse because the mouse does not move continuously in logical coordinate space. + + const auto& displays = Desktop::getInstance().getDisplays(); + const auto physicalPos = displays.logicalToPhysical (screenPos); + + float scale = 1.0f; - if (auto* p = getParentComponent()) - newPos = p->getLocalPoint (nullptr, newPos); + if (auto* p = getPeer()) + scale = (float) p->getPlatformScaleFactor(); - setTopLeftPosition (newPos); + return displays.physicalToLogical (physicalPos - (imageOffset * scale)); + #else + return screenPos - imageOffset; + #endif + })); } void sendDragMove (DragAndDropTarget::SourceDetails& details) const From 038b0d6c9ee652437cae3cbc0aa08f295ca30b1b Mon Sep 17 00:00:00 2001 From: reuk Date: Thu, 28 Nov 2024 20:42:46 +0000 Subject: [PATCH 54/76] Windows: Fix bug where IME failed to display on Windows 11 --- modules/juce_gui_basics/native/juce_Windowing_windows.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp index 6ad1d16dd160..cafc6a727b5a 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -4328,7 +4328,7 @@ class HWNDComponentPeer final : public ComponentPeer case WM_IME_SETCONTEXT: imeHandler.handleSetContext (h, wParam == TRUE); lParam &= ~(LPARAM) ISC_SHOWUICOMPOSITIONWINDOW; - return ImmIsUIMessage (h, message, wParam, lParam); + break; case WM_IME_STARTCOMPOSITION: imeHandler.handleStartComposition (*this); return 0; case WM_IME_ENDCOMPOSITION: imeHandler.handleEndComposition (*this, h); return 0; From cfee7cfc9388c69b881c96f1f01abf13dc35ec72 Mon Sep 17 00:00:00 2001 From: reuk Date: Thu, 28 Nov 2024 20:43:04 +0000 Subject: [PATCH 55/76] Windows: Fix bug where IME displayed at incorrect location on scaled displays --- modules/juce_gui_basics/native/juce_Windowing_windows.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp index cafc6a727b5a..bf8d63c3fa6c 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -4586,9 +4586,10 @@ class HWNDComponentPeer final : public ComponentPeer { if (auto* targetComp = dynamic_cast (target)) { - auto area = peer.getComponent().getLocalArea (targetComp, target->getCaretRectangle()); + const auto screenPos = targetComp->localPointToGlobal (target->getCaretRectangle().getBottomLeft()); + const auto relativePos = peer.globalToLocal (screenPos) * peer.getPlatformScaleFactor(); - CANDIDATEFORM pos = { 0, CFS_CANDIDATEPOS, { area.getX(), area.getBottom() }, { 0, 0, 0, 0 } }; + CANDIDATEFORM pos { 0, CFS_CANDIDATEPOS, D2DUtilities::toPOINT (relativePos), { 0, 0, 0, 0 } }; ImmSetCandidateWindow (hImc, &pos); } } From 0e12c2da92f3d32d7157aa0a8e8bcb22197805dd Mon Sep 17 00:00:00 2001 From: Anthony Nicholls Date: Mon, 2 Dec 2024 15:24:16 +0000 Subject: [PATCH 56/76] VST3 Client: Fix an issue with the reporting of VST3 plugin IDs --- .../utilities/juce_VST3ClientExtensions.cpp | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.cpp b/modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.cpp index b3cd133bb843..8cb875b114bc 100644 --- a/modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.cpp +++ b/modules/juce_audio_processors/utilities/juce_VST3ClientExtensions.cpp @@ -74,13 +74,39 @@ VST3ClientExtensions::InterfaceId VST3ClientExtensions::convertJucePluginId (uin return 0; }); - std::array data; - std::memcpy (&data[0], &word0, sizeof (word0)); - std::memcpy (&data[4], &word1, sizeof (word1)); - std::memcpy (&data[8], &manufacturerCode, sizeof (manufacturerCode)); - std::memcpy (&data[12], &pluginCode, sizeof (pluginCode)); + constexpr auto getByteFromLSB = [] (uint32_t word, int byteIndex) + { + jassert (isPositiveAndNotGreaterThan (byteIndex, 3)); + return (std::byte) ((word >> (byteIndex * 8)) & 0xff); + }; + + #if JUCE_WINDOWS + constexpr auto isWindows = true; + #else + constexpr auto isWindows = false; + #endif - return data; + return { + getByteFromLSB (word0, isWindows ? 0 : 3), + getByteFromLSB (word0, isWindows ? 1 : 2), + getByteFromLSB (word0, isWindows ? 2 : 1), + getByteFromLSB (word0, isWindows ? 3 : 0), + + getByteFromLSB (word1, isWindows ? 2 : 3), + getByteFromLSB (word1, isWindows ? 3 : 2), + getByteFromLSB (word1, isWindows ? 0 : 1), + getByteFromLSB (word1, isWindows ? 1 : 0), + + getByteFromLSB (manufacturerCode, 3), + getByteFromLSB (manufacturerCode, 2), + getByteFromLSB (manufacturerCode, 1), + getByteFromLSB (manufacturerCode, 0), + + getByteFromLSB (pluginCode, 3), + getByteFromLSB (pluginCode, 2), + getByteFromLSB (pluginCode, 1), + getByteFromLSB (pluginCode, 0) + }; } VST3ClientExtensions::InterfaceId VST3ClientExtensions::convertVST2PluginId (uint32_t pluginCode, From a889149cbdef11844c94d8a875d4b6f203f0b3da Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 2 Dec 2024 17:45:31 +0000 Subject: [PATCH 57/76] PixelRGB: Fix pixel order issue when creating CGImages on iOS --- modules/juce_graphics/colour/juce_PixelFormats.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/juce_graphics/colour/juce_PixelFormats.h b/modules/juce_graphics/colour/juce_PixelFormats.h index 7a17a87aa5d5..cd5bc4d168f2 100644 --- a/modules/juce_graphics/colour/juce_PixelFormats.h +++ b/modules/juce_graphics/colour/juce_PixelFormats.h @@ -556,7 +556,7 @@ class JUCE_API PixelRGB //============================================================================== /** The indexes of the different components in the byte layout of this type of colour. */ - #if JUCE_MAC + #if JUCE_MAC || JUCE_IOS enum { indexR = 0, indexG = 1, indexB = 2 }; #else enum { indexR = 2, indexG = 1, indexB = 0 }; @@ -578,7 +578,7 @@ class JUCE_API PixelRGB } //============================================================================== - #if JUCE_MAC + #if JUCE_MAC || JUCE_IOS uint8 r, g, b; #else uint8 b, g, r; From 5c138561bb08dffd3353c97c8720b4123e524f8a Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 2 Dec 2024 19:14:15 +0000 Subject: [PATCH 58/76] MidiDeviceListConnectionBroadcaster: Avoid constructing MessageManager on incorrect thread Previously, if the very first call to MidiDeviceListConnectionBroadcaster::get() happened on a background thread (which could happen on macOS in response to a MIDI setup configuration change), then MessageManager::getInstance and getAvailableDevices could be called on that same thread. With this change in place, midi change notifications will be ignored if there's no message manager available, and getAvailableDevices will only be called on the message thread. --- ...ce_MidiDeviceListConnectionBroadcaster.cpp | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/modules/juce_audio_devices/midi_io/juce_MidiDeviceListConnectionBroadcaster.cpp b/modules/juce_audio_devices/midi_io/juce_MidiDeviceListConnectionBroadcaster.cpp index 89e843d765b4..c80d2168de03 100644 --- a/modules/juce_audio_devices/midi_io/juce_MidiDeviceListConnectionBroadcaster.cpp +++ b/modules/juce_audio_devices/midi_io/juce_MidiDeviceListConnectionBroadcaster.cpp @@ -57,20 +57,22 @@ class MidiDeviceListConnectionBroadcaster final : private AsyncUpdater void notify() { - if (MessageManager::getInstance()->isThisTheMessageThread()) - { - cancelPendingUpdate(); + auto* mm = MessageManager::getInstanceWithoutCreating(); - const State newState; + if (mm == nullptr) + return; - if (std::exchange (lastNotifiedState, newState) != newState) - for (auto it = callbacks.begin(); it != callbacks.end();) - NullCheckedInvocation::invoke ((it++)->second); - } - else + if (! mm->isThisTheMessageThread()) { triggerAsyncUpdate(); + return; } + + cancelPendingUpdate(); + + if (auto prev = std::exchange (lastNotifiedState, State{}); prev != lastNotifiedState) + for (auto it = callbacks.begin(); it != callbacks.end();) + NullCheckedInvocation::invoke ((it++)->second); } static auto& get() @@ -108,7 +110,7 @@ class MidiDeviceListConnectionBroadcaster final : private AsyncUpdater } std::map> callbacks; - State lastNotifiedState; + std::optional lastNotifiedState; MidiDeviceListConnection::Key key = 0; }; From 051e7017806d977162aaf746a78980ed3f63d3c7 Mon Sep 17 00:00:00 2001 From: reuk Date: Tue, 3 Dec 2024 14:26:21 +0000 Subject: [PATCH 59/76] Windowing: Update mousewheel handler on Windows to always process messages in the context of the peer receiving the event --- .../juce_gui_basics/native/juce_Windowing_windows.cpp | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp index bf8d63c3fa6c..1e19b9df8c77 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -2895,14 +2895,8 @@ class HWNDComponentPeer final : public ComponentPeer // the window with focus, despite what the MSDN docs might say. // This is the behaviour we want; if we're receiving a scroll event, we can assume it // should be processed by the current peer. - const auto currentMousePos = getPOINTFromLParam ((LPARAM) GetMessagePos()); - auto* peer = getOwnerOfWindow (WindowFromPoint (currentMousePos)); - - if (peer == nullptr) - return false; - - const auto localPos = peer->globalToLocal (convertPhysicalScreenPointToLogical (D2DUtilities::toPoint (currentMousePos), hwnd).toFloat()); - peer->handleMouseWheel (getPointerType (wParam), localPos, getMouseEventTime(), wheel); + const auto localPos = getLocalPointFromScreenLParam ((LPARAM) GetMessagePos()); + handleMouseWheel (getPointerType (wParam), localPos, getMouseEventTime(), wheel); return true; } From 5878adaecd1b34896d70dbb2f9e9c2246b50b6de Mon Sep 17 00:00:00 2001 From: reuk Date: Thu, 21 Nov 2024 21:12:03 +0000 Subject: [PATCH 60/76] Projucer: Add android.permission.POST_NOTIFICATIONS to manifest when push notifications enabled --- .../DemoRunner/Builds/Android/app/src/main/AndroidManifest.xml | 1 + .../Builds/Android/app/src/main/AndroidManifest.xml | 1 + .../Builds/Android/app/src/main/AndroidManifest.xml | 1 + .../Builds/Android/app/src/main/AndroidManifest.xml | 1 + .../Source/ProjectSaving/jucer_ProjectExport_Android.h | 3 +++ 5 files changed, 7 insertions(+) diff --git a/examples/DemoRunner/Builds/Android/app/src/main/AndroidManifest.xml b/examples/DemoRunner/Builds/Android/app/src/main/AndroidManifest.xml index 062c97cbde5b..d2797d5ca22f 100644 --- a/examples/DemoRunner/Builds/Android/app/src/main/AndroidManifest.xml +++ b/examples/DemoRunner/Builds/Android/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + diff --git a/extras/AudioPerformanceTest/Builds/Android/app/src/main/AndroidManifest.xml b/extras/AudioPerformanceTest/Builds/Android/app/src/main/AndroidManifest.xml index 5b9c0d726613..f499caa9f638 100644 --- a/extras/AudioPerformanceTest/Builds/Android/app/src/main/AndroidManifest.xml +++ b/extras/AudioPerformanceTest/Builds/Android/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + diff --git a/extras/AudioPluginHost/Builds/Android/app/src/main/AndroidManifest.xml b/extras/AudioPluginHost/Builds/Android/app/src/main/AndroidManifest.xml index f99a0ecc2c28..7492a37f5bbb 100644 --- a/extras/AudioPluginHost/Builds/Android/app/src/main/AndroidManifest.xml +++ b/extras/AudioPluginHost/Builds/Android/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + diff --git a/extras/NetworkGraphicsDemo/Builds/Android/app/src/main/AndroidManifest.xml b/extras/NetworkGraphicsDemo/Builds/Android/app/src/main/AndroidManifest.xml index 7c000ddb7acb..a34771de6d83 100644 --- a/extras/NetworkGraphicsDemo/Builds/Android/app/src/main/AndroidManifest.xml +++ b/extras/NetworkGraphicsDemo/Builds/Android/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h index b36320e81df3..8667a254008a 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h @@ -1992,6 +1992,9 @@ class AndroidProjectExporter final : public ProjectExporter if (androidVibratePermission.get()) s.add ("android.permission.VIBRATE"); + if (arePushNotificationsEnabled()) + s.add ("android.permission.POST_NOTIFICATIONS"); + return getCleanedStringArray (s); } From 0329635ed2df61be60fef56c81e348c733506227 Mon Sep 17 00:00:00 2001 From: reuk Date: Thu, 21 Nov 2024 21:26:29 +0000 Subject: [PATCH 61/76] RuntimePermissions: Allow requesting the POST_NOTIFICATIONS permission at runtime on Android --- modules/juce_core/misc/juce_RuntimePermissions.h | 8 +++++++- .../juce_core/native/juce_RuntimePermissions_android.cpp | 7 +++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/modules/juce_core/misc/juce_RuntimePermissions.h b/modules/juce_core/misc/juce_RuntimePermissions.h index 026e55778ef7..acd144f88b4c 100644 --- a/modules/juce_core/misc/juce_RuntimePermissions.h +++ b/modules/juce_core/misc/juce_RuntimePermissions.h @@ -113,7 +113,13 @@ class JUCE_API RuntimePermissions /** Permission to read video files that your app didn't create. Has the same effect as readExternalStorage on iOS and Android versions before 33. */ - readMediaVideo = 8 + readMediaVideo = 8, + + /** Permission to post notifications. + + @see PushNotifications::requestPermissionsWithSettings + */ + postNotification = 9 }; //============================================================================== diff --git a/modules/juce_core/native/juce_RuntimePermissions_android.cpp b/modules/juce_core/native/juce_RuntimePermissions_android.cpp index d66d817b80aa..867bd2129c9e 100644 --- a/modules/juce_core/native/juce_RuntimePermissions_android.cpp +++ b/modules/juce_core/native/juce_RuntimePermissions_android.cpp @@ -81,6 +81,9 @@ static StringArray jucePermissionToAndroidPermissions (RuntimePermissions::Permi case RuntimePermissions::readMediaVideo: return { externalStorageOrMedia ("android.permission.READ_MEDIA_VIDEO") }; + + case RuntimePermissions::postNotification: + return { "android.permission.POST_NOTIFICATIONS" }; } // invalid permission @@ -101,6 +104,7 @@ static RuntimePermissions::PermissionID androidPermissionToJucePermission (const { "android.permission.READ_MEDIA_IMAGES", RuntimePermissions::readMediaImages }, { "android.permission.READ_MEDIA_VIDEO", RuntimePermissions::readMediaVideo }, { "android.permission.BLUETOOTH_SCAN", RuntimePermissions::bluetoothMidi }, + { "android.permission.POST_NOTIFICATIONS", RuntimePermissions::postNotification }, }; const auto iter = map.find (permission); @@ -118,8 +122,7 @@ struct PermissionsRequest //============================================================================== struct PermissionsOverlay final : public FragmentOverlay { - PermissionsOverlay (CriticalSection& cs) : overlayGuard (cs) {} - ~PermissionsOverlay() override = default; + explicit PermissionsOverlay (CriticalSection& cs) : overlayGuard (cs) {} struct PermissionResult { From 98031a814cbab00997854dddf2630c00b064b232 Mon Sep 17 00:00:00 2001 From: reuk Date: Thu, 21 Nov 2024 21:57:40 +0000 Subject: [PATCH 62/76] PushNotifications: Remove unnecessary qualifications in Android impl --- .../native/juce_PushNotifications_android.cpp | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp b/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp index 1d77f035541e..e824c4e29613 100644 --- a/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp +++ b/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp @@ -296,7 +296,7 @@ struct PushNotifications::Pimpl } //============================================================================== - void sendLocalNotification (const PushNotifications::Notification& n) + void sendLocalNotification (const Notification& n) { // All required fields have to be setup! jassert (n.isValid()); @@ -322,7 +322,7 @@ struct PushNotifications::Pimpl { auto* env = getEnv(); - Array notifications; + Array notifications; auto notificationManager = getNotificationManager(); jassert (notificationManager != nullptr); @@ -601,7 +601,7 @@ struct PushNotifications::Pimpl javaString ("notification").get())); } - static LocalRef juceNotificationToJavaNotification (const PushNotifications::Notification& n) + static LocalRef juceNotificationToJavaNotification (const Notification& n) { auto* env = getEnv(); @@ -616,7 +616,7 @@ struct PushNotifications::Pimpl return LocalRef (env->CallObjectMethod (notificationBuilder, NotificationBuilderApi16.build)); } - static LocalRef createNotificationBuilder (const PushNotifications::Notification& n) + static LocalRef createNotificationBuilder (const Notification& n) { auto* env = getEnv(); LocalRef context (getMainActivity()); @@ -648,7 +648,7 @@ struct PushNotifications::Pimpl return LocalRef (env->NewObject (builderClass, builderConstructor, context.get())); } - static void setupRequiredFields (const PushNotifications::Notification& n, LocalRef& notificationBuilder) + static void setupRequiredFields (const Notification& n, LocalRef& notificationBuilder) { auto* env = getEnv(); LocalRef context (getMainActivity()); @@ -696,7 +696,7 @@ struct PushNotifications::Pimpl } } - static LocalRef juceNotificationToBundle (const PushNotifications::Notification& n) + static LocalRef juceNotificationToBundle (const Notification& n) { auto* env = getEnv(); @@ -753,7 +753,7 @@ struct PushNotifications::Pimpl return bundle; } - static void setupOptionalFields (const PushNotifications::Notification& n, LocalRef& notificationBuilder) + static void setupOptionalFields (const Notification& n, LocalRef& notificationBuilder) { auto* env = getEnv(); @@ -818,11 +818,11 @@ struct PushNotifications::Pimpl if (getAndroidSDKVersion() < 24) { - const bool useChronometer = n.timestampVisibility == PushNotifications::Notification::chronometer; + const bool useChronometer = n.timestampVisibility == Notification::chronometer; env->CallObjectMethod (notificationBuilder, NotificationBuilderApi16.setUsesChronometer, useChronometer); } - const bool showTimeStamp = n.timestampVisibility != PushNotifications::Notification::off; + const bool showTimeStamp = n.timestampVisibility != Notification::off; env->CallObjectMethod (notificationBuilder, NotificationBuilderApi17.setShowWhen, showTimeStamp); if (n.groupId.isNotEmpty()) @@ -857,8 +857,8 @@ struct PushNotifications::Pimpl if (getAndroidSDKVersion() >= 24) { - const bool useChronometer = n.timestampVisibility == PushNotifications::Notification::chronometer; - const bool useCountDownChronometer = n.timestampVisibility == PushNotifications::Notification::countDownChronometer; + const bool useChronometer = n.timestampVisibility == Notification::chronometer; + const bool useCountDownChronometer = n.timestampVisibility == Notification::countDownChronometer; env->CallObjectMethod (notificationBuilder, NotificationBuilderApi24.setChronometerCountDown, useCountDownChronometer); env->CallObjectMethod (notificationBuilder, NotificationBuilderApi16.setUsesChronometer, useChronometer | useCountDownChronometer); @@ -874,7 +874,7 @@ struct PushNotifications::Pimpl setupNotificationDeletedCallback (n, notificationBuilder); } - static void setupNotificationDeletedCallback (const PushNotifications::Notification& n, + static void setupNotificationDeletedCallback (const Notification& n, LocalRef& notificationBuilder) { auto* env = getEnv(); @@ -900,7 +900,7 @@ struct PushNotifications::Pimpl env->CallObjectMethod (notificationBuilder, NotificationBuilderBase.setDeleteIntent, deletePendingIntent.get()); } - static void setupActions (const PushNotifications::Notification& n, LocalRef& notificationBuilder) + static void setupActions (const Notification& n, LocalRef& notificationBuilder) { auto* env = getEnv(); LocalRef context (getMainActivity()); @@ -912,7 +912,7 @@ struct PushNotifications::Pimpl auto activityClass = LocalRef (env->CallObjectMethod (context.get(), JavaObject.getClass)); auto notifyIntent = LocalRef (env->NewObject (AndroidIntent, AndroidIntent.constructorWithContextAndClass, context.get(), activityClass.get())); - const bool isTextStyle = action.style == PushNotifications::Notification::Action::text; + const bool isTextStyle = action.style == Notification::Action::text; auto packageNameString = LocalRef ((jstring) (env->CallObjectMethod (context.get(), AndroidContext.getPackageName))); const String notificationActionString = isTextStyle ? ".JUCE_NOTIFICATION_TEXT_INPUT_ACTION." : ".JUCE_NOTIFICATION_BUTTON_ACTION."; @@ -1029,26 +1029,26 @@ struct PushNotifications::Pimpl return bitmap; } - static String typeToCategory (PushNotifications::Notification::Type t) + static String typeToCategory (Notification::Type t) { switch (t) { - case PushNotifications::Notification::unspecified: return {}; - case PushNotifications::Notification::alarm: return "alarm"; - case PushNotifications::Notification::call: return "call"; - case PushNotifications::Notification::email: return "email"; - case PushNotifications::Notification::error: return "err"; - case PushNotifications::Notification::event: return "event"; - case PushNotifications::Notification::message: return "msg"; - case PushNotifications::Notification::taskProgress: return "progress"; - case PushNotifications::Notification::promo: return "promo"; - case PushNotifications::Notification::recommendation: return "recommendation"; - case PushNotifications::Notification::reminder: return "reminder"; - case PushNotifications::Notification::service: return "service"; - case PushNotifications::Notification::social: return "social"; - case PushNotifications::Notification::status: return "status"; - case PushNotifications::Notification::system: return "sys"; - case PushNotifications::Notification::transport: return "transport"; + case Notification::unspecified: return {}; + case Notification::alarm: return "alarm"; + case Notification::call: return "call"; + case Notification::email: return "email"; + case Notification::error: return "err"; + case Notification::event: return "event"; + case Notification::message: return "msg"; + case Notification::taskProgress: return "progress"; + case Notification::promo: return "promo"; + case Notification::recommendation: return "recommendation"; + case Notification::reminder: return "reminder"; + case Notification::service: return "service"; + case Notification::social: return "social"; + case Notification::status: return "status"; + case Notification::system: return "sys"; + case Notification::transport: return "transport"; } return {}; @@ -1081,11 +1081,11 @@ struct PushNotifications::Pimpl } // Reverse of juceNotificationToBundle(). - static PushNotifications::Notification localNotificationBundleToJuceNotification (const LocalRef& bundle) + static Notification localNotificationBundleToJuceNotification (const LocalRef& bundle) { auto* env = getEnv(); - PushNotifications::Notification n; + Notification n; if (bundle.get() != nullptr) { @@ -1100,23 +1100,23 @@ struct PushNotifications::Pimpl n.icon = getStringFromBundle (env, "icon", bundle); n.channelId = getStringFromBundle (env, "channelId", bundle); - PushNotifications::Notification::Progress progress; + Notification::Progress progress; progress.max = getIntFromBundle (env, "progressMax", bundle); progress.current = getIntFromBundle (env, "progressCurrent", bundle); progress.indeterminate = getBoolFromBundle (env, "progressIndeterminate", bundle); n.progress = progress; n.person = getStringFromBundle (env, "person", bundle); - n.type = (PushNotifications::Notification::Type) getIntFromBundle (env, "type", bundle); - n.priority = (PushNotifications::Notification::Priority) getIntFromBundle (env, "priority", bundle); - n.lockScreenAppearance = (PushNotifications::Notification::LockScreenAppearance) getIntFromBundle (env, "lockScreenAppearance", bundle); + n.type = (Notification::Type) getIntFromBundle (env, "type", bundle); + n.priority = (Notification::Priority) getIntFromBundle (env, "priority", bundle); + n.lockScreenAppearance = (Notification::LockScreenAppearance) getIntFromBundle (env, "lockScreenAppearance", bundle); n.groupId = getStringFromBundle (env, "groupId", bundle); n.groupSortKey = getStringFromBundle (env, "groupSortKey", bundle); n.groupSummary = getBoolFromBundle (env, "groupSummary", bundle); n.accentColour = Colour ((uint32) getIntFromBundle (env, "accentColour", bundle)); n.ledColour = Colour ((uint32) getIntFromBundle (env, "ledColour", bundle)); - PushNotifications::Notification::LedBlinkPattern ledBlinkPattern; + Notification::LedBlinkPattern ledBlinkPattern; ledBlinkPattern.msToBeOn = getIntFromBundle (env, "ledBlinkPatternMsToBeOn", bundle); ledBlinkPattern.msToBeOff = getIntFromBundle (env, "ledBlinkPatternMsToBeOff", bundle); n.ledBlinkPattern = ledBlinkPattern; @@ -1127,9 +1127,9 @@ struct PushNotifications::Pimpl n.localOnly = getBoolFromBundle (env, "localOnly", bundle); n.ongoing = getBoolFromBundle (env, "ongoing", bundle); n.alertOnlyOnce = getBoolFromBundle (env, "alertOnlyOnce", bundle); - n.timestampVisibility = (PushNotifications::Notification::TimestampVisibility) getIntFromBundle (env, "timestampVisibility", bundle); - n.badgeIconType = (PushNotifications::Notification::BadgeIconType) getIntFromBundle (env, "badgeIconType", bundle); - n.groupAlertBehaviour = (PushNotifications::Notification::GroupAlertBehaviour) getIntFromBundle (env, "groupAlertBehaviour", bundle); + n.timestampVisibility = (Notification::TimestampVisibility) getIntFromBundle (env, "timestampVisibility", bundle); + n.badgeIconType = (Notification::BadgeIconType) getIntFromBundle (env, "badgeIconType", bundle); + n.groupAlertBehaviour = (Notification::GroupAlertBehaviour) getIntFromBundle (env, "groupAlertBehaviour", bundle); n.timeoutAfterMs = getLongFromBundle (env, "timeoutAfterMs", bundle); } @@ -1216,7 +1216,7 @@ struct PushNotifications::Pimpl return {}; } - static PushNotifications::Notification javaNotificationToJuceNotification (const LocalRef& notification) + static Notification javaNotificationToJuceNotification (const LocalRef& notification) { auto* env = getEnv(); @@ -1230,10 +1230,10 @@ struct PushNotifications::Pimpl return remoteNotificationBundleToJuceNotification (extras); } - static PushNotifications::Notification remoteNotificationBundleToJuceNotification (const LocalRef& bundle) + static Notification remoteNotificationBundleToJuceNotification (const LocalRef& bundle) { // This will probably work only for remote notifications that get delivered to system tray - PushNotifications::Notification n; + Notification n; n.properties = bundleToVar (bundle); return n; @@ -1282,7 +1282,7 @@ struct PushNotifications::Pimpl } #if defined (JUCE_FIREBASE_MESSAGING_SERVICE_CLASSNAME) - static PushNotifications::Notification firebaseRemoteNotificationToJuceNotification (jobject remoteNotification) + static Notification firebaseRemoteNotificationToJuceNotification (jobject remoteNotification) { auto* env = getEnv(); @@ -1325,7 +1325,7 @@ struct PushNotifications::Pimpl propertiesDynamicObject->setProperty ("ttl", ttl); propertiesDynamicObject->setProperty ("data", dataVar); - PushNotifications::Notification n; + Notification n; if (notification != 0) { From ed0092a8bc9685830c8ce87347b8eae13558e9be Mon Sep 17 00:00:00 2001 From: reuk Date: Thu, 21 Nov 2024 22:06:42 +0000 Subject: [PATCH 63/76] PushNotifications: Assert instead of crashing if Android notification icon cannot be located --- .../native/juce_PushNotifications_android.cpp | 92 +++++++++++++------ 1 file changed, 64 insertions(+), 28 deletions(-) diff --git a/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp b/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp index e824c4e29613..fcbefabd1dfc 100644 --- a/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp +++ b/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp @@ -276,7 +276,7 @@ bool PushNotifications::Notification::isValid() const noexcept //============================================================================== struct PushNotifications::Pimpl { - Pimpl (PushNotifications& p) + explicit Pimpl (PushNotifications& p) : owner (p) {} @@ -303,16 +303,18 @@ struct PushNotifications::Pimpl auto* env = getEnv(); - auto notificationManager = getNotificationManager(); - - if (notificationManager.get() != nullptr) + if (auto notificationManager = getNotificationManager()) { - auto notification = juceNotificationToJavaNotification (n); - - auto tag = javaString (n.identifier); - const int id = 0; + if (auto notification = juceNotificationToJavaNotification (n)) + { + auto tag = javaString (n.identifier); + const int id = 0; - env->CallVoidMethod (notificationManager.get(), NotificationManagerBase.notify, tag.get(), id, notification.get()); + env->CallVoidMethod (notificationManager.get(), + NotificationManagerBase.notify, + tag.get(), + id, notification.get()); + } } } @@ -607,11 +609,12 @@ struct PushNotifications::Pimpl auto notificationBuilder = createNotificationBuilder (n); - setupRequiredFields (n, notificationBuilder); - setupOptionalFields (n, notificationBuilder); + notificationBuilder = setupRequiredFields (n, notificationBuilder); + notificationBuilder = setupOptionalFields (n, notificationBuilder); + notificationBuilder = setupActions (n, notificationBuilder); - if (n.actions.size() > 0) - setupActions (n, notificationBuilder); + if (notificationBuilder == nullptr) + return notificationBuilder; return LocalRef (env->CallObjectMethod (notificationBuilder, NotificationBuilderApi16.build)); } @@ -648,8 +651,11 @@ struct PushNotifications::Pimpl return LocalRef (env->NewObject (builderClass, builderConstructor, context.get())); } - static void setupRequiredFields (const Notification& n, LocalRef& notificationBuilder) + static LocalRef setupRequiredFields (const Notification& n, LocalRef notificationBuilder) { + if (notificationBuilder == nullptr) + return notificationBuilder; + auto* env = getEnv(); LocalRef context (getMainActivity()); @@ -676,8 +682,16 @@ struct PushNotifications::Pimpl env->CallObjectMethod (notificationBuilder, NotificationBuilderBase.setContentIntent, notifyPendingIntent.get()); auto resources = LocalRef (env->CallObjectMethod (context.get(), AndroidContext.getResources)); - const int iconId = env->CallIntMethod (resources, AndroidResources.getIdentifier, javaString (n.icon).get(), - javaString ("raw").get(), packageNameString.get()); + const auto iconId = env->CallIntMethod (resources, AndroidResources.getIdentifier, javaString (n.icon).get(), + javaString ("raw").get(), packageNameString.get()); + + if (iconId == 0) + { + // If you hit this, the notification icon could not be located, and the notification + // will not be sent. + jassertfalse; + return {}; + } env->CallObjectMethod (notificationBuilder, NotificationBuilderBase.setSmallIcon, iconId); @@ -688,12 +702,17 @@ struct PushNotifications::Pimpl auto publicNotificationBuilder = createNotificationBuilder (n); - setupRequiredFields (*n.publicVersion, publicNotificationBuilder); - setupOptionalFields (*n.publicVersion, publicNotificationBuilder); + publicNotificationBuilder = setupRequiredFields (*n.publicVersion, publicNotificationBuilder); + publicNotificationBuilder = setupOptionalFields (*n.publicVersion, publicNotificationBuilder); + + if (publicNotificationBuilder == nullptr) + return {}; auto publicVersion = LocalRef (env->CallObjectMethod (publicNotificationBuilder, NotificationBuilderApi16.build)); env->CallObjectMethod (notificationBuilder, NotificationBuilderApi21.setPublicVersion, publicVersion.get()); } + + return notificationBuilder; } static LocalRef juceNotificationToBundle (const Notification& n) @@ -753,8 +772,11 @@ struct PushNotifications::Pimpl return bundle; } - static void setupOptionalFields (const Notification& n, LocalRef& notificationBuilder) + static LocalRef setupOptionalFields (const Notification n, LocalRef& notificationBuilder) { + if (notificationBuilder == nullptr) + return notificationBuilder; + auto* env = getEnv(); if (n.subtitle.isNotEmpty()) @@ -871,12 +893,15 @@ struct PushNotifications::Pimpl env->CallObjectMethod (notificationBuilder, NotificationBuilderApi26.setTimeoutAfter, (jlong) n.timeoutAfterMs); } - setupNotificationDeletedCallback (n, notificationBuilder); + return setupNotificationDeletedCallback (n, notificationBuilder); } - static void setupNotificationDeletedCallback (const Notification& n, - LocalRef& notificationBuilder) + static LocalRef setupNotificationDeletedCallback (const Notification& n, + LocalRef notificationBuilder) { + if (notificationBuilder == nullptr) + return notificationBuilder; + auto* env = getEnv(); LocalRef context (getMainActivity()); @@ -898,16 +923,19 @@ struct PushNotifications::Pimpl 0)); env->CallObjectMethod (notificationBuilder, NotificationBuilderBase.setDeleteIntent, deletePendingIntent.get()); + + return notificationBuilder; } - static void setupActions (const Notification& n, LocalRef& notificationBuilder) + static LocalRef setupActions (const Notification& n, LocalRef notificationBuilder) { + if (notificationBuilder == nullptr || n.actions.isEmpty()) + return notificationBuilder; + auto* env = getEnv(); LocalRef context (getMainActivity()); - int actionIndex = 0; - - for (const auto& action : n.actions) + for (const auto [actionIndex, action] : enumerate (n.actions)) { auto activityClass = LocalRef (env->CallObjectMethod (context.get(), JavaObject.getClass)); auto notifyIntent = LocalRef (env->NewObject (AndroidIntent, AndroidIntent.constructorWithContextAndClass, context.get(), activityClass.get())); @@ -938,6 +966,14 @@ struct PushNotifications::Pimpl iconId = env->CallIntMethod (resources, AndroidResources.getIdentifier, javaString (n.icon).get(), javaString ("raw").get(), packageNameString.get()); + if (iconId == 0) + { + // If this is hit, the notification icon could not be located, so the notification + // cannot be displayed. + jassertfalse; + return {}; + } + auto actionBuilder = LocalRef (env->NewObject (NotificationActionBuilder, NotificationActionBuilder.constructor, iconId, @@ -982,9 +1018,9 @@ struct PushNotifications::Pimpl env->CallObjectMethod (notificationBuilder, NotificationBuilderApi20.addAction, env->CallObjectMethod (actionBuilder, NotificationActionBuilder.build)); - - ++actionIndex; } + + return notificationBuilder; } static LocalRef juceUrlToAndroidUri (const URL& url) From 6d10eb536fc10da8981b25aa868c1714a6431453 Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 25 Nov 2024 13:31:51 +0000 Subject: [PATCH 64/76] PushNotifications: Update Android implementation to properly request permissions --- .../native/juce_PushNotifications_android.cpp | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp b/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp index fcbefabd1dfc..46c50123c884 100644 --- a/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp +++ b/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp @@ -282,6 +282,9 @@ struct PushNotifications::Pimpl bool areNotificationsEnabled() const { + if (getAndroidSDKVersion() >= 33 && ! RuntimePermissions::isGranted (RuntimePermissions::postNotification)) + return false; + if (getAndroidSDKVersion() >= 24) { auto* env = getEnv(); @@ -296,6 +299,28 @@ struct PushNotifications::Pimpl } //============================================================================== + void requestPermissionsWithSettings (const Settings&) + { + RuntimePermissions::request (RuntimePermissions::postNotification, [&] (bool) + { + const auto notifyListeners = [] + { + if (auto* instance = PushNotifications::getInstance()) + instance->listeners.call ([] (Listener& l) { l.notificationSettingsReceived ({}); }); + }; + + if (MessageManager::getInstance()->isThisTheMessageThread()) + notifyListeners(); + else + MessageManager::callAsync (notifyListeners); + }); + } + + void requestSettingsUsed() + { + owner.listeners.call ([] (Listener& l) { l.notificationSettingsReceived ({}); }); + } + void sendLocalNotification (const Notification& n) { // All required fields have to be setup! @@ -651,6 +676,8 @@ struct PushNotifications::Pimpl return LocalRef (env->NewObject (builderClass, builderConstructor, context.get())); } + static constexpr auto FLAG_IMMUTABLE = 0x04000000; + static LocalRef setupRequiredFields (const Notification& n, LocalRef notificationBuilder) { if (notificationBuilder == nullptr) @@ -675,7 +702,7 @@ struct PushNotifications::Pimpl context.get(), 1002, notifyIntent.get(), - 0)); + FLAG_IMMUTABLE)); env->CallObjectMethod (notificationBuilder, NotificationBuilderBase.setContentTitle, javaString (n.title).get()); env->CallObjectMethod (notificationBuilder, NotificationBuilderBase.setContentText, javaString (n.body).get()); @@ -920,7 +947,7 @@ struct PushNotifications::Pimpl context.get(), 1002, deleteIntent.get(), - 0)); + FLAG_IMMUTABLE)); env->CallObjectMethod (notificationBuilder, NotificationBuilderBase.setDeleteIntent, deletePendingIntent.get()); @@ -956,7 +983,7 @@ struct PushNotifications::Pimpl context.get(), 1002, notifyIntent.get(), - 0)); + FLAG_IMMUTABLE)); auto resources = LocalRef (env->CallObjectMethod (context.get(), AndroidContext.getResources)); int iconId = env->CallIntMethod (resources, AndroidResources.getIdentifier, javaString (action.icon).get(), From 269ebbb52568e8cab700d0f3a16a479721dfb3bb Mon Sep 17 00:00:00 2001 From: reuk Date: Thu, 21 Nov 2024 21:26:55 +0000 Subject: [PATCH 65/76] Accessibility: Add AccessibilityHandler::postSystemNotification() function for posting an OS-specific accessible notification --- .../juce_AccessibilityHandler.cpp | 16 +- .../accessibility/juce_AccessibilityHandler.h | 27 ++- .../juce_Accessibility_android.cpp | 5 + .../accessibility/juce_Accessibility_ios.mm | 5 + .../accessibility/juce_Accessibility_mac.mm | 5 + .../juce_Accessibility_windows.cpp | 144 +++++++------ .../misc/juce_PushNotifications.cpp | 200 +++++++++++++----- .../misc/juce_PushNotifications.h | 12 +- .../native/juce_PushNotifications_android.cpp | 25 ++- .../native/juce_PushNotifications_ios.cpp | 12 +- .../native/juce_PushNotifications_mac.cpp | 6 +- 11 files changed, 308 insertions(+), 149 deletions(-) diff --git a/modules/juce_gui_basics/accessibility/juce_AccessibilityHandler.cpp b/modules/juce_gui_basics/accessibility/juce_AccessibilityHandler.cpp index a6ddb49633e9..3e6b1af91b6a 100644 --- a/modules/juce_gui_basics/accessibility/juce_AccessibilityHandler.cpp +++ b/modules/juce_gui_basics/accessibility/juce_AccessibilityHandler.cpp @@ -35,8 +35,6 @@ namespace juce { -AccessibilityHandler* AccessibilityHandler::currentlyFocusedHandler = nullptr; - class NativeChildHandler { public: @@ -403,10 +401,24 @@ void AccessibilityHandler::setNativeChildForComponent (Component& component, voi NativeChildHandler::getInstance().setNativeChild (component, nativeChild); } +#if JUCE_MODULE_AVAILABLE_juce_gui_extra +void privatePostSystemNotification (const String&, const String&); +#endif + +void AccessibilityHandler::postSystemNotification ([[maybe_unused]] const String& notificationTitle, + [[maybe_unused]] const String& notificationBody) +{ + #if JUCE_MODULE_AVAILABLE_juce_gui_extra + if (areAnyAccessibilityClientsActive()) + privatePostSystemNotification (notificationTitle, notificationBody); + #endif +} + #if ! JUCE_NATIVE_ACCESSIBILITY_INCLUDED void AccessibilityHandler::notifyAccessibilityEvent (AccessibilityEvent) const {} void AccessibilityHandler::postAnnouncement (const String&, AnnouncementPriority) {} AccessibilityNativeHandle* AccessibilityHandler::getNativeImplementation() const { return nullptr; } + bool AccessibilityHandler::areAnyAccessibilityClientsActive() { return false; } #endif } // namespace juce diff --git a/modules/juce_gui_basics/accessibility/juce_AccessibilityHandler.h b/modules/juce_gui_basics/accessibility/juce_AccessibilityHandler.h index da8b3f3af4f1..540799839b31 100644 --- a/modules/juce_gui_basics/accessibility/juce_AccessibilityHandler.h +++ b/modules/juce_gui_basics/accessibility/juce_AccessibilityHandler.h @@ -295,6 +295,30 @@ class JUCE_API AccessibilityHandler */ static void postAnnouncement (const String& announcementString, AnnouncementPriority priority); + /** Posts a local system notification. + + In order for this to do anything, the following conditions must be met. + - At build time: + - The juce_gui_extra module must be included in the project. + - Push notifications must be enabled by setting the preprocessor definition + JUCE_PUSH_NOTIFICATIONS=1 + - At run time: + - An accessibility client (narrator, voiceover etc.) must be active. + + Additionally, on Android, an icon is required for notifications. + This must be specified by adding the path to the icon file called + "accessibilitynotificationicon" in the "Extra Android Raw Resources" setting + in the Projucer. + + This will use the push notification client on macOS, iOS and Android. + On Windows this will create a system tray icon to post the notification. + + @param notificationTitle the title of the notification + @param notificationBody the main body text of the notification + */ + static void postSystemNotification (const String& notificationTitle, + const String& notificationBody); + //============================================================================== /** @internal */ AccessibilityNativeHandle* getNativeImplementation() const; @@ -329,8 +353,9 @@ class JUCE_API AccessibilityHandler void grabFocusInternal (bool); void giveAwayFocusInternal() const; void takeFocus(); + static bool areAnyAccessibilityClientsActive(); - static AccessibilityHandler* currentlyFocusedHandler; + static inline AccessibilityHandler* currentlyFocusedHandler = nullptr; //============================================================================== Component& component; diff --git a/modules/juce_gui_basics/native/accessibility/juce_Accessibility_android.cpp b/modules/juce_gui_basics/native/accessibility/juce_Accessibility_android.cpp index 4ab7bd9db89d..418eba3108d6 100644 --- a/modules/juce_gui_basics/native/accessibility/juce_Accessibility_android.cpp +++ b/modules/juce_gui_basics/native/accessibility/juce_Accessibility_android.cpp @@ -1055,4 +1055,9 @@ void AccessibilityHandler::postAnnouncement (const String& announcementString, javaString (announcementString).get()); } +bool AccessibilityHandler::areAnyAccessibilityClientsActive() +{ + return AccessibilityNativeHandle::areAnyAccessibilityClientsActive(); +} + } // namespace juce diff --git a/modules/juce_gui_basics/native/accessibility/juce_Accessibility_ios.mm b/modules/juce_gui_basics/native/accessibility/juce_Accessibility_ios.mm index 21538ae77732..5c4643b49968 100644 --- a/modules/juce_gui_basics/native/accessibility/juce_Accessibility_ios.mm +++ b/modules/juce_gui_basics/native/accessibility/juce_Accessibility_ios.mm @@ -666,4 +666,9 @@ static void sendAccessibilityEvent (UIAccessibilityNotifications notification, i sendAccessibilityEvent (UIAccessibilityAnnouncementNotification, juceStringToNS (announcementString)); } +bool AccessibilityHandler::areAnyAccessibilityClientsActive() +{ + return juce::areAnyAccessibilityClientsActive(); +} + } // namespace juce diff --git a/modules/juce_gui_basics/native/accessibility/juce_Accessibility_mac.mm b/modules/juce_gui_basics/native/accessibility/juce_Accessibility_mac.mm index 48647846ad87..a6001b00e708 100644 --- a/modules/juce_gui_basics/native/accessibility/juce_Accessibility_mac.mm +++ b/modules/juce_gui_basics/native/accessibility/juce_Accessibility_mac.mm @@ -939,4 +939,9 @@ static void sendHandlerNotification (const AccessibilityHandler& handler, NSAccessibilityPriorityKey: @(nsPriority) }); } +bool AccessibilityHandler::areAnyAccessibilityClientsActive() +{ + return juce::areAnyAccessibilityClientsActive(); +} + } // namespace juce diff --git a/modules/juce_gui_basics/native/accessibility/juce_Accessibility_windows.cpp b/modules/juce_gui_basics/native/accessibility/juce_Accessibility_windows.cpp index 0c98af66dd96..9fa451fb4e5a 100644 --- a/modules/juce_gui_basics/native/accessibility/juce_Accessibility_windows.cpp +++ b/modules/juce_gui_basics/native/accessibility/juce_Accessibility_windows.cpp @@ -39,26 +39,83 @@ namespace juce JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token") -static bool isStartingUpOrShuttingDown() +//============================================================================== +struct WindowsAccessibility { - if (auto* app = JUCEApplicationBase::getInstance()) - if (app->isInitialising()) - return true; + WindowsAccessibility() = delete; + + static long getUiaRootObjectId() + { + return static_cast (UiaRootObjectId); + } + + static bool handleWmGetObject (AccessibilityHandler* handler, WPARAM wParam, LPARAM lParam, LRESULT* res) + { + if (isStartingUpOrShuttingDown() || (handler == nullptr || ! isHandlerValid (*handler))) + return false; + + if (auto* uiaWrapper = WindowsUIAWrapper::getInstance()) + { + ComSmartPtr provider; + handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (provider.resetAndGetPointerAddress())); + + if (! uiaWrapper->isProviderDisconnecting (provider)) + *res = uiaWrapper->returnRawElementProvider ((HWND) handler->getComponent().getWindowHandle(), wParam, lParam, provider); - if (auto* mm = MessageManager::getInstanceWithoutCreating()) - if (mm->hasStopMessageBeenSent()) return true; + } - return false; -} + return false; + } -static bool isHandlerValid (const AccessibilityHandler& handler) -{ - if (auto* provider = handler.getNativeImplementation()) - return provider->isElementValid(); + static void revokeUIAMapEntriesForWindow (HWND hwnd) + { + if (auto* uiaWrapper = WindowsUIAWrapper::getInstanceWithoutCreating()) + uiaWrapper->returnRawElementProvider (hwnd, 0, 0, nullptr); + } - return false; -} + static bool isStartingUpOrShuttingDown() + { + if (auto* app = JUCEApplicationBase::getInstance()) + if (app->isInitialising()) + return true; + + if (auto* mm = MessageManager::getInstanceWithoutCreating()) + if (mm->hasStopMessageBeenSent()) + return true; + + return false; + } + + static bool isHandlerValid (const AccessibilityHandler& handler) + { + if (auto* provider = handler.getNativeImplementation()) + return provider->isElementValid(); + + return false; + } + + static bool areAnyAccessibilityClientsActive() + { + const auto areClientsListening = [] + { + if (auto* uiaWrapper = WindowsUIAWrapper::getInstanceWithoutCreating()) + return uiaWrapper->clientsAreListening() != 0; + + return false; + }; + + const auto isScreenReaderRunning = [] + { + BOOL isRunning = FALSE; + SystemParametersInfo (SPI_GETSCREENREADER, 0, (PVOID) &isRunning, 0); + + return isRunning != 0; + }; + + return areClientsListening() || isScreenReaderRunning(); + } +}; //============================================================================== class AccessibilityHandler::AccessibilityNativeImpl @@ -103,31 +160,12 @@ AccessibilityNativeHandle* AccessibilityHandler::getNativeImplementation() const return nativeImpl->accessibilityElement; } -static bool areAnyAccessibilityClientsActive() -{ - const auto areClientsListening = [] - { - if (auto* uiaWrapper = WindowsUIAWrapper::getInstanceWithoutCreating()) - return uiaWrapper->clientsAreListening() != 0; - - return false; - }; - - const auto isScreenReaderRunning = [] - { - BOOL isRunning = FALSE; - SystemParametersInfo (SPI_GETSCREENREADER, 0, (PVOID) &isRunning, 0); - - return isRunning != 0; - }; - - return areClientsListening() || isScreenReaderRunning(); -} - template void getProviderWithCheckedWrapper (const AccessibilityHandler& handler, Callback&& callback) { - if (! areAnyAccessibilityClientsActive() || isStartingUpOrShuttingDown() || ! isHandlerValid (handler)) + if (! WindowsAccessibility::areAnyAccessibilityClientsActive() + || WindowsAccessibility::isStartingUpOrShuttingDown() + || ! WindowsAccessibility::isHandlerValid (handler)) return; if (auto* uiaWrapper = WindowsUIAWrapper::getInstanceWithoutCreating()) @@ -292,41 +330,11 @@ void AccessibilityHandler::postAnnouncement (const String& announcementString, A } } -//============================================================================== -namespace WindowsAccessibility +bool AccessibilityHandler::areAnyAccessibilityClientsActive() { - static long getUiaRootObjectId() - { - return static_cast (UiaRootObjectId); - } - - static bool handleWmGetObject (AccessibilityHandler* handler, WPARAM wParam, LPARAM lParam, LRESULT* res) - { - if (isStartingUpOrShuttingDown() || (handler == nullptr || ! isHandlerValid (*handler))) - return false; - - if (auto* uiaWrapper = WindowsUIAWrapper::getInstance()) - { - ComSmartPtr provider; - handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (provider.resetAndGetPointerAddress())); - - if (! uiaWrapper->isProviderDisconnecting (provider)) - *res = uiaWrapper->returnRawElementProvider ((HWND) handler->getComponent().getWindowHandle(), wParam, lParam, provider); - - return true; - } - - return false; - } - - static void revokeUIAMapEntriesForWindow (HWND hwnd) - { - if (auto* uiaWrapper = WindowsUIAWrapper::getInstanceWithoutCreating()) - uiaWrapper->returnRawElementProvider (hwnd, 0, 0, nullptr); - } + return WindowsAccessibility::areAnyAccessibilityClientsActive(); } - JUCE_END_IGNORE_WARNINGS_GCC_LIKE } // namespace juce diff --git a/modules/juce_gui_extra/misc/juce_PushNotifications.cpp b/modules/juce_gui_extra/misc/juce_PushNotifications.cpp index 4b6bca78be0d..c6d4ee426e15 100644 --- a/modules/juce_gui_extra/misc/juce_PushNotifications.cpp +++ b/modules/juce_gui_extra/misc/juce_PushNotifications.cpp @@ -36,10 +36,50 @@ namespace juce { //============================================================================== -#if ! JUCE_ANDROID && ! JUCE_IOS && ! JUCE_MAC +#if ! JUCE_PUSH_NOTIFICATIONS_IMPL + +struct PushNotifications::Impl +{ + explicit Impl (PushNotifications& o) : owner (o) {} + + void requestPermissionsWithSettings (const Settings&) const + { + owner.listeners.call ([] (Listener& l) { l.notificationSettingsReceived ({}); }); + } + + void requestSettingsUsed() const + { + owner.listeners.call ([] (Listener& l) { l.notificationSettingsReceived ({}); }); + } + + bool areNotificationsEnabled() const { return false; } + void getDeliveredNotifications() const {} + void removeAllDeliveredNotifications() const {} + String getDeviceToken() const { return {}; } + void setupChannels (const Array&, const Array&) const {} + void getPendingLocalNotifications() const {} + void removeAllPendingLocalNotifications() const {} + void subscribeToTopic (const String&) const {} + void unsubscribeFromTopic (const String&) const {} + void sendLocalNotification (const Notification&) const {} + void removeDeliveredNotification (const String&) const {} + void removePendingLocalNotification (const String&) const {} + void sendUpstreamMessage (const String&, + const String&, + const String&, + const String&, + int, + const StringPairArray&) const {} + +private: + PushNotifications& owner; +}; + bool PushNotifications::Notification::isValid() const noexcept { return true; } + #endif +//============================================================================== PushNotifications::Notification::Notification (const Notification& other) : identifier (other.identifier), title (other.title), @@ -82,9 +122,7 @@ PushNotifications::Notification::Notification (const Notification& other) //============================================================================== PushNotifications::PushNotifications() - #if JUCE_PUSH_NOTIFICATIONS - : pimpl (new Pimpl (*this)) - #endif + : pimpl (new Impl (*this)) { } @@ -93,128 +131,90 @@ PushNotifications::~PushNotifications() { clearSingletonInstance(); } void PushNotifications::addListener (Listener* l) { listeners.add (l); } void PushNotifications::removeListener (Listener* l) { listeners.remove (l); } -void PushNotifications::requestPermissionsWithSettings ([[maybe_unused]] const PushNotifications::Settings& settings) +void PushNotifications::requestPermissionsWithSettings (const Settings& settings) { - #if JUCE_PUSH_NOTIFICATIONS && (JUCE_IOS || JUCE_MAC) pimpl->requestPermissionsWithSettings (settings); - #else - listeners.call ([] (Listener& l) { l.notificationSettingsReceived ({}); }); - #endif } void PushNotifications::requestSettingsUsed() { - #if JUCE_PUSH_NOTIFICATIONS && (JUCE_IOS || JUCE_MAC) pimpl->requestSettingsUsed(); - #else - listeners.call ([] (Listener& l) { l.notificationSettingsReceived ({}); }); - #endif } bool PushNotifications::areNotificationsEnabled() const { - #if JUCE_PUSH_NOTIFICATIONS return pimpl->areNotificationsEnabled(); - #else - return false; - #endif } void PushNotifications::getDeliveredNotifications() const { - #if JUCE_PUSH_NOTIFICATIONS pimpl->getDeliveredNotifications(); - #endif } void PushNotifications::removeAllDeliveredNotifications() { - #if JUCE_PUSH_NOTIFICATIONS pimpl->removeAllDeliveredNotifications(); - #endif } String PushNotifications::getDeviceToken() const { - #if JUCE_PUSH_NOTIFICATIONS return pimpl->getDeviceToken(); - #else - return {}; - #endif } -void PushNotifications::setupChannels ([[maybe_unused]] const Array& groups, [[maybe_unused]] const Array& channels) +void PushNotifications::setupChannels (const Array& groups, + const Array& channels) { - #if JUCE_PUSH_NOTIFICATIONS pimpl->setupChannels (groups, channels); - #endif } void PushNotifications::getPendingLocalNotifications() const { - #if JUCE_PUSH_NOTIFICATIONS pimpl->getPendingLocalNotifications(); - #endif } void PushNotifications::removeAllPendingLocalNotifications() { - #if JUCE_PUSH_NOTIFICATIONS pimpl->removeAllPendingLocalNotifications(); - #endif } -void PushNotifications::subscribeToTopic ([[maybe_unused]] const String& topic) +void PushNotifications::subscribeToTopic (const String& topic) { - #if JUCE_PUSH_NOTIFICATIONS pimpl->subscribeToTopic (topic); - #endif } -void PushNotifications::unsubscribeFromTopic ([[maybe_unused]] const String& topic) +void PushNotifications::unsubscribeFromTopic (const String& topic) { - #if JUCE_PUSH_NOTIFICATIONS pimpl->unsubscribeFromTopic (topic); - #endif } - -void PushNotifications::sendLocalNotification ([[maybe_unused]] const Notification& n) +void PushNotifications::sendLocalNotification (const Notification& n) { - #if JUCE_PUSH_NOTIFICATIONS pimpl->sendLocalNotification (n); - #endif } -void PushNotifications::removeDeliveredNotification ([[maybe_unused]] const String& identifier) +void PushNotifications::removeDeliveredNotification (const String& identifier) { - #if JUCE_PUSH_NOTIFICATIONS pimpl->removeDeliveredNotification (identifier); - #endif } -void PushNotifications::removePendingLocalNotification ([[maybe_unused]] const String& identifier) +void PushNotifications::removePendingLocalNotification (const String& identifier) { - #if JUCE_PUSH_NOTIFICATIONS pimpl->removePendingLocalNotification (identifier); - #endif } -void PushNotifications::sendUpstreamMessage ([[maybe_unused]] const String& serverSenderId, - [[maybe_unused]] const String& collapseKey, - [[maybe_unused]] const String& messageId, - [[maybe_unused]] const String& messageType, - [[maybe_unused]] int timeToLive, - [[maybe_unused]] const StringPairArray& additionalData) +void PushNotifications::sendUpstreamMessage (const String& serverSenderId, + const String& collapseKey, + const String& messageId, + const String& messageType, + int timeToLive, + const StringPairArray& additionalData) { - #if JUCE_PUSH_NOTIFICATIONS pimpl->sendUpstreamMessage (serverSenderId, collapseKey, messageId, messageType, timeToLive, additionalData); - #endif } //============================================================================== @@ -234,4 +234,92 @@ void PushNotifications::Listener::upstreamMessageSent ([[maybe_unused]] const St void PushNotifications::Listener::upstreamMessageSendingError ([[maybe_unused]] const String& messageId, [[maybe_unused]] const String& error) {} +//============================================================================== +void privatePostSystemNotification (const String& notificationTitle, const String& notificationBody); +void privatePostSystemNotification ([[maybe_unused]] const String& notificationTitle, + [[maybe_unused]] const String& notificationBody) +{ + #if JUCE_PUSH_NOTIFICATIONS + #if JUCE_ANDROID || JUCE_IOS || JUCE_MAC + auto* notificationsInstance = PushNotifications::getInstance(); + + if (notificationsInstance == nullptr) + return; + + #if JUCE_ANDROID + notificationsInstance->requestPermissionsWithSettings ({}); + + static auto channels = std::invoke ([]() -> Array + { + PushNotifications::Channel chan; + + chan.identifier = "1"; + chan.name = "Notifications"; + chan.description = "Accessibility notifications"; + chan.groupId = "accessibility"; + chan.ledColour = Colours::yellow; + chan.canShowBadge = true; + chan.enableLights = true; + chan.enableVibration = true; + chan.soundToPlay = URL ("default_os_sound"); + chan.vibrationPattern = { 1000, 1000 }; + + return { chan }; + }); + + notificationsInstance->setupChannels ({ PushNotifications::ChannelGroup { "accessibility", "accessibility" } }, + channels); + #else + static auto settings = std::invoke ([] + { + PushNotifications::Settings s; + s.allowAlert = true; + s.allowBadge = true; + s.allowSound = true; + + #if JUCE_IOS + PushNotifications::Settings::Category c; + c.identifier = "Accessibility"; + + s.categories = { c }; + #endif + + return s; + }); + + notificationsInstance->requestPermissionsWithSettings (settings); + #endif + + const auto notification = std::invoke ([¬ificationTitle, ¬ificationBody] + { + PushNotifications::Notification n; + + n.identifier = String (Random::getSystemRandom().nextInt()); + n.title = notificationTitle; + n.body = notificationBody; + + #if JUCE_IOS + n.category = "Accessibility"; + #elif JUCE_ANDROID + n.channelId = "1"; + n.icon = "accessibilitynotificationicon"; + #endif + + return n; + }); + + if (notification.isValid()) + notificationsInstance->sendLocalNotification (notification); + + #else + SystemTrayIconComponent systemTrayIcon; + + Image im (Image::ARGB, 128, 128, true); + systemTrayIcon.setIconImage (im, im); + + systemTrayIcon.showInfoBubble (notificationTitle, notificationBody); + #endif + #endif +} + } // namespace juce diff --git a/modules/juce_gui_extra/misc/juce_PushNotifications.h b/modules/juce_gui_extra/misc/juce_PushNotifications.h index b3d7b550b1b6..b5bafe2511b3 100644 --- a/modules/juce_gui_extra/misc/juce_PushNotifications.h +++ b/modules/juce_gui_extra/misc/juce_PushNotifications.h @@ -403,8 +403,8 @@ class JUCE_API PushNotifications : private DeletedAtShutdown */ struct Category { - juce::String identifier; /**< unique identifier */ - juce::Array actions; /**< optional list of actions within this category */ + String identifier; /**< unique identifier */ + Array actions; /**< optional list of actions within this category */ bool sendDismissAction = false; /**< whether dismiss action will be sent to the app */ }; @@ -703,12 +703,8 @@ class JUCE_API PushNotifications : private DeletedAtShutdown friend struct JuceFirebaseMessagingService; #endif - #if JUCE_PUSH_NOTIFICATIONS - struct Pimpl; - friend struct Pimpl; - - std::unique_ptr pimpl; - #endif + struct Impl; + std::unique_ptr pimpl; }; } // namespace juce diff --git a/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp b/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp index 46c50123c884..25dd16027f2a 100644 --- a/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp +++ b/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp @@ -35,6 +35,8 @@ namespace juce { +#define JUCE_PUSH_NOTIFICATIONS_IMPL 1 + #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (constructor, "", "(Ljava/lang/String;Ljava/lang/CharSequence;I)V") \ METHOD (enableLights, "enableLights", "(Z)V") \ @@ -274,9 +276,9 @@ bool PushNotifications::Notification::isValid() const noexcept } //============================================================================== -struct PushNotifications::Pimpl +struct PushNotifications::Impl { - explicit Pimpl (PushNotifications& p) + explicit Impl (PushNotifications& p) : owner (p) {} @@ -306,7 +308,7 @@ struct PushNotifications::Pimpl const auto notifyListeners = [] { if (auto* instance = PushNotifications::getInstance()) - instance->listeners.call ([] (Listener& l) { l.notificationSettingsReceived ({}); }); + instance->listeners.call ([] (Listener& l) { l.notificationSettingsReceived (makeDefaultSettings()); }); }; if (MessageManager::getInstance()->isThisTheMessageThread()) @@ -318,7 +320,7 @@ struct PushNotifications::Pimpl void requestSettingsUsed() { - owner.listeners.call ([] (Listener& l) { l.notificationSettingsReceived ({}); }); + owner.listeners.call ([] (Listener& l) { l.notificationSettingsReceived (makeDefaultSettings()); }); } void sendLocalNotification (const Notification& n) @@ -1573,6 +1575,15 @@ struct PushNotifications::Pimpl && env->CallBooleanMethod (extras, AndroidBundle.containsKey, javaString ("google.message_id").get()); } + static Settings makeDefaultSettings() + { + Settings settings; + settings.allowAlert = true; + settings.allowBadge = true; + settings.allowSound = true; + return settings; + } + PushNotifications& owner; }; @@ -1640,14 +1651,14 @@ bool juce_handleNotificationIntent (void* intent) { auto* instance = PushNotifications::getInstanceWithoutCreating(); - if (PushNotifications::Pimpl::isDeleteNotificationIntent ((jobject) intent)) + if (PushNotifications::Impl::isDeleteNotificationIntent ((jobject) intent)) { if (instance) instance->pimpl->notifyListenersAboutLocalNotificationDeleted (LocalRef ((jobject) intent)); return true; } - else if (PushNotifications::Pimpl::isLocalNotificationIntent ((jobject) intent)) + else if (PushNotifications::Impl::isLocalNotificationIntent ((jobject) intent)) { if (instance) instance->pimpl->notifyListenersAboutLocalNotification (LocalRef ((jobject) intent)); @@ -1655,7 +1666,7 @@ bool juce_handleNotificationIntent (void* intent) return true; } #if defined (JUCE_FIREBASE_MESSAGING_SERVICE_CLASSNAME) - else if (PushNotifications::Pimpl::isRemoteNotificationIntent ((jobject) intent)) + else if (PushNotifications::Impl::isRemoteNotificationIntent ((jobject) intent)) { if (instance) instance->pimpl->notifyListenersAboutRemoteNotificationFromSystemTray (LocalRef ((jobject) intent)); diff --git a/modules/juce_gui_extra/native/juce_PushNotifications_ios.cpp b/modules/juce_gui_extra/native/juce_PushNotifications_ios.cpp index b093a76434f0..a2d44c7c7eaf 100644 --- a/modules/juce_gui_extra/native/juce_PushNotifications_ios.cpp +++ b/modules/juce_gui_extra/native/juce_PushNotifications_ios.cpp @@ -35,6 +35,8 @@ namespace juce { +#define JUCE_PUSH_NOTIFICATIONS_IMPL 1 + struct PushNotificationsDelegateDetails { //============================================================================== @@ -301,9 +303,9 @@ bool PushNotifications::Notification::isValid() const noexcept } //============================================================================== -struct PushNotifications::Pimpl +struct PushNotifications::Impl { - Pimpl (PushNotifications& p) + explicit Impl (PushNotifications& p) : owner (p) { Class::setThis (delegate.get(), this); @@ -555,7 +557,7 @@ struct PushNotifications::Pimpl Class() : ObjCClass ("JucePushNotificationsDelegate_") { - addIvar ("self"); + addIvar ("self"); addMethod (@selector (application:didRegisterForRemoteNotificationsWithDeviceToken:), [] (id self, SEL, UIApplication*, NSData* data) { @@ -596,8 +598,8 @@ struct PushNotifications::Pimpl } //============================================================================== - static Pimpl& getThis (id self) { return *getIvar (self, "self"); } - static void setThis (id self, Pimpl* d) { object_setInstanceVariable (self, "self", d); } + static Impl& getThis (id self) { return *getIvar (self, "self"); } + static void setThis (id self, Impl* d) { object_setInstanceVariable (self, "self", d); } }; //============================================================================== diff --git a/modules/juce_gui_extra/native/juce_PushNotifications_mac.cpp b/modules/juce_gui_extra/native/juce_PushNotifications_mac.cpp index c994beb3bd4c..a06dd82dc929 100644 --- a/modules/juce_gui_extra/native/juce_PushNotifications_mac.cpp +++ b/modules/juce_gui_extra/native/juce_PushNotifications_mac.cpp @@ -35,6 +35,8 @@ namespace juce { +#define JUCE_PUSH_NOTIFICATIONS_IMPL 1 + JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS namespace PushNotificationsDelegateDetailsOsx @@ -343,9 +345,9 @@ struct PushNotificationsDelegate bool PushNotifications::Notification::isValid() const noexcept { return true; } //============================================================================== -struct PushNotifications::Pimpl : private PushNotificationsDelegate +struct PushNotifications::Impl : private PushNotificationsDelegate { - Pimpl (PushNotifications& p) + explicit Impl (PushNotifications& p) : owner (p) { } From 5d5829927a896c388b0896b3da6321905cc11492 Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 25 Nov 2024 13:29:34 +0000 Subject: [PATCH 66/76] AccessibilityDemo: Add local notifications example Also updates the DemoRunner so that the new push notifications example works properly on Android. --- .../Builds/Android/app/CMakeLists.txt | 4 +- .../res/raw/accessibilitynotificationicon.png | Bin 0 -> 103448 bytes .../res/raw/accessibilitynotificationicon.png | Bin 0 -> 103448 bytes .../DemoRunner/Builds/LinuxMakefile/Makefile | 4 +- .../DemoRunner.xcodeproj/project.pbxproj | 2 + .../VisualStudio2019/DemoRunner_App.vcxproj | 8 +- .../VisualStudio2022/DemoRunner_App.vcxproj | 8 +- .../iOS/DemoRunner.xcodeproj/project.pbxproj | 2 + examples/DemoRunner/CMakeLists.txt | 1 + examples/DemoRunner/DemoRunner.jucer | 6 +- .../Source/accessibilitynotificationicon.png | Bin 0 -> 103448 bytes examples/GUI/AccessibilityDemo.h | 201 +++++++++++++----- 12 files changed, 164 insertions(+), 72 deletions(-) create mode 100644 examples/DemoRunner/Builds/Android/app/src/debug/res/raw/accessibilitynotificationicon.png create mode 100644 examples/DemoRunner/Builds/Android/app/src/release/res/raw/accessibilitynotificationicon.png create mode 100644 examples/DemoRunner/Source/accessibilitynotificationicon.png diff --git a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt index 23f6137865d5..31974123bb1f 100644 --- a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt +++ b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt @@ -34,9 +34,9 @@ include_directories( AFTER enable_language(ASM) if(JUCE_BUILD_CONFIGURATION MATCHES "DEBUG") - add_definitions([[-DJUCE_PROJUCER_VERSION=0x80004]] [[-DJUCE_MODULE_AVAILABLE_juce_analytics=1]] [[-DJUCE_MODULE_AVAILABLE_juce_animation=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_basics=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_devices=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_formats=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_processors=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_utils=1]] [[-DJUCE_MODULE_AVAILABLE_juce_box2d=1]] [[-DJUCE_MODULE_AVAILABLE_juce_core=1]] [[-DJUCE_MODULE_AVAILABLE_juce_cryptography=1]] [[-DJUCE_MODULE_AVAILABLE_juce_data_structures=1]] [[-DJUCE_MODULE_AVAILABLE_juce_dsp=1]] [[-DJUCE_MODULE_AVAILABLE_juce_events=1]] [[-DJUCE_MODULE_AVAILABLE_juce_graphics=1]] [[-DJUCE_MODULE_AVAILABLE_juce_gui_basics=1]] [[-DJUCE_MODULE_AVAILABLE_juce_gui_extra=1]] [[-DJUCE_MODULE_AVAILABLE_juce_javascript=1]] [[-DJUCE_MODULE_AVAILABLE_juce_opengl=1]] [[-DJUCE_MODULE_AVAILABLE_juce_osc=1]] [[-DJUCE_MODULE_AVAILABLE_juce_product_unlocking=1]] [[-DJUCE_MODULE_AVAILABLE_juce_video=1]] [[-DJUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1]] [[-DJUCE_USE_MP3AUDIOFORMAT=1]] [[-DJUCE_PLUGINHOST_VST3=1]] [[-DJUCE_PLUGINHOST_LV2=1]] [[-DJUCE_ALLOW_STATIC_NULL_VARIABLES=0]] [[-DJUCE_STRICT_REFCOUNTEDPOINTER=1]] [[-DJUCE_USE_CAMERA=1]] [[-DJUCE_STANDALONE_APPLICATION=1]] [[-DJUCE_DEMO_RUNNER=1]] [[-DJUCE_UNIT_TESTS=1]] [[-DJUCER_ANDROIDSTUDIO_7F0E4A25=1]] [[-DJUCE_APP_VERSION=8.0.4]] [[-DJUCE_APP_VERSION_HEX=0x80004]] [[-DDEBUG=1]] [[-D_DEBUG=1]]) + add_definitions([[-DJUCE_PROJUCER_VERSION=0x80004]] [[-DJUCE_MODULE_AVAILABLE_juce_analytics=1]] [[-DJUCE_MODULE_AVAILABLE_juce_animation=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_basics=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_devices=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_formats=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_processors=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_utils=1]] [[-DJUCE_MODULE_AVAILABLE_juce_box2d=1]] [[-DJUCE_MODULE_AVAILABLE_juce_core=1]] [[-DJUCE_MODULE_AVAILABLE_juce_cryptography=1]] [[-DJUCE_MODULE_AVAILABLE_juce_data_structures=1]] [[-DJUCE_MODULE_AVAILABLE_juce_dsp=1]] [[-DJUCE_MODULE_AVAILABLE_juce_events=1]] [[-DJUCE_MODULE_AVAILABLE_juce_graphics=1]] [[-DJUCE_MODULE_AVAILABLE_juce_gui_basics=1]] [[-DJUCE_MODULE_AVAILABLE_juce_gui_extra=1]] [[-DJUCE_MODULE_AVAILABLE_juce_javascript=1]] [[-DJUCE_MODULE_AVAILABLE_juce_opengl=1]] [[-DJUCE_MODULE_AVAILABLE_juce_osc=1]] [[-DJUCE_MODULE_AVAILABLE_juce_product_unlocking=1]] [[-DJUCE_MODULE_AVAILABLE_juce_video=1]] [[-DJUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1]] [[-DJUCE_USE_MP3AUDIOFORMAT=1]] [[-DJUCE_PLUGINHOST_VST3=1]] [[-DJUCE_PLUGINHOST_LV2=1]] [[-DJUCE_ALLOW_STATIC_NULL_VARIABLES=0]] [[-DJUCE_STRICT_REFCOUNTEDPOINTER=1]] [[-DJUCE_USE_CAMERA=1]] [[-DJUCE_STANDALONE_APPLICATION=1]] [[-DJUCE_DEMO_RUNNER=1]] [[-DJUCE_UNIT_TESTS=1]] [[-DJUCE_PUSH_NOTIFICATIONS=1]] [[-DJUCER_ANDROIDSTUDIO_7F0E4A25=1]] [[-DJUCE_APP_VERSION=8.0.4]] [[-DJUCE_APP_VERSION_HEX=0x80004]] [[-DDEBUG=1]] [[-D_DEBUG=1]]) elseif(JUCE_BUILD_CONFIGURATION MATCHES "RELEASE") - add_definitions([[-DJUCE_PROJUCER_VERSION=0x80004]] [[-DJUCE_MODULE_AVAILABLE_juce_analytics=1]] [[-DJUCE_MODULE_AVAILABLE_juce_animation=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_basics=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_devices=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_formats=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_processors=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_utils=1]] [[-DJUCE_MODULE_AVAILABLE_juce_box2d=1]] [[-DJUCE_MODULE_AVAILABLE_juce_core=1]] [[-DJUCE_MODULE_AVAILABLE_juce_cryptography=1]] [[-DJUCE_MODULE_AVAILABLE_juce_data_structures=1]] [[-DJUCE_MODULE_AVAILABLE_juce_dsp=1]] [[-DJUCE_MODULE_AVAILABLE_juce_events=1]] [[-DJUCE_MODULE_AVAILABLE_juce_graphics=1]] [[-DJUCE_MODULE_AVAILABLE_juce_gui_basics=1]] [[-DJUCE_MODULE_AVAILABLE_juce_gui_extra=1]] [[-DJUCE_MODULE_AVAILABLE_juce_javascript=1]] [[-DJUCE_MODULE_AVAILABLE_juce_opengl=1]] [[-DJUCE_MODULE_AVAILABLE_juce_osc=1]] [[-DJUCE_MODULE_AVAILABLE_juce_product_unlocking=1]] [[-DJUCE_MODULE_AVAILABLE_juce_video=1]] [[-DJUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1]] [[-DJUCE_USE_MP3AUDIOFORMAT=1]] [[-DJUCE_PLUGINHOST_VST3=1]] [[-DJUCE_PLUGINHOST_LV2=1]] [[-DJUCE_ALLOW_STATIC_NULL_VARIABLES=0]] [[-DJUCE_STRICT_REFCOUNTEDPOINTER=1]] [[-DJUCE_USE_CAMERA=1]] [[-DJUCE_STANDALONE_APPLICATION=1]] [[-DJUCE_DEMO_RUNNER=1]] [[-DJUCE_UNIT_TESTS=1]] [[-DJUCER_ANDROIDSTUDIO_7F0E4A25=1]] [[-DJUCE_APP_VERSION=8.0.4]] [[-DJUCE_APP_VERSION_HEX=0x80004]] [[-DNDEBUG=1]]) + add_definitions([[-DJUCE_PROJUCER_VERSION=0x80004]] [[-DJUCE_MODULE_AVAILABLE_juce_analytics=1]] [[-DJUCE_MODULE_AVAILABLE_juce_animation=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_basics=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_devices=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_formats=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_processors=1]] [[-DJUCE_MODULE_AVAILABLE_juce_audio_utils=1]] [[-DJUCE_MODULE_AVAILABLE_juce_box2d=1]] [[-DJUCE_MODULE_AVAILABLE_juce_core=1]] [[-DJUCE_MODULE_AVAILABLE_juce_cryptography=1]] [[-DJUCE_MODULE_AVAILABLE_juce_data_structures=1]] [[-DJUCE_MODULE_AVAILABLE_juce_dsp=1]] [[-DJUCE_MODULE_AVAILABLE_juce_events=1]] [[-DJUCE_MODULE_AVAILABLE_juce_graphics=1]] [[-DJUCE_MODULE_AVAILABLE_juce_gui_basics=1]] [[-DJUCE_MODULE_AVAILABLE_juce_gui_extra=1]] [[-DJUCE_MODULE_AVAILABLE_juce_javascript=1]] [[-DJUCE_MODULE_AVAILABLE_juce_opengl=1]] [[-DJUCE_MODULE_AVAILABLE_juce_osc=1]] [[-DJUCE_MODULE_AVAILABLE_juce_product_unlocking=1]] [[-DJUCE_MODULE_AVAILABLE_juce_video=1]] [[-DJUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1]] [[-DJUCE_USE_MP3AUDIOFORMAT=1]] [[-DJUCE_PLUGINHOST_VST3=1]] [[-DJUCE_PLUGINHOST_LV2=1]] [[-DJUCE_ALLOW_STATIC_NULL_VARIABLES=0]] [[-DJUCE_STRICT_REFCOUNTEDPOINTER=1]] [[-DJUCE_USE_CAMERA=1]] [[-DJUCE_STANDALONE_APPLICATION=1]] [[-DJUCE_DEMO_RUNNER=1]] [[-DJUCE_UNIT_TESTS=1]] [[-DJUCE_PUSH_NOTIFICATIONS=1]] [[-DJUCER_ANDROIDSTUDIO_7F0E4A25=1]] [[-DJUCE_APP_VERSION=8.0.4]] [[-DJUCE_APP_VERSION_HEX=0x80004]] [[-DNDEBUG=1]]) else() message( FATAL_ERROR "No matching build-configuration found." ) endif() diff --git a/examples/DemoRunner/Builds/Android/app/src/debug/res/raw/accessibilitynotificationicon.png b/examples/DemoRunner/Builds/Android/app/src/debug/res/raw/accessibilitynotificationicon.png new file mode 100644 index 0000000000000000000000000000000000000000..30b40d4d2805c575ed5dd518b82baa13874c5c03 GIT binary patch literal 103448 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