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;
}
}
Comments
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.
Purohit DI'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.
Mark HSince this post is a month old my comment may be left unseen, but just saw it today.
NeverbithRead 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].
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.
Mark HBut 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.
NeverbithWhen 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.
ok I see what you mean. the code needs to update the offset with the total number of bytes read so far.
Mark HHi Mark, this is really neat stuff...
iamnotsmartid 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.
hi iamnotsmart,
Mark HYou 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
Hi Mark,
iamnotsmartThanks 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.
Hi Mark,
PeterI 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?
hi Peter,
Mark Hyes, 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.
The following code for Read in LoopStream together with WaveChannel32 and WaveMixer works, almost, because there's a strange behaviour:
Anonymousa 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
Can I change the pitch while playing back?
Alexthere 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
Mark Hi think we might need to override the dispose method so we can call dispose on sourcestream as well?
Anonymous@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.
Mark HThank you
AnonymousHow can I achieve this with an ISampleProvider? I am using SignalGenerators like an additive synthesizer and adding the final result to your VarispeedSampleProvider. My initial approach involves converting the ISampleProdiver to a WaveProvider, then exporting to a wav file. I read the file back into a WaveStream, but I want to avoid the extra overhead of writing to disk (real-time playback). Using a memory stream instead of a file stream results in memory overflow.
Jyumai DarnIn VB.net, how to apply a mp3 music playback loop?
Pi DiI can not find the code that must be added allowing me to play this mp3 music loops under imports Naudio.Wave.
Could you help me ? Thank you.
Codes :
Imports System.IO.File
Imports System.IO
Imports NAudio.Wave
'MusicBackground
Private Sub Form_Main_Loaded(sender As Object, e As RoutedEventArgs) Handles Form_Main.Loaded
Dim mp3File As MemoryStream = New MemoryStream(My.Resources.My_Music_mp3)
Dim mp3Reader As Mp3FileReader = New Mp3FileReader(mp3File)
Dim waveOut As WaveOut = New WaveOut
waveOut.Init(mp3Reader)
waveOut.Play()
Sharing for future readers. I have managed to use LoopStream with WaveStream, using In-memory stream, filled from ISampleProvider:
David Veselyvar audioFileReader = new AudioFileReader(fileName);
var audioContent = .... // Use whatever you want ISampleProviders on the original audio file
var waveProvider = audioContent.ToWaveProvider();
var memoryStream = new MemoryStream();
WaveFileWriter.WriteWavFileToStream(memoryStream, waveProvider);
memoryStream.Position = 0; // Important for WaveFileReader to be able to read the header chunk bytes
var waveReader = new WaveFileReader(memoryStream);
var loopStreamSampleProvider = new LoopStream(waveReader);
Just beware for correct disposing to prevent memory leaks.