Skip to main content

ProcessBlock Overview

The ProcessBlock() method is the heart of your audio plugin. It’s called repeatedly by the host on a high-priority real-time audio thread.
void ProcessBlock(sample** inputs, sample** outputs, int nFrames) override {
  const double gain = GetParam(kGain)->Value() / 100.;
  const int nChans = NOutChansConnected();
  
  for (int s = 0; s < nFrames; s++) {
    for (int c = 0; c < nChans; c++) {
      outputs[c][s] = inputs[c][s] * gain;
    }
  }
}

Method Signature

virtual void ProcessBlock(
  sample** inputs,   // Input audio buffers (non-interleaved)
  sample** outputs,  // Output audio buffers (non-interleaved)
  int nFrames        // Number of samples per channel this block
);

Inputs

inputs[channel][sample]2D array of input buffers. Unconnected channels contain zeros.

Outputs

outputs[channel][sample]2D array of output buffers. Must write to all connected channels.

nFrames

Block size in samples per channel. Can vary between calls (typically 32-2048).

sample

Either float or double based on SAMPLE_TYPE_DOUBLE define.

Buffer Layout

Non-Interleaved (Planar) Format

iPlug2 uses non-interleaved audio buffers, where each channel is a separate array:
// Non-interleaved (iPlug2)
inputs[0][0]  // Left channel, sample 0
inputs[0][1]  // Left channel, sample 1  
inputs[0][2]  // Left channel, sample 2
inputs[1][0]  // Right channel, sample 0
inputs[1][1]  // Right channel, sample 1
inputs[1][2]  // Right channel, sample 2
NOT interleaved like raw audio files:
// Interleaved (NOT used in iPlug2)
buffer[0]  // Left, sample 0
buffer[1]  // Right, sample 0
buffer[2]  // Left, sample 1
buffer[3]  // Right, sample 1

Accessing Buffers

void ProcessBlock(sample** inputs, sample** outputs, int nFrames) {
  // Get number of connected channels
  const int nIn = NInChansConnected();
  const int nOut = NOutChansConnected();
  
  // Per-sample processing (inner loop = samples)
  for (int c = 0; c < nOut; c++) {
    for (int s = 0; s < nFrames; s++) {
      // Read from input channel c, sample s
      sample input = inputs[c][s];
      
      // Process...
      sample output = input * gain;
      
      // Write to output channel c, sample s
      outputs[c][s] = output;
    }
  }
  
  // Or per-channel processing (inner loop = channels)
  for (int s = 0; s < nFrames; s++) {
    for (int c = 0; c < nOut; c++) {
      outputs[c][s] = inputs[c][s] * gain;
    }
  }
}
Performance tip: Inner loop = samples is usually faster due to CPU cache locality.

Channel I/O

Channel I/O configuration is defined in config.h:
config.h
#define PLUG_CHANNEL_IO "1-1 2-2"  // Mono-to-mono and stereo-to-stereo

Common I/O Configurations

#define PLUG_CHANNEL_IO "2-2"
void ProcessBlock(sample** inputs, sample** outputs, int nFrames) {
  for (int s = 0; s < nFrames; s++) {
    sample left = inputs[0][s];
    sample right = inputs[1][s];
    
    // Stereo processing
    outputs[0][s] = ProcessLeft(left);
    outputs[1][s] = ProcessRight(right);
  }
}

Channel I/O String Format

"inputs-outputs" or "bus1.bus2.busN-bus1.bus2.busN"

Examples:
"1-1"        = Mono to mono
"2-2"        = Stereo to stereo
"1-1 2-2"    = Both mono and stereo (VST3 supports multiple configs)
"1.1-1"      = Mono main + mono sidechain → mono out
"2.2-2"      = Stereo main + stereo sidechain → stereo out
"0-2.2"      = No input, 2 stereo outputs (instrument)
"2-2.2.2.2"  = Stereo in, 4 stereo outputs
From IPlugProcessor.h:256-265 - ParseChannelIOStr() parses this format.

Sample Rate and Block Size

Getting Current Settings

double sampleRate = GetSampleRate();  // e.g., 44100.0, 48000.0
int blockSize = GetBlockSize();       // e.g., 512 (varies per call)
GetBlockSize() returns the maximum block size. Actual nFrames in ProcessBlock() can be smaller!

OnReset() - Initialize DSP

OnReset() is called when:
  • Plugin is first activated
  • Sample rate changes
  • I/O configuration changes
void OnReset() override {
  // Now we know the sample rate - initialize DSP
  const double sr = GetSampleRate();
  
  // Allocate delay buffer (1 second max)
  mDelayBuffer.Resize(static_cast<int>(sr));
  
  // Calculate filter coefficients
  mFilter.SetSampleRate(sr);
  mFilter.CalcCoefficients();
  
  // Clear state
  mPhase = 0.0;
  mEnvelope = 0.0;
  
  TRACE;  // Log that OnReset was called
}
Allocate all DSP buffers in OnReset(), not in the constructor, so they’re sized correctly for the sample rate.

OnActivate() - Plugin Enable/Disable

