首先,这一系列文章均基于本身的理解和实践,可能有不对的地方,欢迎你们指正。
其次,这是一个入门系列,涉及的知识也仅限于够用,深刻的知识网上也有许许多多的博文供你们学习了。
最后,写文章过程当中,会借鉴参考其余人分享的文章,会在文章最后列出,感谢这些做者的分享。android
码字不易,转载请注明出处!git
教程代码:【Github传送门】 |
---|
本文介绍如何使用
FFmpeg
进行音频解码,重点讲解如何使用OpenSL ES
在 NDK 层实现音频渲染播放。github
在上篇文章中,详细介绍了 FFmepg
的播放流程,以及抽象了解码流程框架,整合视频和音频解码流程的共同点,造成了 BaseDecoder
类。经过继承 BaseDecoder
实现了视频解码子类 VideoDeocder
,并整合到了 Player
中,实现了视频的播放渲染。web
本文就利用已经定义好的解码基类 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; //...... } 复制代码
其中,SwrContext
是 FFmpeg
提供的音频转化工具,位于 swresample
中,可用来转换采样率、解码通道数、采样位数等。这里用来将音频数据转换为 双通道立体
声音,统一 采样位数
。编辑器
⚠️ AudioRender
是自定义的音频渲染器,将在后面介绍。ide
其余的变量,则是音频转换中须要配合使用的,转换输出缓冲、缓冲区大小、采样数。
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; } } 复制代码
解码完毕,退出播放的时候,须要将转换器、输出缓冲区释放。
在 Android
上播放音频,一般使用的 AudioTrack
,可是在 NDK
层,没有提供直接的类,须要经过 NDK
调用 Java
层的方式,回调实现播放。相对来讲比较麻烦,效率也比较低。
在 NDK
层,提供另外一种播放音频的方法:OpenSL ES
。
OpenSL ES (Open Sound Library for Embedded Systems)是无受权费、跨平台、针对嵌入式系统精心优化的硬件音频加速API。它为嵌入式移动多媒体设备上的本地应用程序开发者提供标准化,高性能,低响应时间的音频功能实现方法,并实现软/硬件音频性能的直接跨平台部署,下降执行难度。
OpenSL ES 主要提供了录制和播放的功能,本文主讲播放功能。
播放源支持 PCM
、sdcard资源
、 res/assets资源
、 网络资源
。
咱们使用的 FFmpeg
解码,因此播放源是 PCM
。
OpenSL ES 是基于 C
语言开发的库,可是其接口是使用了面向对象的编程思想编写的,它的接口不能直接调用,而是要通过对象建立、初始化后,经过对象来调用。
OpenSL ES 提供了一系列 Object
,它们拥有一些基础操做方法,好比 Realize,Resume,GetState,Destroy,GetInterface等。
一个
Object
拥有一个或多个Interface
方法,可是一个Intefcace
只属于一个Obejct
。
想要调用 Object
中的 Interface
方法,必需要经过 Object
的 GetInterface
先获取到接口 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
中的状态机机制。
Object
被建立后,进入Unrealized
状态,调用Realize()
方法之后会分配相关的内存资源,进入Realized
状态,这时Object
的Interface
方法才能被获取和使用。在后续执行过程当中,若是出现错误,
Object
会进入Suspended
状态。调用Resume()
能够恢复到Realized
状态。
来看一张官方的播放流程图
这张图很是清晰的展现了 OpenSL ES
是如何运做的。
OpenSL ES
播放须要的两个核心是 Audio Player
和 Output Mix
,即 播放起
和 混音器
,而这两个都是由 OpenSL ES
的引擎 Engine
建立(creates)出来的。
因此,整个初始化流程能够总结为:
经过 Engine 建立
Output Mix/混音器
,并将混音器
做为参数,在建立Audio Player/播放器
时,绑定给Audio Player
做为输出。
在建立 Audio Player
的时候,须要给其设置 数据源
和 输出目标
,这样播放器才知道,如何获取播放数据、将数据输出到哪里进行播放。
这就须要用到 OpenSL ES
的两个结构体 DataSource
和 DataSink
。
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
渲染很简单,只需调用播放器的播放接口,而且往缓冲区压入一帧数据,就能够启动渲染流程。
若是是播放一个 sdcard
的 pcm
文件,那只要在回调方法 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); } } 复制代码
其实很简单,就是把解码获得的数据压入队列,而且发送数据缓冲准备完毕信号,通知播放线程能够进入播放了。
这样,就完成了整个流程,总结一下:
OpenSL
,开启「开始播放等待线程」,并进入播放等待;
OpenSL
设置为播放状态,并压入一帧数据;
OpenSL
播放完一帧数据后,自动回调通知继续压入数据;
上文中,已经完成 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(); } } 复制代码