testingfaces commited on
Commit
b2f9f68
·
verified ·
1 Parent(s): 9ea4293

Update denoiser.py

Browse files
Files changed (1) hide show
  1. denoiser.py +34 -112
denoiser.py CHANGED
@@ -1,159 +1,81 @@
1
  """
2
- Department 1 — Denoiser
3
- Uses DeepFilterNet3 for professional noise removal.
4
- Processing order:
5
- convert format → read → stereo→mono → resample →
6
- gentle pre-boost (max 3×) → AI denoise → EBU R128 normalize → save
7
  """
8
 
9
  import os
10
  import time
11
  import subprocess
12
- import tempfile
13
  import numpy as np
14
  import soundfile as sf
15
  import logging
16
 
17
  logger = logging.getLogger(__name__)
18
 
19
- TARGET_SR = 48_000 # DeepFilterNet3 native sample rate
20
- TARGET_LOUDNESS = -23.0 # EBU R128 target LUFS
21
- PRE_BOOST_MAX = 3.0 # max linear gain before denoise
22
 
23
 
24
  class Denoiser:
25
  def __init__(self):
26
- print("[Denoiser] Initialising DeepFilterNet3…")
27
- try:
28
- from df.enhance import enhance, init_df, load_audio, save_audio
29
- self._enhance = enhance
30
- self._init_df = init_df
31
- self._load_audio = load_audio
32
- self._save_audio = save_audio
33
-
34
- # Load model once
35
- self.model, self.df_state, _ = init_df()
36
- print("[Denoiser] ✅ DeepFilterNet3 loaded")
37
- except Exception as e:
38
- logger.warning(f"[Denoiser] DeepFilterNet3 not available: {e}")
39
- self.model = None
40
 
41
- # ── Public ──────────────────────────────────────────────────────
42
  def process(self, audio_path: str, out_dir: str) -> str:
43
- """
44
- Full pipeline: convert → load → mono → resample →
45
- pre-boost → denoise → normalise → save.
46
- Returns path to denoised WAV.
47
- """
48
  t0 = time.time()
49
 
50
- # Step 1: Convert any format → WAV via ffmpeg
51
  wav_path = os.path.join(out_dir, "input.wav")
52
  self._convert_to_wav(audio_path, wav_path)
53
 
54
- # Step 2: Read audio
55
- audio, sr = sf.read(wav_path, always_2d=True) # shape (samples, channels)
56
 
57
- # Step 3: Stereo → mono
58
- if audio.ndim > 1 and audio.shape[1] > 1:
59
  audio = audio.mean(axis=1)
60
  else:
61
  audio = audio.squeeze()
62
 
63
- # Step 4: Resample to 48 kHz
64
- if sr != TARGET_SR:
65
- audio = self._resample(audio, sr, TARGET_SR)
66
- sr = TARGET_SR
67
-
68
- # Step 5: Gentle pre-boost (max 3×)
69
- peak = np.abs(audio).max()
70
- if peak > 0 and peak < 1.0:
71
- boost = min(PRE_BOOST_MAX, 1.0 / peak)
72
- audio = audio * boost
73
 
74
- # Clip to [-1, 1] after boost
75
- audio = np.clip(audio, -1.0, 1.0).astype(np.float32)
76
-
77
- # Step 6: AI Denoise
78
- if self.model is not None:
79
- try:
80
- # DeepFilterNet3 expects (1, samples) tensor
81
- import torch
82
- tensor = torch.from_numpy(audio).unsqueeze(0)
83
- enhanced = self._enhance(self.model, self.df_state, tensor)
84
- audio = enhanced.squeeze(0).numpy()
85
- except Exception as e:
86
- logger.warning(f"[Denoiser] DeepFilterNet3 enhance failed, using raw: {e}")
87
 
88
- # Step 7: EBU R128 loudness normalisation
89
- audio = self._normalise_loudness(audio, sr)
90
 
91
- # Step 8: Save
92
  out_path = os.path.join(out_dir, "denoised.wav")
93
  sf.write(out_path, audio, sr, subtype="PCM_16")
94
 
95
- logger.info(f"[Denoiser] Done in {time.time()-t0:.2f}s → {out_path}")
96
  return out_path
97
 
98
- # ── Private helpers ──────────────────────────────────────────────
99
  def _convert_to_wav(self, src: str, dst: str):
100
- """Convert any audio format to 16-bit PCM WAV using ffmpeg."""
101
- cmd = [
102
- "ffmpeg", "-y", "-i", src,
103
- "-acodec", "pcm_s16le",
104
- "-ar", str(TARGET_SR),
105
- "-ac", "1",
106
- dst
107
- ]
108
  result = subprocess.run(cmd, capture_output=True, text=True)
109
  if result.returncode != 0:
110
- # ffmpeg failed — try soundfile direct read as fallback
111
- logger.warning(f"[Denoiser] ffmpeg conversion failed, trying soundfile direct read")
112
  try:
