JUCE_IGNORE_VST3_MISMATCHED_PARAMETER_ID_WARNING Confusion

 // If you encounter this error there may be an issue migrating parameter
 // automation between sessions saved using the VST2 and VST3 versions of this
 // plugin.
 //
 // If you have released neither a VST2 or VST3 version of the plugin,
 // consider only releasing a VST3 version and disabling JUCE_VST3_CAN_REPLACE_VST2.
 //
 // If you have released a VST2 version of the plugin but have not yet released
 // a VST3 version of the plugin, consider enabling JUCE_FORCE_USE_LEGACY_PARAM_IDS.
 // This will ensure that the parameter IDs remain compatible between both the
 // VST2 and VST3 versions of the plugin in all hosts.
 //
 // If you have released a VST3 version of the plugin but have not released a
 // VST2 version of the plugin, enable JUCE_IGNORE_VST3_MISMATCHED_PARAMETER_ID_WARNING.
 // DO NOT change the JUCE_VST3_CAN_REPLACE_VST2 or JUCE_FORCE_USE_LEGACY_PARAM_IDS
 // values as this will break compatibility with currently released VST3
 // versions of the plugin.
 //
 // If you have already released a VST2 and VST3 version of the plugin you may
 // find in some hosts when a session containing automation data is saved using
 // the VST2 or VST3 version, and is later loaded using the other version, the
 // automation data will fail to control any of the parameters in the plugin as
 // the IDs for these parameters are different. To fix parameter automation for
 // the VST3 plugin when a session was saved with the VST2 plugin, implement
 // VST3ClientExtensions::getCompatibleParameterIds() and enable
 // JUCE_IGNORE_VST3_MISMATCHED_PARAMETER_ID_WARNING.


I have already released a VST2 and VST3 version of my plugin with JUCE6 and JUCE_VST3_CAN_REPLACE_VST2=1 and JUCE_FORCE_USE_LEGACY_PARAM_IDS=0, and now I am migrating to JUCE8 and get this warning.

With this warning, I don’t understand whether I also have to implement getCompatibleParameterIds(). It worked before (with JUCE6), didn’t it? Or are there specific hosts that cause problems which rely an a specific getCompatibleParameterIds() implementation?

JUCE_VST3_CAN_REPLACE_VST2 ensures that the VST3 plugin can be identified as a valid replacement for the VST2 version, and so any data saved using the VST2 version will be handed to the VST3 version which should load all the correct initial settings. However, if any parameters have automation data they will have parameter identifiers attached to them. If the identifiers of these parameters are different between the VST2 and VST3 versions, the automation may not work. This is what happens when JUCE_FORCE_USE_LEGACY_PARAM_IDS is not enabled. Some DAWs (Reaper for example) appear to have worked around this and will normally work, although if you have since added/removed or changed the order or ID of any parameters that strategy might fail too.

So in this case you should implement getCompatibleParameterIds() for maximum compatibility. This allows a DAW to identify the correct parameter to automate given a parameter ID.

The best DAW to test this with is Cubase. First try the following.

  • Install the VST2 version of the plugin
  • Create a session with the VST2 plugin
  • Add automation data to the session
  • Save the session and quit the DAW
  • Remove the VST2 version and install the VST3 version
  • Open the session

I would expect the session to load correctly using the VST3 version, however automation data will likely fail to control any parameters.

Once you implement getCompatibleParameterIds(), as long as you’re testing with a modern version of Cubase the automation data should work correctly. I can’t remember the exact version they implemented this, so just try the latest version.

If you take a look at the docs for getCompatibleParameterIds() you’ll see an example of what the implementation might look like when JUCE_VST3_CAN_REPLACE_VST2 is enabled and JUCE_FORCE_USE_LEGACY_PARAM_IDS is disabled.

Thanks for clarification.
Should be pretty easy to implement, because I never ever change the index of a parameter anyway.

(Which is what I actually thought, that it is also a requirement for automation lanes to be compatible with each other between two VST2 plugin versions).

(I guess that’s also the default lookup method, cause no user has ever reported an issue with is.)

(Which, of course, worries me a little to implement getCompatibleParameterIds(), because normally I don’t “fix” things that aren’t broken.)

It is between two VST2 versions, however some manufacturers have moved away from VST2 but they just want their VST3 version to correctly replace the VST2 version in a session that was originally saved using an older VST2 version. IIRC this is especially important on modern Macs, as the Apple Silicon version of Cubase no longer supports loading VST2 plugins.

Please do check but I think you’ll find this is a problem for you, the most likely case for an end user to see this is probably with Cubase, especially if you are still installing the VST2 version because in that case most DAWs are probably still loading the VST2 version and so there won’t be any compatibility issues.

Thanks! I will check this out.

