NAudio 的 BufferedWaveProvider 在录制和混合音频时会变满

NAudio's BufferedWaveProvider gets full when recording and mixing an audio

我在使用 NAudio 库中的 BufferedWaveProvider 时遇到问题。我正在录制 2 个音频设备(一个麦克风和一个扬声器),将它们合并为一个流并将其发送到编码器(用于视频)。

为此,我执行以下操作:

  1. 创建一个线程,我将使用 WasapiCapture 录制麦克风。
  2. 创建一个线程,我将在其中使用 WasapiLookbackCapture 录制扬声器的音频。 (我也使用 SilenceProvider 所以我的记录没有间隙)。
  3. 我想混合这 2 个音频,所以我必须确保它们具有相同的格式,所以我会检测所有这些音频设备中最好的 WaveFormat。在我的场景中,它是扬声器。所以我决定麦克风音频将通过 MediaFoundationResampler 以调整其格式,使其与扬声器的音频相同。
  4. 来自 Wasapi(Lookback)Capture 的每个音频块都被发送到 BufferedWaveProvider
  5. 然后,我还制作了一个 MixingSampleProvider,其中我从每个记录线程传递 ISampleProvider。所以我为麦克风传递 MediaFoundationResampler,为扬声器传递 BufferedWaveProvider
  6. 在第三个线程的循环中,我从 MixingSampleProvider 读取数据,它应该在填充时异步清空 BufferedWaveProvider(s)。
  7. 因为每个缓冲区可能不会完全同时被填满,我正在查看这两个缓冲区之间的最小公共持续时间是多少,并且我正在从混合样本提供程序中读取这个数量。
  8. 然后我将读取的内容排入队列,以便我的编码器在第 4 个线程中也将并行处理它。

请看下面的流程图,它说明了我上面的描述。

我的问题如下:

在调试模式下查看它,我可以看到:

根据我在 NAudio 作者的博客、文档和 Whosebug 上阅读的内容,我认为我正在做最佳实践(但我可能是错的),即从线程写入缓冲区,然后读取它与另一个平行。当然存在它被填充的速度比我阅读它的速度更快的风险,这基本上就是现在正在发生的事情。但是我不明白为什么。

需要帮助

我需要一些帮助来了解我在这里缺少的东西。以下几点让我感到困惑:

  1. 为什么这个问题只发生在 Discord/Skype/Teams 会议上?我正在使用的视频游戏也使用麦克风,所以我无法想象它像 another app is preventing the microphone/speakers to works correctly.

  2. 我同步两个录音机的启动。为此,我使用一个信号要求记录器启动,当它们都开始生成数据时(通过 DataAvailable 事件),我发送一个信号告诉它们用它们填充缓冲区会在下次活动中领取。它可能并不完美,因为两个音频设备在不同时间发送它们的 DataAvailable,但我们谈论的是 60 毫秒的最大差异(在我的机器上),而不是 5 秒。所以我不明白为什么它会被填满。

  3. 为了理解我在 #2 中所说的内容,我的遥测显示缓冲区正在以这种方式填充(值是虚拟的):

Microphone buffered duration: 0ms | Speakers: 0ms
Microphone buffered duration: 60ms | Speakers: 60ms
Microphone buffered duration: 0ms | Speakers: 0ms <= That's because I read the data from the mixing sample provider
Microphone buffered duration: 60ms | Speakers: 0ms <= Events may not be in sync, that's ok.
Microphone buffered duration: 120ms | Speakers: 60ms <= Alright, next loop, I'll extract 60ms on each buffer.
Microphone buffered duration: 390ms | Speakers: 0ms <= Wait, how?
Microphone buffered duration: 390ms | Speakers: 60ms
[...]
Microphone buffered duration: 5000ms | Speakers: 0ms <= Oh no :(

看来麦克风的缓冲区填充得更快了...但是为什么呢?可能是因为重采样器减慢了麦克风缓冲区的读取速度吗?如果是这样,它也应该减慢扬声器缓冲区的读取速度,因为我正在通过 MixingSampleProvider 读取它,不是吗?

如果有帮助的话,这里是我的代码的简化摘录:


/* THREAD #1 AND #2 */

_audioCapturer = new WasapiCapture(_device); // Or WasapiLookbackCapture + SilenceProvider playing
_audioCapturer.DataAvailable += AudioCapturer_DataAvailable;

// This buffer can host up to 5 second of audio, after that it crashed when calling AddSamples.
// So we should make sure we don't store more than this amount.
_waveBuffer = new BufferedWaveProvider(_audioCapturer.WaveFormat)
{
    DiscardOnBufferOverflow = false,
    ReadFully = false
};

if (DoINeedToResample)
{
    // Create a resampler to adapt the audio to the desired wave format.
    // In my scenario explained above, this happens for the Microphone.
    _resampler = new MediaFoundationResampler(_waveBuffer, targettedWaveFormat);
}
else
{
    // No conversion is required.
    // In my scenario explained above, this happens for the Speakers.
    _resampler = _waveBuffer;
}

        private void AudioCapturer_DataAvailable(object? sender, WaveInEventArgs e)
        {
            NotifyRecorderIsReady();
            if (!AllRecorderAreReady)
            {
                // Don't record the frame unless every other recorders have started to record too.
                return;
            }

            // Add the captured sample to the wave buffer.
            _waveBuffer.AddSamples(e.Buffer, 0, e.BytesRecorded);

            // Notify the "mixer" that a chunk has been recorded.

        }

/* The Mixer, in another class */


_waveProvider = new MixingSampleProvider(_allAudioRecoders.Select(r => r._resampler));
_allAudioRecoders.ForEach(r => r._audioCapturer.StartRecording());

Task _mixingTask = Task.CompletedTask;

        private void OnChunkAddedToBufferedWaveProvider()
        {
            if (_mixingTask.IsCanceled
                || _mixingTask.IsCompleted
                || _mixingTask.IsFaulted
                || _mixingTask.IsCompletedSuccessfully)
            {
                // Treat the buffered audio in parallel.

                _mixingTask = Task.Run(() =>
                {
                    /* THREAD #3 */
                    lock (_lockObject)
                    {
                        TimeSpan minimalBufferedDuration;
                        do
                        {
                            // Gets the common duration of sound that all audio recorder captured.
                            minimalBufferedDuration = _allAudioRecoders.OrderBy(t => t._waveBuffer.Ticks).First().BufferedDuration;

                            if (minimalBufferedDuration.Ticks > 0)
                            {
                                // Read a sample from the mixer.
                                var bufferLength = minimalBufferedDuration.TotalSeconds * _waveProvider!.WaveFormat.AverageBytesPerSecond;
                                var data = new byte[(int)bufferLength];
                                var readData = _waveProvider.Read(data, 0, data.Length);

                                // Send the data to a queue that will be treated in parallel by the encoder.
                            }
                        } while (minimalBufferedDuration.Ticks > 0);
                    }
                });
            }
        }

有没有人知道我做错了什么and/or为什么只有在 Discord/Skype/Teams 上通过语音聊天而不是在线多人游戏时才会重现?

提前致谢!

[更新] 2/9/2021

我可能已经发现了问题,但我不是 100% 确定如何处理它。 似乎我停止从麦克风接收数据,因此扬声器缓冲区已满。 (好像昨天,刚好相反)

[更新] 2/12/2021

这听起来像是出于某种原因,也许(我说也许是因为问题可能是其他原因)BufferedWaveProvider 在某些情况下阅读后不会自行清除。

让我想到的是:

  1. 在阅读 MixingSampleProvider 之前,我记录了多少缓冲持续时间我们有每个缓冲区。
  2. 我也是看完后记录下来的
  3. 大多数时候,这很好,我得到持续显示以下模式的持续数据,持续了十几分钟,甚至一个小时:
BEFORE READING MICROPHONE: 20ms
BEFORE READING SPEAKER: 10ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 0ms

// I don't explain why both buffer are empty considering my algorithm was supposed to read only 10ms, but the output MP4 seems fine and in sync, so it's fine? ...
  1. 然后突然间其中一个缓冲区将在 5 秒内被填满。
BEFORE READING MICROPHONE: 20ms
BEFORE READING SPEAKER: 10ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 0ms
BEFORE READING MICROPHONE: 10ms
BEFORE READING SPEAKER: 20ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 20ms
BEFORE READING MICROPHONE: 20ms
BEFORE READING SPEAKER: 30ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 30ms
BEFORE READING MICROPHONE: 10ms
BEFORE READING SPEAKER: 50ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 50ms
BEFORE READING MICROPHONE: 20ms
BEFORE READING SPEAKER: 70ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 70ms
BEFORE READING MICROPHONE: 20ms
BEFORE READING SPEAKER: 80ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 80ms
BEFORE READING MICROPHONE: 10ms
BEFORE READING SPEAKER: 100ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 100ms
BEFORE READING MICROPHONE: 20ms
BEFORE READING SPEAKER: 110ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 110ms
BEFORE READING MICROPHONE: 10ms
BEFORE READING SPEAKER: 130ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 130ms
[...]
BEFORE READING MICROPHONE: 20ms
BEFORE READING SPEAKER: 4970ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 4970ms
BEFORE READING MICROPHONE: 10ms
BEFORE READING SPEAKER: 4980ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 4980ms
BEFORE READING MICROPHONE: 20ms
BEFORE READING SPEAKER: 5000ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 5000ms
<!-- Crash -->

我可以通过在缓冲区开始不再同步时清除缓冲区来进行脏修复,但我真的很想了解为什么会发生这种情况,以及是否有更好的解决方法。

谢谢

[更新]#2

好的,我想我已经解决了这个问题。这可能是 NAudio 库中的错误。这是我所做的:

  1. 照常播放我的程序。
  2. 当其中一个缓冲区达到 5 秒(又名变满)时,停止填充该特定缓冲区。
  3. 通过这样做,我最终会遇到这样一种情况,即 1 个设备的缓冲区已被填满,而另一个设备的缓冲区未被填满,但我会尽可能地继续读取这些缓冲区。
  4. 这是我发现的:似乎已满的缓冲区大小在读取后从未减少,这解释了为什么它突然变满。不幸的是,它不一致,无法解释原因。

在 GitHub 上进行更多调查和 post:https://github.com/naudio/NAudio/issues/742

我发现我应该监听 MixingSampleProvider.MixerInputEnded 事件并在它发生时将 SampleProvider 重新添加到 MixingSampleProvider。

发生这种情况的原因是我在捕获音频的同时处理音频,有时我处理它的速度可能比录制它的速度快,因此 MixingSampleProvider 认为它没有更多可读取的内容并停止.所以我应该告诉它不,这还没有结束,它应该期待更多。