Tutorial: Build a MIDI synthesiser

This tutorial implements a polyphonic sine wave synthesiser that responds to MIDI input. This makes use of the Synthesiser class and related classes.

Level: Intermediate

Platforms: Windows, macOS, Linux

Classes: Synthesiser, SynthesiserVoice, SynthesiserSound, AudioSource, MidiMessageCollector

Getting started

You should be familiar with handling MIDI input in JUCE and how to generate a sine wave. See Tutorial: Handling MIDI events and Tutorial: Build a sine wave synthesiser.

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.

The demo project

The demo project presents an on-screen keyboard that can be used to play a simple sine wave synthesiser.

The application window containing a MidiKeyboardComponent

Using keys on the computer keyboard the on-screen keyboard can be controlled (using keys A, S, D, F and so on to control musical notes C, D, E, F and so on). This allows you to play the synthesiser polyphonically.

The Synthesiser class

This tutorial makes use of the JUCE Synthesiser class to implement a polyphonic synthesiser. This shows you all the basic elements needed to customise the synthesiser with your own sounds in your own applications. There are various classes needed to get this to work and in addition to our standard MainContentComponent class, these are:

  • SynthAudioSource: This implements a custom AudioSource class called SynthAudioSource, which contains the Synthesiser class itself. This outputs all of the audio from the synthesiser.
  • SineWaveVoice: This is a custom SynthesiserVoice class called SineWaveVoice. A voice class renders one of the voices of the synthesiser mixing it with the other sounding voices in a Synthesiser object. A single instance of a voice class renders one voice.
  • SineWaveSound: This contains a custom SynthesiserSound class called SineWaveSound. A sound class is effectively a description of the sound that can be created as a voice. For example, this may contain the sample data for a sampler voice or the wavetable data for a wavetable synthesiser.

Setting up the keyboard

Our MainContentComponent class contains the following data members.

SynthAudioSource synthAudioSource;
MidiKeyboardState keyboardState;
MidiKeyboardComponent keyboardComponent;

The synthAudioSource and keyboardComponent members are initialised in the MainContentComponent constructor.

: synthAudioSource (keyboardState),
keyboardComponent (keyboardState, MidiKeyboardComponent::horizontalKeyboard)
addAndMakeVisible (keyboardComponent);
setAudioChannels (0, 2);
setSize (600, 160);
startTimer (400);

See Tutorial: Handling MIDI events for more information on the MidiKeyboardComponent class.

In order that we can start playing the keyboard from the computer's keyboard we grab the keyboard focus just after the application starts. To do this we use a simple timer that fires after 400 ms:

void timerCallback() override

AudioAppComponent functions

The application uses the AudioAppComponent to set up a simple audio application (see Tutorial: Build a white noise generator for the most basic application). The three required pure virtual functions simply call the corresponding functions in our custom AudioSource class:

void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override
synthAudioSource.prepareToPlay (samplesPerBlockExpected, sampleRate);
void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
synthAudioSource.getNextAudioBlock (bufferToFill);
void releaseResources() override

The SynthAudioSource class

The SynthAudioSource class does a little more work:

class SynthAudioSource : public AudioSource
SynthAudioSource (MidiKeyboardState& keyState)
: keyboardState (keyState)
for (auto i = 0; i < 4; ++i) // [1]
synth.addVoice (new SineWaveVoice());
synth.addSound (new SineWaveSound()); // [2]
void setUsingSineWaveSound()
void prepareToPlay (int /*samplesPerBlockExpected*/, double sampleRate) override
synth.setCurrentPlaybackSampleRate (sampleRate); // [3]
void releaseResources() override {}
void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
MidiBuffer incomingMidi;
keyboardState.processNextMidiBuffer (incomingMidi, bufferToFill.startSample,
bufferToFill.numSamples, true); // [4]
synth.renderNextBlock (*bufferToFill.buffer, incomingMidi,
bufferToFill.startSample, bufferToFill.numSamples); // [5]
MidiKeyboardState& keyboardState;
Synthesiser synth;
  • [1]: We add some voices to our synthesiser. This number of voices added determines the polyphony of the synthesiser.
  • [2]: We add the sound so that the synthesiser knows which sounds it can play.
  • [3]: The synthesiser needs to know the sample rate of the audio output.
  • [4]: In the getNextAudioBlock() function we pull buffers of MIDI data from the MidiKeyboardState object.
  • [5]: These buffers of MIDI are passed to the synthesiser which will be used to render the voices using the timestamps of the note-on and note-off messages (and other MIDI channel voice messages).
