如何用NAudio获取MIDI事件时长及实现WinForms虚拟钢琴录制
一、正确计算MIDI事件时长并播放
你的原代码存在几个核心问题:
- 直接将
DeltaTime或NoteLength传入Thread.Sleep错误,这些值是MIDI ticks(时间刻度),不是实际毫秒数,必须结合MIDI文件的时间分辨率和**播放速度(Tempo)**转换 Thread.Sleep精度极低,会导致播放节奏偏差- 逻辑混乱,错误关联NoteOn/NoteOff事件,忽略了Tempo事件对播放速度的动态影响
以下是修正后的实现代码:
using NAudio.Midi; using System.Diagnostics; var midiFile = new MidiFile("你的MIDI文件路径"); var ticksPerQuarterNote = midiFile.DeltaTicksPerQuarterNote; long currentTempo = 500000; // 默认120BPM,对应每四分音符500000微秒 using (var midiOut = new MidiOut(0)) { var trackEvents = midiFile.Events.GetTrackEvents(1).ToList(); var stopwatch = Stopwatch.StartNew(); long totalAccumulatedTicks = 0; foreach (var midiEvent in trackEvents) { // 累加当前事件的时间刻度 totalAccumulatedTicks += midiEvent.DeltaTime; // 计算当前累计刻度对应的实际毫秒数 double targetMilliseconds = (currentTempo / 1000.0) * (totalAccumulatedTicks / (double)ticksPerQuarterNote); // 精确等待到目标时间点,避免Thread.Sleep的精度问题 while (stopwatch.Elapsed.TotalMilliseconds < targetMilliseconds) { Thread.Sleep(1); } // 处理速度变更事件,动态更新播放速度 if (midiEvent is TempoEvent tempoEvent) { currentTempo = tempoEvent.Tempo; continue; } // 处理NoteOn事件(注意:Velocity为0时等价于NoteOff) if (midiEvent is NoteOnEvent noteOn) { var message = noteOn.Velocity > 0 ? MidiMessage.StartNote(noteOn.NoteNumber, noteOn.Velocity, noteOn.Channel) : MidiMessage.StopNote(noteOn.NoteNumber, 0, noteOn.Channel); midiOut.Send(message.RawData); } // 处理NoteOff事件 else if (midiEvent is NoteOffEvent noteOff) { var message = MidiMessage.StopNote(noteOff.NoteNumber, noteOff.Velocity, noteOff.Channel); midiOut.Send(message.RawData); } // 发送其他MIDI事件(如音色切换、弯音等) else { midiOut.Send(midiEvent.RawData); } } }
二、WinForms虚拟钢琴MIDI录制实现
1. UI准备
在WinForms窗体上添加多个Button控件作为虚拟钢琴按键,给每个按钮的Tag属性绑定对应的MIDI音符编号(比如C4对应60,D4对应62),按钮文本可设置为音符名称(如"C4")。
2. 录制核心变量
using NAudio.Midi; using System.Diagnostics; private MidiFile _recordingMidi; private List<MidiEvent> _trackEvents; private Stopwatch _recordStopwatch; private long _lastRecordTicks; private const int _recordingTicksPerQuarterNote = 480; // 自定义录制时间分辨率 private long _currentRecordingTempo = 500000; // 默认120BPM
3. 初始化录制
private void StartRecording() { // 创建单轨道MIDI文件 _recordingMidi = new MidiFile(_recordingTicksPerQuarterNote, 1); _trackEvents = new List<MidiEvent>(); _recordStopwatch = Stopwatch.StartNew(); _lastRecordTicks = 0; // 添加初始速度事件 var initialTempoEvent = new TempoEvent(_currentRecordingTempo, 0); _trackEvents.Add(initialTempoEvent); }
4. 按键事件处理
按下按键(MouseDown事件)
private void PianoButton_MouseDown(object sender, MouseEventArgs e) { if (_recordStopwatch == null || !_recordStopwatch.IsRunning) return; var button = sender as Button; int noteNumber = (int)button.Tag; long elapsedMs = _recordStopwatch.ElapsedMilliseconds; // 计算从上一个事件到当前的时间刻度差 long deltaTicks = (long)(elapsedMs * _recordingTicksPerQuarterNote / (_currentRecordingTempo / 1000.0)) - _lastRecordTicks; // 创建NoteOn事件并添加到轨道 var noteOnEvent = new NoteOnEvent(deltaTicks, 1, noteNumber, 127, 0); _trackEvents.Add(noteOnEvent); _lastRecordTicks += deltaTicks; }
松开按键(MouseUp事件)
private void PianoButton_MouseUp(object sender, MouseEventArgs e) { if (_recordStopwatch == null || !_recordStopwatch.IsRunning) return; var button = sender as Button; int noteNumber = (int)button.Tag; long elapsedMs = _recordStopwatch.ElapsedMilliseconds; long deltaTicks = (long)(elapsedMs * _recordingTicksPerQuarterNote / (_currentRecordingTempo / 1000.0)) - _lastRecordTicks; // 创建NoteOff事件并添加到轨道 var noteOffEvent = new NoteOffEvent(deltaTicks, 1, noteNumber, 0, 0); _trackEvents.Add(noteOffEvent); _lastRecordTicks += deltaTicks; }
5. 停止录制并保存
private void StopAndSaveRecording(string savePath) { if (_recordStopwatch == null) return; _recordStopwatch.Stop(); // 将事件添加到MIDI文件轨道 _recordingMidi.Events.AddTrack(_trackEvents); // 保存MIDI文件 _recordingMidi.Save(savePath); // 重置录制状态 _recordStopwatch = null; _trackEvents = null; _recordingMidi = null; }
内容的提问来源于stack exchange,提问作者Наташка С подъезда
相关产品推荐
相关产品推荐

