Thursday, August 23, 2012

Basic Audio EQs

In my last post, I looked at why it's usually better to do EQ (or filtering) in the time domain than the frequency domain as far as audio is concerned, but I didn't spend much time explaining how you might implement a time-domain EQ. That's what I'm going to do now.

The theory behind time-domain filters could fill a book. Instead of trying to cram you full of theory we'll just skip ahead to what you need to know to do it. I'll assume you already have some idea of what a filter is.

Audio EQ Cookbook

The Audio EQ Cookbook by Robert Bristow-Johnson is a great, albeit very terse, description of how to build basic audio EQs. These EQs can be described as second order digital filters, sometimes called "biquads"because the equation that describes them contains two quadratics. In audio, we sometimes use other kinds of filters, but second order filters are a real workhorse. First order filters don't do much: they generally just allow us to adjust the overall balance of high and low frequencies. This can be useful in "tone control" circuits, like you might find on some stereos and guitars, but not much else. Second order filters give us more control -- we can "dial in" a specific frequency, or increase or decrease frequencies above and below a certain threshold, with a fair degree of accuracy, for example. If we need even more control than a second order filter offers, we can often simply take several second order filters and place them in series to simulate the effect of a single higher order filter.

Notice I said series, though. Don't try putting these filters in parallel, because they not only alter the frequency response, but also the phase response, so when you put them in parallel you might get unexpected results. For example, if you take a so-called all-pass filter and put it in parallel with no filter, the result will not be a flat frequency response, even though you've combined the output of two signals that have the same frequency response as the original signal.

