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
};
Sample offset within the audio block where this message occurs
MIDI status byte (includes message type and channel)
First data byte (meaning depends on message type)
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);
Sample offset in block (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.
MIDI note number [0-127] (60 = Middle C)
Note velocity [0-127] (0 = note off in some contexts)
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.
MakePitchWheelMsg
void MakePitchWheelMsg(double value, int channel = 0, int offset = 0);
Create a pitch wheel/bend message.
Normalized pitch bend [-1.0, 1.0] where 0 = no bend
// 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
Normalized value [0.0, 1.0]
// 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.
MakeChannelATMsg
void MakeChannelATMsg(int pressure, int offset, int channel);
Create a Channel AfterTouch message.
MakePolyATMsg
void MakePolyATMsg(int noteNumber, int pressure, int offset, int channel);
Create a Polyphonic AfterTouch message.
Message Parsing
Channel
Get the MIDI channel from the status byte.
Channel [0-15] for MIDI channels 1-16
StatusMsg
EStatusMsg StatusMsg() const;
Get the MIDI message type.
Message type enum value (kNoteOn, kNoteOff, etc.)
NoteNumber
Get the note number from Note On/Off or Poly AT messages.
Note number [0-127], or -1 if not applicable
Velocity
Get the velocity from Note On/Off messages.
Velocity [0-127], or -1 if not applicable
PolyAfterTouch
int PolyAfterTouch() const;
Get pressure from Polyphonic AfterTouch message.
Pressure [0-127], or -1 if not applicable
ChannelAfterTouch
int ChannelAfterTouch() const;
Get pressure from Channel AfterTouch message.
Pressure [0-127], or -1 if not applicable
Program
Get program number from Program Change message.
Program [0-127], or -1 if not applicable
PitchWheel
double PitchWheel() const;
Get pitch wheel value.
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.
ControlChange
double ControlChange(EControlChangeMsg idx) const;
Get the value of a specific CC message.
idx
EControlChangeMsg
required
Controller index to check
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.
Normalized CC value [0.0, 1.0]
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
Clear all message data (sets all fields to 0).
LogMsg
Log the message to trace output (TRACER builds only).
PrintMsg
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.
Status message enum value
String like “noteon”, “noteoff”, “controlchange”, etc.
CCNameStr
static const char* CCNameStr(int idx);
Get the name of a MIDI CC controller.
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);
Pointer to SysEx data (must remain valid)
ISysEx does not own the data. The data pointer must remain valid while the ISysEx object is in use.
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
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);
Initial queue size (default: 512)
Add
void Add(const IMidiMsg& msg);
Add a MIDI message to the queue. Automatically sorted by offset.
Messages are automatically sorted by mOffset for sample-accurate playback.
Peek
Get the next message without removing it.
Reference to the next message in the queue
Remove
Remove the front message from the queue.
Empty
inline bool Empty() const;
Check if the queue is empty.
ToDo
Get the number of messages in the queue.
Number of queued messages
Flush
inline void Flush(int nFrames);
Flush the queue and update offsets for the next block.
Number of frames processed (subtracted from message offsets)
Call at the end of ProcessBlock() to prepare for the next block.
Clear
Clear all messages from the queue.
Resize
Resize the queue.
New size (will not shrink below current message count)
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