AndroidAudio, AndroidSound, and AndroidMusic: Crash, Bang, Boom!

In Chapter 3, we designed three interfaces for all our audio needs: audio, sound, and music. Audio is responsible for creating sound and music instances from asset files. Sound lets us playback sound effects that are stored in RAM, and music streams bigger music files from the disk to the audio card. In Chapter 4, you learned which Android APIs are needed to implement this. We will start with the implementation of AndroidAudio, as shown in Listing 5–2.

Listing 5–2. AndroidAudio.java; Implementing the Audio Interface

package com.badlogic.androidgames.framework.impl;

import java.io.IOException;

import android.app.Activity;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.media.AudioManager;
import android.media.SoundPool;

import com.badlogic.androidgames.framework.Audio;
import com.badlogic.androidgames.framework.Music;
import com.badlogic.androidgames.framework.Sound;


public class AndroidAudio implements Audio {
    AssetManager assets;
    SoundPool soundPool;

The AndroidAudio implementation has an AssetManager and a SoundPool instance. The AssetManager is necessary for loading sound effects from asset files into the SoundPool on a call to AndroidAudio.newSound(). The AndroidAudio instance also manages the SoundPool.

public AndroidAudio(Activity activity) {
        activity.setVolumeControlStream(AudioManager.STREAM_MUSIC);
    this.assets = activity.getAssets();
    this.soundPool = new SoundPool(20, AudioManager.STREAM_MUSIC, 0);
    }

There are two reasons why we pass our game's Activity in the constructor: it allows us to set the volume control of the media stream (we always want to do that), and it gives us an AssetManager instance, which we will happily store in the corresponding class member. The SoundPool is configured to play back 20 sound effects in parallel, which is adequate for our needs.

@Override
public Music newMusic(String filename) {
    try {
        AssetFileDescriptor assetDescriptor = assets.openFd(filename);
        return new AndroidMusic(assetDescriptor);
    } catch (IOException e) {
        throw new RuntimeException("Couldn't load music '" + filename + "'");
    }
}

The newMusic() method creates a new AndroidMusic instance. The constructor of that class takes an AssetFileDescriptor, which it uses to create an internal MediaPlayer (more on that later). The AssetManager.openFd() method throws an IOException in case something goes wrong. We catch it and re-throw it as a RuntimeException. Why not hand the IOException to the caller? First, it would clutter the calling code considerably, so we would rather throw a RuntimeException that does not have to be caught explicitly. Second, we load the music from an asset file. It will only fail if we actually forget to add the music file to the assets/directory, or if our music file contains false bytes. False bytes constitute unrecoverable errors since we need that Music instance for our game to function properly. To avoid such an occurrence, we throw RuntimeExceptions instead of checked exceptions in a few more places in the framework of our game.

    @Override
    public Sound newSound(String filename) {
        try {
            AssetFileDescriptor assetDescriptor = assets.openFd(filename);
            int soundId = soundPool.load(assetDescriptor, 0);
            return new AndroidSound(soundPool, soundId);
        } catch (IOException e) {
            throw new RuntimeException("Couldn't load sound '" + filename + "'");
        }
    }

Finally, the newSound() method loads a sound effect from an asset into the SoundPool and returns an AndroidSound instance. The constructor of that instance takes a SoundPool and the ID of the sound effect assigned to it by the SoundPool. Again, we throw any checked exception and re-throw it as an unchecked RuntimeException.

NOTE: We do not release the SoundPool in any of the methods. The reason for this is that there will always be a single Game instance holding a single Audio instance that holds a single SoundPool instance. The SoundPool instance will, thus, be alive as long as the activity (and with it our game) is alive. It will be destroyed automatically as soon as the activity ends.

Next, we will discuss the AndroidSound class, which implements the Sound interface. Listing 5–3 presents its implementation.

Listing 5–3. Implementing the Sound Interface using AndroidSound.java.

package com.badlogic.androidgames.framework.impl;

import android.media.SoundPool;

import com.badlogic.androidgames.framework.Sound;

public class AndroidSound implements Sound {
    int soundId;
    SoundPool soundPool;

    public AndroidSound(SoundPool soundPool, int soundId) {
        this.soundId = soundId;
        this.soundPool = soundPool;
    }

    @Override
    public void play(float volume) {
        soundPool.play(soundId, volume, volume, 0, 0, 1);
    }

