Síntesis Musical por Ordenador (y III)

Avelino Herrera Morales

segunda parte - blog

Presentaremos, en esta última entrega, la síntesis, mediante guías de ondas, de otros instrumentos reales, hablaremos de la secuenciación y daremos una vuelta por los métodos de procesado de señal más utilizados en música.


Figura 1 - Cuerda ideal con terminaciones rígidas ideales.

En el artículo anterior vimos de forma bastante profunda la síntesis de instrumentos de cuerda mediante guías de ondas. Ciertamente la cuerda pulsada es uno de los tópicos más sencillos de implementar mediante guías de ondas. En esta última entrega veremos por encima cómo se sintetizan los sonidos de otros tipos de instrumentos mediante guías de ondas.

Instrumentos de viento

Para sintetizar instrumentos de viento, los registros de desplazamiento que vimos en el caso de la cuerda pulsada, simbolizan ahora el recorrido de la onda a través de todo el tubo del instrumento. Los extremos de los registros de desplazamiento simbolizan los extremos del instrumento (por la boca se aplica presión de aire y la campana sería el otro extremo).

En la figura 2 tenemos de forma esquemática lo que sería un instrumento de viento madera. ¿Por qué madera? Los instrumentos de viento madera tiene como característica principal que el diámetro a lo largo de todo su recorrido permanece constante (o casi), salvo, quizás, la campana, en la que el diámetro normalmente aumenta. El hecho de que el diámetro permanezca casi constante a lo largo de todo el instrumento permite simplificar enormemente la implementación.


Figura 2 - Esquema de viento madera.

En la figura 3 tenemos ya una implementación bastante concreta de estas guías de ondas. En la campana tenemos que lo que sale por el registro de desplazamiento A se refleja mediante un filtro sobre la entrada del registro de desplazamiento B (al igual que pasaba con la cuerda del artículo anterior) y, además, la salida de audio se ha modelado cogiendo la salida del registro de desplazamiento A y pasándola por un filtro paso alto. En la boca se concentra la mayor parte del trabajo: las bocas son funciones no lineales que caracterizan en mayor medida el timbre final del instrumento. En este caso tenemos que a la salida del registro de desplazamiento B se le aplica una función del tipo: y = p - ((p - x - offset) · h(p - x - offset)). Siendo x la salida del registro de desplazamiento B, y la entrada del registro de desplazamiento A, p un valor proporcional a la presión aplicada a la boca, offset un valor constante y h() una función no lineal y que, en el caso de la figura es, además, no derivable.


Figura 3 - Implementación de viento madera (© 2000 Julius O. Smith III).

Diferentes valores de offset y diferentes funciones h() nos darán diferentes timbres.

Cambios en el diámetro

Los registros de desplazamiento vistos hasta ahora son perfectos cuando el diámetro del instrumento permanece constante a lo largo de todo el recorrido de la onda. Sin embargo a veces se hace necesario modelar los cambios en el diámetro del instrumento (por ejemplo, en el caso de viento metal o si, simplemente, queremos perfeccionar el sonido de un instrumento de viento madera modelando los cambios de diámetro de la campana, por ejemplo).

Un cambio en el diámetro de un tubo provoca, para las ondas que viajan por él, un cambio en su impedancia. Al igual que sucede cuando hablamos de impedancia en circuitos de corriente alterna; los cambios en la impedancia de una onda en un punto determinado de su recorrido provocan que parte de la energía de la onda rebote y parte se trasmita. En la figura 4 tenemos representado lo que se denomina una scattering function o función de dispersión, que no es más que una función con dos entradas y dos salidas y que, aplicada a dos registros de desplazamiento permite modelar cambios en la impedancia de la onda. Para hacernos una idea de esta función baste imaginarnos que tenemos dos registros de desplazamiento A y B en los que se desplazan las muestras en sentidos opuestos (en A de izquierda a derecha y en B de derecha a izquierda). Una función de dispersión se introduce partiendo en algún punto del recorrido, a ambos registros. La función de scattering toma entonces dos entradas (las salidas de A y de B) y genera dos salidas (la entradas en A y en B).


Figura 4 - Función de dispersión (© 2000 Julius O. Smith III).

