HiFi-GAN developer on retainer: persistent 50 Hz power line hum in all vocoded output after domain fine-tuning — training recordings captured in European home studio without notch filter — 50 Hz hum encoded in mel bin 0 of every training frame — generator learns to reproduce it — 3 weeks — HiFi-GAN developer on monthly retainer

HiFi-GAN V1 (config_v1) neural vocoder • 80-dim mel spectrograms at 22,050 Hz • fine-tuned on 10 hours domain-specific speaker recordings • European home studio — no electrical isolation • 50 Hz hum at −40 dBFS in all training recordings (inaudible during data review) • persistent 50 Hz hum in all vocoded utterances after fine-tuning • developer checks recording equipment, fine-tuning optimizer, discriminator architecture for 3 weeks • root cause: mel bin 0 (centered 50–80 Hz) encodes hum across all training frames; fine-tuning trains generator to reproduce bin 0 energy • fix: scipy.signal.iirnotch(50.0, Q=30.0, fs=22050) applied to all training recordings before mel extraction • mel re-extracted • HiFi-GAN re-fine-tuned • hum eliminated • MOS unchanged • 1.5h retainer engagement

Eastfield HiFi-GAN Audio Ltd. had deployed HiFi-GAN V1 as the neural vocoder in their text-to-speech pipeline, producing high-quality speech synthesis for a documentary narration service. The base HiFi-GAN V1 checkpoint, trained on LJSpeech, produced excellent output for the reference female voice. When a client contracted them to synthesize narration in a different speaker’s voice — a male baritone recorded in a home studio in Germany — the team fine-tuned HiFi-GAN V1 on ten hours of recordings from the target speaker.

The fine-tuning ran for 100,000 steps without issue: the generator loss converged, the discriminator maintained stable training, and the vocoded output clearly resembled the target speaker’s voice. When the team listened carefully to the fine-tuned output, they noticed a problem that had not been present in the LJSpeech baseline: a soft but unmistakable low-frequency hum running continuously under all vocoded speech, present on every utterance regardless of the input text. The hum was at a consistent pitch — measured at exactly 50 Hz using a spectrum analyzer — and at approximately −38 dBFS: clearly audible in a quiet listening environment, definitely unacceptable for broadcast-quality documentary narration. The team spent three weeks checking the recording equipment (the client’s studio microphone, preamp, and DAW), experimenting with different fine-tuning optimizer settings, and removing discriminator sub-modules from the training configuration. None of these changes removed the hum. They engaged a HiFi-GAN retainer developer who identified the root cause in the first 15 minutes: the training recordings contained a 50 Hz power line hum at −40 dBFS that was invisible to casual listening but had been faithfully learned by the generator.

The fine-tuning pipeline at the time of the engagement:

# Eastfield HiFi-GAN Audio Ltd. — domain fine-tuning pipeline
# HiFi-GAN V1 (config_v1), Python 3.11, PyTorch 2.1.x
# Fine-tuning target: male baritone, German home studio, 10 hours

import scipy.signal
import librosa
import numpy as np

# ---- Mel spectrogram extraction (the bug: no notch filter) ----
def extract_mel(wav_path, config):
    wav, sr = librosa.load(wav_path, sr=config.sampling_rate)
    # config.sampling_rate = 22,050 Hz
    # Mel filterbank configuration:
    #   n_fft = 1024
    #   hop_length = 256 (mel frame rate = 22050/256 = 86.1 Hz)
    #   n_mels = 80
    #   fmin = 0 Hz  ← bin 0 covers approximately 0–275 Hz
    #   fmax = 8,000 Hz

    # NO notch filter applied before mel extraction
    # The 50 Hz hum at -40 dBFS in wav is extracted into mel bin 0
    # Bin 0 (centered ~50–80 Hz) captures the hum energy in every frame
    # This is consistent across all 10 hours of training recordings

    mel = librosa.feature.melspectrogram(
        y=wav,
        sr=sr,
        n_fft=config.n_fft,
        hop_length=config.hop_length,
        n_mels=config.num_mels,
        fmin=config.fmin,
        fmax=config.fmax,
    )
    mel_db = librosa.power_to_db(mel, ref=np.max)
    return mel_db  # (80, T_frames)

# ---- HiFi-GAN V1 fine-tuning (abbreviated) ----
# Generator: 4 upsampling blocks, MRF (multi-receptive-field fusion)
# Discriminator: MPD (multi-period: periods [2, 3, 5, 7, 11]) + MSD (multi-scale)
#
# Fine-tuning learns to match the spectral distribution of the domain-specific recordings.
# This includes:
#   - Vocal tract resonances (formants): correctly learned → target speaker voice quality
#   - Fundamental frequency patterns (F0): correctly learned → pitch prosody
#   - Noise floor and room acoustics: partially learned
#   - 50 Hz power line hum in mel bin 0: fully learned ← the bug
#
# After 100,000 fine-tuning steps:
#   Generator loss: converged (0.42)
#   MPD loss: stable (0.18)
#   MSD loss: stable (0.21)
#   MOS on target speaker: 4.1 (target ≥ 4.0) ← passes quality gate
#   50 Hz hum in output: -38 dBFS ← not measured in quality gate (bug not caught)

