Files
atlas/transcode_music/split_cue.md
2026-09-08 06:47:00 +08:00

43 KiB
Raw Blame History

split_cue.py 使用手册

批量用 CUE 文件把"整轨大文件 + CUE"式专辑切成分轨的 Python 脚本,支持断点续跑、CJK 编码 CUE、多种源格式,以 shntool + cuetools 作为切割/标签后端。

设计上是 transcode_music.py 的前置伴生工具:先跑 split_cue.py 把整轨拆开,再跑 transcode_music.py 转 MP3,两者组合无缝衔接。


目录


1. 简介

这个工具解决什么问题?

你有很多"整轨型"专辑:一张 CD 打包成一个大 FLAC / WAV / APE 文件,配一个 CUE 索引,例如:

王菲 - 唱游 [FLAC+CUE]/
├── CDImage.flac        (整张 CD 的一个 40 分钟大文件)
└── CDImage.cue         (12 个 TRACK 条目, 标注每首歌的起止)

这类文件不方便用普通播放器逐首播放(除非播放器专门支持 CUE),也不方便直接转成 MP3(transcode_music.py 会检测出这种情况并报警跳过)。

split_cue.py 就是用来把这类专辑批量拆成分轨的:扫描整个源目录树,找出所有"整轨 CUE"型专辑,用 shnsplit 按 CD 帧边界(1/75 秒精度)切成 01 - 红豆.flac、02 - 催眠.flac 这样的分轨文件,用 cuetag 写入 CUE 里的标题/艺人/专辑等元数据,全部原地切割、保留原文件不动。

核心特性:

特性 说明
原地切割 分轨文件写在原专辑目录里,与整轨大文件并存
保留原文件 整轨的 CDImage.flac 从不被删改,可事后自己决定去留
保持源格式 FLAC → FLAC 无损、WAV → WAV、APE → FLAC(ffmpeg 无 APE 编码器)
Sample-exact 切割 shntool 按 CD 帧对齐,每首歌起止精确到 1/75 秒
完整元数据 cuetag 自动写入 TITLE / ALBUM / ARTIST / track / TRACKTOTAL / DATE / GENRE
CJK 支持 CUE 编码自动识别(utf-8/utf-8-sig/gbk/big5/shift_jis/cp936),中日韩音源都能处理
断点续跑 .split_cue_manifest.json 记录状态,任意时刻中断都能从上次位置继续
幂等 已完成的专辑再次运行秒跳过;新增专辑增量处理
自动排除已分轨 若目录里已有分轨文件(situation C),自动 SKIP 不误切
依赖自适应 启动时 probe 依赖,缺 mac 时把 APE 源标 UNSUPPORTED 而非崩溃

2. 系统要求

