อุปกรณ์ Android TV สามารถเชื่อมต่อเอาต์พุตเสียงหลายรายการพร้อมกันได้ เช่น ลำโพงทีวี โฮมเธียเตอร์ที่เชื่อมต่อ HDMI หูฟังบลูทูธ และอื่นๆ อุปกรณ์เอาต์พุตเสียงเหล่านี้รองรับความสามารถด้านเสียงที่แตกต่างกันได้ เช่น การเข้ารหัส (Dolby Digital+, DTS และ PCM), อัตราการสุ่มตัวอย่าง และช่อง ตัวอย่างเช่น ทีวีที่เชื่อมต่อ HDMI รองรับการเข้ารหัสหลายแบบ ขณะที่หูฟังบลูทูธที่เชื่อมต่อมักจะรองรับเฉพาะ PCM
รายการอุปกรณ์เสียงที่พร้อมใช้งานและอุปกรณ์เสียงที่กำหนดเส้นทางอาจเปลี่ยนแปลงได้ด้วยการเสียบอุปกรณ์ HDMI ขณะทำงาน การเชื่อมต่อหรือยกเลิกการเชื่อมต่อหูฟังบลูทูธ หรือผู้ใช้เปลี่ยนการตั้งค่าเสียง เนื่องจากความสามารถด้านเอาต์พุตเสียงอาจเปลี่ยนแปลงได้แม้ในขณะที่แอปกำลังเล่นสื่อ แอปจึงต้องปรับให้เข้ากับการเปลี่ยนแปลงเหล่านี้อย่างราบรื่นและเล่นต่อในอุปกรณ์เสียงที่กำหนดเส้นทางใหม่และความสามารถของอุปกรณ์ การส่งออกรูปแบบเสียงที่ไม่ถูกต้องอาจทำให้เกิดข้อผิดพลาดหรือไม่มีเสียงเล่น
แอปมีความสามารถในการส่งออกเนื้อหาเดียวกันในการเข้ารหัสหลายแบบเพื่อมอบประสบการณ์การฟังเสียงที่ดีที่สุดแก่ผู้ใช้ ทั้งนี้ขึ้นอยู่กับความสามารถของอุปกรณ์เสียง เช่น ระบบจะเล่นสตรีมเสียงที่เข้ารหัส Dolby Digital หากทีวีรองรับ ขณะที่จะเลือกสตรีมเสียง PCM ที่รองรับอย่างกว้างขวางกว่าเมื่อไม่มีการรองรับ Dolby Digital คุณดูรายการตัวถอดรหัส Android ในตัว ที่ใช้แปลงสตรีมเสียงเป็น PCM ได้ใน รูปแบบสื่อที่รองรับ
ในเวลาเล่น แอปสตรีมมิงควรสร้าง
AudioTrack ด้วย
AudioFormat ที่ดีที่สุดซึ่งอุปกรณ์
เสียงเอาต์พุตรองรับ
สร้างแทร็กที่มีรูปแบบที่เหมาะสม
แอปควรสร้าง 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(); }
ในตัวอย่างนี้ preferredFormats คือรายการของ
AudioFormat อินสแตนซ์ โดยเรียงลำดับจากรายการที่ต้องการมากที่สุดไปน้อยที่สุด
getDirectProfilesForAttributes()
จะแสดงผลรายการออบเจ็กต์ที่รองรับสำหรับอุปกรณ์เสียงที่กำหนดเส้นทางอยู่ในปัจจุบันพร้อม
AudioProfile
ที่ระบุ
AudioAttributes ระบบจะวนซ้ำรายการ AudioFormat ที่ต้องการจนกว่าจะพบ AudioProfile ที่รองรับซึ่งตรงกัน และจัดเก็บ AudioProfile นี้เป็น bestAudioProfile
จากนั้นจะกำหนดอัตราการสุ่มตัวอย่างและมาสก์ช่องที่เหมาะสมจาก bestAudioProfile
และสุดท้ายจะสร้างอินสแตนซ์ AudioFormat
ที่เหมาะสม
สร้างแทร็กเสียง
แอปควรใช้ข้อมูลนี้เพื่อสร้าง AudioTrack สำหรับ AudioFormat คุณภาพสูงสุดที่อุปกรณ์เสียงเริ่มต้นรองรับ (และพร้อมใช้งานสำหรับเนื้อหาที่เลือก)
สกัดกั้นการเปลี่ยนแปลงอุปกรณ์เสียง
หากต้องการสกัดกั้นและตอบสนองต่อการเปลี่ยนแปลงอุปกรณ์เสียง แอปควรทำดังนี้
- สำหรับ API ระดับ 24 ขึ้นไป ให้เพิ่ม
OnRoutingChangedListenerเพื่อตรวจสอบการเปลี่ยนแปลงอุปกรณ์เสียง (HDMI, บลูทูธ และอื่นๆ) - สำหรับ API ระดับ 23 ให้ลงทะเบียน
AudioDeviceCallbackเพื่อรับการเปลี่ยนแปลงในรายการอุปกรณ์เสียงที่พร้อมใช้งาน - สำหรับ API ระดับ 21 และ 22 ให้ตรวจสอบเหตุการณ์การเสียบ HDMI และใช้ข้อมูลเพิ่มเติมจากการออกอากาศ
- ลงทะเบียน
BroadcastReceiverเพื่อตรวจสอบBluetoothDeviceการเปลี่ยนแปลงสถานะ สำหรับอุปกรณ์ที่ต่ำกว่า API 23 ด้วย เนื่องจากAudioDeviceCallbackยังไม่ รองรับ
เมื่อตรวจพบการเปลี่ยนแปลงอุปกรณ์เสียงสำหรับ AudioTrack แอปควรตรวจสอบความสามารถด้านเสียงที่อัปเดตแล้ว และหากจำเป็น ให้สร้าง AudioTrack ใหม่ด้วย AudioFormat อื่น ให้ทำเช่นนี้หากระบบรองรับการเข้ารหัสคุณภาพสูงขึ้นแล้วหรือการเข้ารหัสที่ใช้ก่อนหน้านี้ไม่ได้รับการรองรับอีกต่อไป
โค้ดตัวอย่าง
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);