gelpi01 commited on
Commit
48d4cfa
·
1 Parent(s): bf44ef0

Mejora audio: Demucs 6 stems + filtro pasa-altos y normalización

Browse files
Files changed (2) hide show
  1. app.py +20 -29
  2. audio_pipeline.py +71 -99
app.py CHANGED
@@ -9,51 +9,42 @@ from audio_pipeline import (
9
  reducir_ruido
10
  )
11
 
12
- def procesar_wav(input_wav_path):
13
  # 1) Separar 6 stems con Demucs
14
  stems_dir = separar_audio_demucs_6stems(input_wav_path)
15
 
16
- # 2) Limpiar cada stem
17
  limpiar_stems(stems_dir)
18
 
19
- # 3) Recoger rutas a los 5 stems limpios + preparar base
20
- stems = {}
21
- for stem in ["vocals", "drums", "bass", "guitar", "piano", "other"]:
22
- stems[stem] = os.path.join(stems_dir, f"{stem}_cleaned.wav")
23
 
24
- # 4) Generar base instrumental (sin vocals)
25
  combinar_stems_sin_vocales(stems_dir)
26
  base_raw = os.path.join(stems_dir, "base_instrumental.wav")
27
 
28
- # 5) Reducir ruido en la base
29
- base_clean = os.path.join(stems_dir, "base_instrumental_clean.wav")
30
- reducir_ruido(base_raw, base_clean)
31
-
32
- # 6) Devolver primero los 6 stems (incluye guitarra) y luego la base limpia
33
- return (
34
- stems["vocals"],
35
- stems["drums"],
36
- stems["bass"],
37
- stems["guitar"],
38
- stems["piano"],
39
- stems["other"],
40
- base_clean
41
- )
42
 
43
  demo = gr.Interface(
44
  fn=procesar_wav,
45
  inputs=gr.Audio(label="Sube un archivo .wav", type="filepath"),
46
  outputs=[
47
- gr.Audio(label="Vocals (limpio)", type="filepath"),
48
- gr.Audio(label="Drums (limpio)", type="filepath"),
49
- gr.Audio(label="Bass (limpio)", type="filepath"),
50
- gr.Audio(label="Guitar (limpio)", type="filepath"),
51
- gr.Audio(label="Piano (limpio)", type="filepath"),
52
- gr.Audio(label="Other (limpio)", type="filepath"),
53
  gr.Audio(label="Base instrumental limpia", type="filepath"),
54
  ],
55
- title="Stem Separation con Demucs (6 stems) + Base limpia",
56
- description="Obtén vocals, drums, bass, guitar, piano, other y la base instrumental limpia."
57
  )
58
 
59
  if __name__ == "__main__":
 
9
  reducir_ruido
10
  )
11
 
12
+ def procesar_wav(input_wav_path: str):
13
  # 1) Separar 6 stems con Demucs
14
  stems_dir = separar_audio_demucs_6stems(input_wav_path)
15
 
16
+ # 2) Limpiar cada stem (_cleaned.wav)
17
  limpiar_stems(stems_dir)
18
 
19
+ # 3) Recoger rutas a los 6 stems limpios
20
+ labels = ["vocals", "drums", "bass", "guitar", "piano", "other"]
21
+ stems_paths = [os.path.join(stems_dir, f"{lbl}_cleaned.wav") for lbl in labels]
 
22
 
23
+ # 4) Generar base instrumental (mezcla sin vocals)
24
  combinar_stems_sin_vocales(stems_dir)
25
  base_raw = os.path.join(stems_dir, "base_instrumental.wav")
26
 
27
+ # 5) Reducir ruido y normalizar la base
28
+ clean_base = os.path.join(stems_dir, "base_instrumental_clean.wav")
29
+ reducir_ruido(base_raw, clean_base)
30
+
31
+ # 6) Devolver stems + base limpia
32
+ return (*stems_paths, clean_base)
 
 
 
 
 
 
 
 
33
 
34
  demo = gr.Interface(
35
  fn=procesar_wav,
36
  inputs=gr.Audio(label="Sube un archivo .wav", type="filepath"),
37
  outputs=[
38
+ gr.Audio(label="Vocals limpio", type="filepath"),
39
+ gr.Audio(label="Drums limpio", type="filepath"),
40
+ gr.Audio(label="Bass limpio", type="filepath"),
41
+ gr.Audio(label="Guitar limpio", type="filepath"),
42
+ gr.Audio(label="Piano limpio", type="filepath"),
43
+ gr.Audio(label="Other limpio", type="filepath"),
44
  gr.Audio(label="Base instrumental limpia", type="filepath"),
45
  ],
46
+ title="Demucs 6-stems + Calidad Mejorada",
47
+ description="Sube tu WAV y obtén 6 stems limpios (incluye guitarra) más la base instrumental mejorada."
48
  )
