Skip to content

Commit a3e4566

Browse files
committed
Added the patch loader plugin project
1 parent d61b78b commit a3e4566

File tree

4 files changed

+578
-0
lines changed

4 files changed

+578
-0
lines changed
Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
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

tools/plugin/Patch_Plugin_README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#### Patch Loader Plugin
2+
3+
This folder contains a small JUCE project that uses the `soul::patch::SOULPatchLoaderPlugin` utility class to build a VST/AU native plugin which can be used natively in hosts, and which can dynamically load (and hot re-load) patches.
4+
5+
The resulting plugin lets you drag-and-drop a `.soulpatch` file onto its editor window, which will cause it to try to build and run that SOUL patch. Once loaded, any changes to the source code of that patch should cause the plugin to recompile and reload the patch while running.
6+
7+
#### How to build and use it
8+
9+
You'll need to have JUCE installed. To generate some build projects, just load the SOUL_Plugin.jucer into the Projucer, and export. If your JUCE global paths are set up correctly, it should "just work", and you'll get a project that will build various types of VST, AU, and a standalone test-harness app. You can customise the project in the Projucer if you need to tweak any of its settings.
10+
11+
###### Installing the patch DLL
12+
13+
When you first try to run or load any of the binaries that you build, you'll probably see a window saying that it can't find the SOULPatch DLL.
14+
15+
To get this DLL, you should get the latest version from the [Releases](https://github.com/soul-lang/SOUL/releases/latest) folder.
16+
17+
The project contains some logic for searching for this DLL: it'll look:
18+
- In the folder that contains the plugin or app
19+
- (On Mac) In the resources folder inside the plugin or app bundle
20+
- In a folder called "SOUL" in the user's app data folder (e.g. on Mac this will be `~/Library/Application Support/SOUL`, on Windows, it'll look for `AppData/SOUL`, etc.
21+
22+
These search locations are easy to see and modify in the plugin code if you want to customise it.
23+
24+
#### Caveat!
25+
26+
Unfortunately, the VST/AU/AAX formats were never really designed to allow plugins to dynamically update their name, category, number of buses, number of parameters, or any of the other characteristics which normally remain fixed.
27+
28+
So it's inevitable that there will be all kinds of bizarre edge-case bugs if you load this plugin into a host and start hot-loading patches that change its topology in these ways. Very few of the common DAWs and hosts will have been stress-tested to deal robustly with that kind of behaviour.
29+
30+
Also, the number of combinations of host/format/platform/version/patch is too vast for us to possibly attempt to test or document even a fraction of the possible interactions, so we're hoping that enterprising developers will help us by debugging any problems they hit, especially if it seems like something where there might be a workaround. It'll be impossible to fix all the issues, as many would probably require fixes in the hosts, but we might be able to get quite a decent amount of coverage if enough people play with it.
31+
32+
Although the core JIT engine is inside a DLL, all the code that relates to the plugin APIs is completely open-source, so there should be nothing preventing anyone with the right skill-set debugging any host-specific problems.

0 commit comments

Comments
 (0)