Files
nexus/Project/transcode_music/transcode_music.md
2026-08-30 20:34:33 +08:00

895 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#transcode #music #readme #document #guide #mp3 #wav #flac #ffmpeg #cue
# `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
```