"""Light noise reduction via ffmpeg's afftdn filter. Use cases: - Recordings with hiss (cheap mic preamp, RF interference) - Recordings with AC hum (50/60Hz line noise — there's a separate `hum` filter for that, not implemented here) - Tapes digitized with analog tape hiss The `afftdn` filter is ffmpeg's built-in adaptive FFT denoiser. It's lightweight (CPU-friendly) and good for gentle hiss removal without artifacting speech. For heavy noise, consider a real denoiser like RNNoise or DeepFilterNet — out of scope for sermon-clean. """ from __future__ import annotations import subprocess from dataclasses import dataclass from pathlib import Path @dataclass class DenoiseResult: output_path: Path noise_floor_db: float noise_reduction_db: float def to_dict(self) -> dict: return { "output_path": str(self.output_path), "noise_floor_db": self.noise_floor_db, "noise_reduction_db": self.noise_reduction_db, } def denoise( src: Path, out: Path, *, noise_reduction_db: float = 12.0, noise_floor_db: float = -50.0, ) -> DenoiseResult: """Apply FFT-based noise reduction. Args: src: input audio file out: output audio file noise_reduction_db: how much to attenuate the noise component (dB). Higher = more aggressive. 12dB is a reasonable default; 20dB starts to artifact speech. noise_floor_db: expected level of the noise floor below which everything is considered noise. -50dB is conservative. If your recording has louder noise (e.g. AC hum at -35dB), set this higher. Returns: DenoiseResult with the measured parameters. """ src = Path(src) out = Path(out) proc = subprocess.run([ "ffmpeg", "-y", "-v", "error", "-i", str(src), "-af", f"afftdn=nr={noise_reduction_db}:nf={noise_floor_db}", "-ar", "48000", # output 48kHz to match normalize convention str(out), ], capture_output=True, text=True) if proc.returncode != 0: raise RuntimeError(f"denoise failed: {proc.stderr}") return DenoiseResult( output_path=out, noise_floor_db=noise_floor_db, noise_reduction_db=noise_reduction_db, )