FFmpeg Resample(重采样)


此案例结合FFmpeg的官方例子,用于学习FFmpeg的resample的使用。

一、什么是重采样

改变音频的采样率sample rate、采样格式sample format、声道数channels等任一参数,使之按照期望的参数输出。

二、为什么要重采样

  • 在FFmpeg解码音频的时候,不同的音源有不同的格式,采样率等。在解码后的数据中的这些参数也会不一致(最新FFmpeg解码音频后,音频格式为AV_SAMPLE_FMT_FLTP,这个参数应该是一致的)。

    如果接下来需要使用解码后的音频数据做其他操作,而这些参数的不一致导致会有很多额外的工作,此时直接对其进行重采样,获取我们定制的音频参数,这样就会方便很多。

  • 在将音频进行SDL播放的时候,因为当前的SDL2.0不支持planar格式,也不支持浮点型的,而最新的FFmpeg会将音频解码为AV_SAMPLE_FMT_FLTP格式,因此需要进行重采样,使之可以在SDL2.0上进行播放。

三、如何重采样

  1. 采样率sample rate:采样设备每秒从连续信号中提取并组成离散信号的采样个数,用Hz表示;

  2. 采样格式sample format:每种音频格式有不同的量化精度(位宽),位数越多,表示值就越精确,声音表现就越精确;

    • FFmpeg中音频格式有以下几种,每种格式有其占用的字节数:
    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
    }
    • 分片plane和打包packed,比如双声道:
      • 带P(plane)的音频数据格式在存储时,其左声道和右声道的数据是分开存储的。左声道的数据存储在data[0],右声道的数据存储在data[1],每个声道所占用的字节数为linesize[0]和linesize[1],FFmepg默认是plane模式处理。
      • 不带P(packed)的音频数据在存储时,是按照LRLRLR…的格式交替存储在data[0]中,linesize[0]表示总的数据量。
  3. 通道布局channel layout:可以通过此参数获取声道数nb_channels;
    声道分布的定义在FFmpeg\libavutil\channel_layout.h中,一般用的比较多的是AV_CH_LAYOUT_STEREO(双声道)和AV_CH_LAYOUT_SURROUND(三声道),定义如下:

    #define AV_CH_LAYOUT_STEREO     (AV_CH_FRONT_LEFT | AV_CH_FRONT_RIGHT)
    #define AV_CH_LAYOUT_SURROUND   (AV_CH_LAYOUT_STEREO | AV_CH_FRONT_CENTER)
  4. 采样个数(样本数)nb_samples:采样的个数,对于采样频率,采样频率就是一秒的采样个数;

    对于一帧音频的采样个数,AAC固定是一帧采样1024个,MP3格式则为1152个。
    

音频帧的数据量计算:

  • 一帧音频的数据量(字节byte) = channel * nb_samples(样本数) * 每个样本占用的字节数

    比如音频帧是FLTP格式的PCM数据(也就是aac),包含1024个样本,双通道,那么该音频帧包含的音频数据量为:

    data_size * nb_samples * channels = 4 * 1024 * 2 = 8192 byte

    如果是AV_SAMPLE_FMT_FLTP,那么数据量为 8 * 1024 * 2 = 16384 byte

  • 一帧音频的播放时间(ms) = nb_samples样本数 * 1000 / 采样率

    比如以采样率44100Hz来计算,每秒44100个sample,正常一帧为为1024个sample,则每帧播放时间 = 1024 * 1000 / 44100 = 23.2ms

四、FFmpeg中几个重要的API

  1. swr_alloc_set_opts:创建上下文,设置参数,也可以使用swr_alloc()

  2. swr_init:初始化

  3. av_samples_alloc_array_and_samples:给输入源分配内存空间

  4. swr_convert:具体音频帧转换

  5. swr_free:释放上下文占用的资源

    流程图如下:

五、FFmpeg中示例resample_audio

/**
*@Author:Magic Miao
*@Blog: http://www.hust-miao.top
*@date: 2022/08/31
*description:FFmpeg 音频重新采样
* 创建一系列音频帧,然后进行重采样,设定格式和帧率,保存到另一个文件中
* 也就是改变一段音频的相关参数:采样率、采样通道、采样格式等<br>
*/
   
extern "C"
{
#include <libavutil/opt.h>
#include <libavutil/channel_layout.h>
#include <libavutil/samplefmt.h>
   
#include <libswresample/swresample.h>
}
   
