Proyecto: Analizador de espectro con 6 LEDS PWM

Lo he posteado en el foro general de exhibitions, pero supongo que aquí habrá paisanos que no miren tanto el resto de foros. Está en mi blog de vandal, pero como allí no entienden mucho de electrónica lo copio aquí que le pega más.

Esto va de un proyecto que he hecho para que unos LEDs bailen según la música que le pongo en el PC.

Antes tenía un problema de sincronismo (los LEDs se volvian majaras y se intercambiaban posiciones), pero ya lo he arreglado. La parte alta del byte que envia el PC indica para que LED es la información, y la parte baja la intensidad del sonido en esa banda de frecuencias.

Primero comento un poco por encima el programa, y al final os pondre el código. Los que os interese el Arduino echadle un vistazo, así os podeis hacer una idea de lo fácil que es programar para esto (aunque si quieres hacer cosas complicadas, también puedes programar en C).

La parte que está en el Arduino es muy simple, y de hecho esta casi fusilada de un ejemplo que viene con el compilador del Arduino (Dimmer, se llama). Simplemente se indica que los pines capaces de regular voltaje son salida, y luego se espera que lleguen los bytes del PC. Se separa la parte alta y la parte baja, con la alta se elige el LED correspondiente (mediante un vector), y la parte baja lleva la intensidad del volumen ya tratada. Como la luminosidad de los LEDs no es lineal con el voltaje, hay otro vector para esto, entramos como índice con el volumen y ya nos devuelve cual sería el voltaje (que va de 0 a 255, es decir, de 0 a 5 V).

Esto de la luminosidad suena raro, pero realmente lo hice a voleo probando valores. Primero lo puse a tope (255) y luego busque lo que para mí sería un valor medio de luminosidad. El resto de valores esta medio extrapolando medio interpolación lineal.

La parte que lleva el PC (mediante Processing) es un poco más complicada. Utiliza la libreria minim, que la verdad es que me resolvió la peor parte de todo, poder obtener el espectro de frecuencias de la señal de salida de la tarjeta de sonido.

Esto esta hecho muy a voleo, con poca base de utilización de señal y demás. Lo podría haber hecho mejor, pero quería algo molón, no exacto.

Lo que hago primero es obtener la transformada de fourier de la señal de sonido, gracias a la libreria. Una vez obtenido, la separo en 240 barras equidistantes de forma lineal, para agrupar bandas de frecuencias. Y luego lo que hago es escoger para cada LED varias barras de estas, y coger el valor máximo de cualquiera de ellas.

¿Por que lo hago así? Como sólo tengo 6 LEDs si cojo el espectro, lo divido en 6 y que haga la media de amplitud, no se ve casi nada. Si lo hago en plan logarítmico (que es como van la mayoría de estos ecualizadores o analizadores de frecuencia) pues no va mal del todo, pero pasa lo mismo que antes, los LEDs se encienden casi todo el rato y no se llega a distinguir mucho a la vista.

Pero con esto del máximo, si hay un instrumento tocando en la banda de frecuencias de un LED, al tener mucha amplitud pero poco ancho de frecuencia, se notara muchisimo en un led en particular, vamos, lo que pasa en ese video con el silbido, o en el de la entrada anterior con la ocarina.

Aparte de eso, cada rango de frecuencias suele tener una amplitud más baja a medida que la frecuencia es mayor. Para compensarlo tienen coeficientes distintos, y en el ultimo LED que es para detectar ruido o percusiones multiplico la amplitud por la frecuencia.

Luego otra cosa que tiene es lo de “volumen” esto es una variable que intenta detectar el volumen general que tiene la canción o lo que sea, ya que dependiendo de como pongas tu el volumen manualmente, como sea el PC, o si la canción esta más bajita que otra o lo que sea los LEDs se iluminarian más o menos, y no queremos eso. Esto mide el volumen actual (que es in.mix.level, si es muy bajo es que no suena nada en ese preciso instante) y lo va actualizando, porque si lo cogiera de forma instantanea saldrian igual los sonidos fuertes o debiles dentro de una misma canción y eso tampoco lo queremos.

Despues separa la información en LEDs y la intensidad (que lo hace dividiendo por 10, para que sea un valor entre 0 y 30) y se la envia al arduino. Además lo dibuja, que es lo que usaba yo para ajustarlo fino, ya que lo último que hice fue enviar los datos al Arduino.

