This project has moved. For the latest updates, please go here.

Looping of an MP3

Feb 17, 2009 at 9:07 PM
I am trying to implement looping of an MP3 played via WaveOut, and seeking advice for the best way to do so.

My first thought was to handle the PlaybackStopped event and replay from the beginning.  however, that event never seems to be raised. In WaveOut.cs the only method that calls RaisePlaybackStoppedEvent is the Callback method, and that would happen only if buffer.OnDone() returns false.  But buffer.OnDone seems to always return true for my MP3.

So, two questions:
- Since PlaybackStopped is a new (experimental?) addition to the interface, is it the correct way to loop an MP3, or is there a better way?
- Any idea why OnDone() always returns false?

with many thanks in advance
Rob
Coordinator
Feb 17, 2009 at 10:13 PM
Hi Rob,
PlaybackStopped is probably not the best mechanism for looping.

OnDone returns false because the source WaveStream's Read method didn't return 0 bytes. A lot of the WaveStreams in NAudio will always return the number of bytes requested. One simple way to implement looping is to create a new derived WaveStream that in its Read method simply calls the Read method of your source WaveStream (i.e. the one reading from MP3). Then if its Position goes beyond Length and you want to loop, set the source Position back to the start, otherwise return zero.

Here is some very rough example code (sorry I am rushing this post a little)

class LoopingWaveStream : WaveStream
{

....

int Read(byte[] buffer, int offset, int count)
{
    if(source.Position > source.Length)
   {
        if(IsLooping)
            source.Position = 0;
       else
           return 0;
   }
   return source.Read(buffer, offset, count);
}

}

HTH
Mark

Feb 17, 2009 at 10:29 PM
Edited Feb 17, 2009 at 10:30 PM
Mark, this is extremely helpful, thank you. I am basing my code off of the NAudio sample player app.

I have:

                WaveStream mp3Reader = new Mp3FileReader(fileName);
                WaveStream pcmStream = WaveFormatConversionStream.CreatePcmStream(mp3Reader);
                WaveStream blockAlignedStream = new BlockAlignReductionStream(pcmStream);
                inputStream = new WaveChannel32(blockAlignedStream);

Would you replace one of these streams with the LoopingWaveStream?  The outer WaveChannel32 stream perhaps? Or would it be best to wrap the whole thing in another LoopingWaveStream derived as per your code above?
Coordinator
Feb 18, 2009 at 5:54 AM
Hi Rob,

Yes, the LoopingWaveStream would be another wrapper around the top level WaveChannel32.

An alternate approach to looping which I have used on occasion is to simply have a timer on your main form that periodically checks to see if Position > Length and resets it if it is so (or stops if you don't want to loop). Quite simple and effective, although if you absolutely must have no gap in the audio you should go with the LoopingWaveStream (and even so the sample code I gave you doesn't ensure perfect looping - to get that the Read method would need to potentially read a bit from the end, move to the start, and then read a bit more.

Mark
Feb 18, 2009 at 3:42 PM
Edited Feb 18, 2009 at 4:14 PM
Hi Mark,

Your first suggestion was correct - I am trying to create 'perfect' looping right down to the sample, and am willing to invest in extra plumbing to get it.  I also need Volume control, so I created a LoopingWaveChannel32 to wrap the WaveChannel32 as per your first suggestion.

All properties and methods in my LoopingWaveChannel32 pass through to the WaveChannel32, except for the Read method, which is essentially as you suggested.

However, we don't seem to be getting perfect looping.  On the wrapped WaveChannel32, I am setting PadWithZeros to false.  If PadWithZeros were true, I would expect "imperfect" looping (some samples zeroed out at the end), and to be to occasionally be hitting this block in WaveChannel32's Read method:

            // 3. Fill out with zeroes
            if (PadWithZeroes && bytesWritten < numBytes)
            {
                Array.Clear(destBuffer, offset + bytesWritten, numBytes - bytesWritten);
                bytesWritten = numBytes;
            }

I would have thought that sometimes bytesWritten should be < numBytes -- but my custom logic in LoopingWaveChannel32 would handle that situation by looping back around to the beginning of the sound.

Do I need to dig deeper to prevent zero-padding at the end of the sound?

with many thanks again
Rob


[edit] Since writing this note, I've hit a breakpoint set within the if block above twice - once where it required an additional 8 bytes, and once where the bytesWritten was approx. half the size of numBytes.  Is it possible that I have been testing with MP3s of a length that just happens to occasionally perfectly fill the buffer?
Coordinator
Feb 19, 2009 at 12:57 PM
Hi Rob,

You are probably also using the WaveFormatConversionStream, which can also perform zero padding in some circumstances, due to the difficulties of getting MP3 playback working reliably. You could try taking that out of WaveFormatConversionStream and seeing what it does.

Mark