From 9c5c2ebc91e428d228958795cf02a1924ccf9110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Wed, 24 Jun 2026 14:09:09 +0800 Subject: [PATCH] =?UTF-8?q?D144=20=C2=B7=20=E6=B7=B7=E9=9F=B3=E5=99=A8?= =?UTF-8?q?=E5=8D=87=E7=BA=A7:=20=E4=BE=A7=E9=93=BE=E5=8E=8B=E7=BC=A9=20+?= =?UTF-8?q?=20loudnorm=E5=93=8D=E5=BA=A6=E6=A0=87=E5=87=86=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 从 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 --- video-ai-system/engines/audio-mixer.py | 103 ++++++++++++++----------- 1 file changed, 57 insertions(+), 46 deletions(-) diff --git a/video-ai-system/engines/audio-mixer.py b/video-ai-system/engines/audio-mixer.py index 717be14..3b32651 100644 --- a/video-ai-system/engines/audio-mixer.py +++ b/video-ai-system/engines/audio-mixer.py @@ -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:]}")