Spaces:
Running on Zero
Running on Zero
File size: 5,168 Bytes
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 | """
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
|