Posted in:

Every so often I get a request for help from someone wanting to create a simple one-liner function that can play an audio file with NAudio. Often they will create something like this:

// warning: this won't work
public void PlaySound(string fileName)
{
    using (var output = new WaveOut())
    using (var player = new AudioFilePlayer(fileName))
    {
        output.Init(player);
        output.Play();
    }
}

Unfortunately this won’t actually work, since the Play method doesn’t block until playback is finished – it simply begins playback. So you end up disposing the playback device almost instantaneously after beginning playback. A slightly improved option simply waits for playback to stop, creating a blocking call. It uses WaveOutEvent, as the standard WaveOut only works on GUI threads.

// better, but still not ideal 
public void PlaySound(string fileName)
{
    using (var output = new WaveOutEvent())
    using (var player = new AudioFilePlayer(fileName))
    {
        output.Init(player);
        output.Play();
        while (output.PlaybackState == PlaybackState.Playing)
        {
            Thread.Sleep(500);
        }
    }
}

This approach is better, but is still not ideal, since it now blocks until audio playback is complete. It is also not particularly suitable for scenarios in which you are playing lots of short sounds, such as sound effects in a computer game. The problem is, you don’t really want to be continually opening and closing the soundcard, or having multiple instances of an output device active at once. So in this post, I explain the approach I would typically take for an application that needs to regularly play sounds in a “Fire and Forget” manner.

Use a Single Output Device

First, I’d recommend just opening the output soundcard once. Choose the output model you want (e.g. WasapiOut, WaveOutEvent), and play all the sounds through it.

Use a MixingSampleProvider

This means that to play multiple sounds simultaneously, you’ll need a mixer. I always recommend mixing with 32 bit IEEE floating point samples. and in NAudio the best way to do this is through using the MixingSampleProvider class.

Play Continuously

Obviously there are times when your application won’t be playing any sounds, so you could start and stop the output device whenever playback is idle. But it tends to be more straightforward to simply leave the soundcard running playing silence, and then just add inputs to the mixer. If you set the ReadFully property on MixingSampleProvider to true, it’s Read method will return buffers full of silence even when there are no mixer inputs. This means that the output device will keep playing continuously.

Use a Single WaveFormat

The one down-side of this approach is that you can’t mix together audio that doesn’t share the same WaveFormat. The bit depth won’t be a problem, since we are automatically converting everything to IEEE floating point. But if you are working with a stereo mixer, any mono inputs need to be made stereo before playing them. More annoying is the issue of sample rate conversion. If the files you need to play contain a mixture of sample rates, you’ll need to convert them all to a common value. 44.1kHz would be a typical choice, since this is likely to be the sample rate your soundcard is operating at.

Dispose Readers

The MixingSampleProvider has a nice feature where it will automatically remove an input whose Read method returns 0. However, it won’t attempt to Dispose that input for you, leaving you with a resource leak. The easiest way round this is to create a derived ISampleProvider class that encapsulates the AudioFileReader, and auto-disposes it when it reaches the end.

Cache Sounds

In a computer game scenario, you’ll likely be playing the same sounds again and again. You don’t really want to keep reading them from disk (and decoding them if they compressed). So it would be best to load the whole thing into memory, allowing us to replay many copies of it directly from the byte array of PCM data, using a RawSourceWaveStream. This approach has the advantage of allowing you to dispose the AudioFileReader immediately after caching its contents.

Source Code

That’s enough waffling, let’s have a look at some code that implements the features mentioned above. Let’s start with what I’ve called AudioPlaybackEngine. This is responsible for playing our sounds. You can either call PlaySound with a path to a file, for use with longer pieces of audio, or passing in a CachedSound for use with sound effects you want to play many times. I’ve included automatic conversion from mono to stereo, but no resampling is included here, so if you pass in a file of the wrong sample rate it won’t play:

class AudioPlaybackEngine : IDisposable
{
    private readonly IWavePlayer outputDevice;
    private readonly MixingSampleProvider mixer;

