Hello,
I got the e-book "basic synth" which describes very thouroughly how to make many elementary parts of a software synthesizer, and how it relates to the math.

After trying to modify the basic sine patch with an envelope, the IDE keeps giving me an "underrun detected" and then the Bela disconnects from USB and I have to unpower it completely before it will work again.

My code looks like this:

#include <Bela.h>
#include <cmath>
#include <iostream>
using namespace std;

float frequency = 440.0;
float phase;
float inverseSampleRate;

float TWO_PI = M_PI * 2;
float freqRadians;
float phaseIncr;
int envelopeDuration = 3; // seconds
float totalSamples; 
float attackRate = 10;
float decayRate = 10;
float attackTime;
float decayTime;
float decayStart;
float envInc;
float peakAmp = 1;
float volume = 0;

bool setup(BelaContext *context, void *userData)
{
	inverseSampleRate = 1.0 / context->audioSampleRate; // time of one sample
	phase = 0.0;
	freqRadians = TWO_PI / context->audioSampleRate;
	phaseIncr = freqRadians * frequency;
	totalSamples = envelopeDuration * context->audioSampleRate;
	attackTime = attackRate * context->audioSampleRate;
	decayTime = decayRate * context->audioSampleRate;
	decayStart = totalSamples - decayTime;
	if (attackTime > 0) envInc = peakAmp / attackTime;
	
	return true;
}

void render(BelaContext *context, void *userData)
{
	unsigned int n = 0;
	
	for(unsigned int k = 0; k < totalSamples; k++){
		if(k < attackTime || k > decayStart) volume += envInc;
		else if(k == attackTime) volume = peakAmp;
		else if(k == decayStart) envInc = -volume/decayTime;
		
		
		float out = volume * sinf(phase);
		phase += phaseIncr;
		if((phase += phaseIncr) >= TWO_PI) phase -= TWO_PI;

		for(unsigned int channel = 0; channel < context->audioOutChannels; channel++) {
			audioWrite(context, n, channel, out);
		}
		n++;
		if(n > 16) n = 0;
		
	} 
}

void cleanup(BelaContext *context, void *userData)
{

}

Can anyone see what I'm doing wrong maybe? I put the original for loop, that loops around the audio frames, inside a "larger" for loop that (should) loops around my envelope. I hear a constantly loud clicking, very low frequency, which changes it's timbral qualities slowly.. but not the tone or envelope i would expect.

Alternatively, if someone can point me to a simple well-working example of a simple envelope implementation - that would be helpful too.

Thanks

Hi,

Your render function is not quite right, remember the render function should process context->audioFrames samples, in the original code there was:

for(unsigned int n = 0; n < context->audioFrames; n++)
{
  // do stuff here
}

In your code you are processing 3 seconds worth of samples, which is quite a lot more.

So make your k variable global and initialise it to 0 in setup()

Then in the audio loop from the original example you can put your code in, don't use a loop for k, just increment k each time round the original loop.

