Enum vs Integer

I implemented my own parameter system on top of Juce 3 that handled non-linear and linear mappings, text to value and value to text and booleans and enums etc for the various plugin formats. I’m updating to Juce 7 and can see that progress has been made here, but there are still a few missing bits:

Right now AudioProcessorParameter has isBoolean, and looks to be correctly handled.

The only problem I can see is there doesn’t any distinction between an Integer, and an Enum. An Integer I consider as a regular counting number that you would not want to select from combo box / popup menu, for example the number of samples delay for a plugin for a plugin that aligns audio. An Enum I consider to be something I’d like to pick from a combo box / popup menu, for example a filter type of low, band, high, notch etc.

It looks like right now anything that returns true from isDiscrete() is considered an Enum, which means that possibly thousands of strings will be created inside functions like:

StringArray AudioProcessorParameter::getAllValueStrings() const
{
    if (isDiscrete() && valueStrings.isEmpty())
    {
        auto maxIndex = getNumSteps() - 1;

        for (int i = 0; i < getNumSteps(); ++i)
            valueStrings.add (getText ((float) i / (float) maxIndex, 1024));
    }

    return valueStrings;
}

Currently the function isDiscrete() is meant to indicate that the parameter is exposed as being quantised to the host, which would be true for an integral parameter, so there is another function needed to indicate that a list of strings is needed:

virtual bool AudioProcessorParameter::isEnum()
1 Like

I think what you’re looking for is JUCE: AudioParameterChoice Class Reference.

1 Like

Hey Andy, there’s a whole bunch of classes now in JUCE that derive from AudioProcessorParameter that handle ints, floats bools and choices. And for the numbers that includes ranged parameters supporting skew and custom handling using lambda fuctions. There are also attachment classes for linking to corresponding UI controls.

If it’s not worth moving from your existing code then you could probably do worse than look at the source for AudioParameterChoice to see what’s going on.

1 Like

Thanks for the reply’s trying to help, but when I post about something it’s because actual changes are needed in Juce, not because of some easy layering on top of Juce which I already did around 10 years ago. I’m sick of having to constantly alter Juce to make it work properly every time I update.

Let me open the hood a little more so you guys can understand what I’m actually talking about. For example here is the inside the audio unit wrapper:

    void addParameters()
    {
        parameterGroups = juceFilter->getParameterTree().getSubgroups (true);
        ....
        parameterValueStringArrays.ensureStorageAllocated (numParams);

        for (auto* param : juceParameters)
        {
            OwnedArray<const __CFString>* stringValues = nullptr;

            auto initialValue = param->getValue();
            bool paramIsLegacy = dynamic_cast<LegacyAudioParameter*> (param) != nullptr;

            if (param->isDiscrete() && (! forceUseLegacyParamIDs))
            {
                const auto numSteps = param->getNumSteps();
                stringValues = new OwnedArray<const __CFString>();
                stringValues->ensureStorageAllocated (numSteps);

                const auto maxValue = getMaximumParameterValue (param);

                ....

                for (int i = 0; i < numSteps; ++i)
                {
                    auto value = (float) i / maxValue;
                    stringValues->add (CFStringCreateCopy (nullptr, (getTextValue (value).toCFString())));
                }
            }

            if (paramIsLegacy)
                param->setValue (initialValue);

            parameterValueStringArrays.add (stringValues);
        }

        if ((bypassParam = juceFilter->getBypassParameter()) != nullptr)
            bypassParam->addListener (this);
    }

This code rebuilds the list of strings based on not checking in on the StringArray virtual function overload to determine if should build an enum list or not, it instead uses the isDiscrete() flag, which based on the comments in code should be called something like isQuantizedInHost():

    /** Returns whether the parameter uses discrete values, based on the result of
        getNumSteps, or allows the host to select values continuously.

        This information may or may not be used, depending on the host. If you
        want the host to display stepped automation values, rather than a continuous
        interpolation between successive values, override this method to return true.

        @see getNumSteps
    */
    virtual bool isDiscrete() const;