113
- data, sr_in = sf.read(src, always_2d=True)
114
- sf.write(dst, data, sr_in, subtype="PCM_16")
115
- except Exception as e2:
116
- raise RuntimeError(
117
- f"Could not read audio file '{os.path.basename(src)}'. "
118
- f"ffmpeg error: {result.stderr[:200]}"
119
- ) from e2
120
-
121
- def _resample(self, audio: np.ndarray, src_sr: int, tgt_sr: int) -> np.ndarray:
122
- try:
123
- import resampy
124
- return resampy.resample(audio, src_sr, tgt_sr)
125
- except ImportError:
126
- pass
127
- try:
128
- import librosa
129
- return librosa.resample(audio, orig_sr=src_sr, target_sr=tgt_sr)
130
- except ImportError:
131
- pass
132
- # Simple linear interpolation fallback
133
- ratio = tgt_sr / src_sr
134
- n_samples = int(len(audio) * ratio)
135
- indices = np.linspace(0, len(audio) - 1, n_samples)
136
- return np.interp(indices, np.arange(len(audio)), audio).astype(np.float32)
137
-
138
- def _normalise_loudness(self, audio: np.ndarray, sr: int) -> np.ndarray:
139
- """
140
- EBU R128 normalisation.
141
- Targets TARGET_LOUDNESS LUFS; falls back to RMS normalisation if
142
- pyloudnorm is unavailable.
143
- """
144
  try:
145
  import pyloudnorm as pyln
146
  meter = pyln.Meter(sr)
147
  loudness = meter.integrated_loudness(audio)
148
  if np.isfinite(loudness) and loudness < 0:
149
  audio = pyln.normalize.loudness(audio, loudness, TARGET_LOUDNESS)
150
- return np.clip(audio, -1.0, 1.0).astype(np.float32)
151
  except Exception:
152
- pass
153
-
154
- # RMS fallback
155
- rms = np.sqrt(np.mean(audio ** 2))
156
- if rms > 1e-9:
157
- target_rms = 10 ** (TARGET_LOUDNESS / 20.0)
158
- audio = audio * (target_rms / rms)
159
- return np.clip(audio, -1.0, 1.0).astype(np.float32)
 
1
  """
2
+ Department 1 - Denoiser
3
+ Uses noisereduce for noise removal (no Rust/C++ required).
 
 
 
4
  """
5
 
6
  import os
7
  import time
8
  import subprocess
 
9
  import numpy as np
10
  import soundfile as sf
11
  import logging
12
 
13
  logger = logging.getLogger(__name__)
14
 
15
+ TARGET_SR = 16000
16
+ TARGET_LOUDNESS = -23.0
 
17
 
18
 
19
  class Denoiser:
20
  def __init__(self):
21
+ print("[Denoiser] Ready (noisereduce)")
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
 
23
  def process(self, audio_path: str, out_dir: str) -> str:
 
 
 
 
 
24
  t0 = time.time()
25
 
26
+ # Step 1: Convert to WAV
27
  wav_path = os.path.join(out_dir, "input.wav")
28
  self._convert_to_wav(audio_path, wav_path)
29
 
30
+ # Step 2: Read
31
+ audio, sr = sf.read(wav_path, always_2d=True)
32
 
33
+ # Step 3: Stereo to mono
34
+ if audio.shape[1] > 1:
35
  audio = audio.mean(axis=1)
36
  else:
37
  audio = audio.squeeze()
38
 
39
+ audio = audio.astype(np.float32)
 
 
 
 
 
 
 
 
 
40
 
41
+ # Step 4: Denoise with noisereduce
42
+ try:
43
+ import noisereduce as nr
44
+ audio = nr.reduce_noise(y=audio, sr=sr).astype(np.float32)
45
+ except Exception as e:
46
+ logger.warning(f"[Denoiser] noisereduce failed: {e}, using raw audio")
 
 
 
 
 
 
 
47
 
48
+ # Step 5: Normalise loudness
49
+ audio = self._normalise(audio, sr)
50
 
51
+ # Step 6: Save
52
  out_path = os.path.join(out_dir, "denoised.wav")
53
  sf.write(out_path, audio, sr, subtype="PCM_16")
54
 
55
+ logger.info(f"[Denoiser] Done in {time.time()-t0:.2f}s")
56
  return out_path
57
 
 
58
  def _convert_to_wav(self, src: str, dst: str):
59
+ cmd = ["ffmpeg", "-y", "-i", src, "-acodec", "pcm_s16le",
60
+ "-ar", str(TARGET_SR), "-ac", "1", dst]
 
 
 
 
 
 
61
  result = subprocess.run(cmd, capture_output=True, text=True)
62
  if result.returncode != 0:
 
 
63
  try:
64
+ data, sr = sf.read(src, always_2d=True)
65
+ sf.write(dst, data, sr, subtype="PCM_16")
66
+ except Exception as e:
67
+ raise RuntimeError(f"Cannot read audio file: {e}")
68
+
69
+ def _normalise(self, audio: np.ndarray, sr: int) -> np.ndarray:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  try:
71
  import pyloudnorm as pyln
72
  meter = pyln.Meter(sr)
73
  loudness = meter.integrated_loudness(audio)
74
  if np.isfinite(loudness) and loudness < 0:
75
  audio = pyln.normalize.loudness(audio, loudness, TARGET_LOUDNESS)
 
76
  except Exception:
77
+ rms = np.sqrt(np.mean(audio ** 2))
78
+ if rms > 1e-9:
79
+ target = 10 ** (TARGET_LOUDNESS / 20.0)
80
+ audio = audio * (target / rms)
81
+ return np.clip(audio, -1.0, 1.0).astype(np.float32)