Add a Mastering tab and auto-apply toggle for generated voice output

Pedalboard-based chain (warmth EQ, presence dip, compression, light
reverb, hard-ceiling limiter) fixes the spectral imbalance behind
generated TTS sounding tinny - see mastering/master.py docstring and
commit history for the before/after spectral analysis and the A/B tuning
pass against real character clips. Pipeline Studios Generate step can
auto-apply it; the standalone tab works on any clip.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
steve-adminandClaude Sonnet 5 committed 2026-10-16 11:08:53 +00:00
1 parent b4fef19993
commit 57ae9deb30
2 files changed
+178 -4

No files matched your search

+82
View File
@@ -0,0 +1,82 @@
"""
Mastering chain for generated TTS/voice clips: fixes the "tinny" spectral
imbalance common to neural vocoder output (excess 3-6kHz presence energy,
starved low end relative to a naturally-recorded voice - see the project's
spectral comparison: raw recordings skew toward 200-1000Hz, generated clips
dump ~35% of their energy into 3-6kHz) and adds the density/space that
reads as "produced" rather than dry/synthetic.
Chain order matters: EQ before compression (so the compressor reacts to
the corrected tonal balance, not the raw tinny one), reverb after
compression (so the room tail doesn't get squashed), limiter last.
"""
import argparse
import numpy as np
import soundfile as sf
from pedalboard import (
Compressor,
HighpassFilter,
HighShelfFilter,
Limiter,
LowShelfFilter,
PeakFilter,
Pedalboard,
Reverb,
)
def build_chain(
warmth_db: float,
presence_cut_db: float,
air_db: float,
comp_ratio: float,
reverb_wet: float,
) -> Pedalboard:
return Pedalboard([
HighpassFilter(cutoff_frequency_hz=60),
LowShelfFilter(cutoff_frequency_hz=150, gain_db=warmth_db, q=0.7),
PeakFilter(cutoff_frequency_hz=3800, gain_db=presence_cut_db, q=1.0),
HighShelfFilter(cutoff_frequency_hz=6500, gain_db=air_db, q=0.7),
Compressor(threshold_db=-20, ratio=comp_ratio, attack_ms=12, release_ms=120),
Reverb(room_size=0.08, damping=0.85, wet_level=reverb_wet, dry_level=1.0 - reverb_wet),
Limiter(threshold_db=-1.0, release_ms=100),
])
def main() -> None:
parser = argparse.ArgumentParser(description="Apply a studio mastering chain to a voice clip")
parser.add_argument("--input", required=True)
parser.add_argument("--output", required=True)
parser.add_argument("--warmth-db", type=float, default=5.5, help="Low-shelf boost @150Hz")
parser.add_argument("--presence-cut-db", type=float, default=-5.0, help="Peak cut @3.8kHz (the tinny band)")
parser.add_argument("--air-db", type=float, default=-4.5, help="High-shelf @6.5kHz, for softness")
parser.add_argument("--comp-ratio", type=float, default=2.5)
parser.add_argument("--reverb-wet", type=float, default=0.025)
args = parser.parse_args()
audio, sr = sf.read(args.input, always_2d=True)
audio = audio.T.astype(np.float32) # pedalboard wants (channels, samples)
board = build_chain(
args.warmth_db, args.presence_cut_db, args.air_db, args.comp_ratio, args.reverb_wet
)
processed = board(audio, sr)
# pedalboard's Limiter is attack/release-based, not a true brick-wall
# limiter - fast transients can still punch through its threshold (seen
# in practice: in-memory peaks hit 0dBFS despite threshold_db=-1.0,
# producing audible clipping distortion on playback). Hard safety
# ceiling here guarantees the written file never clips regardless.
target_peak = 10 ** (-1.0 / 20)
peak = np.max(np.abs(processed))
if peak > target_peak:
processed = processed * (target_peak / peak)
sf.write(args.output, processed.T, sr)
print(f"[INFO] Saved {args.output}")
if __name__ == "__main__":
main()