Android车载开发启示录(四)

前言

笔者在从事Android车载行业的开发过程中,发现Android车载开发和平时的Android开发还是有很大不同之处,对于一个小白来说或者说如果是刚入行的新人都会很陌生,目前市场也没有很多系统性的知识提供给大家。

所以笔者准备通过一个专栏系列,把自己在车载开发过程中的学习记录和开发经验记录下来并分享出来,希望能给大家带来一些帮助。

在第一篇内容,笔者介绍了Android车载操作系统现状、整个操作系统架构和架构下核心概念:

Android车载开发启示录(一)

第二篇内容,笔者介绍了Android Automotive操作系统中的一个关键组件CarFramework

Android车载开发启示录(二)

第三篇,笔者介绍音频焦点相关的知识,为后续介绍音频相关的内容打好基础

Android车载开发启示录(三)

本篇笔者将分享车载音频系统的相关知识:

1.车载音频系统架构

2.CarAudioManager和CarAudioService

3.多音区音频

车载音频系统

Android Automotive OS (AAOS) 是在核心 Android 音频堆栈的基础之上打造而成。AAOS 负责实现信息娱乐声音(即媒体、导航和通讯声音),但不直接负责具有严格可用性和计时要求的铃声和警告。

虽然 AAOS 提供了信号和机制来帮助车辆管理音频,但最终还是由车辆来决定应为驾驶员和乘客播放什么声音,从而确保对保障安全至关重要的声音和监管声音能被确切听到,而不会中断。

当 Android 管理车辆的媒体体验时,应通过应用来代表外部媒体来源(例如电台调谐器),这类应用可以处理该来源的音频焦点和媒体键事件。

车载音频架构

汽车音频系统可以处理多种声音和声音流,管理来自 Android 应用的声音,同时控制这些应用,并根据其声音类型将声音路由到 HAL 中的输出设备

声音流

声音流包含逻辑声音流和物理声音流:

  • 逻辑声音流 :也称为"声源",使用音频属性进行标记,音频系统会使用这些属性做出混音决策并将系统状态通知给应用。
  • 物理声音流:也称为"设备",在混音后没有上下文信息。

系统实现者必须提供一个混音器,用于接受来自 Android 的一个或多个声音输入流,然后以合适的方式将这些声音流与车辆所需的外部声源组合起来。

为了确保可靠性,外部声音(来自独立声源,例如安全带警告铃声)在 Android 外部(HAL 下方,甚至是在单独的硬件中)进行管理。

HAL 实现和外部混音器负责确保对保障安全至关重要的外部声音能够被用户听到,而且负责在 Android 提供的声音流中进行混音,并将混音结果路由到合适的音响设备。

车载音频配置

车载音频服务会读取车载音频配置文件来为设备设置音频。

将车载音频配置文件放在设备的 vendor\etc\system\etc\ 中,车载音频服务会首先在 vendor\etc\ 中搜索该文件。车载音频服务会读取 car_audio_configuration.xml 来确定音频配置。

在 Android 10 中,car_audio_configuration.xml 取代了 car_volumes_groups.xmlIAudioControl.getBusForContextaudio_policy_configuration.xml 通常包含在 vendor 分区中,表示主板的音频硬件配置。

car_audio_configuration.xml 中引用的所有设备都必须在 audio_policy_configuration.xml 中进行定义。

在 Android 14 中,AAOS 引入了原始设备制造商 (OEM) 插件服务,可以更主动地管理由车载音频服务监督的音频行为。除了新的插件服务之外,车载音频配置文件中还添加了以下更改:

  • OEM 定义的车载音频上下文
  • 非主音频区动态配置

音频上下文

为了简化 AAOS 音频的配置,用法均已归入 CarAudioContexts。这些音频上下文会在整个 CarAudioService 中使用,以定义路由、音量组、音频焦点和冲突管理。

下图列出了 AAOS 中的静态音频上下文

音频控制HAL

