TAI BUI
Back to curriculum
BuildPython

Spectrograms, Mel Scale & Audio Features

Neural nets barely tolerate raw waveforms. Hand them a spectrogram and they get traction; hand them a mel spectrogram and they get good. Every ASR, TTS, and audio classifier in 2026 lives or dies by that one preprocessing choice.

TIME: ~45 minutesPREREQUISITES: Phase 6 · 01 (Audio Fundamentals)

Learning Objectives

  • Explain why raw waveforms are a poor model input and how the STFT turns one into a time-frequency spectrogram.
  • Build a mel filterbank from scratch in Python and produce log-mel spectrograms and MFCCs from raw audio.
  • Match a downstream task (ASR, TTS, classification, speaker ID, music, keyword spotting) to the feature type and frame/hop settings it expects.
  • Catch the production pitfalls that come from feature mismatches: mel count, sample rate, dB vs log, normalization drift, and padding leakage.

The Problem

Take a 10-second clip sampled at 16 kHz. That's 160,000 floats, all sitting in [-1, 1], almost perfectly uncorrelated with the label "dog barking" or "the word cat". The information is in there, just not in a form a model can pull out directly: two identical phonemes spoken 100 ms apart produce completely different raw samples.

A spectrogram fixes that. It throws away the temporal detail human hearing never uses anyway (jitter at the microsecond scale) and keeps the structure perception actually tracks: which frequencies carry energy, over windows of roughly 10–25 ms.

Mel spectrograms take it further. Humans perceive pitch on a log scale, so 100 Hz vs 200 Hz feels like the same gap as 1000 Hz vs 2000 Hz. The mel scale warps the frequency axis to match that perception. A mel-scaled spectrogram has been the single most important feature in speech ML from 2010 through 2026.

Warm-up0/2
1. A clip is recorded at a 16 kHz sample rate. How many samples does 2 seconds of it contain?Warm-up
A two-second timeline marked 0 s, 1 s and 2 s, filled with thin bars that stand for individual samples, with a question mark for the total count.
Bars are schematic: far fewer are drawn than a real recording holds.
2. What does applying a Fourier transform to a short chunk of audio tell you?Warm-up
A short waveform chunk drawn as a plot, an arrow into a box labelled Fourier transform, and an arrow to an unknown labelled with a question mark.
Illustrative chunk: the script adds two sine waves.
Check: The Problem0/2
1. Why is a raw 16 kHz waveform a poor input for a speech-classification model?Explain
A 20 ms stretch of a waveform plotted as amplitude between minus one and plus one, with 320 samples across.
Synthetic vowel-like tone, not speech.
2. What perceptual property does the mel scale approximate?Explain
A line plot of mel value (0 to about 2840) against frequency from 0 to 8000 Hz; the line rises steeply at first and then flattens.
Curve computed with numpy from the lesson's formula.

The Concept

Waveform to STFT to mel spectrogram to MFCC ladder

STFT (Short-Time Fourier Transform). Slice the waveform into overlapping frames (typical: 25 ms window, 10 ms hop, which is 400 samples / 160 samples at 16 kHz). Multiply each frame by a window function (Hann by default; Hamming trades off slightly differently). FFT each frame, then stack the magnitude spectra into a matrix of shape (n_frames, n_freq_bins). That matrix is your spectrogram.

Log-magnitude. Raw magnitudes span 5-6 orders of magnitude, so every production pipeline compresses that range before doing anything else: log(|X| + 1e-6) or 20 * log10(|X|), log-magnitude rather than raw.

Mel scale. Frequency f in Hz maps to mel m by m = 2595 * log10(1 + f / 700). The mapping runs roughly linear below 1 kHz and roughly logarithmic above it. 80 mel bins covering 0–8 kHz is the standard ASR input.

Mel filterbank. A set of triangular filters spaced equally on the mel scale, each one a weighted sum of adjacent FFT bins. Multiply the STFT magnitude by the filterbank matrix and you get the mel spectrogram in one matmul.

Log-mel spectrogram. log(mel_spec + 1e-10). Whisper, Parakeet, and SeamlessM4T all take this as input: the universal 2026 audio frontend.