组件 版本 必需/可选 说明
Python 3.10+ 必需 只用标准库,无需 pip install
shntool 3.0+ 必需 提供 shnsplit 切割命令
cuetools 1.4+ 必需 提供 cuetag 元数据写入命令
flac 1.3+ 处理 FLAC 时必需 shntool 编码 FLAC 输出时调用
mac (Monkey's Audio) 4.x+ 处理 APE 时必需 shntool 解码 APE 输入时调用
ffprobe 6.0+ 可选 仅用于源文件元数据展示(时长、采样率),不影响切割

检查环境:

python3 --version
shnsplit -v | head -1
cuetag --help 2>&1 | head -1
flac --version | head -1
mac 2>&1 | head -1        # 无输出/命令不存在 = APE 源将标 UNSUPPORTED
ffprobe -version | head -1

安装依赖(Ubuntu / Debian / WSL):

sudo apt install shntool cuetools flac
# APE 支持(可选):Ubuntu 官方仓库通常没有,需要第三方 PPA 或源码编译
sudo apt install monkeys-audio    # 如果仓库有的话

macOS:

brew install shntool cuetools flac
# APE:需从 https://monkeysaudio.com 下载或用其他方式

Windows:直接跑不推荐(有 os.fsync(dir_fd) 的 POSIX 依赖),建议 WSL2。

依赖缺失时的行为:

启动时脚本会 probe 所有依赖并打印状态。缺 shnsplit 或 cuetag 直接退出(exit code 3)。缺 mac 或 flac 只影响对应格式,其他格式仍能处理。


3. 快速上手

最常用的三个命令:

# 1. 先分析看看哪些专辑需要切割 (不实际切)
python3 split_cue.py /mnt/z/我的音乐 --analyze-only

# 2. 确认没问题后正式切割 (中断可再跑, 自动 resume)
python3 split_cue.py /mnt/z/我的音乐

# 3. 切完了继续转 MP3 (transcode_music.py 会自动跳过整轨大文件)
python3 transcode_music.py /mnt/z/我的音乐 /mnt/x/mp3输出

就这么简单。整轨型专辑切成分轨后,transcode_music.py 会自动识别为"situation C"(CUE 引用整轨但存在分轨),跳过整轨文件只转分轨——两个脚本天然衔接。


4. 命令行参数详解

完整语法

split_cue.py [SOURCE] [选项...]

或

split_cue.py --source SOURCE [选项...]

参数列表

参数 短选项 类型 是否必需 默认 说明
SOURCE — 位置参数 必需¹ — 源目录,含整轨型专辑的位置
--source -s 字符串 必需¹ — 源目录(与位置参数 SOURCE 等价)
--analyze-only — 开关 可选 关 只分析生成 manifest,不实际切割
--force — 开关 可选 关 忽略现有 manifest,全部重新分析并覆盖已有分轨
--verbose -v 开关 可选 关 显示 DEBUG 级别日志(含跳过项)
--shnsplit — 字符串 可选 shnsplit shnsplit 可执行路径
--cuetag — 字符串 可选 cuetag cuetag 可执行路径
--ffprobe — 字符串 可选 ffprobe ffprobe 可执行路径
--split-timeout — 整数 可选 1800 单个专辑 shnsplit 超时秒数
--help -h 开关 — — 显示帮助并退出

¹ 源目录必须提供,可用位置参数或 flag 两种形式:

# 位置参数 (更简洁)
split_cue.py /mnt/z/music

# flag 长形式
split_cue.py --source /mnt/z/music

# flag 短形式
split_cue.py -s /mnt/z/music

注意:split_cue.py 不需要目标目录,因为它是原地切割——分轨文件直接写到原专辑目录里。这与 transcode_music.py 需要 -s / -t 两个参数不同。

参数详细说明

SOURCE / --source / -s

源目录。脚本会递归扫描此目录及所有子目录,找出每个"整轨 CUE"型专辑处理。

  • 支持含空格、中文、特殊字符的路径
  • 网盘挂载(/mnt/z/、/mnt/x/ 等)都可以
  • Manifest 文件 .split_cue_manifest.json 写入此目录

特殊约定:如果 SOURCE 本身就是一个专辑目录(而不是一堆专辑的父目录),也能正常工作。脚本会把源目录自身也作为分析候选。

--analyze-only

只分析,不切割。用途:

  • 首次跑一个新的大目录时,先看看总共有几个专辑需要切、切成几轨
  • 检查 CUE 解析是否有问题(编码、缺 FILE 引用等)
  • 生成 manifest 后可以手动查看/编辑

强烈推荐每次对新目录都先跑一次 --analyze-only。切割是原地写文件的操作,先看清楚计划再动手更安全。

--force

强制重跑。忽略现有 manifest,重新分析并覆盖已存在的分轨文件。

什么时候用:

  • 之前切错了想重来
  • CUE 修改过了,想重新按新 CUE 切
  • 想清理并重新开始

警告:--force 会覆盖已存在的同名分轨文件。如果之前有你手动编辑过的分轨,会丢失。不确定时先备份。

边界情况:如果专辑的 status 已经因为"situation C"被判为 skipped(因为目录里已有分轨),--force 也不会自动 un-skip。因为脚本无法可靠区分"我上次生成的分轨"和"用户已有的分轨"——安全起见不动。想强制重切,请先手动删掉旧的分轨文件再跑。

--verbose / -v

打开 DEBUG 级别日志。默认 INFO 级别只显示每首切割和错误。加上 -v 会额外显示:

  • [已完成 skip] ... 每个跳过的专辑
  • [SKIP (无需切割): ...] 每个 skipped 目录
  • [已存在, 保留] ... 每个已有分轨文件

大量输出,通常只在排查问题时用。

--shnsplit / --cuetag / --ffprobe

指定可执行文件路径。默认从 PATH 查找。

什么时候用:

  • 装了多个版本想指定用哪个
  • 二进制不在标准 PATH 里
  • 用自己编译的版本

示例:

split_cue.py /src --shnsplit /opt/shntool-3.0.10/bin/shnsplit

--split-timeout

单个专辑 shnsplit 命令的超时时间(秒),默认 1800(30 分钟)。超时算失败,写入 manifest 后继续下一个。

通常不需要调。除非你有超长专辑(比如一个 3 小时的整轨 audiobook)在慢网盘上,可以调大到 3600 或更多。

注意:shnsplit 是单次调用切完整个专辑的,不像 ffmpeg 是逐轨调用,所以超时值应该覆盖整个专辑的切割时间。


5. 常用使用场景

场景 1:第一次处理新目录(推荐流程)

假设你刚下载了一堆 [FLAC+CUE] 到 /mnt/z/新收藏/。

第一步:先分析

python3 split_cue.py /mnt/z/新收藏 --analyze-only

输出会告诉你:

  • 总共扫了几个目录
  • 有几个专辑属于"整轨型"需要切割(analyzed)
  • 有几个源格式不支持(unsupported)
  • 有几个已经是分轨的(skipped)

第二步:查看切割计划

python3 -c "
import json
d = json.load(open('/mnt/z/新收藏/.split_cue_manifest.json'))
for rel, a in d['albums'].items():
    if a['status'] == 'analyzed':
        print(f'{rel}:  {a[\"source_audio\"]} → {len(a[\"tracks\"])} 轨')
        for t in a['tracks'][:3]:
            print(f'    · {t[\"target_name\"]}')
        if len(a['tracks']) > 3:
            print(f'    ... 及另外 {len(a[\"tracks\"]) - 3} 轨')
"

第三步:确认无误后正式切割

python3 split_cue.py /mnt/z/新收藏

已经 analyzed 的专辑会开始切割,skipped / unsupported 的自动跳过。

第四步:接着跑 transcode_music.py 转 MP3

python3 transcode_music.py /mnt/z/新收藏 /mnt/x/music/新收藏

transcode_music.py 会自动识别切出来的分轨(situation C),跳过整轨大文件只转分轨。

场景 2:中断后恢复

切到一半按 Ctrl+C 中断(或断电、脚本崩溃)。只需要再跑同样的命令:

python3 split_cue.py /mnt/z/新收藏

脚本会读取 manifest,跳过所有 completed 状态的专辑,从上次中断的地方继续。不需要任何特殊参数。

场景 3:增量同步(新增了整轨专辑)

以后又下载了几个 [FLAC+CUE] 到 /mnt/z/新收藏/:

python3 split_cue.py /mnt/z/新收藏

已完成的秒跳过,只有新目录会被分析和切割。最舒服的用法。

场景 4:修复失败的专辑

如果某个专辑因为 CUE 解析错误、shnsplit 崩溃等失败了(status: failed),修好后:

# 直接再跑, failed 的会被自动重试
python3 split_cue.py /mnt/z/新收藏

已成功的不动,只重试失败的。

场景 5:只切一个专辑

比如只想切某一张:

python3 split_cue.py "/mnt/z/新收藏/王菲 - 唱游 [FLAC+CUE]"

Manifest 会写到那个专辑目录里,其他专辑不受影响。

场景 6:想重新切某个已完成的专辑

比如切完发现 CUE 元数据有错,改了 CUE 想重切:

方法一:删掉分轨文件 + 编辑 manifest

删除该专辑目录里的分轨文件(01 - Title.flac 等),然后编辑 .split_cue_manifest.json,把该专辑的 status 改成 analyzed、每个 track 的 status 改成 pending。再跑一次即可。

方法二:全局 --force

python3 split_cue.py /mnt/z/新收藏 --force

会重新分析所有专辑并覆盖分轨。但注意:如果目录里已有旧分轨且脚本无法区分,可能会被判为 situation C 而 skip。最保险的做法还是先手动删旧分轨。

场景 7:排查某个专辑的问题

某个专辑 status=failed,用 verbose 模式跑:

python3 split_cue.py "/mnt/z/新收藏/有问题的专辑" -v

输出会显示 shnsplit 的完整错误、cuetag 警告等。

场景 8:切完不满意,回滚

因为原文件从不被删改,回滚很简单:

# 删掉所有分轨文件, 只留原整轨 + CUE
cd "/mnt/z/新收藏/王菲 - 唱游"
rm -f *.flac    # 删所有 flac
# 找回原整轨 (脚本没删过它, 只是你上面 rm 一起删了; 如果只想删分轨:)
# 或更精确: 只删符合 "NN - Title.flac" 命名的

更精确的清理:

# 只删 "01 - ...", "02 - ..." 这种格式的文件
find "/mnt/z/新收藏/王菲 - 唱游" -maxdepth 1 -regex '.*/[0-9]\{2\} - .*\.\(flac\|wav\)$' -delete

# 编辑 manifest 把该专辑 status 改回 analyzed

6. Manifest 文件说明

位置

Manifest 保存在源目录下,文件名 .split_cue_manifest.json(隐藏文件)。

顶层结构

{
  "manifest_version": 1,
  "created_at": "2026-09-06T13:41:03+08:00",
  "updated_at": "2026-09-06T13:41:04+08:00",
  "source_root": "/mnt/z/新收藏",
  "stats": {
    "total_albums": 4,
    "by_album_status": {"completed": 1, "skipped": 3},
    "by_track_status": {"completed": 3},
    "total_tracks": 3
  },
  "albums": {
    "专辑相对路径1": { ... },
    "专辑相对路径2": { ... }
  }
}

单个专辑 (album) 结构

{
  "status": "completed",
  "source_audio": "CDImage.flac",
  "cue": "CDImage.cue",
  "album_title": "唱游",
  "album_performer": "王菲",
  "album_date": "1998",
  "album_genre": "Chinese Pop",
  "source_codec": "flac",
  "source_sample_rate": 44100,
  "source_channels": 2,
  "duration_sec": 40.0,
  "output_format": "flac",
  "tracks": [
    {
      "num": 1,
      "shnsplit_index": 1,
      "title": "红豆",
      "performer": "王菲",
      "target_name": "01 - 红豆.flac",
      "total_tracks": 3,
      "status": "completed",
      "error": null,
      "converted_at": "2026-09-06T13:41:04+08:00"
    },
    { "num": 2, "shnsplit_index": 2, ... },
    { "num": 3, "shnsplit_index": 3, ... }
  ],
  "issues": [],
  "started_at": "2026-09-06T13:41:03+08:00",
  "completed_at": "2026-09-06T13:41:04+08:00"
}

字段含义:

字段 含义
status 专辑级状态,见下表
source_audio 检测到的整轨大文件名(相对路径)
cue 匹配的 CUE 文件名
album_title / album_performer 从 CUE 的顶级 TITLE / PERFORMER 解析
album_date / album_genre 从 CUE 的 REM DATE / REM GENRE 解析
source_codec / source_sample_rate / source_channels / duration_sec 由 ffprobe 探测(仅用于展示)
output_format shnsplit -o 参数(flac 或 wav)
tracks[] 切割计划,每首歌一项
tracks[].num CUE 里声明的 TRACK 编号
tracks[].shnsplit_index shnsplit 内部计数器(1-based,用于对应 tmp 文件名)
tracks[].title / tracks[].performer 从 CUE 的 TRACK 级 TITLE / PERFORMER 解析
tracks[].target_name 最终文件名(已 sanitize)
tracks[].status 分轨级状态
issues[] 人类可读的警告/说明

状态值 (status)

专辑级状态:

status 说明 下次运行时行为
pending 初始状态(一般用不到) 重新分析
analyzed 已识别为整轨 CUE 型,切割计划就绪 执行切割
processing 正在切割(若看到,说明上次中断了) 重新切割整个专辑
completed 所有分轨都成功输出 秒跳过
failed 切割失败(shnsplit / cuetag / 重命名任一失败) 重试整个专辑
skipped 无需切割(无 CUE / 已分轨 situation C / CUE 引用多文件 / 引用不存在) 跳过
unsupported 源格式当前依赖无法处理(如 APE 但 mac 未装、DSF) 跳过

分轨级状态(tracks[] 里):

status 说明
pending 待处理
completed 已完成
failed 失败(error 字段有原因)

手动干预 manifest

Manifest 是纯 JSON,可以用文本编辑器直接改。

例子 1:让某个专辑重新切割

"status": "completed"  →  "status": "analyzed"

同时把该专辑内所有 track 的 status 从 completed 改成 pending,删掉现有分轨文件,再跑脚本。

例子 2:跳过某个有问题的专辑

"status": "analyzed"  →  "status": "skipped"

例子 3:删除某个专辑的记录重新扫描

删除对应的 album 条目,下次运行会自动重新扫描添加。

改完保存直接跑:

python3 split_cue.py /mnt/z/新收藏

Manifest 损坏怎么办?

如果 JSON 损坏(编辑错、盘故障),脚本启动时会自动检测,把坏的 manifest 重命名为 .split_cue_manifest.json.corrupt-<时间戳>,然后重新生成新的 manifest。

已切出来的分轨文件不会丢,但脚本会重新扫描分析。因为已切分的目录会呈现为 situation C(有分轨 + 有整轨),会自动 SKIP,不会重复切割。幂等性保护。


7. 支持的源格式与依赖矩阵

支持矩阵

源格式 输出格式 依赖 备注
.flac .flac shntool + flac 无损 re-encode,sample-exact
.wav .wav shntool 原生 无外部依赖,PCM 样本直切
.ape .flac shntool + mac + flac mac 解码 APE,flac 编码输出(ffmpeg 无 APE 编码器)
.dsf / .dff — 无 不支持 (DSD)。请先手动转 FLAC 再切

依赖启动时 probe

启动时会打印所有依赖状态:

未找到 mac (APE 解码器) → APE 源将标记 UNSUPPORTED. 安装可选: apt install monkeys-audio
支持源格式: ['.flac', '.wav']

缺 shnsplit 或 cuetag:直接退出(exit 3)。这是硬依赖。

缺 flac:FLAC 源会被标为 UNSUPPORTED。只有 WAV 源能处理。

缺 mac:APE 源会被标为 UNSUPPORTED,其他格式不受影响。

缺 ffprobe:警告但不影响切割,只是 manifest 里 source_codec / duration_sec 等字段是空的。

为什么不支持 DSD?

shntool 本身不支持 DSD 格式。理论上可以用 ffmpeg 走另一条路,但:

  1. DSD 打包成整轨很少见(DSD 音源通常出货就是分轨的)
  2. DSD 解码涉及低通滤波、降采样等,输出到 FLAC 会显著改变数据
  3. 处理逻辑复杂度不值得

如果你确实有 DSD 整轨 + CUE,建议先手动用 ffmpeg 转成 FLAC,再跑 split_cue.py:

ffmpeg -i album.dsf -c:a flac -ar 88200 album.flac
# 然后修改 CUE 里的 FILE 指向 album.flac
split_cue.py /path/to/album

8. CUE 检测与匹配逻辑

什么样的目录会被识别为"整轨 CUE 型"?

严格判定条件(全部满足才算):

  1. 目录里至少有 1 个 CUE 文件
  2. 目录里至少有 1 个无损音频文件(FLAC/WAV/APE/DSF/DFF)
  3. 某个 CUE 里 FILE "xxx" WAVE 条目恰好 1 个
  4. 这个 FILE 引用能匹配到目录里的音频文件
  5. 目录里没有其他无损音频文件(除了这个被引用的)
  6. CUE 里有 TRACK 条目(不能是空 CUE)

只要有一项不满足,就不会被切割:

情况 判定 原因
无 CUE skipped 没有切割依据
无无损音频 skipped 没东西可切
CUE 无 FILE 条目 skipped + issues 记录 CUE 无效
CUE 有多个 FILE (>1) skipped 已经是分轨 CUE 索引
CUE 引用的文件不存在 skipped + issues 记录 可能是错配的 CUE
CUE 引用整轨但同时存在分轨 skipped (situation C) 已切过或用户手动分轨了
匹配成功但源格式依赖缺失 unsupported 例如 APE 但 mac 未装

CUE FILE 引用的匹配策略

CUE 里的 FILE "album.wav" WAVE 未必和实际文件名完全一致。脚本用三级 fallback:

  1. 精确匹配:文件名完全一致
  2. 不区分大小写:ALBUM.FLAC 匹配 album.flac
  3. 忽略扩展名:FILE "album.wav" WAVE 匹配到 album.flac(假设目录里只有一个 album.* 无损文件)

三级都不匹配才判为"引用不存在"。

CUE 编码识别

CUE 文件常见的编码:

  • utf-8-sig(有 BOM 的 UTF-8)
  • utf-8
  • gbk(简体中文)
  • big5(繁体中文)
  • shift_jis(日文)
  • cp936(GBK 别名)
  • latin1(最后兜底)

脚本按顺序尝试解码,第一个成功的即采用。中日韩音源基本都能识别。

CUE 里 TITLE / PERFORMER 的语义

CUE 里 TITLE "..." 和 PERFORMER "..." 可能出现在两个位置:

  • 顶级(在任何 TRACK 之前):整张专辑的标题和艺人
  • TRACK 块内:这一首歌的标题和艺人

脚本区分这两种上下文,分别写入 album_title / album_performer 和每首歌的 title / performer。

REM 元数据

CUE 里 REM DATE "1998" 和 REM GENRE "Chinese Pop" 在顶级出现时,会被解析为专辑的年份和流派,写入 manifest 并通过 cuetag 传给分轨的 tag。

CUE 与 transcode_music.py 的区别

transcode_music.py 也有 CUE 检测逻辑(在其文档第 8 节详述了三种情况 A/B/C)。它的目的是报警"这个专辑需要 CUE split",而 split_cue.py 是执行这个 split。

两者对 CUE 的看法一致:

情况 transcode_music.py 视角 split_cue.py 视角
A: CUE + 整轨大文件 needs_cue_split 报警跳过 analyzed,执行切割
B: CUE + 分轨 正常转码 skipped
C: CUE 引用整轨 + 分轨都在 跳过整轨只转分轨 skipped

这就是为什么它们能天然衔接:split_cue.py 把 A 转成 C,然后 transcode_music.py 处理 C。


9. 分轨命名与元数据

文件命名规则

分轨文件名格式:NN - Title.ext

  • NN:CUE 里 TRACK 声明的编号,零填充到至少 2 位(如果总轨数 ≥ 100 会用 3 位)
  • Title:CUE 里 TRACK 块内的 TITLE,经过文件名净化
  • ext:由源格式决定的输出扩展名(见第 7 节表格)

Title 净化规则:

  1. 替换文件系统非法字符 <>:"/\|?* 以及 ASCII 控制字符(\x00-\x1f)为 _
  2. 折叠连续空白为单个空格
  3. 去掉结尾的点号和空格(Windows 要求)
  4. 截断到最多 200 字符(避免文件系统限制)
  5. 净化后为空则用 untitled 兜底

示例:

CUE 里的 TITLE 输出文件名(假设 num=2)
红豆 02 - 红豆.flac
催眠 / 静夜 02 - 催眠 _ 静夜.flac(/ → _)
"Track: A" 02 - _Track_ A_.flac(" 和 : → _)
(无 TITLE) 02 - Track 02.flac(兜底)

注意:文件名里的非法字符被替换,但元数据 tag(写入文件内部)保留 CUE 里的原样。所以 催眠 / 静夜.flac 里的 TITLE tag 仍然是 催眠 / 静夜(含 /),播放器显示正常。

命名冲突处理

极少数情况下多首歌的净化后名字撞车(比如两首歌都叫 未命名 而且 num 不同但被截断)。脚本会在冲突时追加 (2)、(3):

  • 05 - 未命名.flac
  • 06 - 未命名 (2).flac

元数据 (Tag) 映射

由 cuetag 自动写入。对 FLAC 输出,写的是 Vorbis Comment tags:

Tag 来源
TITLE CUE 里 TRACK 块内的 TITLE
ARTIST / PERFORMER CUE 里 TRACK 块内的 PERFORMER(若无则用专辑 PERFORMER)
ALBUM CUE 里顶级 TITLE
track CUE 里 TRACK 编号(补零至 2 位,如 01)
TRACKTOTAL CUE 里总 TRACK 数
DATE CUE 里顶级 REM DATE
GENRE CUE 里顶级 REM GENRE

注意:cuetag 的行为是按位置对应:cuetag album.cue file1.flac file2.flac file3.flac 会把 CUE 的 TRACK 1 写到 file1,TRACK 2 写到 file2,以此类推。脚本按 shnsplit_index 严格排序传参,保证对应正确。

cuetag 失败的后果

如果 cuetag 因某种原因失败(罕见),脚本会警告但不算专辑失败——分轨音频文件本身是完整的,只是没有元数据 tag。可以事后手动跑一次:

cd "专辑目录"
cuetag *.cue 01*.flac 02*.flac 03*.flac    # 按顺序传

10. 与 transcode_music.py 的组合工作流

典型完整流程

# ┌─ 前期整理 (split_cue.py)
python3 split_cue.py /mnt/z/新收藏 --analyze-only
python3 split_cue.py /mnt/z/新收藏

# └─ 转码到便携格式 (transcode_music.py)
python3 transcode_music.py /mnt/z/新收藏 /mnt/x/mp3输出 --analyze-only
python3 transcode_music.py /mnt/z/新收藏 /mnt/x/mp3输出

两个脚本对同一个专辑的看法

举例:王菲 - 唱游 [FLAC+CUE]/ 目录里有 CDImage.flac + CDImage.cue(12 轨)。

只跑 transcode_music.py:

[!] 1 个专辑需先手动 CUE split (已跳过转码):
    • 王菲 - 唱游 [FLAC+CUE]
        CDImage.cue: 整轨型 (单文件 'CDImage.flac', 12 轨) — 需先手动 CUE split 再转码

先跑 split_cue.py 再跑 transcode_music.py:

[split_cue.py]
[1/1] 切割: 王菲 - 唱游 [FLAC+CUE]
        源: CDImage.flac  →  12 轨 (flac)
    ✓ [01/12] 01 - 红豆.flac
    ✓ [02/12] 02 - 催眠.flac
    ...

[transcode_music.py]
    CDImage.cue: 引用 'CDImage.flac' 但存在 12 个分轨 → 跳过 'CDImage.flac' 只转分轨
    转码: 01 - 红豆.flac  (flac, 44100Hz)
    转码: 02 - 催眠.flac  (flac, 44100Hz)
    ...

transcode_music.py 自动检测到 situation C,只转 12 个分轨、不动整轨,产生 12 个 MP3。

Manifest 隔离

两个脚本的 manifest 文件名不同:

  • split_cue.py → .split_cue_manifest.json
  • transcode_music.py → .transcode_manifest.json

互不干扰。可以随时来回跑。

输出结构对比

运行前:

/mnt/z/新收藏/
└── 王菲 - 唱游 [FLAC+CUE]/
    ├── CDImage.flac       (整张 CD 一个大文件)
    └── CDImage.cue

运行 split_cue.py 后:

/mnt/z/新收藏/                              ← 源目录, 分轨写这里
├── .split_cue_manifest.json                ← split_cue 状态
└── 王菲 - 唱游 [FLAC+CUE]/
    ├── CDImage.flac                         ← 原文件保留
    ├── CDImage.cue                          ← 保留
    ├── 01 - 红豆.flac                       ← 新: split_cue 输出
    ├── 02 - 催眠.flac
    └── ... (12 个分轨)

接着运行 transcode_music.py 后:

/mnt/z/新收藏/
├── .split_cue_manifest.json
├── .transcode_manifest.json                ← transcode_music 状态
└── 王菲 - 唱游 [FLAC+CUE]/                 (未变)

/mnt/x/mp3输出/                              ← 目标目录
└── 王菲 - 唱游 [FLAC+CUE]/
    ├── CDImage.cue                          ← CUE 也复制过去
    ├── 01 - 红豆.mp3                        ← 320k CBR
    ├── 02 - 催眠.mp3
    └── ... (12 个 MP3)

注意:CDImage.flac 不会转成 MP3(因为 situation C 检测跳过);CDImage.cue 会被复制到目标目录(作为资源)。

关于 CUE 的一个细节

切完后,CDImage.cue 里的 FILE "CDImage.flac" WAVE 仍指向原整轨。如果用户在目标目录里想用 CUE 播放分轨 MP3,这个 CUE 是不正确的——它指向整轨、但目标目录里没有那个整轨。

如果你实际会用 CUE 索引,考虑:

  1. 手动编辑复制到目标的 CUE,把 FILE 指向 MP3(但 CUE 传统上不支持多 FILE 分轨映射,改起来麻烦)
  2. 直接删除目标目录里的 CUE(分轨 MP3 有 tag,播放器不需要 CUE 也能显示正确信息)

推荐做法:直接删。分轨 MP3 的 ID3 tag 已经包含所有 CUE 元数据。


11. 错误处理与恢复

错误捕获原则

单个专辑失败不影响其他。shnsplit / cuetag / 重命名任一失败时:

  1. 该专辑内所有未完成的 track 状态设为 failed,错误消息写入 error 字段
  2. 该专辑整体状态设为 failed
  3. issues[] 追加人类可读的说明
  4. 继续处理下一个专辑
  5. 最后在报告里列出所有失败项

再次运行时

failed 状态的专辑会自动重试(整个专辑重切,因为 shnsplit 是全轨一次调用的原子操作,无法只重切失败的分轨)。

# 修好源文件后, 直接再跑
python3 split_cue.py /mnt/z/新收藏

常见错误

"shnsplit 失败: unknown format"

CUE 里 FILE "xxx" TYPE 的 TYPE 值 shntool 不认。常见于 FLAC / WAVE / MP3 / BINARY,其他值可能有问题。检查 CUE 内容并手动修正。

"shnsplit 失败: cannot open file"

CUE 的 FILE 引用文件名和实际文件名不一致(尽管脚本会 fallback 匹配到实际文件,但 shnsplit 用的是 CUE 里的原字符串)。可能因为脚本 fallback 匹配到了但 shntool 找不到。手动把 CUE 里的 FILE 名改成实际文件名,再跑。

"cuetag 失败, 音频仍可用但可能无标签"

cuetag 有时对某些 CUE 或 FLAC 版本行为异常。不影响切割本身,可事后手动跑 cuetag 或用别的 tagger(Picard、mp3tag 等)补 tag。

"预期临时文件缺失/过小"

shnsplit 返回 0 但实际没写全所有分轨。极罕见,通常是磁盘满或权限问题。检查目标目录空间和权限。

"临时目录创建失败"

无法在专辑目录里创建 .split_cue_tmp_<pid>。通常是目录只读或磁盘满。

"源文件消失"

分析阶段之后、切割阶段之前源文件被删/移动。删除对应专辑 manifest 条目重新扫描即可。

中断(Ctrl+C)

脚本捕获 SIGINT,保存 manifest 后退出。中断时临时目录 .split_cue_tmp_<pid> 里的 shnsplit 中间产物会保留(因为 finally 清理只在 split_album 函数正常返回时执行)——下次跑会重建。中断专辑的状态可能停留在 processing,重跑时会被视为需要重切。

如果发现有遗留的 .split_cue_tmp_* 目录(比如脚本被 kill -9 强杀),可以手动清理:

find /mnt/z/新收藏 -maxdepth 2 -type d -name '.split_cue_tmp_*' -exec rm -rf {} +

查看失败列表

# 用 jq 快速查看失败的专辑
jq -r '.albums | to_entries[] | select(.value.status=="failed") | .key' \
    /mnt/z/新收藏/.split_cue_manifest.json

# 查看某专辑的具体失败原因
jq '.albums["专辑名"] | {status, issues, tracks: [.tracks[] | select(.status=="failed") | {num, error}]}' \
    /mnt/z/新收藏/.split_cue_manifest.json

关于 unsupported

unsupported 状态的专辑不算失败,是主动跳过。原因写在 issues[] 里,例如:

  • 缺少依赖: mac (APE 解码器) → 装 mac 后再跑,会自动切
  • shntool 不支持 DSD, 请先手动转 FLAC → 手动预处理

装好依赖后,需要用 --force 或删掉对应 manifest 条目才会重新分析。


12. 已知局限

  • 不支持 DSD(.dsf / .dff):shntool 不识别 DSD。若有整轨 DSD + CUE,需先手动 ffmpeg 转 FLAC 再切。
  • shnsplit 是一次调用切完整个专辑:无法只重切失败的某一首歌。失败时整个专辑重切(幂等,无副作用)。
  • 不做 replaygain / 章节标记 / 高级 tag:只做 cuetag 支持的标准 tag(TITLE/ARTIST/ALBUM/DATE/GENRE/track/TRACKTOTAL)。
  • 不支持并行切割:为保证 manifest 状态一致性,专辑串行处理。可对不同子目录同时跑多个脚本实例。
  • CUE 里 track 编号必须连续从 1 开始:极少数非标准 CUE(比如从 TRACK 03 开始)会导致 shnsplit 内部计数器和 CUE 编号错位。目前未特殊处理,可能造成 tag 错乱。
  • 网盘 mtime 不能保留:分轨是新建文件,mtime 是当下时间。原文件不动。
  • --force 不会自动清理旧分轨:如果目录里已有旧分轨且脚本判为 situation C 而 skip,--force 也不会强制切。需手动删旧分轨。
  • shnsplit 依赖 flac 二进制来编码 FLAC:这是 shntool 3.x 的架构选择(它调用外部 encoder)。装了 flac 才能切 FLAC 源;装了 mac 才能切 APE 源。
  • APE 输出为 FLAC,不能原格式:ffmpeg 和 shntool 都没 APE 编码器(APE 是私有格式)。所以 APE → FLAC 是唯一路径。
  • Windows 直接跑未测试:脚本用了 POSIX os.fsync(dir_fd),Windows 上会 best-effort。理论可用,实际推荐 Linux / WSL2 / macOS。

13. FAQ

Q: 为什么要单独一个 split_cue.py?直接让 transcode_music.py 顺便切了不好吗?

A: 分离关注点。切割是修改源目录的操作(写新文件),转码是读源目录写目标目录的操作。两者错误影响面不同:切错了改源,转错了改目标。分开可以:

  1. 先充分 review 切割计划再动手(--analyze-only)
  2. 切割失败不牵连转码,反之亦然
  3. 用户可以只切不转(本地播放器直接消费分轨 FLAC)
  4. 用户可以直接手动切好再让 transcode_music 转(跳过 split_cue)

Q: 切完 CUE 文件删不删?

A: 保留。原文件(CDImage.flac)也保留。用户想删的话手动来,脚本不做破坏性操作。删原文件前建议:

  1. 先播放几首分轨确认没问题
  2. 用 metaflac -l 01*.flac 确认 tag 完整
  3. 备份 CUE(换新 CUE 想重切时有用)

Q: 切割精度和音质怎么样?

A: shntool 是按 CD 帧(1/75 秒)对齐切的,与原始 CUE INDEX 完全一致。对 FLAC → FLAC,音频数据无损(decode + re-encode 是数学等价的 PCM 转换)。对 WAV → WAV 是纯样本操作,字节级精确。对 APE → FLAC,音频数据也无损(FLAC 压缩是无损的),只是容器格式变了。

Q: 分轨没有 gapless 播放会不会有咔嗒声?

A: 用了 shntool 的默认 --append-gaps 模式,pregap 追加到上一轨结尾。相邻曲目之间的过渡应该 sample-exact 无缝,播放器如支持 gapless 就无咔嗒声。

Q: 切完后音频总长度对不上原文件?

A: 应该是精确对上的(每首歌的 duration 加起来 = 原文件 duration)。如果偏差 < 1 秒,可能是取整误差(ffprobe 显示的 duration 有精度限制)。偏差大就是有 bug,请提 issue。

Q: 中文 / 日文 / 韩文的 CUE 处理有问题吗?

A: 应该没问题。脚本会按 utf-8 / gbk / big5 / shift_jis / cp936 / latin1 顺序尝试解码,绝大部分 CJK CUE 都能识别。切割输出的 tag(Vorbis Comment)总是 UTF-8。如果发现某个 CUE 解码错乱,可以用别的工具(比如 iconv 或文本编辑器)先把 CUE 转成 UTF-8 再跑脚本。

Q: cuetag 输出 warning 说什么 metaflac 不 exist?

A: 有些 cuetools 版本的 cuetag 是 Python 或 shell 脚本,内部调用 metaflac(来自 flac 包)。装了 flac 就有 metaflac。如果 cuetag 抱怨 metaflac 不存在,装 flac 包即可(sudo apt install flac)。

Q: 我的 CUE 有奇怪的编码/结构 shntool 处理不了怎么办?

A: 先用文本编辑器把 CUE 转成 UTF-8 无 BOM,去掉不必要的 REM 行,看 TRACK / FILE / INDEX 结构是否规范(每个 TRACK 至少要有 INDEX 01 MM:SS:FF)。规范化后重试。

Q: 能不能自定义分轨文件名格式?比如 "%p - %t.flac"?

A: 目前不能。命名格式固定为 NN - Title.ext。这是刻意的:

  1. 保证目录内文件按 track 顺序排列
  2. 简化文件名,避免复杂替换的 bug
  3. 元数据 tag 里已经有完整信息(艺人、专辑等),不需要塞进文件名

Q: 支持 track 里带 gap(HTOA / hidden track)吗?

A: 部分支持。shnsplit 默认 --append-gaps 会把 gap 归到前一轨末尾。TRACK 01 之前的 pregap(HTOA)目前算作 TRACK 01 的开头。如果你需要独立提取 HTOA,得手动调 shnsplit 参数(脚本目前不暴露此选项)。

Q: 切错了想撤回?

A: 因为原文件从不被删改,撤回很简单:找出符合 NN - Title.ext 格式的新生成分轨全部删除即可。示例见场景 8。

Q: 支持嵌套目录(比如多碟 CD1/CD2/)吗?

A: 支持。脚本会 os.walk 全递归,每一层子目录独立分析。多碟专辑的每个 CDx 目录都会被视为一个独立的专辑单元。

Q: 我可以在 Docker 里跑吗?

A: 可以。写个简单 Dockerfile:

FROM debian:stable-slim
RUN apt-get update && apt-get install -y \
    python3 shntool cuetools flac ffmpeg \
 && rm -rf /var/lib/apt/lists/*
COPY split_cue.py /usr/local/bin/
ENTRYPOINT ["python3", "/usr/local/bin/split_cue.py"]

然后:

docker run --rm -v /mnt/z/music:/music my-split-cue /music --analyze-only

Q: 我想只切某一个格式(比如只 FLAC 不 APE)怎么办?

A: 目前没提供 filter 参数。可以用 shell 循环:

find /mnt/z/新收藏 -type d | while read d; do
    if ls "$d"/*.flac >/dev/null 2>&1; then
        python3 split_cue.py "$d"
    fi
done

Q: 生成的 manifest 会很大吗?

A: 一般不大。50 个专辑的 manifest 约 30 KB。上千个专辑估计几 MB。不影响使用。

Q: transcode_music.py 和 split_cue.py 能同时跑吗?

A: 不建议。两者都会写源目录(split_cue 写分轨、transcode_music 写自己的 manifest)。虽然写的是不同文件不会冲突,但 transcode_music 分析时看到 split_cue 正在切的中间状态可能判断错误。串行跑更安全:split_cue 先跑完,transcode_music 再跑。


附录 A:完整命令示例集

# 分析 (不切割)
python3 split_cue.py /mnt/z/music --analyze-only

# 正式切割 (自动 resume)
python3 split_cue.py /mnt/z/music

# 详细日志
python3 split_cue.py /mnt/z/music -v

# 强制重跑 (覆盖已有分轨)
python3 split_cue.py /mnt/z/music --force

# 使用长参数
python3 split_cue.py --source /mnt/z/music

# 使用短参数
python3 split_cue.py -s /mnt/z/music

# 组合: 位置 + 选项
python3 split_cue.py /mnt/z/music -v --force

# 指定 shnsplit / cuetag 路径
python3 split_cue.py /mnt/z/music \
    --shnsplit /opt/shntool-3/bin/shnsplit \
    --cuetag /opt/cuetools/bin/cuetag

# 加大超时 (超长专辑)
python3 split_cue.py /mnt/z/music --split-timeout 3600

# 只处理某一个专辑
python3 split_cue.py "/mnt/z/music/王菲 - 唱游 [FLAC+CUE]"

# 完整工作流: split 然后 transcode
python3 split_cue.py /mnt/z/music && \
python3 transcode_music.py /mnt/z/music /mnt/x/mp3

附录 B:文件结构示例

运行前(典型的整轨型音乐盘):

/mnt/z/新收藏/
├── 王菲 - 唱游 [FLAC+CUE]/
│   ├── CDImage.flac                       ← 整张 CD 一个大文件, 40 分钟
│   └── CDImage.cue                        ← 12 个 TRACK
├── 蔡琴 - 老歌 [FLAC+CUE]/
│   ├── album.flac                         ← 整张 CD 一个大文件
│   ├── album.cue
│   ├── cover.jpg
│   └── booklet.pdf
├── 张学友精选 [已分轨]/                    ← 已经是分轨的, 会 SKIP
│   ├── 01 吻别.flac
│   ├── 02 一路上有你.flac
│   ├── 03 情网.flac
│   └── (无 CUE)
└── Beatles - White Album [WAV+CUE]/       ← WAV 源, 会切成 WAV
    ├── disc1.wav
    └── disc1.cue

运行 split_cue.py /mnt/z/新收藏 后:

/mnt/z/新收藏/
├── .split_cue_manifest.json                ← 状态记录
├── 王菲 - 唱游 [FLAC+CUE]/
│   ├── CDImage.flac                        ← 保留不动
│   ├── CDImage.cue                         ← 保留
│   ├── 01 - 红豆.flac                      ← 新: 分轨输出 (含 tag)
│   ├── 02 - 催眠.flac
│   ├── 03 - 无常.flac
│   ├── 04 - 童 (国语).flac
│   ├── ... (共 12 个)
│   └── 12 - 精彩.flac
├── 蔡琴 - 老歌 [FLAC+CUE]/
│   ├── album.flac                          ← 保留
│   ├── album.cue                           ← 保留
│   ├── cover.jpg                           ← 保留
│   ├── booklet.pdf                         ← 保留
│   ├── 01 - 恰似你的温柔.flac              ← 新: 分轨
│   ├── 02 - 你的眼神.flac
│   └── ... (12 个)
├── 张学友精选 [已分轨]/                    ← 无变化 (SKIP)
│   ├── 01 吻别.flac
│   ├── 02 一路上有你.flac
│   └── 03 情网.flac
└── Beatles - White Album [WAV+CUE]/
    ├── disc1.wav                           ← 保留 WAV 整轨
    ├── disc1.cue                           ← 保留
    ├── 01 - Back In The U.S.S.R..wav       ← 新: WAV 分轨
    ├── 02 - Dear Prudence.wav
    └── ... (30 个)

接着运行 transcode_music.py /mnt/z/新收藏 /mnt/x/mp3:

/mnt/z/新收藏/                              ← 未变
├── .split_cue_manifest.json
├── .transcode_manifest.json                ← 新: transcode 状态
└── ... (专辑目录未变)

/mnt/x/mp3/                                  ← 目标目录
├── 王菲 - 唱游 [FLAC+CUE]/
│   ├── CDImage.cue                          ← CUE 复制过来
│   ├── 01 - 红豆.mp3                        ← 320k CBR
│   ├── 02 - 催眠.mp3
│   └── ... (12 个 MP3)
├── 蔡琴 - 老歌 [FLAC+CUE]/
│   ├── album.cue
│   ├── cover.jpg
│   ├── booklet.pdf
│   ├── 01 - 恰似你的温柔.mp3
│   └── ... 
├── 张学友精选 [已分轨]/
│   ├── 01 吻别.mp3
│   ├── 02 一路上有你.mp3
│   └── 03 情网.mp3
└── Beatles - White Album [WAV+CUE]/
    ├── disc1.cue
    ├── 01 - Back In The U.S.S.R..mp3
    └── ... (30 个 MP3)

注意:

  • 整轨大文件(CDImage.flac、album.flac、disc1.wav)不会被转成 MP3,因为 transcode_music.py 检测到 situation C 会跳过它们
  • 所有 CUE 文件会被复制到目标目录(作为资源文件)
  • 分轨 MP3 保留完整 ID3 tag(继承自分轨 FLAC 的 Vorbis Comment)

Manifest 位置:

/mnt/z/新收藏/.split_cue_manifest.json        ← split_cue 的状态
/mnt/z/新收藏/.transcode_manifest.json        ← transcode_music 的状态

两个 manifest 都在源目录里,文件名不同,互不冲突。