Android 9 中引入了音频控制 HAL,车载音频服务会与音频控制 HAL 进行通信。从 Android 14 开始,音频控制 HAL 支持:

  1. 淡变和平衡:与 Android 中已提供的通用音效不同,此机制允许系统应用通过 CarAudioManager API 设置音频平衡和淡出;
  2. HAL 音频焦点请求:与 Android 类似,AAOS 依靠应用对音频焦点的积极参与来管理车载音频播放。焦点信息用于管理要进行音量和闪避控制的音频流
  3. 设备静音和闪避:Android 12 引入了音量组静音功能,以便在用户的音频互动期间实现更全面的静音控制。这样一来,音频控制 HAL 便可以接收车载音频服务截获的静音事件;Android 12 引入了车载音频闪避,以优化对音频流并发播放的控制。这样一来,OEM 可以根据汽车的物理音频配置和当前播放状态(由车载音频服务确定)实现自己的闪避行为。
  4. 音频设备增益变化:可以使用这种机制将车载音频系统的音频增益变化传达给车载音频服务。该机制会暴露音频增益音量指数的变化,并提供增益发生变化的对应原因
  5. 音频端口配置更改

CarAudioManager

获取CarAudioManager

获取CarAudioManager对象,首先要获取Car对象,然后通过mCar.getCarManager(Car.AUDIO_SERVICE)

typescript 复制代码
Car mCar;
CarAudioManager mAudioManager;
private void setup(){
    mCar = Car.createCar(mContext, mConnectionCallbacks); 
    mCar.connect();
}

private ServiceConnection mConnectionCallbacks = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
        Log.d(TAG, "onServiceConnected: ");
        mCarAudioManager = (CarAudioManager) mCar.getCarManager(Car.AUDIO_SERVICE);
    }

    @Override
    public void onServiceDisconnected(ComponentName componentName) {
        Log.d(TAG, "onServiceDisconnected: ");
    }
};

CarAudioManager初始化过程