I’m also confused by some topics around this function, so I’m jumping onto this thread as it might be the best to have the issues with it centralized here.

So we also updated our entire plugin portfolio from JUCE6 to JUCE8 in the meantime and I noticed the new assertion when porting the first one.

The plugins all define JUCE_VST3_CAN_REPLACE_VST2 to 1 but JUCE_FORCE_USE_LEGACY_PARAM_IDS to 0, where the second was never a deliberate choice, we just sticked to the defaults. As a side note, we use our own AudioParameter subclass in most products, but as far as I can see, this doesn’t really matter in this case. After running into that build error, I created a Cubase 14 based session containing automation with a VST2 version of a plugin and then loaded that session using the VST3 version and could confirm that automation appeared broken indeed, so I wanted to address the issue.

We have a common base class for all plug-ins that already implements VST3ClientExtensions, where I added an implementation of VST3ClientExtensions::getCompatibleParameterIds like this:

std::map<uint32_t, juce::String> VST3Extensions::getCompatibleParameterIds (const juce::VST3Interface::Id& compatibleClass) const
{
    if (compatibleClass == juce::VST3Interface::vst2PluginId (JucePlugin_PluginCode, JucePlugin_Name))
    {
        std::map<uint32_t, juce::String> parameterIdsMap;

        for (auto* param : processor.getParameters())
        {
            auto idx = param->getParameterIndex();
            parameterIdsMap.emplace (idx, juce::String (idx));
        }

        return parameterIdsMap;
    }

    return juce::VST3ClientExtensions::getCompatibleParameterIds (compatibleClass);
}

With that in place, all previously broken automation worked again.
However, when updating our most complex plugin with 314 parameters recently, I ran into this assertion in JuceAudioProcessor::updateParameterMapping

// This means getCompatibleParameterIds() returned a parameter mapping
// that will hide a parameter in the current plugin! If this is due to
// an ID collision between plugin versions, you may be able to determine
// the mapping to report based on setStateInformation(). If you've
// already done this you can safely ignore this warning. If there is no
// way to determine the difference between the two plugin versions in
// setStateInformation() the best course of action is to remove the
// problematic parameter from the mapping.
jassert (compatibleClass != currentPluginId
         || getParamForVSTParamID (oldParamId) == nullptr
         || parameter == getParamForVSTParamID (oldParamId));

and I’m really confused by that. I’m not really sure what this means, what would be hidden here and what exactly should be done in setStateInformation? Of course, I haven’t tested all 314 parameters but a limited bit of testing revealed working automation when transitioning from VST2 to VST3. Removing our implementation of getCompatibleParameterIds silences this assertion but results in broken automation again.

Could you clarify what this is about and what would be the best course of action here?

This is the main reason we introduced the warning, we were made aware that this incompatibility existed and it was the default behaviour. We couldn’t just change the behaviour without breaking something so we thought it best to just warn users and then give the option so they can make the best decision for the specific case they are in.

Ouch! I really hoped no-one would ever actually hit that assertion. I don’t think I managed to test this case throughly but I put this assert there so it was at least caught if it ever did happen.

I’m going to go off memory here but let me try to simplify the situation to explain what I think is happening.

Image you have a plugin with two parameters

  • Parameter A
  • Parameter B

In the VST2 plugin they have the following IDs

  • Parameter A: 0
  • Parameter B: 1

In the VST3 plugin they have the following IDs

  • Parameter A: 1
  • Parameter B: 123456

Notice that now we have the same parameter ID (1) in both plugins, but for two different parameters. So the question is when automation data for a parameter with an ID of 1 comes in, which parameter should it control?

If the session was actually saved with the VST2 version, maybe you could have some confidence that a parameter ID of 1 is for parameter B. That might be something you could determine from setStateInformation(). Although looking at it again I’m not sure that would actually help, because presumably new automation data is going to be problematic? I would have to test it to be sure.

I suspect the best option is to remove the mapping of parameter B. It would mean old VST2 sessions with automation of parameter B will not be recalled correctly in some DAWs such as Cubase*. However, I think that is better than the alternative, which is that new automation data recorded by parameter A will now control parameter B (this is what I meant by hiding a parameter - at least I think that is what will happen).

If it turns out that I’m wrong about any of the above I do apologise and if that assertion needs updating let me know. I guess the next step is to identify which two parameters are clashing and focus any testing around those. I would think you would want to test loading automation saved with the VST2 and then recording new automation now you’re using the VST3 version, or maybe loading a preset saved using the VST3 version?

*Some DAWs have found their own way around this, for example Reaper appears to handle all this without the need to override anything. My guess is Reaper assumes that the VST3 will have the same parameters in the same order and probably manages the mapping for you at that point.

Thanks for the detailed reply, Anthony.

