深度好文 | Android高性能音频解析

1.引言

1.1目的和对象

该文档用于Android音频流畅性,聚焦优化音频卡顿、杂音问题。适用于Android音频开发人员查看。

1.2背景

游戏、k歌、直播等一些使用场景,音频对时延有较高的要求,保障低延迟,就需要更小的buffer,降低整个链路数据传递时延,但是抗性能抖动能力就会下降。如果音频线程CPU调度延迟,生产数据不及时,系统就会错过buffer周期,产生补0噪声。

一方面,Android音频系统框架对于音频性能策略设计偏保守,没有提供类似IOS,Windows系统那样的一套API 用于标记音频线程,系统也就不知道APP的线程哪些是音频线程,哪些不是。无法对APP音频线程在CPU资源上做一些适度地倾斜。

另一方面,Android生态控制力较弱,多数APP在音频逻辑的实现过程中,忽略了CPU调度因素,没有按照Google的建议设置音频线程的优先级,也就是默认的优先级(Nice 120)。在整机CPU重载场景下,APP内部音频关联线程,特别是解码器线程CPU调度延迟,导致数据生产不及时,触发声音卡顿、杂音问题,这样的问题越来越突出。

一些APP在实现过程中,在关键的播放线程中,调用AudioManager耗时接口,阻塞了数据传递,导致了声音卡顿。

还有一些应用面在主线程/UI渲染线程中,调用Audio的耗时接口,导致游戏画面渲染帧率降低。

与APP开发伙伴交流过程中,发现APP开发者和系统开发者,存在一些理解上的GAP,APP开发者对音频系统不熟悉,而系统开发者对APP内部业务逻辑不熟悉。遇到问题debug起来,耗时较长,不利于提升用户体验。音频流畅性体验是Android基础体验的一个核心,也是一个痛点。本文聚焦音频流畅性分析,希望能有助于音频APP开发者和音频系统开发者联合调优,提升用户音频基础体验。

1.3音频格式

人耳的听觉范围是在20Hz到20KHz之间的频段,不同于人眼的“暂留”生理特性,人耳鼓膜的灵敏度远高于人眼。两耳间距产生的微秒级时延(700us)带来的细微声压、声相变化,可辨别远处的音源方位。普通人可清晰辨识低至10db的响度变化,经过专业训练的“金耳朵”,可识别更低db的响度变化,人耳对时延敏感,对声音卡顿敏感。这也是音频采样率(48000 fps)远高于画面的帧率(60 fps)的原因。通过大幅增加音频流采样次数,使得响度曲线变化更平滑,避免出现邻帧之间有较大的db变化,避免听觉上不适感。

  • 声道数:声场还原,主要用于表达声音方位立体感。例如 单声道(1ch,mono),双声道立体声(2ch,stereo),5.1声道, 7.1声道

8e08a3e5398ce2d461f38abb3da9cd17.png

图1.1 室内5.1声道声场还原

  •  位宽:也称位深,即一个采样点用几个字节编码

ba5c174f0abe5180c94a04f37d2cb9e6.png

表1.1 PCM编码类型

  • 音频帧:即一个采样点,音频帧大小计算方法:声道数据 * 位宽,例如:

16bit,2ch 音乐,它的音频帧大小为:2 * 2 = 4 字节。

32bit,5.1ch 音乐,它的音频帧大小为:4 * 6 = 24 字节。

  • 采样率:也称音频帧率fps,每秒需要取样多少次,一个采样点就是一个音频帧。通过采样对模拟型号离散化成数字信号,采样率越高,声音信息越丰富。人耳听觉频段范围限制,无法听到低频声音,Android支持的采样率通常范围在闭区间[8000HZ, 384000HZ]。

Android最常见采样率:

通话:8k,16k, 32k

视频及游戏:24k, 44.1k,48k

高清音乐:96k, 192k

d822b98798e685cc2bd3ea473af99648.png

图1.2 采样与量化

音频需要足够高的采样率,使得数据更平滑。

  • 码率:1秒时间播放了多少字节数据(或多少bit数据)。

计算公式:bRate = 采样率 *  位宽 * 声道数

