大家晚上好,我对多线程有意见。我正在使用一个并行的.foreach进行多个音频样本的收集。我希望能够同时进行收集。我正在做一种生产者-消费者模式。但制作人部分,也就是音频样本的采集是软弱无力的。
在每个并行线程中:
- 创建一个阻塞集合来收集音频样本
- 创建进度条以监控麦克风输入
- 最后是记录/收集音频输入的记录功能
我为每个进程创建了一个阻塞收集阵列,并使用naudio WaveInEvent从麦克风进行记录。
我面临的挑战是
- 当我最小化窗口时,程序不会恢复
- 有时程序挂起,有时需要一段时间才能挂起,但总的来说,响应能力一点也不好(Jerky(除此之外,这个程序运行良好
我能做些什么来获得更好的性能。
请检查下面的代码。感谢
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Data.OleDb;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.Win32;
using NAudio.Wave;
public partial class frmAudioDetector : Form
{
//static BlockingCollection<AudioSamples> realtimeSource;
BlockingCollection<AudioSamples>[] realtimeSource;
static WaveInEvent waveSource;
static readonly int sampleRate = 5512;
private void frmAudioDetector_Load(object sender, EventArgs e)
{
try
{
var tokenSource = new CancellationTokenSource();
TabControl.TabPageCollection pages = tabControl1.TabPages;
//Tabs pages.Count is 8
List<int> integerList = Enumerable.Range(0, pages.Count).ToList();
//Parallel.foreach for simultaneous threads at the same time
Parallel.ForEach<int>(integerList, i =>
{
realtimeSource[i] = new BlockingCollection<AudioSamples>();
var firstProgressBar = (from t in pages[i].Controls.OfType<ProgressBar>()
select t).FirstOrDefault();
var firstEmptyComboBox = (from c in pages[i].Controls.OfType<ComboBox>()
select c).FirstOrDefault();
int deviceNum = firstEmptyComboBox.SelectedIndex;
//create a separate task for each tab for recording
_ = Task.Factory.StartNew(() => RecordMicNAudio(deviceNum, firstProgressBar, i));
});
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
//Record audio or store audio samples
void RecordMicNAudio(int deviceNum, ProgressBar progressBar, int t)
{
waveSource = new WaveInEvent();
waveSource.DeviceNumber = deviceNum;
waveSource.WaveFormat = new NAudio.Wave.WaveFormat(rate: sampleRate, bits: 16, channels: 1);
waveSource.DataAvailable += (_, e) =>
{
// using short because 16 bits per sample is used as input wave format
short[] samples = new short[e.BytesRecorded / 2];
Buffer.BlockCopy(e.Buffer, 0, samples, 0, e.BytesRecorded);
// converting to [-1, +1] range
float[] floats = Array.ConvertAll(samples, (sample => (float)sample / short.MaxValue));
//collect realtime audio samples
realtimeSource[t].Add(new AudioSamples(floats, string.Empty, sampleRate));
//Display volume meter in progress bar below
float maxValue = 32767;
int peakValue = 0;
int bytesPerSample = 2;
for (int index = 0; index < e.BytesRecorded; index += bytesPerSample)
{
int value = BitConverter.ToInt16(e.Buffer, index);
peakValue = Math.Max(peakValue, value);
}
var fraction = peakValue / maxValue;
int barCount = 35;
if (progressBar.InvokeRequired)
{
Action action = () => progressBar.Value = (int)(barCount * fraction);
this.BeginInvoke(action);
}
else progressBar.Value = (int)(barCount * fraction);
};
waveSource.RecordingStopped += (_, _) => Debug.WriteLine("Sound Stopped! Cannot capture sound from device...");
waveSource.BufferMilliseconds = 1000;
waveSource.StartRecording();
}
}
从非UI线程访问pages[i].Controls
(例如来自Parallel.ForEach的线程池威胁(似乎是错误的。NAudio对象已经使用了事件驱动编程(您提供了DataAvailable
处理程序和RecordingStopped
处理程序(,因此不需要并行或在主UI线程之外的任何线程中进行设置工作。
我不会直接从DataAvailable
处理程序调用音量指示器(进度条(更新。相反,我会在Timer
记号上更新控件,只更新DataAvailable
处理程序中的共享变量。这是事件驱动的编程——除了波形源IO线程已经使用的线程或任务之外,不需要任何线程或任务。
例如:使用带有简单锁的变量。对这些数据的访问必须用锁来控制,因为DataAvailable
处理程序将在IO线程上调用以存储当前卷,但Timer Tick处理程序在UI线程上读取的值(您可以以适度的速率更新(肯定不会比屏幕刷新率快。每秒4或5次可能就足够频繁了。您的BufferMilliseconds
已经是1000毫秒(1秒(,所以您可能每秒只能获得一次采样缓冲区。
表单级别字段
object sharedAccess = new object();
float sharedVolume;
WaveInEvent DataAvailable处理程序
lock (sharedAccess) {
sharedVolume= ...;
}
计时器刻度处理程序
int volume = 0;
lock(sharedAccess) {
volume = sharedVolume;
}
progressBar.Value = /* something based on...*/ volume;
目标:
- 在UI线程上的所有事件处理程序中尽可能地做一些工作
- 在IO线程(
WaveInEvent.DataAvailable
处理程序(中尽可能少地执行工作 - 在必须发生同步时,尽量减少线程之间的同步(尽量在尽可能短的时间内阻止另一个线程(
有了音量计,我同样怀疑BlockingCollection
在调用realtimeSource[t].Add(new AudioSamples(floats, string.Empty, sampleRate));
时如何协调访问。谁在读取该集合中的?字节到达的IO线程和可能正在消耗这些样本的任何线程之间存在争用。
如果您只是记录样本,并且在记录完成之前不需要使用它们,那么您不需要任何特殊的同步或阻塞集合来将每个缓冲区累积到集合中。您可以将缓冲区添加到传统的列表中,前提是在录制完成之前,您不会从任何其他线程访问它。在管理对阻塞集合的访问时可能会产生不需要承担的开销。
当我看到你所做的所有格式转换时,我有点紧张:
- 从传入缓冲区到短路数组
- 从空头阵列到浮动阵列
- 从浮点数组到AudioSamples对象
- 在体积计算过程中,从传入缓冲区到Int16(为什么不使用重用短裤数组?(
似乎可以通过直接从传入格式到浮点数组来剪切缓冲区副本,并在不安全模式下对浮点数组或原始缓冲区数据的固定指针执行卷计算。