Android 音频数据流(1): 从 MediaPlayer 到 AudioTrack
注意:本文基于 Android 8.1 进行分析
Qidi 2020.11.17 (Markdown & Haroopad & EnterpriseArchitect)
0. 前言
在 Android APP 上添加音乐播放功能,使用 MediaPlayer 实现,需要调用这 3 个接口:
-
setDataSource() -
prepare() -
start()
这 3 个接口被调用时,系统做了哪些事情?音频数据是从 APP 先读取再写入底层吗?数据解码是在哪个环节进行的?AudioTrack 又是在什么时候被创建出来?这些是本文试图说明的问题。
1. 类关系
当 APP 调用 Java 层的 MediaPlayer 接口时(setDataSource()、prepare()、start() 等),经过 JNI 会转入 Native 层的 MediaPlayer。这后一个 MediaPlayer,从 Frameworks 的角度来看,是处理 APP 请求的起点,也是 IMediaPlayer 的 Bp 端用户。
IMediaPlayer 的 Bn 端实现类是 MediaPlayerService::Client。具体的 player 实例(比如 NuPlayer)在 MediaPlayerService::Client 类中创建。随后,来自 APP 的调用请求(setDataSource()、prepare()、start() 等)会以 委托(delegation) 方式交由 NuPlayer 实例进行处理。
所以,Java 层只是请求的发起方、以及音频参数的来源,音频数据的读取和解码都是在 Native 层进行的。
当 APP 调用 setDataSource() 设置音频数据源时,MediaPlayerService::Client 类实例会创建出 AudioOutput 类对象,该对象随后会创建 AudioTrack 类实例用于同 AudioFlinger 之间的数据传输;NuPlayer 类实例则会根据数据类型创建相应的具体 Source 对象(比如 GenericSource、HTTPLiveSource、RTSPSource、StreamingSource)。
相关类图如下:

2. 软、硬解码的共有代码逻辑
经过编码的音频数据,比如 mp3、aac、ogg 等,只有在解码后才能播放。
在 Android 系统中,解码方式分为 硬解码 和 软解码 两种,也被称为 Offload 和 non-Offload。 它们的区别在于:硬解码模式下,原始数据直接被写入底层,由硬件 DSP 对音频数据进行解码;软解码模式下,原始数据先经过 OMXCodec 解码为 PCM 数据后,再写入底层。
不过也有特殊情况,那就是受到版权保护的音频编码格式(比如 ac3、dts)。 比如,如果仅仅是在 HAL 层集成了 Dolby 音效支持,那么 ac3 编码的音频数据虽然也是以 Offload 方式被写入底层,但在这种情况下因为硬件也不支持解码,所以依然是通过软件方式在 HAL 中进行解码的。
解码方式虽然有软、硬的区别,但如果只关注数据解码前和解码后的行为,它们的过程却是一样的。
这里以 硬解码 场景为例,为 setDataSource()、prepare()、start() 的调用过程绘制时序图,如下:

软解码 场景及相关的 OMXCodec 等内容,之后将在另外一篇文章中进行说明。
从上方时序图可以看出,media_server 进程启动时,NuPlayerFactory 实例被创建和添加到 MediaPlayerFactory 中。之后,当 APP 依次调用 setDataSource()、prepare() 和 start() 接口时,分别主要发生了这些事情:
-
调用
setDataSource()—— 创建对应的具体Source类实例、创建并配置BufferMonitor; -
调用
prepare()—— 创建对应的Extractor类实例、创建并配置MediaBuffer; -
调用
start()—— 创建并初始化对应的Decoder类实例、创建ABuffer对象并从数据源读取音频数据(并解码)、创建AudioTrack对象并写入数据。
再进一步看看 NuPlayer::DecoderPassThrough 从音频源读取数据和写数据到 AudioTrack 的过程(以下时序图忽略了 AMessage-ALooper-AHandler 的消息分发和处理过程):

