C# NAudio捕获音频电平仅在录音属性面板开启时生效的问题求助
我在VS2015中使用NAudio NuGet包读取3个音频输入的当前电平,用于更新进度条及业务判断,但仅当打开声音控制面板并切换到录音选项卡时才能获取电平数据。
我参考过相关技术帖子仍无法解决,希望得到帮助或更好的实现方案。
我的实现步骤:
- 应用启动时枚举音频设备并添加到下拉框:
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; } }
- 通过定时器每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

