音声フォーカスの管理

複数の 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 つ目のアプリはフォーカス喪失を通知されます。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 には、アプリの音声コンテキストと機能に関する情報が含まれています。 システムによる音声フォーカスの得失の管理は、この情報を使用して 自動的に行われます。音声フォーカスを解放するときも、やはり abandonAudioFocusRequest() を引数に取るメソッドを呼び出します。AudioFocusRequestフォーカスをリクエストするときと解放するときは、同じ 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() が呼び出されてアプリに通知されます。

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

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

`requestAudioFocus()` を呼び出すときは、継続時間のヒントを指定する必要があります。このヒントは、現在フォーカスを保持して再生を行っている別のアプリによって尊重される可能性があります。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の登録が解除されます。一時的なフォーカスをリクエストした場合は、 一時停止またはダッキングしたアプリに対し、再生の継続または 音量の復元が可能になったことが通知されます。

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

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

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() コールバックでこのメソッドを呼び出します。