ความสามารถด้านเสียง

อุปกรณ์ 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);