ffmpeg-音频淡入淡出混合滤镜(二十三)

前言

两段音频拼接到一起,希望一首音频播放完毕过渡到另外一首音频时有一个平滑的转场效果。这就是本文要实现的目的

ffmpeg命令行音频滤镜格式

ffmpeg命令行工具提供了滤镜的语法格式,格式如下:
ffmpeg <-filter/vf/-af/-filter_complex> filtergraph

  • -filter/-vf/-af/-filter_complex代表ffmpeg命令行工具将使用滤镜功能,其中-filter代表使用简单的音视频滤镜。-vf/-af 代表使用简单的音/视频滤镜 ;-filter_complex代表使用复杂的音视频滤镜
  • filtergraph 代表滤镜管道的语法,滤镜管道由至少一个滤镜链组成(多个滤镜链之间用";"分隔),每个滤镜链仅且必须代表一条输出链;每一个滤镜链由至少一个滤镜组成(多个滤镜之间用","分隔)。每一个滤镜的语法格式如下:

[in_link_1]...[in_link_N]filter_name[@id]=arguments[out_link_1]...[out_link_M]

  • in_link_xx 代表滤镜的inputpad的标签名
  • @id 代表滤镜上下文的标识名(最终为filter_name@id),类似于avfilter_graph_alloc_filter()函数传入的名字,用来标识该滤镜上下文,如果省略将按照Parsed_滤镜名_滤镜索引(Parsed_abuffer_0)的格式命名
  • arguments 代表该滤镜的参数,格式有三种情况:
    1、每个参数由key=value形式组成,多个参数之间用:分隔,key代表滤镜对应的AVOption数组中的参数名,value为传递给该参数的值,顺序任意
    2、参数由value形式组成,多个参数之间用:分隔。此形式下无key值,那么将按照AVOption数组中参数声明的顺序依次传给对应的参数
    3、上面1和2的混合形式,不过value必须在前面(而且要满足value形式的规则),key=value在后面

如果滤镜的参数是数组(ps:比如ff_af_adelay滤镜的delays参数)即它的值为多个值的组合,那么这多个值之间用 | 分割,如:
adelay=1500|0|500

这里以音频滤镜acrossfade为例的滤镜语法如下:

# 带@id的滤镜语法 key=value形式的滤镜参数列表
ffmpeg -i test_mp3_1.mp3 -i test_mp3_2.mp3 -filter_complex acrossfade@cossname=d=20:c1=exp:c2=exp output.mp3 -y
# 不带@id的滤镜语法 key=value形式的滤镜参数列表
ffmpeg -i test_mp3_1.mp3 -i test_mp3_2.mp3 -filter_complex acrossfade=d=20:c1=exp:c2=exp output.mp3 -y
# value形式的滤镜参数列表;
ffmpeg -i test_mp3_1.mp3 -i test_mp3_2.mp3 -filter_complex acrossfade=44100:20:1:exp:exp output.mp3 -y
# value形式和key=value形式混合的参数列表;value形式必须再前
ffmpeg -i test_mp3_1.mp3 -i test_mp3_2.mp3 -filter_complex acrossfade=44100:20:c1=exp:c2=exp output.mp3 -y

以上ffmpeg转换命令行是等同的,可以看到包含value形式的滤镜参数列表如果当滤镜参数比较多而且要设置的参数靠后那么写起来还是比较麻烦的

实现思路

  • ffmpeg命令方式实现
ffmpeg -i test_mp3_1.mp3 -i test_mp3_2.mp3 -filter_complex acrossfade=d=20:c1=exp:c2=exp output.mp3 -y

