Skip to main content

Overview

The iPlug2 MIDI system provides lightweight structs for handling MIDI messages (IMidiMsg), System Exclusive messages (ISysEx), and a fast queue implementation (IMidiQueue) for sample-accurate MIDI processing. Header: IPlug/IPlugMidi.h
MIDI messages include a sample offset (mOffset) for sample-accurate timing within audio blocks.

IMidiMsg

Encapsulates a MIDI message with helper functions for creation and parsing.

Structure

struct IMidiMsg
{
  int mOffset;        // Sample offset in block
  uint8_t mStatus;    // Status byte
  uint8_t mData1;     // Data byte 1
  uint8_t mData2;     // Data byte 2
};
mOffset
int
Sample offset within the audio block where this message occurs
mStatus
uint8_t
MIDI status byte (includes message type and channel)
mData1
uint8_t
First data byte (meaning depends on message type)
mData2
uint8_t
Second data byte (meaning depends on message type)

Constructor

IMidiMsg(int offset = 0, uint8_t status = 0, uint8_t data1 = 0, uint8_t data2 = 0);
offset
int
Sample offset in block (default: 0)
status
uint8_t
Status byte (default: 0)
data1
uint8_t
Data byte 1 (default: 0)
data2
uint8_t
Data byte 2 (default: 0)

Enumerations

EStatusMsg

Constants for MIDI message types.
enum EStatusMsg
{
  kNone = 0,
  kNoteOff = 8,
  kNoteOn = 9,
  kPolyAftertouch = 10,
  kControlChange = 11,
  kProgramChange = 12,
  kChannelAftertouch = 13,
  kPitchWheel = 14
};

EControlChangeMsg

Constants for common MIDI CC messages.
enum EControlChangeMsg
{
  kNoCC = -1,
  kModWheel = 1,
  kBreathController = 2,
  kFootController = 4,
  kPortamentoTime = 5,
  kChannelVolume = 7,
  kBalance = 8,
  kPan = 10,
  kExpressionController = 11,
  // ... (many more standard CCs)
  kSustainOnOff = 64,
  kPortamentoOnOff = 65,
  kSustenutoOnOff = 66,
  kSoftPedalOnOff = 67,
  kLegatoOnOff = 68,
  kResonance = 71,
  kReleaseTime = 72,
  kAttackTime = 73,
  kCutoffFrequency = 74,
  kDecayTime = 75,
  kAllNotesOff = 123
};

Message Creation

MakeNoteOnMsg

void MakeNoteOnMsg(int noteNumber, int velocity, int offset, int channel = 0);
Create a Note On message.
noteNumber
int
required
MIDI note number [0-127] (60 = Middle C)
velocity
int
required
Note velocity [0-127] (0 = note off in some contexts)
offset
int
required
Sample offset in block
channel
int
MIDI channel [0-15] (default: 0 = channel 1)
// Example: Trigger note at start of block
IMidiMsg msg;
msg.MakeNoteOnMsg(60, 100, 0, 0); // Middle C, velocity 100
SendMidiMsg(msg);

MakeNoteOffMsg

void MakeNoteOffMsg(int noteNumber, int offset, int channel = 0);
Create a Note Off message.
noteNumber
int
required
MIDI note number [0-127]
offset
int
required
Sample offset in block
channel
int
MIDI channel [0-15]

MakePitchWheelMsg

void MakePitchWheelMsg(double value, int channel = 0, int offset = 0);
Create a pitch wheel/bend message.
value
double
required
Normalized pitch bend [-1.0, 1.0] where 0 = no bend
channel
int
MIDI channel [0-15]
offset
int
Sample offset in block
// Example: Pitch bend up one semitone
IMidiMsg msg;
msg.MakePitchWheelMsg(0.5, 0, 0); // Half range up
SendMidiMsg(msg);

MakeControlChangeMsg

void MakeControlChangeMsg(EControlChangeMsg idx, double value, int channel = 0, int offset = 0);
Create a CC (Control Change) message.
idx
EControlChangeMsg
required
Controller index
value
double
required
Normalized value [0.0, 1.0]
channel
int
MIDI channel [0-15]
offset
int
Sample offset in block
// Example: Send mod wheel CC
IMidiMsg msg;
msg.MakeControlChangeMsg(IMidiMsg::kModWheel, 0.75, 0, 0);
SendMidiMsg(msg);

MakeProgramChange

void MakeProgramChange(int program, int channel = 0, int offset = 0);
Create a Program Change message.
program
int
required
Program number [0-127]
channel
int
MIDI channel [0-15]
offset
int
Sample offset in block

