深入理解Android音频框架AudioTrack到AudioFlinger及Mix过程

Android 音频框架概述

Android 音频框架

Audio 是整个 Android 平台非常重要的一个组成部分,负责音频数据的采集和输出、音频流的控制、音频设备的管理、音量调节等,Android从7.0开始专门给Audio一个server。在此之前,Audio是在MediaServer中启动Server服务的。Audio主要包括如下部分:

  • Audio Application Framework:音频应用框架
  • AudioTrack:负责回放数据的输出,属 Android 应用框架 API 类
  • AudioRecord:负责录音数据的采集,属 Android 应用框架 API 类
  • AudioSystem: 负责音频事务的综合管理,属 Android 应用框架 API 类
  • Audio Native Framework:音频本地框架
  • AudioTrack:负责回放数据的输出,属 Android 本地框架 API 类
  • AudioRecord:负责录音数据的采集,属 Android 本地框架 API 类
  • AudioSystem: 负责音频事务的综合管理,属 Android 本地框架 API 类
  • Audio Services:音频服务
  • AudioPolicyService:音频策略的制定者,负责音频设备切换的策略抉择、音量调节策略等
  • AudioFlinger:音频策略的执行者,负责输入输出流设备的管理及音频流数据的处理传输
  • Audio HAL:音频硬件抽象层,负责与音频硬件设备的交互,由 AudioFlinger 直接调用

与 Audio 强相关的有 MultiMedia,MultiMedia 负责音视频的编解码,MultiMedia 将解码后的数据通过 AudioTrack 输出,而 AudioRecord 采集的录音数据交由 MultiMedia 进行编码。

播放声音可以用MediaPlayer和AudioTrack,两者都提供了java API供应用开发者使用。虽然都可以播放声音,但两者还是有很大的区别的。其中最大的区别是MediaPlayer可以播放多种格式的声音文件,例如MP3,AAC,WAV,OGG,MIDI等。MediaPlayer会在framework层创建对应的音频解码器。而AudioTrack只能播放已经解码的PCM流,如果是文件的话只支持wav格式的音频文件,因为wav格式的音频文件大部分都是PCM流。AudioTrack不创建解码器,所以只能播放不需要解码的wav文件。当然两者之间还是有紧密的联系,MediaPlayer在framework层还是会创建AudioTrack,把解码后的PCM数流传递给AudioTrack,AudioTrack再传递给AudioFlinger进行混音,然后才传递给硬件播放,所以是MediaPlayer包含了AudioTrack。使用AudioTrack播放音乐示例:

