grame-cncm / faust

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

ExpValueConverter not working well #574

Open DBraun opened 3 years ago

DBraun commented 3 years ago

This is part of my Faust code:

declare options "[midi:on]";
declare options "[nvoices:8]";
import("stdfaust.lib");
freq = hslider("v:Env1/[4]Freq1[midi:ctrl 45][scale:exp]", 400,40,10000,10.) : si.smoo;
process = .05*os.osc(freq ) <: _, _;

I used faust2juce on Windows: sh faust2juce -soundfile -midi -nvoices 8 -standalone -jucemodulesdir C:/tools/JUCE/modules synth.dsp, built and launched the exe with the debugger.

I set a breakpoint here: https://github.com/grame-cncm/faust/blob/c06e9c63a7236c3d60e1a77e226b3d91c297a08d/architecture/faust/gui/ValueConverter.h#L286

fmax holds the value 10000, and I think something is going wrong because std::exp(10000) explodes. I would hope ExpValueConverter would be more robust than that.

I have a different idea of what it means to do exponential conversion, so maybe this can help. Take the graph y=b^x and consider it for x between 0 and 1. The endpoints are (0, 1), and (1, b), so the range of y is [1, b]. Maybe b would equal the constant e, but it can be anything.

So suppose we have MIDI values from 0-127. First convert them to 0-1. This is x. Then take b^x. Then remap from domain [1, b] to the output (40 Hz, 10000 Hz).

If I were to re-code an exponential converter, it would be like this (kept slightly verbose for clarity)

class ExpValueConverter : public LinearValueConverter
{
    private:
       Interpolator m_f01_to_UI;
       Interpolator m_fUI_to_01;
       Interpolator m_fBase_to_F;
       Interpolator m_fF_to_Base;
       const double m_base;

    public:

        ExpValueConverter(double umin, double umax, double fmin, double fmax, double base=2.71828182845904523536) :
            LinearValueConverter(umin, umax, fmin, fmax),
            m_fUI_to_01(umin, umax, 0., 1.),
            m_f01_to_UI(0., 1., umin, umax),
            m_fBase_to_F(1., base, fmin, fmax),
            m_fF_to_Base(fmin, fmax, 1., base),
            m_base(base)
        {}

        virtual double ui2faust(double x) {
           float tmp1 = m_fUI_to_01(x);
           float tmp2 = std::pow(m_base, tmp1);
           float tmp3 = m_fBase_to_F(tmp2);
           return tmp3;
        }
        virtual double faust2ui(double x) {
           float tmp1 = m_fF_to_Base(x);
           float tmp2 = std::log(tmp1) / std::log(m_base);
           float tmp3 = m_f01_to_UI(tmp2);
           return tmp3;
        }

};

When I use this code and use MIDI hardware knobs, it sounds correct to me. The exponential is actually meaningful, and if I change the base value even larger, I'm able to get a lot of fine control over the "low" part of the output range. However, the GUI doesn't reflect it. If I set the physical knobs to halfway position, the GUI says that the Hz is about 4900, regardless of what base I compiled with. Changing the base however does change the sound I hear at this physical knob position. It "sounds" correct, but the GUI is wrong.

Another thing I noticed with the current code is that if I set a breakpoint on ExpValueConverter's ui2faust method and wiggle a knob, first I get an update where x is the MIDI value between 0-127. Then I immediately get another call to ui2faust in which x is the value that is going to be applied to the DSP code and UI. For example, x would be 5000 (Hz), and I'd see 5000 Hz on the slider and hear the effect of it. I think that's this second call to ui2faust is weird.

dariosanfilippo commented 3 years ago

Hi, David.

This has been a known issue for some time now; if I remember correctly, the log mapping doesn't work either. And linear sliders with unit-increment still seem to output fractional values.

I've recently written this set of nonlinear mapping functions with adjustable tension parameter; they, too, take an input between 0 and 1 and can be mapped to arbitrary output ranges: https://github.com/dariosanfilippo/nonlinear_mapping, https://www.desmos.com/calculator/6hshyfcyzh.

