Maarten Pennings'
Android development: Sound
fampennings / maarten / android / 07sound

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.


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.

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 sizeogg sizequad ogg
impact 263,850 18,104
flute 4,048,624 239,82980,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.


Download the source of this article.

home
sound
Sound




Downloads:
Source
APK





Editor:
Maarten Pennings
Laatste wijziging:
26 oct 2011
File:
error