¿Qué es el sonido?

Antes de que una máquina pueda entender el habla, reconocer un instrumento musical o generar una voz humana, necesita convertir el sonido en números. Pero, ¿qué es el sonido en primer lugar? Es una onda de presión — una perturbación que se propaga a través del aire (o cualquier medio) alternando regiones de alta y baja presión. Cuando hablas, tus cuerdas vocales vibran, empujando las moléculas de aire juntas y separándolas en rápida sucesión. Esas fluctuaciones de presión viajan hacia afuera hasta llegar a un micrófono, que las convierte en un pequeño voltaje eléctrico que sube y baja al ritmo de la presión. Un convertidor analógico-digital (ADC) luego mide ese voltaje a intervalos regulares, produciendo una secuencia de números — cada uno es un valor de amplitud que representa la presión en ese instante. Esa secuencia es audio digital.

El parámetro clave que controla este proceso es la tasa de muestreo ($f_s$): el número de mediciones (muestras) tomadas por segundo. Las tasas comunes incluyen 16 kHz (usada por modelos de voz como Whisper), 44.1 kHz (audio con calidad de CD) y 48 kHz (video profesional y transmisión). A 16 kHz, un segundo de audio se convierte en 16,000 números. Un minuto se convierte en 960,000 números. Diez minutos de un podcast se convierten en 9.6 millones de números. Esa es una cantidad enorme de datos para que un modelo procese directamente, y gran parte de este track trata sobre cómo comprimimos y transformamos esa señal cruda en algo más manejable.

Pero, ¿por qué 16 kHz para voz y 44.1 kHz para música? La respuesta viene del teorema de muestreo de Nyquist-Shannon (Shannon, 1949) : para capturar perfectamente una frecuencia $f$ en una señal continua, debes muestrear a una tasa de al menos $2f$. Si muestreas más lento que eso, el contenido de alta frecuencia se «pliega» hacia frecuencias más bajas — un fenómeno llamado aliasing — corrompiendo la señal de una manera que no se puede deshacer. La frecuencia más alta que una tasa de muestreo dada puede representar fielmente se llama la frecuencia de Nyquist :

$$f_{\text{max}} = \frac{f_s}{2}$$

Aquí $f_s$ es la tasa de muestreo y $f_{\text{max}}$ es la frecuencia de Nyquist — el límite absoluto de lo que podemos representar. Cualquier contenido de frecuencia por encima de $f_{\text{max}}$ sufrirá aliasing hacia frecuencias más bajas y corromperá la señal. Por debajo de $f_{\text{max}}$, la señal continua original puede reconstruirse perfectamente a partir de las muestras discretas (con suficientes bits por muestra). A 16 kHz de muestreo, $f_{\text{max}} = 8{,}000$ Hz. Los fundamentales del habla humana se sitúan entre aproximadamente 85 Hz (voz masculina grave) y 300 Hz (voz de niño), con energía de consonantes y sibilancia alcanzando hasta unos 8 kHz, por lo que 16 kHz captura bien el habla. Sin embargo, la audición humana se extiende hasta aproximadamente 20 kHz, por eso el audio de CD usa 44.1 kHz ($f_{\text{max}} = 22{,}050$ Hz) — suficiente margen para cubrir todo el rango audible.

💡 ¿Por qué 44.1 kHz específicamente y no, digamos, 40 kHz? El número proviene de principios de los años 1980 cuando Sony y Philips desarrollaron el estándar de CD. Necesitaban una tasa superior a 40 kHz (para cubrir los 20 kHz de audición con margen para filtros anti-aliasing), y 44,100 = 2\textsuperscript{2} \times 3\textsuperscript{2} \times 5\textsuperscript{2} \times 7\textsuperscript{2} era compatible tanto con equipos de video PAL como NTSC utilizados para almacenar masters digitales en cinta de video.

Para hacerlo concreto, el gráfico a continuación muestra una onda sinusoidal de 440 Hz (la nota musical A4, la referencia estándar de afinación) como señal continua, junto con muestras discretas tomadas a 16 kHz. Cada punto es un número que produce el ADC — la forma de onda completa entre puntos se pierde, pero gracias a Nyquist (ya que 440 Hz está muy por debajo del límite de 8 kHz), podríamos reconstruirla perfectamente.

