RobinSchmidt / RS-MET

Codebase for RS-MET products (Robin Schmidt's Music Engineering Tools)
Other
56 stars 6 forks source link

I need the opposite of exponential slider slope & ADSR discussion #80

Open elanhickler opened 7 years ago

elanhickler commented 7 years ago

Could you implement Parameter::scalings::LOGARITHMIC or whatever is the opposite of exp?

RobinSchmidt commented 7 years ago

depends on how you define opposite. if it means "inverse function", the yes, logarithm. but it could also mean just taking the negative or 1-exp(a*x) - which would look like a RC loading curve. for what kind of parameter is this?

elanhickler commented 7 years ago

I needed it because my parameter needs reversing: 1-v, so i need the high end of the slider to to have very small incrementing values rather than the low end of the slider.

But I think I may not need this anymone, I am so frustrated with my ADSR, I want to give up. Gonna look at your analog (overly compelx) adsr.

Or maybe I just need to try a new approach.

elanhickler commented 7 years ago

I'm going to do the one pole trigger signal approach.

Two pole approach!

filerCutoff = onepoleA.getSample(1/newTimingValue); onePoleB.setCutoff(filterCutoff); output_value = onepoleB.getSample(valueToMoveTo);

RobinSchmidt commented 7 years ago

I am so frustrated with my ADSR, I want to give up

why? what's wrong with it? that you can't get the timing decoupled from the shape?

Gonna look at your analog (overly compelx) adsr.

i think, you can just ignore the features you don't need. the default settings should behave neutral, i.e. if you don't call any of the setPeakLevelByVel, etc. stuff, the deafult settings make it behave as if these parameters were not there in the first place.

RobinSchmidt commented 7 years ago

my parameter needs reversing: 1-v

i think, that would be 1-exp(-decay*t) then, i.e. one minus an exponential decay function - which gives an exponential saturation curve - like an RC loading curve

RobinSchmidt commented 7 years ago
filerCutoff = onepoleA.getSample(1/newTimingValue);
onePoleB.setCutoff(filterCutoff);
output_value = onepoleB.getSample(valueToMoveTo);

that looks more like smoothing over time to me rather than an instantaneous mapping function

elanhickler commented 7 years ago

Right now I am using rosic one pole and setting cutoff for each state to get the right timing. I do "if target value was reached, go to next stage" so of course timing is all off.

Then i did "if time passed, go to next stage" except for release because I don't want to cut off release tails, i want to let the filter decay.

Now the problem is... lets say your attack was slow and only for a split second, so your actual value is something low like .1. It takes FOREVER for the filter get to near 0, because actual value and target value different is so little. Your rosic::OnePole must be really bad for timing.

What should I do?

I am so frustrated with my ADSR, I want to give up

why? what's wrong with it? that you can't get the timing decoupled from the shape?

exactly. If I try to do anything more complex than a basic linear ADSR, all my efforts go to s***

elanhickler commented 7 years ago

Hmm, maybe what I really need is a curve function, so I never worry about timing.

elanhickler commented 7 years ago

Ok, I just improved my linear ADSR so it does everything perfectly. Timed-based looped, value-reaching-based loop (I like this untimed behavior for when doing modulation / sound design, but perfectly timed behavior is good for music).

elanhickler commented 7 years ago

I guess maybe I just want a modulatable version of your breakpoint ADSR, using "analog" shape.... does the shape only update on attack/reset? How modulatable is it?

elanhickler commented 7 years ago

ok so it's not modulatable at all!

SmoothedValue:
-see class juce::LinearSmoothedValue
-generalize to an update equation: x[n+1] = a*x[n] + b
-allows for linear and exponential smoothing and anything between
-general term is: 
 x[n] = a^n * x[0] + b * sum_{k=0}^{n-1} a^n 
      = a^n * x0   + b * n * a^n 
      = a^n * (x0 + b*n)
-maybe we can have a kind of "exponentiality" parameter p between 0..1
 with 0, we have linear scaling (a = 1), with 1 exponential (b = 0)
 maybe a = 1-p, b = p * c (for some suitable c, maybe 1/N (?) where N 
 is the desired number of updates to reach the target)

I guess this is what i want!

elanhickler commented 7 years ago

x[n] = a^n x[0] + b sum_{k=0}^{n-1} a^n = a^n x0 + b n a^n = a^n (x0 + b*n)

What are the three equations?

and what is going on here: sum_{k=0}, doesn't look like C++

I could try to implement an ADSR based on this.

RobinSchmidt commented 7 years ago

yeah - it's LaTeX notation. i don't know how to write down equations in plain text otherwise

RobinSchmidt commented 7 years ago

...i'll come back to this

RobinSchmidt commented 7 years ago

https://en.wikipedia.org/wiki/LaTeX

RobinSchmidt commented 7 years ago

it's a typesetting system that includes all the good stuff for mathematics:

https://en.wikibooks.org/wiki/LaTeX/Mathematics

in RSLib, i've used that for the documentation. if you generate a doxygen documentaion there, you'll get a nice pdf document with proper math typesetting. unfortunately, it makes the code comments less readable - you have all that additional distracting markup there. ...sooo you can get a nice pdf documentation but the documentation in the h/cpp files is uglified. i'm not sure what to do in rapt

RobinSchmidt commented 7 years ago

btw. - a very strange observation that i made: when i read texts that include math, the appearance/typesetting seems to make a difference for, how hard or easy it is to understand. if the typesetting is aesthetically pleasing, the content gets easier to understand

elanhickler commented 7 years ago

check out these curves: http://www.mathopenref.com/graphfunctions.html?fx=x^(a/x)&gx=x^c&hx=x^(x/b)^(x*b)&xh=1&xl=0&yh=1&yl=0&ah=2&a=0.22340425531914893&bh=10&bl=-10&b=1.8&c=0.3

RobinSchmidt commented 7 years ago

I guess maybe I just want a modulatable version of your breakpoint ADSR, using "analog" shape.... does the shape only update on attack/reset? How modulatable is it?

not modulatable at all. the coefficients are fixed whenever a breakpoint is reached. even if you drag around the node, it will have effect only the next time the breakpoint gets hit. i think, for a modulatable version, we could use the same value-mapper that we need for meta/macro/parameter mapping. you would just input a time value, and it would read off the function value f(t). so, perhaps, i should just jump into implementing the value mapper now. can be useful in various contexts

RobinSchmidt commented 7 years ago

check out these curves

do you think they are useful? i could make my Parameter class more flexible by allowing the user to pass in a kind of ValueMapper object instead of providing a fixed collection of enumerated mapping functions

RobinSchmidt commented 7 years ago

like: setValueMapper(ValueMapper* newMapper).

then, you could subclass ValueMapper and do any mapping function you want. that would be the strategy pattern:

https://en.wikipedia.org/wiki/Strategy_pattern

...which is, btw., the same idea i want to use for making the gui customizable. there, you could then do something like mySlider->setPainter(mySliderPainter) ....or something

elanhickler commented 7 years ago

those curves are useful for envelope generators, no idea what I will need for mapping, most likely just log/exp curves.

elanhickler commented 7 years ago

i just went back to my old ADSR code and actually made something really awesome with it. It allows for feedback/looping/chaos, sounds nice as an oscillator, will be amazing as an envelope because the shape is very flexible while also being very natural. It's just not the easiest to control. But once you find some sweet spots, it really shines. This thing begs to be modulated and abused. All possible number explosions and infs are solved. Not to mention, it will be perfect for a percussion synth as the envelopes can be extremely snappy/natural.

image

The controls need to be better placed and sized, but it's pretty much finished. It would be nice for you to create the kind of linear smoother/shapable smoother you were talking about above, would love to have that for an ADSR, but for now I will close this issue.

elanhickler commented 7 years ago

https://hearthis.at/soundemote/basicadsr-feedback-looping/

This is the direct output of the envelope generator, used in such a way that it behaves like an oscillator. This is a test of a feedback-shaper-style ADSR in loop mode where the feedback chaotically interacts with the looping mechanism. When using lower ADSR times, the feedback helps to create very natural attack and decay shapes, from smooth/organic to extremely snappy. At first you are hearing an LFO controling envelope timing to create the modulation. At the end I turn off the LFO to modulate timings manually.

level += A*level^2 + B*level + C

elanhickler commented 7 years ago

f*** my ADSR is still locking on to divisions of the samplerate.

My ADSR is basically trying to be a 1 dimensional version of your RayBouncer except that sustain represents a movable boundary, and the sustain value should be held once reached unless it's i looping mode then it should continue bouncing.

I need help with this. I need to commission you to get my ADSR working more like your RayBouncer.

It's really simple:

  1. Start by creating a linear ADSR where value is incremented and decremented based on time it takes to get from currentValue to 1 or 1 to sustainAmp value.

Example: attackIncrement = (1-currentValue) * secondsPerSample / attackTime decayIncrement = (currentValue-sustainAmp) * secondsPerSample / decayTime;

Basically a linear smoother that has a starting value and target value and wants to reach the target value in the time given. This time is updated at the moment of a stage being triggered.

  1. We need Attack/Decay/Sustain/Release stages. Attack stages goes from currentValue to 1. Once 1 is reached, we go from 1 to sustainAmp value. If looping, we go back to 1 based on Attack time. If not looping, hold sustainAmp value until "isSustaining" is set to false, or whatever... release callback. From release, that's easy, just go from currentValue to 0.

  2. If we are in loop mode and we have to calculate multiple reflections per sample, limit to calculating 1 reflection, and after that, set value to random number between 0 and 1.

  3. To shape it into exponential, you simply feedback the currentValue into itself, so you increment more when the value is high and increment less when the value is low.

Example: currentValue += TINY+currentValue*feedbackAmount

My code is so close to working, man it would be nice if you just met me online, you can charge me hourly, and we go over my code and see if we can find an easy fix, see what I'm doing wrong.

RobinSchmidt commented 7 years ago

ok, maybe tomorrow in the late afternoon early evening (my time)? i'm a bit tired at the moment. but what exactly is the problem? is there a problem only when you are looping and have multiple reflections per sample? how does such a situation even occur? you must be running the env at insane speeds and/or have near-zero transition times.

btw. the bill is currently at 543 with 197 for the cycle-mark stuff (included in the 543), so i think, it's not yet time for a new invoice

elanhickler commented 7 years ago

ok, yes tomorrow is fine.

the problem is that the frequency is locking on to divisions of the samplerate, yes I'm running it at insane speeds, it's essentially supposed to be an oscillator with a sustain level. 1 dimensional Ray Bouncer with an adjustable wall called Sustain 😄

RobinSchmidt commented 7 years ago

ok, i'm making a 1D raybouncer with two adjustable walls called floor and ceil...

RobinSchmidt commented 7 years ago

ok..there's a class rsRayBouncer1D in rapt. it happily bounces back and forth between floor and ceiling: image

it started at 0, ceiling is 0.7 and floor is -0.2. for the time being, i just do multiple reflections, if necessarry

RobinSchmidt commented 7 years ago

with an increment of 3.1, we get multiple reflections per sample which results in weird patterns: image

elanhickler commented 7 years ago

It looks like you're getting values below 0. If that only happens at high frequency (when both attack and decay are approaching instantaneous), that is a good time to call makeRandomDecision(double * vaue) (a function that takes a value and changes it randomly between 0 and 1).

Edit: Oops, both your graphs are -.2 to +0.7, ok that's weird, whatever!

RobinSchmidt commented 7 years ago

as said, both, floor and ceiling are adjustable. i had the floor set to -0.2 in the plot (and the ceiling at 0.7)

RobinSchmidt commented 7 years ago

i think, if you want random decisions, i should make that optional (like, having a reflection mode parameter)

elanhickler commented 7 years ago

yeah, I don't exactly know what would work, but maybe... limiting reflections to 2 per sample and then random decision after that might work..? Edit: The goal is that it approaches noise (or, even better, chaos) instead of getting aliasing as you increase frequency.

RobinSchmidt commented 7 years ago

maybe not a random decision but some deterministic formula based on floor, ceil and value (and maybe increment)... that might come out pseudo-random anyway

elanhickler commented 7 years ago

right, yes, that would work too. Here's my code for shapes.

double FeedbackADSR::calculateNextVaue(double inc, Shape shape, double fbAmount)
{
    inc = jmax(1.e-6, inc);

    double out;
    switch (shape)
    {
    case Shape::EXP:
        out = inc + currentValue*fbAmount + fbAmount*r*.01 + r*.01;
        break;
    case Shape::LOG:
        out =  inc + (1-currentValue)*fbAmount + fbAmount*r + r*.001;
        break;
    case Shape::SIGMOID:
        out =  inc + (1-currentValue)*currentValue*fbAmount + fbAmount*r*.0001;
        break;
    case Shape::POW:
        out =  inc + currentValue*currentValue*fbAmount + fbAmount*r*.01 + r*.0001;
        break;
    }

    // level += A*level^2 + B*level + C

    //out = jmin(out, 0.5);

    return out;
}
RobinSchmidt commented 7 years ago

what is r?

elanhickler commented 7 years ago

sorry, im writing you a simplified version, one sec. r is random value / noise. I added noise to the entire system with that in my attempt to make things more interesting. :)

