Android N的Audio系统(六)

  • 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。

udioFlinger 音频流管理由 AudioFlinger::PlaybackThread::Track 实现,Track 与 AudioTrack 是一对一的关系,一个 AudioTrack 创建后,那么 AudioFlinger 会创建一个 Track 与之对应;PlaybackThread 与 AudioTrack/Track 是一对多的关系,一个 PlaybackThread 可以挂着多个 Track。

具体来说:AudioTrack 创建后,AudioPolicyManager 根据 AudioTrack 的输出标识和流类型,找到对应的输出流设备和 PlaybackThread(如果没有找到的话,则系统会打开对应的输出流设备并新建一个 PlaybackThread),然后创建一个 Track 并挂到这个 PlaybackThread 下面。

PlaybackThread 有两个私有成员向量与此强相关:

mTracks:该 PlaybackThread 创建的所有 Track 均添加保存到这个向量中

mActiveTracks:只有需要播放(设置了 ACTIVE 状态)的 Track 会添加到这个向量中;PlaybackThread 会从该向量上找到所有设置了 ACTIVE 状态的 Track,把这些 Track 数据混音后写到输出流设备

音频流控制最常用的三个接口:

AudioFlinger::PlaybackThread::Track::start:开始播放:把该 Track 置 ACTIVE 状态,然后添加到 mActiveTracks 向量中,最后调用 AudioFlinger::PlaybackThread::broadcast_l() 告知 PlaybackThread 情况有变

AudioFlinger::PlaybackThread::Track::stop:停止播放:把该 Track 置 STOPPED 状态,最后调用 AudioFlinger::PlaybackThread::broadcast_l() 告知 PlaybackThread 情况有变

AudioFlinger::PlaybackThread::Track::pause:暂停播放:把该 Track 置 PAUSING 状态,最后调用 AudioFlinger::PlaybackThread::broadcast_l() 告知 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 之间的联系,后面将以该图为脉络展开分析。

这里写图片描述

AudioFlinger::PlaybackThread:回放线程基类,不同输出标识的音频流对应不同类型的 PlaybackThread 实例(分为四种:MixerThread、DirectOutputThread、DuplicatingThread、OffloadThread),具体见 3.4. AudioFlinger 回放录制线程 小节,所有的 PlaybackThread 实例都会添加到 AudioFlinger.mPlaybackThreads 向量中;这个向量的定义: DefaultKeyedVector< audio_io_handle_t, sp> mPlaybackThreads;,可见 audio_io_handle_t 是与 PlaybackThread 是一一对应的,由已知的 audio_io_handle_t 就能找到对应的 PlaybackThread;audio_io_handle_t 在创建 PlaybackThread 时由系统分配,这个值是全局唯一的。

AudioFlinger::PlaybackThread::Track:音频流管理类,创建一块匿名共享内存用于 AudioTrack 与 AudioFlinger 之间的数据交换(方便起见,这块匿名共享内存,以后均简单称为 FIFO),同时实现 start()、stop()、pause() 等音频流常用控制手段;注意,多个 Track 对象可能都注册到同一个 PlaybackThread 中(尤其对于 MixerThread 而言,一个 MixerThread 往往挂着多个 Track 对象),这多个 Track 对象都会添加到 PlaybackThread.mTracks 向量中统一管理

AudioFlinger::TrackHandle:Track 对象只负责音频流管理业务,对外并没有提供跨进程的 Binder 调用接口,而应用进程又需要对音频流进行控制,所以需要一个对象来代理 Track 的跨进程通讯,这个角色就是 TrackHandle,AudioTrack 通过它与 Track 交互

AudioTrack:Android 音频系统对外提供的一个 API 类,负责音频流数据输出;每个音频流对应着一个 AudioTrack 实例,不同输出标识的 AudioTrack 会匹配到不同的 AudioFlinger::PlaybackThread;AudioTrack 与 AudioFlinger::PlaybackThread 之间通过 FIFO 来交换音频数据,AudioTrack 是 FIFO 生产者,AudioFlinger::PlaybackThread 是 FIFO 消费者

AudioTrack::AudioTrackThread:数据传输模式为 TRANSFER_CALLBACK 时,需要创建该线程,它通过调用 audioCallback 回调函数主动从用户进程处索取数据并填充到 FIFO 上;数据传输模式为 TRANSFER_SYNC 时,则不需要创建这个线程,因为用户进程会持续调用 AudioTrack.write() 填充数据到 FIFO;数据传输模式为 TRANSFER_SHARED 时,也不需要创建这个线程,因为用户进程会创建一块匿名共享内存,并把要播放的音频数据一次性拷贝到这块匿名共享内存上了