AudioTrack audio = new AudioTrack(       AudioManager.STREAM_MUSIC,      // 指定流的类型       32000, // 设置音频数据的采样率 32k,如果是44.1k就是44100       AudioFormat.CHANNEL_OUT_STEREO,      // 设置输出声道为双声道立体声,而CHANNEL_OUT_MONO类型是单声道       AudioFormat.ENCODING_PCM_16BIT,      // 设置音频数据块是8位还是16位,这里设置为16位。     // 好像现在绝大多数的音频都是16位的了       AudioTrack.MODE_STREAM      // 设置模式类型,在这里设置为流类型,     // 另外一种MODE_STATIC貌似没有什么效果       );  audio.play(); // 启动音频设备,下面开始音频数据的播放了  // 打开mp3文件,读取数据,解码等操作省略 ...  byte[] buffer = new buffer[4096];  int count;  while(true)  {      // 最关键的是将解码后的数据,从缓冲区写入到AudioTrack对象中      audio.write(buffer, 0, 4096);      if(文件结束) break;  }  //关闭并释放资源  audio.stop();  audio.release();  

AudioTrack构造过程

每一个音频流对应着一个AudioTrack类的一个实例,每个AudioTrack会在创建时注册到 AudioFlinger中,由AudioFlinger把所有的AudioTrack进行混合(Mixer),然后输送到AudioHardware中进行播放,目前Android同时最多可以创建32个音频流,也就是说,Mixer最多会同时处理32个AudioTrack的数据流。

AudioTrack与AudioFlinger及AudioMixer关系

我们知道,AudioPolicyService启动时加载了系统支持的所有音频接口,并且打开了默认的音频输出,打开音频输出时,调用AudioFlinger::openOutput()函数为当前打开的音频输出接口创建一个PlaybackThread线程,同时为该线程分配一个全局唯一的audioiohandlet值,并以键值对的形式保存在AudioFlinger的成员变量mPlaybackThreads中。在这里首先根据音频参数通过调用AudioSystem::getOutput()函数得到当前音频输出接口的PlaybackThread线程id号,同时传递给createTrack函数用于创建Track。AudioTrack在AudioFlinger中是以Track来管理的。不过因为它们之间是跨进程的关系,因此需要一个“桥梁”来维护,这个沟通的媒介是IAudioTrack。函数createTrackl除了为AudioTrack在AudioFlinger中申请一个Track外,还会建立两者间IAudioTrack桥梁。

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 时,先陷入短时间的睡眠,不急着读取数据,让应用程序准备更多的数据

获取音频输出

获取音频输出就是根据音频参数如采样率、声道、格式等从已经打开的音频输出描述符列表中查找合适的音频输出AudioOutputDescriptor,并返回该音频输出在AudioFlinger中创建的播放线程id号,如果没有合适当前音频输出参数的AudioOutputDescriptor,则请求AudioFlinger打开一个新的音频输出通道,并为当前音频输出创建对应的播放线程,返回该播放线程的id号

创建AudioTrackThread线程

初始化AudioTrack时,如果audioCallback为Null,就会创建AudioTrackThread线程。 AudioTrack支持两种数据输入方式: 1) Push方式:用户主动write,MediaPlayerService通常采用此方式; 2) Pull方式: AudioTrackThread线程通过audioCallback回调函数主动从用户那里获取数据

申请Track

音频播放需要AudioTrack写入音频数据,同时需要AudioFlinger完成混音,因此需要在AudioTrack与AudioFlinger之间建立数据通道,而AudioTrack与AudioFlinger又分属不同的进程空间,Android系统采用Binder通信方式来搭建它们之间的桥梁。

当应用程序进程中的AudioTrack请求AudioFlinger在某个PlaybackThread中创建Track对象时,AudioFlinger首先会为应用程序进程创建一个Client对象,同时创建一块大小为2M的共享内存。在创建Track时,Track将在2M共享内存中分配buffer用于音频播放。

AudioFlinger 概述

AudioPolicyService 与 AudioFlinger 是 Android 音频系统的两大基本服务。前者是音频系统策略的制定者,负责音频设备切换的策略抉择、音量调节策略等;后者是音频系统策略的执行者,负责音频流设备的管理及音频流数据的处理传输,所以 AudioFlinger 也被认为是 Android 音频系统的引擎。

从 Audio HAL 中,我们通常看到如下 4 种输出流设备,分别对应着不同的播放场景:

  • primaryout:主输出流设备,用于铃声类声音输出,对应着标识为 AUDIOOUTPUTFLAGPRIMARY 的音频流和一个 MixerThread 回放线程实例
  • lowlatency:低延迟输出流设备,用于按键音、游戏背景音等对时延要求高的声音输出,对应着标识为 AUDIOOUTPUTFLAGFAST 的音频流和一个 MixerThread 回放线程实例
  • deepbuffer:音乐音轨输出流设备,用于音乐等对时延要求不高的声音输出,对应着标识为 AUDIOOUTPUTFLAGDEEP_BUFFER 的音频流和一个 MixerThread 回放线程实例
  • compressoffload:硬解输出流设备,用于需要硬件解码的数据输出,对应着标识为 AUDIOOUTPUTFLAGCOMPRESS_OFFLOAD 的音频流和一个 OffloadThread 回放线程实例

其中 primaryout 设备是必须声明支持的,而且系统启动时就已经打开 primaryout 设备并创建好对应的 MixerThread 实例。其他类型的输出流设备并非必须声明支持的,主要是看硬件上有无这个能力。

