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

问题描述

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

为此,我执行以下操作:

  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 个线程中也将并行处理它。

请参阅下面的流程图,说明我上面的描述。

enter image description here

我的问题如下:

  • 在玩也使用麦克风的视频游戏(用于在线多人游戏)时,将麦克风和扬声器录制超过 1 小时,效果非常好。没有崩溃。缓冲区一直很空。太棒了。
  • 但出于某种原因,每次我尝试我的应用程序 during discord、Skype 或 Teams 音频对话时,我都会立即(在 5 秒内)在 BufferedWaveProvider.AppSamples 崩溃,因为缓冲区已满。

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

  • 扬声器对应的缓冲区几乎是空的。它的音频平均最长为 100 毫秒。
  • 与麦克风(我重新采样的那个)对应的缓冲区已满(5 秒)。

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

需要帮助

我需要一些帮助来理解我在这里遗漏了什么,拜托。以下几点让我感到困惑:

  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,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,data.Length);

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

有没有人知道我做错了什么和/或为什么只有在 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. 通过这样做,我最终会遇到这样一种情况:一台设备的缓冲区被填满,而另一台设备的缓冲区没有填满,但我会尽可能地继续读取这些缓冲区。
  4. 这是我发现的:看起来已满的缓冲区大小在读取后从未减少,这解释了为什么突然变满。不幸的是,这是不一致的,无法解释原因。

解决方法

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

我发现我应该监听 MixingSampleProvider.MixerInputEnded 事件,并在它发生时将 SampleProvider 读入 MixingSampleProvider。

发生这种情况的原因是我在捕获音频的同时处理它,并且在某些时刻我可能会比我记录它更快地处理它,因此 MixingSampleProvider 认为它没有什么可读取并停止。所以我应该告诉它不,它还没有结束,它应该期待更多。