【Android 音视频开发打怪升级:FFmpeg音视频编解码篇】四、Android FFmpeg+OpenSL ES音频解码播放

【声 明】

首先,这一系列文章均基于自己的理解和实践,可能有不对的地方,欢迎大家指正。
其次,这是一个入门系列,涉及的知识也仅限于够用,深入的知识网上也有许许多多的博文供大家学习了。
最后,写文章过程中,会借鉴参考其他人分享的文章,会在文章最后列出,感谢这些作者的分享。

码字不易,转载请注明出处!

教程代码:【Github传送门

目录

一、Android音视频硬解码篇:

二、使用OpenGL渲染视频画面篇

三、Android FFmpeg音视频解码篇


本文你可以了解到

本文介绍如何使用 FFmpeg 进行音频解码,重点讲解如何使用 OpenSL ES 在 DNK 层实现音频渲染播放。

一、音频解码

上篇文章中,详细介绍了 FFmepg 的播放流程,以及抽象了解码流程框架,整合视频和音频解码流程的共同点,形成了 BaseDecoder 类。通过继承 BaseDecoder 实现了视频解码子类 VideoDeocder,并整合到了 Player 中,实现了视频的播放渲染。

本文就利用已经定义好的解码基类 BaseDecoder 实现音频解码子类 AudioDecoder

实现音频解码子类

首先来看下,实现音频解码,需要实现哪些内容。

  • 定义解码流程

我们通过头文件 a_decoder.h ,将需要的成员变量和流程方法定义好。

i. 成员变量定义

//a_decoder.h

class AudioDecoder: public BaseDecoder {
private:

    const char *TAG = "AudioDecoder";

    // 音频转换器
    SwrContext *m_swr = NULL;

    // 音频渲染器
    AudioRender *m_render = NULL;

    // 输出缓冲
    uint8_t *m_out_buffer[1] = {NULL};

    // 重采样后,每个通道包含的采样数
    // acc默认为1024,重采样后可能会变化
    int m_dest_nb_sample = 1024;

    // 重采样以后,一帧数据的大小
    size_t m_dest_data_size = 0;
    
    //......
}

其中,SwrContextFFmpeg 提供的音频转化工具,位于 swresample 中,可用来转换采样率、解码通道数、采样位数等。这里用来将音频数据转换为 双通道立体 声音,统一 采样位数

AudioRender 是自定义的音频渲染器,将在后面介绍。

其他的变量,则是音频转换中需要配合使用的,转换输出缓冲、缓冲区大小、采样数。

ii. 定义成员方法

//a_decoder.h

class AudioDecoder: public BaseDecoder {
private:

    // 省略成员变量......

    /**
     * 初始化转换工具
     */
    void InitSwr();

    /**
     * 初始化输出缓冲
     */
    void InitOutBuffer();

    /**
     * 初始化渲染器
     */
    void InitRender();

    /**
     * 释放缓冲区
     */
    void ReleaseOutBuffer();

    /**
     * 采样格式:16位
     */
    AVSampleFormat GetSampleFmt() {
        return AV_SAMPLE_FMT_S16;
    }

    /**
     * 目标采样率
     */
    int GetSampleRate(int spr) {
        return AUDIO_DEST_SAMPLE_RATE; //44100Hz
    }

public:
    AudioDecoder(JNIEnv *env, const jstring path, bool forSynthesizer);
    ~AudioDecoder();

    void SetRender(AudioRender *render);

protected:
    void Prepare(JNIEnv *env) override;
    void Render(AVFrame *frame) override;
    void Release() override;

    bool NeedLoopDecode() override {
        return true;
    }

    AVMediaType GetMediaType() override {
        return AVMEDIA_TYPE_AUDIO;
    }

    const char *const LogSpec() override {
        return "AUDIO";
    };
};

以上代码也不复杂,都是一些初始化相关的方法,以及对 BaseDecoder 中定义的抽象方法的实现。

重点讲解一下这两个方法:

/**
 * 采样格式:16位
 */
