martes, 15 de diciembre de 2009

Graficador de espectros de audio

El procesamiento digital de señales es un área de la ingeniería que se dedica al análisis y procesamiento de señales (audio, voz, imágenes, video) que son discretas. Aunque comúnmente las señales en la naturaleza nos llegan en forma analógica, también existen casos en que estas son por su naturaleza digitales, por ejemplo, las edades de un grupo de personas, el estado de una válvula en el tiempo (abierta/cerrada), etc.
Se puede procesar una señal para obtener una disminución del nivel de ruido, para mejorar la presencia de determinados matices, como los graves o los agudos y se realiza combinando los valores de la señal para generar otros nuevos.[1] El procesamiento digital de audio es un tipo de procesamiento digital de señales especializado en el tratamiento de la señal de audio.Una señal digital no es audible, ya que requiere ser decodificada antes de su reproducción.[2]

En este post vamos a hablar acerca de la graficación del espectro de audio de un archivo *.wav o *.aif. Pero antes de explicar cómo se realiza la graficación de audio en java, tenemos que tener en cuenta una serie de conceptos que nos ayudarán a comprender mejor la esencia de este proceso.

Primero voy a definir lo que a partir de ahora será Audio Digital.

AUDIO DIGITAL: Proceso de representar una señal de audio como un flujo de
datos numéricos para efectos de almacenamiento, procesamiento o
transmisión. De esta manera prácticamente se elimina la posibilidad de
ruidos o errores en la lectura.

Para poder graficar un archivo de audio lo primero que tenemos que hacer es acceder a los datos numéricos que representan al fichero de audio como una estructura digital. Para eso tendremos que leer el archivo como indico en el siguiente segmento de código:

int totalFramesLeidos = 0;
int totalBytesLeidos = 0;
try {
    AudioInputStream flujoEntradaAudio = AudioSystem.getAudioInputStream(fichero);
    int bytesPorFrame = flujoEntradaAudio.getFormat().getFrameSize();
    int numBytes = 1024 * bytesPorFrame;
    byte[] audioBytes = new byte[numBytes];
    // longitud de archivo de audio en bytes
    int longitudArchivoBytes=(int)flujoEntradaAudio.getFormat().getFrameSize()*(int)flujoEntradaAudio.getFrameLength();
    // objeto datos mas informacion ver clase Datos.java
    Datos datos = new Datos(longitudArchivoBytes,flujoEntradaAudio.getFormat().isBigEndian());
    byte[] datosTemporal=new byte[longitudArchivoBytes];
    int pos=0;
    /* el siguiente procedimiento lee los bytes del archivo de audio a memoria */
    try {
        int numeroBytesLeidos = 0;
        int numeroFramesLeidos = 0;
        while ((numeroBytesLeidos = flujoEntradaAudio.read(audioBytes)) != -1) {
            numeroFramesLeidos = numeroBytesLeidos / bytesPorFrame;
            totalFramesLeidos += numeroFramesLeidos;
            System.arraycopy(audioBytes, 0, datosTemporal, pos, numeroBytesLeidos);
            pos=pos+numeroBytesLeidos;
        }
        datos.llenarByte(datosTemporal);
        //datosVoz=new double[longitudArchivoBytes/bytesPorFrame];
        datosVoz = new ArrayList<Double>();
        datosVoz=datos.convertirByteADouble(longitudArchivoBytes/bytesPorFrame);
        frecuency.setMaximum((int) flujoEntradaAudio.getFrameLength()/100);
        int min = frecuency.getMinimum();
        int max = frecuency.getMaximum();
        frecuency.setValue((min+max)/2);
        ((PanelDeslizable)newContentPane).setIntervalo(frecuency.getValue());
        if(flujoEntradaAudio.getFormat().isBigEndian()) {
            System.out.println("BigEndian");
        } else {
            System.out.println("LittleEndian");
        }
        INFORMACION = flujoEntradaAudio.getFormat().toString();
        //obtiene la frecuencia de sampleo
        FRECUENCIA=(int)flujoEntradaAudio.getFormat().getSampleRate();
        ((PanelDeslizable)newContentPane).setDatos(datosVoz);
        ((PanelDeslizable)newContentPane).repaint();
    } catch (Exception ex) {
    }
}catch(Exception e) {
}