Si analizamos esta función de dispersión tenemos: ya = xa · (1 + k) - (xb · k) y, por otro lado, yb = xb · (1 - k) + (xa · k) (siendo xa el dato que sale de la sección de la izquierda de A, xb el dato que sale de la sección de la derecha de B, ya el dato que entra en la sección de la derecha de A e yb el dato que entra en la sección de la izquierda de B). Como vemos se trata de una función del tipo (ya, yb) = f(xa, xb). Para aplicar esta ecuación al cambio de diámetro en un instrumento de viento el valor de la constante k estará en función de la diferencia de diámetro entre dos secciones adyacentes del tubo. k = (R2 - R1) / (R2 + R1), siendo R1 la impedancia de la onda en la parte izquierda y R2 la impedancia de la onda en la sección de la derecha. Para una sección de tubo cualquiera podemos aproximar su impedancia como la inversa de su radio (R = 1 / radio). Esto es sólo una aproximación burda, para afianzar conocimientos, el lector interesado puede acudir a la web de Julius O. Smith III.

Si en la cuerda pulsada, para que se inicie el sonido, teníamos que inicializar las guías de onda simulando la pulsación de una cuerda real; en el caso de los instrumentos de viento, inicializaremos las guías con una onda de presión.

Cuerda frotada

La implementación de instrumentos de cuerda frotada sigue un esquema parecido al de la cuerda pulsada. Al igual que en la cuerda pulsada, lo que tenemos es una cuerda con dos apoyos en los extremos y con un arco, que entra en contacto con la cuerda en un punto de su recorrido y provoca una perturbación. Normalmente uno de los extremos de la cuerda se simula con un apoyo ideal (que refleja toda la energía) y el otro extremo se simula mediante un apoyo no ideal (refleja parte de la energía, ya sea con una atenuación en todo el rango de frecuencias o una atenuación mayor en las altas frecuencias).

Hasta aquí todo igual que en el caso de la cuerda pulsada. La perturbación que provoca el arco se simula, cómo no, con una función de dispersión que, normalmente se hace no lineal y cuyos parámetros vienen determinados por la velocidad y la presión que genera el arco del instrumento. En la figura 5 podemos ver un esquema de lo que sería un instrumento de cuerda frotada y su implementación mediante guías de onda.


Figura 5 - Esquema de una cuerda frotada (© 2000 Julius O. Smith III).

En el fichero fuentes3.tar.gz adjunto hay dos códigos en C que simulan un instrumento de viento madera y un instrumento de cuerda frotada, respectivamente.

Vamos a hacer música: La secuenciación

Tras leer esta serie de artículos al lector puede quedarle la impresión de que lo único que hemos hecho ha sido sintetizar sonidos sueltos pero no hemos llegado a generar música. La secuenciación consiste en "tocar" los instrumentos y ordenar las notas en el tiempo para obtener música.

Independientemente del tipo de síntesis que utilicemos deberemos tener como mínimo tres funciones: una que inicialice los datos de intrumento (reservar memoria, hacer algunos cálculos previos, cargar muestras de fichero, etc) y que reciba, como mínimo, un parámetro: la nota que queremos tocar de ese instrumento. La segunda función que deberemos tener será una función que sólo pueda ser llamada después de la anterior y que, en cada llamada, nos devuelva un buffer con N muestras de sonido de ese instrumento. Finalmente una tercera función de "liberación" que, al ser llamada, cambia el estado del instrumento y hace que en las sucesivas llamadas a la segunda función se nos devuelvan las últimas muestras del instrumento (a modo de liberación de cada nota).

Por ejemplo, si lo que queremos es implementar una cuerda pulsada mediante guías de ondas lo que haremos será crear una función que reserve memoria, calcule la longitud de las guías, las inicialice, e inicialice las envolventes y los filtros de los apoyos. Otra función que, en cada llamada me devuelva N muestras de ese intrumento. La primera llamada a esta segunda función devuelve las muestras 0 ... N-1, la segunda llamada devuelve las muestras N ... 2N-1, etc. Y otra función que, por ejemplo, active una rampa de amplitud, de pendiente negativa, para las sucesivas muestras generadas y hasta la extinción total del sonido.

