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