这里可能有的朋友会有 2 个疑问:
其一,是我们看到在 NuPlayer::DecoderPassThrough::onConfigure() 中,明明 openAudioSink() 的调用是后于 onRequestInputBuffers() 的,为什么在前一个调用中就在写数据了呢,这个时候 AudioSink 和 AudioTrack 不应该还没创建好吗?实际上不会有这个问题,因为这两个函数的内部实现都依赖于 AMessage-ALooper-AHandler 机制,当它们被 DecoderPassThrough 实例调用时,只要对应的 AMessage 对象被构建和发送后,函数就返回了,后续工作都是在另外的线程中异步完成的;另一方面,由于读取数据的耗时远远超过打开 AudioSink 和创建 AudioTrack 的时间,当数据读取完毕时,后两者也早已经准备就绪。
其二,是这个 AudioTrack::write() 和之后的 AudioTrack::start() 是什么关系,莫非执行 AudioTrack::write() 时还没有将数据写入底层?是的,前者只会把数据写入 匿名共享内存(AshMem) 中;等到 AudioTrack::start() 被调用时,一个 Track 实例会被创建出来,并被添加到 AudioFlinger 中,随后将之前写到 AshMem 中的数据读取出来,经过 AudioFlinger 混音后才会写入 HAL。
在 NuPlayer、NuPlayer::Renderer、NuPlayer::DecoderXxx 及 XxxSource 的实现中大量使用了 Android 的 AMessage-ALooper-AHandler 通信机制。其大致逻辑是:在需要抛出消息的实例中构造 AMessage 对象并指定消息类型,调用 setObject() 方法可以为消息填充额外的信息,然后调用 post() 方法发布该信息;用于捕获和处理消息的类需要继承 AHandler 类并实现 onMessageReceived() 函数,在该函数体中使用 switch...case... 处理自己感兴趣的消息类型。
关于 AMessage-ALooper-AHandler 更细节的内容不在本文介绍,之后也将在另外一篇文章中进行说明。
3. 软、硬解码的不同代码逻辑
软、硬解码场景在代码逻辑上的不同之处,在于音频数据不同时, NuPlayer::instantiateDecoder() 中创建的解码器不同。
NuPlayer 类中支持 2 种解码器 —— DecoderPassThrough 和 Decoder。顾名思义,前者对应于 硬解码 Offload 场景,后者对应于 软解码 non-Offload 场景。因此,软、硬解码场景在数据传输流程上的分歧点也就存在于 NuPlayer 里,以下是对这一区别的大致说明:
音频开始播放前,NuPlayer::onStart() 会被调用,其中又会调用 canOffloadStream() 函数对当前音频流是否支持 Offload 进行判断,并将结果写入成员变量 mOffloadAudio。相应关键代码如下:
void NuPlayer::onStart(int64_t startPositionUs, MediaPlayerSeekMode mode) {
// ......
mOffloadAudio = false;
// ......
mOffloadAudio =
canOffloadStream(audioMeta, hasVideo, mSource->isStreaming(), streamType)
&& (mPlaybackSettings.mSpeed == 1.f && mPlaybackSettings.mPitch == 1.f);
// Modular DRM: Disabling audio offload if the source is protected
if (mOffloadAudio && mIsDrmProtected) {
mOffloadAudio = false;
ALOGV("onStart: Disabling mOffloadAudio now that the source is protected.");
}
if (mOffloadAudio) {
flags |= Renderer::FLAG_OFFLOAD_AUDIO;
}
//......
postScanSources();
}
canOffloadStream() 会依据传入的 MIME 信息,读取出对应的声道数、采样率、格式、音频类型等音频参数,之后调用 AudioPolicyManager::isOffloadSupported(),查找所有在 audio_policy_configuration.xml 中配置的 audio profile,通过对各 profile 支持的音频参数和目标音频参数进行比对,从而得到了当前音频流是否支持 Offload 的信息。
在 NuPlayer::onStart() 函数末尾, postScanSources() 调用随后会执行到 NuPlayer::instantiateDecoder() 函数中。根据上一步得到的 mOffloadAudio 变量值,和当前音频流匹配的解码器将会被创建并初始化:
status_t NuPlayer::instantiateDecoder(
bool audio, sp<DecoderBase> *decoder, bool checkAudioModeChange) {
if (audio) {
// ......
if (mOffloadAudio) {
mSource->setOffloadAudio(true /* offload */);
const bool hasVideo = (mSource->getFormat(false /*audio */) != NULL);
format->setInt32("has-video", hasVideo);
*decoder = new DecoderPassThrough(notify, mSource, mRenderer);
ALOGV("instantiateDecoder audio DecoderPassThrough hasVideo: %d", hasVideo);
} else {
mSource->setOffloadAudio(false /* offload */);
*decoder = new Decoder(notify, mSource, mPID, mUID, mRenderer);
ALOGV("instantiateDecoder audio Decoder");
}
mAudioDecoderError = false;
} else {
// ......
}
(*decoder)->init();
// ......
(*decoder)->configure(format);
// ......
return OK;
}
以上就是 硬解码 和 软解码 两种场景在代码逻辑上的的不同之处,以及数据从音频源读取到写入 AudioTrack 的过程。
数据流再从 AudioTrack 传输到 ALSA 驱动节点的过程,留到下一篇文章《Android 音频数据流(2): 从 AudioTrack 到 ALSA 驱动》进行说明。
1655

被折叠的 条评论
为什么被折叠?



