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,
|
def _mix_with_ducking(self, dialogue, bgm, sfx, original, output_path,
|
||||||
ducking_threshold, ducking_level):
|
ducking_threshold, ducking_level):
|
||||||
"""带自动压低 BGM 的混音"""
|
"""带侧链压缩(ducking) + loudnorm 响度标准化的混音"""
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
inputs = []
|
inputs = []
|
||||||
filter_complex = []
|
filter_complex = []
|
||||||
|
|
||||||
# 输入
|
|
||||||
input_idx = 0
|
input_idx = 0
|
||||||
|
track_map = {} # track_name → filter_label
|
||||||
|
|
||||||
|
# 输入文件
|
||||||
if dialogue:
|
if dialogue:
|
||||||
inputs.extend(["-i", 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
|
input_idx += 1
|
||||||
|
|
||||||
if bgm:
|
if bgm:
|
||||||
inputs.extend(["-i", bgm])
|
inputs.extend(["-i", bgm])
|
||||||
# BGM 默认音量降低 (即使没有对白也降低一些)
|
track_map["bgm"] = input_idx
|
||||||
filter_complex.append(f"[{input_idx}:a]volume={bgm_volume}[a{input_idx}]")
|
|
||||||
input_idx += 1
|
input_idx += 1
|
||||||
|
|
||||||
if sfx:
|
if sfx:
|
||||||
if isinstance(sfx, str):
|
if isinstance(sfx, str):
|
||||||
sfx = [sfx]
|
sfx = [sfx]
|
||||||
for s in sfx:
|
for i, s in enumerate(sfx):
|
||||||
inputs.extend(["-i", s])
|
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
|
input_idx += 1
|
||||||
|
|
||||||
if original:
|
if original:
|
||||||
inputs.extend(["-i", 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
|
input_idx += 1
|
||||||
|
|
||||||
# 混音
|
# 侧链压缩: 对白作为 sidechain,BGM 被压缩
|
||||||
amix_inputs = "".join([f"[a{i}]" for i in range(input_idx)])
|
# sidechaincompress 参数: threshold 阈值, ratio 压缩比, attack 起音, release 释音
|
||||||
filter_complex.append(f"{amix_inputs}amix=inputs={input_idx}:duration=first:dropout_transition=2[aout]")
|
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 = [
|
cmd = [
|
||||||
self.ffmpeg_path,
|
self.ffmpeg_path, "-y",
|
||||||
"-y", # 覆盖输出文件
|
|
||||||
*inputs,
|
*inputs,
|
||||||
"-filter_complex", ";".join(filter_complex),
|
"-filter_complex", ";".join(filter_complex),
|
||||||
"-map", "[aout]",
|
"-map", "[aout]",
|
||||||
@ -253,9 +264,9 @@ class AudioMixer:
|
|||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"output_path": str(output_path),
|
"output_path": str(output_path),
|
||||||
"tracks_used": ["dialogue", "bgm"] + (["sfx"] if sfx else []) + (["original"] if original else []),
|
"tracks_used": list(track_map.keys()),
|
||||||
"warnings": [],
|
"warnings": [],
|
||||||
"method": "simple_ducking"
|
"method": "sidechain_compress+loudnorm"
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
print(f" ❌ FFmpeg 失败: {result.stderr[-500:]}")
|
print(f" ❌ FFmpeg 失败: {result.stderr[-500:]}")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user