# ---- Diagnosis: measuring 50 Hz hum in training recordings ----
def measure_hum(wav_path, target_hz=50.0, sr=22050):
    wav, _ = librosa.load(wav_path, sr=sr)
    # Welch power spectral density estimate
    freqs, psd = scipy.signal.welch(wav, fs=sr, nperseg=sr)
    # Power at target frequency (bin nearest to 50 Hz)
    target_bin = np.argmin(np.abs(freqs - target_hz))
    hum_power = psd[target_bin]
    # Convert to dBFS (relative to full-scale sine wave at RMS 1.0)
    hum_dbfs = 10 * np.log10(hum_power + 1e-12)
    return hum_dbfs

# Measured on 20 random training recordings:
# hum_dbfs range: -42 to -38 dBFS
# Average: -40 dBFS at 50 Hz
# 60 Hz: -62 dBFS (German power grid: 50 Hz, not 60 Hz — as expected)
# Conclusion: systematic 50 Hz hum in all training recordings from the home studio

# ---- Mel bin 0 analysis ----
# With fmin=0 Hz, n_mels=80, fmax=8000 Hz:
# Mel filterbank bin centers (approximate, Hz):
#   bin 0:  0 – 275 Hz  (covers entire 50 Hz region)
#   bin 1:  275 – 550 Hz
#   ...
# 50 Hz hum at -40 dBFS → bin 0 energy elevated by approximately 2 dB above noise floor
# This elevation is consistent across ALL training frames (not just voiced frames)
# Fine-tuning learns bin 0 elevation as a speaker characteristic → hum in all output
print("Root cause: 50 Hz hum at -40 dBFS in training recordings → mel bin 0 contamination")
print("Generator fine-tuned to reproduce bin 0 energy → hum in all vocoded output")

Why mel bin 0 contamination causes HiFi-GAN to synthesize hum after fine-tuning

HiFi-GAN’s fine-tuning process trains the generator to minimize the difference between the mel spectrogram of the generated waveform and the mel spectrogram of the target recording. This is not a selective process — the generator learns to reproduce all spectral characteristics that are consistently present across the training data, including low-frequency hum if that hum is consistently encoded in the mel filterbank. The mel filterbank bin at the lowest frequency (bin 0 with fmin=0, or bin 0 with fmin=80 Hz if the 50 Hz hum falls below fmin) captures energy in the 0–275 Hz range, which includes both the fundamental frequency of the target speaker’s voice and any power line hum present in the recordings.

When the 50 Hz hum at −40 dBFS is present in every training recording, every mel spectrogram in the training set has a characteristic elevated energy in bin 0 relative to a hum-free recording. The fine-tuned generator learns this elevated bin 0 energy as a defining spectral characteristic of the target speaker — indistinguishable from formant structure or vocal tract resonance from the generator’s perspective, because all training examples consistently show it. The multi-period discriminator reinforces this learning by detecting the 50 Hz periodicity (period 441 samples at 22,050 Hz) through its period=2 sub-discriminator and penalizing the generator when the generated waveform lacks it. After fine-tuning, the generator reproduces bin 0 hum in its output from any input mel spectrogram — even from perfectly clean mel spectrograms extracted from recordings without any hum.

# Fix: apply 50 Hz notch filter to all training recordings before mel extraction
# HiFi-GAN V1, Python 3.11, PyTorch 2.1.x

import scipy.signal
import librosa
import numpy as np

def notch_filter_wav(wav, sr, notch_hz=50.0, quality_factor=30.0):
    """
    Apply a notch (band-reject) filter at notch_hz with quality factor Q.
    Q = notch_hz / bandwidth → Q=30 gives bandwidth = 50/30 ≈ 1.67 Hz
    Narrow bandwidth: attenuates 50 Hz specifically, preserves all other frequencies.
    """
    b, a = scipy.signal.iirnotch(w0=notch_hz, Q=quality_factor, fs=sr)
    # Apply filter: zero-phase (forward-backward) to avoid phase distortion
    wav_filtered = scipy.signal.filtfilt(b, a, wav)
    return wav_filtered.astype(np.float32)

def extract_mel_clean(wav_path, config):
    wav, sr = librosa.load(wav_path, sr=config.sampling_rate)
    # sr = 22,050 Hz

    # Apply 50 Hz notch filter BEFORE mel extraction
    wav = notch_filter_wav(wav, sr, notch_hz=50.0, quality_factor=30.0)
    # Note: for North American power grids (60 Hz), use notch_hz=60.0
    # For 50 Hz harmonics (100 Hz, 150 Hz), apply additional notches if needed:
    #   wav = notch_filter_wav(wav, sr, notch_hz=100.0, quality_factor=30.0)
    #   wav = notch_filter_wav(wav, sr, notch_hz=150.0, quality_factor=30.0)
    # In this case, only fundamental 50 Hz was present — harmonics at noise floor

    mel = librosa.feature.melspectrogram(
        y=wav,
        sr=sr,
        n_fft=config.n_fft,
        hop_length=config.hop_length,
        n_mels=config.num_mels,
        fmin=config.fmin,
        fmax=config.fmax,
    )
    mel_db = librosa.power_to_db(mel, ref=np.max)
    return mel_db  # (80, T_frames) — mel bin 0 no longer carries hum energy

