Possible Bug in ComboBoxAttachment?

It seems to me that the AudioProcessorValueTreeState::ComboBoxAttachment requires the user to observe some strange and undocumented restrictions on the itemID.

  • value range must begin with 0
  • itemID must be Item index + 1

The setValue method uses the item index to select the item.

    void setValue (float newValue) override
    {
        const ScopedLock selfCallbackLock (selfCallbackMutex);

        {
            ScopedValueSetter<bool> svs (ignoreCallbacks, true);
            combo.setSelectedItemIndex (roundToInt (newValue), sendNotificationSync);
        }
    }

The comboBoxChanged method on the other hand evaluates the itemID to calculate the a new parameter value.

    void comboBoxChanged (ComboBox* comboBox) override
    {
        const ScopedLock selfCallbackLock (selfCallbackMutex);

        if (! ignoreCallbacks)
        {
            beginParameterChange();
            setNewUnnormalisedValue ((float) comboBox->getSelectedId() - 1.0f);
            endParameterChange();
        }
    }

I would propose to make the implementation more flexible by always using the item ID (just replace setSelectedItemIndex with setSelectedId). Or if that should not be an option at least include the existing restrictions with the documentation. The proposed change will allow to use arbitrary parameter value ranges with arbitrary step size. the only remaining restrictions would be:

  • only integer values allowed
  • no zero allowed
    the latter can be even worked around using appropriate Value<>itemID conversion like this:
int getItemIdFromVal(float val) { return val != 0 ? roundToInt(val) : INT_MAX; }
float getValFromItemId(int itemId) { return itemId != INT_MAX ? (float) itemId : 0.f; }

Linking this threadā€¦

Thanks for the link. Seems I am not the only one to find that odd.
I have now modified some of the AudioProcessorValueTreeState dependent classes and added my own LabelListener class to solve these problems for me.
In your case you might be able to use the fft window size as itemID to maintain the semantics of the implementation and to sort the combobox entries just as you find useful since the item index does not get used to retrieve the item value.
I have even gone a step further and implemented a populate routine that fills the combobox with the appropriate text values provided by the parameter->getText function. This allows the combobox to be populated as soon as it gets connected with the parameter through a comboBoxAttachment.
I am going to upload the solution to github as soon as I have an example jucer project ready.

I share your observation, that the implementation is inconsistent and error prone. However, I would draw slightly different conclusions:

  1. It is a bug to rely on the ID when updating the parameter in comboBoxChanged but relying on the index when updating the comboBox in setValue. The most flexible for the user is to use the ID, but in any case, it needs to use the same information in both directions.

  2. Using INT_MAX as base for the conversion will not work for numerical reasons. The INT uses the same number of bits like a float (IIRC). Taking conversion errors into account, it cannot map reliably every state from int to float and back. But IMHO it is not needed anyway because of the next point:

  3. The callback setValue uses setUnnormalisedValue, hence it takes the range of the parameter into account. So the user is free to create itā€™s own mapping, allowing more states than the combobox displays. The drawback is, it is undefined, if a number in the host is selected, that doesnā€™t exist as ID in the combobox (which means, the combobox is not updated).

TL;DR:
agreeing with @martinrobinson-2 and @soundbytes1, ComboboxAttachment has to use consistently either the ID of combobox items or the index.

Argument for ID: allows the developer to change the order of items retaining the selections in existing states
Argument for index: avoids numbers that canā€™t be mapped on an item in the combobox.

How to fix this is up to @jules or @t0m, who was the last to work on that according to git.

I am using INT_MAX simply as a substitute for the 0 value so that it can be used in the context of the ComboBox. This allows the user to include 0 as a parameter value, which otherwise would not be possible.

Ah sorry, my bad. But I think the current solution shifting the IDs by one works just as well.

Depends. I needed a solution that works with a parameter range (-3 ā€¦ 3).

Was there ever any explanation of this behavior? It sure looks like a bug. It would be really nice to use the ID as the parameter value.

I agree with the above. I just lost some time tracking down the ā€˜bugā€™.
can you at least add a couple of documented assertions in ComboBoxAttachement?

How about normalising the float parameter value using the range, then using that to determine the selected index?

void setValue (float newValue) override
{
    const ScopedLock selfCallbackLock (selfCallbackMutex);

    if (auto* p = state.getParameter (paramID))
    {
        auto normValue = state.getParameterRange (paramID)
                              .convertTo0to1 (newValue);
        auto index = roundToInt (normValue * (combo.getNumItems() - 1));

        if (index != combo.getSelectedItemIndex())
        {
            ScopedValueSetter<bool> svs (ignoreCallbacks, true);
            combo.setSelectedItemIndex (index, sendNotificationSync);
        }
    }
}

void comboBoxChanged (ComboBox*) override
{
    const ScopedLock selfCallbackLock (selfCallbackMutex);

    if (! ignoreCallbacks)
    {
        if (auto* p = state.getParameter (paramID))
        {
            auto newValue = (float) combo.getSelectedItemIndex() / (combo.getNumItems() - 1);
            jassert (isPositiveAndNotGreaterThan (newValue, 1.0f));

            if (p->getValue() != newValue)
            {
                beginParameterChange();

                p->setValueNotifyingHost (newValue);

                endParameterChange();
            }
        }
    }
}

Iā€™ve not given this much testing yet, but this should remove any dependency on IDs. The only assumption here is that your combo box labels are distributed evenly across your (arbitrary) range.

That sounds good to me.
However, another option would be to allow ranges like
NormalisableRange<float> (0, 16, 1), but allowing to connect to a Combobox with less items. That way the developer could reserve items for later use (since you cannot hide items in a ComboBox).
On the other hand, that could be the argument to rely on itemIDs.

The only thing I know is, it should be consistent to use either index or IDā€¦

1 Like

I went with being ID agnostic - itā€™s much less confusing for JUCE users.

https://github.com/WeAreROLI/JUCE/commit/009eb887efaa38b5f00dd513c4f6eaadafaa98ef

1 Like

I had forgotten that it wasnā€™t possible and just got caught doing it.
could we get an assertion for that?
something like:

jassert (c.getNumItems() == state.getParameter (paramID)->getNumSteps());

added to AudioProcessorValueTreeState::ComboBoxAttachment::Pimpl() ?