从 Suno 下载的歌,响度往往各不相同。这篇用 ffmpeg 的
loudnorm做两遍处理(EBU R128 标准):第一遍测量,第二遍按测量值做线性增益,统一到 -14 LUFS、真峰值 -1 dBTP,再写入标题、艺人和“AI-generated with Suno”标签。原文件不动。 遇到动态范围很大的歌,脚本会自动调高目标 LRA,确保只做整体增益,不压缩歌曲的起伏。
先看清成品(box 上真实运行,素材是自己合成的演示音频)
- 处理前:-10.18 LUFS,真峰值 -1.18 dBTP,LRA 17.90。
- 处理后:-14.00 LUFS,真峰值 -5.00 dBTP,LRA 17.90(动态保持不变)。
- 标签写入成功。
- pytest 2 项通过。
合规提醒:截至本文写作时(2026-10),Suno 没有公开的、可自助开通的生成 API。网上流传的“Suno API”封装大多是通过模拟登录或抓取 cookie 调用内部接口,违反服务条款,还可能导致封号,本文一律不用。歌曲请在 Suno 官方网页或 App 里生成,本文只处理两件事:一是调用 Suno 官方公开的免费 C2PA 检测接口,二是对你下载的文件做本地后期处理。权利以 Suno 帮助中心为准:免费计划生成的歌只能个人非商用;在 Pro 或 Premier 订阅期间创作的歌可以商用。歌词和参考素材不得侵犯他人版权,也不要模仿真实歌手的声音冒充本人。在中国境内发布,要遵守《生成式人工智能服务管理暂行办法》,并按《人工智能生成合成内容标识办法》标注“AI 生成”。本文不列订阅价格。
注意:重新编码会生成一个新文件,原文件中的 C2PA 凭据很可能不会被带过去(本文没有用真实的 Suno 文件验证过这一点)。所以 C2PA 检测要对原始下载文件做(见 A 篇),原文件也要和成品一起存档。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
master.py |
两遍 loudnorm 加写入标签;拒绝覆盖原文件 |
make_demo.sh |
生成一段 20 秒、前轻后响的演示音频 |
test_master.py |
测试响度达标、写入标签、宽动态时保持线性 |
需要准备哪些环境?
ffmpeg(需要带 libmp3lame)、Python 3.9 以上、pip install pytest。
-14 LUFS 是很多流媒体平台常用的参考值;广播电视用的 EBU R128 推荐 -23 LUFS。各平台的标准并不相同,请查看你要发布的平台的说明,再用 --lufs 调整。
文件结构
51-suno-B-src/
├── master.py
├── make_demo.sh
├── demo_track.mp3 # make_demo.sh 生成,测试要用
└── test_master.py
第 1 步:代码
"""发布前的本地后期:ffmpeg 两遍 loudnorm(EBU R128)把响度统一到目标值,
写入标题、艺人和“AI 生成”说明标签,原始文件不动。
注意:重新编码会生成一个新文件,原文件里的 C2PA 内容凭据大概率不会被带过去(本文未验证)。
所以 C2PA 检测要对 Suno 原始下载文件做(见 A 篇),原件也要保留。"""
import argparse, json, re, subprocess, pathlib, sys
def run(cmd):
return subprocess.run(cmd, capture_output=True, text=True, check=True)
def measure(path, I=-14, TP=-1, LRA=11):
r = run(["ffmpeg", "-hide_banner", "-nostats", "-i", str(path), "-af",
f"loudnorm=I={I}:TP={TP}:LRA={LRA}:print_format=json", "-f", "null", "-"])
j = re.findall(r"\{[^{}]*\"input_i\"[^{}]*\}", r.stderr, re.S)[-1] # JSON 打在 stderr 末尾
return json.loads(j)
def master(src, dst, I=-14, TP=-1, LRA=11, title="", artist="", ai_note="AI-generated with Suno"):
m = measure(src, I, TP, LRA)
if float(m["input_lra"]) > LRA:
# ffmpeg 文档:目标 LRA 低于源 LRA 时,linear 会退回 dynamic(动态压缩,改变歌曲的起伏)。
# 音乐通常想保留动态,所以把目标 LRA 抬到源 LRA,保证只做整体增益。
print(f"提示:源 LRA {m['input_lra']} > 目标 {LRA},目标 LRA 调整为 {float(m['input_lra']):.1f},以保持线性增益")
LRA = round(float(m["input_lra"]) + 0.1, 1)
af = (f"loudnorm=I={I}:TP={TP}:LRA={LRA}:measured_I={m['input_i']}:measured_TP={m['input_tp']}:"
f"measured_LRA={m['input_lra']}:measured_thresh={m['input_thresh']}:offset={m['target_offset']}:linear=true")
run(["ffmpeg", "-y", "-hide_banner", "-loglevel", "error", "-i", str(src), "-af", af, "-ar", "48000",
"-metadata", f"title={title}", "-metadata", f"artist={artist}",
"-metadata", f"comment={ai_note}", "-metadata", "genre=AI Generated",
"-c:a", "libmp3lame", "-b:a", "320k", str(dst)])
after = measure(dst, I, TP, LRA)
return m, after
def tags(path):
r = run(["ffprobe", "-v", "error", "-show_entries", "format_tags", "-of", "json", str(path)])
return json.loads(r.stdout)["format"].get("tags", {})
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("src"); ap.add_argument("dst")
ap.add_argument("--lufs", type=float, default=-14); ap.add_argument("--tp", type=float, default=-1)
ap.add_argument("--lra", type=float, default=11)
ap.add_argument("--title", default=""); ap.add_argument("--artist", default="")
a = ap.parse_args()
if pathlib.Path(a.src).resolve() == pathlib.Path(a.dst).resolve():
sys.exit("输出不能覆盖原始文件")
before, after = master(a.src, a.dst, a.lufs, a.tp, a.lra, title=a.title, artist=a.artist)
print(f"处理前:{before['input_i']} LUFS,真峰值 {before['input_tp']} dBTP,LRA {before['input_lra']}")
print(f"处理后:{after['input_i']} LUFS,真峰值 {after['input_tp']} dBTP,LRA {after['input_lra']}")
print("标签:", json.dumps(tags(a.dst), ensure_ascii=False))
#!/bin/sh
# 生成一段 20 秒的演示音频:前 10 秒很轻,后 10 秒很响(代替你从 Suno 下载的歌曲)
ffmpeg -y -loglevel error -f lavfi -i "aevalsrc='0.05*sin(2*PI*220*t)+0.04*sin(2*PI*330*t)':d=10:s=44100" \
-f lavfi -i "aevalsrc='0.6*sin(2*PI*262*t)*(0.6+0.4*sin(2*PI*2*t))+0.3*sin(2*PI*392*t)':d=10:s=44100" \
-filter_complex "[0][1]concat=n=2:v=0:a=1" -c:a libmp3lame -b:a 192k demo_track.mp3
第 2 步:测试
import subprocess, master
def test_master_hits_target(tmp_path):
src = tmp_path / "s.mp3"
subprocess.run(["ffmpeg", "-loglevel", "error", "-f", "lavfi", "-i", "sine=f=440:d=8", "-af", "volume=0.1", str(src)], check=True)
before, after = master.master(src, tmp_path / "o.mp3", title="t", artist="a")
assert abs(float(after["input_i"]) - (-14)) < 1.0
assert float(after["input_tp"]) <= -0.5
tg = master.tags(tmp_path / "o.mp3")
assert tg["comment"] == "AI-generated with Suno" and tg["title"] == "t"
def test_wide_dynamics_stays_linear():
# demo_track.mp3:前轻后响,实测源 LRA 17.9 > 默认目标 11
import pathlib
out = pathlib.Path("/tmp/_m_test.mp3")
before, after = master.master("demo_track.mp3", out)
assert abs(float(after["input_i"]) + 14) < 0.3
assert abs(float(after["input_lra"]) - float(before["input_lra"])) < 0.5 # 动态没被压缩
第 3 步:运行
$ ./make_demo.sh
$ python master.py demo_track.mp3 demo_master.mp3 --title "秋日夜市(演示)" --artist "BHo BIN"
提示:源 LRA 17.90 > 目标 11,目标 LRA 调整为 17.9,以保持线性增益
处理前:-10.18 LUFS,真峰值 -1.18 dBTP,LRA 17.90
处理后:-14.00 LUFS,真峰值 -5.00 dBTP,LRA 17.90
标签: {"genre": "AI Generated", "title": "秋日夜市(演示)", "artist": "BHo BIN", "comment": "AI-generated with Suno", "encoder": "Lavf61.7.103"}
$ python master.py demo_track.mp3 demo_track.mp3
输出不能覆盖原始文件
$ python -m pytest -q
2 passed in 1.29s
调试示例
明明写了 linear=true,结果却是 -13.80 LUFS,LRA 也从 17.9 变成了 19.5。 第一版直接用了默认目标 LRA=11,跑出来的结果是:处理后 -13.80 LUFS,真峰值 -3.19,LRA 19.50。响度没有正好落在 -14,歌曲的动态也被改变了。
ffmpeg 文档里 linear 选项的说明是这样的:“Target LRA shouldn’t be lower than source LRA … If any of these conditions aren’t met, normalization mode will revert to dynamic.” 也就是说,目标 LRA 低于源 LRA 时,linear 不会生效,ffmpeg 会悄悄退回动态模式。
这首演示音频的源 LRA 是 17.9,比 11 大,所以触发了回退。把目标 LRA 调到不低于源 LRA 之后,结果正好是 -14.00 LUFS,LRA 保持 17.90 不变。现在代码会自动做这个调整,并打印提示。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 响度没有正好落在目标值 | loudnorm 退回了动态模式 | 目标 LRA 不能低于源 LRA,代码已自动处理 |
| 处理后声音被压扁了 | 用了单遍动态模式 | 改用两遍处理(本文的做法) |
| 发布平台把音量调小了 | 成品响度超过了平台的参考值 | 按平台的说明设置 --lufs |
| 中文标签显示乱码 | 播放器不支持 ID3v2.4 | 换一个播放器,或在发布平台上手动填写信息 |
模板规则
- 原文件只读,成品另存为新文件。
- 响度处理用两遍法,处理前后都要测量。
- 每个成品都写入 AI 生成标识。
项目资源
| 用途 | 地址 |
|---|---|
| Suno 内容凭据与检测接口说明 | https://suno.com/suno-credentials |
| ffmpeg loudnorm 文档 | https://ffmpeg.org/ffmpeg-filters.html#loudnorm |
| EBU R128 标准 | https://tech.ebu.ch/publications/r128 |
响度统一、标签写好之后,再交给 C 篇做发布前的最后检查。