import math, json, js

# Generate a 440 Hz sine wave (A4 note)
freq = 440  # Hz
duration = 0.005  # 5 ms — enough to show ~2 cycles
sample_rate = 16000  # 16 kHz

# "Continuous" signal: very dense sampling for smooth curve
n_continuous = 500
continuous_t = [i * duration / n_continuous for i in range(n_continuous)]
continuous_y = [math.sin(2 * math.pi * freq * t) for t in continuous_t]

# Discrete samples at 16 kHz
n_samples = int(sample_rate * duration)  # 80 samples in 5ms
sample_t = [i / sample_rate for i in range(n_samples)]
sample_y = [math.sin(2 * math.pi * freq * t) for t in sample_t]

# Convert time to milliseconds for readability
continuous_t_ms = [round(t * 1000, 4) for t in continuous_t]
sample_t_ms = [round(t * 1000, 4) for t in sample_t]

plot_data = [
    {
        "title": "440 Hz Sine Wave: Continuous vs Sampled at 16 kHz",
        "x_label": "Time (ms)",
        "y_label": "Amplitude",
        "x_data": continuous_t_ms,
        "lines": [
            {"label": "Continuous signal", "data": [round(y, 4) for y in continuous_y], "color": "#3b82f6"}
        ]
    },
    {
        "title": "Discrete Samples (16 kHz) — Each Dot Is One Number",
        "x_label": "Time (ms)",
        "y_label": "Amplitude",
        "x_data": sample_t_ms,
        "lines": [
            {"label": "Samples (16 kHz)", "data": [round(y, 4) for y in sample_y], "color": "#ef4444", "dotted": True}
        ]
    }
]
js.window.py_plot_data = json.dumps(plot_data)

print(f"Frequency: {freq} Hz (A4 note)")
print(f"Sampling rate: {sample_rate} Hz")
print(f"Nyquist frequency: {sample_rate // 2} Hz")
print(f"Samples in 5ms: {n_samples}")
print(f"440 Hz is well below {sample_rate // 2} Hz => no aliasing")

De formas de onda a frecuencia: La Transformada de Fourier

Un gráfico de forma de onda muestra la amplitud a lo largo del tiempo — te dice cuándo la señal es fuerte o débil, pero no qué frecuencias están presentes. Mira una forma de onda de alguien diciendo «hola» y verás una línea ondulada que casi no da pistas sobre los formantes vocálicos, las explosiones de consonantes o el tono de la voz del hablante. Para extraer esa información, necesitamos descomponer la señal en sus frecuencias constituyentes. Eso es lo que hace la Transformada de Fourier .

La intuición central es sorprendentemente simple: cualquier señal, sin importar cuán compleja sea, puede expresarse como una suma de ondas sinusoidales a diferentes frecuencias, amplitudes y fases. Un acorde de piano es una suma de las frecuencias fundamentales de cada nota más sus armónicos. Una vocal hablada es una suma de la frecuencia fundamental de las cuerdas vocales más las frecuencias resonantes moldeadas por la garganta y la boca. La Transformada de Fourier nos dice exactamente qué ondas sinusoidales sumar para reconstruir la señal original — convierte una representación en el dominio del tiempo (amplitud vs tiempo) en una representación en el dominio de la frecuencia (amplitud vs frecuencia).

Para audio digital discreto (una lista finita de $N$ muestras), usamos la Transformada Discreta de Fourier (DFT) :

$$X[k] = \sum_{n=0}^{N-1} x[n] \cdot e^{-i \, 2\pi k n / N}$$

Desglosemos cada símbolo. $x[n]$ es la $n$-ésima muestra de la señal — uno de nuestros valores de amplitud del ADC. $X[k]$ es el $k$-ésimo bin de frecuencia — un número complejo cuya magnitud $|X[k]|$ nos dice qué tan fuerte es la frecuencia $k$ en la señal, y cuya fase $\angle X[k]$ nos dice el desfase temporal de ese componente de frecuencia. $N$ es el número total de muestras en la ventana de análisis. El término $e^{-i \, 2\pi k n / N}$ es una sinusoide compleja (por la fórmula de Euler, $e^{-i\theta} = \cos\theta - i\sin\theta$) a frecuencia $k$. La sumatoria calcula el producto punto de la señal con esta sinusoide — mide cuánto la señal «se correlaciona con» o «se parece a» una onda sinusoidal a frecuencia $k$. Si la señal contiene un componente fuerte a esa frecuencia, el producto punto es grande; si no, los términos se cancelan y el resultado es cercano a cero.

