Home to Foleys Finest Audio

How to add/override a ComboBox listener?

I’m continuing to explore adapting the Faust Juce architecture to allow PGM options. This week I’m looking into ComboBoxes. I figured out that these are represented in your designs by a Juce::AudioParameterChoice. So I figured out out to add one of those to the Value Tree at the right time and at least it shows up when PGM loads the default GUI. Now I need to make sure it does the right thing.

The way the Faust code works is that you set Menu Label: value pairs in metadata, rather than just a list of Labels. So some adaptation or assumption when writing the original Faust code is needed.

[edit - this, I accomplished.]
The Faust code:

filterdB = vslider("/h:Filter/[03]Slope[style:menu{'12 dB/oct':0;'24 dB/oct':1, '36 dB/oct':2, '58.3 dB/oct':3}]", 0, 0, 3, 1);

generates an entry in the tree that causes this to be discovered:

image

The conversion to a PGM ComboBox takes the menu labels in order and drops the values.

E.g. I think the ComboBox returns a string, whereas my code is looking for an integer.

So how do I find the function that is called when I interact with the ComboBox? The one that sets a value in my DSP code. Or how should I create it? Faust has “zones” which are a pointer to the controlled variable, as well as IDs which as far as I know are unique per plugin and parameter, usually hierarchical, strings.

I’m thinking something like what is in the SignalGenerator example. Main AudioProcessor class.

    treeState.addParameterListener (IDs::mainType, this);

There’s also this ParameterAttachment class which seems like it must figure in there somehow. I can of course associate the control with a parameter address in the editor, so perhaps that is not required to be done in the code.

As you might imagine, the way that Faust does a number of things is:
a) weird (to me, but I’m also not a C++ expert)
b) deeply ingrained and not easily changed, other than what is possible in the architecture file. Which is a lot to be honest!

So I have to reverse engineer both Faust generated code and the Magic stuff and then see how they can be brought together.

Writing down some additional thoughts before they drift away…

Using PGM forces the Faust code to concede all GUI related activities to PGM. In “pure” Faust Juce code, the listeners are created on the fly with each UI control. So I think the best solution involves the PGM code that scans the ValueTree to pick up the default controls. This bit right here:

Something like:

If a matching listener is not found, create one.

Thanks!

I see where listeners are added.

image

The GUI is meant to be standalone. Currently there is no direct coding in the GUI involved.

The ComboBox just like all the other Components are connected to the foleys::MagicGuiState or to an AudioProcessorParameter using a juce::ParameterAttachment.

Some Components allow to bind to a property in the public ValueTree, e.g. ToggleButton or Slider. They have a value property, that can be connected:

In MagicProcessor.postSetStateInformation() you can reference a property that is lazily added:

void EqualizerExampleAudioProcessor::postSetStateInformation()
{
    // MAGIC GUI: let the magicState conveniently handle save and restore the state.
    //            You don't need to use that, but it also takes care of restoring the last editor size
    inputAnalysing.attachToValue (magicState.getPropertyAsValue ("analyser:input"));
    outputAnalysing.attachToValue (magicState.getPropertyAsValue ("analyser:output"));
}

The path in the ValueTree is formed as a path through ValueTree children separated by a colon :, the last part is a property.

It would be a legitimate request to add that to the ComboBox as well. The question is how to supply the list of entries from the Editor.

Thank you for the hint, I will continue investigating. If you are asking me how one might enter a list of text options from the editor, I know an open ended list is more challenging than a single field for everything, but I’d also consider that most combo boxes are only going to have a few selections (though certainly not always the case). For what I’m doing now I’d accept something like a single field for everything with comma separated entries.

Indeed, that is the easiest approach, and it would have to go into a property, so needs to boil down to a single string.
But a ComboBox or any PopupMenu could have a hierarchy, so this approach would be very limiting.
Adding the menu as ValueTree would be theoretically possible, since so far only the View node can have children. But I think that will collide with other options in the future.

I don’t have much in the way of architectural vision, not to mention I just barely understand what is going on. So I am willing to accept improved ComboBox today even if I can’t do multi-level menus. The alternative is less thorough support, so (in my world view anyway) any usable feature helps even if it has limitations.

I’ll have a thought about it, but I am sorry that I cannot give a time estimate when it will happen. I’ll do my best.

No worries, updating the ComboBox text in the editor is not a big deal as I don’t see it changing frequently. As it originates in the Faust code it’s not a big deal to manage it there. Faust ComboBoxes (aka Slider [style:menu]) are different from PGM ComboBoxes and don’t support any hierarchical arrangement. I was thinking about creating a new Component to be a “Faust ComboBox” but:
a) Didn’t think it was worth it
b) Found it easier to do what I did using the existing component even though the mapping was less than perfect.

If you want to use the ComboBox like a Slider, isn’t it an option to use an AudioParameterChoice and connect the ComboBox to that parameter?

