Procesamiento de señales de audio con Python (Parte 3): análisis espectral y FFT

Bienvenidos una vez más a vuestro blog sobre programación en lenguaje Python, en el que hoy os traemos la tercera parte de nuestro serie de artículos sobre procesamiento de señales de audio con Python, que versará sobre el análisis espectral y FFT. En los artículos anteriores exploramos el audio digital desde el dominio del tiempo, generando señales, experimentando con diferentes sample rates y observando cómo el aliasing puede alterar una señal si no respetamos el teorema de Nyquist–Shannon. En esta tercera parte nos trasladamos al dominio de la frecuencia, que nos permite ver de qué está hecha realmente una señal y no solo cómo se comporta en el tiempo. Este enfoque es fundamental en ingeniería de sonido, síntesis musical y análisis de audio profesional, ya que muchas características importantes no se perciben directamente con la forma de onda.

Como ya hemos señalado en alguna ocasión, hasta ahora hemos ido trabajando con señales simples (con una sola frecuencia y amplitud). No obstante, observamos como en la naturaleza lo normal es que los sonidos estén compuestos por múltiples señales con distintas frecuencias y amplitudes (de hecho, las señales puras, son excepción). Es por ello, que en este punto, se hace recomendable ver como podemos generar una señal compuesta, en Python mediante la simple adición de las diferentes subseñales que la componen. Veámoslo con un ejemplo em Python:

Como se ve, generamos aquí una señal con una frecuencia de muestreo de 44100 Hz (valor que teniendo en cuenta el teorema de Nyquist, garantiza la correcta representación de las distintas subseñales), una duración de 2 segundos, compuesta, as su vez, por 3 subseñales de 500hz, 1500hz y 3000hz, con amplitudes de 0.6, 0.4 y 0.3 respectivamente. Tras la creación de la línea de tiempo, procedemos a utilizar un for para concatenar las 3 subseñales con sus correspondientes frecuencias y amplitudes. Esto generará una señal compuesta, cuya gráfica correspondiente a los primeros 2000 samples se ve de esta manera:

Si lo deseamos, también podemos crear el correspondiente archivo sonoro en formato .WAV para oírlo teniendo la debida precaución al reproducirlo si usamos auriculares:

Hasta aquí hemos generado nuestra señal y mostrado graficamente en el dominio del tiempo, ahora es el momento de pasar al dominio de la frecuencia. Para ello utilizamos la Transformada de Fourier, la cual, nos permite descomponer una señal compleja en las frecuencias que la componen. Conceptualmente, lo que hacemos es responder a la pregunta de que frecuencias están presentes en la señal y con que intensidad.

Para ello, el primer paso es aplicar la FFT (Fast Fourier Transform) a nuestra señal temporal. Esta operación matemática toma el vector de muestras de audio y lo transforma en otro vector complejo, donde cada elemento representa una frecuencia concreta. El resultado no es directamente interpretable, porque contiene información tanto de magnitud como de fase:

A continuación, se calcula el eje de frecuencias asociado a la FFT usando la frecuencia de muestreo (sr). Esto es crucial, ya que permite mapear cada bin de la transformada a una frecuencia real en hercios. Sin este paso, el espectro no tendría una escala física correcta:

Después, normalmente nos quedamos solo con la parte positiva del espectro, ya que para señales reales la FFT es simétrica y la mitad negativa no aporta información nueva. Sobre esta parte positiva calculamos el valor absoluto de la FFT, obteniendo así el espectro de magnitud, que indica cuánta energía hay en cada frecuencia:

Finalmente, representemos el espectro en una gráfica, donde el eje horizontal corresponde a la frecuencia y el eje vertical a la magnitud. En el caso de una señal compuesta, aparecen picos claros exactamente en las frecuencias de las ondas senoidales originales, lo que confirma visualmente cómo la señal compleja se puede entender como la suma de componentes más simples:

OUTPUT:

No obstante, viendo la gráfica resultante, podemos observar como las magnitudes observadas en la gráfica no coinciden con las amplitudes originales (0.6, 0.4 y 0.3), sino que muestran valores mucho mayores. Esto puede resultar confuso, ya que intuitivamente esperaríamos que el pico espectral reflejara directamente la amplitud real de cada componente.

La razón es que np.fft.fft() no normaliza el resultado. Es decir, que la transformada acumula la contribución de todas las muestras (N), por lo que el valor del pico resulta proporcional a AN/2A \cdot N / 2. Es decir, la magnitud depende tanto de la amplitud real como del número total de muestras analizadas. Por eso, al aumentar la duración de la señal (y por tanto N), los picos crecen aunque la amplitud física no haya cambiado.

La solución consiste en normalizar correctamente el espectro dividiendo por el número total de muestras y multiplicando por 2 cuando trabajamos solo con la mitad positiva del espectro. En términos prácticos, basta con calcular la magnitud como (2/N) * np.abs(fft[mask]). De este modo, los picos del espectro recuperan las amplitudes reales de las senoides originales, y el análisis espectral pasa a representar fielmente la energía de cada componente de la señal.

Hecha esta modificación, veremos como al graficar, las magnitudes mostradas coincidirán con las amplitudes originales:

CONCLUSIÓN:

En esta tercera parte aprendimos a trasladar el análisis de las señales del dominio del tiempo al dominio de la frecuencia usando la FFT, identificando claramente los componentes de cada señal y comprendiendo cómo Nyquist delimita el rango seguro. En la siguiente lección nos adentraremos en ventanas y espectrogramas, herramientas que permiten estudiar cómo cambia el contenido frecuencial de una señal a lo largo del tiempo, un paso fundamental para analizar audio real, música o voz en movimiento.

Saludos.