PARTE 9: SIN TÍTULO

A medida que avanzaba esta columna, analizamos algunas técnicas de sonido simples en BASIC y algunos trucos avanzados de formas de onda en lenguaje ensamblador. En el camino, nos hemos encontrado con el eterno equilibrio entre la facilidad de programación en BASIC y la velocidad de la máquina en Ensamblador. Este mes vamos a concluir nuestra discusión con una mirada a un lenguaje de nivel medio perfectamente adecuado para trabajar con sonido: C.


Si tiene una copia de C, esta columna debería brindarle un buen punto de partida para tocar música con ruido rosa y aclarar al menos una sorpresa que pueda encontrar en el sonido C. Si no tiene una copia de C, este artículo demostrará parte del poder del lenguaje y tal vez lo anime a obtener una copia. El programa de muestra fue desarrollado con el compilador Deep Blue C por John H. Palevich, vendido por Atari Program Exchange (número de catálogo APX-20166).


Ruido 1/f y Richard Voss. La aplicación que vamos a ver implica un enfoque diferente a la música aleatoria, llamado ruido 1/f. Este algoritmo es un intento de resolver uno de los problemas más complicados inherentes a la composición aleatoria: la falta de orden. La naturaleza misma de un generador de números aleatorios es caótica; la mayoría de la música no lo es. Si escribimos un programa para seleccionar aleatoriamente un montón de notas candidatas y tocarlas, el resultado es muy difícil de escuchar. La probabilidad de que se toque cualquier nota es la misma: este es el equivalente musical del ruido blanco.


Al matemático Richard Voss se le ocurrió un algoritmo muy popular para «filtrar» números aleatorios y darles una tasa de distribución más natural. Este algoritmo simple pero elegante produce un flujo de valores que reflejan los procesos naturales (viento y lluvia, copos de nieve, etc.) en su tendencia a combinar pequeños cambios con grandes saltos ocasionales en el valor. Esto es útil en muchas más situaciones además de la música: digamos que estás escribiendo un videojuego que tiene una araña que se mueve más o menos aleatoriamente. La sustitución del algoritmo de Voss por un generador de números aleatorios convencional produce una diferencia notable en la calidad del movimiento de la araña; parece un poco más inteligente.


Para imaginar cómo funcionan estos «números Voss», imagine una fila de cuatro dados, todos con el 1 boca arriba. Los leemos en orden decimal para producir un total de 1111. Ahora tiramos el dado en la posición de las «unidades». Esto nos dará seis valores posibles, que van desde 1.111 a 1.116. Pero si tiramos el dado en la posición de las «decenas», obtenemos seis valores posibles que van desde 1.111 a 1.161. En lugar de un posible cambio de valor de 5, obtenemos un rango de 50.


Una vez más: lancemos ambos dados en la posición de «decenas» y de «unidades». Ahora podemos obtener treinta y seis valores posibles, que van desde 1.111 a 1.166, un posible cambio de 55. Finalmente, cuando avanzamos hasta tirar los cuatro dados, obtenemos 1.296 valores posibles (6 elevado a la cuarta potencia) con un rango de valores de 5.555.


Ahora, para mostrar el patrón de tiradas de dados de forma más explícita: En este gráfico, un «1» significa tirar el dado, un «0» significa dejarlo en pie.


0001
0010
0011
0100
0101
0110
1111
1000

¡que es, por supuesto, el familiar patrón de conteo binario!


Entonces, para utilizar este sistema de tirada de dados en un programa, necesitamos dos variables para cada parámetro. Necesitamos una variable para realizar un seguimiento del «estado» actual de todos los dados: en esta variable, cada bit representará un único dado de dos caras (De acuerdo, tal vez sea una moneda). Luego necesitamos una segunda variable a la que llamaremos nuestro «filtro». Esta variable nos dirá qué dados podemos «tirar», es decir, cuál de los bits de la variable de dados podemos cambiar. Para lanzar un dado en particular, usaremos el generador de números aleatorios para decidir si ese bit es un 1 o un 0. Finalmente, cada vez que obtengamos un nuevo número tenemos que incrementar la variable de filtro para actualizar nuestro patrón de lanzamiento de dados.


¿Suena complicado? Si no tiene claro el concepto de manipulaciones de bits, lo es. Pero en C, se vuelve terriblemente fácil. Observe:


Almacenaremos los dados y las variables de filtro en un arreglo; recuerde que queremos realizar un seguimiento de los dados y los filtros para cada parámetro que usará la función (volumen, tempo, tono, etc.). Cada una de estas variables tendrá una longitud de un byte. En C, eso se llama carácter y tenemos que declararlo como tipo de datos.


