音訊功能

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

此外,使用有線電源的 HDMI 裝置、連接或中斷連線藍牙耳機,或是使用者變更音訊設定,可變更可用音訊裝置清單和轉送的音訊裝置清單。由於即使應用程式正在播放媒體,音訊輸出功能也可能發生變化,因此應用程式必須妥善適應這些變更,並在新的轉送音訊裝置和相關功能繼續播放。輸出的音訊格式有誤可能會導致錯誤或無法播放聲音。

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

在播放時,串流應用程式應使用輸出音訊裝置支援的最佳 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 物件清單。系統會反覆處理偏好的 AudioFormat 項目清單,直到找到相符的支援的 AudioProfile 為止。此 AudioProfile 會儲存為 bestAudioProfile。最佳取樣率和頻道遮罩的判定依據為 bestAudioProfile。 最後,建立適當的 AudioFormat 執行個體。

建立音軌

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

攔截音訊裝置變更

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

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