例如 48k,7.1声道,16bit pcm编码  码率为:48000 * 8 * 2 = 768000 Bps

2.常见杂音类型

杂音是主观体验的概念,技术上常称为音频卡顿,“闻香可以识女人,看波形也能知音”,不同原因,有不同的杂音波形特征。例如写0,断点,重复数据,削顶,截断,高频/低频截止,白噪声,无规律断点数据。

2.1pop音

常常称为“破音”,是属于断音的一类。数据不连续,有明显跳变发生。听起来,“啪!啪!啪!”的破音,耳朵有不适感。常见的有seek pop noise,在快进,快退,或者倍速播放时,更容易遇到这类杂音。

3c7dddfbfe3e5f07d72556788cd34ace.png

2.2 补0

特征是,删除重点的0数据,数据就是连续的了,因为有连续两次跳变,听起来杂音特征比pop音更明显一些。

ee98eeb6d548b7c3a0a8c15fc6da8f7f.png

删除0后:

5cd17e31bc8871a71d1676478f080d32.png

2.3截断音

属于断音的一种,与pop音的区别是,启播的第一帧,没有做淡入,或者停播的最后一帧,没有做淡出。

常见场景例如刷短视频,切换视频之间,有时候会听到“啪!啪!啪!”的破音。简单来说就是,需要做淡入淡出。FadeInFadeOut

2.3.1切换截断音

002ef8505b023d8754872018c4e1c0de.png

2.3.2起始截断音

开始播放阶段,没有做淡入(Fade In),产生的杂音。

7496ebb3961623533e8ea49da46f7a25.png

2.3.3 结尾截断音

0c3dff6f77ed3c66a09630bbff630092.png

2.4 削顶

削顶,又称为“削波”,原因是音频信号的响度,超过了编码范围,即0db,并不是说音源存在问题,只是音源信号,幅度太大,超过手机音频系统的表达能力,无法进行还原。超出的部分,被统一削减到0db。听起来,声音有连续卡顿的感觉,不自然。

35fb74335c8108ce6f9b6f11a925d68b.png

2.5 蜂鸣音

听感起来,像蜂鸣器,汽笛声。波形上,4ms重复数据,常见于游戏声音卡顿。

c79b7e6d4cba534ddc2715dca0274b08.png

2.6 啸叫

2.6.1 什么是啸叫?

啸叫现象是指音频信号通过扬声器播放后,经过一定的传播路径,再次被麦克风拾取,经过放大器的处理后,最后经由扬声器播放,倘若在 “扬声器-麦克风-扬声器”的闭环电路中,存在某种正反馈导致某些音频频率发生自激振荡,就会产生啸叫现象。

常见于VOIP会议,通话双方,如果距离太近,就容易产生啸叫。听起来,响度非常大,声音尖锐,感官非常难受。啸叫的产生会掩盖正常语音,给人的听感也不好,而且啸叫频点能量很高,严重时甚至能破坏会议中的扩声设备,因此我们需要对啸叫进行抑制。受限于尺寸,和算法功耗,手机侧啸叫抑制能力有限。

从波形上,可以看到,响度非常大。

52587fc735ad20346d0ada861c70c40b.png

2.6.2 啸叫的原理

17c8a36a0d078183d25fd8a9b13bb76d.png

正反馈系统,信号一次又一次的被循环放大。

2.7 环境底噪

主要是指mic录到的环境噪声,经过降噪算法后,通常比较轻微,不仔细几乎听不出来。但是如果降噪算法调音的不均衡,可能导致底噪大,或者音质损失。

4a8f7c3b70787e8866e0698f9f8256d9.png

2.8 其他无规律噪声

白噪声。

29342bfc940008e262ad9fb2b477a9f0.png

3. 音频通路基础

3.1 播放流程拆解

对系统来说,APP是黑盒子,不同的应用有不同的逻辑实现方式,特别是游戏,可能有更复杂的业务逻辑,多达100+个线程,系统不知道那个是音频播放相关的。音频能识别到的只有AudioTrack/AudioRecord调用入口,涉及到最多两个线程。以赖线程之间的唤醒关系推测,梳理大概的流程如下:

音乐、视频类三方APP:

2f9392b698bc80d359d643a7b1168574.png

游戏音频流程:

be8834873273eb688bcd11ef493caf71.png

3.2 音频通路

3.2.1 音频播放链路

cd1513e87c518c5577476221f101cf2f.png

3.2.2 音频通路

e0b2dd28f64daf57ad6272b2145c4ac4.png

限于篇幅,这里不对每个通路进行展开介绍,可自行阅读代码和google官方文档。

3.2.3 AAudio通路

Aaudio是google基于mmap思路构建的,音频通路。尽量减少数据传递环节,以降低负载和功耗,同时降低层层传递带来的时延。这个通路是google强烈推荐使用的API,可替代OpenSL ES,支持音频低延迟。也可以通过设置参数,走到低功耗通路,例如deep buffer通路。

以下以低延迟播放实例来分析AAUDIO工作原理,从systrace上看,aaudio并没有经过audiohal write数据,可以看到audiohal的writer线程并没有被唤醒产生负载。可以证明:aaudio是通过共享内存方式来传递数据,这种方式非常高效,避免了buffer数据层层Copy。

326bf44b37c15f752f2a789367e4f206.png

这里简单介绍一下AAudio性能相关的内容:

aaudio流程图如下:

d55f45d842ee4d2a09b55cdda53dcdee.png

AAudio设置性能模式:

每个 AAudioStream 都具有性能模式,而这对应用行为的影响很大。共有三种模式:

  • AAUDIO_PERFORMANCE_MODE_NONE 是默认模式。这种模式使用在延迟时间与节能之间取得平衡的基本流。

  • AAUDIO_PERFORMANCE_MODE_LOW_LATENCY 使用较小的缓冲区和经优化的数据路径,以减少延迟时间。

  • AAUDIO_PERFORMANCE_MODE_POWER_SAVING 使用较大的内部缓冲区,以及以延迟时间为代价换取节能优势的数据路径。

可以通过调用 setPerformanceMode() 来选择性能模式,并通过调用 getPerformanceMode() 来获得当前模式。

  • 音频低延迟场景,需要设置AAUDIO_PERFORMANCE_MODE_LOW_LATENCY。系统路由到AAUDIO低延迟通路,通路buffer 只有1ms,控制周期为2ms。

  • 如果对低延迟没有要求,尽可能的节省功耗,可设置AAUDIO_PERFORMANCE_MODE_POWER_SAVING。例如播放音乐,会走到deep buffer通路上。

在当前版本的 AAudio 中,为了尽量减少延迟时间,必须将 AAUDIO_PERFORMANCE_MODE_LOW_LATENCY 性能模式与高优先级回调配合使用。请参阅以下示例:

23654d6a7a49342f441a5674ec76f1b5.png

关于AAUDIO的详细介绍,请移步Android SPEC,这里不再详细展开:

https://developer.android.google.cn/ndk/guides/audio/aaudio/aaudio?hl=zh-cn

3.2.4 Low Latency通路

也就是常说的Fast通路,有的平台和primary通路放在一起,有的是独立的Low Latency通路,这样实现依赖平台的性能,特别是ADSP性能,都有各自的优缺点。

480117f57511da1ee2c6f4a169eb97e2.png

FastMixer的buffer 通常设置为4-5ms,通路时延较低,但更容易受到整机性能的影响,产生性能杂音。

FastMixer的使用,通常是通过OpenSL ES,或者Sound pool API。

3.2.5 ULL 通路

极致低延迟通路ULL实质上,为lowlatency通路的进一步压缩buffer,buffer甚至压缩到1ms,Android非实时系统,因此CPU调度上,无法保障1ms内得到调度,因此,这个通路,很少有开放使用。如果追求极致低延迟,推荐使用AAudio来实现。

4. 音频与CPU调度

4.1 TASK运行状态机

