Un espectrógrafo de secuencias de audio

Vicente González Ruiz

June 24, 2014

Contents

1 Para qu sirve el espectro de una seal de audio?
2 La Transformada de Fourier Discreta
3 Relacin entre la frecuencia de muestreo y la ventana de anlisis
4 Minimizando la distorsin provocada por la duracin limitada de la ventana de anlisis
5 El programa RTASA
 5.1 Entradas
 5.2 Controles
 5.3 Salidas
 5.4 RTASA.java
 5.5 ElasticityControl.java
 5.6 FFT.java
 5.7 Window.java
 5.8 YScale.java
6 Sobre la compilacin
 6.1 Compilacin de RTASA
7 Ejemplos de utilizacin

En esta prctica vamos a estudiar cmo calcular el espectro de una seal de audio. El espectro, por definicin, es el mdulo de los coeficientes complejos de Fourier.

1 Para qu sirve el espectro de una seal de audio?

Los sonidos naturales estn constituidos por una sumatoria de sonidos puros o sinusoidales con diferentes fases, amplitudes y frecuencias. Los seres humanos somos especialmente sensibles a la frecuencia de los sonidos cuando vamos a reconocerlos. Por lo tanto, una forma conveniente de caracterizar los sonidos es describir la frecuencia de las componentes puras de los mismos.

Un analizador de espectro es un sistema que es capaz de visualizar y dar informacin en tiempo real acerca del espectro de una seal. Para el caso de una seal unidimensional (como es el caso de una seal de audio), el espectro es una grfica cartesiana donde en el eje X se indica la frecuencia (generalmente en Hz) y en el eje Y la amplitud de dicha frecuencia (generalmente sin dimensiones).

2 La Transformada de Fourier Discreta

La DFT (Discrete Fourier Transform) o Transformada Discreta de Fourier es la herramienta matemtica que permite representar un sonido digitalizado en funcin de sus componentes de frecuencia. Algortmicamente hablando, la rutina DFT calcula a partir de un conjunto de muestras de audio digitalizadas otro conjunto de coeficientes de Fourier. Por definicin, cada coeficiente de Fourier es un nmero complejo. La forma natural de representacin de estos nmeros es mediante su notacin fase-magnitud, donde la magnitud expresa la amplitud del sonido que da lugar a dicha componente de frecuencia y la fase, su fase. Sin embargo, es comn representar dichos nmeros complejos tambin como un nmero real y otro imaginario (dependiendo del formato de entrada/salida requerido por dicha rutina). Generalmente, la rutina DFT necesita que tanto el conjunto de entrada como el de salida de muestras complejas se almacenen en un array. En el array de entrada la posicin dentro del array depende del instante de tiempo en el que la muestra all almacenada se gener. En el array de salida la posicin dentro del array indica la banda de frecuencia a la que corresponde dicho coeficiente de Fourier.

En esta prctica vamos a utilizar un algoritmo de clculo rpido de la DFT conocido como FFT (Fast Fourier Transform). El nmero de operaciones de la DFT es proporcional a N2 donde N es el nmero de muestras procesadas. Por el contrario, la FFT tiene una complejidad de N log 2(N).

3 Relacin entre la frecuencia de muestreo y la ventana de anlisis

Ya que vamos a trabajar con sonidos, el tamao del array (el nmero de muestras del array) junto con la frecuencia de muestreo nos va a determinar el tamao temporal de la ventana de anlisis que estamos utilizando en nuestro analizador de espectro de audio. Por ejemplo, si la frecuencia de muestreo utilizada es 8.000 Hz y el tamao de la ventana medido en muestras es de 1.000 muestras, entonces cada uno de los 500 coeficientes de Fourier calculados por la rutina DFT hablara de un ancho de banda igual a

4.000 Hz = 8 Hz∕coef.
500 coefs

El primer coeficiente (primer nmero complejo) se refiere a la banda [0,8) Hz, el segundo a la banda [8,16) y as sucesivamente. Ntese que el Teorema del Muestreo Uniforme indica que si f es la frecuencia de muestreo utilizada, entonces f∕2 es la mxima componente de frecuencia registrada. Por esta razn, aunque pasamos 1.000 muestras a la rutina DFT, obtenemos slo 500 coeficientes.

Por tanto, el tamao de la ventana de anlisis controla la resolucin del espectro en el dominio de la frecuencia y la frecuencia de muestreo el rango de frecuencias capturado.

4 Minimizando la distorsin provocada por la duracin limitada de la ventana de anlisis

Antes de calcular la FFT de la ventana de anlisis las muestras son multiplicadas por una funcin que minimiza la distorsin espectral. Dicha distorsin est provocada por la duracin no-infinita (por motivos prcticos) de la ventana de anlisis.

Si analizamos desde el punto de vista de la frecuencia qu estamos haciendo cuando calculamos la FFT de la ventana de anlisis, veremos que en realidad estamos calculando el espectro de la seal resultante de multiplicar las muestras por una funcin cuadrada que vale distinto de 0 (en concreto, 1) justamente en el intervalo de tiempo que se toman las muestras.

Si aplicamos el Teorema de Convolucin, el espectro resultante es la convolucin en el dominio de la frecuencia de los espectros de la seal de audio y de la funcin cuadrada. Como el espectro de la funcin cuadrada es una Sinc (la funcin muestreo), el espectro de la seal de audio se presenta distorsionado por dicha Sinc.

Para minimizar esta distorsin podemos hacer dos cosas:

  1. Hacer la ventana de anlisis lo ms grande posible, provocando as que la Sinc se asemeje a un impulso (la funcin ideal para convolucionar si no queremos modificar la forma de onda de la seal convolucionada).
  2. Usar una funcin que disperse menos energa (espectralmente hablando) que una funcin cuadrada. En esta prctica se utiliza la funcin de Hamming.

5 El programa RTASA

El programa usado en esta prctica para calcular el espectro de una seal de audio se llama RTASA (Real Time Audio Spectrum Analyzer).

5.1 Entradas