IAudioTrack:IAudioTrack 是链结 AudioTrack 与 AudioFlinger 的桥梁;它在 AudioTrack 端的对象是 BpAudioTrack,在 AudioFlinger 端的对象是 BnAudioTrack,从图中不难看出,AudioFlinger::TrackHandle 继承自 BnAudioTrack,而 AudioFlinger::TrackHandle 恰恰是AudioFlinger::PlaybackThread::Track 的代理对象,所以 AudioTrack 得到 IAudioTrack 实例后,就可以调用 IAudioTrack 的接口与 AudioFlinger::PlaybackThread::Track 交互
  • audio_io_handle_t:

这里再详细说明一下 audio_io_handle_t,它是 AudioTrack/AudioRecord/AudioSystem、 AudioFlinger、AudioPolicyManager 之间一个重要的链结点。
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 为空),系统都发生了什么事?阐述下大致流程:

如果 cbf(audioCallback 回调函数)非空,那么创建 AudioTrackThread 线程处理 audioCallback 回调函数(MODE_STREAM 模式时,cbf 为空);

根据 streamType(流类型)、flags(输出标识)等参数调用 AudioSystem::getOutputForAttr();经过一系列的调用,进入 AudioPolicyManager::getOutputForDevice():

    如果输出标识置了 AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD 或 AUDIO_OUTPUT_FLAG_DIRECT,那么最终调用 AudioFlinger::openOutput() 打开输出标识对应的输出流设备并创建相应的 PlaybackThread,保存该 PlaybackThread 对应的 audio_io_handle_t 给 AudioTrack;

    如果输出标识是其他类型,那么根据策略选择一个输出流设备和 PlaybackThread,并保存该 PlaybackThread 对应的 audio_io_handle_t 给 AudioTrack;别忘了在 3.4. AudioFlinger 回放录制线程 小节中提到:系统启动时,就已经打开 primary_out、low_latency、deep_buffer 这三种输出流设备,并创建对应的 PlaybackThread 了;

3. 通过 Binder 机制调用 AudioFlinger::createTrack()(注意 step2 中 AudioTrack 已经拿到一个 audio_io_handle_t 了,此时把这个 audio_io_handle_t 传入给 createTrack()):

    根据传入的 audio_io_handle_t 找到它对应的 PlaybackThread;

    PlaybackThread 新建一个音频流管理对象 Track;Track 构造时会分配一块匿名共享内存用于 AudioFlinger 与 AudioTrack 的数据交换缓冲区(FIFO)及其控制块(audio_track_cblk_t),并创建一个 AudioTrackServerProxy 对象(PlaybackThread 将使用它从 FIFO 上取得可读数据的位置);

    最后新建一个 Track 的通讯代理 TrackHandle,并以 IAudioTrack 作为返回值给 AudioTrack;

4.通过 IAudioTrack 接口,取得 AudioFlinger 中的 FIFO 控制块(audio_track_cblk_t),由此再计算得到 FIFO 的首地址;

5.创建一个 AudioTrackClientProxy 对象(AudioTrack 将使用它从 FIFO 上取得可用空间的位置);

AudioTrack 由此建立了和 AudioFlinger 的全部联系工作:

通过 IAudioTrack 接口可以控制该音轨的状态,例如 start、stop、pause持续写入数据到 FIFO 上,实现音频连续播放

通过 audio_io_handle_t,可以找到它对应的 PlaybackThread,从而查询该 PlaybackThread 的相关信息,如所设置的采样率、格式等等;

构造 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:AudioTrack 在 FIFO 中找到一块可用空间,把用户传入的音频数据写入到这块可用空间上,然后更新写位置(对于 AudioFinger 来说,意味 FIFO 上有更多的可读数据了);如果用户传入的数据量比可用空间要大,那么要把用户传入的数据拆分多次写入到 FIFO 中(AudioTrack 和 AudioFlinger 是不同的进程,AudioFlinger 同时也在不停地读取数据,所以 FIFO 可用空间是在不停变化的)

