使用FFmpeg 做音频数据重采样

开发环境

  • FFmpeg 3.4
  • window
  • Visual Studio 2022

使用的开发环境可以在前面的文章中找的哦,依然是在前篇博客里面工程里面增加代码

开发过程

1. 创建SwrResample.h SwrResample.cpp 文件

SwrResample.h 的定义如下

c++ 复制代码
#pragma once
#include <iostream>

extern "C" {
    #include <libavutil/opt.h>
	#include <libavutil/channel_layout.h>
	#include <libavutil/samplefmt.h>
	#include <libswresample/swresample.h>
}

#define WRITE_RESAMPLE_PCM_FILE

class SwrResample
{
public:
	int Init(int64_t src_ch_layout, int64_t dst_ch_layout,
		int src_rate, int dst_rate,
		enum AVSampleFormat src_sample_fmt, enum AVSampleFormat dst_sample_fmt,
		int src_nb_samples);

	int WriteInput(AVFrame* frame);

	int SwrConvert();

	void Close();

private:
	struct SwrContext* swr_ctx;

	uint8_t** src_data_;
	uint8_t** dst_data_;

	int src_nb_channels, dst_nb_channels;
	int src_linesize, dst_linesize;
	int src_nb_samples_, dst_nb_samples_;

	enum AVSampleFormat dst_sample_fmt_;

	enum AVSampleFormat src_sample_fmt_;

#ifdef WRITE_RESAMPLE_PCM_FILE
	FILE* outdecodedswffile;
#endif

};

WRITE_RESAMPLE_PCM_FILE定义了一个宏,开启,就将重采样后的的pcm写文件

2. SwrResample的Init方法初始化

c++ 复制代码
int  SwrResample::Init(int64_t src_ch_layout, int64_t dst_ch_layout,
    int src_rate, int dst_rate,
    enum AVSampleFormat src_sample_fmt, enum AVSampleFormat dst_sample_fmt,
    int src_nb_samples)
{
#ifdef WRITE_RESAMPLE_PCM_FILE
    outdecodedswffile = fopen("decode_resample.pcm", "wb");
    if (!outdecodedswffile) {
        std::cout << "open out put swr file failed";
    }
#endif // WRITE_RESAMPLE_PCM_FILE

    src_sample_fmt_ = src_sample_fmt;
    dst_sample_fmt_ = dst_sample_fmt;

    int ret;
    /* create resampler context */
    swr_ctx = swr_alloc();
    if (!swr_ctx) {
        std::cout << "Could not allocate resampler context" << std::endl;
        ret = AVERROR(ENOMEM);
        return ret;
    }

    /* set options */
    av_opt_set_int(swr_ctx, "in_channel_layout", src_ch_layout, 0);
    av_opt_set_int(swr_ctx, "in_sample_rate", src_rate, 0);
    av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", src_sample_fmt, 0);

    av_opt_set_int(swr_ctx, "out_channel_layout", dst_ch_layout, 0);
    av_opt_set_int(swr_ctx, "out_sample_rate", dst_rate, 0);
    av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", dst_sample_fmt, 0);

    /* initialize the resampling context */
    if ((ret = swr_init(swr_ctx)) < 0) {
        std::cout << "Failed to initialize the resampling context" << std::endl;
        return -1;
    }

    //配置输入的参数
    /*
    * src_nb_samples: 描述一整的采样个数 比如这里就是 1024
    * src_linesize: 描述一行采样字节长度 
    *   当是planr 结构 LLLLLRRR 的时候 比如 一帧1024个采样,32为表示。那就是 1024*4 = 4096 
    *   当是非palner 结构的时候 比如一帧1024采样 32位表示 双通道   1024*4*2 = 8196 要乘以通道
    * src_nb_channels : 可以根据布局获得音频的通道
    * ret 返回输入数据的长度 比如这里 1024 * 4 * 2 = 8196 (32bit,双声道,1024个采样)
    */
    src_nb_channels = av_get_channel_layout_nb_channels(src_ch_layout);
    
    ret = av_samples_alloc_array_and_samples(&src_data_, &src_linesize, src_nb_channels,
        src_nb_samples, src_sample_fmt, 0);
    if (ret < 0) {
        std::cout << "Could not allocate source samples\n" << std::endl;
        return -1;
    }
    src_nb_samples_ = src_nb_samples;

    //配置输出的参数
    int max_dst_nb_samples = dst_nb_samples_ =
        av_rescale_rnd(src_nb_samples, dst_rate, src_rate, AV_ROUND_UP);

    dst_nb_channels = av_get_channel_layout_nb_channels(dst_ch_layout);
    
    ret = av_samples_alloc_array_and_samples(&dst_data_, &dst_linesize, dst_nb_channels,
        dst_nb_samples_, dst_sample_fmt, 0);
    if (ret < 0) {
        std::cout << "Could not allocate destination samples" << std::endl;
        return -1;
    }
}
  • 输入参数为源的pcm参数和转换后的参数,主要是音频通道布局类型(src_cha_layout),音频采样率(src_rate),音频采样格式(src_sample_fmt)。还有一个参数是src_nb_samples,这描述一帧数据有多少采样,对于aac这里一般是1024,这个参数只能从AVFrame里面读取
  • SwrContext 首先swr_alloc() 分配空间 swr_init() 初始化输出参数
  • av_get_channel_layout_nb_channels 可以通过channel_layout 获得有几个音频通道,比如双声道就能判断是两个channel
  • av_rescale_rnd() 这个方法返回了这帧数据在重采样后有多少个采样,这个可能比较难理解一点。比如在48000hz采样率的数据重采样44100hz后数据采样量变少了。那么原来1024个采样也会变小
  • av_samples_alloc_array_and_samples() 这个方法主要是对它的第一个参数分配空间,根据后面的参数,第一个参数,后面用于存放pcm。这里会有一些复杂,这里仔细讲解一下
