Android: Musica de fondo en toda una aplicación

Hace sólo un par de días que hemos publicado Adivinados que es el primer juego para Android que hago. Los últimos detalles han sido lo habitual, poner música de fondo, efectos, corregir alguna animación y demás, pero en este articulo me centraré en como he hecho para mantener la música de fondo a lo largo de todas las actividades, porque me ha parecido un tema curioso y que estoy seguro de que a alguno puede interesar y venir bien.

1. Planteamiento

Se utiliza MediaPlayer para gestionar el sonido. Si pensáis en usar SoundPool mejor dejadlo para otro tipo de sonidos, porque tiene un limite de 5 segundos para cada sonido que se reproduce.

El sonido se gestionará a través de un servicio (sé que hay gente que lo ha hecho con una clase instanciada normal). Y cada actividad que necesite interactuar con el sonido lo hará a través de este servicio, lo llamo AudioService.

Determinadas actividades tienen que poder cambiar el volumen de la musica.

2. El servicio

Creamos un archivo AudioService.java y en el empezamos escribiendo esto:

public class AudioService extends Service {

    static final int DECREASE = 1, INCREASE = 2, START = 3, PAUSE = 4;
    Boolean shouldPause = false;
    MediaPlayer loop;

DECREASE, INCREASE, START y PAUSE són las acciones que podremos hacer con el sonido. shouldPause hará la mágia del punto 4 y loop será el sonido en si.

Estos son los métodos “públicos” para gestionar el sonido:

private void startLoop(){
    if(loop == null){
        loop = MediaPlayer.create(this, R.raw.loop);
    }
    if(!loop.isPlaying()){
        loop.setLooping(true);
        loop.start();
    }
}
private void decrease(){
    loop.setVolume(0.2f, 0.2f);
}
private void increase(){
    loop.setVolume(1.0f, 1.0f);
}
private void start(){
    startLoop();
    shouldPause = false;
}
private void pause(){
    shouldPause = true;
    new android.os.Handler().postDelayed(
        new Runnable() {
            public void run() {
                if(shouldPause) {
                    loop.pause();
                }
            }
        }, 100);
}

Y estos son los métodos necesarios para que el servicio funcione. Lo único a destacar es que onStartCommand se encargará de recibir las ordenes que le demos desde otras actividades, y hará lo que toque.

@Override
public void onCreate() {
    super.onCreate();
    Log.i(getClass().getSimpleName(), "Creating service");
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    super.onStartCommand(intent, flags, startId);
    Log.i(getClass().getSimpleName(), "Intent received");

    try {
        int actionDefault = 0;
        int action = actionDefault;

        if(intent != null){
            if(intent.hasExtra("action")){
                action = intent.getIntExtra("action", actionDefault);
            }
        }

        switch (action) {
            case INCREASE:
                increase();
                break;
            case DECREASE:
                decrease();
                break;
            case START:
                start();
                break;
            case PAUSE:
                pause();
                break;
        }
    }catch (Exception e){
        e.printStackTrace();
    }

    return START_STICKY;
}

@Override
public void onDestroy() {
    super.onDestroy();
    if (loop != null) loop.release();
}

No os olvidéis de declarar el servicio en el AndroidManifest.xml:

<service
    android:name=".AudioService"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name" />

3. Uso del servicio y problemas

Aquí empieza lo interesante. Cada vez que una actividad se abre o se cierra, se iniciará o pausará la música. De esta forma se puede diferenciar entre cerrar una actividad para salir del juego o cerrar una actividad porque cambias a otra. Más o menos quedaría así.

