WaveGlow developer on retainer: 50–100 ms reverberant tail after utterance ending when final mel frames have near-zero energy — normalizing flow affine coupling scale term approaches zero causing numerical instability in inverse mapping — energy-threshold audio trim at utterance end eliminates tail artifact — 3 weeks — WaveGlow developer on monthly retainer
WaveGlow (NVIDIA/waveglow) normalizing-flow-based neural vocoder • Python 3.11; PyTorch 2.1.x • real-time TTS pipeline • correct audio for most inputs but utterances ending in soft consonants (“fish”, “gas”, “wash”) produce a 50–100 ms reverberant-sounding tail after the utterance ends — washed-out echo past the natural sentence ending • artifact absent when same mels are fed to HiFi-GAN • developer checks WaveGlow configuration, mel normalization, and flow-step count for 3 weeks • root cause: affine coupling layer’s scale term approaches zero when final mel frames have near-zero energy; normalizing flow’s inverse mapping becomes numerically unstable in that region; instability produces waveform ringing that decays slowly over 50–100 ms; fix: detect utterance end using 10 ms RMS energy threshold (−50 dBFS) and hard-trim audio at detected boundary • reverberant tail: 50–100 ms → 0 ms • MOS: 3.6 → 4.0 • 1.5h retainer engagement
Westfield WaveGlow Ltd. deployed WaveGlow as the vocoder in a production real-time TTS pipeline. The pipeline used an acoustic model to generate mel spectrograms and WaveGlow to synthesize the final waveform. For the vast majority of utterances WaveGlow produced clean, natural-sounding speech. But a consistent artifact emerged in production: utterances that ended in soft fricative or sibilant consonants — words like “fish”, “gas”, “wash”, “path”, “match”, or any sentence ending with an unvoiced consonant followed by silence — had a 50–100 ms reverberant, washed-out extension after the natural utterance end. The continuation sounded like a faint reverberation or echo of the final consonant, persisting well past where the utterance should have been cleanly silent. The same mel spectrograms fed to HiFi-GAN produced clean endings with no tail, isolating the issue to WaveGlow specifically.
The team initially hypothesized that the mel spectrogram normalization at utterance boundaries was different between WaveGlow’s training distribution and their inference pipeline — that perhaps their upstream acoustic model was generating boundary frames that WaveGlow’s decoder mishandled. They verified mel normalization parameters matched the WaveGlow checkpoint exactly. They then checked whether increasing the number of WaveGlow flow steps would stabilize the boundary behavior and whether the sigma parameter of the sampling distribution at inference was set correctly. Neither change eliminated the tail. After three weeks they engaged a WaveGlow retainer developer who traced the root cause to the normalizing flow’s affine coupling layer becoming numerically unstable when processing near-zero-energy mel frames at utterance boundaries.
The WaveGlow inference pipeline and boundary artifact diagnostic at the time of the engagement:
# Westfield WaveGlow Ltd. — WaveGlow reverberant tail at utterance boundary
# WaveGlow (NVIDIA/waveglow) normalizing-flow-based neural vocoder
# Python 3.11, PyTorch 2.1.x
# Deployment: real-time TTS pipeline
import torch
import numpy as np
from waveglow.denoiser import Denoiser
import sys
sys.path.append('waveglow/')
import glow
# Load WaveGlow checkpoint (NVIDIA pretrained)
waveglow = torch.load('waveglow_256channels_universal_v5.pt')['model']
waveglow = waveglow.remove_weightnorm(waveglow)
waveglow.cuda().eval()
denoiser = Denoiser(waveglow).cuda()
# Load upstream acoustic model mel spectrogram for a soft-consonant ending
# (e.g., "She cleaned the dish" — utterance ends in unvoiced /ʃ/ → near-zero energy)
mel = load_mel_from_acoustic_model("She cleaned the dish")
# mel shape: (1, 80, T) where T ≈ 85 frames for this utterance
# Inspect mel energy at utterance boundary:
mel_energy = mel.squeeze(0).mean(0) # (T,) — mean energy per frame
print(f"Final 3 frames mean energy: {mel_energy[-3:].tolist()}")
# Output: [0.0031, 0.0008, 0.0002] ← near-zero energy in final frames
# Normal frames mid-utterance: 0.15–0.45
# WaveGlow affine coupling layer behavior with near-zero scale:
# (conceptual — the affine coupling layers in WaveGlow compute:
# y = x * exp(log_s) + b (forward)
# x = (y - b) * exp(-log_s) (inverse)
# where log_s and b are conditioned on the mel spectrogram context.
# When mel energy is near-zero, the learned log_s network outputs
# log_s values that cause exp(log_s) → near-zero (the network learned
# that low energy corresponds to near-silence, so it shrinks the scale).
# The inverse then divides by exp(log_s) → 1/near-zero → numerically large.
# This numerical instability propagates through the remaining flow steps,
# producing a slowly-decaying artifact in the final waveform.)
with torch.no_grad():
audio = waveglow.infer(mel.cuda(), sigma=0.666)
audio = denoiser(audio, strength=0.01)[:, 0]
audio_np = audio.squeeze().cpu().numpy() # shape: (N_samples,)
sr = 22050
# Detect reverberant tail using 10 ms RMS energy window
window_samples = int(0.010 * sr) # 220 samples per 10 ms window
def rms_energy_db(segment):
rms = np.sqrt(np.mean(segment ** 2))
return 20 * np.log10(rms + 1e-10)
# Scan from end of audio to find where energy drops below -50 dBFS
# and measure tail duration
expected_end_sample = int(0.68 * sr) # approximate natural utterance end
tail_start = None
for i in range(expected_end_sample, len(audio_np) - window_samples, window_samples // 2):
window = audio_np[i:i + window_samples]
if rms_energy_db(window) > -50: # still above silence threshold
tail_start = i
if tail_start is not None:
tail_duration_ms = (len(audio_np) - tail_start) / sr * 1000
print(f"Reverberant tail detected: {tail_duration_ms:.1f} ms after utterance end")
# Output: Reverberant tail detected: 73.4 ms after utterance end
else:
print("No tail detected (this would be the fixed output)")
Why WaveGlow’s normalizing flow becomes numerically unstable at near-zero energy utterance boundaries
WaveGlow is a generative normalizing flow model: it learns an invertible mapping between mel spectrograms and audio waveforms. The model consists of a sequence of affine coupling layers, each of which applies a scale-and-translate transformation conditioned on a local context window of the mel spectrogram. During training, the model learns the forward flow (audio → latent); during inference, the inverse flow (latent → audio) is applied to a Gaussian noise sample conditioned on the mel input.
Each affine coupling layer’s inverse operation computes x = (y − b) × exp(−log_s), where log_s and b are outputs of a small WaveNet conditioned on the mel context. During training on natural speech recordings, the model learns that near-zero mel energy at utterance boundaries correlates with near-silence in the waveform, so the network learns to output small log_s values (and correspondingly small exp(log_s)) when conditioning on low-energy mel frames. In the inverse flow, exp(−log_s) is then large when exp(log_s) is small, causing the inverse operation to amplify the residual Gaussian noise sample far beyond its normal range. This numerical instability cascades through the remaining coupling layers and manifests in the final reconstructed waveform as slowly-decaying energy in the region following the natural utterance end — the characteristic 50–100 ms reverberant tail.
# Fix: energy-threshold audio trim to remove reverberant tail at utterance end
# Detects the last sample above a -50 dBFS RMS threshold and trims there
# No WaveGlow model modification needed
import torch
import numpy as np
import sys
sys.path.append('waveglow/')
import glow
from waveglow.denoiser import Denoiser
waveglow = torch.load('waveglow_256channels_universal_v5.pt')['model']
waveglow = waveglow.remove_weightnorm(waveglow)
waveglow.cuda().eval()
denoiser = Denoiser(waveglow).cuda()
SAMPLE_RATE = 22050
TRIM_WINDOW_MS = 10 # 10 ms RMS energy window
TRIM_THRESHOLD_DBFS = -50 # silence threshold
TRIM_LOOKAHEAD_MS = 20 # scan from this many ms before expected end
def trim_reverberant_tail(audio_np: np.ndarray, sr: int,
window_ms: int = TRIM_WINDOW_MS,
threshold_dbfs: float = TRIM_THRESHOLD_DBFS) -> np.ndarray:
"""
Trim trailing reverberant tail from WaveGlow output.
Scans from the last above-threshold window forward and hard-cuts there.
"""
window_samples = int(window_ms / 1000 * sr)
hop_samples = window_samples // 2
n = len(audio_np)
last_active_sample = 0
for i in range(0, n - window_samples, hop_samples):
window = audio_np[i:i + window_samples]
rms = np.sqrt(np.mean(window ** 2))
rms_db = 20 * np.log10(rms + 1e-10)
if rms_db > threshold_dbfs:
last_active_sample = i + window_samples
# Add half a window of natural decay after last active sample
end_sample = min(last_active_sample + window_samples, n)
return audio_np[:end_sample]
def synthesize_fixed(mel: torch.Tensor) -> np.ndarray:
"""Synthesize audio with WaveGlow and apply reverberant-tail trim."""
with torch.no_grad():
audio = waveglow.infer(mel.cuda(), sigma=0.666)
audio = denoiser(audio, strength=0.01)[:, 0]
audio_np = audio.squeeze().cpu().numpy()
return trim_reverberant_tail(audio_np, SAMPLE_RATE)
# ---- Results after energy-threshold audio trim fix ----
#
# Test set: 100 utterances ending in unvoiced fricatives and affricates
# ('f', 's', 'sh', 'th', 'ch', 'ts') where final mel frames have < 0.01 mean energy
#
# Reverberant tail detection (10 ms RMS > -50 dBFS after expected utterance end):
# Buggy: 73/100 utterances (73%) had detectable reverberant tail (mean 72 ms)
# Fixed: 0/100 utterances (0%) — trim correctly removes all tails
#
# Natural utterance ending (control — normal energy endings):
# Trim does not activate when energy is above -50 dBFS throughout
# Control set: 0/100 utterances affected by trim (correct behavior)
#
# MOS evaluation (20-listener crowdsource, 50 soft-consonant ending sentences):
# Buggy: MOS 3.6 (σ=0.5) — "echo or reverb after sentence ends"
# Fixed: MOS 4.0 (σ=0.3) — "clean, natural ending"
#
# Normal sentence MOS (control):
# Before fix: MOS 4.0 → After fix: MOS 4.0 (no regression)
#
# RTF impact of trim step: negligible (<0.1 ms per utterance on CPU)
print("Fix: energy-threshold audio trim at utterance end")
print("Reverberant tail: 50–100 ms → 0 ms; MOS: 3.6 → 4.0; 1.5h")
Related retainer posts
- UnivNet developer on retainer — Multi-resolution spectral loss neural vocoder; 3–5 ms clicking artifact at fricative onset when upstream mel energy above 7 kHz exceeds training distribution; GAN generator over-sharpens high-frequency onset; fix: mel high-frequency energy cap at 7 kHz before inference — 3 weeks
- HiFi-GAN developer on retainer — GAN-based neural vocoder; intermittent high-frequency ringing artifact at onset of plosive consonants; multi-period discriminator period-4 sub-band aliasing; fix: mel spectrogram high-frequency smoothing before HiFi-GAN inference — 3 weeks
- LPCNet developer on retainer — LPC + neural vocoder hybrid; whispered speech synthesized correctly for first 400 ms then reverts to voiced speech; GRU hidden state convergence toward training-distribution mean; fix: initialize GRU state with mean whispered-speech state vector — 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 →