使用Direct Sound和C#将声音从麦克风传输到扬声器时,如何避免静音滴答声?

时间:2011-11-22 17:34:55

标签: c# .net directx noise directsound

我尝试使用 DirectSound 麦克风中的声音样本传输到我的扬声器 C#即可。它应该类似于“听麦克风”,但后来我想用它来做其他事情。通过测试我的方法,我注意到背景中的无声的滴答声,破解噪音。我猜这与写入和播放缓冲区之间的延迟有关,这个延迟必须大于写入块的延迟。

如果我将录制和播放之间的延迟设置为小于50毫秒。它大部分都有效,但有时候我会发出非常大的噪音。所以我决定延迟至少50ms。这对我来说没问题,但系统“听设备”的延迟似乎要短得多。我猜它大概是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();

虽然捕获ReportChunk会将数据作为可订阅的事件传递给发言人。 发言人 部分初始化如下:

        // 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);将已排队的块写入缓冲区,并关注bufferSizechunkSize以及wp,它们代表最后一个写入位置。我认为这是我的代码重要的一切。也许缺少定义,至少还有另一种方法可以启动/停止=控制线程。

我已发布此代码,以防我在填充缓冲区时出错,或者我的初始化错误。但我猜这个问题的发生是因为C#字节码的执行太慢或类似的事情。但最后我的问题仍然存在:我的问题是如何减少延迟以及如何避免噪音不应该存在?

2 个答案:

答案 0 :(得分:1)

我知道你的问题的原因以及你可以解决它的方式,但我无法在C#和.Net中实现它,所以我会解释它,希望你能找到自己的方式。

麦克风将录制音频。以指定的频率(例如44100)然后以相同的采样率(再次44100)在声卡上播放,问题是计算输入设备中的时间的晶体(例如麦克风)与晶体不同在声卡中播放声音。 差异也是如此之小,它们不一样(整个世界中没有2个完全相同的水晶),所以一段时间之后,你的播放程序就会出现间隙。

现在解决方案是重新采样数据以匹配输出的采样率但我不知道如何在C#和.Net中做到这一点

答案 1 :(得分:0)

很久以前我发现,这个问题是由Thread.Sleep(1);和高CPU使用率引起的。由于windows timerresolution默认为15,6ms,因此此睡眠并不意味着睡眠1ms,而是睡眠直到达到下一个时钟中断。 (更多阅读this paper)结合高CPU使用率,它可能会累积到一个块的长度甚至更多。

例如:如果我的chunksize是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缓冲区的正确位置。在这种情况下,需要重新同步。