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

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