A bit of basic filter theory

Related to this thread about filtering behavior –

Here’s one of my classroom demos that explains a bit more about what is happening with filters. This is one of my favorite topics – I get kinda excited about it :crazy_face: so it’s a bit long post. Work through the examples one by one.

To start with, let’s do something seemingly trivial: take a signal, and output the average between successive samples.

s.boot;

b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");

// original sample
(
a = {
	var sig = PlayBuf.ar(1, b, loop: 0, doneAction: 2);
	sig.dup
}.play;
)

// two-point averaging filter
(
a = {
	var sig = PlayBuf.ar(1, b, loop: 0, doneAction: 2);
	var previous = Delay1.ar(sig);
	((sig + previous) * 0.5).dup
}.play;
)

It’s hard to hear, but in fact, high frequencies are attenuated. (This is LPZ1 in SC.)

A filter’s impulse response is what you get when feeding a single 1.0 sample into the filter:

// averaging filter, fed an impulse (graph)
(
a = {
	var sig = Impulse.ar(0);
	var previous = Delay1.ar(sig);
	[sig, (sig + previous) * 0.5]
}.plot(duration: 32/44100);
)

filter-vs-convolution-2pt-avg

In this graph, the left channel is nonzero for one sample, and then silent. The right channel (filter) is nonzero for two samples.

So: A filter’s output is always longer than the input. You can’t do filtering without extending the duration.

Instead of an average, you could take the difference between successive samples. This is a rather severe high pass filter (HPZ1).

// two-point difference filter
(
a = {
	var sig = PlayBuf.ar(1, b, loop: 0, doneAction: 2);
	var previous = Delay1.ar(sig);
	((sig - previous) * 0.5).dup
}.play;
)

// difference filter, impulse response (graph)
(
a = {
	var sig = Impulse.ar(0);
	var previous = Delay1.ar(sig);
	[sig, (sig - previous) * 0.5]
}.plot(duration: 32/44100);
)

filter-vs-convolution-2pt-diff

These filters can be expressed as:

// averaging filter
this output sample = (0.5 * current input sample) + (0.5 * previous input sample)

// difference filter
this output sample = (0.5 * current input sample) + (-0.5 * previous input sample)

OR:

y(i) = 0.5 * x(i) + 0.5 * x(i-1)  // avg
y(i) = 0.5 * x(i) + -0.5 * x(i-1)  // diff

So the averaging filter has coefficients a0 = 0.5 and a1 = 0.5, while the difference filter has coefficients a0 = 0.5 and a1 = -0.5. These are feedforward coefficients.

Now look at the impulse response graphs. The average filter’s IR is 0.5, 0.5, 0… and the difference filter’s IR is 0.5, -0.5, 0… exactly matching the coefficients.

In fact, it continues like this. You can have an impulse response of any length, and use those samples as feedforward coefficients: y(i) = sum[over k = 0..n-1](IR(k) * x(i-k)). This is convolution.

The impulse response has a finite length – so, convolution against a finite impulse response is the same as a finite-impulse-response filter (FIR).

// make an IR for the difference filter
k = Buffer.alloc(s, 2048, 1);

k.zero;
k.setn(0, [0.5, -0.5]);

(
a = {
	var sig = PlayBuf.ar(1, b, loop: 0);
	var filt = Convolution2.ar(sig, k, framesize: k.numFrames);
	DetectSilence.ar(filt, doneAction: 2);
	filt.dup
}.play;
)

So we put the impulse response into a buffer and use fixed-frame convolution on the signal, and… it sounds like the difference filter.

We can do the same with a more interesting filter.

(
{
	var sig = Impulse.ar(0);
	var filt = RLPF.ar(sig, 2000, 0.05);
	// HERE: Save the impulse response into 'k'
	RecordBuf.ar(filt, k, loop: 0);
	[sig, filt]
}.plot(duration: k.numFrames / s.sampleRate);
)

filter-vs-convolution-rlpf-ir

This is a nice sinusoidal decay. Technically, it never reaches exactly 0 – so this is an infinite impulse response (IIR). Practically, it eventually gets close enough to zero that we don’t have to record it forever.

You get an IIR by feeding the filter’s output samples back into the filter – so then you have both feedforward and feedback coefficients. y(i) = (a0 * x(i)) + (a1 * x(i-1)) + (a2 * x(i-2)) + (b1 * y(i-1)) + (b2 * y(i-2)) is a second-order filter because it goes back two samples. This formula is also called biquad and it’s the basis of most of SC’s standard filters.

A second-order filter tapers off by 12 dB/oct. A third-order filter goes back three samples and tapers off by 18 dB/oct; fourth-order, four samples, 24 dB/oct. Because of limitations in floating point precision, usually fourth-order filters are implemented as a chain of two equivalent second-order filters: RLPF.ar(RLPF.ar(sig, freq, rq), freq, rq).

OK, compare convolution against the filter UGen:

// convolution against filter's impulse response
(
a = {
	var sig = PlayBuf.ar(1, b);
	var filt = Convolution2.ar(sig, k, framesize: k.numFrames);
	DetectSilence.ar(filt, doneAction: 2);
	filt.dup
}.play;
)

// compare to RLPF
(
a = {
	var sig = PlayBuf.ar(1, b);
	var filt = RLPF.ar(sig, 2000, 0.05);
	DetectSilence.ar(filt, doneAction: 2);
	filt.dup
}.play;
)

And… they sound the same.

So what is the filter’s frequency response?

The impulse response is the time domain response. FFT converts this to frequency domain.

// get frequency response
k.getToFloatArray(wait: -1, action: { |data| d = data.postln });

f = d.as(Signal).fft(Signal.newClear(d.size), Signal.fftCosTable(d.size));

// 'fft' result consists of complex numbers (Cartesian coordinates)
// but we want magnitudes (the lengths of the polar vectors)
f = f.asPolar;

// FFT is a mirror image around the center
// so use only the first half
f.rho[0 .. d.size div: 2].plot;

filter-vs-convolution-rlpf-freq-resp

… which plots 1.0 amplitude for the lowest frequencies, leading up to a resonant “hump,” and tapering off to zero at the highest frequencies – as you would expect for a low pass filter with resonance. (It looks biased to the left because the frequency axis is linear here. Frequency scopes usually convert to a logarithmic scale. The peak frequency is f.rho[0 .. d.size div: 2].maxIndex * (s.sampleRate / k.numFrames) = 2002.587, given an input frequency of 2000 :white_check_mark: )

hjh

16 Likes

Thank you very much for this pîece of knowledge

This is really great. Thanks for sharing!

Thanks for uploading!
Greetings from Vienna!