Sintaxis básica en SuperCollider

Introducción a la sintaxis de SuperCollider. Manejo elemental de variables, valores numéricos, listas y funciones.

electroacústica matemáticas SuperCollider

La base para introducirse en SuperCollider es conocer su sintaxis. La diversidad, riqueza y polimorfismo de este lenguaje lo pueden hacer intimidante para quienes se acercan a él por primera vez. Este texto propone una serie de ejemplos fundamentales para comprender la lógica interna del programa y captar su potencialidad.

En los siguientes ejemplos aún no hay sonido, sino solo fragmentos mínimos de código que muestran el manejo de estructuras. Entender bien estos conceptos sintácticos fundamentales nos permitirá muy pronto sintetizar sonidos y partituras algorítmicas virtualmente sin límites, siempre que sepamos expresar nuestras ideas en este nuevo idioma.

SuperCollider combina dos paradigmas: la programación orientada a objetos y la programación funcional. Sin entrar en detalles, por ahora solo diremos que esos dos elementos, Object y Function, permiten construir estructuras musicales y sonoras de gran complejidad de manera extremadamente concisa.

objetos y variables

Un objeto es simplemente una entidad cuyo comportamiento está bien definido. En SuperCollider, todo es un objeto de alguna clase (Class), desde valores simples a todo tipo de estructuras complejas como listas, matrices, audio, gráficos, funciones, etc.

42 // Integer (número entero) 42.4 // Float (número decimal) 42.0 // también un Float pi // un Float especial "memento mori" // cadena de caracteres (String)

Una variable es una palabra con la que representaremos cualquier objeto.

a = 42 a // devuelve su valor

En SuperCollider los nombres de variables globales pueden ser letras minúsculas sueltas, pero hay que evitar usar la s, ya que representa al servidor de audio. Los nombres de variables más largos y descriptivos empiezan con virgulilla  seguida obligatoriamente por una letra minúscula.

~saludo = "Hola mundo." ~miNumeroPreferido = 13

Una vez declaradas, las variables globales son accesibles en cualquier punto posterior del código de un programa.

Un elemento igualmente importante son los comentarios (comments). Un comentario es un texto dentro del código que es ignorado por el intérprete, pero que es esencial para clarificar lo que sucede dentro, tanto para quien programa como para quien lo vaya a leer después. Como en otros lenguajes, en SuperCollider se usa // para comentar una línea, junto con el par /* y */ para comentar un bloque de varias líneas:

// Esto es un comentario. Todo lo que sigue a // en una línea es ignorado por el intérprete /* Comentario multilínea. Todas las líneas se ignoran hasta que llegue el cierre */

aritmética

Empecemos por ejecutar expresiones aritméticas puras, en modo calculadora. Los espacios en blanco entre símbolos no son obligatorios pero sí muy recomendables para facilitar la lectura del código.

5 + 2 5 + 2 * 10

SuperCollider opera de izquierda a derecha, sin seguir las reglas matemáticas de precedencia. Los paréntesis tienen múltiples usos; entre ellos, determinar qué operaciones se calculan primero:

5 + (2 * 10) // da un resultado diferente a la expresión anterior ( ) // expresión válida, aunque no contenga nada

Como veremos, existen muchas expresiones abreviadas. Por ejemplo, podemos calcular potencias con un doble asterisco **:

2 ** 5 // calcula 2 a la quinta potencia

El módulo es una operación no muy conocida aunque extremadamente útil para la composición algorítmica. Puede entenderse como pedir el resto de una división. Como en muchos lenguajes de programación, la representa el símbolo %.

63 % 10 // devuelve 3, el resto de la división 63 / 10

listas

Al crear música necesitamos continuamente listas de elementos (alturas, duraciones, dinámicas, etc.). En SuperCollider hay muchas clases diferentes para representar colecciones de elementos. La más común es el Array. Una de las maneras más sencillas de construirlos es empleando corchetes []:

[1, 3, 9, -4] [ ] // Array vacío

Se puede operar con los Arrays en bloque:

[1, 3, 9, -4] * 2 [47, 63, 73] % 12 // de alturas MIDI a Pitch Class

Un Array puede agrupar elementos de clases diferentes.

// Integer, Float y String formando un Array [10, 3.45, "banana"]

Si operamos con dos arrays los elementos se agrupan de esta manera: si la longitud de los Arrays es igual, operando de uno a uno, en caso contrario, el Array más corto se itera en loop, repitiendo sus elementos:

[1, 2, 3] * [10, 100, 1000] // devuelve [10, 200, 3000] [1, 2, 3] + [10, 1000] // devuelve [11, 1002, 13]

En SuperCollider hay mucho sugar syntax (maneras breves e intuitivas equivalentes a expresiones más complejas). Con el doble punto .. podemos generar Arrays que contengan sucesiones de esta manera tan conveniente:

(1..4) // genera [1, 2, 3, 4] (0, 3..30) // continúa el intervalo 0, 3, 6, ... hasta 30

métodos

Por método (Method) nos referimos a acciones que pueden aplicarse a determinadas clases. Recogiendo un símil típico, podríamos imaginar un objeto de la clase Bicicleta cuyos métodos sean pedalear, girarManillar, frenar y encenderFaro.

métodos sobre números

Tanto enteros como decimales son objetos de la clase Number. Se les puede aplicar numerosísimos métodos. Veamos algunos:

5.squared // cuadrado 5.sqrt // raíz cuadrada 3.56.round // redondeo 3.56.floor // entero más cercano por debajo 3.56.frac // extrae la parte fraccional

En esta variante sintáctica, el método comienza con un punto y se escribe a continuación del objeto al que se aplica. Ciertos métodos requieren algún valor para operar:

3.pow(5) // 3 elevado a 5 3.max(8) // devuelve el valor máximo de los dos 25.lcm(40) // mínimo común múltiplo 3.56.round(0.1) // redondeo como múltiplo de un número

Hay métodos muy convenientes para trabajar con alturas:

69.midicps // nota MIDI convertida a frecuencia en Hz 880.cpsmidi // método inverso: de Hz a MIDI

De entre los generadores de números aleatorios, .rand y .rrand son los más comunes:

10.rand // retorna números enteros entre 0 y 9 10.0.rand // para obtener números decimales, el número debe tener coma flotante 60.rrand(71) // con .rrand podemos especificar el rango de números escogidos al azar

Forma sintáctica alternativa para escribir expresiones equivalentes a las anteriores, más propia de la programación funcional:

rand(10) rand(10.0) rrand(60, 71)

métodos sobre listas

Los métodos para un solo número pueden aplicarse también a los Arrays:

[1, 3, 9, -4].squared [60, 64, 67].midicps // frecuencias del acorde de do mayor

Pero hay métodos que solo tienen sentido aplicados a una lista:

[1, 3, 9, -4].sort // ordenación ["a", "b", "c"].reverse // reversión [1, 3, 9, -4].sum // suma de todos los valores ["a", "b", "c"].size // devuelve el número de elementos ["a", "b", "c"].powerset // lista todos los conjuntos posibles // Métodos sobre sucesiones generadas con la expresión (a...b): (0..11).scramble // baraja la lista (serie dodecafónica) (0..11).scramble.plot // gráfica con la expresión anterior

Tenemos que emplear variables para almacenar Arrays y poder operar sobre ellos posteriormente. Esto simplifica el código y lo hace mucho más legible. Usando corchetes nos referimos a uno o más elementos del Array.

x = (1..6) x[0] // el elemento 0 es el primero de la lista x[2..5] // lista del tercer al sexto elemento x[3..] // lista del cuarto elemento al final x[..3] // lista del primer al cuarto elemento x.choose // .choose escoge un elemento al azar: un dado x.includes(7) // chequea si el 7 está en la lista

