Adding parameters post launch to a plugin?

Have a short question related to JUCE parameters in v6

I’m doing some tests here and adding new parameters to the plugin breaks automation even with the JUCE_FORCE_USE_LEGACY_PARAM_IDS=0

So post launch, if we add a parameter to the plugin regardless of legacy param ids or not, it will break automation in Logic?

I can’t reproduce this behaviour using new-style param IDs.

I’m testing with JUCE 6.0.8 (9ac96840a) and Logic 10.6.3.

My steps to test:

  • Build the AudioPluginDemo as an AU
  • Load the plugin in a logic project, and save some automation for both parameters
  • Quit Logic
  • Add a third parameter to the plugin and rebuild
      JuceDemoPluginAudioProcessor()
          : AudioProcessor (getBusesProperties()),
            state (*this, nullptr, "state",
                   { std::make_unique<AudioParameterFloat> ("gain",    "Gain",              NormalisableRange<float> (0.0f, 1.0f), 0.9f),
                     std::make_unique<AudioParameterFloat> ("delay",   "Delay Feedback",    NormalisableRange<float> (0.0f, 1.0f), 0.5f),
                     std::make_unique<AudioParameterFloat> ("another", "Another Parameter", NormalisableRange<float> (0.0f, 1.0f), 0.5f) })
    
  • Open the saved project in Logic.

When I carry out the steps above, the automation data is recalled correctly.

Note that the param IDs for all parameters must be identical across versions. This means that if you were previously using legacy param IDs, you must continue to use legacy IDs; if you were previously using non-legacy IDs, you must continue to use non-legacy IDs.

To provide more suggestions, I think I’d need more information about your project. How are you adding parameters to the plugin? Are you sure that you’re not inadvertantly modifying existing param IDs when you add new parameters?

@reuk,

This is strange behavior. Is it possible that the int hash of the parameterID string just happens to mean that the unordermap is maintaining the correct order by chance? When you move into a more production situation where you 100s of parameters, then it starts to show up.

I’m experiencing the same behavior as highlighted in this thread:

“The reason AudioUnit (certainly in Logic Pro, although I’m surprised the JUCE host is doing the same) is showing you the parameters in a different order is because it lists them by alphabetical ID, rather than in the order declared by the plug-in. This also adds the restriction that any new parameters added must list “alphabetically higher” than existing IDs (e.g. “aaaa”, “aaab”), otherwise your automation will be messed up. So Logic will introduce this problem for you either way, unless you create some space between the IDs you use (e.g. “aaaa”, “bbbb”, etc.)."

Thanks, I just tested this in the DSPModulePluginDemo (with 60 parameters) and adding a new parameter does indeed change the assignment of existing automation data in Logic. I’m investigating now.

I’ve done a bit more digging. At the moment, I’m inclined to say that this behaviour is a bug in Logic.

According to the the documentation for the AudioUnit framework, “An audio unit parameter is defined by the triplet of audio unit scope, element and parameterID.” When adding a new parameter to a JUCE plugin, the scopes, elements, and IDs of all existing parameters are unaffected, so I think Logic should treat these as the “same” parameters.

When I tested some other hosts (Live 11.0.6 and Reaper 6.34), they recalled automation correctly after adding a parameter, so the issue only seems to affect Logic.

I noticed that the implementation of the kAudioUnitProperty_ParameterList property was returning a sorted list of AudioUnitParameterID. If I change the implementation to return the IDs in the order that the parameters were originally added to the plugin, then automation recall works correctly in Logic - as long any “new” parameters are added at the end of the list. Perhaps Logic is storing an index into this parameter list instead of storing the parameter’s ID.

I’ll file a bug report with Apple and see what they say.

5 Likes

Thanks @reuk!

It is definitely a major issue as it means something as fundamental as the parameter system is more or less not working as it is supposed to. Hopefully Apple get back with a solution.

Prior to switching to the new JUCE parameter system, we were maintaining versioning over the parameters, if say we added a parameter in v1.1, we’d pass in the version number at construction and we would then sort the list based on versioning before reporting to the DAW.

The remapping I’ve observed here suggests exactly this, and I’d agree it makes AudioProcessorParameterWithID a real problem in Logic Pro. Did you ever get a response from Apple @reuk ?

1 Like

No, unfortunately we never heard back.

Thanks @reuk - that’s disappointing, but not wholly surprising.

With that in mind, would you agree that AudioProcessorParameterWithID in its current form doesn’t allow a reliable way to add/change/remove parameters without possibly breaking previously-saved automation in Logic Pro?

If so, do you think there’s anything in JUCE that could be done to ease the pain for developers with released plugins based on AudioProcessorParameterWithID that need (for example) new parameters added? Otherwise, there seem to be only suboptimal options:

  1. reverse engineer the ID hashing and choose AudioProcessorParameterWithID::paramID values which hash higher than all existing parameters
  2. break saved automation in Logic with every new parameter, or
  3. revert to AudioProcessorParameter (without IDs), breaking host automation in all hosts once but at least providing the opportunity to add parameters later.

Any other ideas would be welcomed of course! :slightly_smiling_face:

Unfortunately, yes.

The best case is that Logic fixes this bug internally. I’ve sent another message to our contact at Apple, just in case they missed it first time round.

It looks like, to work around this issue in JUCE, we would need some mechanism to ensure that the parameters of the ‘current’ version of the plugin are always returned with the same IDs and in the same order, and any further parameters are always returned at the end of the parameter list. This seems difficult to do in an elegant way. Even falling back to the raw parameter order doesn’t seem very robust, as it may prevent parameters from retroactively being added to existing parameter groups.

