Skip to main content
MPE (MIDI Polyphonic Expression) enables per-note control of pitch bend, pressure, and timbre - essential for expressive instruments like Roli Seaboard, Linnstrument, and other MPE controllers.

Overview

MPE extends MIDI by dedicating one MIDI channel per note, allowing independent control of:
  • Pitch Bend - Per-note pitch changes (slides)
  • Channel Pressure - Per-note aftertouch (pressure sensitivity)
  • Timbre (CC74) - Per-note brightness/filter control
Traditional MIDI applies these controls globally to all notes. MPE makes them per-note for expressive playing.

Enabling MPE

Set the MPE flag in config.h:
config.h
#define PLUG_DOES_MPE 1
This tells hosts and the framework that your plugin supports MPE.

MidiSynth MPE Support

The MidiSynth class handles MPE zone configuration automatically:
Basic MPE Setup
#include "MidiSynth.h"

class MyInstrumentDSP
{
public:
  MyInstrumentDSP()
  {
    // Initialize with 16 voices for MPE
    for(int i = 0; i < 16; i++)
    {
      mSynth.AddVoice(new MyVoice(), 0);
    }
    
    // Enable basic MPE mode (zone 0 = channels 2-16)
    mSynth.InitBasicMPE();
  }
  
  MidiSynth mSynth {VoiceAllocator::kPolyModePoly};
};

InitBasicMPE

The InitBasicMPE() method sets up a single MPE zone:
IPlug/Extras/Synth/MidiSynth.h:68
void InitBasicMPE()
{
  SetMPEZones(0, 16);
}
This configures:
  • Lower Zone: Channels 2-16 for note expression
  • Manager Channel: Channel 1 for global parameters

MPE Zones

MPE supports two zones for split keyboard setups:
Advanced MPE Zone Setup
// Lower zone: channels 2-8
mSynth.SetMPEZones(0, 8);  

// Split keyboard: lower + upper zones
mSynth.SetMPEZones(8, 8); // 8 lower, 8 upper
Lower Zone: Channels 2 to (n+1), manager channel 1 Upper Zone: Channels 16 down to (16-n), manager channel 16

Pitch Bend Range

MPE typically uses ±48 semitones, but you can customize:
Setting Pitch Bend Range
// Default non-MPE range
mSynth.SetPitchBendRange(2); // ±2 semitones

// MPE standard
mSynth.SetPitchBendRange(48); // ±48 semitones

// Roli Seaboard default  
mSynth.SetPitchBendRange(24); // ±24 semitones
In the IPlugInstrument example, this is controllable from the UI:
IPlugInstrument.cpp:160
bool IPlugInstrument::OnMessage(int msgTag, int ctrlTag, int dataSize, 
                                const void* pData)
{
  if(ctrlTag == kCtrlTagBender && 
     msgTag == IWheelControl::kMessageTagSetPitchBendRange)
  {
    const int bendRange = *static_cast<const int*>(pData);
    mDSP.mSynth.SetPitchBendRange(bendRange);
  }
  
  return false;
}

Voice Implementation

Voices receive per-note expression through control inputs:
IPlugInstrument_DSP.h:53
void ProcessSamplesAccumulating(sample** inputs, sample** outputs, 
                                int nInputs, int nOutputs,
                                int startIdx, int nFrames) override
{
  // Read per-note controls
  double pitch = mInputs[kVoiceControlPitch].endValue;
  double pitchBend = mInputs[kVoiceControlPitchBend].endValue;  // MPE!
  double pressure = mInputs[kVoiceControlPressure].endValue;     // MPE!
  double timbre = mInputs[kVoiceControlTimbre].endValue;         // MPE!
  
  // Convert to frequency (1v/oct space)
  double freq = 440. * pow(2., pitch + pitchBend);
  
  for(int i = startIdx; i < startIdx + nFrames; i++)
  {
    // Use pressure and timbre for expression
    sample filterCutoff = timbre * 10000.0;
    sample amplitude = pressure * mGain;
    
    outputs[0][i] += ProcessVoice(freq, filterCutoff) * amplitude;
    outputs[1][i] = outputs[0][i];
  }
}

Voice Control Inputs

The SynthVoice base class provides these control inputs:
Available Voice Controls
enum EVoiceControlChannel
{
  kVoiceControlGate,        // Note on/off
  kVoiceControlPitch,       // Base pitch (MIDI note number in 1v/oct)
  kVoiceControlVelocity,    // Note-on velocity
  kVoiceControlPressure,    // Channel pressure (MPE)
  kVoiceControlTimbre,      // CC74 (MPE)
  kVoiceControlPitchBend,   // Per-note pitch bend (MPE)
  kNumVoiceControlChannels
};

Sample-Accurate Control

For smooth modulation, read control ramps:
IPlugInstrument_DSP.h:61
// Get entire ramp for sample-accurate changes
mInputs[kVoiceControlTimbre].Write(mTimbreBuffer.Get(), startIdx, nFrames);

for(int i = startIdx; i < startIdx + nFrames; i++)
{
  sample timbre = mTimbreBuffer.Get()[i - startIdx];
  // Use sample-accurate timbre value
}

Host Support

MPE support varies by host:

Full Support

  • Bitwig Studio
  • Logic Pro
  • Ableton Live 11+
  • Reaper

Limited/None

  • Pro Tools (AAX)
  • Some older hosts

Testing MPE

1

Enable MPE in config.h

#define PLUG_DOES_MPE 1
2

Initialize MPE mode

mSynth.InitBasicMPE();
3

Test with virtual controller

Use Bitwig’s virtual MPE controller or Roli Dashboard for testing without hardware
4

Verify per-note expression

Check that pitch bend, pressure, and timbre work independently per note

MPE Configuration Messages

Hosts send MPE Configuration Messages (MCM) to configure zones:
RPN 6 (MPE Configuration)
- Data: Number of member channels
- Lower zone: Channel 1
- Upper zone: Channel 16
The MidiSynth class handles these automatically when MPE is enabled.

Best Practices

Allocate enough voices for MPE:
// 15 member channels + overhead
for(int i = 0; i < 16; i++)
  mSynth.AddVoice(new Voice(), 0);

Fallback for Non-MPE

Support both MPE and standard MIDI:
Dual-Mode Support
void SetMPEMode(bool enable)
{
  if(enable)
  {
    mSynth.InitBasicMPE();
    mSynth.SetPitchBendRange(48);
  }
  else
  {
    mSynth.SetMPEZones(0, 0); // Disable MPE
    mSynth.SetPitchBendRange(2);
  }
}

See Also

Test with a virtual MPE controller before investing in hardware. Bitwig Studio includes an excellent MPE test keyboard.

Build docs developers (and LLMs) love