FFmpeg 音频可视化解码流程详解
作者:Mystatic 发布时间:2023-04-03 00:41:54
一、解码流程
解码流程大致分为以下三个部分,以FFmpge源码下的ffmpeg\doc\examples\decode_audio.c为参考。
1.1、解析音频信息
avformat_open_input负责打开需要解码的音频文件,如果文件打开成功的话会初始化AVFormatContext,avformat_find_stream_info开启音频流遍历,av_find_best_stream找到最合适解析数据的帧,解析完后我们可以通过返回的AVStream获取到我们需要用的解码器id、通道数、采样率、位深、音频时长、数据排列结构。拿到解码器id我们通过解码器id获取解码器avcodec_find_decoder,有些解码器并不是FFmpeg内置的,所以有些需要在编译时就加进去,我之前的文章也有讲过AAC和MP3编解码第三方库。如果找到了解码器,下一步就是avcodec_alloc_context3对解码器上下文AVCodecContext进行初始化,初始化完成后avcodec_parameters_to_context将解码器参数设置给解码器上下文,例如通道数,采样率,采样位深等等信息。如果未设置可能会出现invalid argument的错误,导致后续无法继续。最后通过avcodec_open2打开解码器,如果打开成功我们就可以开始对音频数据进行读取。
1.2、从原始数据packet到frame
我们解码的目的就是为了拿到底层播放器能播的PCM数据。既然我们已经获取到了解码器,那么下面就是一帧一帧的读取解码器解析出来的数据。首先我们需要av_packet_alloc初始化包对象AVPacket,包对象是未解码的数据,原始的音频数据被打包成一个一个的包,然后送给解码器去把包打开,变成帧对象,所以我们又需要通过av_frame_alloc初始化帧对象AVFrame,把它送给解码器,解码器用数据把它装满后返回回来。av_read_frame就是从打开的文件读取一个数据包,对于AAC/MP3来说他们是未解码的压缩数据。然后通过avcodec_send_packet把数据包送给解码器,返回0表示解码器解包成功,接下来就可以从解码器读数据,这时的数据就是以帧的形式存在,avcodec_receive_frame读取帧,因为一个包可能有几个帧,所以需要循环读取,当avcodec_receive_frame返回0时表示读取成功,可以进行下一步操作,当返回值是AVERROR_EOF表示读取完毕可以跳出循环了,返回AVERROR(EAGAIN)表示解码器输出已经是不可用的状态,必须向解码器送新包来激活输出,同样也可以跳出读取和解析帧的循环。
1.3、从frame到PCM byte
我们的PCM数据就在frame的data里,但是我们并不能直接拿,首先我们得知道拿多少,怎么拿。拿多少取决于采样位数,通道数和帧里面的样本数。例如44100HZ的话一秒就有44100通道数个样本。那一个帧里面就一共有 采样位数/8通道数*样本数个字节数据。怎么拿取决于音频数据的存储方式,音频存储方式有两种:
planar:音频左右声道数据分开放置,数据存储格式为
data[0]:LLLLLLLLLLLLLLLL
data[1]:RRRRRRRRRRRRR
packet:音频左右声道数据交替放置,数据存储格式为
data[0]:LRLRLRLRLRLRLRLR
最终拿到的数据都是以LRLRLRLRLR的方式排列,到这里我们可以把它送给播放器或者在送给播放器前加一些我们自己的音频算法,全部解码完成后,最后记得释放掉相关资源。在这里我们简单点,计算它的分贝,实现音频可视化的功能。
二、分贝计算
我们音频的分贝往往不需要计算每一个样本的分贝数,第一计算密度太大超出人眼感知对显示没有益处,二是计算量太大会导致我们的计算时间大大延长。因为声音具有一定的延续性,所以我们可以计算一个时间段内的平均值来获得一段音频范围的分布值,这样既减小了工作量又达到了合理可视化的效果。首先是获取平均值,假设我们每秒想获取10个分贝值,那么我们需要计算采样率通道数采样位数/8/10个字节数据的平均值,我们不妨自己把它叫dB采样区间样本数,一个16bit位深的音频每两个字节组成一个样本,将区间内样本相加再除以样本数取平均值即可。接下来就是dB的计算,dB其实并不特指分贝,它只是在音频描述领域。它描述的是音频的增益关系,如果想详细了解db是什么可以自行百度相关的知识。分贝的计算公式是
20*log10(value)
所以声音的分贝描述的并不是线性关系而是指数关系,例如70db比50db的声音大了20倍,例如16bit可以描述的音频范围为0-65535那么它的最大dB值在96.3左右,32bit可以描述音频范围在0-4294967296,那么它的最大dB值在192.6。把我们刚才计算的平均值带入value就能获得我们的区间的分贝,把它存起来解析完一起返回或者逐个回调都可以,看你的业务需求。下面是计算16bit采样位数的分贝的方法,32bit的处理方法类似,主要注意值的大小,和每次位移的byte步长。拿到了了分贝我们就可以将它们变成条变成块的绘制到屏幕了。
void getPcmDB16(const unsigned char *pcmdata, size_t size) {
int db = 0;
short int value = 0;
double sum = 0;
for(int i = 0; i < size; i += bit_format/8)
{
memcpy(&value, pcmdata+i, bit_format/8); //获取2个字节的大小(值)
sum += abs(value); //绝对值求和
}
sum = sum / (size / (bit_format/8)); //求平均值(2个字节表示一个振幅,所以振幅个数为:size/2个)
if(sum > 0)
{
db = (int)(20.0*log10(sum));
}
memcpy(wave_buffer+wave_index,(char*)&db,1);
wave_index++;
}
需要注意的是我们在解码时ffmpeg的音频格式类型除了packet和planar两个大类外,对于32位的音频又区分了32位整形和32位浮点型。
enum AVSampleFormat {
AV_SAMPLE_FMT_NONE = -1,
AV_SAMPLE_FMT_U8, ///< unsigned 8 bits
AV_SAMPLE_FMT_S16, ///< signed 16 bits
AV_SAMPLE_FMT_S32, ///< signed 32 bits
AV_SAMPLE_FMT_FLT, ///< float
AV_SAMPLE_FMT_DBL, ///< double
AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar
AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar
AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar
AV_SAMPLE_FMT_FLTP, ///< float, planar
AV_SAMPLE_FMT_DBLP, ///< double, planar
AV_SAMPLE_FMT_S64, ///< signed 64 bits
AV_SAMPLE_FMT_S64P, ///< signed 64 bits, planar
AV_SAMPLE_FMT_NB ///< Number of sample formats. DO NOT USE if linking dynamically
};
浮点型的取值范围在-1到1的区间,所以我们在计算时需要乘以0x7fff来获得和16位同比例的数据,达到同样的显示效果。
void getPcmDBFloat(const unsigned char *pcmdata, size_t size) {
int db = 0;
float value = 0;
double sum = 0;
for(int i = 0; i < size; i += bit_format/8)
{
memcpy(&value, pcmdata+i, bit_format/8); //获取4个字节的大小(值)
sum += abs(value*0x7fff); //绝对值求和
}
sum = sum / (size / (bit_format/8));
if(sum > 0)
{
db = (int)(20.0*log10(sum));
}
memcpy(wave_buffer+wave_index,(char*)&db,1);
wave_index++;
}
三、实现效果
来源:https://juejin.cn/post/7149400286702862349


