Un vúmetro

Vicente González Ruiz

June 24, 2014

Contents

1 El clipping
2 Cmo evitar el clipping?
3 Programacin en Java
 3.1 Estructura de un programa escrito en Java
 3.2 Compilacin y ejecucin de un programa escrito en Java
4 La entrada y la salida estndar
5 El programa VolMeter
 5.1 Entradas
 5.2 Controles
 5.3 Salidas
 5.4 VolMeter.java
6 Sobre la compilacin
 6.1 Compilacin de VolMeter
7 Ejemplos de utilizacin

En esta prctica diseamos un medidor de volumen sencillo. El volumen de una seal de audio est directamente relacionado con la intensidad de seal sonora. Vamos, a mayor volumen, mayor intensidad.

1 El clipping

Conocer el valor del volumen instantneo puede ser til para saber si estamos provocando un clipping de la seal digitalizada al salirnos del rango dinmico del quantificador. El clipping es una forma indeseable de distorsin que provoca una gran cantidad de altas frecuencias.

2 Cmo evitar el clipping?

Suponiendo que estamos usando un micrfono para registrar un sonido, para evitar el clipping podemos hacer dos cosas. La primera es alejar el micrfono de la fuente de sonido y la segunda reducir la ganancia del canal de entrada (al que tenemos conectado el micrfono). En ambos casos el resultado ser semejante (si no hay ruido sonoro en el entorno del micrfono).

3 Programacin en Java

Java es un lenguaje de programacin creado por Sun Microsystems Inc.. Su invencin y uso son relativamente recientes, y se utiliza fundamentalmente porque su vasta biblioteca de objetos son capaces de realizar una gran cantidad de tareas. En nuestro caso vamos a aprovecharnos de su potencia para el diseo de interfaces grficos.

3.1 Estructura de un programa escrito en Java

Java es un lenguaje que sigue la filosofa de la programacin orientada a objetos. De hecho, en Java casi todo es un objeto excepto los tipos de datos primitivos (que no lo son por motivos de eficiencia).

Un objeto es una coleccin de estructuras de datos (simples o compuestas por otras) y un conjunto de mtodos. Adems, en Java es muy corriente usar la herencia, por lo que estos mtodos (funciones miembro) y estructuras de datos pueden estar declaradas en algn punto superior dentro de la jerarqua de clases.

Los objetos (instancias de las clases) se crean siempre de forma dinmica y se llaman unos a otros (o mejor dicho, desde los mtodos de unos se invocan a los mtodos de otros), comenzando siempre por el que contiene el mtodo main. Por tanto, un programa escrito en Java tiene la siguiente estructura genrica:

/* public -> clase heredable. */  
/* final -> Clase no heredable. */  
/* abstract -> Clase no instanciable por tener mtodos abstractos. */  
/* static -> Clase no instanciable. */  
/* strictfp -> Clase con aritmtica en punto flotante determinstica. */  
/* interface -> Clase declara en realidad un interface (todos los mtodos abstract). */  
class ClasePrincipal extends ClaseHeredada implements Interface {  
 
  public variablesAccesiblesDesdeObjetosInstanciados;  
 
  protected variablesInaccesiblesDesdeObjetosInstanciados;  
 
  private variablesSoloAccesiblesMetodosClasePrincipal;  
 
  static variablesComunesTodasInstancias;  
 
  final constantes;  
 
  /* Declaracin de otras clases anidadas. */  
 
  ClasePrincipal() {  
    /* this -> Instancia del objeto actual. */  
    /* supper -> Instancia del objeto padre. */  
    /* Constructor por defecto. */  
  }  
 
  ClasePrincipal(/* Argumentos 1. */) {  
    /* Constructor 1. */  
  }  
 
  ClasePrincipal(/* Argumentos 2. */) {  
    /* Constructor 2. */  
  }  
 
  /* abstract -> Mtodo sin implementacin. */  
  /* final -> Mtodo no sobreescribible. */  
  /* static -> Mtodo que manipula variables static .*/  
  /* synchronized -> Mtodo ejecutado en exclusin mtua. */  
  /* native -> Mtodo llama a cdigo nativo. */  
  /* strictfp -> Mtodo utiliza aritmtica en punto flotante determinstica. */  
  public metodo1ClasePrincipal(/* Argumentos */) {  
  }  
 
