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