Overview
MIDI effect plugins process MIDI messages without generating audio. They’re used for arpeggiators, chord generators, transposers, MIDI monitors, and more.IPlugMidiEffect Template
TheIPlugMidiEffect example demonstrates:
- MIDI pass-through processing
- MIDI message generation from UI
- Zero audio channels (MIDI-only)
- Button to trigger chords
Project Configuration
config.h
MIDI Effect Configuration:
PLUG_TYPE 2identifies it as MIDI effectPLUG_CHANNEL_IO "0-0"= no audio input or output- Still needs
PLUG_DOES_MIDI_INandPLUG_DOES_MIDI_OUT
Basic Structure
- Header
- MIDI Processing
- UI Trigger
IPlugMidiEffect.h
Creating a MIDI Effect
#define PLUG_TYPE 2
#define PLUG_DOES_MIDI_IN 1
#define PLUG_DOES_MIDI_OUT 1
#define PLUG_CHANNEL_IO "0-0"
// Some MIDI effects may want a tail for note-offs
#define PLUG_TAIL_SIZE 4410000 // ~100 seconds at 44.1kHz
enum EParams
{
kArpMode = 0, // Up, Down, Up/Down, Random
kArpRate, // Note rate
kArpGate, // Note length
kArpOctaves, // Octave range
kArpSwing, // Timing swing
kNumParams
};
MyArpeggiator::MyArpeggiator(const InstanceInfo& info)
: Plugin(info, MakeConfig(kNumParams, kNumPresets))
{
GetParam(kArpMode)->InitEnum("Mode", 0,
{"Up", "Down", "Up/Down", "Random"});
GetParam(kArpRate)->InitEnum("Rate", 2,
{"1/32", "1/16", "1/8", "1/4", "1/2", "1/1"});
GetParam(kArpGate)->InitPercentage("Gate", 80.);
GetParam(kArpOctaves)->InitInt("Octaves", 1, 1, 4);
GetParam(kArpSwing)->InitPercentage("Swing", 0.);
#if IPLUG_DSP
SetTailSize(4410000); // Allow long tail for note-offs
#endif
}
class Arpeggiator
{
public:
void NoteOn(int note, int velocity) {
// Add to held notes
if (std::find(mHeldNotes.begin(), mHeldNotes.end(), note) == mHeldNotes.end()) {
mHeldNotes.push_back(note);
SortNotes();
}
}
void NoteOff(int note) {
// Remove from held notes
auto it = std::find(mHeldNotes.begin(), mHeldNotes.end(), note);
if (it != mHeldNotes.end()) {
mHeldNotes.erase(it);
}
}
void ProcessBlock(int nFrames, double tempo, double ppqPos, bool playing) {
if (!playing || mHeldNotes.empty()) return;
// Calculate note rate based on tempo
const double beatsPerSample = (tempo / 60.0) / mSampleRate;
const double noteRate = GetNoteRate(); // e.g., 0.25 for 1/4 note
for (int s = 0; s < nFrames; s++) {
mPhase += beatsPerSample;
if (mPhase >= noteRate) {
mPhase -= noteRate;
// Stop previous note
if (mCurrentNote >= 0) {
IMidiMsg noteOff;
noteOff.MakeNoteOffMsg(mCurrentNote, 0, s);
mOutputQueue.push(noteOff);
}
// Start next note
mCurrentNote = GetNextNote();
IMidiMsg noteOn;
noteOn.MakeNoteOnMsg(mCurrentNote, mCurrentVelocity, 0, s);
mOutputQueue.push(noteOn);
}
// Calculate gate (note-off timing)
const double gatePhase = mPhase + (noteRate * mGate);
if (gatePhase >= noteRate && mCurrentNote >= 0) {
IMidiMsg noteOff;
noteOff.MakeNoteOffMsg(mCurrentNote, 0, s);
mOutputQueue.push(noteOff);
mCurrentNote = -1;
}
}
}
int GetNextNote() {
switch (mMode) {
case kUp:
mCurrentIndex = (mCurrentIndex + 1) % mHeldNotes.size();
break;
case kDown:
mCurrentIndex = (mCurrentIndex - 1 + mHeldNotes.size()) % mHeldNotes.size();
break;
case kRandom:
mCurrentIndex = rand() % mHeldNotes.size();
break;
}
return mHeldNotes[mCurrentIndex];
}
private:
std::vector<int> mHeldNotes;
int mCurrentIndex = 0;
int mCurrentNote = -1;
int mCurrentVelocity = 100;
double mPhase = 0.0;
double mGate = 0.8;
int mMode = 0;
double mSampleRate = 44100.0;
std::queue<IMidiMsg> mOutputQueue;
};
void MyArpeggiator::ProcessMidiMsg(const IMidiMsg& msg)
{
switch (msg.StatusMsg()) {
case IMidiMsg::kNoteOn:
if (msg.Velocity() > 0) {
mArp.NoteOn(msg.NoteNumber(), msg.Velocity());
} else {
mArp.NoteOff(msg.NoteNumber());
}
break;
case IMidiMsg::kNoteOff:
mArp.NoteOff(msg.NoteNumber());
break;
default:
// Pass through other messages
SendMidiMsg(msg);
break;
}
}
void MyArpeggiator::ProcessBlock(sample** inputs, sample** outputs, int nFrames)
{
// Generate arpeggio notes
mArp.ProcessBlock(nFrames, mTimeInfo.mTempo,
mTimeInfo.mPPQPos,
mTimeInfo.mTransportIsRunning);
// Output queued messages
while (!mArp.mOutputQueue.empty()) {
SendMidiMsg(mArp.mOutputQueue.front());
mArp.mOutputQueue.pop();
}
}
MIDI Message Types
Creating Messages
Reading Messages
Common MIDI Effect Patterns
Note Transpose
Velocity Curve
MIDI Monitor
Chord Generator
Timing and Tempo Sync
Access host timing information:Message Offset
For sample-accurate timing within a block:Tail Size
Set tail size to prevent note-offs from being cut:config.h
Testing
Testing MIDI Effects:
- Use a MIDI monitor plugin after your effect
- Test with different DAWs (Logic, Ableton, Reaper)
- Verify note-offs are sent correctly
- Check timing accuracy with metronome
Next Steps
MIDI Reference
Complete IMidiMsg API documentation
Timing
Host tempo and transport information
Testing
Validate MIDI processing in hosts
Examples
Browse more MIDI effect examples