From 800e97131e1881f4510b4e629960e8364fdc557f Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Tue, 16 Apr 2024 09:35:03 +0200 Subject: [PATCH 01/40] Imported JUCE customizations for Acon Digital products --- .../audio_io/juce_AudioDeviceManager.cpp | 5 +- .../juce_audio_plugin_client_AAX.cpp | 61 +++++++++++++++++ .../juce_audio_plugin_client_VST2.cpp | 13 +++- .../format_types/juce_AudioUnitPluginFormat.h | 15 +++++ .../juce_AudioUnitPluginFormat.mm | 66 +++++++++++++++++++ .../format_types/juce_VSTPluginFormat.cpp | 5 +- .../juce_gui_basics/buttons/juce_Button.cpp | 10 ++- modules/juce_gui_basics/buttons/juce_Button.h | 4 ++ .../juce_ApplicationCommandManager.cpp | 12 ++-- .../drawables/juce_SVGParser.cpp | 4 +- .../keyboard/juce_KeyPress.cpp | 14 ++-- .../native/juce_Windowing_windows.cpp | 2 + .../juce_gui_basics/widgets/juce_ComboBox.cpp | 14 +++- .../juce_gui_basics/widgets/juce_ComboBox.h | 5 ++ .../juce_gui_basics/widgets/juce_Label.cpp | 4 ++ .../widgets/juce_TextEditor.cpp | 14 +++- .../juce_gui_basics/widgets/juce_TextEditor.h | 5 ++ 17 files changed, 233 insertions(+), 20 deletions(-) diff --git a/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp b/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp index 447a240a2559..9f6d1120aaf5 100644 --- a/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp +++ b/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp @@ -236,8 +236,9 @@ static void addIfNotNull (OwnedArray& list, AudioIODeviceType void AudioDeviceManager::createAudioDeviceTypes (OwnedArray& list) { addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_WASAPI (WASAPIDeviceMode::shared)); - addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_WASAPI (WASAPIDeviceMode::exclusive)); - addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_WASAPI (WASAPIDeviceMode::sharedLowLatency)); + // Acon Digital modification - no need for special WASAPI modes + //addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_WASAPI (WASAPIDeviceMode::exclusive)); + //addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_WASAPI (WASAPIDeviceMode::sharedLowLatency)); addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_DirectSound()); addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_ASIO()); addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_CoreAudio()); 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 e8bf84295545..80e6a1a5a64f 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 @@ -85,6 +85,11 @@ static_assert (AAX_SDK_CURRENT_REVISION >= AAX_SDK_2p4p0_REVISION, "JUCE require #include #include +#if JucePlugin_Enable_ARA +#include +#include +#endif + JUCE_END_IGNORE_WARNINGS_MSVC JUCE_END_IGNORE_WARNINGS_GCC_LIKE @@ -723,6 +728,12 @@ namespace AAXClasses void mouseUp (const MouseEvent& e) override { callMouseMethod (e, &AAX_IViewContainer::HandleParameterMouseUp); } void mouseDrag (const MouseEvent& e) override { callMouseMethod (e, &AAX_IViewContainer::HandleParameterMouseDrag); } + void resized() override + { + if (pluginEditor != nullptr) + pluginEditor->setBounds (getLocalBounds()); + } + void parentSizeChanged() override { resizeHostWindow(); @@ -847,6 +858,14 @@ namespace AAXClasses return new JuceAAX_Processor(); } + #if JucePlugin_Enable_ARA + AAX_Result Initialize (IACFUnknown* iController) override + { + aaxaraBinding.reset (new ARA::AAX_VARABinding (iController)); + return AAX_CEffectParameters::Initialize (iController); + } + #endif + AAX_Result Uninitialize() override { cancelPendingUpdate(); @@ -866,6 +885,36 @@ namespace AAXClasses AAX_Result EffectInit() override { cancelPendingUpdate(); + + #if JucePlugin_Enable_ARA + ARA::ARAPlugInInstanceRoleFlags knownRoles = 0; + auto result = aaxaraBinding->GetInstanceRoleFlags (&knownRoles, &assignedRoles); + + // If no ARA roles provided - work as a regular AAX plug-in + if (result == AAX_SUCCESS) + { + assignedRoles &= (ARA::kARAPlaybackRendererRole | ARA::kARAEditorRendererRole | ARA::kARAEditorViewRole); + if (assignedRoles) + { + result = aaxaraBinding->GetDocumentController (&documentControllerRef); + if (result != AAX_SUCCESS) + return result; + + auto araPluginExtension = dynamic_cast (pluginInstance.get()); + if (!araPluginExtension) + return AAX_ERROR_NULL_OBJECT; + + auto* const plugInEnxtensionInstance = araPluginExtension->bindToARA (documentControllerRef, knownRoles, assignedRoles); + if (!plugInEnxtensionInstance) + return AAX_ERROR_NULL_OBJECT; + + result = aaxaraBinding->SetPlugInExtensionInstance (plugInEnxtensionInstance); + if (result != AAX_SUCCESS) + return result; + } + } + #endif + check (Controller()->GetSampleRate (&sampleRate)); processingSidechainChange = false; auto err = preparePlugin(); @@ -2290,6 +2339,12 @@ namespace AAXClasses std::atomic state { 0 }; }; + #if JucePlugin_Enable_ARA + std::unique_ptr aaxaraBinding; + ARA::ARADocumentControllerRef documentControllerRef; + ARA::ARAPlugInInstanceRoleFlags assignedRoles; + #endif + RecordingState recordingState; std::atomic processingSidechainChange, sidechainDesired; @@ -2557,6 +2612,12 @@ namespace AAXClasses } } + #if JucePlugin_Enable_ARA + properties->AddProperty (AAX_eProperty_UsesTransport, true); + properties->AddProperty (AAX_eProperty_Constraint_Topology, AAX_eConstraintTopology_Monolithic); + properties->AddPointerProperty (ARA::AAX_eProperty_ARAFactoryPointer, createARAFactory()); + #endif + check (desc.AddProcessProc_Native (algorithmProcessCallback, properties)); } 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 262f0a00c06a..a414981d060a 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 @@ -972,7 +972,18 @@ class JuceVSTWrapper final : public AudioProcessorListener, void getEditorBounds (Vst2::ERect& bounds) { auto editorBounds = getSizeToContainChild(); - bounds = convertToHostBounds ({ 0, 0, (int16) editorBounds.getHeight(), (int16) editorBounds.getWidth() }); + + // Acon Digital modification - circumvention of VST hosting error in EDIUS when runnng high-DPI mode + String hostPath = File::getSpecialLocation (File::hostApplicationPath).getFileNameWithoutExtension(); + if (hostPath.containsIgnoreCase ("edius")) { + auto mainDisplay = Desktop::getInstance().getDisplays().getPrimaryDisplay(); + float desktopScale = mainDisplay != nullptr ? mainDisplay->dpi / 96.f : 1.f; + float ediusCorrectionScale = 1.f + (desktopScale - 1.f) / desktopScale; + bounds = { 0, 0, (int16) roundToInt (editorBounds.getHeight() * ediusCorrectionScale), + (int16) roundToInt (editorBounds.getWidth() * ediusCorrectionScale) }; + } + else + bounds = convertToHostBounds ({ 0, 0, (int16) editorBounds.getHeight(), (int16) editorBounds.getWidth() }); } void attachToHost (VstOpCodeArguments args) diff --git a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.h b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.h index 162484190fc3..fec96649db0e 100644 --- a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.h +++ b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.h @@ -47,6 +47,21 @@ class JUCE_API AudioUnitPluginFormat : public AudioPluginFormat bool canScanForPlugins() const override { return true; } bool isTrivialToScan() const override { return false; } + //============================================================================== + /** Attempts to reload an AU plugin's state from some preset file data. + + @see VSTPluginFormat::loadFromFXBFile + */ + static bool setStateFromAUPresetFile (AudioPluginInstance*, const MemoryBlock&); + + //============================================================================== + /** Attempts to save an AU plugin's state to some preset file data. + + @see VSTPluginFormat::saveToFXBFile + */ + static bool saveStateToAUPresetFile (AudioPluginInstance*, MemoryBlock&); + + //============================================================================== void findAllTypesForFile (OwnedArray&, const String& fileOrIdentifier) override; bool fileMightContainThisPluginType (const String& fileOrIdentifier) override; String getNameOfPluginFromIdentifier (const String& fileOrIdentifier) override; diff --git a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm index 4977541ae568..01092f27ba00 100644 --- a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm +++ b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm @@ -2796,6 +2796,72 @@ void messageCallback() override { } +bool AudioUnitPluginFormat::setStateFromAUPresetFile (AudioPluginInstance* api, const MemoryBlock& rawData) +{ + AudioUnit audioUnit = (AudioUnit) api->getPlatformSpecificData(); + jassert (audioUnit != nullptr); + + CFReadStreamRef stream = CFReadStreamCreateWithBytesNoCopy (kCFAllocatorDefault, (const UInt8*) rawData.getData(), + rawData.getSize(), kCFAllocatorNull); + CFReadStreamOpen (stream); + + CFPropertyListFormat format = kCFPropertyListBinaryFormat_v1_0; + CFPropertyListRef propertyList = CFPropertyListCreateFromStream (kCFAllocatorDefault, stream, 0, + kCFPropertyListImmutable, &format, 0); + CFRelease (stream); + + if (propertyList != 0) + { + AudioUnitSetProperty (audioUnit, kAudioUnitProperty_ClassInfo, kAudioUnitScope_Global, + 0, &propertyList, sizeof (propertyList)); + + AudioUnitParameter param; + param.mAudioUnit = audioUnit; + param.mParameterID = kAUParameterListener_AnyParameter; + + AUParameterListenerNotify (nullptr, nullptr, ¶m); + + CFRelease (propertyList); + return true; + } + else + return false; +} + +bool AudioUnitPluginFormat::saveStateToAUPresetFile (AudioPluginInstance* api, MemoryBlock& rawData) +{ + AudioUnit audioUnit = (AudioUnit) api->getPlatformSpecificData(); + jassert (audioUnit != nullptr); + + CFPropertyListRef propertyList = 0; + UInt32 sz = sizeof (CFPropertyListRef); + + if (AudioUnitGetProperty (audioUnit, + kAudioUnitProperty_ClassInfo, + kAudioUnitScope_Global, + 0, &propertyList, &sz) == noErr) + { + CFWriteStreamRef stream = CFWriteStreamCreateWithAllocatedBuffers (kCFAllocatorDefault, kCFAllocatorDefault); + CFWriteStreamOpen (stream); + + CFIndex bytesWritten = CFPropertyListWriteToStream (propertyList, stream, kCFPropertyListBinaryFormat_v1_0, 0); + CFWriteStreamClose (stream); + + CFDataRef data = (CFDataRef) CFWriteStreamCopyProperty (stream, kCFStreamPropertyDataWritten); + + rawData.setSize ((size_t) bytesWritten); + rawData.copyFrom (CFDataGetBytePtr (data), 0, bytesWritten); + CFRelease (data); + + CFRelease (stream); + CFRelease (propertyList); + + return true; + } + else + return false; +} + void AudioUnitPluginFormat::findAllTypesForFile (OwnedArray& results, const String& fileOrIdentifier) { diff --git a/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp b/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp index d08e11f5daeb..f60c00fdde61 100644 --- a/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp +++ b/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp @@ -1884,14 +1884,15 @@ struct VSTPluginInstance final : public AudioPluginInstance, auto totalLen = sizeof (fxProgramSet) + chunk.getSize() - 8; dest.setSize (totalLen, true); + // Acon Digital modification - fixed errors in FXB format auto set = (fxProgramSet*) dest.getData(); set->chunkMagic = fxbName ("CcnK"); - set->byteSize = 0; + set->byteSize = fxbSwap ((int) totalLen - 8); set->fxMagic = fxbName ("FPCh"); set->version = fxbSwap (fxbVersionNum); set->fxID = fxbSwap (getUID()); set->fxVersion = fxbSwap (getVersionNumber()); - set->numPrograms = fxbSwap (numPrograms); + set->numPrograms = fxbSwap (getNumParameters()); set->chunkSize = fxbSwap ((int32) chunk.getSize()); getCurrentProgramName().copyToUTF8 (set->name, sizeof (set->name) - 1); diff --git a/modules/juce_gui_basics/buttons/juce_Button.cpp b/modules/juce_gui_basics/buttons/juce_Button.cpp index 02818c511d38..949a076f031e 100644 --- a/modules/juce_gui_basics/buttons/juce_Button.cpp +++ b/modules/juce_gui_basics/buttons/juce_Button.cpp @@ -509,8 +509,10 @@ bool Button::isMouseSourceOver (const MouseEvent& e) return isMouseOver(); } -void Button::focusGained (FocusChangeType) +void Button::focusGained (FocusChangeType cause) { + // Acon Digital modification - keep track of focus change cause + lastFocusChangeCause = cause; updateState(); repaint(); } @@ -664,6 +666,12 @@ bool Button::keyPressed (const KeyPress& key) return false; } +// Acon Digital modification - keep track of focus change cause +Component::FocusChangeType Button::getLastFocusChangeCause() +{ + return lastFocusChangeCause; +} + //============================================================================== void Button::setRepeatSpeed (int initialDelayMillisecs, int repeatMillisecs, diff --git a/modules/juce_gui_basics/buttons/juce_Button.h b/modules/juce_gui_basics/buttons/juce_Button.h index e9878fb09f0f..6b50cd8a6ba6 100644 --- a/modules/juce_gui_basics/buttons/juce_Button.h +++ b/modules/juce_gui_basics/buttons/juce_Button.h @@ -384,6 +384,10 @@ class JUCE_API Button : public Component, /** Returns the button's current over/down/up state. */ ButtonState getState() const noexcept { return buttonState; } + + /** Acon Digital modification - keep track of focus change cause */ + FocusChangeType getLastFocusChangeCause(); + //============================================================================== /** This abstract base class is implemented by LookAndFeel classes to provide button-drawing functionality. diff --git a/modules/juce_gui_basics/commands/juce_ApplicationCommandManager.cpp b/modules/juce_gui_basics/commands/juce_ApplicationCommandManager.cpp index f45e32bfd5e1..15315b61a7ca 100644 --- a/modules/juce_gui_basics/commands/juce_ApplicationCommandManager.cpp +++ b/modules/juce_gui_basics/commands/juce_ApplicationCommandManager.cpp @@ -59,11 +59,13 @@ void ApplicationCommandManager::registerCommand (const ApplicationCommandInfo& n // Trying to re-register the same command ID with different parameters can often indicate a typo. // This assertion is here because I've found it useful catching some mistakes, but it may also cause // false alarms if you're deliberately updating some flags for a command. - jassert (newCommand.shortName == getCommandForID (newCommand.commandID)->shortName - && newCommand.categoryName == getCommandForID (newCommand.commandID)->categoryName - && newCommand.defaultKeypresses == getCommandForID (newCommand.commandID)->defaultKeypresses - && (newCommand.flags & (ApplicationCommandInfo::wantsKeyUpDownCallbacks | ApplicationCommandInfo::hiddenFromKeyEditor | ApplicationCommandInfo::readOnlyInKeyEditor)) - == (getCommandForID (newCommand.commandID)->flags & (ApplicationCommandInfo::wantsKeyUpDownCallbacks | ApplicationCommandInfo::hiddenFromKeyEditor | ApplicationCommandInfo::readOnlyInKeyEditor))); + + /** Acon Digital modification - disable unnecessary assert */ + //jassert (newCommand.shortName == getCommandForID (newCommand.commandID)->shortName + // && newCommand.categoryName == getCommandForID (newCommand.commandID)->categoryName + // && newCommand.defaultKeypresses == getCommandForID (newCommand.commandID)->defaultKeypresses + // && (newCommand.flags & (ApplicationCommandInfo::wantsKeyUpDownCallbacks | ApplicationCommandInfo::hiddenFromKeyEditor | ApplicationCommandInfo::readOnlyInKeyEditor)) + // == (getCommandForID (newCommand.commandID)->flags & (ApplicationCommandInfo::wantsKeyUpDownCallbacks | ApplicationCommandInfo::hiddenFromKeyEditor | ApplicationCommandInfo::readOnlyInKeyEditor))); *command = newCommand; } diff --git a/modules/juce_gui_basics/drawables/juce_SVGParser.cpp b/modules/juce_gui_basics/drawables/juce_SVGParser.cpp index 5fec572261ad..858e7c12a0fe 100644 --- a/modules/juce_gui_basics/drawables/juce_SVGParser.cpp +++ b/modules/juce_gui_basics/drawables/juce_SVGParser.cpp @@ -944,7 +944,9 @@ class SVGState FillType type (gradient); - auto gradientTransform = parseTransform (fillXml->getStringAttribute ("gradientTransform")); + /** Acon Digital modification - fixed error in SVG parsing */ + auto gradientTransform = parseTransform (fillXml->getStringAttribute ("gradientTransform")) + .followedBy (transform); if (gradient.isRadial) { diff --git a/modules/juce_gui_basics/keyboard/juce_KeyPress.cpp b/modules/juce_gui_basics/keyboard/juce_KeyPress.cpp index 95645ec45705..368b4626d45b 100644 --- a/modules/juce_gui_basics/keyboard/juce_KeyPress.cpp +++ b/modules/juce_gui_basics/keyboard/juce_KeyPress.cpp @@ -176,7 +176,8 @@ KeyPress KeyPress::createFromDescription (const String& desc) int modifiers = 0; for (int i = 0; i < numElementsInArray (KeyPressHelpers::modifierNames); ++i) - if (desc.containsWholeWordIgnoreCase (KeyPressHelpers::modifierNames[i].name)) + /** Acon Digital modification - translate modifier names */ + if (desc.containsWholeWordIgnoreCase (TRANS (KeyPressHelpers::modifierNames[i].name))) modifiers |= KeyPressHelpers::modifierNames[i].flag; int key = 0; @@ -237,14 +238,15 @@ String KeyPress::getTextDescription() const if (textCharacter == '/' && keyCode != numberPadDivide) return "/"; - if (mods.isCtrlDown()) desc << "ctrl + "; - if (mods.isShiftDown()) desc << "shift + "; + /** Acon Digital modification - translate modifier names */ + if (mods.isCtrlDown()) desc << TRANS ("ctrl") + " + "; + if (mods.isShiftDown()) desc << TRANS ("shift") + " + "; #if JUCE_MAC || JUCE_IOS - if (mods.isAltDown()) desc << "option + "; - if (mods.isCommandDown()) desc << "command + "; + if (mods.isAltDown()) desc << TRANS ("option") + " + "; + if (mods.isCommandDown()) desc << TRANS ("command") + " + "; #else - if (mods.isAltDown()) desc << "alt + "; + if (mods.isAltDown()) desc << TRANS ("alt") + " + "; #endif for (int i = 0; i < numElementsInArray (KeyPressHelpers::translations); ++i) diff --git a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp index f898fa3c2060..fb1ea86617a2 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -2085,6 +2085,8 @@ class HWNDComponentPeer final : public ComponentPeer, void repaint (const Rectangle& area) override { deferredRepaints.add ((area.toDouble() * getPlatformScaleFactor()).getSmallestIntegerContainer()); + /** Acon Digital modification - fix issues with freezed UI */ + dispatchDeferredRepaints(); } void dispatchDeferredRepaints() diff --git a/modules/juce_gui_basics/widgets/juce_ComboBox.cpp b/modules/juce_gui_basics/widgets/juce_ComboBox.cpp index bcedfe3c797e..e670545a06c8 100644 --- a/modules/juce_gui_basics/widgets/juce_ComboBox.cpp +++ b/modules/juce_gui_basics/widgets/juce_ComboBox.cpp @@ -548,7 +548,19 @@ void ComboBox::showPopup() ModalCallbackFunction::forComponent (comboBoxPopupMenuFinishedCallback, this)); } -//============================================================================== +/** Acon Digital modification - allow for visual mouse over indications */ +void ComboBox::mouseEnter (const MouseEvent& e) +{ + Component::mouseEnter (e); + repaint(); +} + +void ComboBox::mouseExit (const MouseEvent& e) +{ + Component::mouseExit (e); + repaint(); +} + void ComboBox::mouseDown (const MouseEvent& e) { beginDragAutoRepeat (300); diff --git a/modules/juce_gui_basics/widgets/juce_ComboBox.h b/modules/juce_gui_basics/widgets/juce_ComboBox.h index 15ee5fabd294..785a46e6113d 100644 --- a/modules/juce_gui_basics/widgets/juce_ComboBox.h +++ b/modules/juce_gui_basics/widgets/juce_ComboBox.h @@ -397,6 +397,11 @@ class JUCE_API ComboBox : public Component, void handleAsyncUpdate() override; /** @internal */ String getTooltip() override { return label->getTooltip(); } + /** Acon Digital modification - allow for visual mouse over indications */ + /** @internal */ + void mouseEnter (const MouseEvent&) override; + /** @internal */ + void mouseExit (const MouseEvent&) override; /** @internal */ void mouseDown (const MouseEvent&) override; /** @internal */ diff --git a/modules/juce_gui_basics/widgets/juce_Label.cpp b/modules/juce_gui_basics/widgets/juce_Label.cpp index 31dad478f551..f26049eabb3d 100644 --- a/modules/juce_gui_basics/widgets/juce_Label.cpp +++ b/modules/juce_gui_basics/widgets/juce_Label.cpp @@ -228,6 +228,10 @@ void Label::showEditor() editor->addListener (this); editor->grabKeyboardFocus(); + /** Acon Digital modification - allow edit boxes with centerred text */ + if (getJustificationType().testFlags (Justification::horizontallyCentred)) + editor->setJustification (Justification::centredTop); + if (editor == nullptr) // may be deleted by a callback return; diff --git a/modules/juce_gui_basics/widgets/juce_TextEditor.cpp b/modules/juce_gui_basics/widgets/juce_TextEditor.cpp index c9360ed92327..fc92fc47d9d7 100644 --- a/modules/juce_gui_basics/widgets/juce_TextEditor.cpp +++ b/modules/juce_gui_basics/widgets/juce_TextEditor.cpp @@ -1839,7 +1839,19 @@ void TextEditor::performPopupMenuAction (const int menuItemID) } } -//============================================================================== +/** Acon Digital modification - allow for visual mouse over indications */ +void TextEditor::mouseEnter (const MouseEvent& e) +{ + Component::mouseEnter (e); + repaint(); +} + +void TextEditor::mouseExit (const MouseEvent& e) +{ + Component::mouseExit (e); + repaint(); +} + void TextEditor::mouseDown (const MouseEvent& e) { beginDragAutoRepeat (100); diff --git a/modules/juce_gui_basics/widgets/juce_TextEditor.h b/modules/juce_gui_basics/widgets/juce_TextEditor.h index c30decc618c9..8f84d28068c1 100644 --- a/modules/juce_gui_basics/widgets/juce_TextEditor.h +++ b/modules/juce_gui_basics/widgets/juce_TextEditor.h @@ -711,6 +711,11 @@ class JUCE_API TextEditor : public TextInputTarget, void paint (Graphics&) override; /** @internal */ void paintOverChildren (Graphics&) override; + /** Acon Digital modification - allow for visual mouse over indications */ + /** @internal */ + void mouseEnter (const MouseEvent&) override; + /** @internal */ + void mouseExit (const MouseEvent&) override; /** @internal */ void mouseDown (const MouseEvent&) override; /** @internal */ From 2b03121ea386894ea5c5c406c55a02ab1edff3a3 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Tue, 16 Apr 2024 12:05:25 +0200 Subject: [PATCH 02/40] Added missing lastFocusChangeCause member in juce_Button class --- modules/juce_gui_basics/buttons/juce_Button.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/juce_gui_basics/buttons/juce_Button.h b/modules/juce_gui_basics/buttons/juce_Button.h index 6b50cd8a6ba6..d6195a881a4d 100644 --- a/modules/juce_gui_basics/buttons/juce_Button.h +++ b/modules/juce_gui_basics/buttons/juce_Button.h @@ -528,6 +528,8 @@ class JUCE_API Button : public Component, bool triggerOnMouseDown = false; bool generateTooltip = false; + /** Acon Digital modification - keep track of focus change cause */ + FocusChangeType lastFocusChangeCause = focusChangedDirectly; void checkToggleableState (bool wasToggleable); void repeatTimerCallback(); From b5b44da857dfdaefe24e2a6dec3c3ea8cca7852c Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Tue, 16 Apr 2024 09:35:03 +0200 Subject: [PATCH 03/40] Imported JUCE customizations for Acon Digital products --- .../audio_io/juce_AudioDeviceManager.cpp | 5 +- .../juce_audio_plugin_client_AAX.cpp | 61 +++++++++++++++++ .../juce_audio_plugin_client_VST2.cpp | 13 +++- .../format_types/juce_AudioUnitPluginFormat.h | 15 +++++ .../juce_AudioUnitPluginFormat.mm | 66 +++++++++++++++++++ .../format_types/juce_VSTPluginFormat.cpp | 5 +- .../juce_gui_basics/buttons/juce_Button.cpp | 10 ++- modules/juce_gui_basics/buttons/juce_Button.h | 4 ++ .../juce_ApplicationCommandManager.cpp | 12 ++-- .../drawables/juce_SVGParser.cpp | 4 +- .../keyboard/juce_KeyPress.cpp | 14 ++-- .../juce_gui_basics/widgets/juce_ComboBox.cpp | 14 +++- .../juce_gui_basics/widgets/juce_ComboBox.h | 5 ++ .../juce_gui_basics/widgets/juce_Label.cpp | 4 ++ .../widgets/juce_TextEditor.cpp | 14 +++- .../juce_gui_basics/widgets/juce_TextEditor.h | 5 ++ 16 files changed, 231 insertions(+), 20 deletions(-) diff --git a/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp b/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp index c0c9537f93fe..50b23f88d699 100644 --- a/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp +++ b/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp @@ -248,8 +248,9 @@ static void addIfNotNull (OwnedArray& list, AudioIODeviceType void AudioDeviceManager::createAudioDeviceTypes (OwnedArray& list) { addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_WASAPI (WASAPIDeviceMode::shared)); - addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_WASAPI (WASAPIDeviceMode::exclusive)); - addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_WASAPI (WASAPIDeviceMode::sharedLowLatency)); + // Acon Digital modification - no need for special WASAPI modes + //addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_WASAPI (WASAPIDeviceMode::exclusive)); + //addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_WASAPI (WASAPIDeviceMode::sharedLowLatency)); addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_DirectSound()); addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_ASIO()); addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_CoreAudio()); 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 afc15f9352fb..31654b060df7 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 @@ -96,6 +96,11 @@ static_assert (AAX_SDK_CURRENT_REVISION >= AAX_SDK_2p4p0_REVISION, "JUCE require #include #include +#if JucePlugin_Enable_ARA +#include +#include +#endif + JUCE_END_IGNORE_WARNINGS_MSVC JUCE_END_IGNORE_WARNINGS_GCC_LIKE @@ -734,6 +739,12 @@ namespace AAXClasses void mouseUp (const MouseEvent& e) override { callMouseMethod (e, &AAX_IViewContainer::HandleParameterMouseUp); } void mouseDrag (const MouseEvent& e) override { callMouseMethod (e, &AAX_IViewContainer::HandleParameterMouseDrag); } + void resized() override + { + if (pluginEditor != nullptr) + pluginEditor->setBounds (getLocalBounds()); + } + void parentSizeChanged() override { resizeHostWindow(); @@ -858,6 +869,14 @@ namespace AAXClasses return new JuceAAX_Processor(); } + #if JucePlugin_Enable_ARA + AAX_Result Initialize (IACFUnknown* iController) override + { + aaxaraBinding.reset (new ARA::AAX_VARABinding (iController)); + return AAX_CEffectParameters::Initialize (iController); + } + #endif + AAX_Result Uninitialize() override { cancelPendingUpdate(); @@ -877,6 +896,36 @@ namespace AAXClasses AAX_Result EffectInit() override { cancelPendingUpdate(); + + #if JucePlugin_Enable_ARA + ARA::ARAPlugInInstanceRoleFlags knownRoles = 0; + auto result = aaxaraBinding->GetInstanceRoleFlags (&knownRoles, &assignedRoles); + + // If no ARA roles provided - work as a regular AAX plug-in + if (result == AAX_SUCCESS) + { + assignedRoles &= (ARA::kARAPlaybackRendererRole | ARA::kARAEditorRendererRole | ARA::kARAEditorViewRole); + if (assignedRoles) + { + result = aaxaraBinding->GetDocumentController (&documentControllerRef); + if (result != AAX_SUCCESS) + return result; + + auto araPluginExtension = dynamic_cast (pluginInstance.get()); + if (!araPluginExtension) + return AAX_ERROR_NULL_OBJECT; + + auto* const plugInEnxtensionInstance = araPluginExtension->bindToARA (documentControllerRef, knownRoles, assignedRoles); + if (!plugInEnxtensionInstance) + return AAX_ERROR_NULL_OBJECT; + + result = aaxaraBinding->SetPlugInExtensionInstance (plugInEnxtensionInstance); + if (result != AAX_SUCCESS) + return result; + } + } + #endif + check (Controller()->GetSampleRate (&sampleRate)); processingSidechainChange = false; auto err = preparePlugin(); @@ -2301,6 +2350,12 @@ namespace AAXClasses std::atomic state { 0 }; }; + #if JucePlugin_Enable_ARA + std::unique_ptr aaxaraBinding; + ARA::ARADocumentControllerRef documentControllerRef; + ARA::ARAPlugInInstanceRoleFlags assignedRoles; + #endif + RecordingState recordingState; std::atomic processingSidechainChange, sidechainDesired; @@ -2568,6 +2623,12 @@ namespace AAXClasses } } + #if JucePlugin_Enable_ARA + properties->AddProperty (AAX_eProperty_UsesTransport, true); + properties->AddProperty (AAX_eProperty_Constraint_Topology, AAX_eConstraintTopology_Monolithic); + properties->AddPointerProperty (ARA::AAX_eProperty_ARAFactoryPointer, createARAFactory()); + #endif + check (desc.AddProcessProc_Native (algorithmProcessCallback, properties)); } 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 6a865e32ef18..323a2e44cd9b 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 @@ -982,7 +982,18 @@ class JuceVSTWrapper final : public AudioProcessorListener, void getEditorBounds (Vst2::ERect& bounds) { auto editorBounds = getSizeToContainChild(); - bounds = convertToHostBounds ({ 0, 0, (int16) editorBounds.getHeight(), (int16) editorBounds.getWidth() }); + + // Acon Digital modification - circumvention of VST hosting error in EDIUS when runnng high-DPI mode + String hostPath = File::getSpecialLocation (File::hostApplicationPath).getFileNameWithoutExtension(); + if (hostPath.containsIgnoreCase ("edius")) { + auto mainDisplay = Desktop::getInstance().getDisplays().getPrimaryDisplay(); + float desktopScale = mainDisplay != nullptr ? mainDisplay->dpi / 96.f : 1.f; + float ediusCorrectionScale = 1.f + (desktopScale - 1.f) / desktopScale; + bounds = { 0, 0, (int16) roundToInt (editorBounds.getHeight() * ediusCorrectionScale), + (int16) roundToInt (editorBounds.getWidth() * ediusCorrectionScale) }; + } + else + bounds = convertToHostBounds ({ 0, 0, (int16) editorBounds.getHeight(), (int16) editorBounds.getWidth() }); } void attachToHost (VstOpCodeArguments args) diff --git a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.h b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.h index c154ab08430d..4fd32ae733b4 100644 --- a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.h +++ b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.h @@ -56,6 +56,21 @@ class JUCE_API AudioUnitPluginFormat : public AudioPluginFormat bool canScanForPlugins() const override { return true; } bool isTrivialToScan() const override { return false; } + //============================================================================== + /** Attempts to reload an AU plugin's state from some preset file data. + + @see VSTPluginFormat::loadFromFXBFile + */ + static bool setStateFromAUPresetFile (AudioPluginInstance*, const MemoryBlock&); + + //============================================================================== + /** Attempts to save an AU plugin's state to some preset file data. + + @see VSTPluginFormat::saveToFXBFile + */ + static bool saveStateToAUPresetFile (AudioPluginInstance*, MemoryBlock&); + + //============================================================================== void findAllTypesForFile (OwnedArray&, const String& fileOrIdentifier) override; bool fileMightContainThisPluginType (const String& fileOrIdentifier) override; String getNameOfPluginFromIdentifier (const String& fileOrIdentifier) override; diff --git a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm index db22b68675bb..b31944b44f13 100644 --- a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm +++ b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm @@ -2830,6 +2830,72 @@ void messageCallback() override { } +bool AudioUnitPluginFormat::setStateFromAUPresetFile (AudioPluginInstance* api, const MemoryBlock& rawData) +{ + AudioUnit audioUnit = (AudioUnit) api->getPlatformSpecificData(); + jassert (audioUnit != nullptr); + + CFReadStreamRef stream = CFReadStreamCreateWithBytesNoCopy (kCFAllocatorDefault, (const UInt8*) rawData.getData(), + rawData.getSize(), kCFAllocatorNull); + CFReadStreamOpen (stream); + + CFPropertyListFormat format = kCFPropertyListBinaryFormat_v1_0; + CFPropertyListRef propertyList = CFPropertyListCreateFromStream (kCFAllocatorDefault, stream, 0, + kCFPropertyListImmutable, &format, 0); + CFRelease (stream); + + if (propertyList != 0) + { + AudioUnitSetProperty (audioUnit, kAudioUnitProperty_ClassInfo, kAudioUnitScope_Global, + 0, &propertyList, sizeof (propertyList)); + + AudioUnitParameter param; + param.mAudioUnit = audioUnit; + param.mParameterID = kAUParameterListener_AnyParameter; + + AUParameterListenerNotify (nullptr, nullptr, ¶m); + + CFRelease (propertyList); + return true; + } + else + return false; +} + +bool AudioUnitPluginFormat::saveStateToAUPresetFile (AudioPluginInstance* api, MemoryBlock& rawData) +{ + AudioUnit audioUnit = (AudioUnit) api->getPlatformSpecificData(); + jassert (audioUnit != nullptr); + + CFPropertyListRef propertyList = 0; + UInt32 sz = sizeof (CFPropertyListRef); + + if (AudioUnitGetProperty (audioUnit, + kAudioUnitProperty_ClassInfo, + kAudioUnitScope_Global, + 0, &propertyList, &sz) == noErr) + { + CFWriteStreamRef stream = CFWriteStreamCreateWithAllocatedBuffers (kCFAllocatorDefault, kCFAllocatorDefault); + CFWriteStreamOpen (stream); + + CFIndex bytesWritten = CFPropertyListWriteToStream (propertyList, stream, kCFPropertyListBinaryFormat_v1_0, 0); + CFWriteStreamClose (stream); + + CFDataRef data = (CFDataRef) CFWriteStreamCopyProperty (stream, kCFStreamPropertyDataWritten); + + rawData.setSize ((size_t) bytesWritten); + rawData.copyFrom (CFDataGetBytePtr (data), 0, bytesWritten); + CFRelease (data); + + CFRelease (stream); + CFRelease (propertyList); + + return true; + } + else + return false; +} + void AudioUnitPluginFormat::findAllTypesForFile (OwnedArray& results, const String& fileOrIdentifier) { diff --git a/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp b/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp index d231fae240ed..7640012a1e7c 100644 --- a/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp +++ b/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp @@ -1893,14 +1893,15 @@ struct VSTPluginInstance final : public AudioPluginInstance, auto totalLen = sizeof (fxProgramSet) + chunk.getSize() - 8; dest.setSize (totalLen, true); + // Acon Digital modification - fixed errors in FXB format auto set = (fxProgramSet*) dest.getData(); set->chunkMagic = fxbName ("CcnK"); - set->byteSize = 0; + set->byteSize = fxbSwap ((int) totalLen - 8); set->fxMagic = fxbName ("FPCh"); set->version = fxbSwap (fxbVersionNum); set->fxID = fxbSwap (getUID()); set->fxVersion = fxbSwap (getVersionNumber()); - set->numPrograms = fxbSwap (numPrograms); + set->numPrograms = fxbSwap (getNumParameters()); set->chunkSize = fxbSwap ((int32) chunk.getSize()); getCurrentProgramName().copyToUTF8 (set->name, sizeof (set->name) - 1); diff --git a/modules/juce_gui_basics/buttons/juce_Button.cpp b/modules/juce_gui_basics/buttons/juce_Button.cpp index d06fd225e725..3ccebebbf4a6 100644 --- a/modules/juce_gui_basics/buttons/juce_Button.cpp +++ b/modules/juce_gui_basics/buttons/juce_Button.cpp @@ -518,8 +518,10 @@ bool Button::isMouseSourceOver (const MouseEvent& e) return isMouseOver(); } -void Button::focusGained (FocusChangeType) +void Button::focusGained (FocusChangeType cause) { + // Acon Digital modification - keep track of focus change cause + lastFocusChangeCause = cause; updateState(); repaint(); } @@ -673,6 +675,12 @@ bool Button::keyPressed (const KeyPress& key) return false; } +// Acon Digital modification - keep track of focus change cause +Component::FocusChangeType Button::getLastFocusChangeCause() +{ + return lastFocusChangeCause; +} + //============================================================================== void Button::setRepeatSpeed (int initialDelayMillisecs, int repeatMillisecs, diff --git a/modules/juce_gui_basics/buttons/juce_Button.h b/modules/juce_gui_basics/buttons/juce_Button.h index 151a1fc6eaa5..ca868c4817f0 100644 --- a/modules/juce_gui_basics/buttons/juce_Button.h +++ b/modules/juce_gui_basics/buttons/juce_Button.h @@ -393,6 +393,10 @@ class JUCE_API Button : public Component, /** Returns the button's current over/down/up state. */ ButtonState getState() const noexcept { return buttonState; } + + /** Acon Digital modification - keep track of focus change cause */ + FocusChangeType getLastFocusChangeCause(); + //============================================================================== /** This abstract base class is implemented by LookAndFeel classes to provide button-drawing functionality. diff --git a/modules/juce_gui_basics/commands/juce_ApplicationCommandManager.cpp b/modules/juce_gui_basics/commands/juce_ApplicationCommandManager.cpp index 56818d28e8fb..e1b58858604e 100644 --- a/modules/juce_gui_basics/commands/juce_ApplicationCommandManager.cpp +++ b/modules/juce_gui_basics/commands/juce_ApplicationCommandManager.cpp @@ -68,11 +68,13 @@ void ApplicationCommandManager::registerCommand (const ApplicationCommandInfo& n // Trying to re-register the same command ID with different parameters can often indicate a typo. // This assertion is here because I've found it useful catching some mistakes, but it may also cause // false alarms if you're deliberately updating some flags for a command. - jassert (newCommand.shortName == getCommandForID (newCommand.commandID)->shortName - && newCommand.categoryName == getCommandForID (newCommand.commandID)->categoryName - && newCommand.defaultKeypresses == getCommandForID (newCommand.commandID)->defaultKeypresses - && (newCommand.flags & (ApplicationCommandInfo::wantsKeyUpDownCallbacks | ApplicationCommandInfo::hiddenFromKeyEditor | ApplicationCommandInfo::readOnlyInKeyEditor)) - == (getCommandForID (newCommand.commandID)->flags & (ApplicationCommandInfo::wantsKeyUpDownCallbacks | ApplicationCommandInfo::hiddenFromKeyEditor | ApplicationCommandInfo::readOnlyInKeyEditor))); + + /** Acon Digital modification - disable unnecessary assert */ + //jassert (newCommand.shortName == getCommandForID (newCommand.commandID)->shortName + // && newCommand.categoryName == getCommandForID (newCommand.commandID)->categoryName + // && newCommand.defaultKeypresses == getCommandForID (newCommand.commandID)->defaultKeypresses + // && (newCommand.flags & (ApplicationCommandInfo::wantsKeyUpDownCallbacks | ApplicationCommandInfo::hiddenFromKeyEditor | ApplicationCommandInfo::readOnlyInKeyEditor)) + // == (getCommandForID (newCommand.commandID)->flags & (ApplicationCommandInfo::wantsKeyUpDownCallbacks | ApplicationCommandInfo::hiddenFromKeyEditor | ApplicationCommandInfo::readOnlyInKeyEditor))); *command = newCommand; } diff --git a/modules/juce_gui_basics/drawables/juce_SVGParser.cpp b/modules/juce_gui_basics/drawables/juce_SVGParser.cpp index e47823f96270..ae6465a95eac 100644 --- a/modules/juce_gui_basics/drawables/juce_SVGParser.cpp +++ b/modules/juce_gui_basics/drawables/juce_SVGParser.cpp @@ -953,7 +953,9 @@ class SVGState FillType type (gradient); - auto gradientTransform = parseTransform (fillXml->getStringAttribute ("gradientTransform")); + /** Acon Digital modification - fixed error in SVG parsing */ + auto gradientTransform = parseTransform (fillXml->getStringAttribute ("gradientTransform")) + .followedBy (transform); if (gradient.isRadial) { diff --git a/modules/juce_gui_basics/keyboard/juce_KeyPress.cpp b/modules/juce_gui_basics/keyboard/juce_KeyPress.cpp index d3c5877d11d0..fea21b05e9f8 100644 --- a/modules/juce_gui_basics/keyboard/juce_KeyPress.cpp +++ b/modules/juce_gui_basics/keyboard/juce_KeyPress.cpp @@ -185,7 +185,8 @@ KeyPress KeyPress::createFromDescription (const String& desc) int modifiers = 0; for (int i = 0; i < numElementsInArray (KeyPressHelpers::modifierNames); ++i) - if (desc.containsWholeWordIgnoreCase (KeyPressHelpers::modifierNames[i].name)) + /** Acon Digital modification - translate modifier names */ + if (desc.containsWholeWordIgnoreCase (TRANS (KeyPressHelpers::modifierNames[i].name))) modifiers |= KeyPressHelpers::modifierNames[i].flag; int key = 0; @@ -246,14 +247,15 @@ String KeyPress::getTextDescription() const if (textCharacter == '/' && keyCode != numberPadDivide) return "/"; - if (mods.isCtrlDown()) desc << "ctrl + "; - if (mods.isShiftDown()) desc << "shift + "; + /** Acon Digital modification - translate modifier names */ + if (mods.isCtrlDown()) desc << TRANS ("ctrl") + " + "; + if (mods.isShiftDown()) desc << TRANS ("shift") + " + "; #if JUCE_MAC || JUCE_IOS - if (mods.isAltDown()) desc << "option + "; - if (mods.isCommandDown()) desc << "command + "; + if (mods.isAltDown()) desc << TRANS ("option") + " + "; + if (mods.isCommandDown()) desc << TRANS ("command") + " + "; #else - if (mods.isAltDown()) desc << "alt + "; + if (mods.isAltDown()) desc << TRANS ("alt") + " + "; #endif for (int i = 0; i < numElementsInArray (KeyPressHelpers::translations); ++i) diff --git a/modules/juce_gui_basics/widgets/juce_ComboBox.cpp b/modules/juce_gui_basics/widgets/juce_ComboBox.cpp index 047cba863a97..9febb7a6bd65 100644 --- a/modules/juce_gui_basics/widgets/juce_ComboBox.cpp +++ b/modules/juce_gui_basics/widgets/juce_ComboBox.cpp @@ -557,7 +557,19 @@ void ComboBox::showPopup() ModalCallbackFunction::forComponent (comboBoxPopupMenuFinishedCallback, this)); } -//============================================================================== +/** Acon Digital modification - allow for visual mouse over indications */ +void ComboBox::mouseEnter (const MouseEvent& e) +{ + Component::mouseEnter (e); + repaint(); +} + +void ComboBox::mouseExit (const MouseEvent& e) +{ + Component::mouseExit (e); + repaint(); +} + void ComboBox::mouseDown (const MouseEvent& e) { beginDragAutoRepeat (300); diff --git a/modules/juce_gui_basics/widgets/juce_ComboBox.h b/modules/juce_gui_basics/widgets/juce_ComboBox.h index 3e427fbe8c57..06401fbbdaa2 100644 --- a/modules/juce_gui_basics/widgets/juce_ComboBox.h +++ b/modules/juce_gui_basics/widgets/juce_ComboBox.h @@ -406,6 +406,11 @@ class JUCE_API ComboBox : public Component, void handleAsyncUpdate() override; /** @internal */ String getTooltip() override { return label->getTooltip(); } + /** Acon Digital modification - allow for visual mouse over indications */ + /** @internal */ + void mouseEnter (const MouseEvent&) override; + /** @internal */ + void mouseExit (const MouseEvent&) override; /** @internal */ void mouseDown (const MouseEvent&) override; /** @internal */ diff --git a/modules/juce_gui_basics/widgets/juce_Label.cpp b/modules/juce_gui_basics/widgets/juce_Label.cpp index 1db52d49c171..15c67d47c3ff 100644 --- a/modules/juce_gui_basics/widgets/juce_Label.cpp +++ b/modules/juce_gui_basics/widgets/juce_Label.cpp @@ -237,6 +237,10 @@ void Label::showEditor() editor->addListener (this); editor->grabKeyboardFocus(); + /** Acon Digital modification - allow edit boxes with centerred text */ + if (getJustificationType().testFlags (Justification::horizontallyCentred)) + editor->setJustification (Justification::centredTop); + if (editor == nullptr) // may be deleted by a callback return; diff --git a/modules/juce_gui_basics/widgets/juce_TextEditor.cpp b/modules/juce_gui_basics/widgets/juce_TextEditor.cpp index e288039b1ff8..c07b2823eff7 100644 --- a/modules/juce_gui_basics/widgets/juce_TextEditor.cpp +++ b/modules/juce_gui_basics/widgets/juce_TextEditor.cpp @@ -1848,7 +1848,19 @@ void TextEditor::performPopupMenuAction (const int menuItemID) } } -//============================================================================== +/** Acon Digital modification - allow for visual mouse over indications */ +void TextEditor::mouseEnter (const MouseEvent& e) +{ + Component::mouseEnter (e); + repaint(); +} + +void TextEditor::mouseExit (const MouseEvent& e) +{ + Component::mouseExit (e); + repaint(); +} + void TextEditor::mouseDown (const MouseEvent& e) { beginDragAutoRepeat (100); diff --git a/modules/juce_gui_basics/widgets/juce_TextEditor.h b/modules/juce_gui_basics/widgets/juce_TextEditor.h index 044e26c2f085..d8f977aafb5d 100644 --- a/modules/juce_gui_basics/widgets/juce_TextEditor.h +++ b/modules/juce_gui_basics/widgets/juce_TextEditor.h @@ -720,6 +720,11 @@ class JUCE_API TextEditor : public TextInputTarget, void paint (Graphics&) override; /** @internal */ void paintOverChildren (Graphics&) override; + /** Acon Digital modification - allow for visual mouse over indications */ + /** @internal */ + void mouseEnter (const MouseEvent&) override; + /** @internal */ + void mouseExit (const MouseEvent&) override; /** @internal */ void mouseDown (const MouseEvent&) override; /** @internal */ From 38689973546a4a77088c5cdb196b0215061ff77e Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Tue, 16 Apr 2024 12:05:25 +0200 Subject: [PATCH 04/40] Added missing lastFocusChangeCause member in juce_Button class --- modules/juce_gui_basics/buttons/juce_Button.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/juce_gui_basics/buttons/juce_Button.h b/modules/juce_gui_basics/buttons/juce_Button.h index ca868c4817f0..627d73147c39 100644 --- a/modules/juce_gui_basics/buttons/juce_Button.h +++ b/modules/juce_gui_basics/buttons/juce_Button.h @@ -537,6 +537,8 @@ class JUCE_API Button : public Component, bool triggerOnMouseDown = false; bool generateTooltip = false; + /** Acon Digital modification - keep track of focus change cause */ + FocusChangeType lastFocusChangeCause = focusChangedDirectly; void checkToggleableState (bool wasToggleable); void repeatTimerCallback(); From b540c1701f793a07556a6b0926baa8a5cf099cbf Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Tue, 16 Apr 2024 09:35:03 +0200 Subject: [PATCH 05/40] Imported JUCE customizations for Acon Digital products --- .../audio_io/juce_AudioDeviceManager.cpp | 5 +- .../juce_audio_plugin_client_AAX.cpp | 61 +++++++++++++++++ .../juce_audio_plugin_client_VST2.cpp | 13 +++- .../format_types/juce_AudioUnitPluginFormat.h | 15 +++++ .../juce_AudioUnitPluginFormat.mm | 66 +++++++++++++++++++ .../format_types/juce_VSTPluginFormat.cpp | 5 +- .../juce_gui_basics/buttons/juce_Button.cpp | 10 ++- modules/juce_gui_basics/buttons/juce_Button.h | 4 ++ .../juce_ApplicationCommandManager.cpp | 12 ++-- .../drawables/juce_SVGParser.cpp | 4 +- .../keyboard/juce_KeyPress.cpp | 14 ++-- .../native/juce_Windowing_windows.cpp | 2 + .../juce_gui_basics/widgets/juce_ComboBox.cpp | 14 +++- .../juce_gui_basics/widgets/juce_ComboBox.h | 5 ++ .../juce_gui_basics/widgets/juce_Label.cpp | 4 ++ .../widgets/juce_TextEditor.cpp | 14 +++- .../juce_gui_basics/widgets/juce_TextEditor.h | 5 ++ 17 files changed, 233 insertions(+), 20 deletions(-) diff --git a/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp b/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp index 447a240a2559..9f6d1120aaf5 100644 --- a/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp +++ b/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp @@ -236,8 +236,9 @@ static void addIfNotNull (OwnedArray& list, AudioIODeviceType void AudioDeviceManager::createAudioDeviceTypes (OwnedArray& list) { addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_WASAPI (WASAPIDeviceMode::shared)); - addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_WASAPI (WASAPIDeviceMode::exclusive)); - addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_WASAPI (WASAPIDeviceMode::sharedLowLatency)); + // Acon Digital modification - no need for special WASAPI modes + //addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_WASAPI (WASAPIDeviceMode::exclusive)); + //addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_WASAPI (WASAPIDeviceMode::sharedLowLatency)); addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_DirectSound()); addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_ASIO()); addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_CoreAudio()); 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 e8bf84295545..80e6a1a5a64f 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 @@ -85,6 +85,11 @@ static_assert (AAX_SDK_CURRENT_REVISION >= AAX_SDK_2p4p0_REVISION, "JUCE require #include #include +#if JucePlugin_Enable_ARA +#include +#include +#endif + JUCE_END_IGNORE_WARNINGS_MSVC JUCE_END_IGNORE_WARNINGS_GCC_LIKE @@ -723,6 +728,12 @@ namespace AAXClasses void mouseUp (const MouseEvent& e) override { callMouseMethod (e, &AAX_IViewContainer::HandleParameterMouseUp); } void mouseDrag (const MouseEvent& e) override { callMouseMethod (e, &AAX_IViewContainer::HandleParameterMouseDrag); } + void resized() override + { + if (pluginEditor != nullptr) + pluginEditor->setBounds (getLocalBounds()); + } + void parentSizeChanged() override { resizeHostWindow(); @@ -847,6 +858,14 @@ namespace AAXClasses return new JuceAAX_Processor(); } + #if JucePlugin_Enable_ARA + AAX_Result Initialize (IACFUnknown* iController) override + { + aaxaraBinding.reset (new ARA::AAX_VARABinding (iController)); + return AAX_CEffectParameters::Initialize (iController); + } + #endif + AAX_Result Uninitialize() override { cancelPendingUpdate(); @@ -866,6 +885,36 @@ namespace AAXClasses AAX_Result EffectInit() override { cancelPendingUpdate(); + + #if JucePlugin_Enable_ARA + ARA::ARAPlugInInstanceRoleFlags knownRoles = 0; + auto result = aaxaraBinding->GetInstanceRoleFlags (&knownRoles, &assignedRoles); + + // If no ARA roles provided - work as a regular AAX plug-in + if (result == AAX_SUCCESS) + { + assignedRoles &= (ARA::kARAPlaybackRendererRole | ARA::kARAEditorRendererRole | ARA::kARAEditorViewRole); + if (assignedRoles) + { + result = aaxaraBinding->GetDocumentController (&documentControllerRef); + if (result != AAX_SUCCESS) + return result; + + auto araPluginExtension = dynamic_cast (pluginInstance.get()); + if (!araPluginExtension) + return AAX_ERROR_NULL_OBJECT; + + auto* const plugInEnxtensionInstance = araPluginExtension->bindToARA (documentControllerRef, knownRoles, assignedRoles); + if (!plugInEnxtensionInstance) + return AAX_ERROR_NULL_OBJECT; + + result = aaxaraBinding->SetPlugInExtensionInstance (plugInEnxtensionInstance); + if (result != AAX_SUCCESS) + return result; + } + } + #endif + check (Controller()->GetSampleRate (&sampleRate)); processingSidechainChange = false; auto err = preparePlugin(); @@ -2290,6 +2339,12 @@ namespace AAXClasses std::atomic state { 0 }; }; + #if JucePlugin_Enable_ARA + std::unique_ptr aaxaraBinding; + ARA::ARADocumentControllerRef documentControllerRef; + ARA::ARAPlugInInstanceRoleFlags assignedRoles; + #endif + RecordingState recordingState; std::atomic processingSidechainChange, sidechainDesired; @@ -2557,6 +2612,12 @@ namespace AAXClasses } } + #if JucePlugin_Enable_ARA + properties->AddProperty (AAX_eProperty_UsesTransport, true); + properties->AddProperty (AAX_eProperty_Constraint_Topology, AAX_eConstraintTopology_Monolithic); + properties->AddPointerProperty (ARA::AAX_eProperty_ARAFactoryPointer, createARAFactory()); + #endif + check (desc.AddProcessProc_Native (algorithmProcessCallback, properties)); } 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 262f0a00c06a..a414981d060a 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 @@ -972,7 +972,18 @@ class JuceVSTWrapper final : public AudioProcessorListener, void getEditorBounds (Vst2::ERect& bounds) { auto editorBounds = getSizeToContainChild(); - bounds = convertToHostBounds ({ 0, 0, (int16) editorBounds.getHeight(), (int16) editorBounds.getWidth() }); + + // Acon Digital modification - circumvention of VST hosting error in EDIUS when runnng high-DPI mode + String hostPath = File::getSpecialLocation (File::hostApplicationPath).getFileNameWithoutExtension(); + if (hostPath.containsIgnoreCase ("edius")) { + auto mainDisplay = Desktop::getInstance().getDisplays().getPrimaryDisplay(); + float desktopScale = mainDisplay != nullptr ? mainDisplay->dpi / 96.f : 1.f; + float ediusCorrectionScale = 1.f + (desktopScale - 1.f) / desktopScale; + bounds = { 0, 0, (int16) roundToInt (editorBounds.getHeight() * ediusCorrectionScale), + (int16) roundToInt (editorBounds.getWidth() * ediusCorrectionScale) }; + } + else + bounds = convertToHostBounds ({ 0, 0, (int16) editorBounds.getHeight(), (int16) editorBounds.getWidth() }); } void attachToHost (VstOpCodeArguments args) diff --git a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.h b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.h index 162484190fc3..fec96649db0e 100644 --- a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.h +++ b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.h @@ -47,6 +47,21 @@ class JUCE_API AudioUnitPluginFormat : public AudioPluginFormat bool canScanForPlugins() const override { return true; } bool isTrivialToScan() const override { return false; } + //============================================================================== + /** Attempts to reload an AU plugin's state from some preset file data. + + @see VSTPluginFormat::loadFromFXBFile + */ + static bool setStateFromAUPresetFile (AudioPluginInstance*, const MemoryBlock&); + + //============================================================================== + /** Attempts to save an AU plugin's state to some preset file data. + + @see VSTPluginFormat::saveToFXBFile + */ + static bool saveStateToAUPresetFile (AudioPluginInstance*, MemoryBlock&); + + //============================================================================== void findAllTypesForFile (OwnedArray&, const String& fileOrIdentifier) override; bool fileMightContainThisPluginType (const String& fileOrIdentifier) override; String getNameOfPluginFromIdentifier (const String& fileOrIdentifier) override; diff --git a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm index 4977541ae568..01092f27ba00 100644 --- a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm +++ b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm @@ -2796,6 +2796,72 @@ void messageCallback() override { } +bool AudioUnitPluginFormat::setStateFromAUPresetFile (AudioPluginInstance* api, const MemoryBlock& rawData) +{ + AudioUnit audioUnit = (AudioUnit) api->getPlatformSpecificData(); + jassert (audioUnit != nullptr); + + CFReadStreamRef stream = CFReadStreamCreateWithBytesNoCopy (kCFAllocatorDefault, (const UInt8*) rawData.getData(), + rawData.getSize(), kCFAllocatorNull); + CFReadStreamOpen (stream); + + CFPropertyListFormat format = kCFPropertyListBinaryFormat_v1_0; + CFPropertyListRef propertyList = CFPropertyListCreateFromStream (kCFAllocatorDefault, stream, 0, + kCFPropertyListImmutable, &format, 0); + CFRelease (stream); + + if (propertyList != 0) + { + AudioUnitSetProperty (audioUnit, kAudioUnitProperty_ClassInfo, kAudioUnitScope_Global, + 0, &propertyList, sizeof (propertyList)); + + AudioUnitParameter param; + param.mAudioUnit = audioUnit; + param.mParameterID = kAUParameterListener_AnyParameter; + + AUParameterListenerNotify (nullptr, nullptr, ¶m); + + CFRelease (propertyList); + return true; + } + else + return false; +} + +bool AudioUnitPluginFormat::saveStateToAUPresetFile (AudioPluginInstance* api, MemoryBlock& rawData) +{ + AudioUnit audioUnit = (AudioUnit) api->getPlatformSpecificData(); + jassert (audioUnit != nullptr); + + CFPropertyListRef propertyList = 0; + UInt32 sz = sizeof (CFPropertyListRef); + + if (AudioUnitGetProperty (audioUnit, + kAudioUnitProperty_ClassInfo, + kAudioUnitScope_Global, + 0, &propertyList, &sz) == noErr) + { + CFWriteStreamRef stream = CFWriteStreamCreateWithAllocatedBuffers (kCFAllocatorDefault, kCFAllocatorDefault); + CFWriteStreamOpen (stream); + + CFIndex bytesWritten = CFPropertyListWriteToStream (propertyList, stream, kCFPropertyListBinaryFormat_v1_0, 0); + CFWriteStreamClose (stream); + + CFDataRef data = (CFDataRef) CFWriteStreamCopyProperty (stream, kCFStreamPropertyDataWritten); + + rawData.setSize ((size_t) bytesWritten); + rawData.copyFrom (CFDataGetBytePtr (data), 0, bytesWritten); + CFRelease (data); + + CFRelease (stream); + CFRelease (propertyList); + + return true; + } + else + return false; +} + void AudioUnitPluginFormat::findAllTypesForFile (OwnedArray& results, const String& fileOrIdentifier) { diff --git a/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp b/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp index d08e11f5daeb..f60c00fdde61 100644 --- a/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp +++ b/modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp @@ -1884,14 +1884,15 @@ struct VSTPluginInstance final : public AudioPluginInstance, auto totalLen = sizeof (fxProgramSet) + chunk.getSize() - 8; dest.setSize (totalLen, true); + // Acon Digital modification - fixed errors in FXB format auto set = (fxProgramSet*) dest.getData(); set->chunkMagic = fxbName ("CcnK"); - set->byteSize = 0; + set->byteSize = fxbSwap ((int) totalLen - 8); set->fxMagic = fxbName ("FPCh"); set->version = fxbSwap (fxbVersionNum); set->fxID = fxbSwap (getUID()); set->fxVersion = fxbSwap (getVersionNumber()); - set->numPrograms = fxbSwap (numPrograms); + set->numPrograms = fxbSwap (getNumParameters()); set->chunkSize = fxbSwap ((int32) chunk.getSize()); getCurrentProgramName().copyToUTF8 (set->name, sizeof (set->name) - 1); diff --git a/modules/juce_gui_basics/buttons/juce_Button.cpp b/modules/juce_gui_basics/buttons/juce_Button.cpp index 02818c511d38..949a076f031e 100644 --- a/modules/juce_gui_basics/buttons/juce_Button.cpp +++ b/modules/juce_gui_basics/buttons/juce_Button.cpp @@ -509,8 +509,10 @@ bool Button::isMouseSourceOver (const MouseEvent& e) return isMouseOver(); } -void Button::focusGained (FocusChangeType) +void Button::focusGained (FocusChangeType cause) { + // Acon Digital modification - keep track of focus change cause + lastFocusChangeCause = cause; updateState(); repaint(); } @@ -664,6 +666,12 @@ bool Button::keyPressed (const KeyPress& key) return false; } +// Acon Digital modification - keep track of focus change cause +Component::FocusChangeType Button::getLastFocusChangeCause() +{ + return lastFocusChangeCause; +} + //============================================================================== void Button::setRepeatSpeed (int initialDelayMillisecs, int repeatMillisecs, diff --git a/modules/juce_gui_basics/buttons/juce_Button.h b/modules/juce_gui_basics/buttons/juce_Button.h index e9878fb09f0f..6b50cd8a6ba6 100644 --- a/modules/juce_gui_basics/buttons/juce_Button.h +++ b/modules/juce_gui_basics/buttons/juce_Button.h @@ -384,6 +384,10 @@ class JUCE_API Button : public Component, /** Returns the button's current over/down/up state. */ ButtonState getState() const noexcept { return buttonState; } + + /** Acon Digital modification - keep track of focus change cause */ + FocusChangeType getLastFocusChangeCause(); + //============================================================================== /** This abstract base class is implemented by LookAndFeel classes to provide button-drawing functionality. diff --git a/modules/juce_gui_basics/commands/juce_ApplicationCommandManager.cpp b/modules/juce_gui_basics/commands/juce_ApplicationCommandManager.cpp index f45e32bfd5e1..15315b61a7ca 100644 --- a/modules/juce_gui_basics/commands/juce_ApplicationCommandManager.cpp +++ b/modules/juce_gui_basics/commands/juce_ApplicationCommandManager.cpp @@ -59,11 +59,13 @@ void ApplicationCommandManager::registerCommand (const ApplicationCommandInfo& n // Trying to re-register the same command ID with different parameters can often indicate a typo. // This assertion is here because I've found it useful catching some mistakes, but it may also cause // false alarms if you're deliberately updating some flags for a command. - jassert (newCommand.shortName == getCommandForID (newCommand.commandID)->shortName - && newCommand.categoryName == getCommandForID (newCommand.commandID)->categoryName - && newCommand.defaultKeypresses == getCommandForID (newCommand.commandID)->defaultKeypresses - && (newCommand.flags & (ApplicationCommandInfo::wantsKeyUpDownCallbacks | ApplicationCommandInfo::hiddenFromKeyEditor | ApplicationCommandInfo::readOnlyInKeyEditor)) - == (getCommandForID (newCommand.commandID)->flags & (ApplicationCommandInfo::wantsKeyUpDownCallbacks | ApplicationCommandInfo::hiddenFromKeyEditor | ApplicationCommandInfo::readOnlyInKeyEditor))); + + /** Acon Digital modification - disable unnecessary assert */ + //jassert (newCommand.shortName == getCommandForID (newCommand.commandID)->shortName + // && newCommand.categoryName == getCommandForID (newCommand.commandID)->categoryName + // && newCommand.defaultKeypresses == getCommandForID (newCommand.commandID)->defaultKeypresses + // && (newCommand.flags & (ApplicationCommandInfo::wantsKeyUpDownCallbacks | ApplicationCommandInfo::hiddenFromKeyEditor | ApplicationCommandInfo::readOnlyInKeyEditor)) + // == (getCommandForID (newCommand.commandID)->flags & (ApplicationCommandInfo::wantsKeyUpDownCallbacks | ApplicationCommandInfo::hiddenFromKeyEditor | ApplicationCommandInfo::readOnlyInKeyEditor))); *command = newCommand; } diff --git a/modules/juce_gui_basics/drawables/juce_SVGParser.cpp b/modules/juce_gui_basics/drawables/juce_SVGParser.cpp index 5fec572261ad..858e7c12a0fe 100644 --- a/modules/juce_gui_basics/drawables/juce_SVGParser.cpp +++ b/modules/juce_gui_basics/drawables/juce_SVGParser.cpp @@ -944,7 +944,9 @@ class SVGState FillType type (gradient); - auto gradientTransform = parseTransform (fillXml->getStringAttribute ("gradientTransform")); + /** Acon Digital modification - fixed error in SVG parsing */ + auto gradientTransform = parseTransform (fillXml->getStringAttribute ("gradientTransform")) + .followedBy (transform); if (gradient.isRadial) { diff --git a/modules/juce_gui_basics/keyboard/juce_KeyPress.cpp b/modules/juce_gui_basics/keyboard/juce_KeyPress.cpp index 95645ec45705..368b4626d45b 100644 --- a/modules/juce_gui_basics/keyboard/juce_KeyPress.cpp +++ b/modules/juce_gui_basics/keyboard/juce_KeyPress.cpp @@ -176,7 +176,8 @@ KeyPress KeyPress::createFromDescription (const String& desc) int modifiers = 0; for (int i = 0; i < numElementsInArray (KeyPressHelpers::modifierNames); ++i) - if (desc.containsWholeWordIgnoreCase (KeyPressHelpers::modifierNames[i].name)) + /** Acon Digital modification - translate modifier names */ + if (desc.containsWholeWordIgnoreCase (TRANS (KeyPressHelpers::modifierNames[i].name))) modifiers |= KeyPressHelpers::modifierNames[i].flag; int key = 0; @@ -237,14 +238,15 @@ String KeyPress::getTextDescription() const if (textCharacter == '/' && keyCode != numberPadDivide) return "/"; - if (mods.isCtrlDown()) desc << "ctrl + "; - if (mods.isShiftDown()) desc << "shift + "; + /** Acon Digital modification - translate modifier names */ + if (mods.isCtrlDown()) desc << TRANS ("ctrl") + " + "; + if (mods.isShiftDown()) desc << TRANS ("shift") + " + "; #if JUCE_MAC || JUCE_IOS - if (mods.isAltDown()) desc << "option + "; - if (mods.isCommandDown()) desc << "command + "; + if (mods.isAltDown()) desc << TRANS ("option") + " + "; + if (mods.isCommandDown()) desc << TRANS ("command") + " + "; #else - if (mods.isAltDown()) desc << "alt + "; + if (mods.isAltDown()) desc << TRANS ("alt") + " + "; #endif for (int i = 0; i < numElementsInArray (KeyPressHelpers::translations); ++i) diff --git a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp index f898fa3c2060..fb1ea86617a2 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -2085,6 +2085,8 @@ class HWNDComponentPeer final : public ComponentPeer, void repaint (const Rectangle& area) override { deferredRepaints.add ((area.toDouble() * getPlatformScaleFactor()).getSmallestIntegerContainer()); + /** Acon Digital modification - fix issues with freezed UI */ + dispatchDeferredRepaints(); } void dispatchDeferredRepaints() diff --git a/modules/juce_gui_basics/widgets/juce_ComboBox.cpp b/modules/juce_gui_basics/widgets/juce_ComboBox.cpp index bcedfe3c797e..e670545a06c8 100644 --- a/modules/juce_gui_basics/widgets/juce_ComboBox.cpp +++ b/modules/juce_gui_basics/widgets/juce_ComboBox.cpp @@ -548,7 +548,19 @@ void ComboBox::showPopup() ModalCallbackFunction::forComponent (comboBoxPopupMenuFinishedCallback, this)); } -//============================================================================== +/** Acon Digital modification - allow for visual mouse over indications */ +void ComboBox::mouseEnter (const MouseEvent& e) +{ + Component::mouseEnter (e); + repaint(); +} + +void ComboBox::mouseExit (const MouseEvent& e) +{ + Component::mouseExit (e); + repaint(); +} + void ComboBox::mouseDown (const MouseEvent& e) { beginDragAutoRepeat (300); diff --git a/modules/juce_gui_basics/widgets/juce_ComboBox.h b/modules/juce_gui_basics/widgets/juce_ComboBox.h index 15ee5fabd294..785a46e6113d 100644 --- a/modules/juce_gui_basics/widgets/juce_ComboBox.h +++ b/modules/juce_gui_basics/widgets/juce_ComboBox.h @@ -397,6 +397,11 @@ class JUCE_API ComboBox : public Component, void handleAsyncUpdate() override; /** @internal */ String getTooltip() override { return label->getTooltip(); } + /** Acon Digital modification - allow for visual mouse over indications */ + /** @internal */ + void mouseEnter (const MouseEvent&) override; + /** @internal */ + void mouseExit (const MouseEvent&) override; /** @internal */ void mouseDown (const MouseEvent&) override; /** @internal */ diff --git a/modules/juce_gui_basics/widgets/juce_Label.cpp b/modules/juce_gui_basics/widgets/juce_Label.cpp index 31dad478f551..f26049eabb3d 100644 --- a/modules/juce_gui_basics/widgets/juce_Label.cpp +++ b/modules/juce_gui_basics/widgets/juce_Label.cpp @@ -228,6 +228,10 @@ void Label::showEditor() editor->addListener (this); editor->grabKeyboardFocus(); + /** Acon Digital modification - allow edit boxes with centerred text */ + if (getJustificationType().testFlags (Justification::horizontallyCentred)) + editor->setJustification (Justification::centredTop); + if (editor == nullptr) // may be deleted by a callback return; diff --git a/modules/juce_gui_basics/widgets/juce_TextEditor.cpp b/modules/juce_gui_basics/widgets/juce_TextEditor.cpp index c9360ed92327..fc92fc47d9d7 100644 --- a/modules/juce_gui_basics/widgets/juce_TextEditor.cpp +++ b/modules/juce_gui_basics/widgets/juce_TextEditor.cpp @@ -1839,7 +1839,19 @@ void TextEditor::performPopupMenuAction (const int menuItemID) } } -//============================================================================== +/** Acon Digital modification - allow for visual mouse over indications */ +void TextEditor::mouseEnter (const MouseEvent& e) +{ + Component::mouseEnter (e); + repaint(); +} + +void TextEditor::mouseExit (const MouseEvent& e) +{ + Component::mouseExit (e); + repaint(); +} + void TextEditor::mouseDown (const MouseEvent& e) { beginDragAutoRepeat (100); diff --git a/modules/juce_gui_basics/widgets/juce_TextEditor.h b/modules/juce_gui_basics/widgets/juce_TextEditor.h index c30decc618c9..8f84d28068c1 100644 --- a/modules/juce_gui_basics/widgets/juce_TextEditor.h +++ b/modules/juce_gui_basics/widgets/juce_TextEditor.h @@ -711,6 +711,11 @@ class JUCE_API TextEditor : public TextInputTarget, void paint (Graphics&) override; /** @internal */ void paintOverChildren (Graphics&) override; + /** Acon Digital modification - allow for visual mouse over indications */ + /** @internal */ + void mouseEnter (const MouseEvent&) override; + /** @internal */ + void mouseExit (const MouseEvent&) override; /** @internal */ void mouseDown (const MouseEvent&) override; /** @internal */ From 3b53f99c405407067fcd481d053b6ec8d78cf395 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Tue, 16 Apr 2024 12:05:25 +0200 Subject: [PATCH 06/40] Added missing lastFocusChangeCause member in juce_Button class --- modules/juce_gui_basics/buttons/juce_Button.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/juce_gui_basics/buttons/juce_Button.h b/modules/juce_gui_basics/buttons/juce_Button.h index 6b50cd8a6ba6..d6195a881a4d 100644 --- a/modules/juce_gui_basics/buttons/juce_Button.h +++ b/modules/juce_gui_basics/buttons/juce_Button.h @@ -528,6 +528,8 @@ class JUCE_API Button : public Component, bool triggerOnMouseDown = false; bool generateTooltip = false; + /** Acon Digital modification - keep track of focus change cause */ + FocusChangeType lastFocusChangeCause = focusChangedDirectly; void checkToggleableState (bool wasToggleable); void repeatTimerCallback(); From eaa2cec6fef5b62c81e501f085df683eac47c810 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Wed, 17 Jul 2024 12:59:37 +0200 Subject: [PATCH 07/40] Updated Pro Tools version check Request from AVID: One requirement for integration of your new ARA products into Pro Tools 2024.6 and later is that the products should not be shown in earlier releases of Pro Tools. Previously we supplied some example code that uses a query of the host version number to determine whether or not to show the plugin. This approach was flawed and could result in the plugin not appearing properly, so we have provided an updated example demonstrating the correct approach. The new example uses a feature query to determine whether the host supports the ARA feature for AAX. This query will indicate that earlier versions of Pro Tools do not support the ARA feature. --- .../juce_audio_plugin_client_AAX.cpp | 34 +++++++++++++++++++ 1 file changed, 34 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 80e6a1a5a64f..ac3706345565 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 @@ -2727,6 +2727,35 @@ void AAX_CALLBACK AAXClasses::algorithmProcessCallback (JUCEAlgorithmContext* co AAXClasses::JuceAAX_Processor::algorithmCallback (instancesBegin, instancesEnd); } +static bool HostDeclaresARASupport(AAX_ICollection* inCollection) +{ + bool result = false; + const AAX_IDescriptionHost* const hostDesc = inCollection->DescriptionHost(); + if (hostDesc) + { + std::unique_ptr featureInfo(hostDesc->AcquireFeatureProperties(AAXATTR_ClientFeature_ARA)); + if (featureInfo) + { + AAX_ESupportLevel featureSupportLevel = AAX_eSupportLevel_Uninitialized; + const AAX_Result err = featureInfo->SupportLevel(featureSupportLevel); + AAX_TRACE_RELEASE(kAAX_Trace_Priority_Normal, "ARA initialization: host support for ARA: %s, err: %ld", + AAX::AsStringSupportLevel(featureSupportLevel).c_str(), (long int)err); + result = (err == AAX_SUCCESS) && (featureSupportLevel == AAX_eSupportLevel_Supported); + } + else + { + AAX_TRACE_RELEASE(kAAX_Trace_Priority_Normal, "ARA initialization: Unable to query AAXATTR_ClientFeature_ARA"); + result = false; + } + } + else + { + AAX_TRACE_RELEASE(kAAX_Trace_Priority_High, "ARA initialization: AAX_IDescriptionHost is null"); + result = false; + } + return result; +} + //============================================================================== AAX_Result JUCE_CDECL GetEffectDescriptions (AAX_ICollection*); AAX_Result JUCE_CDECL GetEffectDescriptions (AAX_ICollection* collection) @@ -2740,6 +2769,11 @@ AAX_Result JUCE_CDECL GetEffectDescriptions (AAX_ICollection* collection) if (auto* descriptor = collection->NewDescriptor()) { + #if JucePlugin_Enable_ARA + if (!HostDeclaresARASupport(collection)) + return AAX_SUCCESS; + #endif + AAXClasses::getPlugInDescription (*descriptor, stemFormatFeatureInfo.get()); collection->AddEffect (JUCE_STRINGIFY (JucePlugin_AAXIdentifier), descriptor); From a8a0c32845bdc525a5891f6105f813e87aba6045 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Fri, 13 Sep 2024 17:11:18 +0200 Subject: [PATCH 08/40] Fixed: ARA plug-in should not show up as a plugin variant in AudioSuite or AAX insert menus (XACONDIGIT-10) --- .../juce_audio_plugin_client/juce_audio_plugin_client_AAX.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 ac3706345565..92beb012a9cd 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 @@ -2550,7 +2550,7 @@ namespace AAXClasses properties->AddProperty (AAX_eProperty_PlugInID_Native, pluginID); - #if ! JucePlugin_AAXDisableAudioSuite + #if ! (JucePlugin_AAXDisableAudioSuite || JucePlugin_Enable_ARA) properties->AddProperty (AAX_eProperty_PlugInID_AudioSuite, extensions.getPluginIDForMainBusConfig (fullLayout.getMainInputChannelSet(), fullLayout.getMainOutputChannelSet(), @@ -2613,6 +2613,7 @@ namespace AAXClasses } #if JucePlugin_Enable_ARA + properties->AddProperty (AAX_eProperty_ShowInMenus, false); properties->AddProperty (AAX_eProperty_UsesTransport, true); properties->AddProperty (AAX_eProperty_Constraint_Topology, AAX_eConstraintTopology_Monolithic); properties->AddPointerProperty (ARA::AAX_eProperty_ARAFactoryPointer, createARAFactory()); From e80b89cb1d9b9313861300b1090fa2f8019a0cf1 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Wed, 18 Sep 2024 15:33:16 +0200 Subject: [PATCH 09/40] Fixed incorrect include statement after updating to ARA 2.3 --- .../juce_audio_plugin_client/juce_audio_plugin_client_ARA.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/juce_audio_plugin_client/juce_audio_plugin_client_ARA.cpp b/modules/juce_audio_plugin_client/juce_audio_plugin_client_ARA.cpp index 1692ee52469f..06c99721fa87 100644 --- a/modules/juce_audio_plugin_client/juce_audio_plugin_client_ARA.cpp +++ b/modules/juce_audio_plugin_client/juce_audio_plugin_client_ARA.cpp @@ -40,7 +40,7 @@ JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4100) #include #include #include -#include +#include JUCE_END_IGNORE_WARNINGS_MSVC JUCE_END_IGNORE_WARNINGS_GCC_LIKE From 8a969f004514feb642d0a7c06a028f08b7c03406 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Fri, 27 Sep 2024 11:31:32 +0200 Subject: [PATCH 10/40] Set highest supported ARA version to 2.3 Final --- .../utilities/ARA/juce_ARADocumentController.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/juce_audio_processors/utilities/ARA/juce_ARADocumentController.h b/modules/juce_audio_processors/utilities/ARA/juce_ARADocumentController.h index c9e315f17548..a7e03488e661 100644 --- a/modules/juce_audio_processors/utilities/ARA/juce_ARADocumentController.h +++ b/modules/juce_audio_processors/utilities/ARA/juce_ARADocumentController.h @@ -428,6 +428,8 @@ class ARADocumentControllerSpecialisation : public ARADocument::Listener, const char* getVersion() const noexcept override { return JucePlugin_VersionString; } const char* getDocumentArchiveID() const noexcept override { return JucePlugin_ARADocumentArchiveID; } + ARA::ARAAPIGeneration getHighestSupportedApiGeneration() const noexcept override { return ARA::kARAAPIGeneration_2_3_Final; } + ARA::ARASize getCompatibleDocumentArchiveIDsCount() const noexcept override { return compatibleDocumentArchiveIDs.size(); From bb1ef3e962cc57409347d592f682931f8b005835 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Fri, 20 Jun 2025 10:29:43 +0200 Subject: [PATCH 11/40] Fixed infinite resize loop in Avid Media Composer --- .../juce_audio_plugin_client/juce_audio_plugin_client_AAX.cpp | 2 ++ 1 file changed, 2 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 92beb012a9cd..d418fa4cc240 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 @@ -728,11 +728,13 @@ namespace AAXClasses void mouseUp (const MouseEvent& e) override { callMouseMethod (e, &AAX_IViewContainer::HandleParameterMouseUp); } void mouseDrag (const MouseEvent& e) override { callMouseMethod (e, &AAX_IViewContainer::HandleParameterMouseDrag); } + #if JucePlugin_Enable_ARA void resized() override { if (pluginEditor != nullptr) pluginEditor->setBounds (getLocalBounds()); } + #endif void parentSizeChanged() override { From 79a308b16e7562207c212613a054f1c4c572d6cd Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Wed, 16 Jul 2025 15:32:04 +0200 Subject: [PATCH 12/40] Modified JUCE for floating window support --- modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm | 3 +++ modules/juce_gui_basics/native/juce_Windowing_windows.cpp | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm b/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm index d1a28d9acbaf..aac1426394f8 100644 --- a/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm +++ b/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm @@ -602,6 +602,9 @@ bool setAlwaysOnTop (bool alwaysOnTop) override : NSFloatingWindowLevel) : NSNormalWindowLevel]; + // Acon Digital modification to prevent topmost windows from overlapping other applications + [window setHidesOnDeactivate:YES]; + isAlwaysOnTop = alwaysOnTop; } diff --git a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp index cdfc4936c907..52b511cd957a 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -2402,7 +2402,8 @@ class HWNDComponentPeer final : public ComponentPeer if (parentToAddTo != nullptr) { - type |= WS_CHILD; + if (!hasClose) + type |= WS_CHILD; } else { From 569d1f0b6f7176e3434745f803791412c9357d07 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Fri, 24 Apr 2026 15:57:20 +0200 Subject: [PATCH 13/40] Fixed potential crashes on Windows --- .../native/juce_DirectWriteTypeface_windows.cpp | 8 ++++++-- modules/juce_graphics/native/juce_DirectX_windows.cpp | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/juce_graphics/native/juce_DirectWriteTypeface_windows.cpp b/modules/juce_graphics/native/juce_DirectWriteTypeface_windows.cpp index 641bc6034477..6adfd75f54b7 100644 --- a/modules/juce_graphics/native/juce_DirectWriteTypeface_windows.cpp +++ b/modules/juce_graphics/native/juce_DirectWriteTypeface_windows.cpp @@ -206,14 +206,18 @@ class WindowsDirectWriteTypeface final : public Typeface ~AnalysisSource() override = default; - JUCE_COMCALL GetLocaleName (UINT32, UINT32*, const WCHAR** localeName) noexcept override + JUCE_COMCALL GetLocaleName (UINT32 textPosition, UINT32* textLength, const WCHAR** localeName) noexcept override { + const auto total = numUtf16Words (character.toUTF16()); + *textLength = textPosition < total ? total - textPosition : 0; *localeName = language.isNotEmpty() ? language.toWideCharPointer() : nullptr; return S_OK; } - JUCE_COMCALL GetNumberSubstitution (UINT32, UINT32*, IDWriteNumberSubstitution** substitution) noexcept override + JUCE_COMCALL GetNumberSubstitution (UINT32 textPosition, UINT32* textLength, IDWriteNumberSubstitution** substitution) noexcept override { + const auto total = numUtf16Words (character.toUTF16()); + *textLength = textPosition < total ? total - textPosition : 0; *substitution = nullptr; return S_OK; } diff --git a/modules/juce_graphics/native/juce_DirectX_windows.cpp b/modules/juce_graphics/native/juce_DirectX_windows.cpp index 5042422183d9..4a104c2f2901 100644 --- a/modules/juce_graphics/native/juce_DirectX_windows.cpp +++ b/modules/juce_graphics/native/juce_DirectX_windows.cpp @@ -906,7 +906,9 @@ auto AggregateFontCollection::mapCharacters (IDWriteFontFallback* fallback, // For reasons I don't understand, the system may pick better substitutions when passing // nullptr, instead of the system collection, as the "default collection to use". auto collectionsToCheck = collections; - collectionsToCheck.insert (collectionsToCheck.begin(), nullptr); + + // Acon Digital: Commented out the below due to spurious crashes on Windows 10 that might be related to the nullptr + // collectionsToCheck.insert (collectionsToCheck.begin(), nullptr); MapResult bestMatch; for (const auto& collection : collectionsToCheck) From 6204de666dcbbd2ee814d062c27cf49589e9300b Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Fri, 8 May 2026 08:21:11 +0200 Subject: [PATCH 14/40] Commented out MIDI initialisation to avoid assertions --- .../audio_io/juce_AudioDeviceManager.cpp | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp b/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp index 3e9e4b39e365..12d804b351d1 100644 --- a/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp +++ b/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp @@ -525,22 +525,23 @@ String AudioDeviceManager::initialiseFromXML (const XmlElement& xml, if (error.isNotEmpty() && selectDefaultDeviceOnFailure) error = initialise (numInputChansNeeded, numOutputChansNeeded, nullptr, false, preferredDefaultDeviceName); - enabledMidiInputs.clear(); + // Disabled to avoid assert due to initialisation in background thread + //enabledMidiInputs.clear(); - const auto midiInputs = [&] - { - Array result; + //const auto midiInputs = [&] + //{ + // Array result; - for (auto* c : xml.getChildWithTagNameIterator ("MIDIINPUT")) - result.add ({ c->getStringAttribute ("name"), c->getStringAttribute ("identifier") }); + // for (auto* c : xml.getChildWithTagNameIterator ("MIDIINPUT")) + // result.add ({ c->getStringAttribute ("name"), c->getStringAttribute ("identifier") }); - return result; - }(); + // return result; + //}(); - const MidiDeviceInfo defaultOutputDeviceInfo (xml.getStringAttribute ("defaultMidiOutput"), - xml.getStringAttribute ("defaultMidiOutputDevice")); + //const MidiDeviceInfo defaultOutputDeviceInfo (xml.getStringAttribute ("defaultMidiOutput"), + // xml.getStringAttribute ("defaultMidiOutputDevice")); - openLastRequestedMidiDevices (midiInputs, defaultOutputDeviceInfo); + //openLastRequestedMidiDevices (midiInputs, defaultOutputDeviceInfo); return error; } From 81de8a7945f21d89084a3bbeaf3f032e0a79984b Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Fri, 22 May 2026 09:45:49 +0200 Subject: [PATCH 15/40] Fix duplicate getHighestSupportedApiGeneration; return kARAAPIGeneration_2_3_Final --- .../ARA/juce_ARADocumentController.h | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/modules/juce_audio_processors_headless/utilities/ARA/juce_ARADocumentController.h b/modules/juce_audio_processors_headless/utilities/ARA/juce_ARADocumentController.h index 1c55bf086fd5..6dd2dd0539cf 100644 --- a/modules/juce_audio_processors_headless/utilities/ARA/juce_ARADocumentController.h +++ b/modules/juce_audio_processors_headless/utilities/ARA/juce_ARADocumentController.h @@ -43,6 +43,42 @@ class ARAEditorView; class ARAInputStream; class ARAOutputStream; +<<<<<<< Updated upstream +======= +/** This class provides customisable configuration options that are available at the ARAFactory + construction's time. + + This is used as the ARAConfigurationType type member inside ARADocumentControllerSpecialisation. + + If you wish to override the default configuration values, shadow + ARADocumentControllerSpecialisation::ARAConfigurationType with a custom type of your own that + has the same member function signatures as this struct. + + @see ARADocumentControllerSpecialisation::ARAConfigurationType +*/ +struct ARADocumentControllerConfiguration +{ + /** In order to report kARAAPIGeneration_2_3_Final or higher, the plugin must notify the host + via the ARA::PlugIn::HostModelUpdateController::notifyDocumentDataChanged() function + whenever any private, opaque document state is changed, that must be persisted. Likewise, + notifyAudioSourceContentChanged() and notifyAudioModificationContentChanged() must be + called whenever the persistent state of the respective objects changes. + + Otherwise data loss may occur. Reporting this version signals to the host that the plugin + conforms to the ARA 2.3 partial persistence features. + + e.g. + @code + getDocumentController()->getHostModelUpdateController()->notifyDocumentDataChanged(); + @endcode + */ + static constexpr ARA::ARAAPIGeneration getHighestSupportedApiGeneration() noexcept + { + return ARA::kARAAPIGeneration_2_3_Final; + } +}; + +>>>>>>> Stashed changes /** This class contains the customisation points for the JUCE provided ARA document controller implementation. @@ -437,8 +473,6 @@ class ARADocumentControllerSpecialisation : public ARADocument::Listener, const char* getVersion() const noexcept override { return JucePlugin_VersionString; } const char* getDocumentArchiveID() const noexcept override { return JucePlugin_ARADocumentArchiveID; } - ARA::ARAAPIGeneration getHighestSupportedApiGeneration() const noexcept override { return ARA::kARAAPIGeneration_2_3_Final; } - ARA::ARASize getCompatibleDocumentArchiveIDsCount() const noexcept override { return compatibleDocumentArchiveIDs.size(); From 5a6d5e15450e0192ffcb6fa10e4d036fe1695cb5 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Fri, 22 May 2026 09:59:12 +0200 Subject: [PATCH 16/40] Return kARAAPIGeneration_2_3_Final for ARA 2.3 partial persistence support --- .../utilities/ARA/juce_ARADocumentController.h | 5 ----- 1 file changed, 5 deletions(-) diff --git a/modules/juce_audio_processors_headless/utilities/ARA/juce_ARADocumentController.h b/modules/juce_audio_processors_headless/utilities/ARA/juce_ARADocumentController.h index d9ea4a87ecaa..a209c81fe70a 100644 --- a/modules/juce_audio_processors_headless/utilities/ARA/juce_ARADocumentController.h +++ b/modules/juce_audio_processors_headless/utilities/ARA/juce_ARADocumentController.h @@ -43,11 +43,6 @@ class ARAEditorView; class ARAInputStream; class ARAOutputStream; -<<<<<<< HEAD -<<<<<<< Updated upstream -======= -======= ->>>>>>> 0df3dbb197b22595aa0818dfab4dcf05b5e6ed37 /** This class provides customisable configuration options that are available at the ARAFactory construction's time. From 0226f4fd0afd83fdc6370dc2f1a10c04d77249a7 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Fri, 22 May 2026 11:49:07 +0200 Subject: [PATCH 17/40] Fix getPlatformSpecificData -> getAudioUnitClient() for JUCE 8.0.13 --- .../format_types/juce_AudioUnitPluginFormat.mm | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm index 2fb01cb1e044..d12a346f943b 100644 --- a/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm +++ b/modules/juce_audio_processors/format_types/juce_AudioUnitPluginFormat.mm @@ -287,8 +287,9 @@ bool hasEditor() const override bool AudioUnitPluginFormat::setStateFromAUPresetFile (AudioPluginInstance* api, const MemoryBlock& rawData) { - AudioUnit audioUnit = (AudioUnit) api->getPlatformSpecificData(); - jassert (audioUnit != nullptr); + auto* auClient = api->getAudioUnitClient(); + jassert (auClient != nullptr); + AudioUnit audioUnit = auClient->getAudioUnitHandle(); CFReadStreamRef stream = CFReadStreamCreateWithBytesNoCopy (kCFAllocatorDefault, (const UInt8*) rawData.getData(), rawData.getSize(), kCFAllocatorNull); @@ -319,8 +320,9 @@ bool hasEditor() const override bool AudioUnitPluginFormat::saveStateToAUPresetFile (AudioPluginInstance* api, MemoryBlock& rawData) { - AudioUnit audioUnit = (AudioUnit) api->getPlatformSpecificData(); - jassert (audioUnit != nullptr); + auto* auClient = api->getAudioUnitClient(); + jassert (auClient != nullptr); + AudioUnit audioUnit = auClient->getAudioUnitHandle(); CFPropertyListRef propertyList = 0; UInt32 sz = sizeof (CFPropertyListRef); From bf2222926def525db30c91ff153e1b4c623ee561 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Tue, 30 Jun 2026 09:10:28 +0200 Subject: [PATCH 18/40] Add ComponentPeer::windowFloatingChild flag for floating child windows Introduce a windowFloatingChild StyleFlag that makes a window float above the parent passed to Component::addToDesktop() without being globally always-on-top, and minimise/restore/close together with that parent. The flag encapsulates the native mechanism per platform: - Windows: owned top-level window (never WS_CHILD). - macOS: [parent addChildWindow:ordered:NSWindowAbove], (re)attached in setVisible() since ordering the window out can detach a child. - Linux: real top-level window related to its parent via WM_TRANSIENT_FOR. All peer changes are gated on the new flag, so existing windows are unaffected. Co-Authored-By: Claude Opus 4.8 --- .../native/juce_NSViewComponentPeer_mac.mm | 23 ++++++++++++++++++- .../native/juce_Windowing_linux.cpp | 6 ++++- .../native/juce_Windowing_windows.cpp | 4 +++- .../native/juce_XWindowSystem_linux.cpp | 12 +++++++++- .../windows/juce_ComponentPeer.h | 5 ++++ 5 files changed, 46 insertions(+), 4 deletions(-) diff --git a/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm b/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm index 18f5521f0ccc..4c42d7b29583 100644 --- a/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm +++ b/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm @@ -184,7 +184,7 @@ static void setOwner (id self, Callback* newOwner) NSViewComponentPeer (Component& comp, const int windowStyleFlags, NSView* viewToAttachTo) : ComponentPeer (comp, windowStyleFlags), safeComponent (&comp), - isSharedWindow (viewToAttachTo != nil), + isSharedWindow (viewToAttachTo != nil && (windowStyleFlags & windowFloatingChild) == 0), lastRepaintTime (Time::getMillisecondCounter()) { appFocusChangeCallback = appFocusChanged; @@ -290,6 +290,12 @@ static void setOwner (id self, Callback* newOwner) scopedObservers.emplace_back (view, frameChangedSelector, NSWindowDidMiniaturizeNotification, window); scopedObservers.emplace_back (view, @selector (windowWillMiniaturize:), NSWindowWillMiniaturizeNotification, window); scopedObservers.emplace_back (view, @selector (windowDidDeminiaturize:), NSWindowDidDeminiaturizeNotification, window); + + // Remember the parent window for floating children. The actual parent/child link is + // established in setVisible(), because ordering the window out (which addToDesktop() + // does before the window is first shown) can detach a child from its parent. + if ((windowStyleFlags & windowFloatingChild) != 0 && viewToAttachTo != nil) + floatingChildParent = [[viewToAttachTo window] retain]; } auto alpha = component.getAlpha(); @@ -322,6 +328,15 @@ static void setOwner (id self, Callback* newOwner) if (! isSharedWindow) { + if (floatingChildParent != nil) + { + if ([window parentWindow] != nil) + [floatingChildParent removeChildWindow: window]; + + [floatingChildParent release]; + floatingChildParent = nil; + } + setOwner (window, nullptr); [window setContentView: nil]; [window close]; @@ -347,6 +362,11 @@ void setVisible (bool shouldBeVisible) override { if (shouldBeVisible) { + // (Re)attach floating children to their parent so they float above it (without + // being globally always-on-top) and minimise/restore/close together with it. + if (floatingChildParent != nil && [window parentWindow] == nil) + [floatingChildParent addChildWindow: window ordered: NSWindowAbove]; + ++insideToFrontCall; [window orderFront: nil]; --insideToFrontCall; @@ -1743,6 +1763,7 @@ auto tie() const NSView* view = nil; WeakReference safeComponent; const bool isSharedWindow = false; + NSWindow* floatingChildParent = nil; #if USE_COREGRAPHICS_RENDERING bool usingCoreGraphics = true; #else diff --git a/modules/juce_gui_basics/native/juce_Windowing_linux.cpp b/modules/juce_gui_basics/native/juce_Windowing_linux.cpp index de5fef558533..aecac33a098e 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_linux.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_linux.cpp @@ -57,7 +57,11 @@ class LinuxComponentPeer final : public ComponentPeer, repainter = std::make_unique (*this); windowH = instance->createWindow (parentToAddTo, this); - parentWindow = parentToAddTo; + + // A floating child is a real top-level window (related to its parent via WM_TRANSIENT_FOR + // inside createWindow), so it must not be treated as an embedded child for coordinate + // purposes - keep parentWindow at 0 in that case. + parentWindow = (windowStyleFlags & windowFloatingChild) != 0 ? 0 : parentToAddTo; setTitle (component.getName()); diff --git a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp index 14aa1353f6e8..bd11a425cc72 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -2039,7 +2039,9 @@ class HWNDComponentPeer final : public ComponentPeer if (parentToAddTo != nullptr) { - if ((styleFlags & windowHasCloseButton) == 0) + // A floating child is an owned top-level window (never WS_CHILD): it floats above its + // owner without being topmost and is minimised/restored together with the owner. + if ((styleFlags & windowHasCloseButton) == 0 && (styleFlags & windowFloatingChild) == 0) result |= WS_CHILD; } else if (titled || usesDropShadow) diff --git a/modules/juce_gui_basics/native/juce_XWindowSystem_linux.cpp b/modules/juce_gui_basics/native/juce_XWindowSystem_linux.cpp index 98e5e3ca514f..840da731fac6 100644 --- a/modules/juce_gui_basics/native/juce_XWindowSystem_linux.cpp +++ b/modules/juce_gui_basics/native/juce_XWindowSystem_linux.cpp @@ -1553,7 +1553,12 @@ ::Window XWindowSystem::createWindow (::Window parentToAddTo, LinuxComponentPeer swa.override_redirect = ((styleFlags & ComponentPeer::windowIsTemporary) != 0) ? True : False; swa.event_mask = getAllEventsMask (styleFlags & ComponentPeer::windowIgnoresMouseClicks); - auto windowH = X11Symbols::getInstance()->xCreateWindow (display, parentToAddTo != 0 ? parentToAddTo : root, + // A floating child must be a real top-level window related to its parent via WM_TRANSIENT_FOR, + // rather than an embedded X11 child of it, so create it under the root window in that case. + const auto floatingChild = (styleFlags & ComponentPeer::windowFloatingChild) != 0; + const auto xParent = (parentToAddTo != 0 && ! floatingChild) ? parentToAddTo : root; + + auto windowH = X11Symbols::getInstance()->xCreateWindow (display, xParent, 0, 0, 1, 1, 0, visualAndDepth.depth, InputOutput, visualAndDepth.visual, CWBorderPixel | CWColormap | CWBackPixmap | CWEventMask | CWOverrideRedirect, @@ -1596,6 +1601,11 @@ ::Window XWindowSystem::createWindow (::Window parentToAddTo, LinuxComponentPeer // Set the window type setWindowType (windowH, styleFlags); + // Relate a floating child to its parent so the window manager keeps it above the parent + // (without it being globally always-on-top) and minimises/restores it together with the parent. + if (floatingChild && parentToAddTo != 0) + xchangeProperty (windowH, XA_WM_TRANSIENT_FOR, XA_WINDOW, 32, &parentToAddTo, 1); + // Define decoration if ((styleFlags & ComponentPeer::windowHasTitleBar) == 0) removeWindowDecorations (windowH); diff --git a/modules/juce_gui_basics/windows/juce_ComponentPeer.h b/modules/juce_gui_basics/windows/juce_ComponentPeer.h index 1ac1523d446f..53ec4d15e436 100644 --- a/modules/juce_gui_basics/windows/juce_ComponentPeer.h +++ b/modules/juce_gui_basics/windows/juce_ComponentPeer.h @@ -84,6 +84,11 @@ class JUCE_API ComponentPeer : private FocusChangeListener asynchronous Core Graphics drawing operations. Use this if there are issues with regions not being redrawn at the expected time (macOS and iOS only). */ + windowFloatingChild = (1 << 12), /**< Indicates that the window should float above the parent window + passed to Component::addToDesktop(), without being globally + always-on-top, and should be minimised/restored/closed together + with that parent. Used to emulate floating child windows, which + JUCE does not otherwise support. */ windowIsSemiTransparent = (1 << 30) /**< Not intended for public use - makes a window transparent. */ }; From 61986e1aa16d1f84af193204500b4ea41ae65dad Mon Sep 17 00:00:00 2001 From: Louis Couka Date: Sun, 12 Jul 2026 23:28:41 +0200 Subject: [PATCH 19/40] Fix VST3 editor reopening in Premiere --- .../juce_audio_plugin_client_VST3.cpp | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) 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 3c0ca0611c02..43eabb33498a 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 @@ -2302,7 +2302,29 @@ class JuceVST3EditController final : public Vst::EditController, void createEditor (AudioProcessor& plugin) { - pluginEditor.reset (plugin.createEditorAndMakeActive()); + // Acon Digital modification - Premiere makes a second view before releasing the + // first, so adopt the existing editor rather than leave two views alive. + if (detail::PluginUtilities::getHostType().isPremiere()) + if (auto* active = plugin.getActiveEditor()) + { + auto* preflight = dynamic_cast (active->getParentComponent()); + if (preflight != nullptr && preflight->pluginEditor.get() == active) + { + active->setHostContext (nullptr); + preflight->editorHostContext.reset(); + pluginEditor = std::move (preflight->pluginEditor); + } + else + { + // Defensive fallback: preserve the working behaviour if JUCE changes + // the component hierarchy in a future update. + plugin.editorBeingDeleted (active); + } + } + + if (pluginEditor == nullptr) + pluginEditor.reset (plugin.createEditorAndMakeActive()); + // Acon Digital modification - End of modification #if JucePlugin_Enable_ARA jassert (pluginEditor->getARAClientExtensions() != nullptr); From 0edb1cb75a0932b9f8ca325c61344877c0a75748 Mon Sep 17 00:00:00 2001 From: Louis Couka Date: Mon, 13 Jul 2026 00:57:49 +0200 Subject: [PATCH 20/40] Mark Premiere workaround as a quick fix --- .../juce_audio_plugin_client/juce_audio_plugin_client_VST3.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 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 43eabb33498a..e8883c17ecbe 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 @@ -2302,8 +2302,7 @@ class JuceVST3EditController final : public Vst::EditController, void createEditor (AudioProcessor& plugin) { - // Acon Digital modification - Premiere makes a second view before releasing the - // first, so adopt the existing editor rather than leave two views alive. + // Acon Digital modification - Quick fix, may be fixed in a newer JUCE version (> 8.0.13). if (detail::PluginUtilities::getHostType().isPremiere()) if (auto* active = plugin.getActiveEditor()) { From 71a4f1c97c2b458dd4d27b22f93e5c111f30fcfb Mon Sep 17 00:00:00 2001 From: Louis Couka Date: Mon, 13 Jul 2026 01:12:36 +0200 Subject: [PATCH 21/40] Remove Premiere editor fallback --- .../juce_audio_plugin_client_VST3.cpp | 6 ------ 1 file changed, 6 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 e8883c17ecbe..d2a3cb3e21ae 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 @@ -2313,12 +2313,6 @@ class JuceVST3EditController final : public Vst::EditController, preflight->editorHostContext.reset(); pluginEditor = std::move (preflight->pluginEditor); } - else - { - // Defensive fallback: preserve the working behaviour if JUCE changes - // the component hierarchy in a future update. - plugin.editorBeingDeleted (active); - } } if (pluginEditor == nullptr) From 28443858d57d4cad673d4cd7dc333267dcd62db7 Mon Sep 17 00:00:00 2001 From: Louis Couka Date: Mon, 13 Jul 2026 00:58:32 +0200 Subject: [PATCH 22/40] Add Audition GDI quick fix --- .../juce_gui_basics/native/juce_Windowing_windows.cpp | 10 +++++++++- 1 file changed, 9 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 bd11a425cc72..b3ddb04d2ca5 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -4660,7 +4660,15 @@ extern bool juce_isRunningInWine(); ComponentPeer* Component::createNewPeer (int styleFlags, void* parentHWND) { - const auto renderer = juce_isRunningInWine() ? 0 : 1; + // Acon Digital modification - Quick fix, may be fixed in a newer JUCE version (> 8.0.13). + static const bool useSoftwareRenderer = [] + { + const auto host = File::getSpecialLocation (File::hostApplicationPath).getFileNameWithoutExtension(); + return juce_isRunningInWine() || host.containsIgnoreCase ("Audition"); + }(); + const auto renderer = useSoftwareRenderer ? 0 : 1; + // Acon Digital modification - End of modification + return new HWNDComponentPeer { *this, styleFlags, (HWND) parentHWND, false, renderer }; } From 14a772e3224c081ac7fcf28829569eb5170650e7 Mon Sep 17 00:00:00 2001 From: Louis Couka Date: Mon, 13 Jul 2026 01:11:57 +0200 Subject: [PATCH 23/40] Simplify Audition renderer selection --- .../juce_gui_basics/native/juce_Windowing_windows.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp index b3ddb04d2ca5..97e27cf08d22 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -4661,11 +4661,10 @@ extern bool juce_isRunningInWine(); ComponentPeer* Component::createNewPeer (int styleFlags, void* parentHWND) { // Acon Digital modification - Quick fix, may be fixed in a newer JUCE version (> 8.0.13). - static const bool useSoftwareRenderer = [] - { - const auto host = File::getSpecialLocation (File::hostApplicationPath).getFileNameWithoutExtension(); - return juce_isRunningInWine() || host.containsIgnoreCase ("Audition"); - }(); + static const auto useSoftwareRenderer = juce_isRunningInWine() + || File::getSpecialLocation (File::hostApplicationPath) + .getFileNameWithoutExtension() + .containsIgnoreCase ("Audition"); const auto renderer = useSoftwareRenderer ? 0 : 1; // Acon Digital modification - End of modification From 068b0c95fc061b3a96a86492058b3cc58abed1a4 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Fri, 7 Aug 2026 17:17:58 +0200 Subject: [PATCH 24/40] Fix silent vblank registration failures that froze floating child windows All painting on Windows is gated on a vblank callback: the GDI renderer only calls InvalidateRect from onVBlank(), and the Direct2D renderer validates the update region in handlePaintMessage() and defers the paint to onVBlank(). A peer that is not attached to a VBlankThread therefore never repaints again, while the application itself stays fully responsive. There were several ways a peer could end up in that state, none with a recovery path: - VBlankDispatcher::updateDisplay() searched the cached DXGI adapter list for the output driving the peer's monitor and, when nothing matched, fell off the end of the loop with the listener attached to nothing at all - no error, no retry. That list is only re-enumerated on a display change, so it goes stale after a driver update, GPU reset or monitor sleep/wake. Windows registered at startup keep working, while a window opened later can silently fail to attach. - HWNDComponentPeer only called updateDisplay() when its monitor *changed*, so after such a failure nothing ever retried. - The monitorUpdateTimer that upstream added to re-register embedded peers was emplaced but never started: TimedCallback's constructor does not start a timer, and the only startTimer() call sat inside the callback that therefore never ran. - A window momentarily intersecting no display resolved to a null monitor, which detaches the peer, and null -> null is a no-op on every later check. - The thread-creation loop broke out of the inner output loop only, so a monitor enumerated on two adapters got two threads holding the same listener while removeListener() erases just the first match. The fixes keep the vsync throttling intact - nothing paints outside onVBlank(): - Refresh the adapters and retry once before giving up, and log a definitive failure. - Add VBlankDispatcher::isRegistered() and re-run updateDisplay() whenever a peer is attached to no thread. This is what turns the timer into a real retry. - Start monitorUpdateTimer, and do it for every peer rather than only parented ones. - Fall back to MONITOR_DEFAULTTONEAREST instead of detaching. - Return rather than break after creating a thread. - Log registration changes in debug builds. Also hardens VBlankThread itself: flagPaintPending is cleared from a ScopeGuard so a throwing listener cannot stall a monitor's stream forever, listeners are iterated over a snapshot with a validity re-check so a re-entrant add or remove cannot invalidate the iteration, and run() tests flagExit on every iteration and waits interruptibly so that stopThread(-1) in ~VBlankThread cannot block the message thread indefinitely. Verified by forcing a detach with an unchanged monitor: the peer is re-attached within ~1s, where the previous code did nothing at all and the window stayed frozen permanently. See Specifications/JUCE-VBlank-Repaint-Freeze.md in the main repository for the full analysis. A JUCE 7 patch for the same symptom was lost in the 8.0 merge because it was an unexplained one-liner. Co-Authored-By: Claude Opus 5 (1M context) --- .../native/juce_VBlank_windows.cpp | 106 ++++++++++++++---- .../native/juce_Windowing_windows.cpp | 59 ++++++++-- 2 files changed, 135 insertions(+), 30 deletions(-) diff --git a/modules/juce_gui_basics/native/juce_VBlank_windows.cpp b/modules/juce_gui_basics/native/juce_VBlank_windows.cpp index 93e2bde96488..e4ec8d1f45ce 100644 --- a/modules/juce_gui_basics/native/juce_VBlank_windows.cpp +++ b/modules/juce_gui_basics/native/juce_VBlank_windows.cpp @@ -117,6 +117,14 @@ class VBlankThread : private Thread, { for (;;) { + // Acon Digital modification - the exit flag used to be tested only inside the success + // branch below, so once WaitForVBlank() started failing (display detached, adapter reset, + // session switch) this loop span forever and the stopThread (-1) in ~VBlankThread blocked + // the message thread indefinitely. + if ((state.load() & flagExit) != 0) + return; + // Acon Digital modification - End of modification + if (output->WaitForVBlank() == S_OK) { const auto now = Time::getMillisecondCounterHiRes(); @@ -136,19 +144,34 @@ class VBlankThread : private Thread, } else { - sleep (1); + // Acon Digital modification - interruptible, so that the notify() issued by + // stopThread() wakes us instead of leaving the caller waiting up to a millisecond + // longer than necessary on every teardown. + wait (1); + // Acon Digital modification - End of modification } } } void handleAsyncUpdate() override { + // Acon Digital modification - clear the pending flag from a scope guard. An exception thrown + // by a listener is swallowed by the message queue, and the flag then stayed set forever, + // which permanently stops this monitor's vblank stream and so freezes every window on it. + const ScopeGuard clearPaintPending { [this] { state &= ~flagPaintPending; } }; + // Acon Digital modification - End of modification + const auto timestampSec = lastVBlankEvent / 1000.0; - for (auto& listener : listeners) - listener.get().onVBlank (timestampSec); + // Acon Digital modification - iterate a snapshot and re-check each listener before calling + // it: a callback may add or remove listeners re-entrantly (creating or destroying a desktop + // window while painting), which would otherwise invalidate the iteration. + const auto listenersToCall = listeners; - state &= ~flagPaintPending; + for (auto& listener : listenersToCall) + if (hasListener (listener.get())) + listener.get().onVBlank (timestampSec); + // Acon Digital modification - End of modification } enum Flags @@ -215,27 +238,35 @@ class VBlankDispatcher final : public DeletedAtShutdown if (threadWithListener != threads.end()) removeListener (threadWithListener, listener); - for (const auto& adapter : directX->adapters.getAdapterArray()) - { - for (UINT i = 0;; ++i) - { - ComSmartPtr output; - const auto result = adapter->dxgiAdapter->EnumOutputs (i, output.resetAndGetPointerAddress()); + // Acon Digital modification - when no enumerated output matched the monitor, this used to + // fall off the end of the loop with the listener attached to nothing at all, silently. The + // adapter array is cached and only re-enumerated on a display change, so it goes stale after + // a driver update, GPU reset or a monitor sleep/wake cycle - which is why a window opened + // later can fail to attach while windows registered at startup keep working. Refresh the + // adapters and retry once before giving up, and report a definitive failure. + if (createThreadForMonitor (listener, monitor)) + return; - if (result == DXGI_ERROR_NOT_CURRENTLY_AVAILABLE) - break; + reconfigureDisplays(); - if (result == DXGI_ERROR_NOT_FOUND) - break; + if (createThreadForMonitor (listener, monitor)) + return; - if (VBlankThread::getMonitorFromOutput (output) != monitor) - continue; + DBG ("VBlank: NO DXGI output matches monitor " << String::toHexString ((pointer_sized_int) monitor) + << " - this window cannot repaint until a later retry attaches it"); + // Acon Digital modification - End of modification + } - threads.emplace_back (std::make_unique (output, monitor, listener)); - break; - } - } + // Acon Digital modification - lets a peer check whether it is actually attached to a vblank + // thread. updateDisplay() can fail to attach it, and a caller that only reacts to a *change* of + // monitor would never notice, so the window would stay frozen for good. + bool isRegistered (const ComponentPeer::VBlankListener& listener) const + { + return std::any_of (threads.begin(), + threads.end(), + [&listener] (const auto& thread) { return thread->hasListener (listener); }); } + // Acon Digital modification - End of modification void removeListener (const ComponentPeer::VBlankListener& listener) { @@ -274,6 +305,41 @@ class VBlankDispatcher final : public DeletedAtShutdown clearSingletonInstance(); } + // Acon Digital modification - factored out of updateDisplay() so that it can be retried against + // a freshly enumerated adapter list. Returns false if no output drives the given monitor. + bool createThreadForMonitor (ComponentPeer::VBlankListener& listener, HMONITOR monitor) + { + for (const auto& adapter : directX->adapters.getAdapterArray()) + { + for (UINT i = 0;; ++i) + { + ComSmartPtr output; + const auto result = adapter->dxgiAdapter->EnumOutputs (i, output.resetAndGetPointerAddress()); + + if (result == DXGI_ERROR_NOT_CURRENTLY_AVAILABLE) + break; + + if (result == DXGI_ERROR_NOT_FOUND) + break; + + if (VBlankThread::getMonitorFromOutput (output) != monitor) + continue; + + threads.emplace_back (std::make_unique (output, monitor, listener)); + + // Returning rather than breaking: a break only left the inner loop, so on machines + // that enumerate the same monitor on more than one adapter (hybrid graphics) the + // outer loop created a second thread for the same monitor holding the same listener, + // and removeListener() only erases the first match - leaving a dangling reference to + // a destroyed peer. + return true; + } + } + + return false; + } + // Acon Digital modification - End of modification + // This may delete the corresponding thread and invalidate iterators, // so be careful! bool removeListener (Threads::iterator it, const ComponentPeer::VBlankListener& listener) diff --git a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp index c04320673db7..132aef6f1180 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -1218,14 +1218,15 @@ class HWNDComponentPeer final : public ComponentPeer updateCurrentMonitorAndRefreshVBlankDispatcher (ForceRefreshDispatcher::yes); - if (parentToAddTo != nullptr) - { - monitorUpdateTimer.emplace ([this] - { - updateCurrentMonitorAndRefreshVBlankDispatcher (ForceRefreshDispatcher::yes); - monitorUpdateTimer->startTimer (1000); - }); - } + // Acon Digital modification - the monitor update timer was emplaced but never started + // (TimedCallback's constructor doesn't start the timer, and the only startTimer() call was + // inside the callback that therefore never ran), so a peer that failed to attach to a vblank + // thread never recovered and its window stopped repainting altogether. Run it for every peer + // rather than only for parented ones: any window can be created or moved while it doesn't + // intersect a display, and only this timer gets it re-attached afterwards. + monitorUpdateTimer.emplace ([this] { updateCurrentMonitorAndRefreshVBlankDispatcher(); }); + monitorUpdateTimer->startTimer (1000); + // Acon Digital modification - End of modification suspendResumeRegistration = ScopedSuspendResumeNotificationRegistration { hwnd }; @@ -3309,10 +3310,48 @@ class HWNDComponentPeer final : public ComponentPeer void updateCurrentMonitorAndRefreshVBlankDispatcher (ForceRefreshDispatcher force = ForceRefreshDispatcher::no) { + // Acon Digital modification - a window that doesn't intersect any display (a peer registering + // before Component::addToDesktop has applied its real bounds, a window dragged off-screen, a + // minimised window) resolved to a null monitor, and passing null to updateDisplay() detaches + // the peer from its vblank thread. Since the monitor is only acted on when it changes, every + // later null -> null check is a no-op and the peer stays detached, so the window never + // repaints again. Attach to the nearest display instead of detaching. auto monitor = MonitorFromWindow (hwnd, MONITOR_DEFAULTTONULL); - if (std::exchange (currentMonitor, monitor) != monitor || force == ForceRefreshDispatcher::yes) - VBlankDispatcher::getInstance()->updateDisplay (*this, currentMonitor); + if (monitor == nullptr) + monitor = MonitorFromWindow (hwnd, MONITOR_DEFAULTTONEAREST); + // Acon Digital modification - End of modification + + const auto previousMonitor = std::exchange (currentMonitor, monitor); + + auto* dispatcher = VBlankDispatcher::getInstance(); + + // Acon Digital modification - also refresh when this peer isn't attached to any vblank + // thread. updateDisplay() can fail to attach it (no enumerated DXGI output drives the + // monitor, typically because the cached adapter list went stale), and reacting only to a + // *change* of monitor meant nothing ever retried afterwards, so the window stayed frozen for + // good. This is what makes the periodic call from monitorUpdateTimer a real retry. + const auto isRegistered = dispatcher->isRegistered (*this); + + if (previousMonitor != monitor || force == ForceRefreshDispatcher::yes || ! isRegistered) + { + // All painting is gated on a vblank callback arriving, so a peer that ends up on the + // wrong monitor, or on none, silently stops repainting. Log it so that never has to be + // diagnosed from scratch again. + const auto isRetry = previousMonitor == monitor && ! isRegistered; + + if (previousMonitor != monitor || isRetry) + { + DBG ("VBlank registration: \"" << component.getName() << "\" monitor " + << String::toHexString ((pointer_sized_int) previousMonitor) << " -> " + << String::toHexString ((pointer_sized_int) monitor) + << (isRetry ? " (RETRY - peer was not attached to any vblank thread)" : "") + << (monitor == nullptr ? " (DETACHED - this window will stop repainting)" : "")); + } + + dispatcher->updateDisplay (*this, currentMonitor); + } + // Acon Digital modification - End of modification } bool handlePositionChanged() From ab13c6a49984471574f89791dc86555a6558e0fa Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Tue, 11 Aug 2026 08:33:38 +0200 Subject: [PATCH 25/40] Avoid a redundant device-type scan and don't leave a stopped device stopped --- .../audio_io/juce_AudioDeviceManager.cpp | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp b/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp index 3138fc02e6fb..873eadf18493 100644 --- a/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp +++ b/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp @@ -703,10 +703,17 @@ void AudioDeviceManager::scanDevicesIfNeeded() { listNeedsScanning = false; + // Acon Digital modification: createDeviceTypesIfNeeded() already scans every type it + // creates, so only rescan when the types were already there. Scanning twice on the very + // first call costs hundreds of milliseconds on Windows, where it means enumerating (and, + // for ASIO, loading) every installed driver a second time. + const auto deviceTypesAlreadyExisted = availableDeviceTypes.size() > 0; + createDeviceTypesIfNeeded(); - for (auto* type : availableDeviceTypes) - type->scanForDevices(); + if (deviceTypesAlreadyExisted) + for (auto* type : availableDeviceTypes) + type->scanForDevices(); } } @@ -817,8 +824,19 @@ String AudioDeviceManager::setAudioDeviceSetup (const AudioDeviceSetup& newSetup if (newSetup != currentSetup) sendChangeMessage(); - else if (currentAudioDevice != nullptr) + else if (currentAudioDevice != nullptr && currentAudioDevice->isOpen()) + { + // Acon Digital modification: the setup is unchanged, so there is no need to re-open the + // device - but a caller may have stopped it in order to change the format of the audio it + // is about to play, so make sure it ends up running again rather than silently staying + // stopped. Restarting also gives the callback the audioDeviceAboutToStart() it needs to + // rebuild its converters. A device that is no longer open falls through to the re-open + // path below. + if (! currentAudioDevice->isPlaying()) + currentAudioDevice->start (callbackHandler->getAudioIODeviceCallback()); + return {}; + } stopDevice(); From 60b1c45373435e0dcd21cc3ec4f827036b6a9f13 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Tue, 18 Aug 2026 16:52:30 +0200 Subject: [PATCH 26/40] Derive the X11 display scale from the desktop's Xft DPI getDisplayScale only ever read integer window-scale keys, so neither GNOME's text-scaling slider nor KDE's fractional scaling had any effect - both surface as an Xft DPI above 96, which was watched for changes but never read. Resolve it from the Xft/DPI XSETTING, falling back to the Xft.dpi X resource in RESOURCE_MANAGER, and take whichever of that and the window scaling factor is larger: GNOME folds the window scale and the text scale into Xft/DPI, whereas XFCE keeps the two apart. Where the window scale is used on its own, apply the text scaling factor by hand. Also stop the gsettings scaling-factor branch returning 1.0 when it reads back "uint32 0". That is modern GNOME's value for "unset", and short-circuiting on it pinned every distro with gsettings installed to an unscaled display, even on genuinely high-DPI panels. It now falls through to the remaining sources. Co-Authored-By: Claude Opus 5 (1M context) --- .../native/juce_Windowing_linux.cpp | 2 +- .../native/juce_XWindowSystem_linux.cpp | 102 ++++++++++++++++-- .../native/juce_XWindowSystem_linux.h | 1 + 3 files changed, 96 insertions(+), 9 deletions(-) diff --git a/modules/juce_gui_basics/native/juce_Windowing_linux.cpp b/modules/juce_gui_basics/native/juce_Windowing_linux.cpp index de578954f75e..24bb001df957 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_linux.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_linux.cpp @@ -609,7 +609,7 @@ class LinuxComponentPeer final : public ComponentPeer, { static StringArray possibleSettings { XWindowSystem::getWindowScalingFactorSettingName(), "Gdk/UnscaledDPI", - "Xft/DPI" }; + XWindowSystem::getXftDpiSettingName() }; if (possibleSettings.contains (settingThatHasChanged.name)) forceDisplayUpdate(); diff --git a/modules/juce_gui_basics/native/juce_XWindowSystem_linux.cpp b/modules/juce_gui_basics/native/juce_XWindowSystem_linux.cpp index ece1f1608a1e..c0d8395bd7a6 100644 --- a/modules/juce_gui_basics/native/juce_XWindowSystem_linux.cpp +++ b/modules/juce_gui_basics/native/juce_XWindowSystem_linux.cpp @@ -1144,7 +1144,75 @@ namespace DisplayHelpers return 96.0; } - static double getDisplayScale (const String& name, double dpi) + // Guards against a garbage Xft.dpi resource rendering the UI unusable. + static constexpr double minDesktopScale = 0.5, maxDesktopScale = 8.0; + + // KDE Plasma, xrdb and most display managers publish the desktop's DPI as an Xft.dpi X + // resource. RESOURCE_MANAGER on the root window holds what "xrdb -query" prints. + static double getXftDpiFromResourceManager (::Display* display) + { + if (display == nullptr) + return 0.0; + + const auto root = X11Symbols::getInstance()->xRootWindow (display, X11Symbols::getInstance()->xDefaultScreen (display)); + + XWindowSystemUtilities::GetXProperty prop (display, root, XA_RESOURCE_MANAGER, 0L, 100000, false, XA_STRING); + + if (! prop.success || prop.actualType != XA_STRING || prop.actualFormat != 8) + return 0.0; + + for (const auto& line : StringArray::fromLines (String ((const char*) prop.data, (int) prop.numItems))) + if (line.startsWithIgnoreCase ("Xft.dpi:")) + return line.fromFirstOccurrenceOf (":", false, false).trim().getDoubleValue(); + + return 0.0; + } + + // The desktop's Xft DPI is the only source that carries fractional and text scaling, and is + // what GTK, Qt and Chromium scale by on X11. Returns 0.0 when the desktop publishes nothing. + static double getScaleFromXftDpi (::Display* display) + { + double xftDpi = 0.0; + + if (auto* xSettings = XWindowSystem::getInstance()->getXSettings()) + { + const auto xftDpiSetting = xSettings->getSetting (XWindowSystem::getXftDpiSettingName()); + + if (xftDpiSetting.isValid() && xftDpiSetting.integerValue > 0) + xftDpi = (double) xftDpiSetting.integerValue / 1024.0; // XSETTINGS stores the DPI scaled by 1024 + } + + if (xftDpi <= 0.0) + xftDpi = getXftDpiFromResourceManager (display); + + const auto scale = xftDpi / 96.0; + + return (scale >= minDesktopScale && scale <= maxDesktopScale) ? scale : 0.0; + } + + // GNOME's text scaling slider. The integer window scaling sources below do not account for + // it, so it has to be folded in by hand; an Xft DPI already includes it. + static double getTextScalingFactor() + { + ChildProcess gsettings; + + if (File ("/usr/bin/gsettings").existsAsFile() + && gsettings.start ("/usr/bin/gsettings get org.gnome.desktop.interface text-scaling-factor", ChildProcess::wantStdOut)) + { + if (gsettings.waitForProcessToFinish (200)) + { + const auto textScale = gsettings.readAllProcessOutput().trim().getDoubleValue(); + + if (textScale >= minDesktopScale && textScale <= maxDesktopScale) + return textScale; + } + } + + return 1.0; + } + + // The integer window scale the desktop asks toolkits to render at. Returns 0.0 when unset. + static double getWindowScalingFactor (const String& name) { if (auto* xSettings = XWindowSystem::getInstance()->getXSettings()) { @@ -1206,19 +1274,37 @@ namespace DisplayHelpers { auto scaleFactor = gsettingsOutput[1].getDoubleValue(); + // Modern GNOME prints "uint32 0" here, meaning "unset" - fall through to + // the other sources rather than reporting an unscaled display. if (scaleFactor > 0.0) return scaleFactor; - - return 1.0; } } } } - // If no scale factor is set by GNOME or Ubuntu then calculate from monitor dpi - // We use the same approach as chromium which simply divides the dpi by 96 - // and then rounds the result - return round (dpi / 96.0); + return 0.0; + } + + static double getDisplayScale (::Display* display, const String& name, double dpi) + { + // GNOME folds both the window scale and the text scaling factor into Xft/DPI, whereas + // XFCE keeps the two apart, so take whichever of the two axes is larger. Note that an + // Xft DPI is global rather than per-output: on a mixed-DPI multi-monitor X11 setup every + // display ends up with the same scale, as it does under GTK and Qt. + const auto xftScale = getScaleFromXftDpi (display); + + auto windowScale = getWindowScalingFactor (name); + + if (windowScale > 0.0 && xftScale <= 0.0) + windowScale *= getTextScalingFactor(); + + if (xftScale > 0.0 || windowScale > 0.0) + return jmax (xftScale, windowScale); + + // Nothing is configured, so calculate from monitor dpi. We use the same approach as + // chromium which simply divides the dpi by 96 and then rounds the result. + return round (dpi / 96.0) * getTextScalingFactor(); } #if JUCE_USE_XINERAMA @@ -2669,7 +2755,7 @@ Array XWindowSystem::findDisplays (float masterScale) const d.dpi = ((static_cast (crtc->width) * 25.4 * 0.5) / static_cast (output->mm_width)) + ((static_cast (crtc->height) * 25.4 * 0.5) / static_cast (output->mm_height)); - auto scale = DisplayHelpers::getDisplayScale (output->name, d.dpi); + auto scale = DisplayHelpers::getDisplayScale (display, output->name, d.dpi); scale = scale <= 0.1 ? 1.0 : scale; d.scale = masterScale * scale; diff --git a/modules/juce_gui_basics/native/juce_XWindowSystem_linux.h b/modules/juce_gui_basics/native/juce_XWindowSystem_linux.h index 295c0eeb1792..db2536b76610 100644 --- a/modules/juce_gui_basics/native/juce_XWindowSystem_linux.h +++ b/modules/juce_gui_basics/native/juce_XWindowSystem_linux.h @@ -256,6 +256,7 @@ class XWindowSystem : public DeletedAtShutdown ResizableBorderComponent::Zone zone); static String getWindowScalingFactorSettingName() { return "Gdk/WindowScalingFactor"; } + static String getXftDpiSettingName() { return "Xft/DPI"; } static String getThemeNameSettingName() { return "Net/ThemeName"; } //============================================================================== From d8cf0e45e60b269d5386b72c008156f6d78c8927 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Thu, 20 Aug 2026 17:21:30 +0200 Subject: [PATCH 27/40] Activate the window when its title bar is clicked A caption click is swallowed rather than passed to DefWindowProc so that the view keeps repainting while the button is held, and the click is only handed over once the mouse moves. But DefWindowProc is also what activates and raises the window for that click, so clicking the title bar of a background window did nothing at all: it stayed behind and unfocused until the mouse was moved and the pending click finally reached the system. Pass the click straight to DefWindowProc when the window is not in the foreground, where the repainting the swallow protects matters least, and keep the existing behaviour for a caption press on the active window. Clear the pending click when the button is released without moving, or the next mouse move over the frame hands a stale click over. The focus update that follows activation was postponed until the mouse event that activated the window arrived, but only a click in the client area produces one. A title-bar click therefore never applied it, leaving the window without keyboard focus - and, for a DocumentWindow, with its menu bar drawn disabled. Only postpone the update when the activating click is in the client area, which is the case it was written for. Co-Authored-By: Claude Opus 5 (1M context) --- .../native/juce_Windowing_windows.cpp | 22 ++++++++++++++++++- 1 file changed, 21 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 132aef6f1180..0a6863fea87d 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -3621,6 +3621,13 @@ class HWNDComponentPeer final : public ComponentPeer return 0; case HTCAPTION: + // A caption click on an inactive window is what activates and raises it, and that + // is done by DefWindowProc. Swallowing the click here would leave the window in + // the background without keyboard focus until the mouse moved, so hand this one + // straight to the system. + if (GetForegroundWindow() != hwnd) + return {}; + // The default click-in-caption handler appears to block the message loop until a // mouse move is detected, which prevents the view from repainting. We want to keep // painting, so log the click ourselves and only defer to DefWindowProc once the @@ -4017,7 +4024,16 @@ class HWNDComponentPeer final : public ComponentPeer if (! component.getMouseClickGrabsKeyboardFocus()) return MA_NOACTIVATE; - mouseActivateFlags |= gotMouseActivate; + // Only a click in the client area is followed by the mouse event that the + // postponed focus update waits for. Clicking the title bar or a resize border + // sends non-client messages instead, so postponing the update there would leave + // the window without keyboard focus until the user clicked inside it. + const auto hitTest = message == WM_POINTERACTIVATE ? HIWORD (wParam) + : LOWORD (lParam); + + if (hitTest == HTCLIENT) + mouseActivateFlags |= gotMouseActivate; + break; } @@ -4153,6 +4169,10 @@ class HWNDComponentPeer final : public ComponentPeer } case WM_NCLBUTTONUP: + // A caption click that was released without moving must not be left pending, or + // the next mouse move over the frame hands a stale click to DefWindowProc. + captionMouseDown.reset(); + switch (wParam) { case HTCLOSE: From 8c0b1df12eb8188656e7973f6919cc40f2ee81b6 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Fri, 21 Aug 2026 09:25:41 +0200 Subject: [PATCH 28/40] Restart an unchanged device even when isPlaying() is stale setAudioDeviceSetup() short-circuits when the requested setup matches the running one: instead of re-opening the device it restarts it, so that a caller which stopped the device in order to change the format it is about to play ends up running again. That restart was guarded on !isPlaying(). isPlaying() cannot be trusted at that point. CoreAudioIODevice::stop() calls CoreAudioInternal::stop (leaveInterruptRunning = true), which clears the device's callback but leaves the IOProc registered, and clears the playing flag only on the branch that also tears the IOProc down. After such a stop the device reports isPlaying() == true while holding no callback at all, so the restart was skipped and the callback never reattached. The IOProc kept firing, the output buffer was cleared on every cycle, and the host application never received a single callback - playback produced silence indefinitely while the device reported itself open and playing. Start unconditionally instead. CoreAudioInternal::start() attaches a callback only when there is none and calls AudioDeviceStart only when no IOProc is registered, so restarting a device that genuinely is running costs nothing. Co-Authored-By: Claude Opus 5 --- .../audio_io/juce_AudioDeviceManager.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp b/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp index 873eadf18493..a8ab5719e4e1 100644 --- a/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp +++ b/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp @@ -832,8 +832,12 @@ String AudioDeviceManager::setAudioDeviceSetup (const AudioDeviceSetup& newSetup // stopped. Restarting also gives the callback the audioDeviceAboutToStart() it needs to // rebuild its converters. A device that is no longer open falls through to the re-open // path below. - if (! currentAudioDevice->isPlaying()) - currentAudioDevice->start (callbackHandler->getAudioIODeviceCallback()); + // + // Started unconditionally: stop() clears the callback but leaves the interrupt running, + // clearing the playing flag only when it also tears the IOProc down. isPlaying() can + // therefore be true with no callback attached, which left the device running silent. + // start() is idempotent - it reattaches a callback only when there is none. + currentAudioDevice->start (callbackHandler->getAudioIODeviceCallback()); return {}; } From dfc69530339cff2fc383354a17a5416dfdb5b20f Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Wed, 26 Aug 2026 11:58:07 +0200 Subject: [PATCH 29/40] VBlank: remove the flagPaintPending latch that duplicated AsyncUpdater's coalescing VBlankThread set a flagPaintPending latch before triggering, skipped the trigger while it was set, and cleared it only from handleAsyncUpdate(). That duplicates what AsyncUpdater already does - triggerAsyncUpdate() is a no-op while its own message is undelivered - but the copy cannot see whether delivery happened, so the two could get out of step. When they did, every vblank was discarded for as long as the delay lasted, which stopped repainting for the whole process instead of costing only the frames actually delayed. Let AsyncUpdater be the only thing tracking its own message. This also subsumes the ScopeGuard added earlier for a listener throwing: with no latch there is nothing left to strand. Co-Authored-By: Claude Opus 5 (1M context) --- .../native/juce_VBlank_windows.cpp | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/modules/juce_gui_basics/native/juce_VBlank_windows.cpp b/modules/juce_gui_basics/native/juce_VBlank_windows.cpp index e4ec8d1f45ce..c7dc9135f302 100644 --- a/modules/juce_gui_basics/native/juce_VBlank_windows.cpp +++ b/modules/juce_gui_basics/native/juce_VBlank_windows.cpp @@ -132,15 +132,20 @@ class VBlankThread : private Thread, if (now - lastVBlankEvent.exchange (now) < 1.0) sleep (1); - const auto stateToRead = state.fetch_or (flagPaintPending); - - if ((stateToRead & flagExit) != 0) + if ((state.load() & flagExit) != 0) return; - if ((stateToRead & flagPaintPending) != 0) - continue; - + // Acon Digital modification - this used to set a flagPaintPending latch and skip the + // trigger whenever it was already set, clearing it only from handleAsyncUpdate(). + // That duplicates the coalescing AsyncUpdater already does - triggerAsyncUpdate() is + // a no-op while a message of its own is undelivered - but the copy cannot see whether + // delivery happened, so the two could get out of step. A single AsyncUpdater message + // delayed by a few seconds (measured: one delayed while the rest of the message queue + // kept flowing) then discarded *every* vblank for as long as the delay lasted, which + // stopped repainting for the whole process, on every window, until it cleared. Let + // AsyncUpdater be the only thing tracking its own message. triggerAsyncUpdate(); + // Acon Digital modification - End of modification } else { @@ -155,12 +160,6 @@ class VBlankThread : private Thread, void handleAsyncUpdate() override { - // Acon Digital modification - clear the pending flag from a scope guard. An exception thrown - // by a listener is swallowed by the message queue, and the flag then stayed set forever, - // which permanently stops this monitor's vblank stream and so freezes every window on it. - const ScopeGuard clearPaintPending { [this] { state &= ~flagPaintPending; } }; - // Acon Digital modification - End of modification - const auto timestampSec = lastVBlankEvent / 1000.0; // Acon Digital modification - iterate a snapshot and re-check each listener before calling @@ -177,7 +176,9 @@ class VBlankThread : private Thread, enum Flags { flagExit = 1 << 0, - flagPaintPending = 1 << 1, + // Acon Digital modification - flagPaintPending removed, see run(). AsyncUpdater already + // coalesces its own undelivered message, and the duplicate latch here could strand the + // stream when delivery was slow. }; //============================================================================== From 625aff8f43c3dfa1dfd1ea544a7752d5e0d8cea1 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Wed, 26 Aug 2026 11:58:17 +0200 Subject: [PATCH 30/40] Messaging: dispatch queued messages one at a time so a modal loop cannot strand the rest InternalMessageQueue::dispatchMessages() swapped the whole queue into a local array before walking it. Dispatching a message can enter a modal loop - an ApplicationCommandTarget command opening a dialog, a button handler calling runModalLoop() - and that loop does not return until the dialog closes. Every message further along the batch had already been taken out of the live queue by the swap, so the nested message pump could not reach it, and it sat undelivered for the whole life of the dialog. On Windows all repainting is gated on a vblank AsyncUpdater callback, so whenever that message landed in such a batch the entire application stopped repainting for the life of the dialog - every window, not just the dialog. Timers kept firing throughout, because the timer thread posts fresh messages into the now-empty queue, which made the queue look as though it were flowing. Confirmed by stack trace: the message thread inside dispatchMessages() one frame below runModalLoop(), with the vblank AsyncUpdater callback count frozen for 23 seconds while the vblank thread triggered 1168 times. Take one message at a time from the live queue instead, so anything still queued stays reachable from a nested pump, and keep a wake-up posted while the queue is non-empty so that pump knows to come back for it. That preserves postMessage()'s invariant that a non-empty queue always has exactly one wake-up outstanding. Still bounded by however many were queued on entry, so it returns to the Win32 pump just as the batch version did. Co-Authored-By: Claude Opus 5 (1M context) --- .../native/juce_Messaging_windows.cpp | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/modules/juce_events/native/juce_Messaging_windows.cpp b/modules/juce_events/native/juce_Messaging_windows.cpp index d24de9862e46..42bab68bd003 100644 --- a/modules/juce_events/native/juce_Messaging_windows.cpp +++ b/modules/juce_events/native/juce_Messaging_windows.cpp @@ -227,23 +227,53 @@ class InternalMessageQueue void dispatchMessages() { - ReferenceCountedArray messagesToDispatch; + // Acon Digital modification - this used to swap the whole queue into a local array and walk + // it. Dispatching a message can enter a modal loop - an ApplicationCommandTarget command + // opening a dialog, a button handler calling runModalLoop() - and that loop does not return + // until the dialog closes. Every message further along the batch had already been taken out + // of the live queue by the swap, so the nested message pump could not reach it, and it sat + // undelivered for the whole life of the dialog. Confirmed by stack trace: a VBlankThread + // AsyncUpdater message stranded behind a CommandMessage, which stopped repainting for the + // entire process - every window on the display - while timers, posted afterwards into the + // now-empty queue, carried on as normal and made it look as though the queue was flowing. + // + // Take one message at a time from the live queue instead, so anything still queued stays + // reachable from a nested pump, and keep a wake-up posted while the queue is non-empty so + // that pump knows to come back for it. That preserves postMessage()'s invariant that a + // non-empty queue always has exactly one wake-up outstanding. + // + // Still bounded by however many were queued on entry, so this returns to the Win32 pump just + // as the batch version did rather than draining a fast producer indefinitely. Anything that + // arrives meanwhile is left queued with a wake-up posted for it. + int numToDispatch = 0; { const ScopedLock sl (lock); - - if (messageQueue.isEmpty()) - return; - - messagesToDispatch.swapWith (messageQueue); + numToDispatch = messageQueue.size(); } - for (int i = 0; i < messagesToDispatch.size(); ++i) + for (int i = 0; i < numToDispatch; ++i) { - auto message = messagesToDispatch.getUnchecked (i); + MessageManager::MessageBase::Ptr message; + bool moreMessagesQueued = false; + + { + const ScopedLock sl (lock); + + if (messageQueue.isEmpty()) + return; + + message = messageQueue.removeAndReturn (0); + moreMessagesQueued = ! messageQueue.isEmpty(); + } + + if (moreMessagesQueued) + PostMessage (juce_messageWindowHandle, customMessageID, 0, 0); + message->incReferenceCount(); dispatchMessage (message.get()); } + // Acon Digital modification - End of modification } //============================================================================== From 431068a8df2e59f07f309a547be4b6504ffdfba2 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Fri, 28 Aug 2026 15:11:17 +0200 Subject: [PATCH 31/40] VST3: record the classes a plug-in replaces, so a host can follow a moved id A VST3 can declare, through IPluginCompatibility, the class ids it replaces, which is how a project saved before a plug-in changed identity keeps resolving. The plug-in client writes that declaration and juce_vst3_helper reads it into a moduleinfo.json, but the host side ignored it entirely: the scan only ever looked at kVstAudioEffectClass, so a saved reference to a superseded id came back as a missing plug-in even though the installed plug-in was volunteering the mapping. PluginDescription gains compatibleUniqueIds, holding those ids in the same hashed forms as uniqueId and deprecatedUid so a host can compare them against a saved reference directly. Both hashes are recorded for each declared class, because a stored reference may carry either depending on the JUCE version and the platform that wrote it. The scan fills it from the manifest when the bundle has one, since that needs no instantiation, and otherwise from the factory - which is the only route for a plug-in built with VST3_AUTO_MANIFEST disabled, and so for every plug-in JUCE builds that way. A class naming its own current id among those it replaces, as JUCE does when JUCE_VST3_CAN_REPLACE_VST2 is combined with JUCE_VST3_COMPATIBLE_CLASSES, is dropped rather than recorded. The field is serialised with the rest of the description, and its absence in a list written before this existed reads as "no information" rather than "declares nothing". Co-Authored-By: Claude Opus 5 (1M context) --- .../format_types/juce_VST3PluginFormatImpl.h | 87 +++++++++++++++++++ .../processors/juce_PluginDescription.cpp | 18 ++++ .../processors/juce_PluginDescription.h | 18 ++++ 3 files changed, 123 insertions(+) diff --git a/modules/juce_audio_processors_headless/format_types/juce_VST3PluginFormatImpl.h b/modules/juce_audio_processors_headless/format_types/juce_VST3PluginFormatImpl.h index feeb930012d3..e1349e68e725 100644 --- a/modules/juce_audio_processors_headless/format_types/juce_VST3PluginFormatImpl.h +++ b/modules/juce_audio_processors_headless/format_types/juce_VST3PluginFormatImpl.h @@ -184,6 +184,83 @@ static int getHashForRange (Range&& range) noexcept return (int) value; } +/* Records a class id the plug-in says it replaces, in both of the hashed forms a description + carries. A saved reference may hold either, depending on the JUCE version and the platform that + wrote it, so matching has to be able to try both. + + A plug-in may name its own current id among the ones it replaces - JUCE does exactly that when + JUCE_VST3_CAN_REPLACE_VST2 is enabled alongside JUCE_VST3_COMPATIBLE_CLASSES - and that entry is + dropped, so this field holds only ids the description does not already carry. +*/ +static void addCompatibleClassId (PluginDescription& description, const TUID& oldCid) +{ + const auto deprecatedForm = getHashForRange (oldCid); + const auto normalisedForm = getHashForRange (getNormalisedTUID (oldCid)); + + if (deprecatedForm == description.deprecatedUid && normalisedForm == description.uniqueId) + return; + + description.compatibleUniqueIds.addIfNotAlreadyThere (deprecatedForm); + description.compatibleUniqueIds.addIfNotAlreadyThere (normalisedForm); +} + +static void addCompatibleClassId (PluginDescription& description, const std::string& oldCid) +{ + if (const auto uid = VST3::UID::fromString (oldCid)) + addCompatibleClassId (description, uid->data()); +} + +/* Records the compatibility mapping a plug-in declares through IPluginCompatibility. + + A bundle carrying a moduleinfo.json states the same thing statically, and that path is preferred + because it needs no instantiation. This one is for the rest: a plug-in built with + VST3_AUTO_MANIFEST disabled ships no manifest, so its factory is the only place the mapping can + be read - which includes every plug-in JUCE builds that way. +*/ +static void addCompatibleClassIdsFromFactory (PluginDescription& description, + IPluginFactory* factory, + const TUID& componentCid) +{ + if (factory == nullptr) + return; + + const auto numClasses = factory->countClasses(); + + for (auto classIndex = decltype (numClasses){}; classIndex < numClasses; ++classIndex) + { + PClassInfo classInfo{}; + + if (factory->getClassInfo (classIndex, &classInfo) != kResultOk) + continue; + + if (std::strcmp (classInfo.category, kPluginCompatibilityClass) != 0) + continue; + + Steinberg::IPluginCompatibility* compatibility = nullptr; + + if (factory->createInstance (classInfo.cid, Steinberg::IPluginCompatibility::iid, (void**) &compatibility) != kResultOk + || compatibility == nullptr) + { + continue; + } + + const ScopeGuard releaseCompatibility { [compatibility] { compatibility->release(); } }; + + Steinberg::MemoryStream stream; + + if (compatibility->getCompatibilityJSON (&stream) != kResultOk) + continue; + + const std::string_view json (stream.getData(), (size_t) stream.getSize()); + + if (const auto entries = Steinberg::ModuleInfoLib::parseCompatibilityJson (json, nullptr)) + for (const auto& entry : *entries) + if (const auto newUid = VST3::UID::fromString (entry.newCID); newUid && *newUid == VST3::UID (componentCid)) + for (const auto& oldCid : entry.oldCID) + addCompatibleClassId (description, oldCid); + } +} + template static void fillDescriptionWith (PluginDescription& description, ObjectType& object) { @@ -238,6 +315,12 @@ static std::vector createPluginDescriptions (const File& plug description.deprecatedUid = getHashForRange (uid->data()); description.uniqueId = getHashForRange (getNormalisedTUID (uid->data())); + // The manifest states the compatibility mapping outright, so no instantiation is needed. + for (const auto& entry : info.compatibility) + if (const auto newUid = VST3::UID::fromString (entry.newCID); newUid && *newUid == *uid) + for (const auto& oldCid : entry.oldCID) + addCompatibleClassId (description, oldCid); + StringArray categories; for (const auto& category : c.subCategories) @@ -1039,6 +1122,8 @@ struct DescriptionLister createPluginDescription (desc, file, companyName, name, info, info2.get(), infoW.get(), numInputs, numOutputs); + addCompatibleClassIdsFromFactory (desc, &factory, info.cid); + component->terminate(); } else @@ -1612,6 +1697,8 @@ struct VST3ComponentHolder totalNumInputChannels, totalNumOutputChannels); + addCompatibleClassIdsFromFactory (description, factory.get(), info.cid); + description.hasARAExtension = hasARAExtension (factory.get(), description.name); } diff --git a/modules/juce_audio_processors_headless/processors/juce_PluginDescription.cpp b/modules/juce_audio_processors_headless/processors/juce_PluginDescription.cpp index 5c331a012cde..9223a661d270 100644 --- a/modules/juce_audio_processors_headless/processors/juce_PluginDescription.cpp +++ b/modules/juce_audio_processors_headless/processors/juce_PluginDescription.cpp @@ -92,6 +92,16 @@ std::unique_ptr PluginDescription::createXml() const e->setAttribute ("uid", String::toHexString (deprecatedUid)); + if (! compatibleUniqueIds.isEmpty()) + { + StringArray hexIds; + + for (const auto id : compatibleUniqueIds) + hexIds.add (String::toHexString (id)); + + e->setAttribute ("compatibleUniqueIds", hexIds.joinIntoString (" ")); + } + return e; } @@ -117,6 +127,14 @@ bool PluginDescription::loadFromXml (const XmlElement& xml) deprecatedUid = xml.getStringAttribute ("uid").getHexValue32(); uniqueId = xml.getStringAttribute ("uniqueId", "0").getHexValue32(); + // Absent in a list written before this field existed, which reads as "no information" + // rather than "declares nothing" - the plug-in supplies it again on the next scan. + compatibleUniqueIds.clearQuick(); + + for (const auto& hexId : StringArray::fromTokens (xml.getStringAttribute ("compatibleUniqueIds"), " ", {})) + if (hexId.isNotEmpty()) + compatibleUniqueIds.add (hexId.getHexValue32()); + return true; } diff --git a/modules/juce_audio_processors_headless/processors/juce_PluginDescription.h b/modules/juce_audio_processors_headless/processors/juce_PluginDescription.h index d762a2fa525b..4a4c4095bbaa 100644 --- a/modules/juce_audio_processors_headless/processors/juce_PluginDescription.h +++ b/modules/juce_audio_processors_headless/processors/juce_PluginDescription.h @@ -126,6 +126,24 @@ class JUCE_API PluginDescription */ int uniqueId = 0; + /** Ids this plug-in has previously been released under and still answers to. + + A VST3 can declare, through IPluginCompatibility, the class ids it replaces, + so that a host can resolve a project saved before the plug-in's identity + moved. This array holds those ids in the same hashed form as uniqueId and + deprecatedUid, so a host can compare them against a saved reference + directly; both hashes are included for each declared class, because a + stored reference may carry either. + + Empty when the plug-in declares nothing, and empty for formats other than + VST3. It is also empty for a description restored from a plug-in list + written before this field existed, so treat it as "no information" rather + than "no compatible ids". + + @see createIdentifierString + */ + Array compatibleUniqueIds; + /** True if the plug-in identifies itself as a synthesiser. */ bool isInstrument = false; From 81471b2b9dad3af5352c26c4c98d8392b5167e12 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Mon, 31 Aug 2026 15:10:22 +0200 Subject: [PATCH 32/40] Hand the foreground to another of our windows before destroying one Closing a dialog dropped the whole application to the background: the foreground went to whatever unrelated application was next in the global z-order, and keyboard input went with it. Windows picks the next foreground window itself when the current one is destroyed, and that choice is not reliable - a trace showed it skipping the Acoustica main window, which sat directly below the dialog being closed and was its Win32 owner, in favour of an editor from another process. Remember the window each peer is raised from, and hand the foreground to it explicitly just before DestroyWindow. It has to happen there, while we still hold the foreground, because that is the one condition under which Windows grants SetForegroundWindow. Giving the dialog a Win32 owner looks like the tidier fix and is not: for a non-child window GetParent() returns the owner, and the layout, scaling and transparency code in this file reads GetParent() as the WS_CHILD parent. Menus are per-pixel transparent, so an owner offset every one of them by the owner's screen position and they drew as displaced black frames. The comment on findWindowToRestoreActivationTo() records that. Co-Authored-By: Claude Opus 5 (1M context) --- .../native/juce_Windowing_windows.cpp | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp index 0a6863fea87d..90a98a3613ff 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -1672,6 +1672,31 @@ class HWNDComponentPeer final : public ComponentPeer return nullptr; } + // Acon Digital modification - picks the window that should get the foreground back when this one + // is destroyed. Windows will not reliably choose it: destroying the foreground window makes it + // walk the global z-order, which lands on another application even when one of ours is directly + // below, so closing a dialog dropped the whole application to the background. Note that the fix + // cannot be to give the window a Win32 owner - for a non-child window GetParent() returns the + // owner, and the layout, scaling and transparency code in this file uses GetParent() to mean the + // WS_CHILD parent. Must be called on the message thread, as GetActiveWindow() is per-thread. + static HWND findWindowToRestoreActivationTo (int styleFlags) noexcept + { + // A minimise or maximise button marks a primary window; there is nothing above one of those + // to hand activation back to. + if ((styleFlags & (windowHasMinimiseButton | windowHasMaximiseButton)) != 0) + return nullptr; + + // The window this one is being raised from, so that dismissing a dialog opened from another + // dialog goes back to that one rather than all the way out to the main window. It is + // re-checked before use, since it may have been destroyed first. + if (auto* active = GetActiveWindow()) + if (JuceWindowIdentifier::isJUCEWindow (active)) + return active; + + return nullptr; + } + // Acon Digital modification - End of modification + //============================================================================== bool isInside (HWND h) const noexcept { @@ -2017,6 +2042,12 @@ class HWNDComponentPeer final : public ComponentPeer private: HWND hwnd, parentToAddTo; + + // Acon Digital modification - where the foreground is handed on destruction, if this window still + // holds it. Purely an activation target, deliberately not a Win32 owner - see + // findWindowToRestoreActivationTo(). + HWND windowToRestoreActivationTo = nullptr; + // Acon Digital modification - End of modification std::unique_ptr shadower; uint32 lastPaintTime = 0; ULONGLONG lastMagnifySize = 0; @@ -2188,6 +2219,12 @@ class HWNDComponentPeer final : public ComponentPeer return appearsOnTaskbar ? WS_EX_APPWINDOW : WS_EX_TOOLWINDOW; }); + // Acon Digital modification - remember where to hand the foreground back, while the window + // this one is being raised from is still the active one. + windowToRestoreActivationTo = parentToAddTo != nullptr ? parentToAddTo + : findWindowToRestoreActivationTo (styleFlags); + // Acon Digital modification - End of modification + hwnd = CreateWindowEx (exstyle, WindowClassHolder::getInstance()->getWindowClassName(), L"", type, 0, 0, 0, 0, parentToAddTo, nullptr, (HINSTANCE) Process::getCurrentModuleInstanceHandle(), nullptr); @@ -2284,6 +2321,18 @@ class HWNDComponentPeer final : public ComponentPeer // NB: we need to do this before DestroyWindow() as child HWNDs will be invalid after EnumChildWindows (hwnd, revokeChildDragDropCallback, 0); + // Acon Digital modification - hand the foreground on ourselves rather than leaving + // Windows to guess, which drops the application to the background. This has to happen + // before DestroyWindow, while we still hold the foreground: that is the condition under + // which SetForegroundWindow is granted. A WS_CHILD window is never the foreground window, + // so a plug-in editor embedded in a host never reaches this. + if (hwnd == GetForegroundWindow()) + if (auto* target = windowToRestoreActivationTo) + if (IsWindow (target) && JuceWindowIdentifier::isJUCEWindow (target) + && IsWindowVisible (target) && IsWindowEnabled (target)) + SetForegroundWindow (target); + // Acon Digital modification - End of modification + DestroyWindow (hwnd); } } From d29fb3d715f201744bca021942ba1450385e0ec6 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Mon, 31 Aug 2026 15:10:28 +0200 Subject: [PATCH 33/40] FileChooser: capture the owner window on the message thread The owner passed to the native dialog was read as GetActiveWindow() inside openDialog(), which for an asynchronous chooser runs on the chooser's own thread. GetActiveWindow() is per-thread, so it returned null there and every launchAsync dialog was shown with no owner at all - free to fall behind the application, and not modal against anything. Read it in open() instead, which is on the message thread in both the synchronous and asynchronous cases. Starting the thread orders the write against the reads in openDialog(), so no atomic is needed. Co-Authored-By: Claude Opus 5 (1M context) --- .../native/juce_FileChooser_windows.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/modules/juce_gui_basics/native/juce_FileChooser_windows.cpp b/modules/juce_gui_basics/native/juce_FileChooser_windows.cpp index 76f739138137..7e4ea4ec6ff7 100644 --- a/modules/juce_gui_basics/native/juce_FileChooser_windows.cpp +++ b/modules/juce_gui_basics/native/juce_FileChooser_windows.cpp @@ -103,6 +103,15 @@ class Win32NativeFileChooser final : private Thread // the thread should not be running nativeDialogRef.set (nullptr); + // Acon Digital modification - the owner used to be read as GetActiveWindow() inside + // openDialog(), which in the async case runs on the thread started below. GetActiveWindow() + // is per-thread, so it returned null there and every async dialog was shown with no owner, + // free to end up behind the application. Read it here instead, while we are still on the + // message thread. Starting the thread orders this write against the reads in openDialog(), + // so no atomic is needed. + ownerWindow = GetActiveWindow(); + // Acon Digital modification - End of modification + if (async) { jassert (! isThreadRunning()); @@ -173,6 +182,8 @@ class Win32NativeFileChooser final : private Thread HeapBlock filters; Atomic nativeDialogRef { nullptr }; + // Acon Digital modification - the dialog's owner, captured on the message thread in open() + HWND ownerWindow = nullptr; bool shouldCancel = false; struct FreeLPWSTR @@ -332,7 +343,7 @@ class Win32NativeFileChooser final : private Thread Events events { *this }; ScopedAdvise scope { dialog, events }; - return dialog.Show (GetActiveWindow()) == S_OK; + return dialog.Show (ownerWindow) == S_OK; // Acon Digital modification - was GetActiveWindow() }(); ScopedLock lock (deletingDialog); @@ -438,7 +449,7 @@ class Win32NativeFileChooser final : private Thread if (selectsDirectories) { BROWSEINFO bi = {}; - bi.hwndOwner = GetActiveWindow(); + bi.hwndOwner = ownerWindow; // Acon Digital modification - was GetActiveWindow() bi.pszDisplayName = files; bi.lpszTitle = title.toWideCharPointer(); bi.lParam = (LPARAM) this; @@ -488,7 +499,7 @@ class Win32NativeFileChooser final : private Thread startingFile.getFullPathName().copyToUTF16 (files, charsAvailableForResult * sizeof (WCHAR)); } - of.hwndOwner = GetActiveWindow(); + of.hwndOwner = ownerWindow; // Acon Digital modification - was GetActiveWindow() of.lpstrFilter = filters.getData(); of.nFilterIndex = 1; of.lpstrFile = files; From 34839ccac07dae5ff492b20e67ec8691b326423a Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Mon, 14 Sep 2026 09:34:55 +0200 Subject: [PATCH 34/40] X11: Accept XDND drags from sources announcing a version newer than 3 mutter's XWayland bridge sends XdndEnter with version 5 regardless of the XdndAware version the target advertises, so every file drag from a Wayland app on GNOME was silently rejected. The later versions only add to version 3, so accept them. Co-Authored-By: Claude Opus 5 (1M context) --- modules/juce_gui_basics/native/juce_DragAndDrop_linux.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/juce_gui_basics/native/juce_DragAndDrop_linux.cpp b/modules/juce_gui_basics/native/juce_DragAndDrop_linux.cpp index 739caff51c7d..9b96bb408a57 100644 --- a/modules/juce_gui_basics/native/juce_DragAndDrop_linux.cpp +++ b/modules/juce_gui_basics/native/juce_DragAndDrop_linux.cpp @@ -219,7 +219,9 @@ class X11DragState dragAndDropCurrentMimeType = 0; auto dndCurrentVersion = (static_cast (clientMsg.data.l[1]) & 0xff000000) >> 24; - if (dndCurrentVersion < 3 || dndCurrentVersion > XWindowSystemUtilities::Atoms::DndVersion) + // Newer sources are accepted: their messages are a superset of version 3, and mutter's + // XWayland bridge always sends version 5 regardless of the XdndAware version we advertise. + if (dndCurrentVersion < 3) { dragAndDropSourceWindow = 0; return; From d7da88d9096f321fdbf855c180707e5e3088e017 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Mon, 14 Sep 2026 10:24:48 +0200 Subject: [PATCH 35/40] NSViewComponentPeer: Detach a floating child window while it is on another display A windowFloatingChild window is attached with addChildWindow:, and a child window lives in its parent's Space. With "Displays have separate Spaces" enabled - the macOS default - a child on any other display is not drawn, so floating windows could not be placed on a second display at all. Keep a floating child attached only while its frame is on the parent window's display. Anywhere else it is detached and floats on its own at NSFloatingWindowLevel with hidesOnDeactivate, so it never covers other applications, and it is re-attached once it is back on the parent's display. The attachment is re-evaluated in setBounds() before the frame is applied, in setVisible(), and when the parent window changes screen. A detached child is ordered out and back in with the parent's miniaturise and deminiaturise. Co-Authored-By: Claude Opus 5 (1M context) --- .../native/juce_NSViewComponentPeer_mac.mm | 115 +++++++++++++++++- 1 file changed, 111 insertions(+), 4 deletions(-) diff --git a/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm b/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm index 4c42d7b29583..fac5b3b2a1b4 100644 --- a/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm +++ b/modules/juce_gui_basics/native/juce_NSViewComponentPeer_mac.mm @@ -296,6 +296,15 @@ static void setOwner (id self, Callback* newOwner) // does before the window is first shown) can detach a child from its parent. if ((windowStyleFlags & windowFloatingChild) != 0 && viewToAttachTo != nil) floatingChildParent = [[viewToAttachTo window] retain]; + + // A child detached onto another display no longer follows its parent by itself - see + // updateFloatingChildAttachment(). + if (floatingChildParent != nil) + { + scopedObservers.emplace_back (view, @selector (floatingParentWillMiniaturize:), NSWindowWillMiniaturizeNotification, floatingChildParent); + scopedObservers.emplace_back (view, @selector (floatingParentDidDeminiaturize:), NSWindowDidDeminiaturizeNotification, floatingChildParent); + scopedObservers.emplace_back (view, @selector (floatingParentDidChangeScreen:), NSWindowDidChangeScreenNotification, floatingChildParent); + } } auto alpha = component.getAlpha(); @@ -364,8 +373,13 @@ void setVisible (bool shouldBeVisible) override { // (Re)attach floating children to their parent so they float above it (without // being globally always-on-top) and minimise/restore/close together with it. - if (floatingChildParent != nil && [window parentWindow] == nil) - [floatingChildParent addChildWindow: window ordered: NSWindowAbove]; + if (floatingChildParent != nil) + { + updateFloatingChildAttachment ([window frame]); + + if (! floatingChildDetached && [window parentWindow] == nil) + [floatingChildParent addChildWindow: window ordered: NSWindowAbove]; + } ++insideToFrontCall; [window orderFront: nil]; @@ -420,8 +434,12 @@ void setBounds (const Rectangle& newBounds, bool) override } else { - [window setFrame: [window frameRectForContentRect: flippedScreenRect (r)] - display: false]; + const auto frame = [window frameRectForContentRect: flippedScreenRect (r)]; + + // Before the frame lands on another display, where an attached child would not be drawn + updateFloatingChildAttachment (frame); + + [window setFrame: frame display: false]; } if (! CGSizeEqualToSize (oldViewSize, r.size)) @@ -631,6 +649,76 @@ bool setAlwaysOnTop (bool alwaysOnTop) override return true; } + // A child window lives in its parent's Space, so with "Displays have separate Spaces" it is not + // drawn on any other display. A floating child is therefore only attached while it is on its + // parent's display; anywhere else it floats on its own, hidden whenever the app is inactive. + void updateFloatingChildAttachment (NSRect frame) + { + if (floatingChildParent == nil) + return; + + NSScreen* parentScreen = [floatingChildParent screen]; + NSScreen* frameScreen = nil; + const auto centre = NSMakePoint (NSMidX (frame), NSMidY (frame)); + + for (NSScreen* screen in [NSScreen screens]) + if (NSPointInRect (centre, [screen frame])) + frameScreen = screen; + + const auto onParentDisplay = parentScreen == nil || frameScreen == nil + || NSEqualRects ([frameScreen frame], [parentScreen frame]); + + if (! onParentDisplay && ! floatingChildDetached) + { + if ([window parentWindow] != nil) + [floatingChildParent removeChildWindow: window]; + + if (! isAlwaysOnTop) + [window setLevel: NSFloatingWindowLevel]; + + [window setHidesOnDeactivate: YES]; + floatingChildDetached = true; + } + else if (onParentDisplay && floatingChildDetached) + { + floatingChildDetached = false; + + if (isAlwaysOnTop) + { + setAlwaysOnTop (true); + } + else + { + [window setLevel: NSNormalWindowLevel]; + [window setHidesOnDeactivate: NO]; + } + + if ([window isVisible] && [window parentWindow] == nil) + [floatingChildParent addChildWindow: window ordered: NSWindowAbove]; + } + } + + void floatingParentWillMiniaturize() + { + if (floatingChildDetached) + [window orderOut: nil]; + } + + void floatingParentDidDeminiaturize() + { + if (floatingChildDetached && component.isVisible()) + { + ++insideToFrontCall; + [window orderFront: nil]; + --insideToFrontCall; + } + } + + void floatingParentDidChangeScreen() + { + updateFloatingChildAttachment ([window frame]); + } + void toFront (bool makeActiveWindow) override { if (isSharedWindow) @@ -1764,6 +1852,7 @@ auto tie() const WeakReference safeComponent; const bool isSharedWindow = false; NSWindow* floatingChildParent = nil; + bool floatingChildDetached = false; #if USE_COREGRAPHICS_RENDERING bool usingCoreGraphics = true; #else @@ -2304,6 +2393,24 @@ static id getAccessibleChild (id self) } }); + addMethod (@selector (floatingParentWillMiniaturize:), [] (id self, SEL, NSNotification*) + { + if (auto* p = getOwner (self)) + p->floatingParentWillMiniaturize(); + }); + + addMethod (@selector (floatingParentDidDeminiaturize:), [] (id self, SEL, NSNotification*) + { + if (auto* p = getOwner (self)) + p->floatingParentDidDeminiaturize(); + }); + + addMethod (@selector (floatingParentDidChangeScreen:), [] (id self, SEL, NSNotification*) + { + if (auto* p = getOwner (self)) + p->floatingParentDidChangeScreen(); + }); + addMethod (@selector (wantsDefaultClipping), [] (id, SEL) { return YES; }); // (this is the default, but may want to customise it in future) addMethod (@selector (worksWhenModal), [] (id self, SEL) From 9f48f64a64c51686effc26b3ba4306021f4c0bde Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Mon, 14 Sep 2026 12:18:56 +0200 Subject: [PATCH 36/40] VST3: report each class as discovery reaches it findAllTypesForFile() instantiates and initialises every audio effect class in a file that has no moduleinfo.json, one after another. A shell housing dozens of plug-ins can take far longer to discover than a host's scan time limit allows, and nothing lets the host tell that slow progress from a hang. VST3PluginFormatHeadless gains onClassDiscoveryStarted, called with each class's name just before findDescriptionsSlow() instantiates it. A host can restart its time limit per class, and name the class a hang happened in. The moduleinfo.json path instantiates nothing and does not call it. Co-Authored-By: Claude Opus 5 (1M context) --- .../format_types/juce_VST3PluginFormatHeadless.cpp | 2 +- .../format_types/juce_VST3PluginFormatHeadless.h | 7 +++++++ .../format_types/juce_VST3PluginFormatImpl.h | 6 +++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/modules/juce_audio_processors_headless/format_types/juce_VST3PluginFormatHeadless.cpp b/modules/juce_audio_processors_headless/format_types/juce_VST3PluginFormatHeadless.cpp index a82f6da2a471..915cca9f12a1 100644 --- a/modules/juce_audio_processors_headless/format_types/juce_VST3PluginFormatHeadless.cpp +++ b/modules/juce_audio_processors_headless/format_types/juce_VST3PluginFormatHeadless.cpp @@ -76,7 +76,7 @@ void VST3PluginFormatHeadless::findAllTypesForFile (OwnedArray onClassDiscoveryStarted; + private: //============================================================================== void createPluginInstance (const PluginDescription&, double initialSampleRate, diff --git a/modules/juce_audio_processors_headless/format_types/juce_VST3PluginFormatImpl.h b/modules/juce_audio_processors_headless/format_types/juce_VST3PluginFormatImpl.h index e1349e68e725..29f88ea87a8a 100644 --- a/modules/juce_audio_processors_headless/format_types/juce_VST3PluginFormatImpl.h +++ b/modules/juce_audio_processors_headless/format_types/juce_VST3PluginFormatImpl.h @@ -1047,7 +1047,8 @@ struct DescriptionLister static std::vector findDescriptionsSlow (VST3HostContextHeadless& host, IPluginFactory& factory, - const File& file) + const File& file, + const std::function& onClassDiscoveryStarted = {}) { std::vector result; @@ -1107,6 +1108,9 @@ struct DescriptionLister foundNames.add (name); + if (onClassDiscoveryStarted != nullptr) + onClassDiscoveryStarted (name); + PluginDescription desc; { From 9e81b10740074675db670f96404c545ce8db85d6 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Mon, 14 Sep 2026 13:02:54 +0200 Subject: [PATCH 37/40] Linux: resolve the main executable through /proc/self/exe instead of argv[0] For the main program, glibc's dladdr reports argv[0] as the file name. When an app is started through PATH (or a .desktop Exec= line) that is a bare name, so currentExecutableFile resolved against the working directory and sibling helper executables could not be found. Co-Authored-By: Claude Opus 5 (1M context) --- .../juce_core/native/juce_BasicNativeHeaders.h | 1 + modules/juce_core/native/juce_SharedCode_posix.h | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/modules/juce_core/native/juce_BasicNativeHeaders.h b/modules/juce_core/native/juce_BasicNativeHeaders.h index a4d03b656ffd..ab1fa4d4bc57 100644 --- a/modules/juce_core/native/juce_BasicNativeHeaders.h +++ b/modules/juce_core/native/juce_BasicNativeHeaders.h @@ -186,6 +186,7 @@ #include #include #include + #include #include #include #include diff --git a/modules/juce_core/native/juce_SharedCode_posix.h b/modules/juce_core/native/juce_SharedCode_posix.h index 4097e427aa9b..8d5ec80b426c 100644 --- a/modules/juce_core/native/juce_SharedCode_posix.h +++ b/modules/juce_core/native/juce_SharedCode_posix.h @@ -624,7 +624,23 @@ File juce_getExecutableFile() Dl_info exeInfo; auto localSymbol = (void*) juce_getExecutableFile; + + #if JUCE_LINUX && defined (__GLIBC__) + link_map* map = nullptr; + + // For the main program glibc reports argv[0], which is a bare name when the app was + // started through PATH, and would then resolve against the working directory. + if (dladdr1 (localSymbol, &exeInfo, RTLD_DL_LINKMAP, &map) != 0 && map != nullptr && map->l_name[0] == 0) + { + const File self ("/proc/self/exe"); + + if (self.isSymbolicLink()) + return self.getLinkedTarget().getFullPathName(); + } + #else dladdr (localSymbol, &exeInfo); + #endif + return CharPointer_UTF8 (exeInfo.dli_fname); } }; From 23af24ee38a8e2047cfcb0120c357b4f3362c252 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Mon, 14 Sep 2026 15:03:57 +0200 Subject: [PATCH 38/40] Linux: fix dladdr1 argument order when resolving the main executable glibc declares dladdr1 as (address, info, extra_info, flags); the flag and the output pointer were swapped, which fails to compile. Co-Authored-By: Claude Opus 5 (1M context) --- modules/juce_core/native/juce_SharedCode_posix.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/juce_core/native/juce_SharedCode_posix.h b/modules/juce_core/native/juce_SharedCode_posix.h index 8d5ec80b426c..681474658c9e 100644 --- a/modules/juce_core/native/juce_SharedCode_posix.h +++ b/modules/juce_core/native/juce_SharedCode_posix.h @@ -630,7 +630,8 @@ File juce_getExecutableFile() // For the main program glibc reports argv[0], which is a bare name when the app was // started through PATH, and would then resolve against the working directory. - if (dladdr1 (localSymbol, &exeInfo, RTLD_DL_LINKMAP, &map) != 0 && map != nullptr && map->l_name[0] == 0) + if (dladdr1 (localSymbol, &exeInfo, reinterpret_cast (&map), RTLD_DL_LINKMAP) != 0 + && map != nullptr && map->l_name[0] == 0) { const File self ("/proc/self/exe"); From f910bd41942e6575212f31e3b6aa17c6eafb2f2e Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Mon, 14 Sep 2026 15:24:48 +0200 Subject: [PATCH 39/40] LV2: report each bundle once, and rescan a plug-in only when its bundle changes searchPathsForPlugins() returned one bundle path per plugin in the whole lilv world, so a bundle holding many plugins was reported once per plugin, and bundles from folders already loaded came back whatever path was asked for. It now reports each bundle once, and only bundles under the requested paths. The default locations are always searched, so a host that stores its search path still sees a later change to LV2_PATH. pluginNeedsRescanning() returned true unconditionally, so a host that trusts it dropped and revalidated every LV2 plugin on every scan. It now returns true only when the URI no longer resolves or the bundle's modification time differs from the one getDescription() recorded. getPluginUrisInBundle() lets a host match a bundle path against descriptions, which name an LV2 plugin by URI. It reads only the loaded manifest. Co-Authored-By: Claude Opus 5 (1M context) --- .../juce_LV2PluginFormatHeadless.cpp | 5 ++ .../juce_LV2PluginFormatHeadless.h | 8 ++++ .../format_types/juce_LV2PluginFormatImpl.h | 47 +++++++++++++++++-- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatHeadless.cpp b/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatHeadless.cpp index 7cc31e684c37..2adeda64245b 100644 --- a/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatHeadless.cpp +++ b/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatHeadless.cpp @@ -86,6 +86,11 @@ FileSearchPath LV2PluginFormatHeadless::getDefaultLocationsToSearch() return pimpl->getDefaultLocationsToSearch(); } +StringArray LV2PluginFormatHeadless::getPluginUrisInBundle (const String& bundlePath) +{ + return pimpl->getPluginUrisInBundle (bundlePath); +} + bool LV2PluginFormatHeadless::requiresUnblockedMessageThreadDuringCreation (const PluginDescription&) const { return false; diff --git a/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatHeadless.h b/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatHeadless.h index b6ef60f3df04..d40846f4aa9f 100644 --- a/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatHeadless.h +++ b/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatHeadless.h @@ -72,6 +72,14 @@ class JUCE_API LV2PluginFormatHeadless : public AudioPluginFormat FileSearchPath getDefaultLocationsToSearch() override; + /** Returns the URIs of the plugins in a bundle that searchPathsForPlugins() has found. + + searchPathsForPlugins() reports bundle paths, but a PluginDescription names an LV2 plugin by its + URI, so the two never compare equal. This lets a host match a bundle against the descriptions + it already holds. Only the bundle's manifest is read; no plugin code is loaded. + */ + StringArray getPluginUrisInBundle (const String& bundlePath); + /** @internal */ class Pimpl; diff --git a/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatImpl.h b/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatImpl.h index 37f8dad6f84f..0b3a8f507351 100644 --- a/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatImpl.h +++ b/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatImpl.h @@ -4265,9 +4265,16 @@ class LV2PluginFormatHeadless::Pimpl return identifier; } - bool pluginNeedsRescanning (const PluginDescription&) + bool pluginNeedsRescanning (const PluginDescription& description) { - return true; + // Discovery reads only the bundle's metadata, so the bundle is all a description can be + // checked against. getDescription() records its modification time for this. + const auto* plugin = findPluginByUri (description.fileOrIdentifier); + + if (plugin == nullptr) + return true; + + return getBundleFile (plugin).getLastModificationTime() != description.lastFileModTime; } bool doesPluginStillExist (const PluginDescription& description) @@ -4275,14 +4282,39 @@ class LV2PluginFormatHeadless::Pimpl return findPluginByUri (description.fileOrIdentifier) != nullptr; } - StringArray searchPathsForPlugins (const FileSearchPath& paths, bool, bool) + StringArray searchPathsForPlugins (const FileSearchPath& paths, bool recursive, bool) { - loadAllPluginsFromPaths (paths); + // The defaults are always searched. A host that stores the search path it was given would + // otherwise never see a later change to LV2_PATH. + auto pathsToSearch = paths; + pathsToSearch.addPath (getDefaultLocationsToSearch()); + + loadAllPluginsFromPaths (pathsToSearch); + // The world holds every bundle loaded so far, from whichever path, with one entry per plugin. + // Report each bundle once, and only the ones under the paths asked for. StringArray result; for (const auto* plugin : world->getAllPlugins()) - result.add (URL { lv2_host::Plugin { plugin }.getBundleUri().getTyped() }.getLocalFile().getFullPathName()); + { + const auto bundle = getBundleFile (plugin); + + if (pathsToSearch.isFileInPath (bundle, recursive)) + result.addIfNotAlreadyThere (bundle.getFullPathName()); + } + + return result; + } + + StringArray getPluginUrisInBundle (const String& bundlePath) + { + std::vector plugins; + findPluginsByFile (File (bundlePath), plugins); + + StringArray result; + + for (const auto* plugin : plugins) + result.add (lv2_host::Plugin { plugin }.getUri().getTyped()); return result; } @@ -4530,6 +4562,11 @@ class LV2PluginFormatHeadless::Pimpl return world->getAllPlugins().getByUri (world->newUri (s.toRawUTF8())); } + static File getBundleFile (const LilvPlugin* plugin) + { + return lv2_host::bundlePathFromUri (lilv_node_as_uri (lilv_plugin_get_bundle_uri (plugin))); + } + void findPluginsByFile (const File& f, std::vector& result) { return world->getAllPlugins().getByFile (f, result); From 4e587a83a67559f891bf474abb5fbbb7d7861610 Mon Sep 17 00:00:00 2001 From: Stian Aagedal Date: Mon, 14 Sep 2026 16:40:22 +0200 Subject: [PATCH 40/40] NSViewComponent: divide the desktop scale out of resizeToFitView to stop an endless resize loop Co-Authored-By: Claude Opus 5 (1M context) --- modules/juce_gui_extra/native/juce_NSViewComponent_mac.mm | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/juce_gui_extra/native/juce_NSViewComponent_mac.mm b/modules/juce_gui_extra/native/juce_NSViewComponent_mac.mm index 5b9127b6b677..18c07845e9b0 100644 --- a/modules/juce_gui_extra/native/juce_NSViewComponent_mac.mm +++ b/modules/juce_gui_extra/native/juce_NSViewComponent_mac.mm @@ -166,7 +166,11 @@ void removeFromParent() { auto* view = static_cast (attachment.get())->view; auto r = [view frame]; - setBounds (Rectangle ((int) r.size.width, (int) r.size.height)); + + // The frame is unscaled, and componentMovedOrResized() multiplies the desktop scale back in. + // Without dividing it out here, the two enlarge each other endlessly at any scale but 1.0. + const auto scale = getTopLevelComponent()->getDesktopScaleFactor(); + setBounds (Rectangle ((int) (r.size.width / scale), (int) (r.size.height / scale))); if (auto* peer = getTopLevelComponent()->getPeer()) {