音声フォーカスの管理

複数の Android アプリから同じ出力ストリームに対して、同時に音声を再生できます。システムはすべてをミックスします。これは技術者から見るとすばらしいことですが、ユーザーにとっては厄介な場合があります。すべての音楽アプリが一斉に再生されないように、Android では「音声フォーカス」という概念が導入されています。一度に 1 つのアプリのみが音声フォーカスを保持できる、という概念です。

アプリで音声を出力する必要がある場合は、音声フォーカスをリクエストします。フォーカスを保持している間は、そのアプリで音声を再生できます。ただし、音声フォーカスを取得しても、再生が完了するまで保持できない場合があります。別のアプリによってフォーカスがリクエストされ、保持している音声フォーカスがプリエンプトされる可能性があるからです。その場合は、アプリで再生を一時停止するか音量を下げて、新しい音声ソースがユーザーに聴こえやすくなるようにします。

Android 12(API レベル 31)より前は、音声フォーカスはシステムによって管理されていませんでした。そのため、アプリ デベロッパーは音声フォーカスのガイドラインに準拠することをおすすめしますが、Android 11(API レベル 30)以前を搭載したデバイスで、音声フォーカスを失った後もアプリが大音量で再生され続ける場合、システムはそれを防ぐことはできません。ただし、このアプリの動作はユーザー エクスペリエンスを低下させ、多くの場合、ユーザーが動作の悪いアプリをアンインストールする原因になります。

適切に設計されたオーディオ アプリでは、以下の一般的なガイドラインに沿って音声フォーカスを管理します。

  • 再生を開始する直前に requestAudioFocus() を呼び出し、呼び出しが AUDIOFOCUS_REQUEST_GRANTED を返すことを確認します。メディア セッションの onPlay() コールバックで requestAudioFocus() を呼び出します。

  • 別のアプリに音声フォーカスを取得された場合は、再生を停止または一時停止するか、音量を下げます。

  • 再生が停止した場合(アプリで再生するコンテンツがなくなった場合など)は、音声フォーカスを解放します。ユーザーが再生を一時停止しても、後で再生を再開する可能性がある場合は、アプリが音声フォーカスを破棄する必要はありません。

  • AudioAttributes を使用して、アプリで再生する音声の種類を記述します。たとえば、音声を再生するアプリの場合は、CONTENT_TYPE_SPEECH を指定します。

音声フォーカスの処理は、実行中の Android のバージョンによって次のように異なります。

Android 12(API レベル 31)以降
音声フォーカスはシステムによって管理されます。別のアプリが音声フォーカスをリクエストすると、システムはアプリからの音声再生を強制的にフェードアウトさせます。また、着信があると音声再生がミュートされます。
Android 8.0(API レベル 26)~ Android 11(API レベル 30)
音声フォーカスはシステムによって管理されませんが、Android 8.0(API レベル 26)以降に導入された変更がいくつかあります。
Android 7.1(API レベル 25)以前
音声フォーカスはシステムによって管理されず、アプリは requestAudioFocus()abandonAudioFocus() を使用して音声フォーカスを管理します。

Android 12 以降の音声フォーカス

音声フォーカスを使用しているメディアアプリやゲームアプリは、フォーカスを失った後に音声を再生すべきではありません。Android 12(API レベル 31)以降では、この動作がシステムによって強制されます。あるアプリが音声フォーカスをリクエストし、別のアプリがフォーカスを保持して再生している場合、システムは再生中のアプリを強制的にフェードアウトさせます。フェードアウトを追加することで、アプリ間の遷移が滑らかになります。

このフェードアウトの動作は、次の条件を満たす場合に発生します。

  1. 1 つ目の(現在再生している)アプリが以下の条件をすべて満たしている。

  2. 2 つ目のアプリが、AudioManager.AUDIOFOCUS_GAIN を使用して音声フォーカスをリクエストしている。

これらの条件が満たされると、オーディオ システムは 1 つ目のアプリをフェードアウトします。フェードアウトが終了すると、1 つ目のアプリはフォーカス喪失を通知されます。アプリが音声フォーカスを再度リクエストするまで、アプリのプレーヤーはミュート状態のままになります。

既存の音声フォーカスの動作

音声フォーカスの切り替えを伴う以下のようなケースについても把握しておく必要があります。

自動ダッキング

自動ダッキング(あるアプリの音声レベルを一時的に下げて、別のアプリの音声が明瞭に聞こえるようにする機能)は、Android 8.0(API レベル 26)で導入されました。