Bueno, en el segmento de código anterior, lo primero que se hace es leer el archivo de audio, luego ir leyendo por frames para poder tener la data en un vector el cual será luego procesado de acuerdo a lo que se quiera obtener.Con eso será suficiente para poder acceder al vector de datos numéricos o sample (Cada uno de los valores que se obtienen del proceso de sampleo. Cada una está cuantizada en una cantidad determinada de bits) que representan un archivo de audio. Se observa también la presencia de una clase, la clase Datos la cuál se encarga de realizar las conversiones respectivas del tipo de datos puesto que un archivo wav es un archivo binario y nosotros procesaremos la data con un tipo de dato más adecuado (Double por ejemplo).

El código que muestro a continuación pertenece a esta clase Datos:

package com.blogspot.rolandopalermo.util;
import java.util.ArrayList;
import java.util.List;
public class Datos {
    byte[] bits ;
    boolean formato;  // para el formato bigEndian y littleEndian
    double mayor, menor;

    public  Datos(int tamano, boolean formato) {
        bits = new byte[tamano];
        this.formato = formato;
        mayor = 0;
        menor = 0;
    }

    public void llenarByte(byte[] bits) {
        this.bits = bits;
    }

    /* ejemplo bits[2]=2 (00000010) bits[3]=3 (00000011)
     se aplica bits[2]<<8 o sea 10 00000000 , luego 11111111(0x000000FF) & bits[3]|bits[2]
     en total da 10 00000011  que es el numero 515 , este es un short de 16 bits , han entrado
     dos bytes en uno (short[i]=contacenar byte[i]+byte[i+1])
     los valores negativos estan en complemento a 2
     */

    public List<Double> convertirByteADouble(int size) {
        List<Double> arrayDouble = new ArrayList<Double>();
        //double[] arrayDouble = new double[bits.length/2];
        if (formato==true) {
            int temp = 0x00000000;
            for (int i = 0, j = 0; j < size ; j++, temp = 0x00000000) {
                temp=(int)bits[i++]<<8;//System.out.println("temp = "+ temp);
                temp |= (int) (0x000000FF & bits[i++]);
                //arrayDouble[j]=(double)temp;
                arrayDouble.add(j, (double)temp);
            }
            return arrayDouble;
        }
        if(formato==false) {  // si el formato es littleEndian
            int temp = 0x00000000;
            for (int i = 0, j = 0; j < size ; j++, temp = 0x00000000) {
                temp=(int)bits[i+1]<<8;//System.out.println("temp = "+ temp);
                temp |= (int) (0x000000FF & bits[(i)]);
                i=i+2;
                arrayDouble.add(j, (double)temp);
                //calcular mayor y menor esto me servira para establecer
                //los parametros en el eje y para la grafica
                if(mayor<arrayDouble.get(j)) {
                    mayor=arrayDouble.get(j);
                }
                if(menor>arrayDouble.get(j)) {
                    menor=arrayDouble.get(j);
                }
            }
            return arrayDouble;
        } else {
            System.out.println("Orden de Bytes desconocido o no soportado");
        }
        return arrayDouble;
    }
}

La forma de graficarlos dependerá de las necesidades que se tenga así como de otros factores como por ejemplo:

FRECUENCIA DE SAMPLEO O MUESTREO: Velocidad, en Hertz, a la cual se extraen las muestras de una señal de audio. Tiene directa relación con el ancho de banda que se obtendrá. Mientras mayor sea la Frecuencia de sampleo, mejor es la respuesta de agudos.

CUANTIZACIÓN: Es el proceso de asignar a una escala "redondeando al peldaño más cercano" el valor de cada muestra. Esta cantidad está determinada por el número de bits utilizados. La cuantización tiene directa relación con el rango dinámico (nivel de ruido) de la señal.

Evidentemente todos estos valores tienen que estar presentes como parámetros al momento de graficar el espectrograma que representará al archivo de audio. Yo recomiendo sean atributos de la clase encargada de mostrar los datos en modo gráfico. En el caso del proyecto que realizé, esta clase tiene el valor de PanelDeslizable (Deslizable puesto que si el gráfico es más grande que la pantalla entonces automáticamente aparecerá un scroll que nos permitirá ubicarnos en la posición que deseemos). El código de esta clase la muestro a continuación:

