How to Create a Simple Low-Pass Filter

Summary: This article shows how to create a simple low-pass filter, starting from a cutoff frequency \(f_c\) and a transition bandwidth \(b\). This article is complemented by a Filter Design tool that allows you to create your own custom versions of the example filter that is shown below, and download the resulting filter coefficients.

How to create a simple low-pass filter? A low-pass filter is meant to allow low frequencies to pass, but to stop high frequencies. Theoretically, the ideal (i.e., perfect) low-pass filter is the sinc filter. The sinc function (normalized, hence the \(\pi\)’s, as is customary in signal processing), is defined as

\[\mathrm{sinc}(x)=\frac{\sin(\pi x)}{\pi x}.\]

The sinc filter is a scaled version of this that I’ll define below. When convolved with an input signal, the sinc filter results in an output signal in which the frequencies up to the cutoff frequency are all included, and the higher frequencies are all blocked. This is because the sinc function is the inverse Fourier transform of the rectangular function. Multiplying the frequency representation of a signal with a rectangular function can be used to generate the ideal frequency response, since it completely removes the frequencies above the cutoff point. And, since multiplication in the frequency domain is equivalent with convolution in the time domain, the sinc filter has exactly the same effect.

The windowed-sinc filter that is described in this article is an example of a Finite Impulse Response (FIR) filter.

Sinc Filter

The sinc function must be scaled and sampled to create a sequence and turn it into a (digital) filter. The impulse response of the sinc filter is defined as

\[h[n]=2f_c\mathrm{sinc}(2f_cn),\]

where \(f_c\) is the cutoff frequency. The cutoff frequency should be specified as a fraction of the sampling rate. For example, if the sampling rate is 10 kHz, then \(f_c=0.1\) will result in the frequencies above 1 kHz being removed. The central part of a sinc filter with \(f_c=0.1\) is illustrated in Figure 1.

Figure 1. Sinc filter.Figure 1. Sinc filter.

The problem with the sinc filter is that it has an infinite length, in the sense that its values do not drop to zero. This means that the delay of the filter will also be infinite, making this filter unrealizable. The straightforward solution is to simply stop computing at a certain point (effectively truncating the filter), but that produces excessive ripple. A better solution is to window the sinc filter, which results in, you guessed it, a windowed-sinc filter.

Window

A window function is a function that is zero outside of some interval. There exist a great variety of these functions, tuned for different properties, but I’ll simply use the well-known Blackman window here, which is a good choice for general usage. It is defined as (for \(N\) points)

\[w[n]=0.42-0.5\cos\left({\frac{2\pi n}{N-1}}\right)+0.08\cos\left({\frac{4\pi n}{N-1}}\right),\]

with \(n\in[0,\,N-1]\). It is shown in Figure 2, for \(N=51\).

Figure 2. Blackman window.Figure 2. Blackman window.

Windowed-Sinc Filter

The final windowed-sinc filter is then simply the product of the two preceding expressions, as follows (with the sinc filter shifted to the range \([0,\,N-1]\)).

\[h[n]=\mathrm{sinc}\left(2f_c\left(n-\frac{N-1}{2}\right)\right)\left(0.42-0.5\cos\left({\frac{2\pi n}{N-1}}\right)+0.08\cos\left({\frac{4\pi n}{N-1}}\right)\right),\]

with \(h[n]=0\) for \(n\notin[0,\,N-1]\). I have dropped the factor \(2f_c\) from the sinc filter, since it is much easier to ignore constants at first and normalize the complete filter at the very end, by simply making sure that the sum of all coefficients is one, giving the filter unity gain, with

\[h_\mathrm{normalized}[n]=h[n]/\sum_{i=0}^{N-1}h[i].\]

This results in the normalized windowed-sinc filter of Figure 3.

Figure 3. Normalized windowed-sinc filter.Figure 3. Normalized windowed-sinc filter.

Transition Bandwidth

The final task is to incorporate the desired transition bandwidth (or roll-off) of the filter. To keep things simple, you can use the following approximation of the relation between the transition bandwidth \(b\) and the filter length \(N\),

\[b\approx\frac{4}{N},\]