AudioFlinger:AudioFlinger 在 FIFO 中找到一块可读数据块,把可读数据拷贝到目的缓冲区上,然后更新读位置(对于 AudioTrack 来说,意味着 FIFO 上有更多的可用空间了);如果FIFO 上可读数据量比预期的要小,那么要进行多次的读取,才能积累到预期的数据量(AudioTrack 和 AudioFlinger 是不同的进程,AudioTrack 同时也在不停地写入数据,所以 FIFO 可读的数据量是在不停变化的)

上面的过程中,如果 AudioTrack 总能及时生产数据,并且 AudioFlinger 总能及时消耗掉这些数据,那么整个过程将是非常和谐的;但系统可能会发生异常,出现如下的状态:

Block:AudioFlinger 长时间不读取 FIFO 上的可读数据,使得 AudioTrack 长时间获取不到可用空间,无法写入数据;这种情况的根本原因大多是底层驱动发生阻塞异常,导致 AudioFlinger 无法继续写数据到硬件设备中,AudioFlinger 本身并没有错

Underrun:AudioTrack 写入数据的速度跟不上 AudioFlinger 读取数据的速度,使得 AudioFlinger 不能及时获取到预期的数据量,反映到现实的后果就是声音断续;这种情况的根本原因大多是应用程序不能及时写入数据或者缓冲区分配过小,AudioTrack 本身并没有错;AudioFlinger 针对这点做了容错处理:当发现 underrun 时,先陷入短时间的睡眠,不急着读取数据,让应用程序准备更多的数据。
  • AudioTrack 写数据流程

我们看一下 AudioTrack 写数据的代码,流程很简单:obtainBuffer() 在 FIFO 中找到一块可用区间,memcpy() 把用户传入的音频数据拷贝到这个可用区间上,releaseBuffer() 更新写位置。

这里写图片描述

  • AudioFlinger 读数据流程

AudioFlinger 消费数据的流程稍微复杂一点, AudioFlinger 回放录制线程 小节中描述了 AudioFlinger::PlaybackThread::threadLoop() 工作流程,这里不累述了,我们把焦点放在“如何从 FIFO 读取数据”节点上。

我们以 DirectOutputThread/OffloadThread 为例说明(MixerThread 读数据也是类似的过程,只不过是在 AudioMixer 中进行的,AudioFlinger 混音器处理 小节中有相关描述)。

这里写图片描述

  • 环形 FIFO 管理

在上述过程中,不知大家有无意识到:整个过程中,最难的是如何协调生产者与消费者之间的步调。上文所说的 FIFO 是环形 FIFO,AudioTrack 写指针、AudioFlinger 读指针都是基于 FIFO 当前的读写位置来计算的。

AudioTrack 与 AudioFlinger 不在同一个进程上,怎么保证读写指针的线程安全

读写指针越过 FIFO 后,怎么处理

AudioTrack 写数据完成后,需要同步状态给 AudioFlinger,让 AudioFlinger 知道当前有可读数据了,而 AudioFlinger 读数据完成后,也需要同步状态给 AudioTrack,让 AudioTrack 知道当前有可用空间了;这里采取什么同步机制

我们回顾下创建 AudioTrack 对象时,FIFO 及其控制块的结构如下所示:

MODE_STREAM 模式下的匿名共享内存结构:
这里写图片描述

MODE_STATIC 模式下的匿名共享内存结构:

这里写图片描述

FIFO 管理相关的类图:

这里写图片描述

AudioTrackClientProxy:MODE_STREAM 模式下,生产者 AudioTrack 使用它在 FIFO 中找到可用空间的位置

AudioTrackServerProxy:MODE_STREAM 模式下,消费者 AudioFlinger::PlaybackThread 使用它在 FIFO 中找到可读数据的位置

StaticAudioTrackClientProxy:MODE_STATIC 模式下,生产者 AudioTrack 使用它在 FIFO 中找到可用空间的位置

StaticAudioTrackServerProxy:MODE_STATIC 模式下,消费者 AudioFlinger::PlaybackThread 使用它在 FIFO 中找到可读数据的位置

AudioRecordClientProxy:消费者 AudioRecord 使用它在 FIFO 中找到可读数据的位置

AudioTrackServerProxy:生产者 AudioFlinger::RecordThread 使用它在 FIFO 中找到可用空间的位置

环形 FIFO 管理是 Android 音频系统的精髓,一个小节并不足以描述其原理及实现细节;Android 环形 FIFO 的实现可说得上精妙绝伦,其他项目如果要用到环形 FIFO,不妨多借鉴它。

  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值