diff --git a/examples/Plugins/HostPluginDemo.h b/examples/Plugins/HostPluginDemo.h new file mode 100644 index 0000000000..f0078715cd --- /dev/null +++ b/examples/Plugins/HostPluginDemo.h @@ -0,0 +1,587 @@ +/* + ============================================================================== + + 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, vs2019, linux_make + + moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=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&, MidiBuffer&) override + { + jassert (! isUsingDoublePrecision()); + } + + void processBlock (AudioBuffer&, 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 (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 (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 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 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 pluginChanged; + +private: + CriticalSection innerMutex; + std::unique_ptr 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 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 + 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)] (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 + PluginEditorComponent (std::unique_ptr editorIn, Callback&& onClose) + : editor (std::move (editorIn)) + { + addAndMakeVisible (editor.get()); + addAndMakeVisible (closeButton); + + childBoundsChanged (editor.get()); + + closeButton.onClick = std::forward (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(); + + setSize (size.getWidth(), margin + buttonHeight + size.getHeight()); + } + +private: + static constexpr auto buttonHeight = 40; + + std::unique_ptr 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(); + + setSize (size.getWidth(), size.getHeight()); + } + + void setScaleFactor (float scale) override + { + currentScaleFactor = scale; + AudioProcessorEditor::setScaleFactor (scale); + + const auto posted = MessageManager::callAsync ([ref = SafePointer (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 (hostProcessor.createInnerEditor(), [this] + { + const auto posted = MessageManager::callAsync ([this] { clearPlugin(); }); + jassertquiet (posted); + }); + + editorComponent->setScaleFactor (currentScaleFactor); + currentEditorComponent = editorComponent.get(); + + editor = [&]() -> std::unique_ptr + { + 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 (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 editor; + PluginEditorComponent* currentEditorComponent = nullptr; + ScopedValueSetter> 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); } +};