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

NAudio默认设备变更时出现随机崩溃与程序挂起问题求助

WinForms音频设备变更相关崩溃与挂起问题

我被这个问题困扰一整天了,整理了所有相关信息寻求解决方案:

我开发的WinForms应用里,有3个ComboBox绑定到MMDeviceEnumerator枚举的设备列表(两个输出设备框、一个输入设备框),同时通过MMDeviceEnumerator注册回调,在设备变更、移除或默认设备修改时触发事件,通过委托在窗体线程重新枚举设备并更新ComboBox,相关代码如下:

public void OnDefaultDeviceChanged(DataFlow dataFlow, Role deviceRole, string defaultDeviceId)
{
    Devices.OnDevicesUpdated();
}

// 该事件触发的处理方法
private void UpdateDeviceSelectors(object? sender = null, EventArgs? e = null)
{
    Invoke(delegate ()
    {
        int primaryIndex = Devices.PrimaryOutput + 1, secondaryIndex = Devices.SecondaryOutput + 2, microphoneIndex = Devices.Microphone + 1;
        Devices.Refresh();

        try
        {
            SecondaryOutputComboBox.SelectedIndex = secondaryIndex;
            PrimaryOutputComboBox.SelectedIndex = primaryIndex;
            MicrophoneSelectComboBox.SelectedIndex = microphoneIndex;
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex.Message);
        }
    });
}

目前存在两个疑似同源的问题:

  • 随机崩溃问题:切换ComboBox选项或修改默认设备时,程序会随机抛出System.ExecutionEngineException并崩溃,根源指向NAudio.Wasapi.dll;
  • 程序挂起问题:启用麦克风注入器(将WaveInEvent录制的选定输入设备音频重定向至WaveOutEvent实现回环)时,在上述设备更新委托中调用注入器的Stop再Start方法刷新设备,程序必然挂起,只能通过任务管理器强制终止。

麦克风注入器相关代码如下:

public void Start()
{
    if (Soundboard.Devices.SecondaryOutput == -2) return;

    micStream = new WaveInEvent();
    micStream.BufferMilliseconds = 50;
    micStream.WaveFormat = new WaveFormat(44100, WaveIn.GetCapabilities(Soundboard.Devices.Microphone).Channels);
    micStream.DeviceNumber = Soundboard.Devices.Microphone;

    WaveInProvider waveIn = new(micStream);
    var volumeSampleProvider = new VolumeSampleProvider(waveIn.ToSampleProvider());
    volumeSampleProvider.Volume = 1 + Settings.Default.MicrophoneGain;

    virtualCable = new WaveOutEvent();
    virtualCable.DesiredLatency = 150;
    virtualCable.DeviceNumber = Soundboard.Devices.SecondaryOutput;
    virtualCable.Init(volumeSampleProvider);

    micStream.StartRecording();
    virtualCable.Play();
}

public void Stop()
{
    try
    {
        if (micStream != null && virtualCable != null)
        {
            micStream.Dispose();
            micStream = null;

            virtualCable.Dispose();
            virtualCable = null;
        }
    }
    catch
    {
        micStream = null;
        virtualCable = null;
    }
}

我确定这两个问题同源,但找不到根因。因功能限制,无法切换到Wasapi、DirectSound或ASIO,必须基于Wave流解决。尝试过调整设备变更检测方式、修改麦克风注入器的调用位置(怀疑线程问题),都没有效果。


问题分析与解决方案

核心问题定位

两个问题的根源都是线程冲突+资源释放不规范:

  1. MMDeviceEnumerator的回调线程、WinForms UI线程、Wave流工作线程存在资源竞争;
  2. WaveInEvent/WaveOutEvent的Dispose操作直接在UI线程执行,未等待音频线程完成,导致死锁或内存访问错误;
  3. 设备刷新时先记录旧索引再枚举,旧索引可能已失效,异常未正确处理引发资源混乱。

具体修复步骤

1. 规范Wave流启停流程,避免线程死锁

修改Stop方法,先停止录制/播放,等待线程结束后再释放资源:

public void Stop()
{
    try
    {
        // 先停止录制,等待线程退出
        if (micStream != null)
        {
            micStream.StopRecording();
            Thread.Sleep(100);
            micStream.Dispose();
            micStream = null;
        }

        // 先停止播放,等待线程退出
        if (virtualCable != null)
        {
            virtualCable.Stop();
            Thread.Sleep(100);
            virtualCable.Dispose();
            virtualCable = null;
        }
    }
    catch (Exception ex)
    {
        Debug.WriteLine($"Stop failed: {ex.Message}");
        micStream = null;
        virtualCable = null;
    }
}

2. 设备刷新时避免索引失效,线程安全更新ComboBox

修改UpdateDeviceSelectors,先枚举设备再通过设备ID查找新索引,而非依赖旧索引:

private void UpdateDeviceSelectors(object? sender = null, EventArgs? e = null)
{
    // 用BeginInvoke避免阻塞回调线程
    BeginInvoke(delegate ()
    {
        Devices.Refresh();
        
        // 替换成你的设备模型类,通过ID匹配索引
        int primaryIndex = GetDeviceIndex(PrimaryOutputComboBox, Devices.PrimaryOutputDeviceId);
        int secondaryIndex = GetDeviceIndex(SecondaryOutputComboBox, Devices.SecondaryOutputDeviceId);
        int microphoneIndex = GetDeviceIndex(MicrophoneSelectComboBox, Devices.MicrophoneDeviceId);

        try
        {
            if (primaryIndex != -1) PrimaryOutputComboBox.SelectedIndex = primaryIndex;
            if (secondaryIndex != -1) SecondaryOutputComboBox.SelectedIndex = secondaryIndex;
            if (microphoneIndex != -1) MicrophoneSelectComboBox.SelectedIndex = microphoneIndex;
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Update combobox failed: {ex.Message}");
        }
    });
}

// 辅助方法:根据设备ID查找ComboBox中的对应索引
private int GetDeviceIndex(ComboBox comboBox, string deviceId)
{
    if (string.IsNullOrEmpty(deviceId)) return -1;
    for (int i = 0; i < comboBox.Items.Count; i++)
    {
        var device = comboBox.Items[i] as YourDeviceModel; // 替换为实际的设备模型类
        if (device?.Id == deviceId) return i;
    }
    return -1;
}

注意:需要让Devices类记录当前选中设备的ID,而非仅记录索引,避免设备变更后索引失效。

3. 隔离回调线程与Wave流操作

设备变更回调中通过后台线程处理注入器启停,避免阻塞UI和回调线程:

private bool isInjectorEnabled; // 记录注入器启用状态

public void OnDefaultDeviceChanged(DataFlow dataFlow, Role deviceRole, string defaultDeviceId)
{
    Task.Run(() =>
    {
        microphoneInjector.Stop();
        Thread.Sleep(200); // 等待设备枚举完成
        Devices.OnDevicesUpdated();
        if (isInjectorEnabled) microphoneInjector.Start();
    });
}

4. 禁用NAudio的Wasapi自动检测

程序启动时添加以下代码,避免Wasapi组件引发冲突:

// 强制使用Wave系列设备枚举器,禁用Wasapi自动加载
WaveOut.DeviceEnumerator = new WaveOutDeviceEnumerator();
WaveIn.DeviceEnumerator = new WaveInDeviceEnumerator();

验证要点

  • 测试设备插拔、默认设备修改,检查是否仍出现ExecutionEngineException;
  • 启用注入器后修改设备,验证程序是否还会挂起;
  • 多次切换ComboBox选项,确认稳定性。

内容的提问来源于stack exchange,提问作者Xavier

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 12:50:26