7、车载音频系统:Android Automotive音频架构

车载音频,说实话,是IVI系统里最容易被低估的模块。很多人觉得不就是放个音乐、接个电话吗?等你真正上车调音、处理多路音频并发、搞定蓝牙和USB切换的时候,就知道这里面的坑有多深了。

我最早接触车载音频是在一个量产项目上,当时客户反馈导航提示音和音乐混在一起,听不清。我查了三天日志,最后发现是音频焦点管理没配好。嗯,从那以后,我对AudioPolicy和CarAudioService就格外上心。

这一章,我们就来拆解Android Automotive的音频架构。我会从底层AudioFlinger讲起,再到上层的CarAudioService,最后聊聊多区域音频和路由策略。你跟着走一遍,基本就能应付大部分车载音频开发场景了。

7.1 Android音频系统概览:AudioPolicy与AudioFlinger

Android的音频系统,核心就两个服务:AudioFlingerAudioPolicyService。前者管音频数据的混音和输出,后者管音频策略——说白了就是「谁的声音该从哪个喇叭出来」。

我习惯把AudioFlinger比作一个调音台。各路音频流(音乐、导航、电话)进来,它负责混音,然后送到硬件设备(扬声器、耳机、蓝牙)。AudioPolicyService则是调音师,它决定哪路音频该被压低、哪路该被放大。

核心要点:

  • AudioFlinger:负责音频数据的混音、重采样、格式转换,最终写入音频硬件抽象层(HAL)。
  • AudioPolicyService:负责音频路由策略、设备选择、音量管理、焦点管理。

在车载场景下,AudioPolicyService的配置尤为重要。因为车上有多个音频输出设备——前门喇叭、后门喇叭、头枕喇叭、蓝牙耳机、USB音频设备。你想想看,如果导航音从后门喇叭出,音乐从前门喇叭出,那体验得多奇怪。

AudioPolicy的配置主要在 audio_policy_configuration.xml 里。这个文件定义了所有音频设备、端口、以及它们之间的连接关系。我贴一个简化版的车载配置示例:

<!-- audio_policy_configuration.xml 片段 -->
<audioPolicyConfiguration version="1.0">
    <globalConfiguration speaker_drc_enabled="false"/>
    <modules>
        <module name="primary" halVersion="3.0">
            <attachedDevices>
                <item>Speaker</item>
                <item>BuiltInMic</item>
            </attachedDevices>
            <devicePorts>
                <devicePort tagName="Speaker" type="AUDIO_DEVICE_OUT_SPEAKER" role="source">
                    <profile name="" format="AUDIO_FORMAT_PCM_16_BIT"
                             samplingRates="48000" channelMasks="AUDIO_CHANNEL_OUT_STEREO"/>
                </devicePort>
                <devicePort tagName="WiredHeadset" type="AUDIO_DEVICE_OUT_WIRED_HEADSET" role="source">
                    <profile name="" format="AUDIO_FORMAT_PCM_16_BIT"
                             samplingRates="48000" channelMasks="AUDIO_CHANNEL_OUT_STEREO"/>
                </devicePort>
            </devicePorts>
            <routes>
                <route type="mix" sink="Speaker" sources="mixport_0,mixport_1"/>
                <route type="mix" sink="WiredHeadset" sources="mixport_0"/>
            </routes>
        </module>
    </modules>
</audioPolicyConfiguration>

这里要注意,车载系统通常会有多个音频模块(primary、usb、bluetooth等)。每个模块管理一组设备。路由规则定义了哪些混音端口(mixport)可以输出到哪些物理设备。

我的经验:调试音频路由问题时,先检查 dumpsys audio_policy 的输出。它能告诉你当前设备连接状态、路由策略、以及每个音频流的输出设备。我每次遇到「没声音」的问题,第一件事就是跑这个命令。

7.2 CarAudioService与音频焦点管理

CarAudioService是Android Automotive在音频方面的核心扩展。它不像手机那样简单——手机只有一个人在用,音频焦点冲突少。车上呢?驾驶员要导航,副驾要听歌,后排小孩要看动画片。音频焦点管理就变得复杂了。

