音声フォーカスの管理

2 つ以上の 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 で音声フォーカスをリクエストします。

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

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

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

自動ダッキング

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

システムにダッキングを実装することで、アプリにダッキングを実装する必要はありません。

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

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

  1. 現在再生中の 1 つ目のアプリは、以下の条件をすべて満たしています。

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

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

ユーザーが音声コンテンツを聞いているときは、番組の一部を聞き逃す可能性があるため、自動ダッキングは行われません。たとえば、運転ルートの音声案内はダッキングされません。

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

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

  • アプリに 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 フィールドは必須ですが、他のフィールドはすべて省略可能です。

メソッドNotes
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() を呼び出したときに指定した AudioFocusChangeListener 内の onAudioFocusChange() メソッドの呼び出しを受け取ります。

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