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

使用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 | 方案一:Tag 模式(ReplayGain) |
对于我的音乐播放设备,它不支持 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 | ffmpeg -hide_banner -i <file> \ |
输出的 JSON 里就有测量到的各项响度数据。
D2:Encode 模式用两遍扫描
ffmpeg loudnorm 支持两遍扫描模式,第一遍只测量不编码,把测量结果喂给第二遍,使用线性增益(linear=true)进行精确编码,音质明显优于一遍模式:
1 | 第一遍:ffmpeg 分析 → 获取 input_i / input_lra / input_tp |
D3:并发用 ThreadPoolExecutor
实际的 CPU 工作都在 ffmpeg 子进程里,Python 这层只是启动进程和等待,属于 IO 密集型操作。ThreadPoolExecutor 完全够用,默认线程数按 CPU 核心数自动决定,最多 8 个。
D4:进度显示用 rich
用 rich 库的 Progress + Live 组合,实时显示每个文件的处理状态和 LUFS 前后对比,体验比干等好很多:
1 | 处理音频文件中… |
拆解任务清单
有了设计文档,拆任务就很直观了,按模块逐一列出:
1 | 1. 项目初始化(目录结构、pyproject.toml、依赖) |
让 Claude Code 实现
规格文档准备好之后,整个实现过程出乎意料地顺利。
Claude Code 读取了设计文档,理解了模块划分和数据结构,然后按任务逐一实现。几个有意思的地方:
mutagen 的多线程坑
在多线程环境下用 mutagen 写标签时,遇到了偶发的 import lock 死锁问题。Claude Code 发现这个问题后,在文件顶部预先导入了所有 mutagen 子模块,避免了运行时的动态导入竞争:
1 | # 顶层导入所有 mutagen 子模块,避免多线程下的 import lock 死锁 |
临时文件保护
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 | # 安装 |
实际处理了几百首 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。