void OnActivate(bool active) override {
  if (active) {
    // Plugin activated - clear buffers
    mDelayBuffer.Clear();
    mPhase = 0.0;
  } else {
    // Plugin deactivated - could free resources
    // (Usually not necessary)
  }
}
Not all hosts support OnActivate() reliably. Use OnReset() for critical initialization.

Real-Time Safety

ProcessBlock() runs on a high-priority real-time thread. Breaking real-time safety causes audio dropouts (glitches, clicks, pops).

❌ NEVER Do These in ProcessBlock

Memory allocation/deallocation:
// ❌ BAD
std::vector<double> buffer(nFrames);  // Allocates!
delete pOldBuffer;                     // Deallocates!

// ✅ GOOD - Pre-allocate in OnReset()
mBuffer.Resize(GetBlockSize());  // Do this in OnReset()
Mutex locks (blocking):
// ❌ BAD
mMutex.lock();
// ... process ...
mMutex.unlock();

// ✅ GOOD - Use lock-free queue (ISender)
mSender.PushData({kTag}, {value});  // Lock-free
File I/O:
// ❌ BAD
fwrite(outputs[0], sizeof(sample), nFrames, mFile);
fread(buffer, sizeof(sample), nFrames, mFile);

// ✅ GOOD - Record to pre-allocated ring buffer,
// write to file on separate thread
System calls that can block:
// ❌ BAD
Sleep(10);                     // Blocks!
system("some command");         // Blocks!
printf("debug info\n");         // Can block I/O!

// ✅ GOOD - Use TRACE macro (disabled in release)
TRACE;  // Only logs in debug builds
Complex operations:
// ❌ BAD (unless you know it's fast)
LoadPreset("preset.txt");      // File I/O!
RecalculateFFT();              // Might allocate!
std::sort(data.begin(), data.end());  // Unpredictable!

// ✅ GOOD - Do in OnParamChange() or separate thread

✅ Safe in ProcessBlock

// ✅ Parameter access (atomic)
double gain = GetParam(kGain)->Value();

// ✅ Pre-allocated buffer access
mDelayBuffer[index] = input;

// ✅ Lock-free queue
mSender.PushData({kTag}, {value});

// ✅ Math operations
double output = sin(mPhase) * gain;
mPhase += frequency / GetSampleRate();

// ✅ Simple control flow
if (bypassed) {
  outputs[c][s] = inputs[c][s];
}

// ✅ Fixed-size stack arrays
sample temp[4];  // Small fixed size OK

// ✅ Reading pre-computed lookup tables
sample value = mWavetable[index];

Common Processing Patterns

Copy input to output:
void ProcessBlock(sample** inputs, sample** outputs, int nFrames) {
  const int nChans = NOutChansConnected();
  
  for (int c = 0; c < nChans; c++) {
    memcpy(outputs[c], inputs[c], nFrames * sizeof(sample));
  }
  
  // Or per-sample:
  for (int s = 0; s < nFrames; s++) {
    for (int c = 0; c < nChans; c++) {
      outputs[c][s] = inputs[c][s];
    }
  }
}

MIDI Processing

ProcessMidiMsg()

Called before ProcessBlock() for each incoming MIDI message:
void ProcessMidiMsg(const IMidiMsg& msg) override {
  int status = msg.StatusMsg();
  int channel = msg.Channel();
  
  switch (status) {
    case IMidiMsg::kNoteOn: {
      int note = msg.NoteNumber();
      int velocity = msg.Velocity();
      mSynth.NoteOn(note, velocity);
      break;
    }
    
    case IMidiMsg::kNoteOff: {
      int note = msg.NoteNumber();
      mSynth.NoteOff(note);
      break;
    }
    
    case IMidiMsg::kControlChange: {
      int cc = msg.ControlChangeIdx();
      int value = msg.ControlChange(cc);
      
      if (cc == 1) {  // Mod wheel
        mModAmount = value / 127.0;
      }
      break;
    }
    
    case IMidiMsg::kPitchWheel: {
      int pitch = msg.PitchWheel();  // -8192 to +8191
      mPitchBend = pitch / 8192.0;   // -1.0 to +1.0
      break;
    }
  }
}
IMidiMsg includes a timestamp for sample-accurate MIDI processing. See IPlugMidi.h

Sample-Accurate MIDI

void ProcessMidiMsg(const IMidiMsg& msg) override {
  // Queue MIDI message with timestamp
  mMidiQueue.Add(msg);
}

void ProcessBlock(sample** inputs, sample** outputs, int nFrames) {
  int samplesRemaining = nFrames;
  int offset = 0;
  
  while (samplesRemaining > 0) {
    // Get next MIDI message at or before current offset
    IMidiMsg msg;
    while (mMidiQueue.Peek(msg, offset)) {
      HandleMidiMessage(msg);
      mMidiQueue.Remove();
    }
    
    // Process samples up to next MIDI event (or end)
    int samplesToProcess = /* calculate based on next MIDI time */;
    ProcessAudio(inputs, outputs, offset, samplesToProcess);
    
    offset += samplesToProcess;
    samplesRemaining -= samplesToProcess;
  }
}
Use IMidiQueue for sample-accurate MIDI. See examples in IPlugInstrument.

