Spaces:
Running on Zero
Running on Zero
Download spectro.py from Azuredraco/audio-denoise: direct link, hf CLI and curl.
- Browser
- Download file 5.17 kB
-
https://huggingface.co/spaces/Azuredraco/audio-denoise/resolve/main/spectro.py
- Command line
-
hf download hf://spaces/Azuredraco/audio-denoise/spectro.py
-
curl -L -o spectro.py https://huggingface.co/spaces/Azuredraco/audio-denoise/resolve/main/spectro.py
5.17 kB
| """ | |
| spectro.py — Espectrogramas de antes, después y su diferencia (para curiosos). | |
| Frecuencia lineal hasta la máxima del fichero, para que se vea bien lo que hay por encima de 20 kHz. | |
| Para que audios de media hora no cuesten nada, cada columna de la imagen es la media de unas pocas | |
| FFT repartidas por su tramo (como mucho ~1600 columnas por imagen). | |
| Diferencia: si los dos audios están alineados (misma frecuencia y misma duración) es «después − antes» | |
| en cada punto (azul = se ha quitado, rojo = se ha añadido); si no (se han recortado silencios o pausas, | |
| o se ha cambiado la frecuencia), se compara el espectro medio de los dos. | |
| """ | |
| from __future__ import annotations | |
| from pathlib import Path | |
| import numpy as np | |
| import dsp | |
| from audio_io import read_audio | |
| NFFT = 2048 | |
| MAX_COLS = 1600 | |
| SUB = 4 | |
| def _spec(mono: np.ndarray, sr: int) -> tuple[np.ndarray, np.ndarray, float]: | |
| """Devuelve (dB [frecuencias × columnas], frecuencias, segundos por columna).""" | |
| n = len(mono) | |
| cols = int(min(MAX_COLS, max(1, (n - NFFT) // (NFFT // 2)))) | |
| hop = max(1, (n - NFFT) / cols) if n > NFFT else 1 | |
| win = np.hanning(NFFT).astype(np.float32) | |
| out = np.empty((NFFT // 2 + 1, cols), dtype=np.float32) | |
| x = np.pad(mono.astype(np.float32), (0, NFFT)) | |
| for c in range(cols): | |
| starts = (c * hop + np.linspace(0, max(0.0, hop - NFFT), SUB)).astype(int) if hop > NFFT else [int(c * hop)] | |
| p = np.mean([np.abs(np.fft.rfft(x[s : s + NFFT] * win)) ** 2 for s in starts], axis=0) | |
| out[:, c] = 10 * np.log10(np.maximum(p / (NFFT * win.sum() / 2), 1e-20)) | |
| return out, np.fft.rfftfreq(NFFT, 1 / sr), hop / sr | |
| def make_png(before: str, after: str, png: Path) -> str | None: | |
| try: | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| da, sra, _ = read_audio(before) | |
| db, srb, _ = read_audio(after) | |
| ma, mb = dsp.to_mono(da)[:, 0], dsp.to_mono(db)[:, 0] | |
| sa, fa, ta = _spec(ma, sra) | |
| sb, fb, tb = _spec(mb, srb) | |
| aligned = sra == srb and len(ma) == len(mb) | |
| fig, axes = plt.subplots(3, 1, figsize=(11, 8.4), gridspec_kw={"height_ratios": [1, 1, 1]}) | |
| fig.patch.set_facecolor("#14141f") | |
| vmax = float(max(np.percentile(sa, 99.9), np.percentile(sb, 99.9))) | |
| for ax, S, f, t, title in ((axes[0], sa, fa, ta, "Antes"), (axes[1], sb, fb, tb, "Después")): | |
| im = ax.imshow(S, origin="lower", aspect="auto", cmap="magma", vmin=vmax - 110, vmax=vmax, | |
| extent=[0, S.shape[1] * t, 0, f[-1] / 1000]) | |
| ax.set_title(title, color="white", fontsize=10, loc="left") | |
| ax.set_ylabel("kHz", color="#ddd") | |
| cb = fig.colorbar(im, ax=ax, pad=0.01) | |
| cb.set_label("dB", color="#ddd") | |
| cb.ax.tick_params(colors="#ddd", labelsize=8) | |
| ax = axes[2] | |
| if aligned: | |
| d = np.clip(sb - sa, -30, 30) | |
| im = ax.imshow(d, origin="lower", aspect="auto", cmap="RdBu_r", vmin=-30, vmax=30, | |
| extent=[0, d.shape[1] * ta, 0, fa[-1] / 1000]) | |
| ax.set_title("Diferencia (después − antes): azul = se ha quitado · rojo = se ha añadido", | |
| color="white", fontsize=10, loc="left") | |
| ax.set_ylabel("kHz", color="#ddd") | |
| cb = fig.colorbar(im, ax=ax, pad=0.01) | |
| cb.set_label("dB", color="#ddd") | |
| cb.ax.tick_params(colors="#ddd", labelsize=8) | |
| ax.set_xlabel("Tiempo (s)", color="#ddd") | |
| else: | |
| la = 10 * np.log10(np.mean(10 ** (sa / 10), axis=1)) | |
| lb = 10 * np.log10(np.mean(10 ** (sb / 10), axis=1)) | |
| fmax = min(fa[-1], fb[-1]) | |
| grid = np.linspace(0, fmax, 1000) | |
| ia, ib = np.interp(grid, fa, la), np.interp(grid, fb, lb) | |
| ax.plot(grid / 1000, ia, color="#f0a050", lw=1, label="Antes") | |
| ax.plot(grid / 1000, ib, color="#50c0f0", lw=1, label="Después") | |
| ax2 = ax.twinx() | |
| diff = np.convolve(ib - ia, np.ones(15) / 15, mode="same") # suavizado para leer la tendencia | |
| ax2.plot(grid / 1000, diff, color="#e05080", lw=1.2, alpha=.9, label="Diferencia") | |
| ax2.axhline(0, color="#888", lw=.6) | |
| ax2.set_ylabel("Diferencia (dB)", color="#e05080") | |
| ax2.tick_params(colors="#ddd", labelsize=8) | |
| ax.set_title("Espectro medio (la duración ha cambiado, así que se compara el promedio)", | |
| color="white", fontsize=10, loc="left") | |
| ax.set_xlabel("kHz", color="#ddd") | |
| ax.set_ylabel("dB", color="#ddd") | |
| ax.legend(loc="upper right", fontsize=8) | |
| ax.set_facecolor("#14141f") | |
| for a in axes: | |
| a.tick_params(colors="#ddd", labelsize=8) | |
| axes[0].set_xlabel("") | |
| axes[1].set_xlabel("Tiempo (s)", color="#ddd") | |
| fig.tight_layout() | |
| fig.savefig(png, dpi=100, facecolor=fig.get_facecolor()) | |
| plt.close(fig) | |
| return str(png) | |
| except Exception as e: # una imagen nunca debe romper la corrección | |
| print(f"[ESPECTRO] {e}") | |
| return None | |