Android audio音频流数据异常问题分析

avatar
SugarTurboS Club @SugarTurboS

一、背景

Android 系统的开发过程当中,音频异常问题通常有如下几类,无声,调节不了声音,爆音,声音卡顿,声音效果异常(忽大忽小,低音缺失等)等。尤其声音效果这部分问题通常从日志上信息量较少,相对难定位根因。想要分析此类问题,便需要对声音传输链路有一定的了解,能够在链路中对各节点的音频流进行采集,通过对比分析音频流的实际效果来缩小问题范围,找出原因。 网上已经有很多音频框架图和相关的大致介绍,这里就不再赘述,只分享下音频流的传输链路,和我们可以重点其中的哪些关键节点,来帮助我们快速定位问题。

二、Android Audio 音频系统

1. 音频链路

抓取音频链路当中的音频数据是分析声音异常问题的有效方法,通过抓取不同节点的声音数据,可以帮助我们快速定位问题发生的原因。下面先来看一张安卓官方的音频系统框架图:

一、背景

Android 系统的开发过程当中,音频异常问题通常有如下几类,无声,调节不了声音,爆音,声音卡顿,声音效果异常(忽大忽小,低音缺失等)等。尤其声音效果这部分问题通常从日志上信息量较少,相对难定位根因。想要分析此类问题,便需要对声音传输链路有一定的了解,能够在链路中对各节点的音频流进行采集,通过对比分析音频流的实际效果来缩小问题范围,找出原因。 网上已经有很多音频框架图和相关的大致介绍,这里就不再赘述,只分享下音频流的传输链路,和我们可以重点其中的哪些关键节点,来帮助我们快速定位问题。

二、Android Audio 音频系统

1. 音频链路

抓取音频链路当中的音频数据是分析声音异常问题的有效方法,通过抓取不同节点的声音数据,可以帮助我们快速定位问题发生的原因。下面先来看一张安卓官方的音频系统框架图: 0.005701938452606489.png Audio 音频数据流整体上经过 APPframeworkhalkernel driver四个部分,从应用端发起,不管调用 audio 还是 media 接口,最终还是会由 AudioTrack 将数据往下传,经由 AudioFlinger 启动 MixThreadDirectThread 等,将多个 APP 的声音混合到一起,将声音传输到 hal 层。系统会根据音频流类型 stream 和音频策略 strategy 来选择对应的 output,从而找到对应的 module,将音频数据传输给 hal 层音频库 so 做声音相关的处理并传给 audio driver。 音频流传输路径图: a811ebf2-3b9f-4825-a5a1-22343fafa943.png 从上述的音频流流程可以看到,我们首先要确认,当前音频流是经由哪一个 hal 层库做处理,是 primaryusb 还是三方 so 等,然后可以在对应的节点抓取相应的音频信息分析。可以根据自己的需要在音频流的部分节点埋下相应的 dump 指令,将 pcm 写入到相应的节点当中。

2. 音频链路关键节点:

  AudioTrack:应用写入音频流的起点,有 MODE_STATICMODE_STREAM 模式,通过 write 接口将数据写入。此节点写入的数据是由应用层最原始的音频数据。   AudioFlinger:负责启动线程完成各个应用的混音,音频流声音调节等工作。设备同时可能存在多个应用播放声音,这时便需要将各个应用的声音混合在一起,并且做音量的调节。例如在车载场景中,音乐应用播放歌曲和地图应用语音导航的声音需要同时存在,便使用到了混音的功能,当导航语音响起时,歌曲声音有一个明显的变小,便可以设置音频流的音量。   audio_hw_halhal 层音频处理的入口,为 Android 原生逻辑,各厂家需要按照规范实现其中的音频设置等接口,声明 HAL_MODULE_INFO_SYM 结构体,实现 legacy_adev_open 方法,承接起连接 frameworkaudio driver 的作用,完成一些音效算法等逻辑处理。   AudioStreamOut:和 audio_hw_hal 一样,是Android 给厂家提供的通用类,厂家在实现自己的通用库实现时需要可以按照谷歌规范,然后在相应的音频处理接口中实现自己的对音频流做音效,增益等处理。 audio_hw_hal.cpp 代码如下,不同厂家这里的实现略有差异,这里只截取部分 AOSP 源码。 551bc872-290d-44c5-a4ce-0522d6e77c29.png 37b7d737-ef11-476a-b4d8-ed90a455cf41.png