elanhickler commented 7 years ago
void incrementValue(double * currentValue, double increment, Shape shape, double shapeAmt)
{
    switch (shape)
    {
    case Shape::LIN:
        *currentValue += inc;
    case Shape::EXP:
        *currentValue += inc + currentValue*shapeAmt;
    case Shape::LOG:
        *currentValue += inc + (1-currentValue)*shapeAmt;
    case Shape::SIGMOID:
        *currentValue += inc + (1-currentValue)*currentValue*shapeAmt;
    case Shape::POW:
        *currentValue += inc + currentValue*currentValue*shapeAmt;
    }
}
elanhickler commented 7 years ago

a simplified formula is this, basically allowing any amounts of all shapes:

~level += A*level^2 + B*level + C~

~and you can go crazier and do~

~level += A*level^pow + B*level + C~

~having adjustable exponentialness.~

but for my purposes/control scheme I'm saving CPU by only doing the needed math for the desired shape.

edit: this would look something like

*currentValue += inc + (1-currentValue)*logShapeAmpt + currentValue^pow*expShapeAmpt

Edit: I dunno if this math is correct

RobinSchmidt commented 7 years ago

the first one looks actually simple enough. i the switch really faster?

elanhickler commented 7 years ago

switch statements are similar speeds or faster than if else: https://stackoverflow.com/questions/1028437/why-switch-case-and-not-if-else-if

