Envelopes for JUCE Synthesiser class?


#1

Hi, I’m fairly new to JUCE and I recently just figured out how to use the JUCE Synthesiser class to create synth voices and sounds. I thought a reasonable next step would to be creating an ADSR envelope. I’ve found many examples online of different ADSR classes however they all seem to be structured in a way thats not so easy to interface with the JUCE Synthesiser class (at least at my level). I was wondering if anyone might be able to share more straightforward examples of synth projects which utilize ADSR envelopes and are based on the JUCE synthesiser class?


#2

Try using a modified version of the envelopes at earlevelengineering.com and remember not to render out your envelope every sample. As far as interfacing with juce simply make the envelope a child class of your voice and use a multiply as your interface.


#3

Thanks for the reply Lopiano. I’ve been reading the articles on earlevelengineering. Very helpful. I ended up getting the ADSR from the Synthesis Toolkit working. In my RenderNextBlock function, I’m multiplying the ADSR value by each output sample:

        double output = getSample() * ADSR.tick();

So you recommend not rendering the envelope for every sample? That makes sense to make it more computationally efficient.


#4

yeah I’d recommend having renderEnvelope be one function that just renders and then have a function called getSmoothedLevel that returns the linear interpolation of the envelopes out level. So you end up linear interpolating ever sample but you only render the envelope every 32 or 64 samples.


#5

i found it very efficient, as there is just one addition and one multiplication (actially two) over the procedure. i dont think one would need to make additional interpolation as perhaps we would end up with similar formula including feedback assignment for the interpolation itself. just look again how sweet the coefs are computed to our advantage, or im tripping ?


#6

You can make pretty efficient ADSR that generates a new sample every sample period, with one multiply and two additions (so roughly the same as linear interpolation, but no convoluted lookup/calculation every 32 samples). The tick()/process() method looks something like this:

float processEnvelope()
{
    counter += alpha[currentState];
    
    if (counter >= 1.0f)
    {
        ++currentState &= 0x4; //increment state
        counter = 0.0f; //reset counter
    }
    
    return alpha[currentState] * (x[currentState] - average) + average;
}

counter is a floating point counter. currentState can be any integral type, uint8 works nicely. It’s a state variable, where 0x0 = Attack, 0x1 = Decay, 0x2 = Sustain, and 0x3 = Release. You can use an enum if you want, but it’s overkill and you need some casting semantics for the incrementing to compile. counter &= 0x4 is equal to counter %= 4.

x[currentState] should be 0.0 for attack, the sustain level for decay and sustain stages, and 0.0 for release.

alpha[currentState] is the coefficient of an exponential moving average filter. You can use a number of equations to calculate it, the simplest is alpha = 1.0 / timeConstant * sampleRate (but that’s not perfect).

average is just an internal filter state. it should be initialized to 0.0.

This method is essentially a state machine that switches the input/time constant to to an exponential moving average filter, based on a timer. It’s not perfect, but it’s fast, and kind of similar to analog ADSR circuitry.


#7

pretty


#8

It’s also neat because if you just use a noteOn message to switch currentState to 0x0 the envelope will restart from the current level instead of resetting it to zero, which can remove pops from noteOn event being received before the release stage is finished or if the you receive a noteOn event before a noteOff.


#9

isn’t counter &= 0x3 equivalent to counter %= 4 ?


#10

Ouch! Ignoring the fact that like Martin said, you presumably meant 3 not 4, then that’s surely undefined behaviour? Does the compiler really accept a pre-increment expression as a valid l-value!??

Always keep it simple:

currentState = (currentState + 1) & 3;

Just flagging this because if you make a habit of writing code like that you’re going to hit obscure UB problems one day!


#11

@jules @martinrobinson you guys are absolutely right, I spaced on that one.

@jules It’s been a long time since I’ve used that trick in real code, but as far as I can remember compilers don’t complain about it. I just double checked and it seemed to work fine. But you’re right, it’s better to keep the code simple!