用 FFmpeg avfilter 实现多路音频混合:RambosPlayer 音频混合功能原理

功能描述

RambosPlayer 的音频混合面板(Ctrl+Shift+A)允许用户在源视频的任意时间点叠加外部音频:
可以是 BGM、配音片段、音效等,每段音频可以独立设置贴入时间来源偏移持续时长混合音量,同时支持将源音频在该区间内自动压低(闪避效果)。
github: https://github.com/johnjiamzhong-project/RambosPlayer

整个处理分两个步骤:

  1. 混音:用 FFmpeg avfilter 把所有音频流叠加 → 输出临时 AAC
  2. 封装:把源视频的视频流 + 新 AAC 按 DTS 顺序交织写入最终 MP4

24

步骤一:avfilter 图的构建

核心是 buildFilterStr() 动态拼出一个 avfilter 字符串。
假设源视频音频为 in0,用户添加了 2 个区间(in1、in2),最终生成:

  [in0]volume=volume=0.200:enable='between(t,10.000,40.000)'[src_adj];
  [in1]atrim=start=0.000:end=30.000,asetpts=PTS-STARTPTS,adelay=10000|10000,apad,volume=0.800[add1];
  [in2]atrim=start=5.000:end=20.000,asetpts=PTS-STARTPTS,adelay=60000|60000,apad,volume=1.000[add2];
  [src_adj][add1][add2]amix=inputs=3:normalize=0:duration=first[out]

逐段拆解:

① 源音频分段降音(闪避)

[in0]volume=volume=0.2:enable='between(t,10,40)'[src_adj]

如果用户设置了 srcVol < 1.0,就在对应时间窗内用 volume 滤镜压低源音频。
关键点:多个区间可能时间重叠,不能串联两个 volume 滤镜(0.5 × 0.5 = 0.25,音量过低)。
实现方式:把所有区间边界点排序,对每个子段取覆盖它的所有区间中最小的 srcVol
每个子段只生成一个 volume 节点:

  ```cpp
  for (size_t bi = 0; bi + 1 < bounds.size(); ++bi) {
      double mid = (bounds[bi] + bounds[bi+1]) * 0.5;
      float minVol = 1.0f;
      for (const auto& r : task_.regions)
          if (mid 覆盖在 r 的范围内 && r.srcVol < 0.999f)
              minVol = std::min(minVol, r.srcVol);
      // 只生成一个 volume 节点
  }

② 附加音频的 atrim + adelay

  [in1]atrim=start=0:end=30,asetpts=PTS-STARTPTS,adelay=10000|10000,apad,volume=0.8[add1]

  - atrim:截取来源音频的指定片段(从哪里开始,取多长)
  - asetpts=PTS-STARTPTS:trim 后重置时间戳从 0 开始,否则 adelay 计算会偏移
  - adelay=10000|10000:延迟 10000ms(即 10 秒),把这段音频"贴"到视频的 10 秒位置
  - apad:末尾补静音,防止短音频导致 amix 提前截断
  - volume:混合时的相对音量

③ amix 混合

  [src_adj][add1][add2]amix=inputs=3:normalize=0:duration=first[out]

  - normalize=0:不做响度归一化,直接叠加(用户自己控制各路音量)
  - duration=first:以 src_adj(源视频音频)的时长为准,超出的附加音频直接截掉

步骤二:多路解码 + 单路编码

构建好滤镜图后,N+1 个输入(1 个源视频音频 + N 个附加音频文件)同时解码,
解码帧送入各自的 abuffersrc,混合后从 abuffersink 取出,用 AAC 编码写入临时文件。

主循环采用轮询式喂帧策略——每轮依次给每个源各喂一帧,
避免某一路先跑完 EOF 导致 amix 内部缓冲饥饿:

  while (activeSrcs > 0) {
      for (int i = 0; i <= N; ++i) {
          if (srcs[i].eof) continue;
          // 读一帧 → 解码 → 送入 abuffersrc
      }
      // 从 abuffersink 取混合结果 → AAC 编码 → 写临时文件
  }

步骤三:封装最终 MP4

临时 AAC 生成后,最后一步把它和源视频的视频流一起封装:

输入 操作 输出
源视频 video stream(copy) av_interleaved_write_frame 输出 MP4
临时 AAC audio stream ↑ 按 DTS 交织写入 movflags=faststart

视频流直接 copy,不重编码,保留原始画质。
写入时按两路的 DTS 大小交替写,保证 MP4 容器内帧的时间顺序正确:

  int64_t vDts = av_rescale_q(videoPkt->dts, srcVideoTB, ovs->time_base);
  int64_t aDts = av_rescale_q(audioPkt->dts, srcAudioTB, oas->time_base);
  writeVideo = (vDts <= aDts);

最后加 movflags=faststart,把 moov box 挪到文件开头,方便边下边播。


● mermaid

  flowchart TD
      config["用户配置\n区间列表 [贴入点, 偏移, 时长, 混合音量, 源音量]"]
      config --> build["buildFilterStr()\n动态拼接 avfilter 字符串"]

      subgraph filter["FFmpeg avfilter 图(execMixAudio)"]
          src0["abuffersrc[0]\n源视频音频"]
          src1["abuffersrc[1..N]\n各附加音频"]

          src0 --> vol["volume\n分段闪避 srcVol"]
          vol --> src_adj["[src_adj]"]

          src1 --> atrim["atrim\n截取偏移+时长"]
          atrim --> adelay["adelay\n延迟到贴入点"]
          adelay --> apad["apad\n末尾补静音"]
          apad --> mvol["volume\n混合音量"]
          mvol --> add1["[add1..N]"]

          src_adj --> amix["amix\nnormalize=0 / duration=first"]
          add1    --> amix
          amix --> sink["abuffersink"]
      end

      build --> filter
      sink --> enc["AAC 编码器 192kbps"]
      enc --> aac["临时 .aac"]

      subgraph mux["封装(execMuxFinal)"]
          video["源视频视频流\ncopy 不重编码"]
          aac2["临时 .aac"]
          video --> interleave["DTS 交织写入"]
          aac2  --> interleave
      end

      aac --> aac2
      interleave --> out["输出 MP4\nmovflags=faststart"]

一个踩过的坑

初版直接用 avfilter_graph_parse2() 解析滤镜字符串,再手动 link。
后来发现 avfilter_graph_parse_ptr() 返回的 gInputs 链表节点名称就是滤镜字符串里的 in0、in1……
只需遍历链表按名字匹配,然后把对应的 abuffersrc link 上去即可:

  for (AVFilterInOut* cur = gInputs; cur; cur = cur->next) {
      int idx = QString(cur->name).mid(2).toInt(); // "in0" → 0
      avfilter_link(srcs[idx].bufSrc, 0, cur->filter_ctx, cur->pad_idx);
  }

另一个坑是 abuffersrc 的 time_base 必须和对应解码器的流 time_base 一致,
不能用 {1, sample_rate},否则 amix 内部 PTS 对齐出错,BGM 混入后无声。


posted @ 2026-06-04 13:03  rambos1996  阅读(22)  评论(0)    收藏  举报