grame-cncm / faust

Functional programming language for signal processing and sound synthesis
http://faust.grame.fr
Other
2.57k stars 322 forks source link

Surprising effects of vgroup/hgroup on how controls and parameters work #725

Closed FLamparski closed 2 years ago

FLamparski commented 2 years ago

I have this DSP (stripped down version of an instrument I'm working on) - this example extracts two partials from white noise. Note that all code examples were tested in the online IDE on the day of the ticket being opened.

declare options "[midi:on][nvoices:8]";

import("stdfaust.lib");

// MIDI hookup: freq is based on note played, gain is velocity, gate is whether the note is on
freq = vslider("freq",200,50,1000,0.01);
gain = vslider("gain",0.5,0,1,0.01);
gate = button("gate");

q = vslider("q", 20, 2, 40, 1);
qcomp = 0.5 - 0.025 * q; // Decrease levels at high Q settings

band(id, freq, q, basegain) = _ : fi.resonbp(freq, q, 0.5) * basegain * gate : _;

tonalizer(freq, q) = _ <:
    hgroup("[0]F", band(0, freq, q, vslider("partial gain 0", 0.5, 0.0, 1.0, 0.01))),
    hgroup("[1]2", band(1, freq * 2, q, vslider("partial gain 1", 1, 0.0, 1.0, 0.01)))
    :> /(4);

process = no.noise : tonalizer(freq, q) * gain <: _, _;

It produces the following DSP UI:

image

I can also confirm by ear that 'freq', 'gate', and 'q' controls are independent for each partial within the DSP UI. Note that 'freq' and 'gate' are only defined in the global scope, and 'q' is passed down as a parameter to each function called from within 'process'. The example is using hgroup for screenshot clarity but vgroup also has this behaviour. I'm also now very confused about how qcomp would be calculated with the grouping bug in effect - would it effectively be undefined behaviour, or would it be computed per partial?

When I remove the hgroup from each band within the tonalizer function, like below, the UI is now more consistent with how I would expect it to work:

tonalizer(freq, q) = _ <:
    band(0, freq, q, vslider("partial gain 0", 0.5, 0.0, 1.0, 0.01)),
    band(1, freq * 2, q, vslider("partial gain 1", 1, 0.0, 1.0, 0.01))
    :> /(4);
image

This is unexpected because the vgroup and hgroup documentation states that "[it's] is not a signal processor per se and is just a way to label/delimitate part of a Faust code."

The issue is also not limited to the Faust IDE UI, as trying to embed this Faust code in an iPlug2 project results in a mismatch between the number of actual DSP parameters (zones) and the parameters exposed to the C++ code (params) as the duplicate params are named the same. The workaround for that will be to remove all vgroups/hgroups from the version of the DSP code that gets put into the plugin - after all I will be providing my own UI so it's not critical that Faust does it for me - but it would be nice to not have to do that, and for use cases where you do want the generated UI, this could be a blocker for some.

The silver lining of this bug is that I will definitely be trying per-partial Q controls in my plugin as it can sound nicer, but I was thinking about doing that at some point anyway...

sletz commented 2 years ago

Not sure I fully understand the problem but could it possibly be solved using Variable Parts of a Label ?

Otherwise you can all connect to Faust Slack or Discord for easier interactions: https://faust.grame.fr/community/help/

FLamparski commented 2 years ago

My confusion is that I defined a variable in the global scope but when using groups, it seems to be pushed down to the local scope within a group, which is not what I would expect from just reading the code. Is this behaviour intentional? If so, is it documented properly?

sletz commented 2 years ago

Which variable? Have a look at with Expression

FLamparski commented 2 years ago

If you look at the screenshot you'll notice that in the first example, each partial ends up with its own sliders for 'freq' and 'q' and its own button for 'gate' despite all 3 of these variables being declared globally. When grouping is removed from the tonalizer function, there are only 3 controls for freq, gain, and q.

I am not sure how a with expression will help with that.

sletz commented 2 years ago

UI items are shared if they have the same path in the UI hierarchy. Try that:

declare options "[midi:on][nvoices:8]";

import("stdfaust.lib");

// MIDI hookup: freq is based on note played, gain is velocity, gate is whether the note is on
freq = vslider("freq",200,50,1000,0.01);
gain = vslider("gain",0.5,0,1,0.01);
gate = button("gate");

q = vslider("q", 20, 2, 40, 1);
qcomp = 0.5 - 0.025 * q; // Decrease levels at high Q settings

band(id, freq, q, basegain) = _ : fi.resonbp(freq, q, 0.5) * basegain * gate : _;

tonalizer(freq, q) = _ <:
    hgroup("foo", band(0, freq, q, vslider("partial gain 0", 0.5, 0.0, 1.0, 0.01))),
    hgroup("foo", band(1, freq * 2, q, vslider("partial gain 1", 1, 0.0, 1.0, 0.01)))
    :> /(4);

process = no.noise : tonalizer(freq, q) * gain <: _,_;
FLamparski commented 2 years ago

Bringing the bands under the same group also works but that's the same as not having the groups at all.

After some fiddling with the groupings, I get a UI closer to my design goals but that's not quite there (and the parameter count agrees):

image

What I'm actually looking for (implementation in iPlug2 with a version of the DSP with no groups whatsoever as I'm defining the UI in C++):