SynthesiserVoice objects must be added to one and only one Synthesiser object. The Synthesiser object manages the lifetime of the voices.

SynthesiserSound objects can be shared between Synthesiser objects if you wish. The SynthesiserSound class is a type of ReferenceCountedObject class therefore the lifetime of SynthesiserSound objects is handled automatically.

If you need to keep a pointer to a SynthesiserSound object you should store it in a YourSoundClass::Ptr variable for this memory management to work.

The SineWaveSound class

Our sound class is very simple, it doesn't even need to contain any data. It just needs to report whether this sound should play on particular MIDI channels and specific notes or note ranges on that channel. In our simple case, it just returns true for both the appliesToNote() and appliesToChannel() functions. As mentioned above, the sound class might be where you would store data that is needed to create the sound (such as a wavetable).

struct SineWaveSound : public SynthesiserSound
SineWaveSound() {}
bool appliesToNote (int) override { return true; }
bool appliesToChannel (int) override { return true; }

Sine wave voice state

The SineWaveVoice class is a bit more complex. It needs to maintain the state of one of the voices of the synthesiser. For our sine wave, we need these data members:

double currentAngle = 0.0, angleDelta = 0.0, level = 0.0, tailOff = 0.0;

See Tutorial: Build a sine wave synthesiser for information on the first three. The tailOff member is used to give each voice a slightly softer release to its amplitude envelope. This gives each voice a slight fade out at the end rather than stopping abruptly.

Exponential release envelope

Checking which sound can be played

The SynthesiserVoice::canPlaySound() function must be overriden to return whether the voice can play a sound. We could just return true in this case but our example illustrates how to use dynamic_cast to check the type of the sound class being passed in.

bool canPlaySound (SynthesiserSound* sound) override
return dynamic_cast<SineWaveSound*> (sound) != nullptr;

Starting a voice

A voice is started by the owning synthesiser by calling our SynthesiserVoice::startNote() function, which we must override:

void startNote (int midiNoteNumber, float velocity,
SynthesiserSound*, int /*currentPitchWheelPosition*/) override
currentAngle = 0.0;
level = velocity * 0.15;
tailOff = 0.0;
auto cyclesPerSecond = MidiMessage::getMidiNoteInHertz (midiNoteNumber);
auto cyclesPerSample = cyclesPerSecond / getSampleRate();
angleDelta = cyclesPerSample * 2.0 * MathConstants<double>::pi;

Again, most of this should be familar to your from Tutorial: Build a sine wave synthesiser. The tailOff value is set to zero at the start of each voice. We also use the velocity of the MIDI note-on event to control the level of the voice.

Rendering a voice

The SynthesiserVoice::renderNextBlock() function must be overriden to generate the audio.

void renderNextBlock (AudioSampleBuffer& outputBuffer, int startSample, int numSamples) override
if (angleDelta != 0.0)
if (tailOff > 0.0) // [7]
while (--numSamples >= 0)
auto currentSample = (float) (std::sin (currentAngle) * level * tailOff);
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
outputBuffer.addSample (i, startSample, currentSample);
currentAngle += angleDelta;
tailOff *= 0.99; // [8]
if (tailOff <= 0.005)
clearCurrentNote(); // [9]
angleDelta = 0.0;
while (--numSamples >= 0) // [6]
auto currentSample = (float) (std::sin (currentAngle) * level);
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
outputBuffer.addSample (i, startSample, currentSample);
currentAngle += angleDelta;
  • [6]: This loop is used for the normal state of the voice, while the key is being held down. Notice that we use the AudioSampleBuffer::addSample() function, which mixes the currentSample value with the value alread at index startSample. This is because the synthesiser will be iterating over all of the voices. It is the responsibility of each voice to mix its output with the current contents of the buffer.
  • [7]: When the key has been released the tailOff value will be greater than zero. You can see the synthesis algorithm is similar.
  • [8]: We use a simple exponential decay envelope shape.
  • [9]: When the tailOff value is small we determine that the voice has ended. We must call the SynthesiserVoice::clearCurrentNote() function at this point so that the voice is reset and available to be reused.
It is very important that you take note of the startSample argument. The synthesiser is very likely to call the renderNextBlock() function mid-way through one of its output blocks. This is because the notes may start on any sample. These start times are based on the timestamps of the MIDI data received.

Stopping a voice

A voice is stopped by the owning synthersiser calling our SynthesiserVoice::stopNote() function, which we must override:

void stopNote (float /*velocity*/, bool allowTailOff) override
if (allowTailOff)
if (tailOff == 0.0)
tailOff = 1.0;
angleDelta = 0.0;

