Skip to main content
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:
IPlugInstrument.h
enum EParams
{
  kParamGain = 0,
  kParamNoteGlideTime,
  kParamAttack,
  kParamDecay,
  kParamSustain,
  kParamRelease,
  kParamLFOShape,
  kParamLFORateHz,
  kParamLFORateTempo,
  kParamLFORateMode,
  kParamLFODepth,
  kNumParams
};

Parameter Categories

  • Gain: Master output level (0-100%)
  • Note Glide Time: Portamento/glide between notes (0-30ms)

Parameter Initialization

The constructor shows various parameter initialization techniques:
IPlugInstrument.cpp
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:
IPlugInstrument.cpp
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
  );
};

Control Tags for Data Sending

Controls that receive realtime data are tagged:
IPlugInstrument.h
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:
IPlugInstrument.cpp
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
}
1

Filter Messages

Check message status to handle only relevant MIDI messages
2

Pass to DSP

Forward valid messages to the DSP engine’s voice allocator
3

Send Through

Echo messages back to host for MIDI monitoring/recording

Audio Processing

The main audio processing delegates to the DSP engine:
IPlugInstrument.cpp
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:
IPlugInstrument.h
IPeakAvgSender<2> mMeterSender;
ISender<1> mLFOVisSender;
IPlugInstrument.cpp
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:
IPlugInstrument_DSP.h
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

1

Trigger

Called when a note-on is received, starts the envelope
2

ProcessSamplesAccumulating

Generates audio samples while the voice is active
3

Release

Called on note-off, begins envelope release phase
4

GetBusy

Returns false when envelope completes, voice can be reused

Parameter Handling

Parameter changes are forwarded to the DSP engine:
IPlugInstrument.cpp
void IPlugInstrument::OnParamChange(int paramIdx)
{
  mDSP.SetParam(paramIdx, GetParam(paramIdx)->Value());
}
The DSP engine updates internal state:
IPlugInstrument_DSP.h
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:
IPlugInstrument.cpp
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:
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