Android TV デバイスには、テレビのスピーカー、HDMI 接続のホームシアター、Bluetooth ヘッドフォンなど、複数のオーディオ出力を同時に接続できます。これらのオーディオ出力デバイスは、エンコード(Dolby Digital+、DTS、PCM)、サンプルレート、チャンネルなど、さまざまなオーディオ機能をサポートできます。たとえば、HDMI 接続のテレビはさまざまなエンコードをサポートしていますが、Bluetooth ヘッドフォンは通常 PCM のみをサポートしています。
使用可能なオーディオ デバイスのリストと転送先のオーディオ デバイスは、HDMI デバイスのホットプラグ、Bluetooth ヘッドフォンの接続または切断、ユーザーによるオーディオ設定の変更によっても変更される場合があります。音声出力機能は、アプリがメディアを再生しているときにも変更される可能性があるため、アプリはこれらの変更に適切に対応し、ルーティングされた新しいオーディオ デバイスとその機能で再生を継続する必要があります。出力する音声形式が正しくないと、エラーが発生したり、音声が再生されなかったりすることがあります。
アプリは、同じコンテンツを複数のエンコードで出力し、オーディオ デバイスの機能に応じてユーザーに最適なオーディオ エクスペリエンスを提供できます。たとえば、テレビがドルビー デジタルをサポートしている場合は、ドルビー デジタルでエンコードされた音声ストリームが再生されます。ドルビー デジタルがサポートされていない場合は、より広くサポートされている PCM 音声ストリームが選択されます。オーディオ ストリームを PCM に変換するために使用される組み込みの Android デコーダの一覧については、サポートされているメディア形式をご覧ください。
再生時に、ストリーミング アプリは、出力オーディオ デバイスでサポートされている最適な AudioFormat
を使用して AudioTrack
を作成する必要があります。
適切な形式のトラックを作成する
アプリは AudioTrack
を作成して再生を開始し、getRoutedDevice()
を呼び出して、音声を再生するデフォルトのオーディオ デバイスを決定する必要があります。たとえば、ルーティングされたデバイスとそのオーディオ機能を特定するためにのみ使用される、安全な短いサイレンス PCM エンコード トラックなどです。
サポートされているエンコードを取得する
デフォルトのオーディオ デバイスで使用可能なオーディオ形式を特定するには、getAudioProfiles()
(API レベル 31 以降)または getEncodings()
(API レベル 23 以降)を使用します。
サポートされているオーディオ プロファイルと形式を確認する
AudioProfile
(API レベル 31 以上)または isDirectPlaybackSupported()
(API レベル 29 以上)を使用して、サポートされている形式、チャンネル数、サンプルレートの組み合わせを確認します。
一部の Android デバイスは、出力オーディオ デバイスでサポートされているエンコード以外のエンコードをサポートできます。これらの追加形式は isDirectPlaybackSupported()
で検出する必要があります。この場合、音声データは出力オーディオ デバイスでサポートされている形式に再エンコードされます。getEncodings()
によって返されたリストに含まれていない場合でも、目的の形式のサポートを適切に確認するには、isDirectPlaybackSupported()
を使用します。
予測オーディオ ルート
Android 13(API レベル 33)では、予測型音声ルートを導入しました。デバイスのオーディオ属性のサポートを予測し、アクティブなオーディオ デバイス用のトラックを準備できます。getDirectPlaybackSupport()
を使用すると、特定の形式と属性について、現在ルーティングされているオーディオ デバイスで直接再生がサポートされているかどうかを確認できます。
Kotlin
val format = AudioFormat.Builder() .setEncoding(AudioFormat.ENCODING_E_AC3) .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) .setSampleRate(48000) .build() val attributes = AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .build() if (AudioManager.getDirectPlaybackSupport(format, attributes) != AudioManager.DIRECT_PLAYBACK_NOT_SUPPORTED ) { // The format and attributes are supported for direct playback // on the currently active routed audio path } else { // The format and attributes are NOT supported for direct playback // on the currently active routed audio path }
Java
AudioFormat format = new AudioFormat.Builder() .setEncoding(AudioFormat.ENCODING_E_AC3) .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) .setSampleRate(48000) .build(); AudioAttributes attributes = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .build(); if (AudioManager.getDirectPlaybackSupport(format, attributes) != AudioManager.DIRECT_PLAYBACK_NOT_SUPPORTED) { // The format and attributes are supported for direct playback // on the currently active routed audio path } else { // The format and attributes are NOT supported for direct playback // on the currently active routed audio path }
または、現在ルーティングされているオーディオ デバイスを介して直接メディアを再生するためにサポートされているプロファイルをクエリすることもできます。これにより、サポートされていないプロファイルや、Android フレームワークによってトランスコードされるプロファイルは除外されます。
Kotlin
private fun findBestAudioFormat(audioAttributes: AudioAttributes): AudioFormat { val preferredFormats = listOf( AudioFormat.ENCODING_E_AC3, AudioFormat.ENCODING_AC3, AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_DEFAULT ) val audioProfiles = audioManager.getDirectProfilesForAttributes(audioAttributes) val bestAudioProfile = preferredFormats.firstNotNullOf { format -> audioProfiles.firstOrNull { it.format == format } } val sampleRate = findBestSampleRate(bestAudioProfile) val channelMask = findBestChannelMask(bestAudioProfile) return AudioFormat.Builder() .setEncoding(bestAudioProfile.format) .setSampleRate(sampleRate) .setChannelMask(channelMask) .build() }
Java
private AudioFormat findBestAudioFormat(AudioAttributes audioAttributes) { Stream<Integer> preferredFormats = Stream.<Integer>builder() .add(AudioFormat.ENCODING_E_AC3) .add(AudioFormat.ENCODING_AC3) .add(AudioFormat.ENCODING_PCM_16BIT) .add(AudioFormat.ENCODING_DEFAULT) .build(); Stream<AudioProfile> audioProfiles = audioManager.getDirectProfilesForAttributes(audioAttributes).stream(); AudioProfile bestAudioProfile = (AudioProfile) preferredFormats.map(format -> audioProfiles.filter(profile -> profile.getFormat() == format) .findFirst() .orElseThrow(NoSuchElementException::new) ); Integer sampleRate = findBestSampleRate(bestAudioProfile); Integer channelMask = findBestChannelMask(bestAudioProfile); return new AudioFormat.Builder() .setEncoding(bestAudioProfile.getFormat()) .setSampleRate(sampleRate) .setChannelMask(channelMask) .build(); }
この例では、preferredFormats
は AudioFormat
インスタンスのリストです。リストは、最も優先度の高いものから順に並べられます。getDirectProfilesForAttributes()
は、指定された AudioAttributes
を使用して、現在ルーティングされているオーディオ デバイスでサポートされている AudioProfile
オブジェクトのリストを返します。優先 AudioFormat
アイテムのリストは、一致するサポート対象の AudioProfile
が見つかるまで反復処理されます。この AudioProfile
は bestAudioProfile
として保存されます。最適なサンプルレートとチャンネル マスクは bestAudioProfile
から決定されます。最後に、適切な AudioFormat
インスタンスが作成されます。
音声トラックを作成する
アプリは、この情報を使用して、デフォルトの音声デバイスでサポートされている(および選択したコンテンツで利用可能な)最高品質の AudioFormat
の AudioTrack
を作成する必要があります。
オーディオ デバイスの変更をインターセプトする
音声デバイスの変更をインターセプトして対応するには、アプリは次のことを行う必要があります。
- API レベルが 24 以上の場合は、
OnRoutingChangedListener
を追加して、オーディオ デバイスの変更(HDMI、Bluetooth など)をモニタリングします。 - API レベル 23 では、
AudioDeviceCallback
を登録して、使用可能なオーディオ デバイスのリストに変更が加えられたことを受け取ります。 - API レベル 21 と 22 では、HDMI プラグイベントをモニタリングし、ブロードキャストからの追加データを使用します。
- また、
AudioDeviceCallback
はまだサポートされていないため、API 23 より前のデバイスのBluetoothDevice
状態の変化をモニタリングするためにBroadcastReceiver
を登録します。
AudioTrack
でオーディオ デバイスの変更が検出された場合は、アプリは更新されたオーディオ機能を確認して、必要に応じて別の AudioFormat
で AudioTrack
を再作成する必要があります。より高品質なエンコードがサポートされた場合や、以前使用していたエンコードがサポートされなくなった場合に行います。
サンプルコード
Kotlin
// audioPlayer is a wrapper around an AudioTrack // which calls a callback for an AudioTrack write error audioPlayer.addAudioTrackWriteErrorListener { // error code can be checked here, // in case of write error try to recreate the audio track restartAudioTrack(findDefaultAudioDeviceInfo()) } audioPlayer.audioTrack.addOnRoutingChangedListener({ audioRouting -> audioRouting?.routedDevice?.let { audioDeviceInfo -> // use the updated audio routed device to determine // what audio format should be used if (needsAudioFormatChange(audioDeviceInfo)) { restartAudioTrack(audioDeviceInfo) } } }, handler)
Java
// audioPlayer is a wrapper around an AudioTrack // which calls a callback for an AudioTrack write error audioPlayer.addAudioTrackWriteErrorListener(new AudioTrackPlayer.AudioTrackWriteError() { @Override public void audioTrackWriteError(int errorCode) { // error code can be checked here, // in case of write error try to recreate the audio track restartAudioTrack(findDefaultAudioDeviceInfo()); } }); audioPlayer.getAudioTrack().addOnRoutingChangedListener(new AudioRouting.OnRoutingChangedListener() { @Override public void onRoutingChanged(AudioRouting audioRouting) { if (audioRouting != null && audioRouting.getRoutedDevice() != null) { AudioDeviceInfo audioDeviceInfo = audioRouting.getRoutedDevice(); // use the updated audio routed device to determine // what audio format should be used if (needsAudioFormatChange(audioDeviceInfo)) { restartAudioTrack(audioDeviceInfo); } } } }, handler);