Complex Numbers, Fourier and Signals
Complex rotations, DFT and FFT, windows, sampling, spectrograms, mel filterbanks and MFCCs. Python from the lessons.
- Concepts and terms
- Tasks and commands
- Annotated code
- Diagrams
- Defaults and numbers
- Troubleshooting
- Questions and tips
Cooley-Tukey FFT, 8 points#
From Lesson 01.20
Waveform to log-mel and MFCC#
Tasks in code#
| Task | Code |
|---|---|
| Rotate a point by 45 degrees | point * euler(math.pi / |
| N points on the unit circle | roots_of_unity(N) |
| Transform and invert, O(N^2) | dft(x), then idft(X) |
| Fast transform, power of 2 | fft(x) |
| Energy per frequency | power_spectrum(X) |
| Linear convolution by FFT | convolve_fft(x, |
| Split a signal into frames | frame(signal, |
| Mel filterbank matrix | mel_filterbank(n_mels, |
From Lesson 01.19, Lesson 01.20, 01.20 code/fourier.py, Lesson 06.02
Spectra in NumPy and SciPy#
waveform, sr = sf.read("clip.wav", dtype="float32")float32 in [-1, 1]spectrum = np.fft.fft(signal)freqs = np.fft.fftfreq(256, d=1/256)frequency of each binpower = np.abs(spectrum) ** 2power spectrumwindow = windows.hann(256)from scipy.signalresult = fftconvolve(signal, kernel, mode='full')frequencies, times, Zxx = stft(signal, fs=sample_rate, nperseg=256)spectrogram = np.abs(Zxx) ** 2(n_frequencies, n_time_frames)print(cmath.exp(1j * cmath.pi))Euler: close to -1From Lesson 01.19, Lesson 01.20, Lesson 06.01
Mel features from scratch#
def hz_to_mel(f): return 2595.0 * math.log10(1.0 + f / 700.0)mag = stft_magnitude(signal, frame_len=400, hop=160)Hann + DFT per framefb = mel_filterbank(n_mels, n_fft, sr)80 mels, n_fft=400 gives (80, 201)mel_spec = apply_filterbank(cmag, fb)one weighted sum per mel binlm = log_transform(mel_spec)eps=1e-10mfcc = dct_ii(lm[len(lm) // 2], 13)coefficient 0 is overall energyKey formulas#
- Multiply
- (a + bi)(c + di) = (ac - bd) + (ad + bc)i. Magnitudes multiply, angles add.
- Polar
- r = \sqrt{a^2 + b^2} and \theta = \mathop{\mathrm{atan2}}(b, a).
- Euler
- e^{i\theta} = \cos\theta + i \sin\theta. Multiply by it to rotate by \theta.
- DFT
- X[k] = \sum_{n=0}^{N-1} x[n] \, e^{-2\pi i k n/N}.
- Inverse DFT
- positive exponent and a 1/N factor. Reconstruction is exact.
- Bins
- f_k = k \cdot f_s / N, resolution f_s / N, maximum f_s / 2.
- Convolution theorem
- x * h = \text{IFFT}(\text{FFT}(x) \cdot \text{FFT}(h)).
- Mel
- m = 2595 \cdot \log_{10}(1 + f / 700).
From Lesson 01.19, Lesson 01.20, Lesson 06.02
Sample rates#
| Command | What it does |
|---|---|
| 8 kHz | Telephony. Nyquist at 4 kHz kills consonants. Avoid for ASR |
| 16 kHz | ASR standard. A 10 s clip is 160,000 floats |
| 22.05 kHz | TTS vocoder training for older models |
| 24 kHz | Modern TTS |
| 44.1 kHz | CD audio, music |
| 48 kHz | Film, pro audio, high-fidelity TTS |
From Lesson 06.01
Window functions#
| Command | What it does |
|---|---|
| Rectangular | No window. Side lobes -13 dB. Signal exactly periodic in N |
| Hann | 0.5 (1 - cos(2 pi n / (N-1))). -31 dB. General purpose |
| Hamming | 0.54 - 0.46 cos(2 pi n / (N-1)). -42 dB. Speech analysis |
| Blackman | Wide main lobe. -58 dB. Side lobe suppression is critical |
From Lesson 01.20
Numbers and defaults#
| Setting | Default | Note |
|---|---|---|
| Frame length at 16 kHz | 400 | 25 ms window |
| Hop at 16 kHz | 160 | 10 ms. 10 s gives 998 frames |
| Mel bins for ASR | 80 | Audio classification uses 128 |
| MFCC coefficients | 13 | Keyword spotting uses 40 |
| Log-mel epsilon | 1e-10 | Inside log(mel_spec + eps) |
| Log-magnitude epsilon | 1e-6 | Inside log(abs(X) + eps) |
| 1024-point FFT at 16 kHz | 15.6 Hz | 512 usable bins over 0 to 8 kHz |
| PCM exchange format | 16-bit | int16, range plus or minus 32,767 |
From Lesson 06.01, Lesson 06.02
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
| Garbage output that looks like a model bug | Sample rate mismatch | Match sample rate first. 16 kHz mono float32 |
| 7 kHz tone peaks at 3 kHz | Aliasing above Nyquist | Low-pass filter before downsampling |
| Spurious high frequencies | Spectral leakage at the frame edges | Apply a Hann or Hamming window |
| Silent failure at inference | Mel count mismatch, 80 against 128 | Log the feature shape at both ends |
| Wrong input for Whisper | dB-mel, not log-mel | Feed log-mel |
| WER doubles in production | Normalization drift | Use the same normalization in training and inference |
| Flat spectrum in the last frames | Zero padding at the end | Pad symmetrically or replicate |
| FFT convolution wraps around | The DFT computes circular convolution | Zero-pad to N + M - 1 |
From Lesson 06.01, Lesson 06.02, Lesson 01.20
Questions#
- Does zero-padding add resolution?
- No. Resolution needs T = 1 / \Delta f seconds of data.
- Why keep only the first N/2 + 1 bins?
- For real input, X[N-k] is the conjugate of X[k].
- Why is i a rotation?
- Multiply by i to turn 90 degrees. Two turns give i^2 = -1.
- How does RoPE use this?
- It rotates queries and keys. Relative position becomes an angle.
- Why mel, not Hz?
- People hear pitch on a log scale. 100 to 200 Hz sounds like 1000 to 2000 Hz.
- What does Parseval say?
- Energy is the same in the time and frequency domains.
From Lesson 01.19, Lesson 01.20, Lesson 06.02
Tips#
- Start with 80 log-mels unless you work on music.
- A larger FFT gives better frequency resolution and worse time resolution.
- Use a power of 2 length for the FFT. Zero-pad to the next power of 2.
- The N roots of unity have magnitude 1 and sum to zero.
- Check
idft(dft(x))against x. It must match to machine precision. - MFCC coefficient 0 encodes overall energy. Models usually drop it.
From Lesson 01.19, Lesson 01.20, Lesson 06.02