Tutorial: The AudioParameter classes

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, Mac OS X

Classes: AudioParameterFloat, AudioParameterBool

Getting started

Download the demo project for this tutorial here: tutorial_audio_parameter.zip. Unzip the project and open it in your IDE.

If you need help with this step, see Tutorial: 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/PlugInSamples 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 GainProcessor.cpp file 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 .cpp, file rather than being split across a .cpp and an .h file. The editor for the processor is in the GainEditor.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, // mininum 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.

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.

The generic editor

This tutorial makes use of the GenericEditor class, which is borrowed from the PlugInSamples projects. The GenericEditor class automatically creates a slider for each of the parameters in the plug-in's processor that is an AudioParameterFloat type.

Note
If you really need a simple interface that contains only sliders for your audio plug-in, then you should use the JUCE class GenericAudioProcessorEditor instead.

Iterating over the processor's parameters

To to this, the GenericEditor class iterates over all of the parameters in the processor:

const OwnedArray<AudioProcessorParameter>& params = parent.getParameters();
for (int i = 0; i < params.size(); ++i)
{
if (const AudioParameterFloat* param = dynamic_cast<AudioParameterFloat*> (params[i]))
{
//... create a slider and label for this parameter
}
}

Within this for() loop the GenericEditor class can access the name and range of each parameter in order to configure the corresponding slider:

//...
Slider* aSlider;
paramSliders.add (aSlider = new Slider (param->name));
aSlider->setRange (param->range.start, param->range.end);
aSlider->setValue (*param);
aSlider->addListener (this);
addAndMakeVisible (aSlider);
Label* aLabel;
paramLabels.add (aLabel = new Label (param->name, param->name));
addAndMakeVisible (aLabel);
//...

Updating sliders from the processor

As you can see in the initialisation code, the sliders are initialised to the current values of the parameters. There are various strategies for updating the UI which depends on the complexity of your plug-in. For simple plug-ins using a Timer is a suitable approach. In this example we update the UI every 100ms iterating over the parameters again and updating the slider values:

void timerCallback() override
{
const OwnedArray<AudioProcessorParameter>& params = getAudioProcessor()->getParameters();
for (int i = 0; i < params.size(); ++i)
{
if (const AudioParameterFloat* param = dynamic_cast<AudioParameterFloat*> (params[i]))
{
if (i < paramSliders.size())
paramSliders[i]->setValue (*param);
}
}
}
Warning
You must not try to update the UI from your processor code within the TutorialProcessor::processBlock() function as this will be running on the audio thread. The Timer callbacks execute on the message thread, so it is safe to update the UI from this callback.

Updating parameters via the sliders

Conversely, the parameters can be updated directly from Slider::Listener callbacks (see Tutorial: Slider values). Our GenericEditor::sliderValueChanged() callback is very simple:

void sliderValueChanged (Slider* slider) override
{
if (AudioParameterFloat* param = getParameterForSlider (slider))
*param = (float) slider->getValue();
}

Here we use a little helper function to retrieve the appropriate parameter for the given slider:

AudioParameterFloat* getParameterForSlider (Slider* slider)
{
const OwnedArray<AudioProcessorParameter>& params = getAudioProcessor()->getParameters();
return dynamic_cast<AudioParameterFloat*> (params[paramSliders.indexOf (slider)]);
}

This will return a nullptr value for parameters that are either out of range, or are not AudioParameterFloat types.

In order for some hosts to record automation correctly, we also need to tell the parameters that we are performing a parameter change gesture. This gesture begins as the user clicks the mouse on the slider, and ends when the mouse button is released. To achieve this we need to implement the two optional Slider::Listener callbacks, Slider::Listener::sliderDragStarted() and Slider::Listener::sliderDragEnded():

