Repository Wiki
bytedance/piano_transcription

MAESTRO 数据集与 HDF5 特征打包

本页剖析 utils/features.py 中 pack_maestro_dataset_to_hdf5 的完整实现:如何读取 MAESTRO v2.0.0 的 CSV 元数据与音频/MIDI 文件、统一重采样后写入 HDF5,以及产出的 .h5 文件模式(schema)与训练侧消费接口。

目的与范围

本页覆盖:

  • pack_maestro_dataset_to_hdf5 离线特征打包流水线的逐步实现(读取 CSV → 读 MIDI → 加载并重采样音频 → 写 HDF5)。
  • 打包产物的 HDF5 文件模式:根属性(attrs)与三个数据集(midi_event、midi_event_time、waveform)的字段、dtype 与设计意图。
  • 命令行入口(argparse 子命令)与输出目录布局({workspace}/hdf5s/maestro/)。
  • 消费端接口边界:utils/data_generator.py 中 MaestroDataset 如何接收 hdf5s_dir,以及 utils/plot_for_paper.py 如何按 split 属性过滤。

有意留给兄弟页面:

  • 训练主循环、损失与模型结构:见 pytorch/main.py、pytorch/models.py 相关页面。
  • MaestroDataset 内部的随机片段采样、音符移调增强等细节:见数据生成器(utils/data_generator.py)相关页面,本页只界定其输入契约。
  • MAPS 数据集打包(pack_maps_dataset_to_hdf5):本页仅在扩展点一节作为对照简述。
  • 推理与评估:见 predict.py、pytorch/inference.py、pytorch/evaluate.py 相关页面。

概述

pack_maestro_dataset_to_hdf5 是一个一次性离线预处理脚本。MAESTRO 官方发布的是 WAV/FLAC 音频 + MIDI + 一张 CSV 索引表;直接在训练循环中反复用 librosa 加载原始音频、解析 MIDI 会带来大量重复 IO 与 CPU 开销。因此该脚本把"音频波形 + 对齐的 MIDI 事件序列 + 元数据"打包成每首曲子一个自包含的 HDF5 文件,训练时只需 h5py 顺序读取即可。

这一设计的关键决策有三点:

  1. 归一化音频:统一通过 librosa.core.load(audio_path, sr=sample_rate, mono=True) 重采样到 config.sample_rate 并混为单声道,保证所有片段的特征提取(CQT 等)输入一致。
  2. int16 波形存储:音频经 float32_to_int16(audio) 由 float32 转为 int16 存储,相比直接存 float32 节省一半磁盘空间,且 16-bit 已覆盖音频位深。
  3. 元数据随文件走:CSV 中的 split(train/validation/test)、year、作曲家等字段写入每个 .h5 的根属性,消费端无需再读 CSV 即可按划分过滤。

架构

Loading diagram...

上图中每个节点都对应真实代码:read_metadata、read_midi 从 utils/utilities.py 导入(见 features.py);sample_rate 取自 utils/config.py(见 features.py);MaestroDataset 的构造签名见 data_generator.py;plot_for_paper.py 中 hf.attrs['split'].decode() 的用法见 plot_for_paper.py。

架构分层意图:打包是纯离线、单进程、幂等('w' 模式覆盖写)的过程,与训练进程完全解耦——训练只依赖 hdf5s_dir 指向的目录,不依赖 MAESTRO 原始目录是否存在。

主内容:打包流水线逐步解析

第一步:路径与日志准备

python
1def pack_maestro_dataset_to_hdf5(args): 2 """Load & resample MAESTRO audio files, then write to hdf5 files. 3 4 Args: 5 dataset_dir: str, directory of dataset 6 workspace: str, directory of your workspace 7 """ 8 9 # Arguments & parameters 10 dataset_dir = args.dataset_dir 11 workspace = args.workspace 12 13 sample_rate = config.sample_rate 14 15 # Paths 16 csv_path = os.path.join(dataset_dir, 'maestro-v2.0.0.csv') 17 waveform_hdf5s_dir = os.path.join(workspace, 'hdf5s', 'maestro') 18 19 logs_dir = os.path.join(workspace, 'logs', get_filename(__file__)) 20 create_logging(logs_dir, filemode='w') 21 logging.info(args)

Source: features.py

