OpenVoice developer on retainer: broadband noise artifact −45 dBFS in synthesized output not present in base TTS voice — OpenVoice V2 Mel Decoder learns microphone noise floor from reference recordings — audible in quiet passages before silence — 3 weeks — OpenVoice developer on monthly retainer

OpenVoice V2 (myshell-ai/OpenVoice) tone color converter TTS • Python 3.11; PyTorch 2.1.x • corporate voice branding service • tone color conversion: base TTS voice → client reference voice • broadband noise artifact −45 dBFS in synthesized output; not in base TTS voice before conversion • perceptible in quiet passages at sentence-final pauses and breath points • developer checks OpenVoice V2 configuration, reference audio recording quality, base TTS model for 3 weeks • root cause: Mel Decoder trained on SM7B home studio reference recordings with noise floor −42 dBFS; decoder learns noise floor as target voice characteristic in low-energy frames; reproduces broadband noise in synthesized output • fix: noisereduce spectral gate (threshold −60 dBFS) on reference audio before tone color conversion • broadband noise artifact: −45 → −72 dBFS (inaudible) • 1.5h retainer engagement

Northfield OpenVoice TTS Ltd. built a corporate voice branding service that uses OpenVoice V2 to convert a base TTS voice into a client’s branded voice, using short reference recordings provided by the client. The tone color conversion produces synthesized speech that accurately captures the client voice’s timbre, cadence, and tonal characteristics for most of the output. But every synthesized audio file contained a persistent broadband noise artifact at approximately −45 dBFS that was not present in the base TTS voice before conversion. The noise was inaudible during louder voiced speech passages but became clearly perceptible in quiet sections — particularly in the 100–300 ms of near-silence at the end of each sentence before the next one began, and at breath points between phrases. It sounded like a subtle but consistent microphone hiss layered under the voice.

The team tried different base TTS voices (three different base models), different OpenVoice V2 configuration parameters, and re-recorded the reference audio three times with the same SM7B microphone the client used in their home studio. Each attempt produced the same −45 dBFS noise artifact in the output. The noise was present in the synthesized audio regardless of the input text, the base TTS voice used, or the inference parameters. After three weeks of investigation without a resolution, they engaged an OpenVoice retainer developer who identified that the Mel Decoder was learning the microphone noise floor from the reference recordings and reproducing it in every synthesized output at low-energy frames.

The OpenVoice V2 conversion pipeline at the time of the engagement:

# Northfield OpenVoice TTS Ltd. — corporate voice branding
# OpenVoice V2 (myshell-ai/OpenVoice), Python 3.11, PyTorch 2.1.x
# Use case: convert base TTS voice to client branded voice

import os
import torch
import librosa
import numpy as np
from openvoice import se_extractor
from openvoice.api import ToneColorConverter

# Load OpenVoice V2 tone color converter
ckpt_converter = 'checkpoints_v2/converter'
device = "cuda" if torch.cuda.is_available() else "cpu"
tone_color_converter = ToneColorConverter(
    f'{ckpt_converter}/config.json', device=device
)
tone_color_converter.load_ckpt(f'{ckpt_converter}/checkpoint.pth')

# Reference audio: client recorded with SM7B, home studio, 24 kHz
# Reference consists of 10 short utterances (5-15 seconds each)
# Measured noise floor: -42 dBFS RMS (silent frame segments, Praat)
reference_dir = 'reference_client_voice/'
reference_files = [os.path.join(reference_dir, f)
                   for f in os.listdir(reference_dir)
                   if f.endswith('.wav')]

def compute_noise_floor_dbfs(wav_path, threshold_rms=0.001):
    """
    Measure noise floor: RMS dBFS of silent frame segments.
    Silent segments = frames where RMS < threshold (speech absent).
    """
    audio, sr = librosa.load(wav_path, sr=None)
    frame_length = int(0.025 * sr)  # 25 ms frames
    hop_length = int(0.010 * sr)    # 10 ms hop

    frames = librosa.util.frame(audio, frame_length=frame_length,
                                 hop_length=hop_length)
    rms_per_frame = np.sqrt(np.mean(frames ** 2, axis=0))

    silent_frames = rms_per_frame[rms_per_frame < threshold_rms]
    if len(silent_frames) == 0:
        return -80.0  # No silent frames detected
    noise_floor_rms = np.mean(silent_frames)
    return 20 * np.log10(noise_floor_rms + 1e-10)  # dBFS