49
 
50
  if __name__ == "__main__":
audio_pipeline.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import os
2
  import subprocess
3
  import sys
@@ -8,101 +10,38 @@ import librosa
8
  import numpy as np
9
  import soundfile as sf
10
  import noisereduce as nr
 
11
 
12
  # Suprime warnings de runtime (p.ej. invalid value encountered in divide)
13
  warnings.filterwarnings("ignore", category=RuntimeWarning)
14
 
15
- # Directorio base donde guardaremos todos los stems
16
  BASE_STEMS_DIR = "data/stems"
17
 
18
- def separar_audio_demucs_6stems(input_file, model="htdemucs_6s"):
 
19
  """
20
- Separa 6 stems con Demucs (vocals, drums, bass, guitar, piano, other),
21
- los guarda en data/stems/<model>/<track_folder>/ y devuelve la ruta de ese folder.
22
  """
23
- out_root = os.path.join(BASE_STEMS_DIR, model)
24
- os.makedirs(out_root, exist_ok=True)
25
-
26
- device = "cuda" if torch.cuda.is_available() else "cpu"
27
- cmd = [
28
- sys.executable,
29
- "-m", "demucs",
30
- "-n", model,
31
- "--out", out_root,
32
- "--device", device,
33
- input_file
34
- ]
35
- subprocess.run(cmd, check=True)
36
-
37
- # Demucs crea un subdirectorio con el nombre de la pista dentro de out_root
38
- # Encuentra el primer subdirectorio que contenga archivos .wav
39
- for entry in os.listdir(out_root):
40
- candidate = os.path.join(out_root, entry)
41
- if os.path.isdir(candidate):
42
- # Verifica que tenga stems
43
- wavs = [f for f in os.listdir(candidate) if f.endswith('.wav')]
44
- if wavs:
45
- return candidate
46
- raise FileNotFoundError(f"No se encontró el folder de stems en {out_root}")
47
-
48
- def limpiar_stems(stems_dir):
49
- """Aplica reducción de ruido a cada stem (_cleaned.wav)."""
50
- for archivo in os.listdir(stems_dir):
51
- if archivo.endswith(".wav"):
52
- ruta = os.path.join(stems_dir, archivo)
53
- y, sr = librosa.load(ruta, sr=None)
54
- reduced = nr.reduce_noise(y=y, sr=sr)
55
- sf.write(ruta.replace(".wav", "_cleaned.wav"), reduced, sr)
56
-
57
- def combinar_stems_sin_vocales(stems_dir):
58
- """Mezcla todos los stems limpios excepto vocals en base_instrumental.wav."""
59
- wavs = [
60
- f for f in os.listdir(stems_dir)
61
- if f.endswith("_cleaned.wav") and "vocals" not in f.lower()
62
- ]
63
- if not wavs:
64
- wavs = [
65
- f for f in os.listdir(stems_dir)
66
- if f.endswith(".wav") and "vocals" not in f.lower()
67
- ]
68
- signals = []
69
- for w in wavs:
70
- y, sr = librosa.load(os.path.join(stems_dir, w), sr=None)
71
- signals.append(y)
72
- if not signals:
73
- raise RuntimeError("No se encontraron stems para combinar.")
74
- maxlen = max(len(s) for s in signals)
75
- mix = sum(np.pad(s, (0, maxlen - len(s))) for s in signals) / len(signals)
76
- sf.write(os.path.join(stems_dir, "base_instrumental.wav"), mix, sr)
77
 
78
- def reducir_ruido(input_file, output_file, noise_duration=0.5):
79
- """Reduce ruido y guarda el resultado."""
80
- y, sr = librosa.load(input_file, sr=None)
81
- noise = y[:int(sr * noise_duration)]
82
- with np.errstate(divide='ignore', invalid='ignore'):
83
- reduced = nr.reduce_noise(y=y, sr=sr, y_noise=noise)
84
- reduced = np.nan_to_num(reduced)
85
- sf.write(output_file, reduced, sr)
86
- import os
87
- import subprocess
88
- import sys
89
- import torch
90
- import warnings
91
- import librosa
92
- import numpy as np
93
- import soundfile as sf
94
- import noisereduce as nr
95
 