MakeChannelATMsg

void MakeChannelATMsg(int pressure, int offset, int channel);
Create a Channel AfterTouch message.
pressure
int
required
Pressure value [0-127]
offset
int
required
Sample offset in block
channel
int
required
MIDI channel [0-15]

MakePolyATMsg

void MakePolyATMsg(int noteNumber, int pressure, int offset, int channel);
Create a Polyphonic AfterTouch message.
noteNumber
int
required
Note number [0-127]
pressure
int
required
Pressure value [0-127]
offset
int
required
Sample offset in block
channel
int
required
MIDI channel [0-15]

Message Parsing

Channel

int Channel() const;
Get the MIDI channel from the status byte.
return
int
Channel [0-15] for MIDI channels 1-16

StatusMsg

EStatusMsg StatusMsg() const;
Get the MIDI message type.
return
EStatusMsg
Message type enum value (kNoteOn, kNoteOff, etc.)

NoteNumber

int NoteNumber() const;
Get the note number from Note On/Off or Poly AT messages.
return
int
Note number [0-127], or -1 if not applicable

Velocity

int Velocity() const;
Get the velocity from Note On/Off messages.
return
int
Velocity [0-127], or -1 if not applicable

PolyAfterTouch

int PolyAfterTouch() const;
Get pressure from Polyphonic AfterTouch message.
return
int
Pressure [0-127], or -1 if not applicable

ChannelAfterTouch

int ChannelAfterTouch() const;
Get pressure from Channel AfterTouch message.
return
int
Pressure [0-127], or -1 if not applicable

Program

int Program() const;
Get program number from Program Change message.
return
int
Program [0-127], or -1 if not applicable

PitchWheel

double PitchWheel() const;
Get pitch wheel value.
return
double
Normalized pitch wheel [-1.0, 1.0], or 0.0 if not applicable

ControlChangeIdx

EControlChangeMsg ControlChangeIdx() const;
Get the controller index from a CC message.
return
EControlChangeMsg
Controller index enum

ControlChange

double ControlChange(EControlChangeMsg idx) const;
Get the value of a specific CC message.
idx
EControlChangeMsg
required
Controller index to check
return
double
Normalized value [0.0, 1.0], or -1.0 if message doesn’t match

ControlChangeOnOff

static bool ControlChangeOnOff(double msgValue);
Helper to interpret CC as boolean.
msgValue
double
required
Normalized CC value [0.0, 1.0]
return
bool
true if value >= 0.5 (on)
// Example: Parse sustain pedal
if (msg.StatusMsg() == IMidiMsg::kControlChange)
{
  double sustainValue = msg.ControlChange(IMidiMsg::kSustainOnOff);
  if (sustainValue >= 0.0)
  {
    bool sustainOn = IMidiMsg::ControlChangeOnOff(sustainValue);
    mSynth.SetSustain(sustainOn);
  }
}

Utility Methods

Clear

void Clear();
Clear all message data (sets all fields to 0).

LogMsg

void LogMsg();
Log the message to trace output (TRACER builds only).

PrintMsg

void PrintMsg() const;
Print the message to debug output (DEBUG builds only).

StatusMsgStr

static const char* StatusMsgStr(EStatusMsg msg);
Get a string description of a status message type.
msg
EStatusMsg
required
Status message enum value
return
const char*
String like “noteon”, “noteoff”, “controlchange”, etc.

CCNameStr

static const char* CCNameStr(int idx);
Get the name of a MIDI CC controller.
idx
int
required
CC index [0-127]
return
const char*
Controller name like “Modulation”, “Pan”, “MainVolume”, etc.

ISysEx

Handles MIDI System Exclusive messages. Does not own the data.

Structure

struct ISysEx
{
  int mOffset;              // Sample offset in block
  int mSize;                // Size in bytes
  const uint8_t* mData;     // Pointer to data (not owned)
};

Constructor

ISysEx(int offset = 0, const uint8_t* pData = nullptr, int size = 0);
offset
int
Sample offset in block
pData
const uint8_t*
Pointer to SysEx data (must remain valid)
size
int
Size of data in bytes
ISysEx does not own the data. The data pointer must remain valid while the ISysEx object is in use.

Clear

void Clear();
Clear the data pointer and size (does not modify external data).

SysExStr

char* SysExStr(char* str, int maxLen, const uint8_t* pData, int size);
Convert SysEx bytes to hex string representation.

LogMsg

void LogMsg();
Log the SysEx message (TRACER builds only).

IMidiQueue

Fast, sample-accurate MIDI queue implementation.
template <class T>
class IMidiQueueBase { /* ... */ };