char d[12];
char f[12];

Esto nos da 13 parámetros para jugar (C usa el elemento 0). Tenga en cuenta que los arreglos se forman con corchetes, no con paréntesis.


Ahora escribamos la función.


++f[p]

Esta frase incrementa automáticamente el elemento de filtro al que apunta «p». En BASIC, diríamos «F(P) = F(P) + 1». La frase:


peek(RANDOM)& ++f[p]

incrementa simultáneamente f[p] y luego realiza un AND lógico (&) del resultado con el generador de números aleatorios. (Esto supone que RANDOM se definió previamente como hexadecimal D20A, que es la posición de memoria del registro de desplazamiento aleatorio). El resultado de esta operación es que siempre que un bit sea 0 en el filtro, obtendremos un 0 en la salida. Pero siempre que un bit sea 1 en el filtro, obtendremos un 1 o un 0 determinado aleatoriamente en la salida: algunos bits del número aleatorio se fuerzan a 0. Esto nos da nuestra tirada de dado aleatoria.


Ahora queremos injertar el nuevo estado de los dados con el antiguo estado de los dados. En C, el carácter "^" hace un OR exclusivo (EOR en ensamblador). Si el bit de nuestro nuevo dado es 0, entonces el bit del dado anterior no se modifica. Si el bit del nuevo dado es un 1, el bit del dado antiguo se invierte: un 1 se convierte en un 0 y viceversa. Esto tiene el efecto de «tirar» sólo los dados que corresponden a unos en la variable de filtro.


d[p] = d[p] ^ (peek(RANDOM)& ++f[p]);

Pero tenemos otro atajo en C. Los programadores siempre dicen cosas como «A = A + 10» o «ZOT = ZOT*25». Al colocar el operador antes del signo igual, podemos omitir la repetición del nombre de la variable: así obtenemos «a + = 10» y «zot* = 25». Si esto le parece un toque frívolo, intente escribir el equivalente BASIC a «r[3.14*g[x + = 10/y]]* = 22;".


d[p] ^ = (peek(ALEATORIO)& ++f[p]); hace lo mismo que la orden anterior de tirar el dado; simplemente está abreviado. Esto hace todo lo que le pedimos a nuestra función «Voss». Para el programa, agregaremos un detalle más: un divisor ® para reducir el resultado final. Si r es 2, el valor más alto posible será 127 en lugar de 255. Finalmente, agruparemos toda la expresión dentro de la orden «return», que finaliza la función y devuelve el valor de la expresión. La forma final de nuestra función se puede ver en la parte inferior del listado del programa de ejemplo.


Son expresiones como esta en donde C realmente brilla. Intente duplicar esta función de una línea en BASIC; Le sorprenderá la expansión que se produce.


La concisión del lenguaje se mejora de otras maneras. Por ejemplo, cada expresión tiene un valor: el valor asignado al lado izquierdo de la expresión. Esto hace que declaraciones como «a = b = c = d = 0» sean legales, ya que la expresión «d = 0» tiene un valor de 0, lo que forma la expresión implícita «c = 0», y así sucesivamente. Si mira cerca de la parte inferior de la función «main», verás la siguiente orden:


plot(x = voss(10,2),y = voss(11,3));

que llama a la función «Voss» y asigna el valor devuelto a x, llama a «Voss» nuevamente y asigna el valor a y, luego llama a la función «plot» y le pasa los nuevos valores de x e y. Este tipo de compresión es increíblemente poderosa, una vez que se acostumbra.


Un Recorrido Por El Programa De Ejemplo. La primera parte del programa es un comentario largo que describe el archivo de enlace necesario para compilar el programa. En C, los comentarios se marcan con un solo «/*", y el comentario no termina hasta que se cierra con un "*/".


Las declaraciones "#define" son comandos simples de reemplazo de texto. Dondequiera que se encuentre la cadena «Parms», la declaración se procesará como si se hubiera escrito «12». Tenga en cuenta que esta es una definición de etiqueta y no tiene nada que ver con variables: no se desperdicia nada de su memoria de tiempo de ejecución.


Luego vienen las declaraciones de los arreglos. Estos se crean encima de la primera definición de función para que sean accesibles, o «globales», para todas las funciones siguientes. C tiene un conjunto completo de reglas para proteger el «alcance» y la «privacidad» de las variables. Las variables que no son de tipo arreglo, por ejemplo, generalmente se pasan por valor, por lo que una función llamada obtiene su propia «copia privada» de la variable que puede modificar sin estropear la copia de la función que llama.


