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.

Quick status update. I’ve learned a lot about how Faust works, that’s for sure.

Using the UI hierarchy metadata, it builds a “tree” of control addresses, e.g. /tremolo/lfo/rate. In a subsequent pass a couple of lists are mapped to the controlled variables in the DSP code, which are managed for the most part by a FAUSTFLOAT * which is just a float *. So now you have something where you have all the controls and their IDs.

This is the “buildUserInterface()” function, which is heavily involved in all of this.

	virtual void buildUserInterface(UI* ui_interface) {
		ui_interface->openTabBox("Echo");
		ui_interface->openHorizontalBox("Delay");
		ui_interface->declare(&fVslider4, "style", "knob");
		ui_interface->addVerticalSlider("EchoDelay", &fVslider4, 0.200000003f, 0.0f, 1.0f, 0.00999999978f);
		ui_interface->declare(&fVslider2, "style", "knob");
		ui_interface->addVerticalSlider("EchoFbk", &fVslider2, 0.300000012f, 0.0f, 1.0f, 0.00999999978f);
		ui_interface->declare(&fVslider5, "style", "knob");
		ui_interface->addVerticalSlider("EchoLvl", &fVslider5, 0.800000012f, 0.0f, 1.0f, 0.00999999978f);
		ui_interface->closeBox();
		ui_interface->openHorizontalBox("Filter");
		ui_interface->declare(&fVslider0, "01", "");
		ui_interface->declare(&fVslider0, "style", "knob");
		ui_interface->addVerticalSlider("LPF", &fVslider0, 10000.0f, 1000.0f, 15000.0f, 0.100000001f);
		ui_interface->declare(&fVslider3, "02", "");
		ui_interface->declare(&fVslider3, "style", "knob");
		ui_interface->addVerticalSlider("HPF", &fVslider3, 100.0f, 40.0f, 1500.0f, 0.100000001f);
		ui_interface->declare(&fVslider1, "03", "");
		ui_interface->declare(&fVslider1, "style", "menu{'12 dB/oct':0;'24 dB/oct':1}");
		ui_interface->addVerticalSlider("Slope", &fVslider1, 0.0f, 0.0f, 1.0f, 1.0f);
		ui_interface->closeBox();
		ui_interface->closeBox();
	}

This is not yet a Juce ValueTree. In fact, in the Faust generated C++ code there are no explicit references to juce ValueTrees at all. The closest we get to that is inheriting MagicProcessor.

The next pass is the “JuceParameterUI” where we take all the things in the list and then create the appropriate entries in a real juce ValueTree.

At this point, for combo boxes anyway, I convert the / path separator used by Faust into a : used by PGM. I did not do any such conversion for sliders and I guess it works OK, but I don’t know if there’s some subtle point here that maybe I am missing.

After the juce:ValueTree is all stuffed full of information (essentially, the FaustProcessor() constructor has finished) then the magicState.createEditor() is called. This is where we read all the info and create the default layout (at least at this point that is what I am doing).

In the PGM SignalGenerator example which shows the use of ComboBox, all of the components are explicitly declared in the Processor class.

image

and the ComboBox is added to the main processor class and the listener also defined here.

This example shows the listener being added to “treeState” which is declared in the main class. I think I should attach the listener to “magicState” because there is no “treeState” defined in the Faust generated class.

I believe that the listener should be added at the same time that the ComboBox itself is created, which is not until the PGM code starts parsing the parameter data in the ValueTree.

image

So the question is whether I can add a listener to magicState in that function. I do not believe that the ComboBox has a listener until one is explicitly added. I think all I could add a listener to in that context would be “node”.

I almost apologize for the stream of consciousness nature of all this, and the possibility that I am really wrong about everything. If anyone reading this concludes that I am wrong about anything, it would help me a lot to know what those things are. Thanks! I know I’m wrong about something as I’ve made several mutually contradictory statements.

By the way, in the current state of the code, I can add e.g. a slider and map it to the same path the ComboBox is controlling. If I switch the ComboBox from 12 to 24, the slider follows. If I switch the ComboBox from 24 to 12, the slider does not follow. Similarly, the ComboBox follows the slider from 12 to 24 but not back. The ComboBox does not correctly control the behavior of the DSP.

Here’s how it looks in Faust IDE:

image

Straight faust2juce:

image

Default PGM:

image

Semi-optimized PGM:

image

Here’s the Faust code for this.

declare name        "Basic Delay Copyright 2021 Holy City Audio";
declare version     "0.96";
declare author      "Gary Worsham";
declare license     "free for non commercial use";
declare copyright   "(c) Gary Worsham 2021";
import("stdfaust.lib");

echoDelay = vslider("h:Delay/EchoDelay[style:knob]", 0.20, 0, 1.0, 0.01) : si.smooth(0.9995);
echoLevel = vslider("h:Delay/EchoLv[style:knob]l", 0.8, 0, 1.0, 0.01) : si.smoo;
echoFeedback = vslider("h:Delay/EchoFbk[style:knob]", 0.3, 0, 1.00, 0.01) : si.smoo;

echoLPF = vslider("h:Filter/[02]LPF[style:knob]", 10000, 1000, 15000, 0.1) : si.smoo;
echoHPF = vslider("h:Filter/[01]HPF[style:knob]", 100, 40, 1500, 0.1) : si.smoo;
filterdB = vslider("h:Filter/[03]Slope[style:menu{'12 dB/oct':0;'24 dB/oct':1}]", 0, 0, 1, 1);