システムによりダッキングの実装が行われるため、デベロッパーがアプリにダッキングを実装する必要はありません。

自動ダッキングは、音声通知が現在再生しているアプリからフォーカスを取得した場合にも発生します。通知の再生開始はダッキング ランプの最後と同期されます。

自動ダッキングは、次の条件を満たす場合に発生します。

  1. 1 つ目の(現在再生している)アプリが以下の条件をすべて満たしている。

  2. 2 つ目のアプリが AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK を使用して音声フォーカスをリクエストしている。

これらの条件が満たされると、オーディオ システムは、2 つ目のアプリがフォーカスを保持している間、1 つ目のアプリのすべてのアクティブなプレーヤーをダッキングします。2 つ目のアプリがフォーカスを破棄すると、1 つ目のアプリのダッキングが解除されます。1 つ目のアプリはフォーカスの喪失時に通知を受けないため、何もする必要はありません。

ユーザーが話し声のコンテンツを聞いている場合、プログラムの一部を聞き逃す可能性があるため、自動ダッキングは行われません。たとえば、ナビの音声案内は音量が下げられません。

着信時に現在の音声再生をミュートする

一部のアプリは正常に動作せず、通話中も音声の再生を続行します。そのため、ユーザーは通話が聞こえるようにするために、問題のアプリを見つけてミュートするか、終了する必要があります。これを防ぐため、着信中に他のアプリの音声をミュートできます。着信した電話通話が受信され、アプリが次の条件を満たしている場合に、システムはこの機能を呼び出します。

  • アプリに AudioAttributes.USAGE_MEDIA または AudioAttributes.USAGE_GAME の使用属性がある。
  • アプリが音声フォーカス(あらゆるフォーカス ゲイン)を正常にリクエストし、音声を再生している。

通話中にアプリが再生を続けると、通話が終了するまで再生はミュートされます。ただし、通話中にアプリが再生を開始した場合、ユーザーが意図的に再生を開始したと見なされるため、そのプレーヤーはミュートされません。

Android 8.0 ~ Android 11 の音声フォーカス

Android 8.0(API レベル 26)以降では、requestAudioFocus() を呼び出すときに AudioFocusRequest パラメータを指定する必要があります。AudioFocusRequest には、アプリの音声コンテキストと機能に関する情報が含まれています。システムによる音声フォーカスの得失の管理は、この情報を使用して自動的に行われます。音声フォーカスを解放するには、AudioFocusRequest を引数に取る abandonAudioFocusRequest() メソッドを呼び出します。フォーカスをリクエストするときと解放するとき、どちらも同じ AudioFocusRequest インスタンスを使用します。

AudioFocusRequest を作成するには、AudioFocusRequest.Builder を使用します。フォーカス リクエストでは常にリクエストのタイプを指定する必要があるため、ビルダーのコンストラクタにそのタイプを含めます。このビルダーのメソッドを使用して、リクエストの他のフィールドを設定します。

FocusGain フィールドは必須ですが、他のフィールドはすべて省略可能です。

メソッド備考
setFocusGain() このフィールドは、すべてのリクエストで必須です。Android 8.0 より前の requestAudioFocus() 呼び出しで使用された durationHint と同様の値(AUDIOFOCUS_GAINAUDIOFOCUS_GAIN_TRANSIENTAUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCKAUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)を取ります。
setAudioAttributes() AudioAttributes はアプリの用途を説明するもので、アプリが音声フォーカスを取得するとき、または失うときに、この値がシステムにより確認されます。この値は、ストリーム タイプよりも優先されます。Android 8.0(API レベル 26)以降では、音量コントロール以外の操作のストリーム タイプはサポートが終了しています。オーディオ プレーヤーで使用するフォーカス リクエストと同じ属性を使用します(この表の下の例を参照)。

まず AudioAttributes.Builder を使用して属性を指定し、次にこのメソッドを使用して属性をリクエストに割り当てます。

指定しない場合、AudioAttributes はデフォルトで AudioAttributes.USAGE_MEDIA になります。