3. 音频库的选择

从音频流传输路径图可以看到,如何找到是哪一个音频 so 处理声音也是至关重要的。我们知道,系统对于应用层曝光的其实只有通道类型。举个例子:当用户打电话时,可以使用通话通道 STREAM_VOICE_CALL,当用户播放视频时,可以使用媒体通道 STREAM_MUSIC,当发送通知时,可以使用 STREAM_NOTIFICATION。那传入这些通道的声音数据,又是怎么最终流向到具体的硬件输出设备呢? c860785d-d93f-487c-aed9-2f8509b063c6.png 以媒体通道为例,当应用层将音频数据往 MUSIC 通道写入时,系统便会根据 StreamType 来生成相应的 audio_attributes_t.usage = AUDIO_USAGE_MEDIA, .content_type = AUDIO_CONTENT_TYPE_MUSIC}),再通过 audio_attributes_t 来获取对应的 ProductStrategySTRATEGY_MEDIA),最后在拿到对应的 outputDeviceAndroid 原生逻辑outputDevice 的选择在 Engine.cpp 上,会具体根据当前设备是否有接蓝牙,耳机等外设,按照优先级来选择相应的外设设备作为输出,可能是耳机 (AUDIO_DEVICE_OUT_WIRED_HEADSET),听筒(AUDIO_DEVICE_OUT_EARPIECE),喇叭(AUDIO_DEVICE_OUT_SPEAKER)等。具体可以看文件 www.androidos.net.cn/android/9.0…getDeviceForStrategyInt 方法。 通过以上分析,我们知道了音频会流向哪个输出设备,那么下一个问题来了,是由谁负责传输和对音频数据做最后的处理呢? 这里就需要看音频设备的策略文件,还是以媒体通道为例,假设设备没有接任何外接设备,选择的 outputDeviceAUDIO_DEVICE_OUT_SPEAKER。接下来就要看哪个 output so 支持 AUDIO_DEVICE_OUT_SPEAKER,符合度最高的 output so 将会负责数据传输,最终经由 tinyalsa 写入到 pcm 节点中。不同的 Android 版本在配置文件上会有些许差异,可能放置在 *audio_policy_configuration.xml 中,有些在 audio_policy.conf 中。

三、案例分析

1. 声音忽大忽小问题

具体分析

有用户反馈使用优酷视频播放视频时,概率性出现声音忽大忽小的问题,一旦出现就是在播放指定音频时是必现的。接下来联系用户帮提供设备的日志信息和操作步骤,按照用户操作来复现问题,通过 demo 还原用户环境参数便能复现。 首先分析确认发现在这个过程中声音音量均无变化,所以初步怀疑可能是和音频流数据出现异常有关。在上图中数字有标识的5个点中分别抓取音频,使用 Audacity 导入音频文件来进行分析,发现位置4的音频正常,而位置5的音频出现了声音异常的现象。具体见下图: 49243c5f-e704-485c-9921-992eb6122469.png 所以便可以确认在音频数据经过 hal 层音效处理前是正常等,经过音效处理后,在某些特殊的声音数据下,音效库缩小了声音的幅值,从而导致声音的异常。为了实锤是音效库 so 导致的问题,通过关闭音效库的功能,最终发现声音忽大忽小的问题消失了。 从以上尝试的结果综合分析可以确认,是音效 so 库对通道声音进行处理时影响到了原有声音的功能。通过修改 so 库最终来解决这个问题。