AVSampleFormat GetSampleFmt() {
    return AV_SAMPLE_FMT_S16;
}

/**
 * 目标采样率
 */
int GetSampleRate(int spr) {
    return AUDIO_DEST_SAMPLE_RATE; //44100Hz
}

首先要知道的是,这两个方法的目的是为了兼容以后编码的。

我们知道音频的采样率和采样位数是音频数据特有的,并且每个音频都有可能不一样,所以在播放或者重新编码的时候,通常会将数据转换为固定的规格,这样才能正常播放或重新编码。

播放和编码的配置也稍有不同,这里,采样位数是 16 位,采样率使用 44100 。

接下来,看看具体的实现。

  • 实现解码流程
// a_decoder.cpp

AudioDecoder::AudioDecoder(JNIEnv *env, const jstring path, bool forSynthesizer) : BaseDecoder(
        env, path, forSynthesizer) {
    
}

void AudioDecoder::~AudioDecoder() {
    if (m_render != NULL) {
        delete m_render;
    }
}

void AudioDecoder::SetRender(AudioRender *render) {
    m_render = render;
}

void AudioDecoder::Prepare(JNIEnv *env) {
    InitSwr();
    InitOutBuffer();
    InitRender();
}

//省略其他....

i. 初始化

重点看 Prepare 方法,这个方法在基类 BaseDecoder 初始化完解码器以后,就会调用。

Prepare 方法中,依次调用了:

InitSwr(),初始化转换器

InitOutBuffer(),初始化输出缓冲

InitRender(),初始化渲染器

下面具体解析如何配置初始化参数。

SwrContext 配置:

// a_decoder.cpp

void AudioDecoder::InitSwr() {
    
    // codec_cxt() 为解码上下文,从父类 BaseDecoder 中获取
    AVCodecContext *codeCtx = codec_cxt();

    //初始化格式转换工具
    m_swr = swr_alloc();

    // 配置输入/输出通道类型
    av_opt_set_int(m_swr, "in_channel_layout",  codeCtx->channel_layout, 0);
    
    // 这里 AUDIO_DEST_CHANNEL_LAYOUT = AV_CH_LAYOUT_STEREO,即 立体声
    av_opt_set_int(m_swr, "out_channel_layout", AUDIO_DEST_CHANNEL_LAYOUT, 0);
    
    // 配置输入/输出采样率
    av_opt_set_int(m_swr, "in_sample_rate", codeCtx->sample_rate, 0);
    av_opt_set_int(m_swr, "out_sample_rate", GetSampleRate(codeCtx->sample_rate), 0);

    // 配置输入/输出数据格式
    av_opt_set_sample_fmt(m_swr, "in_sample_fmt",  codeCtx->sample_fmt, 0);
    av_opt_set_sample_fmt(m_swr, "out_sample_fmt", GetSampleFmt(),  0);

    swr_init(m_swr);
}

初始化很简单,首先调用 FFmpeg 的 swr_alloc 方法,分配内存,得到一个转化工具 m_swr ,接着调用对应的方法,设置输入和输出的音频数据参数。

输入输出参数的设置,也可通过一个统一的方法 swr_alloc_set_opts 设置,具体可以参看该接口注释。

输出缓冲配置:

// a_decoder.cpp

void AudioDecoder::InitOutBuffer() {
    // 重采样后一个通道采样数
    m_dest_nb_sample = (int)av_rescale_rnd(ACC_NB_SAMPLES, GetSampleRate(codec_cxt()->sample_rate),
                                           codec_cxt()->sample_rate, AV_ROUND_UP);
    // 重采样后一帧数据的大小
    m_dest_data_size = (size_t)av_samples_get_buffer_size(
            NULL, AUDIO_DEST_CHANNEL_COUNTS,
            m_dest_nb_sample, GetSampleFmt(), 1);

    m_out_buffer[0] = (uint8_t *) malloc(m_dest_data_size);
}

void AudioDecoder::InitRender() {
    m_render->InitRender();
}

