Jump to content

TIP: Showing/Hiding GUI Controls (without feedback loop)


Dewdman42

Recommended Posts

Apple hints that it is possible to show/hide GUI controls in Scripter dynamically, which can be very helpful in many cases. However many users have reported odd problems when attempting to do so, due to endless feedback loops that can easily be caused by UpdatePluginParameters() call.

 

This article will attempt to explain the problem, why it happens, and how to work around it to create nice dynamic Scripter GUI's. Sorry its so long, but want to make sure its explained clearly.

 

First...a list of known behaviors that need to be taken into consideration...

 

Scripter Built-in PluginParameter Behavior

 

  1. All plugins, including Scripter provide internal plugin parameters. These parameters can be linked to automation, for example. Also, when a LogicPro project is saved with plugins on some channels, the existing current parameter values are saved on each instance and remembered the next time you load the project. Some plugins will provide GUI access to some or all of the configured parameters in the plugin.
     
     
  2. Scripter provides access to plugin parameters in the PluginParameters array. This array can be used to define the plugin parameters as described above, including the ability to display GUI controls for them, link them to automation, etc. They are different then other javascript variables because of how they are saved in a persistent manner with the channel preset or LogicPro project, etc.
     
     
  3. ParameterChanged() is a callback that is called by Scripter whenever a plugin parameter is updated by LogicPro. This can occur when the user changes a control on the GUI or it can happen when automation is used to adjust the value. It can also happen when loading Scripter into the channel strip if it was previously saved in the LogicPro project, or channel preset, etc. As Scripter is loaded, with the script while opening a project; it will read from the project the last known plugin parameter values, and make a call to ParameterChanged for each one once the script has fully bootstrapped.
     
     
  4. Its important to note that ParameterChanged is called AFTER the value has already been changed or set from whatever it previously was. There is no built-in way to know what the previous value was inside the ParameterChanged callback.
     
     
  5. Its important to note that ParameterChanged is sometimes called by Scripter even when the value has not changed at all, for various reasons.
     
     
  6. When you need to change some aspect of the PluginParameters array, perhaps to show/hide certain parameter controls, it is necessary to call the UpdatePluginParameters function. This function will cause the GUI to be updated, but also...it will internally make a call to ParameterChanged, for every single element in the PluginParameters array, regardless of whether any value has changed.
     
     
  7. Due to the previous point, its easily possible to get endless feedback loops with the ParameterChanged function if you attempt to call UpdatePluginParameters from inside ParameterChanged.
     
     
  8. It is also possible to code around this problem, and this article will provide one approach

 

 

Why is there a feedback loop?

 

So why the endless feedback loops? Here is a simple problematic example, which uses a checkbox to indicate whether to show or hide another control. Although it does what you might expect by showing and hiding the slider control, it will end up with an endless feedback loop which you can observe in the bottom of the Scripter Editor window caused by UpdatePluginParameters re-calling ProgramChanged with a value of zero, which still triggers the recursive call to UpdatePluginParameters, and endless loop results.

 

function HandleMIDI(event) {
   event.send();
}

var PluginParameters = [];

PluginParameters.push({
   type: "checkbox",
   name: "show",
   defaultValue:0,
   hidden: false
});

PluginParameters.push({
   type: "lin",
   name: "slider",
   defaultValue:0,
   minValue:0,
   maxValue:500,
   numberOfSteps: 500,
   hidden: true
});

function ParameterChanged(id, val) {

Trace(Date.now() + ` parameter changed ${id} ${val}`);

   if( id == 0 ) {
       if( val == 0 ) {
           PluginParameters[1].hidden = true;
       }
       else {
           PluginParameters[1].hidden = false;
       }
       UpdatePluginParameters();
   }
}

 

here is another similar example, In this case I want to use a single menu control to decide which slider out of three possible sliders to display...a very common scenario.

 

function HandleMIDI(event) {
   event.send();
}

PluginParameters = [];

PluginParameters.push({
   type: "menu",
   name: "chooser",
   valueStrings: ["one","two","three"],
   defaultValue: 0
});

PluginParameters.push({
   type: "lin",
   name: "slider one",
   minValue: 0,
   maxValue: 500,
   numberOfSteps: 500,
   defaultValue: 0,
   hidden: true
});

PluginParameters.push({
   type: "lin",
   name: "slider two",
   minValue: 0,
   maxValue: 500,
   numberOfSteps: 500,
   defaultValue: 0,
   hidden: true
});

PluginParameters.push({
   type: "lin",
   name: "slider three",
   minValue: 0,
   maxValue: 500,
   numberOfSteps: 500,
   defaultValue: 0,
   hidden: true
});

