|
| 1 | +/* |
| 2 | + _____ _____ _____ __ |
| 3 | + | __| | | | | |
| 4 | + |__ | | | | | |__ |
| 5 | + |_____|_____|_____|_____| |
| 6 | +
|
| 7 | + Copyright (c) 2018 - ROLI Ltd. |
| 8 | +*/ |
| 9 | + |
| 10 | +#pragma once |
| 11 | + |
| 12 | +#ifndef JUCE_AUDIO_PROCESSORS_H_INCLUDED |
| 13 | + #error "this header is designed to be included in JUCE projects that contain the juce_audio_processors module" |
| 14 | +#endif |
| 15 | + |
| 16 | +#include "../../soul_patch.h" |
| 17 | +#include "soul_patch_AudioProcessor.h" |
| 18 | +#include "soul_patch_Utilities.h" |
| 19 | +#include "soul_patch_CompilerCacheFolder.h" |
| 20 | + |
| 21 | +namespace soul |
| 22 | +{ |
| 23 | +namespace patch |
| 24 | +{ |
| 25 | + |
| 26 | +//============================================================================== |
| 27 | +/** |
| 28 | + This is a juce::AudioProcessor which can told to dynamically load and run different |
| 29 | + patches. The purpose is that you can build a native (VST/AU/etc) plugin with this |
| 30 | + class which can then load (and hot-reload) any SOUL patch at runtime. |
| 31 | +*/ |
| 32 | +class SOULPatchLoaderPlugin : public juce::AudioProcessor |
| 33 | +{ |
| 34 | +public: |
| 35 | + SOULPatchLoaderPlugin() = default; |
| 36 | + |
| 37 | + ~SOULPatchLoaderPlugin() override |
| 38 | + { |
| 39 | + plugin.reset(); |
| 40 | + patchInstance = nullptr; |
| 41 | + } |
| 42 | + |
| 43 | + /** To allow this utility class to be used with either the patch DLL or a static build, |
| 44 | + this virtual method abstracts away the loading of a patch. |
| 45 | + */ |
| 46 | + virtual soul::patch::PatchInstance::Ptr createPatchInstance (const std::string& url) = 0; |
| 47 | + |
| 48 | + /** This allows a sub-class to provide an error message to be shown in the editor if |
| 49 | + it needs to report a problem. |
| 50 | + */ |
| 51 | + virtual std::string getErrorMessage() = 0; |
| 52 | + |
| 53 | + //============================================================================== |
| 54 | + void prepareToPlay (double sampleRate, int samplesPerBlock) override |
| 55 | + { |
| 56 | + if (plugin != nullptr) |
| 57 | + plugin->prepareToPlay (sampleRate, samplesPerBlock); |
| 58 | + } |
| 59 | + |
| 60 | + void releaseResources() override |
| 61 | + { |
| 62 | + if (plugin != nullptr) |
| 63 | + plugin->releaseResources(); |
| 64 | + } |
| 65 | + |
| 66 | + bool isBusesLayoutSupported (const BusesLayout& layouts) const override |
| 67 | + { |
| 68 | + return plugin == nullptr || plugin->isBusesLayoutSupported (layouts); |
| 69 | + } |
| 70 | + |
| 71 | + void processBlock (juce::AudioBuffer<float>& audio, juce::MidiBuffer& midi) override |
| 72 | + { |
| 73 | + if (plugin != nullptr && ! isSuspended()) |
| 74 | + return plugin->processBlock (audio, midi); |
| 75 | + |
| 76 | + audio.clear(); |
| 77 | + midi.clear(); |
| 78 | + } |
| 79 | + |
| 80 | + //============================================================================== |
| 81 | + const juce::String getName() const override { return "SOUL Patch Loader"; } |
| 82 | + |
| 83 | + juce::AudioProcessorEditor* createEditor() override { return new Editor (*this); } |
| 84 | + bool hasEditor() const override { return true; } |
| 85 | + |
| 86 | + bool acceptsMidi() const override { return true; } |
| 87 | + bool producesMidi() const override { return false; } |
| 88 | + bool supportsMPE() const override { return true; } |
| 89 | + bool isMidiEffect() const override { return false; } |
| 90 | + double getTailLengthSeconds() const override { return plugin != nullptr ? plugin->getTailLengthSeconds() : 0.0; } |
| 91 | + |
| 92 | + //============================================================================== |
| 93 | + int getNumPrograms() override { return 1; } |
| 94 | + int getCurrentProgram() override { return 0; } |
| 95 | + void setCurrentProgram (int) override {} |
| 96 | + const juce::String getProgramName (int) override { return {}; } |
| 97 | + void changeProgramName (int, const juce::String&) override {} |
| 98 | + |
| 99 | + //============================================================================== |
| 100 | + void getStateInformation (juce::MemoryBlock& data) override |
| 101 | + { |
| 102 | + if (plugin != nullptr) |
| 103 | + { |
| 104 | + state.removeAllChildren (nullptr); |
| 105 | + state.addChild (plugin->getUpdatedState(), 0, nullptr); |
| 106 | + } |
| 107 | + |
| 108 | + juce::MemoryOutputStream out (data, false); |
| 109 | + state.writeToStream (out); |
| 110 | + } |
| 111 | + |
| 112 | + void setStateInformation (const void* data, int size) override |
| 113 | + { |
| 114 | + auto s = juce::ValueTree::readFromData (data, (size_t) size); |
| 115 | + |
| 116 | + if (s.hasType (ids.SOULPatchPlugin)) |
| 117 | + { |
| 118 | + state = std::move (s); |
| 119 | + updatePatchState(); |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + void updatePatchState() |
| 124 | + { |
| 125 | + auto stateID = state.getProperty (ids.patchID).toString().toStdString(); |
| 126 | + auto stateURL = state.getProperty (ids.patchURL).toString().toStdString(); |
| 127 | + |
| 128 | + if (patchInstance != nullptr) |
| 129 | + { |
| 130 | + std::string loadedID, loadedURL; |
| 131 | + |
| 132 | + if (auto desc = soul::patch::Description::Ptr (patchInstance->getDescription())) |
| 133 | + { |
| 134 | + loadedID = desc->UID; |
| 135 | + loadedURL = desc->URL; |
| 136 | + } |
| 137 | + |
| 138 | + if (stateID != loadedID || stateURL != loadedURL) |
| 139 | + { |
| 140 | + replaceCurrentPlugin ({}); |
| 141 | + patchInstance = nullptr; |
| 142 | + } |
| 143 | + } |
| 144 | + |
| 145 | + if (patchInstance == nullptr) |
| 146 | + patchInstance = createPatchInstance (stateURL); |
| 147 | + |
| 148 | + if (patchInstance != nullptr) |
| 149 | + { |
| 150 | + if (auto desc = soul::patch::Description::Ptr (patchInstance->getDescription())) |
| 151 | + { |
| 152 | + if (std::string_view (desc->UID).empty()) |
| 153 | + { |
| 154 | + replaceCurrentPlugin ({}); |
| 155 | + } |
| 156 | + else |
| 157 | + { |
| 158 | + state.setProperty (ids.patchID, desc->UID, nullptr); |
| 159 | + |
| 160 | + if (plugin == nullptr) |
| 161 | + { |
| 162 | + auto newPlugin = std::make_unique<soul::patch::SOULPatchAudioProcessor> (patchInstance, getCompilerCache()); |
| 163 | + newPlugin->askHostToReinitialise = [this] { this->childChanged(); }; |
| 164 | + |
| 165 | + if (state.getNumChildren() != 0) |
| 166 | + newPlugin->applyNewState (state.getChild (0)); |
| 167 | + |
| 168 | + newPlugin->setBusesLayout (getBusesLayout()); |
| 169 | + newPlugin->prepareToPlay (getSampleRate(), getBlockSize()); |
| 170 | + replaceCurrentPlugin (std::move (newPlugin)); |
| 171 | + } |
| 172 | + else |
| 173 | + { |
| 174 | + if (state.getNumChildren() != 0) |
| 175 | + plugin->applyNewState (state.getChild (0)); |
| 176 | + } |
| 177 | + } |
| 178 | + } |
| 179 | + } |
| 180 | + } |
| 181 | + |
| 182 | + void setPatchURL (const std::string& newURL) |
| 183 | + { |
| 184 | + if (newURL != state.getProperty (ids.patchURL).toString().toStdString()) |
| 185 | + { |
| 186 | + state = juce::ValueTree (ids.SOULPatchPlugin); |
| 187 | + state.setProperty (ids.patchURL, newURL.c_str(), nullptr); |
| 188 | + updatePatchState(); |
| 189 | + } |
| 190 | + } |
| 191 | + |
| 192 | + void childChanged() |
| 193 | + { |
| 194 | + suspendProcessing (true); |
| 195 | + |
| 196 | + if (plugin != nullptr) |
| 197 | + { |
| 198 | + plugin->setBusesLayout (getBusesLayout()); |
| 199 | + plugin->reinitialise(); |
| 200 | + plugin->prepareToPlay (getSampleRate(), getBlockSize()); |
| 201 | + } |
| 202 | + |
| 203 | + updateHostDisplay(); |
| 204 | + suspendProcessing (false); |
| 205 | + |
| 206 | + if (auto ed = dynamic_cast<Editor*> (getActiveEditor())) |
| 207 | + ed->refreshContent(); |
| 208 | + } |
| 209 | + |
| 210 | + void replaceCurrentPlugin (std::unique_ptr<soul::patch::SOULPatchAudioProcessor> newPlugin) |
| 211 | + { |
| 212 | + if (newPlugin.get() != plugin.get()) |
| 213 | + { |
| 214 | + if (auto ed = dynamic_cast<Editor*> (getActiveEditor())) |
| 215 | + ed->clearContent(); |
| 216 | + |
| 217 | + suspendProcessing (true); |
| 218 | + std::swap (plugin, newPlugin); |
| 219 | + suspendProcessing (false); |
| 220 | + |
| 221 | + if (auto ed = dynamic_cast<Editor*> (getActiveEditor())) |
| 222 | + ed->refreshContent(); |
| 223 | + } |
| 224 | + } |
| 225 | + |
| 226 | + //============================================================================== |
| 227 | + struct Editor : public juce::AudioProcessorEditor, |
| 228 | + public juce::FileDragAndDropTarget |
| 229 | + { |
| 230 | + Editor (SOULPatchLoaderPlugin& p) : juce::AudioProcessorEditor (p), owner (p) |
| 231 | + { |
| 232 | + setLookAndFeel (&lookAndFeel); |
| 233 | + refreshContent(); |
| 234 | + juce::Font::setDefaultMinimumHorizontalScaleFactor (1.0f); |
| 235 | + } |
| 236 | + |
| 237 | + ~Editor() override |
| 238 | + { |
| 239 | + owner.editorBeingDeleted (this); |
| 240 | + setLookAndFeel (nullptr); |
| 241 | + } |
| 242 | + |
| 243 | + void clearContent() |
| 244 | + { |
| 245 | + setDragOver (false); |
| 246 | + pluginEditor.reset(); |
| 247 | + setSize (400, 300); |
| 248 | + repaint(); |
| 249 | + } |
| 250 | + |
| 251 | + void refreshContent() |
| 252 | + { |
| 253 | + clearContent(); |
| 254 | + |
| 255 | + if (owner.plugin != nullptr) |
| 256 | + pluginEditor.reset (owner.plugin->createEditor()); |
| 257 | + |
| 258 | + if (pluginEditor != nullptr) |
| 259 | + { |
| 260 | + addAndMakeVisible (pluginEditor.get()); |
| 261 | + childBoundsChanged (nullptr); |
| 262 | + } |
| 263 | + } |
| 264 | + |
| 265 | + void childBoundsChanged (Component*) override |
| 266 | + { |
| 267 | + if (pluginEditor != nullptr) |
| 268 | + setSize (pluginEditor->getWidth(), |
| 269 | + pluginEditor->getHeight()); |
| 270 | + } |
| 271 | + |
| 272 | + void paint (juce::Graphics& g) override |
| 273 | + { |
| 274 | + auto backgroundColour = getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId); |
| 275 | + g.fillAll (backgroundColour); |
| 276 | + |
| 277 | + if (pluginEditor == nullptr) |
| 278 | + { |
| 279 | + auto message = owner.getErrorMessage(); |
| 280 | + |
| 281 | + if (message.empty()) |
| 282 | + message = "Drag-and-drop a .soulpatch file here to load it"; |
| 283 | + |
| 284 | + g.setColour (backgroundColour.contrasting()); |
| 285 | + g.setFont (juce::Font (19.0f, juce::Font::bold)); |
| 286 | + g.drawFittedText (message, getLocalBounds().reduced (20), juce::Justification::centred, 5); |
| 287 | + } |
| 288 | + } |
| 289 | + |
| 290 | + void paintOverChildren (juce::Graphics& g) override |
| 291 | + { |
| 292 | + if (isDragOver) |
| 293 | + g.fillAll (juce::Colours::lightgreen.withAlpha (0.3f)); |
| 294 | + } |
| 295 | + |
| 296 | + bool isInterestedInFileDrag (const juce::StringArray& files) override { return files.size() == 1 && files[0].endsWith (soul::patch::getManifestSuffix()); } |
| 297 | + void fileDragEnter (const juce::StringArray&, int, int) override { setDragOver (true); } |
| 298 | + void fileDragExit (const juce::StringArray&) override { setDragOver (false); } |
| 299 | + |
| 300 | + void filesDropped (const juce::StringArray& files, int, int) override |
| 301 | + { |
| 302 | + setDragOver (false); |
| 303 | + |
| 304 | + if (files.size() == 1) |
| 305 | + owner.setPatchURL (files[0].toStdString()); |
| 306 | + } |
| 307 | + |
| 308 | + void setDragOver (bool b) |
| 309 | + { |
| 310 | + if (isDragOver != b) |
| 311 | + { |
| 312 | + isDragOver = b; |
| 313 | + repaint(); |
| 314 | + } |
| 315 | + } |
| 316 | + |
| 317 | + SOULPatchLoaderPlugin& owner; |
| 318 | + std::unique_ptr<AudioProcessorEditor> pluginEditor; |
| 319 | + juce::LookAndFeel_V4 lookAndFeel; |
| 320 | + bool isDragOver = false; |
| 321 | + }; |
| 322 | + |
| 323 | +private: |
| 324 | + //============================================================================== |
| 325 | + soul::patch::PatchInstance::Ptr patchInstance; |
| 326 | + std::unique_ptr<soul::patch::SOULPatchAudioProcessor> plugin; |
| 327 | + juce::ValueTree state; |
| 328 | + soul::patch::CompilerCache::Ptr compilerCache; |
| 329 | + |
| 330 | + struct IDs |
| 331 | + { |
| 332 | + const juce::Identifier SOULPatchPlugin { "SOULPatchPlugin" }, |
| 333 | + patchURL { "patchURL" }, |
| 334 | + patchID { "patchID" }; |
| 335 | + }; |
| 336 | + |
| 337 | + IDs ids; |
| 338 | + |
| 339 | + soul::patch::CompilerCache::Ptr getCompilerCache() |
| 340 | + { |
| 341 | + constexpr uint32_t maxNumCacheFiles = 200; |
| 342 | + |
| 343 | + if (compilerCache == nullptr) |
| 344 | + { |
| 345 | + #if JUCE_MAC |
| 346 | + auto tempFolder = juce::File ("~/Library/Caches"); |
| 347 | + #else |
| 348 | + auto tempFolder = juce::File::getSpecialLocation (juce::File::SpecialLocationType::tempDirectory); |
| 349 | + #endif |
| 350 | + |
| 351 | + auto cacheFolder = tempFolder.getChildFile ("dev.soul.SOULPlugin").getChildFile ("Cache"); |
| 352 | + |
| 353 | + if (cacheFolder.createDirectory()) |
| 354 | + compilerCache = soul::patch::CompilerCache::Ptr (new soul::patch::CompilerCacheFolder (cacheFolder, maxNumCacheFiles)); |
| 355 | + } |
| 356 | + |
| 357 | + return compilerCache; |
| 358 | + } |
| 359 | + |
| 360 | + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SOULPatchLoaderPlugin) |
| 361 | +}; |
| 362 | + |
| 363 | + |
| 364 | +} // namespace patch |
| 365 | +} // namespace soul |
0 commit comments