Your MixingSampleProvider is ReadFully = true, which means Read() would always returns the number of samples requested(which is over 0), with rest of the buffer beside actual data zero filled.
so your loop
while (_isRecording)
{
try
{
int read = finalProvider.Read(buffer, 0, buffer.Length);
if (read > 0)
_writer?.Write(buffer, 0, read);
else
Thread.Sleep(10);
}
catch { }
}
would write unending sequence of 0s without delay, with tiny amount of real samples sprinkled.
And new byte[finalProvider.WaveFormat.AverageBytesPerSecond / 10] is 100ms worth of read buffer while Thread.Sleep(10) sleeps for about 10ms. Your buffer would underrun even if you did set MixingSampleProvider.ReadFully = false.
And story doesn't end there.
BufferedWaveProvider is also ReadFully = true by default. Not only it produces garbage 0s on buffer underrun, but also it produces trailing 0s sometimes maybe because of some numerical error while doing sample rate change I guess.
It also should be set to false.
MixingSampleProvider removes sources from its source list when requested bytes count and read bytes count mismatches. Seems that's for detecting end of stream when source is file-backed, but for real-time input like playback capture, it needs extra caution not to underrun buffer.
Before starting main loop I put one second of delay to have some time to fill enough data on buffers to prevent underrun.
Also I used System.Diagnostics.Stopwatch to measure exact time it takes for each loop and use it to write the output.
This is my PoC code.
I'm new to NAudio too so I don't know it's good for long-term use.
At least it works on my machine.
using NAudio.Wave;
using NAudio.Wave.SampleProviders;
using System.Diagnostics;
string resultPath = Path.Combine(Path.GetTempPath(), $"{DateTime.Now:yyyyMMdd_HHmmss}.wav");
Console.WriteLine(resultPath);
WasapiLoopbackCapture outCapture = new();
WaveInEvent inCapture = new();
BufferedWaveProvider outBuffer = new(outCapture.WaveFormat)
{
DiscardOnBufferOverflow = true,
BufferDuration = TimeSpan.FromSeconds(30),
ReadFully = false,
};
BufferedWaveProvider inBuffer = new(inCapture.WaveFormat)
{
DiscardOnBufferOverflow = true,
BufferDuration = TimeSpan.FromSeconds(30),
ReadFully = false,
};
outCapture.DataAvailable += (_, a) => outBuffer?.AddSamples(a.Buffer, 0, a.BytesRecorded);
inCapture.DataAvailable += (_, a) => inBuffer?.AddSamples(a.Buffer, 0, a.BytesRecorded);
const int SampleRate = 16000;
ISampleProvider outSP = new WdlResamplingSampleProvider(outBuffer.ToSampleProvider(), SampleRate).ToMono();
ISampleProvider inSP = new WdlResamplingSampleProvider(inBuffer.ToSampleProvider(), SampleRate).ToMono();
MixingSampleProvider mixer = new([outSP, inSP]) { ReadFully = false };
IWaveProvider mixedWaveProvider = mixer.ToWaveProvider16();
WaveFileWriter resultWriter = new(resultPath, mixedWaveProvider.WaveFormat);
outCapture.StartRecording();
inCapture.StartRecording();
CancellationTokenSource cts = new();
CancellationToken token = cts.Token;
_ = Task.Run(async () =>
{
byte[] buffer = new byte[mixedWaveProvider.WaveFormat.AverageBytesPerSecond];
await Task.Delay(TimeSpan.FromSeconds(1));
Stopwatch stopwatch = Stopwatch.StartNew();
await Task.Delay(TimeSpan.FromSeconds(0.5));
while (token.IsCancellationRequested == false)
{
long elapsedMs = stopwatch.ElapsedMilliseconds;
stopwatch.Restart();
int readByteCount = mixedWaveProvider.Read(buffer, 0, Math.Min(buffer.Length, mixedWaveProvider.WaveFormat.AverageBytesPerSecond * (int)elapsedMs / 1000));
resultWriter.Write(buffer, 0, readByteCount);
await Task.Delay(TimeSpan.FromSeconds(0.5));
}
});
Console.WriteLine("Press any key to continue...");
Console.ReadKey();
cts.Cancel();
outCapture.StopRecording();
inCapture.StopRecording();
resultWriter.Flush();
resultWriter.Close();