前言

不同歌曲的音量大小不同,如果直接导入游戏做自定义电台的话很容易出现音乐时大时小的问题。因此,需要先将音量均衡。

傻瓜脚本

需要安装好ffmpeg且添加到PATH

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import json
import re
import subprocess
from pathlib import Path

TARGET_I = -14
TARGET_TP = -1.5
TARGET_LRA = 12

FFMPEG = "ffmpeg" # 或写 "ffmpeg.exe"


def run(cmd):
# ✅ 强制按 UTF-8 解码,避免 GBK 解码炸裂
p = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
return p.returncode, p.stdout, p.stderr


def extract_loudnorm_json(stderr_text: str) -> dict:
if not stderr_text:
raise RuntimeError("ffmpeg 没有返回 stderr 文本(可能被解码错误吞掉了)。")

# ✅ 更稳:只匹配包含 "input_i" 的那段 JSON(避免误匹配别的花括号)
m = re.search(r'(\{\s*"input_i"[\s\S]*?\})', stderr_text)
if not m:
# 方便你调试:把 stderr 的尾部打印出来看看
tail = stderr_text[-2000:]
raise RuntimeError(f"没找到 loudnorm JSON 输出。stderr 尾部如下:\n{tail}")

return json.loads(m.group(1))


def normalize_file(inp: Path, outp: Path):
outp.parent.mkdir(parents=True, exist_ok=True)

# Pass 1: measure
cmd1 = [
FFMPEG, "-hide_banner", "-nostats", "-y",
"-i", str(inp),
"-af", f"loudnorm=I={TARGET_I}:TP={TARGET_TP}:LRA={TARGET_LRA}:print_format=json",
"-f", "null", "NUL"
]
rc, _, err = run(cmd1)
if rc != 0:
raise RuntimeError(f"Pass1 失败:{inp}\n{err}")

info = extract_loudnorm_json(err)

# Pass 2: apply
cmd2 = [
FFMPEG, "-hide_banner", "-nostats", "-y",
"-i", str(inp),
"-af",
(
"loudnorm=I={I}:TP={TP}:LRA={LRA}:"
"measured_I={mI}:measured_TP={mTP}:measured_LRA={mLRA}:"
"measured_thresh={mTH}:offset={off}:linear=true"
).format(
I=TARGET_I, TP=TARGET_TP, LRA=TARGET_LRA,
mI=info["input_i"], mTP=info["input_tp"], mLRA=info["input_lra"],
mTH=info["input_thresh"], off=info["target_offset"]
),
"-c:a", "libmp3lame", "-q:a", "2",
str(outp)
]
rc, _, err2 = run(cmd2)
if rc != 0:
raise RuntimeError(f"Pass2 失败:{inp}\n{err2}")


def main():
in_dir = Path("MP3")
out_dir = Path("test")

mp3s = sorted(in_dir.rglob("*.mp3"))
if not mp3s:
print("没找到 mp3:请把文件放到 input_mp3/ 下 🙂")
return

for f in mp3s:
rel = f.relative_to(in_dir)
outp = out_dir / rel
print(f"🔧 {rel}")
normalize_file(f, outp)

print(f"✅ 完成!输出在:{out_dir}")


if __name__ == "__main__":
main()