This notebook introduces fundamental digital signal processing (DSP) concepts used in the 02471 Machine Learning for Signal Processing course at the Technical University of Denmark. It is targeted to students who are not familiar with signal processing and need a resource to catch up.
This notebook is far from being as complete as the official course prerequisites. Students are invited to read the course prerequisites or other well established resources for more details.
If you are reading this from the README.md, it is recommended to switch to the IPython notebook, where the math formulas are better rendered. You can also clone the notebook to modify and run the code snippets. If you prefer MATLAB you can also check the transcript.m file where all the code snippets were translated to MATLAB.
The following assumes you are familiar with real analysis mathematics.
A signal usually refers to a time varying function or variable. Signals can be discrete (e.g. characters in a sentence) or continuous (e.g. pressure, voltage) by nature. In the real world, signals are usually captured by sensors (e.g. a microphone captures pressure variations and converts them to an electrical signal).
A digital signal is a discrete representation of a signal. If the signal is continuous by nature, the digital signal has been derived by sampling and quantization. Digital signal processing (DSP) is the analysis and processing of digital signals.
Sampling and quantization is performed by a analog-to-digital converter (ADC). The digital signal can then be processed by DSP processors. If needed, it can be converted back to a continuous signal by a digital-to-analog converter (DAC). ADCs and DACs are embedded in a wide range of devices.
E.g. the electrical signal produced by the microphone on a laptop is fed to a built-in ADC, and this signal can then be compressed by a DSP processor to be sent over the Internet. Conversely, a DAC converts the digital sound signals to continuous electrical signals so they can be emitted by the laptop speakers.
A typical signal processing chain is depicted below.
The sampling of a continuous signal can be described as follows. Let
A digital representation of
where
Example: Common audio sampling frequencies are 8 kHz (telecommunications), 44.1 kHz (music CDs) and 48 kHz (movie tracks).
Note: In signal processing, notations like
Note: The signals above were introduced as taking values in
The sampling of a continuous signal can be seen as the product of the original signal with a Dirac comb. The Dirac comb with period
where
Sampling
This will be useful in the following.
The convolution is a mathematical operation between two functions and outputs a new function. The convolution operator is denoted as
$$ \begin{aligned}
- \colon L^1(\mathbb{R}) \times L^1(\mathbb{R}) &\longrightarrow L^1(\mathbb{R}) \ f, g &\longmapsto f * g \end{aligned} $$
It is defined as
The convolution is commutative:
The discrete convolution is the adaptation to discrete signals and is defined as
For discrete signals with finite lengths, signal values outside the definition range are assumed to be 0, and the sum becomes finite as most of the terms equal zero. E.g. if
The convolution can be seen as a time-inversion of one of the signals, followed by a "delay-and-product-sum" operation; for each delay value
Example: The identity element for the convolution is the Dirac delta impulse,
You can prove it as an exercise.
Example: The L-point moving average of a time series can be expressed as a convolution. Consider
where
Below I use numpy.convolve to convolve an arbitrary signal with
import matplotlib.pyplot as plt
import numpy as np
plt.rcParams['axes.grid'] = True
n = 11 # number of points
x = np.linspace(0, 1, n) ** 2 ; x = np.minimum(x, x[::-1]) # arbitrary signal
L = 5 # number of points to average
h = np.ones(L) / L
y = np.convolve(h, x)
fig, ax = plt.subplots(figsize=(10, 3))
ax.plot(x, 'o-', label='$x$')
ax.plot(h, 'o-', label='$h$')
ax.plot(y, 'o-', label='$y=h*x$')
ax.legend()
plt.show()Let
A periodic signal can also be characterized by its frequency
Examples of periodic signals:
- Sinusoids: $ x(t) = \sin(2 \pi f t), \forall t \in \mathbb{R} $
- Complex exponentials: $ x(t) = e^{i 2 \pi f t}, \forall t \in \mathbb{R} $
- Temperature across years (disregarding rising trend due to global warming)
Any continuous periodic signal can be written as a discrete sum of complex exponentials called Fourier series.
Let
The
If
This means that we can think of any periodic signal as a sum of well-known functions, the complex exponentials, which form a basis of functions in the
The Fourier series are a primary motivation of the Fourier transform (see further below).
Example: Let
Euler's formula allows to rewrite
Here the Fourier coefficients can be directly identified. We have
-
$c_1 = -\frac{i}{2}$ , -
$c_{-1} = \frac{i}{2}$ , -
$c_n = 0$ if$n \notin {-1, 1}$ .
Example: Let
It can be shown that
The Fourier coefficients here are
-
$c_k = \frac{i}{\pi}\frac{(-1)^k}{k}$ if$k>0$ , -
$c_k = -\frac{i}{\pi}\frac{(-1)^k}{k}$ if$k<0$ , -
$c_0 = 0$ if$k=0$ .
Or using
-
$b_k = -\frac{2}{\pi}\frac{(-1)^k}{k}$ for all$k>0$ , -
$a_k = 0$ for all$k\geq 0$ .
In the snippet below I verify this by adding a finite amount of these sines. The more sines are added, the more the total sum resembles a sawtooth wave.
n = 500 # number of points
n_T = 3 # number of periods
t = np.arange(n) / n * n_T * 2 * np.pi
x = (t / np.pi + 1) % 2 - 1
fig, ax = plt.subplots(figsize=(12, 4))
ax.plot(t, x, label='sawtooth')
for n_components in [1, 2, 5, 10]:
x = 0
for k in range(1, n_components + 1):
x += -2 / np.pi * (-1) ** k / k * np.sin(k * t)
ax.plot(t, x, label=f'{k} components')
ax.legend()
plt.show() # you should see the more sines we add, the closer the total sum resembles a sawtooth waveThe Fourier transform is a mathematical operation that decomposes time-domain functions into frequency-domain functions. The term Fourier transform can refer to both the frequency-domain representation of a signal and the mathematical operation itself.
The Fourier transform is first formally defined for continuous signals (not necessarily periodic) and outputs a new continuous function depending on frequency. It is commonly denoted as
In other words, given
This can be seen as the projection of
Notes and properties:
- The Fourier transform of
$x$ is a function of$\omega$ which is a frequency variable in radian per second (rad/s). Sometimes, a frequency variable in Hertz and denoted as$f$ is used instead, in which case$\omega=2\pi f$ and the integral is changed accordingly. - The Fourier transform takes complex values
- The Fourier transform is linear:
$\mathcal{F}(\alpha x + \beta y)=\alpha\mathcal{F}(x)+\beta\mathcal{F}(y)$ - It is common to denote the Fourier transform of
$x$ with an uppercase as follows:$\mathcal{F}(x)=X$ .- Sometimes it is denoted as follows to emphasize on the dependent variables:
$\mathcal{F}[x(t)] = X(\omega)$
- Sometimes it is denoted as follows to emphasize on the dependent variables:
- The inverse Fourier transform of
$X$ is $$ \mathcal{F}^{-1}(X)(t) = \frac{1}{2\pi}\int_{-\infty}^{+\infty}X(\omega)e^{i\omega t}d\omega, \quad \forall t \in \mathbb{R}, $$ which is the same as the forward Fourier transform except there is a normalization factor and a plus sign in the exponential.
Let
Once again this resembles a projection on the basis of complex exponentials, except it is adapted for a discrete signal by replacing the integration sign with a discrete sum over the signal values.
The DTFT is
This is a first adaptation for discrete signals, except the summation is infinite and it still takes values in an infinite and continuous frequency space. The next step is to truncate and sample the DTFT at evenly spaced frequency points, to obtain discrete Fourier transform (DFT).
Let
The inverse DFT is
The DFT takes as input a discrete and finite amount of values and outputs a discrete and finite amount of values, so it can be evaluated in computers, unlike the DTFT.
Below is an overview of the different transforms. For a more complete explanation of the steps between Fourier transform, DTFT and DFT, you can refer to Proakis and Manolakis, Chapters 4 and 7.
The DFT decomposes signals into frequencies. The frequency components are best observed by plotting the modulus (sometimes squared) of the Fourier transform. The modulus of the Fourier transform is often referred to as magnitude spectrum, and the analysis of signals using the Fourier transform as spectral analysis. The phase information is more difficult to interpret and can be disregarded for course 02471.
The DFT is implemented in numpy under numpy.fft.fft. FFT stands for Fast Fourier Transform and is an efficient algorithm to calculate the DFT. The terms FFT and DFT are often used interchangeably.
Example: Let's create a signal consisting of a sum of 2 sinusoids with different frequencies and calculate its DFT. You should see how the DFT is able to resolve the 2 components.
n = 512 # number of points
fs = 16e3 # sampling frequency
f1 = 2000 # frequency of the first component
f2 = 4000 # frequency of the second component
t = np.arange(n) / fs # time axis
x = np.sin(2 * np.pi * f1 * t) + np.sin(2 * np.pi * f2 * t) # time-domain signal
X = np.fft.fft(x) # DFT
f = np.arange(n) / n * fs # frequency axis; see details further below
fig, axes = plt.subplots(1, 2, figsize=(14, 4))
axes[0].plot(t, x)
axes[0].set_title('Time domain')
axes[0].set_xlabel('Time (s)')
axes[1].plot(f, np.abs(X)) # we plot the magnitude as X is complex
axes[1].set_title('Frequency domain')
axes[1].set_xlabel('Frequency (Hz)')
plt.show() # you should see two clean spikes at locations corresponding to f1 and f2
# you should also see two extra spikes at fs-f1 and fs-f2; see details further belowA few practical notes:
- The DFT output is two-sided. Half of its values correspond to negative frequencies. This makes sense for complex-valued signals, but for real-valued signals, values at opposite frequencies are conjugates and the information is thus redundant. It is common to crop half of the FFT output. For more details, you can also check the documentation for
numpy.fft.rfft, which outputs a one-sided signal. - Building the frequency vector axis for the output can be confusing. You should keep in mind that the resolution in the frequency domain is
$\frac{f_s}{N}$ , where$f_s$ is the sampling frequency and$N$ is the number of points.-
This means that increasing
$f_s$ gives a worse resolution in the frequency domain. Conversely a finer resolution in the time domain means a coarser resolution in the frequency domain. This is the time-frequency duality. -
The frequency values corresponding to the raw FFT output are ordered as follows:
- if N is even:
$$ 0,\ \frac{f_s}{N},\ ...,\ \frac{N}{2}\frac{f_s}{N},\ (-\frac{N}{2}+1)\frac{f_s}{N},\ ...,\ -\frac{f_s}{N}$$
- if N is odd:
$$ 0,\ \frac{f_s}{N},\ ...,\ \frac{N-1}{2}\frac{f_s}{N},\ -\frac{N-1}{2}\frac{f_s}{N},\ ...,\ -\frac{f_s}{N}$$
We first have the positive frequencies in increasing order up to
$f_s$ , and then the negative frequencies increasing from$-\frac{f_s}{2}$ to 0. These frequency values are commonly called frequency bins, as if the energy is falling in bins centered at those frequencies.
-
FAQ:
-
But in the figure above you drew the positive frequencies above $\frac{f_s}{2}$?
- Yes, the frequencies above
$\frac{f_s}{2}$ correspond to the negative frequencies. I didn't remove them to show how the raw FFT output looks like. Normally one would discard them usingnumpy.fft.rfft. If you want to plot the entire spectrum including negative frequencies, then you would usually flip the two halves and rename the frequency axis to range from -$\frac{f_s}{2}$ to$\frac{f_s}{2}$ . This can be done usingnumpy.fft.fftshift.
- Yes, the frequencies above
-
What about the frequencies above $\frac{f_s}{2}$ present in the signal?
- If the sampling frequency is
$f_s$ , then the maximum representable frequency in the digital signal is$\frac{f_s}{2}$ . In other words, if a continuous signal is sampled at a sampling frequency$f_s$ , then all the information at frequencies above$\frac{f_s}{2}$ is lost. This is the Nyquist-Shannon sampling theorem (see further below).
- If the sampling frequency is
-
Considering if $N$ is even or odd is tedious... How can I easily and consistently build the frequency vector axis correctly?
-
You can use
numpy.fft.fftfreqornumpy.fft.rfftfreq. You can also do it manually as follows:- Remember the resolution is always
$\frac{f_s}{N}$ and build the entire frequency vector of length$N$ including the frequencies above$\frac{f_s}{2}$ :f = np.arange(n) / n * fs - Find the frequencies above
$\frac{f_s}{2}$ :mask = f > fs/2- For a one-sided spectrum, discard them:
f = f[~mask] - For a two-sided spectrum, subtract
$f_s$ to them:f[mask] -= fs
- For a one-sided spectrum, discard them:
This will consistently give a correct frequency axis regardless of
$N$ being even or odd. - Remember the resolution is always
-
More examples: Below I plot a series of common Fourier transforms.
n_rows = 8
fig, axes = plt.subplots(n_rows, 2, figsize=(12.5, 9))
axes[0, 0].set_title('Time domain')
axes[0, 1].set_title('Frequency domain')
for row in range(n_rows-1):
axes[row, 0].set_xticklabels([])
axes[row, 1].set_xticklabels([])
n = 512
t = np.arange(n) - n // 2
f = np.fft.fftfreq(n) ; f = np.fft.fftshift(f)
def plot_row(row, x, ylabel):
X = np.fft.fft(x) ; X = np.fft.fftshift(X)
axes[row, 0].plot(t, x)
axes[row, 1].plot(f, abs(X))
axes[row, 0].set_ylabel(ylabel)
# dirac
x = np.zeros(n) ; x[n // 2] = 1
plot_row(0, x, 'dirac')
# constant
x = np.ones(n)
plot_row(1, x, 'constant')
# rectangle
x = abs(t) < n * 0.025
plot_row(2, x, 'rectangle')
# sinc
x = np.sinc(t * 0.1)
plot_row(3, x, 'sinc')
# comb
x = np.zeros(n) ; x[t % (n // 32) == 0] = 1
plot_row(4, x, 'comb')
# sine
x = np.sin(2 * np.pi * t * 0.05)
plot_row(5, x, 'sine')
# cosine
x = np.cos(2 * np.pi * t * 0.05)
plot_row(6, x, 'cosine')
# sawtooth
x = (t * 0.1 + 1) % 2 - 1
plot_row(7, x, 'sawtooth')
fig.tight_layout()
plt.show()Some important Fourier transforms (FT) to note:
- The FT of a Dirac impulse is an infinitely-long rectangular window. Conversely, the FT of an infinitely-long rectangular window is a Dirac impulse.
- This again is related to the time-frequency duality. A signal that is very narrow in time is very broad in frequency. Conversely, a signal that is very localized in frequency is very broad in time.
- The FT of a rectangular window is a sinc function (the time-domain plot is the absolute value). Conversely, the FT of a sinc function is a rectangular window.
- In accordance with the time-frequency duality, the narrower the rectangular window in one domain, the wider the sinc function in the other domain.
- The FT of a Dirac comb is also a Dirac comb.
- This will be useful when explaining the Nyquist-Shannon sampling theorem further below.
- The FT of the sawtooth signal shows decaying spikes equally spaced at multiples of the fundamental frequency, in accordance with the Fourier series expression introduced further above.
The convolution theorem states that convolution in the time domain is the same as multiplication in the frequency domain. Conversely, multiplication in the time domain is the same as convolution in the frequency domain.
In the continuous case, let
This means we can derive a convolution in the frequency domain, which is sometimes more efficient. Indeed, an immediate consequence is
I.e. the same result can be obtained with a multiplication in the frequency domain, and back-and-forth Fourier transforms to switch between domains.
Now for discrete signals, let
Note that if
In the snippet below I attempt to verify both properties.
n = 128 # number of points
fig, axes = plt.subplots(2, 1, figsize=(12, 6))
# first property: F(x*y)=F(x)F(y)
x = np.random.randn(n)
y = np.random.randn(n)
z = np.convolve(x, y)
X = np.fft.fft(x, n=len(z)) # forcing the FFT output to be same length as z
Y = np.fft.fft(y, n=len(z)) # forcing the FFT output to be same length as z
Z1 = np.fft.fft(z)
Z2 = X * Y
axes[0].plot(abs(Z1), label=r'$\mathcal{F}(x*y)$')
axes[0].plot(abs(Z2), label=r'$\mathcal{F}(x)\mathcal{F}(y)$')
axes[0].legend(loc='upper right')
# second property: F(xy)=F(x)*F(y)
# this one is a bit trickier as we need to flip the FFTs before convolving
# we also need to filter out all the extra frequencies resulting from the convolution in the frequency domain
x = np.sin(2 * np.pi * np.arange(n) * 0.3) # using random noise here does not give perfect result
y = np.sin(2 * np.pi * np.arange(n) * 0.1) # using random noise here does not give perfect result
z = x * y
X = np.fft.fft(x)
Y = np.fft.fft(y)
X = np.fft.fftshift(X) # flip before convolving
Y = np.fft.fftshift(Y) # flip before convolving
Z1 = np.fft.fft(z)
Z1 = np.fft.fftshift(Z1)
Z2 = np.convolve(X, Y) / n
Z2 = Z2[n // 2 : -n // 2 + 1] # discard extra frequencies created from the convolution
axes[1].plot(abs(Z1), label=r'$\mathcal{F}(xy)$')
axes[1].plot(abs(Z2), label=r'$\frac{1}{N}\mathcal{F}(x)*\mathcal{F}(y)$')
axes[1].legend(loc='upper right')
plt.show() # you should observe the curves overlap in both plotsWhen sampling at a frequency
A simple example to illustrate this is the sampling of a sinusoid. Consider a sinusoid with frequency
f0 = 100 # sinusoid frequency
T = 2e-2 # sinusoid duration in seconds
# first create a sinusoid with a fine time step; this will represent the continuous signal
fs_hi = 8e3 # high sampling frequency
t_cont = np.arange(0, T + 1 / fs_hi, 1 / fs_hi) # fine time vector with time step 1/fs
x_cont = np.sin(2 * np.pi * f0 * t_cont) # this represents the continuous signal
fig, axes = plt.subplots(1, 3, figsize=(16, 3))
# now let's create a coarse digital signals for different low sampling frequencies
for ax, fs_lo in zip(axes, [1000, 500, 200]):
ax.plot(t_cont, x_cont)
t_coarse = np.arange(0, T + 1 / fs_lo, 1 / fs_lo)
x_coarse = np.sin(2 * np.pi * f0 * t_coarse)
ax.stem(t_coarse, x_coarse, 'k', markerfmt='ko', basefmt=' ')
ax.axhline(0, color='k')
ax.set_title(f'$f_s={fs_lo}$ Hz')
plt.show()If
fig, ax = plt.subplots(figsize=(6, 3))
f0 = 100
f1 = 80
x_cont = np.cos(2 * np.pi * f0 * t_cont)
ax.plot(t_cont, x_cont, label=f'$f_0={f0}$ Hz')
x_cont = np.cos(2 * np.pi * f1 * t_cont)
ax.plot(t_cont, x_cont, label=f'$f_1={f1}$ Hz')
fs_lo = 180
t_coarse = np.arange(0, T, 1 / fs_lo)
x_coarse = np.cos(2 * np.pi * f0 * t_coarse)
ax.stem(t_coarse, x_coarse, 'k', markerfmt='ko', basefmt=' ')
ax.axhline(0, color='k')
ax.set_title(f'$f_s={fs_lo}$ Hz')
ax.legend()
plt.show()Both signals produce the exact same samples. It is therefore impossible to know from the samples if the signal is a sinusoid at 80 Hz or 100 Hz.
This can be explained for any signal using the convolution theorem and the Dirac comb function. Remember we saw that sampling is the same as multiplying the signal with the Dirac comb,
where
Now it can be shown that the Fourier transform of a Dirac comb is also a Dirac comb (you can try to prove it as an exercise) with period
Therefore, sampling in the time domain is the same as convolving with a Dirac comb in the frequency domain. And convolving with a Dirac comb is the same as replicating the signal infinitely, with replicas evenly spaced by
The image below illustrates this.
If
We wish to perform the spectral analysis of a short audio recording of 1 second. At 44.1 kHz, this recording would consist of 44100 samples. One option is to perform the 44100-point FFT of the entire signal, and get a spectrum with 44100 frequency bins.
This not very efficient (the FFT complexity is
A better option is to segment the signal in the time domain and perform the FFT of each segment. This is called a Short-time Fourier transform (STFT). The output is a representation of the signal that is both a function of time (frame index) and frequency.
The STFT of
where
and
-
$k$ is the frequency bin index$\in {0, 1, ..., M-1}$ -
$l$ is the frame index -
$N$ is the frame length — the number of signal samples in each segment -
$H$ is the hop length — the number of signal samples between adjacent segments- Sometimes the overlap length
$O$ is specified instead:$O=N-R$
- Sometimes the overlap length
-
$w$ is the analysis window function of length$N$ — commonly used to reduce spectral leakage- Spectral leakage is not covered in this notebook but you can refer to Oppenheim, Schafer and Buck, Chapter 10
-
$M$ is the number of points after zero-padding the windows — the number of FFT points
The STFT is implemented in scipy under scipy.signal.stft,
- the
npersegargument corresponds to$N$ - the
noverlapargument corresponds to$O$ - the
nfftargument corresponds to$M$ - the
windowargument corresponds to$w$
The function also takes as arguments the sampling frequency fs to return the corresponding time and frequency vectors.
In the example below I generate a sinusoid whose frequency is modulated by another sinusoid. The FFT of the entire signal shows high values across the entire range of swept frequencies, while the STFT allows to observe how the frequency changes over time.
from scipy.signal import stft
fs = 4e3 # sampling frequency
f_mod = 1 # modulation frequency
f_delta = 200 # modulation depth
f0 = 800 # carrier frequency
T = 5 # signal duration
t = np.arange(0, T, 1 / fs) # time vector
x = np.sin(2 * np.pi * t * f0 + f_delta / f_mod * np.sin(2 * np.pi * t * f_mod))
fig, axes = plt.subplots(3, 1, figsize=(12, 7))
axes[0].plot(t, x)
axes[0].set_title('Raw time-domain signal')
axes[0].set_xlabel('Time (s)')
axes[0].set_ylabel('Amplitude')
X = np.fft.rfft(x)
f = np.fft.rfftfreq(len(x), 1 / fs)
axes[1].plot(f, abs(X))
axes[1].set_title('FFT')
axes[1].set_xlabel('Frequency (Hz)')
axes[1].set_ylabel('Magnitude')
f, t, S = stft(x, fs, nperseg=512)
axes[2].grid(False)
axes[2].pcolormesh(t, f, abs(S), shading='gouraud')
axes[2].set_title('STFT')
axes[2].set_xlabel('Time (s)')
axes[2].set_ylabel('Frequency (Hz)')
fig.tight_layout()
plt.show()FAQ:
- What if I still want to have a representation of my signal that depends only on frequency? E.g. if I am interested in the average energy in each frequency bin?
- You can use the STFT and average the energy across frames. This is the long-term average spectrum (LTAS).
from scipy.signal import stft
fig, axes = plt.subplots(2, 1, figsize=(12, 5))
X = np.fft.rfft(x)
f = np.fft.rfftfreq(len(x), 1 / fs)
axes[0].plot(f, abs(X))
axes[0].set_title('FFT (ugly)')
axes[0].set_xlabel('Frequency (Hz)')
axes[0].set_ylabel('Magnitude')
f, t, S = stft(x, fs, nperseg=512)
ltas = np.mean(abs(S), axis=1)
axes[1].plot(f, ltas)
axes[1].set_title('LTAS (clean)')
axes[1].set_xlabel('Frequency (Hz)')
axes[1].set_ylabel('Magnitude')
fig.tight_layout()
plt.show()- But the magnitude (y-axis) does not match!
- There are different ways of normalizing the FFT and the STFT. Here the STFT implementation of
scipyapplies a normalization whereasnp.fft.fftdoes not. Windowing also has a influence. But this does not matter so much, provided you use the same method across signals.
- There are different ways of normalizing the FFT and the STFT. Here the STFT implementation of
- How do I chose the length of the frame/window (
npersegargument)?- This is for you to decide. A shorter frame size will give you a better time resolution, but a worse frequency resolution. Conversely a longer frame gives a worse time resolution, but a better frequency resolution. This is again the time-frequency duality. However the FFT is the fastest for signal lengths equal to a power of 2. So common frame sizes for audio analysis are e.g. 256, 512, 1024, 2048 or 4096, depending on the sampling rate. For other applications with different sampling rates, frame sizes must be adapted accordingly.
A filter is a broad term for a system which takes a signal as an input and outputs a new signal.
Filters can be analog for continuous signals (electronics circuits consisting of capacitors and coils in e.g. guitar amps or speaker crossover filters), or digital for discrete signals (integrated circuits). In this notebook we only cover digital filters.
In DSP, common filters are linear time-invariant (LTI) systems. Consider two digital signals
- Linearity:
$\forall\alpha,\beta\in\mathbb{R},\ \mathcal{H}(\alpha x[n]+\beta y[n])=\alpha\mathcal{H}(x[n])+\beta\mathcal{H}(y[n])$ - Can also be written as follows:
$\forall\alpha,\beta\in\mathbb{R},\ \mathcal{H}(\alpha x+\beta y)=\alpha\mathcal{H}(x)+\beta\mathcal{H}(y)$
- Can also be written as follows:
- Time-invariance:
$\forall m\in\mathbb{Z},\ \mathcal{H}(x[n-m])=\mathcal{H}(x)[n-m]$ - Can also be written as follows:
$\forall m\in\mathbb{Z},\ \mathcal{H}(n\mapsto x[n-m])=n\mapsto\mathcal{H}(x)[n-m]$ - In other words, LTI systems do not change over time; if the input is delayed, the output is also delayed by the same amount.
- Can also be written as follows:
In the following,
A filter can be described by its impulse response. The impulse response is, as the name suggests, the output of the filter when presented a Dirac impulse
The reason why
Therefore if we know
A digital filter can also be charaterized by its difference equation,
or, with the output
- The
$a_m$ are the feedback coefficients (similar to autoregressive coefficients in time series analysis) - The
$b_m$ are the feedforward coefficients (similar to moving-average coefficients in time series analysis) - The filter order is
$\max(M, N)$
Note that we usually force
Examples:
- L-point moving average filter:
Here
- Exponential smoothing with factor
$0<\alpha<1$ :
Here
If there are no feedback coefficients (except
FIR filters are very stable, but computationally more expensive.
The impulse response of an FIR filter
Therefore the impulse response of an FIR filter is simply the sequence of feedforward coefficients:
Example: The L-point moving average filter is an FIR filter. Its impulse response is
If there is at least one feedback coefficient (other than
IIR filters can be unstable, but are computationally much cheaper (FIR filters require more taps compared to IIR filters to acheive the same frequency response).
The impulse response of an IIR cannot be evaluated; it is infinite.
Example: The exponential smoothing filter is an IIR filter,
Attempting to write the impulse response would look like this,
Moreover if
Example:
Digital filtering is implemented under scipy.signal.lfilter. The function takes as arguments the sequence of feedforward coefficients, the sequence of feedback coefficients and the input signal. Note the first feedforward coefficient, i.e.
Below I filter a noisy signal with a moving average filter and an exponential smoothing filter using scipy.signal.lfilter.
from scipy.signal import lfilter
# an arbitrary noisy input signal
n = 512 # signal length
x = 0.5 * np.random.randn(n) + np.cos(2 * np.pi * np.arange(n) * 0.01) + np.cos(2 * np.pi * np.arange(n) * 0.005)
# moving average filter
L = 10 # number of points to average
b = np.ones(L) / L # feedforward coefficients
a = [1] # feedback coefficients
y1 = lfilter(b, a, x)
# exponential smoothing filter
alpha = 0.9
b = [1-alpha] # feedforward coefficients
a = [1, -alpha] # feedback coefficients
y2 = lfilter(b, a, x)
fig, ax = plt.subplots(figsize=(13, 4))
ax.plot(x, label='input signal', alpha=0.7)
ax.plot(y1, label='moving average')
ax.plot(y2, label='exponential smoothing')
ax.legend()
fig.tight_layout()
plt.show()We saw that a filter can be characterized with the impulse response; filtering is the same as convolving with the impulse response of the filter. The convolution theorem tells us that a convolution in the time domain is the same as a multiplication in the frequency domain. Therefore, we can perform a time-domain filtering operation in the frequency domain, by multiplying the Fourier transform of the input signal with the Fourier transform of the impulse response,
Another way to look at it is as follows. Consider a fixed
Note: Here
As seen, the pure tone
- But what if the filter is an IIR filter? For an FIR filter, $H(\omega)$ can be obtained by calculating the DTFT of the impulse response, which is simply the sequence of feedforward coefficients. But for an IIR, the impulse response is infinite!
We can still define the frequency response as follows. We saw above that if
Note that if there are no feedback coefficients except
Example:
A filter frequency response can be calculated using scipy.signal.freqz. The function takes as arguments the sequence of feedforward coefficients and the sequence of feedback coefficients. It can also take the number of evenly spaced frequency points worN at which the frequency response is calculated. The function outputs a frequency vector and the complex frequency response. Note the frequency vector ranges from 0 to fs argument to scipy.signal.freqz and the frequency vector output will be correctly scaled.
Let's calculate the frequency responses of the moving average and the exponential smoothing filters.
from scipy.signal import freqz
# moving average filter
L = 10 # number of points to average
b = np.ones(L) / L # feedforward coefficients
a = [1] # feedback coefficients
w1, h1 = freqz(b, a)
# exponential smoothing filter
alpha = 0.9
b = [1-alpha] # feedforward coefficients
a = [1, -alpha] # feedback coefficients
w2, h2 = freqz(b, a)
fig, ax = plt.subplots(figsize=(12, 4))
ax.plot(w1, abs(h1), label='moving average')
ax.plot(w2, abs(h2), label='exponential smoothing')
ax.legend()
fig.tight_layout()
plt.show()It can be seen that both filters act as low-pass filters. They present a high gain at low frequencies, and the gain decreases as the frequency increases. This means the filters attenuate high frequencies, while they let low frequencies pass through. This makes sense, since the filters smooth out the fast and noisy fluctuations, which are the high frequencies.
FAQ:
- Can I use
np.fft.fftinstead ofscipy.signal.freqzto plot the frequency response?- You can technically obtain the same result with
np.fft.fftif you discard the negative frequencies and if you provide the right number of FFT pointsn. But I recommend usingscipy.signal.freqzinstead, since it is specifically meant for filters; it takes coefficientsbandaas arguments and outputs a one-sided, well-defined frequency response. Usingscipy.signal.freqzfor a filter shows you understand what you are doing.
- You can technically obtain the same result with
- Can I use
scipy.signal.freqzto plot a signal spectrum?- No! A signal presents a spectrum (not a frequency response), while a filter is a system that has input/output signals and thus presents a frequency response (not a spectrum). Filter
$\sim$ frequency response, signal$\sim$ spectrum. If you usescipy.signal.freqzto plot the frequency content of a signal, then the signal would be cropped internally and only the firstworNsamples would be analyzed. For signal spectrums, please usescipy.signal.stftand average the energy across time-frames (see LTAS earlier).
- No! A signal presents a spectrum (not a frequency response), while a filter is a system that has input/output signals and thus presents a frequency response (not a spectrum). Filter
Other important DSP subjects not covered in this notebook include:
- window functions
- spectral leakage
- filter design
- biquad filters
- Z-transform
- power spectral density
If you find typos, I would greatly appreciate it if you reported them ❤️.
- Alan V. Oppenheim, Ronald W. Schafer and John R. Buck. Discrete-time signal processing (2nd ed.). Prentice Hall, 1999.
- John G. Proakis and Dimitris G. Manolakis. Introduction to Digital Signal Processing : Principles, Algorithms and Applications (4th ed.). Pearson Prentice Hall, 2007.
- Sascha Spors, Digital Signal Processing - Lecture notes featuring computational examples.
- Lecture notes on 22001 Acoustic signal processing by Tobias May at the Technical University of Denmark


















