0

Tutorial de construcción de un Sintetizador Virtual con Processing

¡Hola, homo fabers! Hoy os traigo un tutorial dedicado especialmente a los amantes de la música electrónica: ¡vamos a construir un sintetizador virtual con Processing! Se tratará de un sintetizador virtual monofónico de Síntesis aditiva, el más simple que podemos implementar. Este tipo de síntesis consiste en mezclar diferentes tipos de ondas simples (normalmente sinusoidales, cuadradas, triangulares y de sierra) para crear sonidos más complejos.

Nuestro sintetizador constará de cuatro osciladores (uno por cada tipo de onda básica) y una pequeña interfície para controlar los envolventes de Attack, Sustain y Release, y el volumen. Para simplificar el código ejecutaremos las notas a través de un controlador externo mediante protocolo MIDI.

La interfaz del sintetizador quedará así

Este tutorial se ha hecho en colaboración con nuestro lector Xavier, que me ha propuesto escribir sobre este tema y me ha echado un cable con el código. A aquéllos de vosotros que os guste la música experimental y la ciencia (y entendáis un poquito el catalán) os recomiendo pasar por su página de proyectos personales, que os encantará :3

¡Venga, pues, poned un poco de música y empezamos!


1 – Configurar processing

Processing es un IDE basado en en Java pensado para artistas multimedia. Como ya he dicho, en este tutorial vamos a programar un sintetizador virtual con este lenguaje, sirviéndonos de algunas librerías para trabajar con audio y MIDI.

Si no tenéis Processing, descargadlo desde aquí (es gratis y multiplataforma). Os recomiendo bajar la versión más reciente, que en el momento de escribir este tutorial es la 3.3.7.

Para hacer el sintetizador vamos a necesitar tres librerías:

  • Sound: nos proporcionará las funciones para poder trabajar con los osciladores y crear ondas. Esta librería sólo funciona con las versiones de Processing a partir de la 3.0+, ¡así que ojo!
  • TheMidiBus: nos permitirá interactuar con el controlador a través del protocolo MIDI.
  • ControlP5: librería para dibujar la GUI sin morir en el intento.

¿Cómo se instalan estas librerías? Abrimos processing y seleccionamos la pestaña Sketch -> Import Library -> Add Library.

Se abrirá una ventana como esta:


Desde aquí se puede buscar entre el repositorio de librerías de Processing (tanto oficiales como contribuciones) e instalarlas automáticamente. La primera librería que nos interesa es la “Sound”, por tanto escribimos “sound” en la barra del buscador. Aparecerá una lista con varias librerías que coinciden con la búsqueda, pero hay que seleccionar la opción “Sound | sound library for processing”. Después pulsamos el botón de “Install”.


La segunda librería es la “TheMidiBus”. La buscamos por “midi” y la instalamos de la misma forma que hemos hecho con la primera librería:


La tercera librería se llama ControlP5. La buscamos por “control”:


Con esto ya tenemos a punto el IDE de Processing.


 

2 – El Controlador

Los controladores MIDI son, por norma general, teclados o instrumentos eléctricos que no generan sonidos por sí mismos. Su función es disparar sonidos y señales de control a otros dispositivos que sí pueden producir sonidos. En nuestro caso el controlador disparará el sintetizador virtual.

2.1 – Escoger un controlador

Si sóis profesionales de la música electrónica o habéis trabajado en el mundo multimedia lo más probable es que ya tengáis un controlador favorito. En ese caso no creo que os pueda explicar nada que no sepáis ya, por tanto podéis saltar al punto 2.2.

En cambio, si sóis aficionados y no tenéis ningún controlador, os daré tres consejos para escoger uno que os pueda servir:

  • Todo está en medida de vuestro presupuesto, pero no es necesario que os gastéis mucho. Podéis encontrar controladores perfectamente válidos por menos de 50€.
  • Buscad uno que sea sensible a la velocidad. En este tutorial vamos a implementar una variación de volumen por velocidad (key velocity) que variará el volumen en función de la fuerza con que se pulse la tecla.
  • Preferiblemente, que tenga puerto USB. Pero si sólo tiene puerto MIDI no os preocupéis: hay cables conversores de USB a MIDI por menos de 10€.

    Un conversor USB-MIDI

  • Además, cualquier instrumento de música que tenga salida MIDI se puede convertir en un controlador conectándole un interfaz USB-MIDI. Por tanto no hace falta que os compréis un controlador si ya tenéis otro instrumento.

 

El modelo de controlador no tiene mucha importancia, pero os puedo recomendar algunos que he utilizado y me han funcionado bien: M-Audio Keystation MINI, Korg nanoKEY2, AKAI LPK-25, Reloop Keyfadr. Y si tenéis un poco más de presupuesto: Arturia Keylab 49, Impulse 25.

 

Otro día os enseñaré a construir vuestro propio controlador desde cero, pero esto es otra historia…

2.2 – Conectar el controlador

El siguiente paso es conectar el controlador al ordenador y asegurarnos de que Processing lo detecta.

Antes de empezar a programar nuestro sintetizador necesitamos saber cuál es el nombre de nuestro controlador, para que más adelante podamos decirle a procesing a qué puerto tiene que conectarse.