Having looked at the implementation for a while now, I’m still failing to see where a wrong mapping could actually happen.

What I see in updateParameterMapping is that there is compatibleParameterIdMap which is a map of maps which maps the id of a certain vst class ID to a map representing which Vst::ParmID is connected to which juce parameter instance. This map is populated right before the assertion is hit.

The only access to this map outside of updateParameterMapping that I could find is getParameterMap, which then again is only called once in the implementation of Vst::IRemapParamID::getCompatibleParamID. From my understanding this is a function especially intended to let the host remap parameter ids for automation when replacing plugin types. Putting a breakpoint in there, I can see Cubase calling it in my test session, so this seems to be the mechanism that makes Cubase successfully perform the migration.

All other parameter functionality always only seems to use getParamForVSTParamID which directly looks up paramMap. So given that, I’m really failing to see how creating a map for a certain compatible class could ever shadow a parameter at runtime? But I have to admit, I’m not probably not as deep into the VST3 implementation details as the JUCE team, so I might be overlooking something here. Could you point me to the code path that you have in mind where that mismatch would happen?

Oh and another thing, there is not one parameter hitting this assertion, this is coming up for 20 different paramers, which seems to be caused by the juce string hashing functions somehow generating hashes liearly going up from 48 for a series of parameter IDs, wich of course covers a wider range of our 314 paramers when indexed in the VST2 style.

To be clear when JUCE_VST3_CAN_REPLACE_VST2 is enabled the VST class ID for the VST2 and VST3 will be the same. That’s how the replacement works. There are other ways this could have been done but I assume at the time that was the best way to ensure a smooth transition in all DAWs.

It’s hard to recall all the details, it’s entirely possible that either something has changed or I got something wrong. It looks like our current implementation stores the current parameter map on this line

compatibleParameterIdMap[currentPluginId] = paramMap;

Given that when JUCE_VST3_CAN_REPLACE_VST2 is enabled the plugin ID will be the same for both the VST2 and VST3 plugins, if both plugins have the same plugin ID, and they share a common parameter ID which identifies a different parameter, then one will override the other in the map.

It seems like the question is really do we need to store paramMap in there at all, I can’t help but feel I must have done that for a reason, but looking at it now I can’t help but feel that it shouldn’t be required, unless the DAW were ever to call getCompatibleParamID() even when no plugin replacement was occurring.

Could you try commenting out the line highlighted above as well as the assertion you’re hitting, does everything still work as expected? If so I’ll take a closer look and see if we can remove that or not.

Ah right, I got that bit wrong, I understood it like they would have different IDs but with the option enabled some magic VST3 compatibility logic would be created. Now I understand the issue better and see how this can be problematic.

So I tried to identify the parameter pairs that this assertion flagged and it happened to be the parameters of band 0 and 4 of the eq. To have a test case for these seemingly conflicting parameters, I created an automation of EQ band 0 and 4 to it. On reloading, I could indeed see that the automation written for band 4 was applied to band 0. Interesting enough, the parameter name in the automation lane however now mentions band 4 for both automations, also the one previously written and stored for band 0.

All that was tested with the current state, that means not commenting out any JUCE code and having our implementation of getCompatibleParameterIds in place which triggers the assertion.

Commenting out these lines as suggested also does not fix anything but let’s me run into another assertion, this time in getCompatibleParamID

        const auto parameterMap = audioProcessor->getParameterMap (toVST3InterfaceId (pluginToReplaceUID));
        const auto iter = parameterMap.find (oldParamID);

        if (iter == parameterMap.end())
        {
            // This suggests a host is trying to load a plugin and parameter ID
            // combination that hasn't been accounted for in getCompatibleParameterIds().
            // Override this method in VST3ClientExtensions and return a suitable
            // parameter mapping to silence this warning.
            jassertfalse;
            return kResultFalse;
        }

Interesting observation here: getCompatibleParamID is also called when loading a session stored with a VST3 version, probably because of restartFlags |= Vst::kParamIDMappingChanged; alwayws being set in setComponentState, so we can not rely on it to only be called in case a session created with a VST2 is loaded.

Now to evalute the suggestion of deciding which mapping to report based on what is restored in setStateInformation I put a breakpoint in both, setStateInformationand getCompatibleParameterIds. Unfortunately, I see getCompatibleParameterIds being called once before setStateInformation is called and once afterwards. So in case we could figure something out about the fact if we load VST2 state data, this would only have effect on the second call. Still I don’t see getCompatibleParamIDbeing called before setStateInformation has been called, so this still might be fine.
But the most important question here to me is: How to figure out, that we are loading a state stored with the VST2 version? I don’t see any obvious way of doing that and we also don’t store the wrapper type the state was stored with in our state data.

That’s what I was afraid of. As you say this means

