Khả năng về âm thanh

Thiết bị Android TV có thể kết nối nhiều đầu ra âm thanh cùng một lúc: loa TV, rạp chiếu phim tại nhà có kết nối HDMI, tai nghe Bluetooth, v.v. Các thiết bị đầu ra âm thanh này có thể hỗ trợ nhiều tính năng âm thanh, chẳng hạn như mã hoá (Dolby Digital+, DTS và PCM), tốc độ lấy mẫu và kênh. Ví dụ: TV có kết nối HDMI hỗ trợ nhiều phương thức mã hoá, trong khi tai nghe Bluetooth được kết nối thường chỉ hỗ trợ PCM.

Danh sách các thiết bị âm thanh hiện có và thiết bị âm thanh được định tuyến cũng có thể thay đổi khi người dùng thay đổi chế độ cài đặt âm thanh thông qua thiết bị HDMI có cắm nóng, kết nối/ngắt kết nối với tai nghe Bluetooth hoặc người dùng thay đổi chế độ cài đặt âm thanh. Vì chức năng đầu ra âm thanh có thể thay đổi ngay cả khi ứng dụng đang phát nội dung nghe nhìn, nên các ứng dụng cần thích ứng linh hoạt với những thay đổi này và tiếp tục phát trên thiết bị âm thanh mới được định tuyến cũng như các tính năng của thiết bị đó. Việc xuất sai định dạng âm thanh có thể dẫn đến lỗi hoặc không phát âm thanh.

Các ứng dụng có khả năng xuất cùng một nội dung ở nhiều chế độ mã hoá để cung cấp cho người dùng trải nghiệm âm thanh tốt nhất tuỳ thuộc vào tính năng của thiết bị âm thanh. Ví dụ: hệ thống sẽ phát luồng âm thanh mã hoá Dolby Digital nếu TV hỗ trợ, trong khi một luồng âm thanh PCM được hỗ trợ rộng rãi hơn sẽ được chọn khi không có Dolby Digital. Bạn có thể tìm thấy danh sách bộ giải mã tích hợp sẵn của Android dùng để chuyển đổi luồng âm thanh thành PCM trong phần Định dạng nội dung nghe nhìn được hỗ trợ.

Tại thời điểm phát, ứng dụng truyền trực tuyến phải tạo một AudioTrack với AudioFormat tốt nhất mà thiết bị âm thanh đầu ra hỗ trợ.

Tạo một bản nhạc có định dạng phù hợp

Ứng dụng phải tạo một AudioTrack, bắt đầu phát nội dung đó rồi gọi getRoutedDevice() để xác định thiết bị âm thanh mặc định dùng để phát âm thanh. Ví dụ: đây có thể là một bản nhạc PCM có khoảng im lặng ngắn an toàn được mã hoá chỉ dùng để xác định thiết bị được định tuyến và khả năng âm thanh của thiết bị đó.

Nhận các chế độ mã hoá được hỗ trợ

Sử dụng getAudioProfiles() (API cấp 31 trở lên) hoặc getEncodings() (API cấp 23 trở lên) để xác định các định dạng âm thanh có trên thiết bị âm thanh mặc định.

Kiểm tra các cấu hình và định dạng âm thanh được hỗ trợ

Sử dụng AudioProfile (API cấp 31 trở lên) hoặc isDirectPlaybackSupported() (API cấp 29 trở lên) để kiểm tra các tổ hợp được hỗ trợ giữa định dạng, số lượng kênh và tốc độ lấy mẫu.

Một số thiết bị Android có thể hỗ trợ phương thức mã hoá ngoài những phương thức mã hoá mà thiết bị âm thanh đầu ra hỗ trợ. Bạn nên phát hiện các định dạng bổ sung này thông qua isDirectPlaybackSupported(). Trong những trường hợp này, dữ liệu âm thanh sẽ được mã hoá lại thành định dạng mà thiết bị âm thanh đầu ra hỗ trợ. Sử dụng isDirectPlaybackSupported() để kiểm tra khả năng hỗ trợ cho định dạng mong muốn một cách chính xác, ngay cả khi định dạng đó không có trong danh sách do getEncodings() trả về.

Tuyến âm thanh dự kiến

Android 13 (API cấp 33) đã ra mắt các tuyến âm thanh dự kiến. Bạn có thể dự đoán khả năng hỗ trợ thuộc tính âm thanh của thiết bị và chuẩn bị các bản nhạc cho thiết bị âm thanh đang hoạt động. Bạn có thể dùng getDirectPlaybackSupport() để kiểm tra xem tính năng phát trực tiếp trên thiết bị âm thanh đang được định tuyến có được hỗ trợ đối với một định dạng và thuộc tính nhất định hay không:

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
}

Ngoài ra, bạn có thể truy vấn những cấu hình được hỗ trợ để phát nội dung nghe nhìn trực tiếp thông qua thiết bị âm thanh hiện được định tuyến. Quy tắc này không bao gồm mọi hồ sơ không được hỗ trợ hoặc sẽ được chuyển mã bằng khung 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();
}

Trong ví dụ này, preferredFormats là danh sách các thực thể AudioFormat. Thẻ này được sắp xếp theo thứ tự ưu tiên nhất xếp trước trong danh sách và theo thứ tự ít được ưu tiên nhất xếp trước. getDirectProfilesForAttributes() trả về danh sách các đối tượng AudioProfile được hỗ trợ cho thiết bị âm thanh hiện được định tuyến với AudioAttributes được cung cấp. Danh sách các mục AudioFormat ưu tiên sẽ được lặp lại cho đến khi tìm thấy AudioProfile được hỗ trợ phù hợp. AudioProfile này được lưu trữ dưới dạng bestAudioProfile. Tốc độ lấy mẫu tối ưu và mặt nạ kênh được xác định từ bestAudioProfile. Cuối cùng, một thực thể AudioFormat thích hợp sẽ được tạo.

Tạo bản âm thanh

Ứng dụng nên sử dụng thông tin này để tạo AudioTrack cho AudioFormat chất lượng cao nhất được thiết bị âm thanh mặc định hỗ trợ (và có sẵn cho nội dung đã chọn).

Chặn thay đổi thiết bị âm thanh

Để chặn và phản ứng với các thay đổi đối với thiết bị âm thanh, ứng dụng nên:

  • Đối với các cấp độ API bằng hoặc lớn hơn 24, hãy thêm OnRoutingChangedListener để theo dõi các thay đổi đối với thiết bị âm thanh (HDMI, Bluetooth, v.v.).
  • Đối với API cấp 23, hãy đăng ký AudioDeviceCallback để nhận thay đổi trong danh sách thiết bị âm thanh hiện có.
  • Đối với API cấp 21 và 22, hãy theo dõi các sự kiện của đầu cắm HDMI và sử dụng dữ liệu bổ sung từ các thông báo truyền tin.
  • Ngoài ra, hãy đăng ký BroadcastReceiver để theo dõi các thay đổi về trạng thái của BluetoothDevice cho các thiết bị thấp hơn API 23, vì AudioDeviceCallback chưa được hỗ trợ.

Khi phát hiện AudioTrack có sự thay đổi về thiết bị âm thanh, ứng dụng sẽ kiểm tra các tính năng âm thanh đã cập nhật và nếu cần, hãy tạo lại AudioTrack bằng một AudioFormat khác. Hãy làm việc này nếu phương thức mã hoá chất lượng cao hơn hiện được hỗ trợ hoặc phương thức mã hoá sử dụng trước đó không còn được hỗ trợ nữa.

Mã mẫu

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);