Hay que tener cuidado dado que ciertos métodos alteran el contenido del Array al que se refiere la variable:

x.add("hi") // añadimos un elemento más a la lista x.remove(4) // eliminamos el 4 de la lista x // el contenido del Array ha cambiado

Un método puede tomar un objeto de una clase para dar como salida un objeto de otra clase. El método .dup, muy utilizado, crea duplicados de un objeto de cualquier clase y los agrupa en un Array:

pi.dup // Array con el número pi duplicado pi.dup(5) // Array con 5 copias de pi

Para esto también hay algo de sugar syntax, utilizando el símbolo !. Versiones abreviadas de las expresiones anteriores:

pi!2 pi!5

serialismo con variables, listas y métodos

Combinando los conceptos anteriores y la capacidad expresiva del lenguaje, planteemos ahora un ejemplo más musical. Será un sistema simple para generar series dodecafónicas aleatorias y manipularlas.

Comenzamos creando la variable global de nombre largo ∼serie para almacenar un Array creado aleatoriamente. La serie de alturas estará representada por sus valores en notación Pitch Class, que asigna números a cada nota de la escala cromática (0 = do, 1 = do♯, etc.).

~serie = (0..11).scramble // serie dodecafónica aleatoria

Existen métodos directos para aplicar las consabidas transformaciones habituales de inversión y retrogradación:

~serie.invert // inversión ~serie.reverse // retrogradación ~serie.invert.reverse // inversión retrogradada

Para la transposición se puede aplicar simplemente una suma a la lista de alturas. La expresión % 12, con el operador módulo, permite resituar los números mayores que 11 en sus valores correspondientes de Pitch Class.

~serie + 7 // transposición de la serie ~serie + 7 % 12 // resitúa los números entre 0 y 11 ~serie // chequeamos que la serie original no ha cambiado

Combinando varios métodos y operaciones aritméticas anteriores, construimos ahora un Array de Arrays que contenga las 12 transposiciones de la serie aleatoria original. La expresión resultante es concisa pero también críptica:

~tablaTranspos = ~serie!12 + (0..11) % 12

El método .plot representa gráficamente las doce series:

~tablaTranspos.plot(discrete: true) // el argumento "discrete" imprime puntos
Plot de la tabla de las transposiciones de una serie dodecafónica
El método .plot aplicado a un Array de Arrays crea una serie apilada de gráficas.

Ahora es trivial manipular cualquier serie derivada de la original:

~tablaTranspos[3] // 4ª transposición ~tablaTranspos[0].invert // inversión de la serie original ~tablaTranspos[8].reverse // retrogradación de la 9ª transposición

Combinando métodos y operaciones podemos obtener las frecuencias en Hz de las notas de una serie en cualquier octava:

(~tablaTranspos[5].reverse.invert + 60).midicps // (60 es el do central en MIDI)

introducción a las funciones

Otra construcción sintáctica fundamental son las funciones. Se declaran entre llaves { } y son el verdadero corazón operativo del lenguaje donde explota la potencialidad expresiva de SuperCollider. Las funciones merecen capítulo aparte, por lo que por ahora solo introduciremos su sintaxis básica.

Podemos entender una función (Function) como una caja negra que toma elementos de entrada, ejecuta un procedimiento y retorna algo como salida:

{ } // función vacía pero válida sintácticamente { 2 + 2 } { 2 + 2 }.value // para obtener la salida de la función se usa el método .value

Las variables también pueden representar funciones:

~tiradaMoneda = { ["cara", "cruz"].choose } ~tiradaMoneda.value // de nuevo, .value retorna el resultado de ejecutar la función ~tiradaMoneda.dup(3) // un Array con tres tiradas de la moneda

Al usar el método .dup es imporante distinguir bien entre lo que implica duplicar un elemento y duplicar una función:

( 10.rand ).dup(3) // genera un Array con tres copias del mismo elemento aleatorio { 10.rand }.dup(3) // genera un Array con tres iteraciones del proceso aleatorio