This may include velocity information from the MIDI note-off message, but in many cases we can ignore this. We may be being asked to stop the voice immediately in which case we call the the SynthesiserVoice::clearCurrentNote() function straight away. Under normal circumstances the synthesiser will allow our voices to end naturally. In our case we have the simple tail-off envelope. We trigger our tail-off by setting our tailOff member to 1.0.

Try adding a slower attack to the voices such that they don't start abruptly.

Adding external MIDI input

Let's add functionality to allow an external MIDI source to control our synthesiser in addition to the on-screen keyboard.

You probably need to try this on one of the desktop platforms such as macOS, Windows, or Linux rather than one of the mobile platforms.

Providing a MIDI input to the SynthAudioSource

First add a MidiMessageCollector object as a member of the SynthAudioSource class. This provides somewhere that MIDI messages can be sent and that the SynthAudioSource class can use them:

MidiMessageCollector midiCollector;

In order to process the timestamps of the MIDI data the MidiMessageCollector class needs to know the audio sample rate. Set this in the SynthAudioSource::prepareToPlay() function [10]:

void prepareToPlay (int /*samplesPerBlockExpected*/, double sampleRate) override
synth.setCurrentPlaybackSampleRate (sampleRate);
midiCollector.reset (sampleRate); // [10]

Then you can pull any MIDI messages for each block of audio using the MidiMessageCollector::removeNextBlockOfMessages() function [11]:

void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
MidiBuffer incomingMidi;
midiCollector.removeNextBlockOfMessages (incomingMidi, bufferToFill.numSamples); // [11]
keyboardState.processNextMidiBuffer (incomingMidi, bufferToFill.startSample,
bufferToFill.numSamples, true);
synth.renderNextBlock (*bufferToFill.buffer, incomingMidi,
bufferToFill.startSample, bufferToFill.numSamples);

We'll need access to this MidiMessageCollector object from outside the SynthAudioSource class, so add an accessor to the SynthAudioSource class like this:

MidiMessageCollector* getMidiCollector()
return &midiCollector;

In our MainContentComponent class we'll add this MidiMessageCollector object as a MidiInputCallback object to our application's AudioDeviceManager object.

Listing MIDI inputs

To present a list of MIDI input devices to user, we'll use some code from Tutorial: Handling MIDI events. Add some members to our MainContentComponent class:

ComboBox midiInputList;
Label midiInputListLabel;
int lastInputIndex = 0;

Then add the following code to the MainContentComponent constructor.

addAndMakeVisible (midiInputListLabel);
midiInputListLabel.setText ("MIDI Input:", dontSendNotification);
midiInputListLabel.attachToComponent (&midiInputList, true);
addAndMakeVisible (midiInputList);
midiInputList.setTextWhenNoChoicesAvailable ("No MIDI Inputs Enabled");
auto midiInputs = MidiInput::getDevices();
midiInputList.addItemList (midiInputs, 1);
midiInputList.onChange = [this] { setMidiInput (midiInputList.getSelectedItemIndex()); };
for (auto midiInput : midiInputs)
if (deviceManager.isMidiInputEnabled (midiInput))
setMidiInput (midiInputs.indexOf (midiInput));
if (midiInputList.getSelectedId() == 0)
setMidiInput (0);

Add the setMidiInput() function that is called in the code above:

void setMidiInput (int index)
auto list = MidiInput::getDevices();
deviceManager.removeMidiInputCallback (list[lastInputIndex], synthAudioSource.getMidiCollector()); // [13]
auto newInput = list[index];
if (! deviceManager.isMidiInputEnabled (newInput))
deviceManager.setMidiInputEnabled (newInput, true);
deviceManager.addMidiInputCallback (newInput, synthAudioSource.getMidiCollector()); // [12]
midiInputList.setSelectedId (index + 1, dontSendNotification);
lastInputIndex = index;

Notice that we add the MidiMessageCollector object from our SynthAudioSource object as a MidiInputCallback object [12] for the specified MIDI input device. We also need to remove the previous MidiInputCallback object for the previously selected MIDI input device if the user changes the selected device using the combo-box [13].

We need to position this ComboBox object and adjust the position of the MidiKeyboardComponent object in our resized() function:

void resized() override
midiInputList .setBounds (200, 10, getWidth() - 210, 20);
keyboardComponent.setBounds (10, 40, getWidth() - 20, getHeight() - 50);

Run the application again and it should look something like this:

The application window showing the MIDI input device list

Of course, the devices listed will depend on your specific system configuration.

The source code for this modified version of the application can be found in the SynthUsingMidiInputTutorial_02.h file of the demo project.
Try adding a slider to control the length of the tail-off for each voice. You could also add a slider to control the length of the attack, if you added this in the earlier exercise.