2. 应用卡顿问题

有用户反馈说是打开应用A播放视频正常,然后直接返回到 home 目录,应用A后台播放时会出现断音的现象。

具体分析

声音卡顿,录音掉帧类的现象在声音问题中非常常见。从现象上来看,就是用户切换到后台时没有暂停播放,视频在后台播放时出现。老规矩,我们先分析相关 log,通过日志分析发现,当问题出现时,日志上频繁打印 get null buffer 的信息。所以怀疑是否是音频数据丢失导致的。dump 音频数据抓取到系统混音后输出到输出设备的原始音频,可以帮助实锤上层系统传下来的数据是否正常。于是发现位置3的音频如下: f6bb3c6f-9ea9-4cf2-bf05-935b896b9d0a.png 从抓取到的音频可以看到,在后台异常播放时,在该时间段内会出现明显音频丢帧的现象。接下来要看看是在哪一块出现了丢帧。进一步分析 audioTrack 传下来的数据,出现了丢失掉一部分音频的现象,时长相当于原音频的一半。基于此,为了实锤是应用A传下来的数据就有缺失,从日志信息跟踪,决定在 audioTrack 上加日志信息来看,发现当切换到后台时,audioTrack 每次还是写 4096 个 byte ,但是写数据的频率降低了一半。   正常:28.600-27.546=1.054 44次 间隔 1.054/43= 0.0245秒/次。   异常:40.839-39.804=1.035 22次 间隔 1.035/21= 0.0493秒/次。 考虑到这一块是否是和后台进程的优先级相关。当进程降低时导致了写数据的线程能够拿到的CPU资源变小,出现了断音的问题。通过和其他型号的平板对比发现,各厂家 Android 10 的平板大部分均有此问题,而 android7 和 android 8 的平板就没有这个问题。基于以上情况,更加怀疑是和 android 的特性相关,可能是新的 android 平板针对后台线程优先级做了处理,目的也很明确,就是限制后台应用的活跃程度,来保证设备性能。此时进一步分析最终发现是和 TimerSlackHigh 的参数相关。 system/core/libprocessgroup/profiles/task_profiles.json

    {
      "Name": "TimerSlackHigh",
      "Actions": [
        {
          "Name": "SetTimerSlack",
          "Params":
          {
            "Slack": "40000000"
          }
        }
      ]
    },
    {
      "Name": "TimerSlackNormal",
      "Actions": [
        {
          "Name": "SetTimerSlack",
          "Params":
          {
            "Slack": "50000"
          }
        }
      ]
    },

该参数影响了应用后台线程 sleep/wait 之间所消耗的时间。可以看到,当应用从前台切换到后台时,这个时间从50 微秒上调到 40 毫秒。从而导致写入音频数据量大大减少。通过修改参数可以解决,但是提高后台线程的活跃度,很可能影响到整体性能,因此不作处理。最终像用户解释,切换后台时可以手动停止播放视频,同时反馈给应用,由应用规范应用流程,起后台进程来做单独处理。

四、总结

按照以上案例,我们来总结下当声音出现异常时一些快速定位调试的手段:

  1. 抓取位置1的音频数据,如果该数据异常。代表从应用端传递下来的数据即为异常,大部分情况下为应用问题。曾经遇到一个是应用默认会将 track 音量调为0,此时调节系统音量时不会有声音的。需要用户点击该应用内的一个静音按钮才有声音,这时候就会在位置1抓到一串无声的音频,这种在安卓版本表现是一致的。但是也有可能是像案例2一样和后台优先级有关,导致只在较高的版本上出现问题。
  2. 抓取位置3的音频数据,若此音频流经过各播放线程时出现问题,则可能是系统 mix, direct 逻辑出现问题,原生逻辑通常不会有问题,有可能是客制化修改引发的。
  3. 抓取位置5的音频数据,此部分逻辑是由 output so 来处理的,可能是音效库处理数据等操作导致声音异常。