Posted in:

I’ve been playing with the new UWP AudioGraph API recently and one of the things I wanted to try was fire and forget audio playback (for fire and forget with NAudio see my post here). This is where you want to play lots of individual pre-recorded sounds, such as in a game, and just want to trigger the beginning of playback in a single line of code and have cleanup handled for you automatically.

Let’s start by creating our AudioGraph, and an AudioDeviceOutputNode, and then we’ll actually start the graph, despite not having anything to play yet. The audio graph is quite happy to play silence.

private AudioGraph audioGraph;
private AudioDeviceOutputNode outputNode;

public MainPage()
{
    this.InitializeComponent();
    this.Loaded += OnLoaded;
}

private async void OnLoaded(object sender, RoutedEventArgs e)
{
    var result = await AudioGraph.CreateAsync(new AudioGraphSettings(AudioRenderCategory.Media));
    if (result.Status != AudioGraphCreationStatus.Success) return;
    audioGraph = result.Graph;
    var outputResult = await audioGraph.CreateDeviceOutputNodeAsync();
    if (outputResult.Status != AudioDeviceNodeCreationStatus.Success) return;
    outputNode = outputResult.DeviceOutputNode;
    audioGraph.Start();
}

Now we’ll create a helper file that will load a sound file bundled with the application, create an AudioFileInputNode from it, connect it to the output device node so it starts playing, and then subscribe to its FileCompleted event. In the FileCompleted event handler, we can remove the AudioFileInputNode from the graph and dispose it. Note that we have to do this on the UI thread which we can ensure by using Dispatcher.RunAsync.

private async Task PlaySound(string file)
{
    var bassFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri($"ms-appx:///Assets/{file}"));
    var fileInputNodeResult = await audioGraph.CreateFileInputNodeAsync(bassFile);
    if (fileInputNodeResult.Status != AudioFileNodeCreationStatus.Success) return;
    var fileInputNode = fileInputNodeResult.FileInputNode;
    fileInputNode.FileCompleted += FileInputNodeOnFileCompleted;

    fileInputNode.AddOutgoingConnection(outputNode);
}

private async void FileInputNodeOnFileCompleted(AudioFileInputNode sender, object args)
{
    await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
    {
        sender.RemoveOutgoingConnection(outputNode);
        sender.FileCompleted -= FileInputNodeOnFileCompleted;
        sender.Dispose();
    });
}

Now we can trigger sounds very easily:

private async void buttonBass_Click(object sender, RoutedEventArgs e)
{
    await PlaySound("bass.wav");
}

private async void buttonBrass_Click(object sender, RoutedEventArgs e)
{
    await PlaySound("brass.wav");
}

This design allows for multiple instances of the same sound to be playing at once. If you don’t need that, you might be able to come up with a more efficient model that just keeps a single instance of an AudioFileInputNode for each sound and reset its position back to zero when you need to replay it. But this technique seems to perform just fine in the simple tests I’ve run it with.