オーディオ機能

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() を介して検出する必要があります。この場合、音声データは出力音声デバイスでサポートされている形式に再エンコードされます。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();
}

この例では、preferredFormatsAudioFormat インスタンスのリストです。リストの先頭が最も優先されるもので、末尾が最も優先されないものになります。getDirectProfilesForAttributes() は、指定された AudioAttributes を持つ現在ルーティングされているオーディオ デバイスでサポートされている AudioProfile オブジェクトのリストを返します。一致するサポート対象の AudioProfile が見つかるまで、優先 AudioFormat アイテムのリストが反復処理されます。この AudioProfilebestAudioProfile として保存されます。最適なサンプルレートとチャンネル マスクは bestAudioProfile から決定されます。最後に、適切な AudioFormat インスタンスが作成されます。

音声トラックを作成する

アプリは、この情報を使用して、デフォルトの音声デバイスでサポートされている(選択したコンテンツで利用可能な)最高品質の AudioFormatAudioTrack を作成する必要があります。

オーディオ デバイスの変更をインターセプト

音声デバイスの変更をインターセプトして対応するには、アプリは次のことを行う必要があります。

  • API レベルが 24 以上の場合は、OnRoutingChangedListener を追加してオーディオ デバイスの変更(HDMI、Bluetooth など)をモニタリングします。
  • API レベル 23 の場合は、AudioDeviceCallback を登録して、利用可能なオーディオ デバイス リストの変更を受け取ります。
  • API レベル 21 と 22 では、HDMI プラグイベントをモニタリングし、ブロードキャストからの追加データを使用します。
  • また、API 23 より前のデバイスでは AudioDeviceCallback がまだサポートされていないため、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);