Android 音频框架概述 Audio 是整个 Android 平台非常重要的一个组成部分,负责音频数据的采集和输出、音频流的控制、音频设备的管理、音量调节等,主要包括如下部分:
与 Audio 强相关的有 MultiMedia,MultiMedia 负责音视频的编解码,MultiMedia 将解码后的数据通过 AudioTrack 输出,而 AudioRecord 采集的录音数据交由 MultiMedia 进行编码。 本文分析基于 Android 7.0 - Nougat。 AudioTrack API 概述播放声音可以使用 MediaPlayer 和 AudioTrack,两者都提供 Java API 给应用开发者使用。两者的差别在于:MediaPlayer 可以播放多种格式的音源,如 mp3、flac、wma、ogg、wav 等,而 AudioTrack 只能播放解码后的 PCM 数据流。从上面 Android 音频系统架构图来看:MediaPlayer 在 Native 层会创建对应的音频解码器和一个 AudioTrack,解码后的数据交由 AudioTrack 输出。所以 MediaPlayer 的应用场景更广,一般情况下使用它也更方便;只有一些对声音时延要求非常苛刻的应用场景才需要用到 AudioTrack。 AudioTrack Java APIAudioTrack Java API 音频流类型: Android 为什么要定义这么多的流类型?这与 Android 的音频管理策略有关,例如:
这些属于 AudioPolicyService 的内容,本文不展开分析了。应用开发者应该根据应用场景选择相应的流类型,以便系统为这道流选择合适的输出设备。 一个 AudioTrack Java API 的测试例子(MODE_STREAM 模式): 详细说明下 getMinBufferSize() 接口,字面意思是返回最小数据缓冲区的大小,它是声音能正常播放的最低保障,从函数参数来看,返回值取决于采样率、采样深度、声道数这三个属性。MODE_STREAM 模式下,应用程序重点参考其返回值然后确定分配多大的数据缓冲区。如果数据缓冲区分配得过小,那么播放声音会频繁遭遇 underrun,underrun 是指生产者(AudioTrack)提供数据的速度跟不上消费者(AudioFlinger::PlaybackThread)消耗数据的速度,反映到现实的后果就是声音断续卡顿,严重影响听觉体验。 可见最小缓冲区的大小 = 最低帧数 声道数 采样深度,(采样深度以字节为单位),到这里大家应该有所明悟了吧,在视频中,如果帧数过低,那么画面会有卡顿感,对于音频,道理也是一样的。最低帧数如何求得,我们到 native 层再解释。 关于 MediaPlayer、AudioTrack,更多更详细的 API 接口说明请参考 Android Developer:
AudioTrack Native APIAudioTrack Native API 四种数据传输模式: AudioTrack Native API 音频流类型: AudioTrack Native API 输出标识: 我们根据不同的播放场景,使用不同的输出标识,如按键音、游戏背景音对输出时延要求很高,那么就需要置 AUDIO_OUTPUT_FLAG_FAST,具体可以参考 ToneGenerator、SoundPool 和 OpenSL ES。 一个 AudioTrack Natvie API 的测试例子(MODE_STATIC/TRANSFER_SHARED 模式),代码文件位置:frameworks/base/media/tests/audiotests/shared_mem_test.cpp: 上个小节还存在一个问题:AudioTrack::getMinFrameCount() 如何计算最低帧数呢? 首先要了解音频领域中,帧(frame)的概念:帧表示一个完整的声音单元,所谓的声音单元是指一个采样样本;如果是双声道,那么一个完整的声音单元就是 2 个样本,如果是 5.1 声道,那么一个完整的声音单元就是 6 个样本了。帧的大小(一个完整的声音单元的数据量)等于声道数乘以采样深度,即 frameSize = channelCount * bytesPerSample。帧的概念非常重要,无论是框架层还是内核层,都是以帧为单位去管理音频数据缓冲区的。 其次还得了解音频领域中,传输延迟(latency)的概念:传输延迟表示一个周期的音频数据的传输时间。可能有些读者一脸懵逼,一个周期的音频数据,这又是啥?我们再引入周期(period)的概念:Linux ALSA 把数据缓冲区划分为若干个块,dma 每传输完一个块上的数据即发出一个硬件中断,cpu 收到中断信号后,再配置 dma 去传输下一个块上的数据;一个块即是一个周期,周期大小(periodSize)即是一个数据块的帧数。再回到传输延迟(latency),传输延迟等于周期大小除以采样率,即 latency = periodSize / sampleRate。 最后了解下音频重采样:音频重采样是指这样的一个过程——把一个采样率的数据转换为另一个采样率的数据。Android 原生系统上,音频硬件设备一般都工作在一个固定的采样率上(如 48 KHz),因此所有音轨数据都需要重采样到这个固定的采样率上,然后再输出。为什么这么做?系统中可能存在多个音轨同时播放,而每个音轨的采样率可能是不一致的;比如在播放音乐的过程中,来了一个提示音,这时需要把音乐和提示音混音并输出到硬件设备,而音乐的采样率和提示音的采样率不一致,问题来了,如果硬件设备工作的采样率设置为音乐的采样率的话,那么提示音就会失真;因此最简单见效的解决方法是:硬件设备工作的采样率固定一个值,所有音轨在 AudioFlinger 都重采样到这个采样率上,混音后输出到硬件设备,保证所有音轨听起来都不失真。 sample、frame、period、latency 这些概念与 Linux ALSA 及硬件设备的关系非常密切,这里点到即止,如有兴趣深入了解的话,可参考:Linux ALSA 音频系统:逻辑设备篇 了解这些前置知识后,我们再分析 AudioTrack::getMinFrameCount() 这个函数: 我们不深入分析 calculateMinFrameCount() 函数了,并不是说这个函数的流程有多复杂,而是它涉及到音频重采样的背景原理,说清楚 how 很容易,但说清楚 why 就很困难了。目前我们只需要知道:这个函数根据硬件设备的配置信息(采样率、周期大小、传输延迟)和音轨的采样率,计算出一个最低帧数(应用程序至少设置多少个帧才能保证声音正常播放)。 说点题外话,Anroid 2.2 时,AudioTrack::getMinFrameCount() 的处理很简单: 从这段来看,最低帧数也是基于重采样来计算的,只不过这里的处理很粗糙:afFrameCount 是硬件设备处理单个数据块的帧数,afSampleRate 是硬件设备配置的采样率,sampleRate 是音轨的采样率,如果要把音轨数据重采样到 afSampleRate 上,那么反推算出应用程序最少传入的帧数为 afFrameCount *sampleRate / afSampleRate,而为了播放流畅,实际上还要大一点,所以再乘以一个系数(可参照 framebuffer 双缓冲,一个缓冲缓存当前的图像,一个缓冲准备下一幅的图像,这样图像切换更流畅),然后就得出一个可以保证播放流畅的最低帧数 minFrameCount = (afFrameCount * sampleRate / afSampleRate) * minBufCount。 为什么 Android 7.0 这方面的处理比 Android 2.2 复杂那么多呢?我想是两个原因:
AudioFlinger 概述AudioPolicyService 与 AudioFlinger 是 Android 音频系统的两大基本服务。前者是音频系统策略的制定者,负责音频设备切换的策略抉择、音量调节策略等;后者是音频系统策略的执行者,负责音频流设备的管理及音频流数据的处理传输,所以 AudioFlinger 也被认为是 Android 音频系统的引擎。 AudioFlinger 代码文件结构记得刚接触 Android 时,版本是 Android 2.2-Froyo,AudioFlinger 只有 3 个源文件:AudioFlinger.cpp、AudioMixer.cpp、AudioResampler.cpp。 现在文件多了许多,代码量就不用说了。但是接口及其基本流程一直没有改变的,只是更加模块化了,Google 把多个子类抽取出来独立成文件,比如 Threads.cpp、Tracks.cpp、Effects.cpp,而 AudioFlinger.cpp 只包含对外提供的服务接口了。另外相比以前,增加更多的功能特性,如 teesink、Offload、FastMixer、FastCapture、FastThread、PatchPanel 等,这里不对这些功能特性扩展描述,有兴趣的可以自行分析。
本文内容主要涉及 AudioFlinger.cpp、Threads.cpp、Tracks.cpp 这三个文件。 AudioFlinger 服务启动从 Android 7.0 开始,AudioFlinger 在系统启动时由 audioserver 加载(之前版本由 mediaserver 加载),详见 frameworks/av/media/audioserver/main_audioserver.cpp: 可见 audioserver 把音频相关的服务都加载了,包括 AudioFlinger、AudioPolicyService、RadioService、SoundTriggerHwService。 main_audioserver.cpp 编译生成的可执行文件存放在 /system/bin/audioserver,系统启动时由 init 进程运行,详见 frameworks/av/media/audioserver/audioserver.rc: AudioFlinger 服务启动后,其他进程可以通过 ServiceManager 来获取其代理对象 IAudioFlinger,通过 IAudioFlinger 可以向 AudioFlinger 发出各种服务请求,从而完成自己的音频业务。 AudioFlinger 服务接口AudioFlinger 对外提供的主要的服务接口如下: 可以归纳出 AudioFlinger 响应的服务请求主要有:
就本文范围而言,主要涉及 openOutput() 和 createTrack() 这两个接口,后面也会详细分析这两个接口的流程。 AudioFlinger 回放录制线程AndioFlinger 作为 Android 的音频系统引擎,重任之一是负责输入输出流设备的管理及音频流数据的处理传输,这是由回放线程(PlaybackThread 及其派生的子类)和录制线程(RecordThread)进行的,我们简单看看回放线程和录制线程类关系:
PlaybackThread 中有个极为重要的函数 threadLoop(),当 PlaybackThread 被强引用时,threadLoop() 会真正运行起来进入循环主体,处理音频流数据相关事务,threadLoop() 大致流程如下(以 MixerThread 为例):
这里说说 PlaybackThread 与输出流设备的关系:PlaybackThread 实例与输出流设备是一一对应的,比方说 OffloadThread 只会将音频数据输出到 compress_offload 设备中,MixerThread(with FastMixer) 只会将音频数据输出到 low_latency 设备中。 从 Audio HAL 中,我们通常看到如下 4 种输出流设备,分别对应着不同的播放场景:
其中 primary_out 设备是必须声明支持的,而且系统启动时就已经打开 primary_out 设备并创建好对应的 MixerThread 实例。其他类型的输出流设备并非必须声明支持的,主要是看硬件上有无这个能力。 可能有人产生这样的疑问:既然 primary_out 设备一直保持打开,那么能耗岂不是很大?这里阐释一个概念:输出流设备属于逻辑设备,并不是硬件设备。所以即使输出流设备一直保持打开,只要硬件设备不工作,那么就不会影响能耗。那么硬件设备什么时候才会打开呢?答案是 PlaybackThread 将音频数据写入到输出流设备时。 下图简单描述 AudioTrack、PlaybackThread、输出流设备三者的对应关系: 我们可以这么说:输出流设备决定了它对应的 PlaybackThread 是什么类型。怎么理解呢?意思是说:只有支持了该类型的输出流设备,那么该类型的 PlaybackThread 才有可能被创建。举个例子:只有硬件上具备硬件解码器,系统才建立 compress_offload 设备,然后播放 mp3 格式的音乐文件时,才会创建 OffloadThread 把数据输出到 compress_offload 设备上;反之,如果硬件上并不具备硬件解码器,系统则不应该建立 compress_offload 设备,那么播放 mp3 格式的音乐文件时,通过 MixerThread 把数据输出到其他输出流设备上。 那么有无可能出现这种情况:底层并不支持 compress_offload 设备,但偏偏有个标识为 AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD 的音频流送到 AudioFlinger 了呢?这是不可能的。系统启动时,会检查并保存输入输出流设备的支持信息;播放器在播放 mp3 文件时,首先看 compress_offload 设备是否支持了,如果支持,那么不进行软件解码,直接把数据标识为 AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD;如果不支持,那么先进行软件解码,然后把解码好的数据标识为 AUDIO_OUTPUT_FLAG_DEEP_BUFFER,前提是 deep_buffer 设备是支持了的;如果 deep_buffer 设备也不支持,那么把数据标识为 AUDIO_OUTPUT_FLAG_PRIMARY。 系统启动时,就已经打开 primary_out、low_latency、deep_buffer 这三种输出流设备,并创建对应的 MixerThread 了;而此时 DirectOutputThread 与 OffloadThread 不会被创建,直到标识为 AUDIO_OUTPUT_FLAG_DIRECT/AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD 的音频流需要输出时,才开始创建 DirectOutputThread/OffloadThread 和打开 direct_out/compress_offload 设备。这一点请参考如下代码,注释非常清晰: 其中 mpClientInterface->openOutput() 最终会调用到 AudioFlinger::openOutput():打开输出流设备,并创建 PlaybackThread 对象: AudioFlinger OffloadThread
这里不知大家有无疑问:为什么 DirectOutputThread 与 OffloadThread 会被单独对待?DirectOuputThread 使用率较低,尚可以理解,但 OffloadThread 的使用率还是很高的,为什么不让 OffloadThread/compress_offload 设备也进入待命状态呢? 要回答这个问题:我们首先得明白 compress_offload 设备是什么东东,与其他输出流设备有什么不同。先看个图: 红色的是 Offload 音频流,它与其他音频流有什么本质的不同?Offload 音频流是未经 NuPlayerDecoder 进行解码的(NuPlayerDecoder 设置了 Passthrough 模式),所以必须把这些音频流数据送到 DSP,让 DSP 对其解码,解码后的 PCM 数据再送到 Codec 输出。 compress_offload 设备,说白了,就是驱动 DSP 解码数据、Codec 输出声音。 而 DSP 要解码数据,首先得知道数据的编码信息,如编码器 codec_id、采样率 sample_rate、声道数 channel_mask、比特率 bit_rate 等信息,由于 DSP 并没有实现 DataSource/Parser 部件,不能自己解析数据的编码信息,所以得有“人”告诉它,这个“人”无疑是 compress_offload 设备。 AudioTrack 构造函数有个 offloadInfo 的参数,参数原型定义如下: NuPlayer DataSource/Parser 解析 mp3、flac 等文件得到数据编码信息,并在构造 AudioTrack 实例时作为参数传入,AudioFlinger 将基于这些编码信息打开 compress_offload 设备。 到这里,大家明白了吗?每个 mp3/flac 文件的编码信息可能是不一样的,比如 a.mp3 文件的编码信息是 mp3&44.1KHZ&16bit… ,而 b.flac 文件的编码信息是 flac&48KHz&24bit…; 播放 a.mp3 时,AudioFlinger 打开一个配置为 mp3&44.1KHz&16bit… 的 compress_offload 设备,接着播放 b.flac,就需要关闭之前的 compress_offload 设备,重新打开一个配置为 flac&48KHz&24bit… 的 compress_offload 设备。所以系统不会提前打开 compress_offload 设备,只有等到播放 mp3、flac 时取到明确的数据编码信息,才基于这些编码信息打开 compress_offload 设备。 编码信息包含很多条目,切换音源时,是否编码信息有一点点不一样,都需要重新打开 compress_offload 设备呢?不能运行时更新信息到 DSP 吗?其实 stagefright 和 compress_offload 是支持运行期更新某些信息的,也就是无缝切换,至于是哪些信息,依赖于 DSP 算法实现;有兴趣深入的可以参考 sendMetaDataToHal() 和 compress_set_gapless_metadata() 。 最后附上 NuPlayer Offload Playback 初始化流程,摘自《Linux Audio Compress Offload Playback and PCM Offload Playback》:
AudioFlinger 音频流管理从 AudioTrack、PlaybackThread、输出流设备三者的关系图中,我们看到 AudioTrack 把音频流数据送入到对应的 PlaybackThread 中,那么应用进程想控制这些音频流的话,比如开始播放 start()、停止播放 stop()、暂停播放 pause(),怎么办呢?注意应用进程与 AudioFlinger 并不在一个进程上。这就需要 AudioFlinger 提供音频流管理功能,并提供一套通讯接口可以让应用进程跨进程控制 AudioFlinger 中的音频流状态(通讯接口参考下一章的描述,暂且不表)。 AudioFlinger 音频流管理由 AudioFlinger::PlaybackThread::Track 实现,Track 与 AudioTrack 是一对一的关系,一个 AudioTrack 创建后,那么 AudioFlinger 会创建一个 Track 与之对应;PlaybackThread 与 AudioTrack/Track 是一对多的关系,一个 PlaybackThread 可以挂着多个 Track。 具体来说:AudioTrack 创建后,AudioPolicyManager 根据 AudioTrack 的输出标识和流类型,找到对应的输出流设备和 PlaybackThread(如果没有找到的话,则系统会打开对应的输出流设备并新建一个 PlaybackThread),然后创建一个 Track 并挂到这个 PlaybackThread 下面。 PlaybackThread 有两个私有成员向量与此强相关:
音频流控制最常用的三个接口:
AudioFlinger::PlaybackThread::threadLoop() 得悉情况有变后,调用 prepareTracks_l() 重新准备音频流和混音器:ACTIVE 状态的 Track 会添加到 mActiveTracks,此外的 Track 会从 mActiveTracks 上移除出来,然后重新准备 AudioMixer。 可见这三个音频流控制接口是非常简单的,主要是设置一下 Track 的状态,然后发个事件通知 PlaybackThread 就行,复杂的处理都在 AudioFlinger::PlaybackThread::threadLoop() 中了。 AudioFlinger 混音器处理// TODO: … AudioTrack 实例创建现在我们开始分析 AudioTrack 的创建过程,特别留意 AudioTrack 与 AudioFlinger 如何建立联系、用于 AudioTrack 与 AudioFlinger 交换数据的匿名共享内存如何分配。 AudioTrack & AudioFlinger 相关类首先看一下 AudioTrack & AudioFlinger 的类图,理一下 AudioFlinger 的主要类及其关系、AudioTrack 与 AudioFlinger 之间的联系,后面将以该图为脉络展开分析。
audio_io_handle_t: 这里再详细说明一下 audio_io_handle_t,它是 AudioTrack/AudioRecord/AudioSystem、AudioFlinger、AudioPolicyManager 之间一个重要的链结点。3.4. AudioFlinger 回放录制线程 小节在 AudioFlinger::openOutput_l() 注释中大致说明了它的来历及其作用,现在回顾下:当打开输出流设备及创建 PlaybackThread 时,系统会分配一个全局唯一的值作为 audio_io_handle_t,并把 audio_io_handle_t 和 PlaybackThread 添加到键值对向量 mPlaybackThreads 中,由于 audio_io_handle_t 和 PlaybackThread 是一一对应的关系,因此拿到一个 audio_io_handle_t,就能遍历键值对向量 mPlaybackThreads 找到它对应的 PlaybackThread,可以简单理解 audio_io_handle_t 为 PlaybackThread 的索引号或线程 id。由于 audio_io_handle_t 具有 PlaybackThread 索引特性,所以应用进程想获取 PlaybackThread 某些信息的话,只需要传入对应的 audio_io_handle_t 即可。例如 AudioFlinger::format(audio_io_handle_t output),这是 AudioFlinger 的一个服务接口,用户进程可以通过该接口获取某个 PlaybackThread 配置的音频格式: AudioTrack 构造过程当我们构造一个 AudioTrack 实例时(以 MODE_STREAM/TRANSFER_SYNC 模式为例,这也是最常用的模式了,此时 sharedBuffer 为空),系统都发生了什么事?阐述下大致流程:
3. 通过 Binder 机制调用 AudioFlinger::createTrack()(注意 step2 中 AudioTrack 已经拿到一个 audio_io_handle_t 了,此时把这个 audio_io_handle_t 传入给 createTrack()): 4.通过 IAudioTrack 接口,取得 AudioFlinger 中的 FIFO 控制块(audio_track_cblk_t),由此再计算得到 FIFO 的首地址; AudioTrack 由此建立了和 AudioFlinger 的全部联系工作:
构造 1 个 AudioTrack 实例时,AudioFlinger 会有 1 个 PlaybackThread 实例、1 个 Track 实例、1 个 TrackHandle 实例、1 个 AudioTrackServerProxy 实例、1 块 FIFO 与之对应。 当同时构造 1 个 AudioTrack with AUDIO_OUTPUT_FLAG_PRIMARY、1 个 AudioTrack with AUDIO_OUTPUT_FLAG_FAST、3 个 AudioTrack with AUDIO_OUTPUT_FLAG_DEEP_BUFFER、1 个 AudioTrack with AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD、1 个 AudioTrack with AUDIO_OUTPUT_FLAG_DIRECT 时(事实上,Android 音频策略不允许出现这种情形的),AudioFlinger 拥有的 PlaybackThread、Track、TrackHandle 实例如下图所示: 最后附上相关代码的流程分析,我本意是不多贴代码的,但不上代码总觉得缺点什么,这里我尽量把代码精简,提取主干,忽略细节。 AudioFlinger::createTrack(),顾名思义,创建一个 Track 对象,将用于音频流的控制: 最后,我们看看 Track 的构造过程,主要分析数据 FIFO 及它的控制块是如何分配的: AudioTrack 数据写入AudioTrack 实例构造后,应用程序接着可以写入音频数据了。如之前所描述:AudioTrack 与 AudioFlinger 是 生产者-消费者 的关系:
上面的过程中,如果 AudioTrack 总能及时生产数据,并且 AudioFlinger 总能及时消耗掉这些数据,那么整个过程将是非常和谐的;但系统可能会发生异常,出现如下的状态:
AudioTrack 写数据流程我们看一下 AudioTrack 写数据的代码,流程很简单:obtainBuffer() 在 FIFO 中找到一块可用区间,memcpy() 把用户传入的音频数据拷贝到这个可用区间上,releaseBuffer() 更新写位置。 AudioFlinger 读数据流程AudioFlinger 消费数据的流程稍微复杂一点,3.4. AudioFlinger 回放录制线程 小节中描述了 AudioFlinger::PlaybackThread::threadLoop() 工作流程,这里不累述了,我们把焦点放在“如何从 FIFO 读取数据”节点上。 我们以 DirectOutputThread/OffloadThread 为例说明(MixerThread 读数据也是类似的过程,只不过是在 AudioMixer 中进行的,3.7. AudioFlinger 混音器处理 小节中有相关描述)。 环形 FIFO 管理在上述过程中,不知大家有无意识到:整个过程中,最难的是如何协调生产者与消费者之间的步调。上文所说的 FIFO 是环形 FIFO,AudioTrack 写指针、AudioFlinger 读指针都是基于 FIFO 当前的读写位置来计算的。
我们回顾下创建 AudioTrack 对象时,FIFO 及其控制块的结构如下所示:
FIFO 管理相关的类图:
到这里,我决定结束本文了。环形 FIFO 管理是 Android 音频系统的精髓,一个小节并不足以描述其原理及实现细节;Android 环形 FIFO 的实现可说得上精妙绝伦,其他项目如果要用到环形 FIFO,不妨多借鉴它。 |
|