    public AudioPlaybackEngine(int sampleRate = 44100, int channelCount = 2)
    {
        outputDevice = new WaveOutEvent();
        mixer = new MixingSampleProvider(WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, channelCount));
        mixer.ReadFully = true;
        outputDevice.Init(mixer);
        outputDevice.Play();
    }

    public void PlaySound(string fileName)
    {
        var input = new AudioFileReader(fileName);
        AddMixerInput(new AutoDisposeFileReader(input));
    }

    private ISampleProvider ConvertToRightChannelCount(ISampleProvider input)
    {
        if (input.WaveFormat.Channels == mixer.WaveFormat.Channels)
        {
            return input;
        }
        if (input.WaveFormat.Channels == 1 && mixer.WaveFormat.Channels == 2)
        {
            return new MonoToStereoSampleProvider(input);
        }
        throw new NotImplementedException("Not yet implemented this channel count conversion");
    }

    public void PlaySound(CachedSound sound)
    {
        AddMixerInput(new CachedSoundSampleProvider(sound));
    }

    private void AddMixerInput(ISampleProvider input)
    {
        mixer.AddMixerInput(ConvertToRightChannelCount(input));
    }

    public void Dispose()
    {
        outputDevice.Dispose();
    }

    public static readonly AudioPlaybackEngine Instance = new AudioPlaybackEngine(44100, 2);
}

The CachedSound class is responsible for reading an audio file into memory. Sample rate conversion would be best done in here as part of the caching process, so it minimises the performance hit of resampling during playback.

class CachedSound
{
    public float[] AudioData { get; private set; }
    public WaveFormat WaveFormat { get; private set; }
    public CachedSound(string audioFileName)
    {
        using (var audioFileReader = new AudioFileReader(audioFileName))
        {
            // TODO: could add resampling in here if required
            WaveFormat = audioFileReader.WaveFormat;
            var wholeFile = new List<float>((int)(audioFileReader.Length / 4));
            var readBuffer= new float[audioFileReader.WaveFormat.SampleRate * audioFileReader.WaveFormat.Channels];
            int samplesRead;
            while((samplesRead = audioFileReader.Read(readBuffer,0,readBuffer.Length)) > 0)
            {
                wholeFile.AddRange(readBuffer.Take(samplesRead));
            }
            AudioData = wholeFile.ToArray();
        }
    }
}

There’s also a simple helper class to turn a CachedSound into an ISampleProvider that can be easily added to the mixer:

class CachedSoundSampleProvider : ISampleProvider
{
    private readonly CachedSound cachedSound;
    private long position;

    public CachedSoundSampleProvider(CachedSound cachedSound)
    {
        this.cachedSound = cachedSound;
    }

    public int Read(float[] buffer, int offset, int count)
    {
        var availableSamples = cachedSound.AudioData.Length - position;
        var samplesToCopy = Math.Min(availableSamples, count);
        Array.Copy(cachedSound.AudioData, position, buffer, offset, samplesToCopy);
        position += samplesToCopy;
        return (int)samplesToCopy;
    }

    public WaveFormat WaveFormat { get { return cachedSound.WaveFormat; } }
}

And here’s the auto disposing helper for when you are playing from an AudioFileReader directly:

class AutoDisposeFileReader : ISampleProvider
{
    private readonly AudioFileReader reader;
    private bool isDisposed;
    public AutoDisposeFileReader(AudioFileReader reader)
    {
        this.reader = reader;
        this.WaveFormat = reader.WaveFormat;
    }

    public int Read(float[] buffer, int offset, int count)
    {
        if (isDisposed)
            return 0;
        int read = reader.Read(buffer, offset, count);
        if (read == 0)
        {
            reader.Dispose();
            isDisposed = true;
        }
        return read;
    }

    public WaveFormat WaveFormat { get; private set; }
}

With all this set up, now we can have our goal of using a very simple fire and forget syntax for playback:

// on startup:
var zap = new CachedSound("zap.wav");
var boom = new CachedSound("boom.wav");

// later in the app...
AudioPlaybackEngine.Instance.PlaySound(zap);
AudioPlaybackEngine.Instance.PlaySound(boom);
AudioPlaybackEngine.Instance.PlaySound("crash.wav");

// on shutdown
AudioPlaybackEngine.Instance.Dispose();

Further Enhancements

This is far from complete. Obviously I’ve not added in the resampler stage here, and it would be nice to add a master volume level for the audio playback engine, as well as allowing you to set individual sound volume and panning positions. You could even have a maximum limit of concurrent sounds. But none of those enhancements are too hard to add.

I’ll try to get something like this added into the NAudio WPF Demo application, maybe with a few of these enhancements thrown in. For now, you can get at the code from this gist.

Want to get up to speed with the the fundamentals principles of digital audio and how to got about writing audio applications with NAudio? Be sure to check out my Pluralsight courses, Digital Audio Fundamentals, and Audio Programming with NAudio.

Comments

Comment by Jonathan

Presumably in the usage snippets you meant 'AudioPlaybackEngine...' not 'AudioFileEngine...'?

