import matplotlib
if not hasattr(matplotlib.RcParams, "_get"):
matplotlib.RcParams._get = dict.get
8.4 The discrete Fourier transform#
We now have everything we need. We substitute the discrete analysis frequencies \(\omega_k\) into our work-in-progress transform and apply the simplification \(e^{-j\omega_k n \Delta t} = e^{-2\pi j k n / N}\) from the previous section:
The result is a clean expression that depends only on the samples and the indices. This is the discrete Fourier transform.
Definition 23 (Discrete Fourier transform)
The discrete Fourier transform of a length-\(N\) signal \(x[n]\) is the length-\(N\) sequence
Intuitively, the DFT does exactly what the Fourier transform did, just over a finite set of frequencies. For each of the \(N\) bins \(k\) (the name for these discrete analysis frequencies), it synthesizes a phasor at \(\omega_k\), multiplies it by the signal to measure their similarity, and sums the result. We are effectively searching a finite set of bins for frequencies that resemble the signal.
Another observation is that the DFT is no longer a function of the sampling rate \(f_s\) at all. Instead, it depends only on \(N\), the number of input samples. This is perhaps counterintuitive, since we originally defined the bin frequencies \(\omega_k\) by dividing the sampling rate by \(N\). The sampling rate has not vanished, though: it is still what tells us the physical frequency, in Hz, that each bin corresponds to. To see this, it helps to work out how far apart adjacent bins are.
Definition 24 (DFT bin spacing)
The DFT bins for \(N\) input samples are evenly spaced in frequency, covering \(f_s\) over \(N\) bins. Recall that \(N = T \cdot f_s\), where \(T\) is duration in seconds. Accordingly:
This yields two equivalent forms:
These two forms highlight some important properties of the DFT. The bin spacing \(\Delta f = 1/T\) depends only on the duration \(T\) of the analyzed segment, not on the sampling rate. Analyzing a longer stretch of audio always gives finer frequency resolution, no matter what \(f_s\) is.
The number of bins, on the other hand, is \(N = T f_s\), which grows with the sampling rate. So for a fixed duration, raising the sampling rate gives you more bins (extending the analysis up to a higher Nyquist frequency), but it does not pack the bins any closer together.
Real and imaginary parts#
Applying Euler’s formula to the definition splits the DFT into a real and an imaginary part, exactly as with the continuous transform:
As before, we usually care about the amplitude spectrum and phase spectrum, obtained by converting each complex bin to polar form:
Intuition: the “winding” view#
The following interactive example makes the “multiply by a phasor and sum” intuition concrete, in the spirit of the winding visualization from Chapter 5. Adjust the frequency of a real input sinusoid and the frequency of the probing phasor, and watch the wound-up signal and its center of mass in the complex plane. When the probe frequency matches a bin containing signal energy, the center of mass swings far from the origin:
Removing redundancy#
What is the “type signature” of the DFT? It takes \(N\) real samples and returns \(N\) complex numbers, so \(\texttt{DFT} : \mathbb{R}^N \to \mathbb{C}^N\). Since a computer stores each complex number as two floats (its real and imaginary parts), we could also view it as \(\mathbb{R}^N \to \mathbb{R}^{2N}\). But that feels wasteful. The DFT, like the Fourier transform, is an invertible bijection, so turning \(N\) numbers into \(2N\) numbers must involve redundancy.
Indeed it does, and the redundancy comes from the symmetry of real signals we met in Chapter 6. Because cosine is even and sine is odd, the DFT of a real signal satisfies
So the upper half of the bins is just a mirror image of the lower half. This is the same even/odd symmetry of the amplitude and phase spectra from Chapter 6. We can tabulate it for a small example, \(N = 8\) at \(f_s = 1000\) Hz:
\(k\) |
\(\blue{0}\) |
\(\blue{1}\) |
\(\blue{2}\) |
\(\blue{3}\) |
\(\blue{4}\) |
\(\red{5}\) |
\(\red{6}\) |
\(\red{7}\) |
|---|---|---|---|---|---|---|---|---|
Frequency (Hz) |
0 |
125 |
250 |
375 |
500 |
625 |
750 |
875 |
Aliased (Hz) |
0 |
125 |
250 |
375 |
500 |
\(-375\) |
\(-250\) |
\(-125\) |
\(R[k]\) |
\(\blue{a}\) |
\(\blue{b}\) |
\(\blue{c}\) |
\(\blue{d}\) |
\(\blue{e}\) |
\(\red{d}\) |
\(\red{c}\) |
\(\red{b}\) |
\(I[k]\) |
\(\purple{0}\) |
\(\blue{g}\) |
\(\blue{h}\) |
\(\blue{i}\) |
\(\purple{0}\) |
\(\red{-i}\) |
\(\red{-h}\) |
\(\red{-g}\) |
Two additional optimizations appear in the table. The imaginary part vanishes at both ends, \(I[0] = 0\) and \(I[N/2] = 0\), because \(\sin(0) = 0\) and \(\sin(\pi n) = 0\) for all integer \(n\). Counting what is left, we need only the bins \(k = 0, 1, \ldots, N/2\), which is \(N/2 + 1\) complex bins, but with two of them (\(k=0\) and \(k=N/2\)) purely real-valued. That works out to exactly \(N\) real numbers to store, matching the \(N\) real inputs. The bijection is tidy after all: \(N\) samples in, \(N\) non-redundant coefficients out.
Important
For a real-valued signal of length \(N\), the DFT has only \(N/2 + 1\) non-redundant bins, spanning \(0\) to \(f_s/2\). This is exactly what NumPy’s np.fft.rfft (“real FFT”) returns, and it is what you will use in practice.
This is an important from an efficiency perspective as well. Because of this symmetry, we only have to run half as many computations! However, this does not change the asymptotic complexity of the DFT, it only improves the constant factor runtime. More on that next.