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 #if !JucePlugin_IsSynth
531 : owner(pluginHolder), deviceSelector(deviceManagerToUse, 0, maxAudioInputChannels, 0,
532 maxAudioOutputChannels, false, false, false, false),
533 #else
534 : owner(pluginHolder), deviceSelector(deviceManagerToUse, 0, maxAudioInputChannels, 0,
535 maxAudioOutputChannels, true, (pluginHolder.processor.get() != nullptr && pluginHolder.processor->producesMidi()), true, false),
536 #endif
537 shouldMuteLabel("Feedback Loop:", "Feedback Loop:"), shouldMuteButton("Mute audio input")
538 {
539 setOpaque(true);
540
541 shouldMuteButton.setClickingTogglesState(true);
542 shouldMuteButton.getToggleStateValue().referTo(owner.shouldMuteInput);
543
544 juce::LookAndFeel::getDefaultLookAndFeel().setColour(juce::ComboBox::backgroundColourId,
545 juce::Colours::black);
546 juce::LookAndFeel::getDefaultLookAndFeel().setColour(juce::ListBox::backgroundColourId,
547 juce::Colours::black);
548 juce::LookAndFeel::getDefaultLookAndFeel().setColour(juce::TextButton::buttonColourId,
549 juce::Colours::black);
550 juce::LookAndFeel::getDefaultLookAndFeel().setColour(juce::ScrollBar::thumbColourId, juce::Colours::white);
551 juce::LookAndFeel::getDefaultLookAndFeel().setColour(juce::PopupMenu::backgroundColourId,
552 juce::Colours::black);
553
554 addAndMakeVisible(deviceSelector);
555 // deviceSelector.setLookAndFeel(&deathMetalLookAndFeel);
556
557 // if (owner.getProcessorHasPotentialFeedbackLoop())
558 // {
559 // addAndMakeVisible(shouldMuteButton);
560 // addAndMakeVisible(shouldMuteLabel);
561
562 // shouldMuteLabel.attachToComponent(&shouldMuteButton, true);
563 // }
564 }
565
566 void paint(Graphics &g) override
567 {
568 // g.fillAll(getLookAndFeel().findColour(ResizableWindow::backgroundColourId));
569 g.fillAll(juce::Colours::black);
570 }
571
572 void resized() override
573 {
574 const ScopedValueSetter<bool> scope(isResizing, true);
575
576 auto r = getLocalBounds();
577
578 if (owner.getProcessorHasPotentialFeedbackLoop())
579 {
580 auto itemHeight = deviceSelector.getItemHeight();
581 auto extra = r.removeFromTop(itemHeight);
582
583 auto seperatorHeight = (itemHeight >> 1);
584 shouldMuteButton.setBounds(Rectangle<int>(extra.proportionOfWidth(0.35f), seperatorHeight,
585 extra.proportionOfWidth(0.60f),
586 deviceSelector.getItemHeight()));
587
588 r.removeFromTop(seperatorHeight);
589 }
590
591 deviceSelector.setBounds(r);
592 }
593
594 void childBoundsChanged(Component *childComp) override
595 {
596 if (!isResizing && childComp == &deviceSelector)
597 setToRecommendedSize();
598 }
599
600 void setToRecommendedSize()
601 {
602 const auto extraHeight = [&] {
603 if (!owner.getProcessorHasPotentialFeedbackLoop())
604 return 0;
605
606 const auto itemHeight = deviceSelector.getItemHeight();
607 const auto separatorHeight = (itemHeight >> 1);
608 return itemHeight + separatorHeight;
609 }();
610
611 setSize(getWidth(), deviceSelector.getHeight() + extraHeight);
612 }
613
614 private:
615 //==============================================================================
617 AudioDeviceSelectorComponent deviceSelector;
618 // DeathMetalLookAndFeel deathMetalLookAndFeel;
619 Label shouldMuteLabel;
620 ToggleButton shouldMuteButton;
621 bool isResizing = false;
622
623 //==============================================================================
624 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SettingsComponent)
625 };
626
627 //==============================================================================
628 void audioDeviceIOCallbackWithContext(const float *const *inputChannelData, int numInputChannels,
629 float *const *outputChannelData, int numOutputChannels, int numSamples,
630 const AudioIODeviceCallbackContext &context) override
631 {
632 if (muteInput)
633 {
634 emptyBuffer.clear();
635 inputChannelData = emptyBuffer.getArrayOfReadPointers();
636 }
637
638 player.audioDeviceIOCallbackWithContext(inputChannelData, numInputChannels, outputChannelData,
639 numOutputChannels, numSamples, context);
640 }
641
642 void audioDeviceAboutToStart(AudioIODevice *device) override
643 {
644 emptyBuffer.setSize(device->getActiveInputChannels().countNumberOfSetBits(),
645 device->getCurrentBufferSizeSamples());
646 emptyBuffer.clear();
647
648 player.audioDeviceAboutToStart(device);
649 player.setMidiOutput(deviceManager.getDefaultMidiOutput());
650 }
651
652 void audioDeviceStopped() override
653 {
654 player.setMidiOutput(nullptr);
655 player.audioDeviceStopped();
656 emptyBuffer.setSize(0, 0);
657 }
658
659 //==============================================================================
660 void setupAudioDevices(bool enableAudioInput, const String &preferredDefaultDeviceName,
661 const AudioDeviceManager::AudioDeviceSetup *preferredSetupOptions)
662 {
663 deviceManager.addAudioCallback(&maxSizeEnforcer);
664 deviceManager.addMidiInputDeviceCallback({}, &player);
665
666 reloadAudioDeviceState(enableAudioInput, preferredDefaultDeviceName, preferredSetupOptions);
667 }
668
669 void shutDownAudioDevices()
670 {
671 saveAudioDeviceState();
672
673 deviceManager.removeMidiInputDeviceCallback({}, &player);
674 deviceManager.removeAudioCallback(&maxSizeEnforcer);
675 }
676
677 void timerCallback() override
678 {
679 auto newMidiDevices = MidiInput::getAvailableDevices();
680
681 if (newMidiDevices != lastMidiDevices)
682 {
683 for (auto &oldDevice : lastMidiDevices)
684 if (!newMidiDevices.contains(oldDevice))
685 deviceManager.setMidiInputDeviceEnabled(oldDevice.identifier, false);
686
687 for (auto &newDevice : newMidiDevices)
688 if (!lastMidiDevices.contains(newDevice))
689 deviceManager.setMidiInputDeviceEnabled(newDevice.identifier, true);
690
691 lastMidiDevices = newMidiDevices;
692 }
693 }
694
695 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(StandalonePluginHolder)
696};
697
698//==============================================================================
708class StandaloneFilterWindow : public DocumentWindow, private Button::Listener
709{
710 public:
711 //==============================================================================
713
714 //==============================================================================
720 StandaloneFilterWindow(const String &title, Colour backgroundColour, PropertySet *settingsToUse,
721 bool takeOwnershipOfSettings, const String &preferredDefaultDeviceName = String(),
722 const AudioDeviceManager::AudioDeviceSetup *preferredSetupOptions = nullptr,
723 const Array<PluginInOuts> &constrainToConfiguration = {},
724#if JUCE_ANDROID || JUCE_IOS
725 bool autoOpenMidiDevices = true
726#else
727 bool autoOpenMidiDevices = false
728#endif
729 )
730 : DocumentWindow(title, backgroundColour, DocumentWindow::minimiseButton | DocumentWindow::closeButton),
731 optionsButton("Options")
732 {
733 setConstrainer(&decoratorConstrainer);
734
735#if JUCE_IOS || JUCE_ANDROID
736 setTitleBarHeight(0);
737#else
738
739 setUsingNativeTitleBar(true);
740 setTitleBarButtonsRequired(DocumentWindow::minimiseButton | DocumentWindow::closeButton, false);
741
742 Component::addAndMakeVisible(optionsButton);
743 optionsButton.addListener(this);
744 optionsButton.setTriggeredOnMouseDown(true);
745#endif
746
747 pluginHolder.reset(new StandalonePluginHolder(settingsToUse, takeOwnershipOfSettings,
748 preferredDefaultDeviceName, preferredSetupOptions,
749 constrainToConfiguration, autoOpenMidiDevices));
750
751#if JUCE_IOS || JUCE_ANDROID
752 setFullScreen(true);
753 updateContent();
754#else
755 updateContent();
756
757 const auto windowScreenBounds = [this]() -> Rectangle<int> {
758 const auto width = getWidth();
759 const auto height = getHeight();
760
761 const auto &displays = Desktop::getInstance().getDisplays();
762
763 if (auto *props = pluginHolder->settings.get())
764 {
765 constexpr int defaultValue = -100;
766
767 const auto x = props->getIntValue("windowX", defaultValue);
768 const auto y = props->getIntValue("windowY", defaultValue);
769
770 if (x != defaultValue && y != defaultValue)
771 {
772 const auto screenLimits = displays.getDisplayForRect({x, y, width, height})->userArea;
773
774 return {
775 jlimit(screenLimits.getX(), jmax(screenLimits.getX(), screenLimits.getRight() - width), x),
776 jlimit(screenLimits.getY(), jmax(screenLimits.getY(), screenLimits.getBottom() - height), y),
777 width, height};
778 }
779 }
780
781 const auto displayArea = displays.getPrimaryDisplay()->userArea;
782
783 return {displayArea.getCentreX() - width / 2, displayArea.getCentreY() - height / 2, width, height};
784 }();
785
786 setBoundsConstrained(windowScreenBounds);
787
788 if (auto *processor = getAudioProcessor())
789 if (auto *editor = processor->getActiveEditor())
790 setResizable(editor->isResizable(), false);
791#endif
792 }
793
794 ~StandaloneFilterWindow() override
795 {
796#if (!JUCE_IOS) && (!JUCE_ANDROID)
797 if (auto *props = pluginHolder->settings.get())
798 {
799 props->setValue("windowX", getX());
800 props->setValue("windowY", getY());
801 }
802#endif
803
804 pluginHolder->stopPlaying();
805 clearContentComponent();
806 pluginHolder = nullptr;
807 }
808
809 //==============================================================================
810 AudioProcessor *getAudioProcessor() const noexcept
811 {
812 return pluginHolder->processor.get();
813 }
814 AudioDeviceManager &getDeviceManager() const noexcept
815 {
816 return pluginHolder->deviceManager;
817 }
818
821 {
822 pluginHolder->stopPlaying();
823 clearContentComponent();
824 pluginHolder->deletePlugin();
825
826 if (auto *props = pluginHolder->settings.get())
827 props->removeValue("filterState");
828
829 pluginHolder->createPlugin();
830 updateContent();
831 pluginHolder->startPlaying();
832 }
833
834 //==============================================================================
835 void closeButtonPressed() override
836 {
837 pluginHolder->savePluginState();
838
839 JUCEApplicationBase::quit();
840 }
841
842 void handleMenuResult(int result)
843 {
844 switch (result)
845 {
846 case 1:
847 pluginHolder->showAudioSettingsDialog();
848 break;
849 case 2:
850 pluginHolder->askUserToSaveState();
851 break;
852 case 3:
853 pluginHolder->askUserToLoadState();
854 break;
855 case 4:
857 break;
858 default:
859 break;
860 }
861 }
862
863 static void menuCallback(int result, StandaloneFilterWindow *button)
864 {
865 if (button != nullptr && result != 0)
866 button->handleMenuResult(result);
867 }
868
869 void resized() override
870 {
871 DocumentWindow::resized();
872 optionsButton.setBounds(8, 6, 60, getTitleBarHeight() - 8);
873 }
874
875 virtual StandalonePluginHolder *getPluginHolder()
876 {
877 return pluginHolder.get();
878 }
879
880 std::unique_ptr<StandalonePluginHolder> pluginHolder;
881
882 private:
883 void updateContent()
884 {
885 auto *content = new MainContentComponent(*this);
886 decoratorConstrainer.setMainContentComponent(content);
887
888#if JUCE_IOS || JUCE_ANDROID
889 constexpr auto resizeAutomatically = false;
890#else
891 constexpr auto resizeAutomatically = true;
892#endif
893
894 setContentOwned(content, resizeAutomatically);
895 }
896
897 void buttonClicked(Button *) override
898 {
899 PopupMenu m;
900 m.addItem(1, TRANS("Audio/MIDI Settings..."));
901 m.addSeparator();
902 m.addItem(2, TRANS("Save current state..."));
903 m.addItem(3, TRANS("Load a saved state..."));
904 m.addSeparator();
905 m.addItem(4, TRANS("Reset to default state"));
906
907 m.showMenuAsync(PopupMenu::Options(), ModalCallbackFunction::forComponent(menuCallback, this));
908 }
909
910 //==============================================================================
911 class MainContentComponent : public Component,
912 private Value::Listener,
913 private Button::Listener,
914 private ComponentListener
915 {
916 public:
917 MainContentComponent(StandaloneFilterWindow &filterWindow)
918 : owner(filterWindow), notification(this),
919 editor(owner.getAudioProcessor()->hasEditor()
920 ? owner.getAudioProcessor()->createEditorIfNeeded()
921 : new GenericAudioProcessorEditor(*owner.getAudioProcessor()))
922 {
923 inputMutedValue.referTo(owner.pluginHolder->getMuteInputValue());
924
925 if (editor != nullptr)
926 {
927 editor->addComponentListener(this);
928 handleMovedOrResized();
929
930 addAndMakeVisible(editor.get());
931 }
932
933 addChildComponent(notification);
934
935 if (owner.pluginHolder->getProcessorHasPotentialFeedbackLoop())
936 {
937 inputMutedValue.addListener(this);
938 shouldShowNotification = inputMutedValue.getValue();
939 }
940
941 inputMutedChanged(shouldShowNotification);
942 }
943
944 ~MainContentComponent() override
945 {
946 if (editor != nullptr)
947 {
948 editor->removeComponentListener(this);
949 owner.pluginHolder->processor->editorBeingDeleted(editor.get());
950 editor = nullptr;
951 }
952 }
953
954 void resized() override
955 {
956 handleResized();
957 }
958
959 ComponentBoundsConstrainer *getEditorConstrainer() const
960 {
961 if (auto *e = editor.get())
962 return e->getConstrainer();
963
964 return nullptr;
965 }
966
967 BorderSize<int> computeBorder() const
968 {
969 const auto nativeFrame = [&]() -> BorderSize<int> {
970 if (auto *peer = owner.getPeer())
971 if (const auto frameSize = peer->getFrameSizeIfPresent())
972 return *frameSize;
973
974 return {};
975 }();
976
977 return nativeFrame.addedTo(owner.getContentComponentBorder())
978 .addedTo(BorderSize<int>{shouldShowNotification ? NotificationArea::height : 0, 0, 0, 0});
979 }
980
981 private:
982 //==============================================================================
983 class NotificationArea : public Component
984 {
985 public:
986 enum
987 {
988 height = 30
989 };
990
991 NotificationArea(Button::Listener *settingsButtonListener)
992 : notification("notification", "Audio input is muted to avoid feedback loop"),
993#if JUCE_IOS || JUCE_ANDROID
994 settingsButton("Unmute Input")
995#else
996 settingsButton("Settings...")
997#endif
998 {
999 setOpaque(true);
1000
1001 notification.setColour(Label::textColourId, Colours::black);
1002
1003 settingsButton.addListener(settingsButtonListener);
1004
1005 addAndMakeVisible(notification);
1006 addAndMakeVisible(settingsButton);
1007 }
1008
1009 void paint(Graphics &g) override
1010 {
1011 auto r = getLocalBounds();
1012
1013 g.setColour(Colours::darkgoldenrod);
1014 g.fillRect(r.removeFromBottom(1));
1015
1016 g.setColour(Colours::lightgoldenrodyellow);
1017 g.fillRect(r);
1018 }
1019
1020 void resized() override
1021 {
1022 auto r = getLocalBounds().reduced(5);
1023
1024 settingsButton.setBounds(r.removeFromRight(70));
1025 notification.setBounds(r);
1026 }
1027
1028 private:
1029 Label notification;
1030 TextButton settingsButton;
1031 };
1032
1033 //==============================================================================
1034 void inputMutedChanged(bool newInputMutedValue)
1035 {
1036 shouldShowNotification = newInputMutedValue;
1037 notification.setVisible(shouldShowNotification);
1038
1039#if JUCE_IOS || JUCE_ANDROID
1040 handleResized();
1041#else
1042 if (editor != nullptr)
1043 {
1044 const int extraHeight = shouldShowNotification ? NotificationArea::height : 0;
1045 const auto rect = getSizeToContainEditor();
1046 setSize(rect.getWidth(), rect.getHeight() + extraHeight);
1047 }
1048#endif
1049 }
1050
1051 void valueChanged(Value &value) override
1052 {
1053 inputMutedChanged(value.getValue());
1054 }
1055 void buttonClicked(Button *) override
1056 {
1057#if JUCE_IOS || JUCE_ANDROID
1058 owner.pluginHolder->getMuteInputValue().setValue(false);
1059#else
1060 owner.pluginHolder->showAudioSettingsDialog();
1061#endif
1062 }
1063
1064 //==============================================================================
1065 void handleResized()
1066 {
1067 auto r = getLocalBounds();
1068
1069 if (shouldShowNotification)
1070 notification.setBounds(r.removeFromTop(NotificationArea::height));
1071
1072 if (editor != nullptr)
1073 {
1074 const auto newPos = r.getTopLeft().toFloat().transformedBy(editor->getTransform().inverted());
1075
1076 if (preventResizingEditor)
1077 editor->setTopLeftPosition(newPos.roundToInt());
1078 else
1079 editor->setBoundsConstrained(
1080 editor->getLocalArea(this, r.toFloat()).withPosition(newPos).toNearestInt());
1081 }
1082 }
1083
1084 void handleMovedOrResized()
1085 {
1086 const ScopedValueSetter<bool> scope(preventResizingEditor, true);
1087
1088 if (editor != nullptr)
1089 {
1090 auto rect = getSizeToContainEditor();
1091
1092 setSize(rect.getWidth(), rect.getHeight() + (shouldShowNotification ? NotificationArea::height : 0));
1093 }
1094 }
1095
1096 void componentMovedOrResized(Component &, bool, bool) override
1097 {
1098 handleMovedOrResized();
1099 }
1100
1101 Rectangle<int> getSizeToContainEditor() const
1102 {
1103 if (editor != nullptr)
1104 return getLocalArea(editor.get(), editor->getLocalBounds());
1105
1106 return {};
1107 }
1108
1109 //==============================================================================
1111 NotificationArea notification;
1112 std::unique_ptr<AudioProcessorEditor> editor;
1113 Value inputMutedValue;
1114 bool shouldShowNotification = false;
1115 bool preventResizingEditor = false;
1116
1117 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainContentComponent)
1118 };
1119
1120 /* This custom constrainer checks with the AudioProcessorEditor (which might itself be
1121 constrained) to ensure that any size we choose for the standalone window will be suitable
1122 for the editor too.
1123
1124 Without this constrainer, attempting to resize the standalone window may set bounds on the
1125 peer that are unsupported by the inner editor. In this scenario, the peer will be set to a
1126 'bad' size, then the inner editor will be resized. The editor will check the new bounds with
1127 its own constrainer, and may set itself to a more suitable size. After that, the resizable
1128 window will see that its content component has changed size, and set the bounds of the peer
1129 accordingly. The end result is that the peer is resized twice in a row to different sizes,
1130 which can appear glitchy/flickery to the user.
1131 */
1132 class DecoratorConstrainer : public BorderedComponentBoundsConstrainer
1133 {
1134 public:
1135 ComponentBoundsConstrainer *getWrappedConstrainer() const override
1136 {
1137 return contentComponent != nullptr ? contentComponent->getEditorConstrainer() : nullptr;
1138 }
1139
1140 BorderSize<int> getAdditionalBorder() const override
1141 {
1142 return contentComponent != nullptr ? contentComponent->computeBorder() : BorderSize<int>{};
1143 }
1144
1145 void setMainContentComponent(MainContentComponent *in)
1146 {
1147 contentComponent = in;
1148 }
1149
1150 private:
1151 MainContentComponent *contentComponent = nullptr;
1152 };
1153
1154 //==============================================================================
1155 TextButton optionsButton;
1156 DecoratorConstrainer decoratorConstrainer;
1157
1158 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(StandaloneFilterWindow)
1159};
1160
1161inline StandalonePluginHolder *StandalonePluginHolder::getInstance()
1162{
1163#if JucePlugin_Enable_IAA || JucePlugin_Build_Standalone
1164 if (PluginHostType::getPluginLoadedAs() == AudioProcessor::wrapperType_Standalone)
1165 {
1166 auto &desktop = Desktop::getInstance();
1167 const int numTopLevelWindows = desktop.getNumComponents();
1168
1169 for (int i = 0; i < numTopLevelWindows; ++i)
1170 if (auto window = dynamic_cast<StandaloneFilterWindow *>(desktop.getComponent(i)))
1171 return window->getPluginHolder();
1172 }
1173#endif
1174
1175 return nullptr;
1176}
1177
1178} // namespace juce
Definition CustomStandaloneFilterWindow.h:709
void resetToDefaultState()
Definition CustomStandaloneFilterWindow.h:820
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:720
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