function ParameterChanged(id, val) {
Trace(Date.now() + ` parameter changed ${id} ${val}`);
   if(id==0) {
       for(let i=1; i<=3; i++) {
           if(i == val+1) {
               PluginParameters[i].hidden=false;
           }
           else {
               PluginParameters[i].hidden=true;
           }
       }
       UpdatePluginParameters();
   }
}

 

Similar as the previous example, it works as expected, but Scripter goes into feedback loop which will cause lots of problems.

 

The reason this is happening in both cases is because of point #6 above.

 

When you need to change some aspect of the PluginParameters array, perhaps to show/hide certain parameter controls, it is necessary to call the UpdatePluginParameters function. This function will cause the GUI to be updated, but also...it will internally make a call to ParameterChanged, for every single element in the PluginParameters array, regardless of whether any value has changed.

 

That behavior can cause a recursive feedback loop without an exit condition, as in the cases above.,

 

Factory Example - One possible approach

 

There is one factory example which has an example for showing/hiding controls. It kind of works around the above dilemma, but not really in a way that is ideal. But I will elaborate. Its the one called "Stutter v2". its not completely obvious from initially looking at it why it works, but the reason is simple once you know to look for it. Basically this example uses two checkboxes like a buttons. One check box shows the controls, the other checkbox hides them. When you click the checkbox, that is the same as setting its value to 1. Inside ParameterChanged, the code to call UpdatePluginParameters is only called if and when the checkbox is set to 1 from clicking on it. But this code also makes sure to set the checkbox back to 0 just before it calls UpdatePluginParameters. That way it doesn't end up in an endless loop. Essentially when you click it once, it signals one call to UpdatePluginParameters and then resets itself back to zero. Zero is the exit condition that avoids recursive endless loop.

 

Here is a simplified script that shows this process in action:

 

1833651021_ScreenShot2021-04-14at11_18_45PM.jpg.e0ce09db685747558dd69aa95c053937.jpg

 

function HandleMIDI(event) {
   event.send();
}

var PluginParameters = [];

PluginParameters.push({
   type: "checkbox",
   name: "show",
   defaultValue:0
});

PluginParameters.push({
   type: "checkbox",
   name: "hide",
   defaultValue:0
});

PluginParameters.push({
   type: "lin",
   name: "slider",
   defaultValue:0,
   minValue:0,
   maxValue:500,
   numberOfSteps: 500,
   hidden: true
});

function ParameterChanged(id, val) {

Trace(Date.now() + ` parameter changed ${id} ${val}`);

   if(id == 0 && val == 1) {
       PluginParameters[2].hidden = false;
       SetParameter(0,0);
       UpdatePluginParameters();
   }
   
   if(id == 1 && val == 1) {
       PluginParameters[2].hidden = true;
       SetParameter(1,0);
       UpdatePluginParameters();
   }
}

 

As you can see, this simple example works as intended, in a manner similar as the factory example. However it's also kind of a clumsy way to show/hide controls on the GUI.

 

 

CheckBox Solution

 

So here is one little design pattern that so far is working pretty well for me, for the checkbox example. This basically will check the current status of the GUI to make sure a change is really happening before calling UpdatePluginParameters unnecessarily. Use this example and you can see it works with no feedback loop:

 

1919606040_ScreenShot2021-04-14at11_20_08PM.jpg.e11a9fa83e5e5bda27ea752e31ab9e6a.jpg

 

function HandleMIDI(event) {
   event.send();
}

var PluginParameters = [];

PluginParameters.push({
   type: "checkbox",
   name: "show",
   defaultValue:0,
   hidden: false
});

PluginParameters.push({
   type: "lin",
   name: "slider",
   defaultValue:0,
   minValue:0,
   maxValue:500,
   numberOfSteps: 500,
   hidden: true
});

function ParameterChanged(id, val) {

Trace(Date.now() + ` parameter changed ${id} ${val}`);

   if( id == 0 ) {
       if( val == 0 ) {
           if(PluginParameters[1].hidden != true) {
               PluginParameters[1].hidden = true;
               UpdatePluginParameters();
           }
       }
       else {
           if(PluginParameters[1].hidden != false) {
               PluginParameters[1].hidden = false;
               UpdatePluginParameters();
           }
       }
   }
}

 

Hopefully you are starting to see by now what is required to avoid recursive feedback loop with UpdatePluginParameters. One important point, do not base the decision of whether to call UpdatePluginParameters on the actual parameter value itself. The reason is because its very very difficult to tell when that value has actually been changed due to point#4 in the list above, but also because when the script is loading it needs to bootstrap initial saved values and so the first time it won't have been changed, for example. This design pattern intentionally looks at the hidden attribute itself, the thing we are going to change, to see if it even needs to be changed. If not...then don't call UpdatePluginParameters. That eliminates the the recursive feedback and also avoids any dependency on knowing what the parameter value itself is or whether it has actually changed, which is very hard to detect with the current mechanisms.

 