# ---- Re-fine-tune HiFi-GAN from pre-trained LJSpeech checkpoint ----
# Apply notch_filter_wav to ALL 10 hours of training recordings
# Re-extract mel spectrograms using extract_mel_clean
# Re-run fine-tuning for 100,000 steps from pre-trained checkpoint
# (not from the hum-contaminated fine-tuned checkpoint — start clean)

# ---- Results after fix ----
# 50 Hz spectral peak in vocoded output:
#   Before fix:  -38 dBFS (audible in quiet environment)
#   After fix:   -72 dBFS (below perceptual threshold; at noise floor)
# MOS comparison (50 utterances, 8 listeners):
#   Before fix:  4.1 ± 0.1
#   After fix:   4.1 ± 0.1  (unchanged — hum was not contributing to quality score)
# PESQ comparison:
#   Before fix:  3.7
#   After fix:   3.7
# Speaker similarity (d-vector cosine):
#   Before fix:  0.78 (target ≥ 0.70)
#   After fix:   0.77 (unchanged — 0.01 within measurement noise)
# Hum audibility test:
#   Before fix:  7/8 listeners detected hum in quiet room
#   After fix:   0/8 listeners detected hum

# ---- Why notch filter with Q=30 ----
# Q = center_frequency / bandwidth
# Q=30 → bandwidth = 50/30 = 1.67 Hz (attenuates 50 Hz ± 0.83 Hz)
# Higher Q (narrower): more precise attenuation, less impact on neighboring frequencies
# Lower Q (wider): catches harmonics but may affect low vocal fundamentals (100–150 Hz)
# Q=30 chosen: narrow enough to preserve male baritone F0 (80–130 Hz),
#              wide enough to fully suppress 50 Hz hum

# ---- Alternative: post-extraction mel denoising ----
# Apply median filter to mel bin 0 across time:
#   mel_db[0, :] -= np.median(mel_db[0, :])  # remove DC offset from bin 0
# Less effective than waveform notch filtering:
#   - Removes bin 0 offset but not the periodic 50 Hz oscillation pattern
#   - 50 Hz at 22,050 Hz sample rate completes one cycle in 441 samples = 1.72 mel frames
#   - Bin 0 oscillates between high and low values at 1.72-frame period
#   - Median filter smooths but does not eliminate the oscillation
#   - Waveform notch filter is 20× more effective (measured in output hum reduction)
# Recommendation: always filter the waveform, not the mel spectrogram

# ---- Data quality audit checklist (run before any HiFi-GAN fine-tuning) ----
def audit_recording(wav_path, sr=22050):
    wav, _ = librosa.load(wav_path, sr=sr)
    issues = []
    # 1. 50 Hz hum (European power grid)
    freqs, psd = scipy.signal.welch(wav, fs=sr, nperseg=sr)
    hum_50 = 10 * np.log10(psd[np.argmin(np.abs(freqs - 50.0))] + 1e-12)
    if hum_50 > -50:
        issues.append(f"50 Hz hum at {hum_50:.1f} dBFS (threshold: -50 dBFS)")
    # 2. 60 Hz hum (North American power grid)
    hum_60 = 10 * np.log10(psd[np.argmin(np.abs(freqs - 60.0))] + 1e-12)
    if hum_60 > -50:
        issues.append(f"60 Hz hum at {hum_60:.1f} dBFS (threshold: -50 dBFS)")
    # 3. Clipping
    if np.any(np.abs(wav) >= 0.999):
        clip_count = np.sum(np.abs(wav) >= 0.999)
        issues.append(f"Clipping: {clip_count} samples at ±1.0")
    # 4. DC offset
    dc = np.mean(wav)
    if abs(dc) > 0.001:
        issues.append(f"DC offset: {dc:.4f} (threshold: ±0.001)")
    return issues

# ---- Hour breakdown ----
# Triage:     0.25h — 50 Hz hum in all output; spectral analysis confirms 50 Hz peak;
#             absent in pre-fine-tuned baseline; appeared only after domain fine-tuning
# Root cause: 0.5h — welch PSD on training recordings: hum at -40 dBFS in all files;
#             mel bin 0 encodes 50 Hz energy; fine-tuning learns it as speaker characteristic;
#             MPD period=2 sub-discriminator reinforces 50 Hz periodicity in generator output
# Fix:        0.5h — iirnotch(50 Hz, Q=30) on all training wavs; mel re-extracted;
#             HiFi-GAN re-fine-tuned from LJSpeech checkpoint for 100,000 steps
# Validation: 0.25h — 50 Hz hum: -72 dBFS (below threshold); MOS 4.1 unchanged; 1.5h total
print("Fix complete: 50 Hz hum eliminated; MOS unchanged at 4.1")

Related retainer posts


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 →