要点:

  • CSV 文件名硬编码为 maestro-v2.0.0.csv,即该脚本绑定 MAESTRO v2.0.0 版本;若使用其它版本的 MAESTRO,需要相应调整此行。
  • 输出目录固定为 {workspace}/hdf5s/maestro/,日志写到 {workspace}/logs/features.py/(get_filename(__file__) 返回脚本名),filemode='w' 表示每次运行重写日志。
  • 采样率来自全局 config.sample_rate,本页未核对其具体数值,请参见 utils/config.py。

第二步:读取 CSV 元数据并逐曲循环

python
1 # Read meta dict 2 meta_dict = read_metadata(csv_path) 3 4 audios_num = len(meta_dict['canonical_composer']) 5 logging.info('Total audios number: {}'.format(audios_num)) 6 7 feature_time = time.time() 8 9 # Load & resample each audio file to a hdf5 file 10 for n in range(audios_num): 11 logging.info('{} {}'.format(n, meta_dict['midi_filename'][n]))

Source: features.py

read_metadata 把 CSV 解析为按列组织的 meta_dict(键包括 canonical_composer、canonical_title、split、year、midi_filename、audio_filename、duration,可从下文写入 attrs 的字段一一对应看出)。曲目总数以 canonical_composer 列长度为准,循环内逐曲记录 midi_filename 以便断点排查。

第三步:读 MIDI、加载音频并写出 HDF5

python
1 # Read midi 2 midi_path = os.path.join(dataset_dir, meta_dict['midi_filename'][n]) 3 midi_dict = read_midi(midi_path) 4 5 # Load audio 6 audio_path = os.path.join(dataset_dir, meta_dict['audio_filename'][n]) 7 (audio, _) = librosa.core.load(audio_path, sr=sample_rate, mono=True) 8 9 packed_hdf5_path = os.path.join(waveform_hdf5s_dir, '{}.h5'.format( 10 os.path.splitext(meta_dict['audio_filename'][n])[0])) 11 12 create_folder(os.path.dirname(packed_hdf5_path)) 13 14 with h5py.File(packed_hdf5_path, 'w') as hf: 15 hf.attrs.create('canonical_composer', data=meta_dict['canonical_composer'][n].encode(), dtype='S100') 16 hf.attrs.create('canonical_title', data=meta_dict['canonical_title'][n].encode(), dtype='S100') 17 hf.attrs.create('split', data=meta_dict['split'][n].encode(), dtype='S20') 18 hf.attrs.create('year', data=meta_dict['year'][n].encode(), dtype='S10') 19 hf.attrs.create('midi_filename', data=meta_dict['midi_filename'][n].encode(), dtype='S100') 20 hf.attrs.create('audio_filename', data=meta_dict['audio_filename'][n].encode(), dtype='S100') 21 hf.attrs.create('duration', data=meta_dict['duration'][n], dtype=np.float32) 22 23 hf.create_dataset(name='midi_event', data=[e.encode() for e in midi_dict['midi_event']], dtype='S100') 24 hf.create_dataset(name='midi_event_time', data=midi_dict['midi_event_time'], dtype=np.float32) 25 hf.create_dataset(name='waveform', data=float32_to_int16(audio), dtype=np.int16)

Source: features.py

关键实现细节:

  • MIDI 解析契约:read_midi 返回的 midi_dict 至少包含 midi_event(字符串事件列表,逐条 .encode() 后以定长 S100 字节串写入)与 midi_event_time(float32 时间戳数组)。事件文本与时间一一平行对齐,这是后续把 note/pedal 事件对齐到波形帧网格的基础。
  • 音频路径来自 CSV:audio_filename 在 MAESTRO v2.0.0 中形如 2004/MIDI-Unprocessed_..._wav.wav(含年份子目录),因此 .h5 文件名取 os.path.splitext(...)[0] 后拼接 .h5,输出时不含年份子目录、全部平铺在 hdf5s/maestro/ 下(create_folder(os.path.dirname(packed_hdf5_path)) 只是确保目录存在)。
  • librosa.core.load 的返回值:函数返回 (audio, sr),脚本用 (audio, _) 丢弃第二个返回值——因为已显式传入 sr=sample_rate,重采样由 librosa 完成,返回采样率必然等于请求值。
  • with 语句保证逐曲落盘:每首曲子打开/关闭一次 HDF5 文件,异常时最多丢当前一曲,已写完的文件不受影响。

HDF5 文件模式(Schema)

单个 .h5 文件的结构如下:

Loading diagram...
位置名称dtype来源说明
attrscanonical_composerS100CSV规范化作曲家名
attrscanonical_titleS100CSV规范化曲名
attrssplitS20CSV数据划分标记(train/validation/test,原样透传 CSV 值)
attrsyearS10CSV录制年份
attrsmidi_filenameS100CSV原始 MIDI 相对路径
attrsaudio_filenameS100CSV原始音频相对路径
attrsdurationfloat32CSV时长(秒)
datasetmidi_eventS100read_midiMIDI 事件文本列表(note on/off、pedal 等)
datasetmidi_event_timefloat32read_midi与 midi_event 平行对齐的事件时间(秒)
datasetwaveformint16librosa + float32_to_int16重采样到 config.sample_rate 的单声道波形

设计意图:元数据放 attrs、大数组放 dataset。attrs 读取开销极小,MaestroDataset 与 plot_for_paper.py 可以只读 attrs 做划分过滤;waveform 是体量最大的数组,独立成 dataset 便于按需/分块读取。

核心流程

打包执行的端到端时序

Loading diagram...

命令行入口与子命令分发

python
1if __name__ == '__main__': 2 3 parser = argparse.ArgumentParser(description='') 4 subparsers = parser.add_subparsers(dest='mode') 5 6 parser_pack_maestro = subparsers.add_parser('pack_maestro_dataset_to_hdf5') 7 parser_pack_maestro.add_argument('--dataset_dir', type=str, required=True, help='Directory of dataset.') 8 parser_pack_maestro.add_argument('--workspace', type=str, required=True, help='Directory of your workspace.') 9 10 # Parse arguments 11 args = parser.parse_args() 12 13 if args.mode == 'pack_maestro_dataset_to_hdf5': 14 pack_maestro_dataset_to_hdf5(args)

Source: features.py

完整可运行的调用形式为:

bash
python utils/features.py pack_maestro_dataset_to_hdf5 \ --dataset_dir /path/to/maestro-v2.0.0 \ --workspace /path/to/your_workspace

(命令行为依据 argparse 定义归纳,参数名与必填性均来自源码。)若 args.mode 不匹配任何子命令,脚本会 raise Exception('Incorrect arguments!')(见 features.py)。

使用示例

消费端:MaestroDataset 的输入契约

训练侧通过 hdf5s_dir 指向打包产物目录:

python
1class MaestroDataset(object): 2 def __init__(self, hdf5s_dir, segment_seconds, frames_per_second, 3 max_note_shift=0, augmentor=None): 4 """ 5 Args: 6 feature_hdf5s_dir: str 7 segment_seconds: float

Source: data_generator.py

注意 docstring 里写的是 feature_hdf5s_dir,而实际参数名为 hdf5s_dir,随后 self.hdf5s_dir = hdf5s_dir 保存(见 data_generator.py)。其取片段的元信息格式(hdf5_name + start_time)形如:

python
'hdf5_name': 'MIDI-Unprocessed_SMF_12_01_2004_01-05_ORIG_MID--AUDIO_12_R1_2004_10_Track10_wav.h5, 'start_time': 65.0}

Source: data_generator.py

这印证了前文的命名规则:.h5 文件名 = audio_filename 去掉扩展名(含年份目录前缀被 os.path.splitext 处理后的 basename 组合结果),本例中 hdf5_name 以 MIDI-Unprocessed_...wav.h5 结尾,与打包侧 {}.h5'.format(os.path.splitext(audio_filename)[0]) 完全一致。

消费端:按 split 过滤

python
1 hdf5s_dir = os.path.join(workspace, 'hdf5s', dataset) 2 3 (hdf5_names, hdf5_paths) = traverse_folder(hdf5s_dir) 4 5 n = 0 6 for n, hdf5_path in enumerate(hdf5_paths): 7 with h5py.File(hdf5_path, 'r') as hf: 8 if hf.attrs['split'].decode() == split: 9 print(n, hdf5_path)

Source: plot_for_paper.py

这里体现了把 split 写入 attrs 的价值:消费端逐个打开文件、只读一个属性即可完成 train/test 划分过滤,无需维护任何外部索引;hf.attrs['split'].decode() 说明 attrs 是字节串(与打包侧 .encode() + S20 dtype 对应)。

配置选项

命令行参数(pack_maestro_dataset_to_hdf5 子命令)

参数类型必填默认说明
--dataset_dirstr是无MAESTRO 数据集根目录(内含 maestro-v2.0.0.csv、音频与 MIDI 子目录)
--workspacestr是无工作区目录;打包产物写入 {workspace}/hdf5s/maestro/,日志写入 {workspace}/logs/

隐式配置(非命令行)

项取值来源说明
sample_rateconfig.sample_rate(utils/config.py)音频统一重采样目标采样率,全局共享给训练/特征
CSV 文件名硬编码 maestro-v2.0.0.csv版本耦合点,换 MAESTRO 版本需修改
输出子目录硬编码 hdf5s/maestro消费端按同一约定拼接路径

API 参考

pack_maestro_dataset_to_hdf5(args)

职责:把 MAESTRO v2.0.0 数据集(CSV 索引 + 音频 + MIDI)打包为逐曲 HDF5 文件。

参数:

  • args (argparse.Namespace):需包含 dataset_dir(str)与 workspace(str)两个属性。

返回值:None。副作用是写文件与日志;结束时记录最后一个写出的路径与总耗时。

抛出/依赖的异常:本函数自身不显式抛出;但底层 librosa.core.load、h5py.File、read_midi/read_metadata 在文件缺失或损坏时会抛异常并中断整个循环(无重试机制)。

消费端签名:MaestroDataset.__init__(hdf5s_dir, segment_seconds, frames_per_second, max_note_shift=0, augmentor=None)

职责:以打包好的 hdf5s/maestro 目录为数据源构建训练数据集。其内部实现(随机片段采样、增强)属于数据生成器页面,此处仅声明契约。

故障模式、边界情况与并发

基于已读源码可确认的边界行为:

  • 无断点续传:循环 for n in range(audios_num) 顺序处理,若在第 k 曲崩溃需重跑;但得益于 'w' 模式覆盖写,重跑是幂等的——已存在的 .h5 会被同名重写,不会产生脏数据。
  • 无并行化:单进程、单线程顺序执行,打包耗时随曲目数线性增长(MAESTRO v2.0.0 约 1200+ 曲,librosa 重采样是主要耗时点)。脚本以 logging.info('Time: {:.3f} s') 记录总时长。
  • 无显式校验:不校验波形时长与 duration 属性一致性,也不校验 MIDI 事件与音频是否对齐——信任 read_midi 与 librosa 的输出。
  • 存储占用:waveform 以 int16 存全曲,无压缩选项(create_dataset 未传 compression),磁盘占用 ≈ 曲长(秒) × sample_rate × 2 字节。
  • attr 长度上限:字符串 attrs 用定长 dtype(S100/S20/S10)。超长作曲家名或曲名会被截断到对应字节长度——split(最长 "validation")用 S20、year 用 S10 足够,但 canonical_title 理论上可能超过 100 字节。
  • 并发写入:脚本不做任何文件锁;若两个进程同时向同一 hdf5s/maestro 打包同名文件,h5py 层面可能产生损坏文件。应保证单实例运行。

性能与运维说明

  • 打包是一次性成本:完成后训练不再触碰原始 WAV/MIDI,磁盘换时间的典型取舍。重复训练实验(调超参、换损失)复用同一份 hdf5s。
  • 日志即进度:logging.info('{} {}'.format(n, meta_dict['midi_filename'][n])) 每曲一行,可 tail -f 监控进度;日志目录含 get_filename(__file__)(即 features.py),便于区分不同脚本产生的日志。
  • float32_to_int16 的精度取舍:从 librosa 的 float32(范围约 [-1, 1])量化到 int16,引入最多 1/65536 量级的量化误差,对以帧级回归为主的训练目标无实质影响。

扩展点

  • 适配其它 MAESTRO 版本:核心改动只有 features.py 的 CSV 文件名。
  • 新增数据集:同文件中的 pack_maps_dataset_to_hdf5(features.py)展示了同一套打包模式如何迁移到 MAPS:目录遍历换成 os.listdir + .mid 后缀过滤,MIDI 解析换成 read_maps_midi,attrs 精简为 split(固定写 'test')/midi_filename/audio_filename,三个 dataset 的名字与 dtype 保持不变(见 features.py)。dataset 名称与 dtype 的一致性是关键约束——消费端 MaestroDataset/评估脚本依赖 waveform、midi_event、midi_event_time 这三个名字。
  • 添加压缩或分块:若需降低磁盘占用,可在 hf.create_dataset 上加 compression='gzip' 等 h5py 参数,属于纯本地改动,不影响 schema。

相关链接

Sources

(1 files)