Skip to content

Latest commit

 

History

History
337 lines (285 loc) · 11.1 KB

04.midi.md

File metadata and controls

337 lines (285 loc) · 11.1 KB

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

Inputs

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

Outputs

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

Note

  • 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.

Handling MIDI Input

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);
}

Handling MIDI Output

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