  public static void main(java.lang.String[] args) {  
    /* Primera funcin ejecutada. Slo existe una funcin main(). */  
    try {  
      /* Cdigo. */  
    } catch (/* Instancia objeto manipulador Excepcin 1. */) {  
      /* Cdigo menejo excepcin 1. */  
    } catch (/* Instancia objeto manipulador Excepcin 2. */) {  
      /* Cdigo menejo excepcin 2. */  
    } finally {  
      /* Ms cdigo que puede lanzar otras excepciones. */  
    }  
  }  
}

En Java se referencia a una clase dentro de la jerarqua de clases usando la directiva import. Esto tiene que hacerse para cada clase referenciada, si es que esta no se define en el directorio en el que se ejecuta la aplicacin o en la variable de entorno que indica qu directorios debe recorrer la mquina virtual a la hora de ejecutar la aplicacin.

3.2 Compilacin y ejecucin de un programa escrito en Java

Para el que nunca ha programado en Java este apartado le ser de utilidad. La teora es bastante simple: (1) escribir el programa segn la sintaxis del programa, (2) compilarlo y (3) ejecutarlo. Bueno, hasta aqu todo bastante sencillo. Sin embargo, hay que tener en cuenta que realmente:

La razn de usar una mquina virtual es que as, el cdigo code-byte se puede ejecutar en cualquier computadora que disponga de una mquina de Java. De esta forma generamos cdigo “ejecutable” totalmente portable.

4 La entrada y la salida estndar

Java (al igual que el C o el C++, por ejemplo) puede manejar dos flujos de datos llamados entrada y salida estndar. Estos flujos se redirigen entre procesos a nivel del shell de Unix usando el smbolo |, llamado normalmente “pipe”. Dicho smbolo indica que vamos a conectar la salida estndar de un proceso (el que se referencia a la izquierda del pipe) a la entrada estndar de otro (el que se llama a la derecha). Los pipes son canales de comunicacin entre procesos buffereados lo que significa que la transferencia se produce por bloques de datos de un determinado tamao (4 Kbytes tpicamente), aunque nosotros transfiramos bloques de un tamao diferente.

5 El programa VolMeter

El medidor de volumen es un programa escrito en Java llamado VolMeter. Es un bucle sin fin que lee la entrada estndar y calcula el valor mximo (en valor absoluto) para cada canal de un conjunto de muestras. Este valor mximo es el que se utiliza para pintar las columnas con los volmenes.

5.1 Entradas

VolMeter 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

VolMeter slo acepta un parmetro de entrada desde la lnea de comandos: el tamao del buffer de audio. Con l controlamos el nmero de muestras a las que se le calcula el mximo antes de calcular el valor del volumen.

5.3 Salidas

La nica salida del programa es una ventana con una doble columna con el volumen de cada canal de la seal. La ventana es redimensionable.

5.4 VolMeter.java

 
/* 
 * Crea una ventana redimensionable en la que aparecen dos columnas 
 * que miden el volumen maximo registrado en dos canales de audio, a 
 * 16 bits por muestra y usando el endian de la maquina. 
 * 
 * Los datos son leidos de la entrada estandar. 
 * 
 * gse. 2006, 2009. 
 */ 
 
/* Evento de manipulacion de excepciones de entrada/salida. */ 
import java.io.IOException; 
 
/* Un JComponent implementa las funciones mas basicas de cualquier 
 * objeto de la biblioteca Swing. En este caso se utiliza para pintar 
 * el medidor de volumen. */ 
import javax.swing.JComponent; 
 
/* Un Frame es una ventana con titulo y marco. */ 
import javax.swing.JFrame; 
 
/* Almacena el contexto grafico. */ 
import java.awt.Graphics; 
 
/* Define un color. */ 
import java.awt.Color; 
 
