Table of Contents
Learn the basics of the MPE standard and how to implement a synthesiser that supports MPE. Hook your application up to a ROLI Seaboard Rise!
Platforms: Windows, Mac OS X, Linux
Download the demo project for this tutorial here: tutorial_mpe_introduction.zip. Unzip the project and open it in your IDE.
If you need help with this step, see Tutorial: Getting started with the Projucer.
- It would be helpful to read Tutorial: Synthesiser using MIDI input first, as this is used as a reference point in a number of places.
The demo project is a simplified version of the
MPETest project in the
juce/examples directory. In order to get the the most out of this tutorial you will need an MPE compatible controller. MPE stands for Multidimensional Polyphonic Expression, which is a new specification to allow multidimensional data to be communicated between audio products.
- The synthesiser may appear very quiet unless your controller transmits MIDI channel pressure and continuous controller 74 (timbre) in the way that the Seaboard RISE does.
With a Seaboard RISE connected to your computer the window of the demo application should look something like the following screenshot:
You will need to enable one of the MIDI inputs (here you can see a Seaboard RISE is shown as an option).
Any notes played on your MPE compatible device will be visualised in the lower portion of the window. This is shown in the following screenshot:
One key feature of MPE is that each new MIDI note event is assigned its own MIDI channel, rather than all notes from a particular controller keyboard being assigned to the same MIDI channel. This allows each individual note to be controlled independently by control change messages, pitch bend message, and so on. In the JUCE implementation of MPE, a playing note is represented by an MPENote object. An MPENote object encapsulates the following data:
- The MIDI channel of the note.
- The initial MIDI note value of the note.
- The note-on velocity (or strike).
- The pitch-bend value for the note: derived from any MIDI pitch-bend messages received on this note's MIDI channel.
- The pressure for the note: derived from any MIDI channel pressure messages received on this note's MIDI channel.
- The timbre for the note: typically derived from any controller messages on this note's MIDI channel for controller 74.
- The note-off velocity (or lift): this is only valid after the note-off event has been received and until the playing sound has stopped.
With no notes playing you can see that the visualiser represents a conventional MIDI keyboard layout. Each note is represented in the visualiser in the demo application as follows:
- A grey filled circle represents the note-on velocity (a larger circle for higher velocity).
- The MIDI channel for the note is displayed above the "+" symbol within this circle;
- The initial MIDI note name is displayed below the "+" symbol.
- An overlaid white circle represents the current pressure for this note (again, a larger circle for higher pressure).
- The horizontal position of the note is derived from the original note and any pitch bend that has been applied to this note.
- The vertical position of the note is derived from the timbre parameter for the note (from MIDI controller 74 on this note's MIDI channel).
Before delving further into other aspects of the MPE specification, which are demonstrated by this application, let's look at some of the other things our application uses.
We also have some important class members in our
The AudioDeviceManager  class handles the audio and MIDI configuration on our computer, while the AudioDeviceSelectorComponent  class gives us a means of configuring this from the graphical user interface (see Tutorial: The AudioDeviceManager class). The MidiMessageCollector  class allow us to easily collect messages into blocks of timestamped MIDI messages in our audio callback (see Tutorial: Synthesiser using MIDI input).
Notice another important argument that is passed to the AudioDeviceSelectorComponent constructor: the
showMidiInputOptions must be
true to show our available MIDI inputs.
handleIncomingMidiMessage() is called when each MIDI message is received from any of the active MIDI inputs in the user interface:
Here we pass each MIDI message to both:
visualiserInstrumentmember — which is used to drive the visualiser display; and
midiCollectormember — which in turn passes the messages to the synthesiser in the audio callback.
Before any audio callbacks are made, we need to inform the
midiCollector members of the device sample rate, in the
audioDeviceIOCallback() function appears to do nothing MPE-specific:
- In fact, this is rather similar to the
SynthAudioSournce::getNextAudioBlock()function in Tutorial: Synthesiser using MIDI input.
The MPEInstrument class maintains the state of the currently playing notes according to the MPE specification. An MPEInstrument object can have one or more listeners attached and it can broadcast changes to notes as they occur. All you need to do is feed the MPEInstrument object the MIDI data and it handles the rest.
MainComponent constructor we configure the MPEInstrument in legacy mode and set the default pitch bend range to 24 semitones:
- See Tutorial: MPE notes, zones and zone layouts for an introuction to more flexible approaches using zones.
MainComponent::handleIncomingMidiMessage() function we pass the MIDI messages on to our
In this example we are using an MPEInstrument object directly as we need it to update our visualiser display. For the purposes of audio synthesis we don't need to create a separate MPEInstrument object. The MPESynthesiser object contains an MPEInstrument object that it uses to drive the synthesiser.
We set our MPESynthesiser with the same configuration as our
visualiserInstrument object (in legacy mode with a pitch bend range of 24 semitones):
The MPESynthesiser class can also handle voice stealing for us, but as you can see here, we turn this off.
As we have already seen in the
MainComponent::audioDeviceAboutToStart() function we need to set the MPESynthesiser object's sample rate to work correctly:
And as we have also already seen in the
MainComponent::audioDeviceIOCallback() function, we simply pass it a MidiBuffer object containing messages that we want it to use to perform its synthesis operation:
You can generally use the MPESynthesiser and MPEInstrument classes as they are (although both classes can be used as base classes if you need to override some behaviours). The most important class you must override in order to use the MPESynthesiser class is the MPESynthesiserVoice class. This actually generates the audio signals from your synthesiser's voices.
- This is similar to the SynthesiserVoice class that is used with the Synthesiser class, but it is customised to implement the MPE specification. See Tutorial: Synthesiser using MIDI input.
The code for our voice class is in the
MPEDemoSynthVoice.h file within the
Source directory of the demo project. Here we implement the
MPEDemoSynthVoice class to inherit from the MPESynthesiserVoice class:
We have some member variables to keep track of values to control the level, timbre, and frequency of the tone that we generate. In particular, we use the LinearSmoothedValue class, which is really useful for smoothing out discontinuities in the signal that would be otherwise caused by value changes (see Tutorial: Sine synthesis).
In the constructor, we initialise some of our members to zero (the LinearSmoothedValue objects will automatically be zero).
The key to using the MPESynthesiserVoice class is to access its MPESynthesiserVoice::currentlyPlayingNote (protected) MPENote member to access the control information about the note during the various callbacks. For example, we override the MPESynthesiserVoice::noteStarted() function like this:
- note-on velocity: in the MPENote::noteOnVelocity member
- pitch bend: in the MPENote::pitchbend member
- pressure: in the MPENote::pressure member
- timbre: in the MPENote::timbre member
- note-off velocity: in the MPENote::noteOffVelocity member
MPEValue objects make it easy create values from 7-bit or 14-bit MIDI value sources, and to obtain these values as floating-point values in the range 0..1 or -1..+1.
- The MPEValue class stores the value internally using the 14-bit range.
MainComponent::noteStopped() function triggers the "release" of the note envelope (or stops it immediately, if requested):
- This is very similar to
SineWaveVoice::stopNote()function in Tutorial: Synthesiser using MIDI input. There isn't anything MPE-specific here.
- Modify the
MainComponent::noteStopped()function to allow the note-off velocity (lift) to modify the rate of release of the note. Faster lifts should result in a shorter release time.
There are callbacks that tell us when either the pressure, pitch bend, or timbre have changed for this note:
Again, we access the MPESynthesiserVoice::currentlyPlayingNote member to obtain the current value for each of these parameters.
MainComponent::renderNextBlock() actually generates the audio signal, mixing this voice's signal into the buffer that is passed in:
MainComponent::getNextSample() to generate the waveform:
This simply cross fades between a sine wave and a (non-bandlimited) square wave, based on the value of the timbre parameter.
- Modify the
MPEDemoSynthVoiceclass to crossfade between two sine waves, one octave appart, in response to the timbre parameter.
In this tutorial we have introduced some of the MPE based classes in JUCE. You should now know:
- What MPE is.
- That MPE compatible devices will allocate each note to their own MIDI channels.
- How the MPENote class stores information about a note including its MIDI channel, the original note number, velocity, pitch bend, and so on.
- That the MPEInstrument class maintains the state of the currently playing notes.
- That the MPESynthesiser class contains an MPEInstrument object that it uses to drive the synthesiser.
- That you must implement a class that inherits from the MPESynthesiserVoice class to implement your synthesiser's audio code.
Generated on Fri Jan 12 2018 09:51:15 for JUCE by 1.8.13