getCompatibleParamID is also called when loading a session stored with a VST3 version

I think that is maybe because this feature can also be used to deal with parameter IDs that have changed between versions of the plugin. The feature isn’t designed for just dealing with VST2 to VST3 migration.

So I think in the first call you would not apply any mapping at all, that way it’s treated as a new plugin insert so there is nothing to map (we handle this by adding the current plugins parameter map).

Once setStateInformation is called, assuming that you could determine it was a VST2 session, you could then ensure that the VST2 to VST3 mapping is applied instead.

When I looked at this before there was some private data that JUCE was adding in JuceVST3Component::setStateInformation(). If I remember correctly, if that private data is there it means the session was definitely saved by a VST3 instance, however unfortunately if the data is not there it doesn’t mean it was saved by a VST2 instance, it could be an older version of JUCE but still VST3. Other than that if JUCE_VST3_CAN_REPLACE_VST2 is enabled then I think the state information is identical so that anything saved with the VST3 version is backwards compatible with VST2 versions.

The thought with setStateInformation() was that users might be saving some additional information you could use as an indicator such as version number, plugin format, or some other relevant data you could use.

Thinking about it more, it may be more complex than that. Imagine you have a session that was originally created with the VST2 instance and automation was added, then later the session was re-opened with the VST3 version that doesn’t have any mappings, and then resaved again. Now the session is saved with a VST3 version but presumably the automation data is still using the older parameter IDs :weary_face:.

Worse than that, even if you can check setStateInformation() there is also the issue that a preset could be loaded much later. In that case you probably don’t want to change the mapping as there won’t be any automation data to deal with. So you would probably have to only apply the detection if setStateInformation() occurs within some short amount of time after the plugin instance is created.

All that is to say I’m not sure I have any great answers right now but I will go away and give this some serious thought.

Actually I have one idea, maybe there is some way to have a user toggle. If a user reports this issue maybe they can toggle this setting, that enables the mapping, then they resave, and turn the setting off again, or something along those lines. I’ll give this some more thought.

I just want to circle back on this. Looking at it, if you have parameter IDs of “0” through to “9” then that would produce the VST parameter IDs 48 through to 57. If you also have other parameters with single character IDs that will be an issue too. Is this what you have? Can you share the problematic parameter IDs? Unfortunately there probably isn’t anything you can do with this information but for me it’s useful to get some sense of how likely this is to occur for other users.

As mentioned, we have our own parameter class that inherits juce::AudioProcessorParameter. Looking into the juce sources I find this in AudioProcessor::getParameterID

if (auto* p = dynamic_cast<HostedAudioProcessorParameter*> (getParameters()[index]))
    return p->getParameterID();

return String (index);

For our parameter class which does not inherit HostedAudioProcessorParameter, the dynamic cast fails, so the ID is always the string representation of the linear parameter index. For the problematic plugin with 314 parameters, this means the IDs are “0”, “1”, … “313”.

I was not even aware of that until this year. Our own parameter class does even define a string ID that is used to reference the parameter plugin internally, so it would have been an easy thing to just derive that class from HostedAudioProcessorParameter and report the actual internal ID, but that parameter class was written years ago and nobody thought about it further, probably because all worked as expected. Well, until now :wink:

I’m thinking of changing our parameter class to inherit HostedAudioProcessorParameter, but that would of course break backwards compatibility entirely for existing product, so that’s rather something to consider for future releases I guess.

OK so when I convert those to hash codes I get

“0” … “9” → 48 … 57

So that explains the first problematic parameters, but the rest of the hash codes should be well out of range, so maybe you are just seeing the assertions twice. Once when the plugin is first instantiated and then again after the state loads, and there are actually only 10 problematic parameters? Not that it helps you much, but useful to know.

To be honest it feels like a long list of things leading to this issue, any one of which could have prevented it. On the plus side I’m hoping that this means it’s less likely to hit other customers.

Is this the only plugin you have that has its own parameter class? or maybe it’s the only one with 49 or more parameters?

You might want to think about new plugins using a parameter type that does inherit from HostedAudioProcessorParameter and reports a parameter ID. Although new plugins may not have a VST2 version which you need compatibility with, as the parameters don’t really have a parameter ID you could face problems in the future if you ever want to insert a new parameter or reorder your parameters.

I just found some time to revisit the issue. You are right, it are only 10 problematic parameters but updateParameterMapping is called twice when the session is restored, which made me count 20 assertions.

Currently we have released 14 plugins based on that parameter class, but it might indeed be the only one with 49 or more parameters until now. We updated our entire portfolio to JUCE 8 in the last months and this was the only one where those assertions were hit. So you might be right that this is rather a case where multiple unfortunate factors come together but hopefully not something that will affect a larger user base.

1 Like