786df2a5fb2ccb67eb6a14b41e8a4b60.png

  • R:(TASK_RUNNING ,running + runnable),运行状态,并不意味着进程一定在运行中,也可以在运行队列里;

  • S:( TASK_INTERRUPTIBLE,sleeping),可中断的睡眠状态,进程在等待事件完成;(浅度睡眠,可以被唤醒)

  • D:( TASK_UNINTERRUPTIBLE ),不可中断睡眠(深度睡眠,不可以被唤醒,通常在磁盘写入时发生,也有非IO的内核原子操作)

  • T:( TASK_STOPPED or TASK_TRACED),停止状态,或者跟踪状态,可以通过发送SIGSTOP信号给进程来停止进程,可以发送SIGCONT信号让进程继续运行

  • X:( TASK_DEAD – EXIT_DEAD ),退出状态,进程即将被销毁;

  • Z:( TASK_DEAD – EXIT_ZOMBIE ),僵尸状态,子进程退出,父进程还在运行,但是父进程没有读到子进程的退出状态,子进程进入僵尸状态;

b00819e63678fba7463c349005c21737.png

4.2 调度器

4.2.1 调度器分类

f50899ff5bcd828c87f1b1f611c937e9.png

Stop调度器,

stop_sched_class:优先级最高的调度类,可以抢占其他所有进程,不能被其他进程抢占;

Deadline调度器, dl_sched_class:使用红黑树,把进程按照绝对截止期限进行排序,选择最小进程进行调度运行;

RT调度器, rt_sched_class:实时调度器,为每个优先级维护一个队列;

CFS调度器, cfs_sched_class:完全公平调度器,采用完全公平调度算法,引入虚拟运行时间概念;

IDLE-Task调度器, idle_sched_class:空闲调度器,每个CPU都会有一个idle线程,当没有其他进程可以调度时,调度运行idle线程;

4.2.2 调度优先级策略

linux内核的三种调度方法:1,SCHED_OTHER 分时调度策略,2,SCHED_FIFO实时调度策略,先到先服务3,SCHED_RR实时调度策略,时间片轮转 

实时线程将得到优先调用,实时进程根据实时优先级决定调度权值,分时进程则通过nice和counter值决定权值,nice越小,counter越大,被调度的概率越大,也就是曾经使用了cpu最少的进程将会得到优先调度。

4.2.3 CFS调度类

cfs是绝对公平调度算法,理想情况下,优先级相同的两个task,运行时间应该各占cpu的50%,同理3个则cpu利用率为1/3。但是cfs中弱化了优先级的概念而是使用权重weight来决定任务的运行时间。

例如:3个任务A,B,C权重(priority)分别1,2,3;则总权重,一个调度周期为6单位时间,理想状态下,A应占用1单位,B为2,C为3。

27bf13b5e96afa8cf5a4dca9c65b3c6c.png

f7536aeb186be253b1daff3f0ef75031.png

cfs中使用虚拟时间vruntime来决定运行的task,nice值-20到20 --> weight -->vruntime; cfs中使用rbtree来管理调度实体se。每次选取vruntime最小的task进行执行。在rb tree中vruntime最小的se在rbtree的最左侧。

cfs是通过限制当前task的运行时间来实现公平的,task的vruntime单调递增,它在rbtree中向右移动,让出cpu使用权给vruntime更小的task。

a5f848cdd10878dd31423db2ad9a3d6a.png

Task数据结构

07fdfdc16e52e6e586fc067c5ea555cc.png

452ae57484cb6259a80d677af5ff17b4.png

4.2.4 RT 调度类

实现调度分为RR和FIFO两类

SHCED_RR和SCHED_FIFO的不同:

当采用SHCED_RR策略的进程的时间片用完,系统将重新分配时间片,并置于就绪队列尾。放在队列尾保证了所有具有相同优先级的RR任务的调度公平。    

SCHED_FIFO一旦占用cpu则一直运行。一直运行直到有更高优先级任务到达或自己放弃。

如果有相同优先级的实时进程(根据优先级计算的调度权值是一样的)已经准备好,FIFO时必须等待该进程主动放弃后才可以运行这个优先级相同的任务。而RR可以让每个任务都执行一段时间。

相同点:

  RR和FIFO都只用于实时任务。

  创建时优先级大于0(1-99)。

  按照可抢占优先级调度算法进行。

  就绪态的实时任务立即抢占非实时任务。

rt 调度类数据结构

96e632d7bc606ba44d578547f5de2f39.png

