Loading...
Searching...
No Matches
Tutorial: Saving and loading your plug-in state

Automatic management of your plug-in parameters. Storing and accessing parameters becomes a breeze and, in particular, makes building effective user interfaces much easier.

Level: Intermediate

Platforms: Windows , macOS , Linux

Classes: AudioProcessorValueTreeState, ValueTree, XmlElement

Getting started

Download the demo project for this tutorial here: PIP | ZIP . Unzip the project and open the first header file in the Projucer.

If you need help with this step, see Tutorial: Projucer Part 1: Getting started with the Projucer.

You should also know how to build an audio plug-in using JUCE and load this into your preferred audio host (such as a Digital Audio Workstation). See Tutorial: Create a basic Audio/MIDI plugin, Part 1: Setting up for an introduction. Ideally, you should also have read Tutorial: Adding plug-in parameters, as an introduction to audio processor parameters.

The demo project

The demo project is loosely based on the GainPlugin project in the JUCE/examples/Plugins directory. This plugin changes the gain of an incoming signal using a single parameter. In addition to this, it also has a phase invert* parameter to invert the phase of the incoming signal.

The gain processor

Most of the code in the TutorialProcessor class is the same as that generated by the Projucer when you use the Audio Plug-In project template. For simplicity, we have bundled the processor code into a single .h file rather than being split across a .cpp and an .h file. The editor for the processor is in the GenericEditor class.

There are several advantages to using the AudioProcessorValueTreeState class for managing your plug-in's parameters:

  • The ValueTree class inherently provides undo support.
  • ValueTree objects already have support for serialising and deserialising (to XML).
  • ValueTree objects can have listeners attached to them. This means that the AudioProcessorValueTreeState class can almost automatically connect to sliders and buttons to keep the state of the UI and the processor up-to-date in a thread safe manner.

To use an AudioProcessorValueTreeState object, you can store one in your processor class:

private:
//==============================================================================
juce::AudioProcessorValueTreeState parameters;

You may store your AudioProcessorValueTreeState object elsewhere but you must be careful that each AudioProcessorValueTreeState object must be:

  • attached to only one processor; and
  • have the same lifetime as the processor (as they will have dependencies on each other).

Storing the AudioProcessorValueTreeState object in the processor class makes it easier to ensure that you satisfy these requirements.

The AudioProcessorValueTreeState constructor requires a reference to the AudioProcessor subclass that it will be attached to, a pointer to an UndoManager object, an Identifier for the ValueTree and an AudioProcessorValueTreeState::ParameterLayout containing the parameters to manage.

UndoManager* undoManagerToUse,
const juce::Identifier& valueTreeType,
ParameterLayout parameterLayout);
This class contains a ValueTree that is used to manage an AudioProcessor's entire state.
Definition juce_AudioProcessorValueTreeState.h:124
Base class for audio processing classes or plugins.
Definition juce_AudioProcessor.h:60
Manages a list of undo/redo commands.
Definition juce_UndoManager.h:64

In this case, we will use a nullptr value for the UndoManager object as we're not going to implement undo support in this tutorial. The nullptr value indicates that we do not want to use undo support.

Configuring the parameters

The AudioProcessorValueTreeState::ParameterLayout parameter of the AudioProcessorValueTreeState contains the parameters of our plug-in. The AudioProcessorValueTreeState can manage any parameters derived from RangedAudioParameter, and the AudioProcessorValueTreeState::ParameterLayout constructor can take a variable number of RangedAudioParameter subclasses or AudioProcessorParameterGroups containing RangedAudioParameters.

Parameters and groups are passed using std::unique_ptr as the APVTS will take ownership of the parameters and groups.

JUCE's built-in parameter types, the same ones we used in Tutorial: Adding plug-in parameters, are subclasses of RangedAudioParameter, so we can use them here too.

