Wednesday, 7 October 2009

Looped Playback in .NET with NAudio

In this post I will explain how to seamlessly loop audio with NAudio. The first task is to create a WaveStream derived class that will loop for us. This class takes a source WaveStream, and in the override Read method, will loop back to the beginning once the source stream stops returning data. Obviously this requires that the source stream you pass in does in fact stop returning data. Another option would be to use the Length property of the source stream, and go back to the beginning once we have sent Length bytes. Here’s my implementation of LoopStream. I might put this into NAudio for the next release: (Update: have fixed a bug in the Read method, thanks Neverbith for spotting it. I will also possibly add a configuration to allow you to use the Source’s Length property as well)

/// <summary>
/// Stream for looping playback
/// </summary>
public class LoopStream : WaveStream
{
    WaveStream sourceStream;

    /// <summary>
    /// Creates a new Loop stream
    /// </summary>
    /// <param name="sourceStream">The stream to read from. Note: the Read method of this stream should return 0 when it reaches the end
    /// or else we will not loop to the start again.</param>
    public LoopStream(WaveStream sourceStream)
    {
        this.sourceStream = sourceStream;
        this.EnableLooping = true;
    }

    /// <summary>
    /// Use this to turn looping on or off
    /// </summary>
    public bool EnableLooping { get; set; }

    /// <summary>
    /// Return source stream's wave format
    /// </summary>
    public override WaveFormat WaveFormat
    {
        get { return sourceStream.WaveFormat; }
    }

    /// <summary>
    /// LoopStream simply returns
    /// </summary>
    public override long Length
    {
        get { return sourceStream.Length; }
    }

    /// <summary>
    /// LoopStream simply passes on positioning to source stream
    /// </summary>
    public override long Position
    {
        get { return sourceStream.Position; }
        set { sourceStream.Position = value; }
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        int totalBytesRead = 0;

        while (totalBytesRead < count)
        {
            int bytesRead = sourceStream.Read(buffer, offset + totalBytesRead, count - totalBytesRead);
            if (bytesRead == 0)
            {
                if (sourceStream.Position == 0 || !EnableLooping)
                {
                    // something wrong with the source stream
                    break;
                }
                // loop
                sourceStream.Position = 0;
            }
            totalBytesRead += bytesRead;
        }
        return totalBytesRead;
    }
}

Now using this to play a looping WAV file is trivial:

private WaveOut waveOut;

private void buttonStartStop_Click(object sender, EventArgs e)
{
    if (waveOut == null)
    {
        WaveFileReader reader = new WaveFileReader(@"C:\Music\Example.wav");
        LoopStream loop = new LoopStream(reader);
        waveOut = new WaveOut();
        waveOut.Init(loop);
        waveOut.Play();
     }
     else
     {
         waveOut.Stop();
         waveOut.Dispose();
         waveOut = null;
     }
}

16 comments:

Webmaster said...

Nice post man, Just let me know how to handle a sound file which has been zipped and transferred via LAN. .net Developers or some other suit I think there which can help with this.

Unknown said...

I'm planning a post on how to play back audio that is streaming over a network in the future, as it is a commonly asked question, and quite easy to do. Zipping audio would not be a good idea though. Better to use MP3.

Neverbith said...

Since this post is a month old my comment may be left unseen, but just saw it today.

Read has a small error on it:

if (bytesRead == 0)

It should be:

if (bytesRead == 0 || sourceStream.Position > sourceStream.Length)

For the cases where the stream size is smaller than the initial bytesRequired [on the first iteration].

Unknown said...

hi neverbith, I do mention this issue at the top of my post. this code assumes that your source stream stops returning data. Sometimes source streams do not have meaningful position / length information. But you are absolutely right, in some cases you will need that check.

Neverbith said...

But I don't think we talked about the same thing (or at least not from what I understand reading the article). The problem I'm describing is with small files.

When using them you already read the whole data on the first try, because of it, the offset never changes, but the position of the source stream changes, so bytes are always got, but they are empty.

I don't know if I'm explained myself well.

Unknown said...

ok I see what you mean. the code needs to update the offset with the total number of bytes read so far.

Benick said...

Hi Mark, this is really neat stuff...
id like to know how i can play sound from mic using wavestream_dataavailable EVENT to sound card without output file...my purpose is to send this buffer to network and client receives that buffer and plays using it.

Unknown said...

hi iamnotsmart,
You need to create a WaveStream (or IWaveProvider) derived class that buffers up the data recieved, and sends it out of its Read method. Then that WaveStream can be used for playback. You should be aware that latency is unlikely to be good

Benick said...

Hi Mark,
Thanks for the reply mark. But Im really new to NAudio, id like to know how i can play sound using WAVEOUT from BUFFER property of "e" on the DataAvailable Event. How do i push that Buffer inside the IWaveProvider derived class so that i can play that stream on WAVEOUT...or may be what i think is wrong...or should i make IWaveProvider with constructor that accepts buffer? and last what do i have to return from the Read implementation of IWaveProvider..hope im not disturbing you alot...thanks again.

Peter said...

Hi Mark,

I found a design problem with the LoopStream wrapper. The problem appears when using it with a Mixer output. The LoopStream Read() method looks for end of stream to be hit (0 bytes read), and then seeks to the beginning, but with a Mixer, the WaveChannel32 base stream never returns 0 bytes Read() because it is designed to pad the resulting read to always return data.

The result is that LoopStream can't loop, because it will not see the end of stream (sourceStream will never Read() and return 0 bytes). I think the fix is to have LoopStream detect when the Read will pass the end of stream boundary and then cobble together the proper result, but I haven't coded it up yet.

Any chance of you fixing this?

Unknown said...

hi Peter,
yes, this is a limitation of this approach, and is why I said that the source stream needs to stop returning data. The WaveChannel32 is designed as a mixer input and so should always return data even when past the end of its input (might reconsider this decision for a future version of NAudio). Looping should therefore be done before going into a WaveChannel32.

I do have another implementation of the loopstream that uses the Source stream's length and position to perform looping which I'll perhaps post at some point, although this approach would not work with an IWaveProvider.

Anonymous said...

The following code for Read in LoopStream together with WaveChannel32 and WaveMixer works, almost, because there's a strange behaviour:
a sample (duration ca. 4 seconds) gets looped 4-5 times correctly, but then the Read method is never called again.

int bytesRequired = (int)Math.Min(count, Length - Position);
int bytesRead = channelStream32.Read(buffer, offset, bytesRequired);

if (bytesRequired < count)
{
channelStream32.Position = 0;
channelStream32.Read(buffer, offset + bytesRead, count - bytesRequired);
}
return count;


Any idea ?
fritz

Alex said...

Can I change the pitch while playing back?

Unknown said...

there is no built-in pitch shifting support in NAudio I am afraid. Look at the Skype Voice Changer project for an example of how to do pitch shifting with NAudio

Anonymous said...

i think we might need to override the dispose method so we can call dispose on sourcestream as well?

Unknown said...

@Anonymous, yes you are right. That's one reason I typically prefer to make IWaveProvider to stop the need to pass through Position and Dispose all the way through the audio playback graph.