An Audio Synthesis Textbook For Musicians, Digital Artists and Programmers by Mike Krzyzaniak

  1. //ADSR.c
  2. //gcc MKAiff.c ADSR.c -o ADSR
  3. #include "MKAiff.h"
  4. #include <math.h>
  5. #define SAMPLE_RATE 44100
  6. #define NUM_CHANNELS 1
  7. #define BITS_PER_SAMPLE 16
  8. #define NUM_SECONDS 2
  9. const int numFrames = NUM_SECONDS * SAMPLE_RATE;
  10. const int numSamples = NUM_SECONDS * SAMPLE_RATE * NUM_CHANNELS;
  11. #define PI 3.141592653589793
  12. const double TWO_PI_OVER_SAMPLE_RATE = 2 * PI / SAMPLE_RATE;
  13. int main()
  14. {
  15. MKAiff* aiff = aiffWithDurationInSeconds(NUM_CHANNELS, SAMPLE_RATE, BITS_PER_SAMPLE, NUM_SECONDS);
  16. if(aiff == NULL) return 0;
  17. float envelope[numFrames];
  18. float attack=0.01, decay=0.1, sustain=0.4, release=1.0;
  19. attack *= SAMPLE_RATE;
  20. decay *= SAMPLE_RATE;
  21. release *= SAMPLE_RATE;
  22. int i;
  23. for(i=0; i<numFrames; i++)
  24. {
  25. if(i<=attack)
  26. envelope[i] = (float)i/(float)attack;
  27. else if(i<=(attack+decay))
  28. envelope[i] = 1-(1-sustain)*((double)(i-attack)/(double)decay);
  29. else if(i<=(numFrames-release))
  30. envelope[i] = sustain;
  31. else
  32. envelope[i] = sustain*((double)((numFrames-i)/(double)release));
  33. }
  34. float audioBuffer[numSamples];
  35. double frequency = 440, angle = 0;
  36. for(i=0; i<numSamples; i+=NUM_CHANNELS)
  37. {
  38. audioBuffer[i] = sin(angle) * envelope[i/NUM_CHANNELS];
  39. angle += frequency * TWO_PI_OVER_SAMPLE_RATE;
  40. }
  41. aiffAppendFloatingPointSamples(aiff, audioBuffer, numSamples, aiffFloatSampleType);
  42. aiffSaveWithFilename(aiff, "ADSR.aif");
  43. aiffDestroy(aiff);
  44. return 0;
  45. }

Output:

Explanation of the Concepts

Although the AR envelope is good for certain applications, a generally more believable model for a wide variety of sounds is the ADSR envelope. ADSR has an attack period (A) during which time the envelope increases in value from 0 to 1. This is followed by a decay period (D) during which time the envelope's value decreases from 1 to the sustain level. During the next phase of the envelope, the sustain (S) level is held. Finally, there is a release period during which the envelope's value decreases from the sustain level to 0. This sequence of events is plotted here for clarity (the envelope's value is plotted as a function of time)

Envelope

It is important to note that three of the values, Attack, Decay and Release, are measured in time, and the other, Sustain, is a level, measured in amplitude.

This is the basic shape of an ADSR envelope. Generally, however, the envelope is triggered by real-time events, like someone pressing a key on a keyboard, so that the duration of the envelope is not known in advance. In such an arrangement, the sustain level would be held indefinitely while the user's finger is holding the key down, and the act of physically releasing the key would trigger the release phase of the envelope. The signal that causes the onset of the envelope is called 'Trigger', and the signal that causes the release period to begin is called 'Gate'. How software deals with Gate and Trigger signals will be discussed in further detail in the 'Real Time Synthesis' chapter, later in this book. For now, we will concern ourselves with the problem of just programing the basic shape of the envelope.

Explanation of the Code

For any sufficiently complex envelope, perhaps like this one, it will generally be more efficient to calculate the entire envelope in advance and store each of its values in an array. This is especially true if the envelope will be used over and over again. This approach will not generally work for real-time applications where the duration is not known in advance, but the sequence of operations will be basically the same in any event, so I will use that approach here without feeling any guilt about it.

Here, we will make an envelope that is the same length as the file. To do so, we will need to first know the number of frames in the file. This is calculated on line 12, and stored as a global constant. Line 22 allocates an array of floats to hold each value for the envelope. Notice that the array has enough space for 1 value per frame of audio. the attack, decay and release periods are declared in seconds on line 23, where the sustain level is also declared. Lines 25-27 convert each period from seconds to number of frames. Lines 29-40 fill the envelope's array with the envelope's values. This is accomplished the same way it was for the AR envelope, by checking to see if the current frame exceeds the number of frames in the attack period, the number of frames in the attack+decay period, or if it within the release-period's number of frames from the end of the file, and calculating the value accordingly. Notice that each segment of the envelope is linear, but each segment could be made exponential by adding the following after line 39:

envelope[i] = exp(envelope[i], exponent);

where the variable 'exponent' had been previously declared and assigned a value. Furthermore, the entire envelope could also be scaled on the subsequent line, as was explained in the previous chapter.

On line 47, the current audio sample (belonging to a sine-wave) is multiplied by the current frame (i/NUM_CHANNELS) of the envelope.