Delphi中TTask.Run内for循环最后一个MIDI音符播放异常问题
MIDI播放最后一个音符时长异常的原因与解决
问题场景
我在Delphi中通过TTask.Run的匿名过程实现了一段MIDI音符播放代码,运行后发现最后一个音符的播放时长明显短于其他音符;当把循环内的Sleep(200)改为Sleep(100)时,最后一个音符甚至完全不发声。在关闭MIDI设备前添加Sleep(200)可以"修复"这个问题,但想知道背后的具体原因。
代码示例
type TMainForm = class(TForm) RandomBtn: TButton; procedure RandomBtnClick(Sender: TObject); end; var MainForm: TMainForm; implementation {$R *.dfm} uses MMSystem, Threading; function MIDIEncodeMessage(Msg, Param1, Param2: Integer): Integer; begin Result := Msg + (Param1 shl 8) + (Param2 shl 16); end; procedure Music(Notes: TArray<Integer>); const MIDI_DEVICE = 0; MIDI_NOTE_ON = $90; MIDI_NOTE_OFF = $80; var MO: HMIDIOUT; n: Integer; begin MidiOutOpen(@MO, MIDI_DEVICE, 0, 0, CALLBACK_NULL); for n in Notes do begin MidiOutShortMsg(MO, MIDIEncodeMessage(MIDI_NOTE_ON, n, 127)); Sleep(200); MidiOutShortMsg(MO, MIDIEncodeMessage(MIDI_NOTE_OFF, n, 127)); end; MidiOutClose(MO); end; procedure TMainForm.RandomBtnClick(Sender: TObject); begin TTask.Run( procedure begin Music([64, 66, 67, 69, 71]); end); end; end.
原因分析
- MIDI消息是异步处理的:调用
MidiOutShortMsg只是把消息投递到MIDI设备的缓冲区,系统不会等待消息实际执行完成就返回。最后一个音符的NOTE_OFF发送后,程序立刻调用MidiOutClose关闭设备,此时设备可能还没完成这个音符的播放流程,甚至缓冲区里的消息都没来得及被处理,导致音符播放不完整或完全没声音。 - 循环内Sleep的作用差异:前面的音符,
Sleep(200)是在NOTE_ON和NOTE_OFF之间等待,确保音符能播放200毫秒后再触发关闭。但最后一个音符的NOTE_OFF发送后没有等待,直接关闭设备,设备没有足够时间完成播放。当把Sleep缩短到100毫秒时,这个时间差变得更小,设备还没响应NOTE_ON就被终止,所以最后一个音符完全听不到。
解决方案
简单可靠的临时修复
在最后一个NOTE_OFF发送后,添加一段等于音符播放时长的等待,确保设备处理完所有消息后再关闭:
for n in Notes do begin MidiOutShortMsg(MO, MIDIEncodeMessage(MIDI_NOTE_ON, n, 127)); Sleep(200); MidiOutShortMsg(MO, MIDIEncodeMessage(MIDI_NOTE_OFF, n, 127)); end; Sleep(200); // 等待最后一个音符的关闭消息处理完成 MidiOutClose(MO);
专业的回调方式(进阶)
使用MIDI的回调机制跟踪消息处理状态,当所有消息都执行完毕后再关闭设备。这种方式不需要依赖Sleep,精度和可靠性更高,适合复杂的MIDI播放场景。
内容的提问来源于stack exchange,提问作者new-to-so
相关产品推荐
相关产品推荐

