Josh Payette

Lifelong learner, coder, and creator.

Link to Josh Payette's GitHub profileLink to Josh Payette's Twitter profileLink to Josh Payette's LinkedIn profileLink to Josh Payette's Mastodon profile

Using NAudio to playback on specific audio device in AvaloniaUI

Cover Image for Using NAudio to playback on specific audio device in AvaloniaUI

After spending some time working with C#, I hit a pretty big wall when trying to play audio on a specific audio device in my AvaloniaUI project. I ended up getting it working after combining a few different solutions, so I decided to put the solution here in case anyone else needs it!

The Problem

All the code I could find to get a list of all active audio output devices required use of the MMDeviceEnumerator class. The problem is that in order to play sound on a specific device, NAudio recommends using the WaveOut class, where you can specify the DeviceNumber property. Unfortunately, MMDeviceEnumerator and WaveOut list the devices in a different order. I couldn't just use the WaveOut class because it limits the name of the audio devices to 31 characters, and I wanted to show the full name.

The First Issue: WaveOut does not exist in current context

The first issue I ran into was that I couldn't use the WaveOut class. All the code samples I found used it, but I couldn't find it in the NAudio library.

After some digging, I found that the I had to add the NAudio.WinForms package to my project. This added the WaveOut class to the NAudio namespace.

Adding the Nuget package

  1. Right-click your project in the Solution Explorer
  2. Click "Manage Nuget Packages"
  3. Search for "NAudio.WinForms"
  4. Click "Install"

The Solution

using Avalonia.Soundboard.Models;
using NAudio.CoreAudioApi;
using NAudio.Wave;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Avalonia.Soundboard.ViewModels
{
    public class MainWindowViewModel : ViewModelBase, IDisposable
    {
        private readonly MMDeviceEnumerator _enumerator;
        private readonly List<AudioDevice> _audioDevices;
        private AudioDevice? _selectedAudioDevice;

        public MainWindowViewModel()
        {
            _enumerator = new();
            _audioDevices = new();

            LoadAudioDevices();
        }

        private void LoadAudioDevices()
        {
            // Select all active audio devices
            // We use the MMDeviceEnumerator to get the audio device full names since WaveOut limits device names to 31 characters.
            // However, we use the WaveOut device order to determine the device number, since the order is different between the two.

            foreach (var mmDevice in _enumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active))
            {
                // Only keep active devices
                if (mmDevice.State != DeviceState.Active)
                {
                    continue;
                }

                // Find the matching WaveOut device for the MMDevice
                for (var deviceNumber = -1; deviceNumber < WaveOut.DeviceCount; deviceNumber++)
                {
                    if (mmDevice.FriendlyName.StartsWith(WaveOut.GetCapabilities(deviceNumber).ProductName))
                    {
                        _audioDevices.Add(new AudioDevice(mmDevice, deviceNumber));
                        break;
                    }
                }
            }

            // If no audio devices
            if (_audioDevices.Count == 0)
            {
                throw new Exception("No audio devices found");
            }
            // TODO - Load this from settings
            _selectedAudioDevice = _audioDevices[0];
        }

        public void PlaySound(string path)
        {
            var reader = new Mp3FileReader(path);
            WaveOutEvent waveOut = new()
            {
                DeviceNumber = _selectedAudioDevice.DeviceNumber
            };
            waveOut.Init(reader);
            waveOut.Play();
        }

        public void Dispose()
        {
            _enumerator.Dispose();
            GC.SuppressFinalize(this);
        }

        public string[] AudioDevices
            => _audioDevices.Select(x => x.Device.FriendlyName).ToArray();

        public string SelectedAudioDevice
        {
            get => _selectedAudioDevice.Device.FriendlyName;
            set => this.RaiseAndSetIfChanged(ref _selectedAudioDevice, _audioDevices.Find(x => x.Device.FriendlyName == value));
        }

    }
}

Conclusion

I hope this helps someone else out there! I spent a lot of time trying to figure this out, and I'm glad I finally got it working.