可能有人产生这样的疑问:既然 primary_out 设备一直保持打开,那么能耗岂不是很大?这里阐释一个概念:输出流设备属于逻辑设备,并不是硬件设备。所以即使输出流设备一直保持打开,只要硬件设备不工作,那么就不会影响能耗。那么硬件设备什么时候才会打开呢?答案是 PlaybackThread 将音频数据写入到输出流设备时。

下图简单描述 AudioTrack、PlaybackThread、输出流设备三者的对应关系:

PlaybackThread&StreamDevice

我们可以这么说:输出流设备决定了它对应的 PlaybackThread 是什么类型。怎么理解呢?意思是说:只有支持了该类型的输出流设备,那么该类型的 PlaybackThread 才有可能被创建。举个例子:只有硬件上具备硬件解码器,系统才建立 compressoffload 设备,然后播放 mp3 格式的音乐文件时,才会创建 OffloadThread 把数据输出到 compressoffload 设备上;反之,如果硬件上并不具备硬件解码器,系统则不应该建立 compress_offload 设备,那么播放 mp3 格式的音乐文件时,通过 MixerThread 把数据输出到其他输出流设备上。

那么有无可能出现这种情况:底层并不支持 compressoffload 设备,但偏偏有个标识为 AUDIOOUTPUTFLAGCOMPRESSOFFLOAD 的音频流送到 AudioFlinger 了呢?这是不可能的。系统启动时,会检查并保存输入输出流设备的支持信息;播放器在播放 mp3 文件时,首先看 compressoffload 设备是否支持了,如果支持,那么不进行软件解码,直接把数据标识为 AUDIOOUTPUTFLAGCOMPRESSOFFLOAD;如果不支持,那么先进行软件解码,然后把解码好的数据标识为 AUDIOOUTPUTFLAGDEEPBUFFER,前提是 deepbuffer 设备是支持了的;如果 deepbuffer 设备也不支持,那么把数据标识为 AUDIOOUTPUTFLAG_PRIMARY。

AudioTrack和AudioFlinger的通信机制

通常,AudioTrack和AudioFlinger并不在同一个进程中,它们通过android中的binder机制建立联系。 AudioFlinger是android中的一个service,在android启动时就已经被加载。

我们可以这样理解:

  • audiotrackcblk_t实现了一个环形FIFO;
  • AudioTrack是FIFO的数据生产者;
  • AudioFlinger是FIFO的数据消费者。

建立联系的过程:

  • Framework或者Java层通过JNI,new AudioTrack();
  • 根据StreamType等参数,通过一系列的调用getOutput();
  • 如有必要,AudioFlinger根据StreamType打开不同硬件设备;
  • AudioFlinger为该输出设备创建混音线程: MixerThread(),并把该线程的id作为getOutput()的返回值返回给AudioTrack;
  • AudioTrack通过binder机制调用AudioFlinger的createTrack();
  • AudioFlinger注册该AudioTrack到MixerThread中;
  • AudioFlinger创建一个用于控制的TrackHandle,并以IAudioTrack这一接口作为createTrack()的返回值;
  • AudioTrack通过IAudioTrack接口,得到在AudioFlinger中创建的FIFO(audiotrackcblk_t);
  • AudioTrack创建自己的监控线程:AudioTrackThread;

自此,AudioTrack建立了和AudioFlinger的全部联系工作,接下来,AudioTrack可以:

  • 通过IAudioTrack接口控制该音轨的状态,例如start,stop,pause等等;
  • 通过对FIFO的写入,实现连续的音频播放;
  • 监控线程监控事件的发生,并通过audioCallback回调函数与用户程序进行交互;

环形FIFO的管理

audiotrackcblkt audiotrackcblkt这个结构是FIFO实现的关键,该结构是在createTrack的时候,由AudioFlinger申请相 应的内存,然后通过IMemory接口返回AudioTrack的,这样AudioTrack和AudioFlinger管理着同一个 audiotrackcblk_t,通过它实现了环形FIFO,AudioTrack向FIFO中写入音频数据,AudioFlinger从FIFO 中读取音频数据,经Mixer后送给AudioHardware进行播放。

