589 lines
20 KiB
C++
589 lines
20 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE examples.
|
|
Copyright (c) 2022 - Raw Material Software Limited
|
|
|
|
The code included in this file is provided under the terms of the ISC license
|
|
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
|
To use, copy, modify, and/or distribute this software for any purpose with or
|
|
without fee is hereby granted provided that the above copyright notice and
|
|
this permission notice appear in all copies.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
|
|
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
|
|
PURPOSE, ARE DISCLAIMED.
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
/*******************************************************************************
|
|
The block below describes the properties of this PIP. A PIP is a short snippet
|
|
of code that can be read by the Projucer and used to generate a JUCE project.
|
|
|
|
BEGIN_JUCE_PIP_METADATA
|
|
|
|
name: HostPluginDemo
|
|
version: 1.0.0
|
|
vendor: JUCE
|
|
website: http://juce.com
|
|
description: Plugin that can host other plugins
|
|
|
|
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
|
juce_audio_plugin_client, juce_audio_processors,
|
|
juce_audio_utils, juce_core, juce_data_structures,
|
|
juce_events, juce_graphics, juce_gui_basics, juce_gui_extra
|
|
exporters: xcode_mac, vs2022, linux_make
|
|
|
|
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
|
JUCE_PLUGINHOST_LV2=1
|
|
JUCE_PLUGINHOST_VST3=1
|
|
JUCE_PLUGINHOST_VST=0
|
|
JUCE_PLUGINHOST_AU=1
|
|
|
|
type: AudioProcessor
|
|
mainClass: HostAudioProcessor
|
|
|
|
useLocalCopy: 1
|
|
|
|
pluginCharacteristics: pluginIsSynth, pluginWantsMidiIn, pluginProducesMidiOut,
|
|
pluginEditorRequiresKeys
|
|
|
|
END_JUCE_PIP_METADATA
|
|
|
|
*******************************************************************************/
|
|
|
|
#pragma once
|
|
|
|
//==============================================================================
|
|
enum class EditorStyle { thisWindow, newWindow };
|
|
|
|
class HostAudioProcessorImpl : public AudioProcessor,
|
|
private ChangeListener
|
|
{
|
|
public:
|
|
HostAudioProcessorImpl()
|
|
: AudioProcessor (BusesProperties().withInput ("Input", AudioChannelSet::stereo(), true)
|
|
.withOutput ("Output", AudioChannelSet::stereo(), true))
|
|
{
|
|
appProperties.setStorageParameters ([&]
|
|
{
|
|
PropertiesFile::Options opt;
|
|
opt.applicationName = getName();
|
|
opt.commonToAllUsers = false;
|
|
opt.doNotSave = false;
|
|
opt.filenameSuffix = ".props";
|
|
opt.ignoreCaseOfKeyNames = false;
|
|
opt.storageFormat = PropertiesFile::StorageFormat::storeAsXML;
|
|
opt.osxLibrarySubFolder = "Application Support";
|
|
return opt;
|
|
}());
|
|
|
|
pluginFormatManager.addDefaultFormats();
|
|
|
|
if (auto savedPluginList = appProperties.getUserSettings()->getXmlValue ("pluginList"))
|
|
pluginList.recreateFromXml (*savedPluginList);
|
|
|
|
MessageManagerLock lock;
|
|
pluginList.addChangeListener (this);
|
|
}
|
|
|
|
bool isBusesLayoutSupported (const BusesLayout& layouts) const override
|
|
{
|
|
const auto& mainOutput = layouts.getMainOutputChannelSet();
|
|
const auto& mainInput = layouts.getMainInputChannelSet();
|
|
|
|
if (! mainInput.isDisabled() && mainInput != mainOutput)
|
|
return false;
|
|
|
|
if (mainOutput.size() > 2)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
void prepareToPlay (double sr, int bs) override
|
|
{
|
|
const ScopedLock sl (innerMutex);
|
|
|
|
active = true;
|
|
|
|
if (inner != nullptr)
|
|
{
|
|
inner->setRateAndBufferSizeDetails (sr, bs);
|
|
inner->prepareToPlay (sr, bs);
|
|
}
|
|
}
|
|
|
|
void releaseResources() override
|
|
{
|
|
const ScopedLock sl (innerMutex);
|
|
|
|
active = false;
|
|
|
|
if (inner != nullptr)
|
|
inner->releaseResources();
|
|
}
|
|
|
|
void reset() override
|
|
{
|
|
const ScopedLock sl (innerMutex);
|
|
|
|
if (inner != nullptr)
|
|
inner->reset();
|
|
}
|
|
|
|
// In this example, we don't actually pass any audio through the inner processor.
|
|
// In a 'real' plugin, we'd need to add some synchronisation to ensure that the inner
|
|
// plugin instance was never modified (deleted, replaced etc.) during a call to processBlock.
|
|
void processBlock (AudioBuffer<float>&, MidiBuffer&) override
|
|
{
|
|
jassert (! isUsingDoublePrecision());
|
|
}
|
|
|
|
void processBlock (AudioBuffer<double>&, MidiBuffer&) override
|
|
{
|
|
jassert (isUsingDoublePrecision());
|
|
}
|
|
|
|
bool hasEditor() const override { return false; }
|
|
AudioProcessorEditor* createEditor() override { return nullptr; }
|
|
|
|
const String getName() const override { return "HostPluginDemo"; }
|
|
bool acceptsMidi() const override { return true; }
|
|
bool producesMidi() const override { return true; }
|
|
double getTailLengthSeconds() const override { return 0.0; }
|
|
|
|
int getNumPrograms() override { return 0; }
|
|
int getCurrentProgram() override { return 0; }
|
|
void setCurrentProgram (int) override {}
|
|
const String getProgramName (int) override { return "None"; }
|
|
void changeProgramName (int, const String&) override {}
|
|
|
|
void getStateInformation (MemoryBlock& destData) override
|
|
{
|
|
const ScopedLock sl (innerMutex);
|
|
|
|
XmlElement xml ("state");
|
|
|
|
if (inner != nullptr)
|
|
{
|
|
xml.setAttribute (editorStyleTag, (int) editorStyle);
|
|
xml.addChildElement (inner->getPluginDescription().createXml().release());
|
|
xml.addChildElement ([this]
|
|
{
|
|
MemoryBlock innerState;
|
|
inner->getStateInformation (innerState);
|
|
|
|
auto stateNode = std::make_unique<XmlElement> (innerStateTag);
|
|
stateNode->addTextElement (innerState.toBase64Encoding());
|
|
return stateNode.release();
|
|
}());
|
|
}
|
|
|
|
const auto text = xml.toString();
|
|
destData.replaceAll (text.toRawUTF8(), text.getNumBytesAsUTF8());
|
|
}
|
|
|
|
void setStateInformation (const void* data, int sizeInBytes) override
|
|
{
|
|
const ScopedLock sl (innerMutex);
|
|
|
|
auto xml = XmlDocument::parse (String (CharPointer_UTF8 (static_cast<const char*> (data)), (size_t) sizeInBytes));
|
|
|
|
if (auto* pluginNode = xml->getChildByName ("PLUGIN"))
|
|
{
|
|
PluginDescription pd;
|
|
pd.loadFromXml (*pluginNode);
|
|
|
|
MemoryBlock innerState;
|
|
innerState.fromBase64Encoding (xml->getChildElementAllSubText (innerStateTag, {}));
|
|
|
|
setNewPlugin (pd,
|
|
(EditorStyle) xml->getIntAttribute (editorStyleTag, 0),
|
|
innerState);
|
|
}
|
|
}
|
|
|
|
void setNewPlugin (const PluginDescription& pd, EditorStyle where, const MemoryBlock& mb = {})
|
|
{
|
|
const ScopedLock sl (innerMutex);
|
|
|
|
const auto callback = [this, where, mb] (std::unique_ptr<AudioPluginInstance> instance, const String& error)
|
|
{
|
|
if (error.isNotEmpty())
|
|
{
|
|
NativeMessageBox::showMessageBoxAsync (MessageBoxIconType::WarningIcon,
|
|
"Plugin Load Failed",
|
|
error,
|
|
nullptr,
|
|
nullptr);
|
|
return;
|
|
}
|
|
|
|
inner = std::move (instance);
|
|
editorStyle = where;
|
|
|
|
if (inner != nullptr && ! mb.isEmpty())
|
|
inner->setStateInformation (mb.getData(), (int) mb.getSize());
|
|
|
|
// In a 'real' plugin, we'd also need to set the bus configuration of the inner plugin.
|
|
// One possibility would be to match the bus configuration of the wrapper plugin, but
|
|
// the inner plugin isn't guaranteed to support the same layout. Alternatively, we
|
|
// could try to apply a reasonably similar layout, and maintain a mapping between the
|
|
// inner/outer channel layouts.
|
|
//
|
|
// In any case, it is essential that the inner plugin is told about the bus
|
|
// configuration that will be used. The AudioBuffer passed to the inner plugin must also
|
|
// exactly match this layout.
|
|
|
|
if (active)
|
|
{
|
|
inner->setRateAndBufferSizeDetails (getSampleRate(), getBlockSize());
|
|
inner->prepareToPlay (getSampleRate(), getBlockSize());
|
|
}
|
|
|
|
NullCheckedInvocation::invoke (pluginChanged);
|
|
};
|
|
|
|
pluginFormatManager.createPluginInstanceAsync (pd, getSampleRate(), getBlockSize(), callback);
|
|
}
|
|
|
|
void clearPlugin()
|
|
{
|
|
const ScopedLock sl (innerMutex);
|
|
|
|
inner = nullptr;
|
|
NullCheckedInvocation::invoke (pluginChanged);
|
|
}
|
|
|
|
bool isPluginLoaded() const
|
|
{
|
|
const ScopedLock sl (innerMutex);
|
|
return inner != nullptr;
|
|
}
|
|
|
|
std::unique_ptr<AudioProcessorEditor> createInnerEditor() const
|
|
{
|
|
const ScopedLock sl (innerMutex);
|
|
return rawToUniquePtr (inner->hasEditor() ? inner->createEditorIfNeeded() : nullptr);
|
|
}
|
|
|
|
EditorStyle getEditorStyle() const noexcept { return editorStyle; }
|
|
|
|
ApplicationProperties appProperties;
|
|
AudioPluginFormatManager pluginFormatManager;
|
|
KnownPluginList pluginList;
|
|
std::function<void()> pluginChanged;
|
|
|
|
private:
|
|
CriticalSection innerMutex;
|
|
std::unique_ptr<AudioPluginInstance> inner;
|
|
EditorStyle editorStyle = EditorStyle{};
|
|
bool active = false;
|
|
|
|
static constexpr const char* innerStateTag = "inner_state";
|
|
static constexpr const char* editorStyleTag = "editor_style";
|
|
|
|
void changeListenerCallback (ChangeBroadcaster* source) override
|
|
{
|
|
if (source != &pluginList)
|
|
return;
|
|
|
|
if (auto savedPluginList = pluginList.createXml())
|
|
{
|
|
appProperties.getUserSettings()->setValue ("pluginList", savedPluginList.get());
|
|
appProperties.saveIfNeeded();
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
constexpr const char* HostAudioProcessorImpl::innerStateTag;
|
|
constexpr const char* HostAudioProcessorImpl::editorStyleTag;
|
|
|
|
//==============================================================================
|
|
constexpr auto margin = 10;
|
|
|
|
static void doLayout (Component* main, Component& bottom, int bottomHeight, Rectangle<int> bounds)
|
|
{
|
|
Grid grid;
|
|
grid.setGap (Grid::Px { margin });
|
|
grid.templateColumns = { Grid::TrackInfo { Grid::Fr { 1 } } };
|
|
grid.templateRows = { Grid::TrackInfo { Grid::Fr { 1 } },
|
|
Grid::TrackInfo { Grid::Px { bottomHeight }} };
|
|
grid.items = { GridItem { main }, GridItem { bottom }.withMargin ({ 0, margin, margin, margin }) };
|
|
grid.performLayout (bounds);
|
|
}
|
|
|
|
class PluginLoaderComponent : public Component
|
|
{
|
|
public:
|
|
template <typename Callback>
|
|
PluginLoaderComponent (AudioPluginFormatManager& manager,
|
|
KnownPluginList& list,
|
|
Callback&& callback)
|
|
: pluginListComponent (manager, list, {}, {})
|
|
{
|
|
pluginListComponent.getTableListBox().setMultipleSelectionEnabled (false);
|
|
|
|
addAndMakeVisible (pluginListComponent);
|
|
addAndMakeVisible (buttons);
|
|
|
|
const auto getCallback = [this, &list, callback = std::forward<Callback> (callback)] (EditorStyle style)
|
|
{
|
|
return [this, &list, callback, style]
|
|
{
|
|
const auto index = pluginListComponent.getTableListBox().getSelectedRow();
|
|
const auto& types = list.getTypes();
|
|
|
|
if (isPositiveAndBelow (index, types.size()))
|
|
NullCheckedInvocation::invoke (callback, types.getReference (index), style);
|
|
};
|
|
};
|
|
|
|
buttons.thisWindowButton.onClick = getCallback (EditorStyle::thisWindow);
|
|
buttons.newWindowButton .onClick = getCallback (EditorStyle::newWindow);
|
|
}
|
|
|
|
void resized() override
|
|
{
|
|
doLayout (&pluginListComponent, buttons, 80, getLocalBounds());
|
|
}
|
|
|
|
private:
|
|
struct Buttons : public Component
|
|
{
|
|
Buttons()
|
|
{
|
|
label.setJustificationType (Justification::centred);
|
|
|
|
addAndMakeVisible (label);
|
|
addAndMakeVisible (thisWindowButton);
|
|
addAndMakeVisible (newWindowButton);
|
|
}
|
|
|
|
void resized() override
|
|
{
|
|
Grid vertical;
|
|
vertical.autoFlow = Grid::AutoFlow::row;
|
|
vertical.setGap (Grid::Px { margin });
|
|
vertical.autoRows = vertical.autoColumns = Grid::TrackInfo { Grid::Fr { 1 } };
|
|
vertical.items.insertMultiple (0, GridItem{}, 2);
|
|
vertical.performLayout (getLocalBounds());
|
|
|
|
label.setBounds (vertical.items[0].currentBounds.toNearestInt());
|
|
|
|
Grid grid;
|
|
grid.autoFlow = Grid::AutoFlow::column;
|
|
grid.setGap (Grid::Px { margin });
|
|
grid.autoRows = grid.autoColumns = Grid::TrackInfo { Grid::Fr { 1 } };
|
|
grid.items = { GridItem { thisWindowButton },
|
|
GridItem { newWindowButton } };
|
|
|
|
grid.performLayout (vertical.items[1].currentBounds.toNearestInt());
|
|
}
|
|
|
|
Label label { "", "Select a plugin from the list, then display it using the buttons below." };
|
|
TextButton thisWindowButton { "Open In This Window" };
|
|
TextButton newWindowButton { "Open In New Window" };
|
|
};
|
|
|
|
PluginListComponent pluginListComponent;
|
|
Buttons buttons;
|
|
};
|
|
|
|
//==============================================================================
|
|
class PluginEditorComponent : public Component
|
|
{
|
|
public:
|
|
template <typename Callback>
|
|
PluginEditorComponent (std::unique_ptr<AudioProcessorEditor> editorIn, Callback&& onClose)
|
|
: editor (std::move (editorIn))
|
|
{
|
|
addAndMakeVisible (editor.get());
|
|
addAndMakeVisible (closeButton);
|
|
|
|
childBoundsChanged (editor.get());
|
|
|
|
closeButton.onClick = std::forward<Callback> (onClose);
|
|
}
|
|
|
|
void setScaleFactor (float scale)
|
|
{
|
|
if (editor != nullptr)
|
|
editor->setScaleFactor (scale);
|
|
}
|
|
|
|
void resized() override
|
|
{
|
|
doLayout (editor.get(), closeButton, buttonHeight, getLocalBounds());
|
|
}
|
|
|
|
void childBoundsChanged (Component* child) override
|
|
{
|
|
if (child != editor.get())
|
|
return;
|
|
|
|
const auto size = editor != nullptr ? editor->getLocalBounds()
|
|
: Rectangle<int>();
|
|
|
|
setSize (size.getWidth(), margin + buttonHeight + size.getHeight());
|
|
}
|
|
|
|
private:
|
|
static constexpr auto buttonHeight = 40;
|
|
|
|
std::unique_ptr<AudioProcessorEditor> editor;
|
|
TextButton closeButton { "Close Plugin" };
|
|
};
|
|
|
|
//==============================================================================
|
|
class ScaledDocumentWindow : public DocumentWindow
|
|
{
|
|
public:
|
|
ScaledDocumentWindow (Colour bg, float scale)
|
|
: DocumentWindow ("Editor", bg, 0), desktopScale (scale) {}
|
|
|
|
float getDesktopScaleFactor() const override { return Desktop::getInstance().getGlobalScaleFactor() * desktopScale; }
|
|
|
|
private:
|
|
float desktopScale = 1.0f;
|
|
};
|
|
|
|
//==============================================================================
|
|
class HostAudioProcessorEditor : public AudioProcessorEditor
|
|
{
|
|
public:
|
|
explicit HostAudioProcessorEditor (HostAudioProcessorImpl& owner)
|
|
: AudioProcessorEditor (owner),
|
|
hostProcessor (owner),
|
|
loader (owner.pluginFormatManager,
|
|
owner.pluginList,
|
|
[&owner] (const PluginDescription& pd,
|
|
EditorStyle editorStyle)
|
|
{
|
|
owner.setNewPlugin (pd, editorStyle);
|
|
}),
|
|
scopedCallback (owner.pluginChanged, [this] { pluginChanged(); })
|
|
{
|
|
setSize (500, 500);
|
|
setResizable (false, false);
|
|
addAndMakeVisible (closeButton);
|
|
addAndMakeVisible (loader);
|
|
|
|
hostProcessor.pluginChanged();
|
|
|
|
closeButton.onClick = [this] { clearPlugin(); };
|
|
}
|
|
|
|
void paint (Graphics& g) override
|
|
{
|
|
g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId).darker());
|
|
}
|
|
|
|
void resized() override
|
|
{
|
|
closeButton.setBounds (getLocalBounds().withSizeKeepingCentre (200, buttonHeight));
|
|
loader.setBounds (getLocalBounds());
|
|
}
|
|
|
|
void childBoundsChanged (Component* child) override
|
|
{
|
|
if (child != editor.get())
|
|
return;
|
|
|
|
const auto size = editor != nullptr ? editor->getLocalBounds()
|
|
: Rectangle<int>();
|
|
|
|
setSize (size.getWidth(), size.getHeight());
|
|
}
|
|
|
|
void setScaleFactor (float scale) override
|
|
{
|
|
currentScaleFactor = scale;
|
|
AudioProcessorEditor::setScaleFactor (scale);
|
|
|
|
const auto posted = MessageManager::callAsync ([ref = SafePointer<HostAudioProcessorEditor> (this), scale]
|
|
{
|
|
if (auto* r = ref.getComponent())
|
|
if (auto* e = r->currentEditorComponent)
|
|
e->setScaleFactor (scale);
|
|
});
|
|
|
|
jassertquiet (posted);
|
|
}
|
|
|
|
private:
|
|
void pluginChanged()
|
|
{
|
|
loader.setVisible (! hostProcessor.isPluginLoaded());
|
|
closeButton.setVisible (hostProcessor.isPluginLoaded());
|
|
|
|
if (hostProcessor.isPluginLoaded())
|
|
{
|
|
auto editorComponent = std::make_unique<PluginEditorComponent> (hostProcessor.createInnerEditor(), [this]
|
|
{
|
|
const auto posted = MessageManager::callAsync ([this] { clearPlugin(); });
|
|
jassertquiet (posted);
|
|
});
|
|
|
|
editorComponent->setScaleFactor (currentScaleFactor);
|
|
currentEditorComponent = editorComponent.get();
|
|
|
|
editor = [&]() -> std::unique_ptr<Component>
|
|
{
|
|
switch (hostProcessor.getEditorStyle())
|
|
{
|
|
case EditorStyle::thisWindow:
|
|
addAndMakeVisible (editorComponent.get());
|
|
setSize (editorComponent->getWidth(), editorComponent->getHeight());
|
|
return std::move (editorComponent);
|
|
|
|
case EditorStyle::newWindow:
|
|
const auto bg = getLookAndFeel().findColour (ResizableWindow::backgroundColourId).darker();
|
|
auto window = std::make_unique<ScaledDocumentWindow> (bg, currentScaleFactor);
|
|
window->setAlwaysOnTop (true);
|
|
window->setContentOwned (editorComponent.release(), true);
|
|
window->centreAroundComponent (this, window->getWidth(), window->getHeight());
|
|
window->setVisible (true);
|
|
return window;
|
|
}
|
|
|
|
jassertfalse;
|
|
return nullptr;
|
|
}();
|
|
}
|
|
else
|
|
{
|
|
editor = nullptr;
|
|
setSize (500, 500);
|
|
}
|
|
}
|
|
|
|
void clearPlugin()
|
|
{
|
|
currentEditorComponent = nullptr;
|
|
editor = nullptr;
|
|
hostProcessor.clearPlugin();
|
|
}
|
|
|
|
static constexpr auto buttonHeight = 30;
|
|
|
|
HostAudioProcessorImpl& hostProcessor;
|
|
PluginLoaderComponent loader;
|
|
std::unique_ptr<Component> editor;
|
|
PluginEditorComponent* currentEditorComponent = nullptr;
|
|
ScopedValueSetter<std::function<void()>> scopedCallback;
|
|
TextButton closeButton { "Close Plugin" };
|
|
float currentScaleFactor = 1.0f;
|
|
};
|
|
|
|
//==============================================================================
|
|
class HostAudioProcessor : public HostAudioProcessorImpl
|
|
{
|
|
public:
|
|
bool hasEditor() const override { return true; }
|
|
AudioProcessorEditor* createEditor() override { return new HostAudioProcessorEditor (*this); }
|
|
};
|