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

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 state changes สำหรับอุปกรณ์ที่ต่ำกว่า API 23 ด้วย เนื่องจากระบบยังไม่รองรับ AudioDeviceCallback is not yet supported.

เมื่อตรวจพบการเปลี่ยนแปลงอุปกรณ์เสียงสำหรับ 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);