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
- Right-click your project in the Solution Explorer
- Click "Manage Nuget Packages"
- Search for "NAudio.WinForms"
- 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.