A one-pole filter

Here’s a very simple workhorse of DSP applications—the one-pole filter. By comparison, biquads implement two zeros and two poles. You can see that our one-pole simply discards the zeros (the feed-forward delay paths) and the second pole (feedback path):

We keep the input gain control, a0, in order to compensate for the gain of the feedback path due to b1, which sets the corner frequency of the filter. Specifically, b1 = -e-2πFc and a0 = 1 – |b1|. (In the implementation, we’ll roll the minus sign in the summation into b1 and make it positive, for convenience.)

Filter types

A pole is a single-frequency point of pushing gain up, and a zero is a single-frequency point of pulling the gain down; with a single pole, you are not going to get complex response curves such as bandpass, peak, and shelving filters that you can get with the two poles and zeros of a biquad. That leave us with lowpass and highpass, which have a single point of change.

However, a one-pole makes a poor highpass filter for cases in which we might be likely to use it—in particular, a DC blocker. That’s because it makes a highpass by pushing response up at high frequencies—we really need a zero to pull the response down at very low frequencies. So, we’ll only implement coefficient calculation for lowpass response here. However, we can still make a highpass filter suitable for a DC blocker by subtracting the output of a one-pole lowpass filter, set to a low frequency, from the direct signal.

Typical uses

What are typical uses of this one-pole lowpass filter? Sometimes, we do need the gentle slope of a first order frequency roll-off. And often, we use this simple filter on parameters instead of one the audio directly—some gentle smoothing of a calculated signal in an automatic gain control, for instance.

The 6 dB per octave slope of the one-pole filter—a halving of amplitude for every doubling of frequency—is gentle and natural. It’s extremely cheap, computationally, so it’s the perfect choice when you need just a bit of smoothing. And it doesn’t “ring” (overshoot), so it’s an excellent choice for filtering knob changes. Run each of your user interface sliders and knobs through a one-pole lowpass set to a low (sub-audio) frequency, and your glitchy, zippered control changes turn into smooth parameter changes.

Note that if you feed the one-pole lowpass an impulse, it will yield a perfect exponential decay—the same decay that we use on typical ADSR envelope generators on synthesizers. To look at it another way, the feedback path is an iterative solution to calculating an exponential curve.

The source code

The one-pole filter is so simple that we’ll make it inline, entirely in the header file:

//
//  OnePole.h
//

#ifndef OnePole_h
#define OnePole_h

#include <math.h>

class OnePole {
public:
OnePole() {a0 = 1.0; b1 = 0.0; z1 = 0.0;};
OnePole(double Fc) {z1 = 0.0; setFc(Fc);};
~OnePole();
void setFc(double Fc);
float process(float in);

protected:
double a0, b1, z1;
};

inline void OnePole::setFc(double Fc) {
b1 = exp(-2.0 * M_PI * Fc);
a0 = 1.0 - b1;
}

inline float OnePole::process(float in) {
return z1 = in * a0 + z1 * b1;
}

#endif


Examples

Here’s a DC blocker:

OnePole *dcBlockerLp = new OnePole(10.0 / sampleRate);
...
// for each sample:
sample -= dcBlockerLp->process(sample);


Need highpass?

It’s trivial to add one-pole highpass response, if you’d like—just negate the lowpass calculation; this inverts the spectrum, so you’ll need to flip the frequency range by subtracting Fc from 0.5:

b1 = -exp(-2.0 * M_PI * (0.5 - Fc));
a0 = 1.0 + b1;


Pole position

You can experiment with how the location of the pole affects frequency response by using the pole-zero placement app. Click the Pair checkbox near the Poles sliders to disable it. Move both Poles sliders to center to move the poles to the origin. (The zeros are already at the origin by default when you open the page. At the origin, poles and zeros have no effect.) Now, move just one pole slider; you’ll see highpass response when the pole is to the left of the origin, and lowpass to the right.

This entry was posted in DC Blocker, Digital Audio, IIR Filters, Source Code. Bookmark the permalink.

17 Responses to A one-pole filter

1. Harold M says:

Hi Nigel,

This is great for me as I am only beginning to understand filter design. Thank you very much for the great resource!

• Nigel Redmon says:

Thank you for your comment, Harold—glad this is a help. I added an additional paragraph about using the pole-zero applet to experiment. While you’re moving the slider and watching the graph, keep in mind that as the pole nears the unit circle, it pushes the frequency response higher and higher. The input gain parameter, a0, compensates by pulling the overall amplitude down to keep the peak amplitude constant.

Nigel

2. shashank says:

Hey Nigel,

Although I am still trying to learn the intricacies of digital filter designing and I have experimented with this idea myself, but I still wonder about a few things regarding the implementation.