Latency and Tail Size

Latency

// In config.h (compile-time)
#define PLUG_LATENCY 0  // samples

// Or set dynamically in OnReset() or OnParamChange()
void OnReset() override {
  // FFT processing adds latency
  int fftSize = 2048;
  SetLatency(fftSize / 2);  // Notify host
}

Tail Size

// Set tail size for reverb/delay effects
void OnParamChange(int paramIdx) override {
  if (paramIdx == kDecayTime) {
    double decaySeconds = GetParam(kDecayTime)->Value();
    int tailSamples = static_cast<int>(decaySeconds * GetSampleRate());
    SetTailSize(tailSamples);
  }
}

// Or infinite tail for drones, etc.
SetTailSize(kTailInfinite);

// No tail (default)
SetTailSize(kTailNone);
Tail size tells the host how long audio continues after input stops. Important for bouncing/rendering.

Bypassing

void ProcessBlock(sample** inputs, sample** outputs, int nFrames) {
  // Check if host has bypassed plugin
  if (GetBypassed()) {
    // Pass through audio unprocessed
    PassThroughBuffers(0.f, nFrames);  // Handled by IPlugProcessor
    return;
  }
  
  // Or manual bypass parameter
  if (GetParam(kBypass)->Bool()) {
    const int nChans = NOutChansConnected();
    for (int c = 0; c < nChans; c++) {
      memcpy(outputs[c], inputs[c], nFrames * sizeof(sample));
    }
    return;
  }
  
  // Normal processing...
}
If your plugin has latency, iPlug2 automatically compensates with a delay line when bypassed.

Transport and Timing

void ProcessBlock(sample** inputs, sample** outputs, int nFrames) {
  // Get transport info
  double samplePos = GetSamplePos();    // Absolute sample position
  double ppqPos = GetPPQPos();          // Position in quarter notes
  double tempo = GetTempo();            // BPM
  bool playing = GetTransportIsRunning();
  
  int numerator, denominator;
  GetTimeSig(numerator, denominator);   // e.g., 4, 4 for 4/4
  
  // Calculate samples per beat
  double samplesPerBeat = GetSamplesPerBeat();
  // = (60.0 / tempo) * sampleRate
  
  // Use for tempo-synced effects
  if (mTempoSync) {
    double delayBeats = GetParam(kDelayBeats)->Value();  // 1/4, 1/2, etc.
    int delaySamples = static_cast<int>(delayBeats * samplesPerBeat);
    // ...
  }
}
For tempo-synced effects, use GetSamplesPerBeat() to convert from beats to samples.

Offline Rendering

void ProcessBlock(sample** inputs, sample** outputs, int nFrames) {
  // Check if rendering offline (bouncing)
  if (GetRenderingOffline()) {
    // Can use more expensive algorithms
    ProcessWithHighQuality(inputs, outputs, nFrames);
  } else {
    // Real-time - use faster algorithm
    ProcessRealtime(inputs, outputs, nFrames);
  }
}

Performance Tips

Minimize Per-Sample Work

Calculate expensive values per-block, not per-sample:
// ✅ Good
double gain = GetParam(kGain)->DBToAmp();
for (int s = 0; s < nFrames; s++) {
  outputs[0][s] = inputs[0][s] * gain;
}

// ❌ Bad  
for (int s = 0; s < nFrames; s++) {
  outputs[0][s] = inputs[0][s] * 
    GetParam(kGain)->DBToAmp();  // Recalculates every sample!
}

Cache Locality

Process samples in sequence for better CPU cache usage:
// ✅ Better cache locality
for (int c = 0; c < nChans; c++) {
  for (int s = 0; s < nFrames; s++) {
    outputs[c][s] = Process(inputs[c][s]);
  }
}

Avoid Branching

Minimize if statements in inner loops:
// ❌ Bad - branches every sample
for (int s = 0; s < nFrames; s++) {
  if (bypassed) {
    outputs[0][s] = inputs[0][s];
  } else {
    outputs[0][s] = Process(inputs[0][s]);
  }
}

// ✅ Good - single branch
if (bypassed) {
  memcpy(outputs[0], inputs[0], nFrames * sizeof(sample));
} else {
  for (int s = 0; s < nFrames; s++) {
    outputs[0][s] = Process(inputs[0][s]);
  }
}

Use SIMD

For heavy DSP, consider SIMD intrinsics:
#include <immintrin.h>  // AVX/SSE

// Process 4 samples at once with SSE
__m128 input = _mm_load_ps(&inputs[0][s]);
__m128 gain = _mm_set1_ps(gainValue);
__m128 output = _mm_mul_ps(input, gain);
_mm_store_ps(&outputs[0][s], output);

Next Steps

Parameters

Use parameters in ProcessBlock for control

Architecture

Understand the threading model

Project Structure

See complete plugin examples

Plugin Formats

Format-specific audio processing details