All Articles

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.

Joshua Hodge

10

·

April 13, 2026

All Content

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.

Joshua Hodge

10

·

April 13, 2026

All Tutorials
Tutorials

Build this Awesome Sampler Plugin | Part 3: Parameters Done Right

SHARE THIS
Speakers
No items found.
SHARE THIS
Speakers
No items found.
All Meetups
Tutorials

Build this Awesome Sampler Plugin | Part 3: Parameters Done Right

SHARE THIS
Speakers
No items found.
SHARE THIS
Speakers
No items found.

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 NormalisableRange and StringFromValueFunction for better parameter display
  • Prototyped quickly with GenericAudioProcessorEditor
  • Connected parameters to SamplerSound objects using dynamic_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

Audio Programming Basics
Audio Software Development

Joshua Hodge

The Audio Programmer

More Tutorials

View All

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.

This is some text inside of a div block.

Vibe Coding an Audio Plugin with Cursor vs Claude Code

A practical first look at Cursor for AI-assisted audio plugin development, covering project setup, code review, debugging, and workflow comparisons with Claude.

This is some text inside of a div block.

Build this Awesome Sampler Plugin | Part 4: JUCE UI Basics

Learn how to build a custom JUCE plugin interface using images, fonts, colours, and reusable UI assets.

This is some text inside of a div block.

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.

This is some text inside of a div block.
View All

More Meetups

View All

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.

This is some text inside of a div block.

The Audio Programmer Virtual Meetup | March 5th, 2025 @ 18:00 UK

Sam Fischmann introduces practical approaches to getting started with Digital Signal Processing, covering key DSP concepts and how they translate into useful tools for music production.

This is some text inside of a div block.

The Audio Programmer Virtual Meetup | February 10th, 2025 @ 17:30 UK

Eric Tarr introduces the Point-to-Point Library, a tool designed to help audio developers easily incorporate analog circuit modeling into their plugins.

This is some text inside of a div block.

The Audio Programmer Virtual Meetup | September 9th, 2025 @ 18:30 UK

Jelle explains how C++ lambdas work under the hood, clears up common misconceptions, and demonstrates how they can be used to write cleaner and more expressive audio code.

This is some text inside of a div block.
View All

More News

View All

The Audio Programmer joins Audiotool's Let's Build! hackathon series

We're joining BBC R&D, the Fraunhofer Institute and Music Hackspace as collaborators in Audiotool's Let's Build! NEXUS Hackathon Series, a free global hackathon running through 6 July 2026.

This is some text inside of a div block.

API London

An evening focused around building the future of music and audio apps, plugins, and creative tools.

This is some text inside of a div block.

Steinberg VST3 & ASIO SDKs Go Open Source

Steinberg announce licensing changes that will have a huge impact for audio software developers.

This is some text inside of a div block.
View All

More Articles

View All

Is AI Killing Music Tech? How to Stay In Demand

AI is reshaping audio development faster than most teams can react. Here are three things that will keep you valuable as the tools get better, drawn from months of pushing them into real codebases.

This is some text inside of a div block.

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.

This is some text inside of a div block.

Why Use a Specialist Recruiter for Music Tech Hiring?

The Audio Programmer gives you access to pre-screened, specialist talent from within the audio development community, without the noise of a standard job board.

This is some text inside of a div block.

Why Work With a Recruiter in Audio Tech Industry?

Working with a specialist recruiter in audio and music tech is a very different experience from applying cold through LinkedIn. Here's what it actually looks like – and why it might be worth a conversation.

This is some text inside of a div block.
View All