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

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