Tutorial: Indicador de decibelios

Jan 5, 2012 10:42:00 AM / by jmanuel.pereira

Cuando nos plantamos delante de nuesto IDE y empezamos a picar lineas de codigo nunca imaginamos esas pequeñas piedras que encontraremos en nuestro camino y que convertiran el desarrollo en un verdadero reto. Hace unos días implementando un medidor de decibelios para un proyecto este fue el caso, son esas pequeñas tonterias o dudas que cuando encuentras la respuesta no tienen muchas mas complejidad que la de leer la solución. Y como yo ya me he peleado con este reto no voy a dejar que vosotros perdais el tiempo buscando soluciones, así que aqui teneis el tutorial de como hacerlo.

 

Una vez creado nuestro proyecto Android, en nuestra Activity principal implementaremos tres metodos que nos ayudaran a gestionar el proceso gracias a la clase MediaRecorder.

 

La clase MediaRecorder es la encargada en la API de gestionar las grabaciones en el dispositivo, tando audio como video, pero en este tutorial daremos por aprendidos unos conocimientos minimos sobre la misma. No obstante no es necesario entenderla en profuncidad para la realización de l tutorial.

 

En nuestra activity definiremos nuestra variable mRecorder que es la que usaran los metodos que os mostramos a continuación:

 

Aquí los metodos:

 

//Metodo que inicializa la escucha
public void start() {
    if (mRecorder == null) {
        //Inicializamos los parametros del grabador
        mRecorder = new MediaRecorder();
        mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
        mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
 
        //No indicamos ningun archivo ya que solo queremos escuchar
        mRecorder.setOutputFile("/dev/null");
 
        try {
            mRecorder.prepare();
        } catch (IllegalStateException e) {
            Log.e("error", "IllegalStateException");
        } catch (IOException e) {
            Log.e("error", "IOException");
            ;
        }
 
        mRecorder.start();
    }
}

 
//Para la escucha
public void stop() {
    if (mRecorder != null) {
        mRecorder.stop();
        mRecorder.release();
        mRecorder = null;
    }
}
 
//Devuelve la mayor amplitud del sonido captado desde la ultima vez que se llamo al metodo
public double getAmplitude() {
    if (mRecorder != null)
        return (mRecorder.getMaxAmplitude());
    else
        return 0;
}

 

Lo destacable aquí y lo que me trajo el gran quebradero de cabeza fue la funcion getMaxAmplitude() ya que esta función no devuelve los datos en decibelios, es más los decibelios ni siquiera son una unidad de magintud y el reto encontrar la manera de mostrar los datos como necesitaba.

 

Según Wikipedia: El decibelio (símbolo dB) es la unidad relativa empleada en acústica, electricidad, telecomunicaciones y otras especialidades para expresar la relación entre dos magnitudes: la magnitud que se estudia y una magnitud de referencia.

 

¿Que magnitud de referencia utilizo? Esta es la pregunta clave. Despues de mil pruebas, buscando la amplitud del sonido ambiente en el aire y algunos datos similares y no dar con la tecla realice los siguientes pasos. Como vereis en muchos sistemas de sonidos digitales las medidas de dB van en valores negativos alcanzando su máximo en 0. Pues sabiendo esto, que el valor máximo que puede alcanzar un int (la funcion getMaxAmplitude() devuelve un int) es 32768 y googleando un poco para encontrar la formula de conversión conseguimos montar la ecuación que convertira nuestros valores y hará que el valor máximo sea 0 gracias a las propiedades de los logaritmos.

 

Double amplitude = 20 * Math.log10(getAmplitude() / 32768.0);

 

Ahora que ya tenemos la manera de obtener los datos en el formato que queremos pasamos a la fase final, mostrarlos por pantalla de una manera atractiva de pero de fase implementación. Para ello usaremos una tarea asincrona (AsyncTask) que será la encargada de ir comprobando el valor de del sonido ambiente e ir actualizando nuestra interfaz. Previamente deberemos haber recuperado en el onCreate los elementos con los que interactuaremos de nuestra interfaz:

 

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
 
    decibelsTx = (TextView) findViewById(R.id.decibels);
    barDB = (RelativeLayout) findViewById(R.id.bardb);
 
}

 

Aquí la AsyncTask que incluiremos como clase interna:

 

public class Ear extends AsyncTask<Void, Double, Void> {
 
        protected void onPreExecute() {
            start();
        }
 
        @Override
        protected Void doInBackground(Void... arg0) {
 
            while(listening) {
                SystemClock.sleep(300); // Si es menor casi siempre da 0
                Double amplitude = 20 * Math.log10(getAmplitude() / 32768.0);
                publishProgress(amplitude);
            }
 
            return null;
        }
 
        @Override
        protected void onProgressUpdate(Double... values) {
            Double value = values[0];
 
            if (value < -80) {
                value = new Double(-80);
            } else if (value > 0) {
                value = new Double(0);
            }
 
            String db = new Formatter().format("%03.1f",value).toString();
 
            decibelsTx.setText(db + " dB");
 
            updateBar(value);
 
        }
 
        @Override
        protected void onPostExecute(Void result) {
            stop();
        }
 
        public void updateBar(Double db) {
            Double width;
 
            // Factor de escala para convertir a Dips
            final float scale = getResources().getDisplayMetrics().density;
 
            width = (db * 250 * scale) / -80; // Anchura en pixeles
 
            RelativeLayout.LayoutParams lyParams = new RelativeLayout.LayoutParams(width.intValue(), barDB.getHeight());
            lyParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
            barDB.setLayoutParams(lyParams);
 
        }
 
    }

 

