MIDI CC messages in VST3?

We’ve got a plugin that we’ve converted to JUCE, but discovered that in our VST3 version, no MIDI CC messages are getting through to the processBlock() function. (As a side note, I added the preprocessor definition JUCE_VST3_EMULATE_MIDI_CC_WITH_PARAMETERS=0 to my JUCE project, so that our mappings do not show up as parameters.)

In testing, I created a MIDI track with MIDI CC data on it, and I have set the output of my MIDI track to be my plugin (on an audio track). But when I go to iterate over the midiMessages, there is nothing there.

If I add another MIDI track, and send regular MIDI notes to my plugin, then those show up fine. But MIDI CC does not seem to show up.

In pre-JUCE VST3 plugins, we implemented MIDI CC behavior via the VST3 function getMIDIControllerAssignment(). Now, in JUCE, we are supposed to just examine the MIDI message ourselves in the processBlock() function, right? So how come MIDI CC is not showing up in our VST3 plugin (in Cubase or Studio One, at least).

Ok, so that was why all of those “hidden” parameters were created - as a hack for VST3 to get those MIDI CC messages. If the VST3 wrapper implemented the getMIDIControllAssignment() function, then we would be fine, but it doesn’t.
I see that the VST3 SDK now includes IMidiLearn. Does the newest JUCE Master branch implement that for us?

Ok, I see that JUCE already implements getMidiControllerAssignment(), but it does so by using those “hidden” parameters. What would really be good for us is if, when NOT using those hidden parameters, if it called that same function in our processor instance instead. Then we could keep our old MIDI CC code as it was before JUCE.

Just going to leave this here:

https://sdk.steinberg.net/viewtopic.php?f=4&t=672

Rather than bolting on one feature of VST3, I think it would probably make more sense to have a sensible way to implement API specific features in the plugin code.

1 Like

Yeah, but we have software we are porting to JUCE, and we can’t call it an “upgrade” to our old software if we remove a feature we’ve had for years. Who wants to pay for an upgrade that has fewer features?

You can workaround this by maintaining your own fork of JUCE, which I know many of us do for reasons like this. It wouldn’t be that hard to add the method call. Or just not support the feature in VST3 and ask your users to use another format, which I understand isn’t ideal on Windows.

But you’re basically asking for a single method in a single API that is incongruent with every other API, in a wrapper around all those other APIs, just to make it easier to port a legacy codebase. It’s a bit of an edge case.

The hack is ugly and it’s an anti-pattern, but it’s the only way to do non-trivial MIDI CC behavior in VST3. Not to mention, the IMidiLearn interface isn’t widely supported outside Steinberg hosts.

But anyway, my point was more that I’d like support for these interfaces (and some stuff in AAX) and a way to implement them without having to fork JUCE. But I think it makes sense to make that possible for all the APIs, irrespective of what interface you want or API you’re targeting. Otherwise it’s just more code bloat.

1 Like

I have the code to implement getMIDIControllerAssignment(), but to get the host to recognize changes in those assignments, I need to call restartComponent(), which is a member of IComponentHandler. Unfortunately, I don’t see any way to call that, since IComponentHandler only seems to exist within the hosting code, not the plugin wrapper. Is there a way to call this function from a JUCE VST3 plugin?

I can add a function to the wrapper that calls the restartComponent() function, but how do I call the VST3 wrapper from my plugin instance (when it’s a VST3 instance, naturally)?

Does calling updateHostDisplay work? I’m away from my machine and can’t check

No, that does make a callback to the host, but not the way that is needed to inform the host of MIDI CC mapping changes.

It calls audioProcessorChanged which in turn calls restartComponent in the Vst3Wrapper unless I’m totally missing something

Yes, but it does so passing (Vst::kLatencyChanged | Vst::kParamValuesChanged), whereas I need to pass kMidiCCAssignmentChanged. I can add my own function in the wrapper to do that, but I’m having trouble tracing out how to get from my plugin to the wrapper, since on the PC it doesn’t see that code as part of my solution in VS for some reason. I’ll break out my Macbook andd see if I can find how to call back to the host via the wrapper from my plugin instance, I guess.

I figured it out. I had to basically mimic what updateHostDisplay did, but calling my own function that had to be defined in the listener class and overridden in the VST3 wrapper’s editor class.
It’s working now! :slight_smile:

2 Likes

Do you mind sharing the code? We would be interested in getting midi-CC to work with VST3 too.

Sure!

In JuceVST3EditController, I modified getMidiControllerAssignment like this:

    tresult PLUGIN_API getMidiControllerAssignment (Steinberg::int32 busIndex, Steinberg::int16 channel,
                                                Vst::CtrlNumber midiControllerNumber, Vst::ParamID& resultID) override
{
		AudioProcessor* pProcessor = audioProcessor->get();
		int id;
		if (pProcessor->getMidiControllerAssignment(busIndex, channel, midiControllerNumber, id))
			resultID = id;

    return kResultTrue; // Returning false makes some hosts stop asking for further MIDI Controller Assignments
}

And I added this function:

		virtual void informHostOfMIDICCMappingChanges() override
	{
		if (auto* handler = getComponentHandler())
			handler->restartComponent(Vst::kMidiCCAssignmentChanged);
	}

Then, in the AudioProcessor class, I added this (in the header):

		virtual bool getMidiControllerAssignment(int busIndex, int channel,
		int midiControllerNumber, int& resultID) {
		return false;
	}

…which I override in my own processor class using the same code I had previously used in my VST3 plugin (pre-JUCE), which assigns the appropriate value to resultID for the given midiControllerNumber (assuming the channel is one I was interested in).

Then, to inform the host I needed to first add this function to the AudioProcessorListener class (header):

		virtual void informHostOfMIDICCMappingChanges() {}

…and overrode that in the JuceVST3EditController class like this:

		virtual void informHostOfMIDICCMappingChanges() override
	{
		if (auto* handler = getComponentHandler())
			handler->restartComponent(Vst::kMidiCCAssignmentChanged);
	}

To get to that function from my plugin, I added a function of the same name to the AudioProcessor class like this:

void AudioProcessor::informHostOfMIDICCMappingChanges() {
for (int i = listeners.size(); --i >= 0;)
	if (auto* l = getListenerLocked(i))
		l->informHostOfMIDICCMappingChanges(); }

…which I then can call from my plugin’s processor class.

But since the editor is the class that needs to inform the host of the changes, I forward a similar call from my editor to my processor at two points: at the end of the editor’s constructor (because I may have read in user preferences that set those mappings) and after the user edits those mapping using a component I made for that. I also set a flag in my processor after loading session data, so that the message thread can make that call in a timer callback (since it needs to be done from the message thread, not the process thread).

I think that covers the changes! Let me know if you need more info.

5 Likes

Thanks :slight_smile: