ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

100首歌声数据集解析:wav/midi/歌词/时长四件套与多模态对齐实践

100首歌声数据集解析:wav/midi/歌词/时长四件套与多模态对齐实践 简介在音频人工智能与音乐信息检索MIR领域高质量带标注的歌声数据远比通用语音稀缺。歌声合成、音高估计等任务依赖wav、midi、歌词与时间戳的多模态对齐而手工标注成本极高。一份包含100首歌曲、配齐wav原始音频、midi旋律标注、歌词文本和句级时长标注的数据集能够大幅降低数据准备门槛。通过对齐校验、midi解析与时间戳清洗可快速构建训练样本支撑歌声合成、歌词识别、音高追踪等方向的研究与工程实践。本文梳理该数据集的格式、解析方法与常见坑提供可直接落地的处理思路。 常年跟音频数据打交道的人多少都收到过不少数据集但真正打开以后能让人眼睛一亮的真不多。这份100首歌声数据集含midi 歌词 标注时长 wav.zip算是我近期看到比较实在的一份。100首歌曲每一首配齐wav原始音频、midi旋律标注、歌词文本和句级时长标注四样东西凑齐基本就是一份可以直接投喂给模型的多模态对齐样本集。这份数据帮我把最繁琐的对齐工作直接省掉了。平时做歌声合成或者音高估计最痛苦的不是调模型而是找数据、扒谱、标时间戳一套流程下来人已经麻了。如果你正在做音乐AI、歌声合成、歌词识别、音高追踪或者MIR音乐信息检索方向这份数据集属于拿过去改改路径就能用的那种省掉大量前置工作。下面我会把文件结构、解析方法、常见坑和几个实际能跑起来的用法全拆开讲。1. 项目概述这份歌声数据集解决了什么问题1.1 为什么带标注的歌声这么难找先聊点背景。在音频AI领域通用语音数据其实不少比如TTS数据集、ASR数据集开箱即用的很多。但歌声数据跟纯语音完全是两回事。歌声有旋律、有音高变化、有节奏感、有共鸣和气息处理标注起来比说话复杂太多。这也是为什么很多做歌声合成SVS的人最后都被困在数据准备阶段而不是模型设计阶段。网上能找到的开源歌声数据要么只有几首歌要么只给了wav没有标注要么标注了但格式乱七八糟。能像这份数据一样把wav、midi、歌词、时长四件套配齐还统一打包成zip分发已经算很良心了。midi在里面的价值很高它相当于把每一句的旋律音高信息用标准格式写死了你不需要自己去跑一个音高估计器去猜后验标注直接拿过来当ground truth用对训练音高预测和歌声合成模型非常友好。1.2 四种文件是怎么配合的这四类文件不是各管各的而是互相锚定的关系。wav是音频本体midi记录了唱了什么音高、什么时候开始唱歌词记录了唱了什么字时长标注记录了每一句从什么时间开始、什么时候结束。我拿一份样本举例假设某首歌有8句话那歌词文件里应该对应8条文本时长标注里对应8组起止时间midi里能提取出跟这些时间区间吻合的音符序列wav就是最终的声音表现。四者只要有三份能对上第四份基本也能对上。这就是多模态对齐数据最有价值的地方也是我强烈建议第一次拿到数据就立刻做一次四文件一致性校验的原因。直接跳到训练而忽略校验后面出了问题会让你痛苦很久。2. 数据集结构与核心格式拆解2.1 解压后的目录与命名规则先花几十秒把zip解压看一眼里面的结构。比较理想的情况下目录应该是这样组织的不同版本可能略有差异但核心思路基本一致data/ ├── wav/ │ ├── 001.wav │ ├── 002.wav │ └── ... ├── mid/ │ ├── 001.mid │ ├── 002.mid │ └── ... ├── lyrics/ │ ├── 001.txt │ ├── 002.txt │ └── ... └── timestamps/ ├── 001.json ├── 002.json └── ...命名的核心逻辑是同一个编号贯穿四种文件。拿到一个编号先把这四份文件都找到确认一一对应再往下做。有些版本会直接把所有标注塞到一个CSV里但大体上编号一致这个原则不会变。这里额外提醒一句如果你的zip解压后看不到清晰的目录层级所有文件都平铺在一个文件夹里也别慌。用编号把关联文件选出来自己重新组织目录就行。后续所有脚本只要依赖编号→四文件路径这个映射就不会乱。2.2 wav音频格式、采样率与时长wav是无压缩的PCM格式打开最方便也最适合直接喂给深度学习模型。这个数据集里的wav一般有两种常见规格一种是44.1kHz采样率、16bit量化这是音乐行业的标准音质另一种是22.05kHz采样率可能是经过后期整理的版本训练速度快显存占用小。拿到wav之后建议第一时间用代码确认实际参数而不是用播放器看一眼就觉得没问题。播放器会自动适配各种格式这个自动适配恰恰会掩盖采样率不统一的问题import wave with wave.open(data/wav/001.wav, rb) as wf: print(采样率:, wf.getframerate()) print(量化位数:, wf.getsampwidth() * 8) print(声道数:, wf.getnchannels()) print(时长(秒):, wf.getnframes() / wf.getframerate())跑完这段代码你就知道这份数据实际是哪种规格了。后续如果要训练模型最好统一重采样到同一个采样率。我之前就吃过亏没检查采样率就直接混合训练结果有些歌听起来整体音调偏高排查了半天才发现是采样率混用了。2.3 midi旋律标注别把它当乐谱看midi文件很多人第一反应是这是乐谱吧其实在歌声数据集里midi的作用更纯粹它是旋律线的时间对齐标注。里面记录的核心信息是note on、note off事件每个音符对应一个音高编号MIDI note number和时长。60对应中央C每加1高半音这个映射关系要记牢。解析midi最常用的Python库是mido读起来非常直接import mido mid mido.MidiFile(data/mid/001.mid) print(ticks_per_beat:, mid.ticks_per_beat) for track in mid.tracks: for msg in track: if msg.type set_tempo: print(tempo变化:, msg.tempo, 对应BPM:, mido.tempo2bpm(msg.tempo)) if msg.type note_on and msg.velocity 0: print(f音高: {msg.note}, 力度: {msg.velocity}, 开始tick: {msg.time})注意这里有个关键细节midi里的时间单位是tick不是秒。要把tick换算成秒必须知道两个参数——tempo每个四分音符的微秒数和ticks_per_beat每个四分音符的tick数。换算逻辑可以这样理解midi里的时间就像相对单位tempo和ticks_per_beat相除得到的是每秒多少个tick。一旦算错midi和wav就会完全对不上后面所有对齐工作全废。所以拿到midi的第一件事就是考虑tempo变化把每个消息的tick都换算成绝对时间。2.4 歌词和时长标注的对齐方式歌词文件通常是纯文本编码可能是UTF-8也可能是GBK。每行一句话或者每个字一行。时长标注则常见有两种格式一种是JSON数组形如[ {start: 0.0, end: 2.8, text: 你}, {start: 2.85, end: 5.2, text: 好}, {start: 5.3, end: 8.1, text: 世}, {start: 8.15, end: 10.5, text: 界} ]另一种是类字幕格式每行三段空格或制表符分隔0.00 2.80 你 2.85 5.20 好 5.30 8.10 世 8.15 10.50 界不管哪种格式核心都是每个字或每句话对应一个时间区间。这个时间区间跟midi音符的起止时间应该能大致对应上。换句话说歌词负责语义层midi负责音高层时长负责时间层三者共同锚定到wav这个物理层。做对齐任务时你只需要在时间轴上把这三层合并起来。3. 从零开始解析一份样本这一章是全文的重点我直接给出一套能跑通的代码流程。照做就行但建议不要无脑复制每段代码的意图我会讲清楚。3.1 环境准备与依赖安装建议用Python 3.8以上版本装几个库就够了pip install mido librosa numpy matplotlibmido用来解析midilibrosa用来读wav和做音频特征numpy做数组运算matplotlib用来可视化波形和音高序列。如果你要跑深度学习还需要自己装torch之类的框架但第一步解析数据用上面这四个库已经足够了。3.2 读取wav并观察基本信息librosa读wav会直接返回numpy数组和采样率内部自动做解码比标准库wave省事很多。这里我特意把sr指定成22050目的是把不同采样率的wav统一到一个水平librosa会自动完成重采样你不需要自己写重采样逻辑import librosa audio, sr librosa.load(data/wav/001.wav, sr22050, monoTrue) print(f音频时长: {len(audio) / sr:.2f} 秒) print(f采样率: {sr}, 样本数: {len(audio)})这一步跑通以后你已经把wav变成了一个一维numpy数组。后续无论是做频谱特征、切片还是计算时长都在这个数组上操作非常顺手。3.3 从midi提取音高时间序列结合上一章的解析思路我封装一个完整的midi解析函数。这里要处理note_off配对问题因为一个音符从note_on开始到note_off结束才算完整。歌声数据集的midi通常只有一个旋律track所以直接遍历第一个track就行import mido def midi_to_notes(midi_path): mid mido.MidiFile(midi_path) notes [] current_tick 0 current_tempo 500000 note_starts {} for msg in mid.tracks[0]: current_tick msg.time if msg.type set_tempo: current_tempo msg.tempo if msg.type note_on and msg.velocity 0: seconds mido.tick2second(current_tick, mid.ticks_per_beat, current_tempo) note_starts[msg.note] seconds elif msg.type note_off and msg.note in note_starts: end_seconds mido.tick2second(current_tick, mid.ticks_per_beat, current_tempo) notes.append({ note: msg.note, start: note_starts.pop(msg.note), end: end_seconds }) notes.sort(keylambda x: x[start]) return notes notes midi_to_notes(data/mid/001.mid) print(音符数量:, len(notes)) for n in notes[:5]: print(n)这个实现里有几个细节值得注意。第一音符开始时间用note_on事件计算结束时间用note_off事件计算两者都换算成秒。第二我用了字典note_starts来缓存未关闭的音符因为note_off事件不一定紧接着note_on出现。第三如果midi中途有tempo变化current_tempo会实时更新换算出来的秒数就是准确的。这三个细节都处理到位midi和wav就能对齐。3.4 把歌词、时长和midi合并到时间轴读入JSON标注之后我们可以做这样一件事把每个字的区间、对应midi音符、对应音频切片全部打包成一个对象方便后续做训练样本import json with open(data/timestamps/001.json, r, encodingutf-8) as f: segments json.load(f) samples [] for seg in segments: start float(seg[start]) end float(seg[end]) text seg[text] # 找到这个区间内的midi音符 note_in_seg [n for n in notes if start n[start] end] # 切出对应的音频片段 audio_seg audio[int(start * sr): int(end * sr)] samples.append({ text: text, start: start, end: end, midi_notes: note_in_seg, audio_seg: audio_seg }) print(句子级样本数:, len(samples)) print(第一个样本:, samples[0][text], samples[0][start], samples[0][end])这一步做完你就有了一个歌词字级/句级对齐的数据结构。在后续做歌声合成、歌词发音对齐、音高预测时这个结构基本可以直接当训练输入的骨架。midi音符列表可能有多个音符落在同一个字区间内也可能没有这两种情况分别对应长音和无音的场景训练时要区别对待。3.5 可视化校验对齐结果做完对齐别急着跑模型先用一张图确认midi音高是否真的落在wav的基频上。这是最直观的校验方式也是我每次处理音频数据都不会跳过的步骤import matplotlib.pyplot as plt import numpy as np # 取前10秒 max_time 10.0 samples_limit int(sr * max_time) audio_part audio[:samples_limit] # 计算基频轨迹用librosa的pyin f0, voiced_flag, voiced_probs librosa.pyin( audio_part, fminlibrosa.note_to_hz(50), fmaxlibrosa.note_to_hz(90), srsr ) times librosa.times_like(f0, srsr) plt.figure(figsize(12, 4)) plt.plot(times, f0, label估计基频, colorblue, alpha0.7) # 叠加midi音符 for n in notes: if n[start] max_time: freq librosa.midi_to_hz(n[note]) plt.plot([n[start], n[end]], [freq, freq], r-, linewidth2) plt.xlabel(时间 (秒)) plt.ylabel(频率 (Hz)) plt.legend() plt.show()如果midi音符线和估计基频轨迹基本重合说明这套数据标注质量不错midi和wav是真实对齐的。如果很多地方对不上那就要小心了。要么是midi记录的是伴奏旋律而不是人声旋律要么是解析时的tempo处理有问题要么是wav本身存在录制偏移。看到midi线和基频轨迹大面积错位时先回3.3节排查代码别急着怀疑数据。4. 实操中一定会踩的坑与排查方法4.1 中文歌词乱码最经典的坑。歌词文件用记事本打开全是乱码多半是编码问题。文本文件常见三种编码UTF-8无BOM、UTF-8带BOM、GBK。Windows下很多工具默认用ANSI即GBKmacOS和Linux默认UTF-8。处理方式很简单用Python打开时逐个尝试编码即可def read_text(path): for enc in [utf-8, utf-8-sig, gbk, gb18030]: try: with open(path, r, encodingenc) as f: return f.read() except UnicodeDecodeError: continue raise ValueError(f无法解析文件: {path})gb18030是GBK的超集支持更多生僻字实战里比gbk更稳。这个函数我建议所有处理文本标注的脚本都加上提前消灭乱码问题。4.2 midi和wav时长对不上这个问题非常常见可能的成因有三个。第一我前面反复提到的tempo换算问题用了固定BPM导致节奏变化处偏移。第二midi文件里可能有多个track我上面为了方便只遍历了tracks[0]如果旋律线在别的track里解析结果就会缺内容。第三wav文件开头有前导静音midi的时间从0开始但wav的人声可能延迟到0.3秒甚至更晚才出现。针对前导静音可以用librosa做静音检测然后把偏移量量化出来import numpy as np def detect_first_sound(audio, sr, threshold0.02): frame_length int(sr * 0.02) # 20ms一帧 rms librosa.feature.rms( yaudio, frame_lengthframe_length, hop_lengthframe_length )[0] for i, val in enumerate(rms): if val threshold: return i * frame_length / sr return 0.0 offset detect_first_sound(audio, sr) print(检测到前导静音偏移:, offset, 秒)拿到偏移量后把midi音符的起止时间统一加上这个偏移量再跟wav对齐即可。这个逻辑很简单但能解决很多莫名其妙的错位。4.3 时长标注的单位与区间不统一有些标注里end字段可能不叫end叫dur有些可能只给start和duration不给end更离谱的是有些文件里的end可能小于start。清洗逻辑很简单一律转成统一的startend形式并校验endstart否则丢弃该行def normalize_segments(raw_segments): clean [] for seg in raw_segments: if end in seg: start, end float(seg[start]), float(seg[end]) elif duration in seg: start float(seg[start]) end start float(seg[duration]) else: continue if end start: clean.append({start: start, end: end, text: seg.get(text, )}) return clean这个逻辑一定要在做数据预处理时就固化不要每次跑代码都手动改。数据清洗是训练流程的第一步它不稳定后面全白搭。4.4 wav采样率不统一虽然大多数情况下数据集的wav规格应该是一致的但保不齐里面有单独一首歌是从其他设备导出的采样率可能是48kHz或32kHz。最好的办法是预处理时全部重采样到统一采样率用librosa.load的sr参数统一指定即可。如果你想严格保留44.1kHz的原生音质但某些文件恰好是48kHz那就需要统一重采样到44.1kHz。不要指望模型能自动适应不同采样率它只会把采样率差异当作音调变化学进去然后给你输出奇怪的结果。4.5 某编号下四件套不齐如果某个编号下面四件套没配齐我的建议是宁可把这个编号整条删掉也不要补齐一份乱凑的标注。训练数据宁缺毋滥一份错误标注对模型的伤害远大于缺一个样本。我做数据集清理时固定会跑一个完整性检查把缺文件的编号全部过滤掉import os ids [f.split(.)[0] for f in os.listdir(data/wav)] valid_ids [] for sid in ids: needed [ fdata/wav/{sid}.wav, fdata/mid/{sid}.mid, fdata/lyrics/{sid}.txt, fdata/timestamps/{sid}.json ] if all(os.path.exists(p) for p in needed): valid_ids.append(sid) print(f有效样本数: {len(valid_ids)} / {len(ids)})这段代码是数据准入的最低门槛。跑完之后把valid_ids存成一个文本文件或者JSON后续所有训练脚本都从这个名单里读数据而不是直接扫描目录。这样即使有人往目录里扔了不完整的文件也不会污染训练流程。5. 围绕这份数据集能做什么方向5.1 训练一个音高估计基线有了midi标注你可以拿它当作音高ground truth训练一个从频谱到midi音符的预测模型。最简单的做法是计算每帧的CQT或STFT特征把midi每秒转成一个目标标注序列然后用一个卷积网络做逐帧分类。100首歌虽然不算多但做一个能跑通全流程的最小demo完全够了。具体来说你可以对每个时间帧生成一个特征向量目标值是该帧对应的midi音符编号没有音符就是0然后做一个多分类任务。这个方向是理解MIR任务的好入口也是很多歌声翻唱检测、音高修正工具的基础。跑通这个小demo后你会对频谱如何映射到音高有非常直观的理解。5.2 做歌词边界检测与语音识别增强歌词和时间戳在这份数据里是现成的你可以直接拿来训练一个歌词边界检测器输入是wav特征输出是每一帧是否在歌词边界。如果以后拿到没有标注的歌声这个模型就能自动帮你分段生成新的训练数据。用这种方式迭代你可以把100首的小数据集扩到几百首甚至更多。这个过程在学术界叫self-training或者伪标注在数据稀缺的场景下非常实用。5.3 结合ffmpeg做一套完整的预处理管线如果你准备把数据喂给深度学习框架建议提前用ffmpeg统一做一遍重采样、格式转换和切片。虽然librosa也能做重采样但大量文件用ffmpeg批处理会更快而且不占Python内存# 统一重采样为22050Hz、单声道、16bit ffmpeg -i input.wav -ar 22050 -ac 1 -sample_fmt s16 output.wav # 截取前10秒 ffmpeg -i input.wav -t 10 output_10s.wavwav是一种极易处理的格式但数据集的原始wav不一定都是标准PCM有些可能是float32位深或者带特殊编码。用ffmpeg批量清洗一次后面再用librosa读取时就会非常顺滑。我之前在项目里就遇到过某个工具无法处理非标准wav的情况导致切片全成了静音最后全部重跑。别在这一步节省时间统一格式一定放在最前面做。5.4 数据增强与扩充100首原始数据不够用时数据增强是常规手段。对音频做音高平移可以改变音高速度变化可以改变节奏还可以叠加一点轻微的混响。不过对歌声合成类任务音高平移要非常小心因为它会改变人声的音符意图。更安全的方式是只做速度微调和轻微噪声注入或者用这个数据集训练的模型去标注其他无标注歌曲实现半监督扩充。从实操角度librosa只有少数几个内置的时域变换做更复杂的数据增强可以考虑专门的库但核心思路就一点增强后的音频必须保持标注有效。音高平移后midi标注也要跟着平移速度变化后时间戳和midi时长也要等比缩放。如果标注没有跟着变换那这批增强数据还不如不用。个人经验收尾我在这份数据集上最大的体会是拿到数据的第一步永远是检查而不是训练。四件套在一个zip里看起来简单真正对不齐的时候会让你怀疑人生。建议你拿到手之后先把3.3和3.4的代码跑一遍确认每一份样本都能完整解析再进入模型环节。否则辛苦调了一周模型最后发现数据有系统偏差那种挫败感我是体会过的。最后再分享一个小技巧处理这类多模态音频数据集时记得保留原始文件不要覆盖任何预处理都输出到单独目录。因为你永远不知道后面哪个环节需要回到原始标注重新算一遍。这个习惯在我自己项目里救过好几次命也推荐给你。本文还有配套的精品资源点击获取
返回列表