記一次Android系統下解決音頻UnderRun問題的過程
【前言】
因為這幾天在為設備從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)的原因與解決辦法》