Android Audio Android Sound and Android Music Crash Bang Boom

We designed three interfaces in Chapter 3 for all our audio needs: Audio, Sound, and Music. Audio is responsible for creating Sound and Music instances from asset files. Sound let's us playback sound effects completely stored in RAM, and Music streams bigger music files from disk to the audio card. In Chapter 4 you learned what Android APIs we need to implement this. We start off with the implementation of AndroidAudio, as shown in Listing 5-2.

Listing 5-2. AndroidAudiojava; 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 needed so that we can load sound effects from asset files into the SoundPool on a call to AndroidAudio.newSound(). The SoundPool itself is also managed by the AndroidAudio instance.

public AndroidAudio(Activity activity) {

activity.setVolumeControlStream(AudioManager.STREAM_MUSIC);

this.assets = activity.getAssets();

this.soundPool = new SoundPool(20, AudioManager.STREAM_MUSIC, 0);

In the constructor we pass in the Activity of our game for two reasons: it allows us to set the volume control to the media stream (remember we always want to do that), and it gives us an AssetManager instance, which we happily store in the corresponding member of the class. The SoundPool is configured to be able to play back 20 sound effects in parallel—enough 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, from which it creates a MediaPlayer internally (more on that in a bit). The AssetManager.openFd() method throws an IOException in case something goes wrong. We catch it and rethrow 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, which 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 bogus bytes. These would constitute unrecoverable errors, as we need that Music instance for our game to function properly. To avoid that, we'll employ the strategy of throwing a RuntimeException instead of checked exceptions in a few more places in our game framework.

^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 the SoundPool assigned to it. We again throw any checked exception and rethrow 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 drops dead.

Next up is the AndroidSound class, which implements the Sound interface. Listing 5-3 shows you its implementation.

Listing 5-3. AndroidSound.java; Implementing the Sound Interface 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);

No surprises here. We simply store the SoundPool and the ID of the loaded sound effect for later playback and disposal via the play() and dispose() methods. It doesn't get any easier. All hail to the Android API.

Finally we have to implement the AndroidMusic class returned by AndroidAudio.newMusic(). Listing 5-4 shows the code for that class. It looks a little more complex than before. That's due to the state machine that the MediaPlayer really uses, which will throw exceptions like mad if we call methods in certain states.

Listing 5-4. AndroidMusic.java; Implementing the Music Interface package com.badlogic.androidgames.framework.impl;

import java.io.IOException;

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; package com.badlogic.androidgames.framework.impl;

import java.io.IOException;

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 not only the Music interface, but also the OnCompletionListener interface. In Chapter 3 we briefly defined this interface as a means to get informed about when a MediaPlayer has stopped playing back a music file. If this happens, then the MediaPlayer needs to be prepared again before we can invoke any of the other methods on it. 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 gets passed in, and we set the isPrepared flag, along with registering the AndroidMusic instance as an OnCompletionListener with the MediaPlayer. If anything goes wrong, we again throw an unchecked RuntimeException.

@Override public void dispose() {

if (mediaPlayer.isPlaying())

mediaPlayer.stop(); mediaPlayer.release();

The dispose() method first checks if the MediaPlayer is still playing, and if so, stops it. Otherwise the call to MediaPlayer.release() would 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 — something MediaPlayer.isPlaying() does not necessarily tell us, as it returns false in case the MediaPlayer is paused but not stopped.

@Override public void play() {

if (mediaPlayer.isPlaying()) return;

synchronized (this) { if (!isPrepared)

mediaPlayer.prepare(); mediaPlayer.start();

} catch (IllegalStateException e) {

e.printStackTrace(); } catch (IOException e) { e.printStackTrace();

The play() method is a little involved. If we are already playing, we simply return from the function. Next we have a mighty try...catch block within which we first check if the MediaPlayer is already prepared based on our flag, and prepare it if needed. If all goes well, we call the MediaPlayer.start() method, which will start the playback. All this is done in a synchronized block, as we use the isPrepared flag, which might get set on a separate thread due to our implementing the OnCompletionListener interface. In case something goes wrong, we again 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 just delegate 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 again.

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

Finally there's the OnCompletionListener.onCompletion() method that the AndroidMusic class implements. All it does is set the isPrepared flag in a synchronized block so the other methods don't start throwing exceptions out of the blue.

Next we'll move on to our input-related classes.

0 0

Post a comment