    @Override
    public void dispose() {
        soundPool.unload(soundId);
    }
}

There are no surprises here. Via the play() and dispose() methods, we simply store the SoundPool and the ID of the loaded sound effect for later playback and disposal. It doesn't get any easier than this, thanks to the Android API.

Finally, we have to implement the AndroidMusic class returned by AndroidAudio.newMusic(). Listing 5–4 shows that class' code, which looks a little more complex than before. This is due to the state machine that the MediaPlayer uses, which will continuously throw exceptions if we call methods in certain states.

Listing 5–4. AndroidMusic.java; Implementing the Music Interface

package com.badlogic.androidgames.framework.impl;

import android.content.res.AssetFileDescriptor;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;

import com.badlogic.androidgames.framework.Music;

public class AndroidMusic implements Music, OnCompletionListener {
    MediaPlayer mediaPlayer;
    boolean isPrepared = false;

The AndroidMusic class stores a MediaPlayer instance along with a Boolean called isPrepared. Remember, we can only call MediaPlayer.start()/stop()/pause() when the MediaPlayer is prepared. This member helps us keep track of the MediaPlayer's state.

The AndroidMusic class implements the Music interface as well as the OnCompletionListener interface. In Chapter 3, we briefly defined this interface as a means of informing ourselves about when a MediaPlayer has stopped playing back a music file. If this happens, the MediaPlayer needs to be prepared again before we can invoke any of the other methods. The method OnCompletionListener.onCompletion() might be called in a separate thread, and since we set the isPrepared member in this method, we have to make sure that it is safe from concurrent modifications.

public AndroidMusic(AssetFileDescriptor assetDescriptor) {
        mediaPlayer = new MediaPlayer();
        try {
            mediaPlayer.setDataSource(assetDescriptor.getFileDescriptor(),
                    assetDescriptor.getStartOffset(),
                    assetDescriptor.getLength());
            mediaPlayer.prepare();
            isPrepared = true;
            mediaPlayer.setOnCompletionListener(this);
        } catch (Exception e) {
            throw new RuntimeException("Couldn't load music");
        }
    }

In the constructor, we create and prepare the MediaPlayer from the AssetFileDescriptor that is passed in, and we set the isPrepared flag, as well as register the AndroidMusic instance as an OnCompletionListener with the MediaPlayer. If anything goes wrong, we throw an unchecked RuntimeException once again.

    @Override
    public void dispose() {
        if (mediaPlayer.isPlaying())
            mediaPlayer.stop();
        mediaPlayer.release();
    }

The dispose() method checks if the MediaPlayer is still playing and, if so, stops it. Otherwise, the call to MediaPlayer.release() will throw a runtime exception.

    @Override
    public boolean isLooping() {
        return mediaPlayer.isLooping();
    }

    @Override
    public boolean isPlaying() {
        return mediaPlayer.isPlaying();
    }

    @Override
    public boolean isStopped() {
        return !isPrepared;
    }

The methods isLooping(), isPlaying(), and isStopped() are straightforward. The first two use methods provided by the MediaPlayer; the last one uses the isPrepared flag, which indicates if the MediaPlayer is stopped. This is something MediaPlayer.isPlaying() does not necessarily tell us since it returns false if the MediaPlayer is paused but not stopped.

    @Override
    public void pause() {
        if (mediaPlayer.isPlaying())
            mediaPlayer.pause();
    }

The pause() method simply checks whether the MediaPlayer instance is playing and calls its pause() method if it is.

    @Override
    public void play() {
        if (mediaPlayer.isPlaying())
            return;
        try {
            synchronized (this) {
                if (!isPrepared)
                    mediaPlayer.prepare();
                mediaPlayer.start();
            }
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

The play() method is a little more involved. If we are already playing, we simply return from the function. Next we have a mighty try…catch block within which we check to see if the MediaPlayer is already prepared based on our flag; we prepare it if needed. If all goes well, we call the MediaPlayer.start() method, which will start the playback. This is conducted in a synchronized block, since we are using the isPrepared flag, which might get set on a separate thread because we are implementing the OnCompletionListener interface. In case something goes wrong, we throw an unchecked RuntimeException.

    @Override
    public void setLooping(boolean isLooping) {
        mediaPlayer.setLooping(isLooping);
    }

    @Override
    public void setVolume(float volume) {
        mediaPlayer.setVolume(volume, volume);
    }

The setLooping() and setVolume() methods can be called in any state of the MediaPlayer and delegated to the respective MediaPlayer methods.

    @Override
    public void stop() {
        mediaPlayer.stop();
        synchronized (this) {
            isPrepared = false;
        }
    }

The stop() method stops the MediaPlayer and sets the isPrepared flag in a synchronized block.

    @Override
    public void onCompletion(MediaPlayer player) {
        synchronized (this) {
            isPrepared = false;
        }
    }
}

Finally there's the OnCompletionListener.onCompletion() method that is implemented by AndroidMusic class. All it does is set the isPrepared flag in a synchronized block so that the other methods don't start throwing exceptions out of the blue. Next, we'll move on to our input-related classes.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.171.66