在转换音频数据之前,我们需要一个数据缓冲区来存储转换后的数据,因此需要知道转换后的音频数据有多大,并以此来分配缓冲区。

影响数据缓冲大小的因素有三个,分别是:采样个数通道数采样位数


采样个数计算

我们知道 AAC 一帧数据包含采样个数是 1024 个。如果对一帧音频数据进行重采样的话,那么采样个数就会发生变化。

如果采样率变大,那么采样个数会变多;采样率变小,则采样个数变少。并且成比例关系。


计算方式如下:【目标采样个数 = 原采样个数 *(目标采样率 / 原采样率)】

FFmpeg 提供了 av_rescale_rnd 用于计算这种缩放关系,优化了计算益处问题。

FFmpeg 提供了 av_samples_get_buffer_size 方法来帮我们计算这个缓存区的大小。只需提供计算出来的目标采样个数通道数采样位数

得到缓存大小后,通过 malloc 分配内存。

ii. 渲染

// a_decoder.cpp

void AudioDecoder::Render(AVFrame *frame) {
    // 转换,返回每个通道的样本数
    int ret = swr_convert(m_swr, m_out_buffer, m_dest_data_size/2,
                          (const uint8_t **) frame->data, frame->nb_samples);
    if (ret > 0) {
        m_render->Render(m_out_buffer[0], (size_t) m_dest_data_size);
    }
}

父类 BaseDecoder 解码数据后,回调子类渲染方法 Render 。在渲染之前,调用 swr_convert 方法,转换音频数据。

以下为接口原型:

/**
 * out:输出缓冲区
 * out_count:输出数据单通道采样个数
 * in:待转换原音频数据
 * in_count:原音频单通道采样个数
 */
int swr_convert(struct SwrContext *s, uint8_t **out, int out_count,
                                const uint8_t **in , int in_count);

最后调用渲染器 m_render 渲染播放。

iii.释放资源

// a_decoder.cpp

void AudioDecoder::Release() {
    if (m_swr != NULL) {
        swr_free(&m_swr);
    }
    if (m_render != NULL) {
        m_render->ReleaseRender();
    }
    ReleaseOutBuffer();
}

void AudioDecoder::ReleaseOutBuffer() {
    if (m_out_buffer[0] != NULL) {
        free(m_out_buffer[0]);
        m_out_buffer[0] = NULL;
    }
}

解码完毕,退出播放的时候,需要将转换器、输出缓冲区释放。

二、接入 OpenSL ES

Android 上播放音频,通常使用的 AudioTrack ,但是在 NDK 层,没有提供直接的类,需要通过 NDK 调用 Java 层的方式,回调实现播放。相对来说比较麻烦,效率也比较低。

NDK 层,提供另一种播放音频的方法:OpenSL ES

什么是 OpenSL ES

OpenSL ES (Open Sound Library for Embedded Systems)是无授权费、跨平台、针对嵌入式系统精心优化的硬件音频加速API。它为嵌入式移动多媒体设备上的本地应用程序开发者提供标准化,高性能,低响应时间的音频功能实现方法,并实现软/硬件音频性能的直接跨平台部署,降低执行难度。

OpenSL ES 提供哪些功能

OpenSL 主要提供了录制和播放的功能,本文主讲播放功能。

播放源支持 PCMsdcard资源res/assets资源网络资源

我们使用的 FFmpeg 解码,所以播放源是 PCM

OpenSL ES 状态机

OpenSL ES 是基于 C 语言开发的库,但是其接口是使用了面向对象的编程思想编写的,它的接口不能直接调用,而是要经过对象创建、初始化后,通过对象来调用。

  • Object 和 Interface

OpenSL ES 提供了一系列 Object ,它们拥有一些基础操作方法,比如 Realize,Resume,GetState,Destroy,GetInterface等。

一个 Object 拥有一个或多个 Interface 方法,但是一个 Intefcace 只属于一个 Obejct

