大家好,如果您还对Android音视频开发进阶:FFmpeg编解码与OpenSL ES音频播放技术详解不太了解,没有关系,今天就由本站为大家分享Android音视频开发进阶:FFmpeg编解码与OpenSL ES音频播放技术详解的知识,包括的问题都会给大家分析到,还望可以解决大家的问题,下面我们就开始吧!
教程代码:【Github传送门】
目录
一、Android音视频硬解码篇:
1.音视频基础知识2.音视频硬解码流程:封装基本解码框架3.音视频播放:音视频同步4.音视频解封装与封装:生成MP4
二、使用OpenGL渲染视频画面篇
1、初步了解OpenGL ES2、使用OpenGL渲染视频图像3、OpenGL渲染多个视频、实现画中画4、深入了解OpenGL的EGL5、OpenGL FBO数据缓冲区6、Android音视频硬编码:生成MP4
三、Android FFmpeg音视频解码篇
1、FFmpeg so库编译2、Android介绍FFmpeg3、Android FFmpeg视频解码与播放4、Android FFmpeg+OpenSL ES音频解码与播放5、Android FFmpeg+OpenGL ES视频播放6、Android FFmpeg简单合成MP4:视频解封和重新打包7、Android FFmpeg视频编码
本文你可以了解到
本文介绍如何使用FFmpeg进行音频解码,重点介绍如何使用OpenSL ES实现DNK层的音频渲染和播放。
一、音频解码
在上一篇文章中,我们详细介绍了FFmepg的播放流程,抽象出了解码流程框架,整合了视频和音频解码流程的共同点,形成了BaseDecoder类。通过继承BaseDecoder,实现视频解码子类VideoDeocder,并集成到Player中,实现视频播放和渲染。
本文使用已经定义的解码基类BaseDecoder来实现音频解码子类AudioDecoder。
实现音频解码子类
首先我们看一下要实现音频解码需要实现哪些功能。
定义解码流程我们通过头文件a_decoder.h定义所需的成员变量和处理方法。
i. 成员变量定义//a_decoder.h
类AudioDecoder: 公共BaseDecoder {
私人:
const char *TAG="音频解码器";
//音频转换器
SwrContext *m_swr=NULL;
//音频渲染器
音频渲染*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是自定义的音频渲染器,后面会介绍。
音频转换中还需要使用其他变量,例如转换输出缓冲区、缓冲区大小和样本数。
ii. 定义成员方法//a_decoder.h
类AudioDecoder: 公共BaseDecoder {
私人:
//省略成员变量.
/**
* 初始化转换工具
*/
无效InitSwr();
/**
* 初始化输出缓冲区
*/
无效InitOutBuffer();
/**
* 初始化渲染器
*/
无效初始化渲染();
/**
* 释放缓冲区
*/
无效ReleaseOutBuffer();
/**
* 采样格式:16位
*/
AVSampleFormat GetSampleFmt() {
返回AV_SAMPLE_FMT_S16;
}
/**
*目标采样率
*/
int GetSampleRate(int spr) {
返回AUDIO_DEST_SAMPLE_RATE; //44100Hz
}
公共:
AudioDecoder(JNIEnv *env, const jstring path, bool forSynthesizer);
音频解码器();
无效SetRender(AudioRender *渲染);
受保护:
无效准备(JNIEnv * env)覆盖;
void Render(AVFrame *frame) 覆盖;
void Release() 覆盖;
bool NeedLoopDecode() 覆盖{
返回真;
}
AVMediaType GetMediaType() 覆盖{
返回AVMEDIA_TYPE_AUDIO;
}
const char *const LogSpec() 覆盖{
返回“音频”;
};
};上面的代码并不复杂。它们都是初始化相关的方法以及BaseDecoder中定义的抽象方法的实现。
我们重点关注这两个方法:
/**
* 采样格式:16位
*/
AVSampleFormat GetSampleFmt() {
返回AV_SAMPLE_FMT_S16;
}
/**
*目标采样率
*/
int GetSampleRate(int spr) {
返回AUDIO_DEST_SAMPLE_RATE; //44100Hz
}首先要知道的是,这两种方法的目的是为了兼容以后的编码。
我们知道,音频的采样率和采样位数是音频数据所特有的,每个音频都可能不同。因此,在播放或重新编码时,通常会将数据转换为固定规格,以便可以正常播放或重新编码。
播放和编码配置也略有不同。这里,采样位数为16位,采样率为44100。
接下来我们看看具体的实现。
实现解码流程//a_decoder.cpp
AudioDecoder:AudioDecoder(JNIEnv *env, const jstring path, bool forSynthesizer) : BaseDecoder(
env, 路径, forSynthesizer) {
}
无效AudioDecoder:~AudioDecoder() {
如果(m_render!=NULL){
删除m_render;
}
}
无效AudioDecoder:SetRender(AudioRender *渲染){
m_render=渲染;
}
void AudioDecoder:Prepare(JNIEnv *env) {
初始化Swr();
初始化输出缓冲区();
初始化渲染();
}
//省略其他.i. 初始化重点关注Prepare方法。该方法将在基类BaseDecoder初始化解码器后被调用。
在Prepare方法中,调用了以下内容:
InitSwr(),初始化转换器
InitOutBuffer(),初始化输出缓冲区
InitRender(),初始化渲染器。下面详细分析如何配置初始化参数。
SwrContext配置:
//a_解码器.cpp
无效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_解码器.cpp
无效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(
空,AUDIO_DEST_CHANNEL_COUNTS,
m_dest_nb_sample, GetSampleFmt(), 1);
m_out_buffer[0]=(uint8_t *) malloc(m_dest_data_size);
}
无效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 **)帧数据,帧nb_样本);
如果(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
无效AudioDecoder:Release(){
如果(m_swr!=NULL){
swr_free(m_swr);
}
如果(m_render!=NULL){
m_render-ReleaseRender();
}
释放输出缓冲区();
}
无效AudioDecoder:ReleaseOutBuffer(){
if (m_out_buffer[0] !=NULL) {
自由(m_out_buffer [0]);
m_out_buffer[0]=NULL;
}
}解码完成后,退出播放时,需要释放转换器和输出缓冲区。
二、接入 OpenSL ES
在Android上播放音频,通常使用AudioTrack,但NDK层没有提供直接的类。需要通过NDK调用Java层并回调来实现播放。相对而言,比较麻烦,效率也较低。
在NDK层,提供了另一种播放音频的方法:OpenSL ES。
什么是 OpenSL ES
OpenSL ES(嵌入式系统开放声音库)是一个免许可、跨平台的硬件音频加速API,针对嵌入式系统进行了精心优化。为嵌入式移动多媒体设备上的本地应用开发者提供了标准化、高性能、低响应时间的音频功能实现方法,并可实现软/硬件音频性能的直接跨平台部署,降低执行难度。
OpenSL ES 提供哪些功能
OpenSL主要提供录音和回放功能。本文重点介绍播放功能。
播放源支持PCM、sdcard资源、res/assets资源、网络资源。
我们使用FFmpeg进行解码,因此播放源是PCM。
OpenSL ES 状态机
OpenSL ES是一个基于C语言开发的库,但其接口是采用面向对象的编程思想编写的。它的接口不能直接调用,必须在对象创建和初始化后通过对象调用。
Object 和 InterfaceOpenSL ES提供了一系列Object,它们有一些基本的操作方法,如Realize、Resume、GetState、Destroy、GetInterface等。
一个对象有一个或多个接口方法,但一个接口只属于一个对象。
如果要调用Object中的Interface方法,首先要通过Object的GetInterface获取接口Interface,然后通过获取到的Interface来调用。
例如:
//创建引擎
SLObjectItf m_engine_obj=NULL;
SLresult 结果=slCreateEngine(m_engine_obj, 0, NULL, 0, NULL, NULL);
//初始化引擎
结果=(*m_engine_obj)-实现(m_engine_obj, SL_BOOLEAN_FALSE);
//获取引擎接口
SLEngineItf m_engine=NULL;
结果=(*m_engine_obj)-GetInterface(m_engine_obj, SL_IID_ENGINE, m_engine);可见Object需要创建并初始化后才能使用。这就是OpenSL ES中的状态机机制。
OpenSL ES状态机Object创建完成后,就进入Unrealized状态。调用Realize()方法后,会分配相关的内存资源,并进入Realized状态。只有这样才能获取并使用Object的Interface方法。
在后续执行过程中,如果发生错误,Object将进入Suspending状态。调用Resume()恢复到Realized状态。
OpenSL ES 播放初始化配置
我们看一下官方的播放流程图。
OpenSL ES 播放流程这张图非常清楚地展示了OpenSL ES 的运作方式。
OpenSL ES 播放所需的两个核心是Audio Player 和Output Mix,即播放和混音器,这两个核心都是由OpenSL ES 引擎Engine 创建的。
因此,整个初始化过程可以概括为:
通过Engine创建一个Output Mix/混音器,并使用混音器作为参数。创建音频播放器/播放器时,将其绑定到音频播放器作为输出。
DataSource 和 DataSink创建音频播放器时,需要为其设置数据源和输出目标,以便播放器知道如何获取播放数据以及将数据输出到哪里进行播放。
这需要使用两个OpenSL ES 结构:DataSource 和DataSink。
typedef 结构SLDataSource_ {
无效*pLocator;
无效*pFormat;
}SL 数据源;
typedef 结构SLDataSink_ {
无效*pLocator;
无效*pFormat;
} SLDataSink;其中,
SLDataSource pLocator 有以下类型:
SLDataLocator_地址
SLDataLocator_BufferQueue
SLDataLocator_IODevice
SLDataLocator_MIDIBufferQueue
SLDataLocator_URI 使用SLDataLocator_BufferQueue 缓冲队列播放PCM。
SLDataSink pLocator 一般为SL_DATALOCATOR_OUTPUTMIX。
另一个参数pFormat是数据的格式。
实现渲染流程
在连接OpenSL ES之前,首先定义上面提到的音频渲染接口,以方便标准化和扩展。
//audio_render.h
音频渲染类{
公共:
虚拟无效InitRender()=0;
虚拟无效渲染(uint8_t *pcm,int大小)=0;
虚拟无效ReleaseRender()=0;
虚拟~AudioRender() {}
};在CMakeList.txt中,开启OpenSL ES支持
#CMakeList.txt
# 省略其他.
# 指定cmake编译目标库时要链接的库
目标链接库(
本机库
阿乌蒂尔
采样
AV编解码器
AV过滤器
swscale
AV格式
AV设备
-landroid
# 开启opensl es支持
开放式SLES
# 将目标库链接到日志库
# 包含在NDK 中。
${log-lib} )初始化i. 定义成员变量首先定义需要用到的引擎、混音器、播放器、缓冲队列接口、音量调节接口等。
//opensl_render.h
类OpenSLRender: 公共AudioRender {
私人:
//引擎接口
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;
SLPl
ayItf 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() {三、整合播放
上文中,已经完成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(); }【Android音视频开发进阶:FFmpeg编解码与OpenSL ES音频播放技术详解】相关文章:
用户评论
终于看到了关于 OpenSL ES 解码播放的文章!期待详细教程,自己也想要尝试用 FFmpeg 和 OpenSL ES 在 Android 上实现音频解码。
有7位网友表示赞同!
FFmpeg 的音视频编解码功能简直太强了,OpenSL ES 也很实用,这俩组合起来玩音频开发真是太棒了。
有12位网友表示赞同!
Android 音频开发我有点苦手啊, 这篇文章正好能帮到我!
有7位网友表示赞同!
之前也研究过 FFmpeg 的音频处理, OpenSL ES 确实更好用一些,交互比较简便。
有11位网友表示赞同!
要实现高品质的音频播放体验,FFmpeg 和 OpenSL ES 是必不可少的工具,期待这篇文章能分享一些经验。
有13位网友表示赞同!
学习 Android 开发,音视频编解码篇是必经之路啊! 希望能看到更浅显易懂的讲解。
有16位网友表示赞同!
OpenSL ES 的 API 设计的还挺好的,上手速度比较快,感觉比其他音频库要容易一些。
有5位网友表示赞同!
期待这篇文章能详细介绍 FFmpeg 和 OpenSL ES 的结合方式,以及一些常见音频码流的解码技巧。
有17位网友表示赞同!
我正在做一款支持多种音频格式播放的 Android 应用,FFmpeg 和 OpenSL ES 可以帮我解决很多难题。
有9位网友表示赞同!
Android 设备上的系统自带媒体框架有时候表现不太稳定,用 FFmpeg 和 OpenSL ES 自行编解码可以提升可靠性 。
有8位网友表示赞同!
这个平台上分享的 FFmpeg 相关内容不多啊,期待这篇文章能提供一些新的思路和解决方案。
有5位网友表示赞同!
FFmpeg 是开源的,这意味着它是一个非常灵活强大的工具,配合 OpenSL ES 可以实现各种音频处理功能。
有12位网友表示赞同!
学习音频开发真的很需要时间和耐心,希望这篇文章能给我一些启发,让我更快地入门。
有18位网友表示赞同!
做 Android 音频开发之前,我从来没有接触过 FFmpeg 和 OpenSL ES,真是太惊喜了!
有6位网友表示赞同!
文章标题说的“打怪升级”真有意思,感觉自己学习音频开发也是一个不断挑战、突破自己的过程。
有5位网友表示赞同!
最近想尝试用 Android 开发一些新颖的音视频应用,这篇文章能给我带来很多灵感!
有7位网友表示赞同!
希望这篇文章能够深入浅出地讲解 FFmpeg 和 OpenSL ES 的使用方法,让新手朋友也能轻松理解。
有19位网友表示赞同!
做Android开发的朋友都知道音频解码的重要性, 这篇文章来得真是太及时了 !
有19位网友表示赞同!
我一直想学习一下FFmpeg 和 OpenSL ES,这个主题的文章正好适合我!
有11位网友表示赞同!