RobinSchmidt / RS-MET

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

Need an envelope follower. Your slew limiter isn't quite doing it! #265

Closed elanhickler closed 2 years ago

elanhickler commented 5 years ago

I'm using your slew limiter to create an envelope follower, but it suffers from "gurgly modulation" where, for example, the individual peaks of a sawtooth causes undesirable modulation. I think a simple hold function would work. Rise and then do not fall until x seconds have passed.

elanhickler commented 5 years ago

You need an "is idle" or "is silent" check for your slew limiters or you need to provide access to last out value.

elanhickler commented 5 years ago

alright gonna subclass.

elanhickler commented 5 years ago

tested, it only works for on/off signals. Do I need to have two slew limiters, one for rise/fall detection, the other to actually rise and fall only after the other one is finished falling? hmmmmmmmmmmmm

class EnvelopeFollower
{
public:
    EnvelopeFollower()
    {
        delayTrigger.setTriggerFunction([this]() { doSkip = false; });
    }
    ~EnvelopeFollower() = default;

    void setSampleRate(double v)
    {
        slewLimiter.setSampleRate(v);
        delayTrigger.setSampleRate(v);
    }

    double getSample(double v)
    {
        double incomingAbsoluteValue = abs(v);

        bool valueIsRising = incomingAbsoluteValue > lastAbsoluteValue;
        lastAbsoluteValue = incomingAbsoluteValue;

        if (valueIsRising)
        {
            doSkip = true;
            delayTrigger.restart();
            heldAbsoluteValue = std::max(heldAbsoluteValue, incomingAbsoluteValue);
        }
        else if (!doSkip)
        {
            heldAbsoluteValue = incomingAbsoluteValue;
        }

        currentSlewedValue = slewLimiter.getSample(heldAbsoluteValue);

        delayTrigger.incrememt();

        return currentSlewedValue;
    }

    bool isIdle()
    {
        return currentSlewedValue <= 1.e-6;
    }

    void setAttackTime(double v)
    {
        slewLimiter.setAttackTime(v*1000.0);
    }

    void setHoldTime(double v)
    {
        delayTrigger.setSeconds(v);
    }

    void setDecayTime(double v)
    {
        slewLimiter.setReleaseTime(v*1000.0);
    }

    RAPT::rsSlewRateLimiter<double, double> slewLimiter;
    DelayedTrigger delayTrigger;
    bool doSkip = false;
    double heldAbsoluteValue = 0;
    double lastAbsoluteValue = 0;
    double currentSlewedValue = 0;
};
elanhickler commented 5 years ago

bah, tried a bunch of different things. Can't get a hold function to clean up some of the signal.

robin i need an envelope followerrrrrrrrrrrrrrrrrrrr 😢

I want to get some nice plucky envelopes if possible, maybe a more advanced transient detector is what I need, or just a good hold function.

RobinSchmidt commented 5 years ago

i added a class rsSlewRateLimiterWithHold - but did not yet try it. will do now..

RobinSchmidt commented 5 years ago

black: enveloped sawtooth, blue: original envelope, green: detected envelope (with hold set to a length of 2 cycles) image image hmmm...i think, what you describe as "gurgly" could be related to the Gibbs ripples that make the maximum excursion of each cycle slightly different. in the example, i have taken a sample rate of 2 kHz and a sawtooth frequency of 100.5 Hz which results in a repetitive pattern of 10 cycles. that's probably a 10 Hz gurgle frequency that disappears when the frequency is exactly 100 Hz (or some other exact fraction of the sample rate). image i think we should try to roll off the Gibbs ripples (maybe with a Bessel lowpass) before applying the envelope follower. i will try that....

RobinSchmidt commented 5 years ago

it seems, with a lowpass in front of the slewrate limiter, i can reasonbaly get rid of the gibbs gurgle (this is without hold): image the envelope still shows modulation with the signal's input frequency (and its harmonics). we could try to get rid of them by another lowpass tuned somewhere below the fundamental of the input. but maybe better would be some sort of sliding maximum extractor. i just wonder, how that could be implemented efficiently (without searching in a circular buffer for a maximum at each sample). but maybe i should just implement it the naive way, see if it gives good results and if so, think about efficiency later

RobinSchmidt commented 5 years ago

is this for realtime purposes? because if not, i could use the non-realtime envelope detection code that i have recently written