Este pequeño código listará todos los dispositivos MIDI que hay conectados al ordenador. Tenéis que copiarlo, correrlo y quedaros con el nombre del dispositivo (podéis anotarlo en un papel).

//Codigo processing para listar todos los dispositivos MIDI-in conectados

import themidibus.*;//importamos libreria MIDI para el teclado controlador
MidiBus myBus; // Mi dispositivo MidiBus
String[] available_inputs = MidiBus.availableInputs();
void setup() {
  size(400, 200);
   MidiBus.list(); //listamos todos los dispositivos MIDI
}

void draw(){
 background(255);
 fill(0);
 for (int k = 0;k < available_inputs.length;k++){
  text(available_inputs[k], 150, 30+15*k);
 }
}

 

Al ejecutar el código aparecerá una ventana con la lista de los nombres y direcciones de todos los dispositivos conectados.

Es suficiente con anotar el nombre, no es necesario copiar también la dirección. ¿Por qué? Como veréis, en el código del sintetizador añadiremos un bucle que buscará entre todos los dispositivos conectados uno cuyo nombre se corresponda al que le hemos introducido, y se conectará a su dirección. Esto lo hacemos así porque cada vez que se reinicia el ordenador puede cambiar la dirección del dispositivo, pero no su nombre.

Por ejemplo, en mi caso tengo conectado un controlador Reloop KeyFadr. Al correr el código me aparece esta lista:


La primera línea contiene el nombre del controlador (KeyFadr) y la dirección [hw:1,0,0]. La segunda línea es el “sintetizador interno” del sistema de mi ordenador. Si tuviera más controladores conectados también aparecerían en la lista. Por tanto, tendré que anotarme el nombre “KeyFadr” para ponerlo después en el código del sintetizador. ¡Eh, pero cuidado! Es sensible a mayúsculas y espacios.

 


3 – Programar el sintetizador

El código del sintetizador estará dividido en tres ficheros: un fichero principal con las funciones setup y draw (sintetizador.pde), otro que se encargará de dibujar la interfaz (gui.pde) y otro que gestionará las conexiones midi y los osciladores (midiSignals.pde).

¿Por qué lo separamos en tres partes? Cuándo hacemos un código un poco complejo, que tenga muchas funciones diferentes, es una buena praxis separar el programa en varios ficheros y que cada uno se ocupe de una función concreta. Así es más fácil encontrar errores, y también queda todo más limpio. ¡Divide y vencerás!

Además, Processing tiene la ventaja de que se encarga automáticamente de juntar todos los ficheros. Por tanto no tendremos que llamarlos desde el fichero principal ni compilarlos todos juntos como se hace en otros lenguajes.

3.1 – Fichero principal (sintetizador.pde)

Este código simplemente llama las funciones de los otros ficheros para conectarse con el controlador, inicializar la interfaz y refrescar los dibujos de la pantalla. Abrid Processing, copiad este código y guardadlo con el nombre “sintetizador.pde”.

/* FICHERO PRINCIPAL DEL SINTETIZADOR
   Contiene las funciones setup() y draw()
   
   Hecho por Glare
   www.robologs.net
   
   y Xavier de Palau
   www.xavierdepalau.net

*/

void setup() 
{
  
  size(800, 200); //Dimensiones de la ventana del sintetizador
  smooth(8); //Nivel de antialias
  background(0); //Color de fondo
  
  //Inicia la conexion con el controlador
  //RECUERDA: Tienes que cambiar "KeyFadr" por el nombre de tu controlador
  conectar_controlador("KeyFadr");
  
  
  inicializar_sinte(); //Crea los osciladores y el envolvente
  inicializar_interfaz(); //Inicializa los botones de la interfaz
 
}

void draw(){
  background(0); //Limpiar la pantalla dibujando el fondo.
  
  dibujar_imagenes(); //Dibujamos las imagenes en la pantalla
  
}

Lo comento rápidamente. Dentro del void setup definimos las características de la ventana en las líneas 16-18.

En la línea 22 se inicia la conexión con el controlador, mediante la función conectar_controlador() que definiremos en el fichero midiSignals.pde . Esta función recibe un string que se corresponde al nombre del controlador y busca en la lista de dispositivos conectados uno que se corresponda. ¡Acordaos de cambiar el nombre “KeyFadr” por el de vuestro controlador!

En la línea 25 llamamos a la función inicializar_sinte() que también definiremos en el fichero midiSignals.pde y servirá para crear los osciladores y las envolventes del sintetizador.

En la línea 26 llamamos la función que inicializa los botones de la interfaz, que estará definida en el fichero gui.pde.

Por último, dentro del void draw limpiamos la pantalla y llamamos la función que dibuja las imágenes de la pantalla. Esta función la definiremos dentro de gui.pde.

 

3.2 – Dibujar la interfaz (gui.pde)

Descargar imágenes

Antes de empezar a programar la interfaz, necesitáis bajar estas imágenes y guardarlas en el mismo directorio dónde tengáis vuestro fichero ‘sintetizador.pde’ . No cambiéis el nombre de ninguna.

Las cuatro primeras imágenes representan los tipos de onda que podrá generar nuestro sintetizador y las colocaremos al lado de los botones de los osciladores. La última es el dibujo de una servidora, y es puramente decorativa 😛

