W/03Writing

The S that was never there

·~9 min read
levellingsynthesisblind test

Can you hear it?

Every S in these takes got replaced. Not ducked, not repaired. Replaced. Four speakers, and you don't get told which take is which. Have a listen before you read on.

blind test · case 1 of 4 · Speaker 1score 0/0

Why replace at all

A de-esser ducks. It spots an S running too hot and pulls it down until it fits. The level drops and that's it. Whatever was wrong with that S is still in there, just quieter.

Levelling does something else. Every sibilant gets moved into a corridor with a floor and a ceiling. Loud ones come down, quiet ones come up. You end up with sibilants sitting at roughly one height across the whole file, instead of just a lid on the loud ones.

The corridor framing came out of a client project. My part was building it and finding out whether it holds.

The pulling up is the awkward half. If an S is quiet because it got damaged, turning it up turns up the damage with it. Noise, distortion, a smeared top end, all of it comes along. At some point gain stops helping. That's where you start building part of the S instead of amplifying it.

levelbeforeafterevery S at its own levelupper boundlower boundall inside the corridororiginalsynthetic
fig. 01Every sibilant gets pulled into a corridor instead of only capped from above. On the right, all five sit inside the same band. The small bar at the bottom is the one knob that decides how much of the correction comes from synthesis instead of the original.

What replace means

Replacing isn't a swap or a cut. It's a blend with a dial on it, the replacement rate, r, from 0 to 1. At r = 0 you get the original event untouched. At r = 1 it's all new material. Anything between is a mix of the two.

You can't just crossfade linearly between them. The original and the replacement aren't correlated, so they add up in power rather than amplitude, and a linear fade loses energy right in the middle. The level dips exactly where you'd notice it, around half replacement.

So the blend runs at constant power instead. D is the level the event should land on inside the corridor.

original level = D · (1 − r)
synthetic level = D · √(1 − (1 − r)²)

Both ends fall out of that on their own: original only at r = 0, replacement only at r = 1, and no dip in between.

levelr = 0r = 1replacement ratethe dip youwould hearoriginal onlyreplacement onlylinear crossfadeconstant power
fig. 02A linear crossfade between two uncorrelated signals loses energy in the middle of the range. The dip lands around half replacement, which is exactly where most of the work happens. Constant power keeps the total flat from one end to the other.

Useful side effect. Since r is just a number, you can render the same file at r = 0 and at r = 1 and put the two next to each other. That's the pair sitting at the top of this page.

Level accuracy inside the corridor came out at -0.12 dB average deviation, worst case -0.63 dB.

Borrowed texture

So what is the new material. Mostly not noise. The first move isn't to synthesize anything, it's to go looking for something to borrow.

The stage searches the rest of the recording for healthy sibilants from the same speaker, takes one, stretches it to the length it needs through STFT magnitude interpolation and splices it in with its own phase. What lands in the gap is real texture from that voice, that room, that mic.

The donor gets picked by health, not by frequency. Going for the spectrally closest event feels right and isn't: events sitting near a damaged one have usually taken the same hit. You want one that's intact, and then you shape it.

If there's nothing in the file worth borrowing, the fallback builds shaped noise instead, with a spectral envelope from an empirical voice class picked by fundamental frequency, and frame to frame texture out of a library built up front. Calibration is a separate problem and gets the next chapter.

There's an order to it because the three aren't equally good. Material from the same recording brings the speaker and the room along for free. Synthesis has to guess at both. So it borrows while there's anything left to borrow. The tier logic came out of the restoration work, there's more on it in the piece on restoration.

single recordinghealthyhealthydamagedborrowed, stretched to lengthfallback pathno donorin the fileshaped noise,voice-class envelopechosen by health, not by frequency
fig. 03The replacement comes out of the same recording. A healthy event gets stretched to length and spliced into the damaged one, which is why it carries the right voice and the right room for free. Shaped noise is the fallback, for files with nothing worth borrowing.
The first version of this was much dumber. Random-phase resynthesis of the event's own spectral envelope, no donor, no voice classes. It went into a blind test against the original and nobody picked it out, me included. Got replaced anyway. On badly damaged material there's no healthy envelope left to resynthesize from, and borrowed texture holds up where invented texture doesn't.