package com.blogspot.rolandopalermo.gui;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.List;
import javax.imageio.ImageIO;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.Timer;

//@author Rolando

public class PanelDeslizable extends JPanel {

    private Dimension area; //indicates area taken up by graphics
    //private Vector<Rectangle> circles; //coordinates used to draw graphics
    private JPanel drawingPane;
    private Timer reloj;
    private List<Double> datos;
    private int w;
    private int h;
    private static int x0 = 15;
    private int y0;
    private int intervalo;
    private int temporizador;
    private int fin;
    private double  escalaX;
    private double escalaY;
    private Color gridcolor;
    private Color ejescolor;
    private Color texto;
    private Color espectrogramac;
    private Color background;
    private Color player;
    private boolean dibujargrid;
    private boolean dibujarejes;
    private boolean dibujarespectrograma;
    private String informacion;

    public PanelDeslizable(final int w, final int h) {
        super(new BorderLayout());

        background = Color.BLACK;
        gridcolor = new Color(120, 176, 145);
        ejescolor = Color.YELLOW;
        texto = Color.YELLOW;
        player = Color.RED;
        //espectrogramac = new Color(85, 255, 255);
        espectrogramac = Color.GREEN;
        
        area = new Dimension(0,0);

        //Set up the drawing area.
        drawingPane = new DrawingPane();
        drawingPane.setBackground(background);
        //drawingPane.addMouseListener(this);

        //Put the drawing area in a scroll pane.
        JScrollPane scroller = new JScrollPane(drawingPane);
        scroller.setPreferredSize(new Dimension(200,200));

        add(scroller, BorderLayout.CENTER);

        datos = null;

        this.w  = w;
        this.h  = h;
        this.y0 = h/2;

        intervalo = 1;

        temporizador = 0;
        fin = 0;

        escalaX = 1;
        escalaY = 1;

        dibujargrid = true;
        dibujarejes = true;
        dibujarespectrograma = false;

        informacion = null;

        reloj = new Timer (1, new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                temporizador = temporizador + 1;
            }
        });

    }

    /** The component inside the scroll pane. */
    class DrawingPane extends JPanel {
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            w  = getSize().width;
            h  = getSize().height;
            y0 = h/2;
            if(dibujargrid) {
                graficarGrid(g);
            }
            if(dibujarejes) {
                graficarEjes(g);
            }
            if(dibujarespectrograma) {
                graficarEscpectrograma(g);
            }
            if(informacion!=null) {
                g.setColor(texto);
                g.drawString(informacion, 20, 20);
            }
        }
    }

    public void graficarGrid(Graphics g) {
        g.setColor(gridcolor);
        for( int i=0; i < w; i+=15) {
            for(int j=0; j<h; j+=40) {
                g.drawLine(i,j,i,h);
                g.drawLine(i,j,w,j);
            }
        }
    }

    public void graficarEjes(Graphics g) {
        g.setColor(ejescolor);
        linea(0,0,w,0,g);
        linea(0,y0,0,-y0,g);
    }

    public void graficarEscpectrograma(Graphics g) {
        g.setColor(espectrogramac);
        int length = datos.size();
        int[] puntoP = new int[2];
        int i, xi=0,xf=0,yi=0,yf=0;
        for(i=0; i<length-intervalo; i+=intervalo) {
            puntoP[0]=(int) (i * escalaX);
            puntoP[1]=(int) (datos.get(i) * escalaY);
            xi = puntoP[0];
            yi = puntoP[1];
            puntoP[0]=(int) ((i + intervalo) * escalaX);
            puntoP[1] = (int) (datos.get(i+intervalo) * escalaY);
            xf = puntoP[0];
            yf = puntoP[1];
            linea(xi, yi, xf, yf, g);
        }
        fin = xf;
        if(xf>w) {
            area.width = xf+100;
            drawingPane.setPreferredSize(area);
            drawingPane.revalidate();
        }
    }
  
    public void iniciarReproduccion() {
        //setTocar(true);
        temporizador = 0;
        reloj.start();
    }

    public void detenerReproduccion() {
        reloj.stop();
    }

    public void reiniciarReproduccion() {
        temporizador = 0;
        reloj.stop();
    }

    public void save(File fichero) {
        BufferedImage imagen = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        Graphics g = imagen.getGraphics();
        drawingPane.paint(g);
        // Escribimos la imagen en el archivo.
        try {
            ImageIO.write(imagen, "jpg", fichero);
        } catch (IOException e) {
            System.out.println("Error de escritura");
        }
    }

    public void ajustarDimension() {
        area.width=0;
        drawingPane.setPreferredSize(area);
        drawingPane.revalidate();
    }

    public void setDibujargrid(boolean dibujargrid) {
        this.dibujargrid = dibujargrid;
    }

    public void setGridcolor(Color gridcolor) {
        this.gridcolor = gridcolor;
    }

    /**
    *  Dibuja una linea desde (x1,y1) hasta (x2,y2).
    *  @param x1 Coordenada x del punto de origen.
    *  @param y1 Coordenada y del punto de origen.
    *  @param x2 Coordenada x del punto de destino.
    *  @param y2 Coordenada y del punto de destino.
    */
    public void linea(double x1, double y1, double x2, double y2, Graphics g) {
        g.drawLine((int)Math.round(x1+x0),
                (int)Math.round(y0-y1),
                (int)Math.round(x2+x0),
                (int)Math.round(y0-y2));
    }

    public void setInformacion(String informacion) {
        this.informacion = informacion;
    }

    public void setDibujarejes(boolean dibujarejes) {
        this.dibujarejes = dibujarejes;
    }

    public void setDibujarespectrograma(boolean dibujarespectrograma) {
        this.dibujarespectrograma = dibujarespectrograma;
    }

    public void setDatos(List<Double> datos) {
        this.datos = datos;
    }

    public void setIntervalo(int intervalo) {
        this.intervalo = intervalo;
    }

    public void setEscalaY(double escalaY) {
        this.escalaY = escalaY;
    }

    public void setEscalaX(double escalaX) {
        this.escalaX = escalaX;
    }
}

