| @@ -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<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); } | |||
| }; | |||