Síntesis Musical por Ordenador (I)

Avelino Herrera Morales

blog - segunda parte

Introducción

La síntesis de sonidos mediante dispositivos electrónicos comenzó su andadura a principios de la década de 1870 cuando un hombre llamado Elisha Gray inventó una máquina que emitía sonidos a través de un oscilador accionado por un teclado, se trataba del Musical Telegraph. Tras el Musical Telegraph vino el Telharmonium del abogado e inventor Thaddeus Cahill cuyo primer prototipo fue terminado en 1906: pesaba 200 toneladas y ocupaba 2 pisos de una casa. El 1955 sale al mercado el RCA Synthesizer, el primer sintetizador tal y como hoy los conocemos del mercado, creado por Harry Olson y Herbert Belar.

Los sintetizadores actuales son máquinas muy potentes y equipadas con algún procesador para control y generado de sonidos. A lo largo de esta serie trataremos el tema de la síntesis (o la creación desde cero) así como del procesado del sonido mediante el ordenador.

No hablaremos de APIs ni de detalles de programación de las tarjetas de sonido, sólo nos centraremos en los métodos y los códigos fuentes estarán todos en ANSI C. La parte de interface con la tarjeta de sonido (o el dispositivo de salida elegido) así como el sistema operativo utilizado será elección del programador. No obstante, los códigos fuente que el autor pone a disposición del lector están hechos para la API de sonido OSS de Unix/Linux.

Señales digitales

Antes de introducirnos de lleno en los aspectos propios de la síntesis vamos a ver algunas cosas relativas al mundo digital.

Formalmente una señal senoidal la definimos como f(t) = cos(w · (t + p)). Siendo t el tiempo en segundos, w la frecuencia angular en radianes por segundo (w = 2 · pi · frecuencia_en_hercios) y p la fase de la señal en segundos. Al movernos dentro del mundo digital hay que introducir varios conceptos nuevos: la resolución en bits de la señal a generar y la frecuencia de muestreo.

La resolución en bits de la señal no afecta a la ecuación anterior y tan solo atañe al valor devuelto por la función cos(). En ANSI C tanto sin() como cos() devuelven un tipo double que, dependiendo de la máquina y el sistema operativo será un número real en coma flotante de 32, 64 ó más bits, pero, en cualquiera de los casos, acotado entre los valores -1.0 y +1.0. Por lo tanto, independientemente de la máquina siempre podremos hacer un escalado de la señal si sabemos los valores límites de ésta. Supongamos que estamos en un PC con una tarjeta de sonido sencillita de 16 bits. Normalmente este tipo de tarjetas maneja señales con signo (esto es, el valor de la señal de sonido está en el rango -32768 y +32767) aunque también las hay que pueden manipular señales sin signo (entre 0 y 65535).

Si tenemos en una variable v una muestra de la señal (un valor real entre -1.0 y +1.0) sencillamente haremos un v_out = (int) rint(v * 32767) y ya habremos convertido la señal en un valor entero entre -32767 y +32767, con lo que es apta para ser enviada al dispositivo de sonido (la tarjeta, en nuestro caso) y ser escuchada.

Veamos ahora la frecuencia de muestreo. Al introducirnos en el mundo digital pasamos de hablar de instantes de tiempo a hablar de muestras. La variable tiempo es una variable continua mientras que la muestra viene determinada por una variable discreta entera no negativa (0, 1, 2, 3, 4 ...). Para sustituir la variable tiempo (t) en la ecuación inicial simplemente hacemos t = n / fm. Siendo n el índice de la muestra (0, 1, 2, 3 ...) y fm la frecuencia de muestreo para el dispositivo, en muestras por segundo (la calidad CD corresponde a 44100 muestras por segundo). Así, cuando n = 0 tenemos t = 0 segundos mientras que si n = fm tenemos t = 1 segundo (en un ejemplo con calidad CD cuando n = 44100 habrá pasado un segundo de tiempo). La ecuación inicial nos queda ahora así: f(n) = cos(w * ((n / fm) + p)), donde w es la frecuencia en radianes por segundo de la señal a generar, n es la muestra a generar (0, 1, 2 ...), fm es la frecuencia de muestreo en muestras por segundo y p es la fase de la señal en segundos.

En el listado 1 podemos ver un código sencillo que genera un tono audible de 440 Hz con una frecuencia de muestreo de 44.1 KHz y muestras de 16 bits con signo (calidad CD).

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define  F_SENIAL  440    /* un LA en la 4ª octava del piano */
#define  F_MUEST   44100  /* frecuencia de muetreo: 44.1 KHz */
#define  TIEMPO    2      /* segundos de reproducción */

#define  MUESTRAS_TOTALES  (TIEMPO * F_MUEST)

signed short int salida[MUESTRAS_TOTALES];

int main(void) {
    int i;
    float v;

    for (i = 0; i < MUESTRAS_TOTALES; i++) {
        v = cos(2 * M_PI * ((float)i / F_MUEST) * F_SENIAL);
        salida[i] = (signed short int) rint(v * 32767);
    }
    return 0;
}
Listado 1 - Código sencillo para generar un coseno audible.

