1. 音频策略实战:从“听个响”到“听得对”
大家好,我是老张,在Android音频这块摸爬滚打了十来年,从功能机时代的单声道铃声做到现在智能座舱里的多路独立音频流。今天咱们不聊那些虚头巴脑的架构图,就聊点实在的:当你开发的会议App在车载大屏上死活不出声,或者用户插着USB耳机却从手机扬声器里传出私密语音时,你该怎么办?
很多开发者第一次接触Android音频策略,可能是在处理一个简单的Bug:用户反馈蓝牙耳机连接后,播放音乐没声音。你查了半天,发现日志里音频流正常,就是没走蓝牙。这时候,如果你去翻看AOSP源码,大概率会一头撞进frameworks/av/services/audiopolicy/这个目录,然后被里面复杂的策略逻辑绕晕。其实,Android音频系统的核心目标很简单:在正确的时间,把正确的声音,送到正确的设备上。所谓“策略”,就是一套决定这个“正确”的规则。
这套规则不是一成不变的。原生Android的策略列表,是谷歌工程师基于大多数消费电子设备(手机、平板)的使用习惯预设的。比如,打电话时优先用听筒或耳机,看视频时优先用扬声器或蓝牙。但当我们面对车载信息娱乐系统、视频会议专用设备、智能家居中控屏这些特殊场景时,原生的优先级可能就“驴唇不对马嘴”了。一个典型的例子:在车载场景下,导航提示音应该从车载扬声器出来,而电话通话应该走蓝牙耳机,两者同时发生时不能互相干扰。这就需要我们深入策略引擎的心脏,去自定义那套选择逻辑。
别怕,这事儿没想象中那么难。你不需要重写整个音频框架,90%的需求,通过修改两个关键函数里的设备列表顺序就能搞定。这篇文章,我就带你像老司机修车一样,把引擎盖(Engine.cpp)打开,看看里面哪个螺丝(设备类型)该紧一紧,哪根管子(策略分支)该换一换。我会用我踩过的坑和填过的代码,让你明白怎么让音频“听话”。
2. 核心战场:认识两个关键函数
想要自定义音频策略,你得先找到“指挥部”在哪。这个指挥部就是frameworks/av/services/audiopolicy/enginedefault/src/Engine.cpp文件。里面住着两位最重要的“指挥官”:
2.1 输出指挥官:getDevicesForStrategyInt
这个函数决定了声音从哪里放出来。它的工作流程就像一个智能调度员:
- 接收指令:系统告诉它现在是什么“战略”(
strategy),比如是播放媒体(STRATEGY_MEDIA)、处理电话(STRATEGY_PHONE)还是播放系统提示音(STRATEGY_SONIFICATION)。 - 清点装备:看看当前系统有哪些可用的输出设备(
availableOutputDevices),比如扬声器、蓝牙耳机、HDMI等等。 - 按规则派单:根据既定的优先级列表,从可用设备里挑出一个或几个最合适的,把音频流分配过去。
它的核心逻辑是一个巨大的switch-case语句,每个case对应一种音频策略。我们最需要关心的通常是STRATEGY_PHONE(语音通话)和STRATEGY_MEDIA(媒体播放)。举个例子,在原生代码的STRATEGY_PHONE策略里,当没有强制使用蓝牙通话(FORCE_BT_SCO)时,它会按照这个顺序选择设备:
devices = availableOutputDevices.getFirstDevicesFromTypes({ AUDIO_DEVICE_OUT_WIRED_HEADPHONE, // 1. 有线耳机(带麦) AUDIO_DEVICE_OUT_WIRED_HEADSET, // 2. 有线耳机(不带麦) AUDIO_DEVICE_OUT_USB_HEADSET, // 3. USB耳机 AUDIO_DEVICE_OUT_USB_DEVICE}); // 4. 其他USB音频设备看到了吗?顺序就是优先级。如果用户同时插着带麦的圆孔耳机和USB耳机,系统会优先选择列表里排第一的AUDIO_DEVICE_OUT_WIRED_HEADPHONE。这就是为什么有时候你插着USB设备,微信语音却从手机听筒里出来的原因——因为听筒可能根本不在这个列表里,或者排在更后面。
2.2 输入指挥官:getDeviceForInputSource
这位指挥官管的是声音从哪里收进来。当你打开麦克风录音、微信发语音、或者开视频会议时,就由它来决定使用哪个录音设备。它的逻辑和输出指挥官类似,也是根据音频来源(inputSource)来决策。比如,当音频来源是AUDIO_SOURCE_VOICE_COMMUNICATION(微信语音、腾讯会议等)时,在没有强制设置的情况下,它的选择列表是:
device = availableDevices.getFirstExistingDevice({ AUDIO_DEVICE_IN_WIRED_HEADSET, // 1. 有线耳机麦克风 AUDIO_DEVICE_IN_USB_HEADSET, // 2. USB耳机麦克风 AUDIO_DEVICE_IN_BUILTIN_MIC}); // 3. 手机内置麦克风这意味着,如果你插着带麦克风的耳机,系统会优先使用耳麦收音,而不是手机底部的麦克风,这能有效降低环境噪音。但问题来了,假如你的设备是一台内置了高质量阵列麦克风的会议平板,你当然希望始终使用内置麦克风,而不是用户随便插的一个耳麦。这时候,你就需要把这个列表的顺序掉个个儿,把AUDIO_DEVICE_IN_BUILTIN_MIC放到最前面。
理解这两个函数,你就掌握了音频路由的命门。它们内部那些getFirstDevicesFromTypes或getFirstExistingDevice的调用,本质上就是在遍历一个“心愿单”,按顺序询问系统:“这个设备有吗?没有?那下一个呢?” 我们的定制工作,就是修改这份“心愿单”的排序,甚至增删上面的项目。
3. 实战定制:手把手修改设备优先级
光说不练假把式,咱们直接上代码,看看怎么根据实际需求动刀。我假设你已经有了一套可编译的Android源码环境,并且知道如何mm编译audiopolicy模块。
3.1 场景一:车载系统,优先保证蓝牙通话
在车机上,用户连接手机蓝牙后,电话和语音消息的音频必须无条件地走蓝牙通道(AUDIO_DEVICE_OUT_BLUETOOTH_SCO),即使用户插着USB音乐U盘(用于播放本地音乐)也应该如此。否则,电话声音从车载扬声器公放出来,就毫无隐私可言了。
我们需要修改STRATEGY_PHONE策略下的默认分支(default: // FORCE_NONE)。找到Engine::getDevicesForStrategyInt函数中对应的部分,通常里面已经有一个处理蓝牙A2DP(音乐蓝牙)的逻辑,但我们想要的是SCO(通话蓝牙)。我们需要在默认设备列表之前,优先检查并选择SCO设备。
修改前(大致逻辑):
default: // FORCE_NONE // ... 可能有一些其他检查 ... devices = availableOutputDevices.getFirstDevicesFromTypes({ AUDIO_DEVICE_OUT_WIRED_HEADPHONE, AUDIO_DEVICE_OUT_WIRED_HEADSET, AUDIO_DEVICE_OUT_USB_HEADSET, AUDIO_DEVICE_OUT_USB_DEVICE}); break;修改后:
default: // FORCE_NONE // 场景定制:在车载环境下,优先使用蓝牙SCO设备进行通话 if (isCarPlatform()) { // 你需要自己实现或利用现有条件判断是否为车机 devices = availableOutputDevices.getDevicesFromType(AUDIO_DEVICE_OUT_BLUETOOTH_SCO); if (!devices.isEmpty()) { break; // 找到蓝牙SCO设备,直接使用,后续列表不再检查 } // 如果没有蓝牙SCO,再检查是否有蓝牙A2DP(有些车机通话也可能走A2DP) if (!isInCall()) { // 非通话中,媒体声音可以走A2DP devices = availableOutputDevices.getDevicesFromType(AUDIO_DEVICE_OUT_BLUETOOTH_A2DP); if (!devices.isEmpty()) { break; } } } // 如果不是车机,或者车机上没有蓝牙设备,则回退到原有的默认优先级 devices = availableOutputDevices.getFirstDevicesFromTypes({ AUDIO_DEVICE_OUT_WIRED_HEADPHONE, AUDIO_DEVICE_OUT_WIRED_HEADSET, AUDIO_DEVICE_OUT_USB_HEADSET, AUDIO_DEVICE_OUT_USB_DEVICE}); break;关键点:这里我引入了一个假设的isCarPlatform()函数来判断平台。在实际项目中,你可以通过系统属性(如ro.build.characteristics)、设备类型或者你自己的产品宏定义来实现。重点是,通过break语句,一旦找到符合条件的设备,就立刻退出选择流程,后面的设备列表就不再被考虑了。
3.2 场景二:会议设备,锁定HDMI和内置麦克风
现在考虑一个视频会议一体机,它通常连接着HDMI显示器和大功率扬声器,并且内置了优秀的降噪麦克风阵列。我们希望:
- 所有媒体播放和系统声音,默认从HDMI关联的音频输出设备(
AUDIO_DEVICE_OUT_AUX_DIGITAL)走。 - 语音通话的输入,始终使用内置麦克风(
AUDIO_DEVICE_IN_BUILTIN_MIC),忽略任何外接耳麦。
首先,修改媒体输出策略(STRATEGY_MEDIA)。在getDevicesForStrategyInt函数中找到STRATEGY_MEDIA的case,在最后组合devices2和devices3的地方动手脚。原生的逻辑会把扬声器(SPEAKER)作为默认,HDMI等设备作为附加。我们要反过来。
修改思路:
case STRATEGY_MEDIA: { // ... 前面的force use检查 ... // 修改默认设备查找逻辑 if (isConferenceDevice()) { // 判断是否为会议设备 // 优先尝试HDMI/DisplayPort等数字输出 devices2 = availableOutputDevices.getDevicesFromTypes({ AUDIO_DEVICE_OUT_AUX_DIGITAL, // HDMI AUDIO_DEVICE_OUT_DGTL_DOCK_HEADSET, // 数字底座 AUDIO_DEVICE_OUT_SPDIF}); // 同轴音频 if (devices2.isEmpty()) { // 如果没有数字输出,再使用扬声器 devices2 = availableOutputDevices.getDevicesFromType(AUDIO_DEVICE_OUT_SPEAKER); } } else { // 非会议设备,保持原有逻辑(先扬声器,后附加数字设备) if (devices2.isEmpty()) { devices2 = availableOutputDevices.getDevicesFromType(AUDIO_DEVICE_OUT_SPEAKER); } DeviceVector devices3; if (strategy == STRATEGY_MEDIA) { devices3 = availableOutputDevices.getDevicesFromTypes({ AUDIO_DEVICE_OUT_HDMI_ARC, AUDIO_DEVICE_OUT_SPDIF, AUDIO_DEVICE_OUT_AUX_LINE}); } devices2.add(devices3); } devices.add(devices2); // ... 后续HDMI系统音频模式处理 ... } break;其次,修改语音通话的输入策略。在getDeviceForInputSource函数中,找到AUDIO_SOURCE_VOICE_COMMUNICATION的default分支(在FORCE_NONE情况下)。
修改后:
default: // FORCE_NONE if (isConferenceDevice()) { // 会议设备:强制使用内置麦克风阵列,忽略所有外接耳机麦克风 device = availableDevices.getDevice( AUDIO_DEVICE_IN_BUILTIN_MIC, String8(""), AUDIO_FORMAT_DEFAULT); // 可以进一步指定后置麦克风或其他内置麦克风 // device = availableDevices.getDevice(AUDIO_DEVICE_IN_BACK_MIC, ...); } else { // 普通设备:保持原有优先级(耳机麦 > USB麦 > 内置麦) device = availableDevices.getFirstExistingDevice({ AUDIO_DEVICE_IN_WIRED_HEADSET, AUDIO_DEVICE_IN_USB_HEADSET, AUDIO_DEVICE_IN_USB_DEVICE, AUDIO_DEVICE_IN_BUILTIN_MIC}); } break;通过这样的条件判断,我们就实现了基于设备类型的差异化策略。一台设备出厂时,它的“身份”(是手机、车机还是会议机)就决定了它处理音频的“性格”。
4. 多设备共存与冲突解决之道
自定义了优先级,并不代表万事大吉。现实场景更复杂:用户可能同时连接了蓝牙耳机、插着USB声卡,HDMI也接着。多个音频流(比如导航提示音和音乐播放)可能同时活跃。这时候,冲突就来了。
4.1 理解“后插优先”与动态优先级
Android音频系统有一个很重要的基础行为,我称之为“后插优先”原则。这不是一个明确的函数,而是一种体现在代码逻辑中的倾向:当新设备插入时,系统会更倾向于将正在活跃或即将启动的音频流切换到新设备上。这个逻辑分散在设备连接状态监听和路由更新的部分。
但“后插优先”有时会坏事。比如在车载场景,音乐正通过USB播放,此时用户连接了蓝牙手机。如果不加干预,“后插优先”可能会把音乐流切到蓝牙去,但用户的本意可能只是用蓝牙接电话。因此,我们的静态优先级列表(在getDevicesForStrategyInt中定义的)需要足够强大,能够抵御这种“诱惑”。通常,我们会为关键策略(如STRATEGY_PHONE)赋予蓝牙SCO极高的优先级,而为媒体策略(STRATEGY_MEDIA)赋予USB或AUX较高的优先级,让它们“各司其职”,减少互相抢夺。
4.2 处理多路音频流并发
当导航提示音(STRATEGY_SONIFICATION)和音乐(STRATEGY_MEDIA)同时播放时,系统需要决定它们是否能共用同一个输出设备,或者必须抢占。这涉及到AudioPolicyManager中关于输出设备“共享”与“独占”的策略。默认情况下,像STRATEGY_PHONE这样的策略具有更高的“抢占”优先级。
如果你想实现“导航音压低音乐”的效果,而不是打断音乐,就需要关注音频焦点(AudioFocus)和音轨(AudioTrack)的配置,这稍微超出了纯设备优先级调整的范围。但你可以通过确保导航提示音和音乐走不同的物理或逻辑通道来避免底层冲突。例如,在支持多路独立音频输出的硬件上,可以将导航指定给AUDIO_DEVICE_OUT_BUS之类的设备,而音乐走AUDIO_DEVICE_OUT_AUX_DIGITAL。
4.3 调试与验证:你的修改生效了吗?
改了代码,编译刷机,怎么验证?我常用的“三板斧”:
看日志:这是最直接的。在
Engine.cpp的关键函数里增加ALOGV或ALOGI日志,打印出最终选择的设备类型。编译时确保eng或userdebug版本,然后通过logcat | grep AudioPolicy来过滤查看。你会看到类似getDevicesForStrategy() strategy X, device Y的输出,确认你修改的列表顺序真的被用到了。用命令测试:
dumpsys media.audio_policy这个命令是宝藏。它可以打印出当前所有音频策略、设备状态、输出输入流详情。重点关注Devices:部分和Outputs:部分,看你的目标音频流(stream)是否真的绑定(routed)到了你期望的设备(device type)上。模拟场景:在代码里临时写死一些条件判断,来模拟你的目标场景。比如,在判断是否为车机的地方,直接
return true;,然后测试蓝牙通话的音频路径是否正确。或者,在getDevicesForStrategyInt开头,根据某个属性强制设置strategy的值,来单独测试某一个策略分支。
修改音频策略是个精细活,有时候改错一个地方,可能导致无声、声音卡顿或者设备热插拔异常。我的经验是:每次只修改一个策略的一个分支,改完立刻编译测试,确认无误后再进行下一个修改。同时,要全面测试各种设备插拔组合和音频场景,确保没有引入回归问题。毕竟,音频是用户体验最敏感的部分之一,出了问题,用户第一时间就能察觉到。