在 Android
系统的开发过程当中,音频异常问题通常有如下几类,无声,调节不了声音,爆音,声音卡顿,声音效果异常(忽大忽小,低音缺失等)等。尤其声音效果这部分问题通常从日志上信息量较少,相对难定位根因。想要分析此类问题,便需要对声音传输链路有一定的了解,能够在链路中对各节点的音频流进行采集,通过对比分析音频流的实际效果来缩小问题范围,找出原因。
网上已经有很多音频框架图和相关的大致介绍,这里就不再赘述,只分享下音频流的传输链路,和我们可以重点其中的哪些关键节点,来帮助我们快速定位问题。
想要学习音视频开发技术的可以上哔哩哔哩找相应视频学习:
- 2022最新音视频开发全套视频
- 音频播放解码与视频播放解码,实现万能格式解码
Android Audio
音频系统
1. 音频链路
抓取音频链路当中的音频数据是分析声音异常问题的有效方法,通过抓取不同节点的声音数据,可以帮助我们快速定位问题发生的原因。下面先来看一张安卓官方的音频系统框架图:
一、背景在 Android
系统的开发过程当中,音频异常问题通常有如下几类,无声,调节不了声音,爆音,声音卡顿,声音效果异常(忽大忽小,低音缺失等)等。尤其声音效果这部分问题通常从日志上信息量较少,相对难定位根因。想要分析此类问题,便需要对声音传输链路有一定的了解,能够在链路中对各节点的音频流进行采集,通过对比分析音频流的实际效果来缩小问题范围,找出原因。 网上已经有很多音频框架图和相关的大致介绍,这里就不再赘述,只分享下音频流的传输链路,和我们可以重点其中的哪些关键节点,来帮助我们快速定位问题。
Android Audio
音频系统
1. 音频链路
抓取音频链路当中的音频数据是分析声音异常问题的有效方法,通过抓取不同节点的声音数据,可以帮助我们快速定位问题发生的原因。下面先来看一张安卓官方的音频系统框架图:
Audio
音频数据流整体上经过 APP
,framework
,hal
,kernel driver
四个部分,从应用端发起,不管调用 audio
还是 media
接口,最终还是会由 AudioTrack
将数据往下传,经由 AudioFlinger
启动 MixThread
或 DirectThread
等,将多个 APP
的声音混合到一起,将声音传输到 hal
层。系统会根据音频流类型 stream
和音频策略 strategy
来选择对应的 output
,从而找到对应的 module
,将音频数据传输给 hal
层音频库 so
做声音相关的处理并传给 audio driver
。 音频流传输路径图:
从上述的音频流流程可以看到,我们首先要确认,当前音频流是经由哪一个 hal
层库做处理,是 primary
,usb
还是三方 so
等,然后可以在对应的节点抓取相应的音频信息分析。可以根据自己的需要在音频流的部分节点埋下相应的 dump
指令,将 pcm 写入到相应的节点当中。
AudioTrack
:应用写入音频流的起点,有 MODE_STATIC
和 MODE_STREAM
模式,通过 write
接口将数据写入。此节点写入的数据是由应用层最原始的音频数据。 AudioFlinger
:负责启动线程完成各个应用的混音,音频流声音调节等工作。设备同时可能存在多个应用播放声音,这时便需要将各个应用的声音混合在一起,并且做音量的调节。例如在车载场景中,音乐应用播放歌曲和地图应用语音导航的声音需要同时存在,便使用到了混音的功能,当导航语音响起时,歌曲声音有一个明显的变小,便可以设置音频流的音量。 audio_hw_hal
:hal
层音频处理的入口,为 Android
原生逻辑,各厂家需要按照规范实现其中的音频设置等接口,声明 HAL_MODULE_INFO_SYM
结构体,实现 legacy_adev_open
方法,承接起连接 framework
和 audio driver
的作用,完成一些音效算法等逻辑处理。 AudioStreamOut
:和 audio_hw_hal
一样,是Android
给厂家提供的通用类,厂家在实现自己的通用库实现时需要可以按照谷歌规范,然后在相应的音频处理接口中实现自己的对音频流做音效,增益等处理。 audio_hw_hal.cpp
代码如下,不同厂家这里的实现略有差异,这里只截取部分 AOSP
源码。
从音频流传输路径图可以看到,如何找到是哪一个音频 so
处理声音也是至关重要的。我们知道,系统对于应用层曝光的其实只有通道类型。举个例子:当用户打电话时,可以使用通话通道 STREAM_VOICE_CALL
,当用户播放视频时,可以使用媒体通道 STREAM_MUSIC
,当发送通知时,可以使用 STREAM_NOTIFICATION
。那传入这些通道的声音数据,又是怎么最终流向到具体的硬件输出设备呢?
以媒体通道为例,当应用层将音频数据往 MUSIC
通道写入时,系统便会根据 StreamType
来生成相应的 audio_attributes_t
(.usage = AUDIO_USAGE_MEDIA, .content_type = AUDIO_CONTENT_TYPE_MUSIC}
),再通过 audio_attributes_t
来获取对应的 ProductStrategy
(STRATEGY_MEDIA
),最后在拿到对应的 outputDevice
。Android
原生逻辑outputDevice
的选择在 Engine.cpp
上,会具体根据当前设备是否有接蓝牙,耳机等外设,按照优先级来选择相应的外设设备作为输出,可能是耳机 (AUDIO_DEVICE_OUT_WIRED_HEADSET
),听筒(AUDIO_DEVICE_OUT_EARPIECE
),喇叭(AUDIO_DEVICE_OUT_SPEAKER
)等。具体可以看文件 www.androidos.net.cn/android/9.0… 的 getDeviceForStrategyInt
方法。 通过以上分析,我们知道了音频会流向哪个输出设备,那么下一个问题来了,是由谁负责传输和对音频数据做最后的处理呢? 这里就需要看音频设备的策略文件,还是以媒体通道为例,假设设备没有接任何外接设备,选择的 outputDevice
是 AUDIO_DEVICE_OUT_SPEAKER
。接下来就要看哪个 output
so
支持 AUDIO_DEVICE_OUT_SPEAKER
,符合度最高的 output
so
将会负责数据传输,最终经由 tinyalsa
写入到 pcm
节点中。不同的 Android
版本在配置文件上会有些许差异,可能放置在 *audio_policy_configuration.xml
中,有些在 audio_policy.conf
中。
有用户反馈使用优酷视频播放视频时,概率性出现声音忽大忽小的问题,一旦出现就是在播放指定音频时是必现的。接下来联系用户帮提供设备的日志信息和操作步骤,按照用户操作来复现问题,通过 demo
还原用户环境参数便能复现。 首先分析确认发现在这个过程中声音音量均无变化,所以初步怀疑可能是和音频流数据出现异常有关。在上图中数字有标识的5个点中分别抓取音频,使用 Audacity
导入音频文件来进行分析,发现位置4的音频正常,而位置5的音频出现了声音异常的现象。具体见下图:
所以便可以确认在音频数据经过 hal 层音效处理前是正常等,经过音效处理后,在某些特殊的声音数据下,音效库缩小了声音的幅值,从而导致声音的异常。为了实锤是音效库 so 导致的问题,通过关闭音效库的功能,最终发现声音忽大忽小的问题消失了。 从以上尝试的结果综合分析可以确认,是音效 so 库对通道声音进行处理时影响到了原有声音的功能。通过修改 so
库最终来解决这个问题。
有用户反馈说是打开应用A播放视频正常,然后直接返回到 home 目录,应用A后台播放时会出现断音的现象。
具体分析声音卡顿,录音掉帧类的现象在声音问题中非常常见。从现象上来看,就是用户切换到后台时没有暂停播放,视频在后台播放时出现。老规矩,我们先分析相关 log
,通过日志分析发现,当问题出现时,日志上频繁打印 get null buffer
的信息。所以怀疑是否是音频数据丢失导致的。dump
音频数据抓取到系统混音后输出到输出设备的原始音频,可以帮助实锤上层系统传下来的数据是否正常。于是发现位置3的音频如下:
从抓取到的音频可以看到,在后台异常播放时,在该时间段内会出现明显音频丢帧的现象。接下来要看看是在哪一块出现了丢帧。进一步分析 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的音频数据,如果该数据异常。代表从应用端传递下来的数据即为异常,大部分情况下为应用问题。曾经遇到一个是应用默认会将
track
音量调为0,此时调节系统音量时不会有声音的。需要用户点击该应用内的一个静音按钮才有声音,这时候就会在位置1抓到一串无声的音频,这种在安卓版本表现是一致的。但是也有可能是像案例2一样和后台优先级有关,导致只在较高的版本上出现问题。 - 抓取位置3的音频数据,若此音频流经过各播放线程时出现问题,则可能是系统 mix, direct 逻辑出现问题,原生逻辑通常不会有问题,有可能是客制化修改引发的。
- 抓取位置5的音频数据,此部分逻辑是由
output so
来处理的,可能是音效库处理数据等操作导致声音异常。
作者:SugarTurboS 链接:https://juejin.cn/post/7137475647495995423 来源:稀土掘金