El índice $k$ va de 0 a $N - 1$, pero para señales de valor real (como siempre es el audio), el espectro es simétrico: $X[k]$ y $X[N - k]$ son conjugados complejos. Así que solo los bins $k = 0$ hasta $k = N/2$ contienen información única. En los límites: $k = 0$ da el componente DC — el valor promedio de la señal (suma de todas las muestras). $k = N/2$ corresponde a la frecuencia de Nyquist — la frecuencia más alta representable a esta tasa de muestreo. Entre estos extremos, cada bin $k$ corresponde a una frecuencia de $k \cdot f_s / N$ Hz.

Calcular la DFT de forma ingenua requiere $N$ multiplicaciones para cada uno de los $N$ bins de frecuencia, dando una complejidad $O(N^2)$. La Transformada Rápida de Fourier (FFT) (Cooley & Tukey, 1965) explota simetrías en las exponenciales complejas para calcular el mismo resultado en $O(N \log N)$. Para una ventana típica de $N = 512$ muestras, eso son aproximadamente 4,600 operaciones en lugar de 262,000 — una aceleración de 57x. La FFT es uno de los algoritmos más importantes en todo el procesamiento de señales, y es lo que hace práctico el análisis de audio en tiempo real.

El gráfico a continuación demuestra la Transformada de Fourier en acción. Creamos una señal que es la suma de tres ondas sinusoidales (200 Hz, 500 Hz y 1200 Hz) y luego calculamos su espectro de magnitud. Los tres picos en el dominio de la frecuencia corresponden exactamente a las tres frecuencias que mezclamos.

import math, json, js

# Build a signal from 3 sine waves: 200Hz, 500Hz, 1200Hz
sample_rate = 16000
duration = 0.025  # 25ms window (400 samples)
N = int(sample_rate * duration)  # 400

# Generate the composite signal
signal = []
for n in range(N):
    t = n / sample_rate
    val = (0.8 * math.sin(2 * math.pi * 200 * t)
         + 0.5 * math.sin(2 * math.pi * 500 * t)
         + 0.3 * math.sin(2 * math.pi * 1200 * t))
    signal.append(val)

# Compute DFT magnitude (only first N/2+1 bins — unique part)
half_N = N // 2 + 1
magnitudes = []
for k in range(half_N):
    re = 0.0
    im = 0.0
    for n in range(N):
        angle = 2 * math.pi * k * n / N
        re += signal[n] * math.cos(angle)
        im -= signal[n] * math.sin(angle)
    mag = math.sqrt(re * re + im * im) / N  # normalise
    magnitudes.append(round(mag, 4))

# Frequency axis: each bin k -> k * fs / N Hz
freqs = [round(k * sample_rate / N, 1) for k in range(half_N)]

# Time axis in ms for waveform
time_ms = [round(n / sample_rate * 1000, 3) for n in range(N)]

plot_data = [
    {
        "title": "Composite Signal: 200 Hz + 500 Hz + 1200 Hz",
        "x_label": "Time (ms)",
        "y_label": "Amplitude",
        "x_data": time_ms,
        "lines": [
            {"label": "Signal", "data": [round(s, 4) for s in signal], "color": "#3b82f6"}
        ]
    },
    {
        "title": "DFT Magnitude Spectrum — Peaks at 200, 500, 1200 Hz",
        "x_label": "Frequency (Hz)",
        "y_label": "Magnitude",
        "x_data": freqs,
        "lines": [
            {"label": "Magnitude", "data": magnitudes, "color": "#10b981"}
        ]
    }
]
js.window.py_plot_data = json.dumps(plot_data)

