In this short exercise, we'll practice writing classes which can manipulate buffers as a whole
Remember that a class definition tells the computer how to create objects which are instances of a class. Each object has a constructor which initialises its state, and (usually) several methods which can be thought of as functions which operate on the object, or perform operations on supplied data using the object
We'll write a Delay
class. A delay object
is told how many samples delay (n
)
it should introduce. Then, when its delay
method is passed an array of
floating-point samples, it returns an array
of the same size as the input
with the contents delayed by n
samples.
Subsequent calls to the delay
method
return the remaining n
samples from the
previous input followed by all but n
samples from the current input. Overall, across multiple calls,
the first n
samples of the output
are set to zero, and the last n
input samples are discarded.
For example:
>>> d = Delay(2)
>>> d.delay([1,2,3,4,5])
[0, 0, 1, 2, 3]
>>> d.delay([-1,-2,-3,-4,-5])
[4, 5, -1, -2, -3]
Also write a class Gain
which returns the
samples in the input buffer scaled by a specified amount.
Plot the result of using Delay and Gain classes on a sine wave. Plot the original waveform on the same axes for comparison.
Filters change the statistics of a signal.
In engineering, we usually assume systems are linear. We go to great lenths to linearise the response of electronic systems, and non-linear systems are still very much regarded as "specialist" compared with LTI (Linear, Time-Invariant) ones. Usually, when we refer to a filter, we mean a device which changes the spectral characteristics of a presented signal.
This lab falls into two parts: designing and implementing a filter directly by placing poles and zeros on the z-plane, and then converting a classical, continuous-time design such as the maximally-flat Butterworth or maximally-steep Tchebychev filters to operate as a sampled system.
First of all, though, we'll need some tools to measure the response of the systems we build. This can be done in the frequency domain by taking the Fourier Tranform of filtered white noise, or in the time domain by passing a varying-frequency sine-wave ("chirp") through the filter and seeing how its amplitude changes as the freqency sweeps across the frequency range of interest.
Look at the coefficients from the Exercise 16 Maths Homework
Write down the transfer function for this system in the form
$\frac{(z-{z}_{1})(z-{z}_{2})...(z-{z}_{n})}{(z-{p}_{1})(z-{p}_{2})...(z-{p}_{n})}$Expand this out to obtain the numerator and denominator as polynomials in z^{-1}, resulting in this form:
$G\frac{1+{a}_{1}{z}^{-1}+{a}_{2}{z}^{-2}}{1+{b}_{1}{z}^{-1}+{b}_{2}{z}^{-2}}$This can be implemented using a second order, bi-quadratic section like this:
On paper, write out the values for a second-order filter, at the output of each delay and the output of the entire filter.
There are a few sub-tasks. Do them in order, and it would be good to get them checked by Nick or Graham at each step.
Noise generation: filters aren't so fascinating on a sine wave, so make a class that generates white noise. This is like your sine-wave oscillator classes, but instead of returning an array of the next 2048 samples of a sine wave, you return 2048 samples of random values.
for fun: after doing white noise, make some other colors of noise, like pink, brown, grey, blue, violet, orange, or black noise. (wikipedia: colors [sic] of noise)
Check the spectrum of the noise is what you expect by plotting the FFT of your noise generator's(') output(s). Hint: numpy provides a routine to take the FFT of real values which you might find useful.
Enhance your Delay class. The improved class will contain a
process(audio_sample)
method, which will return its
arguments delayed by up to N samples. N should be passed in via the
constructor.
For example:
d = Delay(2)
d.process(2.0)
[2.0, 0]
d.process(3.5)
[3.5, 2.0]
d.process(-1.0)
[-1.0, 3.5]
Write a Filter class. For now, you may hard-code the filter
coefficients in the __init__()
function (i.e. you do
not need to pass them as arguments).
Use the coefficients of the filter in Example 16 to test it.
Filters designed to specification rely on determining the required order and type which will best satisfy the requirements in hand.
The following types of filters are commonly used as prototypes:
With all of this development having been put into analogue filter design, one of the best ways of constructing a digital filter "to spec" is to start off with the analogue design and then to transform it into the digital domain.
The design process for a digital filter based on one of the "classical" polynomials is covered in detail in the Making Computer Music PDF file. In (extreme) summary,
To practise this process using a simple filter design. calulate the coefficients of a 1kHz low-pass, second-order Butterworth filter operating at 48000Hz sample rate. The calculation is much simplified if you normalise the sample rate to 1Hz (T=1) and adjust the cutoff frequency appropriately (keeping it the same proportion of the Nyquist rate).
The algebra necessary to design higher-order filters isn't particularly difficult but is very tedious. For this reason, you might think it likely that functions have been written to calculate the polynomial coefficients and/or pole-zero placements necessary to realise a filter of given type and specification. And you would be right.
Read and understand the documentation of the python scipy.signal.iirdesign() function. Use it to generate a filter which delivers an agressive high-pass response removing frequencies below 1.5kHz.
Implement the filter you have designed, and test it both with white noise and by listening to what it does to a voice signal. You may implement the filter directly, or by using a standard filter function (for example, scipy.signal.lfilter())
Unless otherwise noted, all materials on these pages are licenced under a Creative Commons Licence .