DigiDrie
DigiDrie copied to clipboard
Better curve algorithm wanted.
Currently tab macros -> curve
is implemented as:
mapping(x) = pow(x,curveIndex);
curveIndex = select2(curveSlider>0
, 1/(abs(curveSlider)+1)
, curveSlider+1
);
This gives an asymmetric behavior, both between positive and negative values of curveSlider
and between low and high values of x
.
I'm looking for a better algorithm.
It seems like used in this line.
This function is a bit tricky, because both inputs of pow
are variable. I don't have skill to approximate 2 variable function, so I'm not able to preserve the character of the function.
If it's OK to change function, combination of rate limiter and smoother (some 1-pole lowpass) might be an alternative.
// a, b, c, d are tuning constants.
func(x, target) =
x : rate_limiter(up * a * curve, down * b * curve) : smooth(y) with {
y = if(x - target < 0, down * c * curve, up * d * curve);
};
Thanks, but I don't want a time-variable behavior, like a rate limiter. I also don't want to keep the curve I have: it is not symmetric, as explained above.
I want something that looks more like this: https://d29rinwu2hi5i3.cloudfront.net/article_media/ee65c2fe-59f2-47c7-ae99-ea4c400033cf/w768/midi_fig_1.jpg
The formula should have the following features:
- when curve is 0, the function should do nothing
- when curve is positive or negative, the whole graph should be round, so no semi-linear parts like in the current implementation.
- the shape should change about the same amount for curve = +x as it does for curve = -x.
Your proposal does none of the above. The current implementation only does the first point.
Closest I can think of is the top left part of superellipse. However, this doesn't satisfy the third point. Also heavy to compute.
That does come close. But yeah, we do want something light.
Otoh: I'm only calculating this 4 times, so how bad can it be?
I hope I'll find the perfect algo soon!
I put one more. The algorithm in bezier-easing library might be used.
This satisfies 1st and 3rd point, and probably 2nd point with some tuning. The algorithm uses newton-raphson and switches to bisection where slope is close to flat. So it's heavy.
Otoh: I'm only calculating this 4 times, so how bad can it be?
I once profiled SyncSawSynth with valgrind, and iirc, found that 20~30% of CPU time is spent on 2 sin and 1 cos, which are called once per frame.
However, the rule of thumb is better not assume anything until take some benchmarks. I'll open a issue around benchmark soon.
I looked into cubic beziers a while ago, for my new limiter.
The problem is that they give you x and y as a function of t, so you need to solve for t, and you end up with this monster formula!
Or are you suggesting you do it in C++?
Another way would be a 3d lookup-table in faust.
There's an article of numerical algorithm by the author of bezier-easing library. Implementation section has a code.
If I understand correctly, he's solving the same problem:
But it’s not enough. We need to project a point to the Bezier curve, in other words, we need to get the Y of a given X in the bezier curve, and we can’t just get it with the percent parameter of the Bezier computation. We need an interpolation.
He definitely lost me in the part that follows though.
I'd love to implement this in faust, also for my limiter, and this code looks a lot simpler than my monster formula, so I hope it will run quicker too. I have no idea where to start though...
The core algorithm is on GetTForX
. GetSlope
is the derivative of bezier curve. The rest is calculating the equation of bezier curve.
If I understanc correctly, following javascript code:
function KeySpline (mX1, mY1, mX2, mY2) {
this.get = function(aX) {
if (mX1 == mY1 && mX2 == mY2) return aX; // linear
return CalcBezier(GetTForX(aX), mY1, mY2);
}
function A(aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; }
function B(aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; }
function C(aA1) { return 3.0 * aA1; }
// ...
}
can be translated to Faust as:
KeySpline(aX, mX1, mY1, mX2, mY2) = out with {
out = if(mX1 == mY1 && mX2 == mY2,
aX,
CalcBezier(GetTForX(aX), mY1, mY2));
A(aA1, aA2) = 1.0 - 3.0 * aA2 + 3.0 * aA1;
B(aA1, aA2) = 3.0 * aA2 - 6.0 * aA1;
C(aA1) = 3.0 * aA1;
// ...
};
I'm not sure how to translate for loop in GetTForX
.
// Maybe use case of `seq` in Faust?
for (var i = 0; i < 4; ++i) {
var currentSlope = GetSlope(aGuessT, mX1, mX2);
if (currentSlope == 0.0) return aGuessT;
var currentX = CalcBezier(aGuessT, mX1, mX2) - aX;
aGuessT -= currentX / currentSlope;
}
The library implementation is a bit more complicated (link). If you consider using this, I'd recommend to take a look at it.
I just remembered surge has a similar feature, so I looked up how they did it
The curve looks pretty good in desmos and the math is extremely simple. Blue: surge, range -1..1 Purple: mine, range 0..1
Surge's bend3 looks good to me.
Just to point out, it looks like positive/negative rotates the curve 180°.
If we can optimize the synth a lot, I hope to add curve per parameter. What do you think?
Do you mean literally all parameters? Or just parameters under modulation tab?
I was hoping for all, but I'm afraid we can't bring down the DSP-usage enough.
Only the mod tab could be a nice compromise.