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