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

C# NAudio捕获音频电平仅在录音属性面板开启时生效的问题求助

问题描述

我在VS2015中使用NAudio NuGet包读取3个音频输入的当前电平,用于更新进度条及业务判断,但仅当打开声音控制面板并切换到录音选项卡时才能获取电平数据。

我参考过相关技术帖子仍无法解决,希望得到帮助或更好的实现方案。

我的实现步骤:

  1. 应用启动时枚举音频设备并添加到下拉框:
public void runGUISetup() { 
    //Check for ini file 
    if (File.Exists(iniFile)) { 
        // Set the IP Address of Vmix from INI File 
        if (VmixIPAddress.Value != "") { 
            tbVmixIPAddress.Text = VmixIPAddress.Value; 
        } else { } 
    } 
    try { 
        // Enumerate soundcards and add to combobox 
        MMDeviceEnumerator enumerator = new MMDeviceEnumerator(); 
        var devices = enumerator.EnumerateAudioEndPoints(DataFlow.All, DeviceState.Active); 
        cBoxSoundCard1.Items.AddRange(devices.ToArray()); 
        cBoxSoundCard2.Items.AddRange(devices.ToArray()); 
        cBoxSoundCard3.Items.AddRange(devices.ToArray()); 
        //Enumerate IP Local Addresses 
        IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName()); 
        //Populate the drop down list with local IP's 
        cbListeningServerIP.Items.AddRange(ipHostInfo.AddressList); 
    } catch (Exception erSoundcard) { 
        tbDebug.Text += erSoundcard.Message; 
    } 
}
  1. 通过定时器每10ms采集音频电平并存入移动平均值,以此更新进度条,但此部分仅在录音属性面板开启时生效:
// check audio level every 10ms and store in moving average 
private void tmrSC1_Tick(object sender, EventArgs e) { 
    if (appActive == true) { 
        if (cBoxSoundCard1.SelectedItem != null) { 
            var device1 = (NAudio.CoreAudioApi.MMDevice)cBoxSoundCard1.SelectedItem; 
            avg1.AddSample((device1.AudioMeterInformation.MasterPeakValue * 100)); 
        } else { } 
        if (cBoxSoundCard2.SelectedItem != null) { 
            var device2 = (NAudio.CoreAudioApi.MMDevice)cBoxSoundCard2.SelectedItem; 
            avg2.AddSample((device2.AudioMeterInformation.MasterPeakValue * 100)); 
        } else { } 
        if (cBoxSoundCard3.SelectedItem != null) { 
            var device3 = (NAudio.CoreAudioApi.MMDevice)cBoxSoundCard3.SelectedItem; 
            avg3.AddSample((device3.AudioMeterInformation.MasterPeakValue * 100)); 
        } else { } 
    } 
}

更新: 根据反馈,硬编码设备编号时可正常工作,但Wave与CoreAudioAPI的设备编号及描述存在差异,需匹配设备名称但担心可靠性。考虑使用CoreAudioAPI生成设备列表并启动录制,但不知如何实现,恳请帮助。

(注:原问题附带了一张显示选中CoreAudioAPI设备的UI截图)


解决方案

嘿,这个问题我碰到过好几次,本质是Windows音频会话的激活规则在搞鬼——我来给你一步步解决:

为什么只有打开录音面板才有效?

Windows的CoreAudioAPI有个规则:AudioMeterInformation只有当对应的音频设备处于活动状态(也就是有正在运行的音频捕获流)时,才会返回真实的电平数据。打开声音控制面板的录音选项卡时,系统会自动激活设备的音频会话,所以你能读到数据;但程序单独运行时,设备处于闲置状态,自然拿不到有效数据。

具体解决步骤

我们的核心思路是:用NAudio的CoreAudioAPI组件给选中的设备启动一个低开销的捕获流,不需要保存音频数据,只是让设备保持激活状态,这样AudioMeterInformation就能正常工作了。同时解决设备匹配的可靠性问题。

1. 优化设备枚举,确保精准匹配

首先修改设备枚举逻辑,只枚举录音设备(不需要枚举播放设备),并且直接把MMDevice对象加到下拉框,用设备的唯一ID来关联,避免名称/编号差异问题:

public void runGUISetup()
{
    // 原INI文件逻辑保留
    if (File.Exists(iniFile))
    {
        if (!string.IsNullOrEmpty(VmixIPAddress.Value))
        {
            tbVmixIPAddress.Text = VmixIPAddress.Value;
        }
    }

    try
    {
        MMDeviceEnumerator enumerator = new MMDeviceEnumerator();
        // 只枚举活跃的录音设备,更精准
        var captureDevices = enumerator.EnumerateAudioEndPoints(DataFlow.Capture, DeviceState.Active);
        
        // 将MMDevice对象直接加入下拉框,设置显示名称为设备友好名
        foreach (var device in captureDevices)
        {
            cBoxSoundCard1.Items.Add(device);
            cBoxSoundCard2.Items.Add(device);
            cBoxSoundCard3.Items.Add(device);
        }
        // 设置下拉框显示的字段
        cBoxSoundCard1.DisplayMember = "FriendlyName";
        cBoxSoundCard2.DisplayMember = "FriendlyName";
        cBoxSoundCard3.DisplayMember = "FriendlyName";

        // 原IP枚举逻辑保留
        IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName());
        cbListeningServerIP.Items.AddRange(ipHostInfo.AddressList);
    }
    catch (Exception erSoundcard)
    {
        tbDebug.Text += erSoundcard.Message;
    }
}

2. 为每个设备启动低开销捕获流

在窗体类中声明三个捕获流对象,用于激活设备:

private WasapiCapture _captureDevice1;
private WasapiCapture _captureDevice2;
private WasapiCapture _captureDevice3;

写一个通用的启动/停止捕获流的方法:

private void ActivateAudioDevice(MMDevice targetDevice, ref WasapiCapture captureInstance)
{
    // 先清理之前的捕获流
    if (captureInstance != null)
    {
        captureInstance.Stop();
        captureInstance.Dispose();
        captureInstance = null;
    }

    if (targetDevice == null) return;

    // 创建绑定到指定设备的WasapiCapture
    captureInstance = new WasapiCapture(targetDevice);
    // 只需要启动流,不需要处理音频数据,所以空实现DataAvailable事件
    captureInstance.DataAvailable += (s, e) => { };
    // 启动捕获,激活设备
    captureInstance.Start();
}

private void DeactivateAudioDevice(ref WasapiCapture captureInstance)
{
    if (captureInstance != null)
    {
        captureInstance.Stop();
        captureInstance.Dispose();
        captureInstance = null;
    }
}

然后给下拉框的选中事件绑定激活逻辑:

private void cBoxSoundCard1_SelectedIndexChanged(object sender, EventArgs e)
{
    var selectedDevice = cBoxSoundCard1.SelectedItem as MMDevice;
    ActivateAudioDevice(selectedDevice, ref _captureDevice1);
}

private void cBoxSoundCard2_SelectedIndexChanged(object sender, EventArgs e)
{
    var selectedDevice = cBoxSoundCard2.SelectedItem as MMDevice;
    ActivateAudioDevice(selectedDevice, ref _captureDevice2);
}

private void cBoxSoundCard3_SelectedIndexChanged(object sender, EventArgs e)
{
    var selectedDevice = cBoxSoundCard3.SelectedItem as MMDevice;
    ActivateAudioDevice(selectedDevice, ref _captureDevice3);
}

3. 优化电平采集逻辑(现在能正常工作了)

你的定时器采集逻辑不需要大改,现在设备已经被激活,AudioMeterInformation会返回真实的电平数据。另外可以优化代码的可读性:

private void tmrSC1_Tick(object sender, EventArgs e)
{
    if (!appActive) return;

    // 处理第一个设备
    if (cBoxSoundCard1.SelectedItem is MMDevice device1)
    {
        var peakLevel = device1.AudioMeterInformation.MasterPeakValue * 100;
        avg1.AddSample(peakLevel);
        // 更新进度条(示例)
        this.Invoke((Action)(() => progressBar1.Value = (int)Math.Round(avg1.CurrentAverage)));
    }

    // 处理第二个设备
    if (cBoxSoundCard2.SelectedItem is MMDevice device2)
    {
        var peakLevel = device2.AudioMeterInformation.MasterPeakValue * 100;
        avg2.AddSample(peakLevel);
        this.Invoke((Action)(() => progressBar2.Value = (int)Math.Round(avg2.CurrentAverage)));
    }

    // 处理第三个设备
    if (cBoxSoundCard3.SelectedItem is MMDevice device3)
    {
        var peakLevel = device3.AudioMeterInformation.MasterPeakValue * 100;
        avg3.AddSample(peakLevel);
        this.Invoke((Action)(() => progressBar3.Value = (int)Math.Round(avg3.CurrentAverage)));
    }
}

4. 程序退出时清理资源

记得在窗体关闭时停止捕获流,释放资源:

private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
    DeactivateAudioDevice(ref _captureDevice1);
    DeactivateAudioDevice(ref _captureDevice2);
    DeactivateAudioDevice(ref _captureDevice3);
}

额外优化建议

  • 把定时器间隔从10ms调整到30-50ms,音频电平的变化频率没那么高,太频繁的轮询会浪费CPU资源。
  • 可以改用AudioMeterInformation.OnLevelMeterChanged事件监听电平变化,不需要定时器轮询,效率更高:
    // 在激活设备时绑定事件
    captureInstance.Start();
    targetDevice.AudioMeterInformation.OnLevelMeterChanged += (s, args) =>
    {
        var peakLevel = targetDevice.AudioMeterInformation.MasterPeakValue * 100;
        avg1.AddSample(peakLevel);
        // 必须Invoke到UI线程更新控件
        this.Invoke((Action)(() => progressBar1.Value = (int)Math.Round(avg1.CurrentAverage)));
    };
    

这样修改后,你不需要打开声音控制面板,程序就能正常读取三个音频输入的电平数据了,而且设备匹配完全可靠,不会因为编号或描述的差异出问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 04:10:44