DESCARGO

Revisión: 1
Fecha: 19 Oct 1997

La página de efectos DSP


Delay, eco, reverberancia


Si se toman el tiempo de leer las consideraciones generales aclararemos algunas asunciones.
La implementación de un delay con DSP no difiere demasiado de la implementación digital; en realidad, lo que hacemos es transferir al software las tareas que realizaba el hardware dentro del bloque de memoria.
Asignaremos un area de memoria como buffer circular, almacenaremos el dato de entrada en una posición indicada por un puntero, y leeremos el dato de salida en otra posición indicada por otro puntero. La diferencia entre ambos punteros, y por ende la cantidad de palabras de memoria, representa el tiempo de delay en períodos de clock. Así, para lograr una demora de 500ms. a una frecuencia de muestreo de 20KHz, serán necesarias 10Kmuestras, por lo que puede usarse un buffer de 16Kwords, y la diferencia entre los punteros será de 10K.
La realimentación y los niveles de señal serán ahora manejados por software, por lo que el dato leído será convenientemente multiplicado por una constante (feedback level) y sumado a la señal original de entrada, por lo que es necesario realizar la escritura en memoria después de la lectura. Ambos datos, el leído de memoria y el tomado de la señal de entrada serán además multiplicados por sendas constantes (direct y effect level) antes de ser sumados y enviados al conversor de salida. Estas multiplicaciones suelen acarrear inconvenientes cuando se trabaja con CPUs que no soportan coma flotante.
Ambos punteros, de lectura y de escritura, serán incrementados a cada intervalo de muestreo, recorriendo circularmente el buffer.
Como la operación de lectura se realiza antes que la de escritura, leer y escribir en la misma posición corresponde al mayor delay posible, mientras que leer en la posición inmediatamente anterior corresponde al menor delay posible, leer la muestra que fue escrita recién en el intervalo pasado. Entonces, en el ejemplo anterior, el puntero de lectura estará 10K words por debajo del de escritura, o, lo que es lo mismo, 16K-10K=6Kwords por encima.

La reverberancia necesita un enfoque totalmente distinto, basado en el estudio de Schroeder y Moorer. Cuando tenga información clara al respecto la pondré disponible.

Flanger, Chorus


Lo hacemos implementando el bloque básico de delay, pero la historia se complica un poco. La modulación del tiempo de retardo no puede hacerse mediante un VCO como en la implementación digital, debe hacerse por software. Esto nos lleva a generar retardos de tiempo que se encuentran en "fracciones de período de reloj", por lo que deberemos realizar una interpolación entre dos muestras para lograr el verdadero valor.
El retardo mínimo es el tiempo de delay especificado (en milisegundos). El retardo máximo es el retardo mínimo más el ancho de modulación (en milisegundos). Se los traduce a muestras de la siguiente forma:
min_delay = delay * fs / 1000
max_delay = ( delay + width ) * fs / 1000
, donde fs es la frecuencia de muestreo.
Una vez determinados el retardo mínimo y el retardo máximo, calculados a partir del ancho de modulación (width) y el tiempo de demora, se barre el puntero de lectura en ese intervalo, incrementándolo o decrementándolo acorde lo indica la velocidad de barrido (speed) dentro de ese rango.
La velocidad de modulación del retardo se especifica en ciclos por segundo (Hz), por lo que debe convertirse al incremento a hacerse al puntero.
Deberemos recorrer el número de muestras especificado y regresar, la cantidad especificada de veces en un segundo, y tenemos SAMPLE_RATE intervalos para hacerlo.
Entonces , si se barre con una rampa, el incremento del puntero se calcula como:
step = 2 * width * fm / fs, donde fs es la frecuencia de muestreo, fm es la de barrido, y width es el ancho de barrido calculado en muestras (max_delay - min_delay). Esta cantidad será sumada al incremento normal del puntero de lectura a cada intervalo.
Y, si usamos la función exponencial, se calcula como:
step = (max_delay/min_delay) ^(2fm/fs). Este coeficiente será el valor por el cual se multiplicará el puntero de lectura a cada intervalo.
Si el puntero apunta a la posición 1000,523; deberemos interpolar entre la muestra 1000 y la 1001 para hallar el valor que correspondería a la señal analógica de haber sido muestreada en el instante correspondiente a la muestra 1000,523. El método de interpolación dependerá de la potencia de CPU disponible, y la calidad de efecto deseada. Para fines no profesionales, contrariamente a lo indicado por la teoría, se puede utilizar interpolación lineal, especialmente si la tasa de muestreo es muy superior a la máxima componente de la señal analógica.
La interpolación lineal se realiza facilmente descomponiendo al puntero de lectura en su parte entera y su parte fraccionaria, utilizando la parte entera para leer dos datos contiguos de memoria y la parte fraccionaria para pesar ambos, por ejemplo, [1000,523]=0,523*[1001] + (1-0,523)*[1000], donde [posición] indica el dato almacenado en esa posición, es decir: "Para obtener el dato correspondiente a la posición 1000,523; lo calculamos multiplicando la parte fraccionaria por el contenido de la posición que le sigue, más el complemento a uno de la parte fraccionaria por el contenido de la parte entera de la posición".
O, con una multiplicación menos pero un idexado más, lo mismo puede calcularse como [1000.523]=[1000] + ([1001]-[1000]) * 0.523, lo que es lo mismo que decir : "Para obtener el dato correspondiente a la posición 1000,523; lo calculamos multiplicando la parte fraccionaria por la diferencia entre los contenidos de la parte entera de la posición y la siguiente, más el contenido de la parte entera de la posición".
En el caso del chorus, eliminaremos la realimentación (feedback). Puede usarse el mismo algoritmo y colocar la variable 'feedback' en cero, al costo de un par de ciclos de CPU.
La señal de barrido puede ser una senoide, implementada mediante búsqueda en tabla (table lookup), o mejor aún, la señal exponencial, que produce el mejor chorus que he oído.

