All Articles

Blog

Build This Awesome Sampler Plugin | Part 2: Loading and Playing Samples

Learn to build a JUCE sampler plugin: set up the Synthesiser class, load samples from BinaryData, map MIDI notes with BigInteger, and create reusable loading functions.

Joshua Hodge

10

·

November 26, 2025

All Content

News

Build This Awesome Sampler Plugin | Part 2: Loading and Playing Samples

Learn to build a JUCE sampler plugin: set up the Synthesiser class, load samples from BinaryData, map MIDI notes with BigInteger, and create reusable loading functions.

Joshua Hodge

10

·

November 26, 2025

All Tutorials
Tutorials

Build This Awesome Sampler Plugin | Part 2: Loading and Playing Samples

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

Build This Awesome Sampler Plugin | Part 2: Loading and Playing Samples

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

Welcome to Part 2 of our sampler plugin tutorial series! In this episode, we're moving on to the fun stuff: setting up our audio engine, loading sounds, and actually hearing our sampler play. By the end of this tutorial, you'll have a working sampler that responds to MIDI input and plays back samples at the correct pitch.

This tutorial builds on Part 1 where we set up our project structure and CMake configuration. 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

Getting Sounds into Your Project with BinaryData

Before we can play any sounds, we need to make sure our project can access them. JUCE provides a convenient way to embed audio files directly into your plugin using BinaryData. This approach compiles your sound files into the binary itself, so you don't need to worry about external file paths at runtime.

First, ensure that BinaryData is listed as a target link library in your CMakeLists.txt:

target_link_libraries(YourPlugin PRIVATE BinaryData)

After making changes to your CMake file, reinvoke CMake to regenerate your build files. Then, include the BinaryData header in your PluginProcessor.h:

#include "BinaryData.h"

This gives you access to all the audio files you've added to your project through CMake's juce_add_binary_data command.

Creating the MIDI Playback Engine

Here's where things get interesting. JUCE provides a class called Synthesiser that handles all the heavy lifting for MIDI-triggered audio playback. Despite its name, this class works perfectly for samplers too. I like to think of it as a "MIDI Playback Engine" because it handles MIDI routing, voice management, and audio rendering whether you're building a synth or a sampler.

Including the Right Libraries

Rather than including the entire JUCE library with JuceHeader.h, I prefer to include only the specific modules I need. This speeds up compilation and makes dependencies clearer. For our sampler, we need two modules:

#include <juce_audio_basics/juce_audio_basics.h>

#include <juce_audio_formats/juce_audio_formats.h>

Creating the Synthesiser Object

In your PluginProcessor.h, add the synthesiser as a private member:

juce::Synthesiser midiPlaybackEngine;

Adding Voices

The number of voices determines how many notes can play simultaneously. For an 8-voice sampler, we add 8 SamplerVoice objects in the constructor:

static constexpr auto numVoices = 8;  

// In constructor:

for (int i = 0; i < numVoices; ++i)

{    

   midiPlaybackEngine.addVoice(new juce::SamplerVoice());

}

Using static constexpr auto makes the voice count a compile-time constant, which is more efficient than a runtime variable. It also eliminates "magic numbers" from your code, making it more readable.

Setting the Sample Rate

The synthesiser needs to know the current sample rate to pitch samples correctly. Set this in prepareToPlay():

void prepareToPlay(double sampleRate, int samplesPerBlock) override

{    

    midiPlaybackEngine.setCurrentPlaybackSampleRate(sampleRate);

}

Rendering Audio

In processBlock(), call renderNextBlock() to process incoming MIDI and generate audio:

void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) override

{    
   midiPlaybackEngine.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples());

}

That's it for the playback engine setup! Now we need to give it some sounds to play.

Loading and Playing Your First Sample

Loading audio files in JUCE involves a few steps: creating a format manager, reading the audio data, and creating a SamplerSound object.

The AudioFormatManager

First, create an AudioFormatManager as a class member and register the basic audio formats in your constructor:

// In header: juce::AudioFormatManager formatManager;  

// In constructor: formatManager.registerBasicFormats();

This registers handlers for common audio file types like WAV and AIFF.

Loading from BinaryData

To load a sound from BinaryData, we create a MemoryInputStream and pass it to the format manager:

auto inputStream = std::make_unique<juce::MemoryInputStream>(BinaryData::C5_wav, BinaryData::C5_wavSize, false );  

auto reader = formatManager.createReaderFor(std::move(inputStream));

Note the use of std::make_unique and std::move. The createReaderFor() method takes ownership of the input stream, so we need to transfer ownership using std::move.

Creating a SamplerSound

With a valid reader, we can create a SamplerSound. The SamplerSound class takes several parameters:

name - A string identifier for the sound

reader - Reference to the AudioFormatReader

midiNotes - A BigInteger specifying which MIDI notes trigger this sound

originalMidiNote - The MIDI note at which the sample plays at original pitch

attackTime - Attack time in seconds

releaseTime - Release time in seconds

maxSampleLength - Maximum sample length in seconds

if (reader != nullptr)

{    

   juce::BigInteger midiNotes;    

    int originalMidiNote = 60;
   midiNotes.setBit(originalMidiNote);          

    double attack = 0.0;    
   double release = 0.1;    

    double maxLength = 10.0;

         
    midiPlaybackEngine.addSound(new juce::SamplerSound("C5", *reader, midiNotes, originalMidiNote, attack, release, maxLength));

}

Notice the null check on the reader. This is essential defensive programming - createReaderFor() returns nullptr if the audio file can't be read.

Understanding BigInteger for MIDI Note Mapping

The BigInteger parameter might seem unusual at first. Under the hood, it's a 128-bit value where each bit represents one of the 128 possible MIDI notes (0-127). Setting a bit to 1 means that MIDI note will trigger this sample.

To map a sample across multiple keys, simply set multiple bits:

juce::BigInteger midiNotes; std::vector<int> noteSet = { 60, 61, 62, 63, 64, 65, 66, 67 };  

for (auto note : noteSet)

{

   midiNotes.setBit(note);

}

When you map a sample across multiple notes, JUCE automatically pitches the sample up or down based on the originalMidiNote parameter. If you set originalMidiNote to 60 and trigger note 62, the sample plays pitched up by two semitones.

Creating a Reusable Sample Loading Function

Loading one sample works, but we need to load many samples. Let's create a reusable function that handles the loading logic:

juce::SamplerSound* loadSound(const juce::String& name,int originalMidiNote, const std::vector<int>& midiNoteSet, const void* data, size_t sizeInBytes)

{    

    auto inputStream = std::make_unique<juce::MemoryInputStream>(data, sizeInBytes, false);

   auto reader = formatManager.createReaderFor(std::move(inputStream));

         

    if (reader != nullptr)    
    {        
        juce::BigInteger midiNotes;        

          for (auto note : midiNoteSet)        

        {            
            midiNotes.setBit(note);        

        }                  

        double attack = 0.0;        

        double release = 0.1;        

        double maxLength = 10.0;                  

        return new juce::SamplerSound(name, *reader, midiNotes, originalMidiNote, attack, release, maxLength);    

    }          

return nullptr;

}

Now loading samples is clean and simple:

midiPlaybackEngine.addSound(loadSound("C5", 60, { 60 }, BinaryData::C5_wav, BinaryData::C5_wavSize));

Loading Multiple Samples

With our loadSound() function in place, loading an entire keyboard's worth of samples is straightforward. Each sample gets its own set of MIDI notes and original pitch:

midiPlaybackEngine.addSound(loadSound("C5", 60, {60, 61, 62, 63, 64, 65, 66, 67 }, BinaryData::C5_wav, BinaryData::C5_wavSize ));   midiPlaybackEngine.addSound(loadSound("C6", 72, {68, 69, 70, 71, 72, 73, 74, 75 }, BinaryData::C6_wav, BinaryData::C6_wavSize ));  

