Mobile
Article

Managing Multiple Sound Sources in Android with Audio Focus

By Abbas Suterwala

Sound is a great way to grab user attention, give interface feedback or immerse a player in a game. Imagine if multiple apps all tried to play sounds at the same time. This would result in an unintelligible cacophony and make users reach for the mute button. Android provides a simple API to play music and audio effects and manage different sources. The Android audio focus API lets an app request ‘audio focus’ and lets the app know if it has lost focus so it can react. In this tutorial I will show how to use these APIs in your own apps.

You can find the final code for this tutorial on GitHub.

Requesting Audio Focus for Your App

You should request audio focus in your app before playing any sound. To do this you need to get an instance of the AudioManager. Once you have the instance you can then use requestAudioFocus.

Create a new app with an empty activity and replace the contents of activity_main.xml with the following:

<RelativeLayout 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:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView android:text="Audio Focus"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <Button
            android:id="@+id/btnRequestFocus"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Request Audio Focus"/>

        <Button
            android:id="@+id/btnReleaseFocus"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Release Audio Focus"/>

    </LinearLayout>

</RelativeLayout>

This layout uses the LinearLayout style that contains two buttons. One to request focus and one to release it. Update the contents MainActivity.java to the following:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button btnRequestFocus = (Button)findViewById(R.id.btnRequestFocus);
        btnRequestFocus.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                boolean gotFocus = requestAudioFocusForMyApp(MainActivity.this);
                if(gotFocus) {
                    //play audio.
                }
            }
        });


        Button btnReleaseFocus = (Button)findViewById(R.id.btnReleaseFocus);
        btnReleaseFocus.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //Stop playing audio.
                releaseAudioFocusForMyApp(MainActivity.this);
            }
        });
    }

    private boolean requestAudioFocusForMyApp(final Context context) {
        AudioManager am = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);

        // Request audio focus for playback
        int result = am.requestAudioFocus(null,
                // Use the music stream.
                AudioManager.STREAM_MUSIC,
                // Request permanent focus.
                AudioManager.AUDIOFOCUS_GAIN);

        if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            Log.d("AudioFocus", "Audio focus received");
            return true;
        } else {
            Log.d("AudioFocus", "Audio focus NOT received");
            return false;
        }
    }

    void releaseAudioFocusForMyApp(final Context context) {
        AudioManager am = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
        am.abandonAudioFocus(null);
    }
}

This activity adds click listeners to both buttons. When a user clicks on the Request Audio Focus button it gets the AudioManager instance by calling the getSystemService API and passes the Context.AUDIO_SERVICE context. Once you have the AudioManager, you can request focus by calling the requestAudioFocus API which accepts AudioManager.OnAudioFocusChangeListener as its first parameter. You will see this in more detail later, but for now pass null. The second parameter is the stream type which could be one of the following, depending on the type of sound you want to use:

  • STREAM_ALARM: The audio is an alarm.
  • STREAM_MUSIC: The audio is media like music, video, background sound etc.
  • STREAM_NOTIFICATION: The audio is for a notification.
  • STREAM_RING: The audio is phone ringtone.
  • STREAM_SYSTEM: The audio is a system sound.
  • STREAM_DTMF: The audio is a DTFM tone.

The third parameter is the type of focus you want, for example a permanent or transient focus. It accepts one of the following parameters:

  • AUDIOFOCUS_GAIN: When you want focus for a long time to play audio of a long duration.
  • AUDIOFOCUS_GAIN_TRANSIENT: When you want focus for a short time.
  • AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: When you want focus for a short time and it’s OK for other apps to ‘duck’ rather than stop the audio.

Once you call requestAudioFocus with the appropriate parameters, the API will either return AUDIOFOCUS_REQUEST_GRANTED or AUDIOFOCUS_REQUEST_FAILED. The audio focus request can fail if higher priority sound like a phone call is in progress and you should only start playing audio if the requestAudioFocus API succeeds.

The activity above requested audio focus with the stream type as STREAM_MUSIC and duration as AUDIOFOCUS_GAIN to play a media file for a long time. Once the audio completes, release the focus using the AudioManager abandonAudioFocus API. In the example app, this happens when a user clicks the Release Audio Focus button.

App Example

Handling Audio Focus Events in an App