La función que se ejecutará cuando se carga el programa debe denominarse «main». A esto le sigue una lista de parámetros vacía (los paréntesis) para indicar que no se pasan parámetros a esta función. Las cadenas "$(" y "$)" son adaptaciones al Atari, el que no cuenta con los símbolos de llaves. Las declaraciones lógicas se agrupan con llaves: cuando se cierra la última llave, el programa finaliza.


En la inicialización, primero corregimos un error menor en C y le damos a POKEY los valores que nos permitirán usar el canal 4 sin interferencias. Si no lo hace, se quedará con un Atari de tres voces. Luego le damos a nuestros dados y filtros algunos valores iniciales aleatorios, copiamos la cadena de notas candidatas en el arreglo «fn» (la especificación de frecuencia de nota a nota se proporciona en forma de caracteres ATASCII) y configuramos los gráficos para un brillo azul espantosamente brillante.


Después de eso, lo único que queda es entrar en un bucle «while» infinito, tocar las voces y dibujar algunas líneas en el azul horriblemente brillante. C tiene tres tipos de bucles. El familiar for-next se ha convertido en el bucle «for». Las siguientes declaraciones son equivalentes:


for(voice = 0;voice < 4; ++voice)
100 FOR VOICE = 0 to 3 STEP 1

C también tiene un bucle «while» que continúa mientras la expresión sea verdadera, y un bucle «do while» que hace lo mismo pero coloca la prueba en la parte inferior en lugar de en la parte superior. Ah, sí: a diferencia del BASIC, el bucle «for» de C realiza su prueba en la parte superior, donde debería estar. La declaración dentro de este bucle. . .


for(x 5;x<0; ++x)

nunca será ejecutado.


Rastros felices. Y así salimos del alcance del Circle C y concluimos nuestra descripción general de las posibilidades de programación de sonido de la computadora Atari. Con suerte, utilizando las técnicas presentadas aquí durante el último año y medio, haya podido obtener algunos efectos tonales intrigantes de su máquina y haya desarrollado algunas técnicas propias. Me alegro de que podamos ayudar.


Que todos tus programas de juego, presentes y conjeturados, no sean ni sonoramente mansos ni audiblemente irritantes.


/* Music in C
    por Bill Williams

Si el nombre de este archivo es CMUS.C,
su archivo de enlace (CMUS.LNK)
debería verse así:

cmus
aio
graphics
dbc.obj

*/
#define PARMS 12
#define RAND 0xD20A
#define TRUE 1
#define SKCTL 0xD20F
#define SSKCTL 0x232
#define AUDCTL 0x0208

char d[PARMS];  /* dice */
char f[PARMS];  /* pinking filters */
char fn[10];    /* freq/note table */
char vol[4];    /* volume counts */
char note[4];   /* voice’s note */

main()
$(
int i,voice,tempo,x,y;
/*Initialize Pokey for sound */
poke(SSKCTL,3);
poke(SKCTL,3);
poke(AUDCTL,0);
/* Initialize pink numbers */
for(i = 0; i <= PARMS; ++i)$(
d[i] = peek(RAND);
f[i] = peek(RAND);
$)
/* Set up note table.
    Current candidate notes
    make up a two-octave pentatonic
    scale.
*/
strcpy(fn,"!%*29DLUfr");
for(voice = 0;voice < 4;++voice)
  note[voice] = fn[rnd(10)];
/* Set up graphics */
graphics(24);
color(1);
setcolor(1,8,0);
setcolor(2,8,12);
/* Play Music */
while(TRUE)$(
for(voice = 0;voice < 4; ++voice)
play(voice);
for(tempo = voss(9,6);tempo > 0;--tempo);
if(!rnd(8))$(
plot(x = voss(10,2),y = voss(11,3));
drawto(191,peek(RAND)&0xC0);
drawto(319-(x/2),y);
$)
$)
$)
play(v)
char v; /* voice index */
$(
/* if volume is not 0, play voice
and decrement volume. Otherwise,
pick a new starting volume and a new note.
*/
if(vol[v])
  sound(v,note[v], 10,vol[v]--);
else $(
vol[v] = 3 + voss(v,31);
note[v] = fn[voss(v + 4,28)];
$)
$)
/* Voss' 1/f noise algorithm */
voss(p,r)
char p;  /* parameter number */
char r;  /* range (divisor) */
$(
return((d[p]^=(peek(RAND)& ++f[p]))/r);
$)	


Publicado en revista Softline Volumen 3 de Noviembre y Diciembre del 1983, páginas 59 y 60.