This tutorial has introduced the Synthesiser class. After reading this tutorial you should be able to:

See also

virtual bool appliesToChannel(int midiChannel)=0
Returns true if the sound should be triggered by midi events on a given channel.
void setTextWhenNoChoicesAvailable(const String &newMessage)
Sets the message to show when there are no items in the list, and the user clicks on the drop-down bo...
int getSelectedId() const noexcept
Returns the ID of the item that's currently shown in the box.
Collects incoming realtime MIDI messages and turns them into blocks suitable for processing by a bloc...
Definition: juce_MidiMessageCollector.h:41
Base class for a musical device that can play sounds.
Definition: juce_Synthesiser.h:310
void setSelectedId(int newItemId, NotificationType notification=sendNotificationAsync)
Sets one of the items to be the current selection.
bool isMidiInputEnabled(const String &) const
void attachToComponent(Component *owner, bool onLeft)
Makes this label "stick to" another component.
void reset(double sampleRate)
Clears any messages from the queue.
std::function< void()> onChange
You can assign a lambda to this callback object to have it called when the selected ID is changed.
Definition: juce_ComboBox.h:308
Represents a piano keyboard, keeping track of which keys are currently pressed.
Definition: juce_MidiKeyboardState.h:89
Used by AudioSource::getNextAudioBlock().
Definition: juce_AudioSource.h:35
virtual void releaseResources()=0
Allows the source to release anything it no longer needs after playback has stopped.
A component that lets the user choose from a drop-down list of choices.
Definition: juce_ComboBox.h:49
static double getMidiNoteInHertz(int noteNumber, double frequencyOfA=440.0) noexcept
Returns the frequency of a midi note number.
void addItemList(const StringArray &items, int firstItemIdOffset)
Adds an array of items to the drop-down list.
virtual bool appliesToNote(int midiNoteNumber)=0
Returns true if this sound should be played when a given midi note is pressed.
int getSelectedItemIndex() const
Returns the index of the item that's currently shown in the box.
Holds a sequence of time-stamped midi events.
Definition: juce_MidiBuffer.h:45
void removeNextBlockOfMessages(MidiBuffer &destBuffer, int numSamples)
Removes all the pending messages from the queue as a buffer.
int numSamples
The number of samples in the buffer which the callback is expected to fill with data.
Definition: juce_AudioSource.h:84
Describes one of the sounds that a Synthesiser can play.
Definition: juce_Synthesiser.h:44
virtual void prepareToPlay(int samplesPerBlockExpected, double sampleRate)=0
Tells the source to prepare for playing.
void setMidiInputEnabled(const String &, bool)
int startSample
The first sample in the buffer from which the callback is expected to write data.
Definition: juce_AudioSource.h:80
void grabKeyboardFocus()
Tries to give keyboard focus to this component.
AudioBuffer< float > * buffer
The destination buffer to fill with audio data.
Definition: juce_AudioSource.h:76
Base class for objects that can produce a continuous stream of audio.
Definition: juce_AudioSource.h:112
int getNumChannels() const noexcept
Returns the number of channels of audio data that this buffer contains.
Definition: juce_AudioSampleBuffer.h:236
void addSample(int destChannel, int destSample, Type valueToAdd) noexcept
Adds a value to a sample in the buffer.
Definition: juce_AudioSampleBuffer.h:592
void processNextMidiBuffer(MidiBuffer &buffer, int startSample, int numSamples, bool injectIndirectEvents)
Scans a midi stream for up/down events and adds its own events to it.
A component that displays a text string, and can optionally become a text editor when clicked.
Definition: juce_Label.h:40
void addMidiInputCallback(const String &, MidiInputCallback *)
No notification message should be sent.
Definition: juce_NotificationType.h:36
AudioBuffer< float >
A component that displays a piano keyboard, whose notes can be clicked on.
Definition: juce_MidiKeyboardComponent.h:53
void clearActiveBufferRegion() const
Convenient method to clear the buffer if the source is not producing any data.
Definition: juce_AudioSource.h:87
virtual void getNextAudioBlock(const AudioSourceChannelInfo &bufferToFill)=0
Called repeatedly to fetch subsequent blocks of audio data.
void removeMidiInputCallback(const String &, MidiInputCallback *)
static StringArray getDevices()
Commonly used mathematical constants.
Definition: juce_MathsFunctions.h:365
void setText(const String &newText, NotificationType notification)
Changes the label text.
void setBounds(int x, int y, int width, int height)
Changes the component's position and size.