Your laptop fan has been running through every take you recorded, and you don't want to open a GUI editor to fix it. A short Python script can handle that problem. It can't handle every noise problem, and knowing the difference will save you an afternoon of tweaking.
This guide builds a minimal script that loads a WAV file, takes a noise-only sample, reduces steady noise, and checks the result with numbers instead of guesswork. It also shows a failure case so you know when to stop.
Status of the code: every code block below is unexecuted. Nobody ran these scripts for this article, so no console output is recorded. The "expected output" sections describe what the scripts are written to print, not captured results.
What profile-based reduction can and cannot fix
The approach rests on one idea: if the noise stays the same over time, you can measure it once and subtract it everywhere. The dev.to guide on common noise sources draws the same line. Noise-reduction software can often lower constant sounds such as fans, air conditioning and microphone hiss. Audacity's workflow, for example, samples the unwanted noise to build a profile before it processes the recording.
Traffic is different. The guide notes that vehicles make unpredictable sounds that can appear suddenly and overlap speech. Keyboard clicks are short and irregular, and they can land between words. Background conversation shares frequencies with the speaker. Wind creates low-frequency bursts that can overpower speech. A single frozen noise profile can't describe any of these.
That gives you a rule for the rest of this article. If you can describe the noise as "the same sound, all the time," a script is a good fit. If you can't, skip to the decision table.
Step 1: Install the dependencies
The script uses three libraries: numpy for array math, soundfile for reading and writing WAV files, and noisereduce for the reduction itself.
python -m venv .venv
source .venv/bin/activate
pip install numpy soundfile noisereduce
pip freeze | grep -i -E "numpy|soundfile|noisereduce"
This article pins no versions, because none were confirmed against a current release. Run the last command after installing and copy the output into your own requirements file. Before you rely on the parameters below, check them with help(noisereduce.reduce_noise) in your installed version.
Step 2: Pick a noise-only sample
This choice matters more than any other setting. The sample must contain the background noise and nothing else: no breath, no lip smacks, no "okay, here we go."
Open your recording in any player and find a stretch where you were silent but the fan or AC was running. Two seconds is a reasonable target. Note the start and end times in seconds. If you left no silence, record ten seconds of room tone next to the microphone in the same room and use that file instead.
The sample also has to match the noise during speech. If the fan sped up halfway through the recording, a sample from the quiet start will under-correct the louder part.
Step 3: Run the reduction script
Save this as denoise.py. It is unexecuted.
import sys
import numpy as np
import soundfile as sf
import noisereduce as nr
INPUT = 'voice.wav'
OUTPUT = 'voice_clean.wav'
NOISE_START = 0.0 # seconds, noise-only region
NOISE_END = 2.0
STRENGTH = 0.8 # 0.0 to 1.0, start below 1.0
audio, sr = sf.read(INPUT, dtype='float32')
if audio.ndim > 1:
audio = audio.mean(axis=1) # mix down to mono for simplicity For more on this, see [more on google play organization vs personal account: which to pick](/google-play-organization-vs-personal-account-which-to-pick).
noise = audio[int(NOISE_START * sr):int(NOISE_END * sr)]
if len(noise) < sr * 0.5:
sys.exit('Noise sample is shorter than 0.5 seconds. Pick a longer region.')
clean = nr.reduce_noise(
y=audio,
sr=sr,
y_noise=noise,
stationary=True,
prop_decrease=STRENGTH,
)
sf.write(OUTPUT, clean, sr)
print(f'Sample rate: {sr} Hz')
print(f'Duration: {len(audio) / sr:.1f} s')
print(f'Peak before: {np.max(np.abs(audio)):.3f}, after: {np.max(np.abs(clean)):.3f}')
print(f'Wrote {OUTPUT}')
Expected console output
The script should print four lines: the sample rate, the duration, the peak level before and after, and the output filename. The peak after should land close to the peak before. If it jumps far above it, stop and listen before you trust the file.
Setting the strength
prop_decrease controls how much of the estimated noise the script removes. At 1.0 the reduction is at its most aggressive, and that is where an underwater, robotic voice tends to appear. Start at 0.8 and listen, then try 0.6 and 0.9. Choose the lowest value that still gets the noise down to a level you can live with. This is practical advice, not a documented threshold, so trust your ears over any default.
Step 4: Measure the noise floor before and after
Your ears can mislead you here, because a processed file may sound cleaner simply because it is quieter. Measure the same silent region in both files instead. Save this as check.py. It is also unexecuted.
import numpy as np
import soundfile as sf
BEFORE = 'voice.wav'
AFTER = 'voice_clean.wav'
SILENCE = (0.0, 2.0) # noise-only region, seconds
SPEECH = (5.0, 10.0) # a stretch where you are talking We cover related ground in [our guide to how to find good first issues for hacktoberfest 2026](/how-to-find-good-first-issues-for-hacktoberfest-2026).
def rms_db(path, start, end):
x, sr = sf.read(path, dtype='float32')
if x.ndim > 1:
x = x.mean(axis=1)
seg = x[int(start * sr):int(end * sr)]
rms = np.sqrt(np.mean(seg ** 2))
return 20 * np.log10(max(rms, 1e-10))
for label, region in (('silence', SILENCE), ('speech', SPEECH)):
b = rms_db(BEFORE, *region)
a = rms_db(AFTER, *region)
print(f'{label}: before {b:.1f} dBFS, after {a:.1f} dBFS, change {a - b:+.1f} dB')
How to read the result
The script prints two lines, one for silence and one for speech, each with before and after levels in dBFS. You want the silence level to drop clearly (a more negative number) while the speech level barely moves. If both drop by similar amounts, the tool is attenuating your voice along with the noise. Lower STRENGTH and run again.
The silent region you used for the noise profile will always look good, because the profile came from it. Treat it as a first check only. For a fairer test, also measure a different pause between sentences.
Step 5: Listen on headphones
Numbers catch level problems, not artifacts. Play the original and the processed file back to back on headphones, and work through this checklist.
- Words stay clear, especially soft consonants like s, f and t.
- The voice still sounds natural, with no watery or metallic edge.
- The background noise is reduced, not just turned into a different sound.
- Quiet details such as breaths and room ambience haven't vanished in a way that feels eerie.
The dev.to guide makes the same point: a cleanup should improve listening comfort without making the recording sound artificial. If item 2 fails, lower the strength before you change anything else.
The failure case: non-stationary noise
Now break it on purpose. Take a recording with passing cars, a barista talking behind you, or keyboard typing, and run the same script. Then try the non-stationary mode, which estimates the noise as it changes over time. This snippet is unexecuted, and it reuses audio and sr from denoise.py.
Also read: measure core web vitals real user data with web-vitals — background
clean = nr.reduce_noise(
y=audio,
sr=sr,
stationary=False,
prop_decrease=0.7,
)
What follows is an inference from how the method works, not a recorded result. A frozen profile can't know that a truck is noise, so loud bursts will likely pass through mostly intact. Background speech occupies the same frequencies as your voice, so reducing it risks damaging yours. Short clicks between words arrive and disappear faster than an estimate can adapt.
The verification script should show the damage. The silence region may improve slightly while the speech region drops too, and the headphone check will likely fail on item 2. When that happens, stop raising the strength. More reduction aimed at the wrong kind of noise only produces artifacts.
Decision table: prevent, script, or edit manually
The fixes below follow the advice in the dev.to guide. The script column reflects what Steps 1 through 4 can realistically do.
| Noise type | Best fix | Notes | | --- | --- | --- | | Computer fan | Script | Move the microphone away first if you can re-record. | | Air conditioner | Prevent, then script | Turn it off briefly or change rooms. Script the rumble if the take is final. | | Mic hiss | Script | Improve the input level at the source to avoid heavy gain later. | | Electrical hum or buzz | Prevent | Check cables, power connections and nearby equipment first. | | Traffic | Prevent | Record away from busy roads. Results on existing audio depend on overlap with speech. | | Wind | Prevent | Use microphone protection. Severe wind damage may not be fully repairable. | | Keyboard and mouse | Edit manually | Reposition the microphone, or cut individual clicks after recording. | | Room echo | Prevent | Get closer to the microphone and add soft furnishings. | | Background speech | Prevent | Record somewhere quieter. Complete separation is sometimes impossible. |
Hum and echo deserve a closer look. For hum, the guide recommends identifying the source over treating the final recording. Fix the cable or power problem and re-record. Echo is a property of the room, not an added noise, so a noise profile won't remove it.
What to do next
Once the single-file script works, wrap it in a loop over a folder of recordings, with one noise sample per session. Keep the check script in that loop and flag any file where the speech level drops along with the silence level. Adjust STRENGTH per room rather than reusing one value everywhere.



