IPlugInstrument is a full-featured polyphonic synthesizer example that demonstrates MIDI processing, voice allocation, envelope generators, LFO modulation, and realtime audio visualization.
What You’ll Learn
MIDI Processing Handle note on/off, pitch bend, and MPE messages
Voice Management Polyphonic synthesis with voice allocation
Modulation ADSR envelopes and tempo-synced LFO
Data Visualization Send audio data from DSP to UI thread safely
Project Structure
Examples/IPlugInstrument/
├── IPlugInstrument.h # Plugin class and parameter enum
├── IPlugInstrument.cpp # Implementation and UI
├── IPlugInstrument_DSP.h # Synthesizer DSP engine
├── config.h # Plugin metadata
└── resources/ # Assets
This example separates DSP logic into a dedicated header file for better organization.
Parameters Overview
The synthesizer includes 11 parameters for sound shaping:
enum EParams
{
kParamGain = 0 ,
kParamNoteGlideTime ,
kParamAttack ,
kParamDecay ,
kParamSustain ,
kParamRelease ,
kParamLFOShape ,
kParamLFORateHz ,
kParamLFORateTempo ,
kParamLFORateMode ,
kParamLFODepth ,
kNumParams
};
Parameter Categories
Gain & Glide
ADSR Envelope
LFO Modulation
Gain : Master output level (0-100%)
Note Glide Time : Portamento/glide between notes (0-30ms)
Attack : Envelope attack time (1-1000ms)
Decay : Envelope decay time (1-1000ms)
Sustain : Envelope sustain level (0-100%)
Release : Envelope release time (2-1000ms)
LFO Shape : Waveform (Triangle, Sine, Square, etc.)
LFO Rate Hz : Free-running rate (0.01-40 Hz)
LFO Rate Tempo : Tempo-synced rate (1/1, 1/2, 1/4, etc.)
LFO Sync : Toggle between Hz and tempo sync
LFO Depth : Modulation amount (0-100%)
Parameter Initialization
The constructor shows various parameter initialization techniques:
IPlugInstrument :: IPlugInstrument ( const InstanceInfo & info)
: iplug :: Plugin (info, MakeConfig (kNumParams, kNumPresets))
{
GetParam (kParamGain)-> InitDouble ( "Gain" , 100. , 0. , 100.0 , 0.01 , "%" );
GetParam (kParamNoteGlideTime)-> InitMilliseconds ( "Note Glide Time" , 0. , 0.0 , 30. );
// ADSR with power curve shaping for natural feel
GetParam (kParamAttack)-> InitDouble ( "Attack" , 10. , 1. , 1000. , 0.1 , "ms" ,
IParam ::kFlagsNone, "ADSR" , IParam :: ShapePowCurve ( 3. ));
GetParam (kParamDecay)-> InitDouble ( "Decay" , 10. , 1. , 1000. , 0.1 , "ms" ,
IParam ::kFlagsNone, "ADSR" , IParam :: ShapePowCurve ( 3. ));
GetParam (kParamSustain)-> InitDouble ( "Sustain" , 50. , 0. , 100. , 1 , "%" ,
IParam ::kFlagsNone, "ADSR" );
GetParam (kParamRelease)-> InitDouble ( "Release" , 10. , 2. , 1000. , 0.1 , "ms" ,
IParam ::kFlagsNone, "ADSR" );
// LFO parameters
GetParam (kParamLFOShape)-> InitEnum ( "LFO Shape" , LFO <>::kTriangle, {LFO_SHAPE_VALIST});
GetParam (kParamLFORateHz)-> InitFrequency ( "LFO Rate" , 1. , 0.01 , 40. );
GetParam (kParamLFORateTempo)-> InitEnum ( "LFO Rate" , LFO <>::k1, {LFO_TEMPODIV_VALIST});
GetParam (kParamLFORateMode)-> InitBool ( "LFO Sync" , true );
GetParam (kParamLFODepth)-> InitPercentage ( "LFO Depth" );
}
Use IParam::ShapePowCurve() to create non-linear parameter curves that feel more natural for time-based parameters.
UI Construction
The UI includes multiple control types and interactive elements:
mLayoutFunc = [ & ]( IGraphics * pGraphics ) {
pGraphics -> AttachCornerResizer ( EUIResizerMode ::Scale, false );
pGraphics -> AttachPanelBackground (COLOR_GRAY);
pGraphics -> EnableMouseOver ( true );
pGraphics -> EnableMultiTouch ( true );
pGraphics -> LoadFont ( "Roboto-Regular" , ROBOTO_FN);
const IRECT b = pGraphics -> GetBounds (). GetPadded ( - 20. f );
// Keyboard at bottom
IRECT keyboardBounds = b . GetFromBottom ( 300 );
IRECT wheelsBounds = keyboardBounds . ReduceFromLeft ( 100. f ). GetPadded ( - 10. f );
pGraphics -> AttachControl (
new IVKeyboardControl (keyboardBounds),
kCtrlTagKeyboard
);
// Pitch bend and mod wheels
pGraphics -> AttachControl (
new IWheelControl ( wheelsBounds . FracRectHorizontal ( 0.5 )),
kCtrlTagBender
);
pGraphics -> AttachControl (
new IWheelControl (
wheelsBounds . FracRectHorizontal ( 0.5 , true ),
IMidiMsg :: EControlChangeMsg ::kModWheel
)
);
// Control knobs
const IRECT controls = b . GetGridCell ( 1 , 2 , 2 );
pGraphics -> AttachControl (
new IVKnobControl (
controls . GetGridCell ( 0 , 2 , 6 ). GetCentredInside ( 90 ),
kParamGain,
"Gain"
)
);
pGraphics -> AttachControl (
new IVKnobControl (
controls . GetGridCell ( 1 , 2 , 6 ). GetCentredInside ( 90 ),
kParamNoteGlideTime,
"Glide"
)
);
// ADSR sliders
const IRECT sliders = controls . GetGridCell ( 2 , 2 , 6 )
. Union ( controls . GetGridCell ( 3 , 2 , 6 ))
. Union ( controls . GetGridCell ( 4 , 2 , 6 ));
pGraphics -> AttachControl (
new IVSliderControl (
sliders . GetGridCell ( 0 , 1 , 4 ). GetMidHPadded ( 30. ),
kParamAttack,
"Attack"
)
);
pGraphics -> AttachControl (
new IVSliderControl (
sliders . GetGridCell ( 1 , 1 , 4 ). GetMidHPadded ( 30. ),
kParamDecay,
"Decay"
)
);
pGraphics -> AttachControl (
new IVSliderControl (
sliders . GetGridCell ( 2 , 1 , 4 ). GetMidHPadded ( 30. ),
kParamSustain,
"Sustain"
)
);
pGraphics -> AttachControl (
new IVSliderControl (
sliders . GetGridCell ( 3 , 1 , 4 ). GetMidHPadded ( 30. ),
kParamRelease,
"Release"
)
);
// Level meter
pGraphics -> AttachControl (
new IVLEDMeterControl < 2 >(
controls . GetFromRight ( 100 ). GetPadded ( - 30 )
),
kCtrlTagMeter
);
};
Controls that receive realtime data are tagged:
enum EControlTags
{
kCtrlTagMeter = 0 ,
kCtrlTagLFOVis ,
kCtrlTagScope ,
kCtrlTagRTText ,
kCtrlTagKeyboard ,
kCtrlTagBender ,
kNumCtrlTags
};
Control tags allow you to reference UI elements from the DSP thread to send visualization data.
MIDI Processing
The plugin processes MIDI messages in ProcessMidiMsg:
void IPlugInstrument :: ProcessMidiMsg ( const IMidiMsg & msg )
{
int status = msg . StatusMsg ();
switch (status)
{
case IMidiMsg ::kNoteOn:
case IMidiMsg ::kNoteOff:
case IMidiMsg ::kPolyAftertouch:
case IMidiMsg ::kControlChange:
case IMidiMsg ::kProgramChange:
case IMidiMsg ::kChannelAftertouch:
case IMidiMsg ::kPitchWheel:
{
goto handle ;
}
default :
return ;
}
handle :
mDSP . ProcessMidiMsg (msg);
SendMidiMsg (msg); // Echo to host for MIDI-through
}
Filter Messages
Check message status to handle only relevant MIDI messages
Pass to DSP
Forward valid messages to the DSP engine’s voice allocator
Send Through
Echo messages back to host for MIDI monitoring/recording
Audio Processing
The main audio processing delegates to the DSP engine:
void IPlugInstrument :: ProcessBlock ( sample ** inputs , sample ** outputs , int nFrames )
{
// Process synthesizer voices
mDSP . ProcessBlock ( nullptr , outputs, 2 , nFrames,
mTimeInfo . mPPQPos ,
mTimeInfo . mTransportIsRunning );
// Send meter data to UI
mMeterSender . ProcessBlock (outputs, nFrames, kCtrlTagMeter);
// Send LFO visualization data
mLFOVisSender . PushData ({
kCtrlTagLFOVis,
{ float ( mDSP . mLFO . GetLastOutput ())}
});
}
Data Transmission to UI
The example uses ISender classes to safely pass data to the UI thread:
IPeakAvgSender < 2 > mMeterSender;
ISender < 1 > mLFOVisSender;
void IPlugInstrument :: OnIdle ()
{
mMeterSender . TransmitData ( * this );
mLFOVisSender . TransmitData ( * this );
}
Always use ISender classes to pass data from the audio thread to the UI. Direct UI manipulation from ProcessBlock is not thread-safe.
DSP Engine Architecture
The synthesizer engine is defined in a separate header:
template < typename T >
class IPlugInstrumentDSP
{
public:
class Voice : public SynthVoice
{
public:
Voice ()
: mAMPEnv ( "gain" , [ & ](){ mOSC . Reset (); })
{}
bool GetBusy () const override
{
return mAMPEnv . GetBusy ();
}
void Trigger ( double level , bool isRetrigger ) override
{
mOSC . Reset ();
if (isRetrigger)
mAMPEnv . Retrigger (level);
else
mAMPEnv . Start (level);
}
void Release () override
{
mAMPEnv . Release ();
}
void ProcessSamplesAccumulating ( T ** inputs , T ** outputs ,
int nInputs , int nOutputs ,
int startIdx , int nFrames ) override
{
double pitch = mInputs [kVoiceControlPitch]. endValue ;
double pitchBend = mInputs [kVoiceControlPitchBend]. endValue ;
// Get timbre control with sample-accurate ramps
mInputs [kVoiceControlTimbre]. Write ( mTimbreBuffer . Get (), startIdx, nFrames);
// Convert pitch to frequency
double osc1Freq = 440. * pow ( 2. , pitch + pitchBend + inputs [kModLFO][ 0 ]);
// Generate audio
for ( auto i = startIdx; i < startIdx + nFrames; i ++ )
{
float noise = mTimbreBuffer . Get ()[i] * Rand ();
outputs [ 0 ][i] += ( mOSC . Process (osc1Freq) + noise) *
mAMPEnv . Process ( inputs [kModSustainSmoother][i]) *
mGain;
outputs [ 1 ][i] = outputs [ 0 ][i];
}
}
public:
FastSinOscillator < T > mOSC;
ADSREnvelope < T > mAMPEnv;
};
MidiSynth mSynth { VoiceAllocator ::kPolyModePoly, MidiSynth ::kDefaultBlockSize };
LFO < T > mLFO;
};
Voice Lifecycle
Trigger
Called when a note-on is received, starts the envelope
ProcessSamplesAccumulating
Generates audio samples while the voice is active
Release
Called on note-off, begins envelope release phase
GetBusy
Returns false when envelope completes, voice can be reused
Parameter Handling
Parameter changes are forwarded to the DSP engine:
void IPlugInstrument :: OnParamChange ( int paramIdx )
{
mDSP . SetParam (paramIdx, GetParam (paramIdx)-> Value ());
}
The DSP engine updates internal state:
void SetParam ( int paramIdx , double value )
{
using EEnvStage = ADSREnvelope < sample >:: EStage ;
switch (paramIdx) {
case kParamNoteGlideTime:
mSynth . SetNoteGlideTime (value / 1000. );
break ;
case kParamGain:
mParamsToSmooth [kModGainSmoother] = (T) value / 100. ;
break ;
case kParamSustain:
mParamsToSmooth [kModSustainSmoother] = (T) value / 100. ;
break ;
case kParamAttack:
case kParamDecay:
case kParamRelease:
{
EEnvStage stage = static_cast < EEnvStage > (
EEnvStage ::kAttack + (paramIdx - kParamAttack)
);
mSynth . ForEachVoice ([ stage , value ]( SynthVoice & voice ) {
dynamic_cast < IPlugInstrumentDSP ::Voice &> (voice)
. mAMPEnv . SetStageTime (stage, value);
});
break ;
}
case kParamLFODepth:
mLFO . SetScalar (value / 100. );
break ;
case kParamLFORateHz:
mLFO . SetFreqCPS (value);
break ;
// ... more cases
}
}
Use parameter smoothing for audio-rate parameters like gain to avoid zipper noise.
Dynamic UI Updates
Some parameters trigger UI changes:
void IPlugInstrument :: OnParamChangeUI ( int paramIdx , EParamSource source )
{
#if IPLUG_EDITOR
if ( auto pGraphics = GetUI ())
{
if (paramIdx == kParamLFORateMode)
{
const auto sync = GetParam (kParamLFORateMode)-> Bool ();
pGraphics -> HideControl (kParamLFORateHz, sync);
pGraphics -> HideControl (kParamLFORateTempo, ! sync);
}
}
#endif
}
This hides/shows different LFO rate controls based on sync mode.
Plugin Configuration
Key settings in config.h:
#define PLUG_TYPE 1 // Instrument
#define PLUG_DOES_MIDI_IN 1 // Receives MIDI
#define PLUG_DOES_MPE 1 // MPE support
#define PLUG_CHANNEL_IO "0-2" // No input, stereo output
#define VST3_SUBCATEGORY "Instrument|Synth"
#define AAX_PLUG_CATEGORY_STR "Synth"
Key Features Demonstrated
Polyphonic Voices Voice allocation with 16 voices, proper note stealing
ADSR Envelopes Per-voice amplitude envelopes with customizable curves
LFO Modulation Tempo-synced or free-running LFO with multiple shapes
MPE Support MIDI Polyphonic Expression for per-note pitch bend and pressure
Pitch Bend Smooth pitch bend with configurable range
Visualizations Realtime meter and LFO display
Next Steps
IPlugControls Explore all available UI controls
MidiSynth Class Learn about voice allocation
IGraphics Guide Master UI construction