ini 复制代码
public Object getCarManager(String serviceName) {
    CarManagerBase manager;
    ICar service = getICarOrThrow();
    synchronized (mCarManagerLock) {
        manager = mServiceMap.get(serviceName);
        if (manager == null) {
            try {
                IBinder binder = service.getCarService(serviceName);
                if (binder == null) {
                    Log.w(CarLibLog.TAG_CAR, "getCarManager could not get binder for service:" +
                          serviceName);
                    return null;
                }
                manager = createCarManager(serviceName, binder);
                if (manager == null) {
                    Log.w(CarLibLog.TAG_CAR,
                          "getCarManager could not create manager for service:" +
                          serviceName);
                    return null;
                }
                //用来回调onCarDisconnected
                mServiceMap.put(serviceName, manager);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
    }
    return manager;
}
private CarManagerBase createCarManager(String serviceName, IBinder binder) {
    CarManagerBase manager = null;
    switch (serviceName) {
        case AUDIO_SERVICE:
            manager = new CarAudioManager(binder, mContext, mEventHandler);
            break;
    }
}
private void tearDownCarManagers() {
    synchronized (mCarManagerLock) {
        for (CarManagerBase manager: mServiceMap.values()) {
            //回调所有的CarManagerBase
            manager.onCarDisconnected();
        }
        mServiceMap.clear();
    }
}

对外的API

通过CarAudioManager可以通过提供的AP去实现相关的功能,比如音量的调节。 它实际是对CarAudioServive的跨进程调用

CarAudioService

AAOS中对原有的音频机制进行扩充,在CarService中加入了CarAudioService,对音频设备进行更加细致的管理。

音量控制相关的方法

音量相关方法需要关注的是参数,包含zoneIdgroundIdusage

csharp 复制代码
  void setGroupVolume(int zoneId, int groupId, int index, int flags);
  int getGroupMaxVolume(int zoneId, int groupId);
  int getGroupMinVolume(int zoneId, int groupId);
  int getGroupVolume(int zoneId, int groupId); 
  int getVolumeGroupCount(int zoneId);
  int getVolumeGroupIdForUsage(int zoneId, int usage);
  int[] getUsagesForVolumeGroupId(int zoneId, int groupId);

groupId的获取:

通过getVolumeGroupIdForUsage通过传入的usagezoneId进行遍历查找,返回对应的groupID

ini 复制代码
CarVolumeGroup[] groups = mCarAudioZones[zoneId].getVolumeGroups();
for (int i = 0; i < groups.length; i++) {
    int[] contexts = groups[i].getContexts();
    for (int context : contexts) {
        if (getContextForUsage(usage) == context) {
            return i;
        }
    }
}

zoneId的获取

setZoneIdForUidgetZoneIdForUidclearZoneIdForUid是对mUidToZoneMap查询维护,然后进行音频焦点AudioFocus的管理

arduino 复制代码
 int[] getAudioZoneIds();
 int getZoneIdForUid(int uid);
 boolean setZoneIdForUid(int zoneId, int uid);
 boolean clearZoneIdForUid(int uid);
 int getZoneIdForDisplayPortId(byte displayPortId);

通过mUidToZoneMap维护uidzoneId的映射map,通过getZoneIdForUid就可以获取到相应的zoneId

音量的监听

less 复制代码
public void registerVolumeCallback(@NonNull IBinder binder) {}
public void unregisterVolumeCallback(@NonNull IBinder binder) {}

传统模式的音量监听回调是在init方法,通过广播监听音量变化,然后循环遍历binder集合进行回调

java 复制代码
public void init() {
	if (mUseDynamicRouting) {
    }else{
        setupLegacyVolumeChangedListener();
    }
}
private void setupLegacyVolumeChangedListener() {
    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(AudioManager.VOLUME_CHANGED_ACTION);
    intentFilter.addAction(AudioManager.MASTER_MUTE_CHANGED_ACTION);
    mContext.registerReceiver(mLegacyVolumeChangedReceiver, intentFilter);
}
private final BroadcastReceiver mLegacyVolumeChangedReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        final int zoneId = CarAudioManager.PRIMARY_AUDIO_ZONE;
        switch (intent.getAction()) {
            case AudioManager.VOLUME_CHANGED_ACTION:
                int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
                int groupId = getVolumeGroupIdForStreamType(streamType);
                if (groupId == -1) {
                    Log.w(CarLog.TAG_AUDIO, "Unknown stream type: " + streamType);
                } else {
                    callbackGroupVolumeChange(zoneId, groupId, 0);
                }
                break;
            case AudioManager.MASTER_MUTE_CHANGED_ACTION:
                callbackMasterMuteChange(zoneId, 0);
                break;
        }
    }
};
private void callbackGroupVolumeChange(int zoneId, int groupId, int flags) {
    for (BinderInterfaceContainer.BinderInterface<ICarVolumeCallback> callback :
         mVolumeCallbackContainer.getInterfaces()) {
        try {
            callback.binderInterface.onGroupVolumeChanged(zoneId, groupId, flags);
        } catch (RemoteException e) {
            Log.e(CarLog.TAG_AUDIO, "Failed to callback onGroupVolumeChanged", e);
        }
    }
}

动态路由的音量监听是在setupDynamicRouting里,通过AudioPolicybuilder设置的mAudioPolicyVolumeCallback,之后再循环遍历其他监听binder集合

