SpeechBrain developer on retainer: Resample(8000→16000) upsampling telephone audio does not create frequency content above 4 kHz — ECAPA-VDNN attention pooling operates on silent 4–8 kHz band — EER stuck at 0.18 — 3 weeks — SpeechBrain developer on monthly retainer
speechbrain 0.5.x • Python 3.11 • PyTorch 2.1.x • speaker verification on 500-speaker dataset (100 utterances per speaker, 8 kHz telephone audio) • SpeakerRecognition.from_hparams('speechbrain/spkrec-ecapa-voxceleb') pretrained on VoxCeleb 16 kHz wideband • torchaudio.transforms.Resample(orig_freq=8000, new_freq=16000) upsamples to match model • upsampling via linear interpolation does NOT create frequency content above 4 kHz (8 kHz Nyquist limit) • resulting 16 kHz signal has energy only in 0–4 kHz band • 4–8 kHz band entirely silent • ECAPA-VDNN TDNN layers and attention pooling trained on full 0–8 kHz VoxCeleb bandwidth • attention weights operate on empty frequency range • EER stuck at 0.18 (expected <0.05) • developer checks AngularMargin loss, fine-tuning learning rate, pooling strategy for 3 weeks • fix: telephone-domain pretrained model; EER 0.18→0.04 • 1.5h retainer engagement
Eastfield SpeechBrain NLP Ltd. built a speaker verification system for a 500-speaker telephone audio dataset using SpeechBrain 0.5.x. The team used the pretrained speechbrain/spkrec-ecapa-voxceleb ECAPA-VDNN model and fine-tuned it with AngularMargin loss on their telephone corpus. Because the model expects 16 kHz input and the telephone audio was recorded at 8 kHz, the developer applied torchaudio.transforms.Resample(orig_freq=8000, new_freq=16000) before passing audio to the model. After fine-tuning for 20 epochs, the EER was stuck at 0.18 — far above the sub-0.05 target for a 500-speaker closed-set verification task. Three weeks of hyperparameter tuning on the AngularMargin loss margin and scale, fine-tuning learning rates, and attention pooling configurations produced no improvement. A SpeechBrain retainer developer identified the root cause: upsampling 8 kHz telephone audio to 16 kHz creates samples but not frequency content, leaving the 4–8 kHz band that the ECAPA model relies on completely silent.
The audio preprocessing and model loading code at the time of the engagement:
# Eastfield SpeechBrain NLP Ltd. — speaker verification on telephone audio
# speechbrain 0.5.x, Python 3.11, PyTorch 2.1.x, torchaudio 2.1.x
import torch
import torchaudio
import speechbrain as sb
from speechbrain.pretrained import SpeakerRecognition
# Pretrained ECAPA-VDNN model from VoxCeleb
# VoxCeleb audio characteristics:
# - Sampling rate: 16000 Hz
# - Source: YouTube celebrity interviews (wideband)
# - Bandwidth: 0–8000 Hz (full Nyquist range for 16 kHz audio)
# - ECAPA-VDNN was trained to extract speaker embeddings from wideband audio
# with energy distributed across 0–8 kHz.
# TDNN layers: temporal convolutional layers with dilation 1, 2, 3
# Attention pooling: multi-head self-attention over time frames
# The attention mechanism learns to weight frames by speaker-discriminative content.
# Speaker-discriminative features in wideband speech: F2 and F3 formants (1–4 kHz),
# harmonics (1–8 kHz), vocal tract resonance patterns, prosodic micro-patterns (2–6 kHz).
# Application audio characteristics:
# - Sampling rate: 8000 Hz (G.711 telephone codec)
# - Source: call center telephone recordings
# - Bandwidth: 300–3400 Hz (G.711 band-limited by anti-aliasing filter)
# (Some energy up to ~4000 Hz, but significantly attenuated above 3400 Hz)
# - Nyquist frequency of 8 kHz audio: 4000 Hz
# → NO frequency content above 4000 Hz exists in the signal
# ← Problem: Resample(8000→16000) is used to match the model's expected 16 kHz rate.
# WHAT RESAMPLING DOES:
# Input: x[n], sampled at 8000 Hz, N samples → T_8kHz = N/8000 seconds
# Output: y[m], sampled at 16000 Hz, 2N samples → T_16kHz = 2N/16000 = N/8000 = T_8kHz (same duration)
# HOW IT WORKS (linear/polyphase interpolation):
# Inserts new samples between existing samples by weighted interpolation.
# This is mathematically equivalent to: (1) zero-pad in frequency domain, (2) low-pass filter at 4000 Hz.
# The low-pass filter at 4000 Hz CANNOT create new frequency content above 4000 Hz.
# The resulting 16 kHz signal has:
# - Frequency content 0–4000 Hz: PRESENT (same as original 8 kHz signal, amplitude-matched)
# - Frequency content 4000–8000 Hz: ZERO (silent — no information, only interpolation artifacts)
#
# ECAPA-VDNN processing of this upsampled telephone signal:
# The TDNN's first layer convolves a 5-frame context window over mel filterbank features.
# The mel filterbank for 16 kHz audio covers 0–8000 Hz (128 bins).
# For the upsampled telephone audio:
# - Mel bins 0–64: cover 0–2000 Hz → ACTIVE (telephone content present)
# - Mel bins 65–80: cover 2–3.4 kHz → ACTIVE (attenuated telephone content)
# - Mel bins 81–96: cover 3.4–5 kHz → NEAR-SILENT (above telephone bandwidth)
# - Mel bins 97–127: cover 5–8 kHz → SILENT (zero energy from upsampling)
# The attention pooling mechanism, trained on wideband VoxCeleb data, learned to assign
# significant attention weights to the 4–8 kHz frequency range where VoxCeleb speakers
# have discriminative energy. For telephone audio, this range is silent.
# The model computes speaker embeddings over a frequency space that is half-empty.
# Load pretrained ECAPA-VDNN
verifier = SpeakerRecognition.from_hparams(
source="speechbrain/spkrec-ecapa-voxceleb",
savedir="pretrained_models/spkrec-ecapa-voxceleb",
)
def preprocess_audio(audio_path, target_sr=16000):
"""Load 8 kHz telephone audio and upsample to 16 kHz."""
waveform, orig_sr = torchaudio.load(audio_path) # orig_sr = 8000
# ← WRONG: upsampling adds samples but NOT frequency content
# After Resample(8000→16000):
# - Number of samples doubles (T seconds × 16000 = 2 × T seconds × 8000)
# - Frequency content above 4000 Hz: ZERO (8 kHz Nyquist limit)
# - The 4-8 kHz mel bands will contain only interpolation noise, not speaker signal
if orig_sr != target_sr:
resampler = torchaudio.transforms.Resample(
orig_freq=orig_sr,
new_freq=target_sr,
)
waveform = resampler(waveform) # 8000 Hz → 16000 Hz, but 4-8 kHz band is silent
return waveform, target_sr
# Fine-tune on telephone dataset with AngularMargin loss
# After 20 epochs: EER = 0.18 (expected < 0.05)
# Developer checks:
# - AngularMargin margin: 0.1, 0.2, 0.3, 0.4 — EER stays 0.17-0.19
# - Fine-tuning lr: 1e-3, 5e-4, 1e-4, 5e-5 — EER stays 0.17-0.20
# - Freeze first 3 TDNN layers — EER 0.19 (worse)
# - Multi-head attention pooling vs self-attentive — EER 0.18 (no change)
# - 3 weeks, no improvement below 0.17 EER
Why upsampling 8 kHz to 16 kHz creates samples but not frequency content above 4 kHz
The Nyquist-Shannon sampling theorem states that a signal sampled at rate fs can represent frequency content up to fs/2. For G.711 telephone audio sampled at 8000 Hz, the maximum representable frequency is 4000 Hz. The band-limiting filter applied during analog-to-digital conversion in the telephone network ensures no energy above 4000 Hz is present in the digital signal — in practice, the G.711 codec restricts speech to 300–3400 Hz, so even the 3400–4000 Hz region is attenuated.
When this 8 kHz signal is upsampled to 16 kHz using torchaudio.transforms.Resample, the resampler inserts new samples between existing samples via interpolation. This interpolation is mathematically equivalent to zero-padding in the frequency domain followed by applying a low-pass filter at 4000 Hz. The resulting 16 kHz signal contains exactly the same frequency content as the original 8 kHz signal (0–4000 Hz), plus a completely empty 4000–8000 Hz band. The resampler cannot invent frequency content that was not present in the original signal — it can only rearrange the existing samples into a denser time grid.
# Diagnostic: verify that 4-8 kHz band is silent after upsampling
# torchaudio 2.1.x, Python 3.11
import torch
import torchaudio
import numpy as np
def measure_band_energy_ratio(waveform, sr, low_hz, high_hz):
"""Fraction of total FFT energy in [low_hz, high_hz] band."""
# Compute FFT
n_fft = 2048
spec = torch.stft(waveform.squeeze(0),
n_fft=n_fft,
return_complex=True)
power = spec.abs().pow(2).mean(dim=-1) # mean over time frames
# Frequency bins
freqs = torch.linspace(0, sr / 2, n_fft // 2 + 1)
band_mask = (freqs >= low_hz) & (freqs <= high_hz)
total_energy = power.sum().item()
band_energy = power[band_mask].sum().item()
return band_energy / max(total_energy, 1e-10)
# Generate a synthetic telephone speech signal:
# White noise band-limited to 300-3400 Hz (G.711 telephone bandwidth)
sr_telephone = 8000
duration = 3.0
t = torch.linspace(0, duration, int(sr_telephone * duration))
# Simulate telephone signal: frequencies from 300 to 3400 Hz
telephone_signal = torch.zeros_like(t)
for f in range(300, 3400, 50):
telephone_signal += torch.sin(2 * torch.pi * f * t) / (3400 - 300) * 50
telephone_signal = telephone_signal.unsqueeze(0) # [1, N]
# Upsample to 16 kHz
resampler = torchaudio.transforms.Resample(orig_freq=8000, new_freq=16000)
telephone_16k = resampler(telephone_signal)
# Measure energy in key frequency bands
print("=== 8 kHz telephone signal ===")
e_0_4k_8k = measure_band_energy_ratio(telephone_signal, 8000, 0, 4000)
print(f" Energy fraction 0-4 kHz: {e_0_4k_8k:.3f}") # ~1.000 (all energy below Nyquist)
print("\n=== Upsampled to 16 kHz via Resample ===")
e_0_4k = measure_band_energy_ratio(telephone_16k, 16000, 0, 4000)
e_4k_8k = measure_band_energy_ratio(telephone_16k, 16000, 4000, 8000)
print(f" Energy fraction 0-4 kHz: {e_0_4k:.3f}") # ~0.998 (nearly all energy)
print(f" Energy fraction 4-8 kHz: {e_4k_8k:.4f}") # ~0.0003 (essentially zero)
# Energy fraction 0-4 kHz: 0.998
# Energy fraction 4-8 kHz: 0.0003
# ← 4-8 kHz band is 99.97% empty after upsampling.
# ECAPA-VDNN's attention pooling assigns weights to this frequency range
# based on VoxCeleb wideband training, but the input is silent here.
# The attention mechanism effectively ignores half the frequency space
# when processing upsampled telephone audio.
print("\nConclusion:")
print("Upsampling 8 kHz → 16 kHz does NOT create frequency content above 4 kHz.")
print("The 4-8 kHz band in the upsampled signal is essentially silent.")
print("ECAPA-VDNN trained on wideband VoxCeleb expects signal in 4-8 kHz band.")
print("Domain mismatch: EER = 0.18 instead of expected < 0.05")
Fix: use a telephone-domain pretrained model or apply bandpass filtering before feature extraction
The fundamental issue is a domain mismatch between the pretrained model’s training data (VoxCeleb wideband 16 kHz) and the application data (telephone narrowband 8 kHz). Two approaches resolve it. Option A, which is faster to implement, is to switch to a SpeechBrain pretrained model that was trained on data augmented with telephone-condition audio: speechbrain/spkrec-ecapa-voxceleb-aug. This model includes training augmentation that covers narrowband and noise conditions, so its learned representations are robust to the missing 4–8 kHz band. Option B, more comprehensive but slower, is to retain the original model and apply a bandpass filter (300–3400 Hz, the G.711 telephone band) before extracting features, then fine-tune only the aggregation layers on telephone-domain data. This tells the model explicitly that only the 300–3400 Hz band is informative and prevents attention weights from being wasted on the silent 4–8 kHz band.
With Option A applied (switching to the telephone-augmented checkpoint), the EER drops from 0.18 to 0.04 after fine-tuning for 15 epochs on the same telephone corpus. The 1.5-hour retainer engagement identified the domain mismatch by measuring spectral energy in the 4–8 kHz band of the upsampled telephone audio and confirming it was near-zero before attempting any model changes.
# Fix option A: use telephone-domain pretrained model
# speechbrain 0.5.x, Python 3.11, PyTorch 2.1.x
from speechbrain.pretrained import SpeakerRecognition
# ← FIXED: speechbrain/spkrec-ecapa-voxceleb-aug was trained on VoxCeleb
# with augmentation including:
# - Room impulse response convolution (simulates reverb)
# - Additive noise (babble, music, noise)
# - Narrowband simulation (bandpass filtering to simulate telephone bandwidth)
# The model's speaker representations are more robust to the missing 4-8 kHz band.
verifier_fixed = SpeakerRecognition.from_hparams(
source="speechbrain/spkrec-ecapa-voxceleb-aug", # ← telephone-augmented checkpoint
savedir="pretrained_models/spkrec-ecapa-voxceleb-aug",
)
# Audio preprocessing: still upsample to 16 kHz (required for model input),
# but add bandpass filtering to explicitly mark the telephone bandwidth.
import torchaudio.functional as F_audio
def preprocess_telephone_audio(audio_path, target_sr=16000):
"""Load 8 kHz telephone audio with correct preprocessing for ECAPA-VDNN."""
waveform, orig_sr = torchaudio.load(audio_path)
# Step 1: Upsample to 16 kHz (required by ECAPA input pipeline)
if orig_sr != target_sr:
resampler = torchaudio.transforms.Resample(
orig_freq=orig_sr, new_freq=target_sr
)
waveform = resampler(waveform)
# Step 2 (Option B component): Apply bandpass filter to signal the telephone band.
# Bandpass 300-3400 Hz explicitly zeros out the near-silent 4-8 kHz band,
# making the silence intentional rather than a domain artifact.
# lowpass first, then highpass to implement bandpass.
waveform = F_audio.lowpass_biquad(waveform, sample_rate=target_sr, cutoff_freq=3400)
waveform = F_audio.highpass_biquad(waveform, sample_rate=target_sr, cutoff_freq=300)
return waveform, target_sr
# Fine-tune telephoneaugmented model on telephone corpus:
# - Same 500-speaker dataset, 100 utterances per speaker
# - Same AngularMargin loss (margin=0.2, scale=30)
# - 15 fine-tuning epochs
# - EER: 0.18 → 0.04
# Results:
# Option A alone (spkrec-ecapa-voxceleb-aug, no bandpass filter): EER = 0.06
# Option A + B (aug model + bandpass filter): EER = 0.04
# Original model (spkrec-ecapa-voxceleb) with bandpass filter only: EER = 0.09
# ← Full fix: aug model + bandpass filter achieves EER = 0.04 (target: < 0.05)
# ---- Why Option A alone (EER=0.06) is better than bandpass only (EER=0.09) ----
# The aug model's attention pooling was trained knowing some frequency bands
# may be silent — it doesn't waste attention weight on the 4-8 kHz band.
# The original model's attention was trained only on wideband audio and cannot
# easily adapt its frequency-dependent attention weights during fine-tuning
# on a 500-speaker dataset (insufficient data to retrain 60M-parameter ECAPA).
# ---- Hour breakdown ----
# Triage: 0.5h — EER=0.18; spectral energy analysis shows 4-8 kHz band
# near-zero in upsampled telephone audio; ECAPA attention weights
# from VoxCeleb training assign high weight to silent frequency bands
# Root cause: 0.5h — Resample(8000→16000) confirmed not to create new frequency
# content; telephone Nyquist = 4000 Hz; 4-8 kHz band silent;
# pretrained model trained on wideband audio with energy in 4-8 kHz
# Fix: 0.25h — switch to telephone-augmented checkpoint + bandpass filter
# Validation: 0.25h — EER 0.18→0.04; 1.5h total
Related retainer posts
- librosa developer on retainer —
melspectrogram(fmax=8000)truncates glass breaking and bird call frequencies — val_accuracy 0.61 vs expected 0.78+ — 3 weeks - skorch developer on retainer —
Checkpoint(dirname='best_ckpt')shared across allGridSearchCVfolds — fold-5 overwrites best — deployed model uses wrong weights — 3 weeks - TorchMetrics developer on retainer —
Dice.reset()not called between train and val — blended score 0.88 reported as val — deployed model 0.14 Dice below reported — 3 weeks
Track retainer hours without a client portal
HourTab turns your time-tracker CSV into a public URL your client bookmarks. No login. No portal. CSV in, URL out.
See pricing →