public class VolMeter 
    extends JComponent { 
 
    /* Muestras pintadas (maximo de cada canal). */ 
    int leftMax, rightMax; 
 
    /* Para mantener un hisorico temporal del volumen en cada canal. */ 
    int leftSand = 0, rightSand; 
    double elasticity = 0.1; 
 
    /* Anchura y altura por defecto de la ventana. */ 
    static final int WINDOW_WIDTH = 100; 
    static final int WINDOW_HEIGHT = 500; 
 
    /* Tasa de muestreo por cada canal. Si la tasa usada difiere de 
     * esta, el calculo "Refresing time" no sera exacto. */ 
    static final double SAMPLING_RATE = 44100.0; 
 
    /* Tama~no por defecto del buffer de audio, medido en bytes. Si 
     * usamos 44100 muestras por segundo, 2 canales y 2 bytes/muestra, 
     * tardariamos BUF_SIZE/4/44100 segundos en rellenarlo (y 
     * en pintar las columnas del medidor de volumen). */ 
    static final int BUF_SIZE = 22050/*11025*/; 
 
    /* Tama~no del buffer de audio. */ 
    int buf_size; 
 
    /* El buffer de audio. */ 
    byte[] audio_buf; 
 
    /* Tasa de muestreo (de cada canal). */ 
    int sampling_rate; 
 
    /* Crea la ventana e inicializa las variables miembro. Finalmente 
     * lanza el resto de la aplicacion que se encuentra almacenada en 
     * el metodo "run()". */ 
    public VolMeter(int buf_size, double sampling_rate) { 
        JFrame frame = new JFrame("VolMeter"); 
        frame.getContentPane().add(this); 
        frame.setSize(WINDOW_WIDTH, WINDOW_HEIGHT); 
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
        frame.setVisible(true); 
        this.buf_size = buf_size; 
        audio_buf = new byte[buf_size]; 
        System.err.println("Buffersize:" + buf_size + "bytes"); 
        System.err.println("Samplingrate:" + sampling_rate + "Hz"); 
        System.err.println("Refresingtime:" + (double)buf_size/4/SAMPLING_RATE + "seconds(" + 1.0/((double)buf_size/4/SAMPLING_RATE) + "framespersecond)"); 
        run(); 
    } 
 
    /* Lee muestras desde la entrada estandar y las almacena en un 
     * buffer. A continuacion se calcula el maximo del valor absoluto 
     * de cada muestra, por cada canal. */ 
    public void run() { 
        try { 
            for(;;) { 
                /* Cargamos el buffer de datos. */ 
                int bytes_read = 0; 
                while(bytes_read < buf_size) { 
                    bytes_read += System.in.read(audio_buf, bytes_read, buf_size-bytes_read); 
                } 
                /* 
                 * Codigo alternativo para la carga del buffer de 
                 * datos. Seria valido siempre y cuando no se usen 
                 * grandes buffers porque debido al tama~no maximo de 
                 * los pipes, no es nunca superior a 22048. 
                 */ 
                //int bytes_read = System.in.read(audio_buf); 
 
                /* Para depurar ... */ 
                //System.err.println(bytes_read); 
 
                /* Por si quisieramos continuar el pipe. */ 
                //System.out.write(audio_buf, 0, bytes_read); 
 
                /* Calulamos el maximo de cada canal. */ 
                leftMax = rightMax = 0; 
                for(int i=0; i</*buf_size*/bytes_read/4; i+=4) { 
                    int left = (int)(audio_buf[i*2+1])*256 + (int)(audio_buf[i*2]); 
                    int right = (int)(audio_buf[i*2+3])*256 + (int)(audio_buf[i*2+2]); 
                    int absLeft = Math.abs(left); 
                    int absRight = Math.abs(right); 
                    if(leftMax<absLeft) leftMax = absLeft; 
                    if(rightMax<absRight) rightMax = absRight; 
                    if(leftSand<leftMax) leftSand = leftMax; 
                    if(rightSand<rightMax) rightSand = rightMax; 
                } 
 
                /* Un "recordatorio" cercano del maximo volumen. */ 
                leftSand = (int)((1-elasticity)*leftSand + elasticity*leftMax); 
                rightSand = (int)((1-elasticity)*rightSand + elasticity*rightMax); 
                /* Pintamos el componente. */ 
                repaint(); 
            } 
 
        } catch (IOException e) { 
            System.out.println("Errorenelpipe"); 
        } 
    } 
 
    /* Pinta las graficas. */ 
    public void paintComponent(Graphics g) { 
        Color color; 
 
        /* Canal izquierdo. */ { 
            color = Color.red; 
            g.setColor(color); 
            int l = (int)((double)(leftMax)/32768.0*getHeight()); 
            if(leftMax >= 32000) { 
                color = Color.black; 
                g.setColor(color); 
            } 
            g.fillRect(0, getHeight()-l, getWidth()/2, l); 
 
            int ls = (int)((double)(leftSand)/32768.0*getHeight()); 
            g.drawLine(0, getHeight()-ls, getWidth()/2, getHeight()-ls); 
        } 
 
        /* Canal derecho. */ { 
            color = Color.blue; 
            g.setColor(color); 
            int r = (int)((double)(rightMax)/32768.0*getHeight()); 
            if(rightMax >= 32000) { 
                color = Color.black; 
                g.setColor(color); 
            } 
            g.fillRect(getWidth()/2, getHeight()-r, getWidth(), r); 
 
            int rs = (int)((double)(rightSand)/32768.0*getHeight()); 
            g.drawLine(getWidth()/2, getHeight()-rs, getWidth(), getHeight()-rs); 
        } 
    } 
 
    public static void main(String args[]) throws Exception { 
        int buf_size = BUF_SIZE; 
        double sampling_rate = SAMPLING_RATE; 
        try { 
            if(args.length >=1) { 
                buf_size = Integer.parseInt(args[0]); 
                sampling_rate = Integer.parseInt(args[1]); 
            } 
            new VolMeter(buf_size, sampling_rate); 
        } catch (NumberFormatException e) { 
            System.out.println("Errorparsing" + args[1]); 
        } 
    } 
}

