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:
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:
#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:
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).