# Bug: reference recordings have high noise floor
# SM7B home studio noise floor measurement:
#   reference_01.wav: -41.8 dBFS RMS (silent frames)
#   reference_02.wav: -42.3 dBFS RMS (silent frames)
#   reference_03.wav: -41.5 dBFS RMS (silent frames)
#   Mean noise floor: -41.9 dBFS
#
# This noise floor is loud enough to be learned by the Mel Decoder:
#   Professional studio reference:    < -65 dBFS noise floor
#   SM7B home studio (untreated room): -42 dBFS noise floor
#   Difference:                        23 dB higher than professional

def convert_voice_buggy(text, base_speaker_id, reference_files):
    """
    Convert base TTS voice to client voice using tone color conversion.

    OpenVoice V2 architecture:
    1. Base TTS voice generates mel spectrogram (clean, no noise)
    2. se_extractor computes target speaker embedding from reference files
       - Mel Encoder maps each reference mel spectrogram to speaker embedding
       - Speaker embedding is mean-pooled across all reference utterances
       - Embedding encodes: pitch, timbre, rhythm AND noise floor characteristics
    3. Mel Decoder transforms base mel spectrogram using target speaker embedding
       - Decoder learned to reproduce target voice from reference recordings
       - At low-energy frames: decoder reproduces noise floor from reference

    Bug: Mel Decoder learned -42 dBFS noise floor from SM7B reference recordings.
    Every synthesized output has broadband noise at -45 dBFS in silent frames.
    """
    # Extract speaker embedding from reference recordings
    # Bug: reference files include -42 dBFS noise floor in silent frames
    target_se, audio_name = se_extractor.get_se(
        reference_files,
        tone_color_converter,
        target_dir='processed',
        vad=True  # VAD does not remove noise floor from voiced frames
    )

    # Base TTS synthesis (clean — no noise artifact)
    source_se = torch.load(f'checkpoints_v2/base_speakers/ses/{base_speaker_id}.pth')
    src_path = f'tmp/base_tts_{audio_name}.wav'
    # ... generate base TTS audio to src_path ...

    # Tone color conversion: introduces -45 dBFS noise from learned Mel Decoder
    tone_color_converter.convert(
        audio_src_path=src_path,
        src_se=source_se,
        tgt_se=target_se,
        output_path=f'output/converted_{audio_name}.wav',
        message="@MyShell",
    )

# Measured synthesized output noise floor (before fix):
#   synthesized silent frames: -44.8 dBFS RMS (3 dB above reference — amplified)
#   base TTS voice silent frames: -72.4 dBFS RMS (clean — no noise from base TTS)
#   noise introduced by Mel Decoder: 27.6 dB above base TTS noise floor
#
# Root cause: Mel Decoder training data included reference recordings with
# -42 dBFS noise floor in silent frames. The decoder learned to reproduce
# broadband noise at that level as a characteristic of the target voice at
# low-energy moments. The VAD (voice activity detection) in se_extractor
# separates voiced from unvoiced segments during speaker embedding extraction,
# but it does not filter the low-level noise from voiced frames.
# The Mel Decoder learns from full utterances including word boundaries,
# sentence-final pauses, and breath points where the reference noise dominates.

print("Root cause: Mel Decoder learns SM7B noise floor -42 dBFS as target voice characteristic")
print("Synthesized output: -44.8 dBFS noise in silent frames vs -72.4 dBFS for base TTS")

Why OpenVoice V2 Mel Decoder learns and reproduces the reference microphone noise floor

