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

  1. //AR.c
  2. //gcc MKAiff.c AR.c -o AR
  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.5, release=1.0;
  19. attack *= SAMPLE_RATE;
  20. release *= SAMPLE_RATE;
  21. int currentFrame;
  22. float audioBuffer[numSamples];
  23. double frequency = 440, angle = 0, currentVolume;
  24. int i;
  25. for(i=0; i<numSamples; i+=NUM_CHANNELS)
  26. {
  27. currentFrame = i/NUM_CHANNELS;
  28. if(currentFrame<attack)
  29. currentVolume = currentFrame/(float)attack;
  30. else if(currentFrame < attack+release)
  31. currentVolume = 1-(currentFrame-attack)/(float)release;
  32. else
  33. currentVolume = 0;
  34. audioBuffer[i] = sin(angle) * currentVolume;
  35. angle += frequency * TWO_PI_OVER_SAMPLE_RATE;
  36. }
  37. aiffAppendFloatingPointSamples(aiff, audioBuffer, numSamples, aiffFloatSampleType);
  38. aiffSaveWithFilename(aiff, "AR.aif");
  39. aiffDestroy(aiff);
  40. return 0;
  41. }

Output:

Explanation of the Concepts

Most natural sounds, at the very least, get loud gradually and die away gradually. For instance, a guitar string, when plucked, very quickly reaches its reaches its maximum volume (quickly, but not instantaneously) and then slowly dies away as the friction between individual molecules within the string dissipates some of the string's energy as heat. Of course this is a drastic oversimplification (it is easy to hear, for instance, that a guitar string swells while it should be dying away), but it is a useful oversimplification, as it will allow us to create a basic shape for individual sounds. The period of time while the sound is increasing is called the "Attack" period, and the period while the sound is dying away is called the "Release" period. Thus the name "AR (attack/release) envelope". Here is a plot of this envelope's value as a function of time:

Envelope

The envelope's value will always start at 0, move gradually to 1, and end on 0 again. Often, for instance when using the envelope to control pitch, these values will not suffice. The best way to change these values is by scaling the envelope, as discussed in the 'Programing Patterns' section of the 'C-tutorial' earlier in this book. Remember that scaling is accomplished by multiplying the entire envelope by the desired range (maximum-minimum) and then adding the minimum value. For instance, if you wanted an AR envelope to start and end on 440 and peak on 550, the entire envelope would be multiplied by 110 (550-440) and then have 440 added to it. For an envelope that starts and ends on different values, you must use a breakpoint envelope, discussed in a subsequent chapter.

I should point out that some analog synthesizers, like the ARP 2600, have an ASR (attack/sustain/release) envelope that is labeled 'AR envelope'. I will not discuss this envelope's operation explicitly, because its operation is nearly identical to an ADSR envelope, to be discussed in the next chapter.

Explanation of the Code

The attack period and release period are declared in seconds on line 23. on lines 25 and 26 they are converted from seconds to the number of frames in that many seconds by multiplying them by the sample rate. On line 30 a variable called 'currentVolume' is declared to store the value of the envelope during any given frame.

Lines 35-41 calculate the value of the envelope for each frame. The current frame is calculated on line 35 with the familiar expression:

i/NUM_CHANNELS

Line 37 checks to see if the number of elapsed frames is less than the the number of frames in the attack period of the envelope. If it is, the current value of the envelope is set to

currentFrame/attack

which will increase linearly from 0 to 1 over the attack period. If an exponential curve is desired, this line may be modified thus:

currentVolume = pow(currentFrame/attack, exponent);

where the variable 'exponent' will need to have been previouslty declared and assigned a value, like in the chapter on exponential envelopes.

If the current frame has surpassed the number of frames in the attack period, then line 39 checks to see whether the current frame has also surpassed the entire envelope. If not, then it must be in the release phase, and line 39 will calculate the envelope's value. This value will move linearly from 1 to 0 over the course of the release period. Again, this could be made exponential thus:

currentVolume = pow(1-(currentFrame-attack)/release, exponent);

If the current frame is beyond the entire length of the envelope, then lines 40-41 set the envelope's value to 0.

If this envelope needed to be scaled, line 46 would be the ideal place to do it. This could be accomplished like so:

currentVolume = currentVolume*(max-min) + min;

where min and max are the minimum and maximum desired values of the envelope, and have been previously declared and assigned values. Notice that these values will be correct, even when using exponential envelope segments.