Why absolute thresholds fail

A corridor needs a floor and a ceiling, and the obvious move is to pick two levels. That falls over on the second speaker you try.

Sibilant level is a property of the voice. Across the four speakers in the test set the medians sat almost 20 dB apart. A corridor tuned for one of them isn't slightly off for the next, it's in the wrong place entirely, either clamping everything or touching nothing.

So the corridor comes out of the material instead, from the median sibilant level of the file. Which works, and brings its own problem along.

If the file has already been de-essed, that median is damaged too. Build the corridor from it and you've written the damage into everything downstream, very faithfully. Calibrating against your own broken input doesn't get you anywhere.

The way out is that de-essing doesn't touch everything. There's a band below the sibilant range that survives whatever was done to the file, and from that you can estimate where this speaker's sibilants should have been sitting. The measured median gets blended toward that estimate. The file still picks its own corridor, it just doesn't get to diagnose itself.

levelABCDone fixed corridoralmost 20 dB apartthe rescue pathmeasured median,already damagedestimate fromthe untouched bandblended toward it
fig. 04Four speakers, four very different places for their sibilants to sit. One fixed corridor fits exactly one of them. Below: a file that was already de-essed reads too low, so the measured median gets pulled toward an estimate taken from the band de-essing never touched.

How it was tested

One listener. Me. Four speakers, spread over a few weeks. Assignment randomized per file, key in a hidden folder, opened after the answers were written down.

That's not a panel and it isn't a controlled study, and I'm not going to dress it up as one. It's also the one listener who knows exactly what was done to the file and what artifact to go hunting for. I'd take that over ten people who were never told what to listen for.

When borrowed texture replaced the old synthesis I didn't test against the original again. Old against new, three speakers, six files each. Passing meant answering "no difference", which is an odd thing to aim at.

A listening test on its own only proves you didn't hear anything, not that anything happened. So the objective check ran alongside it: replacement was measurably active across every event span, median delta depth +3 dB against the sibilant component. The files aren't indistinguishable because nothing was done to them.

Then 1562 real files went through, no crashes, 82.9 percent clean. The rest got sorted into buckets by failure shape and worked through by ear, which is slower than a metric and catches what the metric wasn't written to look for.

Where it breaks

Not every detected event gets processed. Health checks sit in front, and anything that fails gets passed through. Passed through literally: for vetoed events the measured maximum deviation between input and output is zero. Not small. Zero.

A voiced sibilant isn't the same problem as an unvoiced one. A /z/ has pitch underneath it, and treating it like an /s/ takes that with it, so voiced events get spotted through fundamental frequency and a reduced replacement rate.

The most honest result came from a file with nothing but voiced material in it. Detector produced zero events. The stage got bypassed, signal came out unchanged. Correct behaviour, and a fairly clear note about where the detector would need recalibrating if that material ever mattered.

One known problem is still open. Voice class detection through fundamental frequency misfires on heavily damaged and on sung material. The damage is contained: it hits the envelope shape in the fallback and a calibration offset, and it doesn't reach detection or the health checks. No audible follow-on problems across the large run or the edge cases. Understood, and not fixed.

The edge cases that do come out clean: whispering, high singing, shouting, silence and noise floor, telephone bandwidth.

Wrap-up

The shift that matters here is a small one. An S isn't a spot in the file you have to turn down. It's material, and material can be replaced, as long as you know when that's safe and what to build it out of.

Levelling is what forces the question, because the moment you start lifting quiet events you start lifting whatever's wrong with them. Borrowing from the same recording is what got it through a blind test.

Anyone generating or restoring speech at scale runs into the same thing, without an engineer sitting next to it deciding case by case.

Written by Robin Busse. Part three of a series that started with sibilant detection and continued with sibilance restoration. Based on a client project. Implementation details and tuning values are left out on purpose.