El quid a la hora de secuenciar sonidos sintéticos es definir una "frecuencia de evento", muy inferior a la frecuencia de muestreo. Si tenemos una frecuencia de 10 eventos por segundo y arrancamos la secuenciación en t=0s, sólo se podrán disparar notas o aplicar cambios en el sonido en los instantes t=0.1s, 0.2s, 0.3s, 0.4s, 0.5s, etc. Si estamos secuenciando un fichero MIDI, por ejemplo, deberemos, previamente cuantizar los eventos de nota hasta una resolución de décima de segundo mientras que si estamos leyendo disparos de notas en tiempo real y nos llega una solicitud de disparo tenemos que esperar hasta el siguiente "hueco" para disparar esa nota y que se oiga.

Si estamos en un sistema de tiempo real (por ejemplo, vamos reproduciendo notas a medida que nos llegan de un un puerto MIDI) el retraso máximo que experimenta un evento será de 1/fe segundos, siendo fe la frecuencia de evento que tenemos definida en nuestro sistema. Es lo que se denomina "latencia máxima". Interesa, por tanto frecuencias de evento lo más altas posible, ya que a mayor frecuencia, mayor es la sensación de tiempo real que se produce. El tiempo real absoluto se produce cuando fe coincide con la frecuencia de muestreo del sistema (fm) (esto es, en cada iteración se generan N = 1 muestras).

Como se puede adivinar, un software que lea del puerto MIDI y reproduzca los eventos mediante síntesis de sonido, deberá poseer un bucle principal que itere con una frecuencia fe y que, en cada iteración, calcule N = fm/fe muestras (siendo fm la frecuencia de muestreo).

Otro asunto a tener en cuenta cuando se realiza síntesis por software es el de los canales a utilizar. Definimos los canales como el número máximo de fuentes de audio que el software puede mezclar y enviar a un tiempo al dispositivo de salida.

Podemos resumir el proceso en pseudocódigo como se indica en el listado 1. En este ejemplo de pseudocódigo, nótese que existe un proceso de liberación de la nota (este proceso de liberación normalmente vendrá implementado, para cada instrumento, por una función que haga que, en subsecuentes llamadas a la función de generación de muestras se calcule un fundido, por ejemplo, hasta la extinción de la nota).

Inicializamos el dispositivo de entrada (puerto MIDI, fichero, etc).
Inicializamos el dispositivo de salida (tarjeta de sonido, fichero WAV, etc).
Inicializamos el buffer de mezcla con fm/fe muestras.
Inicializamos los M canales a vacío.
Bucle:
    Borramos el buffer de mezcla.
    Leemos datos del dispositivo de entrada.
    Si hay nuevos eventos en la entrada:
        Si es un evento de disparo de nota (NOTE ON en MIDI):
            Buscamos un canal libre.
            Si encontramos un canal libre:
                Lo marcamos como ocupado.
                Llamamos a la función de inicialización del instrumento.
                Generamos las fm/fe primeras muestras y las añadimos al buffer de mezcla.
            Fin si.
        En otro caso, si es un evento de fin de nota (NOTE OFF en MIDI):
            Buscamos el canal asociado a esta nota.
            Iniciamos el proceso de liberación de la nota.
        Fin si.
    Fin si.
    Para todos los canales ocupados hacemos un bucle:
        Llamamos a la función de generación de muestras del instrumento (N = fm/fe).
        Añadimos las muestras generadas al buffer de mezcla.
        Si hay algún canal para el que haya terminado el proceso de liberació de nota:
            Marcamos el canal como libre.
    Enviamos el buffer de mezcla al dispositivo de salida.
Fin bucle.
Listado 1 - Pseudocódigo para secuenciación en tiempo real.

En este caso la frecuencia de evento se fuerza en el momento de enviar el buffer de mezcla al dispositivo de salida. Como se envían fm/fe muestras cada vez y el dispositivo de salida no puede procesar sino fm muestras por segundo la llamada a la función write() bloqueará el bucle hasta que el dispositivo de salida pueda admitir más muestras para reproducir. Esto provoca que el bucle tenga una frecuencia de fe ciclos por segundo.