6 Sobre la compilacin

Compilar un programa escrito en C o en Java es muy sencillo a travs del intrprete de comandos. Basta con invocar al compilador correspondiente y con los argumentos adecuados. Tras la compilacin generaremos un fichero que puede ser ejecutado directamente por la computadora (en el caso del C) o interpretado por una mquina virtual (en el caso del Java).

El nico inconveniente que tiene compilar usando shell es que en muchas ocasiones escribimos exactamente lo mismo. Para ayudarnos en esta tarea se utiliza el programa make. Este programa ejecuta los comandos interactivos escritos en un fichero llamado Makefile. Dicho fichero tiene una estructura muy simple:

objetivo: dependencias_objetivo  
          comandos para conseguir objetivo

As, cuando escribamos:

make objetivo

el programa make comprobar si objetivo es ms viejo (atendiendo a la fecha de la ltima modificacin de los ficheros) que alguna de sus dependencias y si as es, ejecutar los comandos para conseguir objetivo. Como veremos, los objetivos pueden ser ficheros u objetivos que generen ficheros (como el objetivo all).

6.1 Compilacin de VolMeter

 
# Regla para compilar los fuentes Java 
%.class: %.java 
        javac $*.java 
 
# Clases 
BIN = 
BIN += VolMeter.class 
 
# Objetivo por defecto 
all: $(BIN) 
 
bin:  all 
 
# Creacion del fichero .jar con todas las clases 
jar:  bin 
        jar cvfm VolMeter.jar meta-inf/manifest.mf -C . *.class 
 
test:  jar 
        arecord -f cd -t raw | java -jar VolMeter.jar 
 
clean: 
        rm -f *.class VolMeter.jar

Ntese que el objetivo jar genera un fichero .jar que contiene las clases (en este ejemplo slo una) y un fichero de configuracin almacenado en meta-inf/manifest.mf. El conenido de este fichero es:

 
Manifest-Version: 1.0 
Main-Class: VolMeter 
Created-By: Vicente Gonzalez Ruiz

La idea de generar un fichero .jar se debe a que en ocasiones (aunque no en esta,) los programas en Java generan muchas clases. Una forma de agruparlas en un nico fichero (y de paso, comprimirlas porque un .jar es idntico a un .zip) es hacer este proceso.

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 sobre un fichero y viendo simultaneamente su volumen:
    arecord -t raw -f cd > file.raw &  
    arecord -t raw -f cd | java VolMeter