This section is about the Fourier transform, the tool that turns a list of audio samples into a list of frequencies. It rewrites a short block of samples as a sum of pure tones and reports how strong each tone is. Almost every speech and audio model starts with it, because sounds differ in where their energy sits in frequency, and the raw samples hide that. By the end you will compute the transform by hand and in code, know exactly what it shows and what it smears, and turn a recording into the spectrogram that speech models read.
Here is the problem in one example. Play an A (440 Hz) and the E above it (659.3 Hz) together. The samples form a wiggle that never quite repeats, with no visible trace of either note. A speech recognizer has the same problem with every slice of speech. For each 25 ms of audio it needs to know how much energy sits near 300 Hz, near 1,200 Hz, and near 3,000 Hz, because vowels and consonants differ in exactly that. The Fourier transform pulls those numbers, and the two notes, back out.
A digital recording, as §35.1 built it, is a list of numbers. Each number is the air pressure at the microphone, measured fs times per second, and speech models use fs=16,000 Hz. This section turns such a list into frequencies in three steps.
The discrete Fourier transform (DFT) rewrites a block of N samples as a combination of N sampled pure tones. Nothing is lost, and the block can be rebuilt exactly.
The fast Fourier transform (FFT) computes the DFT with about 2Nlog2N multiplications instead of N2.
The short-time Fourier transform (STFT) slides a short window along the signal and takes the DFT of each slice. Its squared magnitudes form the spectrogram, a matrix with one row per slice and one column per frequency. This is the first time in the chapter that sound becomes a sequence of vectors.
Two facts come up again and again, and the section explains both exactly. A tone that does not complete a whole number of cycles in the block smears across every frequency. And no window is sharp in both time and frequency at once, which is why speech systems settle on windows of about 25 ms.
Sound is a sum of vibrations, so change basis
A vector can be written in many coordinate systems. Recall from §0.10 that an orthonormal basis is a set of unit vectors u1,…,uN at right angles to one another. The coordinate of a vector x along uk is the inner product ck=uk⊤x, and x=∑kckuk. The Fourier transform is that idea and nothing more. The block of samples is a vector x∈RN, and the new basis is a set of N sampled pure tones.
Why that basis? In the usual basis each coordinate is one sample, and a pure tone is spread over all N of them. In a basis of tones, a pure tone is a single coordinate and a chord is a few. Sound is made by things that vibrate, such as strings, air columns, and vocal folds. So most of its energy lands in a few coordinates of the tone basis, and those coordinates are easy to read.
The tones we use are complex, because complex tones turn every proof into a geometric series. Euler's formula, recalled from §5.5, says eiα=cosα+isinα, the point at angle α on the unit circle. Multiplying a complex number by eiα rotates it by α and leaves its length alone. A complex sinusoid at k cycles per block is the sequence
In plain words,bk is a point that walks around the unit circle, k full turns in N steps. A real cosine is the average of two such points turning in opposite directions, cosθ=21(eiθ+e−iθ). The clockwise one is said to have a negative frequency. That is all a negative frequency is, the mirror half of every real tone.
Complex vectors need one change to the inner product. It conjugates its second argument, ⟨x,y⟩=∑nx[n]y[n], so that ⟨x,x⟩=∑n∣x[n]∣2 is a squared length. The inner product of x with bk is then ∑nx[n]e−2πikn/N. Each bk has squared length N, not 1, so this inner product is N times the coordinate. The 1/N comes back in the inverse below.
That sum has a vivid picture, the one 3Blue1Brown's visual introduction to the Fourier transform is built on. Each term x[n]e−2πikn/N places sample n on a circle, rotated clockwise by the angle 2πkn/N. So you wind the signal around a circle at a trial rate of k turns per block. If the signal oscillates at that same rate, its peaks always land on the same side and the sum is large. If not, the points spread around the circle and cancel. The DFT runs this test for every trial rate k at once.
The DFT measures every frequency at once
Here is the transform in general. It takes the inner product of the block with each of the N complex sinusoids.
In plain words,X[k] measures how much of the vibration "k cycles per block" the block contains. It is a complex number. Its magnitude is the strength of that vibration. Its angle is the phase, where in its cycle the vibration starts. The inverse rebuilds the block by adding up all N vibrations with those strengths and phases.
The DFT has a second reading that we need later, for zero padding and windows. It samples a smooth curve.
The factor 1/N could sit on either side, and libraries let you choose. NumPy's default, norm="backward", puts it in the inverse, as above. norm="ortho" puts 1/N on both sides. That makes FN/Nunitary, a complex rotation that preserves lengths. norm="forward" puts 1/N in the forward transform. The three differ only by constants, but those constants change every absolute level you compute, so check which one a library uses.
The basis is orthogonal, so the inverse comes for free
The inverse looks the way it does because the sampled sinusoids are orthogonal, and each has squared length ∑n∣bk[n]∣2=N. In an orthogonal basis, you undo "take inner products" by adding up the basis vectors with those weights, each divided by its squared length. That division is the 1/N.
In plain words,N points equally spaced around a circle sum to zero, unless they are all the same point. Two different trial frequencies wind against each other a whole number of times per block, so they cancel exactly.
In plain words, the energy is the same in both coordinate systems, up to the factor N of the unnormalized convention. This is what lets us speak of "the energy at 1 kHz" and have the energies of all the bins add up to the energy of the signal. With norm="ortho" the factor disappears, because FN/N is a rotation. An interviewer who asks why the inverse DFT has a 1/N wants these two theorems.
The next cell builds F4 and reproduces the two hand computations. Then it checks orthogonality, the inverse, and Parseval for a random block of 64 samples.
Python · cell 1
Warm-up
The DFT as a matrix
Write dft_matrix(N), which returns the complex N×N matrix whose entry (k,n) is e−2πikn/N. Then write dft(x), which applies that matrix along the last axis of an array of any shape. A batch of shape (B, N) comes back as (B, N), and so does any other leading shape. The result must equal np.fft.fft(x, axis=-1), but do not call np.fft or scipy.fft.
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Each bin is a frequency in hertz
The bin index k counts cycles per block. A block of N samples at rate fs lasts N/fs seconds, so k cycles per block is a frequency in hertz,
fk=Nkfs Hz,Δf=Nfs between neighboring bins.
Here is a real example. Whisper, OpenAI's speech recognizer (Radford et al. 2022), transforms blocks of N=400 samples at fs=16,000 Hz, which is 25 ms of audio. Its bins are Δf=16,000/400=40 Hz apart, so a 1 kHz tone sits exactly at bin k=1000/40=25.
The bins above N/2 are negative frequencies. For integer n, e−2πikn/N=e−2πi(k−N)n/N, so bin k and bin k−N are the same basis vector. Bins N/2+1,…,N−1 are therefore the frequencies −(N/2−1)fs/N,…,−fs/N. This is the aliasing of §35.1 seen from the other side. Sampled tones whose frequencies differ by exactly fs produce identical samples. So a DFT can tell apart only the frequencies in a band of width fs, and the natural band is [−fs/2,fs/2). Its edge, fs/2, is the Nyquist frequency, the highest frequency the samples can represent.
For a real signal, the negative half carries no new information.
This is why np.fft.rfft, the transform for real input, returns only N/2+1 bins for even N. For Whisper's N=400 that is 201 bins, from 0 Hz up to the Nyquist frequency of 8,000 Hz.
One consequence is easy to get wrong. Suppose you turn a one-sided spectrum into power and want the powers to add up to the signal's energy, as Parseval promises. Then each bin strictly between 0 and N/2 counts twice, once for itself and once for its negative twin. Bins 0 and N/2 count once.
The next cell puts a 1,000 Hz tone and a 1,010 Hz tone through Whisper's transform size. It asks how much of the energy lands in the peak bin.
Python · cell 2
The 1,000 Hz tone is perfect. All its one-sided energy sits in bin 25. Now move it by 10 Hz, a quarter of a bin. The peak bin keeps only 81 percent of the energy, and 16 bins sit within 30 dB of the peak. (A decibel, recall from §35.1, is 10log10 of a power ratio, so 30 dB is a factor of 1,000 in power.) Nothing about the tone changed except that it no longer completes a whole number of cycles in the block. This smearing is called leakage, and the next part explains it exactly and then reduces it.
A tone between bins leaks into every bin
Every tone takes the same shape in the DFT, and that shape explains why the 1,010 Hz tone smeared. To see it, take the cleanest case, a complex tone x[n]=e2πik0n/N. Its frequency k0 is measured in bins and may be fractional. Its DFT is
where DN(ν)=∑n=0N−1e−2πiνn/N for any real ν, a frequency offset measured in bins. In plain words, the DFT of a tone always has the same shape, DN, centered on the tone's true frequency and read off at the whole-number bins.
∂ Proof of the Dirichlet kernel formula
With r=e−2πiν/N=1, the geometric series gives DN(ν)=(1−rN)/(1−r)=(1−e−2πiν)/(1−e−2πiν/N). Use the identity 1−e−2iθ=e−iθ(eiθ−e−iθ)=2ie−iθsinθ, with θ=πν on top and θ=πν/N below. The 2i cancels, and the phases combine to e−iπν+iπν/N=e−iπν(N−1)/N.
The zeros come from sin(πν)=0 at the integers, while the denominator stays nonzero for 0<∣ν∣<N. At ν=21 the numerator is 1, so ∣DN(21)∣/N=1/(Nsin(π/(2N))). Since Nsin(π/(2N))→π/2, the level is −20log10(Nsin(π/(2N)))→20log10(2/π)=−3.92 dB. The small-ν form uses sin(πν/N)≈πν/N. The sidelobe level is computed numerically in the window cell below. ∎
In plain words, a tone that completes a whole number of cycles in the block lands on one bin and on a zero of every other bin. That is why the 1,000 Hz tone looked perfect. A tone that does not is read off between the zeros, so every bin picks up some of it, and the amount falls off only slowly with distance. This is spectral leakage.
Leakage is not a numerical error. The DFT treats the block as one period of a signal that repeats forever. A tone with a fractional number of cycles then has a jump where the end of the block meets its start, and a jump contains every frequency.
Windows trade a wider peak for much lower sidelobes
The fix is to remove the jump. Multiply the block by a windoww[n], a curve that tapers smoothly to zero at both ends, before transforming. The same computation as above shows that the DFT of a windowed tone is the window's own spectrum shifted to k0,
In plain words, choosing a window means choosing the shape every tone is drawn with. The rectangular windoww[n]=1, which is no window at all, gives W=DN. W(ν) is the window's DTFT with frequency measured in bins, and the DFT shows it only at the points ν=k−k0 for whole numbers k.
The most common taper is the periodic Hann window, w[n]=21−21cos(2πn/N), one period of a raised cosine. Its spectrum is three Dirichlet kernels,
WHann(ν)=21DN(ν)−41DN(ν−1)−41DN(ν+1).
The reason is that the cosine is a sum of two complex sinusoids one bin away, and multiplying by e±2πin/N shifts the kernel by one bin. Away from the peak, the sidelobes of the three kernels nearly cancel. What is left falls off like 1/∣ν∣3, 18 dB each time the distance doubles, against 6 dB for the rectangle. The price is a main lobe, the central peak, twice as wide, with its first zero at ±2 bins instead of ±1.
∂ Why the Hann sidelobes fall off as the cube of the distance
For ∣ν∣≪N, use the small-ν form DN(ν)≈e−iπνNsin(πν)/(πν) for each of the three kernels. For the shifted kernels, the phase factors e−iπ(ν±1) and the signs of sin(π(ν±1)) both flip, so the flips cancel. Then
The extra factor 1/(ν2−1) turns the 1/∣ν∣ fall-off of DN into 1/∣ν∣3. It also cancels the zeros of DN at ν=±1, which is why the first zero of Hann moves out to ±2 bins.
That trade holds for every window. A narrow main lobe separates two tones close in frequency. Low sidelobes let you see a quiet tone next to a loud one. No window has both, and five standard numbers describe where a window sits.
The cell below measures all five for four standard windows. Besides the rectangle and Hann, it uses the Hamming window0.54−0.46cos(2πn/N) and the Blackman window0.42−0.5cos(2πn/N)+0.08cos(4πn/N). It zero pads each window 64 times, which draws ∣W(ν)∣ on a grid of 1/64 bin (the box after the widget explains why that works).
Python · cell 3
The coherent gain explains a puzzle that bites everyone once. The same tone reads 6 dB lower through a Hann window than through a rectangle, because 20log100.5≈−6. Absolute levels therefore depend on the window. Libraries that report calibrated spectra divide by ∑nw[n]. SciPy's scipy.signal.stft does, and torch.stft does not.
In the widget, a complex tone sits at bin k0 of a 64-sample block, and the slider moves it in steps of 0.05 bin. The thin curve is the window's spectrum W(ν−k0), and the dots are the DFT bins that sample it.
Why does a pure tone between two bins smear across the whole spectrum, and what does a window buy?
A pure tone at bin k₀, windowed and transformed. The dots are the DFT bins, sampling the window's spectrum.
Loading interactive figure…
The last question has a definite answer, and interviewers ask it often.
Now the two sides of the trade, measured.
Python · cell 4
The quiet tone is a question of sidelobes. With the rectangular window, the loud tone's leakage at bin 46 is −24.4 dB, 16 dB above the quiet tone, which is buried. Hann leaks only −54 dB there and Blackman −63 dB, so both show the quiet tone at its true −40 dB.
The equal pair goes the other way, because it is a question of main-lobe width. The rectangle's narrow main lobe shows a clear 7.9 dB dip between the two tones. Hann barely separates them, with a 0.4 dB dip. Blackman's wide main lobe merges them into one bump, whose midpoint is 0.15 dB higher than the tones. (These numbers are for two cosines that start together. The depth of a dip also depends on the tones' relative phase.)
So the right window depends on the question. To find weak components next to strong ones, you want low sidelobes. To separate close components of similar strength, you want a narrow main lobe. When you need both, the only real fix is a longer window, which the last part of this section takes up. "Which window would you use, and why?" is a standard interview question, and this trade is the answer.
A parabola finds a frequency between bins
At Whisper's settings a bin is 40 Hz wide, but pitch trackers and tuners need accuracy of a few hertz. You do not need a longer FFT to get there. Near its peak, the log magnitude of a window's main lobe is close to a parabola. For an untruncated Gaussian window it is exactly a parabola, since the transform of a Gaussian is again a Gaussian, as Smith's Spectral Audio Signal Processing notes. So fit a parabola through the three log magnitudes around the peak and read off its top.
In plain words, if the left neighbor is louder than the right one, the true peak lies left of k⋆. How far left depends on how lopsided the three values are, compared with how sharply they curve.
∂ Where the formula for p comes from
Write the parabola as y(u)=a(u−p)2+b, with u measured in bins from k⋆. Plugging in u=−1,0,1 gives α=a(1+p)2+b, β=ap2+b, and γ=a(1−p)2+b. Subtracting, α−γ=4ap and α−2β+γ=2a. Dividing the first by twice the second gives p. Because β is the largest of the three, ∣α−γ∣≤∣α−2β+γ∣, so ∣p∣≤21.
Research
Find a frequency between bins
Write estimate_frequency(x, fs, window). The input x is a real block of length N = len(window) that holds one dominant sinusoid, away from 0 Hz and from the Nyquist frequency.
Window the block and take np.fft.rfft.
Find the peak bin k⋆ among bins 1 to N/2−1.
Fit a parabola through the natural log magnitudes of bins k⋆−1, k⋆, k⋆+1, and return the interpolated frequency in hertz.
Over a sweep of fractional frequencies, the tests demand an error below 1 percent of a bin with a Blackman window and below 2 percent with Hann.
import numpy as np
def estimate_frequency(x, fs, window):
raise NotImplementedError
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Convolution in time is multiplication in frequency
Before the STFT, we need two tools that every audio pipeline uses. The convolution theorem turns filtering into multiplication, and the FFT makes every transform cheap. This part takes the first.
A convolution slides one sequence along another, reversed, and sums the products at each shift. A convolutional layer (§0.8) computes the same sliding sum without the reversal. Convolution is also how any linear filter that does not change over time acts on a signal. Such a filter is described completely by its impulse responseh, its output when the input is a single 1 followed by zeros. For any other input x, the output is the convolution (x∗h)[n]=∑mx[m]h[n−m]. Microphones, rooms, and equalizers all work this way.
The DFT turns convolution into elementwise multiplication, the most useful single fact in signal processing. Because the DFT works on blocks, it pairs exactly with a convolution that wraps around the end of the block.
Start with numbers. Take x=(1,2,3) and h=(1,1), a filter that adds each sample to the one before it. The ordinary convolution is (1,3,5,3), four samples long. Now force the answer into three samples by wrapping the overhanging last sample, the 3, back onto the start. That gives (1+3,3,5)=(4,3,5). This wrapped version is the circular convolution.
Check the definition on the example. Pad h to (1,1,0). For n=0 the sum is x[0]h[0]+x[1]h[2]+x[2]h[1]=1+0+3=4, the wrapped first sample. The theorem below says the DFT turns this wrapped sum into a product, one bin at a time. Bin 0 of a DFT is the plain sum of the samples, so at bin 0 the check is one line. The inputs sum to 6 and 2, and 6×2=12=4+3+5.
Fix m. As n runs over 0,…,N−1, the index j=(n−m)modN also runs over 0,…,N−1, once each. Also e−2πik(n−m)/N=e−2πikj/N, because the two exponents differ by a multiple of 2πi. So the inner sum is ∑jh[j]e−2πikj/N=H[k], the same for every m, and the outer sum is X[k]. ∎
In plain words, convolving in time multiplies in frequency. A sinusoid passes through a filter with its shape unchanged. It only gets scaled, and shifted in phase, by the complex number H[k], because a delayed sinusoid is the same sinusoid times a phase.
Most of the time we want the ordinary, linear convolution. That is what np.convolve computes, and for inputs of lengths Nx and Nh it has length Nx+Nh−1. Zero padding gets it from the circular one. Pad both inputs with zeros to any length N≥Nx+Nh−1, and nothing can wrap around. Here is why. Whenever n−m is negative, the wrapped index is (n−m)modN=N+n−m≥N−(Nx−1)≥Nh. That points at one of the zeros appended to h, so every wrapped term vanishes, and the circular sum equals the linear one.
So a linear convolution is three FFTs of length at least Nx+Nh−1, two forward and one inverse. That costs O(NlogN) against O(NxNh) for the direct sum, a large win once the kernel is long. This is how the state-space models of §21.3 apply a convolution kernel as long as the whole sequence. An interviewer who asks where the FFT speeds up a model wants this answer.
The cell below repeats the hand example, checks the theorem, and shows the wrap-around.
Python · cell 5
The last line shows the wrap-around directly. With N=8, the two samples of the linear result that fall past the end, at indices 8 and 9, are added onto the first two.
A room is a convolution
A room acts on sound as a filter. Its room impulse response (RIR) h[n] is what a microphone in the room records when the source emits a single click. It holds a spike for the direct path, the sound that travels straight from the source to the microphone. Then come early reflections from the nearest walls, and then a dense tail that decays roughly exponentially. Sound waves add linearly, and the room does not change during an utterance. So take a dry source x, recorded close to the mouth with no room in it. A distant microphone records x∗h plus noise. Ko et al. 2017 model far-field speech exactly this way.
That makes reverberation a standard data augmentation, a change to training inputs that keeps their labels. It is used for far-field speech recognition and for speaker embeddings, the fixed-length vectors that identify a voice (§35.6). Clean training speech is convolved with recorded or simulated RIRs, so the model learns to ignore the room. Two numbers describe a room's response.
Offices and living rooms have an RT60 of a few tenths of a second, and concert halls about two seconds. The DRR falls as the talker walks away from the microphone. It is positive close up and negative across a large room.
Now the arithmetic at real scale. It uses one number from the FFT part that follows. A length-L FFT, with L a power of two, takes 2Llog2Lbutterflies, small steps of one complex multiplication and two complex additions each.
The next cell builds a synthetic RIR with RT60=0.4 s. It is Gaussian noise under an exponential envelope that loses 60 dB of energy, a factor of 1,000 in amplitude, every 0.4 s. The cell measures the reverberation time back with Schroeder's integral, then reverberates 10 s of noise standing in for speech.
Python · cell 6
Schroeder's integral gives back 0.41 s for the designed 0.4 s. The DRR of about −8 dB says most of the energy arrives as reverberation, as it does for a distant talker. Your timings will differ, but the FFT handles ten times as much audio in less time than the direct sum takes for one second. After alignment, the reverberant copy lines up with the dry clip at lag 0.
Long signals are convolved in blocks
One FFT of the whole clip has to wait for the whole clip, and it needs 218 points of memory per clip. A streaming system, or a data loader that reverberates thousands of hours, cuts the input into blocks instead.
Let the block length be Nb. Block j is xj[n]=x[jNb+n] for 0≤n<Nb, and zero elsewhere, so that x[n]=∑jxj[n−jNb]. Convolution is linear and commutes with shifts, so
(x∗h)[n]=j∑(xj∗h)[n−jNb].
In plain words, convolve each block on its own and add the results back at the block's position. Each piece xj∗h has Nb+Nh−1 samples, so it spills Nh−1 samples past its block into the next one. An FFT of any length L≥Nb+Nh−1 computes each piece exactly, and H, the FFT of h, is computed once and reused. The spilled tails overlap and are added into place, hence the name overlap-add. The price is latency, since a block produces nothing until it is full. Interviewers ask how to pick Nb, and the box below gives the trade.
The inverse STFT later in this section puts overlapping frames back together in the same way.
Interview
Reverberation augmentation by overlap-add
Write reverb_augment(x, rir, block=4096), the augmentation step of a far-field training pipeline. Both x, the clip, and rir, the room impulse response, are real 1-D arrays. The function does three things.
It computes the full linear convolution of x and rir by overlap-add. Cut x into consecutive blocks of block samples, where the last block may be shorter. Convolve each block with rir through np.fft.rfft and np.fft.irfft at one fixed length L of at least block + len(rir) - 1, such as the smallest power of two that is. Transform rir only once. Add each block's result into place at the block's offset.
It aligns the output to the direct path. Let d be the index of the largest ∣h[n]∣. Keep samples d through d + len(x) - 1, so the output has exactly len(x) samples and no delay.
It rescales the output so its RMS (root mean square) equals that of x. An all-zero clip returns zeros.
Do not call np.convolve, and do not take one FFT of the whole clip. The tests switch off np.convolve and watch the transform lengths. Interviewers follow up by asking how to choose the block size and what it does to latency.
import numpy as np
def reverb_augment(x, rir, block=4096):
raise NotImplementedError
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
The FFT computes the DFT in N log N steps
Multiplying by FN costs N2 complex multiplications. For a 1,024-point transform that is over a million per frame, and a speech model transforms 100 frames for every second of audio it reads. The FFT gets the same answer with about 2Nlog2N multiplications, 5,120 for N=1024. The idea is divide and conquer, and it comes from one line of algebra.
Let N be even. Split the sum that defines X[k] into even samples n=2m and odd samples n=2m+1, for m=0,…,N/2−1,
Since e−2πik(2m)/N=e−2πikm/(N/2), the first sum is the N/2-point DFT of the even samples. Call it E[k]. After pulling out the factor e−2πik/N, the second sum is the N/2-point DFT of the odd samples, O[k]. So
X[k]=E[k]+e−2πik/NO[k].
Now the saving. The two half-size DFTs repeat with period N/2 in k, so E[k+N/2]=E[k] and O[k+N/2]=O[k]. The factor in front only flips sign, e−2πi(k+N/2)/N=e−πie−2πik/N=−e−2πik/N. So for k=0,…,N/2−1,
X[k]=E[k]+tk,X[k+N/2]=E[k]−tk,tk=e−2πik/NO[k].
This pair of equations is the butterfly. One complex multiplication, by the twiddle factore−2πik/N, and two additions produce two outputs. Apply the same split to E and O, and again, until the blocks have length 1. The DFT of one sample is the sample itself. This is the radix-2 decimation-in-time algorithm of Cooley and Tukey (1965). "Radix 2" means it halves at each step, and "decimation in time" means it splits the samples rather than the bins.
For N=1024, that is 512⋅10=5,120 multiplications against 1,048,576, a saving of about 205 times. The ratio, 2N/log2N, keeps growing with N. A few twiddles are trivial, such as 1 or −i, so real implementations do slightly fewer.
Whisper's N=400 is not a power of two, and that is fine. The same split works for any factor. Since 400=24⋅52, a mixed-radix FFT splits by 2 four times and by 5 twice. Libraries such as pocketfft, which sits behind np.fft, and NVIDIA's cuFFT handle any length this way. Powers of two are still the fastest, which is one reason music front ends often choose 1,024 or 2,048.
The cell below implements the recursion in a few lines, checks it against NumPy, and times three ways of computing a 1,024-point DFT.
Python · cell 7
Your timings will differ, and they teach a second lesson. The Python recursion makes 2N−1 function calls, and the interpreter's overhead on each call costs far more than a multiplication. So at N=1024 it can lose to the matrix product, which runs in compiled linear-algebra code. np.fft is both compiled and O(NlogN), and it beats both. Operation counts predict speed only when the constant factors are comparable. That theme returns in the GPU chapter's roofline model and in every chapter on kernels and hardware.
Interview
A radix-2 FFT from scratch
Write fft_radix2(x) for a 1-D array whose length is a power of two (1, 2, 4, ...). It returns the DFT as a complex array of the same length, computed recursively or iteratively. Raise ValueError for any other length, including 0. Do not use np.fft, which the tests switch off while your function runs. The tests also count the exponentials, sines, and cosines you evaluate, so an N×N DFT matrix fails. This is a classic whiteboard question. The follow-ups are always the cost recurrence and why X[k+N/2] needs no extra multiplications.
import numpy as np
def fft_radix2(x):
raise NotImplementedError
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
The STFT slides a window along the signal
A DFT of a whole recording would say which frequencies occur somewhere in it, but not when, and speech is all about when. So we cut the signal into short overlapping slices called frames, window each one, and transform each one. The hop is the number of samples from the start of one frame to the start of the next. Stacking the frames' spectra gives a matrix with one row per frame, a picture of frequency content over time.
A tiny count first. Take 1 s of 16 kHz audio, 16,000 samples, with 400-sample frames that start every 160 samples. Without padding, the last frame must end inside the signal, so the frames start at 0,160,…,15,520. That is 1+⌊(16,000−400)/160⌋=98 frames. The general rule is in the definition.
In plain words, slide a window along the signal in steps of R samples, and take the DFT of what you see each time. Each row of X is one frame's spectrum, and frame t describes the audio around time tR/fs.
The centered count follows from the uncentered one. For even N, padding adds 2⌊N/2⌋=N samples, and 1+⌊(nsamples+N−N)/R⌋=1+⌊nsamples/R⌋. In the 1 s example, centering gives 1+16,000/160=101 frames instead of 98.
Centering is the default in both torch.stft and librosa.stft, but they pad differently. PyTorch mirrors the signal at its edges (pad_mode="reflect", so the padding before x[0] is x[N/2],…,x[1]). librosa has padded with zeros by default since version 0.9. The first and last frames therefore differ between the two libraries, which matters when you port a model.
The STFT is also a layer you already know. Fix a bin k and define the complex filter gk[n]=w[n]e−2πikn/N of length Nw. Then X[t,k]=∑nx[tR+n]gk[n], a sliding dot product evaluated every R samples. That is a 1-D convolution layer in the deep-learning sense of §0.8, which slides its kernel without flipping it. Its kernel size is Nw, its stride is R, and it has no bias. It has N/2+1 complex output channels, or N+2 real ones for the real and imaginary parts. The layer's output-size formula with no padding reproduces the uncentered frame count.
Each filter gk is a band-pass filter, one that passes a narrow band of frequencies and blocks the rest. A tone at bin ν comes out of it with magnitude ∣W(ν−k)∣, the window's spectrum centered on bin k. So the STFT is a bank of N/2+1 evenly spaced band-pass filters, with outputs taken every R samples. §35.4 replaces this whole front end with a stack of seven learned strided convolutions over the raw waveform, which learn their own filters in place of these fixed ones. §35.3 meets front ends that learn filterbanks with only a few parameters per filter.
The next cell builds the STFT from a strided view of the signal and checks both frame-count formulas on 30 s of audio.
Python · cell 8
Count what the STFT produces. Whisper's frames hold 201 complex bins every 160 samples, about 2.5 real numbers per input sample. The complex STFT is redundant, and that redundancy is what makes inversion stable. The power spectrogram keeps 201 real numbers per 160 samples, and the phase is gone.
Interview
An STFT with the framing conventions right
Write stft(x, n_fft, hop, window, center=True) for a real 1-D signal, where window is an array of length n_fft and n_fft is even. With center=True, first reflect-pad n_fft // 2 samples on each side, as torch.stft does. Return the complex array of shape (T, n_fft // 2 + 1), frames as rows, with T given by the formulas above. Avoid a Python loop over frames, and use sliding_window_view or fancy indexing instead. The reference in the tests uses an explicit loop, so you can compare.
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Overlap-add inverts the STFT exactly
Each frame's inverse DFT gives back the windowed slice w[n]x[tR+n]. The slices overlap, so every sample appears in several of them, each time multiplied by a different part of the window. The cleanest way to undo this has three steps. Multiply each slice by the window once more. Add the slices into place, which is overlap-add again. Then divide each sample by the total weight the window gave it.
∂ Proof of perfect reconstruction and of the least-squares property
If Y is the STFT of x, then yt[n]=w[n]x[tR+n]. The numerator becomes ∑tw[m−tR]2x[m], and dividing by the denominator leaves x[m].
For the general claim, apply Parseval to each frame, ∑k∣X[t,k]−Y[t,k]∣2=N∑n∣w[n]x[tR+n]−yt[n]∣2. The objective is then N∑t∑n∣w[n]x[tR+n]−yt[n]∣2, a sum of squares in which sample x[m] appears once in each frame that contains it, with coefficient w[m−tR]. Setting the derivative with respect to x[m] to zero gives ∑tw[m−tR](w[m−tR]x[m]−yt[m−tR])=0, which is the formula. ∎
In plain words, applying the window a second time and dividing by the sum of squared windows undoes the analysis exactly, whatever the hop. The only requirement is that every sample is seen through a nonzero part of some window. When the spectrogram has been edited, as in speech enhancement, the same formula gives the best signal available.
Python · cell 9
All three hops reconstruct to about 10−15, which is machine precision, even though only hop 100 has a constant ∑tw2.
Core
Invert the STFT
Write istft(X, n_fft, hop, window, length). The input X is a complex array of shape (T, n_fft // 2 + 1) from a centered STFT, with reflect padding of n_fft // 2 as in Exercise 35.2e. n_fft is even, and the hop is at most n_fft // 2, so the frames cover every sample.
Invert each frame with np.fft.irfft and apply the synthesis window.
Overlap-add the frames, and divide by the sum of squared windows wherever it is positive.
Remove the n_fft // 2 samples of centering and return a real signal of exactly length samples.
One test uses a modified STFT that no signal produces. There only the recipe above matches the reference. Leaving out the synthesis window and dividing by the sum of window does not.
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Recovering a signal when only the magnitude survives
Models almost never keep the phase. Recognizers read the power spectrogram ∣X[t,k]∣2. Text-to-speech systems predict magnitudes, as mel spectrograms, because phase is erratic and hard to predict. Going back to a waveform then means inventing a phase. Magnitudes alone do not determine the signal, since flipping its sign changes no magnitude at all. But the overlap between frames constrains the phase a great deal, because neighboring frames see the same samples.
Griffin and Lim (1984) turned that constraint into an algorithm. Start from the target magnitudes A[t,k] and any phase. Then repeat two steps.
Build the least-squares signal xi for the current complex array, with the overlap-add formula above.
Take the STFT of xi, keep its phase, and replace its magnitude by A. Call the result Yi, and feed it back into step 1.
∂ Proof that the error never increases
For any complex array Z, the array with magnitude A that is closest to Z in squared error keeps Z's phase, and its distance is ∑∣∣Z∣−A∣2. So di=∥Xi−Yi∥2, with Yi the magnitude-A array built from Xi's phase.
Step one makes xi+1 the signal whose STFT is closest to Yi, so ∥Xi+1−Yi∥2≤∥Xi−Yi∥2=di. Step two picks Yi+1, the closest magnitude-A array to Xi+1, so di+1=∥Xi+1−Yi+1∥2≤∥Xi+1−Yi∥2. Chain the two inequalities. ∎
The proof is exact for the uncentered STFT. The centered one in the cell below behaves the same way, and the cell checks that the error fell at every iteration.
The theorem guarantees steady progress, not the best possible phase. In practice Griffin-Lim stalls at a phase that sounds metallic. The cell below runs it on a half-second chirp, a tone whose pitch rises from 300 Hz to 1,800 Hz.
Python · cell 10
The spectral convergence, the relative error of the magnitudes, falls from 0.61 to 0.08 in 100 iterations, so the magnitudes nearly match. Yet the waveform's signal-to-noise ratio against the original (the SNR of §35.1) is negative. That shows magnitudes alone do not pin down the waveform, and a different phase is not in itself a defect. The defect is what remains, the 8 percent magnitude error at the end, which you hear as a metallic, phasey sound.
Neural vocoders, networks that learn to generate waveforms from mel spectrograms, replaced Griffin-Lim in text-to-speech because they produce plausible phase without that sound (§37.2). §37.1 starts from the mel targets they consume. An interviewer who asks "can you invert a magnitude spectrogram?" wants both halves, Griffin-Lim and why vocoders replaced it.
No window is sharp in both time and frequency
Every choice so far left one number open, the window length Nw. A long window gives fine frequency resolution, with bins fs/Nw apart and a narrow main lobe, but it smears anything that changes during the window. A short window pins events down in time but blurs frequency.
Here are numbers. At 16 kHz, a 25 ms window (400 samples) has bins 40 Hz apart. A 10 ms window has bins 100 Hz apart, too coarse to separate the harmonics of a low voice, but it can place a click within 10 ms. Bin spacing times duration is (fs/Nw)(Nw/fs)=1 for every length. The theorem below says that this trade is a law, not a defect of the DFT.
To state it we need two facts about continuous time. The continuous Fourier transform from §35.1 is X(f)=∫x(t)e−2πiftdt, and the DFT is its sampled, finite version. In this part t is continuous time in seconds and f is frequency in hertz. Plancherel's theorem, the continuous form of Parseval, says ∫∣X(f)∣2df=∫∣x(t)∣2dt.
For a sense of scale, take Whisper's 25 ms Hann window. Its tapered ends carry little energy, so its time spread is only about 3.54 ms, half that of a rectangle of the same length, as a cell below measures. Its frequency spread is about 23.1 Hz. The theorem below says that no window with a time spread of 3.54 ms can have a frequency spread below 1/(4π×3.54ms)≈22.5 Hz, so Hann sits close to the limit.
∂ Proof of the uncertainty inequality
Assume x is smooth and decays fast enough that t∣x(t)∣2→0 as ∣t∣→∞. The general case follows by approximation. Two reductions come first. Replacing x(t) by x(t+μt)e−2πiμft shifts the time density by μt and the frequency density by μf without changing their shapes, so we may assume μt=μf=0. Scaling x changes neither spread, so we may assume ∥x∥=1.
Next, the derivative rule. The transform of x′(t) is 2πifX(f), by integrating by parts, ∫x′(t)e−2πiftdt=[xe−2πift]−∞∞+2πif∫x(t)e−2πiftdt, where the boundary term vanishes. With Plancherel this gives
∫∣x′(t)∣2dt=∫∣2πifX(f)∣2df=4π2σf2.
Now write the unit energy as an integral and integrate by parts, using dtd∣x∣2=xx′+x′x=2Re(xx′),
Hence σtσf≥1/(4π). Equality in Cauchy-Schwarz needs x′(t)=−λtx(t) for a constant λ. Equality in the first step needs Re(xx′)t to have one sign, which forces λ to be real and positive. The solution of x′=−λtx is x(t)=ce−λt2/2, a Gaussian, and undoing the two reductions gives the family in the theorem. ∎
In plain words, a pulse cannot be both short and pure in pitch. Squeeze a pulse to half its duration and its spectrum doubles in width. A window is a pulse, so whatever window you choose, the product of its time spread and its frequency spread is at least 1/(4π)≈0.0796. Only the Gaussian reaches the bound.
Sampled windows of a few hundred samples obey the same bound. The next cell measures how close four windows of Whisper's length come to it. It computes the spreads from w[n]2 in samples and from ∣W∣2 in cycles per sample, so their product has no units.
Python · cell 11
The Gaussian sits on the bound to four decimals. Blackman is within half a percent and Hann within 3 percent. The rectangle is 19 times worse, because its sharp edges spread its spectrum. (In continuous time its σf would be infinite, and here the sampling band caps it.) The Hann row gives the 3.54 ms and 23.1 Hz quoted above, and the rectangle row the 7.2 ms of a window with no taper.
The widget computes a spectrogram of 0.6 s of synthetic audio at 8 kHz while you drag the window length. Its three presets are two close tones followed by two close clicks, a rising chirp, and a vowel whose pitch glides.
How does window length trade time resolution for frequency resolution?
A spectrogram of 0.6 s of synthetic sound at 8 kHz. Drag the window length and watch what blurs.
Loading interactive figure…
Speech uses 25 ms windows for three reasons
So the window length is a choice of which structure you want to see. Speech front ends settle on a standard answer.
There are three reasons for those numbers.
Stationarity. The vocal tract changes shape from sound to sound, but speech scientists treat it as roughly fixed over 20 to 30 ms. A frame of that length sees one steady spectrum. Much longer frames blur the transitions between phones, the individual speech sounds.
Harmonics. A voice with a 100 Hz pitch has harmonics 100 Hz apart, which is 2.5 bins at 40 Hz per bin. Hann's 6 dB main-lobe width, the width of its peak at 6 dB below the top, is 2 bins. The harmonics are farther apart than that, so neighboring ones show a dip between them and are resolved. A higher voice has wider spacing and is easier.
Rate. A 10 ms hop gives 100 frames per second, several frames per phone. That is enough for recognition and keeps sequences short. The Audio Spectrogram Transformer (Gong et al. 2021) and the speaker-embedding network ECAPA-TDNN (Desplanques et al. 2020) use the same 25 ms window and 10 ms hop, the first with a Hamming window.
Music is different. Notes last longer and their pitches sit closer together. A semitone near 110 Hz is about 6.5 Hz. So music analysis typically uses 2,048-sample windows, 46 ms at 44.1 kHz or 93 ms at librosa's default rate of 22,050 Hz. Even so, a 2,048-point bin is 10.8 Hz wide at 22,050 Hz, wider than that semitone, which is one reason music also uses the constant-Q transform below. Percussive onsets want the opposite, since a 2,048-sample window at 22,050 Hz smears a drum hit over 93 ms. An interviewer who asks "what window would you use for music?" wants this contrast with speech.
The same STFT in PyTorch
torch.stft is what Whisper and most training pipelines call. It returns frames on the last axis, (batch, bins, frames), the transpose of our (T, K). It uses no window at all unless you pass one.
🔥 In PyTorchfor a machine with PyTorch
The libraries agree on the mathematics and disagree on almost every default. Check these whenever you move a model between them.
torch.stft
librosa.stft
scipy.signal.stft
Whisper's audio.py
output layout
(..., K, T)
(..., K, T)
(K, T), plus arrays of the frequencies and times
(n_mels, 3000) after its mel step
default window
none (all ones)
periodic Hann
periodic Hann
torch.hann_window(400)
default hop
n_fft // 4
win_length // 4
half the segment
160
centering
center=True, reflect
center=True, zeros (since 0.9)
zero padding at both ends
torch defaults, then drops the last frame
scaling
none
none
divides by the window sum
none
The front end is cheap at real scale
The STFT costs little next to the model that reads it. One hour of 16 kHz audio at a 10 ms hop is 360,000 frames. With a 512-point FFT each frame costs 2512log2512=2,304 butterflies, about 8.3×108 for the hour. At about ten floating-point operations per butterfly, that is roughly 1010 operations.
A Transformer encoder costs about 2 floating-point operations per parameter per frame (§5.6). Even a small encoder of 100 million parameters, reading 50 frames per second, spends 2⋅108⋅50⋅3600≈3.6×1013 operations on the same hour. That is thousands of times more. The front end is never the bottleneck, which is why it can afford to be fixed and exact.
Here are the representations this section offers, side by side.
PyTorch documentation. torch.stft. The exact framing, padding, window, and normalization conventions behind most audio pipelines, including Whisper's.
Watch next
🎬 But what is the Fourier Transform? A visual introduction. · 3Blue1BrownThe winding-machine picture of the Fourier transform that the section's intuition paragraph is built on.🎬 What is a Discrete Fourier Transform? | Week 14 | MIT 18.S191 Fall 2020 | Grant Sanderson · The Julia Programming LanguageGrant Sanderson's lecture on the discrete transform itself, sampled sinusoids as a basis and the DFT as a matrix, which is exactly how the section derives it.🎬 The Fast Fourier Transform (FFT): Most Ingenious Algorithm Ever? · ReducibleA visual walk through the even and odd split and the butterfly, a good companion to the radix-2 derivation and Exercise 35.2d.🎬 The more general uncertainty principle, regarding Fourier transforms · 3Blue1BrownWhy a short pulse cannot have a narrow spectrum, the time-frequency trade-off behind choosing STFT window lengths.