De esto se desprende que todo el procesamiento y el cálculo de muestras en cada iteración deberá hacerse en un tiempo menor a 1/fe segundos. A mayor capacidad de cálculo más canales, menor latencia y mayor complejidad de cálculos podremos conseguir en cada iteración. Si el tiempo que tardamos en realizar los cálculos de una iteración supera los 1/fe segundos, en el dispositivo de salida se producirán rupturas en el audio ya que habrá momentos en los que el dispositivo de salida haya reproducido ya todas las N muestras anteriores pero nuestra aplicación aún no haya podido calcular las N siguientes.

En la síntesis de tiempo diferido esto no ocurre. En tiempo diferido tenemos, en la entrada, definidos los instantes de tiempo en los que ocurrirán los eventos y para calcular la onda de salida no existe el peligro de que no tengamos las muestras calculadas para cuando se necesiten, no hay, a priori, restricciones de tiempo (para síntesis en tiempo diferido el pseudocódigo que podemos usar es igual, con la única diferencia que tanto el dispositivo de entrada como el de salida son ficheros, .MID el primero y .WAV el segundo, por ejemplo).

Entre los códigos fuente adjuntos se incluye un "midiread.c" que lee eventos MIDI en tiempo real de /dev/midi e interpreta los eventos de NOTE ON y NOTE OFF sobre una estructura en memoria compartida; y un "readxi.c" que acepta por parámetros un fichero de muestras formato FastTracker (extensión XI) e interpreta notas en función de la misma estructura en memoria compartida. Si ejecutamos midiread en tty1 y luego en tty2 ejecutamos "readxi piano.xi" el ordenador interpretará los eventos que entren por el puerto MIDI en tiempo real y reproducirá las notas del instrumento XI.

Posicionamiento espacial

El posicionamiento espacial consiste en utilizar, al menos, dos canales de audio independientes para guardar el sonido y que, mediante diferencias de altura y fase entre ambos canales, permiten simular el posicionamiento de la fuente de audio en torno al oyente.

Para un sistema de síntesis estéreo lo lógico es que el buffer de mezcla sea estéreo (2·N muestras) y que las funciones de generación de muestras generen 2·N muestras (N muestras para el canal izquierdo y N muestras para el canal derecho, en cada iteración). En un sistema estéreo, además del resto de parámetros que le tengamos asociado a un instrumento, debemos también pensar en el control "panorámico". Mediante un control panorámico es posible dar mayor consistencia a los sonidos y mayor realismo (por ejemplo, podemos hacer que con un piano, las notas más graves suenen mas "tirando" pa la izquierda y que las notas más agundas suenen más para la derecha). Podemos también aplicar envolventes al panorámico o LFOs (una buena forma de darle algo más de cuerpo a un sonido con trémolo es combinarlo con un panorámico que oscile a la misma o a diference velocidad, que vaya de un lado a otro del campo de audición).

La direccionabilidad de un sonido es directamente proporcional a su frecuencia. Las frecuencias graves son menos direccionales que las agudas. Lo normal en un sistema de síntesis musical es que a los sonidos graves no se les aplique panorámico (se queden en el centro) mientras que con los sonidos medios y agudos se juega más con la posición.

Existen otros tópicos más avanzados entorno al posicionamiento espacial del sonido, como el posicionamiento 3D o el surround que se escapan del alcance de este artículo.

Procesamiento de audio

El procesamiento son las modificaciones que se hacen sobre el sonido una vez éste ha sido generado. No se trata, pués, de técnicas de síntesis. El sonido, como cualquier otra señal, puede ser procesado desde el punto de vista temporal o desde el punto de vista frecuencial. Desde un punto de vista temporal lo que tendremos serán una serie de muestras equiespaciadas en el tiempo mientras que desde un punto de vista frecuencial lo que tendremos será un conjunto de frecuencias audibles que, mediante mezcla, forman el sonido final. Existe otra forma de procesaciento que entra dentro del procesamiento en el dominio del tiempo, pero que se suele nombrar aparte: el procesamiento del rango dinámico. Vayamos por partes.

Procesamiento en el dominio de la frecuencia

El procesamiento en el dominio de la frecuencia consiste básicamente en filtrar. La literatura especializada prefiere utilizar el término "ecualizar" ya que, en efecto, este tipo de procesamiento se basa en atenuar y/o amplificar ciertas frecuencias hasta que un sonido consiga las características que queremos en cuanto a tonalidad.

