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

Android TV รองรับเอาต์พุตเสียงพร้อมกันหลายรายการ ได้แก่ ลำโพงทีวี โฮมเธียเตอร์ HDMI บลูทูธ และอื่นๆ อุปกรณ์เหล่านี้รองรับการเข้ารหัส (เช่น Dolby Digital+, DTS, PCM) อัตราการสุ่มตัวอย่าง และช่องสัญญาณที่แตกต่างกัน ทีวีที่เชื่อมต่อ HDMI รองรับรูปแบบต่างๆ มากมาย แต่ Bluetooth รองรับเฉพาะ PCM

อุปกรณ์เสียงและการกำหนดเส้นทางที่พร้อมใช้งานอาจเปลี่ยนแปลงได้ในระหว่างการเล่น โดยผู้ใช้อาจ เสียบ HDMI ขณะทำงาน เชื่อมต่อบลูทูธ หรือแก้ไขการตั้งค่า แอปต้องปรับตัวอย่างเหมาะสม เพื่อให้เล่นได้อย่างต่อเนื่องโดยใช้ความสามารถของอุปกรณ์ใหม่ รูปแบบที่ไม่รองรับ อาจทำให้เกิดข้อผิดพลาดหรือไม่มีเสียง

เสนอการเข้ารหัสหลายรายการเพื่อมอบประสบการณ์การใช้งานที่ดีที่สุดตามความสามารถของอุปกรณ์ เช่น หากรองรับ Dolby Digital ระบบจะใช้ Dolby Digital แต่หากไม่รองรับ แอปจะกลับไปใช้ PCM ดูรูปแบบสื่อที่รองรับสำหรับตัวถอดรหัส 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);