Pitch shift


Lo implementamos de forma análoga al chorus, realizando un barrido lineal y reiniciándolo al llegar al límite (sin volver atrás). Para minimizar el salto de fase en el reinicio del barrido, se suele utilizar un segundo puntero en cuadratura con el principal, y fundir los datos leídos por ambos mediante algún método de interpolación.
Como se dijera anteriormente, la pendiente de barrido da la cantidad de desplazamiento, y se la calcula como se indicó.
Otra posibilidad es determinar el período entre cruces por cero de la señal (autocorrelación, simple inspección del buffer), y realizar el barrido entre cruces por cero, de forma tal de evitar el salto abrupto de fase.
Hasta ahora no he implementado con demasiado éxito ninguno de estos sistemas. Siendo el más sencillo el primero, no logro un efecto demasiado agradable con algunas señales.

Compresor/limitador, downward expander, noise gate


Aún no he probado este sistema, por consiguiente lo que sigue a continuación es un planteo teórico basado en otro planteo teórico.
Si se toman el tiempo de leer las consideraciones generales aclararemos algunas asunciones.
Trataremos de implementar los bloques básicos mediante DSP. Tomaremos la señal de entrada y hallaremos su valor medio/eficaz/pico; según deseemos sea la respuesta del compresor. Un limitador tal vez deba responder al valor de pico, a fin de que la señal no sobrepase determinado valor. Un compresor para efecto de sustain tal vez deba responder al valor medio/eficaz, para no achatar demasiado los picos de ataque de la fuente de sonido (una guitarra, por ejemplo). Para hallar estos valores, definiremos algún período de integración durante el cual extraeremos el valor deseado. Cabe destacar que necesitamos que el sistema responda a algún valor de la señal pesado durante un intervalo de tiempo (de ataque); una vez superado el umbral, se considerará que la señal regresó al estado inferior al umbral una vez superado el tiempo de liberación (release). Si actuáramos muestra a muestra, actuaríamos sobre la forma de la señal y no sobre su amplitud.
Trataremos de hallar el valor de la reducción de la ganancia para un compresor/limitador:
El valor de nivel de señal obtenido será comparado contra el nivel de umbral. Como este nivel se especifica en dB (dBm, dBV, dBu, VU, etc.), para evitar hacer la conversión muestra por muestra, resulta conveniente realizar la conversión del nivel de umbral de dB a lineal y comparar muestra por muestra con este valor. Sabiendo que la reducción de la ganancia es Vi[dB]-Vo[dB], reemplazamos Vo[dB] por la ecuación del compresor/limitador, resultando GR = (Vi[dB]-Vt[dB]) * (1 - 1 / CR)
Si no se supera el valor de umbral, simplemente no hacemos nada, dejamos pasar la señal tal cual es. Si se supera el valor, calculamos la ecuación anterior convirtiendo Vi a dB mediante búsqueda en tabla (table lookup), restándole el valor Vt en dB (ya conocido), y multiplicando por uno menos la recíproca de la relación de compresión. El resultado obtenido es la reducción de la ganancia (atenuación) en dB necesaria, deberemos pasar este valor a lineal y hallar su recíproca, lo que nos dará el coeficiente de ganancia. En el ejemplo de la definición de compresor, 1 - 1 / CR resulta 1 - 1/4 = 0,75, y (30 - (-10)) * 0,75 = 30
En el caso del limitador (compresión infinita), el término uno menos la recíproca de la relación de compresión resulta igual a uno, por lo que directamente atenuamos la señal en dB la misma cantidad en que ésta excede al umbral; lo que resulta coherente.
Almacenaremos este coeficiente obtenido, ya que será la ganancia por la cual multiplicaremos muestra a muestra la señal de entrada hasta que en el próximo período de integración resulte otro valor y ésta se modifique.
Para el caso del downward expander, realizaremos la misma operación pero a la inversa: superar el umbral será tener una señal de valor inferior al umbral, y la relación de expansión ya es la inversa de la relación de compresión; por lo que uno menos la recíproca de la relación de compresión resulta uno menos la relación de expansión, que será negativo, indicando que la reducción de la ganancia es en realidad un aumento de la ganancia; lo que coincide con lo expresado en la teoría.
Para el caso del noise gate (un downward expander con expansión infinita), nos limitaremos a dejar pasar la señal sólo cuando se supera el umbral.
El tiempo de release se implementa fácilmente de forma analógica, dejando que un capacitor cargado a la tensión de control del atenuador simplemente se descargue, controlando la liberación en forma exponencial decreciente, y por ende linealmente en dB. En DSP, deberemos hallar otro coeficiente que nos permita llegar, al cabo de N multiplicaciones sucesivas a lograr que el coeficiente de ganancia se reduzca en 20 dB (esto sería nuestro equivalente a la aproximación de la constante RC). Este coeficiente se determina de forma similar al coeficiente de la función exponencial usada para barrer el retardo en chorus y phasers.
release = 10 ^ (1/fs), donde fs es la frecuencia de muestreo.
Queda como tarea para los más delirantes la aproximación de la constante RC mediante la transformación bilineal, de manera similar al desarrollo del phaser DSP.