想要调用 Object 中的 Interface 方法,必须要通过 ObjectGetInterface 先获取到接口 Interface ,再通过获取到的 Interface 来调用。

比如:

// 创建引擎
SLObjectItf m_engine_obj = NULL;
SLresult result = slCreateEngine(&m_engine_obj, 0, NULL, 0, NULL, NULL);

// 初始化引擎
result = (*m_engine_obj)->Realize(m_engine_obj, SL_BOOLEAN_FALSE);

// 获取引擎接口
SLEngineItf m_engine = NULL;
result = (*m_engine_obj)->GetInterface(m_engine_obj, SL_IID_ENGINE, &m_engine);

可以看到,Object 需要经过创建、初始化之后才能使用,这就是 OpenSL ES 中的状态机机制。

OpenSL ES 状态机

Object 被创建后,进入 Unrealized 状态,调用 Realize() 方法以后会分配相关的内存资源,进入 Realized 状态,这时 ObjectInterface 方法才能被获取和使用。

在后续执行过程中,如果出现错误,Object 会进入 Suspended 状态。调用 Resume() 可以恢复到 Realized 状态。

OpenSL ES 播放初始化配置

来看一张官方的播放流程图

OpenSL ES 播放流程

这张图非常清晰的展示了 OpenSL ES 是如何运作的。

OpenSL ES 播放需要的两个核心是 Audio PlayerOutput Mix ,即 播放起混音器 ,而这两个都是由 OpenSL ES 的引擎 Engine 创建(creates)出来的。

所以,整个初始化流程可以总结为:

通过 Engine 创建 Output Mix/混音器,并将 混音器 作为参数,在创建 Audio Player/播放器 时,绑定给 Audio Player 作为输出。

  • DataSource 和 DataSink

在创建 Audio Player 的时候,需要给其设置 数据源输出目标 ,这样播放器才知道,如何获取播放数据、将数据输出到哪里进行播放。

这就需要用到 OpenSL ES 的两个结构体 DataSourceDataSink

typedef struct SLDataSource_ {
    void *pLocator;
    void *pFormat;
} SLDataSource;

typedef struct SLDataSink_ {
    void *pLocator;
    void *pFormat;
} SLDataSink;

其中,
SLDataSource pLocator 有以下几种类型:

SLDataLocator_Address
SLDataLocator_BufferQueue
SLDataLocator_IODevice
SLDataLocator_MIDIBufferQueue
SLDataLocator_URI

播放 PCM 使用的是 SLDataLocator_BufferQueue 缓冲队列。

SLDataSink pLocator 一般为 SL_DATALOCATOR_OUTPUTMIX

另外一个参数 pFormat 为数据的格式。

实现渲染流程

在接入 OpenSL ES 之前,先定义好上文提到的音频渲染接口,方便规范和拓展。

// audio_render.h

class AudioRender {
public:
    virtual void InitRender() = 0;
    virtual void Render(uint8_t *pcm, int size) = 0;
    virtual void ReleaseRender() = 0;
    virtual ~AudioRender() {}
};

CMakeList.txt 中,打开 OpenSL ES 支持

# CMakeList.txt

# 省略其他...

# 指定编译目标库时,cmake要链接的库
target_link_libraries(

        native-lib

        avutil
        swresample
        avcodec
        avfilter
        swscale
        avformat
        avdevice

        -landroid
        
        # 打开opensl es支持
        OpenSLES
        
        # Links the target library to the log library
        # included in the NDK.
        ${log-lib} )
  • 初始化

i. 定义成员变量

先定义需要用到的引擎、混音器、播放器、以及缓冲队列接口、音量调节接口等。

// opensl_render.h

class OpenSLRender: public AudioRender {

private:

    // 引擎接口
    SLObjectItf m_engine_obj = NULL;
    SLEngineItf m_engine = NULL;

    //混音器
    SLObjectItf m_output_mix_obj = NULL;
    SLEnvironmentalReverbItf m_output_mix_evn_reverb = NULL;
    SLEnvironmentalReverbSettings m_reverb_settings = SL_I3DL2_ENVIRONMENT_PRESET_DEFAULT;