image

This is also what I'd expect from just reading the code since I am passing q from process to tonalizer which passes it to each invocation of band. It makes me wonder whether I can in fact think of eg. q as a global variable or whether it's more appropriate to think of it as a function that returns the current value of the slider - as in, whether its value is evaluated within process or whether it's evaluated within the implementation of fi.resonbp. Coming from a background in more imperative languages I'm leaning towards the "q is a variable" interpretation. Still, it is kinda surprising to me that a feature documented as merely organising the UI would actually create new lexical scopes that would lead to this sort of thing. If this is the intended behaviour of groups, I think it should be more obviously documented as such.

sletz commented 2 years ago

Can you paste this latest version of the DSP?

sletz commented 2 years ago

Possibly something like:

tonalizer1(freq, q) = _ <: hgroup("Bands", par(i, 4, band(0, freq*(i+1), q, vslider("partial gain %i", 0.5, 0.0, 1.0, 0.01)))) :> /(4);

process = no.noise : hgroup("Master", tonalizer1(freq, q) * gain ) <: _,_;
FLamparski commented 2 years ago

Here's the reduced version of the example above (keeping it simple with just 2 bands and no envelopes):

declare options "[midi:on][nvoices:8]";

import("stdfaust.lib");

// MIDI hookup: freq is based on note played, gain is velocity, gate is whether the note is on
freq = hslider("freq[hidden:1]",200,50,1000,0.01);
gain = hslider("gain[hidden:1]",0.5,0,1,0.01);
gate = button("gate[hidden:1]");

q = hslider("q", 20, 2, 40, 1);
qcomp = 0.5 - 0.025 * q; // Decrease levels at high Q settings

band(id, freq, q, basegain) = _ : fi.resonbp(freq, q, 1) * basegain * gate * qcomp : _;

tonalizer(freq, q) = _ <:
    vgroup("[1]F", band(1, freq, q, vslider("[0]partial gain", 1, 0.0, 1.0, 0.01))),
    vgroup("[2]2", band(2, freq * 2, q, vslider("[0]partial gain", 0.25, 0.0, 1.0, 0.01)))
    :> /(1);

process = no.noise : hgroup("", tonalizer(freq, q)) * gain <: _, _;

This still shows duplicated q and freq controls for each of the bands, whereas I would expect them to be global regardless of having the groups or not - since the groups are in fact useful for arranging the UI to more closely match what I want.

sletz commented 2 years ago

Is that better:


declare options "[midi:on][nvoices:8]";

import("stdfaust.lib");

// MIDI hookup: freq is based on note played, gain is velocity, gate is whether the note is on
freq = vslider("freq[hidden:1]",200,50,1000,0.01);
gain = vslider("gain[hidden:1]",0.5,0,1,0.01);
gate = button("gate[hidden:1]");

q = vslider("q", 20, 2, 40, 1);
qcomp = 0.5 - 0.025 * q; // Decrease levels at high Q settings

band(id, freq, q, basegain) = _ : fi.resonbp(freq, q, 1) * basegain * gate * qcomp : _;

tonalizer(freq, q) = _ <: hgroup("Bands", par(i, 4, band(0, freq*(i+1), q, vslider("Partial gain %i", 1/(i+1), 0.0, 1.0, 0.01)))) :> /(4);

process = no.noise : hgroup("Master", tonalizer(freq, q) * gain) <: _,_;
sletz commented 2 years ago

This should help: https://faustdoc.grame.fr/manual/faq/#surprising-effects-of-vgrouphgroup-on-how-controls-and-parameters-work

sletz commented 2 years ago

@FLamparski can we close this one?

FLamparski commented 2 years ago

@sletz Yep, this looks fine to me. Thank you!