Los archivos que se pueden procesar de esta forma pueden tener el formato WAV o AIF como se había mencionado anteriormente. Acá les dejo algunos screenshots de la aplicación que he escrito en java para graficar espectros de audio:

En Windows:

Photobucket

Photobucket

Photobucket

Photobucket

En Linux:
Photobucket

Photobucket

En la imágen que muestro a continuación se aprecia el árbol de clases del proyecto el cuál fué realizado en netbeans:


A continuación dejo el código fuente para que lo puedan leer y si es posible continuar con el trabajo puesto que hay muchas cosas por mejorar.

Photobucket

Y aquí algunos ejemplos para que puedan probar el programa

Photobucket

Pueden también visitar el blog un gran amigo, Jorge Valverde, quien es un destacado investigador. Ahí podrán encontrar una explicación un poco más detallada de este proceso.

23 comentarios:

Jorge Valverde Rebaza dijo...

Muy buen trabajo Rolando, me gusta bastante que trabajes en esto (y) gracias por el reconocimiento al trabajo que hago en esta área, saludos

Anónimo dijo...

Lo que más me sorprende es que sin llevar el curso hayas implementado este Graficador, me parece muy interesante tu trabajo y espero que lo continúes ya que este tipo de programas son de mucha utilidad hoy en día.

Anónimo dijo...

ahh me olvidaba está muy bonita la interfaz

Anónimo dijo...

Hola amigo, que tal muy buen trabajo, descargue el codigo fuente, pero me marca error en la clase principal, me podrias ayudar, mi msn es jairo_mcs@hotmail.com

Anónimo dijo...

EXELENTE APORTE!!!!!!!!!
Se ve genial pero tuve el mismo probelma q #4 anonimo un error en Principal.java
nose si m puedes ayudar
o el motivo de este error
symbol: class SubstanceGraphiteAquaLookAndFeel
te agradeseria si contestas

Rolando dijo...

Es solo el lookandfeel amigo. Desactívalo en la clase principal: Principal.java

Anónimo dijo...

gracias por el aporte
esta exelente
ERES LO MAXIMO 100% funciona
#4 Anonimo solo importa la libreria y ya esta