4.3 CGroup与TimerSlack

Android平台的进程会根据运行状态,在不同的cgroup进行迁移,不同cgroup设置的timerslack不同,导致后台进程的定时器超时唤醒变长。Oomadj会触发推组,切后台,activity会从top group 推组到background组,相应的timerslack会从0.5ms 提高到40ms。如果播放器,实现上没有使用service,那么就可能会遇到Android应用切换到后台播放音频卡顿。

ac2f35b48d603775b85152ec4d852c85.png

4e908beffc5f4130d90e16335d1690d8.png

d141ff18e84fe4700bd88588606ca7c5.png

4.3.1 TimerSlack基本原理

以下简单介绍一下timerslack基本原理:

6b9c75989bc7a77ecd0ce20bcf77d160.png

4.3.2 音频播放录制请使用Service

Service在Android定义中,一直处于foreground,因此不会有切后台timerslack变大,导致的卡音问题。因此,APP实现中,特别注意,音频解码器,播放关联线程,需要放到service中。避开前后台切换,cgroup迁移的问题。

查看app的timerslack方法:/proc/pid/timerslack_ns

例如,pid为11871:查看timerslack为:0.5ms

7054e520c85e2a6af646db83a03a3dfc.png

当按home按键,这项数值就会变成40000000

4.4 Android音频优先级

AOSP定义上,音频定义为CFS最高优先级Nice。

4eacdd77da9f4d28557aafde481e9cfc.png

Android默认优先级为120, 数量越小,优先级越高。例如最高的 ANDROID_PRIORITY_URGENT_AUDIO 为 120 -19 =101, 对应cfs线程级别的101。

总体优先级为:Audio > Video > UI

5. 性能卡音案例

有了以上的音频性能基础,分析起问题来,就不那么困难了。

分析音频卡顿,最有效的还是systrace,以下介绍几个案例:

5.1 APP播放线程调度延迟

5220f19f066285b923f9c3c41c51941c.png

通过systrace我们可以看到,APP的Aplayer为音频关键线程,出现了调度上的延迟,我们可以看到问题点,缓存buffer已经消化空了,CPU已经满载,但是有明显的压频行为。因此最终原因为:

  1. APP的线程优先级设置不合理,默认nice的120,没有按着google的标准来设置,至少设置为104,必要时设置为101。如果nice设置的过低,CPU满载时,就容易抢不到cpu。

  2. 负载太高,触发温升限频。

5.2 游戏音频线程调度延迟

5.2.1 audio dump发现蜂鸣杂音

在track阶段就dump到了杂音,可以确定问题发生在app内部。

9f171af25eed22c63d6a7208f97579f7.png

持续4ms重复数据

a0c25a8dbe7f56bdc11eaab94de46ab6.png

5.2.2 Systrace发现游戏音频线程调度延迟

06699780b15150b69087d257a1bf69ad.png

b3164179b589811ee2761544ed178ae0.png

5.2.3 根因:NativeThread优先级设置太低

NativeThread使用OpenSL ES发起了FastTrack播放,实时性要求较高,当前优先是默认的120,优先级非常低。推荐APP参考google的建议来将NativeThread线程设置为nice 101即 ANDROID_PRIORITY_URGENT_AUDIO级别。

5.3 音频系统线程调度

c67f90b97e2b5d9b4fabc83b40c9981e.png

Android音频优先级设置偏保守,IOS,Windows系统调度上,对音频有明显的倾斜。在加上链路太长,多个进程周转数据,所以Android音频流畅性体验,要低于IOS和windows。

5.4 APP在播放线程调用音频耗时接口

最常见的接口是isBluetoothScoOn(), 这个接口在系统没有切换时候的时候,会很快返回,但是如果再插拔耳机,或者接听,挂断电话/VOIP电话时,就会耗时很长,导致caller线程,长时间阻塞,不产生数据。而fastmixer callback,一遍又一遍的吧,4ms的buffer重复callback过来,导致了蜂鸣杂音的产生。类似的耗时API还有,isWiredHeadsetOn(),isSpeakerPhoneOn()。

8b1a0cad379d067b48c29ae261d7b116.png