// class below is basic echo with Feedback
echoChannel(delay, level, feedback, low, high) = ( + : echo(echoDelay) ) ~ ( *(echoFeedback))
with {
highPass = _ <: select2(filterdB, fi.highpass(1, echoHPF), fi.highpass(2, echoHPF)) :> _;
lowPass = _ <: select2(filterdB, fi.lowpass(1, echoLPF), fi.lowpass(2, echoLPF)) :> _;
echo(x) = highPass : lowPass : de.delay(ba.sec2samp(1), ba.sec2samp(x));
};

echoOutL = _ <: _,echoChannel(echoDelay, echoLevel, echoFeedback, echoLPF, echoHPF) * echoLevel : +;
echoOutR = _ <: _,echoChannel(echoDelay, echoLevel, echoFeedback, echoLPF, echoHPF) * echoLevel : +;

//=============================================
process =  tgroup("Echo", _,_: echoOutL, echoOutR : _,_);

Yet another debug sequence.

image

Here’s where we “catch” that it’s a juce::AudioParameterChoice variable, used with the ComboBox.

This would be the logical place to add a listener to the program’s tree, but we don’t have that in scope at this time. However we can wait until we get back. I added some code to check out what is going on.

So now we have the magicState in context. I can check what the GUITree is and the ValueTree.

The GUITree contains this.

The ValueTree contains all the information about the controls and DSP settings.

image

I’m not sure if this is correct or not. I only replaced slash with colon on the ComboBox item but it may need to be done for everything. What’s weird about this is that the sliders work.

I know I can look for a specific one in the context of my actual program, but eventually I need to figure out how to generalize that so that the Faust code generator can build it. So I’m thinking about scanning through the list to figure out which ones are ComboBoxes, if that is possible.

One other note, I haven’t figured out how to trigger on anything after the application launches, e.g. when I move a slider to change a value, or after loading the XML file (though that shouldn’t be hard). I’d like to use this Tree View Debugger if possible to see what is going on. Obviously it’s a common issue not to be able to see what is going on even with a simple model.

I think you are mixing two things.
The information what to connect is in the ValueTree.
The method you quoted is actually only creating a ValueTree with the information what would be connected. But nothing is connected t that stage.

Once the ValueTree is known, either by the previous function or loaded from the XML, the MagicBuilder will use that information to create all GUI items.
The connections are done via ParameterAttachments or via valueTree.getPropertyAsValue from the persistent tree.
Each GuiItem is responsible for the connection, because they know what component they wrap and how to connect them. Here an example:
https://github.com/ffAudio/foleys_gui_magic/blob/main/General/foleys_MagicJUCEFactories.cpp#L338

There is a convenience method I mentioned previously that you use to connect to the persistent ValueTree, it is called getPropertyAsValue ("tree:tree:property"). Ideally you attach your values in the processor in the overridden postSetStateInformation() hook, because the persistent state might have been replaced by a load operation from the host.

You can see that in action in the EqualizerExample for the input analyser and output analyser switches:

Now you can use the Value (but only from the message thread!). If you need it thread safe you need to create some atomic wrapper around. I think I have one in some example, maybe even the EqualizerExample…

Hope that clears things up a bit.

Thank you for your response. I know I am probably coming at this from the wrong direction, but I have a simple question.

In the SignalGenerator example, the ComboBox and its listener are explicitly declared in the main processor class. In my Faust generated program, that is not currently happening, although I am putting information into the ValueTree that PGM picks off correctly as a ComboBox. So are you saying that if I use “attachToValue” it creates or enables a ComboBox listener? How can I put a breakpoint on the ComboBox listener to see what it does?

Sorry to be so persistent about this ComboBox and I am reviewing what you say very carefully and comparing the structure of your examples to Faust generated code. I’m afraid that some or all of the complication may be that the Faust generated code works quite a bit differently than your Juce examples. Everything is sort of “built up” rather than being declared outright in the Processor class. It doesn’t help that I don’t understand this all that well either, but I’m sure not going to change it.

There’s nothing that corresponds for example to “inputAnalyzing” in my main class definition.

Over in a “mydsp” class, is the declaration of all of the variables used internally by the DSP code.

e.g.

class mydsp : public faustdsp {
	
 public:
	
	int fSampleRate;
	float fConst0;
	float fConst1;
	float fConst2;
	FAUSTFLOAT fVslider0;
	float fConst3;
	float fRec2[2];
	FAUSTFLOAT fVslider1;
	FAUSTFLOAT fVslider2;
	float fRec4[2];
	float fVec0[2];
	FAUSTFLOAT fVslider3;
	float fRec5[2];
	float fRec3[2];
	float fRec6[3];
	float fVec1[2];
	int IOTA;
	float fRec1[262144];
	FAUSTFLOAT fVslider4;
	float fRec7[2];
	float fRec0[2];
	FAUSTFLOAT fVslider5;
	float fRec8[2];

The mydsp class is instantiated in the FaustPluginAudioProcessor constructor:

image

image

I can see over here in buildUserInterface() that fVslider1 is the variable that tracks the “Slope” setting coming from the ComboBox.