elanhickler commented 5 years ago

realtime. I'm working on FMD and need an envelope follower to open/close the filter.

elanhickler commented 5 years ago

is hold time in seconds?

RobinSchmidt commented 5 years ago

milliseconds - to be consistent with attack and release parameters

elanhickler commented 5 years ago

hmmmmm, I might want to go with a treshold-based envelope trigger. No matter what the settings are, you can't get a plucky envelope, and I've tried yours, Cytomic The Drop and Tone2 FilterBank3 envelope followers. They are all similar in that way.

Edit: But definitely still will use the envelope follower in a larger product where I can have trigger-based and envelope following options.

Edit: I dunno, still trying to decide what to do.

elanhickler commented 5 years ago

ok made a decision, envelope follower it is.

elanhickler commented 5 years ago

It is possible to get plucky envelopes, you just need a plucky input signal. Not ideal.

I'm playing with Cytomic The Drop and they have a trigger system and a "sense" knob. It seems like a transient detector with a sense amount. Yep, that would be ideal. And it has a switch to go from envelope follower to transient detector.

Edit: Do you have a realtime transient detector lying around I can throw in?

RobinSchmidt commented 5 years ago

hmm...not currently in the rs-met codebase. i have some verrrry old legacy code lying around where i did something like that: https://spl.audio/wp-content/uploads/transient_designer_2_9946_manual.pdf i could drag it in and adapt/update/make-compatible-with-new

RobinSchmidt commented 5 years ago

maybe i should just implement it the naive way, see if it gives good results and if so, think about efficiency later

i think, i have found something relevant: https://www.nayuki.io/page/sliding-window-minimum-maximum-algorithm

these links may also be useful: https://stackoverflow.com/questions/8905525/computing-a-moving-maximum https://stackoverflow.com/questions/4802038/implement-a-queue-in-which-push-rear-pop-front-and-get-min-are-all-consta

elanhickler commented 5 years ago

Yeah I can implement and test the transient detector you come up with, not in a rush though.

RobinSchmidt commented 5 years ago

the moving-maximum filter seems to work quite well - the red curve is the moving-max-filtered green curve (with a range/support of one cycle for the filter): image

RobinSchmidt commented 5 years ago

especially in the constant amplitude section, the modulation of the envelope with the sawtooth frequency is amost completely gone: image i've implemented the deque-based algorithm described in the nayuki link. and i actually have an idea how to further reduce the worst-case processing cost to O(log(k))

edit: i'm talking about this point in the article:

Adding an element and updating the deque takes worst-case Θ(k) time but amortized Θ(1) time.

this thing here:

Looping while the deque is non-empty and its tail element is less than x, we remove the tail element.

is what takes O(k) in the worst case. but it does not utilize the fact that the deque is in fact sorted, so we can use binary search to find the number of elements we can throw away (at once) instead of just salami slicing them away one by one in a linear loop

edit: after thinking about it some more, it might well be the case that reducing the worst-case cost from O(k) to O(log(k)) by binary search could thwart the amortized cost of O(1) of the current algorithm, so it might be not so advisable after all...hmmm....

elanhickler commented 5 years ago

Is this a transient detector you're working on, or an improvement to the envelope follower?

RobinSchmidt commented 5 years ago

improvement to the envelope follower. however, the (realtime) transient detection algorithm is based on envelope followers also, so any improvement on envelope detectors may benefit transient detection as well

elanhickler commented 5 years ago

nice. Blue line looks perfect.

RobinSchmidt commented 5 years ago

haha! yes - because that is the original envelope that i plot for reference. the red is the best detected envelope so far. i can get a slight improvement by applying a moving-max and moving-min and take their average. but recovering the (original) blue envelope would be the holy grail

btw. the green and red ones are below the orginal one because of the initial anti-gibbs-gurgle-lowpass. but that's just a matter of scaling

elanhickler commented 5 years ago

ah ok! Don't spend too much time on this, the obvious solution is just to have the plugin with a side chain input for envelope.

RobinSchmidt commented 5 years ago

ok - if the use case is such that we have the original envelope signal available, then this would indeed be the cleanest solution. but this stuff here may be useful for dynamics processors

RobinSchmidt commented 5 years ago