    //pcm播放器
    SLObjectItf m_pcm_player_obj = NULL;
    SLPlayItf m_pcm_player = NULL;
    SLVolumeItf m_pcm_player_volume = NULL;

    //缓冲器队列接口
    SLAndroidSimpleBufferQueueItf m_pcm_buffer;

    //省略其他......
}

ii. 定义相关成员方法

// opensl_render.h

class OpenSLRender: public AudioRender {

private:

    // 省略成员变量...
    
    // 创建引擎
    bool CreateEngine();
    
    // 创建混音器
    bool CreateOutputMixer();
    
    // 创建播放器
    bool CreatePlayer();
    
    // 开始播放渲染
    void StartRender();
    
    // 音频数据压入缓冲队列
    void BlockEnqueue();

    // 检查是否发生错误
    bool CheckError(SLresult result, std::string hint);
    
    // 数据填充通知接口,后续会介绍这个方法的作用
    void static sReadPcmBufferCbFun(SLAndroidSimpleBufferQueueItf bufferQueueItf, void *context);

public:
    OpenSLRender();
    ~OpenSLRender();

    void InitRender() override;
    void Render(uint8_t *pcm, int size) override;
    void ReleaseRender() override;

iii. 实现初始化流程

// opensl_render.cpp

OpenSLRender::OpenSLRender() {
}

OpenSLRender::~OpenSLRender() {
}

void OpenSLRender::InitRender() {
    if (!CreateEngine()) return;
    if (!CreateOutputMixer()) return;
    if (!CreatePlayer()) return;
}

// 省略其他......

创建引擎

// opensl_render.cpp

bool OpenSLRender::CreateEngine() {
    SLresult result = slCreateEngine(&m_engine_obj, 0, NULL, 0, NULL, NULL);
    if (CheckError(result, "Engine")) return false;

    result = (*m_engine_obj)->Realize(m_engine_obj, SL_BOOLEAN_FALSE);
    if (CheckError(result, "Engine Realize")) return false;

    result = (*m_engine_obj)->GetInterface(m_engine_obj, SL_IID_ENGINE, &m_engine);
    return !CheckError(result, "Engine Interface");
}

创建混音器

// opensl_render.cpp

bool OpenSLRender::CreateOutputMixer() {
    SLresult result = (*m_engine)->CreateOutputMix(m_engine, &m_output_mix_obj, 1, NULL, NULL);
    if (CheckError(result, "Output Mix")) return false;

    result = (*m_output_mix_obj)->Realize(m_output_mix_obj, SL_BOOLEAN_FALSE);
    if (CheckError(result, "Output Mix Realize")) return false;
    
    return true;
}

按照前面状态机的机制,先创建引擎对象 m_engine_obj、然后 Realize 初始化,然后再通过 GetInterface 方法,获取到引擎接口 m_engine

创建播放器

// opensl_render.cpp

bool OpenSLRender::CreatePlayer() {

    //【1.配置数据源 DataSource】----------------------
    
    //配置PCM格式信息
    SLDataLocator_AndroidSimpleBufferQueue android_queue = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, SL_QUEUE_BUFFER_COUNT};
    SLDataFormat_PCM pcm = {
            SL_DATAFORMAT_PCM,//播放pcm格式的数据
            (SLuint32)2,//2个声道(立体声)
            SL_SAMPLINGRATE_44_1,//44100hz的频率
            SL_PCMSAMPLEFORMAT_FIXED_16,//位数 16位
            SL_PCMSAMPLEFORMAT_FIXED_16,//和位数一致就行
            SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,//立体声(前左前右)
            SL_BYTEORDER_LITTLEENDIAN//结束标志
    };
    SLDataSource slDataSource = {&android_queue, &pcm};

    //【2.配置输出 DataSink】----------------------
    
    SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, m_output_mix_obj};
    SLDataSink slDataSink = {&outputMix, NULL};

    const SLInterfaceID ids[3] = {SL_IID_BUFFERQUEUE, SL_IID_EFFECTSEND, SL_IID_VOLUME};
    const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};

    //【3.创建播放器】----------------------
    
    SLresult result = (*m_engine)->CreateAudioPlayer(m_engine, &m_pcm_player_obj, &slDataSource, &slDataSink, 3, ids, req);
    if (CheckError(result, "Player")) return false;

    //初始化播放器
    result = (*m_pcm_player_obj)->Realize(m_pcm_player_obj, SL_BOOLEAN_FALSE);
    if (CheckError(result, "Player Realize")) return false;

    //【4.获取播放器接口】----------------------
    
    //得到接口后调用,获取Player接口
    result = (*m_pcm_player_obj)->GetInterface(m_pcm_player_obj, SL_IID_PLAY, &m_pcm_player);
    if (CheckError(result, "Player Interface")) return false;

    //获取音量接口
    result =  (*m_pcm_player_obj)->GetInterface(m_pcm_player_obj, SL_IID_VOLUME, &m_pcm_player_volume);
    if (CheckError(result, "Player Volume Interface")) return false;

    //【5. 获取缓冲队列接口】----------------------
    
    //注册回调缓冲区,获取缓冲队列接口
    result = (*m_pcm_player_obj)->GetInterface(m_pcm_player_obj, SL_IID_BUFFERQUEUE, &m_pcm_buffer);
    if (CheckError(result, "Player Queue Buffer")) return false;

    //注册缓冲接口回调
    result = (*m_pcm_buffer)->RegisterCallback(m_pcm_buffer, sReadPcmBufferCbFun, this);
    if (CheckError(result, "Register Callback Interface")) return false;

    LOGI(TAG, "OpenSL ES init success")
    
    return true;
}

播放器的初始化比较麻烦一些,不过都是根据前面介绍的初始化流程,按部就班。

配置数据源、输出器、以及初始化后,获取播放接口、音量调节接口等。

️ 要注意的是最后一步,即代码中的第【5】。

数据源为 缓冲队列 的时候,需要获取一个缓冲接口,用于将数据填入缓冲区。

那么什么时候填充数据呢?这就是最后注册回调接口的作用。

我们需要注册一个回调函数到播放器中,当播放器中的数据播放完,就会回调这个方法,告诉我们:数据播完啦,要填充新的数据了。

sReadPcmBufferCbFun 是一个静态方法,可以推测出,OpenSL ES 播放音频内部是一个独立的线程,这个线程不断的读取缓冲区的数据,进行渲染,并在数据渲染完了以后,通过这个回调接口通知我们填充新数据。

  • 实现播放

启动 OpenSL ES 渲染很简单,只需调用播放器的播放接口,并且往缓冲区压入一帧数据,就可以启动渲染流程。

如果是播放一个 sdcardpcm 文件,那只要在回调方法 sReadPcmBufferCbFun 中读取一帧数据填入即可。

但是,在我们这里没有那么简单,还记得我们的 BaseDeocder 中启动了一个解码线程吗?而 OpenSL ES 渲染也是一个独立的线程,因此,在这里变成两个线程的数据同步问题。

当然了,也可以将 FFmpeg 做成一个简单的解码模块,在 OpenSL ES 的渲染线程实现解码播放,处理起来就会简单得多。

为了解码流程的统一,这里将会采用两个独立线程。

i. 开启播放等待

上面已经提到,播放和解码是两个所以数据需要同步,因此,在初始化为 OpenSL 以后,不能马上开始进入播放状态,而是要等待解码数据第一帧,才能开始播放。

这里,通过线程的等待方式,等待数据。

在前面的 InitRender 方法中,首先初始化了 OpenSL ,在这方法的最后,我们让播放进入等待状态。

// opensl_render.cpp

OpenSLRender::OpenSLRender() {
}

OpenSLRender::~OpenSLRender() {
}