Menu-driven Example

 

Here is a design pattern for a menu driven approach to showing whatever controls you want. Like the previous example, we basically just make to only do the change and call UpdatePluginParameters if and when the GUI hidden state needs to be changed, therefore, it works without feedback loop.

 

887650252_ScreenShot2021-04-14at11_21_32PM.jpg.59266ab8b008c30b3dae5ab34fd7fa79.jpg

 

function HandleMIDI(event) {
   event.send();
}

PluginParameters = [];

PluginParameters.push({
   type: "menu",
   name: "chooser",
   valueStrings: ["one","two","three"],
   defaultValue: 0
});

PluginParameters.push({
   type: "lin",
   name: "slider one",
   minValue: 0,
   maxValue: 500,
   numberOfSteps: 500,
   defaultValue: 0,
   hidden: false
});

PluginParameters.push({
   type: "lin",
   name: "slider two",
   minValue: 0,
   maxValue: 500,
   numberOfSteps: 500,
   defaultValue: 0,
   hidden: true
});

PluginParameters.push({
   type: "lin",
   name: "slider three",
   minValue: 0,
   maxValue: 500,
   numberOfSteps: 500,
   defaultValue: 0,
   hidden: true
});

function ParameterChanged(id, val) {

Trace(Date.now() + ` parameter changed ${id} ${val}`);
   
   let dirty = false;
   if(id==0) {
       for(let i=1; i<=3; i++) {
           if(i == val+1) {
               if(PluginParameters[i].hidden != false) {
                   PluginParameters[i].hidden=false;
                   dirty = true;
               }
           }
           else {
               if(PluginParameters[i].hidden != true) {
                   PluginParameters[i].hidden=true;
                   dirty = true;
               }
           }
       }
       if(dirty) UpdatePluginParameters();
   }
}

 

Idle Function

 

The Idle Function is an undocumented built in callback function that Scripter calls a few times per second if it has been defined in your script. That may seem like a lot, but actually ProcessMIDI and HandleMIDI can be called a thousand times per second or more depending on the audio buffer size, so a few times per second for the Idle callback is actually very seldom in comparison.

 

In addition to that, the intention is that this function will be called by LogicPro at a time when it is not busy processing audio and midi. We don't know for sure, but most likely this is called on a different thread, instead of on the realtime thread. Any kind of GUI related code are usually always best handled on a lower priority thread and not on the realtime audio/midi thread where HandleMIDI and ProcessMIDI are processed. Another task that is very useful to handle in the Idle callback is buffered Tracing, the built in Tracing basically sucks.

 

The call to UpdatePluginParameters, could be computationally expensive as it updates the GUI controls on the screen. Putting this call into Idle will make sure it happens at a lower priority.

 

function HandleMIDI(event) {
   event.send();
}

PluginParameters = [];

PluginParameters.push({
   type: "menu",
   name: "chooser",
   valueStrings: ["one","two","three"],
   defaultValue: 0
});

PluginParameters.push({
   type: "lin",
   name: "slider one",
   minValue: 0,
   maxValue: 500,
   numberOfSteps: 500,
   defaultValue: 0,
   hidden: false
});

PluginParameters.push({
   type: "lin",
   name: "slider two",
   minValue: 0,
   maxValue: 500,
   numberOfSteps: 500,
   defaultValue: 0,
   hidden: true
});

PluginParameters.push({
   type: "lin",
   name: "slider three",
   minValue: 0,
   maxValue: 500,
   numberOfSteps: 500,
   defaultValue: 0,
   hidden: true
});

var dirty = false;

function ParameterChanged(id, val) {

Trace(Date.now() + ` parameter changed ${id} ${val}`);
   
   if(id==0) {
       for(let i=1; i<=3; i++) {
           if(i == val+1) {
               if(PluginParameters[i].hidden != false) {
                   PluginParameters[i].hidden=false;
                   dirty = true;
               }
           }
           else {
               if(PluginParameters[i].hidden != true) {
                   PluginParameters[i].hidden=true;
                   dirty = true;
               }
           }
       }
   }
}

function Idle() {
   if(dirty==true) {
       dirty=false;
       UpdatePluginParameters();
   }
}

 

In the above example, the code sets a dirty flag, and Idle() only calls UpdatePluginParameters when that dirty flag has been set, after unsetting it of course to eliminate the feedback loop.

 