Phaser o phase shifter


Si se toman el tiempo de leer las consideraciones generales aclararemos algunas asunciones.
Para lograr un phaser con DSP se puede recurrir a implementar la ecuación diferencia de un filtro all-pass. Sin embargo, puede resultar más interesante reproducir fielmente el sonido del phaser analógico original. Para esto, tomamos la ecuación de transferencia de una red desfasadora analógica en el plano s y la pasamos al plano z mediante la transformación bilineal. Luego, implementamos la ecuación diferencia resultante, en la cual el coeficiente determinado guardará una estrecha relación con la red RC original y el período (Td) entre muestras.

Phase shifter formulae

La ecuación diferencia resultante es:

y[n] = a ( x[n] + y[n-1] ) - x[n-1]

Un phaser se define por la frecuencia base (aquella a la cual wRC=1 para R máxima) y la frecuencia máxima (aquella a la cual wRC=1 para R mínima) o su rango en octavas; controlado por la perilla de ancho de modulación (width).

Phase shifter formulae

La señal utilizada para barrer deberá ser una función de tipo exponencial, ya que no tenemos detalles constructivos que compensar:
step = (b_max/b_min) ^(2fm/fs), donde fm es la frecuencia de barrido y fs es la frecuencia de muestreo. Este coeficiente será el valor por el cual se multiplicará al coeficiente b a cada intervalo.
El efecto logrado es superior al viejo phaser analógico original (Q.E.P.D.).

Consideraciones generales

Se asume que la señal analógica es muestreada a una tasa superior a la de Nyquist, siendo el dato leído la representación del valor de la señal en ese instante. A los fines prácticos, se hace referencia al dato leído como la variable DATA_IN, y al dato escrito (el que se envía a la salida) como DATA_OUT. Se usa el término word (palabra) para referirse al ancho de palabra de la memoria, pero en realidad puede tratarse de bytes, 16 bit words, 24bit words, o cualquiera fuera el ancho de palabra empleado en la implementación en particular, es decir, el ancho de palabra del conversor y de la CPU a utilizar.
La constante SAMPLE_RATE representa la frecuencia de muestreo en muestras por segundo.
Se asume que después de la fase de inicialización, es decir, luego de hallados los valores necesarios para el funcionamiento, se realiza el procesamiento de la señal muestra por muestra. Queda por cuenta del lector asumir si este procesamiento se realiza por interrupciones, por polling del conversor A/D, un timer, etc.
Se asume cierta familiaridad con la nomenclatura utilizada en los textos sobre DSP, particularmente Discrete-Time Signal Processing de Oppenheim.

Aclaraciones adicionales

Buffer circular


