IWavePlayer interface in NAudio features an event called
PlaybackStopped. The original idea behind this event was simple: you start playing a file (e.g. pass a
WaveOut.Init and then call
Play), and when it reaches its end you are informed, allowing you to dispose the input stream and playback device if you wanted to, or maybe start playing something else.
The reality was not quite so simple, and I have ended up trying to discourage users of the NAudio library from making use of the
PlaybackStopped event. In this post I will explain some of the reasons behind that and explain how I hope to restore the usefulness of
PlaybackStopped in future versions of NAudio.
The Never-Ending Stream Problem
The way that each implementor of
IWavePlayer determines whether it should automatically stop playback is when the
Read method on the source
IWaveProvider returns 0 (In fact Read should always return the count parameter unless the end of the stream has been reached).
However, there are some
IWaveProviders in NAudio that never stop returning audio. This isn’t a bug – it is quite normal behaviour for some scenarios. Perhaps
BufferedWaveProvider is the best example – it will return a zero-filled buffer if there is not enough queued data for playback. This is useful for streaming from the network where data might not be arriving fast enough but you don’t want playback to stop. Similarly
WaveChannel32 has a property called
PadWithZeroes allowing you to turn this behaviour on or off, which can be useful for scenarios where you are feeding into a mixer.
The Threading Problem
This is one of the trickiest problems surrounding the
PlaybackStopped event. If you are using
WaveOut, the chances are you are using windowed callbacks, which means that the
PlaybackStopped event is guaranteed to fire on the GUI thread. Since only one thread makes calls to the
waveOut APIs, it is also completely safe for the event handler to make other calls into
waveOut, such as calling
Dispose, or starting a new playback.
DirectSound and WASAPI, the
PlaybackStopped event is fired from a background thread we create. Even more problematic are
WaveOut function callbacks and ASIO, where the event is raised from a thread from deep within the OS / soundcard device driver. If you make any calls back into the driver in the handler for the
PlaybackStopped event you run the risk of deadlocks or errors. You also don’t want to give the user a chance to do anything that might take a lot of time in that context.
This problem almost caused me to remove
PlaybackStopped from the
IWavePlayer interface altogether. But I have decided to see if I can give it one last lease of life by using the .NET
SynchronizationContext class. The
SynchronizationContext class allows us easily in both WPF and WinForms to invoke the
PlaybackStopped event on the GUI thread. This greatly reduces the chance of something you do in the handler causing a problem.
The Has it Really Finished Problem
The final problem with
PlaybackStopped is another tricky one. How do you know when playback has stopped? You know when you have reached the end of the source file, since the
Read method returns 0. And you know when you have given the last block of audio to the soundcard. But the audio may not have finished playing yet, particularly if you are working at high latency. The old
WaveOut implementation in particular was guilty of raising
PlaybackStopped too early.
One workaround requiring no changes to the existing
IWavePlayer implementations would be to create a
LeadOutWaveProvider class, deriving from
IWaveProvider. This would do nothing more than append a specified amount of silence onto the end of your source stream, ensuring that it plays completely. Here’s a quick example of how that could be implemented:
private int silencePos = 0;
private int silenceBytes = 8000;
public int Read(byte buffer, int offset, int count)
int bytesRead = source.Read(buffer,offset,count);
if (bytesRead < count)
int silenceToAdd = Math.Min(silenceBytes – silencePos, count – bytesRead);
bytesRead += silenceToAdd;
silencePos += silenceToAdd;
Goals for NAudio 1.5
Fixing all the problems with
PlaybackStopped may not be fully possible in the next version, but my goals are as follows:
- Every implementor of
PlaybackStopped(this is done)
PlaybackStoppedshould be automatically invoked on the GUI thread if at all possible using
SynchronizationContext(this is done)
- It should be safe to call the
PlaybackStoppedhandler (currently testing and bugfixing)
PlaybackStoppedshould not be raised until all audio from the source stream has finished playing (done for
WaveOut– we now raise it when there are no queued buffers, will need to code review other classes to decide if this is the case).
- Keep the number of never-ending streams/providers in NAudio to a minimum and try to make it very clear which ones have this behaviour.