Outcome caveat (added after the human audition)
Every objective metric below passed — floor down 16+ dB, sample-exact alignment, zero ASR word degradation, on-target loudness — and the owner still rejected the result by ear and abandoned the content. The gap: DeepFilterNet is a denoiser, not a dereverberator. Room reverb is what makes a built-in-mic take read as 'the wrong mic', and it survives the chain largely intact. Presence EQ narrows the gap; it does not close it. Escalation to Resemble Enhance (a real dereverb) failed on Apple Silicon: its enhancer hits aten::_weight_norm_interface, unimplemented on MPS (torch 2.x; PYTORCH_ENABLE_MPS_FALLBACK=1 or --device cpu are the untested workarounds; model download also requires git-lfs installed or it dies with 'Failed to pull latest changes'). Calibrate expectations: this chain rescues NOISE problems and salvages B-priority content; for a roomy take where the room itself is the complaint, budget for reshoot.
Situation
A lav (DJI Mic 3 TX) silently failed to feed the iPhone, so the take was captured on the phone's own mics: roomy, noisy, and true stereo. Detection is cheap and definitive without listening: a single-TX lav records DUAL-MONO stereo (L-R ~ -inf), so measure the side channel:
ffmpeg -hide_banner -ss 5 -t 10 -i clip.MOV -vn -af "pan=mono|c0=0.5*c0-0.5*c1,volumedetect" -f null -L-R max under ~-60 dB = lav (dual-mono); L-R max above ~-30 dB = built-in mics. Note -v error suppresses volumedetect entirely (it logs at info level). Check the whole shoot batch: one failed clip usually means the mic was off for the session.
Repair chain (order matters: denoise BEFORE gain)
- Mono downmix to PCM (
pan=mono|c0=0.5*c0+0.5*c1, 48 kHz) — a lav is mono anyway and this kills stereo room decorrelation. - DeepFilterNet Rust CLI (prebuilt binaries incl. aarch64-apple-darwin on the Rikorose/DeepFilterNet releases page, no Python/torch needed):
deep-filter -D -o outdir in.wav.-D(compensate delay) is mandatory when downstream cue timing matters — without it the output is late by the STFT+lookahead delay. With-Dthe output is sample-aligned at t=0 and ~30 ms SHORTER at the tail (the compensation trims the head), soapadback to the container duration before muxing. - Fake the lav's presence:
highpass=f=80, bass=g=2.5:f=200 (shelf), equalizer=f=4500:q=1:g=+3, deesser, acompressor(note:acompressormakeupis a linear multiplier with min 1 —makeup=0errors; omit it for no makeup). - Master last: measure integrated LUFS, linear
volumegain to -14 LUFS,alimiter=limit=0.84:level=false(-1.5 dBTP).
Measured result: quiet-window floor -60.5 -> -76.2 dBFS pre-gain; at final -14 LUFS loudness the floor sat ~-57 dBFS vs ~-40 for the naive gain-first master that prompted the complaint.
Verifying "did the denoiser eat consonants" without listening
Over-denoised speech (gated consonants, watery artifacts) is the classic failure. Two objective checks that catch it:
- Cross-correlate a 2 s speech window of output vs input to prove sample alignment (we measured 0 samples with
-D). - ASR-diff: transcribe input and output with the same local model and diff word sequences. Identical word lists (120/120, only a/the and digit-formatting variance) = no gated/damaged words. Caveat: ASR word TIMESTAMPS jitter up to ~1 s in localized clusters when the silence profile changes — timestamp drift is NOT evidence of audio shift; trust the cross-correlation for timing and the word sequence for intelligibility.
Second caveat: these checks bound intelligibility and timing, NOT subjective quality. They cannot hear reverb, wateriness, or 'wrong mic' character — a human audition is still the accept/reject gate.
Why filtering beats re-rendering
Filtering preserves the timeline exactly, so overlay/card cues authored against the old audio stay valid; remux the repaired track onto the existing video render with -map 0:v -map 1:a -c copy instead of re-rendering the composition.