Los dibujos para los botones y los knobs de las envolventes los generará automáticamente la librería ControlP5, por lo que no necesitamos bajar ninguna imagen más para hacer la interfaz.

El Código

Necesitamos crear un segundo fichero que gestionará la interfaz. Para crear un nuevo fichero con Processing sólo hay que pulsar la flecha que hay al lado del nombre del fichero actual que estemos editando y seleccionar “New Tab”.

 

Pedirá elegir un nombre para el nuevo fichero. Llamadlo “gui”.

 

Ya estamos listos para empezar a escribir.


Empezamos importar la librería ControlP5 y crear los controles de las envolventes y los osciladores (knobs y botones):

import controlP5.*; //importamos libreria GUI

//Interficie (GUI)
ControlP5 cp5; //controladores GUI
Knob knobAT; //knob tiempo de ataque (attack time)
Knob knobST; //knob tiempo de nota sostenida (sustain time)
Knob knobSL; //knob nivel de nota sostenida (sustain level)
Knob knobRT; //knob tiempo relajacion nota (release time)
Knob knobAMP; //knob amplitud del oscilador
Toggle toggSin; //interuptor para onda sinusoidal
Toggle toggSaw; //interruptor para onda de diente de sierra
Toggle toggSqr;//interruptor para onda cuadrada
Toggle toggTri; //interruptor para onda triangular

Después creamos los objetos que guardarán las imágenes:

//Imagenes decorativas de los osciladores. La clase imagen viene por defecto en el IDE
PImage imgSinus;
PImage imgSqr;
PImage imgTri;
PImage imgSaw;
PImage imgGlare;

 

Definimos también algunos colores que más adelante servirán para pintar los knobs:

//Definimos algunos colores con los que pintaremos los knobs
color colorKnob=color(190,255,250,180);
color colorLinias=color(150,150,209,100);
color colorNaranja=color(255,133,57,250);

 

Crearemos también un booleano que nos servirá para indicar si se está pulsando una nota o no. El estado de esta variable lo modificará una función del fichero ‘midiSignals.pde’ que todavía no hemos creado. El fichero que estamos escribiendo ahora sólo leerá esta variable y dibujará un pequeño led rojo si está a ‘true’ (es decir, si se está pulsando una nota).

boolean nota_pulsada = false;

 

La función inicializar_interfaz(), cómo habréis podido adivinar, inicializa los knobs y los botones de la interfaz. Establece sus dimensiones, posición, rangos, colores, etc. Si queréis ver exactamente que hace cada línea, mirad los comentarios del código. También cargamos las imágenes al final.

