Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better curve algorithm wanted. #6

Open
magnetophon opened this issue Aug 3, 2020 · 15 comments
Open

Better curve algorithm wanted. #6

magnetophon opened this issue Aug 3, 2020 · 15 comments
Labels
enhancement New feature or request

Comments

@magnetophon
Copy link
Owner

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.

@ryukau
Copy link

ryukau commented Aug 3, 2020

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

@magnetophon
Copy link
Owner Author

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.

@ryukau
Copy link

ryukau commented Aug 4, 2020

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

@magnetophon
Copy link
Owner Author

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!

@ryukau
Copy link

ryukau commented Aug 4, 2020

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.

@magnetophon
Copy link
Owner Author

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
Copy link
Owner Author

magnetophon commented Aug 4, 2020

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

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

@ryukau
Copy link

ryukau commented Aug 4, 2020

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

@magnetophon
Copy link
Owner Author

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

@ryukau
Copy link

ryukau commented Aug 4, 2020

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.

@magnetophon
Copy link
Owner Author

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 added a commit that referenced this issue Aug 5, 2020
@ryukau
Copy link

ryukau commented Aug 5, 2020

Surge's bend3 looks good to me.

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

@magnetophon
Copy link
Owner Author

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

@ryukau
Copy link

ryukau commented Aug 18, 2020

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

@magnetophon
Copy link
Owner Author

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.

@ryukau ryukau added the enhancement New feature or request label Jul 30, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants