Sound
Download the source of this article.
1 Introduction
Many apps, most notably games, require sound. In this article we use the simpler SoundPool instead of the full featured MediaPlayer. The media player seems not a good idea to use it if you need quick (low-latency) playback of short audio fragments, especially if multiple fragments will be playing simultaneously.
We will develop a wrapper class around SoundPool, that could be made into a library if you want to reuse it in multiple projects. It should be noted however, that our wrapper class is so shallow (adds so little) that it's existance is questionable. Our goal is more to explore options for making sound.
2. The sound class
2.1 The sound pool concepts
The standard android SoundPool class stores a collection of audio fragments. Those fragments should first be loaded from resources of your application into the pool. The amount of fragments that can be stored is limited, the limit being supplied when creating an instance of the sound pool.
Once an audio fragment is added, it can be played (once, a given number of times, or indefinitely) at different speeds (normal speed, half the speed, twice the speed, etc). An audio fragment that is being played can be forcibly stopped.
A single audio fragment can be played multiple times. It can even be played simulataneously with itself or other fragments (e.g. with a difference in start time). The maximum number of fagments that a sound pool can play simultaneously equals the number of fragments that can be loaded into the sound pool.
When a sound pool is configured for max fragments, and it is currently playing max fragments simultaneously, a new play command will first stop the playback of some fragment (priority based), before starting the new fragment.
2.2 Id's in the sound pool
There is one thing confusing about the sound pool: it has three notions of id's. See the figure below for a graphical overview.
-
A resource id (resId) is used to identify a resource in the APK file.
The add function requires a resource id, so that it knows which resource to add to the sound pool. -
A sound id (soundId) is used to identify a sound stored in the sound pool.
The add function returns a sound id.
The play function requires a sound id, so that it knows which sound fragment from the pool to play. -
A stream id (streamId) is used to identify a playing sound.
The play function returns a stream id.
The stop function requires a stream id, so that it knows which playing sound to stop.
The three id's in the sound pool
2.3 The sound class code
In this section, we develop a Sound class that plays audio fragments. As mentioned in the introduction, the sound class is a rather shallow class. It encapsulates a SoundPool to store and play sound fragments. It also encapsulates a helper object AudioManager, which controls the volume.
public class Sound { private SoundPool mSoundPool; private AudioManager mAudioManager; public Sound( Context context, int max ) { mSoundPool = new SoundPool( max, AudioManager.STREAM_MUSIC, 0 ); mAudioManager = (AudioManager)context.getSystemService( Context.AUDIO_SERVICE ); } ... }
The bodies of the methods are rather simple, because not much functionality is added. Note that we have an advanced play function (where the repeat count and replay rate can be passed), and a simple one shot play function. For the full class file (including javadoc), download the accompanying zipped source file.
public int add( Context context, int resId ) { int soundId= mSoundPool.load( context, resId ,1 ); return soundId; } public int play( int soundId, int repeat, float rate ) { float streamVolume= mAudioManager.getStreamVolume( AudioManager.STREAM_MUSIC ); streamVolume= streamVolume / mAudioManager.getStreamMaxVolume( AudioManager.STREAM_MUSIC ); int streamId= mSoundPool.play( soundId, streamVolume, streamVolume, 1, repeat, rate ); return streamId; } public int play( int soundId ) { return play( soundId, 0, 1.0f ); } public void stop( int streamId ) { mSoundPool.stop( streamId ); }
3 Audio fragments
3.1 Getting audio files
Before we switch to the application, let's spend some time on getting audio fragments. I can not find which formats are supported. I use ogg, so that seems to work. The documentatin mentions wav and mp3, so those are likely to work to.
You need to get an audio file from somewhere. I visited free-loops for this article, but if you want to write a real app for the market, you better make sure you have the rights to use audio fragments. From that site I downloaded three fragments.
- impact officially known as 8190-impact-metal at free-loops
- flute officially known as 7987-native-american-flute at free-loops
- anvil officially known as 8194-anvil-impact at free-loops
3.2 Compressing audio files
These downloaded files are wav files, so they eat up quite a lot of space in you application (APK file). To save space, we compress them, and ogg is probably the audio compression standard of choice on android. I used Sound eXchange for that. SoX can be downloaded at sourceforge That website also has the manual.
I think it is good practice, to first check what kind of audio fragment we have at hand. I typically run SoX on the file setting verbosity (-V), and specifying no file (actually the null file) as output (-n). When we do that for the flute we get the following output
C:\Users\Maarten>sox "Native American Flute-23961-Free-Loops.com.wav" -V -n sox.exe: SoX v14.3.2 sox.exe INFO formats: detected file format type `wav' Input File : 'Native American Flute-23961-Free-Loops.com.wav' Channels : 2 Sample Rate : 44100 Precision : 16-bit Duration : 00:00:22.95 = 1012145 samples = 1721.34 CDDA sectors File Size : 4.05M Bit Rate : 1.41M Sample Encoding: 16-bit Signed Integer PCM Endian Type : little Reverse Nibbles: no Reverse Bits : no Output File : '' (null) Channels : 2 Sample Rate : 44100 Precision : 16-bit Duration : 00:00:22.95 = 1012145 samples = 1721.34 CDDA sectors sox.exe INFO sox: effects chain: input 44100Hz 2 channels sox.exe INFO sox: effects chain: output 44100Hz 2 channels
The file takes 22.95 seconds, with 44100 samples per second, each 16 bits for 2 channels. This makes 22.95 × 44100 × 2 × 2 = 4048380 bytes or 4.05 million bytes (see blue). This is big. Let's see what compression does for us size-wise.
sox "Impact Metal-24570-Free-Loops.com.wav" impact.ogg sox "Anvil Impact-24582-Free-Loops.com.wav" anvil.ogg sox "Native American Flute-23961-Free-Loops.com.wav" flute1.ogg sox "Native American Flute-23961-Free-Loops.com.wav" flute2.ogg rate 22050 channels 1
We convert the wav files with the above shown commands. Note the we also try reducing to half the rate and switch to mono for the large flute file. Next, we have a look at the file sizes.
Fragment | wav size | ogg size | quad ogg |
---|---|---|---|
impact | 263,850 | 18,104 | |
flute | 4,048,624 | 239,829 | 80,992 |
anvil | 114,344 | 9,597 |
3.3 Storing audio files
The final step is adding these ogg files as resources to our project. They should be added to a raw directory (which must be ceated) of the resources (res) directory. If you want to load a sound from the raw resource file impact.ogg, you would specify R.raw.impact as the resource ID. Note that the extension is dropped. This means you cannot have both an impact.wav and an impact.ogg in the res/raw directory.
Adding the sound fragments as resources to the project.
4 The activity
4.1 The activity concepts
The activity we develop, uses all three sound fragments: the short impact, the long flute, and the short anvil. We will have three buttons (top row of the activity shown below): the first plays impact once, the second plays flute once and the third plays anvil indefinitly. Note that each button press starts playing a sound fragment, so when pressing the second button twice, we will here two flutes playing simultaneously. Since the anvil will never stop, we dynamically add a stop button for each press on the third button (bottom row of the activity shown below).
The flute and 4 anvils playing
4.2 The activity layout
The layout consists of two horizontal layouts (red), wrapped in a vertical layout. The first horizontal layout contains the three buttons (that each start a sound fragment). The second horizontal layout will contain the buttons to stop anvil play-back.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="wrap_content" > <Button android:text="Impact once" android:id="@+id/butimpactonce" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <Button android:text="Flute once" android:id="@+id/butfluteonce" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <Button android:text="Anvil start" android:id="@+id/butanvilstart" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/linearlayout" android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="wrap_content" > </LinearLayout> </LinearLayout>
Note that the buttons and the second linear layout have been given an id (blue). These views need to be picked up by the java code (to add listeners respectively buttons).
4.3 The activity code
The framework of the code is straightforward. After inflating the xml layout (setContentView) in the onCreate, we lookup the buttons and the (second horizontal) linear layout, and set the button listeners. We also create an instance of our Sound class, and add three sound fragments (see below for details).
public class SoundActivity extends Activity { // References to the views LinearLayout mLinearLayout; Button mButImpactOnce; Button mButFluteOnce; Button mButAnvilStart; // References to the sounds Sound mSound; int mSoundImpact; int mSoundAnvil; int mSoundFlute; // The listeners for the buttons OnClickListener mButImpactOnce_OnClickListener = new OnClickListener() { ... }; OnClickListener mButFluteOnce_OnClickListener = new OnClickListener() { ... }; OnClickListener mButAnvilStart_OnClickListener = new OnClickListener() { ... }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Lookup the views mLinearLayout = (LinearLayout)findViewById(R.id.linearlayout); mButImpactOnce= (Button) findViewById(R.id.butimpactonce); mButFluteOnce = (Button) findViewById(R.id.butfluteonce); mButAnvilStart= (Button) findViewById(R.id.butanvilstart); // Set the listeners mButImpactOnce.setOnClickListener(mButImpactOnce_OnClickListener); mButFluteOnce. setOnClickListener(mButFluteOnce_OnClickListener); mButAnvilStart.setOnClickListener(mButAnvilStart_OnClickListener); // Add the sound fragments mSound= new Sound(this,5); mSoundImpact = mSound.add(this, R.raw.impact); mSoundFlute = mSound.add(this, R.raw.flute); mSoundAnvil = mSound.add(this, R.raw.anvil); } }
Each of the buttons gets a listener, which starts the appropriate sound fragment. The first two listeners are simple.
OnClickListener mButImpactOnce_OnClickListener = new OnClickListener() { @Override public void onClick(View v) { mSound.play(mSoundImpact); } }; OnClickListener mButFluteOnce_OnClickListener = new OnClickListener() { @Override public void onClick(View v) { mSound.play(mSoundFlute); } };
The third listener is tricky. It performs a couple of tasks (red). Firstly, it will start playing the anvil sound fragment continuously. It records the stream id returned by the play command. Next, it will dynamically create a stop button, and give it a caption (with a sequence number). Then, it will set a listener for the stop button. Finally, it adds the stop button to the second horizontal view.
The listener for the stop button is created inline with an anonymous class (check the button click for details on button click handlers). It performs two tasks: stop the anvil sound, and remove the stop button from the horizontal layout.
int stop; OnClickListener mButAnvilStart_OnClickListener = new OnClickListener() { @Override public void onClick(View v) { // Play the sound continuously final int streamId=mSound.play(mSoundAnvil, -1, 0.5f ); // Create a fresh button to stop this newly created sound final Button b= new Button(SoundActivity.this); // The stop button should have a unique caption b.setText( "Stop anvil " + (++stop) ); // A click on the stop button should stop the sound and remove the button b.setOnClickListener( new OnClickListener() { @Override public void onClick(View v) { mSound.stop(streamId); mLinearLayout.removeView(b); } }); // Finally, add the stop button to the linear layout view mLinearLayout.addView(b); } };
5 Behavior of the application
We are done developing our application. It has some properties worth mentioning.
- When we press the "Flute once" button twice (with some delay in between), we indeed hear two flutes simultaneously.
- When we press the "Anvil start" button, we keep on hearing the anvil, until we press the freshly appearing "Stop anvil" button.
- When we press the "Anvil start" button two times, we hear two anvils. When pressing "Stop anvil", one anvil stops, the other continues.
- When we press the "Anvil start" button two times and the "Flute once" two times, we have four fragments playing. Now press "Impact once" five times very quickly (last click before first impact stops playing). The flute and anvils are stopped.
- When the anvil is playing and the back button is pressed, the anvil keeps on playing... Even when your android device is switched off (standby)... This needs to be fixed.
Download the source of this article.