# MIDI I/O
In PureData there are objects to handle interfacing with a machines MIDI device.
heavy doesn't provide cross-platform implementation for MIDI I/O as the requirements tend to change depending on the platform or framework being used.
Instead, it provides wrappers around these objects that route the data to specific hard-coded receivers/parameters in the patch context. For example a [notein]
object will be replaced by a [r __hv_notein]
receiver with input data split into its constituent parts and routed to the appropriate outlet.
The following Pd objects are mapped to their corresponding heavy parameter and internal hash.
Pd object | heavy param | heavy hash |
---|---|---|
[notein] | __hv_notein | 0x67E37CA3 |
[ctlin] | __hv_ctlin | 0x41BE0f9C |
[polytouchin] | __hv_polytouchin | 0xBC530F59 |
[pgmin] | __hv_pgmin | 0x2E1EA03D |
[touchin] | __hv_touchin | 0x553925BD |
[bendin] | __hv_bendin | 0x3083F0F7 |
[midiin] | __hv_midiin | 0x149631bE |
[midirealtimein] | __hv_midirealtimein | 0x6FFF0BCF |
The same principle applies for sending MIDI data out of the heavy context. If you add a [noteout] object there'll be a corresponding sendhook callback with a message containing the MIDI data sent by the patch.
Pd object | heavy sendhook | heavy hash |
---|---|---|
[noteout] | __hv_noteout | 0xD1D4AC2 |
[ctlout] | __hv_ctlout | 0xE5e2A040 |
[polytouchout] | __hv_polytouchout | 0xD5ACA9D1 |
[pgmout] | __hv_pgmout | 0x8753E39E |
[touchout] | __hv_touchout | 0x476D4387 |
[bendout] | __hv_bendout | 0xE8458013 |
[midiout] | __hv_midiout | 0x6511DE55 |
[midiout] | __hv_midioutport | 0x165707E4 |
- Channel numbering in the generator is expect to start at 0. For this reason the midi wrapper objects internally
[+ 1]
and[- 1]
since Pure Data starts channel numbering at 1 and this keeps some expected patch compatibility in place. - Also for compatibility reasons
[bendout]
uses -8192 to 8191 range (and resets the offset with an internal[+ 8192]
). This ensures expected behaviour with pd-vanilla patches. - It is generally the users responsibility to convert to and from the MIDI byte data to the float values used by heavy.
- The
[ctlin]
object is currently unable to match CC message 0. One can of course still filter for this message in the patch itself. - The
[midiout]
object currently does not respond to port numbers.
Some framework targets like DPF already have implementations available. However, if you're integrating the C/C++ code on a custom platform then you'll need to provide your own conversion process.
Here's the DPF
implementation as an example.
The MIDI input is called during the DPF run()
loop where it receives MidiEvent
messages:
#if DISTRHO_PLUGIN_WANT_MIDI_INPUT
// -------------------------------------------------------------------
// Midi Input handler
void {{class_name}}::handleMidiInput(uint32_t frames, const MidiEvent* midiEvents, uint32_t midiEventCount)
{
// Realtime events
// TODO: Continue and Reset
const TimePosition& timePos(getTimePosition());
const bool playing = timePos.playing;
if (playing != wasPlaying)
{
if (playing)
{
_context->sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, 0,
"ff", (float) MIDI_RT_START);
} else {
_context->sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, 0,
"ff", (float) MIDI_RT_STOP);
}
wasPlaying = playing;
}
if (playing && timePos.bbt.valid)
{
float samplesPerBeat = 60 * getSampleRate() / timePos.bbt.beatsPerMinute;
float samplesPerTick = samplesPerBeat / 24.0;
int i = 1;
while (samplesProcessed > samplesPerTick)
{
_context->sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, i * 1000.0*samplesPerTick/getSampleRate(),
"ff", (float) MIDI_RT_CLOCK);
samplesProcessed -= samplesPerTick;
i++;
}
samplesProcessed += frames;
// printf("> ticks: %f - samples: %f \n", samplesPerTick, samplesProcessed);
}
// Midi events
for (uint32_t i=0; i < midiEventCount; ++i)
{
int status = midiEvents[i].data[0];
int command = status & 0xF0;
int channel = status & 0x0F;
int data1 = midiEvents[i].data[1];
int data2 = midiEvents[i].data[2];
// raw [midiin] messages
int dataSize = *(&midiEvents[i].data + 1) - midiEvents[i].data;
for (int i = 0; i < dataSize; ++i) {
_context->sendMessageToReceiverV(HV_HASH_MIDIIN, 1000.0*timePos.frame/getSampleRate(), "ff",
(float) midiEvents[i].data[i],
(float) channel);
}
if(mrtSet.find(status) != mrtSet.end())
{
_context->sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, 1000.0*timePos.frame/getSampleRate(),
"ff", (float) status);
}
// typical midi messages
switch (command) {
case 0x80: { // note off
_context->sendMessageToReceiverV(HV_HASH_NOTEIN, 1000.0*timePos.frame/getSampleRate(), "fff",
(float) data1, // pitch
(float) 0, // velocity
(float) channel);
break;
}
case 0x90: { // note on
_context->sendMessageToReceiverV(HV_HASH_NOTEIN, 1000.0*timePos.frame/getSampleRate(), "fff",
(float) data1, // pitch
(float) data2, // velocity
(float) channel);
break;
}
case 0xB0: { // control change
_context->sendMessageToReceiverV(HV_HASH_CTLIN, 1000.0*timePos.frame/getSampleRate(), "fff",
(float) data2, // value
(float) data1, // cc number
(float) channel);
break;
}
case 0xC0: { // program change
_context->sendMessageToReceiverV(HV_HASH_PGMIN, 1000.0*timePos.frame/getSampleRate(), "ff",
(float) data1,
(float) channel);
break;
}
case 0xD0: { // aftertouch
_context->sendMessageToReceiverV(HV_HASH_TOUCHIN, 1000.0*timePos.frame/getSampleRate(), "ff",
(float) data1,
(float) channel);
break;
}
case 0xE0: { // pitch bend
// combine 7bit lsb and msb into 32bit int
hv_uint32_t value = (((hv_uint32_t) data2) << 7) | ((hv_uint32_t) data1);
_context->sendMessageToReceiverV(HV_HASH_BENDIN, 1000.0*timePos.frame/getSampleRate(), "ff",
(float) value,
(float) channel);
break;
}
default: break;
}
}
}
#endif
// -------------------------------------------------------------------
// DPF Plugin run() loop
#if DISTRHO_PLUGIN_WANT_MIDI_INPUT
void {{class_name}}::run(const float** inputs, float** outputs, uint32_t frames, const MidiEvent* midiEvents, uint32_t midiEventCount)
{
handleMidiInput(frames, midiEvents, midiEventCount);
#else
void {{class_name}}::run(const float** inputs, float** outputs, uint32_t frames)
{
#endif
_context->process((float**)inputs, outputs, frames);
}
For MIDI output you will need to set a heavy sendhook function that will trigger DPF
MIDI output events from the heavy context:
static void hvSendHookFunc(HeavyContextInterface *c, const char *sendName, uint32_t sendHash, const HvMessage *m)
{
{{class_name}}* plugin = ({{class_name}}*)c->getUserData();
if (plugin != nullptr)
{
#if DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
plugin->handleMidiSend(sendHash, m);
#endif
}
}
That can then be attached to the heavy context in the constructor:
_context->setUserData(this);
_context->setSendHook(&hvSendHookFunc);
This will prepare the DPF MidiEvents and needs to take special care for Note Off messages.
Pd does not have specific Note Off events, so velocity 0 is assumed to be Note Off in this case. And because DPF only supports a single midi port we do a % 16
to reduce them to only one.
Bend assumes input values ranged 0 - 16383
for [bendin]
(normal bend range), however as mentioned before [bendout]
uses -8192 to 8191
to stay compatible with pd-vanilla.
#if DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
// -------------------------------------------------------------------
// Midi Send handler
void {{class_name}}::handleMidiSend(uint32_t sendHash, const HvMessage *m)
{
MidiEvent midiSendEvent;
midiSendEvent.frame = 0;
midiSendEvent.dataExt = nullptr;
switch(sendHash){
case HV_HASH_NOTEOUT: // __hv_noteout
{
uint8_t note = hv_msg_getFloat(m, 0);
uint8_t velocity = hv_msg_getFloat(m, 1);
uint8_t ch = hv_msg_getFloat(m, 2);
ch %= 16; // drop any pd "ports"
midiSendEvent.size = 3;
if (velocity > 0){
midiSendEvent.data[0] = 0x90 | ch; // noteon
} else {
midiSendEvent.data[0] = 0x80 | ch; // noteoff
}
midiSendEvent.data[1] = note;
midiSendEvent.data[2] = velocity;
writeMidiEvent(midiSendEvent);
break;
}
case HV_HASH_CTLOUT:
{
uint8_t value = hv_msg_getFloat(m, 0);
uint8_t cc = hv_msg_getFloat(m, 1);
uint8_t ch = hv_msg_getFloat(m, 2);
ch %= 16;
midiSendEvent.size = 3;
midiSendEvent.data[0] = 0xB0 | ch; // send CC
midiSendEvent.data[1] = cc;
midiSendEvent.data[2] = value;
writeMidiEvent(midiSendEvent);
break;
}
case HV_HASH_PGMOUT:
{
uint8_t pgm = hv_msg_getFloat(m, 0);
uint8_t ch = hv_msg_getFloat(m, 1);
ch %= 16;
midiSendEvent.size = 2;
midiSendEvent.data[0] = 0xC0 | ch; // send Program Change
midiSendEvent.data[1] = pgm;
writeMidiEvent(midiSendEvent);
break;
}
case HV_HASH_TOUCHOUT:
{
uint8_t value = hv_msg_getFloat(m, 0);
uint8_t ch = hv_msg_getFloat(m, 1);
ch %= 16;
midiSendEvent.size = 2;
midiSendEvent.data[0] = 0xD0 | ch; // send Touch
midiSendEvent.data[1] = value;
writeMidiEvent(midiSendEvent);
break;
}
case HV_HASH_BENDOUT:
{
uint16_t value = hv_msg_getFloat(m, 0);
uint8_t lsb = value & 0x7F;
uint8_t msb = (value >> 7) & 0x7F;
uint8_t ch = hv_msg_getFloat(m, 1);
ch %= 16;
midiSendEvent.size = 3;
midiSendEvent.data[0] = 0xE0 | ch; // send Bend
midiSendEvent.data[1] = lsb;
midiSendEvent.data[2] = msb;
writeMidiEvent(midiSendEvent);
break;
}
case HV_HASH_MIDIOUT: // __hv_midiout
{
const uint8_t numElements = m->numElements;
if (numElements <=4 )
{
for (int i = 0; i < numElements; ++i)
{
midiSendEvent.data[i] = hv_msg_getFloat(m, i);
}
}
else
{
printf("> we do not support sysex yet \n");
break;
}
// unsigned char* rawData = new unsigned char;
// for (int i = 0; i < numElements; ++i) {
// rawData[i] = (uint8_t) hv_msg_getFloat(m, i);
// printf("> data: %d \n", rawData[i]);
// }
midiSendEvent.size = numElements;
// midiSendEvent.dataExt = (const uint8_t *) rawData;
writeMidiEvent(midiSendEvent);
break;
}
default:
break;
}
}
#endif