In this assignment, we will walk you through incorporating PluginGUIMagic (PGM) into your JUCE project. In the previous , you installed Projucer and built your first JUCE audio-plugin project.
This guide accompanies the lecture video on adding PGM to JUCE.
Key steps to importing PGM into your JUCE PluginProcessor
:
foleys_gui_magic
module to your Projucer projectjuce::AudioProcessor
to foleys::MagicProcessor
juce::AudioProcessorEditor* createEditor()
bool hasEditor()
void getStateInformation (juce::MemoryBlock& destData)
void setStateInformation (const void* data, int sizeInBytes)
AudioProcessorValueTree
to prepare the plugin for parameters
After creating your Projucer audio-plugin project, add the foleys_gui_magic
JUCE module into your project:
The PluginGUIMagic module foleys_gui_magic
depends
on other JUCE modules such as the juce_dsp
module. Include these additional dependencies by clicking on the
"Add missing dependencies" button at the lower left:
Launch your project in your IDE
In PluginProcessor.h
, swap the parent
class of your plugin juce::AudioProcessor
for foleys::MagicProcessor
Reflect this change in your constructor implementation in PluginProcessor.cpp
foleys::MagicProcessor
handles the implementation of
juce::AudioProcessorEditor* createEditor()
bool hasEditor()
void getStateInformation (juce::MemoryBlock& destData)
void setStateInformation (const void* data, int sizeInBytes)
PluginProcessor.h
, and
similarly take out their (empty) implementations in
PluginProcess.cpp
. After building and running your standalone plugin app,
you should see the PGM Editor alongside an empty GUI:
As a last bit of tidying up, go into Projucer and delete the files PluginEditor.h
and
PluginEditor.cpp
PGM uses an XML
file to describe the GUI layout. First, use the PGM Editor to create
a layout that says "Hello World" by dragging
the Label
object into the Editor view hierarchy,
and setting the text to be "Hello World":
Now save your GUI in your project source directory
as Layout.xml
using File > Save XML:
JUCE plugin resources, such as this GUI XML file, binary
images, and other audio/data files, are normally stored inside your plugin executable as
"JUCE Binary Data". This solves a lot of platform-dependency problems.
Projucer
handles this so that your IDE sees it all
as BinaryData
source code (check
out BinaryData.cpp
in your Projucer-generated
project). For this to work, you must include the XML layout
file in your Projucer project, and regenerate your project from
Projucer every time you change the XML (or edit the
generated BinaryData
source yourself, which is
sometimes faster). In the "+" menu shown in the screenshot
below, select "Add Existing Files..." and then choose your
saved Layout.xml
file:
Now that you have your GUI layout stored inside the plugin
executable, you can initialize the GUI at plugin startup.
Since PluginProcessor
inherits from
MagicProcessor
, it has access to the
foleys::MagicProcessorState magicState
,
which can load your GUI XML.
In your PluginProcessor.cpp
constructor:
magicState.setGuiValueTree(BinaryData::Layout_xml, BinaryData::Layout_xmlSize);
This of course assumes you named your GUI XML
file Layout.xml
.
The next time you instantiate your plugin, your GUI should be automatically initialized!
Controllability is key to plugins. To do this we need to initialize an AudioProcessorValueTreeState
object with our
AudioProcessorParameter
descriptions. The
JUCE tutorial
on adding plug-in parameters might be helpful to read at
this point. There is also
a nice
video on the topic.
To be notified of parameter changes, JUCE and PGM support
the listener pattern, more generally called the
Observer
Pattern. In this software design pattern, the GUI editor and
anybody else can register to be a listener for changes in
any
AudioProcessorParameter
. When a parameter is changed,
such as by received MIDI or slider motion, all parameter listeners are
notified with the new value of the parameter.
Add a second private
inheritance from juce::AudioProcessorValueTreeState::Listener
.
Your class definition in PluginProcessor.h
should now look like
class PGMAudioProcessor : public foleys::MagicProcessor,
private juce::AudioProcessorValueTreeState::Listener
This listener class requires that you implement a parameter-change "callback" function:
void parameterChanged(const juce::String
&parameterID, float newValue) override;
Create
a treeState
private member to hold the parameters in
a nicely general ValueTree
structure:
juce::AudioProcessorValueTreeState treeState;
You also need a function that creates all the plugin parameters (and their groups) at startup:
juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
Initialize treeState
in your constructor after initializing your parent MagicProcessor
.
treeState(*this, nullptr, JucePlugin_Name,
createParameterLayout())
This initializes
your treeState
with the parameter-tree returned
by createParameterLayout
.
The treeState
is now uniquely tied to the MagicProcessor
, and must have the same lifetime.
Only the parameter values can be altered after this initialization; the ParameterLayout
is now fixed.
Note that this "ParameterLayout" has nothing to do with your GUI layout. It is
an optionally hierarchical tree-structure containing all of
your AudioProcessorParameters
that PGM will use to
generate selection menus when assigning a parameter to a GUI
widget such as a slider or button. (In the AudioProcessor, this tree gets
flattened into a linear list, presumably because some plugin hosts
can only deal with that.) It is also more than just a "layout". It is all
your parameters, together with their default/min/max/step values,
as well as any hierarchical grouping info.
See the
AudioProcessorValueTreeState
reference doc for more.
Your final constructor should look something like this:
You of course need to implement your createParameterLayout
function which returns a
juce::AudioProcessorValueTreeState::ParameterLayout
. For now, this can be empty like so:
juce::AudioProcessorValueTreeState::ParameterLayout PGMAudioProcessor::createParameterLayout()
{
juce::AudioProcessorValueTreeState::ParameterLayout layout;
return layout;
}
Finally, create an empty implementation of parameterChanged
in PluginProcessor.cpp
. Now
you are all ready to tackle Assignment 1!
An easy way to add parameters to your ParameterLayout is to copy
and modify code from some example, such as the PGM
SignalGenerator example
in
~/PGM/examples/SignalGenerator/Source/.
Search for "parameter" in that directory to find example
implementations of createParameterLayout()
and
parameterChanged()
, along with the needed
parameters themselves, such as mainFreq
.
If you find the PGM examples hard to read, try watching the lecture videos introducing some of the PGM examples in detail.