The Idle approach makes it pretty convenient also to create some simple SHOW and HIDE functions which can make your script code easier to read and less error-prone. You can create some functions like this:

 

function SHOW(id) {
   if(PluginParameters[id].hidden != false) {
       PluginParameters[id].hidden = false;
       dirty = true;
   }
}

function HIDE(id) {
   if(PluginParameters[id].hidden != true) {
       PluginParameters[id].hidden = true;
       dirty = true;
   }
}

 

Once you have done that, and provided the needed dirty flag handling in Idle, then your code to show and hide the controls can be much more simple like this:

 

function ParameterChanged(id, val) {
   
   if(id==0) {
       // If the selection is not the current one
       for(let i=1; i<=3; i++) {
           if(i == val+1) {
               SHOW(i);
           }
           else {
               HIDE(i);
           }
       }
   }
}

 

Still One Problem

 

But wait, there is still one more endless loop problem caused by a bug in Scripter. The bug is that Scripter will randomly call ParameterChanged passing it the value of the default, as defined in PluginParameters; rather then the actual current value of the parameter. It does this seemingly randomly, I have not been able to find a pattern for when and why. It does seem to come up when I have more parameters, but it happens sometimes with only a few too. I think having a bigger PluginParameters array just increases the odds of it happening.

 

Why is this a problem? Well let's say you are using a menu to determine what the following group of controls will be, like the example earlier. If the default is set to zero, then when you call UpdatePluginParameters, Scripter will randomly call ParameterChanged with either the current value or the default value. When it calls the default value, that triggers another change to your UI and call to UpdatePluginParameters, which seems to then call ParameterChanged with the proper current value, but triggers yet another change to the UI and another call to UpdatePluginParameters, which then comes back with the default zero, etc, etc..and goes like that sometimes indefinitely, sometimes just a few times, sometimes more..before it finally somehow doesn't randomly call it with the default. The more parameters you have, the more chance there is of this happening.

 

If that twisted your brain all up, then just remember the bug I mentioned and you probably need this work around if you are using a menu.

 

Work Around

 

This work around will basically setup the menu GUI items always with the first item on the menu (corresponding to index 0) will never have a meaningful purpose. Always set the default to zero also. So the default will be to load the menu at position 0, and position zero will have no meaning. When I say no meaning, I mean put a label such as "Select...", and that's it. If the user selects that item, it will do nothing. If they select another value, those values will all work normally, if they go back to that first meaningless one, it will do nothing and change nothing. Its not the greatest GUI choice, but its not that terrible either...considering it works around this bug.

 

Then inside your ParameterChanged logic, make sure to check for the case of value==0, and when it does...assume it means nothing, so do nothing, including don't call UpdatePluginParameters again from there.

 

Do that and this endless loop problem will be solved.

 

Here is a small example.

 

function HandleMIDI(event) {
   event.send();
}

PluginParameters = [];

PluginParameters.push({
   type: "menu",
   name: "chooser",
   valueStrings: ["Select...","one","two","three"],
   defaultValue: 0
});

PluginParameters.push({
   type: "lin",
   name: "slider one",
   minValue: 0,
   maxValue: 500,
   numberOfSteps: 500,
   defaultValue: 0,
   hidden: false
});

PluginParameters.push({
   type: "lin",
   name: "slider two",
   minValue: 0,
   maxValue: 500,
   numberOfSteps: 500,
   defaultValue: 0,
   hidden: true
});

PluginParameters.push({
   type: "lin",
   name: "slider three",
   minValue: 0,
   maxValue: 500,
   numberOfSteps: 500,
   defaultValue: 0,
   hidden: true
});

var dirty = false;

function ParameterChanged(id, val) {

Trace(Date.now() + ` parameter changed ${id} ${val}`);
   
   if(id==0 && val != 0) {
       for(let i=1; i<=3; i++) {
           if(i == val) {
               SHOW(i);
           }
           else {
               HIDE(i);
           }
       }
   }
}

function SHOW(id) {
   if(PluginParameters[id].hidden != false) {
       PluginParameters[id].hidden=false;
       dirty = true;
   }
}
function HIDE(id) {
   if(PluginParameters[id].hidden != true) {
       PluginParameters[id].hidden=true;
       dirty = true;
   }
}

function Idle() {
   if(dirty==true) {
       dirty=false;
       UpdatePluginParameters();
   }
}

 

Try that example to see how the menu interacts. It think you will agree is slightly less than optimal, but its also not terrible and it works around the bug mentioned here.

Edited by Dewdman42
  • Like 1
Link to comment
Share on other sites

  • 1 year later...
  • 1 year later...

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...