音訊功能

Android TV 裝置可以同時連接多個音訊輸出裝置:電視喇叭、與 HDMI 連線的家用電影院、藍牙耳機等。這些音訊輸出裝置可支援不同的音訊功能,例如編碼 (Dolby Digital+、DTS 和 PCM)、取樣率及聲道。舉例來說,連接 HDMI 的電視可支援多種編碼,而連線的藍牙耳機通常僅支援 PCM。

可用的音訊裝置清單和已轉送的音訊裝置可能會因為插入熱線的 HDMI 裝置、連接或拔除藍牙耳機,或是使用者變更音訊設定而改變。由於即使應用程式正在播放媒體,音訊輸出功能仍可能有所變化,因此應用程式需要妥善適應這些異動,並在新的轉送音訊裝置和其功能上繼續播放。輸出錯誤的音訊格式可能會導致錯誤或無法播放音效。

應用程式能夠以多種編碼輸出相同內容,依據音訊裝置的功能提供使用者最佳音訊體驗。舉例來說,系統會在電視支援 Dolby Digital 編碼的音訊串流時播放,而在不支援 Dolby Digital 的情況下,則會選擇更廣泛支援的 PCM 音訊串流。如要瞭解用來將音訊串流轉換為 PCM 的內建 Android 解碼器清單,請參閱支援的媒體格式

在播放時,串流應用程式應建立 AudioTrack,並提供輸出音訊裝置支援的最佳 AudioFormat

建立格式正確的音軌

應用程式應建立 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 物件清單。系統會疊代偏好的 AudioFormat 項目清單,直到找到相符的支援的 AudioProfile 為止。這個 AudioProfile 會儲存為 bestAudioProfile。最佳取樣率和管道遮罩由 bestAudioProfile 決定。最後,請建立適當的 AudioFormat 執行個體。

建立音軌

應用程式應使用這項資訊,針對預設音訊裝置 (且適用於所選內容) 支援的最高品質 AudioFormat 建立 AudioTrack

攔截音訊裝置變更

如要攔截及回應音訊裝置變更,應用程式應符合下列條件:

  • 如果 API 級別等於或大於 24,請新增 OnRoutingChangedListener 以監控音訊裝置的變更 (HDMI、藍牙等)。
  • 針對 API 級別 23,註冊 AudioDeviceCallback 以接收可用音訊裝置清單的變更。
  • 若是 API 級別 21 和 22,請監控 HDMI 插頭事件,並使用廣播訊息中的額外資料。
  • 此外,由於系統尚未支援 AudioDeviceCallback,因此也註冊 BroadcastReceiver 以監控 API 23 以下版本裝置的 BluetoothDevice 狀態變更。

當偵測到 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);