void OpenSLRender::InitRender() {
    if (!CreateEngine()) return;
    if (!CreateOutputMixer()) return;
    if (!ConfigPlayer()) return;
    
    // 开启线程,进入播放等待
    std::thread t(sRenderPcm, this);
    t.detach();
}

void OpenSLRender::sRenderPcm(OpenSLRender *that) {
    that->StartRender();
}

void OpenSLRender::StartRender() {
    while (m_data_queue.empty()) {
        WaitForCache();
    }
    (*m_pcm_player)->SetPlayState(m_pcm_player, SL_PLAYSTATE_PLAYING);
    sReadPcmBufferCbFun(m_pcm_buffer, this);
}

/**
 * 线程进入等待
 */
void OpenSLRender::WaitForCache() {
    pthread_mutex_lock(&m_cache_mutex);
    pthread_cond_wait(&m_cache_cond, &m_cache_mutex);
    pthread_mutex_unlock(&m_cache_mutex);
}

/**
 * 通知线程恢复执行
 */
void OpenSLRender::SendCacheReadySignal() {
    pthread_mutex_lock(&m_cache_mutex);
    pthread_cond_signal(&m_cache_cond);
    pthread_mutex_unlock(&m_cache_mutex);
}

最后的 StartRender() 方法是真正被线程执行的方法,进入该方法,首先判断数据缓冲队列是否有数据,没有则进入等待,直到数据到来。

其中,m_data_queue 是自定义的数据缓冲队列,如下:

// opensl_render.h

class OpenSLRender: public AudioRender {

private:
    /**
     * 封装 PCM 数据,主要用于实现数据内存的释放
     */
    class PcmData {
    public:
        PcmData(uint8_t *pcm, int size) {
            this->pcm = pcm;
            this->size = size;
        }
        ~PcmData() {
            if (pcm != NULL) {
                //释放已使用的内存
                free(pcm);
                pcm = NULL;
                used = false;
            }
        }
        uint8_t *pcm = NULL;
        int size = 0;
        bool used = false;
    };
    
    // 数据缓冲列表
    std::queue<PcmData *> m_data_queue;
    
    // 省略其他...
}

ii. 数据同步与播放

接下来,就来看看如何尽心数据同步与播放。

初始化 OpenSL 的时候,在最后注册了播放回调接口 sReadPcmBufferCbFun,首先来看看它的实现。

// opensl_render.cpp

void OpenSLRender::sReadPcmBufferCbFun(SLAndroidSimpleBufferQueueItf bufferQueueItf, void *context) {
    OpenSLRender *player = (OpenSLRender *)context;
    player->BlockEnqueue();
}

void OpenSLRender::BlockEnqueue() {
    if (m_pcm_player == NULL) return;

    // 先将已经使用过的数据移除
    while (!m_data_queue.empty()) {
        PcmData *pcm = m_data_queue.front();
        if (pcm->used) {
            m_data_queue.pop();
            delete pcm;
        } else {
            break;
        }
    }

    // 等待数据缓冲
    while (m_data_queue.empty() && m_pcm_player != NULL) {// if m_pcm_player is NULL, stop render
        WaitForCache();
    }

    PcmData *pcmData = m_data_queue.front();
    if (NULL != pcmData && m_pcm_player) {
        SLresult result = (*m_pcm_buffer)->Enqueue(m_pcm_buffer, pcmData->pcm, (SLuint32) pcmData->size);
        if (result == SL_RESULT_SUCCESS) {
            // 只做已经使用标记,在下一帧数据压入前移除
            // 保证数据能正常使用,否则可能会出现破音
            pcmData->used = true;
        }
    }
}

StartRender() 等待到缓冲数据的到来时,就会通过以下方法启动播放

(*m_pcm_player)->SetPlayState(m_pcm_player, SL_PLAYSTATE_PLAYING);
sReadPcmBufferCbFun(m_pcm_buffer, this);

这时候,经过一层层调用,最后调用的是 BlockEnqueue() 方法。

在这个方法中,

