Understanding Low-Pass Filters

Low-pass filters are some of the most fundamental tools for audio processing. You don’t need to understand loads of maths to have a feel for how they work.

A LPF attenuates some of the higher frequencies in a signal while allowing the lower ones to ‘pass’ through. There is more than one way to do this, however the easiest and most common way is to use destructive interference.

Destructive interference happens when you add a positive number to a negative number. If they are of equal magnitude then the result is 0. If they are not then the result will be somewhere in between the two values.

Digital signals are just a series of numbers (conventionally between -1 and 1), so if you add together two signals of the same length, each ‘sample’ will be added to its counterpart in the other signal. If one signal is positive at the same time as the other is negative, they can cancel each other out.

We can see this by plotting two sinusoids in Python with MatPlotLib. A is a cosine wave with 0 phase, it’s the blue one. B, the green one, is the same cosine wave but with a phase of pi (so half a period out of phase with A). Then we plot the sum of the two in red.

note: This uses a sin generating function, genSine (link at the end of the post). The details of this aren’t important here other than understanding that it computes a sinusoid. The parameters it takes are: (amplitude, freq in Hz, phase in radians, sampling freq, duration in seconds).

import matplotlib.pyplot as plt
import numpy as np

A = genSine(0.5, 2.0, 0.0, 1000.0, 2.0)
B = genSine(0.5, 2.0, np.pi, 1000.0, 2.0)
sum = A + B
plt.plot(A)
plt.plot(B)
plt.plot(sum)

Diagram

So that’s great, if we want to make something silent we can just invert it and add it to itself. This has all sorts of uses in itself (for example, to test if two sounds are identical or not), but how does this help us pull down higher frequencies more than lower ones? Before we answer that let’s look at the opposite of destructive interference: constructive interference.

Constructive interference happens when you add together signals that have similarities to each other, just as destructive interference happened where there were differences. For example, if we add together two identical sinusoids the result will be a sinusoid of the same frequency but twice the amplitude.

Here A and B are in green (overlapping), and sum is in red:

A = genSine(0.5, 2.0, 0.0, 1000.0, 2.0)
B = genSine(0.5, 2.0, 0.0, 1000.0, 2.0)
sum = A + B
plt.plot(A)
plt.plot(B)
plt.plot(Sum)

Diagram

So that’s quite intuitive - one noise plus another identical noise is a louder noise.

Maybe you are wondering what happens when the two are not ether exactly in phase or out of phase. Here are some other sinusoids with different phase relationships, and their sums (in red):



0.5*pi:

Diagram

0.25*pi

Diagram

0.9*pi

Diagram

These different phase relationships reveal a trend:

The more similar two signals are at a given point in time, the more constructive interference they have with each other, the more different they are at a given point in time, the more destructive interference.

We can exploit this quality to filter out “different” sounds and retain “similar” ones. All we need to do is find a signal to add to our input that has the right similarities and differences - one that is pretty similar at the low end but opposite-lookin’ at the high end. Let’s see how…

One-Zero LPF

The One-Zero filter in the block diagram below is a super-simple low pass filter. It works by forking the signal into two, delaying one of the forks by one sample and then adding them back together.

The name has nothing to do with binary, or with what values it outputs by the way, it’s to do with how you would map its response in the z-plane, which is much beyond the scope of this article.

Signal flow

Now, imagine that x in the diagram above is the signal of a cosine wave with a frequency of half of the sampling frequency. This is known as the Nyquist frequency - the highest possible frequency that can be represented for a given sampling frequency. Imagine we are only working with 18 samples in our signal.

Assuming its amplitude is 1, the x values are going to jump right from 1 to -1 to 1 to -1. You can see that this is the fastest oscilation we can represent here (and therefor the highest frequency). If you delay it by one sample you get the opposite sequence, as we can see:

Table sins

Note: for column 0 we have no delayed value - we’ll just set the sum value of this one to 0 to avoid problems in the upcoming example.

So, for the highest frequency we can talk about for a given sampling frequency, the output is 0. This cancels out just like the first plot we did. This is because one sample of delay will cause Nyquist to be exactly pi phase.

However, for lower frequencies, one sample of delay isn’t such a big difference. In fact there is going to be a whole load of constructive interference at low frequencies - this boost to low frequencies is one reason why this “One-Zero” filter is not ideal for many situations.

Let’s implement a simple One-Zero LPF in Python, throw some different sinusoids through it and graph what comes out:

def lpf(input):
	mem = 0;
	for i in range(len(input)):
		temp = mem + input[i]
		mem = input[i]
		input[i] = temp
	input[0] = 0;	
	return input

So first we make a little function that holds a sample in mem, then in iterates along the list and replaces each item with the sum of mem and the value at that point. A temporary value is set up so that we can do the sum and replace mem in the same pass.

Now let’s make a test function for it.

def testSines(length):
    
    results = []
    f = 1
    while(f < length / 2.0):
        sin = genSine(1.0, f, 0.0, length, 1.0)
        filteredSin = lpf(sin)
        amp = max(filteredSin)
        results.append(amp)
        f += 1
    plt.plot(results)
    plt.xlabel('Hz')
    plt.ylabel('Amplitude')
        
testSines(101)

This makes a range of sinusoids with our genSine function. They vary by their f value (frequency) which ranges from 1hz to the length divided by two (Nyquist). These are then put into the results list and plotted. The amplitude here can be found by taking the maximum value in the signal. Notice how the chosen length for the test was 101 - a prime number. I encourage you to run this code yourself with different lengths in order to figure out the significance of that.

Test result

We can see that the high frequencies are attenuated and the low frequencies are boosted. This low boost is one of several reasons that make this filter limitied in its applications.

We’ve made a few assumptions here. For example the assumption that this works with signals that are not just sinusoids. In later posts I’ll write about why we can assume this - Fourier demonstrated that all signals can be made by summing together sinusoids. We’ve also said nothing about what happens at the “in between frequencies”. We’ll get onto some of these things in future posts.

Fourier

Lucas V. Barbosa’s animation of the fourier series of a function. taken from Wikipedia.

There is a lot more to discuss with LPFs, but from this point on it gets more mathsy. However, the basic notion is still the same: a feed-forward delay line summed with a non delayed line. To go deeper read some of the fantastic materials by Julius Smith from Stanford University.

genSine

Written on June 1, 2017