/* ============================================================================== 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&, 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); } };