/**
* description:此函数用于格式转换
* @param fmt
* @param sample_fmt
* @return
*/
static int fc_get_format_from_sample_fmt(const char **fmt,enum AVSampleFormat sample_fmt)
{
    struct sample_fmt_entry
    {
       enum AVSampleFormat sample_fmt;
       const char *fmt_be,*fmt_le;
    } sample_fmt_entries[] =  {
        {AV_SAMPLE_FMT_U8,  "u8",    "u8"   },
        {AV_SAMPLE_FMT_S16, "s16be", "s16le"},
        {AV_SAMPLE_FMT_S32, "s32be", "s32le"},
        {AV_SAMPLE_FMT_FLT, "f32be", "f32le"},
        {AV_SAMPLE_FMT_DBL, "f64be", "f64le"},
    };
   
    *fmt = NULL;
   
    for(int i = 0; i < FF_ARRAY_ELEMS(sample_fmt_entries); i++)
    {
        struct sample_fmt_entry *entry = &sample_fmt_entries[i];
   
        if(sample_fmt == entry->sample_fmt)
        {
           *fmt = AV_NE(entry->fmt_be,entry->fmt_le);
           return 0;
        }
    }
   
    fprintf(stderr,"Sample format %s not supported as output format\n",av_get_sample_fmt_name(sample_fmt));
   
    return AVERROR(EINVAL);
}
   
   
/**
* description:随机产生音频数据,Fill dst buffer with nb_samples,generated starting from t
* @param dst
* @param nb_samples
* @param nb_channels
* @param sample_rate
* @param t
* @return
*/
static void fill_samples(double *dst,int nb_samples,int nb_channels,int sample_rate,double *t)
{
    double tincr = 1.0 / sample_rate, *dstp = dst;
    const double c = 2 * M_PI * 440.0;
   
    //miao:产生双声道440Hz的数据
    //miao:nb_samples是采样数
    //miao:nb_channels为2
    //miao:generate sin tone with 440Hz frequency and duplicated channels
    for(int i = 0; i < nb_samples; i++)
    {
        *dstp = sin(c * *t);
   
        for(int j = 1;j < nb_channels;j++)
        {
            dstp[j] = dstp[0];
        }
   
        dstp += nb_channels;
        *t += tincr;
    }
}
   