该命令将test_mp3_1.mp3(时长60s)和test_mp3_2.mp3(时长71s)文件合并,合并后文件时长(111s),它会将test_mp3_1.mp3文件最后和test_mp3_2.mp3前各取20秒进行混合,然后实现音频播放淡入淡出的过度效果

  • acrossfade滤镜参数详解:
    该滤镜要求至少有两个音频文件,以两个音频文件为例,它会在第一个音频的结尾和第二个文件的开头各取一定时长进行混合,然后按照指定的混合算法实现过度效果。
    nb_samples, ns:要进行混合的采样数,例如:如音频采样数为44100,那么设置此值为441000说明要混合10秒
    duration, d:要混合的时长,如果设定了此值那么将忽略nb_samples, ns的值
    overlap, o:是否进行混合,默认是。如果取值为0,那么音频不会进行混合
    curve1:淡出的算法
    curve2:淡入的算法
    淡出和淡入的算法取值:
    tri、qsin、esin、hsin、log、ipar、qua、cub、squ、cbr、par、exp、iqsin、ihsin、dese、desi、losi、nofade

  • api接口实现

void AudioAcrossfade::doAcrossfade(string apath1, string apath2,string dpath,int duration)
{
    
    if (apath1.length() == 0) {
        LOGD("apath not found");
        return;
    }
    
    if (apath2.length() == 0) {
        LOGD("apath2 not found");
        return;
    }
    
    if (dpath.length() == 0) {
        LOGD("dpath invalid");
        return;
    }
    
    if (!openStream(&infmt1,&de_ctx1,apath1)) {
        LOGD("openStream failed");
        internalRelease();
        return;
    }
    
    if (!openStream(&infmt2,&de_ctx2,apath2)) {
        LOGD("openStream failed");
        internalRelease();
        return;
    }
    
    AVStream *instream = infmt1->streams[0];
    AVStream *instream2 = infmt2->streams[0];
    // 创建编码器
    AVCodec *encodec = avcodec_find_encoder(instream->codecpar->codec_id);
    if (!encodec) {
        LOGD("avcodec_find_encoder failed");
        internalRelease();
        return;
    }
    en_ctx = avcodec_alloc_context3(encodec);
    if (!en_ctx) {
        LOGD("avcodec_alloc_context3 failed");
        internalRelease();
        return;
    }
    // 设置编码参数;这里直接从源文件拷贝
    avcodec_parameters_to_context(en_ctx, instream->codecpar);
    // 打开编码器上下文
    if (avcodec_open2(en_ctx, encodec, NULL) < 0) {
        LOGD("encodec avcodec_open2() failed");
        internalRelease();
        return;
    }
    
    int ret = 0;
    // 创建封装器用于写入拼接的两个音频文件
    if ((ret = avformat_alloc_output_context2(&oufmt, NULL, NULL, dpath.c_str())) < 0) {
        LOGD("avformat_alloc_output_context2() failed");
        internalRelease();
        return;
    }
    // 添加输出流
    AVStream *oustream = avformat_new_stream(oufmt, NULL);
    // 设置输出流参数,这里直接从源文件拷贝。
    if((ret = avcodec_parameters_copy(oustream->codecpar, instream->codecpar))<0){
        LOGD("avcodec_parameters_copy failed");
        internalRelease();
        return;
    }
    // 源文件编码方式一样但是码流格式可能不一样,所以这里将目标的codec_tag设置为0,已解决因为码流格式不一样导致的错误
    oustream->codecpar->codec_tag = 0;
    
    // 打开封装器的输出上下文
    if (!(oufmt->oformat->flags & AVFMT_NOFILE)) {
        if (avio_open(&oufmt->pb, dpath.c_str(), AVIO_FLAG_WRITE) < 0) {
            LOGD("avio_open() failed");
            internalRelease();
            return;
        }
    }
    
    // 初始化滤镜上下文
    if (!initFilterGraph(duration)) {
        LOGD("filter graph init failed");
        internalRelease();
        return;
    }
    
    // 写入头文件
    if ((ret = avformat_write_header(oufmt, NULL)) < 0) {
        LOGD("avformat_write_header() failed");
        internalRelease();
        return;
    }
    
    AVPacket *inpkt = av_packet_alloc();
    // 处理源文件1
    while (av_read_frame(infmt1, inpkt) >= 0) {
        
        // 需要先解码然后再送入滤镜处理
        LOGD("0:pts %d(%s)",inpkt->pts,av_ts2timestr(inpkt->pts,&instream->time_base));
        doDecodec(inpkt,0);
        
    }
    
    // 刷新缓冲区
    doDecodec(NULL,0);
    
    // 处理源文件2
    while (av_read_frame(infmt2, inpkt) >= 0) {
        
        // 则需要先解码然后再送入滤镜处理
        LOGD("1:pts %d(%s)",inpkt->pts,av_ts2timestr(inpkt->pts,&instream2->time_base));
        doDecodec(inpkt,1);
    }
    
    // 刷新缓冲区
    doDecodec(NULL,1);
    
    LOGD("over finish");
    av_write_trailer(oufmt);
    
    internalRelease();
}

