Estimate the noise, then remove it.
On this page
Spectral subtraction removes steady background noise, such as a fan next to a phone call, from speech. It learns the noise power in each frequency bin during pauses, takes that power away from every frame of the noisy spectrogram, keeps each bin’s noisy phase, and turns the result back into sound. It acts once per STFT frame of the clip, and its two settings decide how much extra to subtract and how low a bin may fall.
The STFT (The STFT and Spectrograms) cuts audio into short frames and gives one complex value per frame and frequency bin. Call the speech S, the noise D (for disturbance) and the noisy value Y; noise that simply adds to the speech adds in every bin, Y = S + D, because the recording chain is linear. The fan is stationary (its statistics stay put) while the speech comes and goes.
Average |Y|² bin by bin over frames with no speech, such as the first fraction of a second or frames a voice activity detector marks quiet. That is Welch’s average from Power Spectral Density done per bin, and it gives the noise PSD PD for every bin.
Audio editors do the same (Anushiravani 2016). Adobe Audition asks you to select a stretch with noise only, captures it as a noise profile, and then lets you set how much to remove. With no clean pause, take a low percentile of each bin over the whole recording instead, the 10th to 20th, since speech lifts only some frames and the lowest ones are still noise. It fails when fewer than a fifth of the frames are noise only.
A toy recording, a tone that starts at 0.5 s in steady noise, and its noise PSD from the frames before the tone.
import numpy as np
import librosa
SR, N_FFT, HOP = 8000, 256, 128
TONE_HZ, ONSET_S, NOISE_RMS = 440, 0.5, 0.3
t = np.arange(2 * SR) / SR
voice = np.sin(2 * np.pi * TONE_HZ * t) * (t > ONSET_S)
rng = np.random.default_rng(0)
y = voice + NOISE_RMS * rng.standard_normal(t.size)
Y = librosa.stft(y, n_fft=N_FFT, hop_length=HOP) # (129, frames)
P_D = np.mean(np.abs(Y[:, 1:11]) ** 2, axis=1, keepdims=True)
Boll (1979) subtracted the average noise magnitude from each bin’s magnitude and set negative results to zero. Berouti et al. (1979) worked in power, subtracted a little more than the estimate (α) and stopped at a small floor (β). Lim and Oppenheim (1979) reviewed these rules beside the Wiener filter.
One bin of a speech frame, noise estimate PD = 1, α = 2 and β = 0.01. A noisy power of 5 keeps 5 − 2 = 3. A noisy power of 1.5 would fall to −0.5, so it is lifted to the floor, 0.01. Both keep their noisy angle (illustrative).
Figure 4. Frame at 0.34 s, α = 2, β = 0.01. 12Tones at 500 and 1,500 Hz.
Every bin is a length and an angle (Fourier Transform and the FFT); subtraction changes only the length. Wang and Lim (1982) reported that, at moderate signal-to-noise ratio (SNR, speech power over noise power), the noisy phase serves about as well as the clean one, and estimating the clean phase is much harder.
Every subtraction rule can be rewritten as a gain, one number per bin and frame. With α = 2 and β = 0.01, a bin holding 4 times the noise power keeps about 0.71 of its amplitude, and a bin at the noise level keeps only the floor, 0.1.
A gain per frequency is a filter’s magnitude response (How FIR and IIR Filters Shape Magnitude and Phase), only this one is redrawn every frame.
The noise estimate is an average, but each frame’s noise jumps around it by about its own size (the jitter of a periodogram, one frame’s power spectrum with no averaging, on Power Spectral Density). In five bins of one pause frame with noise estimate 1 and powers [0.6, 1.4, 2.5, 0.9, 3.1], α = 1 leaves 3 survivors, α = 2 leaves 2 and α = 3 leaves 1 (illustrative).
Scattered over time and frequency these survivors sound like faint warbling tones, known as musical noise. Post-filters, run after the subtraction, are a further fix.
Figure 7. 1Noisy. 2Cleaned. Larger α thins tones; a floor hides specks left.
| Step | How often, on what data | State it keeps | Cost |
|---|---|---|---|
| Noise estimate | once from leading silence, or again in each pause | the noise PSD, one value per bin | one mean per bin |
| Subtraction | once per frame and bin, on the noisy STFT frame | only the noise PSD | a subtract, a max and a square root per bin, plus the STFT |
Rates, state and cost come from the sources in References.
The subtraction rule on the same toy recording, with α = 2 and β = 0.01.
import numpy as np
import librosa
SR, N_FFT, HOP = 8000, 256, 128
t = np.arange(2 * SR) / SR
voice = np.sin(2 * np.pi * 440 * t) * (t > 0.5) # 440 Hz tone from 0.5 s
y = voice + 0.3 * np.random.default_rng(0).standard_normal(t.size)
Y = librosa.stft(y, n_fft=N_FFT, hop_length=HOP) # (129, frames)
P_D = np.mean(np.abs(Y[:, 1:11]) ** 2, axis=1, keepdims=True)
ALPHA, BETA = 2.0, 0.01
power = np.maximum(np.abs(Y) ** 2 - ALPHA * P_D, BETA * P_D)
S_hat = np.sqrt(power) * np.exp(1j * np.angle(Y))
s_hat = librosa.istft(S_hat, hop_length=HOP, length=len(y))
| Setting | Default | Source |
|---|---|---|
| Frames | 256 samples, hop 128, at 8 kHz | Boll (1979) |
| Knob, where it enters | Raise it | Lower it | Default |
|---|---|---|---|
| Over-subtraction α, the multiplier on PD | Less musical noise, more speech removed | α = 1 is plain subtraction; below 1 noise is left in | About 4.75 at −5 dB SNR falling to 1 at 20 dB, per frame (Berouti et al. 1979) |
| Floor β, the lowest power kept, β PD | Hides musical noise under a steady hiss | Deeper gaps, more musical noise; β = 0 is no floor | 0.005 to 0.1 (Berouti et al. 1979) |
| Noise averaging, the frames averaged into PD | Steadier, but needs longer silence and tracks change slowly; past the pause it folds in speech | Jumpier, more musical noise; one frame is a single periodogram | The pause before speech, the first 0.5 s in the toy code of Section 2 |
For noise that changes over time, such as traffic, learned noise templates do better (Non-negative Matrix Factorization, section 7.1).
| What you see | Likely cause | What to do |
|---|---|---|
| A door slam or passing car stays in | Noise that changes is not in the estimate | Update the estimate in every pause, or track it |
| Speech is removed | Speech leaked into the frames used as noise | Check the quiet frames, use a voice activity detector |
| Clicks or level jumps at frame edges | Analysis and synthesis STFT settings differ | Same FFT size, hop and window both ways; pass length to istft |
| Good SNR, bad sound | SNR does not measure musical noise | Judge by spectrograms and listening too |
| Some bands stay noisy while others lose speech | The noise is colored differently across bands | Compute the SNR, the noise profile and the subtraction per band |
Every rule on this page multiplies each bin by a gain. The Wiener Filter finds the best one.