Parameter groups result in VST3 validator failures

I’m hitting a couple of issues with the Steinberg VST3 validator reporting failure when I define parameter groups.

To illustrate, if I build the JUCE AudioPluginExample_VST3 code and amend the constructor in examples\CMake\AudioPlugin\PluginProcessor.cpp as follows:

AudioPluginAudioProcessor::AudioPluginAudioProcessor()
     : AudioProcessor (BusesProperties()
                     #if ! JucePlugin_IsMidiEffect
                      #if ! JucePlugin_IsSynth
                       .withInput  ("Input",  juce::AudioChannelSet::stereo(), true)
                      #endif
                       .withOutput ("Output", juce::AudioChannelSet::stereo(), true)
                     #endif
                       )
{
    juce::AudioProcessorParameterGroup rootGroup;
    rootGroup.addChild (std::make_unique<juce::AudioParameterFloat> ("amount", "Amount", 0.0f, 1.0f, 0.5f));

    auto subGroup = std::make_unique<juce::AudioProcessorParameterGroup> ("subgroup", "Sub Group", ">>");
    subGroup->addChild (std::make_unique<juce::AudioParameterFloat> ("subamount", "Sub Amount", 0.0f, 1.0f, 0.5f));

    rootGroup.addChild (std::move (subGroup));
    setParameterTree (std::move (rootGroup));
}

The Steinberg validator complains thus:

[Scan Parameters]
Info:  ===Scan Parameters ====================================
Info:  This component exports 3 parameter(s)
Info:     Parameter 000 (id=733630552): [title="Amount"] [unit=""] [type = F, default = 0.500000, unit = 0]
Info:     Parameter 001 (id=8674968): [title="Sub Amount"] [unit=""] [type = F, default = 0.500000, unit = -2072240065]
ERROR: Parameter 001 (id=8674968): No appropriate unit ID!!!
[XXXXXXX Failed]

and

[Scan Units]
Info:  ===Scan Units ====================================
Info:  This component has 2 unit(s).
Info:     Unit000 (ID = 0): "Root Unit" (parent ID = -1, programlist ID = -1)
ERROR: Unit 001: Invalid ID!
[XXXXXXX Failed]

As far as I can tell, the cause here is that the unit ID (created by hashing the sub group’s juce::String identifier) is negative (in signed 32-bit). This falls into the parameters’ reserved ID range according to 3rd-Party Developers Support & SDKs | Steinberg

Up to 2^31 parameters can be exported with id range [0, 2147483648]
(the range [2147483649, 429496729] is reserved for host application).

(and presumably applies to unit IDs too as -1 is used to represent “no parent unit”). Changing the sub group ID from “subgroup” to “group” makes the hash fall back into positive 32-bit signed range, but this is just luck.

A related problem occurs if you give the top-level group a name, so the constructor code looks like:

AudioPluginAudioProcessor::AudioPluginAudioProcessor()
     : AudioProcessor (BusesProperties()
                     #if ! JucePlugin_IsMidiEffect
                      #if ! JucePlugin_IsSynth
                       .withInput  ("Input",  juce::AudioChannelSet::stereo(), true)
                      #endif
                       .withOutput ("Output", juce::AudioChannelSet::stereo(), true)
                     #endif
                       )
{
    juce::AudioProcessorParameterGroup rootGroup ("rootgroup", "Root Group", "-");
    rootGroup.addChild (std::make_unique<juce::AudioParameterFloat>  ("amount", "Amount", 0.0f, 1.0f, 0.5f));

    auto subGroup = std::make_unique<juce::AudioProcessorParameterGroup> ("group", "Sub Group", "-");
    subGroup->addChild (std::make_unique<juce::AudioParameterFloat>  ("subamount", "Sub Amount", 0.0f, 1.0f, 0.5f));

    rootGroup.addChild (std::move (subGroup));
    setParameterTree (std::move (rootGroup));
}

Though the parameters in the parent group report themselves with unit 0:

[Scan Parameters]
Info:  ===Scan Parameters ====================================
Info:  This component exports 3 parameter(s)
Info:     Parameter 000 (id=733630552): [title="Amount"] [unit=""] [type = F, default = 0.500000, unit = 0]
Info:     Parameter 001 (id=8674968): [title="Sub Amount"] [unit=""] [type = F, default = 0.500000, unit = 98629247]
Info:     Parameter 002 (id=1652125811): [title="Bypass"] [unit=""] [type = T, default = 0.000000, unit = 0]
[Succeeded]

the sub group’s parent ID is reported as invalid:

[Scan Units]
Info:  ===Scan Units ====================================
Info:  This component has 2 unit(s).
Info:     Unit000 (ID = 0): "Root Unit" (parent ID = -1, programlist ID = -1)
ERROR: Unit 001: Invalid parent ID!
[XXXXXXX Failed]

I believe this is because the sub group’s parent ID is the hash of the root group’s (now non-empty) identifier. This works when the root group has no identifier as (luckily) the hash of the empty string is 0 which maps to the Vst::kRootUnitId used in the VST wrapper for the root unit. If you give the root group a non-empty identifier, the hash becomes non-zero and the sub group’s parent ID does not match the root unit.

In my application I can probably enforce that the root group has no ID (as long as other plugin formats are happy with that too) but it seems that the behaviour isn’t by design? In addition it seems like the hashing algorithms will regularly produce names out of the expected range for VST?

Could someone familiar with this confirm whether these are indeed bugs, or whether I’m just using the API incorrectly?

That looks like something amiss in the framework. We’ll investigate.

The implementation of getUnitID is suspicious. Could you try replacing it with this?

        return (group == nullptr || group->getParent() == nullptr) ? Vst::kRootUnitId : std::abs (group->getID().hashCode());

Thanks @t0m - though I might be inclined to go for

 return (group == nullptr || group->getParent() == nullptr) ? Vst::kRootUnitId : (group->getID().hashCode() & 0x7fffffff);

to avoid the edge case where std::abs(std::numeric_limits<int32_t>::min()) can’t be represented as a positive 32-bit integer?

That’s an improvement, but will map it onto 0 which is also problematic in this particular case. Does the suggestion fix your problems (assuming you’re not spectacularly unlucky)? I’ve not yet tested it.

Ah yes indeed :slight_smile: in the unlucky case it’s no better. The suggestion you propose does fix both issues thanks.

I guess there are a number of unlucky strings which might hash to 0 or std::numeric_limits<int32_t>::min(), you’ve just got to be very unlucky.

I’ve added some checks so a debug build will flag any problematic group IDs, including checking for duplicates.

Thanks @t0m - one minor observation, re: your comment:

        // From the VST3 docs:
        // Up to 2^31 parameters can be exported with id range [0, 2147483648]
        // (the range [2147483649, 429496729] is reserved for host application). 

I’d note that the VST3 docs refer to a reserved range for parameter IDs, not unit IDs - but it’s clear the same restriction applies to unit IDs too.

I’m guessing this restriction might also have been the cause of this code in generateVSTParamIDForParam - from that code:

       #if JUCE_USE_STUDIO_ONE_COMPATIBLE_PARAMETERS
        // studio one doesn't like negative parameters
        paramHash &= ~(((Vst::ParamID) 1) << (sizeof (Vst::ParamID) * 8 - 1));
       #endif

When this code was introduced it seems it wasn’t clear/documented that some of the ID range was reserved: Automation not working in Studio One 3

1 Like