In the blog above, we have a function OnePole::process(…) which processes just 1 sample at a time (opposed to a frame of samples).

Won’t the function calling for every sample make the processing slow ?
Why not rather pass address of an entire frame to OnePole::process(…), let it do the DSP on that piece of memory, wont this be faster ?

Please clarify.

Thanks
Shashank

• Nigel Redmon says:

Hi Shashank,

In the end, yes, you generate frames of output. However, you still start with “atomic” DSP objects. Consider that if you need a OnePole inside a reverb process. The reverb may generate its output a frame at a time, but it may require a OnePole in a feedback within the reverb (a common requirement). If you had designed OnePole to work on frames, it would be useless for the Reverb object. On the other had, it’s trivial to use OnePole to do an entire frame—either stick it in a for loop outright, or create a trivial OnePoleFrame object that uses OnePole (perhaps using template meta programming to effectively create unrolled loops for zero loop overhead).

Note that OnePole::process is inline and computationally cheap—there is no function call overhead at all—no extra cost for it to be atomic.

Nigel

PS—Need to sleep now, but I’ll check out your blog—looks interesting.

3. Alan says:

Nice article – this filter is indeed the “workhorse” of DSP!

If you don’t mind a nitpicking comment though: you’ve labelled your filter coefficients a and b as is usual, but unusually, the b coefficients are applied to the output signal and the a coefficients are applied to the input. Convention dictates it should be the other way around.

Obviously this isn’t a problem in the context of this blog post, but as it’s a beginner-type article this switch-around may be confusing for those DSP novices who will generally see the opposite with other DSP resources.

In any case, I look forward to continuing to follow your musings. Cheers!

• Nigel Redmon says:

Hi Alan,

Actually, there is no convention (or there are two). A little more than half of the DSP books on my bookshelf favor the orientation I use, where a is in the numerator of the transfer function. I wish there were a convention—on top of the a/b confusion, some people roll the minus signs in the feedback into the coefficients, other don’t. However, since almost everyone normalizes, you need only look to see whether a0 or b0 is missing to determine the orientation. It’s not always so obvious whether the minus signs are included.

Thanks for reading!

Nigel

4. Jerry Gregoire says:

Nice description. Just curious- why did you switch the traditional nomenclature for the coeff, A and B. I have always seen A in the feedback, (denominator) and B(numerator) in the forward.

• Nigel Redmon says:

Hi Jerry—there is no standard, no tradition. It was natural for me to have A coefficients on top of the transfer function. Some might start with A on the left of the direct form I block diagram, which yields the opposite relationship. I once did a survey of the DSP books on my bookshelf, and A-on-top was used more often (Zölzer uses that convention, for example).

• Saf says:

I only wish more people used b for feedBack, and a for feedforward. 🙂

• David says:

I’m looking at Zölzer DAFX 2nd Ed right now and he uses b in the numerator, a in the denominator in Eq 2.3. Most of my books use b in the numerator. But your point stands – it doesn’t matter as long as it’s clear and consistent across this article / website.

• Nigel Redmon says:

Yes, I noticed that too—I suspect to be consistent with DAFX publications. His Digital Audio Signal Processing is the other way.

5. David Billen says:

You can eliminate a multiply by using z1 += (in – z1)*a0;

• David Billen says:

…I guess that’s * a0…

• Nigel Redmon says:

Good spot, and I like the elegance. However, it’s the same number of operations, and I think most processors these days have single-cycle multiply. It does reduce a fetch, which might not be zero cost in the pipeline in all cases. I recall considering using a single variable, but I think I went with two with the idea of also having a highpass version of setFrequency (which is of marginal utility), as noted in the article but not the code, and after finding no difference in execution time. Testing, the code execution time is consistently identical between the two versions with compiler optimizations enabled (Xeon); for some reason, the single-coefficient version is consistently slower by a trivial amount (2%) without optimizations—I can’t think of a reason for that, but it’s unimportant.

6. Pankaj says:

Very nice article. I am naïve at DSP and trying to learn. One question that comes to my mind is that in determining the coefficients based on cut-off frequency, there is no dependence on the sample rate. Won’t sample rate have impact on the digital filter performance?

• Nigel Redmon says:

I could have made this more clear, sorry: “Fc” is normalized frequency. That is, it’s frequency divided by sample rate. For instance, look at the example for a DC blocker—Fc is set to “10.0 / sampleRate”, or 10 Hz.

7. Josh says:

Interestingly, if this is used to try and dc block a signal from a fast waveform digitizer (500MS/s) with a baseline of the max channel values, 16k, there is a significant distortion at the beginning due to z being set as 0 to start. I was able to get reasonable behavior by starting z as the beginning value of the function for the first sample.