  1. De actividad A a Actividad B, se pausa al salir de A y se vuelve a arrancar al iniciar B.
  2. Finalizando actividad B, se pausa al salir de B y se vuelve a arrancar al iniciar A.
  3. Finalizando actividad A, se pausa al salir de A y como no se inicia ninguna otra, no se vuelve a arrancar.

Y aquí el código que hay que poner en cada actividad:

@Override
public void onPause() {
    super.onPause();
    pausar();
    Intent i = new Intent(this, AudioService.class);
    i.putExtra("action", AudioService.PAUSE);
    startService(i);
}

@Override
public void onResume() {
    super.onResume();
    Intent i = new Intent(this, AudioService.class);
    i.putExtra("action", AudioService.START);
    startService(i);
}

El problema de esto es que provoca unas pausas de unos pocos milisegundos en cada cambio de actividad, un microcorte que a veces no se aprecia siquiera, pero si la nueva actividad es pesada a nivel de interfaz y tarda un poco en cargar, sí se notará. Incluso se notaría en dispositivos de gama baja o antiguos, para los que cualquer actividad por ligera que sea, ya es pesada.

4. Solución a los cortes entre actividades.

La forma de evitar esos cortes es sencilla, el truco está en la variable shouldPause y los métodos start() y pause(). Es más dificil de explicar que de entender, pero bueno:

Cada vez que se le dice que pause(), se esperará 100ms (o el tiempo que se le diga) y después se preguntará si de verdad tiene que pausar. Si no ha pasado nada más, pausará. Si por el contrario ha llegado otra llamada a start() durante esos 100ms, se habrá marcado shouldPause como false y la llamada a pause() se omitirá.

Espero que le sirva a alguien, y que si alguien tiene una forma de hacerlo más sencilla me lo diga, por que la verdad es que no me gusta mucho, sobre todo por tener que marcar todas las actividades con los intents del servicio en el onPause y onResume.




Josep Viciana

Programador de 29 años con una década de experiencia como programador. interesado en el diseño, ilustración y nuevas tecnologías. Dedicado desde siempre a la programación Web y desde hace algunos años también a la móvil.

9 comentarios

Hola, recién descargué tu app, la verdad que muy buena, recién te la califiqué en google play. El diseño es excelente y es muy completa. La verdad no soy de jugar a ese juego pero ya había visto varias apps similares y había pensado en hacer una como esas.

Lo que te comento tengo moto G y en el resto de las apps que probé me funciona perfecto el sensor del movimiento, pero en la tuya no… y es esencial para jugar el juego. No se solo te digo por las dudas que tuviera un error, me parece raro que esté tan completa y no funcione eso.

Otra cosa como sugerencia, ya que veo que no tiene muchas descargas y hay varias apps similares que si las tienen, creo que deberían ponerle algunas palabras claves que tuvieran las otras apps. Por ejemplo “charadas” “adivina la palabra”, etc. Obviamente esto se los digo por si les interesa un mejor posicionamiento. No se si la app la hiciste solo vos o con colegas. Yo tengo varias apps hechas pero las hice sol, acá las podes ver: https://play.google.com/store/search?q=pub%3ASistemas%2B&c=apps

Y bueno, ahora estoy haciendo un nuevo juego y quería ponerle sonido por eso encontre tu tutorial. Por lo que leí en stackoverflow no convenía usar servicios y dicen que este es el mejor modo: http://www.rbgrn.net/content/307-light-racer-20-days-61-64-completion
Pero la verdad me tira varios errores y no logro hacerlo funcionar (te lo paso a ver si a vos te sirve). Bueno después voy a probar tu código y te comento como me fue. Y te quería hacer una única pregunta que es de dónde sacaste el sonido para la app? Esta bastante bueno, necesito uno de ese estilo pero no encuentro, me sería de mucha ayuda.
Saludos y espero que sigas avanzando en lo tuyo.
Pablo

Hola Pablo.

Ese modo de hacerlo ya lo he probado, tampoco me acabó de funcionar y por eso lo hago como digo en el articulo.

Sobre los gestos y tu Moto G, ¿qué generación es?

Y sobre la música, es comprada, no conozco ningún banco de sonidos gratuito. Y de pago no te costará encontrar : )

Ya me contarás que tal te ha ido con mi ejemplo, salud.

Ahí lo probé y me anduvo. Pero tengo un problema y estuve probando varias soluciones sin resultados positivos. Es que quiero pausar la musica a través de un botón y no funciona. Conoces la manera de hacerlo? Para que el usuario pueda elegir escuchar o no la música. Gracias! Saludos

Puedes usar algo así:

Intent i = new Intent(this, AudioService.class);
i.putExtra("action", AudioService.INCREASE);
startService(i);

