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计数持续增长,基本可以确认是供给问题。这时候我会检查:

  1. Buffer大小是否过小?尝试增大到256帧或512帧
  2. 是否有其他高优先级线程?用systrace抓一下调度情况
  3. 混音器负载?看看同时播放的音轨数量

避坑指南:我曾经遇到一个案子,卡顿只在游戏场景出现。查了两天才发现,是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的路由决策。流程大致是:

  1. 硬件检测到设备插拔(如耳机插入中断)
  2. AudioPolicyService收到通知,重新评估路由策略
  3. 通知AudioFlinger关闭旧设备、打开新设备
  4. 音频流重新路由到新设备

常见失败原因

  • 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 → 应用层。任何一个环节断了,录音就无声。

排查步骤(我自己的三板斧)

  1. 检查权限:Android 6.0以上需要动态申请RECORD_AUDIO权限。很多App崩溃或无声都是因为这个。
  2. 检查音频源:是MIC还是CAMCORDER?有些设备不同音频源的增益不同,甚至有的源根本没数据。
  3. 检查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 问题排查的通用方法论

说了这么多具体问题,我想分享一个通用的排查思路。说白了,音频问题再复杂,也逃不出这几个维度:

音频问题排查四步法:

  1. 确认现象:卡顿?爆音?无声?用最简场景复现(比如只用tinyplay/tinycap)
  2. 定位层级:应用层?Framework层?HAL层?驱动层?硬件层?逐层排除
  3. 抓取日志:logcat + dumpsys + systrace + dmesg,四件套缺一不可
  4. 对比验证:换一台正常设备对比,或者回退到上一个正常版本

我习惯在排查前先画一张图,把音频链路画出来,标出每个环节可能出问题的地方。这样思路清晰,不会漏掉。

音频问题排查链路图 应用层 Framework层 HAL层 驱动/硬件层 检查权限、音频源设置 AudioRecord参数 dumpsys media.audio_flinger logcat -s AudioFlinger tinyplay / tinycap 检查HAL库加载 dmesg | grep audio 检查设备节点权限 应用层问题 权限/参数/焦点 Framework层问题 Buffer/路由/混音 HAL层问题 设备打开/配置 驱动/硬件层问题 驱动加载/硬件故障 排查顺序:从上到下,逐层排除

这张图我每次排查问题都会在脑子里过一遍。从上到下,逐层排除,基本不会漏掉。


好了,今天聊了音频卡顿、爆音、设备切换失败、录音无声这四个典型问题。每个问题我都给出了根因分析和排查方法。说实话,音频问题排查是个经验活,遇到多了自然就有感觉了。但核心思路不变——理解链路、逐层排查、善用工具。

希望这些经验对你有帮助。下次遇到音频问题,别慌,按这个思路来,大概率能搞定。

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