void AudioAcrossfade::doDecodec(AVPacket *pkt,int stream_index)
{
    AVCodecContext *ctx = de_ctx1;
    if (stream_index == 1) {
        ctx = de_ctx2;
    }
    
    int ret = 0;
    if(avcodec_send_packet(ctx, pkt) < 0) {
        LOGD("avcodec_send_packet failed");
        return;
    }
    
    if (!de_frame1) {
        de_frame1 = av_frame_alloc();
    }
    
    while (true) {
        ret = avcodec_receive_frame(ctx, de_frame1);
        if (ret == AVERROR(EAGAIN)) {
            return;
        }
        if (ret < 0) {
            break;
        }
        
        // 解码成功,送入滤镜管道进行处理
        string src_filter_name = "abuffer"+to_string(stream_index);
        string sink_filter_name = "abuffersink";
        AVFilterContext *src_ctx = avfilter_graph_get_filter(filterGraph, src_filter_name.c_str());
        AVFilterContext *sink_ctx = avfilter_graph_get_filter(filterGraph, sink_filter_name.c_str());
        if (!src_ctx || !sink_ctx) {
            return;
        }
        
        ret = av_buffersrc_add_frame_flags(src_ctx, de_frame1,AV_BUFFERSRC_FLAG_KEEP_REF);
        while (1) {
            // 从滤镜管道获取滤镜处理后的AVFrame
            AVFrame *frame = av_frame_alloc();
            ret = av_buffersink_get_frame(sink_ctx,frame);
            if (ret == AVERROR_EOF) {
                // 数据处理完毕了则刷新编码器缓冲区;
                doEncodec(NULL);
            }
            
            if (ret < 0) {
                break;
            }
            
            doEncodec(frame);
            // 处理完毕后释放内存
            av_frame_free(&frame);
        }
    }
    
}

void AudioAcrossfade::doEncodec(AVFrame *frame)
{
    int ret = avcodec_send_frame(en_ctx, frame);
    if (ret < 0) {
        LOGD("avcodec_send_frame failed");
        return;
    }
    while (1) {
        AVPacket *pkt = av_packet_alloc();
        ret = avcodec_receive_packet(en_ctx,pkt);
        if (ret < 0) {
            break;
        }
        
        // 编码成功
        doWrite(pkt);
    }
}

void AudioAcrossfade::doWrite(AVPacket *pkt)
{
    av_packet_rescale_ts(pkt, oufmt->streams[0]->time_base, en_ctx->time_base);
    LOGD("write pkt pts %d(%s)",pkt->pts,av_ts2timestr(pkt->pts, &oufmt->streams[0]->time_base));
    if(av_write_frame(oufmt, pkt) < 0) {
        LOGD("av_write_frame failed");
    }
    av_packet_unref(pkt);
}

