FFmpeg swresample的经验整理


音频重采样原理


音频应用有时遇到44.1kHz/48KHz/32kHz/16kHz以及8kHz之间互相转换,这一过程称为SRC(sample rate converter),产品上有用codec芯片硬件实现SRC功能,有用软件实现SRC。


采样率转换的基本思想是抽取和内插,从信号角度看音频重采样就是滤波。滤波函数的窗口大小以及插值函数一旦被确定,其重采样的性能也就确定了。

  • 抽取可能引起频谱混叠,而内插会产生镜频分量。
  • 通常在抽取前先加抗混叠滤波器,在内插后加抗镜频滤波器,在语音识别里所需语音信号采样率由ASR(automatic speech recognition)声学模型输入特征决定的。

resample函数


resample函数是一个数字信号处理中的重采样函数,它用于将一个原始信号的采样频率改变为另一个频率。重采样的目的是改变信号的频率,以便在信号处理过程中对其进行重新调整。

重采样的基本原理是通过使用一种称为插值的技术来生成新的采样点,从而改变信号的频率。插值方法可以是线性插值、插值多项式、插值样条等。

具体的,resample函数会对信号的每一个采样点进行处理,并通过计算与该采样点相邻的数据点的加权平均值来生成新的采样点。最终,它会返回一个新的信号,该信号具有指定的采样频率。


FFmpeg swresample


swresample是FFmpeg中的音频重采样库,它主要包括:

  1. 支持多种采样率和采样格式之间的转换,可以满足不同应用场景的需求。
  2. 支持多通道音频的处理,可以处理立体声、环绕声等多种音频格式。
  3. 支持多种重采样算法,可以根据不同的需求选择不同的算法。
  4. 支持动态调整采样率和通道数,可以根据实际情况进行调整。

swresample支持的插值类型

  • Cubic interpolation是一种插值方法,用于在已知的离散数据点之间估计未知的数据点。它使用三次多项式来逼近数据点之间的曲线,从而得到更平滑的插值结果。Cubic interpolation通常用于图像处理和计算机图形学中,用于对图像进行缩放和旋转等操作。它的优点是插值结果平滑,缺点是可能会引入一些误差,特别是在数据点之间存在急剧变化的情况下。
  • Blackman Nuttall windowed sinc interpolation 是一种数字信号处理中的插值方法,它使用Blackman Nuttall窗口函数对Sinc插值函数进行加窗处理,从而得到更好的频域和时域特性,用于在离散时间信号中进行高质量的插值处理。
  • Kaiser windowed sinc interpolation使用Kaiser窗口函数对Sinc插值函数进行加窗处理,从而得到更好的频域和时域特性,用于在离散时间信号中进行高质量的插值处理。Kaiser窗口函数是一种可调节的窗口函数,可以通过调节窗口函数的参数来平衡频域和时域特性,从而得到更好的插值效果。Kaiser windowed sinc interpolation通常用于音频处理和数字信号处理中,用于对信号进行重采样和插值等操作。它的优点是插值结果平滑,且可以通过调节窗口函数的参数来控制插值效果,缺点是计算复杂度较高。
SWR_FILTER_TYPE_CUBIC,              /**< Cubic */
SWR_FILTER_TYPE_BLACKMAN_NUTTALL,   /**< Blackman Nuttall windowed sinc */
SWR_FILTER_TYPE_KAISER,             /**< Kaiser windowed sinc */

重采样引擎

重采样引擎除了支持sw resampler外,还支持sox resampler,sox依赖于external库soxr。


SWR_ENGINE_SWR,             /**< SW Resampler */
SWR_ENGINE_SOXR,            /**< SoX Resampler */
SWR_ENGINE_NB,              ///< not part of API/ABI

auto resampler

这里说的auto resampler是指ffmpeg中,在没有指定resampler的情况下,根据输入输出,自动调用resample的情况。

给定下面的播放命令:

ffmpeg -i test-44.1k.wav -ar 48000 test-48k.wav -v 56

增加-v参数后,可以从log中看到自动插入了resampler,这里是auto_resampler_0

[AVFilterGraph @ 0x557f1a953080] query_formats: 4 queried, 6 merged, 3 already done, 0 delayed
[auto_resampler_0 @ 0x557f1a9a5000] [SWR @ 0x557f1a9a5380] Using s16p internally between filters
[auto_resampler_0 @ 0x557f1a9a5000] ch:2 chl:stereo fmt:s16 r:44100Hz -> ch:2 chl:stereo fmt:s16 r:48000Hz

auto resample的代码

前面auto_resampler_0的名字是通过下面这个snprintf语句生成,找到对应代码,就可以看到conversion_filter的上下文:

if (!(filter = avfilter_get_by_name(neg->conversion_filter))) {
    av_log(log_ctx, AV_LOG_ERROR,
           "'%s' filter not present, cannot convert formats.\n",
           neg->conversion_filter);
    return AVERROR(EINVAL);
}
snprintf(inst_name, sizeof(inst_name), "auto_%s_%d",
         neg->conversion_filter, converter_count++);

ff_filter_get_negotiation的时候,根据media类型返回video或者audio的negotiate,video用的scale,audio是aresample。

const AVFilterNegotiation *ff_filter_get_negotiation(AVFilterLink *link)
{
    switch (link->type) {
    case AVMEDIA_TYPE_VIDEO: return &negotiate_video;
    case AVMEDIA_TYPE_AUDIO: return &negotiate_audio;
    default: return NULL;
    }
}

static const AVFilterNegotiation negotiate_audio = {
    .nb_mergers = FF_ARRAY_ELEMS(mergers_audio),
    .mergers = mergers_audio,
    .conversion_filter = "aresample",
    .conversion_opts_offset = offsetof(AVFilterGraph, aresample_swr_opts),
};

static const AVFilterNegotiation negotiate_video = {
    .nb_mergers = FF_ARRAY_ELEMS(mergers_video),
    .mergers = mergers_video,
    .conversion_filter = "scale",
    .conversion_opts_offset = offsetof(AVFilterGraph, scale_sws_opts),
};

filter graph

前面的命令中没有指定resampler filter,auto_resampler_0上是通过filter graph插入的,ffmpeg中即使没有显示指定filter,也会configure filter。

从调用栈可以看到,decode_audio的时候,通过send_frame_to_filters将frame发送给filter处理。

query_formats(AVFilterGraph * graph, void * log_ctx) (libavfilter/avfiltergraph.c:487)
graph_config_formats(AVFilterGraph * graph, void * log_ctx) (libavfilter/avfiltergraph.c:1102)
avfilter_graph_config(AVFilterGraph * graphctx, void * log_ctx) (libavfilter/avfiltergraph.c:1172)
configure_filtergraph(FilterGraph * fg) (fftools/ffmpeg_filter.c:1090)
ifilter_send_frame(InputFilter * ifilter, AVFrame * frame, int keep_reference) (fftools/ffmpeg.c:2000)
send_frame_to_filters(InputStream * ist, AVFrame * decoded_frame) (fftools/ffmpeg.c:2076)
decode_audio(InputStream * ist, AVPacket * pkt, int * got_output, int * decode_failed) (fftools/ffmpeg.c:2142)
process_input_packet(InputStream * ist, const AVPacket * pkt, int no_eof) (fftools/ffmpeg.c:2414)
process_input(int file_index) (fftools/ffmpeg.c:4171)
transcode_step() (fftools/ffmpeg.c:4311)
transcode() (fftools/ffmpeg.c:4365)
main(int argc, char ** argv) (fftools/ffmpeg.c:4560)

指定filter_complex

如果命令中指定filter_complex选项,则在parse_optgroup后,根据filter_complex对应的处理函数,会调用opt_filter_complex()函数,完成ffmpeg中的全局变量filtergraphs的内存分配,同时nb_filtergraphs加1:

static int opt_filter_complex(void *optctx, const char *opt, const char *arg)
{
    /* 给filtergraphs分配内存 */
    FilterGraph *fg = ALLOC_ARRAY_ELEM(filtergraphs, nb_filtergraphs);

    fg->index      = nb_filtergraphs - 1;
    fg->graph_desc = av_strdup(arg);
    if (!fg->graph_desc)
        return AVERROR(ENOMEM);

    input_stream_potentially_available = 1;

    return 0;
}

而后再调用init_complex_filters()完成初始化:

/* create the complex filtergraphs */
ret = init_complex_filters();
if (ret < 0) {
    av_log(NULL, AV_LOG_FATAL, "Error initializing complex filters.\n");
    goto fail;
}

没有指定filter_complex

如果命令中没有指定filter_complex选项,对于output stream是audio或者video类型,都会调用init_simple_filtergraph()初始化一个simple graph:

if (ost->st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO ||
    ost->st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
    err = init_simple_filtergraph(ist, ost);
    if (err < 0) {
        av_log(NULL, AV_LOG_ERROR,
               "Error initializing a simple filtergraph between streams "
               "%d:%d->%d:%d\n", ist->file_index, ost->source_index,
               nb_output_files - 1, ost->st->index);
        exit_program(1);
    }
}

ffmpeg的auto_conversion_filters

在ffmpeg_opt.c中有这个定义:

int auto_conversion_filters = 1;

