从音频原理
,音频架构
谈到播放录制,音量调节,通道切换,蓝牙相关等代码解析。(超级长文预警)
专栏相关的介绍,可以查看这篇文章《专栏介绍》
下面是全文,权作记录了,如果能额外帮到您,那算是额外的收获 😄
本文参考多本书籍以及互联网资料,只在内部交流使用,所以不注明相关参考资料.
All right reserved by wizard merlin, viewers’ discretion is advised.
目录:
1.基本原理:(数字化过程/采样过程/录制过程/播放过程/)
2.音频架构: (顶级抽象图/次级抽象图/分层模型/终极抽象图/究极分析图)
———–看热闹的外行可以退场了, 下面要讲的东西及其冗长,枯燥,无聊——
3.音频代码: (音频代码全解析/究极分析图real )
4.AudioPolicyService————–讲代码全解析的时候发现策略太复杂了
5.播放过程:(AudioTrack)—构造构成,启动过程,数据写入过程,停止过程
6.录制过程:(AudioRecord)
7.音量调节过程 (故障貌似主要发生在上层)
8.通道切换过程 (在4的基础上强调bug solving)
9.蓝牙相关音频 (What the hell is this?)
基本原理
音乐的基本属性: 响度, 音调, 音色.
数字化的过程
(基本就是: 模拟信号,电信号,数字信号,机械振动—–推荐书籍<果壳中的宇宙> <超弦理论>, 推荐理由: 振动!)
采样过程
将声波波形信号通过ADC转换成计算机支持的二进制的过程叫做音频采样(Audio Sampling)。
采样(Sampling)的核心是把连续的模拟信号转换成离散的数字信号。
样本(Sample)
这是我们进行采样的初始资料,比如一段连续的声音波形。
_采样器(Sampler)
采样器是将样本转换成终态信号的关键。它可以是一个子系统,也可以指一个操作过程,甚至是一个算法,取决于不同的信号处理场景。理想的采样器要求尽可能不产生信号失真。
量化(Quantization)
采样后的值还需要通过量化,也就是将连续值近似为某个范围内有限多个离散值的处理过程。因为原始数据是模拟的连续信号,而数字信号则是离散的,它的表达范围是有限的,所以量化是必不可少的一个步骤。
编码(Coding)
计算机的世界里,所有数值都是用二进制表示的,因而我们还需要把量化值进行二进制编码。这一步通常与量化同时进行。(有时候会进行压缩)
补充:
奈奎斯特采样理论:
“当对被采样的模拟信号进行还原时,其最高频率只有采样频率的一半”
换句话说,如果我们要完整重构原始的模拟信号,则采样频率就必须是它的两倍以上。
比如人的声音范围是2~ 20kHZ,那么选择的采样频率就应该在40kHZ左右,数值太小则声音将产生失真现象,而数值太大也无法明显提升人耳所能感知的音质。
录制过程
- 音频采集设备(比如Microphone)捕获声音信息。
- 模拟信号通过模数转换器(ADC)处理成计算机能接受的二进制数据。
- 根据需求进行必要的渲染处理,比如音效调整、过滤等等。
- 处理后的音频数据理论上已经可以存储到计算机设备中,比如硬盘、USB设备等等。
- 不过由于这时的音频数据体积相对庞大,不利于保存和传输,通常还会对其进行压缩处理。
- 比如我们常见的mp3音乐,实际上就是对原始数据采用相应的压缩算法后得到的。
- 压缩过程根据采样率、位深等因素的不同,最终得到的音频文件可能会有一定程度的失真
另外,音视频的编解码既可以由纯软件完成,也同样可以借助于专门的硬件芯片来完成
播放过程
- 从存储设备中取出相关文件,并根据录制过程采用的编码方式进行相应的解码
- 音频系统为这一播放实例选定最终匹配的音频回放设备
- 解码后的数据经过音频系统设计的路径传输
- 音频数据信号通过数模转换器(DAC)变换成模拟信号
- 模拟信号经过回放设备,还原出原始声音
请注意上面的 2 & 3 经过音频系统的那个俩关键步骤: 一个是音频系统, 一个是播放设备(工作中还有一个要关心的是录制设备)
音频架构(Android)
顶层抽象
从硬件往上看:
硬件—>寄存器—> C接口(可能是内核)—->Linux/Windows/Android驱动—>国际标准(协议之类的,音视频的标准)—>服务系统/信息系统—->App
次级抽象
(个人认为,之所以在Audio Lib上在进行一次抽象的原因是, Lib的API对于快速开发效率不够,为了照顾相当一部分上层开发人员,
才架构了上层系统; 第二个原因是, 直接调用Lib API的风险,以及出bug的可能性会更高,因为原始的API要求调用者的品质&素质非常好)
分层模型:
软件工程学思想–
一般分层的好处就是利于抽象,解耦和;其实最终目的就是降低复杂度,把原来杂乱,功能复杂,操作步骤繁多的内容细化;利于人员分工.
计算机科学思想—-
凡是原来难以解决的问题,都可以通过增加中间层,使得问题的规模得以讲解.(这话是计算机科学家说的,那个做编译器的)
架构师思想—-
多搞出点儿事儿,好骗老板的钱: 看,这个项目还是挺复杂的…….经费什么的…
App层: 上层开发人员调用封装好的API, 做个播放器啥的.(更多的关注业务,设计,产品; 代码并不是其核心关注点)
Framework层: 相当于c-s模型的服务端,App层作为client层,中间衔接服务(转发)由Framework层解决..就相当于一座桥
- AudioTrack和AudioRecorder(MediaPlayer/MediaRecorder提供了更强大的控制功能,相比前者也更易于使用)
- Android系统还为我们控制音频系统提供了AudioManager、AudioService及AudioSystem类, 但相关的类,都可以在库中找到.
Libraries层: 主要的库不多,从上到下, 核心的 libmedia.so, libaudiofinger.so, libmediaplayer.so,
( libstagefright.so, libeffects.so, libvisualizer.so ….),以及HAL层的libaudio.so ——-代码路径我不写了,做的驱动的人都知道; 非相关人员, 我说了也不白说.
(忘了说了, 这一层的AudioFlinger是 “侠之大者,为国为民国” , 不过我称之为: 避震器 (说HAL层的时候就能够看到它的功能))
HAL层: (这个最烦人了,和高通的恩恩怨怨都在这———一句话,代码不是我写的,我哪知道问题出在哪?别找我,我不说—下面细细的说一下)
从设计上来看,硬件抽象层是AudioFlinger直接访问的对象(个人认为AudioFlinger属于HAL的Client, Libs的Server)
这说明了两个问题:
- AudioFlinger并不直接调用底层的驱动程序
- AudioFlinger上层模块只需要与它进行交互就可以实现音频相关的功能了
可以认为AudioFlinger是Android音频系统中真正的“避震器”,无论下面如何变化,上层的实现都可以保持兼容. 音频方面的硬件抽象层主要分为两部分,即AudioFlinger和AudioPolicyService. (HAL层抓audio_module以及audio_policy_module就可以了)
实际上AudioPolicyService并不是一个真实的设备,只是采用虚拟设备的方式来让厂商可以方便地定制出自己的策略.
Android系统本身就很复杂,复杂性不同于上世纪或者本世纪IBM啊,Windows啊提供的信息系统,他的复杂性在于要兼容许多硬件厂家的设备(以此耍流氓,扰乱市场,破坏大盘安定,借机占个地盘儿), 那AudioFlinger/AudioPolicy本身不能直接操作硬件,所以就加了一层,HAL层,来解决兼容性问题. 换句简单的话说: 抽象层的任务是将AudioFlinger/AudioPolicyService真正地与硬件设备关联起来,但又必须提供灵活的结构来应对变化.(HAL就是接口层, 厂商去实现接口或者称为和AudioFlinger/AudioPolicyService通信的接口—–经验丰富的码农都知道,我不解释了)
那我还是解释一下, 接口是啥?….
就是audio_hw_device、audio_stream_in及audio_stream_out等等(这些结构体大多是函数指针.
当AudioFlinger/AudioPolicyService初始化时,它们会去寻找系统中最匹配的实现(就是audio.primary.,audio.a2dp.)来填充这些“函数指针”.——-面向接口编程,就是面向抽象; 而面向抽象,就是解决现实中复杂的问题.
根据产品的不同,音频设备存在很大差异,在Android的音频架构中,这些问题都是由HAL层的audio.primary等等库来解决的,而不需要大规模地修改上层实现。换句话说,厂商在定制时的重点就是如何提供这部分库的高效实现了.
也就是说,一个码农的软硬件修养, 大部分得以在HAL层体现.
下面我们说一说, audio.primary.*为名字的核心库—————不说了,上游厂商实现的.
上面讲的是不是很简单, audio部分本身是不复杂的; 但是一旦涉及到多种参差不齐的硬件厂商,问题就多了.
终极抽象图
Framework <——JNI——–> Libs (.cpp文件不是I开头)
Lib: Client <——Binder——> Lib: Server (.cpp文件以I开头)
代码的核心内容是这样的:
AudioRcorder和AudioTrack是Audio系统对外提供API类,AudioRcorder主要用于完成音频数据的采集,而AudioTrack则是负责音频数据的输出
AudioFlinger管理着系统中的输入输出音频流,并承担着音频数据的混合,通过读写Audio硬件实现音频数据的输入输出功能;
AudioPolicyService是Audio系统的策略控制中心,掌管系统中声音设备的选择和切换、音量控制等.
AudioSystem是AudioFlinger和AudioPolicyService对外的接口 (可以观察下AudioSystem.cpp保留IAudioFlinger对象以及IAudioPolicyManager对象看出来)
(上面图的audio libs层server端,看最新的代码已经调整了libaudiopolicy*.so—划分的更加详细
————-总体上不影响(下面代码部分会详细说明))
如果上面都明白了, 就会立马知道AudioTrack是怎么和AudioTrack有一腿的? 答案: IPC
音频代码
(给我五毛钱, 我认真讲; 不然我一笔带过)
音频代码全解析
大致的样子就是: JAVA–JNI–(NATIVE–LIB)–HAL
(我大致把nitive—lib的关系解释为client-server—-按照binder的调用来看)
一笔带过的讲法:
好吧,还是详细的说:
Java层: 尽管android.media.*存在Java层的Binder IPC,但是总体来说业务逻辑都是很清晰的,略.
(主要看AudioSystem.java, AudioTrack.java, AudioManager.java等)
JNI层: (JNI或许都不能叫做一层, 只能叫做一个机制)
查看同级的makefile文件,发现: JNI编译到libandroid_runtime.so里面.——也就是说通常不会有问题.
LOCAL_MODULE:= libandroid_runtime
NATIVE层 client
: (专指libmedia.so)—– server是audioflinger.so
(之前的版本代码路径在framework/base/media/libmedia)
现在改到framework/av/include/media 以及framework/av/media/libmedia下面.
这部分,不用看,肯定编译到 libmedia.so, 我用脚趾头想都知道.
这里代码大致功能:(其实完全可以不看, 代码就是手册, 有问题来查看一下就可以了)
- 有I接口的,如IAudioTrack, IAudioRecorder这个不用看, 一看就知道是通过接口调用Binder服务端.
- 谁啊? —–AudioFlinger啊, 它是Audio系统的本地服务端, 就是server.
- 例如:IAudioFlinger.h、IAudioTrack.h 和IAudioRecorder.h 这三个接口就是通过下层来实现, libaudioflinger.so
- 没有I接口的,如 AudioTrack, AudioRecorder, 这些一看就是JNI调用的(实际是Java层调用), 完事儿再利用这些类中调用”带有I”的接口.
- 实际上AudioSystem.h、AudioTrack.h 和AudioRecorder.h 中的接口既供本地程序调用,也可以通过JNI 向Java 层提供接口。
- AudioPolicy就不说了,控制路由策略.
- AudioSystem 负责的是Audio 系统的综合管理功能(什么具体音量等级啊乱七八糟的)
- AudioTrack 和AudioRecorder 分别负责音频数据的输出和输入,即播放和录制
注意: 有一个”奇怪的” “IAudioFlingerClient”—-看名字,根据编码经验,应该是当IAudioFlinger被调用的时候, 实现回调client端某个实例,达到返回服务端即audioflinger相关状态的意图(intent)————–来当场,查一下代码手册,一看究竟.
总结一下: android.media.*
———> JNI ——–> AudioTrack(当然本地也可以调用) ——> IAudioTrack ——–>libaudioflinger.so
NATIVE server端
: (libaudioflinger.so, libaudiopolicyservice.so, libaudiopolicymanager.so)
原来的目录: frameworks/base/libs/audioflinger
后来调整到了 frameworks/av/services下面 (越来越规范, services目录嘛,一看就知道是服务端)
audiofinger.so承担的作用还是蛮多的: (看我圈起来的)
功能虽然多,但概括起来就两句:
1.AudioFlinger继承libmeida中的 “带I” 接口,提供实现库libaudiofilnger.so
(这部分内容没有自己的对外头文件,上层调用的只是libmedia本部分的接口(“不带I”),但实际调用的内容是libaudioflinger.so)
2.通过在线程中读写audio硬件,实现audio的输入输出
(AudioFilinger中的实现, 调用HAL层提供的硬件接口————–HAL层的接口是标准定义的,但是实现却是平台差异的)
HAL层:
终于到了跨世纪, 超越黑洞速度的HAL层
———————从阿尔法世界线, 跳跃到贝塔世界线,可能只是时间轴上的一次平行跳跃,但是你确实进入了不同的世界.
上面已经说了audioflinger肯定会调用这边儿hal层的接口————-这只是说了一个方面 (HAL 层libaudio.so),还有一个重要方面是audiopolicy.
HAL 层hardware/
Audio的硬件抽象层实际上是各个平台开发过程中需要主要关注和独立完成的部分,(你看到qcom_audio/下面就会根据平台不同, 具体的目录根据平台不同而不同):
先说标准接口: libhardware_legacy.so 这个是直接被libaudiofilinger.so, libaudiopolicyservice.so等调用的接口的默认实现
实际上接口都放在hardware\libhardware\include\hardware (这个里面定义了所有hardware的接口,只需要看audio.h就行)
这里看默认实现就行(它提供了一种厂商默认的样例或者模板代码)
HAL:
头文件里面: libhardware_legacy/include/AudioHardwareInterface.h: class AudioStreamOut, class AudioStreamIn.
也就是说, 上面类的实例要在厂商的库里面实现, 至少提供上面三个类的实例.
———再接着看默认实现:
实现文件: libhardware_legacy\libhardware_legacy\audio\audio_hw_hal.cpp
现在更清楚了, 给的默认实现做了封装, 也就是说, 厂商现在要提供3个东西(结构体):
1.legacy_audio_device
2.legacy_stream_out
3.legacy_stream_in
解开封装体, 实际上实现两类实例对象就可以了:
1.AudioHardwareInterface的实现类, 以提供AudioHardInterface & AudioStreamOut & AudioStreamIn的实例—cpp文件使用
2.stream (类型分别为 audio_hw_device
, audio_stream_out
, audio_stream_in
)
仔细查找hardware目录: audio_hw_device
, audio_stream_out
, audio_stream_in
, 应该在这两个头文件中:
在audio.h中
(audio_stream_out以及audio_stream_in中又引用了同一文件的另一个结构体audio_stream———-相当于把公共部分提取出来了)
一般定义struct 和 class做声明,其实意义是一样的 (一个是c语言中的常用伎俩, 另外一个是cpp面向的手段)
感觉有些冗余? —–相关人员说, 为了兼容以前的版本?
我其实是不知道的, 但是我想验证一下, 从默认的实现中找点儿线索?
————理一下思路,看看别人是不是在胡说八道:
先去看下AudioFlinger.cpp怎么调用的:
先记住AudioFlinger.cpp这里调用的是set_mode(…):
追下去: AudioHwDevice.h:
在这个audio.h中该结构体中还定义了相关的函数指针: (也就是取得了一个结构体的函数指针)
(其中发现 set_mode正是在AudioFlinger中调用的, 往上看)
那么可以知道, 在audio_hw.c一定会有相关实现 函数(很可能就是把另外一个函数的首地址,赋值给该函数指针)
利用函数指针, 很容易进行函数调用, 以及指针赋值(从而给予不同的实现体)
(下面的情况就是, 把一个具体的实现)
看个例子就知道了: (注意看 audio_hw.c里面的实现)
这个时候, 只需要看 adev_open_input_stream()这个函数就可以了:
发现是个空实现, 那, 看一下legacy是怎么实现的:
你看到啦, 别人AudioFlinger调用下来, 传入的参数明明是audio_hw_device *dev,
到了这里, 硬是封装成了这样:
也就搞成了这个鬼样子: (实际使用的却是 legay的封装体)
其实结论已经出来了:
- 为了兼容以前的版本? 真的是欺负小朋友,傻啊?
- 封装了一层结构体,硬是从c的函数指针,变成了cpp的对象调用.—-终于面向对象了
(也就是说,原来用c函数指针就能实现的东西,现在硬是给转成了cpp的对象,面向对象的方式)
为什么这么做?
查看audio.h, 我发现,可能是为了扩展(方便扩展) audio hardware module, 我有证据.
先说为什么方便扩展, 你想啊, 原来的函数指针,它能对多少了对象?(同一平台不同的硬件a2dp , primary).
说到底只有一个函数指针是不够的 (当然你可以通过预编译的宏,但是这样没法动态判断,编译时就写死了),\
所以啊,在原来的结构体, 和cpp的AudioHardwareInterface对象做了一个包装, \
C语言的不能扩展就算了, cpp的对象,想多少个,我就定义多少个实现类.
证据:
- audio.h中明确注释了:
- 你去看legacy的实现, 它实现的多种硬件模组的结构:(还是在hardware_legacy那个目录)
当然他们每一种都有特殊的用途(包括代表一种Hw module)
- AudioHardwareGeneric.cpp:实现基于特定驱动的通用Audio硬件抽象层,这是一个真正能够使用的Audio硬件抽象层
- AudioHardwareStub.cpp:实现Audio硬件抽象层的一个桩,这个实现不操作实际的硬件和文件,它所进行的是空操作
- AudioDumpInterface.cpp:实现输出到文件的Audio硬件抽象层,支持Audio的信息输出功能,不支持输入功能
- A2dpAudioInterface.cpp:实现蓝牙音频的Audio硬件抽象层
(google还怕你搞不出来, 特意给你做了一个架构, 以及样例实现…..好尴尬啊)
究极分析图
不说话,直接上图
播放过程(AudioTrack)
Android中用于播放的大概两类:
- MediaPlayer
- AudioTrack
用脚趾头想都知道, MediaPlayer显然与我无关.
MediaPlayer在Java框架层创建音频解码器,所以可以播放wav, ogg, mp3等格式的音频文件.
AudioTrack只能播放wav文件或者PCM, 为什么? 因为简单啊. (实际上你去看看源码, 就发现MediaPlayer也会创建AudioTrack)
关于MediaPlayer 怎么和AudioTrack扯上关系的也是值得一说, 不过因为太复杂, 所以我还是说一说:
(真担心自己一不小心就抢了多媒体科室的饭碗)
1.MediaPlayer并不是直接创建了AudioTrack, 毕竟一个是管理层,一个是研发人员.
2.MediaPayer不仅仅间接创建了AudioTrack, 还创建了VideoTrack (音视频分开)—-用脚趾头想也知道呀
大概是这样的:
media player service控制媒体播放器, 并且在media player service中创建了 media player实例
然后呢, media player通过jni调用android_media_player.cpp中的方法, 大家知道, 其实是调用mediaplayer.cpp中的方法.
然后和音频一样, mediaplayer 由来了一套client-server策略, 通过Binder进行了ipc调用,
从native 的mediaplayer调用到了native的mediaplayer (实际上是 libmedia.so调用到libmediaplayerservice.so)
(但注意, 相对于下面的so库, libmediaplayerservice.so也只能算是client, 毕竟它也提供了MediaPlayerService::Client)
还没完, libmediaplayerservice.so中的media player service也不真正干事儿,它就选适当的播放器, 如midi隔世的选sonivox,
ogg格式的选vorbris, 其他的根据配置文件, 或者默认就使用stagefright来播放
ok, 说到 stagefright这里, 就说到正题了, libstagefright.so提供了 StagefrightPlayer 以及 StagefrightRecorder这两个类.
代码路径: frameworks/av/media/libstagefright
(AudioEffects也对playbackThread产生作用, 一会儿说)
(留意一下,我圈出来的 AudioPlayer.cpp, AudioSource.cpp)
提供stagefright干什么呢?
一目了然啊, 提供一个播放器当然是, 播放 “视频”
这里播放的是视频啊, 可不单单是音频啊!
也就是说, 中间增加的这一层, libstagefright.so其实是为了进行音视频分离, 解码, 播放的.
我怕你不信, 给你截个图:
服不服?
先别, 先说完—只说音频部分好了: (这个过程非常细)
stagefright调用setDataSource()加载音频文件, 根据音频文件的格式(应该是协议或者文件的头字段)不同,
选择不同的解析器, 分离出audioTrack (视频部分是不是交给VideoTrack?),——-音视频分离了.
AudioTrack只是一种实体Bean,根据其mineType不同, 之后由AudioPlayer控制,调用
AuidoSouce进行不同的解码(它是omxCodec的封装, 所以可以解码), 交给AudioTrack.
具体的过程是, audio player调用fillBuffer() 讲解码完毕的数据流传递给AUdioTrack.
因为这个过程实在实在是”太简单”了, 所以, 我还是上个图吧:
AudioTrack拿到的PCM数据, 交给AudioFlinger, 怎么交的?
其实是通过带I的cpp接口方法, AudioTrack通过调用createAudioTrack_得到AudioFlinger返回的IAudioTrack实现,
然后拿到这个对象之后(这个对象并不是AudioTrack的,而是AudioFlinger返回回来的AudioFlinger::createTrack()),
之后AudioTrack就可以把数据写入共享内存,
再之后就是AudioFlinger读出缓存中的数据, 创建 PlayBackThread, 这个时候或者交给Mixer或者AudioEffect处理,
或者直接输出缓存给AudioOutputStream , 哪个AudioOutputStream ? 就是HAL层libaudio.so中实现的那个cpp对象
(在之后就是kernel里面的driver)
其实到这里过程也就是完了, 但是为了辟谣, 网友说,
“MediaPalyer在framework层, 而不是在native层创建的AudioTrack”
是不是这样呢? 我自己验证一下 (结论是, 网友又在胡说八道, 写博客害人.)
—有时间我就代码, 没有时间,我就自己看看得了.
但是从库的角度看是这么个样子的:
framework.jar —-> service.jar —> jni (libandroid_runtime.so, libandroid_jni.so)—> libmedia.so(MediaPlayer.cpp)
——-> libmediaplayerservice.so (MediaPlayerService::Client)—–> libstagefright.so (stagefirght.cpp, stagerecord.cpp)——-> libmedia.so (AudioTrack.cpp)—–Binder—-> libaudioflinger.so (AudioMixer(AudioResampler), PlayBackThread) + libeffects.so
—–>libaudio.so + audio.primary.default.so(HAL: AudioStreamOut)—->AudioDevice
如果只是AuioTrack播放部分,简单的可以理解成这样: (后面构造部分, 解析代码就基本是这个顺序&模式)
上面在说明MediaPlayer以及AudioTrack区别的时候,大致把Audio播放过程说了一下.(总体上的过了一遍)
感觉好简单?————可能是个错觉.
我举个例子大家就知道了, 有一次, 军哥说, buffer的大小, 会对播放延迟产生影响, 可是为什么会产生音响?
没有看代码之前, 正常人大概都能想到:
1.音速(350)远小于光速, 所以对于传播过程中的由于距离, 衍射, 人脑&人体滤波等产生的影响更大.
2.数据大的音频对于实时播放的硬件要求解码能力更多, 所以缓存区的大小可能产生影响
于是,衍生除了两个模型(都是实时播放的): a. 编解码边播放(io不相关) b.解码阻塞播放(时间非常短),播放阻塞解码(io相关)
那么实际上呢? 构造AudioTrack时候所用的 Buffer会不会影响呢?
基本的播放流程:
从上面的代码可以发现, 每一个音频流对应着一个AudioTrack实例.
(但是, 看上面的过程,看不出来具体的播放实现, 必须从构造AudioTrack音频流开始看起)
(buffer size应该是frame字节(大小)的非零整数倍, getMiniBufferSize())
构造过程
frameworks/base/media/java/android/media/AudioTrack.java
AudioTrack有两种数据加载模式:
MODE_STREAM
在这种模式下,应用程序持续地write音频数据流到AudioTrack中,并且write动作将阻塞直到数据流从Java层传输到native层,
同时加入到播放队列中(这种模式适用于播放大音频数据,但该模式也造成了一定的延时)
MODE_STATIC
在播放之前,先把所有数据一次性write到AudioTrack的内部缓冲区中。适用于播放内存占用小、延时要求较高的音频数据
(其内部缓冲区不同于上面Java代码, 读取音频文件时候用到的buffer, AudioTrack内部缓冲区实际上是一块儿共享内存)
这两种模式也和buffer size要求有关, 具体是: ..
(一般不设置的话,就会自己去计算最小值, 特别是MODE_STREAM的时候)
再来看看jni怎么连接的: (JNI做传递的时候, 加入了很多代码技巧, 比如整体封装, 不必在意)
frameworks/base/core/jni/android_media_AudioTrack.cpp
(代码真的是太长了, 我先说大体的流程, 再一起看下代码:
1.检查音频参数
2.创建一个AudioTrackJniStorage对象(AudioTrackJniStorage是音频数据存储的容器,是对匿名共享内存的封装)
3.创建一个AudioTrack(native)对象
4.调用set函数初始化AudioTrack (status = lpTrack->set(…) )
buffersize = frameCount 每帧数据量 = frameCount (Channel数 * 每个Channel数据量)
看代码主要看该函数: android_media_AudioTrack_setup
https://android.googlesource.com/platform/frameworks/base.git/+/master/core/jni/android_media_AudioTrack.cpp
其中最后一部分: (下面马上就仔细看下这个set方法, 如何初始化一个native AudioTrack对象)
(从上面的static模式, 也可以看出来 JniStorage其实是封装了共享内存的)
创建native AudioTrack
frameworks/av/media/libmedia/AudioTrack.cpp
注意上面有一个 transfer_type, 表示音频数据传输给AudioTrack的方式:
framework/av/include/media/AudioTrack.h
(shared mem方式, pull方式(callback方式), push方式(obtain),同步方式)
初始化AudioTrack时,如果cbf为Null,就会创建AudioTrackThread线程。
AudioTrack支持两种数据输入方式:
1) Push方式:用户主动write,MediaPlayerService通常采用此方式
2) Pull方式: AudioTrackThread线程通过cbf回调函数主动从用户那里获取数据
(下面的set方法里面有讲到)
创建AudioTrackJniStorage
这里创建共享内存的过程其实就是映射到当前进程地址空间:
现在仔细看看set方法 (AudioTrack.cpp)
之后再去看那个 createTrack_l(), 代码太多, 直接说结论:
AudioPolicyService启动时加载了系统支持的所有音频接口,并且打开了默认的音频输出,打开音频输出时,
调用AudioFlinger::openOutput()函数为当前打开的音频输出接口创建一个PlaybackThread线程(但是线程也有不同的种类)
同时为该线程分配一个全局唯一的audio_io_handle_t值,并以键值对的形式保存在AudioFlinger的成员变量
mPlaybackThreads中。 结论就是: AudioTrack在AudioFlinger中是以Track来管理的。
output为AudioFlinger中播放线程的id号
并且注意, 创建Track的是audioFlinger, 当stream模式的时候, buffer为空(看下图)
因为stream模式下,匿名共享内存却是在AudioFlinger这边创建
在static模式下,用于存放音频数据的匿名共享内存在AudioTrack这边创建.
并且stream模式下的匿名共享内存头部会创建一个audio_track_cblk_t对象,
用于协调 AudioTrack (生产者) 和 AudioFlinger(消费者) 之间的步调.
Output和AudioTrackThread
获取音频输出就是根据音频参数如采样率、声道、格式等从已经打开的音频输出描述符列表中查找合适的音频输出描述文件descriptor,
并返回该音频输出在AudioFlinger中创建的播放线程id号,如果没有合适当前音频输出参数的AudioOutputDescriptor,
则请求AudioFlinger打开一个新的音频输出通道,并为当前音频输出创建对应的播放线程,返回该播放线程的id号。
(总结就一句话, output始终和thread是绑定的, 不管是AudioTrack这个client还是后面的server AudioFlinger)
关于callback
当采用Pull方式传输数据的时候,AudioTrackThread线程通过audioCallback回调主动从用户那里获取数据
处理则是processAudioBuffer()
关于PlaybackThread,其他的还有音效, 以及共享内存的映射.(AudioTrack和AudioFlinger通信再说)
AudioTrack (client) ————- binder————— AudioFlinger (server)
不过因为它们之间是跨进程的关系,binder只能解决数据传输的途径, 但是具体的还需要一个实体对象
需要一个媒介载体,这个沟通的媒介是IAudioTrack。(就是之前说的client—>server, 亦即libmedia—>libaudioflinger)
函数createTrack_l除了为AudioTrack在AudioFlinger中申请一个Track外,还会建立两者间IAudioTrack桥梁。
简单说就是:
- //得到AudioFlinger的代理对象
- const sp
& audioFlinger = AudioSystem::get_audio_flinger(); - audioFlinger.createTrack()
这个时候可以看看刚刚的AudioFlinger::createTrack
(代码也很简单, 但是冗余的检查也比较多, 这里只截取关键的)
为什么最后还要返回一个trackHandler ? (给AudioTrack)
Track对象只负责音频相关业务,对外并没有提供夸进程的Binder调用接口,
因此需要将通信业务委托给另外一个对象来完成,这就是TrackHandle存在的意义,T
rackHandle负责代理Track的通信业务,它是Track与AudioTrack之间的跨进程通道。
AudioTrack作为client拿到IAudioTrack对象之后,就可以跨进程访问AudioFlinger中PlaybackThread创建的Track了.
(上面已经说了很多遍, Track是在AudioFlinger中进行管理的, 一个Track就绑定在一个PlaybackThread中)
怎么访问? (标准的binder方式)
BpAudioTrack –> BnAudioTrack –> TrackHandle –> Track
AudioFlinger::registerPid_l(pid_t pid)
AudioFlinger的成员变量mClients以键值对的形式保存pid和Client对象,这里首先取出pid对应的Client对象,
如果该对象为空,则为客户端进程创建一个新的Client对象。
AudioFlinger::Client::Client()
构造Client对象时,创建了一个MemoryDealer对象,该对象用于分配共享内存。
(具体参考: frameworks/native/libs/binder/MemoryDealer.cpp)
MemoryDealer是个工具类,用于分配共享内存,每一个Client都拥有一个MemoryDealer对象,
这就意味着每个客户端进程都是在自己独有的内存空间中分配共享内存。
MemoryDealer构造时创建了一个大小为210241024的匿名共享内存,
该客户进程所有的AudioFlinger中创建的Track都是在这块共享内存中分配buffer。
由此可知,当应用程序进程中的AudioTrack请求AudioFlinger,
在某个PlaybackThread中创建Track对象时,
AudioFlinger首先会为应用程序进程创建一个Client对象,同时创建一块大小为2M的共享内存。
在创建Track时,Track将在2M共享内存中分配buffer用于音频播放。
AudioFlinger中创建Track对象sp<AudioFlinger::PlaybackThread::Track> AudioFlinger::PlaybackThread::createTrack_l()
重要信息一般在其父类中声明:————所属线程, 所属client
(这个创建Track的函数比较重要, 因为里面会根据client来判别, 在哪里创建共享内存)
TrackBase构造过程主要是为音频播放分配共享内存,在static模式下,共享内存由应用进程自身分配,但在stream模式,
共享内存由AudioFlinger分配,static和stream模式下,都会创建audio_track_cblk_t对象,唯一的区别在于,
在stream模式下,audio_track_cblk_t对象创建在共享内存的头部(即物理上的紧邻)。
创建Track的过程, 也会依据stream或者static为client创建不同的代理:
在createTrack时由AudioFlinger申请相应的内存,然后通过IMemory接口返回AudioTrack,这样AudioTrack和AudioFlinger管理着同一个audio_track_cblk_t,通过它实现了环形FIFO,AudioTrack向FIFO中写入音频数据,AudioFlinger从FIFO中读取音频数据,经Mixer后送给AudioHardware进行播放:
1.AudioTrack是FIFO的数据生产者;
- AudioFlinger是FIFO的数据消费者;
但是AudioFlinger中的工作线程, 每个线程可能绑定多个Track:
Thread的threadLoop()中,会把该线程中的各个Track进行混合,必要时还要进行ReSample(重采样)的动作,
转换为统一的采样率(44.1K),然后通过音频系统的AudioHardware层输出音频数据
上面的过程总结一下,大概就是:
文字描述一下:
- Framework或者Java层通过JNI创建AudioTrack对象
- 根据StreamType等参数,查找已打开的音频输出设备,如果查找不到匹配的音频输出设备,则请求AudioFlinger打开新的音频输出设备
- AudioFlinger为该输出设备创建混音线程MixerThread,并把该线程的id作为getOutput()的返回值返回给AudioTrack
- AudioTrack通过binder机制调用AudioFlinger的createTrack()创建Track,并且创建TrackHandle Binder本地对象,同时返回IAudioTrack的代理对象
- AudioFlinger注册该Track到MixerThread中
- AudioTrack通过IAudioTrack接口,得到在AudioFlinger中创建的FIFO(audio_track_cblk_t)–stream模式
在回顾一下, 粗略的过程, 大概就是这样的: (因为音频输出设备也是AudioFlinger作用, 所以画在AudioFilinger里面)
最后的最后, 大概是这样的:
其他
其他还有内容有没有写完,不过到此为止吧。后面太长了。🙂
Merlin 2018.3 这是当初在科室培训时写的,PPT 水平一塌糊涂(但那时还是很有趣的) – 花了2个小时整理此篇