SoundPool

Intro

So far, you have learned how to play audio files using the MediaPlayer.  And while it is a fairly simple process once you understand the MediaPlayer lifecycle, the MediaPlayer class has certain properties that make it not so well suited for certain applications.  For example, if you have an app that has many sound effects which repeat often and may need to play simultaneously and instantaneously, then you will most certainly not be satisfied with MediaPlayer! The lag time in loading files, coupled with the CPU load as a result of decoding audio streams would make for an unpleasant user experience.  Luckily, you have another option...use a SoundPool!  The SoundPool class does all those things for you by preloading your sound clips into memory, meaning that they're standing by for instant playback.  And because the audio streams are also decoded in advance, the CPU has less work to do each time a sound is played!

Creating a SoundPool

So I can hear you asking yourself, "Self, how do I get one of these magical SoundPools?"  Well, you've come to the right place.  Up until the release of API 21, SoundPools were created using the SoundPool class's constructor, which has the following definition:

SoundPool(int maxStreams, int streamType, int srcQuality)

Of note, the last parameter (srcQuality) is not implemented, so you can leave it as 0.  So, in order to create a SoundPool object that could handle up to 4 simultaneous audio streams, you call the constructor like so:

SoundPool mSoundPool = new SoundPool(4, AudioManager.STREAM_MUSIC, 0);

With the release of API 21, the method for creating a SoundPool was updated.  The new way to create a SoundPool is by using A SoundPool.Builder, like so:

SoundPool.Builder spb = new SoundPool.Builder();
spb.setMaxStreams(4);
SoundPool mSoundPool = spb.build();

In order to use the newer SoundPool.Builder class, your app must be set to a minimum sdk value of 21 (in Android Studio this is set using the app build.gradle file.  In Eclipse, it is set in the manifest file).

Loading Sound Clips

Once you have a SoundPool, the next step is to load up all the sound clips it will be handling.  The method call to make this happen is load(), which looks like this:

int sound1 = mSoundPool.load(this, R.raw.filename, 1);

The load method takes three arguments: the Context, file location, and priority.  In this example, I have used a file located in the res > raw folder like you did in the MediaPlayer lesson.  But if your file is located in the assets folder or somewhere else in the file structure, have no fear!  The load method is overloaded, so there's probably a version to fit you needs (don't you love polymorphism?).  To see the other versions of load, follow the SoundPool link in the Intro section.

