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 顺序读取即可。
这一设计的关键决策有三点:
- 归一化音频:统一通过
librosa.core.load(audio_path, sr=sample_rate, mono=True)重采样到config.sample_rate并混为单声道,保证所有片段的特征提取(CQT 等)输入一致。 - int16 波形存储:音频经
float32_to_int16(audio)由 float32 转为 int16 存储,相比直接存 float32 节省一半磁盘空间,且 16-bit 已覆盖音频位深。 - 元数据随文件走:CSV 中的
split(train/validation/test)、year、作曲家等字段写入每个.h5的根属性,消费端无需再读 CSV 即可按划分过滤。
架构
上图中每个节点都对应真实代码: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 原始目录是否存在。
主内容:打包流水线逐步解析
第一步:路径与日志准备
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 元数据并逐曲循环
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
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 文件的结构如下:
| 位置 | 名称 | dtype | 来源 | 说明 |
|---|---|---|---|---|
| attrs | canonical_composer | S100 | CSV | 规范化作曲家名 |
| attrs | canonical_title | S100 | CSV | 规范化曲名 |
| attrs | split | S20 | CSV | 数据划分标记(train/validation/test,原样透传 CSV 值) |
| attrs | year | S10 | CSV | 录制年份 |
| attrs | midi_filename | S100 | CSV | 原始 MIDI 相对路径 |
| attrs | audio_filename | S100 | CSV | 原始音频相对路径 |
| attrs | duration | float32 | CSV | 时长(秒) |
| dataset | midi_event | S100 | read_midi | MIDI 事件文本列表(note on/off、pedal 等) |
| dataset | midi_event_time | float32 | read_midi | 与 midi_event 平行对齐的事件时间(秒) |
| dataset | waveform | int16 | librosa + float32_to_int16 | 重采样到 config.sample_rate 的单声道波形 |
设计意图:元数据放 attrs、大数组放 dataset。attrs 读取开销极小,MaestroDataset 与 plot_for_paper.py 可以只读 attrs 做划分过滤;waveform 是体量最大的数组,独立成 dataset 便于按需/分块读取。
核心流程
打包执行的端到端时序
命令行入口与子命令分发
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
完整可运行的调用形式为:
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 指向打包产物目录:
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: floatSource: data_generator.py
注意 docstring 里写的是 feature_hdf5s_dir,而实际参数名为 hdf5s_dir,随后 self.hdf5s_dir = hdf5s_dir 保存(见 data_generator.py)。其取片段的元信息格式(hdf5_name + start_time)形如:
'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 过滤
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_dir | str | 是 | 无 | MAESTRO 数据集根目录(内含 maestro-v2.0.0.csv、音频与 MIDI 子目录) |
--workspace | str | 是 | 无 | 工作区目录;打包产物写入 {workspace}/hdf5s/maestro/,日志写入 {workspace}/logs/ |
隐式配置(非命令行)
| 项 | 取值来源 | 说明 |
|---|---|---|
sample_rate | config.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。
相关链接
- 打包实现:utils/features.py
- 采样率等全局配置:utils/config.py
read_metadata/read_midi/float32_to_int16/create_logging所在工具模块:utils/utilities.py- 消费端数据集:utils/data_generator.py
- 按划分读取 HDF5 的示例:utils/plot_for_paper.py
- 训练入口(使用
MaestroDataset):pytorch/main.py