Yes, in fact this already works as far as I can tell. My big question at the beginning of this thread was essentially “where is the change listener for the ComboBox?” because I sure did not write one. I just wanted to put a breakpoint on it to see what is actually going on.

Going on the example in the SignalGenerator, the ComboBox returns a float which is representative of the index of the selected entry. If so, it probably already works as expected.

Actually, quick follow up, the code does not appear to work on a small example.

Faust code:

import("stdfaust.lib");
s = vslider("/h:test/Signal[style:menu{'Noise':0;'Sawtooth':1; 'Sine':2}]",0,0,2,1);
process = select3(s,no.noise,os.sawtooth(440),os.osc(220)) * 0.125; 

In https://faustide.grame.fr/

In PGM:

image

image

However, changing the selection doesn’t do anything. It just puts out noise.

Here is my code in the architecture file that creates the proper object in the value tree for PGM. e.g.

{'Noise':0;'Sawtooth':1; 'Sine':2}

becomes

{'Noise','Sawtooth','Sine'}
        virtual void addVerticalSlider(const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init, FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step)
        {
            if (isMenu(zone)) { 
#ifdef MAGIC_COMBO_BOX
                // get the Faust style:menu string and pull off the labels into a string array.
                // the values will be ignored by PGM.  In use it will generate
				// values 0 to n - 1 corresponding to the keys in order.
                std::string menuData = fMenuDescription[zone];
                std::size_t beginQuote = menuData.find("\'");
                std::size_t endQuote = menuData.find("\'", beginQuote + 1);
                std::string menuOption = menuData.substr(beginQuote + 1, endQuote - beginQuote - 1);
                juce::StringArray selectionList(menuOption);

                while (TRUE)
                {
                    beginQuote = menuData.find("\'", endQuote + 1);
                    if (beginQuote == std::string::npos)
                        break;
                    endQuote = menuData.find("\'", beginQuote + 1);
                    if (endQuote == std::string::npos)
                        break;
                    menuOption = menuData.substr(beginQuote + 1, endQuote - beginQuote - 1);
                    selectionList.add(menuOption);
                }

                fProcessor->addParameter(new FaustPlugInAudioParameterChoice(this, zone, buildPath(label), label, selectionList, init));

You’re probably going to laugh, but I now am taking a few days to watch a handful of hour long videos about Juce ValueTrees. I hadn’t really imagined getting involved at this level but I’m finding it interesting and fun to learn all this stuff, even though I immediately forget. Plus once I get this working I can actually use it, which was the whole point to begin with.

The way the Trees are used and in fact which types of Trees are used seems to differ between the PGM examples and Faust generated code, so I have to get to the bottom of that next. I’m pretty zoomed in on WHERE in the code something needs to happen.

All right, I’m back. I’m not sure any of that sunk in, but here we go.

This is where PGM reads the “tree” of parameters and builds the default GUI for it. What appears to be missing is the ComboBox listener, which e.g. in the SignalGenerator example, is declared in the main audioprocessor class directly. However, of course in Faust you need that done for you when necessary.

The loop that starts at line 76 pulls parameters from the “tree” one by one until they’re all gone. It processes the entries and adds them onto the ValueTree “node”.

On each one, PGM initially creates a new ValueTree called “child” and marked as “IDs::slider”.

Then we try to “dynamically cast” the param which we pulled off the tree, to see what kind of data it is. So, since data for a ComboBox is initialized as an “AudioParameterChoice”, the test on line 84 returns a valid pointer. Since that worked, we now assign child to a new ValueTree, this time created with “IDs::comboBox”.

Next, I think what wants to happen is to create a ComboBoxParameterAttachment to the parameter and this little ValueTree we just created. I don’t know what happens to the first “child” ValueTree(IDs::slider) we created. Is it a memory leak? Or, it seems more likely, we need to attach the listener to “node” because the “child” is just a local variable and will not survive the end of the function call.

What I’m hitting now is a type mismatch due to the difference in origin of the parameter.

The Faust generated parameter comes through as an AudioProcessorParameter (generic float) while the PGM expects it at this point to be a RangedAudioParameter, from which AudioParamterChoice used by ComboBox inherits. So I think if I can figure out how to make that happen properly, we might be in business.

I’m glad I looked at this. If I can get Faust to push the comboBox data in as an “AudioParameterChoice” it might work because that is inherited from RangedAudioParameter.

This is where AudioProcessorParameters are created in Faust (JuceParameterUI),

image

I’ll see if I can use the other class for the ComboBox case.

I actually already AM using the AudioParameterChoice when creating the menu item.

I think the issue is with the passed in parameter “zone”.

It works much faster if I just accept public shame and post what I’m doing. Here’s another stab in the dark.

This gives:

I’m clearly doing something very, very bad.

Copyright © 2020-2021 Foleys Finest Audio UG. All rights reserved.