Android 音频数据流(1): 从 MediaPlayer 到 AudioTrack

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 对象(比如 GenericSourceHTTPLiveSourceRTSPSourceStreamingSource)。

相关类图如下:

类图


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。


NuPlayerNuPlayer::RendererNuPlayer::DecoderXxxXxxSource 的实现中大量使用了 Android 的 AMessage-ALooper-AHandler 通信机制。其大致逻辑是:在需要抛出消息的实例中构造 AMessage 对象并指定消息类型,调用 setObject() 方法可以为消息填充额外的信息,然后调用 post() 方法发布该信息;用于捕获和处理消息的类需要继承 AHandler 类并实现 onMessageReceived() 函数,在该函数体中使用 switch...case... 处理自己感兴趣的消息类型。

关于 AMessage-ALooper-AHandler 更细节的内容不在本文介绍,之后也将在另外一篇文章中进行说明。


3. 软、硬解码的不同代码逻辑

软、硬解码场景在代码逻辑上的不同之处,在于音频数据不同时, NuPlayer::instantiateDecoder() 中创建的解码器不同。

NuPlayer 类中支持 2 种解码器 —— DecoderPassThroughDecoder。顾名思义,前者对应于 硬解码 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 驱动》进行说明。

  • 8
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值