RTASA acepta una secuencia RAW (sin cabecera) de muestras a travs de entrada estndar. Debe tratarse de una secuencia de muestras de 16 bits, con signo, en formato little endian (el usado en las mquinas Intel). Se esperan dos canales.

5.2 Controles

RTASA acepta parmetros iniciales desde la lnea de comandos e interactivamente (durante la ejecucin), a travs de diferentes elementos de entrada.

Los parmetros iniciales son:

  1. El nmero de bandas de frecuencia analizadas. Por requerimientos de la FFT debe ser una potencia de 2.
  2. La frecuencia de muestreo (en muestras por segundo). Dicha frecuencia debera coincidir con la frecuencia de muestreo de la secuencia de entrada.

Los otros controles interactivos son:

  1. Y-Scale Control: Controla la escala del eje Y del espectro mostrado en la ventana Spectrum. El control se realiza con la rueda del ratn. El paso del incremento o del decremento se controla con los botones.
  2. Elasticity Control: Controla la elasticidad de la grfica que muestra el historial reciente del espectro mostrado en la ventana Spectrum. Dicha elasticidad se puede indicar con el ratn, arrastrando el slider o introducindo un valor en el campo de texto.

5.3 Salidas

La nica salida del programa es la grfica del espectro (ventana Spectrum). En la parte superior aparece el espectro del canal izquierdo y abajo, el del canal derecho. La ventana es redimensionable.

5.4 RTASA.java

 
/* Real Time Audio Spectrum Analyzer. */ 
 
/* 
 * Version inicial creada por Vicente Gonzalez Ruiz <vruiz@ual.es>. 
 * 
 * Mejoras introducidas por Manuel Marin <niram@eresmas.net> 
 *   referentes al calculo de la frecuencia con el puntero del raton. 
 * 
 * gse. 2006. 
 */ 
 
import java.awt.*; 
import javax.swing.*; 
import java.awt.event.*; 
import java.util.*; 
 
//import java.net.DatagramSocket; 
//import java.net.InetAddress; 
//import java.net.DatagramPacket; 
import java.io.IOException; 
 
