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

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

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

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