音声フォーカスの管理

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

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

音声フォーカスは協調を前提としています。アプリでは音声フォーカス ガイドラインに準拠することが推奨されますが、システムによるルールの強制はありません。仮に音声フォーカスを失ったアプリで大音量の再生を続けても、それを妨げるものはありません。ただし、これはユーザーにとって良いエクスペリエンスとは言えないため、このようなマナーの悪いアプリはアンインストールされる可能性が高くなります。

マナーの良いオーディオ アプリと見なされるには、以下の一般的なガイドラインに沿って音声フォーカスを管理します。

  • 再生を開始する直前に requestAudioFocus() を呼び出し、AUDIOFOCUS_REQUEST_GRANTED が返されることを確認します。このガイドの説明に沿ってアプリを設計する場合、requestAudioFocus() の呼び出しはメディア セッションの onPlay() コールバックで行います。
  • 別のアプリに音声フォーカスを取得された場合は、再生を停止または一時停止するか、音量を下げます。
  • 再生が停止したら、音声フォーカスを解放します。

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

  • Android 2.2(API レベル 8)以降の場合、アプリでは requestAudioFocus()abandonAudioFocus() を呼び出すことにより音声フォーカスを管理します。また、コールバックを受信してアプリ固有の音声レベルを管理するため、両方の呼び出しで AudioManager.OnAudioFocusChangeListener を登録する必要があります。
  • Android 5.0(API レベル 21)以降を対象とするアプリの場合、オーディオ アプリで AudioAttributes を使用して、再生する音声の種類を記述する必要があります。たとえば、スピーチを再生するアプリでは、CONTENT_TYPE_SPEECH を指定します。
  • Android 8.0(API レベル 26)以降で実行されるアプリの場合、AudioFocusRequest パラメータを引数に取る requestAudioFocus() メソッドを使用します。AudioFocusRequest には、アプリの音声コンテキストと機能に関する情報が含まれています。システムによる音声フォーカスの得失の管理は、この情報を使用して自動的に行われます。

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

Android 8.0(API レベル 26)以降では、requestAudioFocus() を呼び出すときに 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

    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()
    }
    mediaPlayer = MediaPlayer()
    val focusLock = Any()

    var playbackDelayed = false
    var playbackNowAuthorized = false

    // ...
    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
        }
    }

    // ...
    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) {
                    resumeOnFocusGain = true
                    playbackDelayed = false
                }
                pausePlayback()
            }
            AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
                // ... pausing or ducking depends on your app
            }
        }
    }
    

Java

    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();
    mediaPlayer = new MediaPlayer();
    final Object focusLock = new Object();

    boolean playbackDelayed = false;
    boolean playbackNowAuthorized = false;

    // ...
    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;
        }
    }

    // ...
    @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) {
                    resumeOnFocusGain = true;
                    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 8.0 より前の音声フォーカス

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