«

»

ene 05 2012

Tutorial: Medidor de decibelios

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.

 

Important!

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:

private MediaRecorder mRecorder = null;

 

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.

 

Important!

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.

 

Important!

¡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.

 

Important!

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.

 

Via | Ingens Networks

Codigo Fuente | Google Code

 

Acerca del autor

JMPergar

Mobile Developer at @BeRepublic & Founder of @AndroCode. Silver Speaker & Member of Core Team at @GDGBarcelona.

  • Yesid

    Hola,
    Me gusto mucho el tutorial y estoy haciendolo, pero tengo 2 errores que me gustaria me pudieran ayudar a solucionar.
    El primero es en la linea:

    String db = new Formatter().format(“%03.1f”,value).toString();

    Me dice que format es indefinido para el tipo Formatter. Y la verdad no se porque.

    El otro error que me genera es en la linea:

    RelativeLayout.LayoutParams lyParams = new RelativeLayout.LayoutParams(width.intValue(), barDB.getHeight());
    lyParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
    barDB.setLayoutParams(lyParams);

    Me indica que no puede resolver barDB.

    Y bueno estuve revisando el link del Codigo Fuente para resolverlos y para mirar el layout .XML pero no hay nada.
    Estaria muy agradecido si me pueden ayudar a resolver.

    Muchas gracias

    Yesid

    • http://hackedbrain.blogspot.com/ JMPergar | Editor Jefe

      En cuanto al Formatter comprueba que tienes correctamente hecho el import de la libreria, probablemente estes usando el que no debes. Sería así: “import java.util.Formatter;”. En cuanto a barDB es una variable tipo Relative Layout definida al principio (private RelativeLayout barDB;) e instanciada en el onCreate (barDB = (RelativeLayout) findViewById(R.id.bardb);). Comprueba que en tu XML tiene la etiqueta de id puesta correctamente. Espero te sea de ayuda.

  • http://desandroid.com DesAndrOId

    Mola, pero te falta un pequeño detalle, no has puesto que es necesario establecer algunos permisos en el Manifest para que funcione.

    Salu2

    • http://hackedbrain.blogspot.com/ JMPergar | Editor Jefe

      Cierto y gracias! El permiso es: android.permission.RECORD_AUDIO

  • masky

    Hola,

    en primer lugar felicidades por el post, es muy bueno.

    Estoy siguiendo el tutorial pero como soy relativamente nuevo en Android y Java, me gustaría poder disponer del código que antes estaba en google code pero ya no si no hay problemas en ello, estoy algo atrancado y no se muy bien donde es (sospecho que mi xml no se debe parecer en nada al tuyo).

    Gracias y felicidades de nuevo.

    • http://www.linkedin.com/in/jmpergar JMPergar | Editor Jefe

      El código sigue estando disponible en Google Code, lo acabo de comprobar por si habia sucedido algo y he accedido sin problemas. Vuelve a intentarlo y si sigues teniendo problemas comunicanoslo.

  • masky

    Hola,

    no consigo verlo, entre mediante el enlace a google code, pincho en Downloads y me dice que el proyecto no tiene descargas. Gracias.

    • masky

      Vale, ya lo he encontrado, voy a examinar el xml que seguro que por ahí está el problema. Muchas gracias.

      • masky

        Exacto, ahí estaba el problema. Ahora la funcion getmaxamplitude siempre devuelve 0. He “jugado” con el tiempo entre llamadas a la función pero el problema sigue siendo el mismo. ¿Alguna idea? Gracias.

        • http://www.linkedin.com/in/jmpergar JMPergar | Editor Jefe

          Tienes que poner poco tiempo creo recordar, ya que la funcion te devuelve el max en el intervalo de medición, y siempre hay ruido ambiente e interferencias que producen picos. ¿Con el intervalo de tiempo que viene en el codigo de ejemplo tambien te falla? Tambien ten en cuenta que la sensibilidad de los microfonos difiere en funcion del terminal.

          • masky

            Si, también me falla, ponga más o menos tiempo obtengo el mismo resultado. He estado buscando en muchos foros y veo que muchos tenemos el mismo problema, pero desconozco el motivo.

            He instalado por probar otros sonómetros y funcionan bien (otra cosa es la precisión), por lo que descarto que el problema esté en el móvil. Quizás se me escape poner algún permiso, no se. Chequeo la variable mRecorder y parece que es correcta, ya que la grabación está habilitada y “corriendo”. Ni idea de que puede estar sucediendo.

  • luitro

    Hola gracias por el tutorial.
    He seguido todos los pasos y no me marca ningún error, pero no llega a funcionar. Podrías subir la activity para echarle un vistazo y saber donde me he equivocado.

    Saludos y gracias.

    • http://www.linkedin.com/in/jmpergar JMPergar | Editor Jefe

      Al final del post hay un enlace al codigo fuente ;)

      • luitro

        He entrado en el enlace en download y no hay ningún archivo:

        “This project currently has no downloads.”

        He vuelto a reescribir el código, paso a paso, he dado permisos en el android manifest y nada. Me dá un forzar cierre.

  • David

    Buenas JMPergar,

    Fantástico tutorial funciona a las mil maravillas, simplemente quería saber si el límite de captación de un smartphone esta en 80dbs o es que el código esta capado para que no supere dichos decibelios.
    El umbral del dolor esta en los 140dbs y queria modificar la app para dicho valor.

    Esperando noticias tuyas recibe un cordial saludo!

    • http://www.linkedin.com/in/jmpergar JMPergar

      Supongo que el limite de captacion del smartphone dependera de cada dispositivo, habrá micros de peor y mejor calidad. En este caso el limite impuesto no se acoje a ninguna razon en concreto.

  • David

    Muchas gracias por tu respuesta.

    si se me permite queria preguntar como se deberia modificar el código/fórmula para poder llegar a los 140dbs (umbral del dolor.)

    Saludos y gracias!

    • http://www.linkedin.com/in/jmpergar JMPergar

      La cosa es que el tema de los decibelios es más complejo de lo que parece a primera vista, ya que no es una unidad en si, si no que compara como dos valores y ademas suele haber muchas maneras de representarlo. Por ejemplo, por lo que investigue lo mas común es que estos se midan en escala negativa siendo el máximo el valor 0 y vendrían a representar la amplitud frente al aire (o algo así). Esto se obtendría con la siguiente formula:

      20 * Math.log10(getAmplitude() / 32768.0);

      Este es el valor que muestro, con la peculiaridad de que si es menor de -80 yo sigo mostrando -80. Por tanto el umbral de dolor en este caso no sería 140dB ya que eso correspondería con el silencio, debe serlo en otra escala de visualización, aquí probablemente sería algún valor cercano a 0.

      También hay que tener en cuenta, aunque en este tutorial no se aborda, que la escala es logarítmica y por tanto cuanto más cerca de 0 estamos más cuesta seguir subiendo el valor.

      Espero que te sea de ayuda.

  • Lui

    Hola, Buenas Tardes. Disculpe, pero ya no aparece nada en la sección donde aparecen los codigos, quisiera saber si todavía me lo podría mostrar o ya no. Gracias.

  • bety

    Hola!! muy buen tutorial, pero yo si tengo un gran problema, en realidad soy principiante pero quiero hacer esta aplicación y tu muestras los pasos como para alguien que ya tiene conocimiento sobre android, y mi problema es que la voy hacer en eclipse y no que es lo que le tengo que agregar en el archivo “xml” y asi como lo pones asi es como va?? no se si hay alguna forma para que me puedas pasar el codigo completo (como ya tiene que quedar). t lo agradeceria muxo :)
    saludos

  • http://Enconstrucción... Alfonso Moreno

    Qué sorpresa! Leyendo el tutorial, me encuentro al final con tu firma ;-)

  • Wilfrido Lopez

    Buenos días, disculpen alguien de los compañeros que ya lograron armar y correr la app, podrían apoyar enviando en algún “.txt” el código?? o alguna carpeta Dropbox, GoogleDrive o cualquier otra manera real de descargar el proyecto??, ya que no sé si estoy mal en irlo haciendo, algo mal declarado o que sea. De ante mano gracias.
    mi correo: wilfridocuenta94@gmail.com