Then of course, there is the analog engineer's take on this stuff (this pretty much was inspired by analog circuits designed to implement an ADSR):
https://github.com/transmogrifox/transmogriFX_bela/blob/master/src/adsr.cpp
It looks like this:

Below is the working part of the code with more comments added. Notice it uses first-order IIR filters instead of exp() or pow() functions. The exponential function occurs naturally due to recursion. All the setup code is related to scaling coefficients to the correct values so the recursion works properly.
void
adsr_tick_n(adsr* ad, float* output)
{
//The ad->NS comes from the block size set up in the creation of this struct.
//When used in Bela, this is the "n" variable.
for(int i = 0; i < ad->NS; i++)
{
//These next couple of blocks are for transitioning from one state to the next.
if(ad->trig_timer < 1)
ad->state = ADSR_STATE_RELEASE;
else if (ad->trig_timeout > 0)
ad->trig_timer--;
//if trig_timeout < 0 then sustain forever
if( (ad->state == ADSR_STATE_RELEASE) && (ad->trig_timer > 0) )
{
ad->state = ADSR_STATE_ATTACK;
}
//The switch statement below evaluates whether attack, decay, sustain, or release state should be computed
switch(ad->state)
{
case ADSR_STATE_RELEASE:
ad->sv *= ad->rls; //Just multiply the current value of "sv" by a number less than one for every sample.
break;
case ADSR_STATE_ATTACK:
if(ad->sv >= ad->amplitude) //we trigger state change when state variable "sv" gets to max amplitude
{
ad->sv = ad->amplitude;
ad->state = ADSR_STATE_DECAY;
}
else
ad->sv = ad->sv + ad->atk * (ad->pk - ad->sv); //This is a first-order IIR filter, behaves like an analog RC filter driven with a step input "pk"
break;
case ADSR_STATE_DECAY:
ad->sv = ad->sv + ad->dcy * (ad->sus - ad->sv); //This also is a first-order IIR, driven with step input "sus"
if(ad->trig_timer == 0)
ad->state = ADSR_STATE_RELEASE; //Redundant but paranoia safety (always return to a known state)
break;
default:
ad->state = ADSR_STATE_RELEASE; //more paranoia
break;
}
output[i] = ad->sv; //each time through the loop add the current sample to the output buffer
}
}
Then on every block you multiply the buffer you send in for "output" with your sine tone.
Here is an example of where this is used in Bela for controlling a filter cut-off frequency:
https://github.com/transmogrifox/transmogriFX_bela/blob/master/src/sample_hold_modulator.cpp
It's complicated to sort through that whole blob, so below shows where the ADSR struct is used:
sh_mod*
make_sample_hold(sh_mod* sh, float fs, int N)
{
.
.
.
sh->ad = make_adsr(sh->ad, fs, N);
.
.
.
sh->adsr_env = (float*) malloc(sizeof(float)*N);
.
.
.
for(int i = 0; i < N; i++)
{
sh->adsr_env[i] = 0.0;
.
.
.
}
.
.
.
//Initialize ramp rates
adsr_set_attack(sh->ad, Trate/8.0);
adsr_set_decay(sh->ad, Trate/8.0);
adsr_set_sustain(sh->ad, 0.36);
adsr_set_release(sh->ad, Trate/4.0);
adsr_set_trigger_timeout(sh->ad, 0.5/rate);
.
.
.
}
.
.
.
void
run_sample_hold(sh_mod* sh, float* output)
{
.
.
.
adsr_tick_n(sh->ad, sh->adsr_env);
.
.
.
if(sh->en_adsr)
output[i] = sh->adsr_env[i];
.
.
.
}
This doesn't back it up to the Bela render() function, but my code has to be tracked back to the render() function via the envelope filter code:
https://github.com/transmogrifox/transmogriFX_bela/blob/master/src/envelope_filter.cpp
void
envf_tick_n(env_filter* envf, float* x, float* e)
{
.
.
.
//TODO: move to env_filter struct
run_sample_hold(envf->sh, envf->sh_buff);
.
.
.
Then finally envf->sh_buff gets applied to something (modulating the filter)
//limit to range 0 to 1 and apply to filter frequency setting
ei = clamp(ei*envf->sns*envf->ishmix + envf->sh_buff[i]*envf->shmix);
envf->frq[i] = envf->depth + envf->width*ei;
And envf->frq[] is a buffer to identify a time-varying cut-off frequency
//Run the state variable filter
svf_tick_fmod_soft_clip_n(envf->svf, envf->y, envf->frq, envf->N);
So now where/how is this called from render()?
https://github.com/transmogrifox/transmogriFX_bela/blob/master/src/render.cpp
First you have to see what was done in setup:
bool setup(BelaContext *context, void *userData)
{
gAudioFramesPerAnalogFrame = context->audioFrames / context->analogFrames;
gifs = 1.0/context->audioSampleRate;
gNframes = context->audioFrames;
ch0 = (float*) malloc(sizeof(float)*context->audioFrames);
ch1 = (float*) malloc(sizeof(float)*context->audioFrames);
.
.
.
//Envelope filter
ef = envf_make_filter(ef, context->audioSampleRate, gNframes);
gMaster_Envelope = (float*) malloc(sizeof(float)*gNframes);
for(int i = 0; i<gNframes; i++)
gMaster_Envelope[i] = 0.0;
.
.
.
}
Then here is render()
void render(BelaContext *context, void *userData)
{
format_audio_buffer(context, ch1, 1);
format_audio_buffer(context, ch0, 0);
envf_tick_n(ef, ch1, gMaster_Envelope);
.
.
.
}
I may have gone overboard in explaining the connection back to render(), but the first step from adsr_tick_n() gives the algorithm. It's pretty simple for me because it thinks like I do, but putting myself in the place of trying to digest some other person's code...it's probably more horrible than the author you're referencing 🙂
Anyway let me know if you want some more streamlined examples. The adsr code is a complete C-style struct that implements an ADSR the way an analog synthesizer does it. I haven't benchmarked it, but I expect the code is reasonably efficient. It certainly doesn't make a noticeable increase on CPU usage when I switch between sample/hold filter options. Pretty minimal compared to the double-sampled state-variable filter it's used in 🙂