オーディオ機能

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 プラグイベント をモニタリングし、ブロードキャストの追加データを使用します。
  • AudioDeviceCallback はまだサポートされていないため、API 23 より前のデバイスでは BroadcastReceiver を登録して 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);