Tutorial: Adding plug-in parameters

Add parameters to your audio plug-in to allow control and automation from your digital audio workstation. Learn how to use the audio parameters for processing audio and create a user interface for them.

Level: Beginner

Platforms: Windows, macOS, Linux

Classes: AudioParameterFloat, AudioParameterBool, GenericAudioProcessorEditor

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 (also known as a Digital Audio Workstation — DAW). See Tutorial: Create a basic Audio/MIDI plugin, Part 1: Setting up for an introduction.

The demo project

The demo project is based on the GainPlugin project in the JUCE/examples/Plugins directory. This plug-in simply changes the gain of an incoming signal using a single parameter.

tutorial_audio_parameter_screenshot1.png
The gain plug-in UI in Logic Pro X

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.

Configuring the parameters

In your processor you should store audio parameter members for each of your parameters. In our case we have only one:

//...
private:
//==============================================================================
//...

The processor should allocate and add the parameters that your plug-in needs in its constructor. In our simple example we have only one parameter to set up:

TutorialProcessor()
{
addParameter (gain = new AudioParameterFloat ("gain", // parameter ID
"Gain", // parameter name
0.0f, // minimum value
1.0f, // maximum value
0.5f)); // default value
//...
Note
The base class (AudioProcessor) takes ownership of the parameter objects, which is why we use raw pointers to store our parameters in the derived processor class. This is safe because you know for certain that the base class will be deconstructed after our derived class. In addition to this, you can also assume that the processor's editor component will be deleted before the processor object.

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.

In addition to this, the AudioParameterFloat class allows you to specify the range of values that the parameter can represent. The AudioParameterFloat class also has an alternative constructor which allows you to use a NormalisableRange<float> object instead. JUCE stores all of the parameter values in the range [0, 1] as this is a limitation of some of the target plug-in APIs. We could rewrite the code shown above as:

addParameter (gain = new AudioParameterFloat ("gain", // parameter ID
"Gain", // parameter name
NormalisableRange<float> (0.0f, 1.0f), // parameter range
0.5f)); // default value

This may seem a little pointless in our example (since the parameter range is already in the range [0, 1]!) but using a NormalisableRange<float> object also allows you to specify a skew-factor. This is especially useful if your plug-in needs to use parameters that represent frequency or time properties, since these are often best represented using a non-linear mapping.

Note
The AudioParameterFloat class also has additional optional lambda functions that can be specified to convert the value to text and vice-versa. This is especially useful if you want to display the parameter's value as a string or allow the user to type in the value.

Performing the gain processing

Once the parameters have been created and added, your plug-in can interact with these parameter objects. In our case we simply retrieve the gain value in the TutorialProcessor::processBlock() function:

void processBlock (AudioSampleBuffer& buffer, MidiBuffer&) override
{
buffer.applyGain (*gain);
}

The AudioSampleBuffer::applyGain() function applies our gain value to all samples across all channels in the buffer.

This illustrates the idiom that you should use when using the audio parameter classes: dereference the pointer to the parameter to obtain the parameter value. In this case, because we are using an AudioParameterFloat, we get a float.

The other AudioParameterXXX classes work in a similar way:

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

Our simple gain plug-in has only one thing to save: the gain value itself. Storing this is as easy as writing the floating point value in a binary format:

void getStateInformation (MemoryBlock& destData) override
{
MemoryOutputStream (destData, true).writeFloat (*gain);
}

The AudioProcessor::getStateInformation() callback is called when plug-in needs to have its state stored. For example, this happens when the user saves their DAW project or saves a preset (in some DAWs). We can put anything we like into the MemoryBlock object that is passed to this function.

The AudioProcessor::setStateInformation() function needs to do the opposite: it should read data from a memory location and restore the state of our plug-in:

void setStateInformation (const void* data, int sizeInBytes) override
{
*gain = MemoryInputStream (data, static_cast<size_t> (sizeInBytes), false).readFloat();
}
Exercise
Try storing the gain parameter as a string rather than in a binary format.

Improving the gain processor

There are some improvements that we can make to this gain processor:

  • Changing gain causes discontinuities in the signal and this can be heard as little clicks if the gain is modulated quickly.
  • Storing the plug-in's state is more convenient using XML.

Smoothing gain changes

Using the AudioSampleBuffer class we can easily perform ramping gain changes over the whole block size of the buffer. In order to do this we need to store the value of the gain parameter from the previous audio callback. First, add a member variable to the TutorialProcessor class [1]:

//...
private:
//==============================================================================
float previousGain; // [1]
//...

Then, ensure that this value is initialised in the TutorialProcessor::preparePlay() function:

void prepareToPlay (double, int) override
{
previousGain = *gain;
}

Finally, modify the TutorialProcessor::processBlock() function to perform the gain ramp:

void processBlock (AudioSampleBuffer& buffer, MidiBuffer&) override
{
auto currentGain = gain->get();
if (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.

Note
The source code for this modified version of the plug-in can be found in the AudioParameterTutorial_02.h file of the demo project.
Exercise
Modify the smoothing algorithm to make it independent of the processing block size.

Using XML to store the processor's state

Storing the plug-in state in a binary format results in using less memory and storage space for your plug-in's state. However, it is often more convient to use a format such as XML or JSON. This makes debugging easier and it also simplifies making the stored state information compatible with future versions of your plug-in. In particular, XML makes it easy to:

  • set parameters not found in the information block to default values
  • include version information in the information block to help handle forwards and backwards compatibility for different versions of your plug-in

To store our gain plug-in's state in XML we can do the following:

void getStateInformation (MemoryBlock& destData) override
{
std::unique_ptr<XmlElement> xml (new XmlElement ("ParamTutorial"));
xml->setAttribute ("gain", (double) *gain);
copyXmlToBinary (*xml, destData);
}

The AudioProcessor::copyXmlToBinary() function is a convenient helper function to convert XML to a binary blob. To retrieve the state we can do the opposite:

void setStateInformation (const void* data, int sizeInBytes) override
{
std::unique_ptr<XmlElement> xmlState (getXmlFromBinary (data, sizeInBytes));
if (xmlState.get() != nullptr)
if (xmlState->hasTagName ("ParamTutorial"))
*gain = xmlState->getDoubleAttribute ("gain", 1.0);
}

Where the AudioProcessor::getXmlFromBinary() function converts binary data—created with AudioProcessor::copyXmlToBinary() function—back to XML.

Importantly, you can see the error checking going on here. If the information block is not XML then the function will do nothing. It also checks for the tag name "ParamTutorial" and only proceeds if this name is found. The gain value will also default to 1.0 if the gain parameter isn't found. Adding version information is as simple as adding another attribute for this purpose. Then more error checking would allow you to handle different versions of the state information.

Note
The source code for this modified version of the plug-in can be found in the AudioParameterTutorial_03.h file of the demo project.

Adding a phase invert parameter

Let's add a phase invert parameter to our gain plug-in!

Adding a boolean parameter

First, add an AudioParameterBool* member to the TutorialProcessor class [2]:

//...
private:
//==============================================================================
AudioParameterBool* invertPhase; // [2]
//...

Then we need to allocate and add the parameter in the TutorialProcessor constructor [3]:

TutorialProcessor()
{
addParameter (gain = new AudioParameterFloat ("gain", "Gain", 0.0f, 1.0f, 0.5f));
addParameter (invertPhase = new AudioParameterBool ("invertPhase", "Invert Phase", false)); // [3]
//...

Of course a boolean parameter doesn't have a specifiable range, only a default value. We'll need to update our TutorialProcessor::getStateInformation() function [4]:

void getStateInformation (MemoryBlock& destData) override
{
std::unique_ptr<XmlElement> xml (new XmlElement ("ParamTutorial"));
xml->setAttribute ("gain", (double) *gain);
xml->setAttribute ("invertPhase", *invertPhase); // [4]
copyXmlToBinary (*xml, destData);
}

And the TutorialProcessor::setStateInformation() function [5]:

void setStateInformation (const void* data, int sizeInBytes) override
{
std::unique_ptr<XmlElement> xmlState (getXmlFromBinary (data, sizeInBytes));
if (xmlState.get() != nullptr)
{
if (xmlState->hasTagName ("ParamTutorial"))
{
*gain = (float) xmlState->getDoubleAttribute ("gain", 1.0);
*invertPhase = xmlState->getBoolAttribute ("invertPhase", false); // [5]
}
}
}

We need to add the audio processing code:

void processBlock (AudioSampleBuffer& buffer, MidiBuffer&) override
{
auto phase = *invertPhase ? -1.0f : 1.0f; // [6]
auto currentGain = *gain * phase; // [7]
if (currentGain == previousGain)
{
buffer.applyGain (currentGain);
}
else
{
buffer.applyGainRamp (0, buffer.getNumSamples(), previousGain, currentGain);
previousGain = currentGain;
}
}

Notice here that:

  • [6]: We choose either +1 or -1 depending on the state of the invertPhase parameter.
  • [7]: We multiply this by the value of the gain parameter.
  • The remainder of the code in this function, including the smoothing technique, is the same.

Finally, the previousGain value needs to be initialised in the TutorialProcessor::prepareToPlay() function:

void prepareToPlay (double, int) override
{
auto phase = *invertPhase ? -1.0f : 1.0f;
previousGain = *gain * phase;
}

Summary

In this tutorial we have learned about using audio parameters within the AudioProcessor class. In particular we have explored:

  • Creating AudioParameterFloat objects to represent our processor's variable parameters.
  • Using the values from AudioParameterFloat objects to control audio processing.
  • Storing and retrieving parameter data in the processor's state information.
  • Using AudioParameterBool objects to represent parameters that are in either an on or off state.

See also