Normalmente se recurre al uso de filtros paso banda. Existen básicamente 2 tipos de ecualizadores: los gráficos y los paramétricos. En el primero tenemos una serie de filtros paso-banda situados en frecuencias fijas y logarítmicamente espaciadas (al ser la percepción humana del sonido no lineal) y para los que podemos modificar su ganancia de forma independiente. Se llaman ecualizadores gráficos porque, si utilizamos potenciómetros de recorrido lineal, el aspecto final de todo el conjunto nos permite hacernos una idea de la respuesta en frecuencia del ecualizador en toda la banda de audio.

Los ecualizadores paramétricos son algo más avanzados y sólo están disponibles en sistemas de gama alta. En este caso tenemos un conjunto finito de bandas y, para cada banda, podemos ajustar, no solo su ganancia (como en los ecualizadores gráficos), sino también la frecuencia central de cada una por separado.

Para implementar filtros paso-banda podríamos utilizar el filtro de estado variable analizado con anterioridad en esta serie y tomar como salida la salida del segundo integrador (el del medio). Para un filtro paso-banda de segundo orden como el que incluye el filtro de estado variable, tenemos una pendiente por cada "lado" de 6 dB/octava. Para obtener mayores pendientes habría que implementar varios filtros en serie. Todo dependerá de lo selectivos que queramos que sean los filtros.

Otra alternativa al uso de filtros implementados en digital es el uso de la FFT o transformada rápida de Fourier. Como vimos en la primera entrega, cuando calculamos la FFT sobre un array de muestras, el algoritmo calcula la amplitud y fase de cada una de las componentes frecuenciales de la señal sobre el mismo array de entrada. Supongamos que tenemos una señal de entrada que queremos ecualizar sobre un array w. Mediante FFT(w) obtendremos, en w, las componentes frecuenciales del trozo de señal inicial y si tenemos un array con la respuesta frecuencial "deseada", construido por nosotros, llamado m, podemos hacer w := w · m (elemento a elemento). De esta forma, haciendo la transformada inversa IFFT(w) obtenemos en w la señal original "ecualizada" según la respuesta en frecuencia m. Esto que acabamos de describir es el proceso denominado convolución y permite implementar filtros bastante complejos con un consumo de recursos de la máquina bastante bajo.

Nótese que tanto w como m son arrays de números complejos. Inicialmente w almacenará, en su parte real, la señal de entrada y su parte imaginaria estará toda a 0. En el caso de m también utilizaremos fase cero: factor de amplificación en la parte real, y la parte imaginaria a 0. Hay que tener en cuenta también que, para la FFT, el tamaño de los arrays debe ser potencia de 2 y que si, por ejemplo, queremos aplicar la FFT a 256 muestras utilizaremos un array w de 256 muestras, pero si queremos hacer la FFT a 257 muestras ya tendremos que utilizar un array w de 512 muestras (las posiciones sobrantes se ponen a 0).

Se incluye, entre los fuentes, "fft.c"; una implementación en C de la transformada rápida de Fourier (tanto la directa como la inversa).

Procesamiento en el dominio del tiempo

El procesamiento en el dominio del tiempo se basa casi exclusivamente en aplicar y mezclar determinados retardos en la señal para producir efectos. Si, por ejemplo, mezclamos a la señal original una versión retardada de la misma señal en 0.5 segundos y atenuada en un 50% respecto de la original, obtendremos un eco simple de una sola repetición.

Un elemento básico en el procesamiento en el dominio temporal es la "línea de retardo". Una línea de retardo es un sistema con una única entrada y una única salida, y lo único que hace es retardar la señal de entrada un número determinado de segundos (o de muestras, como queramos mirarlo). En el listado 2 tenemos un ejemplo de implementación bastante eficiente en C de una línea de retardo.

float shift_register[SIZE];   /* Declaración */
int t_in = 0;                 /* Índice de entrada */
int t_out = 1;                /* Índice de salida */

/* "x" es la muestra que entra en el registro, "y" es la muestra que sale */
shift_register[t_in] := x;
y := shift_register[t_out];            /* Todo esto en cada iteración */
t_in = (t_in + 1) % SIZE;
t_out = (t_out + 1) % SIZE;
Listado 2 - Implementación en C de un registro de desplazamiento (para unidades de chorus, delay, reverb, etc).
Delay