首先,将 m_data_queue 中已经使用的数据先删除,回收资源;

接着,判断是否还有未播放的缓冲数据,没有则进入等待;

最后,通过 (*m_pcm_buffer)->Enqueue() 方法,将数据压入 OpenSL 队列。

️ 注:在接下来的播放过程中,OpenSL 只要播放完数据,就会自动回调 sReadPcmBufferCbFun 重新进入以上的播放流程。

  • 压入数据,开启播放

以上是整个播放的流程,最后还有关键的一点,来开启这个播放流程,那就是 AudioRender 定义的渲染播放接口 void Render(uint8_t *pcm, int size)

// opensl_render.cpp

void OpenSLRender::Render(uint8_t *pcm, int size) {
    if (m_pcm_player) {
        if (pcm != NULL && size > 0) {
            // 只缓存两帧数据,避免占用太多内存,导致内存申请失败,播放出现杂音
            while (m_data_queue.size() >= 2) {
                SendCacheReadySignal();
                usleep(20000);
            }
            
            // 将数据复制一份,并压入队列
            uint8_t *data = (uint8_t *) malloc(size);
            memcpy(data, pcm, size);
            
            PcmData *pcmData = new PcmData(pcm, size);
            m_data_queue.push(pcmData);
            
            // 通知播放线程推出等待,恢复播放
            SendCacheReadySignal();
        }
    } else {
        free(pcm);
    }
}

其实很简单,就是把解码得到的数据压入队列,并且发送数据缓冲准备完毕信号,通知播放线程可以进入播放了。

这样,就完成了整个流程,总结一下:

  1. 初始化 OpenSL ,开启「开始播放等待线程」,并进入播放等待;
  2. 将数据压入缓冲队列,通知播放线程恢复执行,进入播放;
  3. 开启播放时,将 OpenSL 设置为播放状态,并压入一帧数据;
  4. OpenSL 播放完一帧数据后,自动回调通知继续压入数据;
  5. 解码线程不断压入数据到缓冲队列;
  6. 在接下来的过程中,「OpenSL ES 播放线程」和「FFMpeg 解码线程」会同时执行,重复「2 ~ 5 」,并且在数据缓冲不足的情况下,「播放线程 」会等待「解码线程」压入数据后,再继续执行,直到完成播放,双方退出线程。

三、整合播放

上文中,已经完成 OpenSL ES 播放器的相关功能,并且实现了 AudioRander 中定义的接口,只要在 AudioDecoder 中正确调用就可以了。

如何调用也已经在第一节中介绍,现在只需把它们整合到 Player 中,就可以实现音频的播放了。

在播放器中,新增音频解码器和渲染器:

//player.h

class Player {
private:
    VideoDecoder *m_v_decoder;
    VideoRender *m_v_render;

    // 新增音频解码和渲染器
    AudioDecoder *m_a_decoder;
    AudioRender *m_a_render;

public:
    Player(JNIEnv *jniEnv, jstring path, jobject surface);
    ~Player();

    void play();
    void pause();
};

实例化音频解码器和渲染器:

// player.cpp

Player::Player(JNIEnv *jniEnv, jstring path, jobject surface) {
    m_v_decoder = new VideoDecoder(jniEnv, path);
    m_v_render = new NativeRender(jniEnv, surface);
    m_v_decoder->SetRender(m_v_render);

    // 实例化音频解码器和渲染器
    m_a_decoder = new AudioDecoder(jniEnv, path, false);
    m_a_render = new OpenSLRender();
    m_a_decoder->SetRender(m_a_render);
}

Player::~Player() {
    // 此处不需要 delete 成员指针
    // 在BaseDecoder中的线程已经使用智能指针,会自动释放
}

void Player::play() {
    if (m_v_decoder != NULL) {
        m_v_decoder->GoOn();
        m_a_decoder->GoOn();
    }
}

void Player::pause() {
    if (m_v_decoder != NULL) {
        m_v_decoder->Pause();
        m_a_decoder->Pause();
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343