setWillPauseWhenDucked() 別のアプリから AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK を使用したフォーカス リクエストがなされた場合、通常、フォーカスを保持するアプリは onAudioFocusChange() コールバックを受信しません。これは、システムによる自動ダッキングが可能なためです。音量を下げる代わりに再生を一時停止する必要がある場合は、自動ダッキングで説明されているように、setWillPauseWhenDucked(true) を呼び出し、OnAudioFocusChangeListener を作成して設定します。
setAcceptsDelayedFocusGain() フォーカスが別のアプリにロックされていると、音声フォーカスのリクエストが失敗する可能性があります。このメソッドにより、遅延フォーカス取得、つまりフォーカスが利用可能になったときに非同期に取得する機能を使用できます。

なお、遅延フォーカス取得は、音声リクエストで AudioManager.OnAudioFocusChangeListener も指定した場合にのみ機能します。これは、アプリでフォーカスが付与されたことを認識するには、コールバックを受信する必要があるためです。

setOnAudioFocusChangeListener() OnAudioFocusChangeListener は、リクエストで willPauseWhenDucked(true) または setAcceptsDelayedFocusGain(true) も指定する場合にのみ必要です。

このリスナーを設定するメソッドには、ハンドラ引数を使用するものと使用しないものの 2 種類があります。ハンドラとは、リスナーが実行されるスレッドです。ハンドラを指定しない場合は、メインの Looper に関連付けられたハンドラが使用されます。

次の例は、AudioFocusRequest.Builder を使用して AudioFocusRequest を作成し、音声フォーカスのリクエストと解放を行う方法を示しています。

Kotlin

// initializing variables for audio focus and playback management
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
    setAudioAttributes(AudioAttributes.Builder().run {
        setUsage(AudioAttributes.USAGE_GAME)
        setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        build()
    })
    setAcceptsDelayedFocusGain(true)
    setOnAudioFocusChangeListener(afChangeListener, handler)
    build()
}
val focusLock = Any()

var playbackDelayed = false
var playbackNowAuthorized = false

// requesting audio focus and processing the response
val res = audioManager.requestAudioFocus(focusRequest)
synchronized(focusLock) {
    playbackNowAuthorized = when (res) {
        AudioManager.AUDIOFOCUS_REQUEST_FAILED -> false
        AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
            playbackNow()
            true
        }
        AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> {
            playbackDelayed = true
            false
        }
        else -> false
    }
}

// implementing OnAudioFocusChangeListener to react to focus changes
override fun onAudioFocusChange(focusChange: Int) {
    when (focusChange) {
        AudioManager.AUDIOFOCUS_GAIN ->
            if (playbackDelayed || resumeOnFocusGain) {
                synchronized(focusLock) {
                    playbackDelayed = false
                    resumeOnFocusGain = false
                }
                playbackNow()
            }
        AudioManager.AUDIOFOCUS_LOSS -> {
            synchronized(focusLock) {
                resumeOnFocusGain = false
                playbackDelayed = false
            }
            pausePlayback()
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
            synchronized(focusLock) {
                // only resume if playback is being interrupted
                resumeOnFocusGain = isPlaying()
                playbackDelayed = false
            }
            pausePlayback()
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
            // ... pausing or ducking depends on your app
        }
    }
}

Java

// initializing variables for audio focus and playback management
audioManager = (AudioManager) Context.getSystemService(Context.AUDIO_SERVICE);
playbackAttributes = new AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_GAME)
        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        .build();
focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
        .setAudioAttributes(playbackAttributes)
        .setAcceptsDelayedFocusGain(true)
        .setOnAudioFocusChangeListener(afChangeListener, handler)
        .build();
final Object focusLock = new Object();

boolean playbackDelayed = false;
boolean playbackNowAuthorized = false;

// requesting audio focus and processing the response
int res = audioManager.requestAudioFocus(focusRequest);
synchronized(focusLock) {
    if (res == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
        playbackNowAuthorized = false;
    } else if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
        playbackNowAuthorized = true;
        playbackNow();
    } else if (res == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
        playbackDelayed = true;
        playbackNowAuthorized = false;
    }
}

// implementing OnAudioFocusChangeListener to react to focus changes
@Override
public void onAudioFocusChange(int focusChange) {
    switch (focusChange) {
        case AudioManager.AUDIOFOCUS_GAIN:
            if (playbackDelayed || resumeOnFocusGain) {
                synchronized(focusLock) {
                    playbackDelayed = false;
                    resumeOnFocusGain = false;
                }
                playbackNow();
            }
            break;
        case AudioManager.AUDIOFOCUS_LOSS:
            synchronized(focusLock) {
                resumeOnFocusGain = false;
                playbackDelayed = false;
            }
            pausePlayback();
            break;
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
            synchronized(focusLock) {
                // only resume if playback is being interrupted
                resumeOnFocusGain = isPlaying();
                playbackDelayed = false;
            }
            pausePlayback();
            break;
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
            // ... pausing or ducking depends on your app
            break;
        }
    }
}