arduino 复制代码
int av_samples_alloc_array_and_samples(uint8_t ***audio_data, int *linesize, int nb_channels,
                                       int nb_samples, enum AVSampleFormat sample_fmt, int align)

audio_data 外面传入的是二级指针的地址,在这个方法里面分配空间。那么它怎么怎么存放pcm数据呢,这里要分两种情况就是pcm是planar格式存放的,还是非planar

  • audio_data[0]: LLLLLLLLLLLLLL audio_data[1]: RRRRRRRRRRRRRR

  • 非planar audio_data[0]: LRLRLRLRLRLRLRLRLRLRLRLRLRLR

    其实从原理来audio_data[0] 都是是指向一个uint8_t *buf数组的,在planar的时候 audio_data[1]指示指向 bufer的R开头的位置

另外一个知识点就是linesize是多少,在不同pcm结构时候也是不一样的,下面是远吗的描述

ini 复制代码
 line_size = planar ? FFALIGN(nb_samples * sample_size,               align) :
                         FFALIGN(nb_samples * sample_size * nb_channels, align);
  • planar: linesize 举个例子就是 1024 * (32/8)
  • 非planar linesize 举个例子就是 1024 * (32/8) * 2

av_sample_fmt_is_planar()方法可以获得planar的类型的,这个保存在一个数组里面,可以进入远吗分析,就比较容易理解。

3. SwrResample的WriteInput(AVFrame* frame)

把一个frame的数据写swr的src_data里面,有了上面知识,就会好理解一些了,很多范例是没有考虑planar的,这样会造成一些兼容问题。大家可以根据注释去理解

这里主要frame->data 是分开L和R存储的,frame->data[0] 是所有的LLLLL,frame->data[1] 是所有的RRR

ini 复制代码
int SwrResample::WriteInput(AVFrame* frame)
{
    int planar = av_sample_fmt_is_planar(src_sample_fmt_);
    int data_size = av_get_bytes_per_sample(src_sample_fmt_);
    if (planar)
    {

        //src是planar类型的话,src_data里面数据是LLLLLLLRRRRR 结构,src_data_[0] 指向全部的L,src_data_[1] 指向全部R
        // src_data_ 里面其实一个长 uint8_t *buf,src_data_[0] 指向L开始的位置,src_data_[1]指向R的位置
        // linesize 是 b_samples * sample_size 就是比如 48000*4
        for (int ch = 0; ch < src_nb_channels; ch++) {
            memcpy(src_data_[ch], frame->data[ch], data_size * frame->nb_samples);
        }
    }
    else
    {
        //src是非planar类型的话,src_data里面数据是LRLRLRLR 结构,src_data_[0] 指向全部数据 没有src_data[1]
        // linesize 是nb_samples * sample_size * nb_channels 比如 48000*4*2
        for (int i = 0; i < frame->nb_samples; i++){
            for (int ch = 0; ch < src_nb_channels; ch++)
            {
                //拷贝后src_data[0] 指针就移动到了尾部
                memcpy(src_data_[0], frame->data[ch], data_size * frame->nb_samples);
            }
        }
    }
    return 0;
}

4. SwrResample的:SwrConvert() 执行

ini 复制代码
int SwrResample::SwrConvert()
{
    int ret = swr_convert(swr_ctx, dst_data_, dst_nb_samples_, (const uint8_t**)src_data_, src_nb_samples_);
    if (ret < 0) {
        fprintf(stderr, "Error while converting\n");
        exit(1);
    }

    int  dst_bufsize = av_samples_get_buffer_size(&dst_linesize, dst_nb_channels,
        ret, dst_sample_fmt_, 1);

   int planar = av_sample_fmt_is_planar(dst_sample_fmt_);
   if (planar)
   {
       int data_size = av_get_bytes_per_sample(dst_sample_fmt_);
       for (int i = 0; i < dst_nb_samples_; i++) {
           for (int ch = 0; ch < dst_nb_channels; ch++)
           {
               fwrite(dst_data_[ch]+i*data_size, 1, data_size, outdecodedswffile);
           }
       }
   }
   else {
       //非planr结构,dst_data_[0] 里面存在着全部数据
       fwrite(dst_data_[0], 1, dst_bufsize, outdecodedswffile);
   }

    return dst_bufsize;
}