public class RTASA 
    extends JComponent 
    implements Runnable, 
               ComponentListener,MouseMotionListener { 
 
    // Variables y metodos relacionados con el raton. 
 
    //Variables para almacenar la posicion actual del raton 
    Dimension d; 
    int posX = 0, posY=0; // Posicion del raton 
    int visualiza_frec; 
 
    // Inicializacion de la funcion de rastreo del raton. 
    public void initMouse() { 
        d = this.getSize(); 
        addMouseMotionListener(this); 
    } 
 
    // Metodo de realizacion de acciones en el caso de arrastrar y 
    // soltar. 
    public void mouseDragged(MouseEvent me) {} 
 
    // Metodo de realizacion de acciones en el caso de movimiento 
    // del raton dentro de la pantalla grafica. Dichas acciones 
    // consisten en calcular el valor de la frecuencia sobre el que 
    // esta situado el puntero del raton. Para realizar el calculo, 
    // se multiplica la variable bandWidth por la coordenada X de la 
    // posicion actual del raton en ese momento, restandole a dicho 
    // resultado el desplazamiento de 20 pixeles existente, ya que la 
    // abcisa empieza en el pixel 20. Para el calculo del valor de 
    // este desplazamiento se multiplica la variable bandWidth por 
    // 20. El valor final de toda esta operacion lo recoge la 
    // variable visualiza_frec. 
    public void mouseMoved(MouseEvent me) { 
        // Si el raton esta situado a la derecha de los 20 pixeles de 
        // xoffset. 
        if (me.getX()>=20){ 
            // Obtiene la frecuencia con la coordenada x. 
            visualiza_frec = (int)(bandWidth*me.getX()-(bandWidth*20)); 
            // Escribe en la consola de texto el valor en Hz. Se puede 
            // prescindir de esta linea ya que dicho valor ya aparece 
            // en la pantalla grafica. 
            //System.err.println("Frecuencia: " + visualiza_frec +" Hz"); 
        } 
        // Si se sale del margen izquierdo, para evitar escribir 
        // valores negativos. 
        else { 
            visualiza_frec = 0; 
            System.err.println("Frecuencia:" + visualiza_frec +"Hz"); 
        } 
 
        // Se almacenan las coordenadas actuales "x" e "y" del raton. 
        posX = me.getX(); 
        posY = me.getY(); 
 
        //Actualizamos la pantalla grafica. 
        this.repaint(); 
    } 
 
    /* Fin de las variables y metodos relacionados con el raton. 
 
    /* Numero de bandas (por defecto). */ 
    static int NUMBER_OF_BANDS = 512; 
 
    /* Numero de muestras por segundo (por defecto). */ 
    static int SAMPLE_RATE = 44100; 
 
    /* A FFT will be performed each STEP samples. */ 
    static int STEP = 512; 
 
    /* Altura inicial de la ventana. */ 
    static int WINDOW_HEIGHT = 512; 
 
    /* Dimensiones de la ventana. */ 
    int windowHeight, windowWidth; 
 
    JFrame frame; 
    Thread thread; /* Este hilo */ 
 
    /* Numero de bytes en cada muestra de audio. */ 
    int bytesPerSample = 2; 
 
    /* Numero de canales. */ 
    int channels = 2; 
 
    /* Numero de muestras por segundo (frecuencia de muestreo). */ 
    float sampleRate; 
 
    /* Ancho de banda mostrado. */ 
    float totalBandWidth; 
 
    /* Anchura de cada banda. */ 
    float bandWidth; 
 
    YScale yScale; 
    ElasticityControl ec; 
    double elasticity; 
 
    /* Factor de escala en el eje Y. */ 
    //float scale; 
 
    /* Buffer de audio. */ 
    byte[] audioBuffer; 
 
    /* Tama~no del buffer de audio en bytes. */ 
    int audioBufferSize; 
 
    double[] leftSpectrum; 
    double[] rightSpectrum; 
    double[][] leftBuffer; 
    double[][] rightBuffer; 
    double[] window; 
 
    /* Arena encima del espectro */ 
    int[] leftSand, rightSand; 
 
    /* Gravedad aplicada a la arena (0 -> ausencia de gravedad, 1 -> 
     * gravedad infinita). */ 
    double sandGravity=0.1; 
 
    /* Tama~no del grano de arena */ 
    int sandSize = 10; 
 
    /* Numero de bandas. */ 
    int numberOfBands; 
 
    int step_bytes; 
 
    /* Numero de muestras de audio. */ 
    int numberOfSamples; 
 
    /* Number of samples avaiable from the last read. */ 
    int availableSamples; 
 
    //int numberOfSpectrumsPerPacket; 
 
    //static final int PACKET_SIZE = 1024; 
    //static final int SIZEOF_SAMPLE = 2; 
    //static final int BUF_SIZE = 4096; 
 
    //static final int PORT = 6789; 
    //DatagramSocket socket; 
    //DatagramPacket sendPacket; 
    //DatagramPacket receivePacket; 
 
    /* Un color. */ 
    Color rojoOscuro; 
 
    public RTASA(int numberOfBands, float sampleRate, int step) throws IOException { 
        System.err.println("+-----------------------------------+"); 
        System.err.println("|RealTimeAudioSpectrumAnalizer|"); 
        System.err.println("+-----------------------------------+"); 
        this.sampleRate = sampleRate; 
        System.err.println("SampleRate=" + sampleRate + "samples/second"); 
        this.numberOfBands = numberOfBands; 
        step_bytes = step * 4; // 2 bytes/sample, 2 channels 
 
        /* Comprobamos que el numero de bandas sea una potencia de dos. */ { 
            int tmp = numberOfBands; 
            boolean numberOfBandsIsAPowerOfTwo = true; 
            int i = 0; 
            while(tmp>1) { 
                if((tmp%2)==1) { 
                    numberOfBandsIsAPowerOfTwo = false; 
                    break; 
                } 
                tmp >>= 1; 
                i++; 
            } 
            if(numberOfBandsIsAPowerOfTwo==false) { 
                System.err.println("Ineedanumberofbandspoweroftwo..."); 
                numberOfSamples = (numberOfSamples>>i); 
            } 
        } 
 
        System.err.println("Numberofbands=" + numberOfBands); 
        numberOfSamples = numberOfBands*2; 
        System.err.println("Numberofsamples=" + numberOfSamples); 
 
        audioBufferSize = channels * numberOfSamples * bytesPerSample; 
        audioBuffer = new byte[audioBufferSize]; 
        //numberOfSpectrumsPerPacket = BUF_SIZE/numberOfSamples/2; 
        leftBuffer = new double[numberOfSamples][2]; 
        rightBuffer = new double[numberOfSamples][2]; 
        leftSpectrum = new double[numberOfSamples]; 
        rightSpectrum = new double[numberOfSamples]; 
        leftSand = new int[numberOfSamples]; 
        rightSand = new int[numberOfSamples]; 
 
        /* Calculamos los coeficientes de la ventana temporal. */ 
        //computeWindow(1); 
        window = new double[numberOfSamples]; 
        Window.computeCoefficients(1/* Ventana tipo 1*/, window); 
 
        totalBandWidth = computeTotalBandWidth(sampleRate); 
        System.err.println("TotalBandWidth=" + totalBandWidth + "Hz"); 
        bandWidth = computeBandWidth(sampleRate, numberOfBands); 
        System.err.println("BandWidth=" + bandWidth + "Hz"); 
        //setDoubleBuffered(true); 
 
        // Create and set up the RTASA window 
        frame = new JFrame("RTASA-Spectrum"); 
 
        //scale = numberOfSamples; 
        // Controlador de la scala en el eje Y 
        yScale = new YScale(numberOfSamples, numberOfSamples/10); 
 
        frame.getContentPane().add(this); 
        windowWidth = numberOfBands + 10; 
        windowHeight = WINDOW_HEIGHT; 
        frame.setSize(windowWidth, windowHeight); 
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
        frame.addComponentListener(this); 
        Container content = frame.getContentPane(); 
        content.setBackground(Color.black); 
        frame.setVisible(true)/*show()*/; 
 
        // Controlador de la elasticidad del espectro azul. */ 
        ec = new  ElasticityControl(); 
 
        rojoOscuro = new Color(100, 0, 0); 
 
        //Capturar movimiento del raton// 
        initMouse(); 
    } 
 
    /* Lanza el hilo */ 
    public void start() { 
        thread = new Thread(this); 
        thread.setName("RealTimeAudioSpectrumAnalizer"); 
        thread.start(); 
        System.err.println("Yopuedoseguirhaciendocosasaqui!"); 
        // for(;;) { 
        //     int total = 0; 
        //     while(total<audioBufferSize) { 
        //     try { 
        //         System.out.write(audioBuffer,total,256); 
        //         total += System.in.read(audioBuffer,total,/*audioBufferSize-total*/256); 
        //         //System.err.print(total + " "); 
        //         System.err.print("."); 
        //         //Thread.sleep(10); 
        //     } catch (IOException e) { 
        //         System.err.println("Error in the pipe."); 
        //     } /*catch (InterruptedException e) { 
        //         System.err.println("InterruptedException."); 
        //         }*/ 
        //     availableSamples = total/4; /* 2 bytes/sample, 2 channels */ 
        //     //ystem.out.write(audioBuffer,total,audioBufferSize-total); 
        //     } 
        //     //System.out.write(audioBuffer,0,audioBufferSize); 
        // } 
    } 
 
    /* Detiene el hilo */ 
    public void stop() { 
        thread = null; 
    } 
 
    public float computeTotalBandWidth(float sampleRate) { 
        return sampleRate/2; 
    } 
 
    public float computeBandWidth(float sampleRate, int numberOfBands) { 
        return (sampleRate/2)/numberOfBands; 
    } 
 
    public void setGravity(float gravity) { 
        this.sandGravity = gravity; 
        System.out.println(gravity); 
    } 
 
    public void run() { 
        for(;;) { 
 
            int total = 0; 
            while(total<audioBufferSize) { 
                try { 
                    if(step_bytes < (audioBufferSize-total)) { 
                        System.out.write(audioBuffer,total,step_bytes); 
                        total += System.in.read(audioBuffer,total,step_bytes); 
                    } else { 
                        System.out.write(audioBuffer,total,audioBufferSize-total); 
                        total += System.in.read(audioBuffer,total,audioBufferSize-total); 
                    } 
                    //System.err.print(total + " "); 
                    //System.err.print("."); 
                    //Thread.sleep(10); 
                } catch (IOException e) { 
                    System.err.println("Errorinthepipe."); 
                } /*catch (InterruptedException e) { 
                    System.err.println("InterruptedException."); 
                    }*/ 
                availableSamples = total/4; /* 2 bytes/sample, 2 channels */ 
 
                /* We have availableSamples from the last read. These 
                 * samples are going to be placed at the end of the arrays 
                 * while the rest of data remain of previous reads. */ 
                for(int i=availableSamples; i<numberOfSamples; i++) { 
                    leftBuffer[i-availableSamples][0] 
                        = (double)(audioBuffer[4*i+1]*256 + 
                                   audioBuffer[4*i]); 
                    /* Parte imaginaria, muestra izquierda. */ 
                    leftBuffer[i-availableSamples][1] = 0.0; 
                    /* Parte real, muestra derecha. */ 
                    rightBuffer[i-availableSamples][0] 
                        = (double)(audioBuffer[4*i+3]*256 + 
                                   audioBuffer[4*i+2]); 
                    /* Parte imaginaria, muestra derecha. */ 
                    rightBuffer[i-availableSamples][1] = 0; 
                } 
 
                /* Now, we concatenate the new samples. */ 
                for(int i=0; i<availableSamples; i++) { 
                    /* Parte real, muestra izquierda. */ 
                    leftBuffer[i+numberOfSamples-availableSamples][0] 
                        = (double)(audioBuffer[4*i+1]*256 + 
                                   audioBuffer[4*i]); 
                    /* Parte imaginaria, muestra izquierda. */ 
                    leftBuffer[i+numberOfSamples-availableSamples][1] = 0.0; 
                    /* Parte real, muestra derecha. */ 
                    rightBuffer[i+numberOfSamples-availableSamples][0] 
                        = (double)(audioBuffer[4*i+3]*256 + 
                                   audioBuffer[4*i+2]); 
                    /* Parte imaginaria, muestra derecha. */ 
                    rightBuffer[i+numberOfSamples-availableSamples][1] = 0; 
                } 
 
                /* Multiplicamos cada muestra con el correspondiente 
                 * coeficiente de la ventana temporal. */ 
                for(int i=0; i<numberOfSamples; i++) { 
                    leftBuffer[i][0] *= window[i]; 
                    rightBuffer[i][0] *= window[i]; 
                } 
 
                /* Transformada de Fourier del canal izquierdo. */ 
                FFT.direct(leftBuffer); 
                /* Transformada de Fourier del canal derecho. */ 
                FFT.direct(rightBuffer); 
 
                /* Obtenemos la elasticitad de la arena. */ 
                elasticity = ec.getElasticity(); 
                //System.err.println(elasticity); 
 
 
                /* Calculamos el espectro (modulo). */ 
                for(int i=0; i<numberOfSamples; i++) { 
                    leftSpectrum[i] 
                        = Math.sqrt(leftBuffer[i][0]*leftBuffer[i][0] + 
                                    leftBuffer[i][1]*leftBuffer[i][1]); 
                    rightSpectrum[i] 
                        = Math.sqrt(rightBuffer[i][0]*rightBuffer[i][0] + 
                                    rightBuffer[i][1]*rightBuffer[i][1]); 
                } 
 
                /* Calculamos la arena. */ 
                for(int i=0; i<numberOfSamples; i++) { 
                    leftSand[i] 
                        = (int)((1-elasticity)*leftSand[i] + 
                                elasticity*leftSpectrum[i]); 
                    rightSand[i] 
                        = (int)((1-elasticity)*rightSand[i] + 
                                elasticity*rightSpectrum[i]); 
                } 
 
                /* Pintamos. */ 
                repaint(); 
            } 
 
        } 
    } 
 
    // Este metodo no deberia estar aqui. Si creamos una clase para 
    // controlar el tama~no de la ventana deberia de llamarse desde 
    // alli. Lo mismo deberia de ocurrir si creamos una clase para 
    // controlar la frecuencia de muestreo.  Esto se debe de hacer asi 
    // porque solo cuando estas clases estan trabajando es cuando debe 
    // de cambiar la escala y no deberia de pintarse siempre que se 
    // presenta el espectro. 
    void drawHz(Graphics g) { 
        Color color = Color.black; 
        g.setColor(color); 
        for(int i=10; i<numberOfSamples/2; i+=50) { 
            g.drawString(i*bandWidth + "", i, 10); 
        } 
    } 
 
    /* Pinta la ventana */ 
    public void paintComponent(Graphics g) { 
        int xOffset = 20; 
        int yOffset = 25; 
        Color color = Color.red; 
        //g.setColor(color); 
 
        /* Pintamos el espectro del canal izquiero arriba. */ 
        //color = new Color(255,0,0); 
        g.setXORMode(new Color(255,0,0)); 
        for(int i=0; i<numberOfSamples/2/*256*/; i++) { 
            //Double y = new Double(spectrum[i]/numberOfSamples); 
            //int x = y.intValue(); 
            int x = (int)(leftSpectrum[i]/yScale.getScale()); 
            //System.err.print(spectrum[i] + " " + x + " "); 
            g.drawLine(i+xOffset, /*460*/yOffset, i+xOffset, /*460-x*/x+yOffset); 
            //paintLine(i,yOffset,i,x+yOffset,g); 
            //int val_i = (data[2*i]*256+data[2*i+1])/256; 
            //int val_i1 = (data[2*(i+1)]*256+data[2*(i+1)+1])/256; 
            //g.drawLine(i,val_i+128,i+1,val_i1+128); 
        } 
 
        /* Pintamos el espectro del canal derecho abajo. */ 
        g.setXORMode(/*Color.green*/new Color(0,255,0)); 
        for(int i=0; i<numberOfSamples/2/*256*/; i++) { 
            //Double y = new Double(spectrum[i]/numberOfSamples); 
            //int x = y.intValue(); 
            int x = (int)(rightSpectrum[i]/yScale.getScale()); 
            //System.err.print(rightSpectrum[i] + " " + x + " "); 
            g.drawLine(i+xOffset, /*460*/yOffset+windowHeight-85, i+xOffset, /*460-x*/windowHeight-85+yOffset-x); 
            //paintLine(i,yOffset,i,x+yOffset,g); 
            //int val_i = (data[2*i]*256+data[2*i+1])/256; 
            //int val_i1 = (data[2*(i+1)]*256+data[2*(i+1)+1])/256; 
            //g.drawLine(i,val_i+128,i+1,val_i1+128); 
        } 
 
        //color = Color.blue; 
        //g.setColor(color); 
        g.setXORMode(/*Color.blue*/new Color(0,0,255)); 
        for(int i=0; i<numberOfSamples/2; i++) { 
            int x = (int)(leftSand[i]/yScale.getScale()); 
            g.drawLine(i+xOffset, /*460-x*/x+yOffset, i+xOffset, /*450-x*/x+sandSize+yOffset); 
        } 
        //color = Color.green; 
        //g.setColor(color); 
 
        //color = Color.cyan; 
        //g.setColor(color); 
        //g.setXORMode(Color.cyan); 
        //g.setColor(Color.cyan); 
        //color = new Color(10,10,200); 
        //g.setXORMode(color); 
        for(int i=0; i<numberOfSamples/2; i++) { 
            int x = (int)(rightSand[i]/yScale.getScale()); 
            g.drawLine(i+xOffset, /*460-x*/yOffset+windowHeight-85-x, i+xOffset, /*450-x*/yOffset+windowHeight-85-sandSize-x); 
        } 
        //if(dakl) drawHz(g); 
        g.setXORMode(Color.white); 
        for(int i=0; i<numberOfBands; i+= 50) { 
            g.drawString("" + (int)(i*bandWidth), i+xOffset, 15); 
            g.drawString("" + (int)(i*bandWidth), i+xOffset, /*505*/windowHeight-40); 
        } 
 
        //g.setColor(Color.red); 
        for(int i=0; i<numberOfBands; i+= 50) { 
            g.drawLine(i+xOffset,18,i+xOffset,21); 
            g.drawLine(i+xOffset,windowHeight-84+yOffset+3,i+xOffset,windowHeight-84+yOffset+6); 
        } 
 
        //Mostrar la frecuencia siguiendo al puntero del raton// 
        if (posX>=xOffset && posX<=windowWidth){ //para no salirse de los margenes establecidos// 
          g.drawString("" +visualiza_frec + "Hz", posX, posY); 
          //Mostrar linea vertical cuyas coordenadas de inicio y fin son los siguientes dos puntos: 
          //  origen(posX, 0) 
          //  fin(posX, windowHeight) 
          g.drawLine(posX,0,posX,windowHeight); 
        } 
    } 
 
    public void componentHidden(ComponentEvent e) { 
    } 
 
    public void componentMoved(ComponentEvent e) { 
    } 
 
    public void componentResized(ComponentEvent e) { 
        Component c = e.getComponent(); 
        windowHeight = c.getSize().height; 
        //System.err.println(c.getSize().width + " " + c.getSize().height); 
    } 
 
    public void componentShown(ComponentEvent e) { 
    } 
 
    public static void main(String[] args) throws Exception { 
        int numberOfBands = NUMBER_OF_BANDS; 
        float sampleRate = SAMPLE_RATE; 
        int step = STEP; 
        if(args.length>=1) { 
            try { 
                numberOfBands = new Integer(args[0]).intValue(); 
            }  catch (NumberFormatException e) { 
                System.err.println("Errorparsing\"" + args[1] + "\""); 
            } 
        } 
        if(args.length>=2) { 
            try { 
                sampleRate = new Float(args[1]).floatValue(); 
            }  catch (NumberFormatException e) { 
                System.err.println("Errorparsing\"" + args[2] + "\""); 
            } 
        } 
        if(args.length>=3) { 
            try { 
                step = new Float(args[2]).intValue(); 
            }  catch (NumberFormatException e) { 
                System.err.println("Errorparsing\"" + args[3] + "\""); 
            } 
        } 
        RTASA analizador = new RTASA(numberOfBands, sampleRate, step); 
        analizador.start(); 
    } 
}

5.5 ElasticityControl.java

 
import java.awt.*; 
import java.awt.event.*; 
import javax.swing.*; 
import javax.swing.event.*; 
import javax.swing.text.NumberFormatter; 
import java.beans.*; 
import java.util.*; 
 
/** 
 * Esta clase maneja la elasticidad del espectro azul. 
 * 
 * @author Vicente Gonzalez Ruiz 
 * @version 1-Junio-2004 
 */ 
 
/* 
 * SliderDemo3.java is a 1.4 application that requires all the 
 * files in the images/doggy directory.  It adds a formatted text 
 * field to SliderDemo.java. 
 */ 
public class ElasticityControl extends JPanel implements /*ActionListener,*/ WindowListener, ChangeListener, PropertyChangeListener { 
 
    static final int ELASTICITY_MIN = 0; 
    static final int ELASTICITY_MAX = 1000; 
    static final int ELASTICITY_INIT = 50;    // Initial elasticity 
    double elasticity = (double)ELASTICITY_INIT/(double)ELASTICITY_MAX; 
    JFormattedTextField textField; 
    JSlider slider; 
 
    public ElasticityControl() { 
        // Colocamos el textField y el slider uno encima de otro 
        setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); 
 
        // Creamos el label del slider 
        JLabel sliderLabel = new JLabel("Elasticity(x1000):", JLabel.CENTER); 
        sliderLabel.setAlignmentX(Component.CENTER_ALIGNMENT); 
 
