4、音频设备管理:设备枚举、描述符、连接与策略

好,我们继续往下走。今天聊的是音频设备管理,说白了就是 AudioFlinger 怎么知道系统里插了什么设备、这些设备长什么样、什么时候拔了、以及最终该用哪个。

这部分内容,我当年刚接触时觉得挺琐碎的。不就是枚举一下设备吗?后来踩了几个坑才明白——设备管理是整个音频路由的基石。你设备都搞错了,后面策略再漂亮也是白搭。

4.1 设备枚举:系统里到底有哪些音频设备?

Android 的设备枚举,不是像 PC 那样去扫描 PCI 总线。它走的是一条更「抽象」的路。

设备信息从哪里来?从 HAL 层来。AudioFlinger 启动时,会去加载 HAL 库(比如 audio.primary.xxx.so),然后调用 HAL 的 get_devices() 函数。这个函数会返回当前所有可用的设备列表。

我习惯把设备分成两类:

  • 内置设备:比如手机的扬声器、听筒、内置麦克风。这些设备是焊死在板子上的,枚举一次基本不变。
  • 外接设备:比如蓝牙耳机、USB 声卡、3.5mm 耳机。这些是热插拔的,需要动态监听。

枚举的时机有两个:

  1. AudioFlinger 初始化时:一次性拉取所有设备。
  2. 设备热插拔事件触发时:比如你插上耳机,HAL 会回调通知 AudioFlinger 重新枚举。

核心要点:枚举不是轮询,是事件驱动的。HAL 层通过 audio_pool_events_callback 把设备变化推给 AudioFlinger。

4.2 设备描述符:DeviceDescriptor 到底装了什么?

枚举回来的设备,不能只是一个字符串名字吧?Android 用 DeviceDescriptor 这个结构体来完整描述一个音频设备。

我给大家拆开看看,里面都有什么:

字段 含义 举例
mDeviceId 设备唯一 ID 0x2 (耳机), 0x1 (扬声器)
mType 设备类型(AudioDeviceType) DEVICE_OUT_WIRED_HEADSET
mAddress 设备地址(蓝牙 MAC 或 USB 路径) "00:11:22:33:44:55"
mName 设备名称(可读字符串) "Speaker" 或 "JBL Tune 500BT"
mEncapsulationModes 支持的封装模式 用于 HDMI/DP 音频
mEncapsulationMetadataTypes 元数据类型 同上

嗯,这里要注意一个细节:mType 是 Android 框架层定义的逻辑类型,而 mAddress 才是真正区分同类型不同设备的钥匙。比如两个蓝牙耳机,它们的 mType 都是 DEVICE_OUT_BLUETOOTH_A2DP,但 mAddress 不同。

避坑指南:我曾经在调试多蓝牙设备切换时,发现 AudioFlinger 总是选错设备。查了半天,原来是 mAddress 在 HAL 层没有正确填充,导致两个设备被当成同一个。所以,如果你遇到设备选择异常,先检查 mAddress 对不对。

4.3 设备连接与断开处理

设备不是永远在线的。耳机拔了、蓝牙断了、USB 声卡松了——这些都需要 AudioFlinger 及时响应。

处理流程大致是这样的:

  1. HAL 检测到事件:比如耳机插入中断触发,HAL 驱动层感知到。
  2. HAL 回调 AudioFlinger:通过 audio_pool_devices_callback 把事件类型(连接/断开)和设备信息传上来。
  3. AudioFlinger 更新设备列表:在 AudioFlinger::onDeviceConnection()onDeviceDisconnection() 中,增删 DeviceDescriptor
  4. 通知策略管理器:告诉 AudioPolicyManager:「设备变了,你重新算一下路由吧」。

这里有个关键点:断开处理比连接处理更复杂。为什么?因为断开时,可能有音频流正在使用这个设备。你不能直接拔掉,得先做「优雅降级」。

警告:如果你在 HAL 层直接断开设备而不通知 AudioFlinger,会导致 AudioFlinger 还在往一个已经不存在的设备写数据。结果就是——无声,甚至 crash。我见过一个案例,某款手机在 USB 声卡热拔时必重启,就是因为 HAL 层没发断开事件。

4.4 设备选择策略:到底用哪个设备出声?

设备枚举好了,描述符也建好了,连接断开也能处理了。接下来就是灵魂拷问:现在有扬声器、有蓝牙耳机、还有 USB 声卡,到底用哪个?

这个决策不是 AudioFlinger 做的,是 AudioPolicyManager 做的。但 AudioFlinger 提供了决策所需的数据基础。

策略的核心逻辑,我总结为三条:

  • 优先级规则:外接设备 > 内置设备。比如插了耳机,就优先用耳机,而不是扬声器。
  • 策略覆盖:App 可以通过 AudioAttributes 指定使用某个设备类型。比如游戏 App 可以强制走扬声器,即使插着耳机。
  • 动态切换:设备连接/断开时,策略管理器会重新评估。比如蓝牙耳机连上了,就从扬声器切到蓝牙。

我给大家画个图,把整个流程串起来:

音频设备管理核心流程 HAL 层 设备枚举 / 热插拔检测 get_devices() / callback AudioFlinger 维护 DeviceDescriptor 列表 onDeviceConnection/Disconnection AudioPolicyManager 设备选择策略:优先级 + 策略覆盖 最终音频输出设备 扬声器 / 耳机 / 蓝牙 / USB 关键数据结构 • DeviceDescriptor • AudioDeviceType • audio_pool_devices • AudioPolicyConfig 策略优先级 1. 外接设备 > 内置设备 2. App 策略覆盖 3. 动态切换

你看,整个链路很清晰。HAL 负责「感知」,AudioFlinger 负责「管理」,AudioPolicyManager 负责「决策」。各司其职。

最后说一个我个人的经验:设备选择策略不是一成不变的。不同厂商、不同产品形态,策略可能完全不同。比如车载系统,可能优先用蓝牙电话通道;而智能音箱,可能永远用内置扬声器。所以 Android 把策略逻辑放在 AudioPolicyManager 里,允许厂商定制。

总结一下:设备管理这块,核心就三件事——把设备找出来(枚举)、把设备描述清楚(Descriptor)、在设备变化时做出正确选择(策略)。搞懂了这三件事,音频路由的底层逻辑你就拿下了。


公众号:蓝海资料掘金营,微信deep3321