btw. - do you even think that my diagnosis to attribute the gurgle to gibbs ripples is correct? do your input signals that lead to gurgly sounds expose such gibbs ripples? it's probably less of an issue with natural, acoustic sounds. or maybe it is? not sure, if i should expect pronounced gibbs ripples from recordings of natural sounds. but maybe i should

elanhickler commented 5 years ago

I think it would be an issue with any signal. To extract the envelope you need to avoid having it be influenced by the dips between oscillations. Even sinewaves will have that.

RobinSchmidt commented 5 years ago

dips between oscillations? you mean the dips that occur in the rectified signal when the sinewave goes through zero?: image

(plot created with: https://sagecell.sagemath.org/?z=eJwryMkv0UhMKtYozszTqNDU1FEw0FEwMtAEAGSzBtU=&lang=sage)

this is actually precisely what the moving-max filter addresses (picking the maximum value in some range). but high-frequency sinewaves could pose another problem - their envelope may look modulated (to the eye as well as to the detector) due to the fact that in each cycle, the wave is sampled at different phases (that's not an issue with lower frequency sines because their cycle is sampled densely enough to ensure a sample close enough to the actual maximum in each cycle)

RobinSchmidt commented 5 years ago

further improvements (blue: original, green: detected): image image but i cheated a little by manually scaling the detected envelope up to match the original. it can also be seen that the detected envelope is always lagging behind the original one. in a realtime context, one would probably compensate by a look-ahead functionality. if we would want to be really fancy, we could try to avoid such a look-ahead/latency by employing some sort of prediction scheme - but maybe that might be overkill and it would never be perfect anyway (there's no way to predict a sudden change in the envelope, like the border between attack and decay, for example - you could only try to continue the direction of past changes)

elanhickler commented 5 years ago

So then all we need for a transient detector is something that sends a trigger when the amplitude envelope travels upward fast enough. Maybe if you have a highpass filter after the envelope extractor and the highpassed signal peak reaches the "sensitivity" threshold, it gets triggered. I think this will also allow for signals that might have multi-level edges (when input signal doesn't reach 0 before another transient)

RobinSchmidt commented 5 years ago

this looks really close, i think (i applied another bessel filter to smooth out the steps/jaggies and simulated an appropriate look-ahead by just time-shifting the whole envelope): image image

all we need for a transient detector is something that sends a trigger

oh - so what you actually want is module that just sends a ping when it detects a transient?

elanhickler commented 5 years ago

Well I can integrate the pinging part. I just need a value that goes from false to true at the time (the sample) that the transient is detected (when conditions of a transient are met).

elanhickler commented 5 years ago

robin, thanks for the envelope follower. It made my plugin: image https://www.soundemote.com/labs/flower-child#s3

RobinSchmidt commented 5 years ago

the gui looks cool! how do you use it? the code for the new env-follower is actually still in the prototypes section of the codebase - not in the library?

aha! nice! fun to watch! https://www.youtube.com/watch?v=tdMpRR4AlQ4

elanhickler commented 5 years ago

I have my own version of RS-MET, and I copied the needed code over. Also, we are getting more and more out of sync, you gotta upgrade your JUCE so we can get back in sync. I use the envelope follower to open/close the filter frequency and output volume.

RobinSchmidt commented 5 years ago

so, are you using an instance of rsMovingMinMaxFilter? do you have a fixed length for the filter or is it user adjustable? because in its current state, i think it actually may cause problems when the length is not fixed. yes - i need to update juce sometime soon. but at the moment i'm really in the flow working on my modal synthesis algorithm again. i hope, i will be able to produce some first sounds with it in the coming days (i mean: have a working AudioModule available in ToolChain - rendering sounds offline in the experiments code section is something i already did long ago, as you know). time to reap the crops, finally. the algorithm was laying dormant sooo long already. if all goes to plan, this will become a very powerful synthesizer for producing acoustic instrument sounds

elanhickler commented 5 years ago

Robin, I am in desperate need of you to finish the fft resynthesis. At least get it to the point of removing/extracting harmonics and rebuilding them at a specific frequency and starting phase. That will allow me to make progress on Orange Tree Samples projects.

Seemed like you were finished or nearly finished with this.

RobinSchmidt commented 5 years ago

ok - just one more day on modal stuff then back to fft. almost finished? if you refer to the sinusoidal modeling stuff, that's far from being finished. or do you mean just spectrogram analysis/resynthesis? the basic roundtrip works since years, but when trying to modify things, one typically wants a higher level representation

elanhickler commented 5 years ago

You just implemented fft harmonic extraction. I'm assuming now everything is ready for phaselocking. Extract harmonics then rebuild them with fixed frequency while using original amplitude.

If that's finished then upgrade your juce so we can get back in sync.

RobinSchmidt commented 5 years ago

ok - the modal stuff is now in a state where i can put it aside for while and may revisit it later. it's just the beginning but it already produces some promising sounds in toolchain (i've added some first patches to the preset repo)

i'll reply on the other thread about phaselocking

elanhickler commented 5 years ago

I need a transient detector for Flower Child Filter and the improved envelope follower. Is that all ready to use?

RobinSchmidt commented 5 years ago

i'll move the envelope follower code over from the prototypes section to the rapt library tomorrow and make a convenience wrapper class that has all the improvements on board already. ..but i thought, you are already using this code (and presumably made such a convenience wrapper yourself?)

transient detector? you mean, the SPL style transient detector? ...i would have to dig out the old legacy code and marry it with the new stuff (i.e. use SPLs "differential envelope" approach (that i have already implemented somewhere - but would also be straightforward to implement again) - but replace the old envelope detectors with the improved ones. that's not yet ready.

i júst had an idea how to solve this issue:

high-frequency sinewaves could pose another problem - their envelope may look modulated (to the eye as well as to the detector) due to the fact that in each cycle, the wave is sampled at different phases (that's not an issue with lower frequency sines because their cycle is sampled densely enough to ensure a sample close enough to the actual maximum in each cycle)

don't look for a maximum value of the samples but instead consider triplets of samples, fit a parabola through each triplet and use the maximum of the parabola maxima ....i'll have to think through the details....

elanhickler commented 5 years ago

I've discarded all my edits to be in sync with your library, so I'll need you to make it available in your latest version of RS-MET.

In terms of the transient detector, I want it to trigger a basic ADSR so we are no longer using an envelope follower, but just a nice triggered decaying envelope. I'll have both options available for the user.

elanhickler commented 5 years ago

What is the class called for the envelope follower I should use for Flower Child Filter?

When you implement the transient detector, let me know what the class is called.

elanhickler commented 5 years ago

I never got to use your envelope follower in Flower Child Filter, I've been using the slew limiter this whole time!

RobinSchmidt commented 5 years ago

the code for the min/max filter stuff is dragged over (Filters/Basic/MovingWindowFilters.h/cpp) - but i still need to implement the convenience class. basically, wrap the whole processing chain that i use in my envelopeFollower() test/expermination function (with which i have created all the plots in this thread) into a class. i was so busy with the supersaw stuff the last few days

elanhickler commented 5 years ago

Alright, no problem! I'll release a flower child filter update with a new filter once you get the envelope follower ready to go.

Also, excited about making this subtractive synth with you. Do you want to do 50/50 on this one? So, no hourly pay on the subtractive synth, royalties only. We will both be bringing a lot to this project. Your supersaw stuff and oscillators and other various RS-MET stuff, my filters, GUI, advertising, product design, and I might ask you to help me in some other areas, like getting your UI library to play better with custom UI stuff, and help me do a few more advanced UI stuff like showing waveform plots.

We could aim for a $40 to $60 price point and aim for average 50 sales a month

RobinSchmidt commented 5 years ago

Also, excited about making this subtractive synth with you. Do you want to do 50/50 on this one? So, no hourly pay on the subtractive synth, royalties only.

yes - ok - i didn't write work-hours for the supersaw work. given that in this case, my code is not the major ingredient (as opposed to snowflake) and you do all the business-stuff, 70/30 would be fine as well

do you think, 50 sales per month is realistic? :-O

elanhickler commented 5 years ago

I have no idea, but I'm just throwing out a number so I can see how close I am when we actually sell it. We have over 3000 e-mail subscribers btw, soooo... if 1.5% of our subscriber base makes a purchase a month, that's 50 sales, and we have other outlets to reach people.

The point of the 50/50 split is so that you can have more incentive to create a better product. Like I said, I might need help on a few things.

elanhickler commented 5 years ago

Gonna need the envelope following stuff nowwwww!