Proposal: Extension for Native VST3 MIDI Support

Disclaimer: This is not a feature that exists today. We’re looking for feedback on this idea to gauge interest.

Context:

VST3 doesn’t currently support MIDI messages. Instead, it uses a variety of different mechanisms, as detailed here, to offer functionality similar to that offered by MIDI.

Some of the mechanisms, such as the IEventQueue for note and other events, allow a fairly seamless conversion between MIDI events and VST3 events. However, MIDI CCs and program change messages are passed as parameter changes rather than being part of the main event queue. This can be problematic because the relative order of paramter-change events with the same timestamp is lost, so it’s not possible to convert CC/PC messages to VST3 events without losing some information from the original MIDI stream.

Additionally, in order for the plugin to receive all possible CCs, it must declare 128 parameters for each of 16 MIDI channels, which can hurt the user experience if these parameters are also displayed to users e.g. in parameter automation lists.

Writing a VST3 plugin that only manipulates note events or other MIDI concepts is quite difficult, as the plugin must implement all of these disparate mechanisms/interfaces (parameters, IMidiMapping, plugin units per-channel), which is significantly more complex than writing a single function that consumes and produces MIDI events.

Plugins/hosts that want to provide full MIDI support have other options, such as AU MIDI FX on macOS, or CLAP. However, for projects that already support VST3, the support and maintenance overhead of adding this feature to such a project will be much lower than adding support for a whole new plugin format.

Proposal

I believe that support for MIDI events could be added to VST3 non-intrusively as a third-party header. An initial draft of the proposed interface is shown below. As long as hosts and plugins that want to support native MIDI all agree to use this header, MIDI can be added to VST3 without requiring an update to the core VST3 spec.

There is precedent for this approach: Presonus has a set of VST3 extensions, as does REAPER.

The header would be licensed permissively (BSD/MIT or similar) to allow maximal adoption.

namespace vst3_ext_midi
{

namespace sb = Steinberg;
namespace sbv = sb::Vst;

/// An event representing a Universal MIDI Packet.
/// This is intended to be passed between host and client using the IEventQueue
/// mechanism.
struct UMPEvent
{
    /// A stand-in EventType value for this event.
    /// Event::type should be set to this value to indicate that the payload is
    /// a UMPEvent.
    static constexpr auto kType = 0x100;

    /// Words of a Universal MIDI Packet.
    /// A UMPEvent will only ever contain a single packet, which means that
    /// the words at indices 2 and 3 may not be used for some events.
    /// Check the first word to find the real length of the packet, and avoid
    /// reading or writing to words that are not contained in the packet.
    sb::uint32 words[4];

    /// If the event is a UMPEvent, returns that event.
    /// Otherwise, returns nullopt.
    static std::optional<UMPEvent> fromEvent (const sbv::Event& e)
    {
        if (e.type != kType)
            return {};

        UMPEvent result;
        memcpy (&result, &e.noteOn, sizeof (UMPEvent));
        return result;
    }

    /// Returns an Event with this UMPEvent as the payload.
    sbv::Event toEvent (sb::int32 busIndex,
                        sb::int32 sampleOffset,
                        sbv::TQuarterNotes ppqPos,
                        sb::uint16 flags) const
    {
        sbv::Event result { busIndex, sampleOffset, ppqPos, flags, kType, {} };
        memcpy (&result.noteOn, this, sizeof (UMPEvent));
        return result;
    }
};

// UMPEvent will re-use the storage of NoteOnEvent.
static_assert (sizeof (UMPEvent) <= sizeof (sbv::NoteOnEvent));
static_assert (alignof (UMPEvent) <= alignof (sbv::NoteOnEvent));

} // namespace vst3_ext_midi

This only shows the general idea for passing UMP events through the IEventQueue. The full extension would likely need an additional interface to extend the processor/edit-controller so that the host can query the plugin to check whether it implements UMP support, and then inform the plugin if it intends to use this new functionality. I imagine that some plugins may opt to hide CC and PC parameters in the case that native MIDI support is available, so the new interface would likely include a function implemented by the plugin, to be called by the host early on in the plugin’s initialisation, so that the plugin can configure itself properly. I haven’t worked out the specifics here yet - we want to ensure that there is interest before spending too much time working out the fine details.

Obviously, not all hosts will support this new feature, so plugins would need to incorporate some fall-back behaviour using the existing VST3 API, even if this means operating in a reduced-functionality mode.

Next steps

If you are a plugin developer, do you have a use-case for native VST3 MIDI that is not covered by the existing VST3 API? We’d be interested to know your requirements, and whether the proposed extension would work for you.

If you develop a plugin host, would you consider implementing an extension like this, or do you foresee any potential issues? Please let us know!