Un buffer circular en software es similar al de hardware, excepto que el 'wrap-around' del puntero debe hacerse por soft, a menos que la CPU provea soporte para buffers circulares por hardware, como el ADSP2181 de Analog Devices. Este 'wrap-around' consiste en volver al inicio del buffer cuando se supera el final del mismo, y suele realizarse fácilmente utilizando un índice dentro del buffer (que va desde 0 hasta la longitud del buffer menos uno) y realizando el AND del índice con la longitud del buffer menos uno (un buffer de 1K implica AND con 0x3FF) en cada operación con el índice.
Así, un retardo de 500 ms a una frecuencia de muestreo de 20KHz necesita 10Ksamples, por lo que en un buffer de 16K (0x4000) haremos un AND con 0x3FFF.

Enteros vs. coma flotante


El problema de usar una CPU que no maneja números en formato de coma flotante es bien conocido por todos quienes transitan por los caminos del DSP; sin embargo, nunca está de más repasar algunos detalles. En estos efectos realizamos algunas multiplicaciones y sumas, pudiendo llegar a 'desbordar' el ancho de palabra de trabajo. Es fundamental restringir el desborde a una saturación (ej.: 240+30=255 en 8 bits) a fin de simular lo que ocurriría en un sistema analógico. Caso contrario, si dejáramos que el desborde normal ocurriera (ej.: 240+30=14 en 8 bits), estaríamos produciendo cosas sumamente extrañas y desagradables sobre la señal. Las nuevas CPU dedicadas a DSP incorporan un modo especial de overflow para estos fines.
Se realizan muchas operaciones con números fraccionarios, por lo que si nuestra CPU soporta sólo enteros, deberemos simularlos usando algún formato de coma fija como por ejemplo 1.15 (1 bit de entero/signo y 15 de fracción).
Esto complica un poco la aritmética, a menos que la CPU soporte números fraccionarios con coma fija (como la mayoría de los DSP).

Transformación bilineal


En la transformación bilineal, se reemplaza la variable s por una función de la variable z, resultando un mapeo de los polos y ceros del plano complejo s en el plano complejo z. En particular, el eje jw resulta mapeado en la circunferencia unitaria, por lo que todos los polos o ceros que se hallen en el eje jw resultarán en la circunferencia de radio uno, y todos los polos o ceros que se hallen a la izquierda del eje jw resultarán dentro de la circunferencia de radio unitario. Así, los criterios de estabilidad de uno y otro mundo resultan comunes, y las respuestas de ambos sistemas resultan equivalentes.

Función exponencial


Esta función recorre distancias iguales (en octavas) en tiempos iguales. Lo hace moviéndose de forma exponencial, de modo de ser una rampa en un eje logarítmico. Como un movimiento en octavas supone multiplicar por 2 cada vez, la ecuación del movimiento es y = y0 * 2 ^x, y0 es la posición inicial, y 2 es el incremento deseado. Calcularemos ahora la ecuación de esta función basándonos en la aplicación de técnicas de DSP.
Tenemos dos límites, uno inferior y0 y uno superior y1; debemos recorrer el camino y regresar (un ciclo) en un segundo (1Hz), lo que significa que tenemos SAMPLE_RATE intervalos para hacerlo. Si con cada intervalo multiplicamos a y0 por un coeficiente a, tal que al cabo de SAMPLE_RATE/2 intervalos lleguemos a y1, resulta a = (y1/y0) ^(2/fs), donde fs es la frecuencia de muestreo (el número de muestras por segundo).
Si queremos realizar más ciclos por segundo, debemos hacer que a se haga más grande, para que con menor número de multiplicaciones recorramos la misma distancia, por lo que resulta a = (y1/y0) ^(2fm/fs), donde fm es la frecuencia de barrido en Hz.
Por ende, la función deseada será y = y0 * a ^n, donde n es la variable independiente y representa el número de muestra.
Si miramos esta función en una escala logarítmica, resulta ln y = n * ln(a * y0) , siendo a e yo constantes, es decir, es una recta. Como vemos , esta función recorre espacios iguales (en octavas) en tiempos iguales.
La derivada de esta función será y' = y0 * a ^n * ln a, lo que equivale a la misma función cambiada de escala por el término ln a, que es constante; con lo cual también recorrerá espacios iguales (en octavas) en tiempos iguales.
Al retroceder desde y1 hasta y0, usaremos el mismo camino, dividiendo por a en vez de multiplicar, o lo que es lo mismo, hacemos a = 1/a, por lo que todo lo anterior resulta igualmente válido, ya que recorrer 2 octavas hacia arriba equivale a multiplicar por 4, y recorrer dos octavas hacia abajo equivale a dividir por 4 o multiplicar por 1/4.
Esta función es muy difícil de implementar en forma analógica, por lo que su utilización queda practicamente limitada a sistemas DSP.

This page hosted by GeoCities Get your own Free Home Page