如何在使用DirectSound和C#将声音从麦克风流式传输到扬声器时避免无声的滴答声
本文关键字:扬声器 传输 风流 滴答声 无声 麦克风 麦克 DirectSound 声音 | 更新日期: 2023-09-27 18:15:30
我尝试使用DirectSound和C#将声音样本从麦克风流式传输到扬声器。它应该类似于"听麦克风",但后来我想用它做其他事情。通过测试我的方法,我注意到背景中有无声的鸣叫声和爆裂声。我想这与写入和播放缓冲区之间的延迟有关,该延迟必须大于写入块的延迟。
如果我将录制和播放之间的延迟设置为小于50ms。大多数情况下它是有效的,但有时我真的会听到很大的爆裂声。所以我决定至少延迟50毫秒。这对我来说还可以,但系统"监听设备"的延迟似乎要短得多。我猜它大约是15-30ms,几乎不引人注意。对于50ms,我至少得到了一点混响效果。
在以下内容中,我将向您展示我的麦克风代码(部分(:初始化是这样完成的:
capture = new Capture(device);
// Creating the buffer
// Determining the buffer size
bufferSize = format.AverageBytesPerSecond * bufferLength / 1000;
while (bufferSize % format.BlockAlign != 0) bufferSize += 1;
chunkSize = Math.Max(bufferSize, 256);
bufferSize = chunkSize * BUFFER_CHUNKS;
this.bufferLength = chunkSize * 1000 / format.AverageBytesPerSecond; // Redetermining the buffer Length that will be used.
captureBufferDescription = new CaptureBufferDescription();
captureBufferDescription.BufferBytes = bufferSize;
captureBufferDescription.Format = format;
captureBuffer = new CaptureBuffer(captureBufferDescription, capture);
// Creating Buffer control
bufferARE = new AutoResetEvent(false);
// Adding notifier to buffer.
bufferNotify = new Notify(captureBuffer);
BufferPositionNotify[] bpns = new BufferPositionNotify[BUFFER_CHUNKS];
for(int i = 0 ; i < BUFFER_CHUNKS ; i ++) bpns[i] = new BufferPositionNotify() { Offset = chunkSize * (i+1) - 1, EventNotifyHandle = bufferARE.SafeWaitHandle.DangerousGetHandle() };
bufferNotify.SetNotificationPositions(bpns);
捕获将在一个额外的线程中像这样运行:
// Initializing
MemoryStream tempBuffer = new MemoryStream();
// Capturing
while (isCapturing && captureBuffer.Capturing)
{
bufferARE.WaitOne();
if (isCapturing && captureBuffer.Capturing)
{
captureBuffer.Read(currentBufferPart * chunkSize, tempBuffer, chunkSize, LockFlag.None);
ReportChunk(applyVolume(tempBuffer.GetBuffer()));
currentBufferPart = (currentBufferPart + 1) % BUFFER_CHUNKS;
tempBuffer.Dispose();
tempBuffer = new MemoryStream(); // Reset Buffer;
}
}
// Finalizing
isCapturing = false;
tempBuffer.Dispose();
captureBuffer.Stop();
if (bufferARE.WaitOne(bufferLength + 1)) currentBufferPart = (currentBufferPart + 1) % BUFFER_CHUNKS; // That on next start the correct bufferpart will be read.
stateControlARE.Set();
在捕获CCD_ 1时,将数据作为可以订阅的事件发送给扬声器。扬声器部分初始化如下:
// Creating the dxdevice.
dxdevice = new Device(device);
dxdevice.SetCooperativeLevel(hWnd, CooperativeLevel.Normal);
// Creating the buffer
bufferDescription = new BufferDescription();
bufferDescription.BufferBytes = bufferSize;
bufferDescription.Format = input.Format;
bufferDescription.ControlVolume = true;
bufferDescription.GlobalFocus = true; // That sound doesn't stop if the hWnd looses focus.
bufferDescription.StickyFocus = true; // - " -
buffer = new SecondaryBuffer(bufferDescription, dxdevice);
chunkQueue = new Queue<byte[]>();
// Creating buffer control
bufferARE = new AutoResetEvent(false);
// Register at input device
input.ChunkCaptured += new AInput.ReportBuffer(input_ChunkCaptured);
数据通过事件方法放入队列,简单地通过:
chunkQueue.Enqueue(buffer);
bufferARE.Set();
填充回放缓冲区和启动/停止回放缓冲区由另一个线程完成:
// Initializing
int wp = 0;
bufferARE.WaitOne(); // wait for first chunk
// Playing / writing data to play buffer.
while (isPlaying)
{
Thread.Sleep(1);
bufferARE.WaitOne(BufferLength * 3); // If a chunk is played and there is no new chunk we try to continue and may stop playing, else may the buffer runs out.
// Note that this may fails if the sender was interrupted within one chunk
if (isPlaying)
{
if (chunkQueue.Count > 0)
{
while (chunkQueue.Count > 0) wp = writeToBuffer(chunkQueue.Dequeue(), wp);
if (buffer.PlayPosition > wp - chunkSize * 3 / 2) buffer.SetCurrentPosition(((wp - chunkSize * 2 + bufferSize) % bufferSize));
if (!buffer.Status.Playing)
{
buffer.SetCurrentPosition(((wp - chunkSize * 2 + bufferSize) % bufferSize)); // We have 2 chunks buffered so we step back 2 chunks and play them while getting new chunks.
buffer.Play(0, BufferPlayFlags.Looping);
}
}
else
{
buffer.Stop();
bufferARE.WaitOne(); // wait for a filling chunk
}
}
}
// Finalizing
isPlaying = false;
buffer.Stop();
stateControlARE.Set();
writeToBuffer
简单地通过this.buffer.Write(wp, data, LockFlag.None);
将排队的块写入缓冲器,并且关心bufferSize
和chunkSize
以及wp
,这表示最后的写入位置。我认为这是我的代码中最重要的部分。也许缺少定义,至少还有另一个方法可以启动/停止=控制线程。
我发布了这段代码,以防我在填充缓冲区时出错或初始化错误。但我想这个问题的出现是因为C#字节码的执行太慢或类似的原因。但最终我的问题仍然悬而未决:我的问题是如何减少延迟,以及如何避免不应该存在的噪音
我知道问题的原因和解决方法,但我无法在C#和中实现它。Net,所以我会解释一下,希望你能找到自己的路。
音频将由您的麦克风录制。以指定的频率(例如44100(,然后以相同的采样率(再次为44100(在声卡上播放,问题是在输入设备(例如麦克风(中计数时间的晶体与在声卡中播放声音的晶体不同。而且差异太小了,它们不一样(整个世界上没有两个完全相同的水晶(,所以过一段时间后,你的播放程序就会有差距。
现在的解决方案是重新采样数据以匹配输出的采样率,但我不知道如何在C#和中做到这一点。净
很久以前,我发现这个问题是由Thread.Sleep(1);
和高CPU使用率共同引起的。由于windows timerresolution默认为15.6ms,因此此睡眠并不意味着睡眠1ms,而是睡眠到下一个时钟中断。(欲了解更多信息,请阅读本文(再加上高CPU使用率,它可能会堆积到一块甚至更多的长度。
例如:如果我的块大小是40ms,这可能是大约46.8ms(3*15.6ms(,这会导致票证。一种解决方案是将分辨率设置为1ms。可以这样做:
[DllImport("winmm.dll", EntryPoint="timeBeginPeriod", SetLastError=true)]
private static extern uint timeBeginPeriod(uint uiPeriod);
[DllImport("winmm.dll", EntryPoint="timeEndPeriod", SetLastError=true)]
private static extern uint timeEndPeriod(uint uiPeriod);
void routine()
{
Thead.Sleep(1); // May takes about 15,6ms or even longer.
timeBeginPeriod(1); // Should be set at the startup of the application.
Thead.Sleep(1); // May takes about 1, 2 or 3 ms depending on the CPU usage.
// ... time depending routines goes here ...
timeEndPeriod(1); // Should end at application shutdown.
}
据我所知,这应该已经由directx完成了。但由于此设置是全局设置,应用程序的其他部分或其他应用程序可能会更改它。如果应用程序设置并撤消一次设置,则不应该发生这种情况。但不知何故,它似乎是由任何脏的编程部件或其他正在运行的应用程序引起的。
还有一件需要注意的事情是,如果你出于任何原因跳过了一个区块,你是否仍在使用directx缓冲区的正确位置。在这种情况下,需要重新同步。