Midi Output, Issues with Velocity and Timing

I’m working on a VST3 plugin that takes MIDI events from process inputEvents, changes their properties and then adds them to outputEvents. I realise not all hosts (like Ableton for example) support this, but BitWig (v5) seems to which I’m testing in. I’m having some issues though

  • I’m setting the velocity of the noteOn I’m sending to the output, but this doesn’t seem to have any effect. Even if I set it to 0, BitWig is still playing the note if I output it. I’m just using “event.noteOn.velocity = x” and then “eventListOutput->addEvent(event);” but I think I’m missing something here. Is this something not all hosts support, or am I doing it wrong?
  • I’m adjusting slightly the “sampleOffset” of the outgoing note, moving it to be slightly later. If it falls within the current samples that I’m processing I add it to the output during that pass, if it’s later, I store it and add it in a later pass. Will this work with hosts that support MIDI output? Is this the right way to do it from the VST 3 SDK perspective? I ask because I’m having some issues with this as well

Any perspective/help would be much appreciated.

Yes it seems to be correct from VST3 point of view… did you try with other velocity values? you could try with Cubase, output event should be handled correctly. If it works with Cubase you could contact Bitwig developers about this issue.

Thanks for the feedback, and good idea about trying in Cubase.

The part I’m still struggling with is understanding how the sampleOffset works for incoming and outgoing events. Say for example I just want to write out all the midi events I’m getting straight back out (as kind of a bypass), I tried this code:

const int32 count = eventListInput->getEventCount();
for (int32 i = 0; i < count; ++i) {
Event e{};
if (eventListInput->getEvent(i, e) == kResultOk) {
eventListOutput->addEvent(e);
}
}

However this seems to add some kind of delay and doesn’t play the MIDI events at the exact time they were originally scheduled for. Is this expected or am I mis-understanding how this is meant to work? In case it’s relevant I’m also returning 0 for getLatencySamples for my processor.

Edit: I guess maybe what I’m asking is: is there read-ahead, or can MIDI events being output only ever go into the next block?

Hi Rusty,

A few things about your midi passthrough:

in the Processor’s initialize() function, you have to add the event buffers:

  tresult VSTProcessor::initialize(FUnknown * context)
  {
      // required for receiving midi events from other tracks
      addEventInput(STR16("Event In"), 16); 

      // required for recording midi events in the host
      addEventOutput(STR16("Event Out"), 16);
  }

That creates 16 channels of midi for both out and in buffers.

For a midi passthrough, you need to perform some error checking, such as ensuring that the input & ouput events are valid and have data before you start forwarding:

  void VSTProcessor::processMidi(Steinberg::Vst::ProcessData &data)
  {
    // ensure the event buffers are valid
    if (data.inputEvents && data.outputEvents)
    {
      Vst::Event e {};
      const int32 n = data.inputEvents->getEventCount();
      for (int32 i = 0; i < n; ++i)
      {
        // you can also suppressed midi events here by not allowing them through
        if (data.inputEvents->getEvent(i, e) == kResultOk)
          data.outputEvents->addEvent(e);
      }
    }
  }

and lastly, about sampleOffsets, you can absolutely schedule midi events across sample blocks. I think your mental model is slightly off. Don’t use sampleOffset because that only pertains to the current sample block. It has no relevance to the next block. What you’d wanna do is convert seconds or milliseconds to samples, e.g.,

auto delayInSamples = static_cast<int64_t>(delayInSeconds * sampleRate);

Now you know how many samples in the future until you need from our current position in order to kick off the delayed midi note. It might be that delayInSamples is larger than your current sample size, which would mean you get to add it in a future process() block. You’ll need some record-keeping and you’ll need to automatically send a midi-off note. You also want to keep track of the noteId separately from the original noteOn.noteId.

Hope that helps.

That makes sense, thanks!