ARTICLE DETAIL

资讯详情

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

Android 8.1多应用同时录音改造:AudioFlinger单读多分发实现

Android 8.1多应用同时录音改造:AudioFlinger单读多分发实现 先回答一个很多人会问的问题Android系统是不是天生就不允许两个App同时用麦克风答案是Google默认设计里确实把录音当成“独占资源”来管理但在Android 8.1这一代这种限制并不是藏在某个单一开关里而是由AudioPolicy、AudioFlinger、底层HAL三层代码共同“默契”形成的。我在做系统定制时为了解决一台设备上同时跑会议App和录音笔App的需求花了不少时间把这条链路捋清楚最后在AudioFlinger这一层做了一处集中改造才让多应用同时录音稳定落地。这篇东西适合正在做Android framework移植、系统定制、或者被“两个App不能同时录音”问题困住的开发同学。我会先把Android 8.1的录音链路彻底拆开说明限制到底卡在哪一层再给出我实测可用的修改思路和核心Patch最后把我踩过的坑全部列出来。1. 先说结论Android 8.1为什么只能一个App录音1.1 你遇到的“第二个App录音失败”是怎么出现的最典型的现场是这样App A正在用AudioRecord录音此时打开App B点击开始录音几秒后App B弹出一个“录音失败”或“麦克风被占用”的提示。更隐蔽的情况是两个App都不报错但先启动的App A突然数据断流或者后启动的App B录到的全是杂音。在Android 8.1上绝大多数的表现是后启动的App收到error code -38也就是INVALID_OPERATION。如果你抓logcat会在AudioRecord_JNI里看到Error -38 starting AudioRecord。很多刚开始做系统定制的人第一反应是去查AudioPolicyManager的占用判断但实际改完之后发现并没有用原因就是这条路线上还有别的地方在拦截。1.2 限制卡在哪一层从录音请求到麦克风数据的完整责任链要定位问题先得知道一次录音请求到底要经过哪些“关卡”。应用层调用AudioRecord.start()之后请求会通过Binder到达IAudioFlinger由AudioFlinger::openRecord处理。在真正创建录音轨道之前AudioPolicyManager::getInputForAttr会先检查当前音频输入源和采集设备决定是新建一个input流还是复用已有的input流。接着AudioFlinger会为该input创建或关联一个RecordThread再在这个线程上创建一个RecordTrack。当track启动后底层HAL的start_input_stream被调用之后就是HAL层持续把麦克风采到的PCM数据往上送。这里每一层都不像表面看起来那么简单。AudioPolicyManager如果发现已经存在相同inputSource和device的input默认会复用并refCount理论上并不会拒绝第二个App。那真正的拒绝逻辑在哪一部分在AudioFlinger::createRecordTrack_l里对采样率、格式、声道数等参数不匹配时的校验另一部分在HAL层的并发能力——大部分平台只有一个物理输入流HAL一旦发现input stream已经start就会直接返回错误。换句话说限制不是单一开关而是一道“组合拳”框架层没有为多track共享同一路PCM数据做设计HAL层也没有为同一个物理麦克风支持多个并发读取者。1.3 我踩过的“假方案”坑我最早踩过一个很典型的坑把AudioPolicyManager里关于input占用判断的逻辑放开以为这样第二个App就能正常录音。结果第二个App的AudioRecord确实能创建成功HAL也确实把同一个input stream给到两个track了但只要两个App同时读数据底层HAL就混乱声音变成一卡一卡的甚至直接把先启动的App踢下线。这个现象让我意识到只放开策略层不够必须让AudioFlinger从“每个track各读一次底层数据”变成“底层只读一次然后分发给所有活跃track”。这才是Android 8.1多应用同时录音改造的真正核心。2. 录音链路原理拆解AudioRecord、AudioFlinger与底层HAL怎么协作2.1 一次正常录音要经过哪些关卡我们先不着急改代码把Android 8.1录音链路彻底看明白后面动手的时候才不会拆东墙补西墙。一次录音从App到硬件主要经过四层AudioRecordJNI/native层负责封装客户端缓冲区把录音请求拆成start、read、stop三步。AudioFlinger系统音频服务管理所有录音线程和track。RecordThread是真正干活的地方它负责从HAL读PCM数据再拷贝到每个App对应的共享内存缓冲区。AudioPolicyManager策略层负责路由和权限判断决定某个录音请求该走哪条input通路。audio HAL直接和声卡驱动打交道read接口通常是一个阻塞式读取每次返回固定大小的PCM数据。其中最关键的是第2层。AudioFlinger内部是“一input对应一个RecordThread一个RecordThread对应多个RecordTrack”的结构。理论上一个线程本来就可以挂多个track但Android 8.1的默认代码在数据读取这块是循环遍历每个活跃track每个track各自从HAL读一次数据。这种设计在单track场景下没有任何问题一旦出现第二个track就会导致同一份底层数据被重复读取互相干扰。2.2 RecordTrack与RecordThread先弄懂这两个东西RecordTrack可以理解成“每个录音App在系统侧的代表”。它有自己的客户端缓冲区、采样率、声道数、格式等信息。RecordThread则是这些track共享的执行环境它内部维护一个mActiveTracks列表记录当前正在录音的track。正常流程是App启动录音后RecordTrack被加入mActiveTracksRecordThread的threadLoop开始循环调用process()。process()里遍历mActiveTracks逐个处理数据。App调用read()从共享内存拿数据时其实拿的是RecordTrack在process()里写好的那份拷贝。所以要想多应用同时录音本质上就是让一个RecordThread能同时为多个RecordTrack提供数据并且彼此不干扰。这在Android 10之后有官方思路雏形但在Android 8.1上需要自己动手把“单路读取”改成“并发分发”。2.3 默认的“单路独占”设计意图为什么Google默认要这么设计我在理解之后觉得原因主要有三点。第一硬件限制。绝大多数消费类设备的声卡输入通路只有一条ADC只有一个HAL显然没法在同一时刻为两个独立的读取者提供两份独立的硬件数据流。第二时序敏感。音频是强实时数据如果多个读取者在HAL层竞争同一路数据任何一方阻塞都会造成数据抖动音质劣化。第三控制策略。通话、录音、语音识别这些输入场景系统往往希望在高优先级场景下压制其他录音请求默认的独占机制是最简单的“互斥锁”。理解了这三点你就会明白真正长远的解法不是简单地把某个if判断删掉而是要让系统侧“主动承担分发职责”把一份底层数据复制多份分别喂给不同的App。3. 多应用同时录音的实现方案对比3.1 方案AHAL层开多路input依赖硬件思路是修改HAL和device tree让一个物理麦克风对外暴露多个input stream。比如高通平台可以在audio_policy_configuration.xml里配置多个input设备或者走USB声卡、多路ADC扩展。优点是不需要大改AudioFlinger每个App拿到的依然是一条独立通路数据隔离性最好。缺点也明显非常依赖硬件方案很多板子根本没有第二路ADC就算有多路同时采集还会引入时钟同步问题成本远高于软件方案。所以这个方案我只推荐给硬件本身就有多路输入能力的平台。3.2 方案BAudioFlinger单读多分发改动集中且通用核心思想是底层HAL只读取一份PCM数据RecordThread在process()中读取一次后再按每个活跃track的缓冲区需求进行拷贝分发。优点很多不动硬件不依赖额外input设备不需要厂商HAL大幅改动所有具备录音能力的Android 8.1设备理论上都能用。缺点是改动集中在系统音频核心代码里对稳定性要求高必须做好buffer管理和线程同步。这也是我最终采用的方案后面会完整展开。3.3 方案C上层录音代理架构重但扩展性强这个方案是做一个系统级录音代理App或服务由它独占麦克风其他App通过Binder向代理请求录音数据代理做集中采集和分发。优点是业务层好扩展比如可以方便地加混音、降噪、转码等能力。缺点是架构重所有录音App都得适配新接口无法做到“对上层透明”。对大多数系统定制项目来说这种改动面太大只适合做上层产品功能不适合解决系统兼容性问题。3.4 我的选型结论如果只是要在Android 8.1设备上实现“两个App同时录音”方案B是投入产出比最高的。它把所有改动收敛在AudioFlinger内部上层App完全无感也不挑硬件平台。下面我分享的修改实录就是按方案B走的。4. Android 8.1具体修改实录AudioFlinger单读多分发改造4.1 改造前先摸清RecordThread::process的现有流程在动手改代码前建议先把你手上的Android 8.1源码打开定位到frameworks/av/services/audioflinger/Threads.cpp中的RecordThread::process()函数。虽然不同厂商的代码会有差异但AOSP主线的整体骨架是一样的。简化后的现有流程大致是这样的void AudioFlinger::RecordThread::process() { // 1. 从mActiveTracks拷贝活跃track列表 Vector spRecordTrack activeTracks; size_t activeCount 0; { Mutex::Autolock _l(mLock); activeTracks mActiveTracks; activeCount activeTracks.size(); } if (activeCount 0) { standby(); // 没有活跃track进入待机 return; } // 2. 对每个活跃track逐次读取底层数据 for (size_t i 0; i activeCount; i) { RecordTrack* track activeTracks[i].get(); // 这里会根据track的参数和buffer状态计算需要读取的帧数 // 然后调用 mInput-read() 从HAL取数据 // 再拷贝到track自己的客户端buffer } }问题就出在第二步。默认实现里每个track都是独立从mInput-read()拿数据。我们可以把mInput想象成一个水龙头每个App都拿自己的杯子去接水水龙头只有一根水管你接一茬我又接一茬数据自然就乱了。4.2 核心Patch把“每个track各读一次”改成“一次读取、多次分发”改造的核心是调整process()的处理顺序先统一算好本轮需要从底层读取的总帧数mInput-read()只调用一次把数据放到一个中间buffer里然后再遍历所有活跃track把这份数据拷贝到各自的缓冲区。这个Patch我在项目里实际改过按Android 8.1的代码结构写出来大致如下不同厂商代码需要对照调整void AudioFlinger::RecordThread::process() { Vector spRecordTrack activeTracks; size_t activeCount 0; { Mutex::Autolock _l(mLock); activeTracks mActiveTracks; activeCount activeTracks.size(); } if (activeCount 0) { standby(); return; } // 第一步统计本轮所有活跃track中最大的读取需求 size_t maxFrames 0; for (size_t i 0; i activeCount; i) { RecordTrack* track activeTracks[i].get(); size_t frames track-framesToReadFromProvider(); if (frames maxFrames) { maxFrames frames; } } if (maxFrames 0) { return; } // 第二步从底层只读一次数据放到线程级共享buffer // mRecordBuffer是RecordThread内部预留的一块临时空间 // 大小要覆盖最大帧数避免溢出 mInput-read(mRecordBuffer, maxFrames); // 第三步遍历所有track把同一份数据按需拷贝分发 for (size_t i 0; i activeCount; i) { RecordTrack* track activeTracks[i].get(); size_t framesForTrack track-framesToReadFromProvider(); if (framesForTrack 0) { track-copyDataFromBuffer(mRecordBuffer, framesForTrack); } } }这只是一个参考骨架。实际工程里framesToReadFromProvider和copyDataFromBuffer需要根据你的需求封装AOSP里并没有现成的同名函数要做一层适配。但思路是固定的读一次、分发多次。另外要注意mRecordBuffer必须提前分配好建议按照maxFrames * channels * sizeof(int16_t)来算大小如果采样率是48kHz、双声道、16bit那1秒数据大概是192KB临时缓冲区建议不小于这个量级。分配时机可以放在RecordThread创建时避免每次process()都做堆内存分配否则录制过程中会出现周期性卡顿。4.3 生命周期处理start/stop与引用计数改完数据分发之后另一个容易出问题的点是录音生命周期。在Android 8.1的AudioFlinger中底层HAL的start_input_stream和stop_input_stream是按RecordThread维度来调用的而不是按track维度。默认逻辑是RecordThread从standby状态切到运行状态时会调用HAL start所有track都stop之后再进入standby调用HAL stop。这个机制在多track场景下反而比较合理基本不需要大改但要确认一点当第二个track start时不能把RecordThread再重新start一次否则HAL第二次调用start_input_stream大概率会报错。AOSP默认是mActiveTracks.size()从0变1时才启动底层流只要你不去乱改这个判断就不会踩这个坑。但这里有一个隐蔽的坑是track stop。第一次改造时我没注意App A停止录音后底层HAL立刻被stop导致还在录音的App B瞬间没有数据。原因就是RecordTrack::stop()内部或者RecordThread的standby判断里对“是否还有活跃track”的计算不严谨。要保证process()里只要mActiveTracks非空就不能让底层流进入standby。建议在RecordThread::standby()里加一层保护bool AudioFlinger::RecordThread::checkStandby() { Mutex::Autolock _l(mLock); return mActiveTracks.size() 0; }这样每次决定是否standby之前都先看一下全局track数量避免误杀还在录音的track。4.4 还可能要动的厂商HALFramework层改完之后还有一个绕不开的环节是厂商HAL。很多HAL在start_input_stream里会加独占判断检查当前input是否已经被start如果是就返回-EBUSY。这种HAL一定得改否则还是会复现“第二个App启动失败”。改法一般是维护一个引用计数记录当前有多少上层使用者start_input_stream由0变1时才真正打开设备stop_input_stream由1变0时才真正关闭设备中间的重复start/stop调用直接返回成功就行。如果一个HAL的read接口在已经被读取的同时又被另一个线程调用会直接返回错误或者卡死那还需要检查HAL内部是否对read做了串行保护。框架层的“单读多分发”改造实际上已经规避了这类问题因为现在无论如何底层同一时刻只有一个reader在调用read。这也是我坚持改AudioFlinger而不是只改HAL的原因只有控制住读取入口才能真正避免HAL并发混乱。5. 编译、刷机与验证怎么确认改造生效5.1 编译audioserver与替换验证改动涉及AudioFlinger也就是系统进程audioserver。在Android 8.1的源码根目录执行source build/envsetup.sh lunch your_device-userdebug m -j16 audioserver编译产物是/system/bin/audioserver另外有改动过的库都需要一起编出来比如libaudioflinger.so等。建议在整机编译时直接make相关的模块然后用fastboot或adb push方式更新到设备。如果是userdebug版本最省事的方式是remount后用adb push替换然后重启audioserver进程adb root adb remount adb push /your_out_path/audioserver /system/bin/audioserver adb push /your_out_path/libaudioflinger.so /system/lib/ adb shell killall audioserver重启后记得立刻抓logcat确认audioserver没有crashadb logcat -b all | grep -i audioflinger\|audioserver\|assert\|FATAL5.2 两个App并发录音的验证清单建议写一个极简的测试App不要在真机复杂的App环境里做第一步验证。测试App就两个按钮一个负责启动AudioRecord另一个负责读取并写入WAV文件允许同时开启两个录音实例。验证时按下面清单逐项测先启动App A录音等待3秒再启动App B录音看B是否报错。两个App同时录音10秒分别生成WAV文件检查文件时长是否一致。A停止后B是不是还能继续正常录音不卡顿。用logcat搜索RecordThread、RecordTrack的关键log确认mActiveTracks.size()在并发时为2。我实际验证时还额外做了一步在process()里临时加了一行log打印activeCount和每次分发后的track buffer状态。这个log在正式版本去掉就好但调测阶段非常有用能直观看到数据到底有没有分发给两个track。5.3 数据同步与音质验证并发录音最容易出现的坑是两路数据不同步具体表现是两个WAV文件播放出来节奏上差了那么几十毫秒或者干脆一个快一个慢。如果发现明显不同步优先检查AudioRecord创建参数的bufferSize是不是一样。两个App如果申请的buffer大小差异太大会导致AudioFlinger分发的帧数无法对齐。一个临时解决办法是让两路AudioRecord使用完全相同的采样率、声道数、采样格式和bufferSize这样分发逻辑最简单。如果想要支持不同参数并发录音就得在RecordThread里按track分别做重采样和格式转换这个工作量要重新评估。音质方面最直接的验证方式是录一段固定频率的音频比如1kHz正弦波转成WAV后用音频编辑软件看波形是否连续、有无爆音。如果出现了周期性杂音大概率是mRecordBuffer不够大或者分发时拷贝越界建议第一步先加大共享buffer并检查边界。6. 常见问题与排查实录6.1 并发时第一个App掉线这个现象通常发生在第二个App启动录音触发HAL重建的时候。检查一下RecordThread是不是在第二个track加入时被重新start了如果是说明你在switch状态逻辑里漏掉了一个“已经active就不重复启动底层流”的判断。还有一种可能是App A本身设置了OnRecordPositionUpdateListener在短时间数据中断后主动报错。这种属于客户端容错可以在验证代码里适当放宽超时阈值排除干扰。6.2 并发后声音卡顿、杂音卡顿优先检查共享buffer的分配大小和分发逻辑。一个很典型的错误是我用最大活跃track的帧数去读了底层数据但分发时又按每个track自己的mFrameCount去拷贝结果有的track拷多了把下一个track的数据盖掉了。分发时一定要以每个track实际请求的帧数为准不要统一按最大帧数分发。杂音问题还要检查HAL pause/resume是否在并发时被意外调用。某些HAL在input stream被反复start/stop时会在ADC端产生pop音这个可以在HAL代码层面抑制或者把HAL的start/stop次数降到最低。6.3 一个App停止后另一个App没声音还是生命周期问题。我项目里第一次调试就踩了这个坑App A stop后process()里的activeCount从2变1逻辑上应该继续跑但底层HAL却被stop了导致App B拿不到数据。解决办法前面说过在RecordThread决定standby之前必须检查全局mActiveTracks的size而不是某个track自己的状态。修改后要重点回归“A停B不停”“B停A不停”这两个子场景。6.4 无法忽视的录音权限最后加一个容易被忽略的点Android 8.1虽然还没有Android 10之后的麦克风指示灯这类隐私机制但运行时权限还是要确保两个App都拿到了RECORD_AUDIO。如果App B没有授权应用层根本不会发起录音请求框架层改得再完美也没用。我遇到过一种比较隐蔽的情况App A有录音权限且正在使用麦克风App B在Android 8.1的权限弹窗被用户授权后依然因为某些第三方ROM的“麦克风占用保护”被拦截。这种是ROM的杂音策略不是AOSP逻辑排查时可以先把系统设置里“麦克风权限使用提示”之类的开关关掉再做对比测试。从整个改造过程来看Android 8.1做多应用同时录音并不是一个“删一行代码”就能解决的简单需求。核心价值在于理解AudioFlinger的读取模型——只要把“每个track各读一次”改成“一次读取、多次分发”同时管好底层HAL的start/stop生命周期这套方案就能在不大改硬件和上层App的前提下落地。最后再提醒一句分段分发涉及线程和内存改动之后一定要反复跑并发压力测试音频这类实时性要求高的问题最容易在长时间运行后暴露隐患。
返回列表