快速按下MIDI键盘时NAudio应用不稳定的问题排查与修复
MIDI快速按键时程序不稳定的问题排查与修复
核心问题分析
你的程序在快速按键时出现静音、音频同时播放等异常,核心原因是音频资源管理混乱、未处理MIDI停止信号、阻塞式等待逻辑错误,以及线程安全隐患。
具体修改点
1. 修复方法调用参数不匹配
原代码调用PlayWavFileAsync(fileName);时缺少noteNumber参数,与方法定义不符,需补充参数:
PlayWavFileAsync(fileName, noteNumber);
2. 用players字典管理播放器实例
players字典的设计目的是关联音符与对应的播放器,需实现以下逻辑:
- 新增
NoteOff事件处理,收到停止信号时立即终止对应音符的播放并释放资源 - 播放前检查字典中是否已有该音符的播放器,避免重复创建;若有则先停止旧实例
- 用锁保护字典的多线程访问,避免回调线程与播放线程冲突
3. 替换固定时长阻塞为异步等待播放完成
原代码Task.Delay(1000).Wait();是固定阻塞1秒,不管音频实际长度,导致音频提前释放或播放不完整。应监听WaveOutEvent.PlaybackStopped事件,异步等待播放结束:
var tcs = new TaskCompletionSource<bool>(); player.PlaybackStopped += (s, args) => tcs.SetResult(true); player.Play(); await tcs.Task;
4. 移除回调中频繁获取文件夹的逻辑
folder = TelegramBot.SetFolder();放在MIDI回调中会导致每次按键都重新获取文件夹,不仅影响性能,还可能引发线程安全问题。建议通过SetMessageText方法外部控制文件夹更新,回调中仅使用已初始化的folder变量。
5. 正确管理音频资源生命周期
原代码中AudioFileReader被using包裹,会在固定等待结束后立即释放,导致播放器失去数据源而静音。需确保播放器停止后再释放资源,或调整资源持有逻辑。
修改后的完整代码示例
using NAudio.Wave; using NAudio.Midi; using System; using System.Collections.Generic; using System.Threading.Tasks; namespace MidiReader { public class MidiPlayer { private string folder; private Dictionary<int, string> noteMappings; private MidiIn midiIn; private Dictionary<int, WaveOutEvent> players; private readonly object playersLock = new object(); public void SetMessageText(string text) { folder = text; } public MidiPlayer(Dictionary<int, string> noteMappings, string folder) { this.noteMappings = noteMappings; this.midiIn = new MidiIn(3); this.midiIn.MessageReceived += MidiIn_MessageReceived; this.folder = folder; this.players = new Dictionary<int, WaveOutEvent>(); } public void Start() { this.midiIn.Start(); Console.WriteLine("Press any key to stop."); Console.ReadKey(); // 停止所有播放器并释放资源 lock (playersLock) { foreach (var player in players.Values) { player.Stop(); player.Dispose(); } players.Clear(); } midiIn.Stop(); midiIn.Dispose(); } private void MidiIn_MessageReceived(object sender, MidiInMessageEventArgs e) { var midiMessage = e.MidiEvent; if (midiMessage.CommandCode == MidiCommandCode.NoteOn) { var noteOnEvent = (NoteOnEvent)midiMessage; // 兼容部分设备用NoteOn+Velocity=0表示NoteOff的情况 if (noteOnEvent.Velocity == 0) { StopNote(noteOnEvent.NoteNumber); return; } int noteNumber = noteOnEvent.NoteNumber; if (noteMappings.ContainsKey(noteNumber)) { string fileName = Path.Combine(folder, noteMappings[noteNumber]); Console.WriteLine(fileName); _ = PlayWavFileAsync(fileName, noteNumber); } } else if (midiMessage.CommandCode == MidiCommandCode.NoteOff) { var noteOffEvent = (NoteOffEvent)midiMessage; StopNote(noteOffEvent.NoteNumber); } } private void StopNote(int noteNumber) { lock (playersLock) { if (players.TryGetValue(noteNumber, out var player)) { player.Stop(); player.Dispose(); players.Remove(noteNumber); } } } public async Task PlayWavFileAsync(string fileName, int noteNumber) { Console.WriteLine("Logged into PlayWavFile"); WaveOutEvent player = null; AudioFileReader audioFile = null; try { audioFile = new AudioFileReader(fileName); player = new WaveOutEvent(); player.Init(audioFile); // 加入字典管理,先清理旧实例 lock (playersLock) { if (players.ContainsKey(noteNumber)) { players[noteNumber].Stop(); players[noteNumber].Dispose(); players.Remove(noteNumber); } players[noteNumber] = player; } Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("Started playing"); Console.ResetColor(); // 异步等待播放完成 var tcs = new TaskCompletionSource<bool>(); void PlaybackStoppedHandler(object s, StoppedEventArgs args) { tcs.SetResult(true); player.PlaybackStopped -= PlaybackStoppedHandler; StopNote(noteNumber); audioFile?.Dispose(); } player.PlaybackStopped += PlaybackStoppedHandler; player.Play(); await tcs.Task; } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"播放错误: {ex.Message}"); Console.ResetColor(); // 异常时清理资源 player?.Dispose(); audioFile?.Dispose(); lock (playersLock) { players.Remove(noteNumber); } } } } }
额外说明
- 新增
playersLock锁,确保字典在多线程环境下的访问安全 - 兼容部分MIDI设备用
NoteOn+Velocity=0模拟NoteOff的情况,避免遗漏停止信号 - 播放完成后自动清理资源,避免内存泄漏
- 文件夹更新逻辑移至外部,避免回调线程中的不必要操作
内容的提问来源于stack exchange,提问作者olegtutaev
相关产品推荐
相关产品推荐