RobinSchmidt commented 7 years ago

uuuh...your formula seems to ultra sensitive to the feedback. a little too much and it gets stuck at the ceiling...

elanhickler commented 7 years ago

what? you must be getting reverse feedback. Are you incrementing/decrementing correctly and not JUST incrementing?

+= vs -=

RobinSchmidt commented 7 years ago
  void increment()
  {
    x += dx + shape*x;
  }
elanhickler commented 7 years ago

and you increment TOWARD 0 in the case of bouncing off the ceiling?

RobinSchmidt commented 7 years ago

my reflection code is a bit complex due to supporting multiple reflections, the whole state update procedure currently goes like this:

  void increment()
  {
    x += dx + shape*x;
  }

  void reflect()
  {
    while(x > ceil || x < floor)
    {
      if(x > ceil)
      {
        T over = x - ceil;
        x = ceil - over;
        dx = -inc;
      }
      if(x < floor)
      {
        T under = floor - x;
        x = floor + under;
        dx = inc;
      }
    }
  }

  inline T getSample()
  {
    T out = x;
    increment();
    reflect();
    return out;
  }
elanhickler commented 7 years ago

wait a second, we need separate increment/decrement times. you only have function "set increment"? edit: I guess it'd be easy for me to add. Edit: I'm getting ready to test.

RobinSchmidt commented 7 years ago

ahh...ok - yes. i currently use the same value either positive or negative. but that's a quick change...and should not really have much to do with feedback sensitivity/instability

elanhickler commented 7 years ago

ok, going to pull your changes and see if I can figure out the feedback problem.