It would be great if we could put things together and perhaps have more nonlinear functions available.

Ideally, we could have four keywords, each for the four curve types, where a specified tension parameter (T) can be set to determine increasing (positive T) slopes or decreasing ones (negative T).

Dario

DBraun commented 3 years ago

Thanks for this info and your repo. I'll leave the issue open until it's fixed. The online IDE works fine. It was hard to trace down where the bug is in the C++ side.

josmithiii commented 3 years ago

Since std::exp(10000) is "Inf", even in quad precision, "explodes" sounds correct. - Julius

On Fri, Apr 16, 2021 at 5:01 PM David Braun @.***> wrote:

Thanks for this info and your repo. I'll leave the issue open until it's fixed. The online IDE works fine. It was hard to trace down where the bug is in the C++ side.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/grame-cncm/faust/issues/574#issuecomment-821729075, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAQZKFNXVN4YKMC6RHN7DVTTJDFXFANCNFSM43CH6LQA .

-- Julius O. Smith III @.***> Professor of Music and, by courtesy, Electrical Engineering CCRMA, Stanford University http://ccrma.stanford.edu/~jos/

sletz commented 3 years ago

he GUI says that the Hz is about 4900, regardless of what base I compiled with. Changing the base however does >change the sound I hear at this physical knob position. It "sounds" correct, but the GUI is wrong.

Another thing I noticed with the current code is that if I set a breakpoint on ExpValueConverter's ui2faust method and >wiggle a knob, first I get an update where x is the MIDI value between 0-127. Then I immediately get another call to >ui2faust in which x is the value that is going to be applied to the DSP code and UI. For example, x would be 5000 (Hz), >and I'd see 5000 Hz on the slider and hear the effect of it. I think that's this second call to ui2faust is weird.

On which architecture do you see that?

sletz commented 3 years ago

From @orlarey (internal exchange in french translated):


It seems to me that the problem is not there. Indeed exp(10000) explodes, but that's normal, it's just math! To control pitches, you should use a logarithmic scale (like the keys of a piano) and not an exponential one !

The following code in FaustLive works very well and has the expected behavior : the middle of the slider corresponds to 220 Hz. I guess it should be the same for Juce ?

import("stdfaust.lib");

vol             = hslider("volume [unit:dB]", 0, -96, 0, 0.1) : ba.db2linear : si.smoo ;
freq            = hslider("freq [scale:log][unit:Hz]", 220, 110, 440, 1);

process         = vgroup("Oscillator", os.osc(freq) * vol);

But if you run this example in the IDE, it doesn't work. The IDE doesn't do the right conversion...


dariosanfilippo commented 3 years ago

Hi, @orlarey.

The behaviour that you describe for the frequency slider is right. I'm guessing that Faust implements something like the following; perhaps we could even extend it and add a base parameter:

import("stdfaust.lib");
MIN1 = 4;
MAX1 = 64;
B1 = 2;
MIN2 = 110;
MAX2 = 440;
B2 = 10;
slider = hslider("slider", 0, 0, 1, .001);
logScale(base, min_, max_, x) = x * range + logMin : pow(base, _)
    with {
        logMin = log(min_)/log(base);
        logMax = log(max_)/log(base);
        range = logMax - logMin;
    };
process = logScale(B1, MIN1, MAX1, slider) , logScale(B2, MIN2, MAX2, slider);

But could you also check these sliders below?

import("stdfaust.lib");
lin0 = hslider("[0]linear 0-1", .5, 0, 1, .000001);
lin1 = hslider("[3]linear 1-1000", 500, 1, 1000, .000001);
exp0 = hslider("[1]exponential 0-1[scale:exp]", .5, 0, 1, .000001);
exp1 = hslider("[4]exponential 1-1000[scale:exp]", 500, 1, 1000, .000001);
log0 = hslider("[2]logarithmic 0-1[scale:log]", .5, 0, 1, .000001);
log1 = hslider("[5]logarithmic 1-1000[scale:log]", 500, 1, 1000, .000001);
process = lin0 , lin1 , exp0 , exp1 , log0 , log1;