Y creo que eso es todo. Pongo el código a continuación (lleva muchos comentarios de como va cada cosa, eso sí, en inglés porque quiero ponerlo en los foros del arduino), y si alguno quereis saber más pues me lo preguntais aquí mismo.

Código para el arduino:

/*You'll need 6 LEDS to make it work, connected to the PWM pins on the arduino duemilanove.
If you have the arduino mega, you could put more LEDs, feel free to change the code.

I used a 100 ohm resistor, you can watch the LEDs I used (and a demo) on this video:

http://www.youtube.com/watch?v=pH9U5miKfcc

*/

const int vectledPin[]={3,5,6,9,10,11};

/*These are the values that will be given to the PWM ports on the arduino. The arduino will receive values between 0 and 30,
that measures the "volume" linearlly. However, the LED's don't bright linearlly, so I tested it a bit and put these values
as my own testing. I used the DIMMER sketch example on the arduino program to know what values to use.
Feel free to change them. One small change would be to make two vectors, as I used two different types of LEDs.*/

const int vectbright[]={0,1,2,3,4,6,8,19,14,18,22,27,32,38,47,56,66,76,86,96,105,114,122,130,138,148,162,190,222,255,255};
int i=0;

void setup()
{
 // initialize the serial communication:
 Serial.begin(9600);
 // initialize the ledPin as an output:
 for (i=0;i<6;i++)
   pinMode(vectledPin[i], OUTPUT);
 i=0;
}

void loop() {
 byte brightness;
 byte led;

 // check if data has been sent from the computer:
 if (Serial.available()) {
   // read the most recent byte (which will be from 0 to 255):
   brightness = Serial.read();
   led=(brightness>>5);
   //This reads what LED it is (it is stored on the 3 high bits)
   
   brightness=((brightness)&0x1F);
   //and this reads the 5 lower bits only. It puts a 0 in the higher ones.
   
  analogWrite(vectledPin[led], vectbright[brightness]);
  //and finally, using both vectors and the brightness index, the PWM value is sent
 }
}

(Código para processing en siguiente post, si no no me cabe)

Código para Processing (necesitais también la libreria minim)

/*A very important note. You have first to compile and upload the arduino code, otherwise
it will give you a serial error.

Also, if you want the stereo mix as an input (the sound that is going through your speakers), you
have to change it in windows (or mac and linux as well, i'm not sure) as the main input for recording.
This is easily done in windows vista and 7 right clicking the general volume button, choosing recording
options, and selecting stereo mix as the main one. You'll probably have to switch off (software) the 
microphone input, too. If you don't see this options, right click your microphone, and check all
options to make sure stereo mix (or something like that) is shown. You'll have to restart this Processing
sketch in order to make it work.

The things you need to make it work are told in the arduino code.
*/
import processing.serial.*;
import ddf.minim.analysis.*;
import ddf.minim.*;

Serial port;

AudioInput in;
Minim minim;
FFT fftLin;
//I used an example of FFT as a base, and tweaked it a bit.

float vectormedias[]=new float[6]; // this keeps the averages of the 6 bands of frequency.
float magnitud=4; //general setting, it multiplies all volume signal.
float volumen;  // this measures the general volume, but it changes slowly (like a P control) to avoid noise 
int volcaptado; // this is the final volume sent to arduino for a certain led. It gives the index to the brightness vector.

void setup()
{
  println(Serial.list());
  if (port==null)
    port = new Serial(this, Serial.list()[1], 9600);
    //perhaps in your case is Serial.list()[0]
  size(512, 300, P3D);
  volumen=1;

  minim = new Minim(this);
  in = minim.getLineIn(Minim.STEREO, 2048);
  fftLin = new FFT(in.bufferSize(), in.sampleRate());
  fftLin.linAverages(240);
  //It takes 240 bands of frequency (linear). This is to avoid noise or strange sounds. (perhaps not needed)
 // Later, the maximum of a certain range of this bars will be taken.
  rectMode(CORNERS);
}

