DigiDrie icon indicating copy to clipboard operation
DigiDrie copied to clipboard

Better curve algorithm wanted.

Open magnetophon opened this issue 4 years ago • 15 comments

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.

magnetophon avatar Aug 03 '20 14:08 magnetophon

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);
  };

ryukau avatar Aug 03 '20 22:08 ryukau

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.

magnetophon avatar Aug 03 '20 23:08 magnetophon

Closest I can think of is the top left part of superellipse. However, this doesn't satisfy the third point. Also heavy to compute.

ryukau avatar Aug 04 '20 00:08 ryukau

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!

magnetophon avatar Aug 04 '20 07:08 magnetophon

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.

ryukau avatar Aug 04 '20 08:08 ryukau

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!

magnetophon avatar Aug 04 '20 08:08 magnetophon

Or are you suggesting you do it in C++?

Another way would be a 3d lookup-table in faust.

magnetophon avatar Aug 04 '20 08:08 magnetophon

There's an article of numerical algorithm by the author of bezier-easing library. Implementation section has a code.

ryukau avatar Aug 04 '20 08:08 ryukau

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...

magnetophon avatar Aug 04 '20 08:08 magnetophon

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.

ryukau avatar Aug 04 '20 10:08 ryukau

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

magnetophon avatar Aug 05 '20 12:08 magnetophon

Surge's bend3 looks good to me.

Just to point out, it looks like positive/negative rotates the curve 180°.

ryukau avatar Aug 05 '20 23:08 ryukau

If we can optimize the synth a lot, I hope to add curve per parameter. What do you think?

magnetophon avatar Aug 18 '20 12:08 magnetophon

Do you mean literally all parameters? Or just parameters under modulation tab?

ryukau avatar Aug 18 '20 12:08 ryukau

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.

magnetophon avatar Aug 18 '20 12:08 magnetophon