96
- warnings.filterwarnings("ignore", category=RuntimeWarning)
 
 
 
 
 
 
97
 
98
- BASE_STEMS_DIR = "data/stems"
99
 
100
- def separar_audio_demucs_6stems(input_file, model="htdemucs_6s"):
101
  """
102
- Separa 6 stems con Demucs (vocals, drums, bass, guitar, piano, other),
103
- busca recursivamente la carpeta con .wav y la devuelve.
104
  """
105
- base = os.path.splitext(os.path.basename(input_file))[0]
106
  out_root = os.path.join(BASE_STEMS_DIR, model)
107
  os.makedirs(out_root, exist_ok=True)
108
 
@@ -116,26 +55,46 @@ def separar_audio_demucs_6stems(input_file, model="htdemucs_6s"):
116
  ]
117
  subprocess.run(cmd, check=True)
118
 
119
- # Ahora buscamos recursivamente el primer directorio que contenga .wav
120
- for root, dirs, files in os.walk(out_root):
121
  if any(f.endswith(".wav") for f in files):
122
  return root
123
 
124
- # Si no aparece ninguno, error
125
  raise FileNotFoundError(f"No se encontró el folder de stems en {out_root}")
126
 
127
 
128
- def limpiar_stems(stems_dir):
129
- """Aplica reducción de ruido a cada stem (_cleaned.wav)."""
 
 
 
 
 
 
130
  for archivo in os.listdir(stems_dir):
131
  if archivo.endswith(".wav"):
132
- ruta = os.path.join(stems_dir, archivo)
133
- y, sr = librosa.load(ruta, sr=None)
134
- reduced = nr.reduce_noise(y=y, sr=sr)
135
- sf.write(ruta.replace(".wav", "_cleaned.wav"), reduced, sr)
 
 
 
 
 
 
 
136
 
137
- def combinar_stems_sin_vocales(stems_dir):
138
- """Mezcla todos los stems limpios excepto vocals en base_instrumental.wav."""
 
 
 
 
 
 
 
 
139
  wavs = [
140
  f for f in os.listdir(stems_dir)
141
  if f.endswith("_cleaned.wav") and "vocals" not in f.lower()
@@ -145,21 +104,34 @@ def combinar_stems_sin_vocales(stems_dir):
145
  f for f in os.listdir(stems_dir)
146
  if f.endswith(".wav") and "vocals" not in f.lower()
147
  ]
 
148
  signals = []
 
149
  for w in wavs:
150
  y, sr = librosa.load(os.path.join(stems_dir, w), sr=None)
151
  signals.append(y)
152
- if not signals:
 
153
  raise RuntimeError("No se encontraron stems para combinar.")
 
154
  maxlen = max(len(s) for s in signals)
155
  mix = sum(np.pad(s, (0, maxlen - len(s))) for s in signals) / len(signals)
156
  sf.write(os.path.join(stems_dir, "base_instrumental.wav"), mix, sr)
157
 
158
- def reducir_ruido(input_file, output_file, noise_duration=0.5):
159
- """Reduce ruido y guarda el resultado."""
 
 
 
 
 
 
 
160
  y, sr = librosa.load(input_file, sr=None)
161
  noise = y[:int(sr * noise_duration)]
162
- with np.errstate(divide='ignore', invalid='ignore'):
163
- reduced = nr.reduce_noise(y=y, sr=sr, y_noise=noise)
164
- reduced = np.nan_to_num(reduced)
165
- sf.write(output_file, reduced, sr)
 
 
 
1
+ # audio_pipeline.py
2
+
3
  import os
4
  import subprocess
5
  import sys
 
10
  import numpy as np
11
  import soundfile as sf
12
  import noisereduce as nr
13
+ from scipy.signal import butter, sosfilt
14
 
15
  # Suprime warnings de runtime (p.ej. invalid value encountered in divide)
16
  warnings.filterwarnings("ignore", category=RuntimeWarning)
17
 
18
+ # Carpeta raíz donde guardamos stems
19
  BASE_STEMS_DIR = "data/stems"
20
 
21
+
22
+ def highpass_filter(y: np.ndarray, sr: int, cutoff: float = 100.0, order: int = 4) -> np.ndarray:
23
  """
24
+ Aplica un filtro Butterworth de paso alto a la señal.
25
+ Recorta frecuencias por debajo de `cutoff` Hz para mayor claridad.
26
  """