Este tipo de procesamiento es uno de los más simples y se basa en el uso de una línea de retardo de duración constante y realimentada que provoca ecos equiespaciados en el tiempo. La señal de entrada entra en la línea de retardo y, a la salida de esta línea, la señal retardada se mezcla con la señal de entrada para generar la salida del delay. Normalmente se añade un control de realimentación que permite producir más de un retardo. Supongamos que la función de la línea de retardo es y = R(x), siendo x la señal que le entra e y la señal que sale. Para un delay tendremos dos ecuaciones: y = x + MEZCLA·R(z), z = x + REALIMENTACIÓN·R(z) siendo, en este caso x la entrada del delay e y la salida del delay (ver figura 6). Como se puede comprobar si MEZCLA=0 la salida del delay es igual que la entrada, sin eco, mientras que con MEZCLA=1 el primer eco tendrá la misma amplitud que la señal inicial. Si REALIMENTACIÓN=0 se produce un solo eco, mientras que con REALIMENTACIÓN=1 se produce una realimentación total (infinitos ecos). Una valor 0 < REALIMENTACIÓN < 1 provoca ecos equiespaciados que tardan en extinguirse un tiempo proporcional al valor de REALIMENTACIÓN.


Figura 6 - Delay o eco.
Reverberación

La reverberación es uno de los efectos más utilizados en la música actual. Consiste en el conjunto de múltiples ecos que apompañan a los sonidos cuando estos se emiten bajo determinadas circunstancias física. Por ejemplo, podríamos pensar en el conjunto de ecos que se forma en una sala de conciertos, o el conjunto de ecos que se generan en un cuarto de baño. La reverberación es un proceso natural que sufre el sonido y que debe ser añadido artificialmente a los sonidos sintetizados para que sean más agradables al oido humano.


Figura 7 - Reverberación: Ecos producidos y respuesta impulsional de un recito cerrado.

Existen múltiples modelos de reverberación, cada cual más complicado que los anteriores. Hablaremos del reverberador "JCRev" desarrollado por John Chowning del CCRMA (Center for Computer Research in Music and Acoustics, Universidad de Stanford) y basado en las ideas de Schroeder. Se trata de un reverberador con el que se obtienen unos resultados bastante aceptables y está compuesto por 3 filtros pasa-todo en serie, seguidos de una batería de 4 filtros peine con realimentación hacia delante en paralelo.


Figura 8 - Reverberador JCRev (John Chowning).

Un filtro peine con realimentación hacia delante (feedforward) se puede definir matemáticamente como: yt = Rm(x) + g·x. Siendo b = Rm(a) una línea de retardo de m muestras de longitud con entrada a y salida b, x la entrada del filtro, y la salida del filtro y g una constante. Un filtro paso-todo es algo más complejo y se define matemáticamente como: y = g·w + Rm(w) con w = x - g·Rm(w) (w es la señal de entrada de la línea de retardo y el resto de letras tiene el mismo significado que en la ecuación del filtro peine).

Sea y = Pm,g(x) la ecuación del filtro peine con línea de retardo de tamaño m, constante g, entrada x y salida y. Sea y = Tm,g(x) la ecuación de un filtro paso-todo con los mismos parámetros. Un reverberador JCRev lo podemos definir así: y = Pm4,g4(w) + Pm5,g5(w) + .... + Pmi,gi(w) con w = Tm3,g3(Tm2,g2(Tm1,g1(x))). Siendo m1...mi y g1...gi los parámetros de cada bloque, x la entrada de audio e y la salida de la señal reverberada. Normalmente con 4 filtros peine hay más que suficiente (i=7), el tamaño de las líneas de retardo debe ajustarse empíricamente sin que llegue a producir oscilaciones en la banda audible (>40 milisegundos) y las constantes de realimentación (g) deben también ajustarse hasta conseguir la cantidad e intensidad de ecos deseada. Afortunadamente John Chowning y su equipo ajustaron estos valores según sus gustos y la verdad es que el resultado es bastante sorprendente.