The key is matching each sample's originalMidiNote with the actual pitch of the recorded sample. This ensures accurate pitch shifting when triggering notes above or below the original.

Summary

We covered a lot of ground in this tutorial:

•       Set up a JUCE Synthesiser as our MIDI playback engine

•       Added SamplerVoice objects for polyphonic playback

•       Loaded audio files from BinaryData using MemoryInputStream

•       Created SamplerSound objects with proper MIDI note mapping

•       Built a reusable loadSound() function for cleaner code

•       Mapped samples across multiple keys with automatic pitch shifting

This is one of the foundational episodes in the series. We've gone from a silent gray rectangle to a working sampler that responds to MIDI input and plays back samples at the correct pitch.

In the next tutorial, we'll add a parameter system and build a basic user interface. Until then, happy coding!

Resources

GitHub Repository: github.com/TheAudioProgrammer/JuceSamplerAudioPlugin

JUCE Documentation: juce.com/learn/documentation

Join Our Community: theaudioprogrammer.com/community

Audio Programming Basics
Audio Software Development
C++
JUCE
Music Tech

Joshua Hodge

The Audio Programmer

More Tutorials

View All

Build this Awesome Sampler Plugin | Pt 1: Intro

Episode 1 outlines the sampler’s features and architecture and walks through the initial C++/JUCE project setup – the perfect starting point for intermediate developers building a real audio plugin from scratch.

This is some text inside of a div block.

How to Create an Audio Plugin Part 0: What is the JUCE Framework?

This article introduces beginners to the JUCE Framework, showing how it simplifies building cross-platform audio plugins and helps creators turn their musical ideas into professional software.

This is some text inside of a div block.

Monthly Meetup (April 13, 2021)

Processing Sound on the GPU by Alexander Prokopchuk (CTO, Braingines), Basil Sumatokhin (CPO, Braingines), and Alexander 'Sasha' Talashov (Technology Architect, Braingines)

This is some text inside of a div block.

Monthly Meetup (May 11, 2021)

Spectral Subtraction In Python by Alexx Mitchell (Audio Software Engineer, Madison Square Garden) and Beyond The Code with Céline Dedaj (Spazierendenken).

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

More Meetups

View All

Monthly Meetup (April 13, 2021)

Processing Sound on the GPU by Alexander Prokopchuk (CTO, Braingines), Basil Sumatokhin (CPO, Braingines), and Alexander 'Sasha' Talashov (Technology Architect, Braingines)

This is some text inside of a div block.

Monthly Meetup (May 11, 2021)

Spectral Subtraction In Python by Alexx Mitchell (Audio Software Engineer, Madison Square Garden) and Beyond The Code with Céline Dedaj (Spazierendenken).

This is some text inside of a div block.

Monthly Meetup (Jun 8, 2021)

SignalFlow DSP Engine by Daniel Jones (Audio Software Engineer, Independent) and Beyond The Code with Matt Tytel (Vital Synth).

This is some text inside of a div block.

Monthly Meetup (July 13, 2021)

Beyond The Code with Gerhard Behles (CEO, Ableton) and Nestup—A Language for Musical Rhythms by Sam Tarakajian &amp; Alex Van Gils (Cycling '74).

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

More News

View All

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 Music Tech Heading for a Collapse...or a Revolution?

A look at the current state of music technology and why innovation feels stuck – along with the key technical and industry pressures behind it. Drawing on insights from the Audio Developer Conference, this video highlights the patterns holding developers back and the opportunities that could spark the next wave of creativity in music tech.

This is some text inside of a div block.

How We Helped Create StageBox with Matt Robertson

The ultimate live performance tool for keyboard players

This is some text inside of a div block.

The Audio Developer Conference: 5 Talks I'm Looking Forward To...

Josh from The Audio Programmer shares 5 must-see talks at ADC 2025 in Bristol, from plugin design to AI and the future of music tech.

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