如果是0,那么audio conversion是都可以关掉的,这段代码在configure_filtergraph()函数中,flag设置为AVFILTER_AUTO_CONVERT_NONE,所有的自动转换会被禁用。

if (!auto_conversion_filters)
    avfilter_graph_set_auto_convert(fg->graph, AVFILTER_AUTO_CONVERT_NONE);

resample & rematrix

44100到48000:resamle

ffmpeg -i chengdu-44100.wav -ar 48000 chengdu-48000-ff.wav -v 56

1ch到2ch:rematrix

ffmpeg -i mono-lc3.wav -ac 2 2ch-testout.wav

resample的实现

在libswresample/resample_dsp.c中根据不同的format参数会生成不同的resample函数:

#define TEMPLATE_RESAMPLE_S16
#include "resample_template.c"
#undef TEMPLATE_RESAMPLE_S16

#define TEMPLATE_RESAMPLE_S32
#include "resample_template.c"
#undef TEMPLATE_RESAMPLE_S32

#define TEMPLATE_RESAMPLE_FLT
#include "resample_template.c"
#undef TEMPLATE_RESAMPLE_FLT

#define TEMPLATE_RESAMPLE_DBL
#include "resample_template.c"
#undef TEMPLATE_RESAMPLE_DBL

#elif    defined(TEMPLATE_RESAMPLE_S16)

#    define RENAME(N) N ## _int16

static int RENAME(resample_common)(ResampleContext *c,
                                   void *dest, const void *source,
                                   int n, int update_ctx)

这里用的是resample_common_int16:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OILDTCpW-1681640653492)(.images/image-20230324161150964.png)]

resample_init的时候,会根据不同参数创建filter bank,在resample_common_int16中,通过filterbank计算输出的采样值,这个会计算到每个字节的数据,所以整体是比较耗时的。

    for (dst_index = 0; dst_index < n; dst_index++) {
        FELEM *filter = ((FELEM *) c->filter_bank) + c->filter_alloc * index;

        FELEM2 val = FOFFSET;
        FELEM2 val2= 0;
        int i;
        for (i = 0; i + 1 < c->filter_length; i+=2) {
            val  += src[sample_index + i    ] * (FELEM2)filter[i    ];
            val2 += src[sample_index + i + 1] * (FELEM2)filter[i + 1];
        }
        if (i < c->filter_length)
            val  += src[sample_index + i    ] * (FELEM2)filter[i    ];
#ifdef FELEML
        OUT(dst[dst_index], val + (FELEML)val2);
#else
        OUT(dst[dst_index], val + val2);
#endif

        frac  += c->dst_incr_mod;
        index += c->dst_incr_div;
        if (frac >= c->src_incr) {
            frac -= c->src_incr;
            index++;
        }

        while (index >= c->phase_count) {
            sample_index++;
            index -= c->phase_count;
        }
    }

    if(update_ctx){
        c->frac= frac;
        c->index= index;
    }

    return sample_index;
}

rematrix的实现

ffmpeg通过swr_init()初始化swresample的时候,如果输入输出channel不一致,就会初始化rematrix:

s->rematrix = av_channel_layout_compare(&s->out_ch_layout, &s->in_ch_layout) ||
    s->rematrix_volume!=1.0 ||
    s->rematrix_custom;

if(s->rematrix || s->dither.method) {
    ret = swri_rematrix_init(s);
    if (ret < 0)
        goto fail;
}

以2channel到1channel的rematrix来看,s->matrix的值经过计算,s->matrix的值如下:

#其中s->matrix修改为s->matrix[4][4]

s->matrix[0][0] = 0.5
s->matrix[0][1] = 0.5
s->matrix[0][2] = 0
s->matrix[0][3] = 0

从输出log中也可以看到:

[auto_resampler_0 @ 0x557f75bfa6c0] [SWR @ 0x557f75bfab40] Matrix coefficients:
[auto_resampler_0 @ 0x557f75bfa6c0] [SWR @ 0x557f75bfab40] FC: FL:0.500000 FR:0.500000 

然后调用swri_rematrix完成rematrix:

if(s->resample_first){
    if(postin != midbuf)
        if ((out_count = resample(s, midbuf, out_count, postin, in_count)) < 0)
            return out_count;
    if(midbuf != preout) // rematrix
        swri_rematrix(s, preout, midbuf, out_count, preout==out);
}else{
    if(postin != midbuf) // rematrix
        swri_rematrix(s, midbuf, postin, in_count, midbuf==out);
    if(midbuf != preout)
        if ((out_count = resample(s, preout, out_count, midbuf, in_count)) < 0)
            return out_count;
}

参考

FFmpeg源码分析:resample重采样

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值