print(f"Window: {N} samples ({duration*1000:.0f} ms at {sample_rate} Hz)")
print(f"Frequency bins: {half_N} (0 to {sample_rate//2} Hz)")
print(f"Frequency resolution: {sample_rate/N} Hz per bin")
print(f"Peak bins near 200, 500, 1200 Hz visible in the spectrum")
💡 La resolución en frecuencia de la DFT es $f_s / N$ Hz por bin. Con $f_s = 16{,}000$ y $N = 400$ (una ventana de 25ms), cada bin abarca 40 Hz. Eso significa que no podemos distinguir dos frecuencias separadas por menos de 40 Hz dentro de una sola ventana. Ventanas más largas dan una resolución en frecuencia más fina pero difuminan el eje temporal — un compromiso fundamental al que volveremos al hablar de espectrogramas.

Espectrogramas: Frecuencia a lo largo del tiempo

La DFT nos da el contenido frecuencial de una señal, pero analiza toda la señal de una vez. Eso está bien para un tono constante, pero el habla y la música cambian rápidamente — una sola palabra puede contener una vocal sonora, una consonante fricativa y un silencio, cada uno con perfiles de frecuencia completamente diferentes. Si ejecutamos una sola DFT sobre toda la palabra, esos diferentes sonidos se promedian y perdemos la capacidad de ver cuándo cada frecuencia estuvo activa. Necesitamos una forma de ver cómo el contenido frecuencial evoluciona a lo largo del tiempo .

La solución es la Transformada de Fourier de Tiempo Corto (STFT) : cortar la señal en ventanas cortas y superpuestas, y calcular la DFT en cada ventana independientemente. Cada ventana es lo suficientemente corta como para que la señal sea aproximadamente estacionaria dentro de ella (el contenido frecuencial no cambia mucho en 25 milisegundos), pero lo suficientemente larga como para dar una resolución en frecuencia razonable.

Tres parámetros controlan la STFT:

  • Tamaño de ventana (n_fft): el número de muestras en cada trama de análisis. Típicamente 25 ms, que a 16 kHz son 400 muestras. Esto determina la resolución en frecuencia: $f_s / \text{n\_fft} = 16{,}000 / 400 = 40$ Hz por bin. Ventanas más grandes dan mejor resolución en frecuencia pero difuminan el eje temporal.
  • Tamaño de salto (hop_length): cuánto avanzamos entre ventanas consecutivas. Típicamente 10 ms (160 muestras a 16 kHz). Un salto más corto que el tamaño de ventana significa que las ventanas se superponen, asegurando que no perdamos eventos transitorios que caigan entre tramas.
  • Función de ventana: una atenuación aplicada a cada trama antes de calcular la DFT. La elección estándar es una ventana de Hann ($0.5 - 0.5 \cos(2\pi n / N)$), que suaviza la señal a cero en los bordes de la trama. Sin esto, la truncación abrupta en los límites de la trama crea artefactos artificiales de alta frecuencia llamados fuga espectral .

El resultado es una matriz 2D llamada espectrograma . Un eje es el tiempo (cada columna es una ventana), el otro es la frecuencia (cada fila es un bin de frecuencia), y los valores son magnitudes $|X[k]|$. Con una ventana de 25 ms y un salto de 10 ms, un segundo de audio produce 100 tramas temporales. Cada trama tiene $\text{n\_fft}/2 + 1 = 201$ bins de frecuencia. Así que un segundo de audio se convierte en una matriz de $201 \times 100$ — ya una compresión masiva respecto a las 16,000 muestras crudas originales, y una que organiza la información de una manera mucho más útil.

Para ilustrar, el código a continuación genera una señal chirp — una onda sinusoidal cuya frecuencia aumenta linealmente de 200 Hz a 3000 Hz durante medio segundo — y calcula su espectrograma STFT. En la salida, puedes ver cómo la frecuencia pico en cada trama temporal se desplaza hacia arriba, exactamente como esperaríamos de un chirp. Esta es información que la forma de onda cruda oculta pero que el espectrograma revela inmediatamente.

import math, json, js

# Generate a chirp: frequency sweeps from 200Hz to 3000Hz over 0.1s
sample_rate = 16000
duration = 0.1  # 100ms to keep computation small
N_total = int(sample_rate * duration)  # 1600 samples

