FFmpeg音频重采样流程笔记

本文是ffmpeg学习 函数分析swr_convert_wg-CSDN博客_swr_convert的笔记

对PCM原始数据的采样率、帧格式、通道数进行重采样功能封装

通常音频编码之前都会重采样,重采样不仅仅是频率改变44.1k转48k
libswresample主要是用于音频的重采样和格式转换的,包含如下功能:

采样频率转换:对音频的采样频率进行转换的处理,例如把音频从一个高的44100Hz的采样频率转换到8000Hz;从高采样频率到低采样频率的音频转换是一个有损的过程

声道格式转换:对音频的声道格式进行转换的处理,例如立体声转换为单声道;当输入通道不能映射到输出流时,这个过程是有损的,因为它涉及不同的增益因素和混合。

采样格式转换:对音频的样本格式进行转换的处理,例如把s16(AV_SAMPLE_FMT_S16)的PCM数据转换为s8格式或者f32的PCM数据;此外提供了Packed和Planar包装格式之间相互转换的功能

样本格式
FFmpeg音频重采样流程笔记_第1张图片

带P和不带P,关系到了AVFrame中的data的数据排列,不带P,则是LRLRLRLRLR排列,带P则是LLLLLRRRRR排列,若是双通道则带P则意味着data[0]全是L,data[1]全是R(注意:这是采样点不是字节),PCM播放器播放的文件需要的是LRLRLRLR的 

自定义实现:

采样格式转换
采样数据从32位float类型数据转换位无符号8位uchar类型,需要将取值范围转换到[0,255]。

for(int n = 0; n < frame->nb_samples; n++)
    for(int c = 0; c < frame->channels; c++) {
        float vsrc = *(float *)(frame->data[c] + n*in_sample_bytes);
        unsigned char vdst = (vsrc*128 + 128);
        fwrite(&vdst, sizeof(unsigned char), 1, fpcm);
    }

采样数据从从32位float类型数据转换位16位short类型,需要将取值范围转换到[-32768~32767]。

for(int n = 0; n < frame->nb_samples; n++)
    for(int c = 0; c < frame->channels; c++) {
        float vsrc = *(float *)(frame->data[c] + n*in_sample_bytes);
        short vdst = vsrc*32768;
        fwrite(&vdst, sizeof(short), 1, fpcm);
    }

notes: float范围是[-1.0,1.0], uchar [0,255], short [-32768,32768]
float 转成uchar,short就是乘以一个增益. 增加一个offset


声道格式转换
通道从少变多,可以复制一个通道数据。
从多变少,可以直接保留需要的声道。
从原来的2个通道,保存为1个通道,可以选择保存一个或者去平均;

for(int n = 0; n < frame->nb_samples; n++) {
    float vdst = 0;
    for(int c = 0; c < frame->channels; c++) 
        vdst += *(float *)(frame->data[c] + n*in_sample_bytes);
    vdst /= frame->channels;
    fwrite(&vdst, sizeof(float), 1, fpcm);
}

notes:上例是平均两个声道,写一个byte.(通常两个声道差不多)

采样频率转换
这里仅给出,转换前频率是转换后频率的整数倍,例如转换前后频率分别为48000和8000。
我们将输入的采样数据每间隔6个保存一个即可。例如

for(int n = 0; n < frame->nb_samples; n+=6)
    for(int c = 0; c < frame->channels; c++) {
        float vsrc = *(float *)(frame->data[c] + n*in_sample_bytes);
        char vdst = vsrc*128;
        fwrite(&vdst, sizeof(char), 1, fpcm);
    }
}

notes:上例将减少了采样率,拷贝的时候从每组6个中取1,其他的舍弃.

libswresample库使用
当音频的采样率与播放器的采样率不一致时,那么想在播放器正常播放,就需要对音频进行重采样,否则可能会出现音频变速的问题(两个采样频率不能整除,手动处理需要插值补齐等)。
这里着重介绍使用libswresample库处理音频采样数据的转换。

 

在swr_convert之前少了 av_rescale_rnd ,重新计算nb_sample

1) 、重采样准备设置

  1. swr_alloc() :创建一个SwrContext 音频转换上下文结构体
  2. swr_alloc_set_opts(swrContext, out_channel_layout, out_sample_fmt, out_sample_rate,in_channel_layout, in_sample_fmt, in_sample_rate,0, NULL) : 设置输出和输入格式, 其中采样率最好采用动态获取,因为每个音频的采样率可能不同
  3. swr_init(swrContext) :初始化上下文
  4. 计算缓冲数据输出

2) 、开始转换
(1) 动态计算输出数量

int64_t dst_nb_samples = av_rescale_rnd(swr_get_delay(swrContext,avFrame->sample_rate) + avFrame->nb_samples, out_sample_rate,avFrame->sample_rate,AV_ROUND_UP);

av_rescale_q 是time_base转换函数, 计算方法"a * b / c" 
flv 封装格式的 time_base 为{1,1000},ts 封装格式的 time_base 为{1,90000}
看第一帧的时间戳,计算关系:80×{1,1000} == 7200×{1,90000} == 0.080000

(2)、swr_convert() 真正转换的api,avFrame->data 中 往缓冲中输入数据
(3)、计算采样缓冲的大小方便,然后从缓冲往文件中写
av_rescale_q   详见 FFmpeg时间戳详解 - 叶余 - 博客园

示例代码

输入pcm文件格式为数据深度16位、44100Hz采样频率、双通道(packed),
输出pcm文件格式为数据深度32位整形、44100Hz采样频率、双通道(plannar)。

主要steps:swr_convert 实现重采样,然后按照packed,plannar改写左右声道output顺序

int main()
{
    //输入文件和参数
    FILE *in_file = fopen("../files/Titanic_44100_s16_stero.pcm", "rb");
    const int in_sample_rate = 44100;
    AVSampleFormat in_sfmt = AV_SAMPLE_FMT_S16;  // 输入数据交错存放,非plannar
    uint64_t in_channel_layout = AV_CH_LAYOUT_STEREO;
    int in_channels = av_get_channel_layout_nb_channels(in_channel_layout);
    const int in_nb_samples = 2048;

    int in_spb = av_get_bytes_per_sample(in_sfmt);

    // 输出文件和参数
    FILE *out_file = fopen("out.pcm", "wb");
    const int out_sample_rate = 48000;
    AVSampleFormat out_sfmt = AV_SAMPLE_FMT_S32P;
    uint64_t out_channel_layout = AV_CH_LAYOUT_STEREO;
    int out_channels = av_get_channel_layout_nb_channels(out_channel_layout);
    int out_nb_samples = av_rescale_rnd(in_nb_samples, out_sample_rate, in_sample_rate, AV_ROUND_UP);

    int out_spb = av_get_bytes_per_sample(out_sfmt);

    //使用AVFrame分配缓存音频pcm数据,用于转换
    AVFrame *in_frame = av_frame_alloc();
    av_samples_alloc(in_frame->data, in_frame->linesize, in_channels, in_nb_samples, in_sfmt, 1);

    AVFrame *out_frame = av_frame_alloc();
    av_samples_alloc(out_frame->data, out_frame->linesize, out_channels, out_nb_samples, out_sfmt, 1);

    // swr上下文
    //SwrContext *swr_ctx = swr_alloc();
    //av_opt_set_channel_layout(swr_ctx, "in_channel_layout", in_channel_layout, 0);
    //av_opt_set_channel_layout(swr_ctx, "out_channel_layout", out_channel_layout, 0);
    //av_opt_set_int(swr_ctx, "in_sample_rate", in_sample_rate, 0);
    //av_opt_set_int(swr_ctx, "out_sample_rate", out_sample_rate, 0);
    //av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", in_sfmt, 0);
    //av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", out_sfmt, 0);
    //swr_init(swr_ctx);

    SwrContext *swr_ctx = NULL;
    swr_ctx = swr_alloc_set_opts(swr_ctx, 
                                 out_channel_layout, out_sfmt, out_sample_rate, 
                                 in_channel_layout, in_sfmt, in_sample_rate, 0, NULL);
    swr_init(swr_ctx);

    修改参数
    //av_opt_set_int(swr_ctx, "in_sample_rate", in_sample_rate, 0);
    //swr_init(swr_ctx);

    // 用于读取的缓冲数据
    int buf_len = in_spb*in_channels*in_nb_samples;
    void *buf = malloc(buf_len);
    
    // 转换保存
    int frameCnt = 0;

    while(1) {  
        // read samples
        int read_samples = fread(in_frame->data[0], in_spb*in_channels,in_nb_samples, in_file);
        // convert prepare
        int dst_nb_samples = av_rescale_rnd(
            swr_get_delay(swr_ctx, in_sample_rate) + in_nb_samples,
            out_sample_rate,
            in_sample_rate, AV_ROUND_UP);

        if(dst_nb_samples > out_nb_samples) {
            av_frame_unref(out_frame);
            out_nb_samples = dst_nb_samples;
            av_samples_alloc(out_frame->data, out_frame->linesize, out_channels, out_nb_samples, out_sfmt, 1);
        }

        // convert
        int out_samples = swr_convert(swr_ctx, 
                                      out_frame->data, out_nb_samples,
                                      (const uint8_t**)in_frame->data, read_samples);

        // write
        if(av_sample_fmt_is_planar(out_sfmt)) { // plannar
            for(int i = 0; i < out_samples; i++) {
                for(int c = 0; c < out_channels; c++)
                    fwrite(out_frame->data[c] + i*out_spb, 1, out_spb, out_file);
            }
        }
        else {  // packed
            fwrite(out_frame->data[0], out_spb*out_channels, out_samples, out_file);
        }
}....}