Cambiando el AudioService.INCREASE por cualquiera de las opciones que hay en el ejemplo, INCREASE, DECREASE, START o PAUSE.

Salud.

Gracias, ahí me funcionó. Te dejo el código a ver si te sirve o a alguien más 😀
Hice esto:

//VARIABLES
        Intent i;
	SharedPreferences settings;
	SharedPreferences.Editor editor;
	private int reproduciendo;

//EN EL ONCREATE

             //CARGO ESTADO DE LA MUSICA EN LA VARIABLE reproduciendo. POR DEFECTO EN 0, QUE SERIA QUE SE ESTA REPRODUCIENDO
		 SharedPreferences settings = getSharedPreferences(Preferences.PREFS_NAME, 0);
	     reproduciendo = settings.getInt(Preferences.PREFFS_MUSICA, 0);
     
   //NUEVAMENTE CREO LAS PREFERENCIAS YA QUE NO PUEDO CREARLAS DENTRO DEL METODO ONCLICK, Y TAMBIEN EL INTENT PARA DECIRLE AL SERVICE SI ACTIVARSE O NO
       settings = this.getApplicationContext().getSharedPreferences(Preferences.PREFS_NAME, 0);
        editor = settings.edit();
    	i = new Intent(this, AudioService.class);

//BOTON
//ACA LE DIGO SI NO SE ESTA REPRODUCIENDO (reproduciendo=1), QUE SE REPRODUZCA
//DE LO CONTRARIO (reproduciendo=0), QUE NO SE REPRODUZCA
//DE ESTA MANERA ACTIVO Y DESACTIVO LA MUSICA
b1.setOnClickListener(new OnClickListener() {
           	@Override
			public void onClick(View arg0) {
           		if(reproduciendo==1){
           			i.putExtra("action", AudioService.START);
               		startService(i);
           			reproduciendo=0;
           		}else{
           		i.putExtra("action", AudioService.STOP);
           		startService(i);
           		reproduciendo=1;
           		}
                       //GUARDO EL ESTADO
           		editor.putInt(Preferences.PREFFS_MUSICA, reproduciendo);
        		editor.commit();
           	}
		});

Y EN TODAS LAS ACTIVIDADES TENGO QUE CARGAR LAS PREFERENCIAS EN EL ONCREATE
SharedPreferences settings = getSharedPreferences(Preferences.PREFS_NAME, 0);
reproduciendo = settings.getInt(Preferences.PREFFS_MUSICA, Players.REPRODUCIR_ON);

//Y AHORA EN EL METODO ONRESUME Y ONPAUSE DE TODAS LAS ACTIVIDADES, SOLO ACTIVO EL SERVICIO SI reproduciendo=0. DE LO CONTRARIO NO HAGO NADA, YA QUE NO REPRODUCIRIA MUSICA

  @Override
	protected void onResume() {
		// TODO Auto-generated method stub
		super.onResume();
		if(reproduciendo==0){
		Intent i = new Intent(this, AudioService.class);
	    i.putExtra("action", AudioService.START);
	    startService(i);
		}
	}
	
	@Override
	protected void onPause() {
		// TODO Auto-generated method stub
		super.onPause();
		if(reproduciendo==0){
		Intent i = new Intent(this, AudioService.class);
	    i.putExtra("action", AudioService.PAUSE);
	    startService(i);
		}
	}

Ah… en la clase AudioService creé el método stop(); ya que con el pause() reanudaba la musica a los segundos. Quedó así:

static final int DECREASE = 1, INCREASE = 2, START = 3, PAUSE = 4, STOP = 5;
private void stop(){
    	shouldPause=true;
    	loop.pause();
    }

No se si es la mejor manera, pero es la forma que me salió intuitivamente.
Bueno espero que le sirva a alguien 🙂
Y con respecto a la version de mi moto g, es de la primera generación.
Saludos y gracias por responder.
Pablo

A mi me marca error en el pause();

del override

 @Override
    public void onPause() {
        super.onPause();
        pause();
        Intent i = new Intent(this, AudioService.class);
        i.putExtra("action", AudioService.PAUSE);
        startService(i);
    }

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Conoce cómo se procesan los datos de tus comentarios.