Correct hash generation of plugin parameters

Currently hash codes for plugin parameters are not generated correctly to guarantee to be unique (up to the limit of what a Uint32 can store). It does take a little more work to so it “properly” so it won’t break, but it is possible. Here is an example of how it is currently done if you don’t use legacy indices - too bad if you’re unlucky right?:

static AudioUnitParameterID generateAUParameterID (const AudioProcessorParameter& param)
{
    ...
    AudioUnitParameterID paramHash = static_cast<AudioUnitParameterID> (juceParamID.hashCode());
    ...
}

void JuceAU::addParameters()
{
    ...
    for (auto* param : juceParameters)
    {
        const AudioUnitParameterID auParamID = generateAUParameterID (*param);

        // Consider yourself very unlucky if you hit this assertion. The hash codes of your
        // parameter ids are not unique.
        jassert (paramMap.find (static_cast<int32> (auParamID)) == paramMap.end());

        auParamIDs.add (auParamID);
        paramMap.emplace (static_cast<int32> (auParamID), param);
        Globals()->SetParameter (auParamID, param->getValue());
    }
    ...
}

Here is a possible solution:

  • When parameters are added there needs to be some way to keep track of when, eg the version hint, so each time new groups of parameters are added this version hint gets incremented so it is numerically larger to indicate newer. The version hint could just be the date the parameters were added eg 20230526.
  • Once all parameters are added in the order you want them to appear in the DAW, then the hashes can be generated.
  • All parameters get sorted first by version / date then alphabetically (they have to have a unique name as well, so this will produce a fixed ordering of the parameters)
  • Hashes are generated using that order, and stored into each parameter.
  • If a later parameter has a hash which clashes with an earlier one, a new hash needs to be generated for that parameter, eg by adding 1 until the hash is unique, then that hash is stored.
  • If parameters are removed, they still need to be added to the list with fixed name and version / date, but an additional flag is needed to disable the parameter, so it does not appear anywhere else, but since a fixed ordering and hash resolution is required for the entire life of the plugin the parameter still needs to be added in the first place.

I’m in the middle of going a release which has to use legacy indices, so this isn’t currently an issue for me, and I’m happy to write the code to make this happen.

I want to also point out that “Consider yourself very unlucky if you hit this assertion. The hash codes of your parameter ids are not unique.” doesn’t reflect the reality of the situation. It is blaming the plugin author for choosing parameter ids that don’t hash uniquely, instead of taking responsibility for the choices made by the programmer that actually wrote the code that doesn’t always work and instead just asserts when there is a problem.

What would be useful in the comment is something like: “This code doesn’t work properly in all situations because a single hash like this isn’t guaranteed to be unique. It needs to be fixed in the future. Sorry if your plugin breaks because of it, it’s not your fault, but this code has now made it your problem.”

2 Likes

There is also a pretty easy workaround for all of this:

  • always use a 32bit hash, since that is the lowest common denominator
  • the AudioProcessor needs to check for clashes of parameters as they are added when it is created in all code branches, not just debug
  • if there is an issue with a parameter’s hash clashing then a runtime prompt is needed saying there has been a clash (giving the name and index), and to try appending “_1” or “_2” etc to the parameter id (not the parameter name) by trial and error until the clash is resolved
  • this check needs to be done in the AudioProcessor and by the AudioProcessor, and the hash stored inside each parameter, and this is the hash that is returned to the various plugin formats to use instead of leaving it up to each plugin format to compute their own 32 or 64 bit hashes which could clash

From what I can see in the following code the AAX wrapper just silently fails if there is a clash in the hashes:

void JuceAAX_Processor::addAudioProcessorParameters()
{
    ...
    for (auto* juceParam : juceParameters)
    {
        ...
        aaxParamIDs.add (paramID);
        auto* aaxParamID = aaxParamIDs.getReference (parameterIndex++).toRawUTF8();

        paramMap.set (AAXClasses::getAAXParamHash (aaxParamID), juceParam);
        ...
    }
    ...
}

where this is the type definition of paramMap:

HashMap<int32, AudioProcessorParameter*> paramMap;

and this is the definition of the set function on a HashMap, note that the comment says this will replace an existing item if it already exists:

//==============================================================================
/** Adds or replaces an element in the hash-map.
    If there's already an item with the given key, this will replace its value. Otherwise, a new item
    will be added to the map.
*/
void set (KeyTypeParameter newKey, ValueTypeParameter newValue)        { getReference (newKey) = newValue; }

It also looks like having a juce parameter id of “MasterBypassID” would be bad for AAX plugins since this would clash with their definition of cDefaultMasterBypassID. No doubt other plugin formats also have string ids to be avoided, and those kind of checks would be good for a robust system of parameter ids and hashes.

Also in VST3 wrapper there are these defines:

        paramPreset               = 0x70727374, // 'prst'
        paramMidiControllerOffset = 0x6d636d00, // 'mdm*'
        paramBypass               = 0x62797073  // 'byps'

the last one of which is used as a hash which could collide with parameter hashes.

edit: actually looking into the code all of them can clash, including mdm* which the last character looks like it could be any value 0x00 to 0xff