简介:这份中文乐谱数据集面向歌声合成、深度学习与音乐信息处理方向的研究人员和开发者,提供数百首以XML格式编码的中文乐谱,可用于序列建模、旋律生成与歌声合成模型的训练与验证。压缩包共172个文件,全部为xml乐谱文件,整体约1.05MB,每份文件记录音符、节奏、节拍、音高与力度等结构化信息,便于程序解析与批量处理。已有550人学习下载,适合作为RNN、Transformer或VAE等模型的输入素材,也可用于音乐信息检索与情感分析等任务。借助这些乐谱,读者能够搭建乐谱到歌声的转换流程,探索个性化歌声合成与智能音乐创作,并对照XML结构理解歌曲的旋律与和声组织方式,为学术研究与工程实践提供可直接使用的数据基础。
1. 中文乐谱数据集:几百首 XML 乐谱,歌声合成到底能不能直接喂
前阵子有个做 AI 翻唱的朋友找我,说他手头攒了一堆干声,想训一个歌声合成模型,结果卡在数据准备上——MIDI 音高对不齐、歌词和音符对不上、网上下载的谱子格式五花八门。我把自己之前整理的一份中文乐谱数据集丢给他,几百首,统一 XML 格式,他当天下午就跑通了第一版音高对齐脚本。这份资源解决的就是这个问题:把「找谱、转谱、对齐」这三件脏活压缩成一个可以直接读的 XML 集合,让你把精力放在声学模型和声码器上,而不是在乐谱解析上反复翻车。
它适合三类人:一是做歌声合成(SVS)或翻唱变声的算法同学,需要批量拿到音符序列、时值、歌词三元组;二是做音乐信息检索(MIR)的,想拿中文旋律做训练或评测;三是单纯想研究 XML 乐谱结构、写解析器的工程师。数据集本身是 XML 格式,意味着它天然可被程序读取,不需要你先去啃扫描版五线谱。下面我从「这份资源里到底有什么」讲到「怎么把它变成模型能吃的张量」,中间该踩的坑一个不落。
2. 拆开 XML 乐谱:从文件结构到歌声合成可用的三元组
2.1 一份 XML 乐谱里到底存了什么
先别急着写代码,把压缩包解开,随便挑一个 XML 文件用文本编辑器打开。你会看到它本质是一棵嵌套的标签树,根节点通常是 score 或 score-partwise 这类结构,往下是 part(声部)、measure(小节)、note(音符)。每个 note 节点里,pitch 给出 step 和 octave,duration 给出时值,lyric 或 text 节点挂歌词。这跟 MusicXML 的通用约定是一致的,所以哪怕这份数据集不是标准 MusicXML,解析思路也能直接迁移。
对歌声合成来说,你真正要抽的是三样东西:音高序列(pitch)、时长序列(duration)、歌词/音节序列(lyric)。这三者按时间轴对齐,就是声学模型的条件输入。很多人一上来就想把整个 XML 塞进模型,这是典型的误用——XML 是给人看和给程序解析的中间格式,不是训练格式。正确做法是先解析成结构化数组,再转成帧级或音素级特征。
提示:先统计一下 XML 里 note 节点的总数和歌词覆盖率,如果大量 note 没有 lyric 子节点,说明这份谱子偏器乐,做歌声合成时需要额外补词或过滤。
2.2 用 Python 把 XML 解析成音符三元组
解析 XML 有两条路:标准库 xml.etree.ElementTree 和第三方 lxml。前者零依赖,后者快且容错好。我一般先用 ElementTree 快速验证结构,确认字段名后再换 lxml 批量跑。下面这段脚本把单个 XML 转成 (pitch, duration, lyric) 列表:
import xml.etree.ElementTree as ET def parse_score(xml_path): tree = ET.parse(xml_path) root = tree.getroot() notes = [] # 遍历所有 note 节点,兼容带命名空间和不带命名空间两种情况 for note in root.iter(): tag = note.tag.split('}')[-1] # 去掉命名空间前缀 if tag != 'note': continue # 跳过休止符:没有 pitch 子节点的 note 通常是 rest pitch_node = None duration_node = None lyric_node = None for child in note: ctag = child.tag.split('}')[-1] if ctag == 'pitch': pitch_node = child elif ctag == 'duration': duration_node = child elif ctag in ('lyric', 'text'): lyric_node = child if pitch_node is None: continue # 休止符,歌声合成里可单独标记 step = octave = None for p in pitch_node: ptag = p.tag.split('}')[-1] if ptag == 'step': step = p.text elif ptag == 'octave': octave = int(p.text) duration = int(duration_node.text) if duration_node is not None else 0 lyric = lyric_node.text if lyric_node is not None and lyric_node.text else '' notes.append((step, octave, duration, lyric)) return notes if __name__ == '__main__': result = parse_score('demo.xml') for r in result[:10]: print(r)逻辑说明:root.iter()会递归遍历所有节点,比逐层 find 更抗结构差异;tag.split('}')[-1]是处理命名空间的标准手法,很多 XML 解析报错就是因为标签带了{http://...}前缀导致匹配失败。参数上,duration的单位取决于数据集定义,常见是四分音符的分数或 tick,转秒之前必须先确认 tempo 和 divisions 两个字段,否则时值全错。
2.3 从音符到帧级特征:音高和时长的映射
拿到三元组后,下一步是映射到模型输入。歌声合成常用的是音高转 MIDI 编号、时长转帧数。step 到半音的映射是固定的:C=0, D=2, E=4, F=5, G=7, A=9, B=11,再叠加(octave+1)*12得到 MIDI 值。时长则要看数据集里的 divisions 定义,常见做法是秒数 = duration / divisions * (60 / tempo)。
STEP_TO_SEMITONE = {'C':0,'D':2,'E':4,'F':5,'G':7,'A':9,'B':11} def to_midi(step, octave): return STEP_TO_SEMITONE[step] + (octave + 1) * 12 def duration_to_seconds(duration, divisions, tempo): # divisions 表示每四分音符多少 tick,tempo 为 BPM return duration / divisions * (60.0 / tempo)参数说明:divisions和tempo通常写在 XML 的 attributes 或 direction 节点里,解析时要单独抽出来,不能默认 480 或 120,不同来源的谱子差异很大。这一步做完,你就有了一条按时间排列的 MIDI 音高序列和对应秒数,可以直接喂给对齐工具或声学模型。
3. 批量处理几百首乐谱:目录遍历、清洗与格式统一
3.1 批量解析的目录结构和并发策略
几百首 XML 单线程跑也能跑完,但如果你后面还要做特征提取,串行会拖慢迭代。我一般用pathlib遍历加concurrent.futures做进程池,因为 XML 解析是 CPU 密集型,多进程比多线程有效。
from pathlib import Path from concurrent.futures import ProcessPoolExecutor def batch_parse(root_dir): files = list(Path(root_dir).rglob('*.xml')) print(f'共发现 {len(files)} 个 XML 文件') results = {} with ProcessPoolExecutor(max_workers=8) as ex: for path, notes in zip(files, ex.map(parse_score, files)): results[str(path)] = notes return results逻辑说明:rglob('*.xml')递归匹配所有 XML,避免手动拼路径;max_workers按 CPU 核数设,一般 8 到 16 之间,设太大反而因 IO 争抢变慢。返回的字典以文件路径为 key,方便后续按曲目切分训练集和验证集。
3.2 清洗:空歌词、异常时值和重复音符
真实数据集不可能干净。我跑完几百首后统计过,常见脏数据有三类:歌词为空(纯器乐段)、duration 为 0 或异常大(解析错位)、同一时间点重复音符(和弦被拆成多个 note)。处理策略要按用途定:做歌声合成,空歌词的段要么丢弃要么用占位符;duration 异常的直接按四分音符默认值兜底;重复音符保留最高音或合并。
def clean_notes(notes, default_duration=480): cleaned = [] for step, octave, duration, lyric in notes: if step is None or octave is None: continue if duration <= 0 or duration > 10000: duration = default_duration if not lyric: lyric = '<SP>' # 用占位符标记无词段 cleaned.append((step, octave, duration, lyric)) return cleaned参数说明:default_duration要和你数据集的 divisions 对齐,不能拍脑袋写;<SP>这类占位符在后续转音素时要单独处理,别让它混进词典。
3.3 统一格式:输出成 JSON 或 CSV 供下游使用
解析清洗完,建议落成中间格式,别每次训练都重新解析 XML。JSON 保留嵌套结构,CSV 方便用 pandas 快速统计。我一般两个都存,JSON 给训练脚本,CSV 给自己做数据探查。
import json, csv def dump_json(results, out_path): with open(out_path, 'w', encoding='utf-8') as f: json.dump(results, f, ensure_ascii=False, indent=2) def dump_csv(results, out_path): with open(out_path, 'w', encoding='utf-8', newline='') as f: writer = csv.writer(f) writer.writerow(['file', 'step', 'octave', 'duration', 'lyric']) for path, notes in results.items(): for n in notes: writer.writerow([path, *n])逻辑说明:ensure_ascii=False保证中文歌词不被转义成\uXXXX,方便肉眼检查;CSV 每行一个音符,适合直接丢进 pandas 做分布统计。到这一步,几百首 XML 就变成了模型可读的结构化数据。
4. 避坑与排查:XML 乐谱处理里最容易翻车的五件事
4.1 现象:解析报错「no element found」或标签匹配不上
原因:XML 文件带 BOM 头、编码声明与实际编码不符,或者标签带命名空间前缀。解决:读取时显式指定encoding='utf-8-sig'去掉 BOM;标签匹配统一用tag.split('}')[-1]剥离命名空间。这是最高频的坑,十次解析失败有六次是它。
4.2 现象:音高整体偏高或偏低一个八度
原因:不同数据集对 octave 的基准定义不同,有的以中央 C 为 octave 4,有的为 3。解决:先抽一首已知旋律的谱子,人工核对 MIDI 值,确认基准后再批量跑。别信文档,信耳朵和已知曲目。
4.3 现象:时长转秒后整体快一倍或慢一倍
原因:tempo 单位理解错,或者 divisions 取错节点。解决:把 tempo 和 divisions 打印出来,用duration/divisions*(60/tempo)手算一个小节的总秒数,和实际听感对比。这个坑我在第一次做对齐时踩了整整一天。
4.4 现象:歌词和音符错位,唱出来对不上
原因:XML 里歌词挂在 note 上,但有些谱子用单独的 lyric 节点按顺序排列,需要按时间轴重新绑定。解决:检查 lyric 是 note 的子节点还是兄弟节点,前者直接取,后者要维护一个指针按序分配。绑定完抽几首人工唱一遍验证。
4.5 现象:批量处理时内存爆掉
原因:一次性把所有 XML 解析结果读进内存,几百首大谱子能吃掉几个 G。解决:改成生成器逐文件处理,或者解析完立刻落盘再释放。ProcessPoolExecutor的map是惰性的,但如果你用list()包起来就全加载了,注意别多此一举。
5. 进阶玩法:把 XML 乐谱接到歌声合成流水线上的三个技巧
第一个技巧是做音素对齐预标注。XML 里的歌词是汉字或拼音,直接喂模型粒度太粗。我一般先用 pypinyin 把汉字转成带声调的拼音,再按声母韵母切分成音素,和 MIDI 音高序列按音符边界对齐。这样得到的音素级标注可以直接用于训练 Duration 和 Pitch 预测器,省掉大量人工标注。
from pypinyin import pinyin, Style def lyric_to_phoneme(lyric): if lyric == '<SP>': return ['SP'] py = pinyin(lyric, style=Style.TONE3, neutral_tone_with_five=True) return py[0] if py else ['SP']参数说明:Style.TONE3输出带数字声调的拼音,方便区分同音不同调的字;neutral_tone_with_five=True把轻声标成 5,避免和一声混淆。这一步做完,你的歌词序列就从「字」变成了「音素+声调」,模型收敛会明显更快。
第二个技巧是用 XML 里的 tempo 和拍号做数据增强。同一段旋律,改 tempo 就得到不同语速的训练样本,改拍号能生成变奏。我一般对每首谱子生成 2 到 3 个 tempo 变体,扩充数据量,同时保持音高和歌词不变。注意变体要分到同一个 split,别让训练集和验证集出现同一旋律的不同速度版本,否则评测虚高。
第三个技巧是验证解析正确性。别只看代码跑通,要听。把解析出的 MIDI 序列用 pretty_midi 写成 MIDI 文件,用播放器放一遍,和原谱子对比。我现在的习惯是每处理完一批数据,随机抽三首导出 MIDI 试听,确认音高、节奏、歌词都对得上才继续。从那以后我每次拿到新的乐谱数据集,都强制走一遍「解析→导出 MIDI→试听」的闭环,再也没出现过训练到一半发现数据错位的后悔药场景。希望这份 XML 乐谱数据集和上面的处理思路,能帮你把歌声合成的数据准备这一步真正跑通。
本文还有配套的精品资源,点击获取