音频功能

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

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

应用能够以多种编码输出相同的内容,以便根据音频设备功能为用户提供最佳音频体验。例如,如果电视支持杜比数字编码的音频串流,则会播放该音频串流;如果电视不支持杜比数字,则会选择更广泛受支持的 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();
}

在此示例中,preferredFormatsAudioFormat 实例的列表。该列表按首选顺序排列,最首选的列在前面,最不首选的列在后面。getDirectProfilesForAttributes() 会使用所提供的 AudioAttributes 返回当前路由的音频设备的受支持 AudioProfile 对象列表。系统会迭代首选 AudioFormat 项列表,直到找到匹配的受支持 AudioProfile。此 AudioProfile 会存储为 bestAudioProfile。最佳采样率和通道掩码由 bestAudioProfile 确定。最后,系统会创建适当的 AudioFormat 实例。

创建音轨

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

拦截音频设备更改

如需拦截音频设备更改并对其做出响应,应用应:

  • 对于 API 级别等于或高于 24 的设备,请添加 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);