MFCCs. Take the log-mel spectrogram, apply a DCT (type II), and keep the first 13 coefficients. That decorrelates the features and compresses them further. It was the dominant feature until around 2015, when CNNs and Transformers running on raw log-mels caught up, though it still shows up in speaker recognition (x-vectors, ECAPA).

Resolution trade. A larger FFT buys better frequency resolution at the cost of worse time resolution. 25 ms / 10 ms is the audio-ML default; 50 ms / 12.5 ms fits music better; 5 ms / 2 ms is for transient detection (drum hits, plosives).

Check: The Concept0/12
1. In audio feature extraction, what is a frame?Define
A log-mel spectrogram image with 80 rows (mel bins 0 to 79) and 98 columns (indices 0 to 97) for a one-second clip.
Synthetic vowel-like tone, not speech. Computed with numpy, window 400 and hop 160.
2. What is a hop in a spectrogram pipeline?Define
Three overlapping frames laid on a sample axis from 0 to 720: frame 0 covers 0 to 400, frame 1 covers 160 to 560 and frame 2 covers 320 to 720, with the hop left as a question mark.
Sample positions are the lesson's settings at 16 kHz (400-sample window, 160-sample step).
3. What is a window function in the STFT?Define
A 400-sample waveform frame of a sine-like signal labelled sample 0 and sample 399.
Illustrative sine wave generated by the script, not speech.
4. What is the STFT?Define
A one-second waveform of 16,000 samples goes into a box labelled STFT and comes out as a heat-map image of 98 by 201 numbers.
Synthetic tone, not speech. Window 400, hop 160, computed with numpy; the image is pooled along frequency to fit.
5. What is the mel scale for, and what is its formula?Define
A 0 to 8000 Hz axis with even ticks every 1000 Hz above a second axis with eight even steps from 0 to 2840, joined by dashed lines from each mel tick to the Hz position it lands on.
Positions computed with numpy from the mel mapping.
6. What is a mel filterbank?Define
A heat-map of the 80 by 201 filterbank matrix: most rows have a short run of non-zero values (rows 1, 3, 6 and 10 are empty), and the runs shift to the right and widen from the bottom row to the top.
Captured by running the lesson's mel_filterbank (brighter means a larger weight; colours are brightened for visibility).
7. What is a log-mel spectrogram?Define
A table of six mel bins from one frame with their mel spectrogram values in scientific notation and an unknown column marked with question marks.
Values come from a synthetic vowel-like tone, computed with numpy.
8. To compute MFCCs from a log-mel spectrogram, the lesson says you apply what transform and keep how many coefficients?Define
A bar chart of the 80 values of one log-mel frame with an arrow to an unknown transform and an unknown number of kept coefficients, both marked with question marks.
Bars show one frame of a synthetic tone's log-mel spectrogram (shifted so the lowest bar is zero).
9. Why does every production pipeline apply a log to the spectrogram magnitudes?Explain
A heat-map of STFT magnitudes, frequency up and frame index across, drawn with a linear colour scale from 0 to the maximum; most of the image is dark with a few bright bands.
Synthetic tone, not speech. Computed with numpy, window 400, hop 160, pooled along frequency.
10. What frame/hop setting suits transient detection such as drum hits or plosives, compared to the 25 ms / 10 ms default?Explain
A 60 ms waveform that is nearly silent until 20 ms, then a short burst that dies away within about 10 ms.
Illustrative synthetic hit (decaying noise burst), not a recording.
11. Why does MFCC extraction apply a DCT to the log-mel frame?Explain
A bar chart of one log-mel frame with 80 bars, an arrow labelled DCT, and an unknown result marked with a question mark.
Frame from a synthetic tone's log-mel spectrogram (shifted so the lowest bar is zero).
12. You track the pitch of a sustained note with 5 ms frames and the harmonics blur together. What is the cause?Analyze
A smooth spectrum curve from 0 to 1600 Hz in which the peaks for five harmonics of a 220 Hz note run together, with orange ticks marking where the harmonics really are.
Synthetic note of five harmonics; averaged spectrum of 5 ms Hann frames, computed with numpy.

Build It

Step 1: frame the waveform

python
def frame(signal, frame_len, hop):
    n = 1 + (len(signal) - frame_len) // hop
    return [signal[i * hop : i * hop + frame_len] for i in range(n)]

A 10-second 16 kHz clip with frame_len=400, hop=160 yields 998 frames.