f_start, f_end = 200, 3000
signal = []
for n in range(N_total):
    t = n / sample_rate
    # Instantaneous frequency increases linearly
    f_inst = f_start + (f_end - f_start) * t / duration
    phase = 2 * math.pi * (f_start * t + 0.5 * (f_end - f_start) * t * t / duration)
    signal.append(math.sin(phase))

# STFT parameters
n_fft = 256
hop_length = 128

# Hann window
hann = [0.5 - 0.5 * math.cos(2 * math.pi * n / n_fft) for n in range(n_fft)]

# Compute STFT
n_frames = (N_total - n_fft) // hop_length + 1
half_bins = n_fft // 2 + 1  # 129 frequency bins

# For the table, show peak frequency per frame
rows = []
for frame_idx in range(n_frames):
    start = frame_idx * hop_length
    # Apply Hann window
    windowed = [signal[start + n] * hann[n] for n in range(n_fft)]

    # DFT of windowed frame (only positive frequencies)
    best_k = 0
    best_mag = 0.0
    for k in range(half_bins):
        re = 0.0
        im = 0.0
        for n in range(n_fft):
            angle = 2 * math.pi * k * n / n_fft
            re += windowed[n] * math.cos(angle)
            im -= windowed[n] * math.sin(angle)
        mag = math.sqrt(re * re + im * im)
        if mag > best_mag:
            best_mag = mag
            best_k = k

    peak_freq = best_k * sample_rate / n_fft
    time_ms = round((start + n_fft / 2) / sample_rate * 1000, 1)
    rows.append([str(frame_idx), f"{time_ms}", f"{peak_freq:.0f}"])

js.window.py_table_data = json.dumps({
    "headers": ["Frame", "Centre Time (ms)", "Peak Frequency (Hz)"],
    "rows": rows
})

print(f"Chirp: {f_start} Hz -> {f_end} Hz over {duration*1000:.0f} ms")
print(f"Window: {n_fft} samples, Hop: {hop_length} samples")
print(f"Frames: {n_frames}, Freq bins: {half_bins}")
print(f"Peak frequency rises with each frame — the spectrogram reveals the chirp")

Observa cómo la frecuencia pico sube constantemente a través de las tramas, trazando el barrido del chirp de 200 Hz hacia 3000 Hz. Una forma de onda cruda simplemente parecería una línea ondulada que se hace ligeramente más rápida — el espectrograma hace explícita la estructura frecuencial. Por eso los espectrogramas (y sus variantes en escala mel, que vienen a continuación) son la representación de entrada estándar para modelos de voz y audio.

La escala Mel: Oír como un humano

El espectrograma lineal trata todas las frecuencias por igual: la diferencia entre 100 Hz y 200 Hz obtiene el mismo número de bins que la diferencia entre 7,900 Hz y 8,000 Hz. Pero la audición humana no funciona así. El salto de 100 Hz a 200 Hz — una octava — suena como un cambio de tono dramático (piensa en la nota más baja de un bajo versus una octava arriba). El salto de 5,000 Hz a 5,100 Hz es apenas perceptible. Nuestros oídos tienen una resolución en frecuencia aproximadamente logarítmica: somos muy sensibles a las diferencias en frecuencias bajas e incrementalmente menos precisos en frecuencias altas.

La escala mel (Stevens, Stanley & Volkmann, 1937) formaliza esta distorsión perceptual. Mapea la frecuencia lineal (en Hz) a una escala que se ajusta mejor a cómo los humanos perciben el tono:

$$m = 2595 \cdot \log_{10}\!\left(1 + \frac{f}{700}\right)$$

