Synth DIY : Software for Generating ADSR Envelopes

I’ve spent a fair amount of time detailing the hardware that Rockit uses for it’s Voltage-Controlled Amplifier. It’s a transconductance amplifier-based design and you can read about it here. I haven’t, however, discussed the software behind implementing the envelope generator. It ends up being more useful than just controlling amplitude. The same envelope generator code works for the filter envelope too. I didn’t end up using exactly the same code, but it could be made entirely modular, taking some basic inputs like timing and parameter data and returning an appropriate value for an envelope. Let’s just focus on how to code an ADSR envelope for now. If you’d like to get the source code for Rockit, please download it from Rockit’s Sourceforge Page.

Fundamental Characteristics

Before we discuss the software in detail, it’s important to note some distinctions. Rockit is a hardware synthesizer and it’s ADSR envelope is really in hardware, it’s just being controlled digitally. As such, the software is limited by the hardware. We cannot have multiple amplitude envelopes running at the same time for multiple notes because we only have one amplitude generator. This isn’t really a problem, but we have to work with it. In a soft synth, you may be able to calculate multiple software envelopes and have them all running at the same time on your polyphonic soft synth. That’s just not possible in this case. With this is in mind, there are some changes you could make to make a soft synth using this same code, but I am making some distinctions and special behavior to interact with hardware. For instance, one of these behaviors comes from the fact that the zero gain value for the control input on a transconductance amplifier is not in fact 0V, but two diode drops above 0V, or 1.2-1.4V. In the code, we have to make sure to stay above this voltage with our control signal, so our envelope generator can’t start at 0, but at a minimum value.

Variable Declarations

Fundamentally, an ADSR is time-varying amplitude scaling. And to scale amplitude, all we have to do is perform multiplication. If something has a value of 1, and we want it to be 5 times bigger, we just multiply it by 5. Et voila. In order to do that over time, we have to have some time interval over which to perform repeated multiplications to make something grow and increase, or decrease. So, we’ll have to declare some variables to store information like time passing.

static unsigned char uc_adsr_timer = 0;
static unsigned char uc_state = 0;

The uc_adsr_timer variable will allow us to keep track of time. The uc_state variable will keep track of which state we are in: Attack, Decay, Sustain, or Release. We need this to be a permanent variable because this ADSR envelope generator software is run repeatedly, but not continuously. In between running the envelope generator, the software must perform all of its other duties, of which  there are many.

We’ll also need to keep track of our multiplier, the value which we use in the multiplication to scale our signal. The multiplier value is contained in a global, (accessible everywhere), array of values called global_setting. This structure contains all the values of needed to run Rockit. We access members of the structure by using a pointer, which is passed to many functions, called p_global_setting. I add the p_ suffix so that any time I encounter it in the software, I know that this is a pointer, a memory reference to the location where the structure of global values is stored. We access the multiplier value by using the structure access operator “->”. The multiplier variable inside the structure is called uc_adsr_multiplier. So anytime we want to get that value or change it, we use the variable name

p_global_setting->uc_adsr_multiplier.

We access other stored parameters like the length of each state of the envelope similarly and you’ll see it throughout this code.

State Machine

An ADSR has four stages, which conveniently in software match up with four states. This makes the code a perfect candidate for a state machine. The concept of a state machine is a bit much to explain here, but, in brief, it is a software algorithm with different states through which the software progresses based on inputs and outputs. We declared the uc_state variable above to keep track of which state we are in. We can technically set this variable to any value, but just any value won’t do because there are only a certain number of states. To limit the number of states and to keep things clear, we use an enumerated set of constants, otherwise known as an enumerated type. If you look in the associated header file for this C code, you’ll find this:

enum
{
ATTACK = 0,
DECAY,
SUSTAIN,
RELEASE
};

This snippet creates an enumerated list for the different states. This way we don’t have to keep track of which number relates to which state. We can just use its name instead and the compiler will know that when we say ATTACK, we mean 0. This declaration makes ATTACK = 0, DECAY = 1, SUSTAIN = 2, and RELEASE = 3. Another convention to note is that these names are all in caps to let us know that they are constants, not variables.

Let’s Begin the Envelope

The state machine ends up pretty simple in this case. We start with the Attack phase, which we begin when a key is pressed.