Anónimo dijo...

perdona pero por q no sale el ela escala de la nueva musica q le puse deve llevar algo mas

Anónimo dijo...

perdona pero por que motivo no sale las escalas de la musica acaso solo sirven colas que tu posteaste.
o deven llevar algun formato diferente a .wav

Rolando dijo...

os archivos que se pueden procesar de esta forma pueden tener el formato WAV o AIF como se había mencionado anteriormente. Y no deben ser ficheros pesados.

4st4r0th dijo...

Estimado Rolando, Antes que todo quería agradecer tu buena voluntad para publicar tus codigos.

Segundo, tengo una duda que creo que tu podrías resolverme. Me gustaría implementar la posibilidad de indicar cuando al analizar el archivo de sonido me indicara en que partes alcanza el rango mas alto de la escala(los sonidos mas fuertes) y cuando al alcanza el minimo (los sonidos mas bajos), osea, los que estan por sobre y bajo la media.
Por ejemplo, si se analiza y en 3 partes encuentra escalas altas indicarlo de alguna manera.
Mis conocimientos de Java son bastante limitados por eso te pido esta ayuda.
De antemano, muchisimas gracias.

Rolando dijo...

Gracias por tu comentario; es grato saber que los proyectos que posteo son de utilidad. Respondiendo a tu pregunta para determinar el rango más alto y el más bajo puedes hacer un algoritmo de ordenamiento a la lista "List datos". Con eso tendrías la información que requieres.

Saludos.

Anónimo dijo...

hola, me marca error tu archivo .rar, tiene clave o algo??

J.D.N dijo...

Execelmte aporte amigo pero tengo una duda es posible con estas librerias poder procesar sonido de tal manera que se pueda comparar dos archivos un que este ya guardado y otro que ingres por un microfono y comparar ambos si tienen similitud tengo una guia con lo que proyectaste y es comparando la informacion y utlizar algo de estadistica con lo svalores, alguna ayuda mi mail es arsc86@gmail.com

Anónimo dijo...

Hola Rolando que gran trabajo! yo estoy buscando alg similar, como podria leer los valores de una placa de audio via usb, para comparar la entrada de 3 microfonos en el tiempo!! graciass!! mi mail: diemg_87@hotmail.com

Rolando Palermo Rodríguez Cruz dijo...

Hola, gracias por tu comentario. Te recomiendo que revises este post: http://blog.rolandopalermo.com/2012/10/java-usb-pic-conversor-analogico-digital.html en donde explico la manera de cómo usar un microcontrolador PIC con Java.

Saludos.

JeSs dijo...

Hola Rolando, mi nombre es Jessica, agradezco tu aporte, me ayuda mucho para una investigacion :) Gracias totales, en definitiva guardare tu referencia para futuras consultas!

Rolando Palermo Rodríguez Cruz dijo...

Gracias Jessica por tus palabras, que gusto me da que esto sea de utilidad.
Saludos.

Anónimo dijo...

hola rolando. tengo un problema, en el principal.java me sale error en la linea
import org.jvnet.substance.api.skin.SubstanceGraphiteAquaLookAndFeel;
dice que no encuentra el paquete o no existe.. que puede ser?

Anónimo dijo...

disculpa yo tengo el mismo problema que algunos de los que estamos comentando, en la parte de import org.jvnet.substance.api.skin.SubstanceGraphiteAquaLookAndFeel me marca error, supongo que es por que no tengo la libreria, no sabes donde puedo conseguirla?, si serias tan amable de proporcionarmela, soi un estudiante de ingenieria en tics y nesesito entender las señales de sonido atravez de un programa asi.

Anónimo dijo...

ya encontre como hacerlo funcionar, oye no das cursos?. me gusto un buen tu programa, sube la explicacion en un video a youtube. eres genial.

Anónimo dijo...

Rolando estoy trabajando para hacer algunas modificaciones, Y necesito que el graficador consuma la información desde un Pic USb ya tengo todas las librerpia sfuncionales solo necesito que grafique en tiempo real la data que recibe desde el usb.

Unknown dijo...

No logro reproducir archivos .mp3, y los .wav no muestra la gráfica

Publicar un comentario