Para los filtros pasa-todo tenemos: m1=1051, g1=0.7, m2=337, g2=0.7, m3=113 y g3=0.7; mientras que para los filtros peine: m4=4799, g4=0.742, m5=4799, g5=0.733, m6=5399, g6=0.715, m7=5801 y g7=0.597.

Existen otra técnicas a la hora de implementar una reverberación, algunas de las más interesantes son convolucionar la señal original con varias versiones desfasadas de la misma o aplicar modelado físico a sistemas electromecánicos de reverberación. Entre estos últimos tenemos la "spring reverb", que utiliza un muelle por el que se hace circular la onda de presión, o la "plate reverb", algo más cara de implementar, y en la que, mediante transductores, se transmite la onda a una lámina de metal en la que se colocan algunos micrófonos en sitios estratégicos. Debido a las condiciones de contorno de una lámina metálica, parte de la energía de las ondas que llegan al borde de la lámina rebota hacia adentro, creando una especie de "recinto" (la lámina) donde queda rebotando la señal inyectada.

En el fichero fuentes3.tar.gz se incluye un fichero "rev.c" que implementa un reverberador JCRev. Este reverberador lee muestras de la entrada estándar y las envía a /dev/dsp. Una forma de probarlo es mediante el sintetizador de percusión que se incluye (dsynth.c). Si hacemos "./dsynth" a secas oiremos un ritmo tecno normal a través de /dev/dsp y si hacemos "./dsynth -" enviamos las muestras del ritmo a la salida estándar. Haciendo "./dsynth - | ./rev" oiremos el ritmo a través del reverberador JCRev.

Efectos de retardo corto: Chorus y flange

El chorus (o coro, en español) es un efecto que permite simular varias voces de un mismo instrumento aun teniendo una única fuente de audio de dicho instrumento. Es como si grabáramos nuestra voz cantando en un cassette y, poniéndolo luego a reproducir, nos pusiésemos a cantar exactamente lo mismo que está en la cinta mientras se oye. El resultado son dos voces de la misma persona (la grabada y la natural) pero que, al no estar perfectamente sincronizadas y al no estar perfectamente afinadas entre sí, siguen siendo reconocibles como dos voces diferentes.

La implementación digital de un chorus es muy sencilla. Si tenemos una línea de retardo como en el caso del delay, con y = R(x); lo que tenemos es: s = e + MEZCLA · R(e). Siendo e la entrada de audio, s la salida y MEZCLA un valor entre 0 y 1 que determina la mezcla de la señal original y la retardada. Hasta aquí lo que tenemos simplemente es una especia de delay sin relimentación. La diferencia estriba en la línea de retardo. En este caso, está gobernada por un LFO, mediante el cual variamos la longitud, en muestras, de la línea.


Figura 9 - Chorus o coro.

¿Qué conseguimos con este LFO que va variando el tamaño de la línea de retardo? Supongamos que dentro de la línea tenemos N muestras con la salida del LFO en reposo. Si un instante después, por acción del LFO la longitud de la línea debe disminuirse a N-k muestras, debemos "desechar" k muestras y/o interpolar las N existentes: Esto provoca que la onda almacenada en la línea de retardo sufra una disminución de longitud de onda y, por consiguiente, un aumento de tono.

Algo parecido sucedería si, en un momento dado, hay que aumentar la longitud de la línea de retardo. En este caso, si aumentamos la longitud en p muestras, debemos "inventarnos" estas p muestras ya sea por interpolación o por repetición de muestras vecinas en el tiempo. Esto provoca el efecto contrario: el tono percibido disminuye.

Son estos cambios de tono "dinámicos" los que provocan un efecto de chorus en la mezcla ya que permiten simular voces ligeramente desafinadas y retardadas en el tiempo respecto de la señal principal.

Para cada voz adicional que queramos meter en la señal final deberemos usar una línea de retardo de éstas; además, el tiempo de retardo medio de la línea será de 40 milisegundos en adelante aproximadamente.

Con retardos inferiores a 40 milisegundos, la mezcla de las señales original y retardada llega a generar componentes frecuenciales audibles; es lo que se conoce como flange. Este efecto fue muy popularizado en los años 60 y es el clásico efecto de "avión volando" que se oye en algunas grabaciones. Un flange es un chorus con las siguientes características: tamaño medio de la línea de retardo más corto que el chorus y una frecuencia del LFO por lo general más baja.

