问题描述
我遇到了来自 NAudio 库的 BufferedWaveProvider 的问题。我正在录制 2 个音频设备(一个麦克风和一个扬声器),将它们合并为 1 个流并将其发送到编码器(用于视频)。
为此,我执行以下操作:
- 创建一个话题,我将在其中使用
WasapiCapture
录制麦克风。 - 创建一个线程,我将在其中使用
WasapiLookbackCapture
录制扬声器音频。 (我也使用SilenceProvider
,所以我记录的内容没有空白)。 - 我想混合这 2 个音频,所以我必须确保它们具有相同的格式,以便我检测所有这些音频设备中最好的 WaveFormat。在我的场景中,它是扬声器。因此,我决定麦克风音频将通过
MediaFoundationResampler
以调整其格式,使其与扬声器中的音频相同。 - 来自 Wasapi(Lookback)Capture 的每个音频块都被发送到
BufferedWaveProvider
。 - 然后,我还制作了一个
MixingSampleProvider
,其中我从每个录制线程传递ISampleProvider
。所以我要传递麦克风的MediaFoundationResampler
和扬声器的BufferedWaveProvider
。 - 在第三个线程的循环中,我从
MixingSampleProvider
读取数据,它应该在BufferedWaveProvider
(s) 被填满时异步清空它。 - 因为每个缓冲区可能不会完全在同一时间填充,我正在查看这两个缓冲区之间的最短公共持续时间是多少,并且我正在从混合样本提供程序中读取这个数量。
- 然后我将读取的内容放入队列,以便我的编码器在第 4 个线程中也将并行处理它。
请参阅下面的流程图,说明我上面的描述。
我的问题如下:
- 在玩也使用麦克风的视频游戏(用于在线多人游戏)时,将麦克风和扬声器录制超过 1 小时,效果非常好。没有崩溃。缓冲区一直很空。太棒了。
- 但出于某种原因,每次我尝试我的应用程序
during
discord、Skype 或 Teams 音频对话时,我都会立即(在 5 秒内)在BufferedWaveProvider.AppSamples
崩溃,因为缓冲区已满。
在调试模式下看它,我可以看到:
- 扬声器对应的缓冲区几乎是空的。它的音频平均最长为 100 毫秒。
- 与麦克风(我重新采样的那个)对应的缓冲区已满(5 秒)。
从我在 NAudio 的作者博客、文档和 StackOverflow 上阅读的内容来看,我认为我正在做最佳实践(但我可能是错的),即从线程写入缓冲区并并行读取从另一个。当然有一种风险,它比我阅读它的速度更快,这基本上就是现在正在发生的事情。但我不明白为什么。
需要帮助
我需要一些帮助来理解我在这里遗漏了什么,拜托。以下几点让我感到困惑:
-
为什么这个问题只会在 discord/Skype/Teams 会议中出现?我正在使用的视频游戏也在使用麦克风,所以我无法想象它像
another app is preventing the microphone/speakers to works correctly
之类的东西。 -
我同步了两个录音机的启动。这样做,我使用一个信号来要求记录器启动,当它们都开始生成数据时(通过
DataAvailable
事件),我发送一个信号告诉他们用它们填充缓冲区将在下次活动中收到。这可能并不完美,因为两个音频设备在不同的时间发送它们的DataAvailable
,但我们谈论的最大差异为 60 毫秒(在我的机器上),而不是 5 秒。所以我不明白为什么它会被填满。
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
在某些情况下无法在阅读后自行清除。
让我想到的是以下内容:
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? ...
- 然后突然一个缓冲区将在 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 库中的一个错误。这是我所做的:
- 像往常一样播放我的节目。
- 当其中一个缓冲区达到 5 秒(即变满)时,停止填充该特定缓冲区。
- 通过这样做,我最终会遇到这样一种情况:一台设备的缓冲区被填满,而另一台设备的缓冲区没有填满,但我会尽可能地继续读取这些缓冲区。
- 这是我发现的:看起来已满的缓冲区大小在读取后从未减少,这解释了为什么突然变满。不幸的是,这是不一致的,无法解释原因。
解决方法
在 GitHub 上进行更多调查和帖子:https://github.com/naudio/NAudio/issues/742
我发现我应该监听 MixingSampleProvider.MixerInputEnded
事件,并在它发生时将 SampleProvider 读入 MixingSampleProvider。
发生这种情况的原因是我在捕获音频的同时处理它,并且在某些时刻我可能会比我记录它更快地处理它,因此 MixingSampleProvider 认为它没有什么可读取并停止。所以我应该告诉它不,它还没有结束,它应该期待更多。