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(); }
在此範例中,preferredFormats
是 AudioFormat
執行個體的清單。這會依清單中優先順序由高至低排列,最後使用最低偏好排序。getDirectProfilesForAttributes()
會透過提供的 AudioAttributes
,針對目前轉送的音訊裝置傳回支援的 AudioProfile
物件清單。系統會反覆處理偏好的 AudioFormat
項目清單,直到找到相符的支援的 AudioProfile
為止。此 AudioProfile
會儲存為 bestAudioProfile
。最佳取樣率和頻道遮罩的判定依據為 bestAudioProfile
。
最後,建立適當的 AudioFormat
執行個體。
建立音軌
應用程式應使用這項資訊,建立 AudioTrack
,以獲得預設音訊裝置支援的最高高品質 AudioFormat
(且適用於所選內容)。
攔截音訊裝置變更
如要攔截及回應音訊裝置變更,應用程式應符合下列條件:
- 如果 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);