bool AudioAcrossfade::openStream(AVFormatContext**infmt,AVCodecContext**de_ctx,string path)
{
    if (infmt == NULL || *infmt != NULL) {
        LOGD("infmt is invalide");
        return false;
    }
    if (de_ctx == NULL || *de_ctx != NULL) {
        LOGD("de_ctx is invalide");
        return false;
    }
    
    // 解封装apath1和apath2用于之后读取数据
    int ret = 0;
    AVFormatContext *fmt = NULL;
    if ((ret = avformat_open_input(&fmt, path.c_str(), NULL, NULL)) < 0) {
        LOGD("avformat_open_input apath1 failed");
        return false;
    }
    if ((ret = avformat_find_stream_info(fmt, NULL)) < 0) {
        LOGD("avformat_find_stream_info apath1 failed");
        avformat_close_input(&fmt);
        return false;
    }
    *infmt = fmt;
    
    AVStream *instream = fmt->streams[0];
    // 创建用于源文件1的解码器
    AVCodec *decodec1 = avcodec_find_decoder(instream->codecpar->codec_id);
    if (!decodec1) {
        LOGD("can not found decoder %s",avcodec_get_name(decodec1->id));
        return false;
    }
    AVCodecContext *ctx = avcodec_alloc_context3(decodec1);
    if (!ctx) {
        LOGD("avcodec_alloc_context3() failed");
        avformat_close_input(&fmt);
        return false;
    }
    // 设置解码器参数;这里直接从源文件解封装器拷贝
    if((ret = avcodec_parameters_to_context(ctx, instream->codecpar)) < 0){
        LOGD("decodec avcodec_parameters_to_context() failed");
        avformat_close_input(&fmt);
        return false;
    }
    ctx->pkt_timebase = instream->time_base;
    
    // 打开解码器上下文
    if (avcodec_open2(ctx, decodec1, NULL) < 0) {
        LOGD("decodec avcodec_open2() failed");
        avformat_close_input(&fmt);
        return false;
    }
    
    *de_ctx = ctx;
    
    return true;
}