For example, exp1 appears to be broken, while log0 is essentially always 0 except for the last few pixels of the slider run.

It could be due to how my brain is wired and it might as well be wrong, but when I think of piloting a parameter through a logarithmic slider, I think of the slider as a linear input to a log-like function. I think the same way for an exponential slider. That is, I expect more resolution towards the upper side of the slider for a log-like behaviour, and vice versa for the exponential one. The behaviour that I describe appears to be inverted in the other sliders of the last code example.

Having maybe one or two more sliders with one of the functions here would be great for me as I currently cannot use nonlinear mapping properly for my systems. I'm not sure if others would also benefit from them.

Ciao, Dario

magnetophon commented 5 months ago

In case new slider types are still on the table, I think nih-plug handles this very nicely: https://github.com/robbert-vdh/nih-plug/blob/master/src/params/range.rs No need to understand rust, everything is properly commented.

DBraun commented 5 months ago

@magnetophon I also have a proposal here that creates a new slider primitive whose scale is more configurable upfront.

magnetophon commented 5 months ago

@DBraun Looks great!

dariosanfilippo commented 5 months ago

Why not have a slider that allows for a tension parameter too? The octave-scale frequency mapping shown in the previous examples would just be a special case of a more general exponential mapping function: https://www.desmos.com/calculator/elbwhqa1po.

josmithiii commented 5 months ago

Looks great to me as well! I'm not sure I am completely up to speed, but I have the following comments so far:

  1. It would be great if we could use existing widget names, with a new optional last argument. For example, hslider(str,cur,min,max,step) could be extended to hslider(str,cur,min,max,step,map) and similarly for vslider and nentry. If omitted, map(x) would default to a linear map from [0,1] to [min,max]. When map is present, the scale metadata key and value would be ignored.
  2. Don't we already have "widget modulation"?: https://faustdoc.grame.fr/manual/syntax/#widget-modulation Is this not sufficient?
  3. Clipping to min and max is already enforced, so the name clipslider could be confusing.
  4. I assume min can be negative with no difficulties.
sletz commented 5 months ago

@orlarey and @DBraun had a recent mail exchange on this subject or something near, trying to write the wanted behaviour using "widget modulation" and lowest/highest primitives (still not documented...), that deliver the lowest (respectively highest) value of the signal range (internally computed by the so-called interval library used in the compiler).

So basically the point is to see is the desired behaviour can be written with the current Faust and compiler. @DBraun can possible explain the situation better than me.

DBraun commented 5 months ago

These are my goals:

  1. Need to have parameters with user-defined scales.
  2. The output of the scale must show up in the UI.
  3. These parameters need to work well with CV-style modular synthesis where inputs are normalized.
  4. The modulation should also be visualized in the Faust IDE, and it should be easy to access in other architectures.
  5. The code style should make sense for reusability via a package manager, since that's a GSoC proposal.

Maybe we should start the discussion with what's ideal for a package manager. Should there be both non-GUI and GUI versions of the same function? For example, look at https://faustlibraries.grame.fr/libs/reverbs/#restereo_freeverb.

Usage
_,_ : stereo_freeverb(fb1, fb2, damp, spread) : _,_
Where:

fb1: coefficient of the lowpass comb filters (0-1)
fb2: coefficient of the allpass comb filters (0-1)
damp: damping of the lowpass comb filter (0-1)
spread: spatial spread in number of samples (for stereo)

This happens to be an example in which the parameters are (0-1), which is friendly for modular synthesis, but keep in mind that's not always the case.

I think that for the sake of a package distribution, many functions should also be distributed with normalized "safe-input" alternatives with GUIs. For example, there could also be this stereo reverb function:

import("stdfaust.lib");
stereo_freeverb_ui(_fb1, _fb2, _damp, _spread, _wetAmount) = hgroup("Reverb", ef.dryWetMixer(wetAmount, reverb))
with {
    fb1 = _fb1 + vslider("[0] FB 1 [style:knob]", .1, 0., 1., .01) : aa.clip(0, 1);
    fb2 = _fb2 + vslider("[1] FB 2 [style:knob]", .1, 0., 1., .01) : aa.clip(0, 1);
    damp = _damp + vslider("[2] Damp [style:knob]", .1, 0., 1., .01) : aa.clip(0, 1);
    spread = _spread + vslider("[3] Width [style:knob]", 1, 0., 1., .01) : aa.clip(0, 1);
    wetAmount = _wetAmount + hslider("[4] Mix [style:knob]", 1, 0., 1., .01) : aa.clip(0, 1);
    reverb = re.stereo_freeverb(fb1, fb2, damp, spread);
};
process = stereo_freeverb_ui;

By default, this function exposes every parameter to modular synthesis, rather than requiring it via widget modulation.

This is an example of reusing that code:

import("stdfaust.lib");
reverbs= library("downloaded_reverb.lib");
wetModulation = os.osc(.5)*.1;
dampModulation = os.osc(.4)*.2;

process = reverbs.stereo_freeverb_ui(0, 0, dampModulation, 0, wetModulation);

Personally, I feel that being aware of the parameter indices and passing zero to most of them is less burdensome than widget modulation. Also, because 0 is passed as the modulation for FB 1. FB 2, Damp, and Spread, maybe the aa.clip function can be optimized away during compilation.

I think the weakness of this code style is how repetitive the internals of stereo_freeverb_ui are. I mentioned this to Yann and he proposed using the lowest/highest operators.

This was his snippet, which is general, not about my reverb example.

import("stdfaust.lib");

// the circuit we want to modulate
generator = hslider("gain", 0, 0, 1, 0.01) : hbargraph("g", -0.1, 1.1) : *(os.osc(440));

// the modulation circuit, s: slider and m: modulation signal -1..1
mc(s,m) = s + m*mi : max(lo) : min(hi) with {lo=lowest(s); hi=highest(s); mi=(lo+hi)/2;};

process = os.osc(1) * hslider("modulation",0.5,0,1,0.01) : ["gain":mc -> generator];

Note that : max(lo) : min(hi) : is doing the clipping. The trick is that lowest and highest are able to look at s, which is actually an hslider with a known output range. Seeing Yann's example, I wrote the following:

import("stdfaust.lib");

// the modulation circuit, s: slider and m: modulation signal -1..1
mc(s,m) = s + m*depth : max(lo) : min(hi) : hbargraph("graph 2", lo, hi) with {lo=lowest(s); hi=highest(s); depth=hi-lo;};

myfilter = fi.lowpass(1, cutoff)
with {
    FREQ_MIN = 8;
    FREQ_MAX = 20050;
    cutoff = hslider("cutoff", FREQ_MIN, FREQ_MIN, FREQ_MAX, .01) : hbargraph("graph 1", FREQ_MIN, FREQ_MAX);
};

modulation = button("gate") : si.smoo : _* hslider("modulation",0.5,0,1,0.01);

process = modulation, _ : ["cutoff":mc -> myfilter] <: _, _;

We have a frequency cutoff parameter whose range is FREQ_MIN to FREQ_MAX, and we are not yet addressing the non-linear scale requirement. We're just showing how Yann's idea works when the hslider isn't already programmed to be between 0 and 1 like his gain slider was. Note that modulation, which is intended to be between [-1,1], comes into the mc(s,m) function as m, and it gets multipled by depth, which is a fairly large value FREQ_MAX-FREQ_MIN. Then it gets added to s, the original slider, and then clipped.

This approach is ok, but it doesn't actually create a non-linear scale. If you were to use the GUI, you'd get a linear response between 8 and 20050. If you were to use the modulation, you'd also get a linear response.

So let's trying coding it again with the goal of having a custom scale.

import("stdfaust.lib");

FREQ_MIN = 8;
FREQ_MAX = 20050;

