27、常见问题与解决方案:音频卡顿、杂音/爆音、设备切换失败、录音无声等典型问题分析
做音频系统这么多年,我遇到过最崩溃的一次——客户在发布会现场演示产品,结果播放音乐时突然爆音,全场都听到了那声刺耳的"啪"。嗯,从那以后,我对音频问题的敬畏心就上来了。
今天咱们就聊聊AudioFlinger开发中那些最让人头疼的典型问题。说白了,这些问题你迟早会遇到,早了解早避坑。
27.1 音频卡顿(Stutter / Underrun)
音频卡顿,我习惯叫它"打嗝"。声音断断续续,像老唱片跳针一样。为什么会这样?
核心原因:音频数据供给跟不上消费速度,导致缓冲区空了(underrun)。
常见根因分析
- Buffer配置不合理:缓冲区太小,CPU稍微忙一下就跟不上了
- 高优先级线程抢占:比如GPU渲染、网络IO把AudioFlinger的线程饿死了
- 混音负载过高:同时播放太多音轨,混音器算不过来
- 底层驱动延迟抖动:ALSA或HAL层回调时间不稳定
排查思路
我个人习惯先看dumpsys。你想想看,AudioFlinger内部有详细的统计信息:
adb shell dumpsys media.audio_flinger
// 重点关注:
// 1. Track的underrun计数
// 2. 每个MixerThread的帧率是否稳定
// 3. 是否有大量pending数据
如果underrun计数持续增长,基本可以确认是供给问题。这时候我会检查:
- Buffer大小是否过小?尝试增大到256帧或512帧
- 是否有其他高优先级线程?用systrace抓一下调度情况
- 混音器负载?看看同时播放的音轨数量
避坑指南:我曾经遇到一个案子,卡顿只在游戏场景出现。查了两天才发现,是GPU驱动把CPU核心频率锁低了。音频线程跑在低频核心上,buffer就填不满。解决方案是把AudioFlinger线程绑定到大核上。
27.2 杂音/爆音(Pop / Click / Noise)
爆音这个问题,说大不大,说小不小。但用户体验极差——你想想,正听歌呢突然"啪"一声,多闹心。
爆音产生的三大场景
| 场景 | 原因 | 典型表现 |
|---|---|---|
| 播放开始/停止 | 音频数据从0突变到非0值,产生直流偏移 | 开头或结尾"啪"一声 |
| 音量突变 | 音量系数瞬间变化,波形不连续 | 调节音量时"咔咔"响 |
| 设备切换 | 路由切换时,音频流被截断或重复 | 插拔耳机时爆音 |
解决方案
说白了,爆音的本质是波形不连续。解决思路就是让变化"平滑":
- 淡入淡出(Fade In/Out):播放开始前几帧做渐变,从0慢慢升到目标音量
- 音量渐变(Volume Ramp):音量变化时,用线性或指数插值,不要一步到位
- 静音填充(Zero Padding):设备切换时,先输出几毫秒的静音数据
// AudioFlinger中淡入淡出的实现示例
void AudioFlinger::applyFadeIn(audio_buffer_t* buffer, int fadeFrames) {
for (int i = 0; i < fadeFrames && i < buffer->frameCount; i++) {
float gain = (float)i / fadeFrames; // 线性渐变
for (int ch = 0; ch < buffer->channelCount; ch++) {
buffer->data[i * buffer->channelCount + ch] *= gain;
}
}
}
注意:淡入淡出的帧数不能太少,否则听不出来效果。我一般建议至少16帧(约0.3ms @48kHz)。但也不能太多,否则感觉声音"慢半拍"。
27.3 设备切换失败
这个场景我太熟了——插上耳机没声音,拔掉耳机外放不响,蓝牙连上了但音频还在手机喇叭里。用户骂娘,产品经理找你。
切换流程回顾
设备切换的核心是AudioPolicyService的路由决策。流程大致是:
- 硬件检测到设备插拔(如耳机插入中断)
- AudioPolicyService收到通知,重新评估路由策略
- 通知AudioFlinger关闭旧设备、打开新设备
- 音频流重新路由到新设备
常见失败原因
- HAL层设备打开失败:驱动没准备好,或者设备被占用
- 路由策略配置错误:audio_policy_configuration.xml中设备映射不对
- 音频焦点冲突:某个App占着焦点不放,导致路由被阻塞
- 时序问题:切换过程中,旧设备已经关闭,新设备还没打开,出现"空窗期"
排查方法
我一般先看logcat,重点关注这几个tag:
adb logcat -s AudioPolicyManager AudioFlinger AudioHardware
// 关键日志:
// "setDeviceConnectionState" - 设备连接状态变化
// "getOutputForDevice" - 路由决策
// "openOutput" / "closeOutput" - 设备打开/关闭
如果看到"openOutput failed",那基本是HAL层的问题。我会检查:
- 驱动是否加载成功?
cat /proc/asound/cards - 设备节点权限是否正确?
ls -l /dev/snd/ - HAL库是否匹配?
dlopen一下看看
避坑指南:我曾经遇到一个奇葩问题——蓝牙耳机连上后,音乐播放正常,但通话没声音。查了三天,发现是audio_policy_configuration.xml中,蓝牙SCO的路由配置写错了,把"voice_call"映射到了"speaker"。改一行配置就好了。所以,XML配置一定要仔细核对。
27.4 录音无声
录音无声这个问题,比播放问题更隐蔽。因为播放没声音你马上能发现,录音没声音可能要等到回放才知道。
录音链路回顾
录音的路径是:麦克风 → 驱动 → HAL → AudioFlinger的RecordThread → 应用层。任何一个环节断了,录音就无声。
排查步骤(我自己的三板斧)
- 检查权限:Android 6.0以上需要动态申请RECORD_AUDIO权限。很多App崩溃或无声都是因为这个。
- 检查音频源:是MIC还是CAMCORDER?有些设备不同音频源的增益不同,甚至有的源根本没数据。
- 检查HAL层:用tinycap直接录一下,看底层是否有数据。
// 用tinycap验证底层录音是否正常
adb shell tinycap /sdcard/test.wav -D 0 -d 0 -r 48000 -c 2 -b 16 -t 5
// 如果tinycap能录到声音,说明问题在AudioFlinger或上层
// 如果tinycap也无声,那问题在驱动或硬件
常见根因
| 问题层 | 具体原因 | 排查方法 |
|---|---|---|
| 应用层 | 未申请权限、音频源设置错误 | 检查Manifest和运行时权限 |
| Framework层 | AudioRecord参数不匹配、Buffer太小 | 检查sampleRate、channelMask、format |
| HAL/驱动层 | 麦克风未使能、增益为0、DMA通道异常 | tinycap + dmesg |
| 硬件层 | 麦克风损坏、接触不良 | 换硬件测试 |
注意:录音无声还有一种隐蔽情况——录音数据全是0,但系统认为正常。这是因为麦克风物理损坏或偏置电压没加上。我遇到过一例,客户说录音无声,查了三天软件,最后发现是麦克风焊盘虚焊。所以,硬件问题也要纳入排查范围。
27.5 问题排查的通用方法论
说了这么多具体问题,我想分享一个通用的排查思路。说白了,音频问题再复杂,也逃不出这几个维度:
音频问题排查四步法:
- 确认现象:卡顿?爆音?无声?用最简场景复现(比如只用tinyplay/tinycap)
- 定位层级:应用层?Framework层?HAL层?驱动层?硬件层?逐层排除
- 抓取日志:logcat + dumpsys + systrace + dmesg,四件套缺一不可
- 对比验证:换一台正常设备对比,或者回退到上一个正常版本
我习惯在排查前先画一张图,把音频链路画出来,标出每个环节可能出问题的地方。这样思路清晰,不会漏掉。
这张图我每次排查问题都会在脑子里过一遍。从上到下,逐层排除,基本不会漏掉。
好了,今天聊了音频卡顿、爆音、设备切换失败、录音无声这四个典型问题。每个问题我都给出了根因分析和排查方法。说实话,音频问题排查是个经验活,遇到多了自然就有感觉了。但核心思路不变——理解链路、逐层排查、善用工具。
希望这些经验对你有帮助。下次遇到音频问题,别慌,按这个思路来,大概率能搞定。