La flexibilidad de las funciones reside en el trabajo con parámetros de entrada, denominados arguments y declarados con la palabra clave arg:

// Declaramos el argumento x y luego especificamos cómo se opera con él ~media = { arg x; "La media de" + x + "es" + (x.sum / x.size) } // Para obtener el resultado de la función, usamos .value ~media.value([7, 8, -23, 5]) // También simplemente con un punto, omitiendo "value" ~media.([7, 8, -23, 5])

Escribimos ahora una función en varias líneas, que es lo habitual. Otro cometido de los paréntesis es enmarcar un bloque de código para facilitar su evaluación: si el cursor está dentro de los paréntesis, se ejecutan todas las líneas internas sin tener que seleccionarlas expresamente. El punto y coma ; es obligatorio para separar expresiones, usualmente al final de cada línea; de lo contrario obtendremos un error.

( // Calcula la distancia en semitonos temperados entre dos frecuencias en hercios ~semitonosEntreFrecuencias = { // Declaración de parámetros (arguments). Puede haber varios en una función. arg frecA, frecB; // frecuencias en Hz // Operación con los arguments. Distancia entre frecuencias redondeada a centésimas: var distancia = (frecB.cpsmidi - frecA.cpsmidi).round(0.01); "Distancia en semitonos:" + distancia // aquí el signo + une elementos de texto } )

En la función anterior hay que distinguir entre arg, que declara los parámetros de entrada (arguments), y var, que declara variables locales no accesibles desde fuera, cuya misión es ayudar a organizar y clarificar el código interno de la función.

Al emplear esta nueva función, hay varias formas de especificar los valores de entrada para los argumentos frecA y frecB:

~semitonosEntreFrecuencias.value // error: argumentos sin definir ~semitonosEntreFrecuencias.value(frecA: 800, frecB: 900) // un tono perfecto pitagórico ~semitonosEntreFrecuencias.value(1500, 1000) // sintaxis abreviada; 5ª pitagórica ~semitonosEntreFrecuencias.(1500, 1000) // aún más abreviada

Hemos sacado partido aquí de varias reglas para abreviar el código:

métodos que usan funciones

Para terminar, un concepto más avanzado: métodos que requieren funciones como argumento. Podemos perfeccionar nuestro algoritmo serial anterior definiendo una función que nos permita traducir los números a nombre de notas, haciendo así mucho más legibles los resultados.

Primero creamos un Array de Strings con los nombres de todas las notas, de modo que su lugar en el Array se corresponda convenientemente con su equivalente en Pitch Class:

~nombreNotas = ["do", "do#", "re", "mib", "mi", "fa", "fa#", "sol", "sol#", "la", "sib", "si"]

El siguiente paso es definir la función que convierta un número en el nombre de la nota:

~traduceNota = { arg pitchClass; ~nombreNotas[pitchClass] } ~traduceNota.value(pitchClass: 8) // testeamos la función ~traduceNota.(8) // equivalente abreviado

Ahora podemos emplear nuestra nueva función ∼traduceNota con el método .collect, el cual devuelve un Array tras aplicar una función a cada elemento de un Array inicial:

[10, 9, 0, 11].collect(~traduceNota)

Finalmente, aplicamos esta construcción (un método que toma una función) a ~tablaTranspos, calculando transformaciones de la 7ª transposición y mostrando los resultados como un Arrays con los nombres de cada nota.

( ~tablaTranspos[3] ).collect(~traduceNota) ( ~tablaTranspos[3].reverse ).collect(~traduceNota) ( ~tablaTranspos[3].invert ).collect(~traduceNota) ( ~tablaTranspos[3].reverse.invert ).collect(~traduceNota)

*      *      *

Una versión condensada en un solo archivo con todos estos ejemplos y explicaciones, en el formato propio de SuperCollider, puede descargarse en el siguiente enlace: