音訊功能

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

此外,請拔下 HDMI 裝置、連接或拔除藍牙耳機連線,或是使用者變更音訊設定,查看可用音訊裝置清單和轉送的音訊裝置清單。即使應用程式正在播放媒體,音訊輸出功能仍可能變更,因此應用程式需要妥善調整這些變更,並繼續在新的路由音訊裝置及其功能上播放內容。輸出錯誤的音訊格式可能會導致錯誤或沒有聲音播放。

應用程式可透過多種編碼方式輸出相同內容,根據音訊裝置功能為使用者提供最佳音訊體驗。舉例來說,如果電視支援 Dolby Digital,系統會播放 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() 偵測。在這些情況下,音訊資料會重新編碼為輸出音訊裝置支援的格式。使用 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();
}

在這個範例中,preferredFormatsAudioFormat 例項的清單。這會依清單中優先順序由高至低排列,最後使用最低偏好排序。getDirectProfilesForAttributes() 會透過提供的 AudioAttributes,針對目前轉送的音訊裝置傳回支援的 AudioProfile 物件清單。系統會重複執行偏好 AudioFormat 項目的清單,直到找到相符的支援 AudioProfile 為止。這個 AudioProfile 會儲存為 bestAudioProfile。最佳取樣率和通道遮罩會根據 bestAudioProfile 決定。最後,系統會建立適當的 AudioFormat 例項。

建立音軌

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

攔截音訊裝置變更

如要攔截並回應音訊裝置變更,應用程式應:

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