Blog
Build this Awesome Sampler Plugin | Part 3: Parameters Done Right
Learn how to add parameters to your JUCE sampler plugin and pick up the optimization tricks that make them feel polished and professional.
News
Build this Awesome Sampler Plugin | Part 3: Parameters Done Right
Learn how to add parameters to your JUCE sampler plugin and pick up the optimization tricks that make them feel polished and professional.
Build this Awesome Sampler Plugin | Part 3: Parameters Done Right
Build this Awesome Sampler Plugin | Part 3: Parameters Done Right
Welcome to Part 3 of our sampler plugin tutorial series! In this episode, we're going beyond the basics and focusing on something that separates a functional plugin from a polished one: parameters that actually feel good to use. We'll add decay and reverb controls, wire them up through JUCE's APVTS system, and then layer on a set of optimizations that make everything feel considered and professional.
This tutorial builds on Part 2 where we set up our audio engine and got our sampler responding to MIDI. If you haven't completed that yet, I recommend going back and doing so first.
GitHub Repository
All the code for this tutorial is available on GitHub. I've organized the repository with separate branches for each episode, and within each branch, I've created commits for each major section. This makes it easy to follow along and see exactly what code changes at each step.
View the code on GitHub
Understanding APVTS
The Audio Processor Value Tree State (APVTS) is JUCE's system for managing plugin parameters, and everything in this tutorial flows through it. When the plugin initializes, it runs a function called createParameterLayout() that builds a list of all available parameters and hands them to the APVTS. From that point on, any time you need a parameter's current value — in your process block, your UI, anywhere — you go through the APVTS to get it.
Parameters aren't scattered across your codebase. They're declared in one place, stored in one object, and accessed through a consistent interface. Once that clicks, adding new parameters becomes mechanical rather than confusing.
Setting Up Your Parameter Layout
To add parameters, you create a createParameterLayout() function that returns a juce::AudioProcessorValueTreeState::ParameterLayout. Inside it, you use std::make_unique to create each parameter and std::move to transfer ownership into the layout:
cpp
juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout()
{
juce::AudioProcessorValueTreeState::ParameterLayout layout;
layout.add(std::move(std::make_unique<juce::AudioParameterFloat>(
"decay", "Decay",
juce::NormalisableRange<float>(0.0f, 100.0f, 1.0f),
50.0f,
juce::AudioParameterFloatAttributes().withStringFromValueFunction(
[](float value, int) { return juce::String(value, 0) + "%"; })
)));
return layout;
}
The std::move here is essential. Without it, the parameter would be deallocated when the function finishes. The layout needs to own the parameter's memory — think of it like handing off a baton in a relay race. If you don't actually let go, the next runner can't carry it forward.
NormalisableRange: More Than Min and Max
When you create an AudioParameterFloat, the NormalisableRange argument does more than set boundaries. It also accepts an interval value, which controls the step size as the user drags a slider, and a skew factor, which shifts where the midpoint falls. This is critical for parameters like filter cutoffs where the range spans 20Hz to 20,000Hz — you probably want the midpoint around 500Hz, not 10,000Hz.
A small but impactful addition is the StringFromValueFunction attribute. By passing a lambda that appends a percent sign to the float value, the parameter display changes from "75" to "75%" in the DAW's generic UI. It's a few lines of code, but it communicates intent to the user and builds trust in your product.
Prototyping with GenericAudioProcessorEditor
Before building a full custom UI, JUCE offers a class called GenericAudioProcessorEditor that automatically generates sliders for every parameter your plugin exposes. Swapping it in takes one line change in createEditor():
cpp
juce::AudioProcessorEditor* PluginProcessor::createEditor()
{
return new juce::GenericAudioProcessorEditor(*this);
}
Don't wait until you've built a polished UI to verify your parameters work correctly. The generic editor lets you validate the audio behaviour first, then layer the visual design on top of something you already know is functional.
Connecting Parameters to Your Sampler Sounds
To connect the decay parameter to the actual audio, you need to reach your SamplerSound objects through the JUCE Synthesiser class. The getSound() method returns a generic SynthesiserSound pointer, so you need a dynamic_cast to access SamplerSound-specific methods like setEnvelopeParameters():
cpp
for (int i = 0; i < midiPlaybackEngine.getNumSounds(); ++i)
{
if (auto* sound = dynamic_cast<juce::SamplerSound*>(midiPlaybackEngine.getSound(i).get()))
{
sound->setEnvelopeParameters({ attack, decayValue, 1.0f, release });
}
}
The if statement makes the cast safe — if it fails, the code simply skips that iteration. This is a common pattern in JUCE development and one worth internalising, because you'll encounter it any time you need to access functionality that lives on a subclass but the API hands you a base class pointer.
Only Update When Something Actually Changes
The process block runs hundreds or thousands of times per second. Setting envelope parameters on every single callback — even when the user hasn't touched a slider — is unnecessary work. The fix is simple: store the previous parameter value, compare it to the current one, and only run the update code when they differ.
cpp
float currentDecay = apvts.getRawParameterValue("decay")->load();
if (!juce::approximatelyEqual(currentDecay, oldDecayValue))
{
updateDecay(currentDecay);
oldDecayValue = currentDecay;
}
Use JUCE's approximatelyEqual() rather than a direct float equality check — floating-point comparisons need a tolerance. One important detail: initialise your stored value to -1.0f (a value the parameter can never be) in prepareToPlay(), so the update is guaranteed to run on the first callback and the plugin starts in a correct state.
Quadratic Mapping for Better Control Feel
This technique comes from Matthijs Hollemans and it's elegant in its simplicity. Instead of using the normalised parameter value directly, you square it:
cpp
float normalisedDecay = currentDecay / 100.0f;
float skewedDecay = normalisedDecay * normalisedDecay;
The result is still between 0 and 1, but the distribution changes. The lower range of the slider now covers more of the output range, giving finer control over small decay values where the differences are most audible. The higher values, which are less commonly needed, get compressed together. It's the same principle as the NormalisableRange skew factor, but applied after normalisation as a simple arithmetic operation.
A Tiny Offset Prevents a Jarring Experience
When the decay parameter is at zero, the user hears essentially no sound — which can feel like the plugin is broken. The fix is a small offset so the actual minimum is 0.05 even when the slider shows 0:
cpp
float finalDecay = (skewedDecay * 0.95f) + 0.05f;
The user still sees a range of 0 to 100%, but the audio never drops to true silence. These small decisions accumulate into the overall feeling of quality. No single one is dramatic, but together they're the difference between a plugin that feels considered and one that feels like a raw prototype.
Keeping Your Process Block Readable
By the end of the tutorial, the process block has accumulated decay updating logic, reverb parameter handling, MIDI rendering, and reverb processing. The final step is to extract the decay and reverb update code into dedicated helper functions — updateDecay() and updateReverb(). The process block then becomes a clean, readable sequence:
cpp
void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) override
{
updateDecay();
updateReverb();
midiPlaybackEngine.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples());
reverb.processStereo(buffer.getWritePointer(0), buffer.getWritePointer(1), buffer.getNumSamples());
}
Your process block is the heartbeat of your plugin. Being able to see the full signal flow at a glance — without scrolling through implementation details — makes debugging and future development dramatically easier.
Summary
We covered a lot of ground in this tutorial:
- Added decay and reverb parameters using APVTS and
createParameterLayout() - Used
NormalisableRangeandStringFromValueFunctionfor better parameter display - Prototyped quickly with
GenericAudioProcessorEditor - Connected parameters to
SamplerSoundobjects usingdynamic_cast - Implemented change detection to keep the process block efficient
- Applied quadratic mapping and a value offset for a more polished control feel
- Refactored update logic into helper functions for a clean, readable process block
This is where the plugin starts to feel like something a user could actually enjoy working with — not just functional, but considered. In the next tutorial, we'll build a custom UI to replace the generic editor. Until then, happy coding!
Resources
GitHub Repository: github.com/TheAudioProgrammer/JuceSamplerAudioPluginJUCE
Documentation: juce.com/learn/documentation
Join Our Community: theaudioprogrammer.com/community
Joshua Hodge
The Audio Programmer
More Tutorials


We Built a Multi-Player Audio App With AI: Intro to Audiotool Nexus
Nexus is Audiotool's new extension layer that lets a browser-based app read and write a live project in real time, something a traditional VST can't do. Silas Gyger, lead engineer at Audiotool, shows how far an AI agent can take you by building three working apps from scratch.


The Audio Programmer Virtual Meetup | April 9th, 2025 @ 17:00 UK
Jani Huoponen, Scott Kramer, and Claus Trelby explore Eclipsa Audio – Google and Samsung's open-source spatial audio format – and what it means for creators working across music, film, TV, and the open web.
More Meetups


The Audio Programmer Virtual Meetup | April 9th, 2025 @ 17:00 UK
Jani Huoponen, Scott Kramer, and Claus Trelby explore Eclipsa Audio – Google and Samsung's open-source spatial audio format – and what it means for creators working across music, film, TV, and the open web.
More News
More Articles


The audio industry is bigger than you think – and harder to hire into
Audio engineering has quietly fragmented across safety systems, embedded sensing, hearing tech and machine learning. The companies hiring in these fields are no longer just competing with other audio companies – and most of them don't realise it.









