FahlGrahn Audio v1.0.0
Loading...
Searching...
No Matches
CustomStandaloneFilterWindow.h
1/*
2 ==============================================================================
3
4 This file is part of the JUCE library.
5 Copyright (c) 2022 - Raw Material Software Limited
6
7 JUCE is an open source library subject to commercial or open-source
8 licensing.
9
10 By using JUCE, you agree to the terms of both the JUCE 7 End-User License
11 Agreement and JUCE Privacy Policy.
12
13 End User License Agreement: www.juce.com/juce-7-licence
14 Privacy Policy: www.juce.com/juce-privacy-policy
15
16 Or: You may also use this code under the terms of the GPL v3 (see
17 www.gnu.org/licenses).
18
19 JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20 EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21 DISCLAIMED.
22
23 ==============================================================================
24*/
25
26#pragma once
27
28#include <juce_audio_utils/juce_audio_utils.h>
29#include <juce_gui_extra/juce_gui_extra.h>
30// #include "CustomLookAndFeel/DeathMetalLookAndFeel.h"
31
32#ifndef DOXYGEN
33#include <juce_audio_plugin_client/detail/juce_CreatePluginFilter.h>
34#endif
35
36namespace juce
37{
38
39//==============================================================================
49class StandalonePluginHolder : private juce::AudioIODeviceCallback, private Timer, private Value::Listener
50{
51 public:
52 //==============================================================================
55 {
56 short numIns, numOuts;
57 };
58
59 //==============================================================================
74 StandalonePluginHolder(PropertySet *settingsToUse, bool takeOwnershipOfSettings = true,
75 const String &preferredDefaultDeviceName = String(),
76 const AudioDeviceManager::AudioDeviceSetup *preferredSetupOptions = nullptr,
77 const Array<PluginInOuts> &channels = Array<PluginInOuts>(),
78#if JUCE_ANDROID || JUCE_IOS
79 bool shouldAutoOpenMidiDevices = true
80#else
81 bool shouldAutoOpenMidiDevices = false
82#endif
83 )
84
85 : settings(settingsToUse, takeOwnershipOfSettings), channelConfiguration(channels),
86 autoOpenMidiDevices(shouldAutoOpenMidiDevices)
87 {
88 shouldMuteInput.addListener(this);
89 shouldMuteInput = !isInterAppAudioConnected();
90
91 handleCreatePlugin();
92
93 auto inChannels = (channelConfiguration.size() > 0 ? channelConfiguration[0].numIns
94 : processor->getMainBusNumInputChannels());
95
96 if (preferredSetupOptions != nullptr)
97 options.reset(new AudioDeviceManager::AudioDeviceSetup(*preferredSetupOptions));
98
99 auto audioInputRequired = (inChannels > 0);
100
101 if (audioInputRequired && RuntimePermissions::isRequired(RuntimePermissions::recordAudio) &&
102 !RuntimePermissions::isGranted(RuntimePermissions::recordAudio))
103 RuntimePermissions::request(
104 RuntimePermissions::recordAudio,
105 [this, preferredDefaultDeviceName](bool granted) { init(granted, preferredDefaultDeviceName); });
106 else
107 init(audioInputRequired, preferredDefaultDeviceName);
108 }
109
110 void init(bool enableAudioInput, const String &preferredDefaultDeviceName)
111 {
112 setupAudioDevices(enableAudioInput, preferredDefaultDeviceName, options.get());
113 reloadPluginState();
114 startPlaying();
115
116 if (autoOpenMidiDevices)
117 startTimer(500);
118 }
119
120 ~StandalonePluginHolder() override
121 {
122 stopTimer();
123
124 handleDeletePlugin();
125 shutDownAudioDevices();
126 }
127
128 //==============================================================================
129 virtual void createPlugin()
130 {
131 handleCreatePlugin();
132 }
133
134 virtual void deletePlugin()
135 {
136 handleDeletePlugin();
137 }
138
139 int getNumInputChannels() const
140 {
141 if (processor == nullptr)
142 return 0;
143
144 return (channelConfiguration.size() > 0 ? channelConfiguration[0].numIns
145 : processor->getMainBusNumInputChannels());
146 }
147
148 int getNumOutputChannels() const
149 {
150 if (processor == nullptr)
151 return 0;
152
153 return (channelConfiguration.size() > 0 ? channelConfiguration[0].numOuts
154 : processor->getMainBusNumOutputChannels());
155 }
156
157 static String getFilePatterns(const String &fileSuffix)
158 {
159 if (fileSuffix.isEmpty())
160 return {};
161
162 return (fileSuffix.startsWithChar('.') ? "*" : "*.") + fileSuffix;
163 }
164
165 //==============================================================================
166 Value &getMuteInputValue()
167 {
168 return shouldMuteInput;
169 }
170 bool getProcessorHasPotentialFeedbackLoop() const
171 {
172 return processorHasPotentialFeedbackLoop;
173 }
174 void valueChanged(Value &value) override
175 {
176 muteInput = (bool)value.getValue();
177 }
178
179 //==============================================================================
180 File getLastFile() const
181 {
182 File f;
183
184 if (settings != nullptr)
185 f = File(settings->getValue("lastStateFile"));
186
187 if (f == File())
188 f = File::getSpecialLocation(File::userDocumentsDirectory);
189
190 return f;
191 }
192
193 void setLastFile(const FileChooser &fc)
194 {
195 if (settings != nullptr)
196 settings->setValue("lastStateFile", fc.getResult().getFullPathName());
197 }
198
200 void askUserToSaveState(const String &fileSuffix = String())
201 {
202 stateFileChooser =
203 std::make_unique<FileChooser>(TRANS("Save current state"), getLastFile(), getFilePatterns(fileSuffix));
204 auto flags = FileBrowserComponent::saveMode | FileBrowserComponent::canSelectFiles |
205 FileBrowserComponent::warnAboutOverwriting;
206
207 stateFileChooser->launchAsync(flags, [this](const FileChooser &fc) {
208 if (fc.getResult() == File{})
209 return;
210
211 setLastFile(fc);
212
213 MemoryBlock data;
214 processor->getStateInformation(data);
215
216 if (!fc.getResult().replaceWithData(data.getData(), data.getSize()))
217 {
218 auto opts = MessageBoxOptions::makeOptionsOk(AlertWindow::WarningIcon, TRANS("Error whilst saving"),
219 TRANS("Couldn't write to the specified file!"));
220 messageBox = AlertWindow::showScopedAsync(opts, nullptr);
221 }
222 });
223 }
224
226 void askUserToLoadState(const String &fileSuffix = String())
227 {
228 stateFileChooser =
229 std::make_unique<FileChooser>(TRANS("Load a saved state"), getLastFile(), getFilePatterns(fileSuffix));
230 auto flags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles;
231
232 stateFileChooser->launchAsync(flags, [this](const FileChooser &fc) {
233 if (fc.getResult() == File{})
234 return;
235
236 setLastFile(fc);
237
238 MemoryBlock data;
239
240 if (fc.getResult().loadFileAsData(data))
241 {
242 processor->setStateInformation(data.getData(), (int)data.getSize());
243 }
244 else
245 {
246 auto opts = MessageBoxOptions::makeOptionsOk(AlertWindow::WarningIcon, TRANS("Error whilst loading"),
247 TRANS("Couldn't read from the specified file!"));
248 messageBox = AlertWindow::showScopedAsync(opts, nullptr);
249 }
250 });
251 }
252
253 //==============================================================================
254 void startPlaying()
255 {
256 player.setProcessor(processor.get());
257
258#if JucePlugin_Enable_IAA && JUCE_IOS
259 if (auto device = dynamic_cast<iOSAudioIODevice *>(deviceManager.getCurrentAudioDevice()))
260 {
261 processor->setPlayHead(device->getAudioPlayHead());
262 device->setMidiMessageCollector(&player.getMidiMessageCollector());
263 }
264#endif
265 }
266
267 void stopPlaying()
268 {
269 player.setProcessor(nullptr);
270 }
271
272 //==============================================================================
275 {
276 DialogWindow::LaunchOptions o;
277
278 int maxNumInputs = 0, maxNumOutputs = 0;
279
280 if (channelConfiguration.size() > 0)
281 {
282 auto &defaultConfig = channelConfiguration.getReference(0);
283
284 maxNumInputs = jmax(0, (int)defaultConfig.numIns);
285 maxNumOutputs = jmax(0, (int)defaultConfig.numOuts);
286 }
287
288 if (auto *bus = processor->getBus(true, 0))
289 maxNumInputs = jmax(0, bus->getDefaultLayout().size());
290
291 if (auto *bus = processor->getBus(false, 0))
292 maxNumOutputs = jmax(0, bus->getDefaultLayout().size());
293
294 auto content = std::make_unique<SettingsComponent>(*this, deviceManager, maxNumInputs, maxNumOutputs);
295 content->setSize(500, 550);
296 content->setToRecommendedSize();
297
298 o.content.setOwned(content.release());
299
300 o.dialogTitle = TRANS("Audio Settings");
301 o.dialogBackgroundColour = o.content->getLookAndFeel().findColour(ResizableWindow::backgroundColourId);
302 o.escapeKeyTriggersCloseButton = true;
303 o.useNativeTitleBar = true;
304 o.resizable = false;
305
306 o.launchAsync();
307 }
308
309 void saveAudioDeviceState()
310 {
311 if (settings != nullptr)
312 {
313 auto xml = deviceManager.createStateXml();
314
315 settings->setValue("audioSetup", xml.get());
316
317#if !(JUCE_IOS || JUCE_ANDROID)
318 settings->setValue("shouldMuteInput", (bool)shouldMuteInput.getValue());
319#endif
320 }
321 }
322
323 void reloadAudioDeviceState(bool enableAudioInput, const String &preferredDefaultDeviceName,
324 const AudioDeviceManager::AudioDeviceSetup *preferredSetupOptions)
325 {
326 std::unique_ptr<XmlElement> savedState;
327
328 if (settings != nullptr)
329 {
330 savedState = settings->getXmlValue("audioSetup");
331
332#if !(JUCE_IOS || JUCE_ANDROID)
333 shouldMuteInput.setValue(settings->getBoolValue("shouldMuteInput", true));
334#endif
335 }
336
337 auto inputChannels = getNumInputChannels();
338 auto outputChannels = getNumOutputChannels();
339
340 if (inputChannels == 0 && outputChannels == 0 && processor->isMidiEffect())
341 {
342 // add a dummy output channel for MIDI effect plug-ins so they can receive audio callbacks
343 outputChannels = 1;
344 }
345
346 deviceManager.initialise(enableAudioInput ? inputChannels : 0, outputChannels, savedState.get(), true,
347 preferredDefaultDeviceName, preferredSetupOptions);
348 }
349
350 //==============================================================================
351 void savePluginState()
352 {
353 if (settings != nullptr && processor != nullptr)
354 {
355 MemoryBlock data;
356 processor->getStateInformation(data);
357
358 settings->setValue("filterState", data.toBase64Encoding());
359 }
360 }
361
362 void reloadPluginState()
363 {
364 if (settings != nullptr)
365 {
366 MemoryBlock data;
367
368 if (data.fromBase64Encoding(settings->getValue("filterState")) && data.getSize() > 0)
369 processor->setStateInformation(data.getData(), (int)data.getSize());
370 }
371 }
372
373 //==============================================================================
374 void switchToHostApplication()
375 {
376#if JUCE_IOS
377 if (auto device = dynamic_cast<iOSAudioIODevice *>(deviceManager.getCurrentAudioDevice()))
378 device->switchApplication();
379#endif
380 }
381
382 bool isInterAppAudioConnected()
383 {
384#if JUCE_IOS
385 if (auto device = dynamic_cast<iOSAudioIODevice *>(deviceManager.getCurrentAudioDevice()))
386 return device->isInterAppAudioConnected();
387#endif
388
389 return false;
390 }
391
392 Image getIAAHostIcon([[maybe_unused]] int size)
393 {
394#if JUCE_IOS && JucePlugin_Enable_IAA
395 if (auto device = dynamic_cast<iOSAudioIODevice *>(deviceManager.getCurrentAudioDevice()))
396 return device->getIcon(size);
397#else
398#endif
399
400 return {};
401 }
402
403 static StandalonePluginHolder *getInstance();
404
405 //==============================================================================
406 OptionalScopedPointer<PropertySet> settings;
407 std::unique_ptr<AudioProcessor> processor;
408 AudioDeviceManager deviceManager;
409 AudioProcessorPlayer player;
410 Array<PluginInOuts> channelConfiguration;
411
412 // avoid feedback loop by default
413 bool processorHasPotentialFeedbackLoop = true;
414 std::atomic<bool> muteInput{true};
415 Value shouldMuteInput;
416 AudioBuffer<float> emptyBuffer;
417 bool autoOpenMidiDevices;
418
419 std::unique_ptr<AudioDeviceManager::AudioDeviceSetup> options;
420 Array<MidiDeviceInfo> lastMidiDevices;
421
422 std::unique_ptr<FileChooser> stateFileChooser;
423 ScopedMessageBox messageBox;
424
425 private:
426 //==============================================================================
427 void handleCreatePlugin()
428 {
429 processor = createPluginFilterOfType(AudioProcessor::wrapperType_Standalone);
430 processor->disableNonMainBuses();
431 processor->setRateAndBufferSizeDetails(44100, 512);
432
433 processorHasPotentialFeedbackLoop = (getNumInputChannels() > 0 && getNumOutputChannels() > 0);
434 }
435
436 void handleDeletePlugin()
437 {
438 stopPlaying();
439 processor = nullptr;
440 }
441
442 //==============================================================================
443 /* This class can be used to ensure that audio callbacks use buffers with a
444 predictable maximum size.
445
446 On some platforms (such as iOS 10), the expected buffer size reported in
447 audioDeviceAboutToStart may be smaller than the blocks passed to
448 audioDeviceIOCallbackWithContext. This can lead to out-of-bounds reads if the render
449 callback depends on additional buffers which were initialised using the
450 smaller size.
451
452 As a workaround, this class will ensure that the render callback will
453 only ever be called with a block with a length less than or equal to the
454 expected block size.
455 */
456 class CallbackMaxSizeEnforcer : public AudioIODeviceCallback
457 {
458 public:
459 explicit CallbackMaxSizeEnforcer(AudioIODeviceCallback &callbackIn) : inner(callbackIn)
460 {
461 }
462
463 void audioDeviceAboutToStart(AudioIODevice *device) override
464 {
465 maximumSize = device->getCurrentBufferSizeSamples();
466 storedInputChannels.resize((size_t)device->getActiveInputChannels().countNumberOfSetBits());
467 storedOutputChannels.resize((size_t)device->getActiveOutputChannels().countNumberOfSetBits());
468
469 inner.audioDeviceAboutToStart(device);
470 }
471
472 void audioDeviceIOCallbackWithContext(const float *const *inputChannelData,
473 [[maybe_unused]] int numInputChannels, float *const *outputChannelData,
474 [[maybe_unused]] int numOutputChannels, int numSamples,
475 const AudioIODeviceCallbackContext &context) override
476 {
477 jassert((int)storedInputChannels.size() == numInputChannels);
478 jassert((int)storedOutputChannels.size() == numOutputChannels);
479
480 int position = 0;
481
482 while (position < numSamples)
483 {
484 const auto blockLength = jmin(maximumSize, numSamples - position);
485
486 initChannelPointers(inputChannelData, storedInputChannels, position);
487 initChannelPointers(outputChannelData, storedOutputChannels, position);
488
489 inner.audioDeviceIOCallbackWithContext(storedInputChannels.data(), (int)storedInputChannels.size(),
490 storedOutputChannels.data(), (int)storedOutputChannels.size(),
491 blockLength, context);
492
493 position += blockLength;
494 }
495 }
496
497 void audioDeviceStopped() override
498 {
499 inner.audioDeviceStopped();
500 }
501
502 private:
503 struct GetChannelWithOffset
504 {
505 int offset;
506
507 template <typename Ptr> auto operator()(Ptr ptr) const noexcept -> Ptr
508 {
509 return ptr + offset;
510 }
511 };
512
513 template <typename Ptr, typename Vector> void initChannelPointers(Ptr &&source, Vector &&target, int offset)
514 {
515 std::transform(source, source + target.size(), target.begin(), GetChannelWithOffset{offset});
516 }
517
518 AudioIODeviceCallback &inner;
519 int maximumSize = 0;
520 std::vector<const float *> storedInputChannels;
521 std::vector<float *> storedOutputChannels;
522 };
523
524 CallbackMaxSizeEnforcer maxSizeEnforcer{*this};
525
526 //==============================================================================
527 class SettingsComponent : public Component
528 {
529 public:
530 SettingsComponent(StandalonePluginHolder &pluginHolder, AudioDeviceManager &deviceManagerToUse,
531 int maxAudioInputChannels, int maxAudioOutputChannels)
532 #if !JucePlugin_IsSynth
533 : owner(pluginHolder), deviceSelector(deviceManagerToUse, 0, maxAudioInputChannels, 0,
534 maxAudioOutputChannels, false, false, false, false),
535 #else
536 : owner(pluginHolder), deviceSelector(deviceManagerToUse, 0, maxAudioInputChannels, 0,
537 maxAudioOutputChannels, true, (pluginHolder.processor.get() != nullptr && pluginHolder.processor->producesMidi()), true, false),
538 #endif
539 shouldMuteLabel("Feedback Loop:", "Feedback Loop:"), shouldMuteButton("Mute audio input")
540 {
541 setOpaque(true);
542
543 shouldMuteButton.setClickingTogglesState(true);
544 shouldMuteButton.getToggleStateValue().referTo(owner.shouldMuteInput);
545
546 juce::LookAndFeel::getDefaultLookAndFeel().setColour(juce::ComboBox::backgroundColourId,
547 juce::Colours::black);
548 juce::LookAndFeel::getDefaultLookAndFeel().setColour(juce::ListBox::backgroundColourId,
549 juce::Colours::black);
550 juce::LookAndFeel::getDefaultLookAndFeel().setColour(juce::TextButton::buttonColourId,
551 juce::Colours::black);
552 juce::LookAndFeel::getDefaultLookAndFeel().setColour(juce::ScrollBar::thumbColourId, juce::Colours::white);
553 juce::LookAndFeel::getDefaultLookAndFeel().setColour(juce::PopupMenu::backgroundColourId,
554 juce::Colours::black);
555
556 addAndMakeVisible(deviceSelector);
557 // deviceSelector.setLookAndFeel(&deathMetalLookAndFeel);
558
559 // if (owner.getProcessorHasPotentialFeedbackLoop())
560 // {
561 // addAndMakeVisible(shouldMuteButton);
562 // addAndMakeVisible(shouldMuteLabel);
563
564 // shouldMuteLabel.attachToComponent(&shouldMuteButton, true);
565 // }
566 }
567
568 void paint(Graphics &g) override
569 {
570 // g.fillAll(getLookAndFeel().findColour(ResizableWindow::backgroundColourId));
571 g.fillAll(juce::Colours::black);
572 }
573
574 void resized() override
575 {
576 const ScopedValueSetter<bool> scope(isResizing, true);
577
578 auto r = getLocalBounds();
579
580 if (owner.getProcessorHasPotentialFeedbackLoop())
581 {
582 auto itemHeight = deviceSelector.getItemHeight();
583 auto extra = r.removeFromTop(itemHeight);
584
585 auto seperatorHeight = (itemHeight >> 1);
586 shouldMuteButton.setBounds(Rectangle<int>(extra.proportionOfWidth(0.35f), seperatorHeight,
587 extra.proportionOfWidth(0.60f),
588 deviceSelector.getItemHeight()));
589
590 r.removeFromTop(seperatorHeight);
591 }
592
593 deviceSelector.setBounds(r);
594 }
595
596 void childBoundsChanged(Component *childComp) override
597 {
598 if (!isResizing && childComp == &deviceSelector)
599 setToRecommendedSize();
600 }
601
602 void setToRecommendedSize()
603 {
604 const auto extraHeight = [&] {
605 if (!owner.getProcessorHasPotentialFeedbackLoop())
606 return 0;
607
608 const auto itemHeight = deviceSelector.getItemHeight();
609 const auto separatorHeight = (itemHeight >> 1);
610 return itemHeight + separatorHeight;
611 }();
612
613 setSize(getWidth(), deviceSelector.getHeight() + extraHeight);
614 }
615
616 private:
617 //==============================================================================
619 AudioDeviceSelectorComponent deviceSelector;
620 // DeathMetalLookAndFeel deathMetalLookAndFeel;
621 Label shouldMuteLabel;
622 ToggleButton shouldMuteButton;
623 bool isResizing = false;
624
625 //==============================================================================
626 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SettingsComponent)
627 };
628
629 //==============================================================================
630 void audioDeviceIOCallbackWithContext(const float *const *inputChannelData, int numInputChannels,
631 float *const *outputChannelData, int numOutputChannels, int numSamples,
632 const AudioIODeviceCallbackContext &context) override
633 {
634 if (muteInput)
635 {
636 emptyBuffer.clear();
637 inputChannelData = emptyBuffer.getArrayOfReadPointers();
638 }
639
640 player.audioDeviceIOCallbackWithContext(inputChannelData, numInputChannels, outputChannelData,
641 numOutputChannels, numSamples, context);
642 }
643
644 void audioDeviceAboutToStart(AudioIODevice *device) override
645 {
646 emptyBuffer.setSize(device->getActiveInputChannels().countNumberOfSetBits(),
647 device->getCurrentBufferSizeSamples());
648 emptyBuffer.clear();
649
650 player.audioDeviceAboutToStart(device);
651 player.setMidiOutput(deviceManager.getDefaultMidiOutput());
652 }
653
654 void audioDeviceStopped() override
655 {
656 player.setMidiOutput(nullptr);
657 player.audioDeviceStopped();
658 emptyBuffer.setSize(0, 0);
659 }
660
661 //==============================================================================
662 void setupAudioDevices(bool enableAudioInput, const String &preferredDefaultDeviceName,
663 const AudioDeviceManager::AudioDeviceSetup *preferredSetupOptions)
664 {
665 deviceManager.addAudioCallback(&maxSizeEnforcer);
666 deviceManager.addMidiInputDeviceCallback({}, &player);
667
668 reloadAudioDeviceState(enableAudioInput, preferredDefaultDeviceName, preferredSetupOptions);
669 }
670
671 void shutDownAudioDevices()
672 {
673 saveAudioDeviceState();
674
675 deviceManager.removeMidiInputDeviceCallback({}, &player);
676 deviceManager.removeAudioCallback(&maxSizeEnforcer);
677 }
678
679 void timerCallback() override
680 {
681 auto newMidiDevices = MidiInput::getAvailableDevices();
682
683 if (newMidiDevices != lastMidiDevices)
684 {
685 for (auto &oldDevice : lastMidiDevices)
686 if (!newMidiDevices.contains(oldDevice))
687 deviceManager.setMidiInputDeviceEnabled(oldDevice.identifier, false);
688
689 for (auto &newDevice : newMidiDevices)
690 if (!lastMidiDevices.contains(newDevice))
691 deviceManager.setMidiInputDeviceEnabled(newDevice.identifier, true);
692
693 lastMidiDevices = newMidiDevices;
694 }
695 }
696
697 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(StandalonePluginHolder)
698};
699
700//==============================================================================
710class StandaloneFilterWindow : public DocumentWindow, private Button::Listener
711{
712 public:
713 //==============================================================================
715
716 //==============================================================================
722 StandaloneFilterWindow(const String &title, Colour backgroundColour, PropertySet *settingsToUse,
723 bool takeOwnershipOfSettings, const String &preferredDefaultDeviceName = String(),
724 const AudioDeviceManager::AudioDeviceSetup *preferredSetupOptions = nullptr,
725 const Array<PluginInOuts> &constrainToConfiguration = {},
726#if JUCE_ANDROID || JUCE_IOS
727 bool autoOpenMidiDevices = true
728#else
729 bool autoOpenMidiDevices = false
730#endif
731 )
732 : DocumentWindow(title, backgroundColour, DocumentWindow::minimiseButton | DocumentWindow::closeButton),
733 optionsButton("Options")
734 {
735 setConstrainer(&decoratorConstrainer);
736
737#if JUCE_IOS || JUCE_ANDROID
738 setTitleBarHeight(0);
739#else
740
741 setUsingNativeTitleBar(true);
742 setTitleBarButtonsRequired(DocumentWindow::minimiseButton | DocumentWindow::closeButton, false);
743
744 Component::addAndMakeVisible(optionsButton);
745 optionsButton.addListener(this);
746 optionsButton.setTriggeredOnMouseDown(true);
747#endif
748
749 pluginHolder.reset(new StandalonePluginHolder(settingsToUse, takeOwnershipOfSettings,
750 preferredDefaultDeviceName, preferredSetupOptions,
751 constrainToConfiguration, autoOpenMidiDevices));
752
753#if JUCE_IOS || JUCE_ANDROID
754 setFullScreen(true);
755 updateContent();
756#else
757 updateContent();
758
759 const auto windowScreenBounds = [this]() -> Rectangle<int> {
760 const auto width = getWidth();
761 const auto height = getHeight();
762
763 const auto &displays = Desktop::getInstance().getDisplays();
764
765 if (auto *props = pluginHolder->settings.get())
766 {
767 constexpr int defaultValue = -100;
768
769 const auto x = props->getIntValue("windowX", defaultValue);
770 const auto y = props->getIntValue("windowY", defaultValue);
771
772 if (x != defaultValue && y != defaultValue)
773 {
774 const auto screenLimits = displays.getDisplayForRect({x, y, width, height})->userArea;
775
776 return {
777 jlimit(screenLimits.getX(), jmax(screenLimits.getX(), screenLimits.getRight() - width), x),
778 jlimit(screenLimits.getY(), jmax(screenLimits.getY(), screenLimits.getBottom() - height), y),
779 width, height};
780 }
781 }
782
783 const auto displayArea = displays.getPrimaryDisplay()->userArea;
784
785 return {displayArea.getCentreX() - width / 2, displayArea.getCentreY() - height / 2, width, height};
786 }();
787
788 setBoundsConstrained(windowScreenBounds);
789
790 if (auto *processor = getAudioProcessor())
791 if (auto *editor = processor->getActiveEditor())
792 setResizable(editor->isResizable(), false);
793#endif
794 }
795
796 ~StandaloneFilterWindow() override
797 {
798#if (!JUCE_IOS) && (!JUCE_ANDROID)
799 if (auto *props = pluginHolder->settings.get())
800 {
801 props->setValue("windowX", getX());
802 props->setValue("windowY", getY());
803 }
804#endif
805
806 pluginHolder->stopPlaying();
807 clearContentComponent();
808 pluginHolder = nullptr;
809 }
810
811 //==============================================================================
812 AudioProcessor *getAudioProcessor() const noexcept
813 {
814 return pluginHolder->processor.get();
815 }
816 AudioDeviceManager &getDeviceManager() const noexcept
817 {
818 return pluginHolder->deviceManager;
819 }
820
823 {
824 pluginHolder->stopPlaying();
825 clearContentComponent();
826 pluginHolder->deletePlugin();
827
828 if (auto *props = pluginHolder->settings.get())
829 props->removeValue("filterState");
830
831 pluginHolder->createPlugin();
832 updateContent();
833 pluginHolder->startPlaying();
834 }
835
836 //==============================================================================
837 void closeButtonPressed() override
838 {
839 pluginHolder->savePluginState();
840
841 JUCEApplicationBase::quit();
842 }
843
844 void handleMenuResult(int result)
845 {
846 switch (result)
847 {
848 case 1:
849 pluginHolder->showAudioSettingsDialog();
850 break;
851 case 2:
852 pluginHolder->askUserToSaveState();
853 break;
854 case 3:
855 pluginHolder->askUserToLoadState();
856 break;
857 case 4:
859 break;
860 default:
861 break;
862 }
863 }
864
865 static void menuCallback(int result, StandaloneFilterWindow *button)
866 {
867 if (button != nullptr && result != 0)
868 button->handleMenuResult(result);
869 }
870
871 void resized() override
872 {
873 DocumentWindow::resized();
874 optionsButton.setBounds(8, 6, 60, getTitleBarHeight() - 8);
875 }
876
877 virtual StandalonePluginHolder *getPluginHolder()
878 {
879 return pluginHolder.get();
880 }
881
882 std::unique_ptr<StandalonePluginHolder> pluginHolder;
883
884 private:
885 void updateContent()
886 {
887 auto *content = new MainContentComponent(*this);
888 decoratorConstrainer.setMainContentComponent(content);
889
890#if JUCE_IOS || JUCE_ANDROID
891 constexpr auto resizeAutomatically = false;
892#else
893 constexpr auto resizeAutomatically = true;
894#endif
895
896 setContentOwned(content, resizeAutomatically);
897 }
898
899 void buttonClicked(Button *) override
900 {
901 PopupMenu m;
902 m.addItem(1, TRANS("Audio/MIDI Settings..."));
903 m.addSeparator();
904 m.addItem(2, TRANS("Save current state..."));
905 m.addItem(3, TRANS("Load a saved state..."));
906 m.addSeparator();
907 m.addItem(4, TRANS("Reset to default state"));
908
909 m.showMenuAsync(PopupMenu::Options(), ModalCallbackFunction::forComponent(menuCallback, this));
910 }
911
912 //==============================================================================
913 class MainContentComponent : public Component,
914 private Value::Listener,
915 private Button::Listener,
916 private ComponentListener
917 {
918 public:
919 MainContentComponent(StandaloneFilterWindow &filterWindow)
920 : owner(filterWindow), notification(this),
921 editor(owner.getAudioProcessor()->hasEditor()
922 ? owner.getAudioProcessor()->createEditorIfNeeded()
923 : new GenericAudioProcessorEditor(*owner.getAudioProcessor()))
924 {
925 inputMutedValue.referTo(owner.pluginHolder->getMuteInputValue());
926
927 if (editor != nullptr)
928 {
929 editor->addComponentListener(this);
930 handleMovedOrResized();
931
932 addAndMakeVisible(editor.get());
933 }
934
935 addChildComponent(notification);
936
937 if (owner.pluginHolder->getProcessorHasPotentialFeedbackLoop())
938 {
939 inputMutedValue.addListener(this);
940 shouldShowNotification = inputMutedValue.getValue();
941 }
942
943 inputMutedChanged(shouldShowNotification);
944 }
945
946 ~MainContentComponent() override
947 {
948 if (editor != nullptr)
949 {
950 editor->removeComponentListener(this);
951 owner.pluginHolder->processor->editorBeingDeleted(editor.get());
952 editor = nullptr;
953 }
954 }
955
956 void resized() override
957 {
958 handleResized();
959 }
960
961 ComponentBoundsConstrainer *getEditorConstrainer() const
962 {
963 if (auto *e = editor.get())
964 return e->getConstrainer();
965
966 return nullptr;
967 }
968
969 BorderSize<int> computeBorder() const
970 {
971 const auto nativeFrame = [&]() -> BorderSize<int> {
972 if (auto *peer = owner.getPeer())
973 if (const auto frameSize = peer->getFrameSizeIfPresent())
974 return *frameSize;
975
976 return {};
977 }();
978
979 return nativeFrame.addedTo(owner.getContentComponentBorder())
980 .addedTo(BorderSize<int>{shouldShowNotification ? NotificationArea::height : 0, 0, 0, 0});
981 }
982
983 private:
984 //==============================================================================
985 class NotificationArea : public Component
986 {
987 public:
988 enum
989 {
990 height = 30
991 };
992
993 NotificationArea(Button::Listener *settingsButtonListener)
994 : notification("notification", "Audio input is muted to avoid feedback loop"),
995#if JUCE_IOS || JUCE_ANDROID
996 settingsButton("Unmute Input")
997#else
998 settingsButton("Settings...")
999#endif
1000 {
1001 setOpaque(true);
1002
1003 notification.setColour(Label::textColourId, Colours::black);
1004
1005 settingsButton.addListener(settingsButtonListener);
1006
1007 addAndMakeVisible(notification);
1008 addAndMakeVisible(settingsButton);
1009 }
1010
1011 void paint(Graphics &g) override
1012 {
1013 auto r = getLocalBounds();
1014
1015 g.setColour(Colours::darkgoldenrod);
1016 g.fillRect(r.removeFromBottom(1));
1017
1018 g.setColour(Colours::lightgoldenrodyellow);
1019 g.fillRect(r);
1020 }
1021
1022 void resized() override
1023 {
1024 auto r = getLocalBounds().reduced(5);
1025
1026 settingsButton.setBounds(r.removeFromRight(70));
1027 notification.setBounds(r);
1028 }
1029
1030 private:
1031 Label notification;
1032 TextButton settingsButton;
1033 };
1034
1035 //==============================================================================
1036 void inputMutedChanged(bool newInputMutedValue)
1037 {
1038 shouldShowNotification = newInputMutedValue;
1039 notification.setVisible(shouldShowNotification);
1040
1041#if JUCE_IOS || JUCE_ANDROID
1042 handleResized();
1043#else
1044 if (editor != nullptr)
1045 {
1046 const int extraHeight = shouldShowNotification ? NotificationArea::height : 0;
1047 const auto rect = getSizeToContainEditor();
1048 setSize(rect.getWidth(), rect.getHeight() + extraHeight);
1049 }
1050#endif
1051 }
1052
1053 void valueChanged(Value &value) override
1054 {
1055 inputMutedChanged(value.getValue());
1056 }
1057 void buttonClicked(Button *) override
1058 {
1059#if JUCE_IOS || JUCE_ANDROID
1060 owner.pluginHolder->getMuteInputValue().setValue(false);
1061#else
1062 owner.pluginHolder->showAudioSettingsDialog();
1063#endif
1064 }
1065
1066 //==============================================================================
1067 void handleResized()
1068 {
1069 auto r = getLocalBounds();
1070
1071 if (shouldShowNotification)
1072 notification.setBounds(r.removeFromTop(NotificationArea::height));
1073
1074 if (editor != nullptr)
1075 {
1076 const auto newPos = r.getTopLeft().toFloat().transformedBy(editor->getTransform().inverted());
1077
1078 if (preventResizingEditor)
1079 editor->setTopLeftPosition(newPos.roundToInt());
1080 else
1081 editor->setBoundsConstrained(
1082 editor->getLocalArea(this, r.toFloat()).withPosition(newPos).toNearestInt());
1083 }
1084 }
1085
1086 void handleMovedOrResized()
1087 {
1088 const ScopedValueSetter<bool> scope(preventResizingEditor, true);
1089
1090 if (editor != nullptr)
1091 {
1092 auto rect = getSizeToContainEditor();
1093
1094 setSize(rect.getWidth(), rect.getHeight() + (shouldShowNotification ? NotificationArea::height : 0));
1095 }
1096 }
1097
1098 void componentMovedOrResized(Component &, bool, bool) override
1099 {
1100 handleMovedOrResized();
1101 }
1102
1103 Rectangle<int> getSizeToContainEditor() const
1104 {
1105 if (editor != nullptr)
1106 return getLocalArea(editor.get(), editor->getLocalBounds());
1107
1108 return {};
1109 }
1110
1111 //==============================================================================
1113 NotificationArea notification;
1114 std::unique_ptr<AudioProcessorEditor> editor;
1115 Value inputMutedValue;
1116 bool shouldShowNotification = false;
1117 bool preventResizingEditor = false;
1118
1119 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainContentComponent)
1120 };
1121
1122 /* This custom constrainer checks with the AudioProcessorEditor (which might itself be
1123 constrained) to ensure that any size we choose for the standalone window will be suitable
1124 for the editor too.
1125
1126 Without this constrainer, attempting to resize the standalone window may set bounds on the
1127 peer that are unsupported by the inner editor. In this scenario, the peer will be set to a
1128 'bad' size, then the inner editor will be resized. The editor will check the new bounds with
1129 its own constrainer, and may set itself to a more suitable size. After that, the resizable
1130 window will see that its content component has changed size, and set the bounds of the peer
1131 accordingly. The end result is that the peer is resized twice in a row to different sizes,
1132 which can appear glitchy/flickery to the user.
1133 */
1134 class DecoratorConstrainer : public BorderedComponentBoundsConstrainer
1135 {
1136 public:
1137 ComponentBoundsConstrainer *getWrappedConstrainer() const override
1138 {
1139 return contentComponent != nullptr ? contentComponent->getEditorConstrainer() : nullptr;
1140 }
1141
1142 BorderSize<int> getAdditionalBorder() const override
1143 {
1144 return contentComponent != nullptr ? contentComponent->computeBorder() : BorderSize<int>{};
1145 }
1146
1147 void setMainContentComponent(MainContentComponent *in)
1148 {
1149 contentComponent = in;
1150 }
1151
1152 private:
1153 MainContentComponent *contentComponent = nullptr;
1154 };
1155
1156 //==============================================================================
1157 TextButton optionsButton;
1158 DecoratorConstrainer decoratorConstrainer;
1159
1160 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(StandaloneFilterWindow)
1161};
1162
1163inline StandalonePluginHolder *StandalonePluginHolder::getInstance()
1164{
1165#if JucePlugin_Enable_IAA || JucePlugin_Build_Standalone
1166 if (PluginHostType::getPluginLoadedAs() == AudioProcessor::wrapperType_Standalone)
1167 {
1168 auto &desktop = Desktop::getInstance();
1169 const int numTopLevelWindows = desktop.getNumComponents();
1170
1171 for (int i = 0; i < numTopLevelWindows; ++i)
1172 if (auto window = dynamic_cast<StandaloneFilterWindow *>(desktop.getComponent(i)))
1173 return window->getPluginHolder();
1174 }
1175#endif
1176
1177 return nullptr;
1178}
1179
1180} // namespace juce
Definition CustomStandaloneFilterWindow.h:711
void resetToDefaultState()
Definition CustomStandaloneFilterWindow.h:822
StandaloneFilterWindow(const String &title, Colour backgroundColour, PropertySet *settingsToUse, bool takeOwnershipOfSettings, const String &preferredDefaultDeviceName=String(), const AudioDeviceManager::AudioDeviceSetup *preferredSetupOptions=nullptr, const Array< PluginInOuts > &constrainToConfiguration={}, bool autoOpenMidiDevices=false)
Definition CustomStandaloneFilterWindow.h:722
Definition CustomStandaloneFilterWindow.h:50
void askUserToLoadState(const String &fileSuffix=String())
Definition CustomStandaloneFilterWindow.h:226
void showAudioSettingsDialog()
Definition CustomStandaloneFilterWindow.h:274
void askUserToSaveState(const String &fileSuffix=String())
Definition CustomStandaloneFilterWindow.h:200
StandalonePluginHolder(PropertySet *settingsToUse, bool takeOwnershipOfSettings=true, const String &preferredDefaultDeviceName=String(), const AudioDeviceManager::AudioDeviceSetup *preferredSetupOptions=nullptr, const Array< PluginInOuts > &channels=Array< PluginInOuts >(), bool shouldAutoOpenMidiDevices=false)
Definition CustomStandaloneFilterWindow.h:74
Definition CustomStandaloneFilterWindow.h:55