Procesamiento del rango dinámico

El rango dinámico de una señal es la diferencia entre la parte de menor amplitud de la onda y la de mayor amplitud. Existen básicamente dos procesos relacionados con el rango dinámico: la compresión y la expansión. Al contrario que ocurre con el procesamiento temporal, este tipo de procesamiento no requiere de elementos de memoria (líneas de retardo): Se actua de forma aislada para cada muestra.

Cuando procesamos el rango dinámico de una señal, en lo que nos fijamos es en su envolvente temporal. La envolvente de una señal la podemos definir como la señal que se formaría si uniésemos los puntos más altos de cada período de onda.

Compresión

Comprimir consiste en reducir el rango dinámico de una señal; esto es, reducir la diferencia de amplitud entre las zonas de máxima amplitud y las zonas de mínima amplitud en una señal. Se utiliza principalmente para "equilibrar" pistas de audio (por ejemplo, es muy adecuado para aplicar a la voz, ya que la distancia entre la boca y el micrófono varía, así como la intensidad emitida por una persona y la compresión permite oir a esa persona como si hubiese cantado a una distanci constante del micrófono y a un volumen de voz también prácticamente constante). Con la compresión se corre el peligro, por otro lado, de desnaturalizar un sonido y hacerlo demasiado "plano".

El principio es simple. Para cada valor de la envolvente de la señal de entrada, si su amplitud está por encima del valor umbral (U) la atenuamos (la multiplicamos por un valor menor que 1) mientras que si está por debajo del umbral (U) la amplificamos (la multiplicamos por un valor mayor que 1). Esto sería el algoritmo básico y tiene como parámetros el umbral y la atenuación/amplificación aplicada.

Un parámetro que suelen traer los compresores hardware comerciales es el tiempo de ataque. El tiempo de ataque de un compresor es el tiempo que tarda el compresor en "darse cuenta" de los cambios en la entrada (es algo así como el tiempo que tarda en reaccionar). En el ejemplo de pseudocódigo que describimos antes lo que tenemos es un compresor de ataque 0 o sin ataque.

Para simular un ataque en un compresor hecho por software podemos dividir la señal de entrada en dos señales: la original y una versión retardada de la misma (mediante una línea de retardo). De esta manera los valores que comparamos con el umbral (U) son los valores que salen de la línea de retardo, mientras que los cambios de amplitud los aplicamos a la señal original, sin retardar. Así, el tiempo que "tarda en actuar" el compresor es igual al tiempo de retardo de la línea.

Para evitar saltos bruscos en la amplitud podemos hacer que los valores de amplificación/atenuación no puedan variar de forma drástica, sino de forma progresiva (simulando un ataque real, tal y como lo vimos en las envolventes).


Figura 10 - Rango dinámico de una señal.
Expansión y puertas de ruido

La expansión del rango ninámico se utiliza para acentuar las zonas de alta intensidad y atenuar las que tienen una intensidad más baja. Esto a veces se utiliza para sonidos percusivos, para darles mayor crudeza así como para eliminar ruido de baja intensidad en grabaciones.

Para implementar un expansor simplemente cogemos la implementación de un compresor e invertimos las condiciones del bucle: si el valor supera el umbral, amplificamos, mientras que si no lo supera, atenuamos.

Un caso extremo de expansión son las puertas de ruido. Una puerta de ruido es un expansor que, cuando la intensidad de la señal baja de un humbral determinado, corta dicha señal (factor de amplificación 0).

Al igual que en el caso de los compresores lo ideal, para simular el comportamiento de un expansor ideal, es implementar tiempos y rampas de ataque.

En el fichero fuentes3.tar.gz se incluye un fuente "env.c" que implementa un detector de envolvente. A partir de este detector de envolvente podremos implementar los algoritmos de cambio del rango dinámico que queramos.

Conclusiones

En esta serie de artículos se ha hecho una introducción más que amplia al mundo de la síntesis y el procesado de música por ordenador; aunque se quedan en el tintero algunos tópicos que por razones obvias no pueden ser descritos en su totalidad. Al lector interesado le remito a algunas direcciones web bastante interesantes:

Para contactar con el autor: avelinoherrera@gmail.com.

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