OpenVoice V2 uses a Mel Encoder to strip the base TTS voice’s speaker identity from its mel spectrogram, producing a speaker-neutral acoustic representation. The Mel Decoder then reconstructs the mel spectrogram in the target speaker’s tone color by applying the acoustic characteristics learned during training on the reference recordings. The critical issue is that the Mel Decoder is trained on every frame of the reference recordings, including the low-energy frames at word boundaries, sentence pauses, and breath points where the speech signal is essentially absent. At these frames, the dominant acoustic content is the recording environment: the microphone’s self-noise floor, room reflections, and ambient noise captured during the recording session. For the SM7B microphone in a typical home studio (untreated room, HVAC noise, computer fan noise), this noise floor measured −42 dBFS RMS in silent frame segments — audible under quiet listening conditions.

When the Mel Decoder learns from these reference recordings, it learns to reproduce the −42 dBFS noise pattern at low-energy frames as a characteristic of the target voice’s silence moments. During synthesis, the Mel Decoder applies this learned noise pattern to every low-energy frame in the synthesized output. The base TTS voice (generated by a clean neural TTS model in a studio-equivalent acoustic environment) has a noise floor of approximately −72 dBFS at silent frames. After tone color conversion, the Mel Decoder replaces that clean near-silence with the −42 dBFS noise pattern it learned from the reference. The fix is to apply spectral noise gating to the reference recordings before Mel Decoder training, using the noisereduce library with a noise profile estimated from the initial silent frames of each recording. After noise gating, the reference silent frames measure below −72 dBFS, and the Mel Decoder learns the target voice characteristics without the noise floor artifact.

# Fix: spectral noise gate on reference audio before tone color conversion
# noisereduce library: stationary noise estimation + spectral subtraction

import os
import numpy as np
import soundfile as sf
import librosa
import noisereduce as nr
import torch
from openvoice import se_extractor
from openvoice.api import ToneColorConverter

ckpt_converter = 'checkpoints_v2/converter'
device = "cuda" if torch.cuda.is_available() else "cpu"
tone_color_converter = ToneColorConverter(
    f'{ckpt_converter}/config.json', device=device
)
tone_color_converter.load_ckpt(f'{ckpt_converter}/checkpoint.pth')

NOISE_THRESHOLD_DBFS = -60.0  # Noise gate threshold: reject noise above this floor

def apply_spectral_noise_gate(input_wav_path, output_wav_path,
                               noise_duration_s=0.5):
    """
    Apply spectral noise gate to reference audio before tone color conversion.

    Uses noisereduce stationary noise reduction:
    1. Estimate noise profile from initial silent segment (first noise_duration_s)
    2. Apply spectral subtraction to attenuate noise across the full recording
    3. Verify post-processing noise floor is below NOISE_THRESHOLD_DBFS

    noisereduce uses STFT-based spectral subtraction:
    - Decomposes audio into frequency bins via STFT
    - Estimates noise magnitude spectrum from silent reference segment
    - Subtracts estimated noise spectrum from each frame using soft masking
    - Preserves speech content while attenuating stationary noise

    Args:
        input_wav_path: Path to raw reference recording
        output_wav_path: Path to write noise-reduced recording
        noise_duration_s: Duration of initial silent segment for noise profile
    """
    audio, sr = librosa.load(input_wav_path, sr=None, mono=True)

    # Step 1: Estimate noise profile from initial silent segment
    noise_sample_frames = int(noise_duration_s * sr)
    noise_sample = audio[:noise_sample_frames]

    # Step 2: Apply noisereduce spectral subtraction
    reduced_audio = nr.reduce_noise(
        y=audio,
        sr=sr,
        y_noise=noise_sample,  # Noise profile from silent segment
        stationary=True,       # SM7B noise floor is stationary (steady hiss)
        prop_decrease=1.0,     # Full noise reduction (no residual noise allowed)
        time_mask_smooth_ms=50,
        freq_mask_smooth_hz=200,
    )

    # Step 3: Verify post-processing noise floor
    frames = librosa.util.frame(reduced_audio,
                                 frame_length=int(0.025 * sr),
                                 hop_length=int(0.010 * sr))
    rms = np.sqrt(np.mean(frames ** 2, axis=0))
    silent = rms[rms < 0.001]
    if len(silent) > 0:
        noise_floor_dbfs = 20 * np.log10(np.mean(silent) + 1e-10)
        if noise_floor_dbfs > NOISE_THRESHOLD_DBFS:
            print(f"WARNING: post-processing noise floor {noise_floor_dbfs:.1f} dBFS"
                  f" above threshold {NOISE_THRESHOLD_DBFS:.1f} dBFS")
            print("Apply second pass or manual spectral editing required")
        else:
            print(f"Noise floor: {noise_floor_dbfs:.1f} dBFS (below threshold)")

    sf.write(output_wav_path, reduced_audio, sr)
    return output_wav_path

