My 13-year-old son and I have been writing a simple puzzle game to run on his Kindle Fire. For him it’s a chance to learn something about coding. For me it’s a chance to spend time with him and play around with some of the “fun” API calls in the Android SDK that I never seem to find an excuse to use in my day-to-day app development, which tends to be more business focused.
This past weekend we decided the game was officially far enough along that it needed some sound to liven it up. In the past, I’d written a tutorial on creating a sound board. In that post I used the Android media player class, so this seemed like the place to start. In no time I had the title screen playing an MIDI file, and I thought we were jamming. My son was not impressed.
He told me it felt like “an old man game” and what we really needed were cool little guitar riff sound effects. He proceeded to find a series of short audio clips he wanted to inject at different key parts of the game play. This was all fine and good, but the media player class in Android is slow to spin up, has some overhead, and streams the audio from media rather than memory. The end result is that it’s too big and clunky to use for real-time sound effects during game play.
Determined not to be the author of “an old man game,” I dove into the Android documentation. That’s where I discovered the sound pool class. SoundPool is an Android library specifically targeted for playing short audio clips. It’s designed to be responsive, light-weight, and flexible.
The project that follows is a short demonstration of how to use the SoundPool API. Feel free to follow along, or download the entire project.
1. Start a new project in Eclipse. Target the Android SDK 1.6 or higher. Depending on what version of the ADT plugin you have, you may need to rename your startup activity to Main.java in order to match up with my project files.
2. In the /res folder, we need to create a directory called /raw. This is where you will place your sound clips. The documentation says the sound pool class can handle WAV files, but I found it to behave inconsistently until I switched to OGG. Luckily, there are lots of free applications on the web to convert between WAV and OGG, so this shouldn’t be a huge issue.
3. In the /res/raw folder, I’ve added two audio clips: set_trap.ogg and spring_trap.ogg. These are just a couple of audio clips I downloaded of a mouse trap being set and subsequently sprung.
4. Now we need to put together a simple layout in /res/layout/main.xml. For the purposes of the demo, we’ll use a linear layout with a text view and a few buttons.
main.xml <?xml version=<em>"1.0"</em> encoding=<em>"utf-8"</em>?>
<LinearLayout xmlns:android=<em>"http://schemas.android.com/apk/res/android"</em>
android:layout_width=<em>"fill_parent"</em>
android:layout_height=<em>"fill_parent"</em>
android:orientation=<em>"vertical"</em> >
<TextView
android:layout_width=<em>"fill_parent"</em>
android:layout_height=<em>"wrap_content"</em>
android:text=<em>"Sound FX Demo"</em>
android:textSize=<em>"24sp"</em>
android:gravity=<em>"center"</em>
android:textColor=<em>"#ffffff"</em>
android:paddingBottom=<em>"20dip"</em>/>
<LinearLayout
android:layout_width=<em>"fill_parent"</em>
android:layout_height=<em>"wrap_content"</em>
android:orientation=<em>"horizontal"</em>
android:gravity=<em>"center"</em>
android:padding=<em>"8dip"</em> >
<Button
android:layout_width=<em>"wrap_content"</em>
android:layout_height=<em>"wrap_content"</em>
android:text=<em>"Play #1"</em>
android:id=<em>"@+id/fx01"</em>/>
<Button
android:layout_width=<em>"wrap_content"</em>
android:layout_height=<em>"wrap_content"</em>
android:text=<em>"Play #2"</em>
android:id=<em>"@+id/fx02"</em>/>
<Button
android:layout_width=<em>"wrap_content"</em>
android:layout_height=<em>"wrap_content"</em>
android:text=<em>"Stop All"</em>
android:id=<em>"@+id/stop"</em>/>
</LinearLayout>
</LinearLayout>
5. The next step is to code up our Main.java file in the /src folder. We’ll start by declaring a few class level variables and defining a couple of constants.
Main.java
<strong>package</strong> com.authorwjf.soundfx;
<strong>import</strong> java.util.HashMap;
<strong>import</strong> android.app.Activity;
<strong>import</strong> android.content.Context;
<strong>import</strong> android.media.AudioManager;
<strong>import</strong> android.media.SoundPool;
<strong>import</strong> android.os.Bundle;
<strong>import</strong> android.view.View;
<strong>import</strong> android.view.View.OnClickListener;
<strong>import</strong> android.widget.Button;
<strong>public</strong> <strong>class</strong> Main <strong>extends</strong> Activity <strong>implements</strong> OnClickListener{
<strong>private</strong> SoundPool mSoundPool;
<strong>private</strong> AudioManager mAudioManager;
<strong>private</strong> HashMap<Integer, Integer> mSoundPoolMap;
<strong>private</strong> <strong>int</strong> mStream1 = 0;
<strong>private</strong> <strong>int</strong> mStream2 = 0;
<strong>final</strong> <strong>static</strong> <strong>int</strong> <em>LOOP_1_TIME</em> = 0;
<strong>final</strong> <strong>static</strong> <strong>int</strong> <em>LOOP_3_TIMES</em> = 2;
<strong>final</strong> <strong>static</strong> <strong>int</strong> <em>SOUND_FX_01</em> = 1;
<strong>final</strong> <strong>static</strong> <strong>int</strong> <em>SOUND_FX_02</em> = 2;
}
6. It’s time to add our on create override. We have to initialize the sound pool, load the audio clips into memory, and wire up the buttons. Be sure to examine the constructor for the sound pool. In particular, that first parameter is the number of concurrent streams your sound pool can play. Since I only have two audio clips, I’ve passed in two for the purposes of our demo.
Main.java
@Override
<strong>public</strong> <strong>void</strong> onCreate(Bundle savedInstanceState) {
<strong>super</strong>.onCreate(savedInstanceState);
setContentView(R.layout.<em>main</em>);
//set up our audio player
mSoundPool = <strong>new</strong> SoundPool(2, AudioManager.<em>STREAM_MUSIC</em>, 0);
mAudioManager = (AudioManager)getSystemService(Context.<em>AUDIO_SERVICE</em>);
mSoundPoolMap = <strong><span style="text-decoration: underline;">new</span></strong><span style="text-decoration: underline;"> HashMap()</span>;
//load <span style="text-decoration: underline;">fx</span>
mSoundPoolMap.put(<em>SOUND_FX_01</em>, mSoundPool.load(<strong>this</strong>, R.raw.<em>set_trap</em>, 1));
mSoundPoolMap.put(<em>SOUND_FX_02</em>, mSoundPool.load(<strong>this</strong>, R.raw.<em>spring_trap</em>, 1));
//wire buttons
Button b = (Button)findViewById(R.id.<em>fx01</em>);
b.setOnClickListener(<strong>this</strong>);
b = (Button)findViewById(R.id.<em>fx02</em>);
b.setOnClickListener(<strong>this</strong>);
b = (Button)findViewById(R.id.<em>stop</em>);
b.setOnClickListener(<strong>this</strong>);
}
7. We need to write our on click handler. When playing a sound, you need to set the volume between 0.0f to 0.99f. It’s unlikely your user will appreciate you just blasting your sound FX at full volume if they have already turned down the media volume on their device. So, if you take a look at the first part of my onClick override, you’ll see a little trick that works nicely for setting up the correct volume level. Two other noteworthy items are the loop parameter of the play method, which is n – 1 (not n) and the stop method, which uses the handle of a previously opened stream and not the hash index.
Main.java
@Override
<strong>public</strong> <strong>void</strong> onClick(View v) {
<strong>float</strong> streamVolume = mAudioManager.getStreamVolume(AudioManager.<em>STREAM_MUSIC</em>);
streamVolume = streamVolume / mAudioManager.getStreamMaxVolume(AudioManager.<em>STREAM_MUSIC</em>);
<strong>switch</strong> (v.getId()) {
<strong>case</strong> R.id.<em>fx01</em>:
mSoundPool.stop(mStream1);
mStream1= mSoundPool.play(mSoundPoolMap.get(<em>SOUND_FX_01</em>), streamVolume, streamVolume, 1, <em>LOOP_1_TIME</em>, 1f);
<strong>break</strong>;
<strong>case</strong> R.id.<em>fx02</em>:
mSoundPool.stop(mStream2);
mStream2= mSoundPool.play(mSoundPoolMap.get(<em>SOUND_FX_02</em>), streamVolume, streamVolume, 1, <em>LOOP_3_TIMES</em>, 1f);
<strong>break</strong>;
<strong>case</strong> R.id.<em>stop</em>:
mSoundPool.stop(mStream1);
mSoundPool.stop(mStream2);
<strong>break</strong>;
}
}
That’s it. Turn up your speakers and take it for a test drive. As is often the case with the Android SDK, SoundPool has its quirks. However, if you stick with OGG files, are cognizant of the volume, and keep the clips under a few seconds, I suspect you’ll find the class quite handy. And more importantly, you won’t be accused of writing “an old man” app!
Figure A