主要函数说明:
swr_get_delay

av_rescale_rnd(swr_get_delay(swr_ctx, 48000) + 1024, 44100, 48000, AV_ROUND_UP);
这里48000是输入音频A的采样率,44100是输出视频B的采样率,这行代码的意思是说,如果转换1024个音频A,能够生成多少个音频B.
那为什么要使用swr_get_delay呢,大家可以这么理解,当我们的项目是一个实时的推流项目的时候,假设我们的输入音频是48000采样率,推流输出需要44100的采样率,为了保证音视频的同步,我们必须保证每秒钟能够输出44100 * 2个采样点B(这里假设是双声道).但是要知道重采样本身也需要时间,因此,我们需要将这个时间计算出来,转换为相当于几个输入音频A的采样点,这样就可以保证每秒的输出为44100*2个采样点B,从而保证音视频的同步。

注意:多出来的采样点B用静默音0来填充。(当低采样率变成高采样率的时候)

本例而言理解为:in_nb_samples表示播放这些sample的时间。
swr_get_delay(swr_ctx, in_sample_rate) 表示重采样这些sample的时间。
等于编码器1秒编出来,44100*2+1024个sample,赶回来花费的重采样时间,保证了1s还有44100*2个采样点B

输出采样数据个数
输出采样频率发生变化,那么单通道采样个数也相应发生变化。频率变高,采样数据增加;频率降低,采样数据减少。计算方式为

int out_nb_samples = av_rescale_rnd(in_nb_samples, out_sample_rate, in_sample_rate, AV_ROUND_UP);

转换数据个数计算

在实际使用中,可能存在输入采样数据个数变化/延时,当输入增大,swr_ctx内部会进行缓冲,不及时取出可能造成数据堆积,影响输出(例如实时推流)。
此时需要重新分配空间,接收当前转换数据及缓冲数据,

int dst_nb_samples = av_rescale_rnd(
            swr_get_delay(swr_ctx, in_sample_rate) + in_nb_samples,
            out_sample_rate,
            in_sample_rate, AV_ROUND_UP);
if(dst_nb_samples > out_nb_samples) {
    // 释放原空间,重新分配
}

swr_convert调用及结果处理

传参时,输出的缓冲数据区和对应的采样数据量,是动态调整的结果值。处理转换后的采样数据时,应该以swr_convert返回值为准。

例如实际转换得到的采样数据数量为out_samples,则后续处理为

    // write
    if(av_sample_fmt_is_planar(out_sfmt)) { // plannar
        for(int i = 0; i < out_samples; i++) {
            for(int c = 0; c < out_channels; c++)
                 fwrite(out_frame->data[c] + i*out_spb, 1, out_spb, out_file);
        }
    }
    else {  // packed
        fwrite(out_frame->data[0], out_spb*out_channels, out_samples, out_file);
    }

最后flush时的输出处理也同上。

你可能感兴趣的:(Janus,音视频)