CarAudioService主要做了几件事:

  • 管理车载特有的音频上下文(AudioContext),比如导航、语音命令、电话、媒体、安全警告等。
  • 实现多用户音频焦点管理(不同座位可能有不同用户)。
  • 提供音量分组和独立调节能力。
  • 与车辆硬件交互,控制音频路由。

音频焦点,说白了就是「谁有资格出声」。在车载场景下,焦点的优先级是硬编码的。比如安全警告的优先级最高,导航次之,媒体最低。我见过一个案例,第三方音乐App在导航播报时强行获取焦点,导致导航音被完全压制。后来我们在CarAudioService里加了焦点策略校验,才解决这个问题。

焦点管理的核心接口是 AudioManager.requestAudioFocus()。但在车载上,我们通常用 CarAudioManager 提供的扩展方法:

// 获取CarAudioManager实例
CarAudioManager carAudioManager = Car.createCar(context).getCarManager(CarAudioManager.class);

// 请求音频焦点,指定音频上下文为导航
AudioAttributes audioAttributes = new AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
        .build();

AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
        .setAudioAttributes(audioAttributes)
        .setOnAudioFocusChangeListener(focusChange -> {
            if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
                // 失去焦点,暂停导航播报
                pauseNavigation();
            } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
                // 重新获得焦点,恢复播报
                resumeNavigation();
            }
        })
        .build();

int result = carAudioManager.requestAudioFocus(focusRequest);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // 焦点获取成功
    startNavigationGuidance();
}

这里有个细节:车载的音频焦点是分区域的。驾驶员区域的导航焦点和副驾区域的媒体焦点可以共存。CarAudioService内部维护了一个焦点矩阵,按区域和上下文来管理。

注意:不要滥用 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK。在车载上,duck(降低音量)策略容易引起用户困惑。我建议导航和电话场景用 AUDIOFOCUS_GAIN_TRANSIENT,直接暂停其他音频,体验更清晰。

7.3 多区域音频(Zone Audio)配置

多区域音频,是车载系统区别于手机的最显著特征。一辆车可能有2到4个独立音频区域。每个区域可以播放不同的音频源,互不干扰。

Android Automotive通过 CarAudioZone 来抽象这个概念。每个Zone对应一个物理区域(比如前排、后排),有自己的音频设备列表、音量控制、焦点管理。

配置多区域音频,需要在 car_audio_configuration.xml 中定义:

<!-- car_audio_configuration.xml 片段 -->
<carAudioConfiguration>
    <zones>
        <zone name="前排区域" volumeGroup="0">
            <deviceAddresses>
                <address>bus_device_0_front_speakers</address>
                <address>bus_device_1_front_headphones</address>
            </deviceAddresses>
            <audioContexts>
                <context name="MEDIA" usage="AUDIO_USAGE_MEDIA"/>
                <context name="NAVIGATION" usage="AUDIO_USAGE_NAVIGATION"/>
            </audioContexts>
        </zone>
        <zone name="后排区域" volumeGroup="1">
            <deviceAddresses>
                <address>bus_device_2_rear_speakers</address>
                <address>bus_device_3_rear_headphones</address>
            </deviceAddresses>
            <audioContexts>
                <context name="MEDIA" usage="AUDIO_USAGE_MEDIA"/>
                <context name="ENTERTAINMENT" usage="AUDIO_USAGE_GAME"/>
            </audioContexts>
        </zone>
    </zones>
</carAudioConfiguration>

每个Zone可以绑定不同的音频上下文。比如前排支持媒体和导航,后排支持媒体和游戏。这样,前排导航播报时,后排的娱乐音频不受影响。

我在一个项目中遇到过这样的需求:驾驶员想听导航,副驾想听音乐,后排小孩要看动画片。三个区域互不干扰。实现方案就是给每个区域分配独立的AudioFlinger混音线程,通过硬件总线隔离。嗯,这个方案对硬件要求比较高,需要SoC支持多路I2S或TDM输出。