Aquí $f$ es la frecuencia en Hz y $m$ es el valor mel correspondiente. Veamos qué hace realmente esta fórmula. La clave es el argumento del logaritmo: $1 + f/700$. Cuando $f$ es pequeña relativa a 700 (digamos, $f = 100$ Hz), $f/700 \approx 0.14$, y $\log_{10}(1.14) \approx 0.057$, así que $m \approx 2595 \times 0.057 \approx 148$. Esto es aproximadamente proporcional a $f$ — el mapeo es casi lineal a frecuencias bajas. Pero cuando $f$ es grande (digamos, $f = 8{,}000$ Hz), $f/700 \approx 11.4$, y el $+1$ se vuelve despreciable, así que $\log_{10}(1 + f/700) \approx \log_{10}(f/700)$. Ahora el mapeo es logarítmico — duplicar $f$ añade una constante fija en el espacio mel. Este es exactamente el comportamiento que queremos: resolución fina donde la audición humana es sensible (frecuencias bajas) y resolución más gruesa donde no lo es (frecuencias altas).

En los límites: $f = 0$ da $m = 2595 \cdot \log_{10}(1) = 0$ mel. $f = 700$ Hz da $m = 2595 \cdot \log_{10}(2) \approx 781$ mel — este es aproximadamente el punto de transición entre los regímenes lineal y logarítmico. $f = 8{,}000$ Hz (el límite de Nyquist a 16 kHz de muestreo) da $m = 2595 \cdot \log_{10}(1 + 8000/700) \approx 2595 \cdot \log_{10}(12.43) \approx 2840$ mel.

La tabla a continuación calcula valores mel para varias frecuencias importantes en el habla y el audio, ilustrando la compresión de la escala en frecuencias altas.

import math, json, js

def hz_to_mel(f):
    return 2595 * math.log10(1 + f / 700)

freqs = [
    (0, "Silence / DC"),
    (85, "Low male voice fundamental"),
    (200, "Average male voice fundamental"),
    (300, "Average female voice fundamental"),
    (700, "Linear-to-log transition"),
    (1000, "Reference frequency (1 kHz)"),
    (2000, "Vowel second formant region"),
    (4000, "Consonant energy / sibilance"),
    (8000, "Nyquist limit at 16 kHz"),
    (16000, "Nyquist limit at 32 kHz"),
    (22050, "Nyquist limit at 44.1 kHz (CD)")
]

rows = []
for f, desc in freqs:
    m = hz_to_mel(f)
    rows.append([f"{f:,}", f"{m:.0f}", desc])

js.window.py_table_data = json.dumps({
    "headers": ["Frequency (Hz)", "Mel Value", "Description"],
    "rows": rows
})

print("Key insight: 0-1000 Hz spans ~1000 mel, but 1000-8000 Hz")
print("(7x the Hz range) spans only ~1840 mel.")
print("The mel scale compresses high frequencies aggressively.")

Para construir un espectrograma mel , no solo convertimos el eje de frecuencia — aplicamos un banco de filtros mel : un conjunto de filtros triangulares de paso de banda superpuestos cuyas frecuencias centrales están equiespaciadas en la escala mel. Debido a que los valores mel están comprimidos en frecuencias altas, esto coloca más filtros en el rango de baja frecuencia (donde la percepción humana es fina) y menos en el rango de alta frecuencia. Un banco de filtros típico para voz usa 80 canales mel (como en Whisper). Cada filtro suma la energía de varios bins de frecuencia adyacentes del espectrograma lineal, produciendo un número por filtro por trama temporal.

El paso final es tomar el logaritmo de cada energía del banco de filtros. Esto tiene dos motivaciones: la percepción humana de la intensidad sonora es aproximadamente logarítmica (un sonido debe duplicar su potencia para parecer notablemente más fuerte), y la compresión logarítmica reduce el rango dinámico, haciendo que los valores sean más fáciles de trabajar para las redes neuronales. El pipeline completo es: forma de onda $\rightarrow$ STFT $\rightarrow$ espectrograma de magnitud $\rightarrow$ banco de filtros mel $\rightarrow$ log $\rightarrow$ espectrograma log-mel . Esto es lo que Whisper, la mayoría de los sistemas de reconocimiento de voz y los modelos de texto a voz como F5-TTS usan como representación de entrada.

💡 La configuración específica de Whisper: 80 canales mel, ventana de 25 ms (400 muestras a 16 kHz), salto de 10 ms (160 muestras), resultando en 100 tramas por segundo. Un clip de audio de 30 segundos se convierte en una matriz de $80 \times 3000$ — 240,000 valores, comparado con 480,000 muestras crudas. Eso es una compresión de 2x, pero más importante aún es una compresión motivada perceptualmente que enfatiza las frecuencias que importan para entender el habla.