void inicializar_interfaz()
{
      //Creamos el objeto que gestiona la interficie. Lo llamamos cp5.
  cp5 = new ControlP5(this);
    //creamos los controles / knobs 
   knobAT=cp5.addKnob("knobAtack")
       .setRange(0.001,0.9) //Rango minimo y maximo
       .setValue(0.001) //Valor por defecto al arrancar
       .setPosition(width/8, height/4) //Posicion en la ventana
       .setRadius(30) //Radio del knob
       .setColorForeground(colorKnob) //Color de la parte interior
       .setColorBackground(colorLinias) //Color de la parte exterior
       .setColorCaptionLabel(255) //Color del texto interior
       .setStartAngle(HALF_PI) //Angulo inicial del knob
       .setAngleRange(TWO_PI) //Angulo para ir del valor inicial al final
       .setColorActive(colorNaranja) //Color al pasar el mouse por encima del knob
       .setCaptionLabel("Attack time") //Texto de la etiqueta
       .setColorLabel(255) //Color de la etiqueta
       .setDecimalPrecision(3) //Precision del knob 
       ;
  
  knobST= cp5.addKnob("knobSustainTime")
   .setRange(0.001,0.9)
       .setValue(0.001)
       .setPosition(width/4, height/4)
       .setRadius(30)
       .setColorForeground(colorKnob) 
       .setColorBackground(colorLinias)
       .setColorCaptionLabel(255)
       .setStartAngle(HALF_PI)
       .setAngleRange(TWO_PI)
       .setColorActive(colorNaranja)
       .setCaptionLabel("Sustain time")
       .setColorLabel(255)
       .setDecimalPrecision(3)    
       ;
       
  knobSL= cp5.addKnob("knobSustainLevel")
       .setRange(0.1,0.9)
       .setValue(0.5)
       .setPosition(width/2-width/8, height/4)
       .setRadius(30)
       .setColorForeground(colorKnob) 
       .setColorBackground(colorLinias)
       .setColorCaptionLabel(255)
       .setStartAngle(HALF_PI)
       .setAngleRange(TWO_PI)
       .setColorActive(colorNaranja)
       .setCaptionLabel("Sustain level")
       .setColorLabel(255)
       .setDecimalPrecision(1)    
       ;
     
  knobRT= cp5.addKnob("knobReleaseTime")
       .setRange(0.1,0.9)
       .setValue(0.1)
       .setPosition(width/2, height/4)
       .setRadius(30)
       .setColorForeground(colorKnob) 
       .setColorBackground(colorLinias)
       .setColorCaptionLabel(255)
       .setStartAngle(HALF_PI)
       .setAngleRange(TWO_PI)
       .setColorActive(colorNaranja)
       .setCaptionLabel("Release Time")
       .setColorLabel(255)
       .setDecimalPrecision(3)    
       ;
     
  knobAMP= cp5.addKnob("knobAmplitude")
       .setRange(0.01,0.99)
       .setValue(0.5)
       .setPosition(width/2+width/8, height/4)
       .setRadius(35)
       .setColorForeground(colorKnob) 
       .setColorBackground(colorLinias)
       .setColorCaptionLabel(255)
       .setStartAngle(HALF_PI)
       .setAngleRange(TWO_PI)
       .setColorActive(colorNaranja)
       .setCaptionLabel("Amplitude/Volume")
       .setColorLabel(255)
       .setDecimalPrecision(2)    
       ;
       
       
//botonones para los osciladores 

//sinusoidal
  toggSin= cp5.addToggle("Sinus")
     .setValue(false) //Inicialmente esta apagado
     .setPosition(width/8,height*3/4) //Posicion dentro de la pantalla
     .setCaptionLabel("SINUS") //Texto de la etiqueta
     .setColorBackground(color(255,0,0,180)) //Color cuando esta apagado
     .setColorActive(color(0,255,0,200)) //Color cuando esta activo
     .setColorForeground(colorNaranja) //Color al pasar el mouse
     .setSize(25,25) //Dimensiones del boton (ancho y alto respec.)
     ;     
//diente sierra
   toggSaw= cp5.addToggle("Sawtooth")
     .setValue(false)
     .setPosition(width/4,height*3/4)
     .setCaptionLabel("SAWTOOTH")
     .setColorBackground(color(255,0,0,180))
     .setColorActive(color(0,255,0,200))
     .setColorForeground(colorNaranja)
      .setSize(25,25)     
     ;       
  toggTri= cp5.addToggle("Tri")
     .setValue(false)
     .setPosition(width/2-width/8,height*3/4)
     .setCaptionLabel("TRIANGLE")
     .setColorBackground(color(255,0,0,180))
     .setColorActive(color(0,255,0,200))
     .setColorForeground(colorNaranja)
      .setSize(25,25)     
     ;
         
  toggSqr= cp5.addToggle("Sqr")
     .setValue(false)
     .setPosition(width/2,height*3/4)
     .setCaptionLabel("SQUARE")
     .setColorBackground(color(255,0,0,180))
     .setColorActive(color(0,255,0,200))
     .setColorForeground(colorNaranja)
      .setSize(25,25)     
     ;  

  //cargamos imagenes decorativas . Deben estar en el mismo directorio que el programa!!!
  imgSinus = loadImage("sinus.png");
  imgSaw = loadImage("saw.png");
  imgTri=loadImage("triangle.png");
  imgSqr=loadImage("square.png");
  imgGlare=loadImage("glare.png");
  noFill();
}

 

Declaramos otra función, llamada dibujar_imagenes(), que sirve para dos cosas. La primera es dibujar todas las imágenes estáticas que habéis bajado hace un momento (los tipos de onda y mi logotipo). La segunda es comprobar cuál es el estado de la variable booleana nota_pulsada, y si es ‘true’ dibujará un pequeño circulito rojo para indicar que se está pulsando una tecla del controlador.

void dibujar_imagenes()
{
    //Dibujar las imagenes decorativas
  image(imgSinus,width/8+30,height*3/4);
  image(imgSaw,width/4+30,height*3/4);
  image(imgTri,width/2-width/8+30,height*3/4);
  image(imgSqr,width/2+30,height*3/4);
  image(imgGlare,width*3/4+30,height/8);
  
  
  //Encenderemos una lucecita cada vez que se envie una nota
  if(nota_pulsada)
  {
    fill(255,0,0);
    ellipse(width/2+width/8,height*3/4,20,20);
  }
  
  //Notese que los knobs y los botones los dibuja automaticamente
  //la libreria, y no tenemos que preocuparnos de ellos. Yay!
}

 

 

Por tanto, el código completo será este:

/* GUI
   Este fichero se encarga de dibujar la interfaz y las imagenes
   
   Hecho por Glare
   www.robologs.net
   
   y Xavier de Palau
   www.xavierdepalau.net
*/

import controlP5.*; //importamos libreria GUI

//Interficie (GUI)
ControlP5 cp5; //controladores GUI
Knob knobAT; //knob tiempo de ataque (attack time)
Knob knobST; //knob tiempo de nota sostenida (sustain time)
Knob knobSL; //knob nivel de nota sostenida (sustain level)
Knob knobRT; //knob tiempo relajacion nota (release time)
Knob knobAMP; //knob amplitud del oscilador
Toggle toggSin; //interuptor para onda sinusoidal
Toggle toggSaw; //interruptor para onda de diente de sierra
Toggle toggSqr;//interruptor para onda cuadrada
Toggle toggTri; //interruptor para onda triangular

//Imagenes decorativas de los osciladores. La clase imagen viene por defecto en el IDE
PImage imgSinus;
PImage imgSqr;
PImage imgTri;
PImage imgSaw;
PImage imgGlare;

//Definimos algunos colores con los que pintaremos los knobs
color colorKnob=color(190,255,250,180);
color colorLinias=color(150,150,209,100);
color colorNaranja=color(255,133,57,250);


boolean nota_pulsada = false;