        // Create the formatted text field and its formatter. 
        java.text.NumberFormat numberFormat = java.text.NumberFormat.getIntegerInstance(); 
        NumberFormatter formatter = new NumberFormatter(numberFormat); 
        formatter.setMinimum(new Double((double)ELASTICITY_MIN/(double)ELASTICITY_MAX)); 
        formatter.setMaximum(new Double((double)ELASTICITY_MAX)); 
        textField = new JFormattedTextField(formatter); 
        textField.setValue(new Integer(ELASTICITY_INIT)); 
        textField.setColumns(3); //get some space 
        textField.addPropertyChangeListener(this); 
 
        //React when the user presses Enter. 
        textField.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),"check"); 
        textField.getActionMap().put("check", new AbstractAction() { 
            public void actionPerformed(ActionEvent e) { 
                if (!textField.isEditValid()) { //The text is invalid. 
                    Toolkit.getDefaultToolkit().beep(); 
                    textField.selectAll(); 
                } else try {                    //The text is valid, 
                    textField.commitEdit();     //so use it. 
                } catch (java.text.ParseException exc) { } 
            } 
        }); 
 
        //Create the slider. 
        slider = new JSlider(JSlider.HORIZONTAL, ELASTICITY_MIN, ELASTICITY_MAX, ELASTICITY_INIT); 
        slider.addChangeListener(this); 
 
        //Turn on labels at major tick marks. 
        slider.setMajorTickSpacing(500); 
        slider.setMinorTickSpacing(50); 
        slider.setPaintTicks(true); 
        slider.setPaintLabels(true); 
        slider.setBorder(BorderFactory.createEmptyBorder(0,0,10,0)); 
 
        // Le redefinimos las etiquetas al slider 
        Dictionary labelTable = slider.getLabelTable(); 
        JLabel cero = new JLabel("0.0"); 
        JLabel puntocinco = new JLabel("0.5"); 
        JLabel uno = new JLabel("1.0"); 
        labelTable.put(new Integer(ELASTICITY_MIN), cero); 
        labelTable.put(new Integer((ELASTICITY_MAX - ELASTICITY_MIN)/2), puntocinco); 
        labelTable.put(new Integer(ELASTICITY_MAX), uno); 
        slider.setLabelTable(labelTable); 
 
        // Create a subpanel for the label and text field. 
        JPanel labelAndTextField = new JPanel(); //use FlowLayout 
        labelAndTextField.add(sliderLabel); 
        labelAndTextField.add(textField); 
 
        // Put everything together. 
        add(labelAndTextField); 
        add(slider); 
        setBorder(BorderFactory.createEmptyBorder(10,10,10,10)); 
        //createAndShowGUI(); 
 
        //Make sure we have nice window decorations. 
        JFrame.setDefaultLookAndFeelDecorated(true); 
 
        //Create and set up the window. 
        JFrame frame = new JFrame("RTASA-ElasticityControl"); 
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
 
        //Create and set up the content pane. 
        //ElasticityControl animator = new ElasticityControl(); 
        this.setOpaque(true); //content panes must be opaque 
        frame.setContentPane(this); 
 
        //Display the window. 
        frame.pack(); 
        frame.setVisible(true); 
    } 
 
    double getElasticity() { 
        return elasticity; 
   } 
 
    /** Add a listener for window events. */ 
    void addWindowListener(Window w) {} 
 
    //React to window events. 
    public void windowIconified(WindowEvent e) {} 
    public void windowDeiconified(WindowEvent e) {} 
    public void windowOpened(WindowEvent e) {} 
    public void windowClosing(WindowEvent e) {} 
    public void windowClosed(WindowEvent e) {} 
    public void windowActivated(WindowEvent e) {} 
    public void windowDeactivated(WindowEvent e) {} 
 
    /** Listen to the slider. */ 
    public void stateChanged(ChangeEvent e) { 
        JSlider source = (JSlider)e.getSource(); 
        int elast = (int)source.getValue(); 
        elasticity = (double)elast/(double)ELASTICITY_MAX; 
        if (!source.getValueIsAdjusting()) { //done adjusting 
            textField.setValue(new Integer(elast)); 
        } else { //value is adjusting; just set the text 
            textField.setText(String.valueOf(elast)); 
        } 
    } 
 
    /** 
     * Listen to the text field.  This method detects when the 
     * value of the text field (not necessarily the same 
     * number as youd get from getText) changes. 
     */ 
    public void propertyChange(PropertyChangeEvent e) { 
        if ("value".equals(e.getPropertyName())) { 
            Number value = (Number)e.getNewValue(); 
            if (slider != null && value != null) { 
                slider.setValue(value.intValue()); 
            } 
        } 
    } 
}