猜你喜欢
- 摘要: 如何解决页面之间跳转时的黑屏问题呢?在默认情况下,Android应用程序启动时,会有一个黑屏的时期。原因是,首个activity会加
- 1、一个示例回顾Future一些业务场景我们需要使用多线程异步执行任务,加快任务执行速度。JDK5新增了Future接口,用于描述一个异步计
- 模式虽然精妙,却难完美,比如观察者模式中观察者生命周期的问题;比如访问者模式中循环依赖的问题等等;其它很多模式也存在这样那样的一些不足之处,
- mybatis多层级collection嵌套json结构第一步查询第一层查询,将第一层的id传递到第二层当条件查询
- requestFoucs();无效。requestFoucsFromTouch();无效。webview.setTouchListener;
- 1. 什么是局部类型?C# 2.0 引入了局部类型的概念。局部类型允许我们将一个类、结构或接口分成几个部分,分别实现在几个不同的.cs文件中
- Service概念及用途:Android中的服务,它与Activity不同,它是不能与用户交互的,不能自己启动的,运行在后台的程序,如果我们
- 委托:顾名思义,让别人帮你办件事。委托是C#实现回调函数的一种机制。可能有人会问了,回调函数是个啥???举个例子:我现在是一家公司的老板,公
- 转成 Base64 形式的 System.String:string a = "base64字符串与普通字符串互转";
- 本文实例讲述了C#实现跨线程操作控件方法,分享给大家供大家参考。具体实现方法如下:由于在.net平台下Winform、wpf禁止跨线程直接访
- 如果一个项目内有很多个界面,那么在layout下会有太多的activity***.xml文件,这个时候就需要使用文件夹对这些分别存放了。当然
- 什么是 MyBatis ?MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有
- 本文实例讲述了C#使用IComparer自定义List类实现排序的方法。分享给大家供大家参考。具体如下:List类中不带参数的Sort函数可
- 这几天在弄后端管理系统向指定的Android
- 全面解析java注解Java中的常见注解 a.JDK中的注解 @Override 覆盖父类或者父接口的方
- 本文实例为大家分享了如何利用AOP实现SqlSugar自动事务,供大家参考,具体内容如下先看一下效果,带接口层的三层架构:BL层: publ
- 1. System.Char 字符char 是 System.Char 的别名。System.Char 占两个字节,16个二进制位。Syst
- 一、概述之前公司app里面有个功能是一个可以双向滑动的范围选择器,我在网上百度过一些实现方法,感觉各有利弊吧,但是都不太适合我们的需求。所以
- 在Linux中创建一个新进程的唯一方法是使用fork()函数。fork()函数是Linux中一个非常重要的函数,和以往遇到的函数有一些区别,
- trim中prefix与suffix等标签用法1.prefix 前缀增加的内容2.suffix 后缀增加的内容3.prefixOverrid