Using the Audio EQ Cookbook, we can design a peaking, high-pass, low-pass, band-pass, notch (or band-stop), or shelving filter. These are the basic filters used in audio. We can even design that crazy all-pass filter I mentioned which actually does come in handy if you are building a phaser. (It has other uses, too, but that's for another post.)

Bell Filter

Let's design a "bell", or "peaking" filter using RBJ's cookbook. Most other filters in the cookbook are either similar to the bell or simpler, so once you understand the bell, you're golden. To start with, you will need to know the sample rate of the audio going into and coming out of your filter, and the center frequency of your filter. The center frequency, in the case of the bell filter, is the frequency that "most affected" by your filter. You will also want to define the width of the filter, which can be done in a number of ways usually with some variation on "Q" or "quality factor" and "bandwidth". RBJ's filters define bandwidth in octaves, and you want to be careful that you don't extend the top of the bandwidth above the Niquist frequency (or 1/2 the sample rate), or your filter won't work. We also need to know how much of our center frequency to add in dB (if we want to remove, we just use a negative value, and for no change, we set that to 0).

Fs = Sample Rate
F0 = Center Frequency (always less than Fs/2)
BW = Bandwidth in octaves
g = gain in dB

Great! Now we are ready to begin our calculations. First, RJB suggests calculating some intermediate values:

A = 10^(g/40)
w0 = 2*pi*f0/Fs c = cos(w0) s = sin(w0) alpha = s*sinh( ln(2)/2 * BW * w0/s )

This is a great chance to use that hyperbolic sin button on your scientific calculator that, until now, has only been collecting dust. Now that we've done that, we can finally calculate the filter coefficients, which we use when actually processing data:

b0 = 1 + alpha*A b1 = -2*c b2 = 1 - alpha*A a0 = 1 + alpha/A a1 = -2*c a2 = 1 - alpha/A

Generally speaking, we want to "normalize" these coefficients, so that a0 = 1. We can do this by dividing each coefficient by a0. Do this in advance or the electrical engineers will laugh at you:

b0 /= a0 b1 /= a0 b2 /= a0 a1 /= a0 a2 /= a0

Now, in pseudocode, here's how we process our data, one sample at a time using a "process" function that looks something like this:

number xmem1, xmem2, ymem1, ymem2;

void reset() {
xmem1 = xmem2 = ymem1 = ymem2 = 0;
}
number process( number x ) {
number y = b0*x + b1*xmem1 + b2*xmem2 - a1*ymem1 - a2*ymem2;

xmem2 = xmem1;
xmem1 = x;
ymem2 = ymem1;
ymem1 = y;

return y;
}

You'll probably have some kind of loop that your process function goes in, since it will get called once for each audio sample.

There's actually more than one way to implement the process function given that particular set of coefficients. This implementation is called "Direct Form I" and happens to work pretty darn well most of the time. "Direct form II" has some admirers, but those people are either suffering from graduate-school-induced trauma or actually have some very good reason for doing what they are doing that in all likelihood does not apply to you. There are of course other implementations, but DFI is a good place to start.

You may have noticed that the output of the filter, y, is stored and used as an input to future iterations. The filter is therefore "recursive". This has several implications:

  • The filter is fairly sensitive to errors in the recursive values and coefficients. Because of this, we need to take care of what happens with the error in our y values. In practice, on computers, we usually just need to use a high resolution floating point value (ie double precision) to store these (on fixed point hardware, it is often another matter).
  • Another issue is that you can't just blindly set the values of your coefficients, or your filter may become unstable. Fortunately, the coefficients that come out of RJB's equations always result in stable filters, but don't go messing around. For example, you might be tempted to interpolate coefficients from one set of values to another to simulate a filter sweep. Resist this temptation or you will unleash the numerical fury of hell! The values in between will be "unstable" meaning that your output will run off to infinity. Madness, delirium, vomiting and broken speakers are often the unfortunate casualties.
  • On some platforms you will have to deal with something called "denormal" numbers. This is a major pain in the ass, I'm sorry to say. Basically it means our performance will be between 10 and 100 times worse than it should be because the CPU is busy calculating tiny numbers you don't care about. This is one of the rare cases where I would advocate optimizing before you measure a problem because sometimes your code moves around and it comes up and it's very hard to trace this issue. In this case, the easiest solution is probably to do something like this (imagine we are in C for a moment):


#DEFINE IS_DENORMAL(f) (((*(unsigned int *)&(f))&0x7f800000) == 0)
float xmem1, xmem2, ymem1, ymem2;

void reset() {
xmem1 = xmem2 = ymem1 = ymem2 = 0;
}
float process( float x ) {
number y = b0*x + b1*xmem1 + b2*xmem2 - a1*ymem1 - a2*ymem2;

if( IS_DENORMAL( y ) )
y = 0;

xmem2 = xmem1;
xmem1 = x;
ymem2 = ymem1;
ymem1 = y;

return y;
}

Okay, happy filtering!

7 comments:

  1. Great material! I'm a (late) student of audio and sound techniques in the last semester or my career (I hope :P ) and Instead of do a recording for my final project I choose to try to do a audio soft for ear training, because of this, I got stuck with the design and code of the filter for the eq.

    I find your blog Trying to have a better understand the RBJ cookbook, and this post! In the day of my B-day! haha cool! and thanks!

    Ps: The mailing list of xonami is working ok BUT even when the fields were correctly filled, the page says that an error occurred (either way the verification email for the list arrive ok)

    ReplyDelete
    Replies
    1. Thanks Distante. I have not noticed the issue with the Xonami mailing list, but I'll be sure to look into it.

      Delete
  2. Hey Bjorn!

    Like you said, "even more control than a second order filter offers, we can often simply take several second order filters and place them in series to simulate the effect of a single higher order filter."

    Is it also possible to put 2 (or more) first order filters to simulate a 2nd order filter ?

    thanks,
    shashank

    ReplyDelete
    Replies
    1. Thanks for your comment. I may revisit this after giving it some more thought, but I think the best short answer is "probably not the way you want." Second order filters give you a huge amount of flexibility you just can't get from a first order filter, which is why they are such a workhorse.

      Delete
  3. hi bjorn. thanks so much for this.

    you say:
    ""Direct form II" has some admirers, but those people are either suffering from graduate-school-induced trauma or actually have some very good reason for doing what they are doing that in all likelihood does not apply to you."

    ...i probably naively thought that the direct form II, or more importantly 'transposed' direct form II, was simply a more efficient (less calculations) version of the direct form I, and therefore good for "we can often simply take several second order filters and place them in series to simulate the effect of a single higher order filter"

    why am i so wrong? any help for this simpleton would be greatly appreciated.

    and if you are feeling generous: rather than simply 'repeating' 2nd order filters to create a cascade to a higher order filter, what optimisations could be made in this technique? also, i never understand how to go about making odd-numbered ordered filters in this stacking technique; do we use first order filters?

    ReplyDelete
    Replies
    1. I was being somewhat hyperbolic with the goal of steering people towards something that will be the right decision most of the time, even if they heard otherwise from someone. But you ask a fair question, of course.

      Both DF I and DF II can be implemented with 5 multiplies and 4 adds (for a standard biquad) so by simple measure of computational complexity they are equally efficient. However, DF II requires only 2 memory locations as opposed to 4 for DF I, so it is more space efficient. This is why some people call it "canonical" since it's the minimum number of memories. However, it is not as stable, and does not have as good error propagation properties. If you are so pressed for memory that you are worried about those two memory locations, you are probably on very limited hardware. In this case, you need to start thinking seriously about what kind of memory locations you have. Some hardware (and here I am not an expert) does not have enough overflow for example. So, even in the case of extremely limited hardware DF I might be the better choice. Hardware designers often use exotic implementations with more multiplies and adds to minimize these kinds of problems, but DF I is a good start.

      As to odd-order filters: yes, it is common to use first and second order filters in series, as well as third and second.

      Delete
  4. bjorn, very kind, very informative, thanks.

    ReplyDelete