Search guides and the APICtrl K
  1. Docs
  2. Guides

MIDI and MPE

Notes in, expression per note, and MIDI out.

On this page

An instrument

Setting isInstrument in the descriptor gives the plugin a note input, drops its audio input bus and files it with the instruments in every format. The DSP takes the notes in a third argument:

C++
void process(sonore::AudioBlock<float>& io, const float* p, sonore::MidiBuffer& midi);

or, for everything at once – MIDI in and out, per-note expression, aux outputs, the sidechain – the sonore::ProcessContext form, void process(sonore::ProcessContext& ctx, const float* p).

Each event in a sonore::MidiBuffer carries its frame (samplePosition) within the block; applying it at that frame rather than at the block’s start is what keeps an instrument tight. sonore::MidiMessage::getChannel() answers 1 to 16, as MIDI is written, while noteOn(channel, ...) and the other constructors take 0 to 15: noteOn(m.getChannel() - 1, ...) is the round trip.

sonore::VoiceManager does the bookkeeping for a polyphonic instrument: which voice a note gets, which one to steal (a released voice first, then a repeat of the same key, then the oldest that is neither the lowest nor the highest note held), and the sustain and sostenuto pedals, through sonore::applyPedals. A voice type needs four methods: noteOn(int note, float velocity), noteOff(), bool isActive() and float render().

MPE

An MPE controller bends, presses and brightens each finger on its own. supportsMpe = true in the descriptor declares MPE to the hosts that ask, decodes MPE’s channel-per-note MIDI into expression events, and fills ctx.expression. It needs the ProcessContext form: the simpler signatures have nowhere to put the expression. Match each event to its voice by key AND channel – under MPE two fingers on the same key are two notes:

C++Compiled and run in CI
#define SONORE_NUM_PARAMS 2
#include <sonore/dsp.h>

#include <cmath>

struct Voice {
  sonore::Oscillator osc;
  sonore::ADSR env;
  int key = 60;
  float velocity = 1.0f, bend = 0.0f, pressure = 0.0f;

  void setSampleRate(float sr) {
    osc.setSampleRate(sr);
    env.setSampleRate(sr);
    sonore::ADSR::Parameters a;
    a.attack = 0.005f;
    a.decay = 0.2f;
    a.sustain = 0.7f;
    a.release = 0.3f;
    env.setParameters(a);
  }
  // The four methods VoiceManager calls.
  void noteOn(int note, float vel) {
    key = note;
    velocity = vel;
    bend = pressure = 0.0f;
    retune();
    osc.reset();
    env.noteOn();
  }
  void noteOff() { env.noteOff(); }
  bool isActive() const { return env.isActive(); }
  float render() { return osc.saw() * env.getNextSample() * velocity * (0.6f + 0.4f * pressure); }

  void retune() {
    osc.setFreq((float) sonore::MidiMessage::getMidiNoteInHertz(key) * std::pow(2.0f, bend / 12.0f));
  }
};

struct SonoreDsp {
  sonore::VoiceManager<Voice, 8> voices;
  sonore::Smooth levelSm;

  void prepare(const sonore::ProcessSpec& spec) {
    for (int i = 0; i < voices.kNumVoices; ++i) voices.voice(i).setSampleRate((float) spec.sampleRate);
    levelSm.setup((float) spec.sampleRate, 15.0f);
    levelSm.snap(0.25f);
  }

  void process(sonore::ProcessContext& ctx, const float* p) {
    sonore::AudioBlock<float>& io = ctx.main;
    auto event = ctx.midi.begin();
    const auto end = ctx.midi.end();
    const sonore::NoteExpressionBuffer::Entry* expr = ctx.expression ? ctx.expression->begin() : nullptr;
    const sonore::NoteExpressionBuffer::Entry* exprEnd = ctx.expression ? ctx.expression->end() : nullptr;
    const float level = sonore::dbToGain(p[0]);

    for (size_t i = 0; i < io.getNumSamples(); ++i) {
      // Notes at their own frame. The channel travels with the note.
      for (; event != end && (size_t) event->samplePosition <= i; ++event) {
        const sonore::MidiMessage& m = event->getMessage();
        if (sonore::applyPedals(voices, m)) continue; // sustain and sostenuto
        if (m.isNoteOn())
          voices.noteOn(m.getNoteNumber(), m.getFloatVelocity(), m.getChannel() - 1);
        else if (m.isNoteOff())
          voices.noteOff(m.getNoteNumber(), m.getChannel() - 1);
      }
      // Per-note expression, also at its own frame.
      for (; expr != exprEnd && (size_t) expr->samplePosition <= i; ++expr) {
        const int v = voices.voiceForNote(expr->key, expr->channel);
        if (v < 0) continue;
        Voice& voice = voices.voice(v);
        if (expr->expression == sonore::kExprTuning) { // semitones from the key
          voice.bend = expr->value * p[1];
          voice.retune();
        } else if (expr->expression == sonore::kExprPressure) { // 0..1
          voice.pressure = expr->value;
        }
      }
      const float s = voices.render() * levelSm.next(level);
      for (size_t c = 0; c < io.getNumChannels(); ++c) io.getChannelPointer(c)[i] = s;
    }
  }
};

#include <sonore/plugin.h>

static const sonore::ParamInfo kParamTable[SONORE_NUM_PARAMS] = {
    {"level", "Level", "dB", -40.0f, 6.0f, -12.0f},
    {"bendAmount", "Bend Amount", "", 0.0f, 1.0f, 1.0f},
};

static const sonore::PluginDescriptor kDesc = [] {
  sonore::PluginDescriptor d;
  d.id = "com.mycompany.mpesaw";
  d.name = "MPE Saw";
  d.vendor = "My Company";
  d.version = "1.0.0";
  d.isInstrument = true; // note input, no audio input, filed as an instrument
  d.params = kParamTable;
  d.numParams = SONORE_NUM_PARAMS;
  d.category = "synth";
  d.supportsMpe = true; // fills ctx.expression; needs the ProcessContext form
  return d;
}();

#include <sonore/formats.h>

The descriptor here is built field by field in a lambda, as the showcase example builds its own: easier to read than a positional list once it sets fields far down the struct.

MIDI out

A plugin whose output is notes – an arpeggiator, a chord generator – sets producesMidi and takes the ProcessContext form; it adds events to ctx.midiOut with their frames, as they arrive on the input side:

C++
ctx.midiOut.addEvent(sonore::MidiMessage::noteOn(0, note, 100), (int) i);

Pass on what the plugin does not understand – the sustain pedal, the mod wheel, a clock – or everything downstream of it stops working; sonore::forwardSysex(ctx.midi, ctx.midiOut) does it for SysEx. The arp example (sdk/examples/arp/plugin.cpp) is the whole pattern.

Beyond a plugin’s notes

The MIDI and OSC area of the API reference has the rest: Standard MIDI Files (midi_file.h), MIDI 2.0’s Universal MIDI Packets (ump.h, ump128.h) and MIDI-CI (midi_ci.h), MIDI clock, live MIDI devices for the standalone, and Open Sound Control (osc.h).

↑↓ to move↵ to openEvery public name in the headers, and every guide