5.6 FFT.java

 
 
/** 
 * This class implements the FFT (Fast Fourier Transform) routines. 
 * 
 * @author Vicente Gonzalez Ruiz. 
 */ 
public class FFT { 
    /** 
     * Constructor for objects of class FFT_1D 
     */ 
    public FFT() { 
    } 
    /** 
     * The Fast Fourier Transform (FFT). 
     * The array length must be a power of two. 
     * The array size is [L][2], where each sample is complex. 
     * array[n][0] is the real part and array[n][1] is the imaginary part of 
     * the sample n. 
     * 
     * @author URL: http://www.nauticom.net/www/jdtaft/JavaFFT.htm 
     */ 
    static void direct(double[][] array) { 
        double u_r,u_i, w_r,w_i, t_r,t_i; int ln, nv2, k, l, le, le1, j, ip, i, n; 
 
        n = array.length; 
        ln = (int)( Math.log( (double)n )/Math.log(2) + 0.5 ); 
        nv2 = n / 2; 
        j = 1; 
        for (i = 1; i < n; i++ ) { 
            if (i < j) { 
                t_r = array[i - 1][0]; 
                t_i = array[i - 1][1]; 
                array[i - 1][0] = array[j - 1][0]; 
                array[i - 1][1] = array[j - 1][1]; 
                array[j - 1][0] = t_r; 
                array[j - 1][1] = t_i; 
            } 
            k = nv2; 
            while (k < j) { 
                j = j - k; 
                k = k / 2; 
            } 
            j = j + k; 
        } 
 
        for (l = 1; l <= ln; l++) /* loops thru stages */ { 
            le = (int)(Math.exp( (double)l * Math.log(2) ) + 0.5 ); 
            le1 = le / 2; 
            u_r = 1.0; 
            u_i = 0.0; 
            w_r =  Math.cos( Math.PI / (double)le1 ); 
            w_i = -Math.sin( Math.PI / (double)le1 ); 
            for (j = 1; j <= le1; j++) /* loops thru 1/2 twiddle values per stage */ { 
                for (i = j; i <= n; i += le) /* loops thru points per 1/2 twiddle */ { 
                    ip = i + le1; 
                    t_r = array[ip - 1][0] * u_r - u_i * array[ip - 1][1]; 
                    t_i = array[ip - 1][1] * u_r + u_i * array[ip - 1][0]; 
 
                    array[ip - 1][0] = array[i - 1][0] - t_r; 
                    array[ip - 1][1] = array[i - 1][1] - t_i; 
 
                    array[i - 1][0] =  array[i - 1][0] + t_r; 
                    array[i - 1][1] =  array[i - 1][1] + t_i; 
                } 
                t_r = u_r * w_r - w_i * u_i; 
                u_i = w_r * u_i + w_i * u_r; 
                u_r = t_r; 
            } 
        } 
    } 
 