27
+ sos = butter(order, cutoff, btype="highpass", fs=sr, output="sos")
28
+ return sosfilt(sos, y)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
+ def normalize(y: np.ndarray) -> np.ndarray:
32
+ """
33
+ Normaliza la señal para que su pico absoluto sea 1.0,
34
+ manteniendo la relación de amplitudes.
35
+ """
36
+ peak = np.max(np.abs(y)) or 1.0
37
+ return y / peak
38
 
 
39
 
40
+ def separar_audio_demucs_6stems(input_file: str, model: str = "htdemucs_6s") -> str:
41
  """
42
+ Usa Demucs para separar en 6 stems (vocals, drums, bass, guitar, piano, other).
43
+ Devuelve la ruta al directorio donde están los .wav resultantes.
44
  """
 
45
  out_root = os.path.join(BASE_STEMS_DIR, model)
46
  os.makedirs(out_root, exist_ok=True)
47
 
 
55
  ]
56
  subprocess.run(cmd, check=True)
57
 
58
+ # Busca recursivamente el primer folder con .wav
59
+ for root, _, files in os.walk(out_root):
60
  if any(f.endswith(".wav") for f in files):
61
  return root
62
 
 
63
  raise FileNotFoundError(f"No se encontró el folder de stems en {out_root}")
64
 
65
 
66
+ def limpiar_stems(stems_dir: str) -> None:
67
+ """
68
+ Para cada stem:
69
+ 1) Reduce ruido
70
+ 2) Filtro pasa-altos
71
+ 3) Normaliza
72
+ 4) Guarda como *_cleaned.wav
73
+ """
74
  for archivo in os.listdir(stems_dir):
75
  if archivo.endswith(".wav"):
76
+ ruta_in = os.path.join(stems_dir, archivo)
77
+ y, sr = librosa.load(ruta_in, sr=None)
78
+
79
+ # 1) reducción de ruido
80
+ y_denoised = nr.reduce_noise(y=y, sr=sr)
81
+
82
+ # 2) paso alto
83
+ y_hp = highpass_filter(y_denoised, sr, cutoff=100.0)
84
+
85
+ # 3) normalización
86
+ y_norm = normalize(y_hp)
87
 
88
+ # 4) guardar
89
+ ruta_out = ruta_in.replace(".wav", "_cleaned.wav")
90
+ sf.write(ruta_out, y_norm, sr)
91
+
92
+
93
+ def combinar_stems_sin_vocales(stems_dir: str) -> None:
94
+ """
95
+ Mezcla todos los stems *_cleaned.wav excepto 'vocals'
96
+ en un único archivo 'base_instrumental.wav'.
97
+ """
98
  wavs = [
99
  f for f in os.listdir(stems_dir)
100
  if f.endswith("_cleaned.wav") and "vocals" not in f.lower()
 
104
  f for f in os.listdir(stems_dir)
105
  if f.endswith(".wav") and "vocals" not in f.lower()
106
  ]
107
+
108
  signals = []
109
+ sr = None
110
  for w in wavs:
111
  y, sr = librosa.load(os.path.join(stems_dir, w), sr=None)
112
  signals.append(y)
113
+
114
+ if not signals or sr is None:
115
  raise RuntimeError("No se encontraron stems para combinar.")
116
+
117
  maxlen = max(len(s) for s in signals)
118
  mix = sum(np.pad(s, (0, maxlen - len(s))) for s in signals) / len(signals)
119
  sf.write(os.path.join(stems_dir, "base_instrumental.wav"), mix, sr)
120
 
121
+
122
+ def reducir_ruido(input_file: str, output_file: str, noise_duration: float = 0.5) -> None:
123
+ """
124
+ Procesa un WAV completo:
125
+ 1) Reduce ruido usando los primeros `noise_duration` s
126
+ 2) Aplica filtro pasa-altos
127
+ 3) Normaliza
128
+ 4) Guarda en output_file
129
+ """
130
  y, sr = librosa.load(input_file, sr=None)
131
  noise = y[:int(sr * noise_duration)]
132
+ y_denoised = nr.reduce_noise(y=y, sr=sr, y_noise=noise)
133
+
134
+ y_hp = highpass_filter(y_denoised, sr, cutoff=100.0)
135
+ y_norm = normalize(y_hp)
136
+
137
+ sf.write(output_file, y_norm, sr)