void draw()
{
  background(0);
  // perform a forward FFT on the samples in jingle's mix buffer
  // note that if jingle were a MONO file, this would be the same as using jingle.left or jingle.right
  fftLin.forward(in.mix);

  noStroke();
  fill(255);
  int w = int(width/6);
  //next follows a for that gets the maximum of certain ranges of bars, for 6 LEDs
  for(int i = 0; i < 120; i++)
  {
    if (i<=1)
    {
      if (vectormedias[0]<2*fftLin.getAvg(i))
        vectormedias[0]=2*fftLin.getAvg(i);
      // Each range has a multiplier, to adjust the visual response. This is based on tests, feel free to change it.
      // I changed them a lot, feel free to change what bars are included too. Some will be OK for certain music, or not.
    }
    else if (i<=5)
    {
      if (vectormedias[1]<1.2*fftLin.getAvg(i))
        vectormedias[1]=1.2*fftLin.getAvg(i);
    }
    else if (i<=12)
    {
      if (vectormedias[2]<1.6*fftLin.getAvg(i))
        vectormedias[2]=1.6*fftLin.getAvg(i);
    }
    else if (i<=22)
    {
      if (vectormedias[3]<1.75*fftLin.getAvg(i))
        vectormedias[3]=1.75*fftLin.getAvg(i);
    }
    else if (i<=55)
    {
      if (vectormedias[4]<2*fftLin.getAvg(i))
        vectormedias[4]=2*fftLin.getAvg(i);
    }
    if (i<=160)
    {
      //This is the last one. It is multiplied by the bar index, to give more weight to the final frequencies (that tend to be lower always).
      //This gives good feedback for noise, high pitch sounds, or percusion instruments.
      vectormedias[5]=i*fftLin.getAvg(i)/300+vectormedias[5];
    }
    if (i==119)
    {
      for (byte k=0;k<6;k++)
      { delay(10);
        fill(255);
          //The following number is important. It is the level that is taken as real input, if you catch noise
         //you will have to make it a bit bigger. If you don't have any noise, perhaps a lower number is better 
          if (in.mix.level()>0.001)
          { /*This keeps the volume changing with the in.mix level. However, we don't want it to change
             very fast, because in.mix.level is very variable. Otherwise, you'll see low sounds as big as high sounds.
             And we don't want it to be constant, otherwise when the volume is low you won¡t see anything.
             Feel free to change the values to change faster or slower though (keeping the sum as 1.00, don't mind the *10)*/
            volumen=volumen*0.998+in.mix.level()*0.002*10; // the *10 is cause the in.mix.level is usually 0.1. I wanted it to be closer to 1.
              //println(in.mix.level());  //uncomment this if you want to try to change the volume control
             volcaptado=int(magnitud*vectormedias[k]/volumen);
             //This is the last step. It gives the amount of volume to the graphic bars and the arduino.
              if (volcaptado>300)
                volcaptado=300;
                //we don't want it to be VERY big.
              volcaptado=volcaptado/10;
              //the arduino will get a value between 0 and 30 as the volume input. A same, but it is necessary as we'll need 3 bits to select the LED,
              //and we have 5 more (up to 31). I could have used 0 to 31, but I was lazy.
             
             port.write((k<<5)|byte(volcaptado));
             //This gives the arduino wich led it is with K, and the value of volcaptado.
             // it works like this:  XXX | XXXXX
             // first the number of the LED, and last the amount of volume. The serial port only send one byte (eight bits) so it has to be like that.
             // Another option would be giving a certain number to sync arduino and processing, like 0, v1, v2, v3, v4, v5, v6 ,  (and repeat).
             // However, the LED's change so fast that with 30 values I found it enough. Feel free to change it, though.
             rect(k*w, height, k*w + w, height - magnitud*vectormedias[k]/volumen);
             //and this draws the rectangles in your processing window If you don't want this feedback, just comment the line above.
          }
          else{
            //We get here if the in.mix.level is very low. So no sound is received. You'll receive noise only.
            volumen=0.4;
            //We put a low volume as a base (so when a sound comes in, the LED will bright more than usual)
            port.write((k<<5));
            // We tell the arduino to put all LED's with a volume of 0.
            delay(100);
            // change the delay if you want, or even delete it.
          }
       
        vectormedias[k]=0;
        // we delete the previous maximums, to make new ones in the next iteration.

      }
    }

  }

}

void stop()
{
  // always close Minim audio classes when you are done with them
  in.close();
  // always stop Minim before exiting
  minim.stop();

  super.stop();
  // send led=0 before exiting
  port.write(0);
}

Lo dicho, si lo probais y le haceis algún cambio que mole más, o quereis comentar algo, dudas, etc, contestad en este post o bien mandadme un correo si lo preferís. Gracias por el interés.