【发布时间】:2021-05-12 19:57:50
【问题描述】:
我遇到了来自 NAudio 库的 BufferedWaveProvider 的问题。我正在录制 2 个音频设备(一个麦克风和一个扬声器),将它们合并为 1 个流并将其发送到编码器(用于视频)。
为此,我执行以下操作:
- 创建一个线程,我将使用
WasapiCapture录制麦克风。 - 创建一个线程,我将使用
WasapiLookbackCapture录制扬声器音频。 (我也使用SilenceProvider,所以我记录的内容没有空白)。 - 我想混合这两种音频,所以我必须确保它们具有相同的格式,所以我检测所有这些音频设备中最好的 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 秒。所以我不明白为什么它会被填满。 -
根据我在 #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);
}
});
}
}
有没有人知道我做错了什么和/或为什么只有在 Discord/Skype/Teams 上通过语音聊天而不是通过在线多人游戏时才会重现?
提前致谢!
[更新] 2021 年 2 月 9 日
我可能已经发现了问题,但我不能 100% 确定如何处理它。 似乎我停止从麦克风接收数据,因此扬声器缓冲区已满。 (好像昨天,正好相反)。
[更新] 2021 年 2 月 12 日
出于某种原因,这听起来像这样,也许(我说也许是因为问题可能是其他问题)BufferedWaveProvider 在某些情况下阅读后不会自行清除。
让我想到以下几点:
- 在阅读
MixingSampleProvider之前,我会记录我们拥有的每个缓冲区的缓冲持续时间。 - 我也是在阅读后记录下来的。
- 大多数时候,这很好,我得到持续的数据显示以下模式数十分钟,甚至一个小时:
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 秒(也就是已满)时,停止填充该特定缓冲区。
- 通过这样做,我最终会遇到这样一种情况,即 1 个设备的缓冲区被填满,而另一个设备的缓冲区没有被填满,但我会尽可能继续读取这些缓冲区。
- 这是我发现的:看起来已满的缓冲区的大小在读取后从未减少,这解释了为什么它突然变满了。遗憾的是不一致,无法解释原因。
【问题讨论】:
标签: c# audio audio-streaming naudio wasapi