音频特效滤镜 via FFmpeg Filter
音频特效定义
音效或声效(Sound effects 或 Audio effects)是人工制造或加强的声音,用来增强对电影、电子游戏、音乐或其他媒体的艺术或其他内容的声音处理。
常见的音效技术有:回声、合唱、均衡(EQ)、过滤、变调、移相、压缩 / 拉伸、3D、调制和共鸣等等。
FFmpeg filter
FFmpeg 中的 libavfilter 提供了一整套的基于 filter 的机制。Filter 本身是一个插件的形式,可以快速的组装出需要的效果。
比如下面的 filter,可以实现音频的回声效果。
ffplay.exe sample.mp3 -af aecho=0.8:0.9:1000:0.3
FFmpeg 中 filter 分为:
- source filter (只有输出)
- audio filter
- video filter
- multimedia filter
- sink filter (只有输入)
这里 演示了一些非常精美的 filter。
FFmpeg filter graph
FFmpeg 将 filtergraph 分为 simple filtergraph 和 complex filtergraph。
- Simple filtergraph 通常只有一个输入和输出,FFmpeg 命令行中使用 -vf、-af 识别,基本原理图如下:
- Complex filtergraph 通常是具有多个输入输出文件,并有多条执行路径,FFmpeg 命令行中使用 -lavfi、-filter_complex 识别,基本原理图如下:
Filter graph 的语法
FFmpeg 中 filter 包含三个层次,filter → filterchain → filtergraph。
Filter 是 libavfilter 提供的基础单元。在同一个 filterchain 中的 filter 使用逗号分隔,在不同 filterchain 中的 filter 使用分号隔开,比如下面的例子:
ffmpeg -i INPUT -vf "split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2" OUTPUT
Filtergraph 将输入流分成两个流,其中一个通过 crop filter 和 vflip filter,然后通过 overlay filter 将这个流和原始输入流合成一个流输出。如果 filter 需要输入参数,多个参数使用冒号分割。
这里 crop、vflip 处于同一个 filterchain,split、overlay 位于另一个 filterchain。二者通过命名的 label 实现连接(中括号中的即是 label 的名字)。在上例中 split filter 有两个输出,依次命名为 [main] 和 [tmp];[tmp] 作为 crop filter 的输入,之后通过 vflip filter 输出 [flip];overlay 的输入是 [main] 和 [flilp]。
av_filter_base 类
用 FFmpeg 的 filter 开发时主要区别就是把不同的 filter description string 传到 filtergraph 中,然后设定 sink 端 prefer 的参数,其他部分几乎是一样的,所以我把 filter 的使用封装成了一个 av_filter_base 类:
typedef int (*pf_filter_callback)(AVFrame *frame);
class av_filter_base
{
public:
av_filter_base();
virtual ~av_filter_base();
int init(AVCodecContext *dec_ctx, AVRational stream_time_base, const char *filters_descr);
int do_filter(AVFrame* in_frame, pf_filter_callback callback_proc);
int do_filter(AVFrame* in_frame, std::queue<AVFrame*>& out_frames);
void finalize();
protected:
virtual int init_opts(); // to override this function in sub class
private:
int audio_filter_init_begin(AVCodecContext *dec_ctx, AVRational stream_time_base);
int video_filter_init_begin(AVCodecContext *dec_ctx, AVRational stream_time_base);
int filter_init_end(const char *filters_descr);
protected:
AVFilterContext *m_buffersink_ctx;
AVFilterContext *m_buffersrc_ctx;
AVFilterGraph *m_filter_graph;
AVFrame *m_filt_frame;
};
av_filter_base::init 函数
基类的 init 函数使用了设计模式中的模板方法,子类需要实现 init_opts() 虚函数对 sink 端设置 prefer 参数。
// template method design pattern
int init(AVCodecContext *dec_ctx, AVRational stream_time_base, const char *filters_descr)
{
int hr = -1;
if (dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO)
hr = audio_filter_init_begin(dec_ctx, stream_time_base);
else
hr = video_filter_init_begin(dec_ctx, stream_time_base);
RETURN_IF_FAILED(hr);
hr = init_opts();
RETURN_IF_FAILED(hr);
hr = filter_init_end(filters_descr);
RETURN_IF_FAILED(hr);
return 0;
}
av_filter_base::audio_filter_init_begin 函数
创建 audio source 和 sink filter。
int audio_filter_init_begin(AVCodecContext *dec_ctx, AVRational stream_time_base)
{
int hr = -1;
m_filter_graph = avfilter_graph_alloc();
RETURN_IF_NULL(m_filter_graph);
if (!dec_ctx->channel_layout)
dec_ctx->channel_layout = av_get_default_channel_layout(dec_ctx->channels);
char args[512] = {0};
_snprintf_s(args, sizeof(args),
"time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%"PRIx64,
stream_time_base.num, stream_time_base.den, dec_ctx->sample_rate,
av_get_sample_fmt_name(dec_ctx->sample_fmt), dec_ctx->channel_layout);
/* buffer audio source: the decoded frames from the decoder will be inserted here. */
const AVFilter *abuffersrc = avfilter_get_by_name("abuffer");
hr = avfilter_graph_create_filter( &m_buffersrc_ctx, abuffersrc, "in", args, NULL, m_filter_graph );
RETURN_IF_FAILED(hr);
/* buffer audio sink: to terminate the filter chain. */
const AVFilter *abuffersink = avfilter_get_by_name("abuffersink");
hr = avfilter_graph_create_filter( &m_buffersink_ctx, abuffersink, "out", NULL, NULL, m_filter_graph );
RETURN_IF_FAILED(hr);
return hr;
}
Audio_filter 子类
init_opts 函数需要子类去重写,下面的例子设定了 sink filter 支持的参数(格式、声道和采样率)范围。
class Audio_filter : public av_filter_base
{
protected:
virtual int init_opts()
{
int hr = -1;
const enum AVSampleFormat out_sample_fmts[] = { AV_SAMPLE_FMT_S16, AV_SAMPLE_FMT_NONE };
const int64_t out_channel_layouts[] = { AV_CH_LAYOUT_MONO, AV_CH_LAYOUT_STEREO, -1 };
const int out_sample_rates[] = { 16000, 22050, 44100, 48000, -1 };
hr = av_opt_set_int_list(m_buffersink_ctx, "sample_fmts", out_sample_fmts, -1, AV_OPT_SEARCH_CHILDREN);
RETURN_IF_FAILED(hr);
hr = av_opt_set_int_list(m_buffersink_ctx, "channel_layouts", out_channel_layouts, -1, AV_OPT_SEARCH_CHILDREN);
RETURN_IF_FAILED(hr);
hr = av_opt_set_int_list(m_buffersink_ctx, "sample_rates", out_sample_rates, -1, AV_OPT_SEARCH_CHILDREN);
RETURN_IF_FAILED(hr);
return 0;
}
};
av_filter_base::filter_init_end 函数
这里完成了 filter graph 的构建(src → filter(s) → sink)。没看懂?拿去用就行了~
int filter_init_end(const char *filters_descr)
{
int hr = -1;
/* The buffer source output must be connected to the input pad of the first filter described by
* filters_descr; since the first filter input label is not specified, it is set to "in" by default. */
AVFilterInOut *outputs = avfilter_inout_alloc();
outputs->name = av_strdup("in");
outputs->filter_ctx = m_buffersrc_ctx;
outputs->pad_idx = 0;
outputs->next = NULL;
/* The buffer sink input must be connected to the output pad of the last filter described by
* filters_descr; since the last filter output label is not specified, it is set to "out" by default. */
AVFilterInOut *inputs = avfilter_inout_alloc();
inputs->name = av_strdup("out");
inputs->filter_ctx = m_buffersink_ctx;
inputs->pad_idx = 0;
inputs->next = NULL;
hr = avfilter_graph_parse_ptr(m_filter_graph, filters_descr, &inputs, &outputs, NULL);
hr = avfilter_graph_config(m_filter_graph, NULL);
m_filt_frame = av_frame_alloc();
avfilter_inout_free(&inputs);
avfilter_inout_free(&outputs);
return hr;
}
av_filter_base::do_filter 函数
一进 N 出(N >= 0),新的 FFmpeg API 似乎都这样,注意判断 API 的返回值(AVERROR(EAGAIN) ,AVERROR_EOF)即可。剩下的都丢给回调函数。
typedef int (*pf_filter_callback)(AVFrame *frame);
int do_filter(AVFrame* in_frame, pf_filter_callback callback_proc)
{
RETURN_IF_NULL(in_frame);
int hr = -1;
/* push the decoded frame into the filtergraph */
hr = av_buffersrc_add_frame_flags( m_buffersrc_ctx, in_frame, AV_BUFFERSRC_FLAG_KEEP_REF );
RETURN_IF_FAILED(hr);
/* pull filtered frame from the filtergraph */
while (true) {
hr = av_buffersink_get_frame( m_buffersink_ctx, m_filt_frame );
if (hr == AVERROR(EAGAIN) || hr == AVERROR_EOF)
break;
RETURN_IF_FAILED(hr);
if (NULL != callback_proc)
callback_proc(m_filt_frame);
av_frame_unref(m_filt_frame);
}
return hr;
}
结语
本文讲解的是如何使用 FFmpeg 的 filter,而不是如何写一个 filter,那么可以自己写一个标准的 filter 吗?答案是肯定的,BUT,要自己把 filter 集成到 libavfilter 中去,或者写信给 FFmpeg 组织请求他们把你的 filter 集成到官方的 libavfilter 中去。So,还是踏实点写个算法函数吧 (╯-╰)。
其他框架的滤镜
– EOF –