audiotrackcblk_t的主要数据成员:

  • user -- AudioTrack当前的写位置的偏移
  • userBase -- AudioTrack写偏移的基准位置,结合user的值方可确定真实的FIFO地址指针
  • server -- AudioFlinger当前的读位置的偏移
  • serverBase -- AudioFlinger读偏移的基准位置,结合server的值方可确定真实的FIFO地址指针
  • frameCount -- FIFO的大小,以音频数据的帧为单位,16bit的音频每帧的大小是2字节
  • buffers -- 指向FIFO的起始地址
  • out -- 音频流的方向,对于AudioTrack,out=1,对于AudioRecord,out=0

最后到达AudioMixer,做混音处理。

AudioMixer

AudioMixer是Android的混音器,通过混音器可以把各个音轨的音频数据混合在一起,然后输出到音频设备。

关于混音,混音以track为源,mainBuffer为目标,frameCount为一次混音长度。AudioMixer最多能维护32个track。track可以对应不同mainBuffer,尽管一般情况下他们的mainBuffer都是同一个。

最终调用了resampler的resample方法进行重采样。

原文发布于微信公众号 - 何俊林(DriodDeveloper)

原文发表时间:2017-11-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏草根专栏

.NET Core TDD 前传: 编写易于测试的代码 -- 依赖项

还是使用建造汽车的例子. 生产汽车的时候需要轮胎, 组装时需要什么型号的轮胎, 就请求该型号的轮胎, 然后相关人员会从库房把该型号的轮胎送到产线用于组装. 

8920
来自专栏张善友的专栏

Web 单点登录系统

对于企业内部系统来说,CAS系统是一个应用最广的开源单点登陆实现了,其实现模仿Kerberos的一些概念,例如KDC、TGS等等,都是来自于Kerberos。具...

341100
来自专栏UE4技术专场

UE4物理制作流程和规范

n 通俗的讲就是自己是什么,每一个可以碰撞的对象都会有一种碰撞类型,并且定义了它和别的对象类型之间的交互响应,主要是用来处理物体和物体之间运动的时候碰撞的关系

61160
来自专栏疯狂的小程序

次次获得《头脑王者》满分的秘诀

最近答题类的应用实在是太火了,什么冲顶大会、百万英雄啊,动不动就几十上百万的奖金,着实让人看着很是眼红...然后本弱鸡学疏才浅...题目全靠蒙,便不凑什么热闹了...

28590
来自专栏Java后端技术栈

记录一次壮烈牺牲的阿里巴巴面试!

今天本是一个阳光明媚,鸟语花香的日子。于是我决定在逛街中感受春日的阳光~结果晚上七点的时候,蚂蚁金服后端大佬来了电话,要进行一轮的技术面试。我一脸黑人问号???...

9710
来自专栏Urahara Blog

Smbtouch漏洞判断与FuzzBunch攻击说明

40430
来自专栏阮一峰的网络日志

如何制作DVDrip?

我就研究了一下,如何制作DVDrip。下面是我的一点笔记。因为我对视频转换其实完全不懂,所以说得不对的地方,欢迎大家指正。如果你知道其他好的工具软件,也欢迎推荐...

54320
来自专栏CDA数据分析师

一个 Pythoner的 Awesome List

? 从大三接触 Python 到现在几乎已经有两年的接触经验了,除去中间有一年左右接私活写写 Android 和 Lamp 之外,有 Python 实际项目开...

31060
来自专栏iOSDevLog

聊天机器人教学:使用Dialogflow (API.AI)开发 iOS Chatbot App

1.2K30
来自专栏清晨我上码

国标协议解析

2323 起始标示 02 命令标示(这里表示实时数据上报) FE 应答标示 4C4C584132413430344A41303030303231 vi码...

19910

扫码关注云+社区

领取腾讯云代金券