/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ #pragma once #ifndef DOXYGEN #include "../utility/juce_CreatePluginFilter.h" #endif #ifdef JUCE_MAC #undef Point #include class MacWorkgroupsHelper { public: MacWorkgroupsHelper(){} ~MacWorkgroupsHelper() { juce::ScopedLock lock(threadWorkGroupRegistrationLock); if (__builtin_available(macOS 11.0, *)) { // for unlikely event threads took too long to exit... this additional workgroup removal loop will avoid issues std::list>::iterator it; for(it = threadTokenList.begin(); it!=threadTokenList.end() ; it++) { os_workgroup_leave(it->second.workgroup, it->second.token); } threadTokenList.clear(); } } void updateDeviceWorkgroup(juce::String deviceName) { //create device ID objects for us to fill when we fine the device int deviceID = -1; AudioDeviceID devID; //find the device UInt32 size; AudioObjectPropertyAddress pa; pa.mSelector = kAudioHardwarePropertyDevices; pa.mScope = kAudioObjectPropertyScopeWildcard; pa.mElement = kAudioObjectPropertyElementMain; if (AudioObjectGetPropertyDataSize (kAudioObjectSystemObject, &pa, 0, nullptr, &size) == noErr) { juce::HeapBlock devs; devs.calloc (size, 1); if (AudioObjectGetPropertyData (kAudioObjectSystemObject, &pa, 0, nullptr, &size, devs) == noErr) { auto num = (int) size / (int) sizeof (AudioDeviceID); for (int i = 0; i < num; ++i) { char name[1024]; size = sizeof (name); pa.mSelector = kAudioDevicePropertyDeviceName; if (AudioObjectGetPropertyData (devs[i], &pa, 0, nullptr, &size, name) == noErr) { auto nameString = juce::String::fromUTF8 (name, (int) strlen (name)); if(nameString == deviceName){ //we have devID = devs[i]; deviceID = num; size = 0; pa.mSelector =kAudioDevicePropertyIOThreadOSWorkgroup; if(AudioObjectGetPropertyDataSize( devs[i], &pa, 0, nullptr, &size) == noErr) { os_workgroup_t _Nonnull wkgroup; if (AudioObjectGetPropertyData (devs[i], &pa, 0, nullptr, &size, &wkgroup) == noErr) { currentWorkgroup = wkgroup; } } } } } } } } int joinCurrentAuWorkGroup(void* threadId) // call from thread at run, before while loop { // Join this thread to the workgroup. if (__builtin_available(macOS 11.0, *)) { juce::ScopedLock lock(threadWorkGroupRegistrationLock); thread_local os_workgroup_join_token_s joinToken{}; const int result = os_workgroup_join(currentWorkgroup, &joinToken); if (result == 0) { WorkgroupAndToken toStore = {currentWorkgroup , &joinToken}; threadTokenList.emplace_back(threadId, toStore); return 1;// Success. } else if (result == EALREADY) { // The thread is already part of a workgroup that can't be // nested in the the specified workgroup. return -1; } else if (result == EINVAL) { // The workgroup has been canceled. return -1; } } return -1; } void leaveCurrentAuWorkGroup(void* threadId) // Before exiting the thread, leave the workgroup. { if(__builtin_available(macOS 11.0, *)) { juce::ScopedLock lock(threadWorkGroupRegistrationLock); std::list>::iterator it; for(it = threadTokenList.begin(); it!=threadTokenList.end() ; it++) { if(it->first == threadId) { os_workgroup_leave(it->second.workgroup, it->second.token); threadTokenList.erase(it); break; } } } } private: os_workgroup_t currentWorkgroup; class WorkgroupAndToken { public: os_workgroup_t workgroup; os_workgroup_join_token_t token; }; std::list> threadTokenList; juce::CriticalSection threadWorkGroupRegistrationLock; }; #endif namespace juce { //============================================================================== /** An object that creates and plays a standalone instance of an AudioProcessor. The object will create your processor using the same createPluginFilter() function that the other plugin wrappers use, and will run it through the computer's audio/MIDI devices using AudioDeviceManager and AudioProcessorPlayer. @tags{Audio} */ class StandalonePluginHolder : private AudioIODeviceCallback, private Timer, private Value::Listener { public: //============================================================================== /** Structure used for the number of inputs and outputs. */ struct PluginInOuts { short numIns, numOuts; }; //============================================================================== /** Creates an instance of the default plugin. The settings object can be a PropertySet that the class should use to store its settings - the takeOwnershipOfSettings indicates whether this object will delete the settings automatically when no longer needed. The settings can also be nullptr. A default device name can be passed in. Preferably a complete setup options object can be used, which takes precedence over the preferredDefaultDeviceName and allows you to select the input & output device names, sample rate, buffer size etc. In all instances, the settingsToUse will take precedence over the "preferred" options if not null. */ StandalonePluginHolder (PropertySet* settingsToUse, bool takeOwnershipOfSettings = true, const String& preferredDefaultDeviceName = String(), const AudioDeviceManager::AudioDeviceSetup* preferredSetupOptions = nullptr, const Array& channels = Array(), #if JUCE_ANDROID || JUCE_IOS bool shouldAutoOpenMidiDevices = true #else bool shouldAutoOpenMidiDevices = false #endif ) : settings (settingsToUse, takeOwnershipOfSettings), channelConfiguration (channels), autoOpenMidiDevices (shouldAutoOpenMidiDevices) { shouldMuteInput.addListener (this); shouldMuteInput = ! isInterAppAudioConnected(); createPlugin(); auto inChannels = (channelConfiguration.size() > 0 ? channelConfiguration[0].numIns : processor->getMainBusNumInputChannels()); if (preferredSetupOptions != nullptr) options.reset (new AudioDeviceManager::AudioDeviceSetup (*preferredSetupOptions)); auto audioInputRequired = (inChannels > 0); if (audioInputRequired && RuntimePermissions::isRequired (RuntimePermissions::recordAudio) && ! RuntimePermissions::isGranted (RuntimePermissions::recordAudio)) RuntimePermissions::request (RuntimePermissions::recordAudio, [this, preferredDefaultDeviceName] (bool granted) { init (granted, preferredDefaultDeviceName); }); else init (audioInputRequired, preferredDefaultDeviceName); } void init (bool enableAudioInput, const String& preferredDefaultDeviceName) { setupAudioDevices (enableAudioInput, preferredDefaultDeviceName, options.get()); reloadPluginState(); startPlaying(); if (autoOpenMidiDevices) startTimer (500); } ~StandalonePluginHolder() override { stopTimer(); deletePlugin(); shutDownAudioDevices(); } //============================================================================== virtual void createPlugin() { processor.reset (createPluginFilterOfType (AudioProcessor::wrapperType_Standalone)); processor->disableNonMainBuses(); processor->setRateAndBufferSizeDetails (44100, 512); processorHasPotentialFeedbackLoop = (getNumInputChannels() > 0 && getNumOutputChannels() > 0); } virtual void deletePlugin() { stopPlaying(); processor = nullptr; } int getNumInputChannels() const { if (processor == nullptr) return 0; return (channelConfiguration.size() > 0 ? channelConfiguration[0].numIns : processor->getMainBusNumInputChannels()); } int getNumOutputChannels() const { if (processor == nullptr) return 0; return (channelConfiguration.size() > 0 ? channelConfiguration[0].numOuts : processor->getMainBusNumOutputChannels()); } static String getFilePatterns (const String& fileSuffix) { if (fileSuffix.isEmpty()) return {}; return (fileSuffix.startsWithChar ('.') ? "*" : "*.") + fileSuffix; } //============================================================================== Value& getMuteInputValue() { return shouldMuteInput; } bool getProcessorHasPotentialFeedbackLoop() const { return processorHasPotentialFeedbackLoop; } void valueChanged (Value& value) override { muteInput = (bool) value.getValue(); } //============================================================================== File getLastFile() const { File f; if (settings != nullptr) f = File (settings->getValue ("lastStateFile")); if (f == File()) f = File::getSpecialLocation (File::userDocumentsDirectory); return f; } void setLastFile (const FileChooser& fc) { if (settings != nullptr) settings->setValue ("lastStateFile", fc.getResult().getFullPathName()); } /** Pops up a dialog letting the user save the processor's state to a file. */ void askUserToSaveState (const String& fileSuffix = String()) { stateFileChooser = std::make_unique (TRANS("Save current state"), getLastFile(), getFilePatterns (fileSuffix)); auto flags = FileBrowserComponent::saveMode | FileBrowserComponent::canSelectFiles | FileBrowserComponent::warnAboutOverwriting; stateFileChooser->launchAsync (flags, [this] (const FileChooser& fc) { if (fc.getResult() == File{}) return; setLastFile (fc); MemoryBlock data; processor->getStateInformation (data); if (! fc.getResult().replaceWithData (data.getData(), data.getSize())) AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, TRANS("Error whilst saving"), TRANS("Couldn't write to the specified file!")); }); } /** Pops up a dialog letting the user re-load the processor's state from a file. */ void askUserToLoadState (const String& fileSuffix = String()) { stateFileChooser = std::make_unique (TRANS("Load a saved state"), getLastFile(), getFilePatterns (fileSuffix)); auto flags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles; stateFileChooser->launchAsync (flags, [this] (const FileChooser& fc) { if (fc.getResult() == File{}) return; setLastFile (fc); MemoryBlock data; if (fc.getResult().loadFileAsData (data)) processor->setStateInformation (data.getData(), (int) data.getSize()); else AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, TRANS("Error whilst loading"), TRANS("Couldn't read from the specified file!")); }); } //============================================================================== void startPlaying() { player.setProcessor (processor.get()); #if JucePlugin_Enable_IAA && JUCE_IOS if (auto device = dynamic_cast (deviceManager.getCurrentAudioDevice())) { processor->setPlayHead (device->getAudioPlayHead()); device->setMidiMessageCollector (&player.getMidiMessageCollector()); } #endif } void stopPlaying() { player.setProcessor (nullptr); } //============================================================================== /** Shows an audio properties dialog box modally. */ void showAudioSettingsDialog() { DialogWindow::LaunchOptions o; int maxNumInputs = 0, maxNumOutputs = 0; if (channelConfiguration.size() > 0) { auto& defaultConfig = channelConfiguration.getReference (0); maxNumInputs = jmax (0, (int) defaultConfig.numIns); maxNumOutputs = jmax (0, (int) defaultConfig.numOuts); } if (auto* bus = processor->getBus (true, 0)) maxNumInputs = jmax (0, bus->getDefaultLayout().size()); if (auto* bus = processor->getBus (false, 0)) maxNumOutputs = jmax (0, bus->getDefaultLayout().size()); auto content = std::make_unique (*this, deviceManager, maxNumInputs, maxNumOutputs); content->setSize (500, 550); content->setToRecommendedSize(); o.content.setOwned (content.release()); o.dialogTitle = TRANS("Audio/MIDI Settings"); o.dialogBackgroundColour = o.content->getLookAndFeel().findColour (ResizableWindow::backgroundColourId); o.escapeKeyTriggersCloseButton = true; o.useNativeTitleBar = true; o.resizable = false; o.launchAsync(); } void saveAudioDeviceState() { if (settings != nullptr) { auto xml = deviceManager.createStateXml(); settings->setValue ("audioSetup", xml.get()); #if ! (JUCE_IOS || JUCE_ANDROID) settings->setValue ("shouldMuteInput", (bool) shouldMuteInput.getValue()); #endif } } void reloadAudioDeviceState (bool enableAudioInput, const String& preferredDefaultDeviceName, const AudioDeviceManager::AudioDeviceSetup* preferredSetupOptions) { std::unique_ptr savedState; if (settings != nullptr) { savedState = settings->getXmlValue ("audioSetup"); #if ! (JUCE_IOS || JUCE_ANDROID) shouldMuteInput.setValue (settings->getBoolValue ("shouldMuteInput", true)); #endif } auto inputChannels = getNumInputChannels(); auto outputChannels = getNumOutputChannels(); if (inputChannels == 0 && outputChannels == 0 && processor->isMidiEffect()) { // add a dummy output channel for MIDI effect plug-ins so they can receive audio callbacks outputChannels = 1; } deviceManager.initialise (enableAudioInput ? inputChannels : 0, outputChannels, savedState.get(), true, preferredDefaultDeviceName, preferredSetupOptions); } //============================================================================== void savePluginState() { if (settings != nullptr && processor != nullptr) { MemoryBlock data; processor->getStateInformation (data); settings->setValue ("filterState", data.toBase64Encoding()); } } void reloadPluginState() { if (settings != nullptr) { MemoryBlock data; if (data.fromBase64Encoding (settings->getValue ("filterState")) && data.getSize() > 0) processor->setStateInformation (data.getData(), (int) data.getSize()); } } //============================================================================== void switchToHostApplication() { #if JUCE_IOS if (auto device = dynamic_cast (deviceManager.getCurrentAudioDevice())) device->switchApplication(); #endif } bool isInterAppAudioConnected() { #if JUCE_IOS if (auto device = dynamic_cast (deviceManager.getCurrentAudioDevice())) return device->isInterAppAudioConnected(); #endif return false; } Image getIAAHostIcon (int size) { #if JUCE_IOS && JucePlugin_Enable_IAA if (auto device = dynamic_cast (deviceManager.getCurrentAudioDevice())) return device->getIcon (size); #else ignoreUnused (size); #endif return {}; } static StandalonePluginHolder* getInstance(); //============================================================================== OptionalScopedPointer settings; std::unique_ptr processor; AudioDeviceManager deviceManager; AudioProcessorPlayer player; Array channelConfiguration; // avoid feedback loop by default bool processorHasPotentialFeedbackLoop = true; std::atomic muteInput { true }; Value shouldMuteInput; AudioBuffer emptyBuffer; bool autoOpenMidiDevices; std::unique_ptr options; Array lastMidiDevices; std::unique_ptr stateFileChooser; private: #ifdef JUCE_MAC MacWorkgroupsHelper workgroupsHelper; #endif /* This class can be used to ensure that audio callbacks use buffers with a predictable maximum size. On some platforms (such as iOS 10), the expected buffer size reported in audioDeviceAboutToStart may be smaller than the blocks passed to audioDeviceIOCallbackWithContext. This can lead to out-of-bounds reads if the render callback depends on additional buffers which were initialised using the smaller size. As a workaround, this class will ensure that the render callback will only ever be called with a block with a length less than or equal to the expected block size. */ class CallbackMaxSizeEnforcer : public AudioIODeviceCallback { public: explicit CallbackMaxSizeEnforcer (AudioIODeviceCallback& callbackIn) : inner (callbackIn) {} void audioDeviceAboutToStart (AudioIODevice* device) override { maximumSize = device->getCurrentBufferSizeSamples(); storedInputChannels .resize ((size_t) device->getActiveInputChannels() .countNumberOfSetBits()); storedOutputChannels.resize ((size_t) device->getActiveOutputChannels().countNumberOfSetBits()); inner.audioDeviceAboutToStart (device); } void audioDeviceIOCallbackWithContext (const float* const* inputChannelData, int numInputChannels, float* const* outputChannelData, int numOutputChannels, int numSamples, const AudioIODeviceCallbackContext& context) override { jassertquiet ((int) storedInputChannels.size() == numInputChannels); jassertquiet ((int) storedOutputChannels.size() == numOutputChannels); int position = 0; while (position < numSamples) { const auto blockLength = jmin (maximumSize, numSamples - position); initChannelPointers (inputChannelData, storedInputChannels, position); initChannelPointers (outputChannelData, storedOutputChannels, position); inner.audioDeviceIOCallbackWithContext (storedInputChannels.data(), (int) storedInputChannels.size(), storedOutputChannels.data(), (int) storedOutputChannels.size(), blockLength, context); position += blockLength; } } void audioDeviceStopped() override { inner.audioDeviceStopped(); } private: struct GetChannelWithOffset { int offset; template auto operator() (Ptr ptr) const noexcept -> Ptr { return ptr + offset; } }; template void initChannelPointers (Ptr&& source, Vector&& target, int offset) { std::transform (source, source + target.size(), target.begin(), GetChannelWithOffset { offset }); } AudioIODeviceCallback& inner; int maximumSize = 0; std::vector storedInputChannels; std::vector storedOutputChannels; }; CallbackMaxSizeEnforcer maxSizeEnforcer { *this }; //============================================================================== class SettingsComponent : public Component { public: SettingsComponent (StandalonePluginHolder& pluginHolder, AudioDeviceManager& deviceManagerToUse, int maxAudioInputChannels, int maxAudioOutputChannels) : owner (pluginHolder), deviceSelector (deviceManagerToUse, 0, maxAudioInputChannels, 0, maxAudioOutputChannels, true, (pluginHolder.processor.get() != nullptr && pluginHolder.processor->producesMidi()), true, false), shouldMuteLabel ("Feedback Loop:", "Feedback Loop:"), shouldMuteButton ("Mute audio input") { setOpaque (true); shouldMuteButton.setClickingTogglesState (true); shouldMuteButton.getToggleStateValue().referTo (owner.shouldMuteInput); addAndMakeVisible (deviceSelector); if (owner.getProcessorHasPotentialFeedbackLoop()) { addAndMakeVisible (shouldMuteButton); addAndMakeVisible (shouldMuteLabel); shouldMuteLabel.attachToComponent (&shouldMuteButton, true); } } void paint (Graphics& g) override { g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId)); } void resized() override { const ScopedValueSetter scope (isResizing, true); auto r = getLocalBounds(); if (owner.getProcessorHasPotentialFeedbackLoop()) { auto itemHeight = deviceSelector.getItemHeight(); auto extra = r.removeFromTop (itemHeight); auto seperatorHeight = (itemHeight >> 1); shouldMuteButton.setBounds (Rectangle (extra.proportionOfWidth (0.35f), seperatorHeight, extra.proportionOfWidth (0.60f), deviceSelector.getItemHeight())); r.removeFromTop (seperatorHeight); } deviceSelector.setBounds (r); } void childBoundsChanged (Component* childComp) override { if (! isResizing && childComp == &deviceSelector) setToRecommendedSize(); } void setToRecommendedSize() { const auto extraHeight = [&] { if (! owner.getProcessorHasPotentialFeedbackLoop()) return 0; const auto itemHeight = deviceSelector.getItemHeight(); const auto separatorHeight = (itemHeight >> 1); return itemHeight + separatorHeight; }(); setSize (getWidth(), deviceSelector.getHeight() + extraHeight); } private: //============================================================================== StandalonePluginHolder& owner; AudioDeviceSelectorComponent deviceSelector; Label shouldMuteLabel; ToggleButton shouldMuteButton; bool isResizing = false; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SettingsComponent) }; //============================================================================== void audioDeviceIOCallbackWithContext (const float* const* inputChannelData, int numInputChannels, float* const* outputChannelData, int numOutputChannels, int numSamples, const AudioIODeviceCallbackContext& context) override { if (muteInput) { emptyBuffer.clear(); inputChannelData = emptyBuffer.getArrayOfReadPointers(); } player.audioDeviceIOCallbackWithContext (inputChannelData, numInputChannels, outputChannelData, numOutputChannels, numSamples, context); } void audioDeviceAboutToStart (AudioIODevice* device) override { emptyBuffer.setSize (device->getActiveInputChannels().countNumberOfSetBits(), device->getCurrentBufferSizeSamples()); emptyBuffer.clear(); player.audioDeviceAboutToStart (device); player.setMidiOutput (deviceManager.getDefaultMidiOutput()); #ifdef JUCE_MAC workgroupsHelper.updateDeviceWorkgroup(device->getName()); processor->setWorkgroupJoinFunctionPointer([=](void* threadId) { workgroupsHelper.joinCurrentAuWorkGroup(threadId); }); processor->setWorkgroupLeaveFunctionPointer([=](void* threadId) { workgroupsHelper.leaveCurrentAuWorkGroup(threadId); }); #endif } void audioDeviceStopped() override { player.setMidiOutput (nullptr); player.audioDeviceStopped(); emptyBuffer.setSize (0, 0); } //============================================================================== void setupAudioDevices (bool enableAudioInput, const String& preferredDefaultDeviceName, const AudioDeviceManager::AudioDeviceSetup* preferredSetupOptions) { deviceManager.addAudioCallback (&maxSizeEnforcer); deviceManager.addMidiInputDeviceCallback ({}, &player); reloadAudioDeviceState (enableAudioInput, preferredDefaultDeviceName, preferredSetupOptions); } void shutDownAudioDevices() { saveAudioDeviceState(); deviceManager.removeMidiInputDeviceCallback ({}, &player); deviceManager.removeAudioCallback (&maxSizeEnforcer); } void timerCallback() override { auto newMidiDevices = MidiInput::getAvailableDevices(); if (newMidiDevices != lastMidiDevices) { for (auto& oldDevice : lastMidiDevices) if (! newMidiDevices.contains (oldDevice)) deviceManager.setMidiInputDeviceEnabled (oldDevice.identifier, false); for (auto& newDevice : newMidiDevices) if (! lastMidiDevices.contains (newDevice)) deviceManager.setMidiInputDeviceEnabled (newDevice.identifier, true); lastMidiDevices = newMidiDevices; } } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StandalonePluginHolder) }; //============================================================================== /** A class that can be used to run a simple standalone application containing your filter. Just create one of these objects in your JUCEApplicationBase::initialise() method, and let it do its work. It will create your filter object using the same createPluginFilter() function that the other plugin wrappers use. @tags{Audio} */ class StandaloneFilterWindow : public DocumentWindow, private Button::Listener { public: //============================================================================== typedef StandalonePluginHolder::PluginInOuts PluginInOuts; //============================================================================== /** Creates a window with a given title and colour. The settings object can be a PropertySet that the class should use to store its settings (it can also be null). If takeOwnershipOfSettings is true, then the settings object will be owned and deleted by this object. */ StandaloneFilterWindow (const String& title, Colour backgroundColour, PropertySet* settingsToUse, bool takeOwnershipOfSettings, const String& preferredDefaultDeviceName = String(), const AudioDeviceManager::AudioDeviceSetup* preferredSetupOptions = nullptr, const Array& constrainToConfiguration = {}, #if JUCE_ANDROID || JUCE_IOS bool autoOpenMidiDevices = true #else bool autoOpenMidiDevices = false #endif ) : DocumentWindow (title, backgroundColour, DocumentWindow::minimiseButton | DocumentWindow::closeButton), optionsButton ("Options") { setConstrainer (&decoratorConstrainer); #if JUCE_IOS || JUCE_ANDROID setTitleBarHeight (0); #else setTitleBarButtonsRequired (DocumentWindow::minimiseButton | DocumentWindow::closeButton, false); Component::addAndMakeVisible (optionsButton); optionsButton.addListener (this); optionsButton.setTriggeredOnMouseDown (true); #endif pluginHolder.reset (new StandalonePluginHolder (settingsToUse, takeOwnershipOfSettings, preferredDefaultDeviceName, preferredSetupOptions, constrainToConfiguration, autoOpenMidiDevices)); #if JUCE_IOS || JUCE_ANDROID setFullScreen (true); updateContent(); #else updateContent(); const auto windowScreenBounds = [this]() -> Rectangle { const auto width = getWidth(); const auto height = getHeight(); const auto& displays = Desktop::getInstance().getDisplays(); if (auto* props = pluginHolder->settings.get()) { constexpr int defaultValue = -100; const auto x = props->getIntValue ("windowX", defaultValue); const auto y = props->getIntValue ("windowY", defaultValue); if (x != defaultValue && y != defaultValue) { const auto screenLimits = displays.getDisplayForRect ({ x, y, width, height })->userArea; return { jlimit (screenLimits.getX(), jmax (screenLimits.getX(), screenLimits.getRight() - width), x), jlimit (screenLimits.getY(), jmax (screenLimits.getY(), screenLimits.getBottom() - height), y), width, height }; } } const auto displayArea = displays.getPrimaryDisplay()->userArea; return { displayArea.getCentreX() - width / 2, displayArea.getCentreY() - height / 2, width, height }; }(); setBoundsConstrained (windowScreenBounds); if (auto* processor = getAudioProcessor()) if (auto* editor = processor->getActiveEditor()) setResizable (editor->isResizable(), false); #endif } ~StandaloneFilterWindow() override { #if (! JUCE_IOS) && (! JUCE_ANDROID) if (auto* props = pluginHolder->settings.get()) { props->setValue ("windowX", getX()); props->setValue ("windowY", getY()); } #endif pluginHolder->stopPlaying(); clearContentComponent(); pluginHolder = nullptr; } //============================================================================== AudioProcessor* getAudioProcessor() const noexcept { return pluginHolder->processor.get(); } AudioDeviceManager& getDeviceManager() const noexcept { return pluginHolder->deviceManager; } /** Deletes and re-creates the plugin, resetting it to its default state. */ void resetToDefaultState() { pluginHolder->stopPlaying(); clearContentComponent(); pluginHolder->deletePlugin(); if (auto* props = pluginHolder->settings.get()) props->removeValue ("filterState"); pluginHolder->createPlugin(); updateContent(); pluginHolder->startPlaying(); } //============================================================================== void closeButtonPressed() override { pluginHolder->savePluginState(); JUCEApplicationBase::quit(); } void handleMenuResult (int result) { switch (result) { case 1: pluginHolder->showAudioSettingsDialog(); break; case 2: pluginHolder->askUserToSaveState(); break; case 3: pluginHolder->askUserToLoadState(); break; case 4: resetToDefaultState(); break; default: break; } } static void menuCallback (int result, StandaloneFilterWindow* button) { if (button != nullptr && result != 0) button->handleMenuResult (result); } void resized() override { DocumentWindow::resized(); optionsButton.setBounds (8, 6, 60, getTitleBarHeight() - 8); } virtual StandalonePluginHolder* getPluginHolder() { return pluginHolder.get(); } std::unique_ptr pluginHolder; private: void updateContent() { auto* content = new MainContentComponent (*this); decoratorConstrainer.setMainContentComponent (content); #if JUCE_IOS || JUCE_ANDROID constexpr auto resizeAutomatically = false; #else constexpr auto resizeAutomatically = true; #endif setContentOwned (content, resizeAutomatically); } void buttonClicked (Button*) override { PopupMenu m; m.addItem (1, TRANS("Audio/MIDI Settings...")); m.addSeparator(); m.addItem (2, TRANS("Save current state...")); m.addItem (3, TRANS("Load a saved state...")); m.addSeparator(); m.addItem (4, TRANS("Reset to default state")); m.showMenuAsync (PopupMenu::Options(), ModalCallbackFunction::forComponent (menuCallback, this)); } //============================================================================== class MainContentComponent : public Component, private Value::Listener, private Button::Listener, private ComponentListener { public: MainContentComponent (StandaloneFilterWindow& filterWindow) : owner (filterWindow), notification (this), editor (owner.getAudioProcessor()->hasEditor() ? owner.getAudioProcessor()->createEditorIfNeeded() : new GenericAudioProcessorEditor (*owner.getAudioProcessor())) { inputMutedValue.referTo (owner.pluginHolder->getMuteInputValue()); if (editor != nullptr) { editor->addComponentListener (this); componentMovedOrResized (*editor, false, true); addAndMakeVisible (editor.get()); } addChildComponent (notification); if (owner.pluginHolder->getProcessorHasPotentialFeedbackLoop()) { inputMutedValue.addListener (this); shouldShowNotification = inputMutedValue.getValue(); } inputMutedChanged (shouldShowNotification); } ~MainContentComponent() override { if (editor != nullptr) { editor->removeComponentListener (this); owner.pluginHolder->processor->editorBeingDeleted (editor.get()); editor = nullptr; } } void resized() override { auto r = getLocalBounds(); if (shouldShowNotification) notification.setBounds (r.removeFromTop (NotificationArea::height)); if (editor != nullptr) { const auto newPos = r.getTopLeft().toFloat().transformedBy (editor->getTransform().inverted()); if (preventResizingEditor) editor->setTopLeftPosition (newPos.roundToInt()); else editor->setBoundsConstrained (editor->getLocalArea (this, r.toFloat()).withPosition (newPos).toNearestInt()); } } ComponentBoundsConstrainer* getEditorConstrainer() const { if (auto* e = editor.get()) return e->getConstrainer(); return nullptr; } BorderSize computeBorder() const { const auto nativeFrame = [&]() -> BorderSize { if (auto* peer = owner.getPeer()) if (const auto frameSize = peer->getFrameSizeIfPresent()) return *frameSize; return {}; }(); return nativeFrame.addedTo (owner.getContentComponentBorder()) .addedTo (BorderSize { shouldShowNotification ? NotificationArea::height : 0, 0, 0, 0 }); } private: //============================================================================== class NotificationArea : public Component { public: enum { height = 30 }; NotificationArea (Button::Listener* settingsButtonListener) : notification ("notification", "Audio input is muted to avoid feedback loop"), #if JUCE_IOS || JUCE_ANDROID settingsButton ("Unmute Input") #else settingsButton ("Settings...") #endif { setOpaque (true); notification.setColour (Label::textColourId, Colours::black); settingsButton.addListener (settingsButtonListener); addAndMakeVisible (notification); addAndMakeVisible (settingsButton); } void paint (Graphics& g) override { auto r = getLocalBounds(); g.setColour (Colours::darkgoldenrod); g.fillRect (r.removeFromBottom (1)); g.setColour (Colours::lightgoldenrodyellow); g.fillRect (r); } void resized() override { auto r = getLocalBounds().reduced (5); settingsButton.setBounds (r.removeFromRight (70)); notification.setBounds (r); } private: Label notification; TextButton settingsButton; }; //============================================================================== void inputMutedChanged (bool newInputMutedValue) { shouldShowNotification = newInputMutedValue; notification.setVisible (shouldShowNotification); #if JUCE_IOS || JUCE_ANDROID resized(); #else if (editor != nullptr) { const int extraHeight = shouldShowNotification ? NotificationArea::height : 0; const auto rect = getSizeToContainEditor(); setSize (rect.getWidth(), rect.getHeight() + extraHeight); } #endif } void valueChanged (Value& value) override { inputMutedChanged (value.getValue()); } void buttonClicked (Button*) override { #if JUCE_IOS || JUCE_ANDROID owner.pluginHolder->getMuteInputValue().setValue (false); #else owner.pluginHolder->showAudioSettingsDialog(); #endif } //============================================================================== void componentMovedOrResized (Component&, bool, bool) override { const ScopedValueSetter scope (preventResizingEditor, true); if (editor != nullptr) { auto rect = getSizeToContainEditor(); setSize (rect.getWidth(), rect.getHeight() + (shouldShowNotification ? NotificationArea::height : 0)); } } Rectangle getSizeToContainEditor() const { if (editor != nullptr) return getLocalArea (editor.get(), editor->getLocalBounds()); return {}; } //============================================================================== StandaloneFilterWindow& owner; NotificationArea notification; std::unique_ptr editor; Value inputMutedValue; bool shouldShowNotification = false; bool preventResizingEditor = false; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent) }; /* This custom constrainer checks with the AudioProcessorEditor (which might itself be constrained) to ensure that any size we choose for the standalone window will be suitable for the editor too. Without this constrainer, attempting to resize the standalone window may set bounds on the peer that are unsupported by the inner editor. In this scenario, the peer will be set to a 'bad' size, then the inner editor will be resized. The editor will check the new bounds with its own constrainer, and may set itself to a more suitable size. After that, the resizable window will see that its content component has changed size, and set the bounds of the peer accordingly. The end result is that the peer is resized twice in a row to different sizes, which can appear glitchy/flickery to the user. */ struct DecoratorConstrainer : public ComponentBoundsConstrainer { void checkBounds (Rectangle& bounds, const Rectangle& previousBounds, const Rectangle& limits, bool isStretchingTop, bool isStretchingLeft, bool isStretchingBottom, bool isStretchingRight) override { auto* decorated = contentComponent != nullptr ? contentComponent->getEditorConstrainer() : nullptr; if (decorated != nullptr) { const auto border = contentComponent->computeBorder(); const auto requestedBounds = bounds; border.subtractFrom (bounds); decorated->checkBounds (bounds, border.subtractedFrom (previousBounds), limits, isStretchingTop, isStretchingLeft, isStretchingBottom, isStretchingRight); border.addTo (bounds); bounds = bounds.withPosition (requestedBounds.getPosition()); if (isStretchingTop && ! isStretchingBottom) bounds = bounds.withBottomY (previousBounds.getBottom()); if (! isStretchingTop && isStretchingBottom) bounds = bounds.withY (previousBounds.getY()); if (isStretchingLeft && ! isStretchingRight) bounds = bounds.withRightX (previousBounds.getRight()); if (! isStretchingLeft && isStretchingRight) bounds = bounds.withX (previousBounds.getX()); } else { ComponentBoundsConstrainer::checkBounds (bounds, previousBounds, limits, isStretchingTop, isStretchingLeft, isStretchingBottom, isStretchingRight); } } void setMainContentComponent (MainContentComponent* in) { contentComponent = in; } private: MainContentComponent* contentComponent = nullptr; }; //============================================================================== TextButton optionsButton; DecoratorConstrainer decoratorConstrainer; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StandaloneFilterWindow) }; inline StandalonePluginHolder* StandalonePluginHolder::getInstance() { #if JucePlugin_Enable_IAA || JucePlugin_Build_Standalone if (PluginHostType::getPluginLoadedAs() == AudioProcessor::wrapperType_Standalone) { auto& desktop = Desktop::getInstance(); const int numTopLevelWindows = desktop.getNumComponents(); for (int i = 0; i < numTopLevelWindows; ++i) if (auto window = dynamic_cast (desktop.getComponent (i))) return window->getPluginHolder(); } #endif return nullptr; } } // namespace juce