void inicializar_interfaz()
{
      //Creamos el objeto que gestiona la interficie. Lo llamamos cp5.
  cp5 = new ControlP5(this);
    //creamos los controles / knobs 
   knobAT=cp5.addKnob("knobAtack")
       .setRange(0.001,0.9) //Rango minimo y maximo
       .setValue(0.001) //Valor por defecto al arrancar
       .setPosition(width/8, height/4) //Posicion en la ventana
       .setRadius(30) //Radio del knob
       .setColorForeground(colorKnob) //Color de la parte interior
       .setColorBackground(colorLinias) //Color de la parte exterior
       .setColorCaptionLabel(255) //Color del texto interior
       .setStartAngle(HALF_PI) //Angulo inicial del knob
       .setAngleRange(TWO_PI) //Angulo para ir del valor inicial al final
       .setColorActive(colorNaranja) //Color al pasar el mouse por encima del knob
       .setCaptionLabel("Attack time") //Texto de la etiqueta
       .setColorLabel(255) //Color de la etiqueta
       .setDecimalPrecision(3) //Precision del knob 
       ;
  
  knobST= cp5.addKnob("knobSustainTime")
   .setRange(0.001,0.9)
       .setValue(0.001)
       .setPosition(width/4, height/4)
       .setRadius(30)
       .setColorForeground(colorKnob) 
       .setColorBackground(colorLinias)
       .setColorCaptionLabel(255)
       .setStartAngle(HALF_PI)
       .setAngleRange(TWO_PI)
       .setColorActive(colorNaranja)
       .setCaptionLabel("Sustain time")
       .setColorLabel(255)
       .setDecimalPrecision(3)    
       ;
       
  knobSL= cp5.addKnob("knobSustainLevel")
       .setRange(0.1,0.9)
       .setValue(0.5)
       .setPosition(width/2-width/8, height/4)
       .setRadius(30)
       .setColorForeground(colorKnob) 
       .setColorBackground(colorLinias)
       .setColorCaptionLabel(255)
       .setStartAngle(HALF_PI)
       .setAngleRange(TWO_PI)
       .setColorActive(colorNaranja)
       .setCaptionLabel("Sustain level")
       .setColorLabel(255)
       .setDecimalPrecision(1)    
       ;
     
  knobRT= cp5.addKnob("knobReleaseTime")
       .setRange(0.1,0.9)
       .setValue(0.1)
       .setPosition(width/2, height/4)
       .setRadius(30)
       .setColorForeground(colorKnob) 
       .setColorBackground(colorLinias)
       .setColorCaptionLabel(255)
       .setStartAngle(HALF_PI)
       .setAngleRange(TWO_PI)
       .setColorActive(colorNaranja)
       .setCaptionLabel("Release Time")
       .setColorLabel(255)
       .setDecimalPrecision(3)    
       ;
     
  knobAMP= cp5.addKnob("knobAmplitude")
       .setRange(0.01,0.99)
       .setValue(0.5)
       .setPosition(width/2+width/8, height/4)
       .setRadius(35)
       .setColorForeground(colorKnob) 
       .setColorBackground(colorLinias)
       .setColorCaptionLabel(255)
       .setStartAngle(HALF_PI)
       .setAngleRange(TWO_PI)
       .setColorActive(colorNaranja)
       .setCaptionLabel("Amplitude/Volume")
       .setColorLabel(255)
       .setDecimalPrecision(2)    
       ;
       
       
//botonones para los osciladores 

//sinusoidal
  toggSin= cp5.addToggle("Sinus")
     .setValue(false) //Inicialmente esta apagado
     .setPosition(width/8,height*3/4) //Posicion dentro de la pantalla
     .setCaptionLabel("SINUS") //Texto de la etiqueta
     .setColorBackground(color(255,0,0,180)) //Color cuando esta apagado
     .setColorActive(color(0,255,0,200)) //Color cuando esta activo
     .setColorForeground(colorNaranja) //Color al pasar el mouse
     .setSize(25,25) //Dimensiones del boton (ancho y alto respec.)
     ;     
//diente sierra
   toggSaw= cp5.addToggle("Sawtooth")
     .setValue(false)
     .setPosition(width/4,height*3/4)
     .setCaptionLabel("SAWTOOTH")
     .setColorBackground(color(255,0,0,180))
     .setColorActive(color(0,255,0,200))
     .setColorForeground(colorNaranja)
      .setSize(25,25)     
     ;       
  toggTri= cp5.addToggle("Tri")
     .setValue(false)
     .setPosition(width/2-width/8,height*3/4)
     .setCaptionLabel("TRIANGLE")
     .setColorBackground(color(255,0,0,180))
     .setColorActive(color(0,255,0,200))
     .setColorForeground(colorNaranja)
      .setSize(25,25)     
     ;
         
  toggSqr= cp5.addToggle("Sqr")
     .setValue(false)
     .setPosition(width/2,height*3/4)
     .setCaptionLabel("SQUARE")
     .setColorBackground(color(255,0,0,180))
     .setColorActive(color(0,255,0,200))
     .setColorForeground(colorNaranja)
      .setSize(25,25)     
     ;  

  //cargamos imagenes decorativas . Deben estar en el mismo directorio que el programa!!!
  imgSinus = loadImage("sinus.png");
  imgSaw = loadImage("saw.png");
  imgTri=loadImage("triangle.png");
  imgSqr=loadImage("square.png");
  imgGlare=loadImage("glare.png");
  noFill();
}




