记一次Android系统下解决音频UnderRun问题的过程
来源:互联网 发布:网络语35是什么意思 编辑:程序博客网 时间:2024/05/22 16:49
【前言】
因为这几天在为设备从 Android M 升级到 Android N 的 bringup 做准备,所以一直没写博客。趁现在刚刚把 Kernel 部分的移植做完,忙里偷闲把 2 周前解决的一个音频 UnderRun 问题记录一下,留作以后参考。
问题现象是:使用腾讯视频 APP 播放视频,一段时间后会出现 pop-click 噪音,听起来类似“哒哒”一样的声音。
【排查问题】
看到这个问题的现象后,我的第一猜测就是设备出现了 UnderRun。
大胆假设后还需小心求证。于是我在代码中开启 Verbose Log打印并重新编译系统镜像烧写到设备上。然后复现问题,并查看出现问题的时间点附近的 Log 消息。日志文件中出现了大量如下记录:
12-08 10:09:15.565 2825 3426 V AudioFlinger: track(0xea5dda80) underrun, framesReady(1024) < framesDesired(1026)12-08 10:09:15.599 2825 3426 V AudioFlinger: mixer(0xeb440000) throttle begin: ret(4096) deltaMs(0) requires sleep 10 ms12-08 10:09:15.625 2825 3426 D AudioFlinger: mixer(0xeb440000) throttle end: throttle time(20)
果然这是个 UnderRun 问题,Log 中的信息证实了猜想:音频播放需要 1026 帧数据,但 APP 只准备好了 1024 帧。但我们也知道,Android系统对于 underrun 出现后是有一套默认的处理流程来消除问题的,也就是紧接着的 2 条和 throttle 相关的 Log 所对应的操作。
简单介绍一下 Android 系统默认处理 underrun 问题的流程:当检测到当前写入音频数据的时间与上次出现警告的时间间隔大于预定的最大时间间隔(5纳秒)后,系统将判定音频播放过程出现了 underrun。然后系统会调用 usleep() 函数对当前 PlaybackThread 进行短时间阻塞,这样上层 APP 就能为 PlaybackThread 准备好更多音频数据。这个 usleep() 的时长是根据相邻 2 次写入音频数据的时间间隔实时计算出的。
相应的代码可以在 frameworks/av/sevices/audioflinger/Threads.cpp 中的 AudioFlinger::PlaybackThread::threadLoop() 函数中找到:
bool AudioFlinger::PlaybackThread::threadLoop(){ ...... if (mType == MIXER && !mStandby) { // write blocked detection nsecs_t now = systemTime(); nsecs_t delta = now - mLastWriteTime; // 相邻 2 次写入音频数据操作的时间间隔 if (delta > maxPeriod) { mNumDelayedWrites++; if ((now - lastWarning) > kWarningThrottleNs) { // 如果本次写入数据时间与上次警告出现时间间隔大于kWarningThrottleNs(5纳秒)则判断出现underrun ATRACE_NAME("underrun"); ALOGW("write blocked for %llu msecs, %d delayed writes, thread %p", ns2ms(delta), mNumDelayedWrites, this); lastWarning = now; } } if (mThreadThrottle && mMixerStatus == MIXER_TRACKS_READY // we are mixing (active tracks) && ret > 0) { // we wrote something // The throttle smooths out sudden large data drains from the device, // e.g. when it comes out of standby, which often causes problems with // (1) mixer threads without a fast mixer (which has its own warm-up) // (2) minimum buffer sized tracks (even if the track is full, // the app won't fill fast enough to handle the sudden draw). const int32_t deltaMs = delta / 1000000; const int32_t throttleMs = mHalfBufferMs - deltaMs; if ((signed)mHalfBufferMs >= throttleMs && throttleMs > 0) { //usleep(throttleMs * 1000); // 通过usleep()短时间阻塞当前PlaybackThread,让app可以准备更多的数据 usleep((throttleMs + 3) * 1000); /* 增加 3ms 的延时时间 * 修复腾讯视频APP播放视频有噪声的问题 20161216 */ // notify of throttle start on verbose log ALOGV_IF(mThreadThrottleEndMs == mThreadThrottleTimeMs, "mixer(%p) throttle begin:" " ret(%zd) deltaMs(%d) requires sleep %d ms", this, ret, deltaMs, throttleMs); mThreadThrottleTimeMs += throttleMs; } else { uint32_t diff = mThreadThrottleTimeMs - mThreadThrottleEndMs; if (diff > 0) { // notify of throttle end on debug log ALOGD("mixer(%p) throttle end: throttle time(%u)", this, diff); mThreadThrottleEndMs = mThreadThrottleTimeMs; } } } } ......}
【解决问题】
前文贴出的 Log 表明,Android 系统已经检测到了 UnderRun 问题并进行了延时处理来让 APP 准备更多的音频数据。可是我们在使用腾讯视频 APP 时依然会继续发生 UnderRun 的问题,原因在于代码中计算出的延时时间对腾讯视频 APP 来说还是太短。在 Log 中我们可以看到需要的数据量为 1026 帧但实际准备好的数据为 1024 帧,所以我们可以稍微增加 usleep() 的延时时间来为 PlaybackThread 准备足够的数据。经过试验,我决定在原有延时时间上增加 3 毫秒。
重编系统镜像后烧入设备进行验证,问题得到解决。
【扩展阅读】
[1] 《音频出现Xrun(underrun或overrun)的原因与解决办法》
- 记一次Android系统下解决音频UnderRun问题的过程
- 记一次Android系统下解决音频UnderRun问题的过程
- 记一次Android系统下解决音频UnderRun问题的过程
- Android系统下解决音频underrun噪声问题的一种更优方法
- 一次繁杂问题的解决过程
- 记一次Android的音频卡断
- 记一次PermGen持续增长的解决过程
- 记一次PermGen持续增长的解决过程
- 一次内存泄漏问题的发现与解决过程
- 记录一次自定义参数绑定错误问题的解决过程
- MDSS underrun问题调试
- 记一次处理日志文件过大问题的解决过程(SQL Server)
- 记一次KeyCenter中遇到Invalid signature format问题的解决过程
- 记一次线上问题的排查过程
- Android音频开发过程中遇到的问题
- 记一次bug解决过程
- netbeans下 解决glassFish只能启动一次的问题
- 音频出现Xrun(underrun或overrun)的原因与解决办法
- ionic 隐藏ion-tab
- IntelliJ IDEA14.0.3+Maven+SpringMVC+Spring+Hibernate光速构建Java权限管理系统(三)
- android关机重启流程代码
- 设计模式--[6]命令模式
- 关于吞吐量和延迟的精彩比喻
- 记一次Android系统下解决音频UnderRun问题的过程
- jssdk重写
- python
- python复杂网络库networkx:算法
- C语言编译器
- Android系统中的进程管理
- Python 标准异常总结
- python import 迷宫
- select取数据库值设为默认值