关于CarAudioZone也就是多音区的一个概念,主要是在AndroidQ上实现的。我们可以参照官方的文档Multi-Zone Overview,我的英语实在不敢恭维,这里就不翻译了,大家阅读自行翻译吧。我简单描述下多音区的概念,就是这么一种环境,后排乘客通过后排屏幕可以看电影,前排司机通过前排屏幕可以听音乐,大家互不影响。每一个屏都有自己专属的一个区域也就是zone的概念。这种前后屏的概念可能在的一些士的上体现的更好,我记得我曾经去过一个城市,乘客坐后排通过后座的屏幕可以了解这个城市的旅游景点,文化特色等,而前排司机就可以不受干扰的听他的交通广播了,扯远了哈,其实这种场景还有一种使用场景就是带主副屏的车机,比如现在好多汽车都是主副屏联动的,有了这个,我们都可以参照来实现了。
今天还是从源码的角度来分析carAudioZone这块,我们也是从CarAudioService中开始说吧,虽然之前分析了Android9.0CarAudio分析之一启动过程但我们今天基于Android10.0在来看看CarAudioZone这块。首先
private final boolean mUseDynamicRouting;
如果想使用CarAudioZone这套,mUseDynamicRouting一定要设置为true。这个值是来自
packages/services/Car/service/res/values/config.xml
<bool name="audioUseDynamicRouting">false</bool>
中,我们发现默认是false,但我们一般很少去改动原生目录下的配置,建议通过overlay的方式实现。 如果audioUseDynamicRouting为false,则CarAudio中的逻辑就会走到Android原生的AudioManager和AudioService中去了。这也算是一个兼容处理吧
我们先看下CarAudioService的构造函数
public CarAudioService(Context context) {
mContext = context;
mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
mUseDynamicRouting = mContext.getResources().getBoolean(R.bool.audioUseDynamicRouting);
mPersistMasterMuteState = mContext.getResources().getBoolean(
R.bool.audioPersistMasterMuteState);
mUidToZoneMap = new HashMap<>();
}
相比之前版本多了mUidToZoneMap,一个map的集合定义如下
private Map<Integer, Integer> mUidToZoneMap;
这个后续我们用到在细说,构造函数结束就是init了
public void init() {
synchronized (mImplLock) {
if (mUseDynamicRouting) {
AudioDeviceInfo[] deviceInfos = mAudioManager.getDevices(
AudioManager.GET_DEVICES_OUTPUTS);
if (deviceInfos.length == 0) {
Log.e(CarLog.TAG_AUDIO, "No output device available, ignore");
return;
}
SparseArray<CarAudioDeviceInfo> busToCarAudioDeviceInfo = new SparseArray<>();
for (AudioDeviceInfo info : deviceInfos) {
Log.v(CarLog.TAG_AUDIO, String.format("output id=%d address=%s type=%s",
info.getId(), info.getAddress(), info.getType()));
if (info.getType() == AudioDeviceInfo.TYPE_BUS) {
final CarAudioDeviceInfo carInfo = new CarAudioDeviceInfo(info);
if (carInfo.getBusNumber() >= 0) {
busToCarAudioDeviceInfo.put(carInfo.getBusNumber(), carInfo);
Log.i(CarLog.TAG_AUDIO, "Valid bus found " + carInfo);
}
}
}
setupDynamicRouting(busToCarAudioDeviceInfo);
} else {
Log.i(CarLog.TAG_AUDIO, "Audio dynamic routing not enabled, run in legacy mode");
setupLegacyVolumeChangedListener();
}
if (mPersistMasterMuteState) {
boolean storedMasterMute = Settings.Global.getInt(mContext.getContentResolver(),
VOLUME_SETTINGS_KEY_MASTER_MUTE, 0) != 0;
setMasterMute(storedMasterMute, 0);
}
}
}
init逻辑分了几个阶段第一个阶段是找output device。关于找output device的逻辑可参照Android10.0AudioFocus之如何使用(一)和二。然后根据deviceType是TYPE_BUS的拿出来在重新封装到CarAudioDeviceInfo中,我们在getDevices的时候,还记得有的device是AUDIO_DEVICE_OUT_BUS,所有使用AUDIO_DEVICE_OUT_BUS的都会有个address,而且address一定是不相同的。其实这里过滤的就是这个deviceType是AUDIO_DEVICE_OUT_BUS的即
public static final int DEVICE_OUT_BUS = 0x1000000;
我们在简单看下CarAudioDeviceInfo吧
CarAudioDeviceInfo(AudioDeviceInfo audioDeviceInfo) {
mAudioDeviceInfo = audioDeviceInfo;
mBusNumber = parseDeviceAddress(audioDeviceInfo.getAddress());
mSampleRate = getMaxSampleRate(audioDeviceInfo);
mEncodingFormat = getEncodingFormat(audioDeviceInfo);
mChannelCount = getMaxChannels(audioDeviceInfo);
final AudioGain audioGain = Preconditions.checkNotNull(
getAudioGain(), "No audio gain on device port " + audioDeviceInfo);
mDefaultGain = audioGain.defaultValue();
mMaxGain = audioGain.maxValue();
mMinGain = audioGain.minValue();
mCurrentGain = -1;
}
说下mBusNumber 这个我们看下audio_policy_volumes.xml中的定义就明白了
<devicePort tagName="bus0_media_out" role="sink" type="AUDIO_DEVICE_OUT_BUS"
address="bus0_media_out">
<profile name="" format="AUDIO_FORMAT_PCM_16_BIT"
samplingRates="48000" channelMasks="AUDIO_CHANNEL_OUT_STEREO"/>
<gains>
<gain name="" mode="AUDIO_GAIN_MODE_JOINT"
minValueMB="-3200" maxValueMB="600" defaultValueMB="0" stepValueMB="100"/>
</gains>
</devicePort>
mBusNumber 就是取得bus0_media_out,这个是怎么截取的呢,源码就不贴了,找到bus和第一个”_”中间的字符截取出来转成int,比如这个mBusNumber 就是0,关于截取这个最大是三位,像之前的版本都是bus00X_XXXX的。采样率 channel数都是根据配置来的,AudioGain是控制音量的。
我们继续,拿到了CarAudioDeviceInfo的集合后,则调用了setupDynamicRouting(busToCarAudioDeviceInfo)方法,代码很长,我们一点一点分析
private void setupDynamicRouting(SparseArray<CarAudioDeviceInfo> busToCarAudioDeviceInfo) {
final AudioPolicy.Builder builder = new AudioPolicy.Builder(mContext);
builder.setLooper(Looper.getMainLooper());
mCarAudioConfigurationPath = getAudioConfigurationPath();
if (mCarAudioConfigurationPath != null) {
try (InputStream inputStream = new FileInputStream(mCarAudioConfigurationPath)) {
CarAudioZonesHelper zonesHelper = new CarAudioZonesHelper(mContext, inputStream,
busToCarAudioDeviceInfo);
mCarAudioZones = zonesHelper.loadAudioZones();
} catch (IOException | XmlPullParserException e) {
throw new RuntimeException("Failed to parse audio zone configuration", e);
}
} else {
final IAudioControl audioControl = getAudioControl();
if (audioControl == null) {
throw new RuntimeException(
"Dynamic routing requested but audioControl HAL not available");
}
CarAudioZonesHelperLegacy legacyHelper = new CarAudioZonesHelperLegacy(mContext,
R.xml.car_volume_groups, busToCarAudioDeviceInfo, audioControl);
mCarAudioZones = legacyHelper.loadAudioZones();
}
for (CarAudioZone zone : mCarAudioZones) {
if (!zone.validateVolumeGroups()) {
throw new RuntimeException("Invalid volume groups configuration");
}
zone.synchronizeCurrentGainIndex();
Log.v(CarLog.TAG_AUDIO, "Processed audio zone: " + zone);
}
final CarAudioDynamicRouting dynamicRouting = new CarAudioDynamicRouting(mCarAudioZones);
dynamicRouting.setupAudioDynamicRouting(builder);
builder.setAudioPolicyVolumeCallback(mAudioPolicyVolumeCallback);
if (sUseCarAudioFocus) {
mFocusHandler = new CarZonesAudioFocus(mAudioManager,
mContext.getPackageManager(),
mCarAudioZones);
builder.setAudioPolicyFocusListener(mFocusHandler);
builder.setIsAudioFocusPolicy(true);
}
mAudioPolicy = builder.build();
if (sUseCarAudioFocus) {
mFocusHandler.setOwningPolicy(this, mAudioPolicy);
}
int r = mAudioManager.registerAudioPolicy(mAudioPolicy);
if (r != AudioManager.SUCCESS) {
throw new RuntimeException("registerAudioPolicy failed " + r);
}
}
我们一点一点分析,先看下获取getAudioConfigurationPath();
private static final String[] AUDIO_CONFIGURATION_PATHS = new String[] {
"/vendor/etc/car_audio_configuration.xml",
"/system/etc/car_audio_configuration.xml"
};
private String getAudioConfigurationPath() {
for (String path : AUDIO_CONFIGURATION_PATHS) {
File configuration = new File(path);
if (configuration.exists()) {
return path;
}
}
return null;
}
首先加载vendor/etc下的,如果vendor/etc没有则加载system/etc,android系统中一般配置文件都是这么一个加载顺序,先加载vendor下,如果没有,在加载系统默认的。拿到path后则开始解析
try (InputStream inputStream = new FileInputStream(mCarAudioConfigurationPath)) {
CarAudioZonesHelper zonesHelper = new CarAudioZonesHelper(mContext, inputStream,
busToCarAudioDeviceInfo);
mCarAudioZones = zonesHelper.loadAudioZones();
} catch (IOException | XmlPullParserException e) {
throw new RuntimeException("Failed to parse audio zone configuration", e);
}
我们继续看下CarAudioZonesHelper的创建和它的loadAudioZones()过程。
CarAudioZonesHelper(Context context, @NonNull InputStream inputStream,
@NonNull SparseArray<CarAudioDeviceInfo> busToCarAudioDeviceInfo) {
mContext = context;
mInputStream = inputStream;
mBusToCarAudioDeviceInfo = busToCarAudioDeviceInfo;
mNextSecondaryZoneId = CarAudioManager.PRIMARY_AUDIO_ZONE + 1;
mPortIds = new HashSet<>();
}
构造方法很简单,做了一些初始化,继续看load
CarAudioZone[] loadAudioZones() throws IOException, XmlPullParserException {
List<CarAudioZone> carAudioZones = new ArrayList<>();
parseCarAudioZones(carAudioZones, mInputStream);
return carAudioZones.toArray(new CarAudioZone[0]);
}
这里主要是对刚传入path的一个解析,为了理解解析的过程,我把car_audio_configuration.xml贴出来
<?xml version="1.0" encoding="utf-8"?>
<carAudioConfiguration version="1">
<zones>
<zone name="primary zone" isPrimary="true">
<volumeGroups>
<group>
<device address="bus0_media_out">
<context context="music"/>
</device>
<device address="bus3_call_ring_out">
<context context="call_ring"/>
</device>
</group>
<group>
<device address="bus1_navigation_out">
<context context="navigation"/>
</device>
</group>
</volumeGroups>
<displays>
<display port="1"/>
<display port="2"/>
</displays>
</zone>
<zone name="rear seat zone">
<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"/>
</device>
</group>
</volumeGroups>
</zone>
</zones>
</carAudioConfiguration>
我简单说下解析过程,先找name和isPrimary,在CarAudioZone中isPrimary只能有一个,如果是isPrimary则id是PRIMARY_AUDIO_ZONE ,否则是mNextSecondaryZoneId+1,关于除了primary的id的计算挺有意思。
private int getNextSecondaryZoneId() {
int zoneId = mNextSecondaryZoneId;
mNextSecondaryZoneId += 1;
return zoneId;
}
看到源码mNextSecondaryZoneId在初始化的时候,已经是1了,这里加1就变成了2,但虽然计算了,但返回的还是上次的值,也就是1,说白了就是primary是0,剩下累加1,有点意思。根据zone的标签我们知道了最终会创建多少个CarAudioZone,也就是最终返回的List< CarAudioZone>的size大小,我们继续再看具体的CarAudioZone里面有什么,首先是mVolumeGroups,CarAudioZone中会有一个mVolumeGroups的集合,而每个CarVolumeGroup都有什么呢?
CarVolumeGroup(Context context, int zoneId, int id) {
mContentResolver = context.getContentResolver();
mZoneId = zoneId;
mId = id;
mStoredGainIndex = Settings.Global.getInt(mContentResolver,
CarAudioService.getVolumeSettingsKeyForGroup(mZoneId, mId), -1);
}
mZoneId 我们知道就是之前分析的那个zoneId,mId就是我们我们每次mVolumeGroups.add的时候会传入一个从0开始累加的一个数,也就可以理解为list的索引,mStoredGainIndex数据库存存储的值。我们继续,group下就是device了,接下来就是根据device找的busNumber,busNumber之前也说过了,拿到busNumber后,解析context,有个map关系,根据context可以找到contextNumber
static {
CONTEXT_NAME_MAP = new HashMap<>();
CONTEXT_NAME_MAP.put("music", ContextNumber.MUSIC);
CONTEXT_NAME_MAP.put("navigation", ContextNumber.NAVIGATION);
CONTEXT_NAME_MAP.put("voice_command", ContextNumber.VOICE_COMMAND);
CONTEXT_NAME_MAP.put("call_ring", ContextNumber.CALL_RING);
CONTEXT_NAME_MAP.put("call", ContextNumber.CALL);
CONTEXT_NAME_MAP.put("alarm", ContextNumber.ALARM);
CONTEXT_NAME_MAP.put("notification", ContextNumber.NOTIFICATION);
CONTEXT_NAME_MAP.put("system_sound", ContextNumber.SYSTEM_SOUND);
}
有了busNumber,还可以找CarAudioDeviceInfo即mBusToCarAudioDeviceInfo.get(busNumber),mBusToCarAudioDeviceInfo还记得刚才构建CarAudioDeviceInfo的之后我们创建了CarAudioZonesHelper传入的。这样拿到了busNumber、contextNumber和CarAudioDeviceInfo我们就可以做CarVolumeGroup的bind了
void bind(int contextNumber, int busNumber, CarAudioDeviceInfo info) {
if (mBusToCarAudioDeviceInfo.size() == 0) {
mStepSize = info.getAudioGain().stepValue();
} else {
Preconditions.checkArgument(
info.getAudioGain().stepValue() == mStepSize,
"Gain controls within one group must have same step value");
}
mContextToBus.put(contextNumber, busNumber);
mBusToCarAudioDeviceInfo.put(busNumber, info);
if (info.getDefaultGain() > mDefaultGain) {
mDefaultGain = info.getDefaultGain();
}
if (info.getMaxGain() > mMaxGain) {
mMaxGain = info.getMaxGain();
}
if (info.getMinGain() < mMinGain) {
mMinGain = info.getMinGain();
}
if (mStoredGainIndex < getMinGainIndex() || mStoredGainIndex > getMaxGainIndex()) {
mCurrentGainIndex = getIndexForGain(mDefaultGain);
} else {
mCurrentGainIndex = mStoredGainIndex;
}
}
因为group下所有音量都是一个步长,所以步长只赋值一次。mContextToBus把contextNumber和 busNumber存入map,mBusToCarAudioDeviceInfo则是把busNumber和info又重新存了一下,一个device下不管多少个context,volume对应的都是一次赋值。max min 以及current volume都是如此。
简单总结每个group下的所有音量的步长都是一样的,每个group下device下的所有context的最大、最小、默认以及当前音量都是一样的。这有一个小问题要注意就是这个步长是以第一个device的步长为准,如果我们group下很多device,每个device的步长又不一样,那么就以第一device的步长为准
到此音量就结束了,还有一个display的标签,刚才没有分析的mPortIds,其实就是通过解析这个display标签下的port然后存入mPortIds的集和中的,mPortIds除了判断重复好像也没啥大用。然后根据portId创建了一个DisplayAddress.Physical physicalDisplayAddress,然后加入到这个 private final List
今天就先分析到这里,从CarAudioService到解析构造CarVolumeGroup的过程,我们发现其实在CarAudio中的声音其实是根据group下的device下的context来区分的,一个device下的所有context的音量是一样的。明天继续分析CarZonesAudioFocus~