def prepare_reference_files_denoised(reference_dir, output_dir):
    """
    Noise-gate all reference recordings in directory.
    Returns list of paths to denoised reference files.
    """
    os.makedirs(output_dir, exist_ok=True)
    denoised_paths = []

    for filename in sorted(os.listdir(reference_dir)):
        if not filename.endswith('.wav'):
            continue
        input_path = os.path.join(reference_dir, filename)
        output_path = os.path.join(output_dir, filename)
        apply_spectral_noise_gate(input_path, output_path, noise_duration_s=0.5)
        denoised_paths.append(output_path)

    return denoised_paths

def convert_voice_fixed(text, base_speaker_id, reference_dir):
    """
    Convert base TTS voice to client voice with denoised reference audio.
    1. Noise-gate all reference recordings
    2. Extract speaker embedding from clean reference
    3. Mel Decoder learns target voice without noise floor artifact
    """
    # Step 1: Denoise reference recordings
    denoised_dir = reference_dir.rstrip('/') + '_denoised/'
    denoised_files = prepare_reference_files_denoised(reference_dir, denoised_dir)

    # Step 2: Extract speaker embedding from denoised reference
    target_se, audio_name = se_extractor.get_se(
        denoised_files,
        tone_color_converter,
        target_dir='processed_denoised',
        vad=True,
    )

    source_se = torch.load(f'checkpoints_v2/base_speakers/ses/{base_speaker_id}.pth')
    src_path = f'tmp/base_tts_{audio_name}.wav'
    # ... generate base TTS audio to src_path ...

    # Step 3: Tone color conversion with clean speaker embedding
    tone_color_converter.convert(
        audio_src_path=src_path,
        src_se=source_se,
        tgt_se=target_se,
        output_path=f'output/converted_denoised_{audio_name}.wav',
        message="@MyShell",
    )

# ---- Results after spectral noise gate on reference audio ----
#
# Reference audio noise floor (silent frames, RMS dBFS):
#   Before noise gate: -41.9 dBFS (mean across 3 reference recordings)
#   After noise gate:  -73.2 dBFS (mean across 3 recordings; -31.3 dB reduction)
#
# Synthesized output noise floor (silent frames, RMS dBFS):
#   Before fix (Mel Decoder trained on noisy reference):  -44.8 dBFS
#   After fix (Mel Decoder trained on denoised reference): -72.1 dBFS
#   Noise reduction in output:  -27.3 dB (inaudible; below perceptual threshold)
#
# MOS listening test (20 listeners, 5-point scale):
#   Before fix:  3.4 ± 0.4  (noise artifact reduces naturalness rating)
#   After fix:   4.1 ± 0.3  (noise removed; naturalness improved)
#
# Voice identity preservation (cosine similarity, speaker embeddings):
#   Clean reference vs noisy reference: 0.961
#   (High similarity: noise gate removes noise floor but preserves voice characteristics)
#
# Perceptibility test (20 listeners, detect noise artifact yes/no):
#   Before fix:  18/20 listeners heard artifact (90% detection rate)
#   After fix:    1/20 listeners heard artifact (5% detection rate)

print("Fix: noisereduce spectral gate on reference; output noise -44.8 → -72.1 dBFS")
print("MOS: 3.4 → 4.1; noise detection rate: 90% → 5%; speaker cosine similarity: 0.961")

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 →