Assignment PGM: Hello PGM

Due: Start of Week 3
Deliverables: None

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).

Using PGM in your JUCE Project

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.

  1. Add the foleys_gui_magic module to your Projucer project
  2. Change juce::AudioProcessor to foleys::MagicProcessor in your PluginProcessor
  3. Also in your 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)
  4. Optional for now: Declare a juce::AudioProcessorValueTree that will hold your plugin parameters

1. Add Plugin GUI Magic (PGM)

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

2. (I got the) MagicProcessor (in me)

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

3. Remove the editor function overrides

foleys::MagicProcessor handles the implementation of

Comment-out or delete these override declarations in 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.

Saving your layout

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!

Audio Processor Value Tree State (say that five times fast)

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.hshould 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.