15 Likes

I’m working on a host that makes extensive use of MIDI2 internally and would love to see more MIDI plugins! I can’t use JUCE for hosting or authoring plugins for the application, but it would be fantastic if JUCE plugin authors got MIDI i/o “for free” in their VST3 plugins.

One problem with this event type is that it shoves some additional complexity onto the plugin. There are MIDI2 sequences that are logically one event from the perspective of the host/plugin but encoded as multiple packets. It would be a little more convenient for the event data to be const uint8_t* (insert hand waving about bounds checking/memory safety here), and the host can just set the pointer for each event as an offset into its internal UMP buffer.

The bigger complexity is how the plugin and host negotiate the intersection of event types. Most(all?) of the VST3 events can be represented as UMPs. You also can’t necessarily borrow from the existing IComponent methods to get additional info about event buses, or define a new MediaType constant. This problem also exists in CLAP, and they have a solution. Borrowing a similar API would make sense, and also go a long way to help out hosts that are going to share code for CLAP and VST3. Something like this:

namespace vst3_ext_midi
{
typedef vst::int32 EventBusProtocol;
/// Bitflags that represent supporting "VST" events or "UMP" events.
enum
{
  kVstEvents = 1 << 0,
  kUmpEvents = 1 << 1
};

typedef struct
{
  /// A set of bitflags that designate which protocols are supported by an event bus.
  EventBusProtocol supported;

  /// A single flag that indicates which the plugin would prefer to use.
  EventBusProtocol preferred;
} EventBusProtocolSupport;

class IEventBusProtocolSupport: public vst::IComponent
{
public:
  virtual ~IEventBusProtocolSupport();

  /// Called after `IComponent::getBusInfo()` to find out what protocols the plugin supports. 
  virtual vst::kresult getEventBusProtocolSupport(
    vst::BusDirection dir, 
    int index, 
    EventBusProtocolSupport* protocolSupport
  );

  /// Called in the "inactive" state, after "setupProcessing" is called to configure the protocol.
  virtual vst::kresult setEventBusProtocolSupport(
    vst::BusDirection dir,
    int index,
    EventBusProtocol protocol
  )
};
}

Another thing to consider is that whether a plugin is a “MIDI” plugin or not is important to know during scanning, but none of the interfaces that allow you to access information about event busses are available until after the plugin is in the “inactive” state after setupProcessing (in JUCE terms - after prepareToPlay has been called). Some additional metadata either in the plugin manifest or an IPluginFactory extension would be necessary to hint to the host that a plugin is a “MIDI” plugin during scanning to give the users something useful to display before trying to instantiate it.


There is a meta point: why go through the trouble of extending VST3 and making scanning/intializing more difficult with more edges for hosts/plugins instead of just supporting clap? VST3 hosting is the single largest area of complexity with the highest likelihood of crashes in my code, and touching it is higher risk than supporting CLAP.

1 Like

I’m not sure how flexible we can be here. A VST3 Event is 48 bytes with 24 bytes of header, which puts the max payload size at 24 bytes. Therefore, we can’t fit more than 3 two-word packets into a single Event, and we can only fit a single four-word packet. I think the simplest approach is to always have a single UMP packet per VST3 Event, and to have the plugin combine packets itself.

My idea was that, if the plugin implements some yet-to-be-defined interface (IUMPClient), then the host can assume that the plugin would prefer to receive UMPs if possible on all event buses. Before sending the first Event to the plugin, the host needs to call some member function of IUMPClient to inform the plugin that the host is UMP-aware.

I think this is orthogonal to the original feature idea. MIDI events might be used by synth or effect plugins, as well as pure MIDI-FX. In any case, I’m sure we could supply a standard “MIDIFX” key to be used in a similar way to the existing PlugType::kInstrumentSynth, kFx etc. from ivstaudioprocessor.h to advertise the plugin as a MIDI-only plugin during scanning.

Good question! My impression is that, for those with existing VST3 codebases, this would likely be a smaller, simpler feature to implement than full CLAP support. That said, I haven’t tried implementing a CLAP host yet, so I can’t say for sure. That’s one of the reasons I wanted to start this discussion: maybe everyone who wants to make/host MIDI plugins has already decided to use CLAP or some other format, in which case there wouldn’t be much point in pursuing this idea.

2 Likes

A VST3 Event is 48 bytes with 24 bytes of header, which puts the max payload size at 24 bytes.

The payload can contain pointers, like data/scale/note text events. But I take the point that most of the time, your event will fit in a single packet.

That said, you could avoid creating a new event type altogether and stick the UMP bytes into data events, but you still need an interface (probably as an extension of IComponent) to do the protocol upgrade/negotiation.