void dibujar_imagenes()
{
    //Dibujar las imagenes decorativas
  image(imgSinus,width/8+30,height*3/4);
  image(imgSaw,width/4+30,height*3/4);
  image(imgTri,width/2-width/8+30,height*3/4);
  image(imgSqr,width/2+30,height*3/4);
  image(imgGlare,width*3/4+30,height/8);
  
  
  //Encenderemos una lucecita cada vez que se envie una nota
  if(nota_pulsada)
  {
    fill(255,0,0);
    ellipse(width/2+width/8,height*3/4,20,20);
  }
  
  //Notese que los knobs y los botones los dibuja automaticamente
  //la libreria, y no tenemos que preocuparnos de ellos. Yay!
}

 

3.3 – Gestión de la conexión MIDI y los osciladores (midiSignals.pde)

Este fichero se encarga de gestionar la conexión MIDI con el controlador, así como los osciladores y las envolventes.

Para empezar, hay que crear otro fichero de la misma forma que hemos hecho antes. Llamadlo ‘midiSignals’.

Empezamos por importar las librerías Sound y TheMidiBus:

import themidibus.*;//importamos libreria MIDI para el teclado controlador
import processing.sound.*; //importamos libreria audio.Compatible con versiones de Processing 3.0 o superior

 

Inicializamos a cero la variable que indicará la nota que se está tocando sobre el controlador:

int notaMidi=0; //Indica la nota que se esta tocando

 

Después creamos el objeto MidiBus que guardará el dispositivo controlador, un índice que indica la dirección y un string que almacenará todos los nombres de los dispositivos conectados:

//MIDI
MidiBus myBus; // Mi dispositivo MidiBus
int controlador=1; //dispositivo por defecto 
String[] available_inputs = MidiBus.availableInputs(); //guardaremos todos los dispositivos encontrados en este vector

 

Después definimos los osciladores y las variables de los parámetros de ataque, sustain, etc, para las envolventes:

//Motor de sonido
TriOsc triOsc; //creamos un oscilador de onda triangular
SinOsc sinOsc; //creamos un oscilador sinusoidal
SawOsc sawOsc; //creamos un oscilador de diente de sierra
SqrOsc sqrOsc; //creamos un oscilador de onda cuadrada
Env env;  //y su envolvente ADSR

//Parametros por defecto del envolvente
float attackTime = 0.009; //Tiempo de respuesta (ataque)
float sustainTime = 0.004; //Tiempo que tarda a estabilizar el sonido de una tecla sostenida (sustain)
float sustainLevel = 0.5; //Nivel de volumen del sustain
float releaseTime = 0.2; //Cola de la nota (release)
float amplitude=0.8; //Amplitud de la onda (amplitude)

 

Ahora construimos la función conectar_controlador que habíamos llamado dentro del void setup del fichero sintetizador.pde . Esta función recibirá un string correspondiente al nombre del controlador al que queremos conectarnos. Después listará todos los dispositivos que hay conectados al ordenador y buscará el que tenga un  nombre que se corresponda al string para conectarse a él.

void conectar_controlador(String palabra_clave)
{
   MidiBus.list(); //listamos todos los dispositivos MIDI 

  //Buscamos el dispositivo cuyo nombre se corresponda con el nombre del dispositivo
  //que hemos encontrado en el codigo anterior.
  for (int k = 0;k < available_inputs.length;k++)
  { 
    if(available_inputs[k].contains(palabra_clave)==true)
    {
      controlador=k;
      println("Controlador encontrado: "+k);
    }
  }
  myBus = new MidiBus(this, controlador, -1); //Nos conectamos al controlador. El -1 indica que desestimamos la salida
}

 

Construimos una función que, dada una nota MIDI (un número entero entre 0 y 127) calcule su frecuencia de sonido mediante esta formula:

//Esta funcion calcula la frecuencia de una nota MIDI
float midiToFreq(int nota) {
  //Recibe la posicion de la nota en el teclado y la convierte a frecuencia (vibraciones por segundo)
  //segun esta formula:  https://en.wikipedia.org/wiki/MIDI_tuning_standard
  return (pow(2, ((nota-69)/12.0)))*440;
}

 

Creamos también la función inicializar_sinte():

void inicializar_sinte()
{
 
  //Creamos los osciladores
  triOsc = new TriOsc (this);
  sawOsc = new SawOsc(this);
  sinOsc = new SinOsc(this);
  sqrOsc = new SqrOsc(this);
 
 
  // Creamos la envolvente 
  env  = new Env(this); 
}

 

La función noteOn es propia de la librería TheMidiBus y se invoca cada vez que se pulse una tecla del controlador y el ordenador reciba la señal MIDI. Esta función recibe como parámetro el canal MIDI por el que se ha recibido la señal, la nota (o botón) que se ha pulsado y su velocidad (fuerza con que se presiona la tecla). A partir de esto, se actualiza la variable nota_pulsada, calculamos su volumen y timbre y hacemos sonar uno o varios tipos de onda en función de qué osciladores estén activos (es decir, comprobamos cuáles son los botones de osciladores que están pulsados).

