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

快速按下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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 20:50:15