    double[][] inverse(  double[][] array ) { 
        double  u_r,u_i, w_r,w_i, t_r,t_i; 
        int     ln, nv2, k, l, le, le1, j, ip, i, n; 
 
        n = array.length; 
        ln = (int)( Math.log( (double)n )/Math.log(2) + 0.5 ); 
        nv2 = n / 2; 
        j = 1; 
        for (i = 1; i < n; i++ ) { 
            if (i < j) { 
                t_r = array[i - 1][0]; 
                t_i = array[i - 1][1]; 
                array[i - 1][0] = array[j - 1][0]; 
                array[i - 1][1] = array[j - 1][1]; 
                array[j - 1][0] = t_r; 
                array[j - 1][1] = t_i; 
            } 
            k = nv2; 
            while (k < j) { 
                j = j - k; 
                k = k / 2; 
            } 
            j = j + k; 
        } 
 
        for (l = 1; l <= ln; l++) /* loops thru stages */ { 
            le = (int)(Math.exp( (double)l * Math.log(2) ) + 0.5 ); 
            le1 = le / 2; 
            u_r = 1.0; 
            u_i = 0.0; 
            w_r =  Math.cos( Math.PI / (double)le1 ); 
            w_i =  Math.sin( Math.PI / (double)le1 ); 
            for (j = 1; j <= le1; j++) /* loops thru 1/2 twiddle values per stage */ { 
                for (i = j; i <= n; i += le) /* loops thru points per 1/2 twiddle */ { 
                    ip = i + le1; 
                    t_r = array[ip - 1][0] * u_r - u_i * array[ip - 1][1]; 
                    t_i = array[ip - 1][1] * u_r + u_i * array[ip - 1][0]; 
 
                    array[ip - 1][0] = array[i - 1][0] - t_r; 
                    array[ip - 1][1] = array[i - 1][1] - t_i; 
 
                    array[i - 1][0] =  array[i - 1][0] + t_r; 
                    array[i - 1][1] =  array[i - 1][1] + t_i; 
                } 
                t_r = u_r * w_r - w_i * u_i; 
                u_i = w_r * u_i + w_i * u_r; 
                u_r = t_r; 
            } 
        } 
        return array; 
    } /* end of ifft_1d */ 
}

5.7 Window.java

 
 
/** 
 * Write a description of class Window here. 
 * 
 * @author (your name) 
 * @version (a version number or a date) 
 */ 
public class Window { 
    /** 
     * Constructor for objects of class Window 
     */ 
    public Window() { 
    } 
 
    /** 
     * http://www.nauticom.net/www/jdtaft/JavaWindows.htm 
     */ 
    static double[] computeCoefficients( int win_type, double[] coeffs ) { 
        int n; 
        int m; 
        double twopi; 
 
        m = coeffs.length; 
        twopi = 2.*Math.PI; 
        switch( win_type ) { 
            case 1:  /* Hamming   */ 
            for( n = 0; n < m; n++ ) { 
                coeffs[n] = 0.54 - 0.46*Math.cos( twopi*n/(m-1) ); 
                coeffs[n] *= 0.5*(1. - Math.cos(twopi*n/(m-1)) ); 
            } 
            break; 
            case 2:  /* von Hann (sometimes improperly called Hanning)  */ 
            for( n = 0; n < m; n++ ) { 
                coeffs[n] = 0.5*(1.0 - Math.cos(twopi*n/(m-1)) ); 
            } 
            break; 
            case 3:  /* Blackman  */ 
            for( n = 0; n < m; n++ ) { 
                coeffs[n] = 0.42 - 0.5*Math.cos(twopi*n/(m-1)) + 
                           0.08*Math.cos(2.*twopi*n/(m-1)); 
            } 
            case 4:  /* Bartlett  */ 
            for( n = 0; n <= (m-1)/2; n++ ) { 
                coeffs[n] = 2.*n/(m-1); 
            } 
            for( n = (m-1)/2; n < m; n++ ) { 
                coeffs[n] = 2. - 2.*n/(m-1); 
            } 
            break; 
            case 5: /* 4 term Blackman-Harris  */ 
            double a0; 
            double a1; 
            double a2; 
            double a3; 
 
            a0 = 0.35875; 
            a1 = 0.48829; 
            a2 = 0.14128; 
            a3 = 0.01168; 
 
            for( n = 0; n < m; n++ ) { 
                coeffs[n] = a0 - a1* Math.cos(twopi*(double)(n+0.5)/m) + 
                             a2*Math.cos(twopi*2.*(double)(n+0.5)/m) - 
                             a3*Math.cos(twopi*3.*(double)(n+0.5)/m); 
            } 
            break; 
            default: 
            break; 
        } 
        return coeffs; 
    } 
}

5.8 YScale.java

 
 
import java.awt.*; 
import javax.swing.*; 
import java.awt.event.*; 
 
