A spectrum for every moment.
On this page
The short-time Fourier transform (STFT) cuts a signal into short overlapping frames, fades the edges of each frame, and takes the FFT of each one. It runs once per frame, a new frame every 10 ms in a typical speech system, so one second is 100 FFTs. The output is a spectrogram, with time across, frequency up and brightness for strength, and the frame length trades sharp timing against sharp pitch.
Sound changes over time, but one spectrum of a whole clip adds every moment together.
Cut the signal into short frames of L samples, each starting R samples (the hop) after the last. Fade each frame’s edges with a window w[n], take its DFT, and stand the result up as one column. Figure 2 does it to a tone that jumps from 500 to 1,500 Hz.
Figure 2. 1A frame of L samples. 2Windowed. 3Its DFT, one column.
Frames usually overlap by a half or three quarters. The DFT size N can exceed L by padding with zeros, which adds bins but no detail.
Dennis Gabor showed in 1946 that no signal is sharp in both time and frequency at once. In an STFT the window length L sets the balance. At 8 kHz, a 64-sample window spans 8 ms and blurs frequency over 125 Hz; a 1,024-sample one spans 128 ms and blurs it over only 7.8 Hz.
Figure 4. 1The window. 2Clicks 40 ms apart. 3Tones at 1,000 and 1,050 Hz.
The DFT treats a frame as one period of a signal that repeats forever. A frame cut with no fade, w[n] = 1, uses the rectangular window; the Hann window fades both ends to zero.
A jump contains every frequency, so a tone that does not fit a whole number of cycles spills energy into far bins. This is leakage. A window’s spectrum is a central main lobe, whose width sets how close two tones can be, plus smaller side lobes, whose height sets how far a loud tone leaks. Heights are in decibels (dB) below the peak; every 20 dB down is ten times smaller in amplitude.
Figure 7. 1The window in orange. 2Bars over the dash are within 50 dB.
| Window | 3 dB width | Highest side lobe | Pick it when |
|---|---|---|---|
| Rectangular | 0.89 bins | −13 dB | the tone sits exactly on a bin, or nothing loud sits nearby |
| Hann | 1.44 bins | −32 dB | in doubt; the default in SciPy and librosa |
| Hamming | 1.30 bins | −43 dB | leakage into the nearest bins matters more than into far ones |
| Blackman | 1.68 bins | −58 dB | a weak tone must stay visible next to a strong one |
Values from Harris (1978), Table 1, rounded; the last column is common practice.
Denoising edits a spectrogram and then needs the sound back.
A 4-point Hann window, [0, 0.5, 1, 0.5], slid by 2 samples adds to 1 at every sample, 0 + 1 and 0.5 + 0.5.
scipy.signal.istft and librosa.istft window each inverse frame again, add, and divide every sample by this sum of squared windows, so they need only that it is never zero. This is NOLA, nonzero overlap-add. Hann with hop 300 of 400 fails line 1 but passes line 2 and still rebuilds the signal exactly.A run of ones checks line 1 (Anushiravani 2016): send a constant through the STFT and plain overlap-add, and if it comes back flat, the window and hop meet COLA. Windowing again on the way back without that division, weighted overlap-add (WOLA), needs the squared windows to add to a constant instead; the square root of a Hann window does, summing to 1 at hop L/2 and to 2 at hop L/4.
The round trip in SciPy at hop 300, where the Hann windows no longer sum flat, with the largest error printed at the end.
import numpy as np
from scipy import signal
FS, L, R = 16000, 400, 300
print(signal.check_COLA('hann', L, L - R), signal.check_NOLA('hann', L, L - R)) # False True
w = signal.get_window('hann', L)
starts = range(0, FS - L + 1, R) # one window every hop
ones_back = sum(np.pad(w, (m, FS - L - m)) for m in starts)
print(np.ptp(ones_back[L:-L])) # 0.71, top to bottom
x = np.random.default_rng(0).standard_normal(FS)
_, _, Z = signal.stft(x, fs=FS, window='hann', nperseg=L, noverlap=L - R)
_, x_back = signal.istft(Z, fs=FS, window='hann', nperseg=L, noverlap=L - R)
print(np.max(np.abs(x - x_back[:len(x)]))) # 3.8e-15, near machine precision
istft windows each inverse frame again, adds the frames and divides by the summed squared windows; the largest error, 3.8e-15, is near machine precision.This is how Spectral Subtraction for Denoising rebuilds audio after cleaning the spectrogram.
Speech systems usually take 25 ms frames every 10 ms, because speech is close to stationary, its spectrum nearly steady, over about 20 to 30 ms. At 16 kHz that is 400 samples with a hop of 160, and the DFT size is the next power of two, 512. The speech recognizer Conformer reads features on exactly such 10 ms frames, and CTC Loss, one loss used to train such recognizers, lines the transcript up with them.
The speech settings in librosa, window L = 400 and hop R = 160. One second of samples goes in, and the complex STFT comes out with 257 bins by 101 frames.
import numpy as np
import librosa
SR, N_FFT, L, R = 16000, 512, 400, 160
x = np.random.default_rng(0).standard_normal(SR)
X = librosa.stft(x, n_fft=N_FFT, hop_length=R, win_length=L)
power = np.abs(X) ** 2
print(power.shape) # (257, 101)
| Step | How often, on what data | State it keeps | Cost |
|---|---|---|---|
| STFT | once per frame, every hop, on one windowed frame | none; frames are independent | one FFT per frame |
| Overlap-add | once per frame, on one frame’s inverse FFT | the overlapping tail of earlier frames | one inverse FFT and one add per sample |
| Setting | Default | Source |
|---|---|---|
| Kaldi frames | 25 ms window, 10 ms shift | torchaudio.compliance.kaldi |
librosa.stft | n_fft 2048, hop n_fft//4, Hann, centered frames | librosa 0.11 documentation |
scipy.signal.stft | nperseg 256, overlap nperseg//2, periodic Hann window, which divides by L rather than L − 1 so shifted copies sum flat | scipy.signal.stft |
| Knob, where it enters | Raise it | Lower it | Default |
|---|---|---|---|
| Window length L, the frame in the STFT card | Finer frequency detail, blurrier timing | Sharper timing, blurrier frequency | 25 ms (torchaudio kaldi.mfcc) |
| Hop R, the shift from frame m to m + 1 | Fewer frames and less cost. Plain overlap-add needs a hop that splits L evenly; istft works at any hop (Section 5) | More frames, smoother tracks, more cost | 10 ms (torchaudio kaldi.mfcc) |
| Window type w[n], the fade in the STFT card | Smoother tapers (Hann, then Blackman) lower leakage but widen each peak | The rectangular window keeps peaks narrowest but leaks into every bin | Hann (scipy.signal.stft) |
| FFT size N, at least L | Zero padding gives a finer grid, not finer resolution | Below L the frame would be cut | Window rounded up to a power of two (torchaudio kaldi.mfcc) |
librosa documents the inverse at librosa.istft; SciPy marks scipy.signal.stft as legacy and recommends ShortTimeFFT.
librosa pads the signal so each frame is centered on its hop position; set center=False to start frames there instead.| What you see | Likely cause | What to do |
|---|---|---|
| A pure tone smears over many bins | No window, so the frame edges jump | Use a Hann window (Section 4) |
| The rebuilt signal ripples in loudness | Hand-written overlap-add with windows that do not sum flat at that hop | Divide by the summed squared windows as istft does, or use Hann at 50% overlap |
| The first frames look quieter | librosa pads the start with zeros to center frames | Expect it, or pass center=False |
How is the power of noise spread over frequency? That is Power Spectral Density.