Don’t forget LV2.

1 Like

I won’t comment on this directly. I will just tell you that the VST3 SDK will soon add support for more MIDI2 stuff and that the MIDI Association is working on a plug-in client library which will incorporate the VST3 SDK stuff.

2 Likes

Excellent,

VST3 is one of the cleaner plugin APIs. But the main frustration I’ve experienced with it is having to use hundreds of dummy parameters to implement a MIDI instrument that supports all the continuous controllers.

I do support the concept of having the DAW able to map MIDI controllers to the plugin parameters, but this mechanism becomes unwieldy when you need to support all MIDI channels and all MIDI continuous controllers (that’s 129 * 16 dummy parameters).

To assess the feasibility of this proposal I implemented it.
It took 1 hour of time to add MIDI 2.0 support to both a plugin and a host.

Here it is in action. At the top is the VST3 plugins editor showing a keyboard responding to the events, and a scope showing the synth producing sound, at the bottom is the DAW feeding some MIDI 2.0 data into the plugin.

MIDI2Extension

I added to the DAW the following code:

#define VST3_USE_MIDI_EXTENSION 1

#include "ivstmidi2extension.h"

void ProcessorWrapper::onMidi2Message(const midi::message_view msg, int timeDelta)
{
	Steinberg::Vst::Event m = {};

#if	VST3_USE_MIDI_EXTENSION

	m.type = vst3_ext_midi::UMPEvent::kType;
	auto& midi2event = *reinterpret_cast<vst3_ext_midi::UMPEvent*>(&m.noteOn);

	memcpy(&midi2event.words, msg.begin(), msg.size());

#else // convert to VST3 note events...

I added to the plugin an additional case in the event handing ‘switch’ statement…

tresult PLUGIN_API SeProcessor::process (ProcessData& data)
{
	const int32 numEvents = data.inputEvents ? data.inputEvents->getEventCount() : 0;
	for (int32 eventIndex = 0; eventIndex < numEvents ; ++eventIndex)
	{
		switch (e.type)
		{
			case vst3_ext_midi::UMPEvent::kType:
			{
				auto& midi2event = *reinterpret_cast<vst3_ext_midi::UMPEvent*>(&e.noteOn);

				const auto midi2data = reinterpret_cast<const unsigned char*>(&midi2event.words);
				const int midi2size = 8; // asumming common 8-byte MIDI 2.0 message for now

				synthEditProject.MidiIn(e.sampleOffset, midi2data, midi2size);
			}
			break;
			
			case Event::kNoteOnEvent:
			{

I hope this information is helpful to assess the feasibility if this proposal.

8 Likes

I rarely feel that my input on this kind of post would be valuable, but I found myself having deja vu reading this. For a total of 2 years I have worked on MIDI in MIDI out plugins of some sort full-time, for two different employers.

These particular problems were a huge headache for us on both teams and we (in one case) mostly came up dry and resorted to a reduced functionality mode in these circumstances.

I can speak for all involved in saying that finding a sufficiently accessible way to allow this support from hosts would expand our list of working DAWs.

5 Likes

This proposal would enable us to support VST3 for one of our main MIDI processing plugins. I had to remove VST3 because it caused too many headaches, luckily I have a VST2 license and the VST2 build can handle being a MIDI effect without issues.

So a big +1 from me!

3 Likes

It took 1 hour of time to add MIDI 2.0 support to both a plugin and a host.

There’s a lot of talk about Midi 2.0 in this thred. What about Midi1.1 equipment, which I believe is the main part of existing equipment and will be for the forseeabale future?

If I record my Midi 1.1 synth, will this code allow it’s cc:s to be recorded and/or transferrerd to next midi effect plugin in line? If I play back a midi 1.1 track, will its cc and program change events be available in the processBlock?

1 Like

Yes, MIDI 1.0 messages can be encoded in Universal MIDI Packet format, so this would work for messages in 1.0 and 2.0 protocol. That said, some additional API may be necessary so that the host and client can decide on a common protocol to use.

3 Likes

Apple handles this very nicely in AU - The plugin specifies if it wants MIDI 1.0 or MIDI 2.0 and it’s up to the DAW to perform any necessary conversions.
for example my plugins deal only with MIDI 2.0 now and I got to rip out all the MIDI 1.0 handing, with all the mess and complexity of multi-byte messages. And MIDI 2.0 is provided in all Apple DAWs by the framework, even when the DAWs themselves are using MIDI 1.0.

The alternative of negotiation - where one DAW gets to support only MIDI 1.0 and another only 2.0 is that our plugins end up forever bloated with code to handle both. Why put that burden of converting MIDI on a thousand plugin developers when we can place it on just 10 DAW companies?

7 Likes

Having direct access to midi-CC would be very helpful. Lots of our customers prefer them to host automation, and being able to get rid of the list of 2,048 automation parameters would be nice, too.

4 Likes

This is wonderful but standards are not supposed to be there so everyone picks the stuff that they want to implement. They are meant to be fully implemented so all devices adopting it can talk to each other seamlessly. VST3 should start by implementing MIDI 1.0 fully and that includes AllNotesOff messages. If VST3 refuses to do so, those who believe in making compatible apps/devices will have no other choice but to migrate to CLAP/AU/AAX.

3 Likes

At this moment AU can be used for Mac plugins but for Windows plugins only AAX and CLAP fully implement MIDI 1.0 and not all hosts are supporting those formats. As a result this is a great proposal but we would have to wait anyway until all hosts implement this (if they want to do so).

IMHO this is absolutely essential to live in the present and one thing that JUCE is missing is support for (multi-channel) Program Changes. The developers of the Vienna Symphonic Library and Blue Cat Audio were able to pull it out, so the JUCE team should also be able to do so if they can invest time in it.

Personally I didn’t have time to fully implement it so I just patched JUCE to accept program change messages in channel 1 which is enough for a basic operation of the plugins that I develop (see VST3, Programs and MIDI Program Changes) but this obviously not good enough.

For a discussion of how to support multi-channel program change messages in VST3 see: MIDI Program Change - VST 3 SDK - Steinberg Forums

When this is done, then we can wait until all hosts either adopt CLAP or implement this extension (having two options is always better than having only one). This would allow us (in Windows plugins) to get rid of those annoying 2048 automation parameters needed to support CC in VST3 and support MIDI 1.0 messages like AllNotesOff (unlike Program Changes those are more rare but since they are in the MIDI 1.0 spec some people do use them!).

Once backwards compatibility with MIDI 1.0 is adressed, two other features (already available for audio plugins) that would open new possibilities for MIDI plugins are:

  1. Side chains (receive another MIDI track as input).
  2. ARA for MIDI (be able to asynchronously read the full MIDI track contents).

A use case for 1) would be a harmonizer for a bass line.
A use case for 2) would be a tool to automatically quantize each bar to a MIDI scale by analyzing its context in the song.

However those tools are used as selling points for some DAW’s, so I assume it will be difficult to get them onboard. So it looks more realistic to get them to support MIDI 1.0/2.0 first.

you folks are a tough audience.

Regarding how to do this - Us plugin developers have enough work without having to support a bunch of competing standards that all do essentially the same thing. I’m talking about MIDI 1.0, MIDI 2.0, MPE, Steinberg note events and CLAP note events.

I like Apples approach with AU. The plugin tells the host what it needs. No negotiation. The plugin need support only the one format that you prefer. Any converting from or to MIDI 1.0 is the DAWs problem.

Here’s the interface in Steinberg-style:

  class IProcessMidiProtocol : public Steinberg::FUnknown
  {
  public:
      enum Flags
      {
          kMIDIProtocol_1_0 = 1 << 0,
          kMIDIProtocol_2_0 = 1 << 1,
      };
      virtual Steinberg::uint32 PLUGIN_API getProcessMidiProtocol() = 0;
      //------------------------------------------------------------------------
      static const Steinberg::FUID iid;
  };

  DECLARE_CLASS_IID(IProcessMidiProtocol, 0x61C7B395, 0xC49643B4, 0x93DCEB01, 0x603E29EA)

the idea is that your plugin implements that one method which allows the DAW to query which flavor of MIDI you prefer.

e.g.

class MyProcessor :
	  public AudioEffect
	, public vst3_ext_midi::IProcessMidiProtocol
{
public:

	uint32 PLUGIN_API getProcessMidiProtocol() override
	{
		return vst3_ext_midi::IProcessMidiProtocol::kMIDIProtocol_1_0;
	}

	// etc...

MIDI 1.0 is wrapped in a UMP message. Which is to say that there is one extra byte appended at the start that you can ignore.
Decoding MIDI 1.0 is as simple as…

case vst3_ext_midi::UMPEvent::kType:
	{
		auto& midi2event = *reinterpret_cast<vst3_ext_midi::UMPEvent*>(&e.noteOn);

		const uint8_t* umpData = reinterpret_cast<uint8_t*>(&midi2event.words);

		if (gmpi::midi_2_0::ChannelVoice32 == (umpData[0] >> 4)) // MIDI 1.0 message wrapped in a UMP packet.
		{
			const uint8_t* midi_1_0_bytes = umpData + 1; // just skip the first header byte, the remainder is unadulterated MIDI 1.0

(tested and working).

8 Likes

UPDATE: I put it on github.

2 Likes