オーディオ機能

Android TV デバイスでは、テレビ スピーカー、HDMI 接続のホーム シネマ、Bluetooth ヘッドフォンなどの複数のオーディオ出力を同時に接続できます。これらのオーディオ出力デバイスは、エンコード(ドルビー デジタル+、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() で検出されます。このような場合、音声データは出力オーディオ機器でサポートされている形式に再エンコードされます。isDirectPlaybackSupported() を使用すると、目的の形式が getEncodings() によって返されるリストに存在しない場合でも、その形式のサポートを適切に確認できます。

予測音声ルート

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 が見つかるまで反復処理されます。この AudioProfilebestAudioProfile として保存されます。最適なサンプルレートとチャンネル マスクは bestAudioProfile から決定されます。最後に、適切な AudioFormat インスタンスを作成します。

音声トラックを作成

アプリはこの情報を使用して、デフォルトのオーディオ機器でサポートされる(および選択したコンテンツで利用可能な)高品質の AudioFormatAudioTrack を作成する必要があります。

オーディオ機器の変更をインターセプトする

オーディオ機器の変更をインターセプトして対応するには、アプリで以下のことを行う必要があります。

  • API レベルが 24 以上の場合は、OnRoutingChangedListener を追加してオーディオ デバイス(HDMI、Bluetooth など)の変更をモニタリングします。
  • API レベル 23 の場合、AudioDeviceCallback を登録して、使用可能なオーディオ デバイスリストの変更を受信します。
  • API レベル 21 と 22 の場合は、HDMI プラグイベントをモニタリングし、ブロードキャストの追加データを使用します。
  • また、BroadcastReceiver を登録して、API 23 より前のデバイスでは AudioDeviceCallback がまだサポートされていないため、BluetoothDevice の状態変化をモニタリングします。

AudioTrack のオーディオ機器の変更が検出されると、アプリは更新されたオーディオ機能を確認し、必要に応じて別の AudioFormatAudioTrack を再作成します。これは、高品質のエンコードがサポートされるようになった場合や、以前に使用したエンコードがサポートされなくなった場合に行います。

サンプルコード

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);