The loading of sound clips occurs asynchronously (I'm sure you can imagine why).  We wouldn't want to attempt playing a sound that hasn't finished loading, so we need some way of knowing when the loading process is complete.  To accomplish this, we'll use the SoundPool OnLoadCompleteListener:

public class MainActivity extends ActionBarActivity implements SoundPool.OnLoadCompleteListener {

Next, set the SoundPool to use  the implemented listener:

mSoundPool.setOnLoadCompleteListener(this);

Finally, implement the OnLoadComplete method:

@Override
public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
    // do stuff here
}

Each time a sound is loaded in the SoundPool, the OnLoadComplete listener is triggered.  If you have more than one sound clip, you should keep track of which ones have been loaded.

Playing Sounds

Once all sounds are loaded, they're ready to play as needed.  Playing a sound is as simple as calling the play() method...but there are several options that you must specify.  The method definition looks like this:

play (int soundID, float leftVolume, float rightVolume, int priority, int loop, float rate)

The play method returns an integer value.  A return value of 0 means something went wrong.  The play parameters deserve a more detailed explanation:

Like the MediaPlayer class, there are also methods for pausing, resuming and stopping a sound after it is playing.

Releasing the SoundPool

When you are finished using the SoundPool, you should use the release() method to free up any system resources.  Once a SoundPool has been released, it can no longer be used, so you should nullify it:

if(mSoundPool != null) {
    mSoundPool.release();
    mSoundPool = null;
}

I'll leave it to you to think about the appropriate place for this to occur in terms of your Activity's life cycle.

Sample App

You know what I always say: the best way to learn this stuff is by doing.  Let's build a sample app to implement everything we've discussed so far.  Instead of starting from scratch, we'll just add to the app that we built in the MediaPlayer lesson. For this sample app, download the following set of sound effects and place them in your app's res > raw directory:

Sample Sound Effects 

Now we're ready to go.  The first this we'll need to modify is the layout file.  all we need to do is to add buttons.  When clicked, each button will trigger the SoundPool to play a sound clip:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="10dp"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="MediaPlayer"
        android:textAppearance="?android:attr/textAppearanceLarge" />

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:paddingBottom="25dp">

        <ImageButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="playPlayer"
            android:src="@android:drawable/ic_media_play" />

        <ImageButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="pausePlayer"
            android:src="@android:drawable/ic_media_pause" />

        <ImageButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="stopPlayer"
            android:src="@android:drawable/ic_menu_close_clear_cancel" />

    </LinearLayout>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="SoundPool"
        android:textAppearance="?android:attr/textAppearanceLarge" />

    <Button
        android:id="@+id/soundButton1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Sound 1" />

    <Button
        android:id="@+id/soundButton2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Sound 2" />

    <Button
        android:id="@+id/soundButton3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Sound 3" />

    <Button
        android:id="@+id/soundButton4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Sound 4" />

</LinearLayout>

Once the layout file is updated, we'll need to update the MainActivity.Java file.  Use the sample code from the lesson above to make the necessary changes.  Your end result should look something like this:

public class MainActivity extends ActionBarActivity implements View.OnClickListener, SoundPool.OnLoadCompleteListener {

    MediaPlayer mediaPlayer;
    
    SoundPool mSoundPool;
    Button soundButton1, soundButton2, soundButton3, soundButton4;
    int sound1, sound2, sound3, sound4;
    int numSoundsLoaded = 0;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // instantiate buttons for SoundPool example
        soundButton1 = (Button) findViewById(R.id.soundButton1);
        soundButton2 = (Button) findViewById(R.id.soundButton2);
        soundButton3 = (Button) findViewById(R.id.soundButton3);
        soundButton4 = (Button) findViewById(R.id.soundButton4);
        
        // set onClick listener for each button
        soundButton1.setOnClickListener(this);
        soundButton2.setOnClickListener(this);
        soundButton3.setOnClickListener(this);
        soundButton4.setOnClickListener(this);
        
    }

    @Override
    public void onStart(){
        super.onStart();
        
        // Create and prepare MediaPlayer
        initializePlayer();
        
        // create SoundPool
        mSoundPool = new SoundPool(4, AudioManager.STREAM_MUSIC, 0);
        
        // the way to create a SoundPool with API 21 and up:
//        SoundPool.Builder spb = new SoundPool.Builder();
//        spb.setMaxStreams(4);
//        SoundPool mSoundPool = spb.build();

        // use onLoadComplete Listener implemented by Activity
        mSoundPool.setOnLoadCompleteListener(this);
        
        // load sound files into SoundPool using res > raw id
        // parameters: (context, file_id, priority)
        sound1 = mSoundPool.load(this, R.raw.wilhelm_scream, 1);
        sound2 = mSoundPool.load(this, R.raw.boing, 1);
        sound3 = mSoundPool.load(this, R.raw.blast, 1);
        sound4 = mSoundPool.load(this, R.raw.wand, 1);
        
    }

    @Override
    public void onStop(){
        super.onStop();
        
        if(mediaPlayer != null) {
            destroyPlayer();
        }

        if(mSoundPool != null) {
            mSoundPool.release();
            mSoundPool = null;
        }
    }

    @Override
    public void onClick(View v){
        if(numSoundsLoaded == 4){
            if(v == soundButton1){
                mSoundPool.play(sound1, 1, 1, 1, 1, 1);
            } else if(v == soundButton2){
                mSoundPool.play(sound2, 1, 1, 1, 1, 1);
            } else if(v == soundButton3){
                mSoundPool.play(sound3, 1, 1, 1, 1, 1);
            } else if(v == soundButton4){
                mSoundPool.play(sound4, 1, 1, 1, 1, 1);
            }
        }
        
    }

    @Override
    public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
        // let us know that a sound has been loaded by the SoundPool
        numSoundsLoaded++;
        Toast.makeText(this, "Sound " + numSoundsLoaded + " Loaded", Toast.LENGTH_SHORT).show();
    }

    private void initializePlayer(){
        if(mediaPlayer == null){
            mediaPlayer = MediaPlayer.create(getBaseContext(),
                    R.raw.sound_file_1);
        } 
    }

    private void destroyPlayer(){
        if(mediaPlayer.isPlaying()){
            mediaPlayer.stop();
        }
        mediaPlayer.release();
        mediaPlayer = null;
    }

    public void playPlayer(View view){
        initializePlayer();
        mediaPlayer.start();
    }

    public void pausePlayer(View view){
        if(mediaPlayer != null) {
            mediaPlayer.pause();
        }
    }

    public void stopPlayer(View view){
        if(mediaPlayer != null) {
            destroyPlayer();
        }
    }

}

Now, run the app and have fun with your new sound board!