// SPDX-License-Identifier: GPLv3-or-later WITH Appstore-exception //#if JUCE_MODULE_AVAILABLE_juce_audio_plugin_client //extern juce::AudioProcessor* JUCE_API JUCE_CALLTYPE createPluginFilterOfType (juce::AudioProcessor::WrapperType type); //#endif #include "CrossPlatformUtils.h" #include "PluginEditor.h" 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 { 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 = false #else bool shouldAutoOpenMidiDevices = false #endif ) : settings (settingsToUse, takeOwnershipOfSettings), channelConfiguration (channels), shouldMuteInput(var((bool)false)), //shouldMuteInput (! isInterAppAudioConnected()), autoOpenMidiDevices (shouldAutoOpenMidiDevices) { 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); } virtual ~StandalonePluginHolder() { stopTimer(); deletePlugin(); shutDownAudioDevices(); } //============================================================================== virtual void createPlugin() { #if JUCE_MODULE_AVAILABLE_juce_audio_plugin_client processor.reset (::createPluginFilterOfType (AudioProcessor::wrapperType_Standalone)); #else AudioProcessor::setTypeOfNextNewPlugin (AudioProcessor::wrapperType_Standalone); processor.reset (createPluginFilter()); AudioProcessor::setTypeOfNextNewPlugin (AudioProcessor::wrapperType_Undefined); #endif jassert (processor != nullptr); // Your createPluginFilter() function must return a valid object! processor->disableNonMainBuses(); processor->setRateAndBufferSizeDetails (44100, 512); int inChannels = (channelConfiguration.size() > 0 ? channelConfiguration[0].numIns : processor->getMainBusNumInputChannels()); int outChannels = (channelConfiguration.size() > 0 ? channelConfiguration[0].numOuts : processor->getMainBusNumOutputChannels()); // processorHasPotentialFeedbackLoop = (inChannels > 0 && outChannels > 0); } virtual void deletePlugin() { stopPlaying(); processor = nullptr; } static String getFilePatterns (const String& fileSuffix) { if (fileSuffix.isEmpty()) return {}; return (fileSuffix.startsWithChar ('.') ? "*" : "*.") + fileSuffix; } //============================================================================== Value& getMuteInputValue() { return shouldMuteInput; } bool getProcessorHasPotentialFeedbackLoop() const { return processorHasPotentialFeedbackLoop; } //============================================================================== 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()) { #if JUCE_MODAL_LOOPS_PERMITTED FileChooser fc (TRANS("Save current state"), getLastFile(), getFilePatterns (fileSuffix)); if (fc.browseForFileToSave (true)) { 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!")); } #else ignoreUnused (fileSuffix); #endif } /** Pops up a dialog letting the user re-load the processor's state from a file. */ void askUserToLoadState (const String& fileSuffix = String()) { #if JUCE_MODAL_LOOPS_PERMITTED FileChooser fc (TRANS("Load a saved state"), getLastFile(), getFilePatterns (fileSuffix)); if (fc.browseForFileToOpen()) { 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!")); } #else ignoreUnused (fileSuffix); #endif } //============================================================================== 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(Component * calloutTarget=nullptr, Component * calloutParent=nullptr) { int minNumInputs = std::numeric_limits::max(), maxNumInputs = 0, minNumOutputs = std::numeric_limits::max(), maxNumOutputs = 0; auto updateMinAndMax = [] (int newValue, int& minValue, int& maxValue) { minValue = jmin (minValue, newValue); maxValue = jmax (maxValue, newValue); }; /* if (channelConfiguration.size() > 0) { auto defaultConfig = channelConfiguration.getReference (0); updateMinAndMax ((int) defaultConfig.numIns, minNumInputs, maxNumInputs); updateMinAndMax ((int) defaultConfig.numOuts, minNumOutputs, maxNumOutputs); } */ if (auto* bus = processor->getBus (true, 0)) { auto maxsup = bus->getMaxSupportedChannels(128); updateMinAndMax (maxsup, minNumInputs, maxNumInputs); updateMinAndMax (bus->getDefaultLayout().size(), minNumInputs, maxNumInputs); if (bus->isNumberOfChannelsSupported(1)) { updateMinAndMax (1, minNumInputs, maxNumInputs); } if (bus->isNumberOfChannelsSupported(0)) { updateMinAndMax (0, minNumInputs, maxNumInputs); } } if (auto* bus = processor->getBus (false, 0)) { auto maxsup = bus->getMaxSupportedChannels(128); updateMinAndMax (maxsup, minNumOutputs, maxNumOutputs); updateMinAndMax (bus->getDefaultLayout().size(), minNumOutputs, maxNumOutputs); if (bus->isNumberOfChannelsSupported(1)) { updateMinAndMax (1, minNumOutputs, maxNumOutputs); } if (bus->isNumberOfChannelsSupported(0)) { updateMinAndMax (0, minNumOutputs, maxNumOutputs); } } minNumInputs = jmin (minNumInputs, maxNumInputs); minNumOutputs = jmin (minNumOutputs, maxNumOutputs); auto * content = new SettingsComponent (*this, deviceManager, minNumInputs, maxNumInputs, minNumOutputs, maxNumOutputs); if (calloutTarget && calloutParent) { auto wrap = std::make_unique(); wrap->setViewedComponent(content, true); // takes ownership of content //std::unique_ptr contptr(content); int defWidth = 450; int defHeight = 550; #if JUCE_IOS defWidth = 320; defHeight = 400; #endif content->setSize (defWidth, defHeight); wrap->setSize(jmin(defWidth, calloutParent->getWidth() - 20), jmin(defHeight, calloutParent->getHeight() - 24)); auto bounds = calloutParent->getLocalArea(nullptr, calloutTarget->getScreenBounds()); auto & cb = CallOutBox::launchAsynchronously(std::move(wrap), bounds, calloutParent); cb.setDismissalMouseClicksAreAlwaysConsumed(true); } else { DialogWindow::LaunchOptions o; o.content.setOwned (content); o.content->setSize (500, 550); 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) { std::unique_ptr 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 totalInChannels = processor->getMainBusNumInputChannels(); auto totalOutChannels = processor->getMainBusNumOutputChannels(); if (channelConfiguration.size() > 0) { auto defaultConfig = channelConfiguration.getReference (0); totalInChannels = defaultConfig.numIns; totalOutChannels = defaultConfig.numOuts; } deviceManager.initialise (enableAudioInput ? totalInChannels : 0, totalOutChannels, 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; } #if JUCE_MODULE_AVAILABLE_juce_gui_basics 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 {}; } #endif void urlOpened(const URL& url) { if (urlOpenedCallback) urlOpenedCallback(url); } std::function urlOpenedCallback; static StandalonePluginHolder* getInstance(); //============================================================================== OptionalScopedPointer settings; std::unique_ptr processor; AudioDeviceManager deviceManager; AudioProcessorPlayer player; Array channelConfiguration; // avoid feedback loop by default bool processorHasPotentialFeedbackLoop = false; // or not Value shouldMuteInput; AudioBuffer emptyBuffer; bool autoOpenMidiDevices = false; std::unique_ptr options; Array lastMidiDevices; private: //============================================================================== class SettingsComponent : public Component { public: SettingsComponent (StandalonePluginHolder& pluginHolder, AudioDeviceManager& deviceManagerToUse, int minAudioInputChannels, int maxAudioInputChannels, int minAudioOutputChannels, int maxAudioOutputChannels) : owner (pluginHolder), deviceSelector (deviceManagerToUse, minAudioInputChannels, maxAudioInputChannels, minAudioOutputChannels, maxAudioOutputChannels, true, // show midi (pluginHolder.processor.get() != nullptr && pluginHolder.processor->producesMidi()), false, 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); } #if JUCE_IOS || JUCE_ANDROID deviceSelector.setItemHeight(38); #endif } void paint (Graphics& g) override { g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId)); } void resized() override { 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); } private: //============================================================================== StandalonePluginHolder& owner; AudioDeviceSelectorComponent deviceSelector; Label shouldMuteLabel; ToggleButton shouldMuteButton; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SettingsComponent) }; //============================================================================== void audioDeviceIOCallback (const float** inputChannelData, int numInputChannels, float** outputChannelData, int numOutputChannels, int numSamples) override { const bool inputMuted = shouldMuteInput.getValue(); if (inputMuted) { emptyBuffer.clear(); inputChannelData = emptyBuffer.getArrayOfReadPointers(); } player.audioDeviceIOCallback (inputChannelData, numInputChannels, outputChannelData, numOutputChannels, numSamples); } void audioDeviceAboutToStart (AudioIODevice* device) override { emptyBuffer.setSize (device->getActiveInputChannels().countNumberOfSetBits(), device->getCurrentBufferSizeSamples()); emptyBuffer.clear(); player.audioDeviceAboutToStart (device); player.setMidiOutput (deviceManager.getDefaultMidiOutput()); #if 0 #if JUCE_IOS if (auto iosdevice = dynamic_cast (deviceManager.getCurrentAudioDevice())) { processorHasPotentialFeedbackLoop = !iosdevice->isHeadphonesConnected() && device->getActiveInputChannels() > 0; shouldMuteInput.setValue(processorHasPotentialFeedbackLoop); } #endif #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 (this); deviceManager.addMidiInputDeviceCallback ({}, &player); reloadAudioDeviceState (enableAudioInput, preferredDefaultDeviceName, preferredSetupOptions); } void shutDownAudioDevices() { saveAudioDeviceState(); deviceManager.removeMidiInputDeviceCallback ({}, &player); deviceManager.removeAudioCallback (this); } 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); } } 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, public 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 = false #else bool autoOpenMidiDevices = false #endif ) : DocumentWindow (title, backgroundColour, DocumentWindow::minimiseButton | DocumentWindow::closeButton), optionsButton ("Options") { #if JUCE_IOS || JUCE_ANDROID setTitleBarHeight (0); #else setTitleBarButtonsRequired (DocumentWindow::minimiseButton | DocumentWindow::closeButton, false); Component::addAndMakeVisible (optionsButton); optionsButton.addListener (this); optionsButton.setTriggeredOnMouseDown (true); setUsingNativeTitleBar(true); #endif setResizable (true, false); pluginHolder.reset (new StandalonePluginHolder (settingsToUse, takeOwnershipOfSettings, preferredDefaultDeviceName, preferredSetupOptions, constrainToConfiguration, autoOpenMidiDevices)); #if JUCE_IOS || JUCE_ANDROID setFullScreen (true); setContentOwned (new MainContentComponent (*this), false); #else setContentOwned (new MainContentComponent (*this), true); if (auto* props = pluginHolder->settings.get()) { const int x = props->getIntValue ("windowX", -100); const int y = props->getIntValue ("windowY", -100); if (x != -100 && y != -100) setBoundsConstrained ({ x, y, getWidth(), getHeight() }); else centreWithSize (getWidth(), getHeight()); } else { centreWithSize (getWidth(), getHeight()); } #endif } ~StandaloneFilterWindow() { #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(); setContentOwned (new MainContentComponent (*this), true); pluginHolder->startPlaying(); } //============================================================================== void closeButtonPressed() override { pluginHolder->savePluginState(); JUCEApplicationBase::quit(); } 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)); } 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: //============================================================================== class MainContentComponent : public Component, private Value::Listener, private Button::Listener, private ComponentListener { public: MainContentComponent (StandaloneFilterWindow& filterWindow) : owner (filterWindow), notification (this), editor (owner.getAudioProcessor()->createEditorIfNeeded()) { // because the plugin editor may have changed this //filterWindow.setBackgroundColour(LookAndFeel::getDefaultLookAndFeel().findColour (ResizableWindow::backgroundColourId)); filterWindow.setBackgroundColour(Colours::black); Value& inputMutedValue = owner.pluginHolder->getMuteInputValue(); if (editor != nullptr) { // hack to allow editor to get devicemanager if (auto * sonoeditor = dynamic_cast(editor.get())) { sonoeditor->getAudioDeviceManager = [this]() { return &owner.getDeviceManager(); }; sonoeditor->showAudioSettingsDialog = [this](Component* calloutTarget, Component* calloutParent) { owner.pluginHolder->showAudioSettingsDialog(calloutTarget, calloutParent); }; owner.pluginHolder->urlOpenedCallback = [sonoeditor](const URL& url) { sonoeditor->urlOpened(url); }; } 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() { if (editor != nullptr) { editor->removeComponentListener (this); owner.pluginHolder->processor->editorBeingDeleted (editor.get()); editor = nullptr; } } void resized() override { auto r = getLocalBounds(); bool portrait = getWidth() < getHeight(); bool tall = getHeight() > 500; float safetop=0.0f, safebottom=0.0f, safeleft=0.0f, saferight=0.0f; int notchPos = 0; getSafeAreaInsets(getWindowHandle(), safetop, safebottom, safeleft, saferight, notchPos); if (portrait != isPortrait || isTall != tall || orientation != notchPos) { isPortrait = portrait; isTall = tall; orientation = notchPos; // call resized again if on iOS, due to dumb stuff related to safe area insets not being updated #if JUCE_IOS Timer::callAfterDelay(150, [this]() { this->resized(); }); //return; #endif } topInset = safetop; bottomInset = safebottom; leftInset = safeleft * (notchPos == 3 ? 0.75f : 0.5f); rightInset = saferight * (notchPos == 4 ? 0.75f : 0.5f); r.removeFromTop(topInset); r.removeFromBottom(bottomInset); r.removeFromLeft(leftInset); r.removeFromRight(rightInset); if (shouldShowNotification) { notification.setBounds (r.removeFromTop (NotificationArea::height)); topInset += NotificationArea::height; } editor->setBounds (r); } private: bool isPortrait = false; bool isTall = false; int orientation = 0; //============================================================================== class NotificationArea : public Component { public: enum { height = 60 }; NotificationArea (Button::Listener* settingsButtonListener) : notification ("notification", "Audio input is muted to avoid\nfeedback loop.\nHeadphones recommended!"), #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 setSize (editor->getWidth(), editor->getHeight() + (shouldShowNotification ? NotificationArea::height : 0)); #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 wasResized) override { if (wasResized && editor != nullptr) setSize (editor->getWidth() + leftInset + rightInset, editor->getHeight() + topInset + bottomInset); } //============================================================================== StandaloneFilterWindow& owner; NotificationArea notification; std::unique_ptr editor; bool shouldShowNotification = false; int topInset = 0; int bottomInset = 0; int leftInset = 0; int rightInset = 0; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent) }; //============================================================================== TextButton optionsButton; 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