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 驱动》进行说明。