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

  1. //Breakpoint.c.c
  2. //gcc MKAiff.c Breakpoint.c -o Breakpoint
  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.31
  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 values[] = {100, 20, 500, 2000, 0, 1400, 400};
  18. double durations[] = {0.1, 0.1, 0.01, 1.3, 0.5, 0.3};
  19. float numSegments = sizeof(durations) / sizeof(*durations);
  20. int i, j;
  21. int envelopeDuration = 0;
  22. for(i=0; i<numSegments; i++)
  23. {
  24. durations[i] *= SAMPLE_RATE;
  25. envelopeDuration += durations[i];
  26. }
  27. float envelope[envelopeDuration];
  28. float* envelopePointer = envelope;
  29. for(i=0; i<numSegments; i++)
  30. {
  31. for(j=0; j<durations[i]; j++)
  32. {
  33. *envelopePointer = j/durations[i];
  34. *envelopePointer *= values[i+1] - values[i];
  35. *envelopePointer += values[i];
  36. envelopePointer++;
  37. }
  38. }
  39. float audioBuffer[numSamples];
  40. double frequency , angle = 0;
  41. for(i=0; i<numSamples; i+=NUM_CHANNELS)
  42. {
  43. if(i/NUM_CHANNELS < envelopeDuration) frequency = envelope[i/NUM_CHANNELS];
  44. audioBuffer[i] = sin(angle);
  45. angle += frequency * TWO_PI_OVER_SAMPLE_RATE;
  46. }
  47. aiffAppendFloatingPointSamples(aiff, audioBuffer, numSamples, aiffFloatSampleType);
  48. aiffSaveWithFilename(aiff, "Breakpoint.aif");
  49. aiffDestroy(aiff);
  50. return 0;
  51. }

Output:

Explanation of the Concepts

A breakpoint envelope is essentially a generalization of all of the other types of envelopes, except that, unlike ADSR envelopes, its duration is always fixed. A breakpoint envelope can have any arbitrary number of segments, and those segments may be of any arbitrary duration, and start and end on any arbitrary value. The coordinates where the individual segments meet are called the 'Breakpoints'. Here is the plot the envelope generated by the above program, where the envelope's value is plotted as a function of time:

Envelope

Although the envelope is called a "Breakpoint" envelope, the envelope is not usually specified in terms of the x and y coordinates of its breakpoints. It is, rather, defined as arrays of values and durations, where a value is the y coordinate of the breakpoint, and the duration is the distance between breakpoints, of specifically the x coordinate of a breakpoint minus the x coordinate of the previous breakpoint. It is important to notice that this means that there will always be one more value than there are durations.

Breakpoint envelopes are very useful for controlling the amplitude of a sound, because, given enough breakpoints, they can model any complex real-world behavior. Here the envelope is used to control pitch, just for variety.

Explanation of the Code

Here, as in the previous chapter, we will take the approach of calculating each frame of the envelope and saving it in an array before proceeding to apply it to anything.

The values and durations that define the envelope are declared on lines 22 and 23. Notice that there is one more value than duration. This is important because if there are too few values then a segmentation fault is likely to happen as the computer attempts to read beyond the bounds of that array. Because there could be any arbitrary number of segments, the computer must be responsible for figuring out how many there are, which it does indeed do on line 25.

Lines 27-33 make the familiar conversion from duration in seconds to duration in frames by multiplying each duration in the array by the sample rate. Concurrently, the total duration in frames of the envelope is calculated by summing the durations of the individual segments.

Line 35 declares an array to hold the envelope, with enough space for one float per frame. Line 36 declares a pointer to the first member of the envelope. This is because we will be iterating over the envelope's array in an unusual fashion, so it will be easier to find the current value in the array by incrementing a pointer than to try and calculate what the index for the array should be to look up that value.

Lines 38-47 fill the envelope's array with its values. It starts on line 38 by setting up a loop that iterates over each segment of the array with the variable i. This means that the expression

values[i]

refers to the initial value for the current segment,

values[i+1]

refers to the final value of the current segment, and

durations[i]

refers to the duration of the current segment.

Line 40 then sets up another loop that iterates over each frame in the current segment. The value of the current frame is accessed by dereferencing the pointer to it. Line 42 sets the current frame to a value that will start at 0 and progress to 1 over the duration of the segment. Lines 43 and 44 scale that value to the actual starting and ending values of that segment. (Scaling is discussed in the 'Programming Patterns' section of the 'C Tutorial' of this book). Line 45 increments the pointer so that it points to the next frame in the envelope's buffer, so that is ready for the next iteration of the loop.

Once the envelope has been calculated the rest is straightforward with respect to the previous chapters. Line 54 checks to see whether the current frame of the audio buffer is beyond the bounds of the envelope, and if it is not, it sets the frequency of the sine-wave to the value of the current frame of the envelope.