When your app receives focus it can pass an AudioManager.OnAudioFocusChangeListener which provides callbacks for when an focus change happens.

Suppose your app gains audio focus, and another app requests transient audio focus, focus will be given to the other app. Android will notify your app via an OnAudioFocusChangeListener so that your app can respond to the change. Once the other app abandons its focus your app will regain focus and receive an appropriate callback. This is conceptually similar to the Activity life-cycle events like onStart, OnStop, OnPause, OnResume etc.

To receive focus events, you need to pass an instance of AudioManager.OnAudioFocusChangeListener to the requestAudioFocus and abandonAudioFocus calls. Update code MainActivity.java to the following to receive the callbacks:

public class MainActivity extends Activity {

    private final static String TAG = "AudioFocus";
    private AudioManager.OnAudioFocusChangeListener mOnAudioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {

        @Override
        public void onAudioFocusChange(int focusChange) {
            switch (focusChange) {
                case AudioManager.AUDIOFOCUS_GAIN:
                    Log.i(TAG, "AUDIOFOCUS_GAIN");
                    //restart/resume your sound
                    break;
                case AudioManager.AUDIOFOCUS_LOSS:
                    Log.e(TAG, "AUDIOFOCUS_LOSS");
                    //Loss of audio focus for a long time
                    //Stop playing the sound
                    break;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                    Log.e(TAG, "AUDIOFOCUS_LOSS_TRANSIENT");
                    //Loss of audio focus for a short time
                    //Pause playing the sound
                    break;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                    Log.e(TAG, "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK");
                    //Loss of audio focus for a short time.
                    //But one can duck. Lower the volume of playing the sound
                    break;

                default:
                    //
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button btnRequestFocus = (Button)findViewById(R.id.btnRequestFocus);
        btnRequestFocus.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                boolean gotFocus = requestAudioFocusForMyApp(MainActivity.this);
                if(gotFocus) {
                    //play audio.
                }
            }
        });


        Button btnReleaseFocus = (Button)findViewById(R.id.btnReleaseFocus);
        btnReleaseFocus.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //Stop playing audio.
                releaseAudioFocusForMyApp(MainActivity.this);
            }
        });
    }

    private boolean requestAudioFocusForMyApp(final Context context) {
        AudioManager am = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);

        // Request audio focus for playback
        int result = am.requestAudioFocus(mOnAudioFocusChangeListener,
                // Use the music stream.
                AudioManager.STREAM_MUSIC,
                // Request permanent focus.
                AudioManager.AUDIOFOCUS_GAIN);

        if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            Log.d(TAG, "Audio focus received");
            return true;
        } else {
            Log.d(TAG, "Audio focus NOT received");
            return false;
        }
    }

    void releaseAudioFocusForMyApp(final Context context) {
        AudioManager am = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
        am.abandonAudioFocus(mOnAudioFocusChangeListener);
    }
}

In the AudioManager.OnAudioFocusChangeListener you need to override the onAudioFocusChange function to which the focus change event is passed.

The events can be one of the following:

  • AUDIOFOCUS_LOSS: Audio focus is lost by the app. You should stop playing the sound and release any assets acquired to play the sound.
  • AUDIOFOCUS_LOSS_TRANSIENT: Audio focus is lost by the app for a short period of time. You should pause the audio.
  • AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: Audio focus is lost by the app for a short period of time. You can continue to play the audio but should lower the volume.
  • AUDIOFOCUS_GAIN: Your app has regained audio focus after a loss. You should restart it and increase the volume if decreased in a prior event.

Ducking

At times the Android system or other apps may need to play short high priority sounds. In these cases it will request audio focus as AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK. If your app held audio focus you will receive the event as AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK and you should continue to play sound, but lower the volume. If you are using the standard android MediaPlayer you can lower the volume using the setVolume function.

Respecting Audio Focus

Imagine a conference where all the speakers spoke into the mike at the same time. It would be total chaos and audience members will leave.

Whilst your app isn’t required to respect the focus of the Android system or other apps, it’s a good user experience to so. If you don’t your app will receive bad reviews or frequent uninstalls.

What you app does to react to these events depends on the intended functionality, but it’s important to take this into account.

If you have any comments or questions, please let me know below.

More:

No Reader comments

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Mobile, once a week, for free.