音频功能

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() 检测。在这些情况下,音频数据会重新编码为输出音频设备支持的格式。使用 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() 会返回当前路由的音频设备(具有提供的 AudioAttributes)支持的 AudioProfile 对象列表。系统会遍历首选 AudioFormat 项列表,直到找到匹配的支持 AudioProfile。此 AudioProfile 存储为 bestAudioProfile。最佳采样率和声道掩码由 bestAudioProfile 确定。最后,系统会创建适当的 AudioFormat 实例。

创建音轨

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

拦截音频设备更改

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

  • 对于等于或大于 24 的 API 级别,请添加 OnRoutingChangedListener 以监控音频设备更改(HDMI、蓝牙等)。
  • 对于 API 级别 23,请注册 AudioDeviceCallback,以便在可用音频设备列表发生更改时收到通知。
  • 对于 API 级别 21 和 22,请监控 HDMI 插拔事件,并使用广播中的额外数据。
  • 此外,还要注册一个 BroadcastReceiver,以监控 API 级别低于 23 的设备的 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);