bool AudioAcrossfade::initFilterGraph(int duration)
{
    // 创建滤镜管道
    filterGraph = avfilter_graph_alloc();
    AVStream *in_stream = infmt1->streams[0];
    // 输入滤镜描述符
    char src_des[200];
    sprintf(src_des, "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%" PRIX64,
    in_stream->time_base.num,in_stream->time_base.den,in_stream->codecpar->sample_rate,av_get_sample_fmt_name((enum AVSampleFormat)in_stream->codecpar->format),(uint64_t)in_stream->codecpar->channel_layout);
    AVFilterContext *src1_filter_ctx = NULL;
    AVFilterContext *src2_filter_ctx = NULL;
    const AVFilter *src_filter1 = avfilter_get_by_name("abuffer");
    const AVFilter *src_filter2 = avfilter_get_by_name("abuffer");
    int ret = avfilter_graph_create_filter(&src1_filter_ctx, src_filter1, "abuffer0", src_des, NULL, filterGraph);
    if (ret < 0) {
        LOGD("avfilter_graph_create_filter1 fail");
        return false;
    }
    ret = avfilter_graph_create_filter(&src2_filter_ctx, src_filter2, "abuffer1", src_des, NULL, filterGraph);
    if (ret < 0) {
        LOGD("avfilter_graph_create_filter2 fail");
        return false;
    }
    AVFilterContext *sink_filter_ctx = NULL;
    const AVFilter  *sink_filter = avfilter_get_by_name("abuffersink");
    ret = avfilter_graph_create_filter(&sink_filter_ctx, sink_filter, "abuffersink", NULL, NULL, filterGraph);
    if (ret < 0) {
        LOGD("avfilter_graph_create_filter3 fail");
        return false;
    }
    
    /** 用于创建滤镜链的字符串,跟ffmpeg中滤镜命令一样,格式语法如下:
     * 1、滤镜管道由至少一个滤镜链组成(多个滤镜链之间用";"分隔),每个滤镜链仅且必须代表一条输出链,前一个滤镜链必须通过标签和后一个滤镜链的输入进行关联
     * 2、每一个滤镜链由至少一个滤镜组成(多个滤镜之间用","分隔),每一个滤镜的语法格式如下
     * [in_link_1]...[in_link_N]filter_name[@id]=arguments[out_link_1]...[out_link_M]
     *
     *  in_link_xx 代表滤镜的输入端口的标签名;out_link_1代表滤镜的输出端口的标签名,如果一个滤镜有两个以上输入或者输出端口,则必须通过这样的标签名来进行区分
     *  @id 代表滤镜上下文的标识名(最终为filter_name@id)
     *  arguments 代表该滤镜的参数,格式如下:
     *      1、每个参数由key=value形式组成,多个参数之间用:分隔,key代表滤镜对应的AVOption数组中的参数名,value为传递给该参数的值,顺序任意
     *      2、参数由value形式组成,多个参数之间用:分隔。此形式下无key值,那么将按照AVOption数组中参数声明的顺序依次传给对应的参数
     *      3、上面1和2的混合形式,不过value必须在前面(而且要满足value形式的规则),key=value在后面
     *      如果滤镜的参数是数组(ps:比如ff_af_adelay滤镜的delays参数)即它的值为多个值的组合,那么这多个值之间用 | 分割,如:adelay=1500|0|500
     */
    // 这里acrossfade滤镜有两个输入端口,所以必须再它前面定义两个滤镜链且定义acrossfade输入标签,且这两个滤镜链的输出标签要和acrossfade输入标签对应,具体如下:
    char chain[400] = {0};
    sprintf(chain, "aresample=44100[ou1];aresample=44100[ou2];[ou1][ou2]acrossfade@acrossfade=441000:%d:c1=exp:c2=exp",duration);
    ret = avfilter_graph_parse2(filterGraph,chain, &inputs, &ouputs);
    if (ret < 0) {
        LOGD("avfilter_graph_parse_ptr failed");
        return false;
    }
    
    /** 遇到问题:滤镜管道没有正常组织
     *  分析原因:刚开始将abuffer滤镜和abuffersink滤镜也加入前面的滤镜描述符中了,这是错误的,因为滤镜描述符中不能包括abuffer滤镜和abuffersink滤镜。
     *  解决方案:abuffer滤镜和abuffersink要单独和滤镜描述符进行连接
     */
    AVFilterInOut *p = inputs;
    inputs = inputs->next;
    p->next = NULL;
    ret = avfilter_link(src1_filter_ctx, 0, p->filter_ctx, p->pad_idx);
    if (ret < 0) {
        LOGD("avfilter_link 1 failed ");
        return false;
    }
    p = inputs;
    p->next = NULL;
    avfilter_link(src2_filter_ctx, 0, p->filter_ctx, p->pad_idx);
    if (ret < 0) {
        LOGD("avfilter_link 2 failed ");
        return false;
    }
    avfilter_link(ouputs->filter_ctx, 0, sink_filter_ctx, 0);
    if (ret < 0) {
        LOGD("avfilter_link 3 failed ");
        return false;
    }
    
    // 初始化滤镜管道
    ret = avfilter_graph_config(filterGraph, NULL);
    if (ret < 0) {
        LOGD("avfilter_graph_config faied");
        return false;
    }
    
    /** 遇到问题:接收到的AVFrame的nbsamples大于了enc_ctx的frame_size的大小
     *  分析原因:因为acrossfade滤镜处理两个输入流进行淡入淡出算法的处理,所以会缓冲数据,当处理完成后调用av_buffersink_get_frame()时就一次性返回给AVFrame了,这个问题就产生了。
     *  解决方案:设置每次调用av_buffersink_get_frame()的AVFrame的大小
     */
    av_buffersink_set_frame_size(sink_filter_ctx, en_ctx->frame_size);
    
    return true;
}

遇到问题

  • 1、
    遇到问题:滤镜管道没有正常组织
    分析原因:刚开始将abuffer滤镜和abuffersink滤镜也加入前面的滤镜描述符中了,这是错误的,因为滤镜描述符中不能包括abuffer滤镜和abuffersink滤镜。
    解决方案:abuffer滤镜和abuffersink要单独和滤镜描述符进行连接

  • 2、
    遇到问题:接收到的AVFrame的nbsamples大于了enc_ctx的frame_size的大小
    分析原因:因为acrossfade滤镜处理两个输入流进行淡入淡出算法的处理,所以会缓冲数据,当处理完成后调用av_buffersink_get_frame()时就一次性返回给AVFrame了,这个问题就产生了。
    解决方案:设置每次调用av_buffersink_get_frame()的AVFrame的大小

项目地址

https://github.com/nldzsz/ffmpeg-demo

位于cppsrc目录下文件audio_acrossfade.hpp/audio_acrossfade.cpp

项目下示例可运行于iOS/android/mac平台,工程分别位于demo-ios/demo-android/demo-mac三个目录下,可根据需要选择不同平台

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352