D144 · 混音器升级: 侧链压缩 + loudnorm响度标准化
- 从 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:
parent
679a57b791
commit
9c5c2ebc91
@ -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]")
|
||||
# 侧链压缩: 对白作为 sidechain,BGM 被压缩
|
||||
# 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:]}")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user