Spaces:
Running on Zero
Running on Zero
File size: 20,795 Bytes
7e8e229 fc527e7 7e8e229 7a957eb 7e8e229 7a957eb 7e8e229 7a957eb 7e8e229 7a957eb fc527e7 7e8e229 c6a2c55 d903372 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 | """
dsp.py — Procesado de señal: remuestreo, filtros, detección de voz, silencios y nivel.
Convención: el audio va como float32 con forma (muestras, canales).
"""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
import soxr
from numpy.lib.stride_tricks import sliding_window_view
from scipy import signal
from scipy.ndimage import minimum_filter1d
FRAME_MS = 20
DIGITAL_SILENCE_DB = -140.0
def db(x: float) -> float:
return 20 * np.log10(x) if x > 0 else -np.inf
def to_mono(x: np.ndarray) -> np.ndarray:
return x.mean(axis=1, keepdims=True) if x.shape[1] > 1 else x
def resample(x: np.ndarray, sr_in: int, sr_out: int) -> np.ndarray:
"""Remuestreo de muy alta calidad (soxr VHQ, fase lineal, sin desfase)."""
if sr_in == sr_out:
return x
y = soxr.resample(x, sr_in, sr_out, quality="VHQ")
return np.asarray(y, dtype=np.float32).reshape(-1, x.shape[1])
def highpass(x: np.ndarray, sr: int, fc: float | None) -> np.ndarray:
"""Paso alto Butterworth de fase cero (−24 dB/oct efectivos). Quita retumbes, golpes graves y DC."""
if not fc:
return x
sos = signal.butter(2, fc, btype="highpass", fs=sr, output="sos")
return signal.sosfiltfilt(sos, x, axis=0).astype(np.float32)
# ── Reducción de ruido a banda completa ──────────────────────────────────────
HF_SPLIT_HZ = 20000.0
def _crossover_lowpass(x: np.ndarray, sr: int) -> np.ndarray:
"""Paso bajo FIR de fase lineal en HF_SPLIT_HZ (transición ±1 kHz); complementario exacto con x − LP(x)."""
taps = signal.firwin(1023, HF_SPLIT_HZ, width=2000, fs=sr, window=("kaiser", 10.0))
return signal.oaconvolve(x, taps[:, None], mode="same", axes=0).astype(np.float32)
def _hf_gain_track(x48: np.ndarray, y48: np.ndarray, n_out: int, sr_out: int, floor: float) -> np.ndarray:
"""
Ganancia temporal para la banda > 20 kHz: cuánto ha bajado la energía de 12–20 kHz con
DeepFilterNet en cada instante. Así los ultrasonidos se atenúan igual que el ruido audible
y se conservan donde hay voz (sibilantes, aire).
"""
hop = 480 # 10 ms a 48 kHz
sos = signal.butter(4, [12000, 20000], btype="bandpass", fs=48000, output="sos")
bx = signal.sosfiltfilt(sos, x48.mean(axis=1))
by = signal.sosfiltfilt(sos, y48.mean(axis=1))
nfr = len(bx) // hop
if nfr == 0:
return np.ones(n_out, dtype=np.float32)
ex = (bx[: nfr * hop].reshape(nfr, hop) ** 2).mean(axis=1)
ey = (by[: nfr * hop].reshape(nfr, hop) ** 2).mean(axis=1)
g = np.sqrt(np.clip(ey / np.maximum(ex, 1e-20), 0, 1))
g = signal.filtfilt(np.ones(5) / 5, [1.0], g) if nfr > 15 else g # suaviza ±25 ms
g = np.clip(g, floor, 1.0)
t_fr = (np.arange(nfr) + 0.5) * hop / 48000
t_out = np.arange(n_out) / sr_out
return np.interp(t_out, t_fr, g).astype(np.float32)
def denoise(x: np.ndarray, sr: int, atten_db: float | None, keep_ultrasonic: bool = True,
use_gpu: bool = True, progress=None) -> tuple[np.ndarray, dict]:
"""
Reduce el ruido conservando la frecuencia de muestreo original.
Si sr > 48 kHz y keep_ultrasonic, la banda > 20 kHz no pasa por el modelo (que llega a 24 kHz):
se conserva del original con una ganancia que sigue la reducción aplicada a 12–20 kHz.
"""
from denoise_core import DF_SR, enhance_48k
x48 = resample(x, sr, DF_SR)
y48, info = enhance_48k(x48.T, atten_db, use_gpu=use_gpu, progress=progress)
y48 = y48.T
y = resample(y48, DF_SR, sr)
n = x.shape[0]
y = y[:n] if y.shape[0] >= n else np.pad(y, ((0, n - y.shape[0]), (0, 0)))
if keep_ultrasonic and sr > DF_SR:
floor = 10 ** (-atten_db / 20) if atten_db else 0.0
high = x - _crossover_lowpass(x, sr)
g = _hf_gain_track(x48, y48, n, sr, floor)
y = _crossover_lowpass(y, sr) + high * g[:, None]
info["avisos"].append("Agudos extremos (más de 20 kHz) conservados del original")
return y.astype(np.float32), info
# ── Detección de voz y silencios ─────────────────────────────────────────────
@dataclass
class VoiceRegion:
start: int # primera muestra con voz
end: int # última muestra con voz
noise_db: float # nivel estimado de ruido de fondo por trama
start_th_db: float
frame_db: np.ndarray # nivel por trama (20 ms)
frame_len: int
found: bool
def frame_levels(mono: np.ndarray, sr: int) -> tuple[np.ndarray, int]:
flen = max(64, int(sr * FRAME_MS / 1000))
nfr = max(1, int(np.ceil(len(mono) / flen)))
padded = np.pad(mono.astype(np.float64), (0, nfr * flen - len(mono)))
p = (padded.reshape(nfr, flen) ** 2).mean(axis=1)
return 10 * np.log10(np.maximum(p, 1e-30)), flen
def detect_voice(mono: np.ndarray, sr: int, start_above: float = 14.0, end_above: float = 10.0,
min_ms: int = 120) -> VoiceRegion:
"""
Busca el primer y el último tramo con voz sostenida (≥ min_ms por encima del umbral).
Un clic o un golpe aislado, por cortos, no cuentan como voz.
El ruido de fondo se estima con el percentil 10 de las tramas no digitales.
"""
fdb, flen = frame_levels(mono, sr)
live = fdb[fdb > DIGITAL_SILENCE_DB]
if live.size == 0:
return VoiceRegion(0, len(mono) - 1, -np.inf, -np.inf, fdb, flen, False)
noise = float(np.percentile(live, 10))
loud = float(np.percentile(live, 95))
start_th = max(noise + start_above, loud - 45)
end_th = max(noise + end_above, loud - 50)
k = max(2, int(round(min_ms / FRAME_MS)))
if len(fdb) < k:
return VoiceRegion(0, len(mono) - 1, noise, start_th, fdb, flen, False)
win_min = sliding_window_view(fdb, k).min(axis=1)
starts = np.flatnonzero(win_min >= start_th)
if starts.size == 0:
return VoiceRegion(0, len(mono) - 1, noise, start_th, fdb, flen, False)
first = int(starts[0])
ends = np.flatnonzero(win_min[first:] >= end_th)
last = first + int(ends[-1]) + k - 1
start = first * flen
end = min(len(mono) - 1, (last + 1) * flen - 1)
return VoiceRegion(start, end, noise, start_th, fdb, flen, True)
def detect_voice_v4(mono: np.ndarray, sr: int) -> VoiceRegion:
"""
Réplica exacta de detectVoiceRegion() del «Analizador de audio v4» (HTML):
ruido = media en dB de las tramas de los primeros y últimos 0,4 s; inicio = primeras 6 tramas
seguidas ≥ ruido + 14 dB; final = últimas 6 tramas seguidas ≥ ruido + 10 dB.
Así lo que mide y corrige la app coincide con lo que mide esa herramienta.
"""
flen = max(64, round(sr * FRAME_MS / 1000))
nfr = max(1, int(np.ceil(len(mono) / flen)))
x = np.asarray(mono, dtype=np.float64)
padded = np.pad(x, (0, nfr * flen - len(x)))
sums = (padded.reshape(nfr, flen) ** 2).sum(axis=1)
counts = np.full(nfr, flen, dtype=np.float64)
counts[-1] = len(x) - (nfr - 1) * flen # la última trama puede ser más corta (como en el HTML)
with np.errstate(divide="ignore"):
fdb = 10 * np.log10(sums / counts)
edge = max(3, int(0.4 * sr / flen))
vals = np.concatenate([fdb[: min(edge, nfr)], fdb[max(0, nfr - edge) :]])
vals = vals[np.isfinite(vals)]
noise = float(vals.mean()) if vals.size else -90.0
start_th, end_th = noise + 14, noise + 10
k = max(2, round(120 / FRAME_MS))
fdb_safe = np.where(np.isfinite(fdb), fdb, -1e9)
if nfr < k:
return VoiceRegion(0, len(mono) - 1, noise, start_th, fdb_safe, flen, False)
win_min = sliding_window_view(fdb_safe, k).min(axis=1)
starts = np.flatnonzero(win_min >= start_th)
if starts.size == 0:
return VoiceRegion(0, len(mono) - 1, noise, start_th, fdb_safe, flen, False)
first = int(starts[0])
ends = np.flatnonzero(win_min[first:] >= end_th)
last = first + int(ends[-1]) + k - 1 if ends.size else nfr - 1
return VoiceRegion(first * flen, min(len(mono) - 1, (last + 1) * flen - 1), noise, start_th, fdb_safe, flen, True)
def voice_region(mono: np.ndarray, sr: int, method: str | None = None) -> VoiceRegion:
"""Detector de voz según el perfil: «analizador_v4» (compatible con el HTML) o el propio por defecto."""
return detect_voice_v4(mono, sr) if method == "analizador_v4" else detect_voice(mono, sr)
def find_pauses(mono: np.ndarray, sr: int, region: VoiceRegion, min_s: float = 0.3) -> list[tuple[int, int]]:
"""
Pausas internas (entre la primera y la última voz) de al menos `min_s`, en muestras [ini, fin).
Una trama es pausa si queda por debajo del umbral de «fin de voz» (ruido + 10 dB).
"""
if not region.found:
return []
flen, fdb = region.frame_len, region.frame_db
quiet = fdb < max(region.start_th_db - 4, DIGITAL_SILENCE_DB)
f0, f1 = region.start // flen, region.end // flen
pauses, run = [], None
for f in range(f0, f1 + 1):
if quiet[f] and run is None:
run = f
elif not quiet[f] and run is not None:
if (f - run) * flen >= min_s * sr:
pauses.append((run * flen, f * flen))
run = None
return pauses
def shorten_pauses(x: np.ndarray, sr: int, region: VoiceRegion, max_s: float) -> tuple[np.ndarray, list[float]]:
"""
Acorta a `max_s` las pausas internas más largas. Conserva la mitad de `max_s` pegada a cada lado
(respiraciones y finales de palabra intactos), quita el centro y une con un fundido de 30 ms.
Devuelve (audio, duraciones originales de las pausas acortadas).
"""
mono = to_mono(x)[:, 0]
long_ones = [(a, b) for a, b in find_pauses(mono, sr, region) if (b - a) > max_s * sr + 0.05 * sr]
if not long_ones:
return x, []
half = int(max_s * sr / 2)
xf = min(int(0.03 * sr), half // 2)
fade = np.sin(np.linspace(0, np.pi / 2, xf, dtype=np.float32))[:, None] if xf else None
pieces, pos = [], 0
for a, b in long_ones:
cut_a, cut_b = a + half, b - half - xf # se elimina [cut_a, cut_b); el fundido solapa xf muestras
left = x[pos:cut_a].copy()
right_start = cut_b
if xf:
# fundido de potencia constante entre el final del tramo izquierdo y el inicio del derecho
tail = left[-xf:] * fade[::-1]
head = x[right_start : right_start + xf] * fade
left[-xf:] = tail + head
right_start += xf
pieces.append(left)
pos = right_start
pieces.append(x[pos:])
return np.concatenate(pieces).astype(np.float32), [(b - a) / sr for a, b in long_ones]
def _room_tone(x: np.ndarray, region: VoiceRegion, length: int, sr: int) -> np.ndarray:
"""
Construye `length` muestras de "tono de sala" a partir del tramo sin voz más silencioso
de la propia grabación, encadenando trozos con fundidos de potencia constante.
"""
flen = region.frame_len
seg_frames = max(5, int(0.25 * sr / flen)) # 250 ms
fdb = region.frame_db
if len(fdb) <= seg_frames:
return np.zeros((length, x.shape[1]), dtype=np.float32)
# trama inicial del tramo de 250 ms con menos energía (y sin silencio digital)
win_mean = sliding_window_view(fdb, seg_frames).mean(axis=1)
win_mean[sliding_window_view(fdb, seg_frames).min(axis=1) <= DIGITAL_SILENCE_DB] = np.inf
if not np.isfinite(win_mean).any():
return np.zeros((length, x.shape[1]), dtype=np.float32)
i = int(np.argmin(win_mean))
piece = x[i * flen : (i + seg_frames) * flen]
xf = min(len(piece) // 4, int(0.02 * sr))
fade_in = np.sin(np.linspace(0, np.pi / 2, xf))[:, None] if xf else None
out = np.zeros((length + len(piece), x.shape[1]), dtype=np.float32)
pos, flip = 0, False
while pos < length:
p = piece[::-1] if flip else piece # alternar sentido evita repeticiones evidentes
if pos and xf:
out[pos - xf : pos] *= fade_in[::-1]
p = p.copy()
p[:xf] *= fade_in
out[pos - xf : pos - xf + len(p)] += p
pos += len(p) - xf
else:
out[pos : pos + len(p)] = p
pos += len(p)
flip = not flip
return out[:length]
def set_edge_silences(x: np.ndarray, sr: int, region: VoiceRegion, target_s: float,
fill: str = "tono_sala", sides=("ini", "fin")) -> tuple[np.ndarray, list[str]]:
"""
Deja exactamente `target_s` antes de la primera voz («ini») y/o después de la última («fin»).
Solo toca los lados indicados en `sides`; el resto queda idéntico.
"""
notes = []
tgt = int(round(target_s * sr))
head, tail = region.start, x.shape[0] - 1 - region.end
core = x[region.start : region.end + 1]
filler = (lambda n: _room_tone(x, region, n, sr)) if fill == "tono_sala" else \
(lambda n: np.zeros((n, x.shape[1]), dtype=np.float32))
def edge(seg_len, side):
if side not in sides: # lado que ya cumple: intacto
return x[: region.start] if side == "ini" else x[region.end + 1 :]
if seg_len >= tgt: # recortar (se conserva el tramo pegado a la voz)
return x[region.start - tgt : region.start] if side == "ini" else x[region.end + 1 : region.end + 1 + tgt]
extra = filler(tgt - seg_len)
if side == "ini":
return np.concatenate([extra, x[: region.start]])
return np.concatenate([x[region.end + 1 :], extra])
pre, post = edge(head, "ini"), edge(tail, "fin")
out = np.concatenate([pre, core, post]).astype(np.float32)
# fundidos muy cortos en los extremos del fichero (evitan clics al cortar)
f = min(int(0.005 * sr), len(out) // 2)
if f:
ramp = np.linspace(0, 1, f, dtype=np.float32)[:, None]
if "ini" in sides:
out[:f] *= ramp
if "fin" in sides:
out[-f:] *= ramp[::-1]
for nombre, antes, side in (("inicial", head, "ini"), ("final", tail, "fin")):
if side in sides and abs(antes - tgt) > sr * 0.001:
accion = "recortado" if antes > tgt else ("alargado con el silencio natural de la sala" if fill == "tono_sala" else "alargado con silencio")
notes.append(f"Silencio {nombre}: {antes / sr:.2f} s → {target_s:.2f} s ({accion})")
return out, notes
def pause_expander(x: np.ndarray, sr: int, range_db: float = 15.0) -> tuple[np.ndarray, dict]:
"""
Expansor suave: baja `range_db` el ruido residual en las pausas sin tocar la voz.
La zona de voz se ensancha 100 ms antes y 200 ms después (no se comen finales de palabra
ni respiraciones) y la ganancia se suaviza ~100 ms para que no haya «bombeo».
"""
mono = to_mono(x)[:, 0]
fdb, flen = frame_levels(mono, sr)
live = fdb[fdb > DIGITAL_SILENCE_DB]
if live.size == 0:
return x, {"pausas_pct": 0.0}
noise = float(np.percentile(live, 10))
loud = float(np.percentile(live, 95))
core = fdb >= max(noise + 10, loud - 50)
voice = core.copy()
for s in range(1, int(100 / FRAME_MS) + 1): # 100 ms antes de cada trama con voz
voice[:-s] |= core[s:]
for s in range(1, int(200 / FRAME_MS) + 1): # 200 ms después
voice[s:] |= core[:-s]
g = np.where(voice, 1.0, 10 ** (-range_db / 20))
k = np.hanning(7)[1:-1]
g = np.convolve(np.pad(g, 2, mode="edge"), k / k.sum(), mode="valid")
g = np.maximum(g, np.where(voice, 1.0, 0.0)) # la voz nunca se atenúa
t_fr = (np.arange(len(g)) + 0.5) * flen
gs = np.interp(np.arange(x.shape[0]), t_fr, g).astype(np.float32)
return (x * gs[:, None]).astype(np.float32), {"pausas_pct": float(100 * (~voice).mean())}
# ── Nivel ────────────────────────────────────────────────────────────────────
def peak_limit(x: np.ndarray, sr: int, ceiling_db: float, window_ms: float = 5.0) -> tuple[np.ndarray, dict]:
"""
Limitador de picos sin sobreoscilaciones (offline, con anticipación):
ganancia = mínimo deslizante de (techo / |x|) suavizado con una ventana del mismo ancho.
Garantiza |salida| ≤ techo. Solo actúa alrededor de los picos que superan el techo.
"""
ceiling = 10 ** (ceiling_db / 20)
peak = np.abs(x).max(axis=1)
if peak.max() <= ceiling:
return x, {"muestras_afectadas": 0, "reduccion_max_db": 0.0}
w = max(1, int(sr * window_ms / 1000))
g = np.minimum(1.0, ceiling / np.maximum(peak, 1e-12))
g = minimum_filter1d(g, size=2 * w + 1, mode="nearest")
win = np.hanning(2 * w + 3)[1:-1]
g = signal.oaconvolve(np.pad(g, w, mode="edge"), win / win.sum(), mode="valid")
y = (x * g[:, None]).astype(np.float32)
# seguridad numérica: nunca por encima del techo
over = np.abs(y) > ceiling
if over.any():
y[over] = np.sign(y[over]) * ceiling
return y, {"muestras_afectadas": int((g < 0.999).sum()), "reduccion_max_db": float(-db(g.min()))}
def rms_db(x: np.ndarray) -> float:
return db(float(np.sqrt(np.mean(np.asarray(x, dtype=np.float64) ** 2)))) if x.size else -np.inf
# ── Ruidos sueltos en las pausas (cláxones, golpes, puertas…) ────────────────
EVENT_ABOVE_DB = 12.0 # un ruido suelto destaca al menos 12 dB sobre el ruido de fondo
EVENT_GUARD_S = 0.25 # no se toca nada a menos de 250 ms de la voz (inicios y finales de palabra)
EVENT_LOUD_DB = 15.0 # y llega a menos de 15 dB del volumen de la voz (respiraciones y ruiditos se quedan)
EVENT_MAX_S = 8.0 # más largo que esto no es un «ruido suelto» (música, obra…): se deja
def remove_events(x: np.ndarray, sr: int, speech_prob: np.ndarray, hop_s: float) -> tuple[np.ndarray, list]:
"""
Sustituye por tono de sala los ruidos fuertes que caen en las pausas (donde el detector neuronal
dice que NO hay voz, el nivel sube ≥ 12 dB sobre el fondo y llega cerca del volumen de la voz).
La voz y sus alrededores no se tocan; las respiraciones y ruiditos suaves, tampoco. Si el detector no
encuentra voz en todo el audio (risas, llantos…), no se toca nada.
speech_prob: probabilidad de voz por ventana de `hop_s` segundos. Devuelve (audio, [(inicio_s, dur_s, dB)]).
"""
mono = to_mono(x)[:, 0]
hop = hop_s * sr
n = min(len(speech_prob), int(len(mono) / hop))
if n < 10:
return x, []
idx = (np.arange(n + 1) * hop).astype(int)
lvl = np.array([rms_db(mono[idx[i] : idx[i + 1]]) for i in range(n)])
live = lvl[lvl > DIGITAL_SILENCE_DB]
if live.size < 10:
return x, []
floor = float(np.percentile(live, 10))
p = speech_prob[:n]
speech = p > 0.4
if speech.sum() < 10:
return x, []
loud = float(np.median(lvl[p > 0.5])) - EVENT_LOUD_DB if (p > 0.5).any() else float(np.median(lvl[speech])) - EVENT_LOUD_DB
g = max(1, int(round(EVENT_GUARD_S / hop_s)))
near = np.convolve(speech.astype(int), np.ones(2 * g + 1, dtype=int), mode="same") > 0
cand = (p < 0.1) & (lvl > floor + EVENT_ABOVE_DB) & ~near
# ensanchar 2 ventanas (colas del ruido) sin entrar en la zona protegida
cand = (np.convolve(cand.astype(int), np.ones(5, dtype=int), mode="same") > 0) & ~near
events, i = [], 0
while i < n:
if not cand[i]:
i += 1
continue
j = i
while j + 1 < n and (cand[j + 1] or (j + 3 < n and cand[j + 2:j + 4].any())):
j += 1
if (j + 1 - i) * hop_s <= EVENT_MAX_S and lvl[i : j + 1].max() >= loud:
events.append((i, j + 1))
i = j + 1
if not events:
return x, []
region = voice_region(mono, sr)
y = x.copy()
xf = int(0.015 * sr)
out = []
for a, b in events:
s0, s1 = idx[a], idx[min(b, n)]
tone = _room_tone(x, region, s1 - s0, sr)
w = np.ones(s1 - s0, dtype=np.float32)
k = min(xf, (s1 - s0) // 2)
if k:
ramp = np.sin(np.linspace(0, np.pi / 2, k)) ** 2
w[:k], w[-k:] = ramp, ramp[::-1]
y[s0:s1] = x[s0:s1] * (1 - w[:, None]) + tone[: s1 - s0] * w[:, None]
out.append((s0 / sr, (s1 - s0) / sr, float(lvl[a:b].max() - floor)))
return y.astype(np.float32), out
def remove_hum(x: np.ndarray, sr: int, harmonics: list[float], q: float = 35.0) -> np.ndarray:
"""Filtros de muesca muy estrechos (Q 35: ~1,5 Hz a 50 Hz) en cada armónico del zumbido, sin desfase."""
y = x.astype(np.float64)
for f0 in harmonics:
if f0 < sr / 2 - 50:
b, a = signal.iirnotch(f0, q, fs=sr)
y = signal.filtfilt(b, a, y, axis=0)
return y.astype(np.float32)
|