Pasamos a detallaros un poco mejor la tarea asincrona. En los metodos onPreExecute y onPostExecute no encargaremos de arrancar el sistema de escucha y detenerlo respectivamente mientras que en doInBackground iremos recogiendo los datos que iremos mandando a onProgressUpdate que se encargara de actualizar nuestra interfaz. en doInBackground realizaremos un bucle para que no deje de consultar los valores y utilizaremos la bandera “listening” para detener el proceso cuando creamos conveniente.

Como podeis comprobar dormimos el proceso 200 ms en cada iteración y esto tiene un porque. El método getAmplitude devuelve el mayor valor medido desde la última vez que se llamo al método, si llamamos al método constantemente un altisimo porcentaje de las veces la llamada nos devolvera 0 y por tanto nuestros decibelios se nos iran a menos infinito. Despues de alguna pruebas he comprobado que 200 ms aproximadamente es el valor minimo con el que el sistema se comporta de manera correcta.

 

Deberemos sobreescribri los metodos onResume y onPause para gestionar el arranque y finalización de la tarea asincrona y asi evitar un gasto innecesario de bateria mientras nuestra app se encuentra en segundo plano.

 

@Override
    protected void onResume() {
        listening = true;
        new Ear().execute();
        super.onResume();
    }
 
    @Override
    protected void onPause() {
        listening = false;
        super.onPause();
    }

 

Antes de visualizar los datos ponemos unos limites para evitar que en caso puntuales se nos disparen los valores y nunca sea mayor que 0 o menor que -80. Relamente el no deberiamos controlar que fuera mayor que 0 pues esto ya esta controlado en la manera que hemos planteado la ecuación.

 

 

A la hora de visualizar los datos por pantalla hemos obtado por mostrarlos en modo texto y con una barra con degradado que marcaría la intensidad. En el caso de la barra nos limitamos a ocultar un porcentaje de la barra en función del nivel de decibelios. Entremos en detalle en esto:

 

public void updateBar(Double db) {
            Double width;
 
            // Factor de escala para convertir a Dips
            final float scale = getResources().getDisplayMetrics().density;
 
            width = (db * 250 * scale) / -80; // Anchura en pixeles
 
            RelativeLayout.LayoutParams lyParams = new RelativeLayout.LayoutParams(width.intValue(), barDB.getHeight());
            lyParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
            barDB.setLayoutParams(lyParams);
 
        }

 

 

Nuestra barra mide exactamente 250 dips pero los métodos que usaremos para modificar las medidas de esta en tiempo de ejecución esperan recibir las medidas en pixeles, por tanto deberemos de convertir nuestros valores. Para ello y gracias al metro getResources().getDisplayMetrics().density obtenemos el factor de escala en funcion de la densidad de la pantalla y hacemos los calculos en consecuencia. Como nuestra barra mide 250 dips la multiplicamos por el factor de escala y dividimos por 80 que es el valor maximo de la barra, con esto y multiplicando por el valor en dB tendremos el numero de pixles que ocupara nuestra barra y solo nos quedará actualizar via código estas medidas.

 

¡ojo! Recuerda que no es la barra de colores la que estamos actualizando, si no una negra superior que oculta la parte de esta que no queremos mostrar. Esto lo hacemos por que si no con los cambios de tamaño el degradado también oscilaria.

 

Por ultimo añadiremos una SeekBar para seleccionar el nivel de decibelios que queremos guardar. Como siempre lo primero tener definido los elementos en nuestro xml y recuperarlos en el onCreate para poder interactuar con ellos.

 

Es importante saber que el seekbar solo contempla valores positivos empezando por 0 y lo que podemos configurar es el maximo, que en nuestro caso sera 80 y luego nosotros haremos la conversion necesaría para que los valores aparezcan en negativo.

 

seekbarDB = (SeekBar) findViewById(R.id.seekbar_db);
decibeliosSeekbar = (TextView) findViewById(R.id.decibelios_seekbar);

 

Para interactuar con la SeekBar deberemos implementar una clase que implemente la interfaz SeekBar.onSeekBarChangeListener que será la que que se encargue de gestionar los cambios de la misma. En este caso utilizaremos la propia Activity a la que deberemos de añadirle tres metodos, pero en principio solo nos preocupara implementar uno:

 

@Override
    public void onProgressChanged(SeekBar seekBar, int progress,
            boolean fromUser) {
        decibeliosSeekbar.setText((-80 + progress) + " dB");
    }
 
    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
        // TODO Auto-generated method stub
 
    }
 
    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {
        // TODO Auto-generated method stub
 
    }

 

En el metodo onProgressChanged que se ejecutara cada vez que el valor del SeekBar sea modificado haremos las conversión de valores de la que hablabamos  y modificaremos el valor mostrado por pantalla.

 

Es importante asociar el listener y el SeekBar en el onCreate, si no estos metodos no tendrán efecto.

 

seekbarDB.setOnSeekBarChangeListener(this);

 

 

Y con esto hemos terminado, espero que os sirva de ayuda y hasta la proxima.

 

 

Código Fuente | Google Code

 

Topics: Android, MediaRecorder, Programación

jmanuel.pereira

Written by jmanuel.pereira

Lists by Topic

see all
Servicios gestionados

Categorías

Ver todas