894 lines
29 KiB
Markdown
894 lines
29 KiB
Markdown
# `transcode_music.py` 使用手册
|
||
|
||
批量将无损音乐(FLAC / WAV / DSF / DFF / APE)转码为 MP3 320k CBR 的 Python 脚本,支持断点续跑、错误恢复、CUE 整轨检测、DSD 转码。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
- [1. 简介](#1-简介)
|
||
- [2. 系统要求](#2-系统要求)
|
||
- [3. 快速上手](#3-快速上手)
|
||
- [4. 命令行参数详解](#4-命令行参数详解)
|
||
- [5. 常用使用场景](#5-常用使用场景)
|
||
- [6. Manifest 文件说明](#6-manifest-文件说明)
|
||
- [7. 文件分类规则](#7-文件分类规则)
|
||
- [8. CUE 整轨检测逻辑](#8-cue-整轨检测逻辑)
|
||
- [9. 采样率与 DSD 处理](#9-采样率与-dsd-处理)
|
||
- [10. 错误处理与恢复](#10-错误处理与恢复)
|
||
- [11. 已知局限](#11-已知局限)
|
||
- [12. FAQ](#12-faq)
|
||
|
||
---
|
||
|
||
## 1. 简介
|
||
|
||
**这个工具解决什么问题?**
|
||
|
||
你有一大堆无损音乐(比如 `/mnt/z/` 网盘上几百 GB 的 FLAC / WAV / DSF),想批量转成体积更小的 MP3 320k 存到手机/车载/移动设备,同时**保持原有的目录结构和文件名**,并且要能**中途暂停、下次继续**。
|
||
|
||
**核心特性**:
|
||
|
||
| 特性 | 说明 |
|
||
|---|---|
|
||
| 保持目录结构 | 目标目录树与源目录一一对应,多碟专辑 (CD1/CD2) 分开处理 |
|
||
| 保留所有资源 | 封面图片 (jpg/png/…)、说明 TXT、EAC log、CUE 都会复制过去 |
|
||
| 断点续跑 | 转码状态记录在源目录的 `.transcode_manifest.json`,任意时刻中断都能从上次位置继续 |
|
||
| CUE 整轨检测 | 分析阶段自动识别 "CUE + 单个大 flac 未分轨" 的专辑,报警并跳过等你手动 split |
|
||
| CUE 已分轨识别 | 若 CUE 引用的是整轨但同时存在分轨文件,自动跳过冗余的整轨文件 |
|
||
| 高采样率处理 | 24bit / 96kHz / 192kHz 输入自动重采样到 44.1kHz (符合 MP3 规范) |
|
||
| DSD 支持 | DSF/DFF (DSD64 / DSD128) 自动解码降采样后编码 MP3 |
|
||
| 保留封面 | 内嵌于 FLAC/DSF 的封面会写入 MP3 的 ID3v2 tag |
|
||
| 错误隔离 | 单个文件失败不中断整体流程,错误写入 manifest 供后续排查 |
|
||
| 幂等 | 已完成的文件/专辑,再次运行时秒跳过 |
|
||
|
||
---
|
||
|
||
## 2. 系统要求
|
||
|
||
| 组件 | 版本 | 说明 |
|
||
|---|---|---|
|
||
| Python | 3.10+ | 只用标准库,无需 pip install 任何依赖 |
|
||
| ffmpeg | 6.0+ | 需支持 `libmp3lame` 编码器,`dsd_lsbf/msbf` 解码器 (DSD 用) |
|
||
| ffprobe | 6.0+ | 通常随 ffmpeg 一起安装 |
|
||
|
||
**检查环境**:
|
||
|
||
```bash
|
||
python3 --version
|
||
ffmpeg -version | head -1
|
||
ffprobe -version | head -1
|
||
# 确认支持 mp3 编码
|
||
ffmpeg -codecs 2>/dev/null | grep libmp3lame
|
||
# 确认支持 DSD 解码 (如果你有 DSF 文件)
|
||
ffmpeg -codecs 2>/dev/null | grep -i dsd
|
||
```
|
||
|
||
**安装 ffmpeg**(如果没有):
|
||
|
||
```bash
|
||
# Ubuntu/Debian/WSL
|
||
sudo apt install ffmpeg
|
||
|
||
# macOS
|
||
brew install ffmpeg
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 快速上手
|
||
|
||
**最常用的三个命令**:
|
||
|
||
```bash
|
||
# 1. 先分析看看有没有问题 (不实际转码)
|
||
python3 transcode_music.py /mnt/z/我的音乐 /mnt/x/mp3输出 --analyze-only
|
||
|
||
# 2. 确认没问题后, 正式转码 (中断可再跑, 自动 resume)
|
||
python3 transcode_music.py /mnt/z/我的音乐 /mnt/x/mp3输出
|
||
python3 transcode_music.py /mnt/d/music/ /mnt/x/music/20260823
|
||
|
||
# 3. 后来加了新专辑, 再跑一次 (已完成的会自动跳过, 只处理新加的)
|
||
python3 transcode_music.py /mnt/z/我的音乐 /mnt/x/mp3输出
|
||
```
|
||
|
||
``` shell
|
||
# 4. Macmini上的命令路径(NAS上 /volume2/navidrome已经成功mount):
|
||
python3 transcode_music.py ~/MyMusic ~/mnt/volume2/navidrome/music/20260823
|
||
|
||
```
|
||
[[Mac Mini NAS 挂载配置指南]]
|
||
|
||
就这么简单。下面是完整的参数说明和更多用法。
|
||
|
||
---
|
||
|
||
## 4. 命令行参数详解
|
||
|
||
### 完整语法
|
||
|
||
```
|
||
transcode_music.py [SOURCE] [TARGET] [选项...]
|
||
```
|
||
|
||
或
|
||
|
||
```
|
||
transcode_music.py --source SOURCE --target TARGET [选项...]
|
||
```
|
||
|
||
### 参数列表
|
||
|
||
| 参数 | 短选项 | 类型 | 是否必需 | 默认 | 说明 |
|
||
|---|---|---|---|---|---|
|
||
| `SOURCE` | — | 位置参数 | 必需¹ | — | 源目录,无损音乐所在位置 |
|
||
| `TARGET` | — | 位置参数 | 必需¹ | — | 目标目录,MP3 输出位置 |
|
||
| `--source` | `-s` | 字符串 | 必需¹ | — | 源目录(与位置参数 SOURCE 等价) |
|
||
| `--target` | `-t` | 字符串 | 必需¹ | — | 目标目录(与位置参数 TARGET 等价) |
|
||
| `--bitrate` | `-b` | 字符串 | 可选 | `320k` | MP3 CBR 比特率。可选 `320k` / `256k` / `192k` / `128k` 等 |
|
||
| `--analyze-only` | — | 开关 | 可选 | 关 | 只分析生成 manifest,不实际转码 |
|
||
| `--force` | — | 开关 | 可选 | 关 | 忽略现有 manifest,全部重新分析并转码 |
|
||
| `--verbose` | `-v` | 开关 | 可选 | 关 | 显示 debug 级别日志(包括跳过的项) |
|
||
| `--ffmpeg` | — | 字符串 | 可选 | `ffmpeg` | ffmpeg 可执行文件路径(默认从 PATH 查找) |
|
||
| `--ffprobe` | — | 字符串 | 可选 | `ffprobe` | ffprobe 可执行文件路径 |
|
||
| `--timeout` | — | 整数 | 可选 | `3600` | 单文件转码超时秒数(超时算失败) |
|
||
| `--sample-rate-when-high` | — | 整数 | 可选 | `44100` | 输入采样率 > 48kHz 时的输出采样率,可选 `44100` / `48000` |
|
||
| `--help` | `-h` | 开关 | — | — | 显示帮助并退出 |
|
||
|
||
¹ **源目录 和 目标目录 必须提供,但可以用位置参数或 flag 两种形式之一**:
|
||
|
||
```bash
|
||
# 位置参数 (更简洁, 推荐)
|
||
transcode_music.py /mnt/z/music /mnt/x/mp3
|
||
|
||
# flag 长形式
|
||
transcode_music.py --source /mnt/z/music --target /mnt/x/mp3
|
||
|
||
# flag 短形式
|
||
transcode_music.py -s /mnt/z/music -t /mnt/x/mp3
|
||
|
||
# 混合也可以
|
||
transcode_music.py /mnt/z/music -t /mnt/x/mp3
|
||
```
|
||
|
||
### 参数详细说明
|
||
|
||
#### `SOURCE` / `--source` / `-s`
|
||
|
||
**源目录**。脚本会递归扫描此目录下所有子目录,对每个包含无损音频的子目录识别为一个"专辑单元"处理。
|
||
|
||
- 支持含空格、中文、特殊字符的路径
|
||
- 网盘挂载(`/mnt/z/`、`/mnt/x/` 等)都可以
|
||
- Manifest 文件 `.transcode_manifest.json` 会写入此目录
|
||
|
||
#### `TARGET` / `--target` / `-t`
|
||
|
||
**目标目录**。MP3 输出位置。目录结构会与源目录保持一致。
|
||
|
||
- 目录会自动创建(含中间层)
|
||
- 已存在的目标文件如果 manifest 记录 `completed` 会自动跳过
|
||
|
||
#### `--bitrate` / `-b`
|
||
|
||
MP3 输出比特率(CBR,Constant Bit Rate)。默认 `320k`。
|
||
|
||
**推荐取值**:
|
||
|
||
| 值 | 说明 |
|
||
|---|---|
|
||
| `320k` | 最高质量,几乎无损感知,推荐(默认) |
|
||
| `256k` | 高质量,体积减小约 20% |
|
||
| `192k` | 良好质量,体积减小约 40% |
|
||
| `128k` | 一般质量,仅在存储极度紧张时使用 |
|
||
|
||
**注意**: 一旦确定比特率跑过一次,配置写入 manifest。**中途改比特率**不会导致已完成的文件重新转码(脚本以文件 completed 状态为准)。想改需配合 `--force`。
|
||
|
||
#### `--analyze-only`
|
||
|
||
**只分析,不转码**。用途:
|
||
|
||
- 首次跑一个新的大目录时,先看看总专辑数、CUE 整轨的有没有
|
||
- 探测有没有解析错误的文件
|
||
- 生成 manifest 后可以手动查看/编辑
|
||
|
||
**推荐**每次对新目录都先跑一次 `--analyze-only`。
|
||
|
||
#### `--force`
|
||
|
||
**强制重跑**。忽略源目录现有的 manifest,重新分析并转码所有文件。
|
||
|
||
**什么时候用**:
|
||
- 换了比特率想全部重新转
|
||
- 怀疑之前的输出有问题
|
||
- 想清理并重新开始
|
||
|
||
**警告**:会覆盖已存在的 MP3。之前的 manifest 不会删除但会被覆盖。
|
||
|
||
#### `--verbose` / `-v`
|
||
|
||
打开 DEBUG 级别日志。默认 INFO 级别只显示正在处理和错误。加上 `-v` 会额外显示:
|
||
|
||
- `[SKIP (已完成): ...]` 每个跳过的专辑
|
||
- `[SKIP (空目录): ...]` 跳过的空目录
|
||
- ffprobe/ffmpeg 的调试信息
|
||
|
||
大量输出,通常只在排查问题时用。
|
||
|
||
#### `--ffmpeg` / `--ffprobe`
|
||
|
||
指定 ffmpeg / ffprobe 的路径。默认从 `PATH` 查找。
|
||
|
||
**什么时候用**:
|
||
- 系统装了多个 ffmpeg 版本,想指定用哪个
|
||
- ffmpeg 不在标准 PATH 里
|
||
|
||
示例:
|
||
|
||
```bash
|
||
transcode_music.py /src /dst --ffmpeg /opt/ffmpeg-6.1/bin/ffmpeg
|
||
```
|
||
|
||
#### `--timeout`
|
||
|
||
单个文件转码的超时时间(秒),默认 3600(1 小时)。超时算失败,写入 manifest 后继续处理下一个。
|
||
|
||
**通常不需要调整**。除非你有超长的音频文件(比如整轨 3 小时的 wav)在慢网盘上转码,可以调大。
|
||
|
||
#### `--sample-rate-when-high`
|
||
|
||
高采样率输入(> 48kHz)降到多少 kHz。默认 `44100`(CD 标准)。
|
||
|
||
- `44100`:CD 标准,最兼容
|
||
- `48000`:视频/DVD 标准
|
||
|
||
对于 24bit/96kHz、24bit/192kHz、DSD(352.8k / 705.6k)等高采样率源,MP3 编码器不支持 > 48kHz,必须降采样。默认 44.1kHz 是 CD 标准,兼容性最好。
|
||
|
||
如果你的音乐主要来源是从视频/电影音轨提取的,用 48000 可能更保留原有采样率。
|
||
|
||
---
|
||
|
||
## 5. 常用使用场景
|
||
|
||
### 场景 1: 第一次处理新目录(推荐流程)
|
||
|
||
假设你刚下载了一大堆音乐到 `/mnt/z/新收藏/`,想转到 `/mnt/x/music/新收藏/`。
|
||
|
||
**第一步:先分析**
|
||
|
||
```bash
|
||
python3 transcode_music.py /mnt/z/新收藏 /mnt/x/music/新收藏 --analyze-only
|
||
```
|
||
|
||
输出会告诉你:
|
||
- 总共多少个专辑
|
||
- 多少个音频文件、多少资源
|
||
- **有没有需要 CUE split 的专辑(`needs_cue_split`)**
|
||
|
||
**第二步:处理 needs_cue_split 的专辑**(如果有)
|
||
|
||
对于报警的整轨专辑,你需要用别的工具(比如 `shntool`、`CUETools`、`XLD`)手动 split 成分轨,然后再回来跑脚本。
|
||
|
||
**第三步:正式转码**
|
||
|
||
```bash
|
||
python3 transcode_music.py /mnt/z/新收藏 /mnt/x/music/新收藏
|
||
```
|
||
|
||
已经 `analyzed` 的专辑会开始转码,`needs_cue_split` 的会自动跳过(不影响其他)。
|
||
|
||
### 场景 2: 中断后恢复
|
||
|
||
假设转到一半按 Ctrl+C 中断了(或者断电、脚本崩溃了)。
|
||
|
||
只需要**再跑同样的命令**:
|
||
|
||
```bash
|
||
python3 transcode_music.py /mnt/z/新收藏 /mnt/x/music/新收藏
|
||
```
|
||
|
||
脚本会读取 manifest,跳过所有 `completed` 状态的文件和专辑,从上次中断的地方继续。**不需要任何特殊参数**。
|
||
|
||
### 场景 3: 增量同步(新增了专辑)
|
||
|
||
以后你又下载了几个新专辑到 `/mnt/z/新收藏/`,直接跑:
|
||
|
||
```bash
|
||
python3 transcode_music.py /mnt/z/新收藏 /mnt/x/music/新收藏
|
||
```
|
||
|
||
已完成的专辑秒跳过,只有新目录会被分析和转码。这是最舒服的用法。
|
||
|
||
### 场景 4: 修复失败的文件
|
||
|
||
如果某个专辑因为源文件损坏等原因失败了(`status: failed`),你修复源文件后:
|
||
|
||
```bash
|
||
# 直接再跑一次, failed 的会被自动重试
|
||
python3 transcode_music.py /mnt/z/新收藏 /mnt/x/music/新收藏
|
||
```
|
||
|
||
已成功的不会重跑,只有失败的会重试。
|
||
|
||
### 场景 5: 换比特率重新转码
|
||
|
||
想把之前的 320k 全部改成 256k:
|
||
|
||
```bash
|
||
# 加 --force 强制重跑
|
||
python3 transcode_music.py /mnt/z/新收藏 /mnt/x/music/新收藏 -b 256k --force
|
||
```
|
||
|
||
### 场景 6: 只转一小部分做质量测试
|
||
|
||
比如你想先转一个专辑试听下音质,别一下转几百 GB。
|
||
|
||
**方法一**:直接指定那个专辑的父目录
|
||
|
||
```bash
|
||
# 只处理 "李宗盛精选" 这一个专辑
|
||
python3 transcode_music.py "/mnt/z/新收藏/李宗盛精选" /mnt/x/music/李宗盛精选
|
||
```
|
||
|
||
**方法二**:跑 `--analyze-only` 看总数,觉得数量能接受再跑正式的。
|
||
|
||
### 场景 7: 只想复制资源不想转码
|
||
|
||
不太常见。可以用位置参数让源和目标一致(虽然实际上 target 是别的,但 --analyze-only 后手动处理):
|
||
|
||
```bash
|
||
# 只分析生成 manifest, 不转码
|
||
python3 transcode_music.py /mnt/z/music /mnt/x/mp3 --analyze-only
|
||
```
|
||
|
||
然后手动编辑 manifest 把所有音频状态改成 `skipped` 再跑。**不推荐**,通常你就是想转码。
|
||
|
||
### 场景 8: 排查某个专辑的问题
|
||
|
||
某个专辑失败了或者行为奇怪,用 verbose 模式跑:
|
||
|
||
```bash
|
||
python3 transcode_music.py "/mnt/z/新收藏/有问题的专辑" /mnt/x/music/test -v
|
||
```
|
||
|
||
输出会更详细,包括 ffprobe 探测细节、跳过原因等。
|
||
|
||
### 场景 9: 用不同 ffmpeg 版本
|
||
|
||
```bash
|
||
python3 transcode_music.py /mnt/z/music /mnt/x/mp3 \
|
||
--ffmpeg /opt/ffmpeg-7.0/bin/ffmpeg \
|
||
--ffprobe /opt/ffmpeg-7.0/bin/ffprobe
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Manifest 文件说明
|
||
|
||
### 位置
|
||
|
||
Manifest 保存在**源目录**下,文件名 `.transcode_manifest.json`(隐藏文件,前缀有点)。
|
||
|
||
### 顶层结构
|
||
|
||
```json
|
||
{
|
||
"manifest_version": 1,
|
||
"created_at": "2026-08-16T15:05:03+08:00",
|
||
"updated_at": "2026-08-16T15:19:06+08:00",
|
||
"source_root": "/mnt/z/test",
|
||
"target_root": "/mnt/x/music/test",
|
||
"config": {
|
||
"bitrate": "320k",
|
||
"id3v2_version": "3",
|
||
"output_sample_rate_when_high": 44100
|
||
},
|
||
"stats": {
|
||
"total_albums": 5,
|
||
"by_album_status": {"completed": 5},
|
||
"by_audio_status": {"completed": 50},
|
||
"by_asset_status": {"completed": 7},
|
||
"total_audio_files": 50,
|
||
"total_asset_files": 7
|
||
},
|
||
"albums": {
|
||
"专辑相对路径1": { ... },
|
||
"专辑相对路径2": { ... }
|
||
}
|
||
}
|
||
```
|
||
|
||
### 单个专辑 (album) 结构
|
||
|
||
```json
|
||
{
|
||
"status": "completed",
|
||
"issues": [],
|
||
"audio_files": {
|
||
"01 - 北风.flac": {
|
||
"type": "flac",
|
||
"size": 29671831,
|
||
"sample_rate": 44100,
|
||
"channels": 2,
|
||
"duration_sec": 297.36,
|
||
"bits_per_sample": 16,
|
||
"codec": "flac",
|
||
"status": "completed",
|
||
"target_rel": "01 - 北风.mp3",
|
||
"error": null,
|
||
"converted_at": "2026-08-16T15:18:04+08:00"
|
||
}
|
||
},
|
||
"asset_files": {
|
||
"cover.jpg": {
|
||
"type": "image",
|
||
"size": 234567,
|
||
"status": "completed",
|
||
"target_rel": "cover.jpg",
|
||
"error": null,
|
||
"copied_at": "2026-08-16T15:18:05+08:00"
|
||
}
|
||
},
|
||
"ignored_files": ["Thumbs.db"],
|
||
"has_audio": true,
|
||
"started_at": "2026-08-16T15:18:04+08:00",
|
||
"completed_at": "2026-08-16T15:18:06+08:00",
|
||
"error": null
|
||
}
|
||
```
|
||
|
||
### 状态值 (status)
|
||
|
||
**专辑级状态**:
|
||
|
||
| status | 说明 | 下次运行时行为 |
|
||
|---|---|---|
|
||
| `pending` | 初始状态(一般用不到) | 会重新分析 |
|
||
| `analyzed` | 已分析,等待转码 | 继续转码 |
|
||
| `processing` | 正在处理(若看到,说明上次中断了) | 会重新处理未完成部分 |
|
||
| `needs_cue_split` | 是 CUE + 整轨大文件,需先手动 split | **跳过**,不处理 |
|
||
| `completed` | 所有文件都成功了 | **秒跳过** |
|
||
| `failed` | 有文件失败 | 会重试失败的部分 |
|
||
| `skipped` | 空目录,无音频且无资源 | 跳过 |
|
||
|
||
**文件级状态**(audio_files 和 asset_files 里):
|
||
|
||
| status | 说明 |
|
||
|---|---|
|
||
| `pending` | 待处理 |
|
||
| `completed` | 已完成 |
|
||
| `failed` | 失败(`error` 字段有原因) |
|
||
| `skipped` | 主动跳过(如 CUE 引用的整轨已有分轨) |
|
||
|
||
### 手动干预 manifest
|
||
|
||
Manifest 是纯 JSON,你可以用文本编辑器直接改:
|
||
|
||
**例子 1:让某个专辑重新处理**
|
||
```json
|
||
"status": "completed" → "status": "analyzed"
|
||
```
|
||
把该专辑内的音频文件也改成:
|
||
```json
|
||
"status": "completed" → "status": "pending"
|
||
```
|
||
|
||
**例子 2:跳过某个损坏的文件**
|
||
```json
|
||
"01 - 有问题.flac": {
|
||
...
|
||
"status": "skipped",
|
||
"error": "手动跳过,源文件有损坏"
|
||
}
|
||
```
|
||
|
||
**例子 3:删除某个专辑的记录,让它从头分析**
|
||
删除对应的 album 条目,下次运行会自动重新扫描添加。
|
||
|
||
**改完保存直接跑**:
|
||
|
||
```bash
|
||
python3 transcode_music.py /mnt/z/music /mnt/x/mp3
|
||
```
|
||
|
||
### Manifest 损坏怎么办?
|
||
|
||
如果 JSON 损坏(编辑错了、盘故障),脚本启动时会自动检测,将坏 manifest 重命名为 `.transcode_manifest.json.corrupt-<时间戳>`,然后**重新生成新的 manifest**。已完成的 MP3 文件不会丢,但脚本会重新扫描所有文件(对已有 MP3 的处理由目标文件是否存在来判断,见"文件级幂等")。
|
||
|
||
**手动恢复方法**:如果损坏的 manifest 里有大量已完成状态,可以试着修复 JSON 语法后重命名回去。
|
||
|
||
---
|
||
|
||
## 7. 文件分类规则
|
||
|
||
脚本会根据扩展名把文件分到几类:
|
||
|
||
### 无损音频(会转码为 MP3)
|
||
|
||
`.flac` `.wav` `.dsf` `.dff` `.ape`
|
||
|
||
### 有损音频(当作资源直接复制,不转码)
|
||
|
||
`.mp3` `.m4a` `.aac` `.ogg` `.opus` `.wma` `.mp2`
|
||
|
||
**为什么不再压?** 因为它们已经是有损,再压一次损失更多。
|
||
|
||
### 图片(复制)
|
||
|
||
`.jpg` `.jpeg` `.png` `.bmp` `.gif` `.webp` `.tif` `.tiff`
|
||
|
||
### 文档(复制)
|
||
|
||
`.txt` `.log` `.pdf` `.md` `.nfo` `.doc` `.docx` `.rtf`
|
||
|
||
### CUE(复制)
|
||
|
||
`.cue`
|
||
|
||
### 忽略的文件(不处理)
|
||
|
||
- `.ds_store`、`thumbs.db`、`desktop.ini`、`.directory`(系统文件)
|
||
- 以 `._` 开头的(macOS resource fork)
|
||
- `.transcode_manifest.json` 及其 tmp 中间文件
|
||
|
||
### 其他扩展名
|
||
|
||
未列出的扩展名会被当作"other"资源直接复制,不会丢。
|
||
|
||
---
|
||
|
||
## 8. CUE 整轨检测逻辑
|
||
|
||
CUE 文件常见有三种搭配:
|
||
|
||
### 情况 A:CUE + 单个整轨大文件(需先 split)
|
||
|
||
```
|
||
album.cue (指向 album.flac, 里面 12 个 TRACK 条目)
|
||
album.flac (整张 CD 打包成一个大文件)
|
||
```
|
||
|
||
**处理**:脚本标记专辑状态为 `needs_cue_split`,跳过转码,在报告里警告:
|
||
|
||
```
|
||
[!] 1 个专辑需先手动 CUE split (已跳过转码):
|
||
• album名
|
||
album.cue: 整轨型 (单文件 'album.flac', 12 轨) — 需先手动 CUE split 再转码
|
||
```
|
||
|
||
**处理方式**:用其他工具(`shntool split`、`CUETools`、`XLD`、`foobar2000`)split 成分轨 flac 后,删除大整轨文件(或保留但用 CUE 引用改指向分轨),然后再跑脚本。
|
||
|
||
### 情况 B:CUE + 分轨文件(已 split,直接转码)
|
||
|
||
```
|
||
album.cue (指向 track01.flac, track02.flac...)
|
||
track01.flac
|
||
track02.flac
|
||
...
|
||
```
|
||
|
||
**处理**:脚本正常转码所有分轨 flac,CUE 文件也作为资源复制过去。
|
||
|
||
### 情况 C:CUE 引用整轨 + 但同时存在分轨(跳过冗余)
|
||
|
||
```
|
||
CDImage.cue (指向 CDImage.flac)
|
||
CDImage.flac (整轨大文件, 冗余)
|
||
(01) 曲名.flac (已经手动 split 出来的分轨)
|
||
(02) 曲名.flac
|
||
...
|
||
```
|
||
|
||
**处理**:脚本自动跳过 `CDImage.flac`(避免重复转码),只转分轨,同时复制 CUE。报告里会说明:
|
||
|
||
```
|
||
CDImage.cue: 引用 'CDImage.flac' 但存在 20 个分轨 → 跳过 'CDImage.flac' 只转分轨
|
||
```
|
||
|
||
### 检测算法(技术细节)
|
||
|
||
对每个 CUE 文件:
|
||
1. 解析出所有 `FILE "xxx" WAVE` 条目(可能有多个)
|
||
2. 解析出所有 `TRACK NN AUDIO` 条目
|
||
3. 判断:
|
||
- 若 CUE 只引用 1 个文件,且这个文件是目录里唯一的音频 → 情况 A(整轨)
|
||
- 若 CUE 只引用 1 个文件,但目录里还有其他分轨音频 → 情况 C(跳过整轨)
|
||
- 若 CUE 引用多个文件(>= 2)→ 情况 B(分轨)
|
||
|
||
CUE 文件的编码会尝试多种(`utf-8-sig` / `utf-8` / `gbk` / `big5` / `shift_jis` / `cp936` / `latin1`),中日韩音源基本都能识别。
|
||
|
||
---
|
||
|
||
## 9. 采样率与 DSD 处理
|
||
|
||
### MP3 采样率约束
|
||
|
||
MP3 规范只允许这些采样率:8 / 11.025 / 12 / 16 / 22.05 / 24 / 32 / 44.1 / 48 kHz。
|
||
|
||
超过 48kHz 的输入**必须重采样**,脚本自动处理。
|
||
|
||
### 具体规则
|
||
|
||
| 输入采样率 | 输出采样率 | 备注 |
|
||
|---|---|---|
|
||
| 44100 Hz | 44100 Hz | 透传,CD 标准 |
|
||
| 48000 Hz | 48000 Hz | 透传 |
|
||
| 32000 Hz 及其他 MP3 支持值 | 保持不变 | 透传 |
|
||
| 88200 / 96000 Hz | 44100 Hz | 降采样(默认,可通过 `--sample-rate-when-high` 改为 48000) |
|
||
| 176400 / 192000 Hz | 44100 Hz | 降采样 |
|
||
| 352800 Hz (DSD64 解码后) | 44100 Hz | 精确 8:1 降采样(DSD64 特性)|
|
||
| 705600 Hz (DSD128 解码后) | 44100 Hz | 精确 16:1 降采样 |
|
||
|
||
### DSD (DSF / DFF) 处理
|
||
|
||
DSD 是 1-bit 高频(2.8 或 5.6 MHz)采样格式,与传统 PCM 不同。脚本流程:
|
||
|
||
1. **ffprobe** 探测:报告 `codec_name=dsd_lsbf_planar` 或 `dsd_msbf_planar`,`sample_rate=352800` 或 `705600`
|
||
2. **ffmpeg 解码**:DSD → 高采样率 PCM(自带低通滤波器,96 tap,通带平坦到 48kHz,阻带抑制 ~160dB)
|
||
3. **重采样**:降到 44100 Hz
|
||
4. **编码**:libmp3lame 320k CBR
|
||
|
||
DSD 内嵌的封面(通常是 mjpeg)会被识别为视频流并保留到 MP3 的 ID3v2 tag。
|
||
|
||
### 位深处理
|
||
|
||
- 16-bit 输入:直接编码
|
||
- 24-bit 输入:libmp3lame 内部用 32-bit float 处理,等效于 24-bit 精度参与编码
|
||
- DSD (1-bit):解码为 24-bit PCM 后编码
|
||
|
||
---
|
||
|
||
## 10. 错误处理与恢复
|
||
|
||
### 错误捕获原则
|
||
|
||
**单个文件失败不影响其他**。转码或复制某个文件出错时:
|
||
|
||
1. 该文件状态设为 `failed`,错误消息写入 `error` 字段
|
||
2. 该专辑状态设为 `failed`
|
||
3. **继续处理**下一个文件、下一个专辑
|
||
4. 最后报告里列出所有失败项
|
||
|
||
### 再次运行时
|
||
|
||
`failed` 状态的文件会**自动重试**。已经 `completed` 的不会重跑。
|
||
|
||
```bash
|
||
# 修好源文件后, 直接再跑
|
||
python3 transcode_music.py /mnt/z/music /mnt/x/mp3
|
||
```
|
||
|
||
### 常见错误
|
||
|
||
**"Error opening output files: Invalid argument"**
|
||
|
||
通常是 ffmpeg 无法识别输出格式或权限问题。脚本内部已加 `-f mp3` 强制指定 muxer 避免此问题。若仍出现:
|
||
- 检查目标目录是否可写
|
||
- 检查文件名是否有极端特殊字符
|
||
|
||
**"Operation not permitted"(复制时)**
|
||
|
||
网盘挂载(9p / SMB / NFS)不允许改文件权限。脚本已经改用 `copyfile` + best-effort `utime`,通常应该没问题。若出现:
|
||
- 检查目标目录挂载参数
|
||
- 尝试 `mount -o rw,noatime ...` 之类的选项
|
||
|
||
**ffprobe 失败 / 无法探测采样率**
|
||
|
||
源文件可能损坏。检查 `manifest.audio_files.<文件>.error` 里的具体消息。
|
||
|
||
**ffmpeg 超时**
|
||
|
||
默认 3600 秒。若真有超大文件慢网盘的组合,用 `--timeout 7200` 调大。
|
||
|
||
### 中断(Ctrl+C)
|
||
|
||
脚本捕获 SIGINT,会保存 manifest 后退出。**中断后当前正在转的那个文件的 `.mp3.tmp` 中间文件会被清理**,不会留下坏的 MP3。
|
||
|
||
### 用什么方法查看失败列表
|
||
|
||
```bash
|
||
# 用 jq 快速查看失败的专辑
|
||
jq -r '.albums | to_entries[] | select(.value.status=="failed") | .key' \
|
||
/mnt/z/music/.transcode_manifest.json
|
||
|
||
# 查看某个专辑里失败的文件
|
||
jq '.albums["专辑名"].audio_files | with_entries(select(.value.status=="failed"))' \
|
||
/mnt/z/music/.transcode_manifest.json
|
||
```
|
||
|
||
### 关于 needs_cue_split
|
||
|
||
`needs_cue_split` 状态的专辑**不算失败**,是主动跳过等你手动处理。若不需要转这些,可以:
|
||
|
||
**选项 1:修 manifest 忽略它们**
|
||
|
||
把状态改成 `skipped`:
|
||
|
||
```json
|
||
"status": "needs_cue_split" → "status": "skipped"
|
||
```
|
||
|
||
**选项 2:手动 CUE split**
|
||
|
||
用其他工具 split 成分轨,然后:
|
||
|
||
```bash
|
||
# 让脚本重新分析
|
||
rm /mnt/z/music/.transcode_manifest.json
|
||
python3 transcode_music.py /mnt/z/music /mnt/x/mp3 --analyze-only
|
||
```
|
||
|
||
---
|
||
|
||
## 11. 已知局限
|
||
|
||
- **不支持并行转码**。是单进程顺序处理。CPU 密集型(LAME 编码)加网盘 IO 是主要瓶颈。多进程可以显著加速,但为保证 manifest 状态一致性没有实现。若性能是问题,可以对不同子目录同时跑多个脚本实例。
|
||
- **不支持 VBR MP3**。只做 CBR。原因:CBR 兼容性更好,且 320k CBR 与 V0 VBR 音质差异对绝大多数人不可闻。
|
||
- **不做 replaygain / ID3v1 补齐 / 章节标记**。只做基本的 ID3v2.3 元数据映射。
|
||
- **CUE split 不会自动做**。检测出整轨专辑后只是报警跳过,不代替你调用外部工具去 split。设计如此以避免不可控的副作用。
|
||
- **不支持 ALAC (.m4a) 转 MP3**。因为 `.m4a` 容器里既可能是 ALAC 无损也可能是 AAC 有损,无法从扩展名区分。这些文件会被当作有损直接复制。若你有确定是 ALAC 的 `.m4a`,需要先用 ffmpeg 手动转成 flac。
|
||
- **网盘 mtime 不能保留**。9p / SMB 挂载不允许改 mtime,所以复制后的资源文件时间戳是当下时间(内容完全一致)。转码后的 MP3 也是同样情况。
|
||
- **不做重复检测**。若源目录有两个不同路径下的同名同内容文件,会各自转码两份。
|
||
|
||
---
|
||
|
||
## 12. FAQ
|
||
|
||
**Q: 我可以中途改比特率吗?**
|
||
|
||
A: 已经 `completed` 的文件不会重转,即使你改了 `-b`。想全部按新比特率重跑就加 `--force`。
|
||
|
||
**Q: 手机不支持 320k 会怎样?**
|
||
|
||
A: 所有正常的 MP3 播放器都支持 320k CBR,几十年的老设备也没问题。
|
||
|
||
**Q: 转码后 MP3 音质怎么样?**
|
||
|
||
A: LAME 320k CBR 是"透明比特率"级别,即绝大多数听者在双盲测试中无法区分 320k MP3 和原始 FLAC。除非你有非常好的耳机和听音训练,几乎听不出差别。
|
||
|
||
**Q: DSD 转 MP3 有没有意义?**
|
||
|
||
A: DSD 主要意义在于极高频响应(超过人耳范围)和低失真。转成 MP3 后这些优势基本没了。但 MP3 320k 仍能保留 DSD 音源的音乐性和大部分动态。若你就是想要个手机能播的版本,DSD → MP3 是合理的。想要"无损感"应该保留 FLAC。
|
||
|
||
**Q: 支持 Windows 直接跑吗?**
|
||
|
||
A: 脚本用了 POSIX 的 `os.fsync(dir_fd)` 做原子写,Windows 上会 best-effort(fsync dir 会 fail 但不影响正确性)。理论上可以跑,但没实测。**推荐 Linux / WSL2 / macOS**。
|
||
|
||
**Q: 网盘断连了怎么办?**
|
||
|
||
A: 单文件失败会记录到 manifest 并继续。重新连上后再跑一次,失败的会自动重试。
|
||
|
||
**Q: 生成的 manifest 会不会很大?**
|
||
|
||
A: 一般不大。50 个专辑(我的测试目录)的 manifest 约 26 KB。上千个专辑估计几 MB。不影响使用。
|
||
|
||
**Q: 我想只转某几个专辑怎么办?**
|
||
|
||
A: 最简单的办法是让 SOURCE 指向那个专辑的父目录:
|
||
|
||
```bash
|
||
python3 transcode_music.py "/mnt/z/music/张学友精选" /mnt/x/mp3/张学友精选
|
||
```
|
||
|
||
或者临时把想转的目录复制/软链接到一个专用目录,跑完再删。
|
||
|
||
**Q: 能不能顺便做元数据整理(比如加艺人、专辑年份)?**
|
||
|
||
A: 目前不做。脚本只做 `-map_metadata 0` 把源文件的 tag 原样映射过去。想批量整理元数据推荐 `beets`、`Picard`。
|
||
|
||
**Q: 转码完想验证质量怎么办?**
|
||
|
||
A: 抽几首用 `ffprobe` 看输出的 `bit_rate` 应该是 320000 左右,`sample_rate` 是 44100 或 48000。
|
||
|
||
```bash
|
||
ffprobe -v error -show_entries "stream=codec_name,sample_rate,bit_rate" \
|
||
-show_entries "format=bit_rate,duration" \
|
||
"/mnt/x/mp3/xxx.mp3"
|
||
```
|
||
|
||
或用播放器听一下确认。
|
||
|
||
---
|
||
|
||
## 附录 A:完整命令示例集
|
||
|
||
```bash
|
||
# 分析(不转码)
|
||
python3 transcode_music.py /mnt/z/music /mnt/x/mp3 --analyze-only
|
||
|
||
# 正式转码(自动 resume)
|
||
python3 transcode_music.py /mnt/z/music /mnt/x/mp3
|
||
|
||
# 详细日志
|
||
python3 transcode_music.py /mnt/z/music /mnt/x/mp3 -v
|
||
|
||
# 使用 256k 比特率
|
||
python3 transcode_music.py /mnt/z/music /mnt/x/mp3 -b 256k
|
||
|
||
# 强制重跑
|
||
python3 transcode_music.py /mnt/z/music /mnt/x/mp3 --force
|
||
|
||
# 使用 48kHz 输出(视频 / DVD 场景)
|
||
python3 transcode_music.py /mnt/z/music /mnt/x/mp3 --sample-rate-when-high 48000
|
||
|
||
# 使用长参数
|
||
python3 transcode_music.py --source /mnt/z/music --target /mnt/x/mp3
|
||
|
||
# 使用短参数
|
||
python3 transcode_music.py -s /mnt/z/music -t /mnt/x/mp3
|
||
|
||
# 组合:位置 + 选项
|
||
python3 transcode_music.py /mnt/z/music /mnt/x/mp3 -b 256k -v --force
|
||
|
||
# 单独指定 ffmpeg 路径
|
||
python3 transcode_music.py /mnt/z/music /mnt/x/mp3 \
|
||
--ffmpeg /opt/ffmpeg-7/bin/ffmpeg \
|
||
--ffprobe /opt/ffmpeg-7/bin/ffprobe
|
||
|
||
# 单文件超长的场景,加大超时
|
||
python3 transcode_music.py /mnt/z/music /mnt/x/mp3 --timeout 7200
|
||
```
|
||
|
||
## 附录 B:文件结构示例
|
||
|
||
**源目录**(真实的音乐盘结构示例):
|
||
|
||
```
|
||
/mnt/z/新收藏/
|
||
├── 张学友精选/
|
||
│ ├── 01 - 吻别.flac ← 转成 mp3
|
||
│ ├── 02 - 一路上有你.flac ← 转成 mp3
|
||
│ ├── cover.jpg ← 复制
|
||
│ └── 专辑说明.txt ← 复制
|
||
├── 王菲 - 天空 (24bit-192kHz)/
|
||
│ ├── 01. 天空.flac ← 24/192 → 降采样后转 mp3
|
||
│ ├── 02. 誓言.flac
|
||
│ └── booklet.jpg
|
||
├── 蔡琴 2CD 精选/
|
||
│ ├── back.jpg ← 顶层封面, 复制
|
||
│ ├── cover.jpg
|
||
│ ├── 目录.txt
|
||
│ ├── CD1/ ← 独立处理
|
||
│ │ ├── 01 恰似你的温柔.wav ← 转成 mp3
|
||
│ │ └── 02 你的眼神.wav
|
||
│ └── CD2/
|
||
│ └── 01 出塞曲.wav
|
||
├── DSD 专辑/
|
||
│ └── 谭咏麟-一生中最爱.dsf ← DSD 解码后转 mp3
|
||
└── 整轨专辑 [FLAC+CUE]/ ← ⚠ needs_cue_split
|
||
├── CDImage.flac ← 整个 CD 一个大文件
|
||
└── CDImage.cue ← CUE 索引
|
||
```
|
||
|
||
**目标目录**(脚本处理后):
|
||
|
||
```
|
||
/mnt/x/mp3/新收藏/
|
||
├── 张学友精选/
|
||
│ ├── 01 - 吻别.mp3 ← 320k CBR
|
||
│ ├── 02 - 一路上有你.mp3
|
||
│ ├── cover.jpg
|
||
│ └── 专辑说明.txt
|
||
├── 王菲 - 天空 (24bit-192kHz)/
|
||
│ ├── 01. 天空.mp3 ← 44.1kHz 320k
|
||
│ ├── 02. 誓言.mp3
|
||
│ └── booklet.jpg
|
||
├── 蔡琴 2CD 精选/
|
||
│ ├── back.jpg
|
||
│ ├── cover.jpg
|
||
│ ├── 目录.txt
|
||
│ ├── CD1/
|
||
│ │ ├── 01 恰似你的温柔.mp3
|
||
│ │ └── 02 你的眼神.mp3
|
||
│ └── CD2/
|
||
│ └── 01 出塞曲.mp3
|
||
├── DSD 专辑/
|
||
│ └── 谭咏麟-一生中最爱.mp3 ← 320k
|
||
└── 整轨专辑 [FLAC+CUE]/ ← 空(因为 needs_cue_split 被跳过)
|
||
```
|
||
|
||
**Manifest**(在源目录):
|
||
|
||
```
|
||
/mnt/z/新收藏/.transcode_manifest.json
|
||
```
|