Managing Multiple Sound Sources in Android with Audio Focus
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.
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.