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三个目录下,可根据需要选择不同平台

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
如果不想安装 ffmpeg,可以考虑使用 Java 中的 Xuggler 库来实现视频转场淡出的效果。以下是一个示例代码,可以实现两个视频的淡出转场效果: ```java import com.xuggle.mediatool.IMediaReader; import com.xuggle.mediatool.IMediaWriter; import com.xuggle.mediatool.ToolFactory; import com.xuggle.xuggler.IContainer; import com.xuggle.xuggler.IStream; import com.xuggle.xuggler.IStreamCoder; public class VideoTransition { public static void main(String[] args) { // 第一个视频文件 String inputVideoFile1 = "/path/to/input1.mp4"; // 第二个视频文件 String inputVideoFile2 = "/path/to/input2.mp4"; // 输出视频文件 String outputVideoFile = "/path/to/output.mp4"; // 淡出的持续时间(单位:秒) double fadeDuration = 1.0; // 创建一个读取第一个视频文件的 MediaReader 对象 IMediaReader reader1 = ToolFactory.makeReader(inputVideoFile1); // 创建一个读取第二个视频文件的 MediaReader 对象 IMediaReader reader2 = ToolFactory.makeReader(inputVideoFile2); // 创建一个写输出视频文件的 MediaWriter 对象 IMediaWriter writer = ToolFactory.makeWriter(outputVideoFile, reader1); // 获取第一个视频流的编码器对象 IContainer container1 = reader1.getContainer(); IStream videoStream1 = container1.getStream(0); IStreamCoder videoCoder1 = videoStream1.getStreamCoder(); // 获取第二个视频流的编码器对象 IContainer container2 = reader2.getContainer(); IStream videoStream2 = container2.getStream(0); IStreamCoder videoCoder2 = videoStream2.getStreamCoder(); // 计算淡出的帧数 int fadeFrames = (int) (fadeDuration * videoCoder1.getFrameRate().getDouble()); // 读取第一个视频文件的帧并写输出文件 while (reader1.readPacket() == null) { writer.encodeVideo(0, reader1.getFrame(0), 0); } // 写第一个视频文件的最后几帧,加上淡出效果 for (int i = 0; i < fadeFrames; i++) { writer.encodeVideo(0, reader1.getFrame(0).getFadeOut(i, fadeFrames), 0); } // 写第二个视频文件的第一帧,加上效果 writer.encodeVideo(0, reader2.readPacket().getFrame(0).getFadeIn(fadeFrames), 0); // 读取第二个视频文件的帧并写输出文件 while (reader2.readPacket() == null) { writer.encodeVideo(0, reader2.getFrame(0), 0); } // 写第二个视频文件的最后几帧,加上淡出效果 for (int i = 0; i < fadeFrames; i++) { writer.encodeVideo(0, reader2.getFrame(0).getFadeOut(i, fadeFrames), 0); } // 关闭所有的读写对象 reader1.close(); reader2.close(); writer.close(); } } ``` 在上面的代码中,我们使用了 Xuggler 库来读取视频文件和写视频文件,并使用 getFadeIn 和 getFadeOut 方法来实现淡出的效果。需要注意的是,Xuggler 库的使用相对于 ffmpeg 更加复杂,需要细心地处理各种异常情况。 希望这个示例代码对你有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值