使用AI写一个批量音频响度归一化工具

起因

最近入手了一个小型音乐播放设备,把收藏多年的 mp3 文件都拷了进去。本来期待着享受高品质音乐,结果发现了一个让人抓狂的问题:歌单顺序播放时,有些歌曲声音震耳欲聋,下一首却轻得几乎听不见,每次都要手忙脚乱地去拧音量旋钮。

这个问题其实由来已久——不同来源、不同年代的音乐文件,录制时的响度标准完全不同。90年代的专辑有的很保守,现代流行音乐为了”更响更有冲击力”则往往压缩得很厉害,两相对比之下差距可以有十几个 dB。

在网上搜索了一圈,有一些图形化工具可以处理,但批量处理几百首歌的体验并不好。于是决定自己动手,用 Claude Code + OpenSpec 来做一个命令行批处理工具。

什么是 LUFS 响度标准

在动手之前,先要搞清楚”响度归一化”到底是什么。

音量有很多种衡量方式:峰值(Peak)、均方根(RMS)、以及 LUFS(Loudness Units Full Scale)。前两种都有明显缺陷——峰值归一化只保证最响的瞬间不超限,RMS 也不能很好地反映人耳的主观感受。

LUFS 基于 EBU R128 标准,更贴近人耳对响度的实际感知。Spotify、YouTube、Apple Music 等主流流媒体平台都采用了这个标准,将目标响度定在 -14 LUFS 左右。这也是这个工具的默认目标值。

解决响度问题有两条路:

1
2
3
4
5
6
7
8
9
方案一:Tag 模式(ReplayGain)
测量响度 → 计算增益值 → 写入文件标签
优点:无损,原始音频数据不变
缺点:需要播放器支持 ReplayGain 标签才能生效

方案二:Encode 模式(重新编码)
测量响度 → ffmpeg loudnorm 两遍扫描 → 重新编码输出
优点:任意播放器直接播放即可
缺点:有损格式(如 MP3)会经历二次编码,轻微降质

对于我的音乐播放设备,它不支持 ReplayGain 标签,所以 Encode 模式是唯一的选择。

用 OpenSpec 先定规格

以前直接让 Claude Code 写代码,往往会遇到一个问题:需求说得越笼统,写出来的东西越容易跑偏,边做边改很费劲。这次换一种方式,先用 OpenSpec 把需求和设计整理清楚,再让 AI 去实现。

OpenSpec 是一个规格驱动的开发工作流,核心思路是在写代码之前先产出三份文档:

  • proposal.md:为什么要做、做什么、影响范围
  • design.md:技术架构、关键决策、数据模型、风险
  • tasks.md:具体的实现任务清单,每项可以被 AI 逐一完成

整理 Proposal

先把想清楚的需求写成 proposal。核心功能很明确:

  • 新增 --tag 模式:分析响度后将 ReplayGain Track Gain 标签写入文件副本(不修改音频数据)
  • 新增 --encode 模式:用 ffmpeg loudnorm 滤镜重新编码音频,使响度永久归一化
  • --target 参数自定义目标响度(默认 -14 LUFS)
  • --jobs 参数控制并发线程数
  • --recursive 参数递归处理子目录
  • 约束:所有模式均输出到新目录(--output),绝不修改原始文件

敲定技术设计

设计阶段是最有价值的部分。把几个关键技术决策提前想清楚:

D1:响度测量用 ffmpeg loudnorm

ffmpeg 内置的 loudnorm 滤镜可以直接测量 Integrated LUFS,一行命令搞定,不需要额外引入音频分析库:

1
2
3
ffmpeg -hide_banner -i <file> \
-filter:a "loudnorm=I=-14:TP=-1:LRA=11:print_format=json" \
-f null /dev/null

输出的 JSON 里就有测量到的各项响度数据。

D2:Encode 模式用两遍扫描

ffmpeg loudnorm 支持两遍扫描模式,第一遍只测量不编码,把测量结果喂给第二遍,使用线性增益(linear=true)进行精确编码,音质明显优于一遍模式:

1
2
3
第一遍:ffmpeg 分析 → 获取 input_i / input_lra / input_tp

第二遍:代入测量值,linear=true 线性增益重编码 → 输出文件

D3:并发用 ThreadPoolExecutor

实际的 CPU 工作都在 ffmpeg 子进程里,Python 这层只是启动进程和等待,属于 IO 密集型操作。ThreadPoolExecutor 完全够用,默认线程数按 CPU 核心数自动决定,最多 8 个。

D4:进度显示用 rich

用 rich 库的 Progress + Live 组合,实时显示每个文件的处理状态和 LUFS 前后对比,体验比干等好很多:

1
2
3
4
5
6
处理音频文件中…
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 120/247 48.6% 0:01:05
✓ 周杰伦 - 晴天.mp3 -18.3 → -14.0 LUFS (+4.3 dB)
✓ 周杰伦 - 七里香.mp3 -16.1 → -14.0 LUFS (+2.1 dB)
⟳ 分析中… 青花瓷.mp3
⟳ 编码中… 富士山下.mp3

拆解任务清单

有了设计文档,拆任务就很直观了,按模块逐一列出:

1
2
3
4
5
6
7
8
9
1. 项目初始化(目录结构、pyproject.toml、依赖)
2. CLI 接口(参数解析、ffmpeg 检查、输入输出验证)
3. 文件扫描器(scanner.py)
4. 响度分析器(analyzer.py)
5. 标签写入器(processor.py - tag 模式)
6. 音频编码器(processor.py - encode 模式)
7. 批处理器(worker.py,ThreadPoolExecutor)
8. 进度显示(progress.py,rich)
9. 集成测试

让 Claude Code 实现

规格文档准备好之后,整个实现过程出乎意料地顺利。

Claude Code 读取了设计文档,理解了模块划分和数据结构,然后按任务逐一实现。几个有意思的地方:

mutagen 的多线程坑

在多线程环境下用 mutagen 写标签时,遇到了偶发的 import lock 死锁问题。Claude Code 发现这个问题后,在文件顶部预先导入了所有 mutagen 子模块,避免了运行时的动态导入竞争:

1
2
3
4
5
# 顶层导入所有 mutagen 子模块,避免多线程下的 import lock 死锁
from mutagen.id3 import ID3, TXXX, error as ID3Error
from mutagen.flac import FLAC
from mutagen.oggvorbis import OggVorbis
# ...

临时文件保护

encode 模式编码时,如果中途中断会留下损坏的半成品文件。Claude Code 参照设计文档里的方案,实现了先写 ._tmp_<filename> 临时文件,成功后再重命名,失败则删除临时文件的保护机制。

各格式 ReplayGain 标签键名不同

不同音频格式写 ReplayGain 标签的方式差异很大,Claude Code 根据设计文档里的对应表一一实现:

格式 Track Gain 键
MP3 (ID3) TXXX:replaygain_track_gain
FLAC / OGG / Opus replaygain_track_gain
M4A / AAC ----:com.apple.iTunes:replaygain_track_gain

WAV 的无奈降级

WAV 格式的标签支持残缺不全,mutagen 处理起来也很麻烦。最终的方案是 tag 模式下 WAV 文件只复制不写标签,输出警告提示用户改用 encode 模式,算是务实的取舍。

整体下来,9 个模块的任务清单全部完成,代码风格也比较统一。

效果

工具用起来很简单:

1
2
3
4
5
6
7
8
9
10
11
# 安装
pip install -e .

# Tag 模式(推荐:无损,需播放器支持 ReplayGain)
music-norm --tag /你的音乐目录 --output /处理后的目录

# Encode 模式(重新编码,兼容任意播放器)
music-norm --encode /你的音乐目录 --output /处理后的目录

# 递归处理 + 自定义目标响度
music-norm --encode --recursive --target -16 ~/Music --output ~/Music_normalized

实际处理了几百首 mp3,encode 模式下平均每首歌需要几秒钟(取决于机器性能)。处理完之后拷回播放设备,来回切歌音量再也不用频繁调了,非常舒适。

AI 辅助开发的体会

这次的体会和之前直接让 Claude Code 写代码不太一样:

先设计再实现,效果更好。以前直接描述需求让 AI 写代码,往往要来回调整好几轮。这次用 OpenSpec 把设计文档写清楚再给 AI,它能更准确地理解意图,实现结果和预期更接近,返工少了很多。

文档本身就是价值。写 proposal 和 design 的过程,其实也是自己把需求想清楚的过程。很多模糊的决策(比如 WAV 怎么处理、两遍扫描还是一遍等等)在写文档时就逼着自己想清楚了,这部分价值和 AI 无关。

AI 处理”繁琐但有规律”的工作最省力。各格式的 ReplayGain 标签写入逻辑、各种边界情况的错误处理、参数校验……这些工作如果手写相当枯燥,AI 处理这类有规律的重复工作非常高效。

还是需要人来把关关键决策。响度测量方案、并发模型、临时文件策略……这些设计决策还是需要人来思考和拍板,AI 只是帮你把想法落地成代码。

项目地址

项目已开源,有需要的可以自取:

👉 https://cnb.cool/shellingford/music-volume-balance

依赖很简单:Python 3.8+、ffmpeg(系统级安装)、mutagen、rich。