从systrace上看,nativeThread被阻塞,根据唤醒关系结合logcat,可以定位到,此时正在做isBluetoothScoOn接口调用。由于lock被切换设备的线程持有,而切换设备又比较耗时,从而导致,nativethread被卡住,无法生产数据。Audio系统callback到了重复的杂音数据。

误区:AudioManager接口可以立刻返回。事实上,AudioService内部有复杂的逻辑,例如切换设备,有很多业务逻辑要处理。一个简单的场景,voip来电,这时候,需要和蓝牙耳机建立链路,握手过程,可能就需要150-300ms,整个过程非常耗时。如果这时候在主线程去调用,getMode,isBluetoothScoOn等接口,都会被阻塞掉,直到耳机返回,或者连接超时。

2837867e677979b69815998a769cc8a4.png

5.5 渲染线程调用AudioTrack导致帧率低

例如这份日志里,在渲染线程中,去执行AudioTrack.start(),结果阻塞580ms,导致画面卡顿。应尽量避免在UI线程,图形render线程中,调用AudioTrack接口。

2612c6f7058d6ec7595c268636896853.png

5.6 FrameCount设置过大过小

FrameCount的作用是控制APP起播的起播缓存大小,设置越大,时延越高,越不容易卡音。设置越小,时延越低,越容易卡音。

2efe83f4cb71f92ab0c0832510500338.png

5.6.1 短促音FrameCount设置过大导致无声

4de73adcd0f67b93316cacfa431cb98e.png

5.6.2 FrameCount设置过小卡音

常见的问题是使用OpenSL ES播放,framecount设置的太小,缓存buffer提效,当cpu调度不及时,就容易触发性能卡音。

d3b34e3d951869788c61e9e7279e619f.png

5.7 频繁调用Audio接口,导致整机重启

Android系统只提供了31个Binder线程池,也就是系统最大并发数为31个,如果APP频繁调用Audio耗时接口,binder被占用,甚至被耗尽,导致整机重启。以下某APP,持续不停的去查音量,导致系统Binder资源被耗尽,最终触发重启。

d24f676d78323350202528563710d21f.png

SystemServer Binder线程池设置为31个

7fc91fa7232af02fd4c8106ae4e56f4a.png

6.音频联合调优建议

6.1规范设置音频优先级

2fee06af429937fc1f53199478000edd.png

APP 需要参考AOSP标准,将音频数据关联线程,例如解码器,至少nice设置为104级别。

6.2 高实时的线程,避免调用耗时的音频API

尽量不要在音频解码器,以及主线程中调用音频耗时接口,例如isBluetoothScoOn(),getMode等等接口,这些接口大都会持锁,导致切换设备时,耗时较长。

6.3播放逻辑进Service

尽量将音频播放逻辑,放在service中,避免切后台,timerslack被改大,CPU被限频限核,导致的声音卡顿。

6.4 非必要避免频繁调用耗时的Audio接口

例如查询音量,音频设备连接状态。

7. 参考文献

[1]https://developer.apple.com/documentation/audiotoolbox/workgroup_management/understanding_audio_workgroups/#3625591

[2] https://learn.microsoft.com/en-us/windows/win32/procthread/multimedia-class-scheduler-service

[3] https://zhuanlan.zhihu.com/p/556295381?utm_id=0

[4]https://zhuanlan.zhihu.com/p/658488322

a4773f34e8d733247c4a9321047465e4.png

高通camx入门学习 | camx初认识

学习完Camera入门课程视频,可以去找工作了?

Camera Hal|如何学习一个新平台

Android Camera 学习路线 | 个人推荐

一篇文章带你了解Android 最新Camera框架

独家 | Android Camera 面试流程、经验分享

ee4f7dbc6f8a48bb3ea3626fbd0672c4.png

《Android Camera开发入门》视频课程、《Camx入门学习》已经上架了,可以加我微信咨询,目前针对星球成员免费开放,也欢迎加入“小驰成长圈”星球b208c82499d917666bd87bb3d4b6f765.png

f2faaef120dfa1018784c0aa0d2a985f.jpeg

觉得不错,点个赞呗 c96f397c8c789acb80ec4d154fee2d1d.png

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值