if(g_uc_adsr_midi_sync_flag == 1)
{
uc_state = ATTACK;
g_uc_adsr_midi_sync_flag = 0;

This statment kicks it off. The ADSR envelope code checks a global flag every time it runs. If the MIDI code has received a new note, then it will set this flag. The ADSR code then sets its uc_state variable to ATTACK and clears the flag so the next time that it comes around, it doesn’t start over again.

The next part of the code incorporates the velocity of the note played to set a maximum for the envelope. This is how we get quieter and louder notes based on velocity. There’s a special case here for the drone and loop mode, which does not have ADSR enveloping. So, we make sure that neither of these modes is active. If they are active, then the envelope is simply set at it’s maximum value of 255. If it’s not active, we access the current note velocity and the minimum envelope value to determine the maximum of the envelope. The velocity is going to be a number between 0 and 127, but we scale it to 8 bits and at the same time account for the minimum value. When you do the scaling and accounting it looks like this:

if(!g_uc_drone_flag && !g_uc_loop_flag)
{
un_velocity_calc = p_global_setting->uc_note_velocity << 1;
un_velocity_calc *= (NUMBER_OF_ADSR_STEPS – ADSR_MIN_VALUE);
uc_velocity = un_velocity_calc >> 8;
uc_velocity += ADSR_MIN_VALUE;
}
else
{
uc_velocity = 255;
}
If the key is released at any point, we move directly to the Release phase. The key press flag is cleared by the MIDI routine when it receives a Note Off command on the MIDI input.

if(0 == g_uc_key_press_flag)
{
uc_state = RELEASE;
}
else
{
g_uc_note_on_flag = 1;//turn on the output
}

The ADSR envelope is also in control of turning on the output because we turn off the output only after the Release phase has ended. It also turns on the output when the global flag variable g_uc_key_press_flag is set by the MIDI code.

Timing

The length of each phase of the ADSR envelope is determined by the setting of a pot in Rockit’s ADSR section. The position of these pots is read by the microcontroller as a voltage which scales linearly from 0 to 255. Higher numbers mean longer times for each segment, or in the case of Sustain, a higher level. The reading is translated into a length of time via a timer. The timer is stored in the variable declared earlier, uc_adsr_timer. This variable is decremented each time the ADSR routine is run. The length of time in between is set externally by timers running in the main routine which determine how often each of Rockit’s subroutines runs. The ADSR state machine is only run when this timer reaches 0. So, we can get longer and shorter stages by initializing this timer with higher or lower numbers. For instance, if the Attack pot is set to a position which correlates to a microcontroller reading of 5, the timer will get initialized with the value 5. The next time the ADSR routine is run, the timer will get decremented to 4. The next time to 3, the next time to 2… until it reaches 0, at which point, we run the state machine. This is all encapsulated in the statement:

if(uc_adsr_timer > 0)
{
uc_adsr_timer–;

}

else{

Run the state machine when the timer reaches 0.

Back to the State Machine

The state machine starts with a case statement:

switch(uc_state)
{

This lets us go to the correct state based on the variable uc_state. The first state is, of course, ATTACK:

case ATTACK:

We start the Attack phase when we get a key press, but how do we know when it’s over? When we hit the maximum, which we said before is determined by the velocity and our little scaling calculation of it. Determining whether or not to continue with the Attack state is the first thing we do when we begin the state:

if(p_global_setting->uc_adsr_multiplier < uc_velocity)
{

Another Hardware Aside

The ADSR envelope steps through the multiplier values from the minimum, set by the realities of the hardware, to the maximum. The highest maximum is 255, which is the maximum value for an 8 bit variable. We saw already that the moment to moment maximum is determined by the velocity though. We can step through the multiplier values one at a time. For example, start at 0, then 1, then 2, then 3… The problem I found is that it’s not always possible then to have a large range of speeds for the segments of the ADSR. So, I allow for stepping by multiples. You could step by twos, threes, fours, or whatever if you want a really fast envelope. The only limitation is that you might hear the steps if you move to quickly.

I segmented the speed of the phases in order to get faster and slower possibilities than if only one step size was allowed. I did this because I wanted to get really fast attacks, but also allow for really slow attacks. If you do it linearly and you only have 8 bits, it’s difficult to have both without this type of segmentation. The length of each state is derived from the position of a pot, which may have any value from 0 to 255. We can then segment this range into smaller segments, some of which have faster incrementing than others.

In practice, it looks like this:
if(p_global_setting->auc_synth_params[ADSR_ATTACK] < 192)
{
uc_adsr_timer = p_global_setting->auc_synth_params[ADSR_ATTACK]>>2;

if(p_global_setting->uc_adsr_multiplier < 253)
{
p_global_setting->uc_adsr_multiplier += 2;
}
else
{
p_global_setting->uc_adsr_multiplier = NUMBER_OF_ADSR_STEPS;
}
}
else
{
uc_adsr_timer = p_global_setting->auc_synth_params[ADSR_ATTACK]>>1;

if(p_global_setting->uc_adsr_multiplier < 254)
{
p_global_setting->uc_adsr_multiplier += 1;
}
else
{
p_global_setting->uc_adsr_multiplier = NUMBER_OF_ADSR_STEPS;
}}
The first if statement segments the Attack into two regions, one for pot settings below 192 and one for above. The settings below 192 will increment two multiplier values every time they are incremented. Setting above 192 will increment only one multiplier value each time they are incremented, making them half as fast as the lower, faster segment. The other secondary if statements are there to guarantee that we don’t go over the maximum during our incrementing. If you go past 255 with an 8 bit variable, it wraps back around 0. The value 256 in 8 bit world is better know as 0. You may say that the secondary if statement, where we are incrementing by 1, is unnecessary. But I leave it there to allow for future experimentation. If you change it to 2 or more, you’ll need some statement here to protect the code from overrun. Always be careful that you don’t exceed the range for the multiplier if you start monkeying with the speed of the incrementing.

Decay

As we noted above, the Attack phase ends when the multiplier value reaches the maximum that we calculated above. When this happens, the else of the if statement is executed.

if(p_global_setting->uc_adsr_multiplier < uc_velocity)

{….

}else{

uc_state = DECAY;

We set the state to Decay for the next time we run the state machine. Then, we have to perform the next calculations to determine the limits for the Decay phase. How do we know when the Decay phase is over? It depends on the sustain level. The Decay phase decrements the multiplier in the same way that the Attack phase increments the multiplier. The difference is that the Decay phase ends when the multiplier reaches the Sustain level. Once again, we have to take into account the hardware and it’s non-zero zero level. We take the Sustain pot setting and scale it so that it ranges appropriately.

un_sustain_calc = uc_velocity * p_global_setting->auc_synth_params[ADSR_SUSTAIN];

un_sustain_calc >>= 8;

un_sustain_calc *= (NUMBER_OF_ADSR_STEPS – SUSTAIN_MIN_VALUE);

un_sustain_calc >>= 8;

uc_sustain_level = un_sustain_calc + SUSTAIN_MIN_VALUE;

Now, knowing how the Decay phase ends, we can begin the decrementing, each time checking to make sure that we haven’t encountered the end.

if(p_global_setting->uc_adsr_multiplier > uc_sustain_level)

{

The code for decrementing is exactly the same as the Attack incrementing, just headed the other direction. In this case though, there are more segments to allow for more variation in the speed of the Decay phase.

if(uc_adsr_timer < 48){

if(p_global_setting->uc_adsr_multiplier > 4){

p_global_setting->uc_adsr_multiplier -= 4;

}else{

p_global_setting->uc_adsr_multiplier = 0;

}}else if(uc_adsr_timer < 96){

if(p_global_setting->uc_adsr_multiplier > 2){

p_global_setting->uc_adsr_multiplier -= 2;

}else{

p_global_setting->uc_adsr_multiplier = 0;

}}else{

p_global_setting->uc_adsr_multiplier–;

}

Sustain

There’s nothing particularly interesting about the Sustain phase. When you get there, you stay there until the key is released at which point you go to the Release phase. The Sustain phase is just an empty piece of code with a break; statement.

Release

The release phase is almost exactly the same as the Decay phase. The only difference is that the minimum is a fixed value, given by the hardware. I won’t copy the code here because there’s nothing new to see with it.

As I mentioned before, if a new note is pressed while the envelope is in the Release phase, the envelope generator moves back into the Attack phase and begins incrementing from present multiplier value, not the minimum value.

That’s it! It seems more complicated than it is, but it’s really a pretty simple piece of code. As always, if you have any questions, send me a note.

Posted in Programming, Rockit and tagged , , , , , , , .

One Comment

  1. This is great! I’m trying to figure out how to code a very basic synth on an Arduino uno at the moment. Keep ’em coming! 🙂

Leave a Reply