新增音訊處理: 音量增益 / 強制壓限 / 低通(命名參考 Adobe PR)

- volume_boost: 音量增益(對應 PR 增益/音量),預設開,幅度 volume_boost_percent 預設 +30%(x1.3)
- hard_limiter: 強制壓限(PR Hard Limiter),拉大音量後把峰值壓在 0dB 以下避免破音
- lowpass: 低通(PR Lowpass),濾掉高頻刺耳聲;截止頻率 lowpass_hz 預設 15000Hz
- 處理順序: 音量增益 -> 低通 -> 強制壓限(壓限擺最後當煞車)
- 套用在重編路徑(轉場/xfade)的 acrossfade 之後;單一片段時畫面照樣無損複製只重編音訊
- transition=false(無損)時音訊不處理並提示;已實測同段處理後平均 +2.7dB
- 同步更新 README 參數說明

本次調整由 Claude Opus 4.8 (1M context) 協助開發與處理。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 19:29:44 +08:00
parent cebe6e3bd5
commit be5f4a60b8
3 changed files with 66 additions and 5 deletions
+10
View File
@@ -96,6 +96,11 @@ python auto_remove_redlight.py 01.MOV 03.MOV 05.MOV
| `transition` | `true` | 剪接點是否加轉場(淡出淡入 + 恆定功率交叉淡化)。**開啟會強制重新編碼**,有 NVIDIA 顯卡時自動用 NVENC 加速 |
| `transition_duration` | `0.5` | 轉場長度(秒),畫面與聲音共用以維持同步 |
| `video_transition` | `fadeblack` | 畫面轉場類型:`fadeblack`=淡出到黑再淡入;`fade`=交叉溶接;其餘見 FFmpeg `xfade` 文件 |
| `volume_boost` | `true` | **音量增益**(Adobe PR「增益/音量」):是否放大音量 |
| `volume_boost_percent` | `30` | 音量增加幅度(%),`30` = 放大成 130%(×1.3 ≈ +2.3dB |
| `hard_limiter` | `true` | **強制壓限**Adobe PR「強制壓限/Hard Limiter」):拉大音量後把峰值壓在 0dB 以下,避免破音 |
| `lowpass` | `true` | **低通**Adobe PR「低通/Lowpass」):濾掉高頻刺耳聲(風切/嘶聲) |
| `lowpass_hz` | `15000` | 低通截止頻率(Hz |
| `base_dir` | `E:\\videos` | 影片根目錄 |
> 參數優先順序:**命令列參數 > config.json > 程式內建預設**
@@ -105,6 +110,11 @@ python auto_remove_redlight.py 01.MOV 03.MOV 05.MOV
- `transition: false` → 使用 FFmpeg `-c copy` 串流複製,**畫質無損、速度快**,但剪接點是硬切。
- `transition: true` → 在每個剪接點加上**畫面淡出淡入**(`xfade`)與**聲音恆定功率交叉淡化**`acrossfade``c1=qsin:c2=qsin`)。此模式必須重新編碼(非無損),4K 影片較耗時,建議搭配 NVIDIA 顯卡用 NVENC 加速。
### 關於音訊處理
音量增益、強制壓限、低通三項(命名參考 Adobe Premiere Pro)會在重新編碼時一併套用,處理順序為 **音量增益 → 低通 → 強制壓限**(壓限擺最後當煞車)。
由於需要重新編碼,這些音訊處理**僅在 `transition: true`(重編模式)時生效**;若 `transition: false`(無損快剪)則音訊維持原樣不處理。
---
## 輸出結果
+51 -5
View File
@@ -128,6 +128,14 @@ VIDEO_TRANSITION = "fadeblack" # 畫面轉場 (FFmpeg xfade 類型):
# 其它如 dissolve, smoothleft... 見 ffmpeg xfade 文件
# 聲音固定用 acrossfade 等功率曲線(c1=qsin:c2=qsin)=恆定功率
# --- 音訊處理(命名參考 Adobe Premiere Pro 效果;需重新編碼,即 TRANSITION=true 時生效)---
VOLUME_BOOST = True # 音量增益: 是否放大音量(對應 PR「增益 / 音量」)
VOLUME_BOOST_PERCENT = 30 # 音量增加幅度 (%)。30 = 放大成 130% (x1.3 ≈ +2.3dB)
HARD_LIMITER = True # 強制壓限 (PR「強制壓限 / Hard Limiter」):
# 拉大音量後把峰值壓在 0dB 以下,避免破音(擺在最後當煞車)
LOWPASS = True # 低通 (PR「低通 / Lowpass」): 濾掉高頻刺耳聲(風切/嘶聲)
LOWPASS_HZ = 15000 # 低通截止頻率 (Hz)。低於此保留,高於此衰減
# --- 批次模式 -----------------------------------------------
# 會被當成影片來處理的副檔名(小寫比較)
VIDEO_EXTS = {".mov", ".mp4", ".m4v", ".avi", ".mkv", ".mts", ".m2ts", ".insv"}
@@ -476,6 +484,22 @@ def ffmpeg_cut_concat(input_path: str, keeps: List[Tuple[float, float]],
shutil.rmtree(tmpdir, ignore_errors=True) # 清掉暫存檔
def _audio_filter_chain() -> List[str]:
"""
依設定組音訊濾鏡鏈(對應 Adobe PR 的 音量 / 強制壓限 / 低通)。
回傳 ffmpeg 濾鏡片段清單,順序為: 音量增益 → 低通 → 強制壓限(壓限擺最後當煞車)。
"""
chain: List[str] = []
if VOLUME_BOOST and abs(VOLUME_BOOST_PERCENT) > 0:
mult = 1.0 + VOLUME_BOOST_PERCENT / 100.0 # +30% → x1.3
chain.append(f"volume={mult:.4f}") # 音量增益
if LOWPASS:
chain.append(f"lowpass=f={int(LOWPASS_HZ)}") # 低通 (Lowpass)
if HARD_LIMITER:
chain.append("alimiter=limit=0.95") # 強制壓限 (Hard Limiter)
return chain
def has_audio(path: str) -> bool:
"""用 ffprobe 判斷影片是否有音訊軌。出錯時保守假設『有』。"""
if shutil.which("ffprobe") is None:
@@ -504,12 +528,20 @@ def ffmpeg_cut_concat_xfade(input_path: str, keeps: List[Tuple[float, float]],
n = len(keeps)
durs = [e - s for (s, e) in keeps]
# 只有一段 → 沒有剪接點,不需轉場,直接無損切出
# 只有一段 → 沒有剪接點,不需轉場
if n == 1:
s, e = keeps[0]
run_cmd(["ffmpeg", "-y", "-ss", f"{s:.3f}", "-t", f"{e - s:.3f}",
"-i", input_path, "-c", "copy",
"-avoid_negative_ts", "make_zero", output_path])
af = _audio_filter_chain()
if af and has_audio(input_path):
# 仍要做音量處理: 畫面照樣無損複製,只重編音訊
run_cmd(["ffmpeg", "-y", "-ss", f"{s:.3f}", "-t", f"{e - s:.3f}",
"-i", input_path, "-af", ",".join(af),
"-c:v", "copy", "-c:a", "aac", "-b:a", "256k",
"-avoid_negative_ts", "make_zero", output_path])
else:
run_cmd(["ffmpeg", "-y", "-ss", f"{s:.3f}", "-t", f"{e - s:.3f}",
"-i", input_path, "-c", "copy",
"-avoid_negative_ts", "make_zero", output_path])
print(f"[INFO] 完成(單一片段,免轉場)! 輸出檔案: {output_path}")
return
@@ -556,6 +588,11 @@ def ffmpeg_cut_concat_xfade(input_path: str, keeps: List[Tuple[float, float]],
f"[{prev_a}][{j}:a]acrossfade=d={d:.3f}:c1=qsin:c2=qsin[{out}]")
prev_a = out
alabel = prev_a
# 串接完再做音量處理(音量增益 / 低通 / 強制壓限)
af = _audio_filter_chain()
if af:
parts.append(f"[{alabel}]" + ",".join(af) + "[aout]")
alabel = "aout"
filtergraph = ";".join(parts)
@@ -876,11 +913,15 @@ def process_one(video_path: str, roi: Tuple[int, int, int, int],
"removed": removed_total}
# 4. FFmpeg 切割 + 拼接
audio_proc = VOLUME_BOOST or HARD_LIMITER or LOWPASS
if TRANSITION and len(keeps) >= 1:
# 有轉場: xfade 淡出淡入 + acrossfade 恆定功率(需重新編碼)
# 有轉場: xfade 淡出淡入 + acrossfade 恆定功率 + 音量處理(需重新編碼)
ffmpeg_cut_concat_xfade(video_path, keeps, out_path,
use_gpu=args.gpu, tmp_parent=out_dir)
else:
if audio_proc:
print("[WARN] 音量處理(音量增益/強制壓限/低通)需重新編碼,"
"目前 transition=false 為無損模式,音訊將維持原樣不處理。")
# 無轉場: 維持無損快剪
ffmpeg_cut_concat(video_path, keeps, out_path, args.reencode,
tmp_parent=out_dir)
@@ -910,6 +951,11 @@ _CONFIG_KEYS = {
"transition": "TRANSITION",
"transition_duration": "TRANSITION_DURATION",
"video_transition": "VIDEO_TRANSITION",
"volume_boost": "VOLUME_BOOST",
"volume_boost_percent": "VOLUME_BOOST_PERCENT",
"hard_limiter": "HARD_LIMITER",
"lowpass": "LOWPASS",
"lowpass_hz": "LOWPASS_HZ",
"base_dir": "BASE_DIR",
}
+5
View File
@@ -20,5 +20,10 @@
"transition": true,
"transition_duration": 0.5,
"video_transition": "fadeblack",
"volume_boost": true,
"volume_boost_percent": 30,
"hard_limiter": true,
"lowpass": true,
"lowpass_hz": 15000,
"base_dir": "E:\\videos"
}