Cheers and thanks for this guide on event triggered audio!

Jonathan
Comment by Jonathan

Presumably you meant 'AudioPlaybackEngine.*' not 'AudioFileEngine.*' in the usage snippets?

Thanks ever so much for the guide!

Jonathan
Comment by Mark H

thanks Jonathan, good spot

Comment by Anurag Misra

Hi Mark
Is it possible to take out any particular audio file from Mixer and dispose it ?
For example suppose we are playing two wave files w1 and w2 and we want to take out w1 in middle of its playing and continue playing w2.
If not then is there any alternative solution ?
Thanks & Regards
Anurag Misra

Anurag Misra
Comment by Mark Heath

There is a RemoveMixerInput method on MixingSampleProvider so as long as you track the inputs, you can ask for one to be removed and dispose it yourself.

Mark Heath
Comment by Anurag Misra

Hi Mark, both AddMixerInput and RemoveMixerInput requires input of type ISampleProvider. For add to mixer we use string names of audio file so we call PlaySound method which coverts the file to ISampleProvider and add to mixer.
For RemoveMixerInput I use the same coding and convert the file to ISampleProvider by calling "new AutoDisposeFileReader(input)" and feed it to RemoveMixerInput. But it is not working.
Could you please guide on this?
Regards
-AM

Anurag Misra
Comment by Mark Heath

this article is about fire and forget - the whole point is that you don't need to remove audio. For your usage you need to keep track of every object you add to the mixer, and then ask for that specific object to be removed later.

Mark Heath
Comment by Zoey

Hello,
Is there anyway to specify the output device driver with this example? Ive been combing through it for the past hour trying to see if there is a way, but the best i can figure is IWavePlayer doesn't support this. The end goal for my project is to have the output device selectable by the user at runtime.

Zoey
Comment by Zoey

Hello again,
I managed to hack my way around the issue by changing IWavePlayer to WasapiOut and making my main form static so i can get the selected MMDevice. Of course with the instance already created the device cant be changed after the first call to playsound, however the project is function, so its good enough for now.
If there is a better option ill take any advice you have.
Thanks.

Zoey
Comment by Mark Heath

WASAPI uses MMDevice, WaveOut just uses DeviceNumbers. So the UI you present for choosing a device depends on the driver model. Take a look at the playback example in the NAudioDemo project

Mark Heath
Comment by SethEllis

Is there a limit to the number of sounds you could play at once with this method? I want to create a sonification like a geiger counter that can process thousands of events at once.

SethEllis
Comment by Mark Heath

Playing thousands of sounds at once will likely cause clipping and performance issues. No one will be able to hear that many sounds anyway. Most audio samplers have a maximum "polyphony" (e.g. 64 notes), so you could just put a limit on at the point where it's not possible to tell anymore how many are playing.

Mark Heath
Comment by Christian Pflugradt

I'm trying to use your example with WAV-Files I've set up in the projects resources.
I use this code for the input:
Stream WAVfile = Resources.ResourceManager.GetStream("ExampleFile");
var input = new WaveFileReader(WAVfile);
But I can't figure out how to get it to work with the resulting stream instead of the file path. Do you have any idea?

Christian Pflugradt
Comment by YJ

I modified the CachedSound initialization section to play the resource stream.
Apply the code below. (When initializing CachedSound, I sent Properties.Resources.MySoundResource as a parameter)

public CachedSound(Stream sound)
{
using (var audioFileReader = new WaveFileReader(sound))
{
WaveFormat = audioFileReader.WaveFormat;
var sp = audioFileReader.ToSampleProvider();
var wholeFile = new List<float>((int)(audioFileReader.Length / 4));
var sourceSamples = (int)(audioFileReader.Length / (audioFileReader.WaveFormat.BitsPerSample / 8));
var sampleData = new float[sourceSamples];
int samplesread;
while ((samplesread = sp.Read(sampleData, 0, sourceSamples)) > 0)
{
wholeFile.AddRange(sampleData.Take(samplesread));
}
AudioData = wholeFile.ToArray();
}
}

I hope it helps.

YJ
Comment by Adam Bruss

Hi Mark,
Thanks for the articles and NAudio. I'm using this example to play sounds in a game. The sounds are short effects and songs. How do I know when a sound is finished playing? For example I play a song and want to play another song once the first one is finished. I'm not seeing a way to know when a mixer input is finished playing.
P.S. I realize this article is called fire and forget. I hope I can ask here.
-Adam

Adam Bruss
Comment by Tola

Hi Anurag, have you implemented this? Can you share an example?

Tola