I think using a custom hash function, like you suggested, is probably the way to go, but I’m currently not sure of the best way to integrate that into JUCE. The hash function would need a fair amount of plugin-specific knowledge (at a minimum, it would need a consistently-ordered list of “new” parameter IDs, so that we can do something like newIDs.contains (paramID) ? (large_num + newIDs.indexof (paramID)) : oldHashingFunction (paramID)). Perhaps we could make this configurable with a preprocessor definition, but I’d rather avoid adding yet another obscure configuration option if possible.

I’ll give Apple a bit of time to reply (and myself a bit of time to mull this over) and I’ll try to return to this next week.

Thanks @reuk - I really hope Apple picks up on this too.

It’s going to be difficult to do this in a way that preserves the integrity of existing plugins, but any solution (even ignoring this backward compatibility) seems to end up pretty much looking like VST2-era indexes, with only the ability to add new ones at the “end” using higher IDs.

Good luck with your mulling! Happy to discuss at any time.

Thanks @reuk for going over this very important issue!

Can you please explain more on why/how this solution could fail?

If you call setParameterTree to set the processor’s parameter groups, then the grouped representation will be converted to a flat index-able list using a depth-first traversal. This means that adding a parameter to an earlier group may ‘shift’ along by one index position any parameters that are yet to be traversed.

1 Like

Thanks @reuk, that makes sense.

In that case, would be possible to expose a way for the user to reorder flatParameterList before it gets sent to the host?

We currently don’t send the flat parameter list directly to the host. Instead, the AU wrapper builds up an ordered list of parameter IDs and reports those to the host.

We could certainly change the behaviour of the AU wrapper so that it reports the IDs in a different order. IMO the trickiest part of this problem is designing an interface for the reordering operation that isn’t confusing.

Allowing for arbitrary parameter reordering doesn’t feel like a great solution. It seems like an overly-general solution for a problem that only affects a single DAW in a single format. It adds quite a lot of complexity, and the API would also be easy to use incorrectly. Users might provide a ‘reordered’ list that has a different length to the plugin’s parameter list. They might provide a list that references unregistered or non-existent parameters, or with duplicate entries.

I’m not ruling it out completely, but I’d like to try out some other ideas first.

1 Like

FWIW I’ve now put together a very simple AU plugin using Apple’s AudioUnit SDK that exhibits the same problem:

#include "AUEffectBase.h"

class Testau : public ausdk::AUEffectBase {
public:
  explicit Testau(AudioComponentInstance ci) : AUEffectBase{ci} {}

  OSStatus GetParameterList(AudioUnitScope inScope,
                            AudioUnitParameterID *outParameterList,
                            UInt32 &outNumParameters) override {
    if (inScope != kAudioUnitScope_Global)
      return noErr;

    if (outParameterList != nullptr)
      std::copy(parameters.cbegin(), parameters.cend(), outParameterList);

    outNumParameters = parameters.size();
    return noErr;
  }

  OSStatus GetParameterInfo(AudioUnitScope inScope,
                            AudioUnitParameterID inParameterID,
                            AudioUnitParameterInfo &outParameterInfo) override {
    if (inScope != kAudioUnitScope_Global)
      return noErr;

    auto const str = std::to_string(inParameterID);
    outParameterInfo.cfNameString =
        CFStringCreateWithCString(nullptr, str.c_str(), kCFStringEncodingASCII);
    outParameterInfo.minValue = 0.0;
    outParameterInfo.defaultValue = 0.5;
    outParameterInfo.maxValue = 1.0;
    outParameterInfo.clumpID = 0;
    outParameterInfo.unit = kAudioUnitParameterUnit_Generic;
    outParameterInfo.flags = kAudioUnitParameterFlag_IsReadable |
                             kAudioUnitParameterFlag_IsWritable |
                             kAudioUnitParameterFlag_HasCFNameString;
    return noErr;
  }

private:
  // Adding a new parameter ID to the beginning of this list will break
  // saved automation in Logic.
  std::vector<AudioUnitParameterID> parameters{5, 4, 3, 2, 1};
};

AUSDK_COMPONENT_ENTRY(ausdk::AUBaseFactory, Testau)

To test, I built this plugin as shown above, and saved a Logic project that automated the parameters named “1” and “5”. I then added a parameter ID 7 to the beginning of the parameters vector and rebuilt. Now, when I open the Logic project, parameters “7” and “2” are automated.

Given that this is a completely JUCE-free plugin, I’m very confident that the bug is in Logic.

2 Likes

If you ever foresee changing anything about the host-facing parameter list in future versions of your plugin, then I think maintaining some kind of versioning system (and saving this version information with your plugin state) is absolutely essential.

Of course, the ideal is that DAW sessions created with an older version of your plugin and loaded with a new one will “just work”, but if anything ever goes wrong, having version information accessible to you in setStateInformation will probably make any bugs much easier to identify and fix.

1 Like

FWIW there is also FinalCutPro. I made a simple plugin 3-4 years ago and the only way to get the order of parameters consistent was to set the JUCE_FORCE_USE_LEGACY_PARAM_IDS.

I didn’t update the plugin lately, because I don’t work there any longer, so I don’t know what the current state is.

Please check FCP when making changes. It doesn’t seem to support ParameterGroups either.

1 Like

We’ve now heard back from Apple on this topic. This is a known limitation of Logic, but not one that is likely to be fixed.

We’ll add some sort of workaround to JUCE, but this will need some careful thought to ensure that the parameter system remains intuitive, maintainable, and consistent between plugin formats. I’ll update this thread once we’ve made some progress.

2 Likes

Thanks for the update @reuk - you’d imagine that the company that developed both the plugin standard and the host might make them work together properly, but “known limitation” it is… :-/

Good luck with your thinking, I hope you can find a good approach. Please let us know if there’s any help we can offer.

1 Like