/** 
 * Esta clase maneja la escala del eje Y (potencia). 
 * 
 * @author Vicente Gonzalez Ruiz 
 * @version 28-Mayo-2004 
 */ 
public class YScale { 
    private JFrame frame; 
    private JPanel panel; 
    private JLabel scaleLabel, stepLabel; 
 
    /* Factor de escala en el eje Y */ 
    private double scale, step; 
 
    class ScaleControl extends JLabel implements MouseWheelListener { 
 
        ScaleControl(String str, int horizontalAlignment){ 
            super(str,horizontalAlignment); 
            super.addMouseWheelListener(this); 
        } 
 
        public void mouseWheelMoved(MouseWheelEvent e) { 
            int wheelRotationDir = e.getWheelRotation(); 
            if(wheelRotationDir < 0) { 
                scale -= step; 
                if(scale <= 1.0) scale = 1.0; 
            } else { 
                scale += step; 
            } 
            this.setText("Scale=" + scale); 
        } 
    } 
 
    class StepControl extends JPanel implements ActionListener { 
 
        String initialStepString; 
        String x100String = "Step=100.0"; 
        String x10String = "Step=10.0"; 
        String x1String = "Step=1.0"; 
 
        double initialStep; 
 
        StepControl(double initialStep) { 
            this.initialStep = initialStep; 
            Double initialStepObj = new Double(initialStep); 
            initialStepString = new String("Step=" + initialStepObj.toString()); 
 
            // 1. Creating the radio buttons 
            JRadioButton initialStepButton = new JRadioButton(initialStepString); 
            initialStepButton.setActionCommand(initialStepString); 
            initialStepButton.setSelected(true); 
 
            JRadioButton x100Button = new JRadioButton(x100String); 
            x100Button.setActionCommand(x100String); 
            //x100Button.setMnemonic(KeyEvent.VK_A); 
            //x100Button.setSelected(true); 
 
            JRadioButton x10Button = new JRadioButton(x10String); 
            x10Button.setActionCommand(x10String); 
 
            JRadioButton x1Button = new JRadioButton(x1String); 
            x1Button.setActionCommand(x1String); 
 
            // 2. Grouping the radio buttons 
            ButtonGroup group = new ButtonGroup(); 
            group.add(initialStepButton); 
            group.add(x100Button); 
            group.add(x10Button); 
            group.add(x1Button); 
 
            // 3. Registering a listener for the radio buttons 
            initialStepButton.addActionListener(this); 
            x100Button.addActionListener(this); 
            x10Button.addActionListener(this); 
            x1Button.addActionListener(this); 
 
            // 4. Put the radio buttons in a column in a panel 
            JPanel radioPanel = new JPanel(new GridLayout(0,1)); 
            radioPanel.add(initialStepButton); 
            radioPanel.add(x100Button); 
            radioPanel.add(x10Button); 
            radioPanel.add(x1Button); 
 
            add(radioPanel, BorderLayout.LINE_START); 
            setBorder(BorderFactory.createEmptyBorder(10,10,10,10)); 
        } 
 
        // Listens to the radio buttons 
        public void actionPerformed(ActionEvent e) { 
            String choice = e.getActionCommand(); 
            if(choice == x100String) { 
                step = 100; 
            } else if(choice == x10String) { 
                step = 10; 
            } else if(choice == x10String) { 
                step = 1; 
            } else { 
                step = initialStep; 
            } 
        } 
    } 
    /** 
     * Constructor for objects of class YScale 
     */ 
    public YScale(double scale, double step/*, JFrame panel*/) { 
        this.scale = scale; 
        this.step = step; 
 
        // Create and set up the window 
        frame = new JFrame("RTASA-Y-ScaleControl"); 
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
        frame.setSize(100,50); 
 
        // Create and set up the panel 
        panel = new JPanel(new GridLayout(1,0)); 
 
        // Create and add the widgets 
        Double scaleObj = new Double(scale); 
        Double stepObj = new Double(step); 
        ScaleControl scaleLabel2 = new ScaleControl("Scale=" + scaleObj.toString(), SwingConstants.LEFT); 
        StepControl stepLabel = new StepControl(step); 
        panel.add(scaleLabel2); 
        panel.add(stepLabel); 
 
        // Add the panel to the window 
        frame.getContentPane().add(panel, BorderLayout.CENTER); 
 
        // Display the window 
        frame.pack(); 
        frame.setVisible(true); 
 
        //Register for mouse-wheel events on the text area. 
        //scaleLabel.addMouseWheelListener(this); 
    } 
 
    /** 
     * An example of a method - replace this comment with your own 
     * 
     * @param  y   a sample parameter for a method 
     * @return     the sum of x and y 
     */ 
    public double getScale() { 
        return scale; 
    } 
 
}

6 Sobre la compilacin

Como viene siendo habitual, usaremos la utilidad make para facilitar esta tarea.

6.1 Compilacin de RTASA

 
%.class: %.java 
        javac $*.java 
 
RTASA.jar: RTASA.class 
        jar cvfm RTASA.jar meta-inf/manifest.mf -C . *.class 
 
EXE = RTASA.jar 
 
all:  $(EXE) 
 
jar:  all 
        jar cvfm RTASA.jar meta-inf/manifest.mf -C . *.class 
 
clean: 
        rm -f *.class $(EXE) 
 
test:  jar 
        arecord -f cd | java -jar RTASA.jar 512 44100 
 
install:      $(EXE) 
        cp $(EXE) ~/bin 
 
publish: 
        rm -rf /tmp/RTASA 
        svn export . /tmp/RTASA 
        tar --create --file=/tmp/RTASA.tar -C /tmp RTASA 
        gzip -9 /tmp/RTASA.tar 
        scp /tmp/RTASA.tar.gz www.ace.ual.es:~/public_html/imyso 
        rm /tmp/RTASA.tar.gz

7 Ejemplos de utilizacin

  1. Visualizando el espectro de la seal que captura la tarjeta de sonido:
    make all; make jar; make test

  2. Capturando audio y viendo simultaneamente su espectro:
    arecord -t raw -f cd > file.raw &  
    arecord -t raw -f cd | java RTASA