This assignment walks you through incorporating Plugin GUI
Magic (PGM) into your JUCE project.
For the
previous
assignment, you installed Projucer and built your first JUCE audio-plugin project.
In the lecture
for this week, we cloned the PGM repo and looked it over.
We assume you have a symbolic link ~/PGM
in your home directory that points to the directory
foleys_gui_magic
which contains the subdirectory
modules/foleys_gui_magic
(the actual PGM module).
Following are the main steps for integrating PGM into your JUCE projects.
This
video covers the main steps in a super easy-to-remember way
(simply change your parent-class name
from juce::AudioProcessor
to foleys::MagicProcessor
in
your PluginProcessor
and try to compile),
leveraging debugging support. It then illustrates some simple
GUI rapid-prototyping with PGM.
foleys_gui_magic
module to your Projucer projectjuce::AudioProcessor
to foleys::MagicProcessor
in your PluginProcessor
PluginProcessor
, remove the function overrides now taken over by PGM:
juce::AudioProcessorEditor* createEditor()
bool hasEditor()
void getStateInformation (juce::MemoryBlock& destData)
void setStateInformation (const void* data, int sizeInBytes)
juce::AudioProcessorValueTree
that will hold your plugin parameters
After creating your Projucer audio-plugin project, add the foleys_gui_magic
JUCE module into your project:
The PGM 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:
Export your IDE project file from Projucer and open it in your IDE
In PluginProcessor.h
, change the parent
class of your plugin juce::AudioProcessor
to foleys::MagicProcessor
Make this change to your constructor's initialization as well
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 comment-out or
remove 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
Your plugin editor is now magic.
PGM uses an XML
file to describe the GUI layout. First, use the PGM Editor (that came up when you launched your standalone plugin)
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 in the PGM Editor:
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 YouTube
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 or longer.
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 contains 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
.