Step 2: Hann window

python
import math
 
def hann(N):
    return [0.5 * (1 - math.cos(2 * math.pi * n / (N - 1))) for n in range(N)]

Multiply this into each frame element-wise before the FFT. It removes the spectral leakage that comes from truncating a frame at non-zero endpoints.

Step 3: STFT magnitude

python
def stft_magnitude(signal, frame_len=400, hop=160):
    win = hann(frame_len)
    frames = frame(signal, frame_len, hop)
    return [magnitudes(dft([w * s for w, s in zip(win, f)])) for f in frames]

Production code uses torch.stft or librosa.stft (FFT-backed, vectorized). The loop above is pedagogical; it runs fine on the short clips in code/main.py.

Step 4: mel filterbank

python
def hz_to_mel(f):
    return 2595.0 * math.log10(1.0 + f / 700.0)
 
def mel_to_hz(m):
    return 700.0 * (10 ** (m / 2595.0) - 1)
 
def mel_filterbank(n_mels, n_fft, sr, fmin=0, fmax=None):
    fmax = fmax or sr / 2
    mels = [hz_to_mel(fmin) + (hz_to_mel(fmax) - hz_to_mel(fmin)) * i / (n_mels + 1)
            for i in range(n_mels + 2)]
    hzs = [mel_to_hz(m) for m in mels]
    bins = [int(h * n_fft / sr) for h in hzs]
    fb = [[0.0] * (n_fft // 2 + 1) for _ in range(n_mels)]
    for m in range(n_mels):
        for k in range(bins[m], bins[m + 1]):
            fb[m][k] = (k - bins[m]) / max(1, bins[m + 1] - bins[m])
        for k in range(bins[m + 1], bins[m + 2]):
            fb[m][k] = (bins[m + 2] - k) / max(1, bins[m + 2] - bins[m + 1])
    return fb

80 mels covering 0–8 kHz with n_fft=400 gives an (80, 201) matrix. Multiply the (n_frames, 201) STFT magnitude by its transpose and you get an (n_frames, 80) mel spectrogram.

Step 5: log-mel

python
def log_mel(mel_spec, eps=1e-10):
    return [[math.log(max(v, eps)) for v in frame] for frame in mel_spec]

Common alternatives: librosa.power_to_db (reference-normalized dB), 10 * log10(power + eps). Whisper uses a more involved clip-and-normalize routine (see Whisper's log_mel_spectrogram).

Step 6: MFCCs

python
def dct_ii(x, n_coeffs):
    N = len(x)
    return [
        sum(x[n] * math.cos(math.pi * k * (2 * n + 1) / (2 * N)) for n in range(N))
        for k in range(n_coeffs)
    ]

Apply DCT to each log-mel frame and keep the first 13 coefficients. That's your MFCC matrix. The first coefficient is usually dropped, since it just encodes overall energy.

Check: Build It0/8
1. What is the purpose of multiplying each frame by a Hann window before the FFT?Explain
A 1030 Hz sine frame multiplied by a Hann curve, which is zero at both ends and one in the middle, then passed to an FFT box.
Illustrative frame and curve computed with numpy; the Hann curve is stretched to fill its panel.
2. Why does the lesson drop the first MFCC coefficient?Explain
Thirteen boxes labelled c0 to c12 holding the MFCC values of one frame, with the c0 box outlined.
Values from the lesson's dct_ii on one frame of a synthetic tone.
3. Using frame() with frame_len=400 and hop=160, how many frames come from a 1-second 16 kHz clip (16,000 samples)?Apply
A bar for a 16,000-sample clip with the first three overlapping 400-sample frames starting at 0, 160 and 320, an ellipsis, and the frame count left as a question mark.
Frame positions drawn to scale.
4. What does hann(400)[0], the first window value, evaluate to?Apply
The lesson's hann function in Python, and the expression hann(400)[0] with its value left as a question mark.
5. What is hz_to_mel(700) from the lesson's formula, to the nearest mel?Apply
The lesson's hz_to_mel function and a Python prompt where hz_to_mel(700) has been entered with its output left as a question mark.
Typed prompt; the output is deliberately left blank.
6. The lesson's mel_filterbank code produces an (80, 201) matrix for n_fft=400. What do the 201 columns represent?Apply
A rectangle labelled mel_filterbank(80, 400, 16000) with shape (80, 201), 80 rows on its left, and the number of columns marked with a question mark.
Schematic of the shape the lesson states.
7. The STFT magnitude of a 10-second clip has shape (998, 201). The filterbank has shape (80, 201). What is the mel spectrogram shape after the multiplication the lesson describes?Apply
A tall block labelled STFT magnitude with shape (998, 201), a wider block labelled filterbank with shape (80, 201), and a dashed unknown block for the mel spectrogram marked with a question mark.
Schematic; block sizes are not to scale.
8. Log-mel frames of 80 values each go through dct_ii with n_coeffs=13, and the first coefficient is then dropped. How many values describe each frame?Apply
A flow from a log-mel frame of 80 values, through dct_ii with n_coeffs=13, then dropping the first coefficient, ending at an unknown count marked with a question mark.
Schematic.

Use It

The 2026 stack:

TaskFeatures
ASR (Whisper, Parakeet, SeamlessM4T)80 log-mels, 10 ms hop, 25 ms window
TTS acoustic model (VITS, F5-TTS, Kokoro)80 mels, 5–12 ms hop for fine temporal control
Audio classification (AST, PANNs, BEATs)128 log-mels, 10 ms hop
Speaker embedding (ECAPA-TDNN, WavLM)80 log-mels or raw-waveform SSL
Music (MusicGen, Stable Audio 2)EnCodec discrete tokens (not mels)
Keyword spotting40 MFCCs for tiny devices

Rule of thumb: if you are not working on music, start with 80 log-mels. The burden of proof is on any deviation.

Check: Use It0/5
1. What rule of thumb does the lesson give for choosing a feature type?Explain
Horizontal bars for the count of numbers in a 10 second clip: 160,000 for the raw waveform, 127,744 for 128 log-mels, 79,840 for 80 log-mels and 12,974 for 13 MFCCs.
Computed from the lesson's 998-frame count.
2. Which feature does the lesson's table assign to music generation models like MusicGen and Stable Audio 2?Explain
A music waveform on the left, an unknown feature step marked with a question mark in the middle, and the models MusicGen and Stable Audio 2 on the right.
Schematic.
3. You are building a keyword-spotting model for a tiny always-on device. Which feature choice does the lesson's table support?Design
A microphone feeding an unknown feature step marked with a question mark, which feeds a tiny always-on device doing keyword spotting.
Schematic.
4. You are starting a new speech recognition model with no special constraints. What input should you pick, and why?Design
Speech audio feeding an unknown input-feature step marked with a question mark, which feeds a speech recognition model that outputs text.
Schematic.
5. You are building a TTS acoustic model and want fine control over the timing of the generated speech. Which feature setting does the lesson support?Design
A text box feeding a TTS acoustic model that outputs a row of mel frames; the spacing between frames is left as a question mark.
Schematic; the number of frames drawn is arbitrary.

Pitfalls that still ship in 2026

  • Mel count mismatch. Train on 80 mels, run inference on 128, and nothing crashes. It just fails silently. Log the feature shape on both ends and catch it before it costs you a debugging session.
  • Sample-rate mismatch upstream. Mels computed at 22.05 kHz don't look like mels computed at 16 kHz. Fix the sample rate before featurization, not after.
  • dB vs log. Whisper expects log-mel, not dB-mel. A few HF pipelines autodetect the difference; your custom code will not.
  • Normalization drift. Per-utterance normalization during training, global normalization during inference: a production bug that quietly doubles WER (word error rate, the standard ASR accuracy metric).
  • Leakage from padding. Zero-padding the end of a clip leaves a flat spectrum in the trailing frames. Pad symmetrically, or replicate instead.
Check: Pitfalls that still ship in 20260/10
1. What happens if a model is trained with 80 mels but run at inference with 128 mels?Explain
Two feature tensors, training features of shape (998, 80) and inference features of shape (998, 128), both pointing at a model trained on 80 mels, with the result left as a question mark.
Shapes use the 998 frames of a 10 s clip.
2. Mel spectrograms computed at 22.05 kHz look different from those computed at 16 kHz. What fix does the lesson recommend?Explain
Two mel spectrogram images of the same 1 kHz tone, one at 16,000 Hz and one at 22,050 Hz sample rate; the bright horizontal line sits at a different mel bin in each.
Computed with numpy, one filterbank per sample rate (0 to half the rate).
3. Which representation does Whisper expect, and what happens if custom code feeds it the other one?Explain
A table of five mel bins with their mel values and two columns of rescaled values, natural log and ten times log base 10.
Computed with numpy from a synthetic tone.
4. What production bug does the lesson describe under normalization drift?Explain
Two identical four-step pipelines, audio to log-mel to normalize to model, one labelled Training and one Inference; in each the normalize step has its statistics source marked with a question mark.
Schematic.
5. What problem does zero-padding the end of a clip cause, and what is the fix?Explain
A one-second waveform whose first half carries a tone and whose second half is flat at zero, outlined as the padding.
Synthetic tone, not speech.
6. A model fine-tuned on 80-mel features performs far worse after you swap the preprocessing library at inference. There are no errors. What should you check first?Analyze
Two preprocessing libraries, A used for fine-tuning and B used at inference, both feeding one model that was fine-tuned on 80-mel features; the result is no error but worse performance.
Schematic of the stem.
7. An ASR service trained with per-utterance normalization has double the WER in production, where features use a global mean and variance. Which pitfall is this?Analyze
Two boxes, training using per-utterance normalization and production using a global mean and variance, with a note that word error rate has doubled.
Schematic of the stem.
8. Short clips in a batch are zero-padded at the end, and the model behaves oddly on them compared to long clips. What does the lesson say is happening?Analyze
Three horizontal bars of different lengths for clips A, B and C; the shorter two have a dashed continuation labelled zeros that brings them to the same length as clip A.
Schematic; lengths are not measured values.
9. Your custom code feeds Whisper features made with 10 * log10(power + eps), and transcripts degrade with no error. What is the most likely cause?Analyze
Two lines of custom Python: features computed as 10 times log base 10 of power plus eps, then passed to a Whisper model; the result is degraded transcripts with no error.
Typed from the stem, not captured.
10. You must ship a model whose feature pipeline will be reimplemented by another team. Which practice best avoids silent mismatches?Design
Two teams' pipelines, the original training one and a reimplementation, both producing a feature tensor of shape (n_frames, ?) that feeds one model.
Schematic.

Ship It

Save this as outputs/skill-feature-extractor.md. The skill picks feature type, mel count, frame/hop, and normalization for whatever model you're targeting.

Author's Note

The mel-count mismatch pitfall above is the one I've personally lost a day to. Fine-tune a model on 80-mel features, swap in a slightly different preprocessing library at inference time, and it quietly defaults to 128 mels. No error, no warning, just a model that suddenly performs like it forgot how to listen. If you take one habit from this lesson, make it this: log the exact shape of your feature tensor at training time, and assert it matches at inference time, every single run. It costs one line and saves you a debugging session that won't look anything like a feature-extraction bug until you're three hours into it.

Exercises

  1. Easy. Run code/main.py. It synthesizes a chirp (frequency swept 200 → 4000 Hz) and prints the argmax mel bin per frame. Plot it if you want, and confirm the peak tracks the sweep.
  2. Medium. Re-run with n_mels in {40, 80, 128} and frame_len in {200, 400, 800}. Measure the sharp-peak bandwidth across the time axis. Which combination resolves the chirp best?
  3. Hard. Implement power_to_db and compare ASR accuracy of a tiny CNN classifier on AudioMNIST using (a) raw log-mel, (b) dB-mel with ref=max, (c) MFCC-13 + delta + delta-delta. Report top-1 accuracy.

Key Terms

TermWhat people sayWhat it actually means
FrameA slice25 ms chunk of waveform fed to one FFT.
HopStrideSamples between consecutive frames; 10 ms is ASR default.
WindowHann/Hamming thingPoint-wise multiplier that tapers the frame edges to zero.
STFTSpectrogram generatorFramed + windowed FFT; yields time × frequency matrix.
MelWarped frequencyLog-perception scale; m = 2595·log10(1 + f/700).
FilterbankThe matrixTriangular filters that project STFT onto mel bins.
Log-melWhisper's inputlog(mel_spec + eps); standardized in 2026.
MFCCOld-school featureDCT of log-mel; 13 coeffs, decorrelated.

Further Reading

Code

Ships With