www.ethanwiner.com - since 1997 |
Coding an Equalizer
This article expands the section on page 20 of my book The Audio Expert that describes briefly how digital equalizers work. As explained in the book, all equalizers are based on filters of various types. Computer code that implements a filter is called Digital Signal Processing, or DSP for short. Most digital filters emulate equivalent analog filters, and the common language for all filters is mathematics. Therefore, several trigonometry formulas are shown below, and there's no escaping this! But the basic operation of the computer code that implements an equalizer is not too difficult to follow, even if you don't understand the formulas. To keep this example as brief as possible, the code below implements a simple high-pass filter having one pole (6 dB per octave). Formulas to implement other filter types including those used in parametric equalizers are shown on the Cookbook Formulae web page by Robert Bristow-Johnson.
The computer code below is written in the C programming language, which is typical for audio plug-ins. In C, a semicolon is used to mark the end of each command. Note that text bounded by the markers /* and */ are comments, meaning it's not part of the code but rather explains what the code does. For a simple program like this filter, any competent C programmer can easily understand how the code works. But comments are necessary for more complex programs, especially if the code might be supported or modified later by other programmers.
The initial part of the program defines several named values that are used by the filter portion that follows. If you look at the other filter examples on the web page linked above, you'll see they all use a group of values labeled a0 through b2. The specific formulas used depend on the filter type being implemented.
/* These floating point values are used by the filter
code below */
float Fs = 44100; /* sample rate in samples per second */
float Pi = 3.141592; /* the value of Pi */
/* These floating point values implement the specific
filter type */
float f0 = 100;
/* cut-off (or center) frequency in Hz */
float Q = 1.5;
/* filter Q */
float w0 = 2 * Pi * f0 / Fs;
float alpha = sin(w0) / (2 * Q);
float a0 = 1 + alpha;
float a1 = -2 * cos(w0);
float a2 = 1 - alpha;
float b0 = (1 + cos(w0)) / 2;
float b1 = -(1 + cos(w0));
float b2 = (1 + cos(w0)) / 2;
This next section defines two floating point arrays that are used as memory buffers:
/* The Buffer[] array holds the incoming samples,
PrevSample[] holds intermediate results */
float Buffer[1024]; /* this array
holds 1024 elements numbered 0 through 1023 */
float PrevSample[3]; /* this array holds 3
elements numbered 0 through 2 */
An array is a collection of values that share a common name. Early in the program, Pi is assigned a value of 3.141592. Once this is defined, the code can use the shorter and more meaningful name Pi, rather than having to type the same long number repeatedly. Using named values also reduces the chance of introducing "bugs" in the program due to typing errors. Unlike single values such as Pi, an array defines a collection of related values that are stored in adjacent memory locations. Arrays are also named by the programmer, which is clearer and more reliable than referring to memory locations by their numeric addresses. So declaring the array Buffer[1024] sets aside enough memory to hold 1,024 audio samples. Each sample is 32-bits, or four bytes, so the array occupies a total of 4,096 bytes of memory. As explained in my book's Computers chapter, 1 Kilobyte comprises 1,024 bytes, rather than 1,000 bytes, because computer memory is organized in powers of 2. In this case 2^10 equals 1,024. This is why the available buffer size options in a DAW program are always a power of 2, such as 128 bytes, 256 bytes, 512 bytes, and so forth.
The first array named Buffer holds the 32-bit floating point PCM audio samples as they're being processed. The host audio program deposits 1,024 sequential samples into this area of memory, then invokes the filter code. At a sample rate of 44.1 KHz, this represents about 23 milliseconds of audio. After the filter has processed all of the samples in the buffer, the host program retrieves them and deposits the next group of 1,024 samples into the same buffer.
The second array, PrevSample, holds the previous three samples that were already processed. DSP filters process the audio samples using a "rolling" method, where math applied to the current sample depends in part on the value of previous samples before the EQ math was applied to them. Since the code overwrites each sample in the buffer one at a time as it works, the PrevSample array is needed to retain the original contents of the previous three samples. The previous samples are shuffled through the PrevSample array as each sample in the main buffer is processed, as shown in Figure 1.
Figure 1: As each sample in the main Buffer array is processed, the previous samples are shuffled through the PrevSample array so the last three are always available. Then the lowest array element number 0 is replenished from the current sample from the main Buffer array. |
Finally we get to the actual EQ code, which runs in what is called a loop. There are several types of loops, and the "while" loop shown here is very common. In this case, "while the current value of I is less than the constant value 1024 defined elsewhere as N, execute the block of code between the open and closed curly brackets { } repeatedly:"
while (I < N) { /* this
is the beginning of the code that loops 1,024 times */
...
I = I + 1; /* increment the counter
I by adding 1 */
}
/* this is the end of the code loop */
The first block of code within the loop shuffles the contents of the PrevSample array as explained earlier to retain the last three values:
PrevSample[2] = PrevSample[1]; /*
Slide the samples over one position */
PrevSample[1] = PrevSample[0];
PrevSample[0] = Buffer[I];
The next block performs a series of math calculations and assigns the result to the current sample number "I" in the main buffer. This block is a single command, but there are many intermediate calculations so it's split across separate lines to be easier to read. In plain English this line of code says, "assign to Buffer element 'I' the result of b0 divided by a0 times Previous Sample number 0. Then add to that the result of b1 divided by a0 times Previous Sample number 1. Then add to that the result of ..." and so forth:
Buffer[I] = ( b0 / a0 * PrevSample[0]) +
(b1 / a0 * PrevSample[1]) +
(b2 / a0 * PrevSample[2]) -
(a1 / a0 * Buffer[I-1]) -
(a2 / a0 * Buffer[I-2]);
The last line of code increments (adds 1 to) the counter "I" so it refers to the next element in the main buffer array:
I = I + 1;
The loop continues until the counter "I" reaches a value of 1024, at which point the program returns back to the calling host to supply the next batch of 1,024 samples.
Note that this program has been simplified to be easier to follow. All computer programs include "header" statements that let the program access services in the operating system such as allocating memory or reading a file, among other details. Also, code like this high-pass filter would normally be set up as a subroutine, which is a self-contained block of code called by name by the host program. Another simplification is omitting code to check if the main buffer contains less than the full 1,024 samples, which is likely the last time it runs. Additional code is also needed to avoid an error when the code first runs. When the while counter "I" has a value less than 1 or 2, the statements Buffer[I-1] and Buffer[I-2] reference elements in the array that don't exist.
Finally, here's the entire program in context:
/* High Pass Filter based on RBJ Cookbook linked above
*/
/* Analog Transfer Function for this filter: H(s) = s^2 / (s^2 + s/Q + 1) */
/* These floating point values are used by the filter
code below */
float Fs = 44100; /* sample rate in samples per second */
float Pi = 3.141592; /* the value of Pi */
/* These floating point values implement the specific
filter type */
float f0 = 100;
/* cut-off (or center) frequency in Hz */
float Q = 1.5;
/* filter Q */
float w0 = 2 * Pi * f0 / Fs;
float alpha = sin(w0) / (2 * Q);
float a0 = 1 + alpha;
float a1 = -2 * cos(w0);
float a2 = 1 - alpha;
float b0 = (1 + cos(w0)) / 2;
float b1 = -(1 + cos(w0));
float b2 = (1 + cos(w0)) / 2;
/* The Buffer[] array holds the incoming samples,
PrevSample[] holds intermediate results */
float Buffer[1024]; /* this array
holds 1024 elements numbered 0 through 1023 */
float PrevSample[3]; /* this array holds 3
elements numbered 0 through 2 */
/* These integer (whole number) variables are used
below to process 1,024 iterations at a time*/
int I = 0;
int N = 1024;
/* The code below executes repeatedly as long as the
value of I is less than N */
/* Since I was initialized to 0 above, and N was set to 1024, this code executes 1,024
times */
while (I < N) {
/*
this is the beginning of the code that loops 1,024 times */
PrevSample[2] =
PrevSample[1]; /* Slide the samples over one position */
PrevSample[1] = PrevSample[0];
PrevSample[0] = Buffer[I];
Buffer[I] = ( b0 / a0 * PrevSample[0]) +
(b1 / a0 * PrevSample[1]) +
(b2 / a0 * PrevSample[2]) -
(a1 / a0 * Buffer[I - 1]) -
(a2 / a0 * Buffer[I - 2]);
I = I + 1; /* increment
the counter I by adding 1 */
} /*
this is the end of the code loop */
Special thanks to Grekim Jennings for providing the C code that made this tutorial possible.
Ethan Winer has been an audio pro and skeptic for most of his adult life. He now heads up RealTraps, where he designs acoustic treatment products for recording studios and home listening rooms.
Entire contents of this web site Copyright © 1997- by Ethan Winer. All rights reserved.