El gráfico a continuación muestra la curva de la escala mel, haciendo claramente visible la transición de casi-lineal (por debajo de ~700 Hz) a logarítmica (por encima de ~700 Hz).

import math, json, js

def hz_to_mel(f):
    return 2595 * math.log10(1 + f / 700)

# Generate curve from 0 to 22050 Hz
freqs = [i * 50 for i in range(441)]  # 0 to 22000 Hz
mels = [round(hz_to_mel(f), 1) for f in freqs]

# Mark key points
annotations = {
    85: "Low voice",
    300: "Female voice",
    700: "Linear/log transition",
    4000: "Consonants",
    8000: "Nyquist (16 kHz)",
}

plot_data = [
    {
        "title": "The Mel Scale: Hz to Mel Mapping",
        "x_label": "Frequency (Hz)",
        "y_label": "Mel Value",
        "x_data": freqs,
        "lines": [
            {"label": "Mel scale", "data": mels, "color": "#8b5cf6"}
        ]
    }
]
js.window.py_plot_data = json.dumps(plot_data)

print("Below ~700 Hz: nearly linear (m roughly proportional to f)")
print("Above ~700 Hz: logarithmic (doubling f adds ~300 mel)")

table_rows = []
for f, label in sorted(annotations.items()):
    table_rows.append([f"{f:,} Hz", f"{hz_to_mel(f):.0f} mel", label])

js.window.py_table_data = json.dumps({
    "headers": ["Frequency", "Mel Value", "Description"],
    "rows": table_rows
})

MFCCs: El estándar previo al deep learning

Antes de que el deep learning tomara el control, los sistemas de reconocimiento de voz necesitaban un vector de características compacto y de tamaño fijo para cada trama de audio. El espectrograma log-mel era un buen punto de partida — 80 números por trama, motivados perceptualmente — pero sus canales del banco de filtros están correlacionados (los filtros mel adyacentes se superponen y capturan energía similar), lo que causaba problemas para los modelos estadísticos de la época (particularmente los Modelos de Mezcla de Gaussianas, que asumían características independientes). Los Coeficientes Cepstrales en Frecuencia Mel (MFCCs) resuelven esto aplicando una transformación más para decorrelacionar las características.

El pipeline es: forma de onda $\rightarrow$ STFT $\rightarrow$ energías del banco de filtros mel $\rightarrow$ log $\rightarrow$ Transformada Discreta del Coseno (DCT) $\rightarrow$ quedarse con los primeros 12-13 coeficientes. La DCT es similar en espíritu a la DFT pero opera sobre datos de valor real y produce un conjunto de coeficientes de base coseno. La propiedad clave es que empaqueta la mayor parte de la energía de la señal en los primeros coeficientes. Los MFCCs de bajo orden capturan la forma espectral amplia (qué vocal se está pronunciando, el timbre general), mientras que los coeficientes de alto orden capturan detalles espectrales finos que generalmente son ruido para propósitos de reconocimiento de voz. Al descartar todo por encima del coeficiente 13, obtenemos un vector compacto de 13 dimensiones por trama que captura la envolvente espectral esencial.

Los MFCCs dominaron el reconocimiento de voz durante décadas a lo largo de la era GMM-HMM (sistemas de Modelo de Mezcla de Gaussianas–Modelo Oculto de Markov, aproximadamente 1990–2012). Todos los principales reconocedores de voz — desde CMU Sphinx hasta las primeras recetas de Kaldi — usaban MFCCs como característica de entrada principal. Siguen siendo relevantes hoy: HuBERT (Hsu et al., 2021) usa MFCCs en su paso inicial de agrupamiento k-means para generar pseudo-etiquetas antes de que el modelo haya aprendido representaciones propias.