If you are still stuck after trying I can give you some more pointers...

    AndyCap

    Making k a global variable instead of traversing it with a for in render solved that issue - thank you so much.

    I've succesfully implemented a simple linear 2 segment envelope (an extremely tiny step for mankind, but....), and now I'm trying to implement a pretty basic exponential curve. It's not really working out though, and I'm wondering if the author of basic synth did something wrong (probably not) or if I misunderstood something. Here's how he does it in his book (comments are my additions):

    range = peakAmp / totalSamples; // peakAmp is 1.0, totalSamples is attack+decay sample amounts
    offset = 0; // he's never using this variable, don't get why he has it here then 
    expIncr = 1 / attackTime; // here I changed 1 to "1.0" and attackTime is the samplesize of attack segment
    expX = 0; // changed this to 1.0
    expB = 2; // changed this to 1.5
    for (n = 0; n < totalSamples; n++){ // here I did as you said, and my "n" is instead called "k"
       if(n < attackTime || n > decayStart){
         volume = range * pow(expX, expB); // volume peak is very low. see below
         expX += expIncr;
       }
       else if(n == decayStart){
         expIncr = -1 / decayTime; // changed -1 to -1.0
         expX = 1; // changed to 1.0
       }
       sample[n] = volume * sin(phase); // here I set the float "out" instead of writing to an array
       if((phase += phaseIncr) >= twoPI) 
         phase -= twoPI;
    }

    I guess I'm just wondering if anyone dsp experienced can easily recognize this code as good practice ? If there's anything one should be particular careful with. To me, the "range" variable seems odd.. when 1.0 (peakamp) is divided by the total amount of samples, you get a very tiny number.. like eg, 0,000023.

    I can not wrap my head around why it isn't working, but here's my code:

    
    #include <Bela.h>
    #include <cmath>
    
    float frequency = 440;
    float phase;
    float inverseSampleRate;
    float TWO_PI = M_PI * 2.0;
    float freqRadians;
    float phaseIncr;
    unsigned long int totalSamples; 
    float attackTime;
    float decayTime;
    unsigned long int attackSamples;
    unsigned long int decaySamples;
    unsigned long int decayStart;
    float peakAmp;
    float volume;
    unsigned long int k;
    
    float range;
    float expIncr;
    float expX;
    float expB;
    
    bool setup(BelaContext *context, void *userData)
    {
    	expX = 0.0;
    	expB = 1.5;
    	attackTime = 0.01;
    	decayTime = 1.0;
    	peakAmp = 1.0;
    	volume = 0.0;
    
    	attackSamples = attackTime * context->audioSampleRate;
    	decaySamples = decayTime * context->audioSampleRate;
    	k = 0;
    	inverseSampleRate = 1.0 / context->audioSampleRate;
    	phase = 0.0;
    	freqRadians = TWO_PI / context->audioSampleRate;
    	phaseIncr = freqRadians * frequency;
    	totalSamples = attackSamples + decaySamples;
    	decayStart = totalSamples - decaySamples;
    	
    	range = peakAmp / totalSamples;
    	expIncr = 1.0 /  attackSamples;
    	return true;
    }
    
    void render(BelaContext *context, void *userData)
    {
    	for(unsigned int n = 0; n < context->audioFrames; n++) {
    		if(k < attackSamples || k > decayStart) {
    			volume = range * pow(expX, expB);
    			expX += expIncr;
    		}
    		else if(k == decayStart) {
    			expIncr = -1.0/decaySamples;
    			expX = 1.0;
    		}
    		
    		float out = volume * sinf(phase) * 0.5;
    		if((phase += phaseIncr) > TWO_PI)
    			phase -= TWO_PI;
    
    		for(unsigned int channel = 0; channel < context->audioOutChannels; channel++) {
    			audioWrite(context, n, channel, out);
    		}
    		k++;
    		if(k == totalSamples) {
    			k = 0;
    			expIncr = 1.0 / attackSamples;
    			expX = 0.0;
    			expB = 1.5;
    			volume = 0.0;
    		}
    	}
    }
    
    void cleanup(BelaContext *context, void *userData)
    {
    
    }

    I'm not sure about that code, as expX can never go above 1.0f then it doesn't matter what you raise it to it will always be <= 1.0f;

    As a guess you want

    volume = pow(expX, expB)
    

    I'm not at all sure what that range variable is for!

      AndyCap I think, not sure though, that expX is supposed to be scaled to 0-1/1-0 to each segment length. It's representing the x (time), but I'm not at all sure about range either.

      According to the author, range is calculated like this:
      alt text
      where Y is the amplitude, and Yend and Ystart are amplitude values at the start and end times (x). According to his code, n is implemented as "totalsamples", which I take for granted must be samples of the whole envelope (attackSamples + decaySamples)

      The formula that he's building his code around is this:

      alt text
      where c is a constant offset.
      so Xn becomes expX, and the power "b" becomes expB, and a is the range. Yn is the amplitude at the given sample "n"

      EDIT: removing the range variable as you suggested, does make it work smooth.. but it still bothers me that I don't understand what the author is trying to do here. Obviously, it must be me doing something wrong. I mean, he wrote a huge book on DSP - he must know what he's doing.. Or if it actually doesn't make complete sense what he's doing, I should hurry up and find something else to read.

      The book might not be showing all the code. Is there companion code you can download off the internet for the book?

        AndyCap there is companion code yes, but the examples in there are widely differing (and more complicated) from the examples early on in the book. perhaps I should just proceed and disregard his range variable. maybe it makes sense later on. thanks for the input andy - very appreciated!

        The range makes no sense to me, so good idea to ignore it 🙂

        If you are looking for some good books on this sort of thing have a look at the two books by Will Pirkle.

        The "designing software synthesiser plug-ins in C++" book might be well worth a look.

        a month later

        I have the same book and stumbled upon this question while searching the web. The short answer is: instead of

        volume = range * pow(expX, expB);

        it really should be

        volume = peakAmp * pow(expX, expB);

        Usually I just take the code in the book as inspiration/pseudocode to understand the main points and rewrite it myself with my own thoughts.

        I spent some time reading the code now to understand what you are pointing out.

        For better reference here is the code (I'll just keep the stuff relevant to the attack segment) and add some comments:

        // totalsamples = count of samples for the entire signal, including attack, sustain and decay time
        range = peakAmp / totalSamples; // peakAmp = peak amplitude in the entire signal
        offset = 0; // ystart (also called c in the book); not used because it's pseudocode and not too consistent
        expX = 0; // Xn for attack portion only, goes from 0.0 to 1.0
        expB = 2; // b
        // attackTime = count of samples for the attackTime segment
        expIncr = 1 / attackTime; // step size to go from one Xn to Xn+1 (from current Xn to the next Xn)
        for (n = 0; n < totalSamples; n++)
        {
          // our current sample index n is below the attackTimeLimit = we are in the attack segment of the signal
          if (n < attackTime)
          {
            // pow(expX, expB) is always in 0.0 to 1.0
            volume = range * pow(expX, expB);
            expX += expIncr;
          }
        }
        

        To answer the questions. Expx should really be called Xn, because it always stores the current X, or in other words the n-th X. This is obvious if you look at expIncr = 1 / attackTime.
        Rewriting this more logically, it means expIncr = 1 / sampleCountForAttackTime. So exprIncr is the increment for x for each sample, to go from x = 0, to x=1. So yes, the range of expX is from 0 to 1; 0 = attack segment start, 1=attack segment end.

        Main part
        Now why is range = peakAmp / totalSamples ?
        The answer is that it's a mistake, it should simply be range == peakAmp. I think this was an oversight due to scaling everything based on the amount of samples for a segment, or in this case the entire signal. Since the book really shows pseudocode this probably wasn't noticed.

        To see why this is true, imagine the following case. You have a signal with a peak amplitude of say 0.5, but pow(expX, expB) can range from 0.0 to 1.0. So it has to be scaled down to not exceed the peak amplitude of the original signal. That's why volume = peakAmp * pow(expX, expB); is the correct term.

        I am not entirely sure where this mistake comes from, maybe from the linear segment envelope on page 44, where a = (yend- ystart)/n.
        In this case it's correct, because n is the count of segment samples (not the entire signal -- which isn't clear at first either, but looking at the code and using logic it has to be..., yes a few things could be more clear) and a would in this case be the slope of the line.

        When you look at page 50, the code actually confirmes my understanding, because there volume == peakAmp when n == attackTime. Translated to this expression volume = peakAmp * pow(expX, expB) it means that expX == 1 (when n == attackTime), and thus pow(1,expB) == 1 => volume == peakAmp.

        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 🙂