int main(int argc, char *argv[])
{
    //miao:输入立体声,输出布局环绕声
    AVChannelLayout src_ch_layout = AV_CHANNEL_LAYOUT_STEREO,dst_ch_layout = AV_CHANNEL_LAYOUT_SURROUND;
   
    //miao:输入和输出的采样率
    int src_rate = 48000,dst_rate = 44100;
   
    uint8_t **src_data = NULL,**dst_data = NULL;
    int src_nb_channels = 0,dst_nb_channels = 0;
    int src_linesize,dst_linesize;
    int src_nb_samples = 1024,dst_nb_samples,max_dst_nb_samples;
   
    //miao:输入采样格式为“双精度”,输出为16位
    enum AVSampleFormat src_sample_fmt = AV_SAMPLE_FMT_DBL,dst_sample_fmt = AV_SAMPLE_FMT_S16;
    const char *dst_filename = NULL;
   
    FILE *dst_file;
    int dst_bufsize;
    const char *fmt;
    struct SwrContext *swr_ctx;
    char buf[64];
    double t;
    int ret;
   
    if(argc != 2)
    {
        fprintf(stderr,"Usage: %s output_file\n"
                "API example program to show how to resample an audio stream with libswresample.\n"
                "This program generates a series of audio frames,resamples them to a specified"
                "output format and rate and saves them to an output file named output_file.\n",argv[0]);
        exit(1);
    }
   
    dst_filename = argv[1];
   
    //miao:以“写出”模式,打开输出文件
    dst_file = fopen(dst_filename,"wb");
    if(!dst_file)
    {
        fprintf(stderr,"Could not open destination file %s\n",dst_filename);
        exit(1);
    }
   
    //miao:Create resampler context
    //miao:重采样上下文分配空间,重采样就是用来进行音频转换的
    swr_ctx = swr_alloc();
    if(!swr_ctx)
    {
        fprintf(stderr,"Could not allocate resampler context\n");
        ret = AVERROR(ENOMEM);
        goto end;
    }
   
    //miao:set options
    //miao:创建输入和输出音频的参数
    av_opt_set_chlayout(  swr_ctx,  "in_chlayout",    &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_chlayout(  swr_ctx, "out_chlayout",   &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);
   
    //miao:initialize the resampling context
    //miao:设置完参数后,需要init初始化
    if((ret = swr_init(swr_ctx)) < 0)
    {
        fprintf(stderr,"Failed to initialize the resampling context\n");
        goto end;
    }
   
    //miao:allocate source and destination samples buffers
    //miao:src通道布局的通道数
    src_nb_channels = src_ch_layout.nb_channels;
   
    //miao:利用通道数、采样格式、采样数,分配数据指针数组和数据长度
    ret = av_samples_alloc_array_and_samples(&src_data,&src_linesize,src_nb_channels,
                                                 src_nb_samples,src_sample_fmt,0);
    if(ret < 0)
    {
        fprintf(stderr,"Could not allocate source samples\n");
        goto end;
    }
   
    //miao:compute the number of converted samples:buffering is avoided ensuring that
    //miao:the output buffer will contain at lease all the converted input samples
    //miao:a * bq / cq,计算转换后的样本数,也就是输出文件的样本数,AV_ROUND_UP向上圆整
    max_dst_nb_samples = dst_nb_samples = av_rescale_rnd(src_nb_samples,dst_rate,src_rate,AV_ROUND_UP);
   
    //miao:buffer is going to be directly written to a rawaudio file,no alignment
    dst_nb_channels = dst_ch_layout.nb_channels;
   
    //miao:为dst数据分配指针数组,和src一样
    ret = av_samples_alloc_array_and_samples(&dst_data,&dst_linesize,dst_nb_channels,
                                                 dst_nb_samples,dst_sample_fmt,0);
    if(ret < 0)
    {
        fprintf(stderr,"Could not allocate destination samples\n");
        goto end;
    }
   
    t = 0;
    do
    {
        //miao:这里根据t来处理循环,t又累加1/采样率,因此这里是转换10s的数据
   
        //miao:generate synthetic audio
        //miao:随机产生音频数据,添到src_data[0]数组中,src_data为二维数组
        fill_samples((double*)src_data[0],src_nb_samples,src_nb_channels,src_rate,&t);
   
        //miao:compute destination number of samples
        //miao:a * b / c,计算目标采样数,同时加入了输入样本到输出样本间的延迟,得到输出的采样数目
        dst_nb_samples = av_rescale_rnd(swr_get_delay(swr_ctx,src_rate) + src_nb_samples,dst_rate,src_rate,AV_ROUND_UP);
        if(dst_nb_samples > max_dst_nb_samples)
        {
            //miao:计算的样本有误,需要重新获取数据指针
            av_freep(&dst_data[0]);
   
            //miao:重新分配输出数据的指针数组,这里更新了dst_linesize的数据
            ret = av_samples_alloc(dst_data,&dst_linesize,dst_nb_channels,dst_nb_samples,dst_sample_fmt,1);
            if(ret < 0) break;
   
            max_dst_nb_samples = dst_nb_samples;
        }
   
        //miao:convert to desitination format
        //miao:根据样本数目,把src数据转换成dst数据
        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");
            goto end;
        }
   
        //miao:根据格式获取指定大小,也就是dst_buffer实际写入的数据
        dst_bufsize = av_samples_get_buffer_size(&dst_linesize,dst_nb_channels,ret,dst_sample_fmt,1);
        if(dst_bufsize < 0)
        {
            fprintf(stderr,"Could not get sample buffer size\n");
            goto end;
        }
   
        printf("t:%f in:%d out:%d\n",t,src_nb_samples,ret);
   
        //miao:把数据写到输出文件
        fwrite(dst_data[0],1,dst_bufsize,dst_file);
    }
    while( t < 10);
   
    if((ret = fc_get_format_from_sample_fmt(&fmt,dst_sample_fmt)) < 0) goto end;
   
    av_channel_layout_describe(&dst_ch_layout,buf,sizeof(buf));
   
    fprintf(stderr,"Resampling succeeded.Play the output filie with the command:\n"
            "ffplay -f %s -channel_layout %s -channels %d -ar %d %s\n",
            fmt,buf,dst_nb_channels,dst_rate,dst_filename);
   
end:
    fclose(dst_file);

    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);
   
    return ret < 0;
}
   

Author: Magic Miao
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Magic Miao !
评论
  TOC