Open DBraun opened 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
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.
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/
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?
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...
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
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.
@magnetophon I also have a proposal here that creates a new slider primitive whose scale is more configurable upfront.
@DBraun Looks great!
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.
Looks great to me as well! I'm not sure I am completely up to speed, but I have the following comments so far:
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.clipslider
could be confusing.min
can be negative with no difficulties.@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.
These are my goals:
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);
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
mc
utility function is still clunky, given that it would be my most common usage of widget modulation.
This is part of my Faust code:
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 becausestd::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 takeb^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)
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 wherex
is the MIDI value between 0-127. Then I immediately get another call toui2faust
in whichx
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 toui2faust
is weird.