using IMidiQueue = IMidiQueueBase<IMidiMsg>;

Constructor

IMidiQueue(int size = DEFAULT_BLOCK_SIZE);
size
int
Initial queue size (default: 512)

Add

void Add(const IMidiMsg& msg);
Add a MIDI message to the queue. Automatically sorted by offset.
msg
const IMidiMsg&
required
MIDI message to add
Messages are automatically sorted by mOffset for sample-accurate playback.

Peek

IMidiMsg& Peek() const;
Get the next message without removing it.
return
IMidiMsg&
Reference to the next message in the queue

Remove

inline void Remove();
Remove the front message from the queue.

Empty

inline bool Empty() const;
Check if the queue is empty.
return
bool
true if queue is empty

ToDo

inline int ToDo() const;
Get the number of messages in the queue.
return
int
Number of queued messages

Flush

inline void Flush(int nFrames);
Flush the queue and update offsets for the next block.
nFrames
int
required
Number of frames processed (subtracted from message offsets)
Call at the end of ProcessBlock() to prepare for the next block.

Clear

inline void Clear();
Clear all messages from the queue.

Resize

int Resize(int size);
Resize the queue.
size
int
required
New size (will not shrink below current message count)
return
int
Actual new size

GetSize

inline int GetSize() const;
Get the allocated queue size.

Complete Example

class MySynth : public iplug::Plugin
{
public:
  MySynth(const iplug::InstanceInfo& info)
  : Plugin(info, MakeConfig(kNumParams, 1))
  {
    // Initialize...
  }
  
  void OnReset() override
  {
    mMidiQueue.Resize(GetBlockSize());
  }
  
  void ProcessMidiMsg(const IMidiMsg& msg) override
  {
    mMidiQueue.Add(msg);
  }
  
  void ProcessBlock(sample** inputs, sample** outputs, int nFrames) override
  {
    // Clear output
    for (int c = 0; c < NOutChansConnected(); c++)
      memset(outputs[c], 0, nFrames * sizeof(sample));
    
    for (int s = 0; s < nFrames; s++)
    {
      // Process MIDI at correct sample offset
      while (!mMidiQueue.Empty())
      {
        IMidiMsg& msg = mMidiQueue.Peek();
        if (msg.mOffset > s) break;
        
        // Parse MIDI message
        switch (msg.StatusMsg())
        {
          case IMidiMsg::kNoteOn:
          {
            int note = msg.NoteNumber();
            int velocity = msg.Velocity();
            
            if (velocity > 0)
            {
              // Note on
              double freq = 440.0 * pow(2.0, (note - 69) / 12.0);
              mVoiceManager.NoteOn(note, velocity, freq);
            }
            else
            {
              // Note on with velocity 0 = note off
              mVoiceManager.NoteOff(note);
            }
            break;
          }
          
          case IMidiMsg::kNoteOff:
          {
            mVoiceManager.NoteOff(msg.NoteNumber());
            break;
          }
          
          case IMidiMsg::kPitchWheel:
          {
            double bend = msg.PitchWheel();
            mVoiceManager.SetPitchBend(bend);
            break;
          }
          
          case IMidiMsg::kControlChange:
          {
            // Check for sustain pedal
            double sustainVal = msg.ControlChange(IMidiMsg::kSustainOnOff);
            if (sustainVal >= 0.0)
            {
              bool sustain = IMidiMsg::ControlChangeOnOff(sustainVal);
              mVoiceManager.SetSustain(sustain);
            }
            
            // Check for mod wheel
            double modVal = msg.ControlChange(IMidiMsg::kModWheel);
            if (modVal >= 0.0)
            {
              mLFO.SetDepth(modVal);
            }
            break;
          }
          
          case IMidiMsg::kChannelAftertouch:
          {
            int pressure = msg.ChannelAfterTouch();
            mVoiceManager.SetAftertouch(pressure / 127.0);
            break;
          }
        }
        
        mMidiQueue.Remove();
      }
      
      // Generate audio for this sample
      sample output = mVoiceManager.ProcessSample();
      
      // Write to all output channels
      for (int c = 0; c < NOutChansConnected(); c++)
        outputs[c][s] = output;
    }
    
    // Prepare queue for next block
    mMidiQueue.Flush(nFrames);
  }
  
  void ProcessSysEx(const ISysEx& msg) override
  {
    // Handle SysEx if needed
    DBGMSG("Received SysEx: %d bytes\n", msg.mSize);
  }
  
private:
  IMidiQueue mMidiQueue;
  VoiceManager mVoiceManager;
  LFO mLFO;
};

See Also