naudio 的 midifile 类可直接加载并遍历 midi 音符事件,需调用 gettrackevents() 获取轨道事件,筛选 noteonevent(含力度为0的)和 noteoffevent,以 (channel, notenumber) 为键配对起止时间,注意格式0/1差异及 deltaticks 换算。

用 MidiFile 类(NAudio)直接加载并遍历音符事件
NAudio 是 C# 中最稳定、文档最清晰的 MIDI 解析库,MidiFile 类能正确处理标准格式 0 和格式 1 的 .mid 文件。它不播放音频,只解析结构,适合提取音符起止、通道、力度等原始数据。
关键点:必须调用 GetTrackEvents() 获取每个轨道的事件列表,再筛选出 NoteOnEvent 和 NoteOffEvent;注意 NoteOnEvent 力度为 0 时等效于 NoteOffEvent(这是 MIDI 标准行为,不是 bug)。
- 安装 NAudio:
dotnet add package NAudio - 读取后需检查
MidiFile.Format判断是格式 0(单轨合并)还是格式 1(多轨分离),影响遍历逻辑 - 时间戳单位是
DeltaTicks,需结合MidiFile.DeltaTicksPerQuarterNote换算为真实时间(如需秒级精度) - 避免直接遍历
Events属性——它已被弃用,应使用GetTrackEvents()
如何识别和配对 NoteOn/NoteOff(含通道与音高)
一个音符由“按下”和“释放”两个事件组成,但 MIDI 允许它们出现在不同轨道、甚至同一轨道中顺序错乱(尤其格式 1 多轨文件)。不能简单按顺序两两配对。
实操建议:先收集所有 NoteOnEvent(力度 > 0),用 (Channel, NoteNumber) 作键存入字典,值为起始 tick;遇到同键的 NoteOnEvent(力度 = 0)或 NoteOffEvent 时,立即取出并生成完整音符片段(start tick、end tick、duration、velocity)。
-
NoteOnEvent的Data1是音高(0–127),Data2是力度(0–127) -
NoteOffEvent的Data2是释放速度(常为 0 或忽略),实际不用 - 某些设备导出的文件可能缺失
NoteOffEvent,只靠力度为 0 的NoteOnEvent,务必同时监听这两类
处理常见错误:空轨道、无音符事件、格式不支持
打开 .mid 却读不到音符?大概率不是代码错,而是文件本身问题。NAudio 不会抛异常,但 GetTrackEvents() 可能返回空集合或只含 MetaEvent(如 TextEvent、TempoEvent)。
- 先检查
MidiFile.Tracks数量是否 ≥ 1,再逐个调用GetTrackEvents(i)确认非空 - 打印前几条事件的
GetType().Name和ToString(),确认是否存在NoteOnEvent - 极少数老游戏或硬件导出的格式 2 文件(多序列独立播放)不被 NAudio 支持,会静默跳过——此时
MidiFile.Format返回 2,需换用DryWetMIDI库 - 若只有
ProgramChangeEvent没有音符,说明该轨道只是设置音色,实际演奏在别轨
DryWetMIDI 作为 NAudio 的补充方案(支持格式 2 和更细粒度控制)
当 NAudio 无法满足需求(比如要精确到微秒的时间戳、处理 SysEx 事件、或解析格式 2),DryWetMIDI 是更现代的选择。它 API 更直观,事件模型更严格区分类型,且默认将 NoteOn(力度=0)自动转为 NoteOff。
- 安装:
dotnet add package Melanchall.DryWetMidi - 核心类是
MidiFile.Read()→GetNotes(),一行就能拿到全部音符对象(含TimeSpan起止时间) - 注意:它的
TimeSpan基于文件内 tempo map 计算,比手动换算DeltaTicks更准,但内存占用略高 - 若需修改再写回文件,
DryWetMIDI支持完整读-改-写流程,NAudio 则只读不写
真正难的不是“怎么读”,而是判断哪条轨道承载主旋律、如何对齐多轨时间、以及处理 tempo 变化导致的节奏偏移——这些都得结合具体文件结构做逻辑判断,没有通用解法。