void sliderDragStarted (Slider* slider) override
{
if (AudioParameterFloat* param = getParameterForSlider (slider))
param->beginChangeGesture();
}
void sliderDragEnded (Slider* slider) override
{
if (AudioParameterFloat* param = getParameterForSlider (slider))
param->endChangeGesture();
}
Exercise
Open the plug-in into your DAW to record and playback some automation of this plug-in parameter.

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
{
const float currentGain = *gain;
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 GainProcessor_02.cpp file in the Source directory 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
{
ScopedPointer<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
{
ScopedPointer<XmlElement> xmlState (getXmlFromBinary (data, sizeInBytes));
if (xmlState != 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 GainProcessor_03.cpp file in the Source directory 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
{
ScopedPointer<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
{
ScopedPointer<XmlElement> xmlState (getXmlFromBinary (data, sizeInBytes));
if (xmlState != 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
{
const float phase = *invertPhase ? -1.0f : 1.0f; // [6]
const float 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
{
const float phase = *invertPhase ? -1.0f : 1.0f;
previousGain = *gain * phase;
}

Updating the editor to show boolean parameters

We can't use the plug-in yet, since our editor is only configured to display AudioParameterFloat parameters. Let's modify the GenericEditor class to handle AudioParameterBool parameters by displaying a ToggleButton component in these cases.

First, add two array members to the class:

//...
Label noParameterLabel;
OwnedArray<Slider> paramSliders;
OwnedArray<Label> paramLabels;
OwnedArray<Button> paramToggles; // [8]
Array<Component*> controls; // [9]
};
  • [8]: This array will hold (and own) the ToggleButton objects.
  • [9]: This array will hold pointers to the Slider or ToggleButton objects in the same order as the parameters.

Then in the constructor we can detect whether one of the parameters is an AudioParameterBool. Add code from the line marked [10]:

//...
for (int i = 0; i < params.size(); ++i)
{
if (const AudioParameterFloat* param = dynamic_cast<AudioParameterFloat*> (params[i]))
{
Slider* aSlider;
paramSliders.add (aSlider = new Slider (param->name));
aSlider->setRange (param->range.start, param->range.end);
aSlider->setValue (*param);
aSlider->addListener (this);
addAndMakeVisible (aSlider);
Label* aLabel;
paramLabels.add (aLabel = new Label (param->name, param->name));
addAndMakeVisible (aLabel);
controls.add (aSlider); // [11]
}
else if (const AudioParameterBool* param = dynamic_cast<AudioParameterBool*> (params[i])) // [10]
{
ToggleButton* aButton;
paramToggles.add (aButton = new ToggleButton (param->name));
aButton->addListener (this);
addAndMakeVisible (aButton);
controls.add (aButton);
}
}
//...

Notice that we also need to add the sliders to our controls array [11].

The GenericEditor::resized() function needs to be updated, now iterating over the controls array:

void resized() override
{
Rectangle<int> r = getLocalBounds();
noParameterLabel.setBounds (r);
for (int i = 0; i < controls.size(); ++i)
{
Rectangle<int> paramBounds = r.removeFromTop (kParamControlHeight);
if (Slider* aSlider = dynamic_cast<Slider*> (controls[i]))
{
Rectangle<int> labelBounds = paramBounds.removeFromLeft (kParamLabelWidth);
const int sliderIndex = paramSliders.indexOf (aSlider);
paramLabels[sliderIndex]->setBounds (labelBounds);
aSlider->setBounds (paramBounds);
}
else if (ToggleButton* aButton = dynamic_cast<ToggleButton*> (controls[i]))
{
aButton->setBounds (paramBounds);
}
}
}

To respond to button clicks we need to make the editor a Button::Listener [11]:

class GenericEditor : public AudioProcessorEditor,
public Button::Listener, // [11]
private Timer
//...

Implement a helper function to retrieve an AudioParameterBool object for a given button:

AudioParameterBool* getParameterForButton (Button* button)
{
const OwnedArray<AudioProcessorParameter>& params = getAudioProcessor()->getParameters();
return dynamic_cast<AudioParameterBool*> (params[controls.indexOf (button)]);
}

And implement the Button::Listener::buttonClicked() callback registering the gesture all in one go:

void buttonClicked (Button* button) override
{
if (AudioParameterBool* param = getParameterForButton (button))
{
param->beginChangeGesture();
*param = button->getToggleState();
param->endChangeGesture();
}
}

We also need to update our GenericEditor::getParameterForSlider() function to inspect the controls array instead of the paramSliders array [12]:

AudioParameterFloat* getParameterForSlider (Slider* slider)
{
const OwnedArray<AudioProcessorParameter>& params = getAudioProcessor()->getParameters();
return dynamic_cast<AudioParameterFloat*> (params[controls.indexOf (slider)]); // [12]
}

Finally, our timer callback needs to be updated to handle the AudioParameterBool types:

void timerCallback() override
{
const OwnedArray<AudioProcessorParameter>& params = getAudioProcessor()->getParameters();
for (int i = 0; i < controls.size(); ++i)
{
if (Slider* slider = dynamic_cast<Slider*> (controls[i]))
{
AudioParameterFloat* param = static_cast<AudioParameterFloat*> (params[i]);
slider->setValue ((double) *param, dontSendNotification);
}
else if (Button* button = dynamic_cast<Button*> (controls[i]))
{
AudioParameterBool* param = static_cast<AudioParameterBool*> (params[i]);
}
}
}

Build the plug-in and open it in your DAW.

tutorial_audio_parameter_screenshot2.png
The gain plug-in UI with the phase invert parameter added (in Logic Pro X)
Note
The source code for this modified version of the plug-in can be found in the GainProcessor_04.cpp and GenericEditor_04.cpp files in the Source directory of the demo project.
Exercise
Change the phase invert parameter to be an AudioParameterChoice type with two choices "Not inverted" and "Inverted". You will need to modify the processing code and update the GenericEditor class to handle AudioParameterChoice parameters. In these cases the editor should display a ComboBox containing the choices (see Tutorial: The ComboBox class).

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.
  • Updating AudioParameterFloat objects from a user interface.
  • 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