
Vocals · 6 October 2026 · 7 min
Harsh S’s get louder after mastering. De-ess before the limiter
Mastering lifts the whole track and usually adds top end, so an S that was borderline in the mix comes out sharper on the master, and those bright bursts eat true-peak headroom. Find the worst S’s on the vocal, tame them in the mix, then master.
Here’s how it usually goes. The mix sounds smooth. You master it, or send it out, and suddenly every “this,” “so,” and “she’s” cuts like a paper edge, especially on earbuds. Nothing in the vocal chain changed. The master just made what was already there louder and brighter.
Three things stack up. The master raises the overall level, so the sibilants come up with everything else. Most mastering chains add some top end, a shelf, a little air, maybe a saturator, and that’s exactly where S’s live. And lossy encoders have a hard time with dense, noisy high frequencies, so an S that’s only a bit hot on the WAV can turn splashy on a stream.
Find the S’s before your ears get used to them
Sibilance mostly lives between about 4.5 and 10 kHz. Lower voices tend to hiss around 5–7 kHz, and higher voices can reach toward 8–10. The job is to find the few moments that are actually a problem, not to de-ess the whole take into a lisp. This script scans a vocal stem in 10 ms steps, flags bursts where the 4.5–10 kHz band jumps over the body of the voice, and lists the worst ones with timestamps so you can go straight to them.
#!/usr/bin/env python3
"""essscan.py — find the S's, T's and SH's that will jump out once the track is mastered.
Usage: python3 essscan.py vocals.wav (a vocal stem works best; a full mix works too)
Needs: numpy + ffmpeg on PATH."""
import subprocess, sys
import numpy as np
SR, N, HOP = 48000, 1024, 480 # ~21 ms window, 10 ms hop
raw = subprocess.run(["ffmpeg", "-v", "error", "-i", sys.argv[1], "-ac", "1", "-ar", str(SR),
"-f", "f32le", "-"], capture_output=True, check=True).stdout
x = np.frombuffer(raw, dtype=np.float32)
win = np.hanning(N)
frames = np.lib.stride_tricks.sliding_window_view(x, N)[::HOP] * win
spec = np.abs(np.fft.rfft(frames, axis=1)) ** 2
f = np.fft.rfftfreq(N, 1 / SR)
ess = spec[:, (f >= 4500) & (f <= 10000)].sum(axis=1) # where sibilance lives
body = spec[:, (f >= 150) & (f < 4500)].sum(axis=1) # where the voice lives
ess_db, body_db = 10 * np.log10(ess + 1e-12), 10 * np.log10(body + 1e-12)
voiced = body_db > body_db.max() - 40 # ignore silence and room tone
ref = np.median(body_db[voiced]) # the vocal's typical body level
# A sibilant frame: the 4.5-10 kHz band is louder than the body, and loud relative to the take.
hot = (ess_db > body_db) & (ess_db > ref - 6)
events, start = [], None
for i, h in enumerate(np.append(hot, False)):
if h and start is None:
start = i
elif not h and start is not None:
if i - start >= 3: # at least ~30 ms long
events.append((start, i, ess_db[start:i].max() - ref))
start = None
mins = len(x) / SR / 60
print(f"sibilant events : {len(events)} ({len(events) / mins:.0f} per minute)")
if events:
over = np.array([e[2] for e in events])
print(f"how hot : median {np.median(over):+.1f} dB, worst {over.max():+.1f} dB "
f"vs the vocal's typical body level")
print("worst five :")
for s, e, o in sorted(events, key=lambda ev: -ev[2])[:5]:
t = s * HOP / SR
print(f" {int(t // 60)}:{t % 60:05.2f} {o:+.1f} dB ({(e - s) * 10} ms)")
Run python3 essscan.py vocals.wav. On a synthetic take, a sung tone with 120 ms bursts of 5–9 kHz noise dropped in like S’s at three levels, it flags the loud and medium bursts, with the worst at +7.8 dB over the voice’s body, and leaves the quiet ones alone. After ffmpeg’s built-in de-esser at moderate strength, the worst drops to +2.0 dB. Watch the worst and median numbers rather than the count. Once the S’s sit near the threshold, the count can wobble up even while the harshness drops.
# moderate de-ess on the vocal stem, 24-bit out, original untouched
ffmpeg -i vocals.wav -af deesser=i=0.35:m=0.5:f=0.5 -c:a pcm_s24le vocals_deessed.wav
# check integrated loudness and true peak on the finished bounce
ffmpeg -hide_banner -nostats -i master.wav -af ebur128=peak=true -f null -
It shows up on the meter too
S’s aren’t only an ear problem. Bright, fast bursts tend to overshoot between samples more than slow, low material does, and a sample-peak limiter can’t see those overshoots. On the same synthetic take, pushed 10 dB into ffmpeg’s limiter at a −1 dBFS ceiling, the version without de-essing measured −0.1 dBTP and the de-essed one −0.4 dBTP at the same integrated loudness. Neither passes a −1.0 dBTP spec without a true-peak limiter, but the S’s spent an extra 0.3 dB of headroom on nothing. You’d rather spend that on the drums.
Tame them, cheapest fix first
| Fix | Use it when | Watch for |
|---|---|---|
| Clip gain on the worst words | Only a handful of S’s are hot | Turn down the S itself 3–6 dB, not the whole word. Fade the edits. |
| De-esser on the vocal | Sibilance shows up all through the take | Set it to grab only the worst. If the singer starts to lisp, back off. |
| Dynamic EQ at the exact spot | One voice with one sharp frequency, say around 6 kHz | Keep it narrow and only acting when it crosses. A static cut dulls the whole vocal. |
| Soften the doubles | Stacks and ad-libs pile S’s on the same moment | De-ess the doubles harder, or cut their S’s so only the lead carries the consonant. |
Doubles multiply S’s. In R&B and pop stacks, three voices saying “so” a few milliseconds apart turn one S into a smear. Keeping the consonants on the lead and softening them on the doubles is an old vocal-production move, and it sounds cleaner than any de-esser on the vocal bus.
Hats live in the same neighborhood. In trap and drill, the hats and the S’s fight over the top octave. If the vocal only feels harsh in the hook where the hat roll comes in, pull the hats back a touch there before you crush the vocal.
Mind the order. De-ess after any EQ that brightens the voice, or the boost undoes the work. If you saturate the vocal, de-ess before the saturation, because distortion makes S’s denser.
Judge at normal speed. A slowed, pitched-down version like a screw edit moves the S’s lower, so they hide. Do your de-essing on the original, then make the ScrewAI version from the fixed bounce.
If all you have is the bounce
Sometimes the session’s gone. You can de-ess a full mix gently, but it grabs hats and cymbals too. A better first move is to split the bounce with StemSplit, run the scan on the vocal stem to see where it’s harsh, and take those timestamps back to whoever has the session. Stems diagnose, the session fixes. Once the fix is bounced, run the new master through CoProducer and check the true-peak row before you upload.
- Why do my vocals sound harsh after mastering?
- Mastering raises the level and usually the top end, so sibilance that was borderline in the mix becomes obvious. De-ess the vocal in the mix, then master again.
- Should I put a de-esser on the master?
- Only as a last resort when there’s no session. It catches hats and cymbals too. De-essing the vocal in the mix is cleaner.