with the additional condition that it is best to make \(N\) odd. This is not really required, but an odd-length symmetrical FIR filter has a delay that is an integer number of samples, which makes it easy to compare the filtered signal with the original one. Setting \(N=51\) above was reached by setting \(b=0.08\). As for \(f_c\), the parameter \(b\) should be specified as a fraction of the sampling rate. Hence, for a sampling rate of 10 kHz, setting \(b=0.08\) results in a transition bandwidth of about 800 Hz, which means that the filter transitions from letting through frequencies to blocking them over a range of about 800 Hz. The values for \(f_c\) and \(b\) in this article were chosen to make the figures as clear as possible. The frequency response of the final filter (with \(f_c=0.1\) and \(b=0.08\)) is shown in Figure 4.

Figure 4. Frequency response on a linear (left) and logarithmic (right) scale.Figure 4. Frequency response on a linear (left) and logarithmic (right) scale.

Python Code

In Python, all these formulas can be implemented concisely.

# All scripts on TomRoelandts.com assume Python 3.
 
import numpy as np
 
fc = 0.1  # Cutoff frequency as a fraction of the sampling rate (in (0, 0.5)).
b = 0.08  # Transition band, as a fraction of the sampling rate (in (0, 0.5)).
N = int(np.ceil((4 / b)))
if not N % 2: N += 1  # Make sure that N is odd.
n = np.arange(N)
 
# Compute sinc filter.
h = np.sinc(2 * fc * (n - (N - 1) / 2.))
 
# Compute Blackman window.
w = 0.42 - 0.5 * np.cos(2 * np.pi * n / (N - 1)) + \
    0.08 * np.cos(4 * np.pi * n / (N - 1))
 
# Multiply sinc filter with window.
h = h * w
 
# Normalize to get unity gain.
h = h / np.sum(h)

Applying the filter \(h\) to a signal \(s\) by convolving both sequences can then be as simple as writing the single line:

s = np.convolve(s, h)

In the Python script above, I compute everything in full to show you exactly what happens, but, in practice, shortcuts are available. For example, the Blackman window can be computed with w = np.blackman(N).

In the follow-up article How to Create a Simple High-Pass Filter, I convert this low-pass filter into a high-pass one using spectral inversion. Both kinds of filters are then combined in How to Create Simple Band-Pass and Band-Reject Filters

Filter Design Tool

This article is complemented with a Filter Design tool. Experiment with different values for \(f_c\) and \(b\), visualize the resulting filters, and download the filter coefficients. Try it now!

Filter designer.Filter designer.
Submitted by Tom Roelandts on 15 April 2014

Comments

Thanks! That's a very useful article.
Could you explain, what can I do with delay between source and filtered signals?

The nice thing about these kinds of filters is that it is easy to compensate for their delay. Symmetrical FIR filters, of which the presented windowed-sinc filter is an example, delay all frequency components in the same way. This means that the delay can be characterized by a single number. The delay of a filter of length M equals (M-1)/2. Hence, the shown filter with 51 coefficients has a delay of exactly 25 samples. So, if you want to overlay the original signal with the filtered one to compare them, you just need to shift one of them by 25 samples!

Really appreciate how concise this article is. Thanks!

Thank you! It's a very nice article. Could you maybe explain why it is necessary to normalize each element in h by the sum(h)?

It’s not really necessary, but if you make sure that the sum of the coefficients is one, then the gain of the filter at DC is also one. Additionally, it allows you to make the gain of the filter whatever you want simply by multiplying the coefficients of the normalized filter by the required gain factor.

Thanks very much for your article... I had considered this topic whole week and now my mind is clear!

Thanks, this is really helpful !

Great article!
How do you make your plots?
Thanks

Thanks! The plots for most of the articles, including this one, were made with Python (using matplotlib).

Very interesting and clear article, thanks Tom! Question: You say that the number of coefficients is approx 4/b. If i use your fir designer tool and design a lowpass (windowed sinc) with Samplerate=44100, Cutoff=1000, Bandwidth=1000, i would expect approx 4/(1000/44100) = 176.4 thus 177 coefficients, but it gives 203 coefficients. I have been searching for the logic in that but i don't understand it. How do you define the specific number of coefficients? Thanks.

