音声フォーカスの管理

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

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

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

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

自動ダッキング

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

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

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

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

  1. 1 つ目の現在再生中のアプリが、次の条件をすべて満たしている。

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

これらの条件が満たされると、オーディオ システムは、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 フィールドは必須ですが、他のフィールドはすべて省略可能です。

メソッドメモ
setFocusGain() このフィールドは、すべてのリクエストで必須です。Android 8.0 より前の requestAudioFocus() の呼び出しで使用されていた durationHint と同じ値(AUDIOFOCUS_GAIN、AUDIOFOCUS_GAIN_TRANSIENT、AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK、AUDIOFOCUS_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() コールバックでこのメソッドを呼び出す必要があります。