下面这张图展示了多区域音频的核心架构:

车载多区域音频架构图 应用层 导航App | 音乐App | 视频App | 语音助手App CarAudioService 音频焦点管理 | 音量分组 | 区域路由决策 Zone 1:前排区域 媒体 + 导航 Zone 2:副驾区域 媒体 + 视频 Zone 3:后排区域 娱乐 + 游戏 AudioFlinger(多线程混音) 前门扬声器 副驾耳机 后排扬声器

从图中可以看到,CarAudioService根据区域配置,将不同应用的音频流路由到对应的AudioFlinger混音线程,最终输出到不同的物理设备。每个区域的音频是独立处理的,互不干扰。

7.4 车载音频路由策略:蓝牙、USB、AUX

音频路由策略,说白了就是「声音走哪条路」。车载上常见的音频源有蓝牙(A2DP/HFP)、USB(Android Auto/CarPlay)、AUX(有线输入)。每种源的路由策略都不一样。

我整理了一个表格,方便你对比:

音频源 典型用途 路由策略 注意事项
蓝牙 A2DP 手机音乐播放 默认路由到媒体Zone 注意编解码器兼容性(SBC/AAC/aptX)
蓝牙 HFP 电话通话 路由到驾驶员Zone,优先级最高 需要处理语音上行和下行路径
USB Audio Android Auto / CarPlay 路由到指定Zone,通常独占 需要处理USB热插拔和权限
AUX IN 外部音频设备 路由到所有Zone或指定Zone 模拟信号,注意噪声和增益

路由策略的实现,主要在 CarAudioServiceonAudioDeviceAdded()onAudioDeviceRemoved() 回调中。当检测到新设备接入,系统会根据设备类型和当前配置,自动切换路由。

举个例子,蓝牙连接时的路由流程:

// 伪代码:蓝牙A2DP设备连接时的路由处理
void onA2dpDeviceConnected(BluetoothDevice device) {
    // 1. 获取蓝牙设备的音频地址
    String address = device.getAddress();
    
    // 2. 检查当前是否有媒体正在播放
    boolean isMediaPlaying = audioManager.isMusicActive();
    
    // 3. 如果有媒体播放,将路由切换到蓝牙设备
    if (isMediaPlaying) {
        AudioDeviceInfo btDevice = findAudioDeviceByAddress(address);
        if (btDevice != null) {
            // 设置蓝牙设备为媒体输出
            audioManager.setPreferredDeviceForUsage(
                AudioAttributes.USAGE_MEDIA, btDevice);
        }
    }
    
    // 4. 通知CarAudioService更新路由策略
    carAudioService.updateAudioRouting();
}

这里有个坑:蓝牙设备断开时,系统需要自动切回扬声器。如果切得不及时,用户会听到「声音突然没了」。我建议在 onA2dpDeviceDisconnected 回调中,提前把路由切回默认设备,而不是等系统自动处理。

避坑指南:我曾经在一个项目上遇到蓝牙通话时声音从所有喇叭出来,导致回声严重。后来发现是HFP路由策略没配好,把通话音频路由到了全车Zone。正确的做法是只路由到驾驶员Zone,并且启用回声消除(AEC)。

USB音频路由稍微复杂一些。Android Auto和CarPlay通过USB传输音频,系统需要识别USB设备类型,然后建立独立的音频会话。我建议在 car_audio_configuration.xml 中为USB设备预留专用的音频总线,避免和系统音频冲突。

AUX输入的路由相对简单,但要注意模拟信号的噪声问题。我见过一些车,AUX输入时有明显的底噪。解决方案是在硬件层面增加噪声门限,或者在软件层面做静音检测。

好了,这一章的内容就到这里。车载音频系统涉及的面很广,从底层AudioFlinger到上层CarAudioService,再到多区域配置和路由策略,每一步都有细节。你把这些搞懂了,车载音频开发的基本功就算扎实了。


公众号:蓝海资料掘金营,微信deep3321