That’s a good observation. The reason for this is that the number of coefficients in the tool depends on the window function. This is because the window has a large influence on the transition bandwidth, so that, e.g., the rectangular window can get by with much less coefficients than the Blackman window. Maybe I'll write a separate article on this with more details, because it’s quite interesting stuff. In practice, the tool uses 4.6/b for Blackman, 3.1/b for Hamming, and 0.91/b for rectangular. From your example, I can tell that you’ve used a Blackman window, since 4.6/(1000/44100) = 202.86.

Thanks for your tutorial, well detailed!
Is the signal that you filter an array containing the amplitude of the signal?

Thanks! And yes, the variable s in the example is an array of numbers. I would say that this is the signal, and not just its amplitude.

Hi, Can you explain why its not just the amplitudes?

Great articles, thank you very much!

I was just being nitpicky a bit, I think... :-) The signal is indeed really just a series of numbers that represent an amplitude. It’s just that saying “the amplitude of the signal” could suggest that you’ve left something out or did some sort of preprocessing. That’s why I’d just write “the signal”…

Thanks for this great article. I have a question regarding Figure 4. How did you create the frequency response diagram?

This is another great idea for a follow-up article! I'll do one where I explain this in detail. In short, you first pad the filter with zeros to increase the resolution of the frequency plot, then take an fft, compute the power, and plot the result, either on a linear scale or in dB.

Thank you. Would you write something about IIR?

Currently, I indeed only have an article on the (important) low-pass single-pole IIR filter. Are there any specific things on IIR filters or filter types that you would find interesting?

Well-elaborated! Many thanks.

This was very useful, thanks!

Hello, i'm implementing a fir filter, band reject to be exact, could the signal s be the data of an fft from an audio.wav?

Your signal s should be the data of the audio.wav file, not the FFT of the data. Of course, if you are going to implement the convolution in the frequency domain, then you need to take the FFT of both the signal and the filter coefficients.

Sorry Tom,
I was wrong in my earlier comment. fS/fL evaluates to zero in Python 3 as well, so the code gives the Blackman weights.

Your plots are, of course, correct but the display maybe doesn't come from the sample code shown below ?

I am a geophysicist, seismic sampling is often 500 -1000Hz The input box is labelled Hz.

Once again, thanks !
very impressive !

Jim
______________________________
import numpy as np

# Example code, computes the coefficients of a low-pass windowed-sinc filter.

# Configuration.
fS = 1000 # Sampling rate, ***** works fS = 1000.0 ? *****
fL = 20 # Cutoff frequency, *****works fL = 20.0 ? *****
N = 461 # Filter length, must be odd.

# Compute sinc filter.
h = np.sinc(2 * fL / fS * (np.arange(N) - (N - 1) / 2.))

# Apply window.
h *= np.blackman(N)

# Normalize to get unity gain.
h /= np.sum(h)

print(h)

# Applying the filter to a signal s can be as simple as writing
# s = np.convolve(s, h)

Hello Jim,

fL/fS (or fS/fL) does definitely not evaluate to zero in Python 3. Maybe two version of Python are installed on your computer, and you're still using Python 2? The plots on fiiir.com are indeed not generated in Python.

Tom

Tom,
Yes, indeed, I did have both versions active and must have used 2.7 again.

I just ran it in Python 3.4.5 and it works fine, a lesson learned.

I should have checked, was confused debugging in Idle

Jim

Awesome! you made windowed-sinc-filter design very easy!

Hi Tom,
I'm trying to apply this filter to my data which has sample rate of 250Hz and contains 1000 samples. After applying the filter my data has 1050 samples instead of 1000. Am I doing something wrong? Please help.

Thanks in advance.

This is normal. The length of the output signal is the length of the input signal plus the length of the filter minus one. This follows from the definition of convolution, but for a more intuitive understanding, have a look at the nice animations on https://en.wikipedia.org/wiki/Convolution#Visual_explanation. An effect of this is that you will see a so-called transient response of the filter in the beginning of your output signal, and that you have to wait a number of samples (the length of the filter, i.e., 51 samples in case of the example filter) before the filter is "filled up" and you get the actual response for which the filter was designed (the so-called steady state response).

Thanks Tom. I understand it better now.

I found it myself :). That was due to numpy.convolve. I used mode='same' and I got what I want.

Add new comment