音频路由

在 Android 10 中,car_audio_configuration.xml 取代了 car_volumes_groups.xmlIAudioControl.getBusForContext。新的配置文件定义了一个音频区列表。每个音频区都拥有一个或多个音量组及其关联设备,而每台设备都具有应在该音频区内进行路由的上下文。所有上下文都必须在每个音频区内表示。

配置音频路由

音频政策文件通常位于 vendor 分区中,表示主板的音频硬件配置。car_audio_configuration.xml 中引用的所有设备必须在 audio_policy_configuration.xml 中进行定义。

启用 AAOS 路由

如需使用基于 AAOS 的路由,您必须将 audioUseDynamicRouting 标记设为 true

<resources>
    <bool name="audioUseDynamicRouting">true</bool>
</resources>

如果设为 false,路由和大部分 CarAudioService 将被停用,并且操作系统将回退到 AudioService 的默认行为。

主音频区

默认情况下,所有音频都将路由到主音频区。只能有一个主音频区,该音频区在配置中通过属性 isPrimary="true" 进行指示。

示例配置

例如,车辆可能拥有两个音频区:主音频区和后座娱乐系统。对于该配置,可能的 car_audio_configuration.xml 的定义如下:

<audioZoneConfiguration version="2.0">
       <zone name="primary zone" isPrimary="true">
           <volumeGroups>
               <group>
                   <device address="bus0_media_out">
                       <context context="music"/>
                       <context context="announcement"/>
                   </device>
                   <device address="bus3_call_ring_out">
                       <context context="call_ring"/>
                   </device>
                   <device address="bus6_notification_out">
                       <context context="notification"/>
                   </device>
                   <device address="bus7_system_sound_out">
                       <context context="system_sound"/>
                       <context context="emergency"/>
                       <context context="safety"/>
                       <context context="vehicle_status"/>
                   </device>
               </group>
               <group>
                   <device address="bus1_navigation_out">
                       <context context="navigation"/>
                   </device>
                   <device address="bus2_voice_command_out">
                       <context context="voice_command"/>
                   </device>
               </group>
               <group>
                   <device address="bus4_call_out">
                       <context context="call"/>
                   </device>
               </group>
               <group>
                   <device address="bus5_alarm_out">
                       <context context="alarm"/>
                   </device>
               </group>
           </volumeGroups>
       </zone>
        <zone name="rear seat zone" audioZoneId="1">
           <volumeGroups>
               <group>
                   <device address="bus100_rear_seat">
                       <context context="music"/>
                       <context context="navigation"/>
                       <context context="voice_command"/>
                       <context context="call_ring"/>
                       <context context="call"/>
                       <context context="alarm"/>
                       <context context="notification"/>
                       <context context="system_sound"/>
                       <context context="emergency"/>
                       <context context="safety"/>
                       <context context="vehicle_status"/>
                       <context context="announcement"/>
                   </device>
               </group>
           </volumeGroups>
    </zones>
</audioZoneConfiguration>

在这里,主音频区将上下文分离给不同的设备。这样一来,HAL 便可使用车辆的硬件,在各个设备输出中应用不同的后处理效果和混音。设备已划分为四个音量组:媒体、导航、通话和闹钟。如果系统配置为 useFixedVolume,则每个组的音量级别都将传递到 HAL,以应用于这些设备的输出。

对于辅助音频区,应通过单台输出设备进行输出。在此示例中,所有使用请求都将路由到单台设备和音量组,以简化操作。

乘员区的音频配置

在 Android 11 中,car_audio_configuraton.xml 已经过进一步扩展,引入了 audioZoneIdoccupantZoneId 这两个新字段。首先,audioZoneId 可用于更好地控制音频区管理;另一方面,occupantZoneId 可用于配置基于用户 ID 的路由。

若要使用这些新字段,必须使用 V2 的 car_audio_configuration.xml。如果想重新访问上述音频配置,但利用该新字段进行乘员区 ID 和音频区 ID 之间的映射,则可按如下所示设置不含音频组定义的新配置:

<audioZoneConfiguration version="2.0">
       <zone name="primary zone" isPrimary="true" occupantZoneId="0">
         ...
       </zone>
       <zone name="rear seat zone" audioZoneId="1" occupantZoneId="1">
         ...
       </zone>
    </zones>
</audioZoneConfiguration>

上面的配置定义了主音频区到乘员区 0 以及 audioZoneId 1 到 occupantZoneId 1 的映射。一般来说,可以配置乘员区和音频区之间的任意映射,但这种映射必须是一对一的。以下是定义这两个新字段的规则:

  • 主音频区的 audioZoneId 始终为零
  • audioZoneIdoccupantZoneId 的编号不可重复
  • audioZoneIdoccupantZoneId 之间只能是一对一的映射

通过应用 UID 路由

在 Android 10 中,向 CarAudioManager 引入了一系列隐藏 API,使应用可以查询并设置音频区和焦点。

int[] getAudioZoneIds();
int getZoneIdForUid(int uid);
boolean setZoneIdForUid(int zoneId, int uid);
boolean clearZoneIdForUid(int uid);

您可以使用上面的 API 让第一方应用根据应用的 UID 管理音频路由。这样,您还需要同时提供音频区 ID 和应用的 UID。掌握这些信息后,您便可以使用 CarAudioManager#setZoneIdForUid API 来设置音频路由。

更改应用的音频区

默认情况下,所有音频将路由到主音频区。如需更新应用以路由到其他音频区,请使用 CarAudioManager#setZoneIdForUid

// Find zone to play
int zoneId = ...

// Find application's uid
Int uid = mContext.getPackageManager()
        .getApplicationInfo(mContext.getPackageName(), 0)
        .uid;

if (mCarAudioManager.setZoneIdForUid(zoneId, info.uid)) {
    Log.d(TAG, "Zone successfully updated");
} else {
    Log.d(TAG, "Failed to change zone");
}

注意:声音流无法动态地切换音频区。因此,必须停止播放并重新请求焦点,才能更改音频区。

使用用户 ID 进行路由

虽然基于应用 UID 的路由可以精细控制每个应用的音频路由,但它还需要在应用实际请求获得音频焦点和播放音频之前,为每个应用定义音频路由。为了消除此问题并进一步帮助第三方应用无需修改音频即可播放音频,CarAudioService 会使用汽车乘员区和音频区之间的映射来定义基于用户 ID 的路由。这样,当用户登录到乘员区时,车载音频服务就会收到通知。收到此信号后,系统会自动为所有音频区配置音频焦点管理和路由。

基于应用 UID 的路由仍然可以使用,但必须在基于用户 ID 的路由之外单独使用。也就是说,如果定义了乘员区到车载音频区的映射,则基于 UID 的路由会处于停用状态;尝试调用 CarAudioManager#setZoneidForUid 的话,系统就会抛出错误。

虽然通过乘员区管理简化了音频路由和焦点管理,但还是必须为乘员区分配用户。您可以使用 CarOccupantZoneManager#assignProfileUserToOccupantZone 实现这一点。此 API 需要管理用户的权限。目前,原始设备制造商 (OEM) 应通过某种系统界面管理乘员区用户分配。完成后,系统便会自动为该用户配置应用启动、音频路由以及焦点管理所有这三项功能。

使用 setPreferredDevice 进行路由

除了上述更改外,Android 11 还新增了下列 API 来查询与各个音频区关联的输出设备:CarAudioManager#getOutputDeviceForUsage(int zoneId, int usage)。

该 API 可用于查询用于特定音频区的输出设备以及音频属性用法。通过这种方式,第一方应用便可利用播放器的 setPreferredDevice API,将音频路由到不同的音频区。getOutputDeviceForUsage API 需使用 PERMISSION_CAR_CONTROL_AUDIO_SETTINGS,并且是系统 API。以下示例展示了如何查找用于特定音频区的媒体设备以及如何使用 setPreferredDevice API 路由到该设备。

audioZoneId = ... ;
mediaDeviceInfo = mCarAudioManager
            .getOutputDeviceForUsage(audioZoneId, AudioAttributes.USAGE_MEDIA);
…
mPlayer.setPreferredDevice(mediaDeviceInfo);