I’m not seeing anywhere here that isDiscrete → isEnum, but that’s how it’s being used, incorrectly. To remind you of the actual issue: the case I’m talking about is a discrete Integer vs discrete Enum, they are not the same. The integer does not need a combo box / popup menu for each and every number, but something that is an Enum does.

I don’t see any required changes. What you’re describing is a choice parameter which is something JUCE already supports.

I’ll break it down even more for you, this is about how the parameters are exposed to DAWs, not about Juce UIs or anything like that. This is what I’m talking about:

  1. create an au plugin
  2. add an integer using whatever class you want, but make sure it returns true from isDiscrete()
  3. call the parameter “samples delay” and make it range from 0 to 100,000, with default value 50,000
  4. compile the plugin and load it in Ableton Live
  5. click on the |> triangle to show the parameters in the bottom Ableton editor
  6. click on the combo box samples delay and have your computer hang for 5 minutes or so while 100,001 strings are created and stuck into a list to display in a popup menu

Here is what happens:

or if you reduce the size of the integer from 0 to 10,000, then you get a list like this of 10,001 items, not ideal!

Are we on the same page yet?

If you look at the implementation of AudioParameterInt, you’ll see that it doesn’t override isDiscrete(), so it returns false. I don’t know about AU, but in VST3 it’s not possible to distinguish a list of numbers from any other list. You just have ParameterInfo::stepCount:

Anything with stepCount > 0 shows a list in Cubase, which is filled by calling getParamStringByValue on all possible values. Returning true from isDiscrete() has the effect of setting stepCount, so what you’re actually telling the host is “this is a list of values”. If you don’t want that, you have to return false, and set a normalisable range with an interval of 1, like AudioParameterInt does. You can’t have stepped automation lines without lists of values. There are a number of plugins out there which seem to have this confused, to this unfortunate effect:

Indeed.

Thanks kamedin for understanding what I’m talking about. The quantised dB values are a great example of this going wrong, thanks for posting this - it’s a perfect example of how the flawed comment in the Juce code and oddly named isDiscrete function have lead to issues for plugin developers.

It sounds like the best workaround for now is just to rename isDiscrete, to isEnum, as this is what it’s doing. I’m fine with quantising a normalised float to whatever range int internally if DAWs are too challenged to support this. I doubt the person wanted quantised dB values would have returned true to isEnum(), and so this mess would have been avoided.

We understood what you were talking about, that’s how we were able to offer you the right solution using modern best practices. Yet you insist on reinventing wheels.

What you keep calling an enum parameter is what JUCE calls juce::AudioParameterChoice. If you want a parameter that uses numeric, whole numbers JUCE also has juce::AudioParameterInt, as @Nitsuj70 pointed out.

There’s really no reason to create your own derivatives of juce::AudioProcessorParameter unless for some reason you need something other than Bool, Choice, Float, or Int - but I doubt any host supports anything other than those.

The naming in the underlying framework is causing issues. I would be fine if isDiscrete() was renamed isChoice() or isEnum(), but both the comment for and the name of isDiscrete() are misleading.

There are many reasons to derive from AudioProcessorParameter’s. My main reason is they didn’t exist in Juce 2 when I wrote my all my classes. It’s great that Juce has finally caught up with most of what I’ve been doing for a long time. Trying to extract parts of what I wrote around 15 years ago to co-exist with what has more recently been added to Juce just to use the Juce classes is actually more work than just using the underlying AudioProcessorParameter classes with a thin wrapper - as long as that class has clearly documented and named methods!

Additionally, it looks like slapping in a AudioParameterChoice would also break my current plugins, because the mapping used does not have equal range at the endpoints. I’ll start a new thread to point out the issues with the current default implementation.

Here is the new thread talking about the quantised mapping spacing: Shortened endpoint domain of choice mapping

I’m double checking all the parameter scaling in the AAX wrapper and noticed this:

        static int32_t getSafeNumberOfParameterSteps (const AudioProcessorParameter& param)
        {
            return jmin (param.getNumSteps(), 2048);
        }

So anyone writing an AAX plugin had better not have more than 2048 steps, otherwise automation and perhaps even project load save may be broken.