// scale takes something in [0,1] and remaps to whatever you want.
// Here we have scale(0)==FREQ_MIN and scale(1)==FREQ_MAX
scale(x) = it.interpolate_linear(x, log(FREQ_MIN), log(FREQ_MAX)) : exp;

// the modulation circuit, s: slider and m: modulation signal -1..1
mc(s,m) = s + m*depth : max(lo) : min(hi) : hbargraph("graph 2", lo, hi) with {lo=lowest(s); hi=highest(s); depth=hi-lo;};

myfilter = fi.lowpass(1, cutoff)
with {
    cutoff = hslider("cutoff", 0, 0, 1, .01) : scale : hbargraph("graph 1", FREQ_MIN, FREQ_MAX);
};

modulation = button("gate") : si.smoo : _* hslider("modulation",0.5,0,1,0.01);

process = modulation, _ : ["cutoff":mc -> myfilter] <: _, _;

Here myfilter is something you'd want to distribute to others. This succeeds in having a non-linear scale, which the bargraph shows. However, I'd really like for the value to appear in the slider, without requiring a bargraph. Furthermore, it would be useful to see the visualization in the slider. Right now, an hslider set to knob style has a yellow fill. What if the real-time modulation could be shown in an additional color like red?

Let's go back to the GUI-featured reverb and update it:

import("stdfaust.lib");
fx_reverb_ui = hgroup("Reverb", ef.dryWetMixer(wetAmount, reverb))
with {
    fb1 = vslider("[0] FB 1 [style:knob]", .1, 0., 1., .01);
    fb2 = vslider("[1] FB 2 [style:knob]", .1, 0., 1., .01);
    damp = vslider("[2] Damp [style:knob]", .1, 0., 1., .01);
    spread = vslider("[3] Width [style:knob]", 1, 0., 1., .01);
    wetAmount = hslider("[4] Mix [style:knob]", 1, 0., 1., .01);
    reverb = re.stereo_freeverb(fb1, fb2, damp, spread);
};

// the modulation circuit, s: slider and m: modulation signal -1..1
mc(s,m) = s + m*mi : max(lo) : min(hi) with {lo=lowest(s); hi=highest(s); mi=(lo+hi)/2;};

wetModulation = os.osc(.5)*.1;
dampModulation = os.osc(.4)*.2;

process = wetModulation, dampModulation, si.bus(2) : ["Mix":mc, "Damp":mc -> fx_reverb_ui];

Like I said earlier, the strength is that the internals of fx_reverb_ui don't repeat the boilerplate involving _inputPar + hslider(...) : aa.clip. The weakness is that you still don't get a non-linear scale, and it would start to look bulky with 3 modulated parameters compared to just

process = reverbs.stereo_freeverb_ui(0, fb2modulation, dampModulation, 0, wetModulation);
DBraun commented 5 months ago

Refactoring the last fx_reverb_ui a little bit:

import("stdfaust.lib");
fx_reverb_ui = hgroup("Reverb", ef.dryWetMixer(wetAmount, reverb))
with {
    fb1 = vslider("[0] FB 1 [style:knob]", .1, 0., 1., .01);
    fb2 = vslider("[1] FB 2 [style:knob]", .1, 0., 1., .01);
    damp = vslider("[2] Damp [style:knob]", .1, 0., 1., .01);
    spread = vslider("[3] Width [style:knob]", 1, 0., 1., .01);
    wetAmount = hslider("[4] Mix [style:knob]", 1, 0., 1., .01);
    reverb = re.stereo_freeverb(fb1, fb2, damp, spread);
};

// the modulation circuit, s: slider and m: modulation signal -1..1
mc(m,s) = s + m*mi : max(lo) : min(hi) with {lo=lowest(s); hi=highest(s); mi=(lo+hi)/2;};

wetModulation = os.osc(.5)*.1;
dampModulation = os.osc(.4)*.2;

process = ["Mix":mc(wetModulation), "Damp":mc(dampModulation) -> fx_reverb_ui];

Note that mc(s,m) is now mc(m,s). Overall it's better, but