前言
关于car_audio_configuration.xml的解析这部分在Android R上还是有一点变化的。具体我们一步一步来分析下其解析原理和过程。
car_audio_configuration.xml
首先关于这个xml配置文件的位置,aosp的源码位置为/device/generic/car/emulator/audio/car_emulator_audio.mk,当然也可具体根据自己的情况来定,反正最后放到车机的路径是固定的,具体如下
device/generic/car/emulator/audio/car_audio_configuration.xml:$(TARGET_COPY_OUT_VENDOR)/etc/car_audio_configuration.xml
然后看下这个配置文件里面的内容
<!--
Defines the audio configuration in a car, including
- Audio zones
- Context to audio bus mappings
- Volume groups
in the car environment.
-->
<carAudioConfiguration version="2">
<zones>
<zone name="primary zone" isPrimary="true" occupantZoneId="0">
<volumeGroups>
<group>
<device address="bus0_media_out">
<context context="music"/>
</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"/>
<context context="announcement"/>
</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 1" audioZoneId="1">
<volumeGroups>
<group>
<device address="bus100_audio_zone_1">
<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>
</zone>
<zone name="rear seat zone 2" audioZoneId="2">
<volumeGroups>
<group>
<device address="bus200_audio_zone_2">
<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>
</zone>
</zones>
</carAudioConfiguration>
从xml的注释就看出来来了,这个配置文件主要有
- Audio zones
- Context to audio bus mappings
- Volume groups
这三个功能,首先说下Audio Zones,通过xml可以看到有primary zone和"rear seat zone 1和"rear seat zone 2一共三个Zone。首先看下Zone的加载时序
在CaeAudioService启动的时候,会调用 setupDynamicRoutingLocked进行动态路由的注册,注册前首先会解析这个xml,即调用 loadCarAudioZonesLocked,我们看下源码
private void loadCarAudioZonesLocked() {
//主要获取Device是AUDIO_DEVICE_OUT_BUS的device,然后再过滤掉输入的Device,剩下就是用于输出的Device,
//最后将Device转化成CarAudioDeviceInfo
List<CarAudioDeviceInfo> carAudioDeviceInfos = generateCarAudioDeviceInfos();
mCarAudioConfigurationPath = getAudioConfigurationPath();
if (mCarAudioConfigurationPath != null) {
// 解析的xml主要存在vendor/etc下,如果vendor下不存在,则会去system/etc下去寻找。
mCarAudioZones = loadCarAudioConfigurationLocked(carAudioDeviceInfos);
} else {
mCarAudioZones = loadVolumeGroupConfigurationWithAudioControlLocked(
carAudioDeviceInfos);
}
CarAudioZonesValidator.validate(mCarAudioZones);
}
具体关于CarAudioDeviceInfo都有哪些,这里不细说了,可以参照之前的分析。其实这些Devices的信息都是来自audio_policy_configuration.xml中的devicePort下type为AUDIO_DEVICE_OUT_BUS的Device,这个Device的type全部为AUDIO_DEVICE_OUT_BUS,而且address都是以bus+数字开头的。
接下来具体看下CarAudioZone是如何解析的,继续看loadCarAudioConfigurationLocked()
private SparseArray<CarAudioZone> loadCarAudioConfigurationLocked(
List<CarAudioDeviceInfo> carAudioDeviceInfos) {
// 获取输入的device设备信息
AudioDeviceInfo[] inputDevices = getAllInputDevices();
try (InputStream inputStream = new FileInputStream(mCarAudioConfigurationPath)) {
//CarAudioZonesHelper 来具体解析car_audio_configuration.xml
CarAudioZonesHelper zonesHelper = new CarAudioZonesHelper(mCarAudioSettings,
inputStream, carAudioDeviceInfos, inputDevices);
//一个zoneID和occupantZoneId对应的集合
mAudioZoneIdToOccupantZoneIdMapping =
zonesHelper.getCarAudioZoneIdToOccupantZoneIdMapping();
// 加载xml
return zonesHelper.loadAudioZones();
} catch (IOException | XmlPullParserException e) {
throw new RuntimeException("Failed to parse audio zone configuration", e);
}
}
这里说下inputDevices,之前的carAudioDeviceInfos找到主要是输出设备即device的role = sink,而inputDevices 则找到主要是输入设备即device的role = source,对应的type = AUDIO_DEVICE_IN_BUS。
拿到输出和输入设备以及xml的path,把这些给到CarAudioZonesHelper来做解析和初始化。
略过CarAudioZonesHelper的构造函数,直接进入loadAudioZones()。
SparseArray<CarAudioZone> loadAudioZones() throws IOException, XmlPullParserException {
//mInputStream即CarAudioService中传入的用于解析xml的FileInputStream
return parseCarAudioZones(mInputStream);
}
继续看parseCarAudioZones(),即开始真正的解析。
private SparseArray<CarAudioZone> parseCarAudioZones(InputStream stream)
throws XmlPullParserException, IOException {
........
/* 略掉无用的部分 直奔主题*/
// Get all zones configured under <zones> tag
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
//开始真正的解析,首先找到 <zones>标签
if (TAG_AUDIO_ZONES.equals(parser.getName())) {
// parser即XmlPullParser
return parseAudioZones(parser);
} else {
skip(parser);
}
}
throw new RuntimeException(TAG_AUDIO_ZONES + " is missing from configuration");
}
通过xml可以看到只有一组 zones标签,即while只执行一次parseAudioZones
private SparseArray<CarAudioZone> parseAudioZones(XmlPullParser parser)
throws XmlPullParserException, IOException {
// 创建carAudioZones,最终返给CarAudioService的mCarAudioZones
SparseArray<CarAudioZone> carAudioZones = new SparseArray<>();
//固定的解析模板,开始while循环
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
// 开始解析 zone标签
if (TAG_AUDIO_ZONE.equals(parser.getName())) {
// 一层一层向内部解析,继续解析 zone标签内部的元素
CarAudioZone zone = parseAudioZone(parser);
//check 检查是已经存在有primary zone,如果存在而且多个便会抛出异常,
// 因为有primary zone必须要有且只能有一个
verifyOnlyOnePrimaryZone(zone, carAudioZones);
carAudioZones.put(zone.getId(), zone);
} else {
skip(parser);
}
}
//check 检查是否有primary zone,因为一定要有primary zone 否则会抛异常
verifyPrimaryZonePresent(carAudioZones);
//返回carAudioZones,最终给到CarAudioService
return carAudioZones;
}
这里开始解析zone标签,通过xml可以看到这里有三组zone分别是primary zone 、rear seat zone 1和rear seat zone 2,
要调用三次parseAudioZone(parser)即创建三个CarAudioZone。继续看parseAudioZone()
private CarAudioZone parseAudioZone(XmlPullParser parser)
throws XmlPullParserException, IOException {
// 解析isPrimary属性,这里primary zone 是ture
final boolean isPrimary = Boolean.parseBoolean(
parser.getAttributeValue(NAMESPACE, ATTR_IS_PRIMARY));
// 解析xml得到 zoneName
final String zoneName = parser.getAttributeValue(NAMESPACE, ATTR_ZONE_NAME);
// 解析xml得到 zoneId
final int audioZoneId = getZoneId(isPrimary, parser);
// check Occupant zone Id
parseOccupantZoneId(audioZoneId, parser);
// 构建 CarAudioZone
final CarAudioZone zone = new CarAudioZone(audioZoneId, zoneName);
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
// Expect one <volumeGroups> in one audio zone
if (TAG_VOLUME_GROUPS.equals(parser.getName())) {
// 解析 volume group
parseVolumeGroups(parser, zone);
} else if (TAG_INPUT_DEVICES.equals(parser.getName())) {
//解析 inputdevices
parseInputAudioDevices(parser, zone);
} else {
skip(parser);
}
}
return zone;
}
zoneName 的获取直接通过解析对应的属性就拿到了,简单说下audioZoneId,我们看到了不是直接去解析xml而是调用了getZoneId(isPrimary, parser)函数,其实这个函数对primary zone做了特殊处理。如果我们传入的isPrimary是true,那么它的audioZoneId就是0即PRIMARY_AUDIO_ZONE。因此这个xml里对于primary zone也就未定义audioZoneId。这里还对primary zone的 audioZoneId做了强制要求就是如果我们定义了audioZoneId,它的值必须必须是0,而且其他zoneId不可以定义0.
再说下parseOccupantZoneId(audioZoneId, parser),这个Android R新增的。这里是解析occupantZoneId然后把它和audioZoneId一起放到map中,即 mZoneIdToOccupantZoneIdMapping.put(audioZoneId, occupantZoneId),occupantZoneId不能重复,但还没看到是否可以定义多个,以及occupantZoneId是否也是必须是从0开始。但是一般只定义一个,如 occupantZoneId="0"这样。
最后关于这个函数再看下CarAudioZone的定义
CarAudioZone(int id, String name) {
mId = id;// audioZoneId
mName = name;// zoneName
mVolumeGroups = new ArrayList<>(); // VolumeGroup list
mInputAudioDevice = new ArrayList<>();// InputAudioDevice list
}
好的继续看VolumeGroups的解析,这个算是重点了。
private void parseVolumeGroups(XmlPullParser parser, CarAudioZone zone)
throws XmlPullParserException, IOException {
int groupId = 0;
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
if (TAG_VOLUME_GROUP.equals(parser.getName())) {
//继续解析 group 标签
zone.addVolumeGroup(parseVolumeGroup(parser, zone.getId(), groupId));
// groupId 从0开始 累加
groupId++;
} else {
skip(parser);
}
}
这里分析下primary zone的group解析,其他zone相同,primary zone里共有四组group,我们先看第一组group的解析parseVolumeGroup(parser, zone.getId(), groupId)
private CarVolumeGroup parseVolumeGroup(XmlPullParser parser, int zoneId, int groupId)
throws XmlPullParserException, IOException {
// 创建volumeGroup 对象
CarVolumeGroup group = new CarVolumeGroup(mCarAudioSettings, zoneId, groupId);
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
// 解析device标签
if (TAG_AUDIO_DEVICE.equals(parser.getName())) {
// 获取device标签下的 address属性
String address = parser.getAttributeValue(NAMESPACE, ATTR_DEVICE_ADDRESS);
// check address有效性
validateOutputDeviceExist(address);
// 主要处理声音分组
parseVolumeGroupContexts(parser, group, address);
} else {
skip(parser);
}
}
return group;
}
这里开始解析volumeGroup了,这块先看下时序图
时序画的简单些了,具体看下每个过程,主要这个函数parseVolumeGroupContexts(),我们知道car_audio_configuration.xml这个xml做了三件事,其中这个就是Volume groups。我们再具体看下这个函数:
private void parseVolumeGroupContexts(
XmlPullParser parser, CarVolumeGroup group, String address)
throws XmlPullParserException, IOException {
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
if (TAG_CONTEXT.equals(parser.getName())) {
// 解析context标签
@AudioContext int carAudioContext = parseCarAudioContext(
parser.getAttributeValue(NAMESPACE, ATTR_CONTEXT_NAME));
// check carAudioContext 的正确性
validateCarAudioContextSupport(carAudioContext);
// 根据address得到CarAudioDeviceInfo
CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(address);
// 将caraudiodeviceinfo 和carAudioContext bind到一起
group.bind(carAudioContext, info);
// If V1, default new contexts to same device as DEFAULT_AUDIO_USAGE
// 这里走不到 用的都是version 2
if (isVersionOne() && carAudioContext == CarAudioService.DEFAULT_AUDIO_CONTEXT) {
bindNonLegacyContexts(group, info);
}
}
// Always skip to upper level since we're at the lowest.
skip(parser);
}
}
首先说下carAudioContext是怎么取到的,因为xml给的是一个string,这里是int,肯定是在哪里做了映射,映射的地方如下
static {
CONTEXT_NAME_MAP = new HashMap<>(CarAudioContext.CONTEXTS.length);
CONTEXT_NAME_MAP.put("music", CarAudioContext.MUSIC);
CONTEXT_NAME_MAP.put("navigation", CarAudioContext.NAVIGATION);
CONTEXT_NAME_MAP.put("voice_command", CarAudioContext.VOICE_COMMAND);
CONTEXT_NAME_MAP.put("call_ring", CarAudioContext.CALL_RING);
CONTEXT_NAME_MAP.put("call", CarAudioContext.CALL);
CONTEXT_NAME_MAP.put("alarm", CarAudioContext.ALARM);
CONTEXT_NAME_MAP.put("notification", CarAudioContext.NOTIFICATION);
CONTEXT_NAME_MAP.put("system_sound", CarAudioContext.SYSTEM_SOUND);
CONTEXT_NAME_MAP.put("emergency", CarAudioContext.EMERGENCY);
CONTEXT_NAME_MAP.put("safety", CarAudioContext.SAFETY);
CONTEXT_NAME_MAP.put("vehicle_status", CarAudioContext.VEHICLE_STATUS);
CONTEXT_NAME_MAP.put("announcement", CarAudioContext.ANNOUNCEMENT);
SUPPORTED_VERSIONS = new SparseIntArray(2);
SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_1, SUPPORTED_VERSION_1);
SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_2, SUPPORTED_VERSION_2);
}
这样我们就拿到了carAudioContext,完事CarAudioDeviceInfo的获取就不说了(上面说过了)拿到info和contex后一起传给了CarVolumeGroup的bind
void bind(int carAudioContext, CarAudioDeviceInfo info) {
Preconditions.checkArgument(mContextToAddress.get(carAudioContext) == null,
String.format("Context %s has already been bound to %s",
CarAudioContext.toString(carAudioContext),
mContextToAddress.get(carAudioContext)));
synchronized (mLock) {
// 第一次mAddressToCarAudioDeviceInfo的size是0的,
// 存在一个device标签下有多个context的情况,要bind 多次
// 只有第一次拿info的step
if (mAddressToCarAudioDeviceInfo.size() == 0) {
// 这个值最早来自audio_policy_configuration.xml下的stepValueMB
mStepSize = info.getStepValue();
} else {
// 如果不是第一次调用group的bind ,需要check每个device的StepValue是否相同,
// 即同一个group下不同device的step必须是一样的,因为同一个group的音量是一起调节的
Preconditions.checkArgument(
info.getStepValue() == mStepSize,
"Gain controls within one group must have same step value");
}
// 存入mAddressToCarAudioDeviceInfo
mAddressToCarAudioDeviceInfo.put(info.getAddress(), info);
// 存入mContextToAddress
mContextToAddress.put(carAudioContext, info.getAddress());
// 更新DefaultGain,同一个group里deviceinfo里最大的DefaultGain
if (info.getDefaultGain() > mDefaultGain) {
// We're arbitrarily selecting the highest
// device default gain as the group's default.
mDefaultGain = info.getDefaultGain();
}
// 更新mMaxGain,同一个group里deviceinfo里最大的MaxGain
if (info.getMaxGain() > mMaxGain) {
mMaxGain = info.getMaxGain();
}
// // 更新mMinGain ,同一个group里deviceinfo里最小的MinGain
if (info.getMinGain() < mMinGain) {
mMinGain = info.getMinGain();
}
// 更新当前音量,有存储的音量,用存储的音量,没有存储的音量用(default - min)/setp
updateCurrentGainIndexLocked();
}
}
bind是一个很重要的函数,主要是给音量分组,更新当前组音量的最大/小值,默认值以及当前音量值。并且维护了两个很重要的的map是mAddressToCarAudioDeviceInfo(address为key,CarAudioDeviceInfo 为value)和mContextToAddress(context是key,address是value),其实有了这两个map我们就可以根据context找到address,根据address找到CarAudioDeviceInfo 。把context和CarAudioDeviceInfo 路由在了一起。
我们继续再回头看下 CarVolumeGroup里还有什么, mZoneId 和mId(groupId),这样一个CarVolumeGroup对应的zoneId以及自己的Id和内部维护的context以及CarAudioDeviceInfo 就全部关联上了。
总结
其实到这里整个xml基本就要解析完成了,回顾下这三步:
- Audio zones
有三个zone,一个primary zone也是occupant Zone - Context to audio bus mappings
在解析VolumeGroup时将context和CarAudioDeviceInfo mapping到一起了,具体就是bind中做的 - Volume groups
每个zone都至少有个volumeGroups,同一个volumeGroups里有几个group就有几组音量。同一组的音量相同。
这个car_audio_configuration.xml看似很简单,但是如果我们在项目中需要定制,需要修改某一部分一定要多注意,因为每个标签的属性都是有一定的关联和限定的。
下一次继续分析解析car_audio_configuration.xml后做的动态路由,以及多音区的焦点管理。