Table of Contents

WaveStream, IWaveProvider and ISampleProvider

When you play audio with NAudio or construct a playback graph, you are typically working with either IWaveProvider or ISampleProvider interface implementations. This article explains the three main base interfaces and classes you will encounter in NAudio and when you might use them.

WaveStream

WaveStream was the first base class in NAudio, inherits from System.IO.Stream, and implements IWaveProvider in newer versions. It represents a stream of audio data, and its format can be determined by querying the WaveFormat property. Typically, it is also seekable (repositionable), which means that it is possible to get any sample of the stream at will.

Seeking (reposition) is implemented through the System.IO.Stream Length and Position properties. These properties are both measured in terms of bytes, not samples. When a WaveStream is seekable (repositionable), care must often be taken to seek (reposition) to a multiple of the BlockAlign of the WaveFormat. If, for example, the stream produces 16 bit samples, you should always seek (reposition) to an even numbered byte position.

Audio data are retrieved from a stream using the Read method which has the signature:

int Read(Span<byte> buffer);

This method is also inherited from System.IO.Stream, and works in the standard way. The buffer parameter is the buffer into which audio is expected to be written. Its Length property denotes how many bytes are expected to be written to it.

The return value of the Read method must provide the number of bytes that were read. This should never be more than buffer.Length bytes and can only be less if the end of the audio stream is reached. By convention, NAudio playback devices will stop playing when Read returns 0.

WaveStream is the base class for NAudio file reader classes such as WaveFileReader, Mp3FileReader, AiffFileReader and MediaFoundationReader. It is a good choice of base class because these inherently support repositioning. RawSourceWaveStream is also a WaveStream, and delegates repositioning requests down to its source stream.

For a more detailed look at all the methods on WaveStream, see this article.

IWaveProvider

The WaveStream class is not suitable for all possible audio cases, and implementing all the properties/methods it requires can seem like overkill. Additionally, not every audio primitive provides immutable samples (samples that are not changed during the lifetime of the stream). It is also not necessary to implement when the source is non-seekable (not repositionable). Other audio primitives are often wrapping other primitives to implement an effect or analyze the incoming samples.

For all these reasons described above, the IWaveProvider interface was created. It provides a much simpler and straightforward interface that simply has the Read method, and a WaveFormat property:

public interface IWaveProvider
{
    WaveFormat WaveFormat { get; }
    int Read(Span<byte> buffer);
}

The IWavePlayer interface only needs an IWaveProvider passed to its Init method in order to be able to play audio. WaveFileWriter.CreateWaveFile and MediaFoundationEncoder.EncodeToMp3 also only need an IWaveProvider to dump the audio out to a WAV file. So, in many cases you won't need to create a WaveStream implementation, just implement IWaveProvider and you have an audio source that can be played or rendered to a file.

BufferedWaveProvider is a good example of a IWaveProvider as it has no ability to reposition - it simply returns any buffered audio from its Read method.

Ending a buffered stream, and spotting glitches

BufferedWaveProvider is usually the boundary between a real-time producer (a capture callback or a network thread calling AddSamples) and a pull-based pipeline calling Read. With ReadFully = true (the default) it pads any underrun with silence, so it never returns 0 and the stream never ends: playback won't raise PlaybackStopped, and WaveFileWriter.CreateWaveFile would run forever.

When the producer has no more audio, call CompleteAdding(). Read then returns whatever is still buffered, ends with one short read (not padded, even with ReadFully), and returns 0 from then on, which is what NAudio's read contract expects of a finished source. AddSamples after CompleteAdding throws InvalidOperationException. Completion is permanent: ClearBuffer does not undo it (clearing a completed buffer just ends the stream straight away), so create a new BufferedWaveProvider for a new stream.

var buffered = new BufferedWaveProvider(waveIn.WaveFormat);
waveIn.DataAvailable += (s, a) => buffered.AddSamples(a.Buffer, 0, a.BytesRecorded);
waveIn.RecordingStopped += (s, a) => buffered.CompleteAdding();

Two counters show what the padding hides. UnderrunSilenceBytes is the total silence Read has inserted because the buffer ran dry (including reads made before any audio arrived), and DiscardedBytes is the total incoming audio thrown away because the buffer was full and DiscardOnBufferOverflow is set. They are counters rather than events because Read normally runs on the audio thread, where an event handler could make the glitch worse; poll them from a timer or UI thread instead, and call ResetCounters() to start counting again.

MixingSampleProvider has the same issue with ReadFully = true, and the same fix: CompleteAdding() means no more inputs will be added, so once the current inputs have all ended the mixer returns a short read and then 0. AddMixerInput after that throws InvalidOperationException.

ISampleProvider

While implementing IWaveProvider is a useful approach to implement an audio effect, it has the caveat that decoding the samples depends of the bit depth (number of bits per sample) declared on the WaveFormat. And, it can be difficult to get the samples as numbers and cover all the possible bit depths.

While the IWaveProvider is able to represent a lot of audio formats, just doing any signal processing suddenly becomes one of the hardest tasks to do.

For these reasons, the ISampleProvider interface was created.

This particular interface makes it easy to process the samples because its Read method provides 32-bit IEEE floating-point samples:

public interface ISampleProvider
{
    WaveFormat WaveFormat { get; }
    int Read(Span<float> buffer);
}

The WaveFormat will always be 32 bit floating point, but the number of channels or sample rate may of course vary by implementation.

The Read method's buffer parameter specifies the buffer to place the processed samples into. The buffer's Length property denotes how many samples are expected to be written to it.

Finally, the return value indicates how many samples were written to the buffer. You should return 0 to signal that you do not have any other data to provide.

To summarize, the ISampleProvider is a great base interface to use when you are implementing any kind of an audio effect.

In the Read method you typically read from your source ISampleProvider, then modify the floating point samples, before returning them. Here's the implementation of the Read method in VolumeSampleProvider showing how simple this can be:

public int Read(Span<float> buffer)
{
    int samplesRead = source.Read(buffer);
    if (volume != 1f)
    {
        for (int n = 0; n < samplesRead; n++)
        {
            buffer[n] *= volume;
        }
    }
    return samplesRead;
}

To make the development of effects easier, NAudio provides built-in converters so that the transition from an IWaveProvider to an ISampleProvider (and vice-versa) is easy. For example, to convert any IWaveProvider to an ISampleProvider instance, you use the ToSampleProvider extension method. You can also get back to an IWaveProvider at any time you want by using the ToWaveProvider extension method. There is the ToWaveProvider16 extension method too if you want specifically to get PCM 16 bit integer samples.

You can also use AudioFileReader which reads a wide variety of file types and implements ISampleProvider.