音频功能

Android TV 设备可以同时连接多个音频输出:电视扬声器、通过 HDMI 连接的家庭影院、蓝牙耳机等。这些音频输出设备可能支持不同的音频功能,例如编码(Dolby Digital+、DTS 和 PCM)、采样率和声道。例如,通过 HDMI 连接的电视支持多种编码,而蓝牙耳机通常仅支持 PCM。

可用音频设备列表以及当前路由的音频设备可能会因热插拔 HDMI 设备、连接或断开蓝牙耳机,或用户更改音频设置而发生变化。由于即使在应用播放媒体时音频输出功能也可能发生变化,应用需要妥善适应这些变化,并在新路由的音频设备及其支持的功能上继续播放。输出错误的音频格式可能会导致错误或无声。

应用有能力以多种编码输出相同内容,以便根据音频设备的功能为用户提供最佳的音频体验。例如,如果电视支持,则播放 Dolby Digital 编码的音频流;如果不支持 Dolby Digital,则选择更广泛支持的 PCM 音频流。用于将音频流转换为 PCM 的内置 Android 解码器列表可以在“支持的媒体格式”中找到。

在播放时,流媒体应用应使用输出音频设备支持的最佳 AudioFormat 创建 AudioTrack

创建具有正确格式的轨道

应用应创建一个 AudioTrack,开始播放,并调用 getRoutedDevice() 来确定播放声音的默认音频设备。例如,这可以是一个安全的短静音 PCM 编码轨道,仅用于确定路由设备及其音频功能。

获取支持的编码

使用 getAudioProfiles()(API 31 级及以上)或 getEncodings()(API 23 级及以上)来确定默认音频设备上可用的音频格式。

检查支持的音频配置文件和格式

使用 AudioProfile(API 31 级及以上)或 isDirectPlaybackSupported()(API 29 级及以上)来检查格式、声道数和采样率的支持组合。

一些 Android 设备能够支持超出输出音频设备所支持范围的编码。应通过 isDirectPlaybackSupported() 检测这些额外的格式。在这种情况下,音频数据会被重新编码为输出音频设备支持的格式。即使某个格式未出现在 getEncodings() 返回的列表中,也请使用 isDirectPlaybackSupported() 来正确检查对所需格式的支持。

预期音频路由

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() 会针对当前路由的音频设备以及提供的 AudioAttributes 返回一个受支持的 AudioProfile 对象列表。程序将遍历偏好的 AudioFormat 项目列表,直到找到匹配的受支持 AudioProfile。该 AudioProfile 被存储为 bestAudioProfile。最佳采样率和声道掩码将根据 bestAudioProfile 确定。最后,创建一个适当的 AudioFormat 实例。

创建音频轨道

应用应使用此信息为默认音频设备支持(且所选内容可用)的最高质量 AudioFormat 创建 AudioTrack

拦截音频设备更改

为了拦截并响应音频设备更改,应用应:

  • 对于 API 24 级或更高级别,添加一个 OnRoutingChangedListener 以监控音频设备更改(HDMI、蓝牙等)。
  • 对于 API 23 级,注册一个 AudioDeviceCallback 以接收可用音频设备列表中的更改。
  • 对于 API 21 和 22 级,监控 HDMI 插件事件并使用来自广播的额外数据。
  • 此外,对于低于 API 23 的设备,注册一个 BroadcastReceiver 来监控 BluetoothDevice 的状态变化,因为此时尚不支持 AudioDeviceCallback

当检测到 AudioTrack 的音频设备发生更改时,应用应检查更新后的音频功能,并在必要时使用不同的 AudioFormat 重新创建 AudioTrack。如果当前支持更高质量的编码,或者之前使用的编码不再受支持,请执行此操作。

示例代码

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