Hemos de tener en cuenta el teorema de Nyquist que dice que para una frecuencia de muetreo m sólo podemos llegar a reproducir señales cuya frecuencia máxima es de m/2. Para una calidad CD de 44100 Hz la frecuencia máxima que se puede reproducir o grabar sobre una señal digital será de 22050 Hz que corresponde con el umbral de audición humano. Si intentamos superar esa barrera se producirán fenómenos de aliasing, que provocan un reflejo hacia el grave de las frecuencias por encima de m/2, un fenómeno, por lo general indeseable.

Otro aspecto vital a la hora de abordar la síntesis es el de obtener la frecuencia en hercios (Hz) de una señal a partir de su nota musical. Según está internacionalmente dispuesto, la frecuencia de una nota LA en la cuarta octava de un piano debe ser de 440 Hz. Si tenemos en cuenta, además, que por cada octava que subimos doblamos la frecuencia y por cada octava que bajamos dividimos la frecuencia a la mitad podemos calcular fácilmente la frecuencia en hercios que debe tener una nota cualquiera con sólo conocer su nombre (DO, FA#, etc) y su octava.

Enumeremos las notas de 0 a 11 (DO = 0, DO# = 1, RE = 2, RE# = 3, MI = 4, FA = 5, FA# = 6, SOL = 7, SOL# = 8, LA = 9, LA# = 10, SI = 11). La ecuación final nos queda así: frec = FREC_LA_4 * pow(2.0, ((nota - 9) / 12.0) + (octava - 4)). Donde FREQ_LA_4 = 440 Hz y nota indica la nota (0 = DO, 6 = FA#, etc). En el exponente del 2 tenemos dos sumandos, en el primero obtenemos un número correspondiente a la nota que queremos tocar entre -9/12 (DO) y 2/12 (SI) (se le resta 9 debido a que la potencia se multiplica luego por la frecuencia fundamental del LA, si la potencia se multiplicase por la frecuencia fundamental del DO, no haría falta realizar la resta). El segundo sumando se utiliza para doblar o dividir a la mitad las veces necesarias la frecuencia en función de la octava elegida.

Síntesis tradicional

Dentro de la síntesis tradicional englobaremos el conjunto de técnicas basadas en los sintetizadores más vendidos y utilizados en el mercado. Existen varios métodos básicos que se utilizan para la síntesis tradicional: la síntesis aditiva, AM, sustractiva, FM, etc. Son métodos de síntesis sencillos y que generan, por lo general, timbres muy básicos.

Síntesis aditiva

Según Fourier, cualquier señal puede ser descrita perfectamente mediante una suma de cosenos de diferente fase, amplitud y frecuencia. Para sintetizar un sonido mediante síntesis aditiva se realiza una suma algebráica de varias señales senoidales (cosenos o senos) de diferente frecuencia, fase y amplitud. Normalmente, un sonido musical se caracteriza por una frecuencia fundamental y una serie de frecuencias denominadas armónicos. La frecuencia de cada armónico se define como un múltiplo de la frecuencia fundamental. De esta manera obtenemos todas las frecuencias de los cosenos que componen una señal con sólo dar su frecuencia fundamental (el tono que se percibe). En la realidad, a la hora de percibir el sonido de un intrumento musical entran en juego otros factores, como las resonancias entre cuerdas (en el caso de los instrumentos de cuerda) y otros sonidos no musicales (por ejemplo, en un piano, el ruido que producen los martillos al golpear las cuerdas o la presencia de frecuencias no múltiplas de la frecuencia fundamental).

Si, por ejemplo, pensamos en una señal compuesta por los armónicos 1 (la frecuencia fundamental), 4, 7 y 12, con una frecuencia de 440 Hz (un LA en la 4ª octava del piano) obtenemos las frecuencias: 440, 4 * 440 = 1760, 7 * 440 = 3080 y 12 * 440 = 5280 hercios; y asi con cualquier frecuencia que deseemos. Si analizamos el espectro de esta señal veremos 4 picos, cada uno de ellos en una frecuencia de las generadas.

En el listado 2 podemos apreciar un código que genera un timbre mediante síntesis aditiva. El número de componentes frecuenciales está limitado a 3 aunque puede ser variado fácilmente. Que el lector intente realizar diferentes pruebas cambiando la fase, la amplitud y la frecuencia de los armónicos.

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define  F_MUEST        44100  /* frecuencia de muestreo: 44.1 KHz */
#define  F_SENIAL       440    /* un LA en la 4ª octava del piano */
#define  TIEMPO         2      /* 2 segundos de reproducción */
#define  NUM_ARMONICOS  3      /* número de componentes de la señal a generar */

#define  MUESTRAS_TOTALES  (TIEMPO * F_MUEST)

int armonico[NUM_ARMONICOS] = {1, 2, 4};
float amplitud[NUM_ARMONICOS] = {1.0, 0.4, 0.3};
float fase[NUM_ARMONICOS] = {0.0, 0.0, 0.1};

signed short int salida[MUESTRAS_TOTALES];

int main(void) {
    int i, a;
    float v;

    for (i = 0; i < MUESTRAS_TOTALES; i++) {
        v = 0;
        /* cálculo de la síntesis aditiva */
        for (a = 0; a < NUM_ARMONICOS; a++) {
            v += amplitud[a] * cos(2 * M_PI * ((float)i / F_MUEST + fase[a]) *
                       (F_SENIAL * armonico[a]));
        }
        /* limitamos la señal por si hay desbordamiento */
        if (v > 1.0)
            v = 1.0;
        else if (v < -1.0)
            v = -1.0;
        /* escribimos en la salida */
        salida[i] = (signed short int) rint(v * 32767);
    }
    return 0;
}
Listado 2 - Ejemplo de síntesis aditiva.

Este tipo de síntesis se utiliza más a menudo junto con métodos de análisis para emular sonidos previamente grabados. Para el análisis de las componentes frecuenciales de una señal se utiliza la transformada de Fourier; dicha transformada permite calcular a partir de una señal cualquiera la magnitud y fase de cada una de las componentes frecuenciales que posee (permite obtener una imagen de la señal en el dominio de la frecuencia, lo que se denomina "ecuación de análisis"). Se trata de una transformada integral reversible ya que permite mediante otra transformación pasar del dominio de la frecuencia al dominio del tiempo (restaurar la señal original) mediante lo que se denomina antitransformada o transformada inversa (ecuación de síntesis). Es esta antitransformada la que realiza una síntesis aditiva como se puede apreciar en la figura 1.


Figura 1 - Transformada de Fourier. La ecuación de síntesis, como se ve, es una suma infinita de señales senoidales de diferente amplitud, frecuencia y fase.

Hablaremos en un capítulo aparte de la transformada de Fourier y de su utilidad en el procesado de sonidos.

La modulación

Como se puede ver, la síntesis aditiva es muy sencilla y permite controlar de forma exhaustiva el espectro del sonido que queremos generar. Aun con estas cualidades, no deja de ser un sistema bastante tedioso para sintetizar sonidos y más si tenemos en cuenta que para sonidos cercanos a instrumentos reales o bastante más ricos, la cantidad de componentes frecuenciales se dispara (20 o más en algunos casos).

Es por esto que se han buscado métodos para generar espectros ricos sin necesidad de utilizar muchos osciladores. Las soluciones adoptadas pasaron por la utilización de osciladores no senoidales (onda triangular, onda de diente de sierra y ondas cuadradas, mayoritariamente; todas ellas fácilmente implementables en osciladores electrónicos), este método de por sí no deja de ser bastante estático al poseer una onda triangular, por ejemplo, un espectro bien definido, reconocible y fijo. Pero también pasaron por otras formas de generar sonidos más ricos: la modulación de parámetros de la onda.

La modulación consiste en cambiar algún parámetro de la ecuación de la onda en función de la amplitud de una segunda onda. Como vimos antes, los parámetros que definen una onda periódica (ya que hemos introducido el concepto de señales no senoidales, generalicemos también el de "onda senoidal" a "onda periódica") son la amplitud, la frecuencia y la fase. Es a partir de estos tres parámetros de donde se sacan los tres grandes métodos de modulación: la AM (Amplitude Modulation), la FM (Frequency Modulation) y la PM (Phase Modulation).

Síntesis AM y Anillo

En la síntesis AM la amplitud de una onda modula la amplitud de otra. En términos generales y para ondas senoidales: f(t) = (A + M * cos(wm * t)) * cos(wc * t). Siendo M la amplitud de la moduladora, wm la frecuencia angular de la onda moduladora (m), wc la frecuencia angular de la onda portadora (c = carrier) y A la componente de continua aplicada a la señal moduladora. Cuando A=0 se produce una multiplicación de ambas señales (es lo que se denomina modulación en anillo) mientras que si A>0 se produce una modulación de amplitud normal de la señal del oscilador wc.

Como se puede apreciar, cuando A=0, en ausencia de moduladora, f(t)=0, mientras que si A>0 en ausencia de moduladora f(t) = A * cos(wc * t), por lo general 0<=A<=1. Con A=1 y M=0 tenemos como salida sólo la señal portadora. En el espectro resultante de una modulación en AM entre dos ondas senoidales, aparte de la frecuencia wc, aparecen dos frecuencias adicionales: wc - wm y wc + wm. Son las denominadas en terminología radioeléctrica, bandas laterales. Ver la figura 2.


Figura 2 - Modulación AM y su espectro. Dos bandas laterales alrededor de la frecuencia de la portadora.

Los timbres generados mediante modulación AM son bastante más ricos que los generados mediante síntesis aditiva. El espectro descrito anteriormente corresponde a ondas senoidales tanto para la moduladora como para la portadora, en el caso de que utilicemos ondas de diferente tipo el espectro se hará también más rico en armónicos. En el listado 3 tenemos un código que implementa una modulación AM entre dos ondas senoidales.

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define  F_MUEST        44100  /* frecuencia de muestreo: 44.1 KHz */
#define  F_SENIAL       440    /* un LA en la 4ª octava del piano */
#define  A              0.5    /* componente de continua de la moduladora */
#define  M              0.5    /* amplitud de la moduladora */
#define  MULT_MOD       4      /* factor multiplicador de la onda moduladora */
#define  TIEMPO         2      /* 2 segundos de reproducción */

#define  MUESTRAS_TOTALES  (TIEMPO * F_MUEST)

signed short int salida[MUESTRAS_TOTALES];

int main(void) {
    int i;
    float v;

    for (i = 0; i < MUESTRAS_TOTALES; i++) {
        /* cálculo de la síntesis AM */
        v = A + M * cos(2 * M_PI * ((float)i / F_MUEST) * F_SENIAL * MULT_MOD);
        v *= cos(2 * M_PI * ((float)i / F_MUEST) * F_SENIAL);
        /* limitamos la señal por si hay desbordamiento */
        if (v > 1.0)
            v = 1.0;
        else if (v < -1.0)
            v = -1.0;
        /* escribimos en la salida */
        salida[i] = (signed short int) rint(v * 32767);
    }
    return 0;
}
Listado 3 - Ejemplo de síntesis AM con ondas senoidales.

La forma habitual de implementación de modulación AM en síntesis musical es la de utilizar para la onda portadora la frecuencia base del tono y aplicar un factor de multiplicación a la frecuencia de la señal moduladora. Así, si tenemos un factor multiplicador de 3 con una onda portadora de 440 Hz, la onda moduladora será de 1320 Hz.

Se invita al lector a que realice diferente pruebas con el código anterior cambiando la amplitud de la moduladora y la componente de continua.

Síntesis FM

Los otros dos médotos de modulación entre ondas los hemos agrupado ya que en realidad ambos tipos de modulación son muy parecidos desde el punto de vista matemático y los espectros resultantes son prácticamente iguales. En la modulación FM lo que se hace es modular la frecuencia de la onda portadora en función de la amplitud de la onda moduladora. Tenemos por tanto, para señales senoidales: f(t) = A * cos(wc * t + (I * cos(wm * t))). Donde, como en el caso de la AM; wc es la frecuencia angular de la portadora, A es la amplitud de la portadora, I es la amplitud de la moduladora y wm es la frecuencia angular de la onda moduladora. Vemos que si A=1 e I=0 tenemos que f(t) = cos(wc * t). A I también se le denomina "índice de modulación" y viene expresado por I = Δf/fm, siendo Δf el incremento de frecuencia sobre la portadora y fm la frecuencia de la moduladora (al ser w=2·PI·f, I también lo podemos expresar como Δw/wm). El significado de Δf es sencillo: si queremos que una portadora de 300Hz varíe entre 290Hz y 310Hz hacemos Δf=10. En el caso de la síntesis FM se generan parciales por encima y por debajo de la frecuencia de la moduladira fc: fi = fc ± (i * fm) con i = 0..(I+2) aproximadamente. En la realidad se generan infinitas bandas laterales alrededor de fc pero estas bandas se van atenuando a medida que aumenta la i y a partir de i=I+2 la amplitud de éstas es despreciable. La atenuación en estas bandas viene dada por las llamadas funciones de Bessel. Ver la figura 3. El espectro de una onda resultante de una modulación FM es un espectro muchísimo más rico que el que se genera mediante AM ya que, en teoría obtenemos un espectro infinito. La síntesis FM no cobró vida hasta la aparición de los osciladores digitales.


Figura 3 - Modulación FM y su espectro. Tenemos infinitas bandas laterales alrededor de la frecuencia de la portadora.

En el listado 4 tenemos un ejemplo de modulación en FM con ondas senoidales. En terminología FM un oscilador se denomina operador y existe la posibilidad de que un oscilador se module a sí mismo (lo que se denomina retroalimentación o feedback). Esta última posibilidad inexistente en modulación AM y que permite, mediante pocos operadores, conseguir timbres sumamente complejos (por ejemplo, el sintetizador DX7 de Yamaha consiguió una de las mejores emulaciones de piano real de su tiempo mediante síntesis FM con 6 operadores senoidales).

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define  F_MUEST     44100  /* frecuencia de muestreo: 44.1 KHz */
#define  F_SENIAL    440    /* un LA en la 4ª octava del piano */
#define  A           1      /* amplitud de la portadora */
#define  DELTA_F     4000   /* desviación de la frecuencia de la portadora */
#define  MULT_MOD    3      /* factor multiplicador de la onda moduladora */
#define  TIEMPO      2      /* 2 segundos de reproducción */

/* índice de modulación */
#define  I  ((float)DELTA_F / (F_SENIAL * MULT_MOD))

#define  MUESTRAS_TOTALES  (TIEMPO * F_MUEST)


signed short int salida[MUESTRAS_TOTALES];

int main(void) {
    int i;
    float v, mod;

    for (i = 0; i < MUESTRAS_TOTALES; i++) {
        /* cálculo de la síntesis FM */
        mod = I * cos(2 * M_PI * ((float)i / F_MUEST) * F_SENIAL * MULT_MOD);
        v = A * cos(2 * M_PI * F_SENIAL * ((float)i / F_MUEST) + mod);
        /* limitamos la señal por si hay desbordamiento */
        if (v > 1.0)
            v = 1.0;
        else if (v < -1.0)
            v = -1.0;
        /* escribimos en la salida */
        salida[i] = (signed short int) rint(v * 32767);
    }
    return 0;
}
Listado 4 - Ejemplo de síntesis FM con ondas senoidales.

Tanto en modulación AM como FM no tenemos por qué limitarnos a hablar de una señal portadora y una señal moduladora. Se pueden realizar modulaciones en cascada: [oscilador 1] -AM-> [oscilador 2] -FM-> [oscilador 3], obteniendo la señal de salida del oscilador 3. O en configuraciones más extrañas: [oscilador 1] -FM-> [oscilador 2] -AM-> [oscilador 3] <-FM- [oscilador 4], obteniendo la señal de salida del oscilador 3.

Osciladores y ondas no senoidales

Hasta ahora en los listados hemos usado siempre la función cos() de la librería matemática para obtener ondas senoidales, sin embargo, esta función utiliza cálculos muy complejos para obtener el valor del coseno de un ángulo y, generalmente, es una función que consume mucho tiempo de procesador. Existen varias soluciones que nos permiten implementar osciladores más rápidos y versatiles para cuestiones musicales teniendo en cuenta determinadas restricciones. Por un lado tenemos los resonadores. Un resonador en terminología electrónica es un circuito amplificador sintonizado a una determinada frecuencia y que, bajo ciertas circunstancias, puede provocar una autooscilación y convertirse en un generador de señal senoidal cuya frecuencia es la frecuencia de sintonía del amplificador. Un resonador digital se puede obtener a partir de la discretización de las ecuaciones diferenciales de un resonador electrónico o directamente partiendo de diagramas de ceros y polos en el plano z. Este método de implementar osciladores digitales tiene una gran pega: sólo puede generar ondas senoidales. Además se trata de un método cuya explicación caería más dentro del capítulo de filtros.

Nosotros utilizaremos un método más sencillo y que requiere muy poco tiempo de procesador: las tablas de ondas. Para implementar un oscilador digital mediante tablas de ondas lo que se hace es precalcular la onda que queramos reproducir durante un periodo (por ejemplo si queremos reproducir una onda senoidal obtendremos las muestras que van de sin(0) a sin(2 * PI)) y almacenar los valores en una tabla de tamaño finito (512, 1024 ó 2048 muestras, por ejemplo). Cuando queramos hacer funcionar nuestro oscilador lo que haremos será recorrer la tabla precalculada comenzando por la posición 0 y dando saltos de tamaño inc = (TAM_TABLA / F_MUEST) * frec. Siendo TAM_TABLA el tamaño en muestras de nuestra tabla precalculada, F_MUEST la frecuencia de muestreo del dispositivo de salida y frec la frecuencia en hercios a la que queramos que salga la señal. Como se puede observar la variable inc debe ser de tipo real ya que los saltos de tamaño entero sólo corresponderán a múltiplos y submúltiplos de la frecuencia de muestreo.

De esta manera, mediante una multiplicación y una división inicial tendremos calculado el salto entre muestras (inc) y para leer las muestras tan sólo tendremos que mantener una variable real que indique "por donde vamos" en la tabla (nosotros la llamaremos t) e ir incrementando esta variable de la forma t += inc cada vez que leamos una muestra. En el listado 5 tenemos un código que precalcula una tabla con un coseno y posteriormente la utiliza para generar un tono a 440 Hz.

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define  F_MUEST        44100  /* frecuencia de muestreo: 44.1 KHz */
#define  F_SENIAL       440    /* un LA en la 4ª octava del piano */
#define  TAM_TABLA      2048   /* tamaño de la tabla precalculada de onda */
#define  TIEMPO         2      /* 2 segundos de reproducción */

#define  MUESTRAS_TOTALES  (TIEMPO * F_MUEST)

float tabla_coseno[TAM_TABLA];
signed short int salida[MUESTRAS_TOTALES];

int main(void) {
    int i;
    float v, inc, t;

    /* precalculamos la tabla con el coseno */
    for (i = 0; i < TAM_TABLA; i++)
        tabla_coseno[i] = cos(2 * M_PI * (float)i / TAM_TABLA);
    /* calculamos el incremento para la tabla */
    inc = (float)(TAM_TABLA * F_SENIAL) / F_MUEST;
    t = 0;
    for (i = 0; i < MUESTRAS_TOTALES; i++) {
        /* leemos de la tabla precalculada */
        v = tabla_coseno[(int) rint(t)];
        /* limitamos la señal por si hay desbordamiento */
        if (v > 1.0)
            v = 1.0;
        else if (v < -1.0)
            v = -1.0;
        /* escribimos en la salida */
        salida[i] = (signed short int) rint(v * 32767);
        /* nos preparamos para leer la siguiente muestra de la tabla */
        t += inc;
        /* controlamos el desbordamiento del índice t */
        if (t >= TAM_TABLA)
            t -= TAM_TABLA;
    }
    return 0;
}
Listado 5 - Ejemplo de síntesis mediante tabla de ondas.

Hay varias cosas importantes a señalar. Por un lado tenemos el hecho de que hay que vigilar cuando nuestra variable que indexa la tabla (t) llegue al final de ésta, en cuyo caso tendrá que volver a comenzar por el principio de la tabla y en la posición adecuada, esto es: si, por ejemplo tenemos una tabla de tamaño 1024 y la variable t, tras un incremento, pasa a valer 1025, su valor debe ser puesto a t=1; mientras que si, tras un incremento, pasa a valer 1024.468, su valor debe ser puesto a t = 0.468; y así sucesivamente (de ahí el if con la resta al final del bucle). Otra cuestión a tener en cuenta es el hecho de que rara vez la variable índice t es un valor entero. En nuestro caso hemos hecho una interpolación por el vecino más próximo (si, por ejemplo, t=0.8 lo que se lee es el contenido del índice 1 de la tabla, si t=10.2 lo que se lee es el contenido del índice 10 de la tabla, y así sucesivamente). Para la experimentación es un método perfectamente válido aunque para obtener resultados más precisos y de calidad sonora profesional se hace necesario realizar interpolación. Interpolar es calcular, a partir de un número finito de muestras, el valor de muestras que no se encuentran dentro del conjunto inicial. En nuestro caso si, por ejemplo tabla[0]=1 y tabla[1]=2, tabla[0.5] será un valor entre 1 y 2 que, aunque no lo tenemos en la tabla, si que lo podemos estimar.

Podemos interpolar de diferentes formas: de forma lineal, cuadrática, cúbica, mediante splines, etc. La interpolación lineal es, sin duda la más utilizada al ser la interpolación que menos tiempo de procesador requiere. Lo único que se hace es imaginar una línea recta entre la muestra conocida inmendiatamente anterior y la muestra conocida inmediatamente posterior y obtener el valor de la muestra interpolada sobre dicha recta (ver la figura 4). En el caso anterior, si tabla[0]=1 y tabla[1]=2, tabla[0.5] lo podemos estimar como 1.5. Por tanto, para interpolar linealmente: valor = tabla[floor(t)] + (t - floor(t)) * (tabla[ceil(t)] - tabla[floor(t)]). Esta ecuación para cuando floor(t) ≠ ceil(t), es decir, para cuando t no sea un valor entero. Cuando t sea un valor entero simplemente valor = tabla[t]. floor() y ceil() son dos funciones de la librería matemática que devuelven el entero más próximo por defecto y el entero más próximo por exceso, respectivamente, del parámetro.


Figura 4 - Interpolación lineal de muestras.

Figura 5 - Tipos de onda más utilizados en música electrónica.

Las tablas de ondas precalculadas permiten implementar osciladores con cualquier tipo de onda periódica (senoidal, diente de sierra, triangular, cuadrada, etc) ya que ésta va almacenada en una tabla en memoria. La calidad final del oscilador dependerá de la resolución de la tabla (a mayor cantidad de muestra utilizadas mejor) y del tipo de interpolación que se utilice.

Envolventes

Una envolvente es un señal que da forma a otra señal a lo largo del tiempo. Normalmente las envolventes se utilizan para modular la amplitud de los sonidos. Cuando oimos una nota en un piano, por ejemplo, al principio el sonido comienza con un volumen alto y a medida que pasa el tiempo, el sonido se va apagando hasta extinguirse totalmente, es su envolvente en amplitud la que define este comportamiento. En el campo de la síntesis de sonidos, para generar este tipo de efectos se utilizan los generadores de envolvente.

En nuestro caso, una envolvente, será una función e(n) que devuelve un valor normalizado (entre 0 y 1). Existen varios tipos de envolventes: AD (Attack Decay), ADSR (Attack Decay Sustain Release, la más utilizada) y las envolventes multipunto (figura 6).


Figura 6 - Tipos de envolvente.

Las envolventes son mayormente utilizadas para modular la amplitud de las señales, como vimos antes; así, si queremos modular un tono simple haremos: out[n] = e(n) * cos(2 · PI · F_SENIAL · ((float)n / F_MUEST)). Siendo e(n) la función de la envolvente y el resto de valores, los usuales. Las envolventes se definen a escalas de tiempo mayores que las señales audibles, así, normalmente, una envolvente dura entre 1 décima de segundo y varios segundos o incluso hasta el infinito si queremos un sonido sostenido (por ejemplo, para modular en amplitud una señal que simule la sección de cuerdas de una orquesta).

En el listado 6 tenemos un código que aplica una envolvente de tipo AD a un coseno de 440 Hz. Se invita al lector a que aplique esta envolvente, por ejemplo, al índice de modulación en la FM del apartado anterior o a la amplitud de la moduladora en AM. Las posibilidades son casi infinitas.

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define  F_SENIAL  440    /* un LA en la 4ª octava del piano */
#define  F_MUEST   44100  /* frecuencia de muestreo: 44.1 KHz */
#define  TIEMPO    2      /* segundos de reproducción */

/* valores de tiempo para la envolvente AD */
#define  TIEMPO_ATAQUE    0.01
#define  TIEMPO_CAIDA     1
#define  MUESTRAS_ATAQUE  ((int) rint(TIEMPO_ATAQUE * F_MUEST))
#define  MUESTRAS_CAIDA   ((int) rint(TIEMPO_CAIDA * F_MUEST))

#define  MUESTRAS_TOTALES  (TIEMPO * F_MUEST)

signed short int salida[MUESTRAS_TOTALES];


float e(int i) {
    if (i < MUESTRAS_ATAQUE)
        return ((float)i / MUESTRAS_ATAQUE);
    else if (i < (MUESTRAS_ATAQUE + MUESTRAS_CAIDA))
        return 1.0 - ((float)(i - MUESTRAS_ATAQUE) / MUESTRAS_CAIDA);
    else
        return 0;
}

int main(void) {
    int i;
    float v;

    for (i = 0; i < MUESTRAS_TOTALES; i++) {
        v = e(i) * cos(2 * M_PI * ((float)i / F_MUEST) * F_SENIAL);
        salida[i] = (signed short int) rint(v * 32767);
    }
    return 0;
}
Listado 6 - Ejemplo de envolvente AD aplicada a una onda senoidal.
LFOs

Los LFOs u osciladores de baja frecuencia (Low Frequency Oscillators) son generadores de señal que no difieren de los osciladores o generadores de señal normales, con la salvedad de que generan frecuencias por debajo de los 15Hz (por debajo del humbral de audición). Los LFOs se utilizan, al igual que las envolventes, para modular cualquier parámetro de una señal.

Si, por ejemplo, en el listado 4 (síntesis FM), hacemos que la onda moduladora oscile a una frecuencia por debajo de 15Hz, obtendremos un efecto de vibrato y mediante el índice de modulación conseguiremos variar la profundidad del vibrato. En el listado 7 podemos ver como queda esta modificación.

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define  F_MUEST     44100  /* frecuencia de muestreo: 44.1 KHz */
#define  F_SENIAL    440    /* un LA en la 4ª octava del piano */
#define  A           1      /* amplitud de la portadora */
#define  DELTA_F     7      /* profundidad del LFO */
#define  F_LFO       4      /* frecuencia del LFO */
#define  TIEMPO      2      /* 2 segundos de reproducción */

/* índice de modulación */
#define  I  ((float)DELTA_F / F_LFO)

#define  MUESTRAS_TOTALES  (TIEMPO * F_MUEST)

signed short int salida[MUESTRAS_TOTALES];

int main(void) {
    int i;
    float v, mod;

    for (i = 0; i < MUESTRAS_TOTALES; i++) {
        /* cálculo del vibrato */
        mod = I * cos(2 * M_PI * ((float)i / F_MUEST) * F_LFO);
        v = A * cos(2 * M_PI * F_SENIAL * ((float)i / F_MUEST) + mod);
        /* limitamos la señal por si hay desbordamiento */
        if (v > 1.0)
            v = 1.0;
        else if (v < -1.0)
            v = -1.0;
        /* escribimos en la salida */
        salida[i] = (signed short int) rint(v * 32767);
    }
    return 0;
}
Listado 7 - Ejemplo de vibrato.

Los LFOs se pueden aplicar, como las envolventes, al índice de modulación de una FM, a la amplitud de una moduladora en AM, a la amplitud de cualquier señal o a cualquier parámetro que nos apetezca variar en el tiempo de forma periódica (por ejemplo, nada nos impide aplicar un LFO a la amplitud del tercer armónico en la síntesis aditiva, o incluso aplicar un LFO distinto a cada armónico con profundidad y frecuencia diferente para cada LFO).

Filtros y síntesis sustractiva

La síntesis sustractiva es el concepto inverso de la síntesis aditiva: partiendo de una señal rica en armónicos aplicamos filtros hasta obtener el espectro de señal deseado. Un filtro es un dispositivo que amplifica ciertas frecuencias y atenúa otras. Existen diferentes tipos de filtros en función de las frecuencias que atenúan y amplifican. Tenemos los filtros paso-bajo, que dejan pasar las señales por debajo de una determinada frecuencia llamada frecuencia de corte; los filtros paso-alto, que dejan pasar las señales por encima de una frecuencia de corte; los fitros paso-banda, que sólo dejan pasar las señales comprendidas entre dos frecuencias determinadas; y los filtros elimina-banda, que atenúan o eliminan las señales comprendidas entre dos frecuencias determinadas.

Los filtros paso-bajo y paso-alto vienen determinados por la frecuencia de corte y la pendiente de filtrado y los filtros paso-banda y elimina-banda vienen determinados por la frecuencia central de la banda, la anchura de la banda y la pendiente de filtrado a ambos lados de la banda (ver figura 7).


Figura 7 - Tipos de filtro.

Existen otro tipo de filtros como son los ecualizadores que permiten dar la forma que deseemos a la respuesta en frecuencia (atenuar determinadas frecuencias y amplificar otras que nosotros queramos). El funcionamiento general de un filtro se puede apreciar en la figura 8.


Figura 8 - Funcionamiento de un filtro.

Para implementar un filtro digital podemos hacerlo de dos formas: o bien discretizando un filtro analógico conocido a partir de sus ecuaciones o realizando un diseño directo mediante diagramas de ceros y polos sobre el plano z.

Utilizaremos por ahora el primer método y partiremos de un circuito muy conocido: el filtro de estado variable. En la figura 9 Podemos ver un circuito típico de un filtro de estado variable. Un filtro de estado variable (state variable filter) es un circuito de segundo orden formado por dos integradores y un sumador. En el circuito de la figura cada integrador se monta entorno a un amplificador operacional de trasconductancia (CA3080) y a un amplificador operacional normal de ganancia unidad a modo de separador (741). El sumador se implementa entorno a un amplificador operacional normal (741). Este circuito es muy versátil y posee tres salidas diferentes: una salida paso-alto en el pin de salida del amplificador sumador con una pendiente de filtrado de 12 dB/octava, una salida paso-banda en el pin de salida del primer integrador con una pendiente de 6 dB/octava y una salida paso-bajo en el pin de salida del segundo integrador con una pendiente de filtrado de 12 dB/octava. Todo en un mismo circuito.


Figura 9 - Esquema eléctrico de un filtro de estado variable.

La frecuencia de corte del filtro se define mediante la corriente en el pin BIAS de los amplificadores de trasconductancia. En el caso que nos ocupa, los dos amplificadores de trasconductancia están montados como integradores, por tanto: salida = Constante * BIAS * Integral(entrada), esto para cada integrador; siendo BIAS la corriente del pin BIAS del operacional y Constante un valor fijo que viene determinado por el condensador y las resistencias montadas alrededor del CA3080. La resonancia del filtro (pico de ganancia en la frecuencia de corte) viene determinada por el potenciómetro de realimentación que hay entre el primer integrador y el sumador y que regula la cantidad de señal que regresa al sumador desde la salida del integrador.

Si discretizamos el circuito tal y como está, realizando la sustitución: dv/dt = (v[n] - v[n-1]) / T en la ecuación diferencial resultante del circuito, obtenemos: salida_paso_alto = -entrada - (salida_paso_banda * resonancia) - salida_paso_bajo; salida_paso_banda += salida_paso_alto * corte; salida_paso_bajo += salida_paso_banda * corte. Siendo corte = Constante · BIAS · Periodo_muestreo (siendo Constante y BIAS los mismos valores de antes). Como se puede ver, en el primer trozo de código lo que hacemos es simplemente realizar la suma de señales que llegan al amplificador sumador y en la segunda y la tercera línea realizamos la integración de la señal de cada entrada (al discretizar, las integrales pasan a convertirse en sumatorios). Esto se calcula para cada muestra. La resonancia estará entre 0 (máxima resonancia = auto-oscilación) y 1 (resonancia mínima), la frecuencia de corte estará entre 0 (continua) y 1 (F_MUEST / 2 = frecuencia máxima).

En el listado 8 tenemos la implementación de un filtro de estado variable. La señal a filtrar es una señal totalmente aleatoria (ruido blanco) para que se pueda apreciar mejor el efecto. Sin embargo, el efecto se puede ver igualmente si se utiliza cualquier señal rica en armónicos (como, por ejemplo, la señal resultante de una síntesis FM o una onda de tipo pulso, cuadrada o de diente de sierra).

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define  F_MUEST        44100  /* frecuencia de muestreo: 44.1 KHz */
#define  TIEMPO         2      /* 2 segundos de reproducción */
#define  CORTE          0.1    /* frecuencia de corte del SVF */
#define  RESONANCIA     0.1    /* resonancia del SVF */

#define  MUESTRAS_TOTALES  (TIEMPO * F_MUEST)

signed short int salida[MUESTRAS_TOTALES];

int main(void) {
    int i;
    float u, v;
    float paso_alto = 0, paso_banda = 0, paso_bajo = 0;

    for (i = 0; i < MUESTRAS_TOTALES; i++) {
        /* generamos un ruido blanco (muestras aleatorias) */
        u = (2 * ((float)rand() / RAND_MAX)) - 1.0;
        /* realizamos el filtrado */
        paso_alto = u - (paso_banda * RESONANCIA) - paso_bajo;
        paso_banda += paso_alto * CORTE;
        paso_bajo += paso_banda * CORTE;
        /* seleccionamos como salida el paso-bajo */
        v = paso_bajo;
        /* limitamos la señal por si hay desbordamiento */
        if (v > 1.0)
            v = 1.0;
        else if (v < -1.0)
            v = -1.0;
        /* escribimos en la salida */
        salida[i] = (signed short int) rint(v * 32767);
    }
    return 0;
}
Listado 8 - Implementación de un filtro de estado variable (pendiente 12dB/octava).

Conclusiones

En esta primera entrega hemos visto cómo se sintetizan sonidos sencillos desde C, aplicando métodos de síntesis muy extendidos como la aditiva, AM, FM y la sustractiva. Hemos visto, también las envolventes y los LFOs y cómo aplicarlos para modificar parámetros de una señal sonora.

En la próxima entrega hablaremos un poco más de los filtros digitales e introduciremos nuevos métodos de síntesis tales como las tablas de ondas y haremos una pequeña introducción al modelado físico de instrumentos reales.

Creative Commons License
Esta obra está bajo una licencia de Creative Commons.