Playing Sounds with SoundPool in Android

Like MediaPlayer, SoundPool can also be used to play audio files. However, unlike MediaPlayer, SoundPool is more suited for quick sound effects as opposed to longer audio files which require streaming. Before we even begin, we’ll need some sounds to work with (preferably ones with a creative commons license, or similar). You can use google for this or if you’re lazy, http://www.wavsource.com/sfx/sfx.htm.

Storing our sounds

Download any number of sounds you like. In your project, create a new folder under app called assets (do not store your sound files in the res folder). You can achieve this by right clicking on app, New -> Folder -> Assets folder. Your project structure should look like the following afterwards, Once you’ve created your assets folder, create another folder within assets. You can name this folder anything you want – we’ll use it for storing our sound files.

Accessing our sound assets

To take steps to make our code clean, we will be making a class dedicated to loading and playing our sounds outside of our Activities and Fragments. We’ll start with building that class now. Create a class called SoundPlayer. We’ll need a few methods which will be used to hide away the logic of accessing and loading our sound files from outside classes. Here’s my implementation of SoundPlayer. You can change/add some code to suit your needs but this is the basic gist.

public class SoundPlayer {
    private static final String TAG = "SoundPlayer";
    private static final String SOUND_FOLDER = "sounds";
    private static final int MAX_SOUNDS = 3;
    private List<Sound> soundList;
    private SoundPool soundPool;
    private final AssetManager assetManager;

    public SoundPlayer(Context appContext) {
        this.assetManager = appContext.getAssets();
        soundPool = new SoundPool(MAX_SOUNDS, AudioManager.STREAM_MUSIC, 0);
        soundList = new ArrayList<>();
        fetchSounds();
    }

    private void fetchSounds() {
        String[] soundFiles;
        try {
            soundFiles = assetManager.list(SOUND_FOLDER);
            Log.d(TAG, "Fetched " + soundFiles.length + " sound files");
        } catch (IOException e) {
            Log.e(TAG, "Error accessing sound folder", e);
            return;
        }
        for (String fileName : soundFiles) {
            try {
                String path = SOUND_FOLDER + "/" + fileName;
                Sound s = new Sound(path);
                load(s);
                soundList.add(s);
            } catch (IOException e) {
                Log.e(TAG, "Could not load sound: " + fileName, e);
            }
        }
    }

    private void load(Sound sound) throws IOException {
        AssetFileDescriptor fileDescriptor = assetManager.openFd(sound.getPathName());
        int soundId = soundPool.load(fileDescriptor, 1);
        sound.setId(soundId);
    }

    public void play(Sound sound) {
        Integer soundId = sound.getId();
        if (soundId == null) return;
        soundPool.play(soundId, 1.0f, 1.0f, 1, 0, 1.0f);
    }

    public void release() {
        Log.d(TAG, "Cleaning resources..");
        soundPool.release();
    }

    public List<Sound> getSounds() {
        return this.soundList;
    }
}

The core components of this class consist of:

  • List for storing our Sound objects
  • AssetManager for accessing our app’s asset files.
  • SoundPool for playing our sound files.

You will also notice two different constants, SOUND_FOLDER and MAX_SOUNDS. They specify the name of the folder we’re storing our sounds and the maximum amount of sounds we allow our SoundPool to play at once, respectively.

fetchSounds() is executed as soon as our class is instantiated. It first tries to locate our sounds folder and lists the number of items inside of it via logcat. For any encountered IOException, the error is printed and the method returns. If our sound files are successfully found, we then loop through each (Strings representing their file path), get its Id via the load() method and add it to the class level soundList. The play(), getSounds() and release() methods are otherwise self explanatory.

Modelling sound files

We will want to create a Sound object to easily access the name, path and id of each of our individual sound files. The object for this will look like the following:

public class Sound {

    private String pathName;
    private String fileName;
    private Integer id;

    public Sound(String pathName){
        this.pathName = pathName;
        String[] splitPath = pathName.split("/");
        this.fileName = splitPath[splitPath.length - 1];
    }

    public String getPathName() {
        return pathName;
    }

    public String getFileName() {
        return fileName;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
}

We can also create a layout which will represent an individual sound in our list. For the purposes of this tutorial, it will be quite basic. Go ahead and create a new layout resource file and name it list_item_sound.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_height="wrap_content"
    android:layout_width="match_parent">

    <Button
        android:id="@+id/sound_button"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        tools:text="name"/>

</FrameLayout>

Displaying our sounds

Now that we have a few of our basic components, let’s make the adapter we’ll be using to display our sounds in a RecyclerView.

public class SoundRecyclerAdapter extends RecyclerView.Adapter<SoundRecyclerAdapter.SoundViewHolder>{

    private static final String TAG = "SoundRecyclerAdapter";

    private final SoundPlayer soundPlayer;

    public SoundRecyclerAdapter(SoundPlayer soundPlayer) {
        this.soundPlayer = soundPlayer;
    }

    @Override
    public SoundViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View soundView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sound, parent, false);
        return new SoundViewHolder(soundView);
    }

    @Override
    public void onBindViewHolder(SoundViewHolder holder, int position) {
        Sound s = soundPlayer.getSounds().get(position);

        holder.sound.setText(s.getFileName());
    }


    @Override
    public int getItemCount() {
        return soundPlayer.getSounds().size();
    }

    public void cleanUp() {
        soundPlayer.release();
    }

    class SoundViewHolder extends RecyclerView.ViewHolder{

        Button sound;

        SoundViewHolder(View itemView) {
            super(itemView);
            sound = itemView.findViewById(R.id.sound_button);
            sound.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    soundPlayer.play(soundPlayer.getSounds().get(getAdapterPosition()));
                }
            });
        }
    }
}

The setup for this RecyclerView.Adapter is quite basic if you’re used to seeing other adapters. Take note to the fact we have an instance of our SoundPlayer at the class level. We encapsulate the logic of our SoundPlayer within the adapter to achieve a separation of concerns. Doing this allows us to remove any code related to playing sounds from our Activities or Fragments. We let the adapter worry about managing and playing our sound files.

Wrap up

As you can see it doesn’t take much work to play different sound files in an Android app. If you choose not to display sounds in a RecyclerView.Adapter, SoundPlayer can be used pretty much anywhere outside of one. Just be sure to call release() at certain lifecycle callbacks such as onPause() or OnDestroy() to clean up memory usage. Full source code can be found here.

Liked the article? Share it!

Leave a Reply

avatar

This site uses Akismet to reduce spam. Learn how your comment data is processed.

  Subscribe  
Notify of