TutorialProcessor()
: parameters (*this, nullptr, juce::Identifier ("APVTSTutorial"),
{
std::make_unique<juce::AudioParameterFloat> ("gain", // parameterID
"Gain", // parameter name
0.0f, // minimum value
1.0f, // maximum value
0.5f), // default value
std::make_unique<juce::AudioParameterBool> ("invertPhase", // parameterID
"Invert Phase", // parameter name
false) // default value
})
{

Adding your parameters to an AudioProcessorValueTreeState automatically adds them to the attached AudioProcessor too.

The parameter ID should be a unique identifier for this parameter. Think of this like a variable name; it can contain alphanumeric characters and underscores, but no spaces. The parameter name is the name that will be displayed on the screen.

Performing the gain processing

To help avoid clicks in the signal, we smooth gain changes and changes in signal phase. To do this, we store the previously calculated gain value in our processor [1] :

private:
//==============================================================================
juce::AudioProcessorValueTreeState parameters;
float previousGain; // [1]
std::atomic<float>* phaseParameter = nullptr;
std::atomic<float>* gainParameter = nullptr;
//==============================================================================
};

We also store pointers to our parameters at the end of our constructor to dereference them later on:

phaseParameter = parameters.getRawParameterValue ("invertPhase");
gainParameter = parameters.getRawParameterValue ("gain");

The changes are initialised in the TutorialProcessor::prepareToPlay() function:

void prepareToPlay (double, int) override
{
auto phase = *phaseParameter < 0.5f ? 1.0f : -1.0f;
previousGain = *gainParameter * phase;
}

Here we calculate the phase inversion factor (+1 or -1) and multiply this by the gain, ready for the first processing callback. You can see that we use the AudioProcessorValueTreeState::getRawParameterValue() function to get a pointer to the float value representing the parameter value. We dereference this to get the actual value. The processing is performed in the TutorialProcessor::processBlock() function:

void processBlock (juce::AudioSampleBuffer& buffer, juce::MidiBuffer&) override
{
auto phase = *phaseParameter < 0.5f ? 1.0f : -1.0f;
auto currentGain = *gainParameter * phase;
if (juce::approximatelyEqual (currentGain, previousGain))
{
buffer.applyGain (currentGain);
}
else
{
buffer.applyGainRamp (0, buffer.getNumSamples(), previousGain, currentGain);
previousGain = currentGain;
}
}

Here you can see that if the value hasn't changed, then we simply apply a constant gain. If the value has changed, then we apply the gain ramp, then update the previousGain value for next time.

Storing and retrieving parameters

In addition to providing routines for processing audio you also need to provide methods for storing and retrieving the entire state of your plug-in into a block of memory. This should include the current values of all of your parameters, but it can also include other state information if needed (for example, if your plug-in deals with files, it might store the file paths).

Using an AudioProcessorValueTreeState object to store your plug-in's state makes this really simple as a ValueTree object can easily be converted to and from XML.

The AudioProcessor::getStateInformation() callback asks your plug-in to store its state into a MemoryBlock object. To do this using XML via the ValueTree object the code is simply:

void getStateInformation (juce::MemoryBlock& destData) override
{
auto state = parameters.copyState();
std::unique_ptr<juce::XmlElement> xml (state.createXml());
copyXmlToBinary (*xml, destData);
}

The XmlElement object created will have a tag name of "APVTSTutorial", which we used to initialise the ValueTree object earlier.

Restoring the state from XML is almost as straightforward:

void setStateInformation (const void* data, int sizeInBytes) override
{
std::unique_ptr<juce::XmlElement> xmlState (getXmlFromBinary (data, sizeInBytes));
if (xmlState.get() != nullptr)
if (xmlState->hasTagName (parameters.state.getType()))
parameters.replaceState (juce::ValueTree::fromXml (*xmlState));
}

Here we include some error checking for safety. We also check that the ValueTree-generated XML is of the correct ValueTree type for our plug-in by inspecting the XML element's tag name.

The gain editor

Take a look at the GenericEditor class in the project. You might notice that the declaration of the GenericEditor class is very simple:

class GenericEditor : public juce::AudioProcessorEditor
{
public:

You might expect that we would need to inherit from the Slider::Listener class and the Button::Listener class in order to respond to slider and button interaction. But this is again one of the benefits of using the AudioProcessorValueTreeState class. Instead we can use the attachment classes within the AudioProcessorValueTreeState class.

Component attachments

In fact, as the names of these classes can become very long, we have included a typedef for each of the attachment classes we need:

typedef juce::AudioProcessorValueTreeState::SliderAttachment SliderAttachment;
typedef juce::AudioProcessorValueTreeState::ButtonAttachment ButtonAttachment;

Our GenericEditor class contains a number of members, including a slider, a toggle button, and some of these attachment objects:

private:
juce::AudioProcessorValueTreeState& valueTreeState;
juce::Label gainLabel;
juce::Slider gainSlider;
std::unique_ptr<SliderAttachment> gainAttachment;
juce::ToggleButton invertButton;
std::unique_ptr<ButtonAttachment> invertAttachment;
};

We also need to refer to the AudioProcessorValueTreeState object so we also keep a reference to that.

The constructor for our GenericEditor class sets up these objects:

GenericEditor (juce::AudioProcessor& parent, juce::AudioProcessorValueTreeState& vts)
valueTreeState (vts)
{
gainLabel.setText ("Gain", juce::dontSendNotification);
addAndMakeVisible (gainLabel);
addAndMakeVisible (gainSlider);
gainAttachment.reset (new SliderAttachment (valueTreeState, "gain", gainSlider));
invertButton.setButtonText ("Invert Phase");
addAndMakeVisible (invertButton);
invertAttachment.reset (new ButtonAttachment (valueTreeState, "invertPhase", invertButton));
setSize (paramSliderWidth + paramLabelWidth, juce::jmax (100, paramControlHeight * 2));
}

This is called by our processor's TutorialProcessor::createEditor() function:

juce::AudioProcessorEditor* createEditor() override { return new GenericEditor (*this, parameters); }

You may notice that we don't even need to set up the slider's value range. This is done automatically by the SliderAttachment class. All we need to do is pass the attachment constructor the AudioProcessorValueTreeState, the parameter ID and the Slider object that it should attach to.

Note
We still retain ownership of the Slider object. You should ensure that the attachment class has the same lifetime as the Slider object.

The ButtonAttachment class still requires us to provide the button name. (And the AudioProcessorValueTreeState::ComboBoxAttachment class, which can attach to a ComboBox object, requires us to populate the ComboBox manually.)

Exercises
  • Change the plug-in to support only a 2-channel main bus and add a channel swap parameter that can optionally swap the left and right channels.
  • Again using a 2-channel only plug-in, add a balance parameter to balance the left and right channel levels.

Adding Parameters Programatically

You can also add parameters (or AudioProcessorParameterGroups) to an AudioProcessorValueTreeState programatically, by calling add on it. An example of how to do this is shown below:

juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout()
{
juce::AudioProcessorValueTreeState::ParameterLayout params;
for (int i = 1; i < 9; ++i)
params.add (std::make_unique<AudioParameterInt> (String (i), String (i), 0, i, 0));
return params;
}
YourAudioProcessor()
: parameters (*this, nullptr, "PARAMETERS", createParameterLayout())
{
//...
The JUCE String class!
Definition juce_String.h:68

Deprecated Methods

Before JUCE version 5.4 the only way to add parameters to an AudioProcessorValueTreeState was to use the now deprecated createAndAddParameter method with many function parameters.

Code that previously looked like

createAndAddParameter (paramID1, paramName1, ...);

can be re-written as

using Parameter = juce::AudioProcessorValueTreeState::Parameter;
createAndAddParameter (std::make_unique<Parameter> (paramID1, paramName1, ...));

but using the new AudioProcessorValueTreeState constructor described in this tutorial is a much better approach:

YourAudioProcessor()
: apvts (*this, nullptr, "PARAMETERS", { std::make_unique<Parameter> (paramID1, paramName1, ...),
std::make_unique<Parameter> (paramID2, paramName2, ...),
... })
A parameter class that maintains backwards compatibility with deprecated AudioProcessorValueTreeState...
Definition juce_AudioProcessorValueTreeState.h:459

Summary

In this tutorial we have introduced the AudioProcessorValueTreeState class and shown how it can help you to:

  • Manage your plug-in parameters.
  • Store and retrieve your plug-in state using XML.
  • Connect your plug-in parameters to buttons and sliders in a threadsafe manner.

See also

linkedin facebook pinterest youtube rss twitter instagram facebook-blank rss-blank linkedin-blank pinterest youtube twitter instagram