You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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,提问作者Наташка С подъезда

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 03:01:04