void noteOn(int channel, int pitch, int velocity) {
  //Al pulsar una nota, la hace sonar.
  //INPUTS: channel  -> canal MIDI recibido por el controlador
  //        pitch    -> nota que ha enviado el controlador
  //        velocity -> 'fuerza' con que se ha pulsado la tecla
  
  //Actualizamos la variable del LED para indicar que se ha pulsado una nota
  nota_pulsada = true;
  
  
  //Convertimos el valor de la velocidad que nos llega (entre 0 y 127)
  //a un valor entre 0 y 1 para pasarlo como parametro al knob:
  amplitude=map(velocity,0,127,0,1)+knobAMP.getValue();
  
  //Guardar la nota MIDI que se ha pulsado
  notaMidi=pitch;
  
  //Miramos qué osciladores hay activados. No ponemos un 'else if' para que puedan sumarse ondas
  if(toggTri.getValue()==1){
    triOsc.stop(); //Hay que parar el oscilador
    triOsc.play(midiToFreq(notaMidi), amplitude); //Convertimos la nota a una frecuencia
    //Leemos la posicion de los knobs de la envolvente:
    attackTime=knobAT.getValue(); 
    sustainTime=knobST.getValue();
    sustainLevel=knobSL.getValue();
    releaseTime=knobRT.getValue();
    env.play(triOsc, attackTime, sustainTime, sustainLevel, releaseTime); //Funcion por defecto de la libreria sound. Hace sonar la nota con los parametros del envolvente.
  }
  //No voy a comentar el resto de osciladores: funcionan todos de la misma forma que el anterior.
  if(toggSaw.getValue()==1){
      sawOsc.stop();
      sawOsc.play(midiToFreq(notaMidi),amplitude);
      attackTime=knobAT.getValue();
      sustainTime=knobST.getValue();
      sustainLevel=knobSL.getValue();
      releaseTime=knobRT.getValue();
      env.play(sawOsc, attackTime, sustainTime, sustainLevel, releaseTime);
  }
  if(toggSqr.getValue()==1){
      sqrOsc.stop();
      sqrOsc.play(midiToFreq(notaMidi),amplitude);
      attackTime=knobAT.getValue();
      sustainTime=knobST.getValue();
      sustainLevel=knobSL.getValue();
      releaseTime=knobRT.getValue();
      env.play(sqrOsc, attackTime, sustainTime, sustainLevel, releaseTime);
  }
  if(toggSin.getValue()==1){
      sinOsc.stop();
      sinOsc.play(midiToFreq(notaMidi),amplitude);
      attackTime=knobAT.getValue();
      sustainTime=knobST.getValue();
      sustainLevel=knobSL.getValue();
      releaseTime=knobRT.getValue();
      env.play(sinOsc, attackTime, sustainTime, sustainLevel, releaseTime);
  }
 
  
  
}

 

La función noteOff también es propia de la librería TheMidiBus y se activa cuándo deja de pulsarse una tecla del controlador. Simplemente haremos que ponga la variable nota_pulsada a false para indicar que se ha soltado la tecla.

//Esta funcion se activara cuando se suelte una tecla del controlador
void noteOff(int channel, int pitch, int velocity) {

  nota_pulsada = false;
  
}

 

Por tanto, ¿cómo quedará el código completo de este fichero?

/* MIDISIGNALS.pde
   Este fichero se encarga de gestionar la conexión MIDI con el controlador,
   así como los osciladores y las envolventes.
   
   Hecho por Glare
   www.robologs.net
   
   y Xavier de Palau
   www.xavierdepalau.net
*/

import themidibus.*;//importamos libreria MIDI para el teclado controlador
import processing.sound.*; //importamos libreria audio.Compatible con versiones de Processing 3.0 o superior

int notaMidi=0; //Indica la nota que se esta tocando


//MIDI
MidiBus myBus; // Mi dispositivo MidiBus
int controlador=1; //dispositivo por defecto 
String[] available_inputs = MidiBus.availableInputs(); //guardaremos todos los dispositivos encontrados en este vector

//Motor de sonido
TriOsc triOsc; //creamos un oscilador de onda triangular
SinOsc sinOsc; //creamos un oscilador sinusoidal
SawOsc sawOsc; //creamos un oscilador de diente de sierra
SqrOsc sqrOsc; //creamos un oscilador de onda cuadrada
Env env;  //y su envolvente ADSR

//Parametros por defecto del envolvente
float attackTime = 0.009; //Tiempo de respuesta (ataque)
float sustainTime = 0.004; //Tiempo que tarda a estabilizar el sonido de una tecla sostenida (sustain)
float sustainLevel = 0.5; //Nivel de volumen del sustain
float releaseTime = 0.2; //Cola de la nota (release)
float amplitude=0.8; //Amplitud de la onda (amplitude)

void conectar_controlador(String palabra_clave)
{
   MidiBus.list(); //listamos todos los dispositivos MIDI 

  //Buscamos el dispositivo cuyo nombre se corresponda con el nombre del dispositivo
  //que hemos encontrado en el codigo anterior.
  for (int k = 0;k < available_inputs.length;k++)
  { 
    if(available_inputs[k].contains(palabra_clave)==true)
    {
      controlador=k;
      println("Controlador encontrado: "+k);
    }
  }
  myBus = new MidiBus(this, controlador, -1); //Nos conectamos al controlador. El -1 indica que desestimamos la salida
}