Dicho esto, los sistemas modernos de deep learning han dejado atrás en gran medida los MFCCs. La razón es directa: una red neuronal con suficiente capacidad puede aprender mejores características de los datos que cualquier pipeline diseñado a mano. Los espectrogramas log-mel le dan a la red un punto de partida motivado perceptualmente mientras preservan más información que los MFCCs (80 canales vs 13 coeficientes), y algunas arquitecturas (wav2vec 2.0, HuBERT) se saltan el espectrograma por completo, aprendiendo directamente de las formas de onda crudas. La tendencia es clara: mover la frontera de extracción de características más profundamente dentro del modelo y dejar que el descenso por gradiente determine la mejor representación.

💡 Entender los MFCCs es útil por contexto histórico — los encontrarás en artículos, libros de texto y sistemas heredados en todas partes. Pero para trabajo nuevo, casi siempre comenzarás con espectrogramas log-mel o formas de onda crudas y dejarás que el modelo se encargue del resto.

El pipeline: Del aire a la entrada del modelo

Pongamos toda la cadena junta. Cuando hablas a un micrófono y un modelo de aprendizaje automático procesa tus palabras, esto es lo que sucede en cada etapa:

  • Ondas de presión del aire → un micrófono convierte las variaciones de presión en una señal de voltaje eléctrico.
  • Conversión analógico-digital → un ADC muestrea el voltaje $f_s$ veces por segundo (por ejemplo, 16,000), produciendo una secuencia de valores de amplitud.
  • STFT → la secuencia de muestras se corta en ventanas superpuestas de 25 ms (con salto de 10 ms), cada una multiplicada por una ventana de Hann, y cada una transformada mediante FFT en un espectro de frecuencia.
  • Espectrograma de magnitud → las salidas complejas de la FFT se convierten en magnitudes, produciendo una matriz 2D tiempo-frecuencia.
  • Banco de filtros mel → filtros triangulares espaciados en la escala mel comprimen el eje de frecuencia de $N/2 + 1$ bins lineales a 80 (o 128) canales mel.
  • Logaritmo → la compresión logarítmica reduce el rango dinámico y se alinea con la percepción humana de la intensidad sonora, produciendo el espectrograma log-mel final.

Esta es la ruta del espectrograma mel , y es la que usan la mayoría de los sistemas de producción actuales. Whisper, F5-TTS y muchos modelos de reconocimiento de emociones en el habla comienzan aquí. El procesamiento de señales es explícito, bien entendido y probado durante décadas. Pero hay una segunda ruta moderna:

La ruta de forma de onda cruda se salta la mayor parte del procesamiento de señales y alimenta la secuencia de muestras crudas directamente a un codificador aprendido. Modelos como wav2vec 2.0 (Baevski et al., 2020) y HuBERT (Hsu et al., 2021) usan un codificador de características convolucional que toma formas de onda crudas a 16 kHz y aprende a extraer las características que la tarea requiera. Los codecs de audio neuronales como EnCodec (D\'{e}fossez et al., 2022) también operan sobre formas de onda crudas, comprimiéndolas en tokens discretos. La ventaja es que el modelo no está limitado por las suposiciones incorporadas en los bancos de filtros mel — puede descubrir características que los humanos no pensarían en diseñar.

Ambas rutas sirven en última instancia al mismo objetivo: convertir una señal cruda de alta dimensionalidad y altamente redundante en una representación compacta que un transformer u otro modelo de secuencia pueda procesar eficientemente. El resto de este track cubre lo que sucede después de esa transformación: cómo modelos como Whisper codifican el habla para reconocimiento (artículo 2), cómo los modelos autosupervisados aprenden representaciones de audio sin etiquetas (artículo 3), cómo los codecs neuronales discretizan el audio en tokens (artículo 4), cómo funciona la síntesis de voz (artículo 5) y cómo los modelos multimodales combinan audio con texto y visión (artículos 6–7).

Quiz

Pon a prueba tu comprensión de los fundamentos del procesamiento de señales de audio.

A una tasa de muestreo de 16 kHz, ¿cuál es la frecuencia más alta que puede representarse fielmente?

¿Qué representa la magnitud $|X[k]|$ de un bin de la DFT?

¿Por qué la escala mel usa un mapeo logarítmico por encima de ~700 Hz?

¿Por qué los sistemas modernos de deep learning han dejado atrás los MFCCs en favor de espectrogramas log-mel o formas de onda crudas?