D144 · 混音器升级: 侧链压缩 + loudnorm响度标准化
Some checks failed
自动更新代码和重启 / update-and-restart (push) Has been cancelled
CI检查 + 自动部署 / check (push) Has been cancelled
CI检查 + 自动部署 / deploy (push) Has been cancelled

- 从 simple_ducking 升级为真正的 sidechaincompress 侧链压缩
- 对白作为 sidechain,BGM 自动 ducking (attack=25ms, release=200ms, ratio=4:1)
- 新增 loudnorm: EBU R128, 目标 -14 LUFS (短视频平台标准)
- 新增 alimiter 峰值限幅保护: -1.0dB

铸渊 ICE-GL-ZY001 · D144 · 2026-06-24
This commit is contained in:
冰朔 2026-06-24 14:09:09 +08:00
parent 679a57b791
commit 9c5c2ebc91

View File

@ -164,71 +164,82 @@ class AudioMixer:
def _mix_with_ducking(self, dialogue, bgm, sfx, original, output_path,
ducking_threshold, ducking_level):
"""带自动压低 BGM 的混音"""
# FFmpeg 命令构建
# 思路:
# 1. 检测对白音轨的能量
# 2. 当对白能量 > threshold 时,降低 BGM 音量
# 3. 混音所有音轨
# 方法: 使用 `volume` 滤镜 + `enable` 条件
# 但 FFmpeg 的 `enable` 不支持"当另一个音轨有声音时"
# 所以需要更聪明的方法:
# 实际可行方法:
# 1. 侧链压缩 (sidechain compress)
# 2. 用 `sidechaincompress` 滤镜
# 简单的实现: 先检测对白时段,生成音量包络,应用到 BGM
# 方法A (简单): 假设对白是连续的,直接降低 BGM 整体音量
# 方法B (复杂但正确): 检测对白时段,生成 volume filter 的 enable 条件
# 这里实现方法A (简单可用)方法B 作为 TODO
print(f" 📤 方法: 简单 ducking (整体降低 BGM 音量)")
# 简单方法: BGM 音量 * 0.3 (降低 10dB ≈ 0.3)
bgm_volume = 10 ** (ducking_level / 20) # -10dB ≈ 0.316
"""带侧链压缩(ducking) + loudnorm 响度标准化的混音"""
inputs = []
filter_complex = []
# 输入
input_idx = 0
track_map = {} # track_name → filter_label
# 输入文件
if dialogue:
inputs.extend(["-i", dialogue])
filter_complex.append(f"[{input_idx}:a]volume=1[a{input_idx}]")
track_map["dialogue"] = input_idx
input_idx += 1
if bgm:
inputs.extend(["-i", bgm])
# BGM 默认音量降低 (即使没有对白也降低一些)
filter_complex.append(f"[{input_idx}:a]volume={bgm_volume}[a{input_idx}]")
track_map["bgm"] = input_idx
input_idx += 1
if sfx:
if isinstance(sfx, str):
sfx = [sfx]
for s in sfx:
for i, s in enumerate(sfx):
inputs.extend(["-i", s])
filter_complex.append(f"[{input_idx}:a]volume=1[a{input_idx}]")
track_map[f"sfx{i}"] = input_idx
input_idx += 1
if original:
inputs.extend(["-i", original])
filter_complex.append(f"[{input_idx}:a]volume=0.5[a{input_idx}]") # 原视频音轨降低 50%
track_map["original"] = input_idx
input_idx += 1
# 混音
amix_inputs = "".join([f"[a{i}]" for i in range(input_idx)])
filter_complex.append(f"{amix_inputs}amix=inputs={input_idx}:duration=first:dropout_transition=2[aout]")
# 侧链压缩: 对白作为 sidechainBGM 被压缩
# sidechaincompress 参数: threshold 阈值, ratio 压缩比, attack 起音, release 释音
if "dialogue" in track_map and "bgm" in track_map:
d_idx = track_map["dialogue"]
b_idx = track_map["bgm"]
# 对白音轨通过
filter_complex.append(f"[{d_idx}:a]aformat=sample_fmts=fltp:channel_layouts=stereo[d_pre]")
filter_complex.append(f"[{b_idx}:a]aformat=sample_fmts=fltp:channel_layouts=stereo[b_pre]")
# 侧链压缩: BGM 被对白压缩
filter_complex.append(
f"[b_pre][d_pre]sidechaincompress="
f"threshold={ducking_threshold/20:.3f}:ratio=4:"
f"attack=25:release=200:level_sc={ducking_level/20:.3f}[bgm_comp]"
)
filter_complex.append(f"[d_pre]volume=1[dialogue_out]")
bgm_label = "bgm_comp"
else:
bgm_label = None
# 音效/原视频音轨
mix_inputs = []
if "dialogue" in track_map:
mix_inputs.append("[dialogue_out]" if bgm_label else f"[{track_map['dialogue']}:a]")
if bgm_label:
mix_inputs.append(f"[{bgm_label}]")
elif "bgm" in track_map:
mix_inputs.append(f"[{track_map['bgm']}:a]")
for key in track_map:
if key.startswith("sfx"):
filter_complex.append(f"[{track_map[key]}:a]volume=0.8[s_{key}]")
mix_inputs.append(f"[s_{key}]")
if "original" in track_map:
filter_complex.append(f"[{track_map['original']}:a]volume=0.4[orig_out]")
mix_inputs.append("[orig_out]")
# 混音
n_inputs = len(mix_inputs)
amix_inputs = "".join(mix_inputs)
filter_complex.append(
f"{amix_inputs}amix=inputs={n_inputs}:duration=first:dropout_transition=3,"
# 响度标准化: EBU R128, 目标 -14 LUFS (短视频平台标准)
f"loudnorm=I=-14:LRA=11:TP=-1.5,"
# 峰值限幅保护
f"alimiter=limit=-1.0dB:attack=5:release=50[aout]"
)
# 构建完整命令
cmd = [
self.ffmpeg_path,
"-y", # 覆盖输出文件
self.ffmpeg_path, "-y",
*inputs,
"-filter_complex", ";".join(filter_complex),
"-map", "[aout]",
@ -253,9 +264,9 @@ class AudioMixer:
return {
"success": True,
"output_path": str(output_path),
"tracks_used": ["dialogue", "bgm"] + (["sfx"] if sfx else []) + (["original"] if original else []),
"tracks_used": list(track_map.keys()),
"warnings": [],
"method": "simple_ducking"
"method": "sidechain_compress+loudnorm"
}
else:
print(f" ❌ FFmpeg 失败: {result.stderr[-500:]}")