//Esta funcion calcula la frecuencia de una nota MIDI
float midiToFreq(int nota) {
  //Recibe la posicion de la nota en el teclado y la convierte a frecuencia (vibraciones por segundo)
  //segun esta formula:  https://en.wikipedia.org/wiki/MIDI_tuning_standard
  return (pow(2, ((nota-69)/12.0)))*440;
}

void inicializar_sinte()
{
 
  //Creamos los osciladores
  triOsc = new TriOsc (this);
  sawOsc = new SawOsc(this);
  sinOsc = new SinOsc(this);
  sqrOsc = new SqrOsc(this);
 
 
  // Creamos la envolvente 
  env  = new Env(this); 
}

void noteOn(int channel, int pitch, int velocity) {
  //Al pulsar una nota, la hace sonar.
  //INPUTS: channel  -> canal MIDI recibido por el controlador
  //        pitch    -> nota que ha enviado el controlador
  //        velocity -> 'fuerza' con que se ha pulsado la tecla
  
  //Actualizamos la variable del LED para indicar que se ha pulsado una nota
  nota_pulsada = true;
  
  
  //Convertimos el valor de la velocidad que nos llega (entre 0 y 127)
  //a un valor entre 0 y 1 para pasarlo como parametro al knob:
  amplitude=map(velocity,0,127,0,1)+knobAMP.getValue();
  
  //Guardar la nota MIDI que se ha pulsado
  notaMidi=pitch;
  
  //Miramos qué osciladores hay activados. No ponemos un 'else if' para que puedan sumarse ondas
  if(toggTri.getValue()==1){
    triOsc.stop(); //Hay que parar el oscilador
    triOsc.play(midiToFreq(notaMidi), amplitude); //Convertimos la nota a una frecuencia
    //Leemos la posicion de los knobs de la envolvente:
    attackTime=knobAT.getValue(); 
    sustainTime=knobST.getValue();
    sustainLevel=knobSL.getValue();
    releaseTime=knobRT.getValue();
    env.play(triOsc, attackTime, sustainTime, sustainLevel, releaseTime); //Funcion por defecto de la libreria sound. Hace sonar la nota con los parametros del envolvente.
  }
  //No voy a comentar el resto de osciladores: funcionan todos de la misma forma que el anterior.
  if(toggSaw.getValue()==1){
      sawOsc.stop();
      sawOsc.play(midiToFreq(notaMidi),amplitude);
      attackTime=knobAT.getValue();
      sustainTime=knobST.getValue();
      sustainLevel=knobSL.getValue();
      releaseTime=knobRT.getValue();
      env.play(sawOsc, attackTime, sustainTime, sustainLevel, releaseTime);
  }
  if(toggSqr.getValue()==1){
      sqrOsc.stop();
      sqrOsc.play(midiToFreq(notaMidi),amplitude);
      attackTime=knobAT.getValue();
      sustainTime=knobST.getValue();
      sustainLevel=knobSL.getValue();
      releaseTime=knobRT.getValue();
      env.play(sqrOsc, attackTime, sustainTime, sustainLevel, releaseTime);
  }
  if(toggSin.getValue()==1){
      sinOsc.stop();
      sinOsc.play(midiToFreq(notaMidi),amplitude);
      attackTime=knobAT.getValue();
      sustainTime=knobST.getValue();
      sustainLevel=knobSL.getValue();
      releaseTime=knobRT.getValue();
      env.play(sinOsc, attackTime, sustainTime, sustainLevel, releaseTime);
  }
 
  
  
}

//Esta funcion se activara cuando se suelte una tecla del controlador
void noteOff(int channel, int pitch, int velocity) {

  nota_pulsada = false;
  
}

4 – Vale… ¿cómo se toca este Sintetizador?

Para hacer sonar este sintetizador hay que contectar el controlador y darle al botón RUN de Processing. Cuándo se abra la ventana de la interfaz, haced clic encima de uno de los botones rojos para seleccionar el tipo de onda. También podéis hacer clic en varios para sumar las frecuencias. ¡Y a tocar!

Si queréis cambiar alguna de las envolventes, como el ataque o el sustain, podeís hacer girar el knob correspondiente haciendo clic y arrastrando o con la rueda del ratón. ¡Y recordad que es un sintetizador monofónico!

Como siempre, si tenéis alguna duda o problema con el tutorial (o queréis hacer alguna sugerencia) podéis escribirme en los comentarios. ¡Chau!

Gl4r3

Brillante, luminosa y cegadora a veces, Glare es tan artista como técnica. Le encanta dar rienda suelta a sus módulos de imaginación y desdibujar los ya de por si delgados límites que separan el mundo de la electrónica y el arte. Su mayor creación hasta la fecha es un instrumento capaz de convertir los colores y la luz en música. Cuándo sus circuitos no están trabajando en una nueva obra electrónica, le gusta dedicar sus ciclos a la lectura o a documentar sus invenciones para beneficio de los humanos. Sus artilugios favoritos son aquellos que combinan una funcionalidad práctica con un diseño elegante y artístico.

Deja un comentario

avatar