这个方法就想对比较简单,swr_convert() 就是具体执行,返回执行结果。dst_bufsize是计算得出的转换后的这一个帧数据的大小,下面就是更具planar的类型存储。我们存储pcm文件的时候是采用非planar结构的,也就是LRLRLR,方便工具读取

5. SwrResample的 close 执行

scss 复制代码
void SwrResample::Close()
{

#ifdef WRITE_RESAMPLE_PCM_FILE
    fclose(outdecodedswffile);
#endif

    if (src_data_)
        av_freep(&src_data_[0]);

    av_freep(&src_data_);

    if (dst_data_)
        av_freep(&dst_data_[0]);

    av_freep(&dst_data_);

    swr_free(&swr_ctx);
}

调用,我们在上一个解码后的音频后面调用

ini 复制代码
int FileDecode::DecodeAudio(AVPacket* originalPacket)
{
    int ret = avcodec_send_packet(codecCtx, originalPacket);
    if (ret < 0)
    {
        return -1;
    }
    AVFrame* frame = av_frame_alloc();
    ret = avcodec_receive_frame(codecCtx, frame);
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){
        return -2;
    }else if (ret < 0) {
        std::cout << "error decoding";
        return -1;
    }

    int data_size = av_get_bytes_per_sample(codecCtx->sample_fmt);
    if (data_size < 0) {
        /* This should not occur, checking just for paranoia */
        std::cout << "Failed to calculate data size\n";
        return -1;
    }

#ifdef WRITE_DECODED_PCM_FILE
    for (int i = 0; i < frame->nb_samples; i++)
        for (int ch = 0; ch < codecCtx->channels; ch++)
            fwrite(frame->data[ch] + data_size * i, 1, data_size, outdecodedfile);
#endif

    // 把AVFrame里面的数据拷贝到,预备的src_data里面
    if (swrResample == NULL)
    {
        swrResample = new SwrResample();

        //创建重采样信息
        int src_ch_layout = codecCtx->channel_layout;
        int src_rate = codecCtx->sample_rate;
        enum AVSampleFormat src_sample_fmt = codecCtx->sample_fmt;

        int dst_ch_layout = AV_CH_LAYOUT_STEREO;
        int dst_rate = 44100;
        enum AVSampleFormat dst_sample_fmt = codecCtx->sample_fmt;

        //aac编码一般是这个,实际这个值只能从解码后的数据里面获取,所有这个初始化过程可以放在解码出第一帧的时候
        int src_nb_samples = frame->nb_samples;

        swrResample->Init(src_ch_layout, dst_ch_layout,
            src_rate, dst_rate,
            src_sample_fmt, dst_sample_fmt,
            src_nb_samples);
    }
   
    ret = swrResample->WriteInput(frame);

    int res = swrResample->SwrConvert();
   
    av_frame_free(&frame);
    return 0;
}

FileDecode 见上一个博客的描述

运行

执行后会输出一个decode_resample.pcm 文件,用Audioactity去播放验证自己的代码是否正确

两个波形图一致,且能正确播放,就说明转换成功

6 总结

设计音频转换这一块其实,理解难度还是有的,特别对于初学这,里面有很多方法,而且功能类似,大家可以读ffmpeg源代码的方式帮助理解。

其他:

相关推荐
yunhuibin1 小时前
ffmpeg面向对象——拉流协议匹配机制探索
学习·ffmpeg
cuijiecheng20187 小时前
音视频入门基础:FLV专题(13)——FFmpeg源码中,解析任意Type值的SCRIPTDATAVALUE类型的实现
ffmpeg·音视频
小神.Chen1 天前
YouTube音视频合并批处理基于 FFmpeg的
ffmpeg·音视频
昱禹3 天前
记一次因视频编码无法在浏览器播放、编码视频报错问题
linux·python·opencv·ffmpeg·音视频
寻找09之夏3 天前
【FFmpeg 深度解析】:全方位视频合成
ffmpeg·音视频
zanglengyu3 天前
ffmpeg取rtsp流音频数据保存声音为wav文件
ffmpeg·音视频
cuijiecheng20183 天前
音视频入门基础:FLV专题(11)——FFmpeg源码中,解析SCRIPTDATASTRING类型的ScriptDataValue的实现
ffmpeg·音视频
汪子熙3 天前
什么是 LDAC、SBC 和 AAC 音频编码技术
ffmpeg·音视频·aac
cpp_learners3 天前
Windows环境 源码编译 FFmpeg
windows·ffmpeg·源码编译·ffmpeg源码编译
cuijiecheng20184 天前
音视频入门基础:FLV专题(8)——FFmpeg源码中,解码Tag header的实现
ffmpeg·音视频