自動ダッキング

Android 8.0(API レベル 26)では、別のアプリから AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK でフォーカスがリクエストされた際、システムによる自動での音量のダッキングと復元が可能です。その際、アプリの onAudioFocusChange() コールバックは呼び出されません。

自動ダッキングの動作は、音楽や動画の再生アプリでは許容されますが、話し言葉のコンテンツ(オーディオ ブックアプリなど)には向きません。このような場合は、代わりにアプリを一時停止します。

ダッキングを求められたときに、音量を下げる代わりにアプリを一時停止する場合は、その一時停止と再開の動作を実装する onAudioFocusChange() コールバック メソッドを使用して OnAudioFocusChangeListener を作成します。setOnAudioFocusChangeListener() を呼び出してリスナーを登録し、setWillPauseWhenDucked(true) を呼び出して、自動ダッキングを実行する代わりにこのコールバックを使用するようシステムに指示します。

遅延フォーカス取得

通話中の場合など、フォーカスが別のアプリに「ロック」されているため、音声フォーカスのリクエストが許可されないことがあります。この場合、requestAudioFocus() からは AUDIOFOCUS_REQUEST_FAILED が返されます。このようなときは、アプリではフォーカスを取得していないため、音声の再生を開始すべきではありません。

setAcceptsDelayedFocusGain(true) メソッドを使用することで、アプリでフォーカス リクエストを非同期に処理します。このフラグを設定すると、フォーカスがロックされている間になされたリクエストからは AUDIOFOCUS_REQUEST_DELAYED が返されます。音声フォーカスのロック状態が解消されると(通話終了など)、保留中のフォーカス リクエストがシステムにより許可され、onAudioFocusChange() が呼び出されてアプリに通知されます。

遅延フォーカス取得を処理するには、目的の動作を実装する onAudioFocusChange() コールバック メソッドを使用して OnAudioFocusChangeListener を作成し、setOnAudioFocusChangeListener() を呼び出してリスナーを登録する必要があります。

Android 7.1 以前の音声フォーカス

requestAudioFocus() を呼び出すときは、継続時間のヒントを指定する必要があります。このヒントは、現在フォーカスを保持して再生を行っている別のアプリによって尊重される可能性があります。

  • 近い将来に音声(音楽など)を再生する予定で、前の音声フォーカスの保持者に再生の停止を期待する場合は、永続的な音声フォーカス(AUDIOFOCUS_GAIN)をリクエストします。
  • 音声を短時間だけ再生する予定で、前の保持者に再生の一時停止を期待する場合は、一時的なフォーカス(AUDIOFOCUS_GAIN_TRANSIENT)をリクエストします。
  • 音声を短時間だけ再生する予定で、ダッキング(音量低下)がなされるなら前の保持者がそのまま再生を継続してもかまわない場合は、ダッキングを伴う一時的なフォーカス(AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)をリクエストします。この場合は、両方の音声出力が同じ音声ストリームにミックスされます。ダッキングは、音声による運転ナビなど、音声ストリームを断続的に使用するアプリに特に適しています。

requestAudioFocus() メソッドには AudioManager.OnAudioFocusChangeListener も必要です。このリスナーは、メディア セッションを所有するアクティビティまたはサービスで作成します。これにより、他のアプリが音声フォーカスを取得または解放したときにアプリが受け取るコールバック onAudioFocusChange() を実装します。

次のスニペットでは、ストリーム STREAM_MUSIC 上で永続的な音声フォーカスをリクエストし、以降の音声フォーカスの変化を処理するために OnAudioFocusChangeListener を登録しています(変化リスナーについては、音声フォーカスの変化への対応で説明しています)。

Kotlin

audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
lateinit var afChangeListener AudioManager.OnAudioFocusChangeListener

...
// Request audio focus for playback
val result: Int = audioManager.requestAudioFocus(
        afChangeListener,
        // Use the music stream.
        AudioManager.STREAM_MUSIC,
        // Request permanent focus.
        AudioManager.AUDIOFOCUS_GAIN
)

if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // Start playback
}

Java

AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
AudioManager.OnAudioFocusChangeListener afChangeListener;

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

if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // Start playback
}

再生が終了したら、abandonAudioFocus() を呼び出します。

