FFmpeg音频解码-音频可视化

1,887 阅读6分钟

“我正在参加「掘金·启航计划」”最近在做一个音频可视化的业务,网上有Java层的实现方法,但是业务需要用C实现,从原理出发其实很简单,先对音频进行解码,再计算分贝。这比把大象放进冰箱还简单。本文从音频可视化的业务为依托,以FFmpeg为基础实现解码,计算,绘制。

一、解码流程

        解码流程大致分为以下三个部分,以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++;
}

三、实现效果

       

         欢迎大家交流讨论,批评指正。