java 复制代码
private void setupDynamicRouting(SparseArray<CarAudioDeviceInfo> busToCarAudioDeviceInfo) {
        final AudioPolicy.Builder builder = new AudioPolicy.Builder(mContext);
        builder.setAudioPolicyVolumeCallback(mAudioPolicyVolumeCallback);
}
    private final AudioPolicy.AudioPolicyVolumeCallback mAudioPolicyVolumeCallback =
            new AudioPolicy.AudioPolicyVolumeCallback() {
        @Override
        public void onVolumeAdjustment(int adjustment) {
            final int usage = getSuggestedAudioUsage();
            Log.v(CarLog.TAG_AUDIO,
                    "onVolumeAdjustment: " + AudioManager.adjustToString(adjustment)
                            + " suggested usage: " + AudioAttributes.usageToString(usage));
            // TODO: Pass zone id into this callback.
            final int zoneId = CarAudioManager.PRIMARY_AUDIO_ZONE;
            final int groupId = getVolumeGroupIdForUsage(zoneId, usage);
            final int currentVolume = getGroupVolume(zoneId, groupId);
            final int flags = AudioManager.FLAG_FROM_KEY | AudioManager.FLAG_SHOW_UI;
            switch (adjustment) {
                case AudioManager.ADJUST_LOWER:
                    int minValue = Math.max(currentVolume - 1, getGroupMinVolume(zoneId, groupId));
                    setGroupVolume(zoneId, groupId, minValue , flags);
                    break;
                case AudioManager.ADJUST_RAISE:
                    int maxValue =  Math.min(currentVolume + 1, getGroupMaxVolume(zoneId, groupId));
                    setGroupVolume(zoneId, groupId, maxValue, flags);
                    break;
                case AudioManager.ADJUST_MUTE:
                    setMasterMute(true, flags);
                    callbackMasterMuteChange(zoneId, flags);
                    break;
                case AudioManager.ADJUST_UNMUTE:
                    setMasterMute(false, flags);
                    callbackMasterMuteChange(zoneId, flags);
                    break;
                case AudioManager.ADJUST_TOGGLE_MUTE:
                    setMasterMute(!mAudioManager.isMasterMute(), flags);
                    callbackMasterMuteChange(zoneId, flags);
                    break;
                case AudioManager.ADJUST_SAME:
                default:
                    break;
            }
        }
    };

多音区音频

随着现在的汽车领域发展,多个用户同时与平台互动并且每个用户都希望使用单独媒体的需求出来了。比如,后座上的乘客在后座显示屏上观看视频时,司机可以在驾驶舱中播放音乐。

所以多区音频的技术也就应需而生,它通过允许不同的音频源在车辆的不同音频区同时进行播放来实现此目的。

从 Android 10 开始提供的多区音频让原始设备制造商 (OEM) 能够将音频配置到单独的音频区。每个音频区由车辆内的一组设备组成,并且有各自的音量组、上下文路由配置以及焦点管理。通过这种方式,可以将主驾驶舱配置为一个音频区,而将后座显示屏的耳机插孔配置为第二个音频区。

每个音频区的焦点也是单独维护的。这使得不同音频区中的应用可以单独生成音频,而不会彼此干扰,同时让应用保持关注其所在音频区内焦点的变化。CarAudioService 内中的 CarZonesAudioFocus 负责管理每个音频区的焦点。

这些音频区被定义为 car_audio_configuration.xml 的一部分。然后,CarAudioService 读取该配置,并帮助 AudioService 根据关联的音频区路由音频流。每个音频区仍会根据上下文和应用 UID 定义路由规则。创建播放器时,CarAudioService 会确定播放器与哪个音频区相关联,然后根据用法确定 AudioFlinger 应将音频路由到哪个设备。

参考资料

  1. 音频属性的介绍 source.android.google.cn/docs/core/a...

  2. 车载音频配置 source.android.com/docs/automo...

  3. Android Q CarAudio 汽车音频学习笔记 blog.csdn.net/sinat_18179...

相关推荐
Geeker555 小时前
如何在忘记密码的情况下解锁Android手机?
android·网络·macos·华为·智能手机·电脑·手机
wxx21506 小时前
【android】【adb shell】写一个shell脚本,监听进程pid变化
android·adb
心死翼未伤7 小时前
【MySQL基础篇】多表查询
android·数据结构·数据库·mysql·算法
喂_balabala7 小时前
Android手机拍照或从本地相册选取图片设置头像-高版本适配
android·开发语言
飞翔的佩奇8 小时前
Java项目:基于SSM框架实现的德云社票务管理系统【ssm+B/S架构+源码+数据库+开题报告+毕业论文】
java·数据库·spring·架构·maven·ssm框架·票务系统
_小马快跑_8 小时前
Android | StandardCharsets.UTF_8.toString() 遇到的兼容问题记录
android
wxx21509 小时前
【Android】【多屏】多屏异显异触调试技巧总结
android
人民的石头11 小时前
Android增量更新----java版
android
鲁鲁51712 小时前
梧桐数据库:存算分离和存算一体架构的分布式数据库技术分析
数据库·分布式·架构·梧桐数据库
向阳逐梦12 小时前
对回收站里的文件进行操作
算法·程序员·架构