NAudio supplies wrappers for four different audio output APIs. In addition, some of them support several different modes of operation. This can be confusing for those new to NAudio and the various Windows audio APIs, so in this article I will explain what the four main options are and when you should use them.
We’ll start off by discussing the common interface for all output devices. In NAudio, each output device implements IWavePlayer
, which has an Init
method into which you pass the Wave Provider that will be supplying the audio data. Then you can call Play
, Pause
and Stop
which are pretty self-explanatory, except that you need to know that Play
only begins playback.
You should only call Init
once on a given instance of an IWavePlayer
. If you need to play something else, you should Dispose
of your output device and create a new one.
You will notice there is no capability to get or set the playback position. That is because the output devices have no concept of position – they just read audio from the IWaveProvider
supplied until it reaches an end, at which point the PlaybackStopped
event is fired. Alternatively, you can ignore PlaybackStopped
and just call Stop
whenever you decide that playback is no longer required.
You may notice a Volume
property on the interface that is marked as [Obsolete]
. This was marked obsolete because it is not supported on all device types, but most of them do.
Finally there is a PlaybackState
property that can report Stopped
, Playing
or Paused
. Be careful with Stopped though, since if you call the Stop
method, the PlaybackState
will immediately go to Stopped
but it may be a few milliseconds before any background playback threads have actually exited.
WaveOutEvent
should be thought of as the default audio output device in NAudio. If you don’t know what to use, choose WaveOutEvent
. It essentially wraps the Windows waveOut
APIs, and is the most universally supported of all the APIs.
The WaveOutEvent
(or WaveOut
) object allows you to configure several things before you get round to calling Init
. Most common would be to change the DeviceNumber
property. –1 indicates the default output device, while 0 is the first output device (usually the same in my experience). To find out how many WaveOut
output devices are available, query the static WaveOut.DeviceCount
property.
You can also set DesiredLatency
, which is measured in milliseconds. This figure actually sets the total duration of all the buffers. So in fact, you could argue that the real latency is shorter. In a future NAudio, I might reduce confusion by replacing this with a BufferDuration
property. By default the DesiredLatency
is 300ms, which should ensure a smooth playback experience on most computers. You can also set the NumberOfBuffers
to something other than its default of 2 although 3 is the only other value that is really worth using.
One complication with WaveOut
is that there are several different "callback models" available. Understanding which one to use is important. Callbacks are used whenever WaveOut
has finished playing one of its buffers and wants more data. In the callback we read from the source wave provider and fill a new buffer with the audio. It then queues it up for playback, assuming there is still more data to play. As with all output audio driver models, it is imperative that this happens as quickly as possible, or your output sound will stutter.
Event callback is the default and recommended approach if you are using waveOut APIs, and this is implemented in the WaveOutEvent
class unlike the other callback options which are accessed via the WaveOut
class.
The implementation of event callback is similar to WASAPI and DirectSound. A background thread simply sits around filling up buffers when they become empty. To help it respond at the right time, an event handle is set to trigger the background thread that a buffer has been returned by the soundcard and is in need of filling again.
This is a good approach if you are creating a WaveOut
object from the GUI thread of a Windows Forms or WPF application. Whenever WaveOut
wants more data it posts a message that is handled by the Windows message pump of an invisible new window. You get this callback model by default when you call the empty WaveOut
constructor. However, it will not work on a background thread, since there is no message pump.
One of the big benefits of using this model (or the Existing Window model) is that everything happens on the same thread. This protects you from threading race conditions where a reposition happens at the same time as a read.
note: The reason for using a new window instead of an existing window is to eliminate bugs that can happen if you start one playback before a previous one has finished. It can result in WaveOut picking up messages it shouldn’t.
Existing Window is essentially the same callback mechanism as New Window, but you have to pass in the handle of an existing window. This is passed in as an IntPtr to make it compatible with WPF as well as WinForms. The only thing to be careful of with this model is using multiple concurrent instances of WaveOut as they will intercept each other’s messages (I may fix this in a future version of NAudio).
note: with both New and Existing Window callback methods, audio playback will deteriorate if your windows message pump on the GUI thread has too much other work to do.
Function callback was the first callback method I attempted to implement for NAudio, and has proved the most problematic of all callback methods. Essentially you can give it a function to callback, which seems very convenient, these callbacks come from a thread within the operating system.
To complicate matters, some soundcards really don’t like two threads calling waveOut functions at the same time (particularly one calling waveOutWrite while another calls waveOutReset). This in theory would be easily fixed with locks around all waveOut calls, but some audio drivers call the callbacks from another thread while you are calling waveOutReset, resulting in deadlocks.
Function callbacks should be considered as obsolete now in NAudio, with WaveOutEvent
a much better choice.
DirectSound is a good alternative if for some reason you don’t want to use WaveOut
since it is simple and widely supported.
To select a specific device with DirectSoundOut
, you can call the static DirectSoundOut.Devices
property which will let you get at the GUID for each device, which you can pass into the DirectSoundOut
constructor. Like WaveOut
, you can adjust the latency (overall buffer size).
DirectSoundOut
uses a background thread waiting to fill buffers (same as WaveOutEvent
). This is a reliable and uncomplicated mechanism, but as with any callback mechanism that uses a background thread, you must take responsibility yourself for ensuring that repositions do not happen at the same time as reads (although some of NAudio’s built-in WaveStreams can protect you from getting this wrong).
WASAPI is the latest and greatest Windows audio API, introduced with Windows Vista. But just because it is newer doesn’t mean you should use it. In fact, it can be a real pain to use, since it is much more fussy about the format of the IWaveProvider
passed to its Init
function and will not perform resampling for you.
To select a specific output device, you need to make use of the MMDeviceEnumerator
class, which can report the available audio "endpoints" in the system.
WASAPI out offers you a couple of configuration options. The main one is whether you open in shared
or exclusive
mode. In exclusive mode, your application requests exclusive access to the soundcard. This is only recommended if you need to work at very low latencies.
You can also choose whether event callbacks are used. I recommend you do so, since it enables the background thread to get on with filling a new buffer as soon as one is needed.
Why would you use WASAPI? I would only recommend it if you want to work at low latencies or are wanting exclusive use of the soundcard. Remember that WASAPI is not supported on Windows XP. However, in situations where WASAPI would be a good choice, ASIO out is often a better one…
ASIO is the de-facto standard for audio interface drivers for recording studios. All professional audio interfaces for Windows will come with ASIO drivers that are designed to operate at the lowest latencies possible. ASIO is probably a better choice than WASAPI for low latency applications since it is more widely supported (you can use ASIO on XP for example).
ASIO Out devices are selected by name. Use the AsioOut.GetDriverNames() to see what devices are available on your system. Note that this will return all installed ASIO drivers. It does not necessarily mean that the soundcard is currently connected in the case of an external audio interface, so Init
can fail for that reason.
ASIO drivers support their own customised settings GUI. You can access this by calling ShowControlPanel()
. Latencies are usually set within the control panel and are typically specified in samples. Remember that if you try to work at a really low latency, your input IWaveProvider’s Init
function needs to be really fast.
ASIO drivers can process data in a whole host of native WAV formats (e.g. big endian vs little endian, 16, 24, 32 bit ints, IEEE floats etc), not all of which are currently supported by NAudio. If ASIO Out doesn’t work with your soundcard, create an issue on the NAudio GitHub page, as it is fairly easy to add support for another format.