Kotlin

audioManager.abandonAudioFocus(afChangeListener)

Java

// Abandon audio focus when playback complete
audioManager.abandonAudioFocus(afChangeListener);

これにより、フォーカスが不要になったことがシステムに通知され、関連する OnAudioFocusChangeListener の登録が解除されます。一時的なフォーカスをリクエストした場合は、一時停止またはダッキングしたアプリに対し、再生の継続または音量の復元が可能になったことが通知されます。

音声フォーカスの変化への応答

アプリで音声フォーカスを取得した場合は、別のアプリが音声フォーカスをリクエストしたときに、それを解放できなければなりません。このとき、アプリでは requestAudioFocus() を呼び出した際に指定した AudioFocusChangeListeneronAudioFocusChange() メソッドの呼び出しを受けます。

onAudioFocusChange() に渡される focusChange パラメータは、発生中の変化の種類を示しています。これは、フォーカスを取得しようとしているアプリの継続時間のヒントに対応します。これに対し、アプリでは適切に応答する必要があります。

フォーカスの一時的な喪失
フォーカスの変化が一時的なもの(AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK または AUDIOFOCUS_LOSS_TRANSIENT)の場合、アプリではダッキング(自動ダッキングに依存していない場合)するか、再生を一時停止する必要がありますが、それ以外は同じ状態を維持します。

音声フォーカスが一時的に失われている間は、音声フォーカスの変化を監視し続け、フォーカスを取り戻したときに通常の再生を再開できるよう備える必要があります。ブロック元のアプリからフォーカスが解放されると、こちらのアプリではコールバック(AUDIOFOCUS_GAIN)を受け取ります。この時点で、音量を通常のレベルに復元するか、再生を再開できます。

フォーカスの永続的な喪失
音声フォーカスを永続的に失う(AUDIOFOCUS_LOSS)のは、別のアプリで音声を再生する場合です。アプリではすぐに再生を一時停止するようにし、AUDIOFOCUS_GAIN コールバックは受信しません。再生を再開するには、ユーザーが明示的なアクション(通知やアプリの UI で再生トランスポート コントロールを押すなど)を行う必要があります。

次のコード スニペットは、OnAudioFocusChangeListener とその onAudioFocusChange() コールバックの実装方法を示しています。音声フォーカスが永続的に失われた場合、Handler を使用して停止コールバックを遅らせている点に注意してください。

Kotlin

private val handler = Handler()
private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
    when (focusChange) {
        AudioManager.AUDIOFOCUS_LOSS -> {
            // Permanent loss of audio focus
            // Pause playback immediately
            mediaController.transportControls.pause()
            // Wait 30 seconds before stopping playback
            handler.postDelayed(delayedStopRunnable, TimeUnit.SECONDS.toMillis(30))
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
            // Pause playback
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
            // Lower the volume, keep playing
        }
        AudioManager.AUDIOFOCUS_GAIN -> {
            // Your app has been granted audio focus again
            // Raise volume to normal, restart playback if necessary
        }
    }
}

Java

private Handler handler = new Handler();
AudioManager.OnAudioFocusChangeListener afChangeListener =
  new AudioManager.OnAudioFocusChangeListener() {
    public void onAudioFocusChange(int focusChange) {
      if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
        // Permanent loss of audio focus
        // Pause playback immediately
        mediaController.getTransportControls().pause();
        // Wait 30 seconds before stopping playback
        handler.postDelayed(delayedStopRunnable,
          TimeUnit.SECONDS.toMillis(30));
      }
      else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
        // Pause playback
      } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
        // Lower the volume, keep playing
      } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
        // Your app has been granted audio focus again
        // Raise volume to normal, restart playback if necessary
      }
    }
  };

ハンドラでは、次のような Runnable を使用します。

Kotlin

private var delayedStopRunnable = Runnable {
    mediaController.transportControls.stop()
}

Java

private Runnable delayedStopRunnable = new Runnable() {
    @Override
    public void run() {
        getMediaController().getTransportControls().stop();
    }
};

ユーザーが再生を再開した場合に遅延停止が作動しないようにするには、状態の変化に応じて mHandler.removeCallbacks(mDelayedStopRunnable) を呼び出します。たとえば、コールバックの onPlay()onSkipToNext() などで removeCallbacks() を呼び出します。サービスで使用したリソースをクリーンアップする場合も、サービスの onDestroy() コールバックでこのメソッドを呼び出します。