ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android录音设备选择全解:AudioRecord.setPreferredDevice从入门到实战

Android录音设备选择全解:AudioRecord.setPreferredDevice从入门到实战 做 Android 音频开发的兄弟应该都遇到过录音设备指向不对的问题明明插着带麦克风的有线耳机录出来的却是机身麦克风的声音蓝牙耳机连着系统却把录音通道给了内置麦克风。这类问题在 Android 16 上依旧高频出现解决它的正路就是 AudioRecord.setPreferredDevice。今天我把这个 API 的用法、调用时机、验证方法、常见坑全部拆开讲代码可以直接抄适配从老设备到 Android 16 的各种场景。1. 为什么录音会选错设备系统默认路由策略的局限1.1 系统默认录音设备选择的逻辑在聊 setPreferredDevice 之前先搞清楚一个前提默认情况下 AudioRecord 一创建系统就会自动给它分配一个录音设备。这个分配不是随机来的而是走了一套内部路由策略大致逻辑是“根据当前音频模式、设备连接状态、是否处于通话中、有没有被通信模块占用”等条件挑一个系统认为最合适的输入设备。这套策略设计初衷是好的比如插上耳机打电话就自动切到耳机麦克风拔掉耳机就切回内置麦克风。但对 App 开发者来说它不一定符合产品需求。最常见的一个反例你在做录音类 App用户戴着蓝牙耳机想用耳机上的麦克风录音但系统可能因为蓝牙 SCO 连接尚未稳定直接把输入路由到内置麦克风上。另一个反例是剧场录音场景用户外接了一个 USB 麦克风结果系统却选了机身麦克风导致录出来一堆环境噪音。这其实就是工程师常说的“策略正确业务不匹配”系统要的是不打断通话、尽量稳定而你的 App 要的是音质和指向性。1.2 必须手动指定录音设备的典型场景哪些场景必须手动指定设备我按实际需求频率给你列几个语音会议 App用户选择了蓝牙耳机作为接听和说话设备App 必须把录音输入也切到蓝牙 SCO不能靠系统默认。录音笔/播客应用支持外接 USB 麦克风插入后要自动切到 USB 输入否则录出来的音质没有意义。在线教育/直播互动用户有时插着有线耳机但希望用桌面麦克风收音这种业务规则只有业务代码才知道。手机端 AI 语音助手需要明确指定车机或外接麦克风避免在车内这种高噪声环境中选错输入源。在这些场景里如果还指望系统默认路由十有八九会翻车。而 AudioRecord.setPreferredDevice 就是官方提供给我们的“单实例音频输入路由指定”接口注意它是针对某个 AudioRecord 实例的不会影响其他 App 的录音设备这是它和通信模式切换方案最本质的差别。2. 搞懂 AudioRecord.setPreferredDevice方法签名与调用时机2.1 方法签名、参数和返回值setPreferredDevice 是在 API 23 加入 AudioRecord 的方法一直延用到现在Android 16 上也仍然是一等公民。它的完整签名是public boolean setPreferredDevice(AudioDeviceInfo deviceInfo)参数是一个 AudioDeviceInfo不是设备 ID也不是设备名这一点很容易搞错。很多新手拿到一个 int 型的 deviceId 就想往里传编译都过不去。要先把设备描述符 AudioDeviceInfo 拿到手再传给这个方法。返回值是 boolean表示系统是否成功接收了这个“偏好设备”请求。注意它只表示“接收”不代表“最终一定路由到这个设备”。要构造一个合法可输入的 AudioDeviceInfo思路是从 AudioManager 查询所有当前可用的输入设备然后按条件过滤。设备来源有三种GET_DEVICES_INPUTS输入设备、GET_DEVICES_OUTPUTS输出设备、GET_DEVICES_ALL全部。录音场景只关心输入设备。传入 null 表示清除偏好让系统重新按默认策略路由。这个细节在日常开发中非常有用比如你的页面切到了后台、用户拔出了外接设备你可以先把 preferred device 置空避免录音继续锁在一个已经失效的设备上。2.2 调用时机setPreferredDevice 必须在 startRecording 之前吗这是被问得最多的一个问题。官方文档没有把话说死但从实际工程经验看我强烈建议在 startRecording() 之前调用。原因是 AudioRecord 内部负责设备路由的 AudioFlinger 线程会在 start 阶段根据当前 priority 最高的设备约束去做路由如果你在 start 之后才调用 setPreferredDevice有相当一部分机型不会重新触发路由导致 API 返回 true 但录音数据仍然来自旧设备。如果确实在录音过程中需要切设备稳妥做法是先调用 stop()再 setPreferredDevice最后 start()。我实测过很多次这个顺序在各种国产 ROM 上都是可靠的。直接在录音中调 setPreferredDevice在原生 Android 和 Pixel 上偶尔能成功但在某些厂商系统上基本不生效所以不要赌。另外还有一个容易被忽视的点AudioRecord 必须在 setPreferredDevice 之前保证不被 start否则你设置的“首选设备”不会重建 routing 状态。换句话说这个偏爱设置是“下一次 start 时才生效”的语义居多而不是“立即切换”。3. 实战完整实现“优先选择蓝牙/USB/内置”录音设备切换3.1 权限声明与运行时检查写代码之前先过权限。录音本身需要 RECORD_AUDIO这个大家都懂uses-permission android:nameandroid.permission.RECORD_AUDIO /如果你要访问蓝牙设备列表Android 12API 31开始还必须声明 BLUETOOTH_CONNECT 权限并且在运行时申请。没有这个权限AudioManager.getDevices() 拿到的列表里根本不会出现蓝牙相关的 AudioDeviceInfo你连 TYPE_BLUETOOTH_SCO 都看不见这不是代码逻辑问题是权限问题。uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT /还有一点值得提醒Android 16 对麦克风隐私更敏感安装后首次录音系统会明确提示麦克风被哪个 App 使用。如果你在真机上调试发现 getDevices() 能返回设备但 startRecording() 不报错也不出数据先检查运行时权限是否真的 granted以及系统是否把麦克风隐私开关关了这个排查顺序最省时间。3.2 枚举输入设备并过滤目标类型核心代码第一步是获取音频管理器枚举所有输入设备val audioManager getSystemService(Context.AUDIO_SERVICE) as AudioManager val inputDevices audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)拿到的 List 里每个元素的 type 字段是判断设备类型的依据。常用的输入设备类型常量有这些类型常量含义典型设备TYPE_BUILTIN_MIC内置麦克风手机机身麦TYPE_WIRED_HEADSET有线耳麦带麦克风3.5mm 耳麦TYPE_WIRED_HEADPHONES有线耳机无麦克风某些耳机TYPE_BLUETOOTH_SCO蓝牙通话场景输入蓝牙耳机TYPE_USB_DEVICEUSB 音频设备USB 声卡、USB 麦克风TYPE_USB_HEADSETUSB 头戴式耳麦USB 耳机TYPE_TELEPHONY电话上行链路蜂窝通话TYPE_FM_TUNERFM 收音机输入极少见TYPE_REMOTE_SUBMIX远程子混音输入屏幕录制过滤时别只认死一个类型。比如 USB 设备某些厂商的 USB 麦克风报到 TYPE_USB_DEVICE某些报到 TYPE_USB_HEADSET建议把这两种都算进“外接 USB 输入”的候选里。同理有线麦克风也要同时看 TYPE_WIRED_HEADSET 和 TYPE_WIRED_HEADPHONES 两处因为有些带麦耳机在系统里注册成 HEADPHONES。3.3 核心代码从设备列表到 setPreferredDevice下面给一个带注释的完整实现思路是传入“设备优先级列表”例如你想优先 USB、其次蓝牙、最后内置函数会按顺序挑出第一个当前可用的输入设备并设置给 AudioRecordfun AudioRecord.trySetPreferredInput( context: Context, priorities: ListInt // 设备 type 优先级从高到低 ): Boolean { val am context.getSystemService(Context.AUDIO_SERVICE) as AudioManager val inputs am.getDevices(AudioManager.GET_DEVICES_INPUTS) // 优先类型中存在多个同类型设备时可以再按 id 或 name 细化 var target: AudioDeviceInfo? null for (type in priorities) { target inputs.firstOrNull { it.type type } if (target ! null) break } target ?: return false return setPreferredDevice(target) } // 用法示例优先 USB其次内置麦克风 val ok audioRecord.trySetPreferredInput(this, listOf( AudioDeviceInfo.TYPE_USB_DEVICE, AudioDeviceInfo.TYPE_USB_HEADSET, AudioDeviceInfo.TYPE_BUILTIN_MIC ))这段代码有几点要说明第一个匹配到的 type 就直接用了没有做更细的“同类型多设备”区分。如果你接了两个 USB 声卡那不是 type 能解决的需要用 device.id 或 device.productName 再筛一层。如果所有优先设备都不在线返回 false此时我建议 fallback 到“不设置”也就是保持系统默认路由而不是强行指定一个不在列表里的设备。setPreferredDevice 只接受 AudioDeviceInfo不接受“抽象的意图”所以枚举列表这步不能省。这里再给一个设备描述信息的辅助函数调试时直接打印能帮你快速定位列表里到底有什么fun dumpInputDevices(am: AudioManager) { val inputs am.getDevices(AudioManager.GET_DEVICES_INPUTS) for (dev in inputs) { Log.d(AudioDevice, id${dev.id} type${dev.type} name${dev.productName} isSource${dev.isSource}) } }把这个输出打出来你会发现真机上常见的情况是同一时间可能有好几个 TYPE_BUILTIN_MIC因为部分平板会有多个内置麦。type 过滤只是第一步后面还得处理“同型号多个”的情况特别是做阵列麦克风的老铁一定要把多麦逻辑考虑进去。4. 确认设备切换是否生效getRoutedDevice 与设备回调4.1 用 getRoutedDevice 验证实际路由结果setPreferredDevice 返回 true 并不代表最终路由一定切换成功。这一点我在很多项目里都踩过三星、小米、OPPO 的某些音频 HAL 实现对“首选设备”的处理是尽力而为并不强制。尤其是通话状态下蓝牙 SCO 和内置麦的切换经常被 Telephony 框架抢占。因此务必在 startRecording() 之后读取实际路由结果val actualDevice: AudioDeviceInfo? audioRecord.routedDevice // Kotlin 属性 // Java: AudioDeviceInfo actual audioRecord.getRoutedDevice(); if (actualDevice ! null actualDevice.type AudioDeviceInfo.TYPE_BLUETOOTH_SCO) { // 确实切到了蓝牙 } else { // 没生效根据业务决定是否重试或提示用户 }routedDevice 是当前 AudioRecord 实际使用的输入设备和 getDevices() 返回的列表是同一个类型体系。养成“set 后必查 routedDevice”的习惯能避免很多隐藏故障。4.2 监听音频设备变化来做热插拔兜底真实产品里用户不会老实等到你代码写完了再插拔麦克风。所以还需要监听设备变化。官方给的是 AudioManager.registerAudioDeviceCallbackval callback object : AudioDeviceCallback() { override fun onAudioDevicesAdded(addedDevices: Arrayout AudioDeviceInfo?) { super.onAudioDevicesAdded(addedDevices) // 设备插上了重新走一遍 setPreferredDevice reapplyPreferredDevice() } override fun onAudioDevicesRemoved(removedDevices: Arrayout AudioDeviceInfo?) { super.onAudioDevicesRemoved(removedDevices) // 设备拔掉后检查 routedDevice 是否已经自动切换 val current audioRecord?.routedDevice Log.d(AudioDevice, removed, now routed to ${current?.productName}) } } audioManager.registerAudioDeviceCallback(callback, Handler(Looper.getMainLooper()))这里有个实战经验不要在 onAudioDevicesAdded 回调里立刻 setPreferredDevice因为某些机型在 HAL 层报“设备添加”时音频驱动还没有完全 ready立即设置会失败。稳妥做法是 post 一个 200~300ms 的延迟后再设置。我甚至见过某些低端机需要延迟到 500ms 以上才认设备具体值最好在真机上调试一把。注册回调别忘了在 onDestroy 或 onStop 里注销audioManager.unregisterAudioDeviceCallback(callback)这步漏了会内存泄漏而且会引发一长串多余的路由重设逻辑属于典型低级错误。5. 常见问题与排障实录5.1 setPreferredDevice 返回 true 但录音设备没换遇到最多的情况就是“返回 true 但设备没换”。排查顺序我建议按这三步走第一步打印所有输入设备列表确认你要的目标设备确实 isSource 为 true。会出现 type 是 TYPE_WIRED_HEADPHONES 但 isSource 为 false 的情况这种设备只能输出不能作为输入来源set 了也白 set。第二步确认调用顺序。录音进行中直接调用会大概率无效这是厂商差异导致的老问题先 stop 再设置再 start。第三步看 routedDevice 到底是谁。如果 routedDevice 返回了系统默认设备说明 HAL 层没采纳你的首选设备此时需要走业务兜底——干脆不让用户选择而是提示“当前设备不可用”。5.2 蓝牙设备列表拿不到 / 地址变成 01:00:00:00:00:00没有 BLUETOOTH_CONNECT 权限时getDevices() 里不出现任何蓝牙设备这是最隐蔽的第一个坑。第二个坑是从 Android 12 开始出于隐私考虑AudioDeviceInfo.getAddress() 对蓝牙设备返回的不再是真实 MAC而是一个固定的占位地址 “01:00:00:00:00:00”。很多老代码依赖 getAddress() 去匹配具体蓝牙耳机型号到这里就挂了。解决办法是把“按地址匹配”改成“按 productName 匹配”或者维护一份用户在 UI 上选择的设备名到 AudioDeviceInfo 的映射关系。蓝牙连接状态本身用 BluetoothDevice 的 BondState 和 AudioDeviceCallback 一起判断不要再依赖 AudioDeviceInfo.getAddress()。5.3 不同 Android 版本的权限差异这个排障点特别容易漏我整理成表Android 版本权限要求注意点Android 6~11RECORD_AUDIOgetDevices() 可正常返回蓝牙设备Android 12RECORD_AUDIO BLUETOOTH_CONNECT蓝牙设备列表必须申请 BLUETOOTH_CONNECTAndroid 14麦克风隐私指示录音时状态栏有绿点/绿标调试期注意识别Android 16延续上述要求自带应用相机/语音类对麦克风使用更敏感别小看这些差异很多“换个真机就出问题”的 bug 其实是权限没有按版本适配。蓝牙权限还有一个附加规则只在运行时申请manifest 声明还不够必须在应用运行过程中弹窗授权否则 getDevices() 返回的蓝牙输入设备一直为空。5.4 常见问题速查表问题现象可能原因解决办法setPreferredDevice 编译报错把 int 型 deviceId 传进去了需要传入 AudioDeviceInfo 对象返回 true 但录音没切换调用时正在录音先 stopset 后再 start蓝牙耳机不在设备列表没申请 BLUETOOTH_CONNECT运行时申请权限USB 麦克风 type 不确定厂商适配差异同时考虑 TYPE_USB_DEVICE 和 TYPE_USB_HEADSET拔掉外接设备后录音异常没有监听设备移除回调registerAudioDeviceCallback 做兜底列表里有多个同类型设备同型号多麦/多声卡用 getAddress/productName 再筛一层6. AudioRecord.setPreferredDevice 与周边方案的横向对比6.1 与 AudioManager.setCommunicationDevice 的分工差异很多做语音会议 App 的朋友会问AudioManager.setCommunicationDevice 不也能指定输入设备吗和 setPreferredDevice 有什么区别区别在于作用域和适用模式。setCommunicationDevice 是全局通信设备设置它会把整个应用的音频路由切到指定的“通信设备”比如蓝牙 SCO、听筒、扬声器等它只对设置了 MODE_IN_COMMUNICATION 的音频流生效语义是“这个 App 走通信模式时用哪个设备”。而 AudioRecord.setPreferredDevice 是单实例级别的输入设备偏好它不依赖于通信模式普通 MediaRecorder 场景录出来的数据源也不会被通信框架抢占。从工程实践看通信类 App 里两者经常搭配使用setCommunicationDevice 管全局输入输出路由setPreferredDevice 管某个具体录音通道的输入设备。但如果你只是做一个普通录音工具不想处理 MODE_IN_COMMUNICATION 那套状态机直接用 setPreferredDevice 反而更简单、影响范围更小。6.2 与 AAudio 的 setDeviceId 如何取舍AAudio 是更高性能的低延迟音频库它也有 setDeviceId(int) 方法可以指定输入输出设备。如果你的项目对延迟极其敏感比如做乐器类 App、低延迟卡拉OK可以优先考虑 AAudio。它的问题是 API 更底层、错误处理更复杂而且在不同厂商驱动上的表现参差。AudioRecord 的优势是 Java/Kotlin 层 API 稳定错误码清晰对普通录音场景完全不虚。它和 AAudio 在设备选择上的语义也有一点差异AAudio 的 setDeviceId 是“直接用这个设备”更像强制路由AudioRecord 的 setPreferredDevice 是“优先选这个设备”系统在设备不可用或 HAL 不支持时可能退回到默认设备。所以我的选型建议是基础录音选 AudioRecord setPreferredDevice低延迟乐器场景选 AAudio setDeviceId。不建议在 AudioRecord 上强行追求 AAudio 的低延迟那是另一个方向。抛开 API 本身我最后想分享一个踩过的坑在首次进入录音页时别急着 setPreferredDevice先等 100ms 左右让系统完成音频设备枚举否则某些外接设备还没挂载到音频 HAL你设置就会落空。这个小延迟看着不优雅但能省掉整晚排查“为什么第一次设置总失败”的烦恼。如果你在多个真机上测过一定会认同这个做法。
返回列表