Important Update
The Guide Feature will be discontinued after December 15th, 2023. Until then, you can continue to access and refer to the existing guides.
Author avatar

Murat Aykanat

Building a WPF Media Player using NAudio

Murat Aykanat

  • Dec 15, 2018
  • 44 Min read
  • 37,285 Views
  • Dec 15, 2018
  • 44 Min read
  • 37,285 Views
Microsoft.NET

Introduction

Last year I needed to build a Windows Presentation Foundation (WPF) application for an electronic stethoscope to record respiratory audio, save it to wave files, and play the wave files at a later time upon user request. At that point my only experience with audio in general was my experiences with Unity3D - which has some great tools for handling audio - and with Matlab. I remember thinking to myself "How hard can it be? I already know C# can play wave files. There must be some advanced tools in the core libraries!". I was, of course, very wrong.

Not only I was shocked to find no core libraries, I was also amazed how audio is a very deep and challenging subject in programming. Lucky for me, Pluralsight has great courses on audio in general and on NAudio, so I was able to finish my project successfully.

I believe that, to learn something properly, you need to create a basic project using it. So in this tutorial I will explain how to build a simple media player from the ground-up using a very popular .NET audio library, NAudio.

Why Build Your Own Media Player?

  • You might have a project where you need to make a in-app media player to achieve your goals like my situation.
  • You might need a very special feature that no other media player provides.
  • You might also not trust 3rd party media players in your secure environment where you absolutely have to use a media player.
  • Building a media player is a coding project that results in a tool that you can use and feel good about on a daily basis.

Whatever your reason is, you have challenges waiting for you down the road when it comes to audio programming.

Challenges associated with audio programming

The first challenge arises from the fact that audio is not a traditional object, but a stream object. A stream is usually a sequence of bytes. If you want to read the stream, you must treat it like an array. However, streams can't be manipulated easily; you have to dig into the byte array and figure out what to do with each byte to achieve what you want. Furthermore, due to the numerous considerations (or variables) when it comes to audio, such as the number of channels, frequency, file formats, and so on, debugging an audio program can become messy.

Additionally, there are key memory management issues. Typically, when you are done with a stream you need to dispose of it properly. Otherwise, it will stay in the memory causing a memory leak. For example, if you are working with hundreds of audio streams and you don't dispose them properly, you could quickly run out of memory, and your application may crash. So in essence, you have to manually manage memory.

When you think about this project, you might have many questions. "How do we know if we reach the end of an audio file?", "How do we stop or pause the stream?", "How do we resume where we left off?". Of course there are many more issues you need to address but these are the core challenges that we need to tackle.

Considering these challenges, a simple action such as playing an audio file now seems rather complicated!

NAudio to the Rescue

Luckily there is an audio library for .NET called NAudio which will do most of this work for us. We will still be working with streams and we will have to face several challenges that comes with them. However NAudio removes the menial complexity of simple playing and recording audio files. The good thing about NAudio library is, it can be used in every type of project. If you simply want build and application to play or record audio files, you can do that without delving deep into streams. If you want something complex such as a platform for manipulating audio or creating filter, NAudio provides very good tools for that as well.

There are 3 ways of getting NAudio:

I usually go with NuGet since it is the easiest of them all, however if you want to see how it actually works there are great examples and documentation on the Codeplex and GitHub pages.

Features of our Media Player

The main purpose of this tutorial is to build a simple media player which will be able to:

  • Play audio files with various formats (wav, mp3, ogg, flac etc)
  • Skip to beginning, skip to end, play/pause, stop, shuffle buttons and their functionality
  • Have a volume control
  • Have a seekbar
  • Create and manipulate a playlist
  • Add a file to this playlist
  • Add a folder of files to this playlist
  • Save and load playlists
  • Automatically switch to the next song in the playlist after the prior song finishes.

It should look like this at the end (with my Witcher 3 soundtrack playlist): Our finalized media player

Implementation

In the solution, we will have two projects. One for the actual application and one for the NAudio abstraction. NAudio is great in abstracting us from details but I want to make it very easy for us to do everything with a few methods, rather than calling NAudio codes for playing, pausing, stopping etc.

However there is a choice we have to make when it comes to the main project where the UI (User Interface) is: do we use the traditional event driven architecture or do we use Model View ViewModel architecture (MVVM). You might think that you would use MVVM because that's what all the cool kids use nowadays. However both architectures have advantages and disadvantages especially when it comes to developing a real-time application such as this.

I developed media players using both architectures on two different projects:

  • If you use MVVM you write way less code. In this case, however, property binding has a nasty habit of breaking down and causing bugs if done incorrectly. This becomes problematic when it comes to implementing a real-time two-way bound seekbar control.
  • If you use the good old event-driven architecture, you will undoubtedly write a lot more UI event code. But you will also have full control over what happens during those events, so it is easier to implement a real time control such as a seekbar.

In this tutorial, I will use the MVVM architecture. However since this is not an MVVM tutorial I won't go into details on how MVVM works. For more on MVVM, check out CodeProject's tutorial

Now that we've chosen our architecture, we need to generate the right namespaces in our project. Our solution structure will be like this:

  • Solution
    • Project: NaudioPlayer
      • Models
      • ViewModels
      • Views
      • Services
      • Images
    • Project: NaudioWrapper

Creating the UI

I always like to start with the UI part when it comes to WPF projects because it provides me with a visual list of features that I need to implement.

Images, Namespaces, and Blend

Before we start on the UI, however, I will provide you the link for the images I used for the buttons so you will have them ready. For icons I used Google's free material design icons. You can get them from Google's material design page. After you download the icons, do a quick search of "play" or "pause" in the download directory to find the relevant icons.

We also need System.Windows.Interactivity and Microsoft.Expression.Interaction namespaces for our event bindings. I said we won't use event-driven architecture, but sometimes we cannot avoid events. In this case, namespaces provide events the MVVM way. Adding these namespaces can be tricky and might not always work as expected. These namespaces actually come with Blend, a UI development tool for XAML based projects. So if you have Blend installed (it comes with the Visual Studio installer), you can find those DLL (Dynamic Link-Library) files from:

  • For .NET 4.5+ C:\Program Files (x86)\Microsoft SDKs\Expression\Blend\.NETFramework\v4.5\Libraries
  • For .NET 4.0 C:\Program Files (x86)\Microsoft SDKs\Expression\Blend\.NETFramework\v4.0\Libraries

If you don't have Blend installed, just run the Visual Studio installer and modify/add it to your installation.

However, in my experience adding those in Visual Studio doesn't always work. So what I usually do:

  • Create the WPF project in Visual Studio
  • Add xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" to the Window.
  • Close Visual Studio and open the project in Blend.
  • From the menu click Project->Add Reference and add the references from that menu.
  • Close Blend and open the project back in Visual Studio

Now everything is ready for our UI code.

UI Code

Here is our Extensible Application Markup Language (XAML) code for the UI:

1<Window x:Class="NaudioPlayer.MainWindow"
2        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
5        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
6        xmlns:local="clr-namespace:NaudioPlayer"
7        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
8        xmlns:viewModels="clr-namespace:NaudioPlayer.ViewModels"
9        mc:Ignorable="d"
10        Title="{Binding Title}" Height="350" Width="525">
11    <Window.DataContext>
12        <viewModels:MainWindowViewModel/>
13    </Window.DataContext>
14    <DockPanel LastChildFill="True">
15        <Menu DockPanel.Dock="Top">
16            <MenuItem Header="File">
17                <MenuItem Header="Save Playlist" Command="{Binding SavePlaylistCommand}"/>
18                <MenuItem Header="Load Playlist" Command="{Binding LoadPlaylistCommand}"/>
19                <MenuItem Header="Exit" Command="{Binding ExitApplicationCommand}"/>
20            </MenuItem>
21            <MenuItem Header="Media">
22                <MenuItem Header="Add File to Playlist..." Command="{Binding AddFileToPlaylistCommand}"/>
23                <MenuItem Header="Add Folder to Playlist..." Command="{Binding AddFolderToPlaylistCommand}"/>
24            </MenuItem>
25        </Menu>
26        <Grid DockPanel.Dock="Bottom" Height="30">
27            <Grid.ColumnDefinitions>
28                <ColumnDefinition Width="30"/>
29                <ColumnDefinition Width="30"/>
30                <ColumnDefinition Width="30"/>
31                <ColumnDefinition Width="30"/>
32                <ColumnDefinition Width="*"/>
33                <ColumnDefinition Width="30"/>
34            </Grid.ColumnDefinitions>
35            <Button Grid.Column="0" Margin="3" Command="{Binding RewindToStartCommand}">
36                <Image Source="../Images/skip_previous.png" VerticalAlignment="Center" HorizontalAlignment="Center"/>
37            </Button>
38            <Button Grid.Column="1" Margin="3" Command="{Binding StartPlaybackCommand}">
39                <Image Source="{Binding PlayPauseImageSource}" VerticalAlignment="Center" HorizontalAlignment="Center"/>
40            </Button>
41            <Button Grid.Column="2" Margin="3" Command="{Binding StopPlaybackCommand}">
42                <Image Source="../Images/stop.png" VerticalAlignment="Center" HorizontalAlignment="Center"/>
43            </Button>
44            <Button Grid.Column="3" Margin="3" Command="{Binding ForwardToEndCommand}">
45                <Image Source="../Images/skip_next.png" VerticalAlignment="Center" HorizontalAlignment="Center"/>
46            </Button>
47            <Button Grid.Column="5" Margin="3" Command="{Binding ShuffleCommand}">
48                <Image Source="../Images/shuffle.png" VerticalAlignment="Center" HorizontalAlignment="Center"/>
49            </Button>
50        </Grid>
51        <Grid DockPanel.Dock="Bottom" Margin="3">
52            <Grid.ColumnDefinitions>
53                <ColumnDefinition Width="3*"/>
54                <ColumnDefinition Width="20"/>
55                <ColumnDefinition Width="20"/>
56                <ColumnDefinition Width="*"/>
57            </Grid.ColumnDefinitions>
58            <Slider Grid.Column="0" Minimum="0" Maximum="{Binding CurrentTrackLenght, Mode=OneWay}" Value="{Binding CurrentTrackPosition, Mode=TwoWay}" x:Name="SeekbarControl" VerticalAlignment="Center">
59                <i:Interaction.Triggers>
60                    <i:EventTrigger EventName="PreviewMouseDown">
61                        <i:InvokeCommandAction Command="{Binding TrackControlMouseDownCommand}"></i:InvokeCommandAction>
62                    </i:EventTrigger>
63                    <i:EventTrigger EventName="PreviewMouseUp">
64                        <i:InvokeCommandAction Command="{Binding TrackControlMouseUpCommand}"></i:InvokeCommandAction>
65                    </i:EventTrigger>
66                </i:Interaction.Triggers>
67            </Slider>
68            <Image Grid.Column="2" Source="../Images/volume.png"></Image>
69            <Slider Grid.Column="3" Minimum="0" Maximum="1" Value="{Binding CurrentVolume, Mode=TwoWay}" x:Name="VolumeControl" VerticalAlignment="Center">
70                <i:Interaction.Triggers>
71                    <i:EventTrigger EventName="ValueChanged">
72                        <i:InvokeCommandAction Command="{Binding VolumeControlValueChangedCommand}"></i:InvokeCommandAction>
73                    </i:EventTrigger>
74                </i:Interaction.Triggers>
75            </Slider>
76        </Grid>
77        <Grid DockPanel.Dock="Bottom">
78            <Grid.ColumnDefinitions>
79                <ColumnDefinition Width="70"/>
80                <ColumnDefinition Width="Auto"/>
81            </Grid.ColumnDefinitions>
82            <TextBlock Grid.Column="0" Text="Now playing: "></TextBlock>
83            <TextBlock Grid.Column="1" Text="{Binding CurrentlyPlayingTrack.FriendlyName, Mode=OneWay}"/>
84        </Grid>
85        <ListView x:Name="Playlist" ItemsSource="{Binding Playlist}" SelectedItem="{Binding CurrentlySelectedTrack, Mode=TwoWay}">
86            <ListView.ItemTemplate>
87                <DataTemplate>
88                    <Grid>
89                        <Grid.ColumnDefinitions>
90                            <ColumnDefinition></ColumnDefinition>
91                        </Grid.ColumnDefinitions>
92                        <TextBlock Grid.Column="0" Text="{Binding Path=FriendlyName, Mode=OneWay}"></TextBlock>
93                    </Grid>
94                </DataTemplate>
95            </ListView.ItemTemplate>
96        </ListView>
97    </DockPanel>
98</Window>
xml

UI-based Commands

So by looking at our XAML code, in our MainWindowViewModel viewmodel, we need to implement the following commands:

  • Menu Commands
    • SavePlaylistCommand: This command will save our Playlist into a text file, simply by writing the path of each Track in the Playlist as <filename>.playlist.
    • LoadPlaylistCommand: This command will read the .playlist file of our choosing and generate a Playlist for us.
    • ExitApplicationCommand: This command will dispose all our audio streams and close the application.
    • AddFileToPlaylistCommand: This command will add a single audio file to our Playlist.
    • AddFolderToPlaylistCommand: This command will add a folder of files to our Playlist.
  • Player Commands
    • RewindToStartCommand: This command will set the CurrentTrackPosition to 0, effectively skipping to start.
    • StartPlaybackCommand: This command will start playback of the CurrentlySelectedTrack. When pressed during playback, it will pause the playback.
    • StopPlaybackCommand: This command will stop the playback.
    • ForwardToEndCommand: This command will skip to the last second of the currently playing audio file.
    • ShuffleCommand: This command will shuffle our Playlist randomly.
  • MVVM Events
    • TrackControlMouseDownCommand: This command will run when we press mouse button on the seekbar slider control.
    • TrackControlMouseUpCommand: This command will run when we release mouse button from the seekbar slider control.
    • VolumeControlValueChangedCommand: This command will run when we change the value of the volume control slider.

and properties:

  • Title: The title of the window.
  • CurrentTrackPosition: While the audio is playing, this property is used to store the current position in seconds.
  • CurrentVolume: This property sets the volume.
  • Playlist: This property represents our playlist.
  • CurrentlySelectedTrack: This property represents the currently selected track in our playlist.
  • CurrentlyPlayingTrack: This property represents the currently playing track in our playlist.
  • PlayPauseImageSource: This property sets either play or pause images depending on whether the audio is playing or paused to our play button.

NaudioWrapper

Before we move on to the ViewModel we need the basic features abstracted away from the core NAudio code. To do this, we must first add NAudio to this project via NuGet. Then we must create an AudioPlayer class to hold all these features in its methods, so our ViewModel can access and use the class and its public methods.

Looking at our feature list, we need to be able to actually play an audio file. To do this in NAudio, first we must set the file path of the audio and read the file with a reader. There are many specific readers for specific file types. However you can simply use AudioFileReader to read all supported files.

To play the file, we need to set an output. We want to play all kinds of audio files so we will use DirectSoundOut. You can use many other output types, however we will use DirectSoundOut in this example.

We must also create some events to let ViewModel know that we are playing, paused or stopped. These events will come in handy to manipulate our UI accordingly.

And lastly, we must create a flag to set or get the stop type. This is necessary because we need to know if we are stopping to play the next file in our playlist or if we are stopping because the user wants to stop playing the current file.

1public class AudioPlayer
2{
3    public enum PlaybackStopTypes
4    {
5        PlaybackStoppedByUser, PlaybackStoppedReachingEndOfFile
6    }
7
8    public PlaybackStopTypes PlaybackStopType { get; set; }
9
10    private AudioFileReader _audioFileReader;
11
12    private DirectSoundOut _output;
13
14    private string _filepath;
15
16    public event Action PlaybackResumed;
17    public event Action PlaybackStopped;
18    public event Action PlaybackPaused;
19}
csharp

Let's take a look at our methods. In the constructor we must initialize all our fields and their events.

1public AudioPlayer(string filepath, float volume)
2{
3    PlaybackStopType = PlaybackStopTypes.PlaybackStoppedReachingEndOfFile;
4
5    _audioFileReader = new AudioFileReader(filepath) { Volume = volume };
6
7    _output = new DirectSoundOut(200);
8    _output.PlaybackStopped += _output_PlaybackStopped;
9
10    var wc = new WaveChannel32(_audioFileReader);
11    wc.PadWithZeroes = false;
12
13    _output.Init(wc);
14}
csharp

We default for PlaybackStopTypes.PlaybackStoppedReachingEndOfFile as our PlaybackStopType because our playback would typically stop once the audio file is over. Here, the most important bit is wc.PadWithZeroes = false because typically the PlaybackStopped event fires when the reader finds byte value 0. If we don't pad with zeros, then playback would go on forever, and there would be no way for us to know if the clip is finished.

Implementing the remaining methods

Now that we have initialized everything, we can implement the rest of our methods.

We need methods for:

  • PlaybackStopped event
  • Playing
  • Pausing
  • Stopping
  • Toggling between Play and Pause
  • Getting and setting current track position for our UI
  • Getting and setting current volume for our UI
  • And a Dispose() method to clear up the memory
1public void Play(PlaybackState playbackState, double currentVolumeLevel)
2{
3    if (playbackState == PlaybackState.Stopped || playbackState == PlaybackState.Paused)
4    {
5        _output.Play();
6    }
7
8    _audioFileReader.Volume = (float) currentVolumeLevel;
9
10    if (PlaybackResumed != null)
11    {
12        PlaybackResumed();
13    }
14}
15
16private void _output_PlaybackStopped(object sender, StoppedEventArgs e)
17{
18    Dispose();
19    if (PlaybackStopped != null)
20    {
21        PlaybackStopped();
22    }
23}
24
25public void Stop()
26{
27    if (_output != null)
28    {
29        _output.Stop();
30    }
31}
32
33public void Pause()
34{
35    if (_output != null)
36    {
37        _output.Pause();
38
39        if (PlaybackPaused != null)
40        {
41            PlaybackPaused();
42        }
43    }
44}
45
46public void TogglePlayPause(double currentVolumeLevel)
47{
48    if (_output != null)
49    {
50        if (_output.PlaybackState == PlaybackState.Playing)
51        {
52            Pause();
53        }
54        else
55        {
56            Play(_output.PlaybackState, currentVolumeLevel);
57        }
58    }
59    else
60    {
61        Play(PlaybackState.Stopped, currentVolumeLevel);
62    }
63}
64
65public void Dispose()
66{
67    if (_output != null)
68    {
69        if (_output.PlaybackState == PlaybackState.Playing)
70        {
71            _output.Stop();
72        }
73        _output.Dispose();
74        _output = null;
75    }
76    if (_audioFileReader != null)
77    {
78        _audioFileReader.Dispose();
79        _audioFileReader = null;
80    }
81}
82
83public double GetLenghtInSeconds()
84{
85    if (_audioFileReader != null)
86    {
87        return _audioFileReader.TotalTime.TotalSeconds;
88    }
89    else
90    {
91        return 0;
92    }
93}
94
95public double GetPositionInSeconds()
96{
97    return _audioFileReader != null ? _audioFileReader.CurrentTime.TotalSeconds : 0;
98}
99
100public float GetVolume()
101{
102    if (_audioFileReader != null)
103    {
104        return _audioFileReader.Volume;
105    }
106    return 1;
107}
108
109public void SetPosition(double value)
110{
111    if (_audioFileReader != null)
112    {
113        _audioFileReader.CurrentTime = TimeSpan.FromSeconds(value);
114    }
115}
116
117public void SetVolume(float value)
118{
119    if (_output != null)
120    {
121        _audioFileReader.Volume = value;
122    }
123}
csharp

The ViewModel

After coding our abstraction layer, we can move on the ViewModel. However, before we implement the ViewModel we need to define how commands work as in every MVVM project. We will use the commonly used RelayCommand class to do this:

1public class RelayCommand : ICommand
2{
3    private readonly Action<object> _execute;
4    private readonly Predicate<object> _canExecute;
5
6    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
7    {
8        _execute = execute;
9        _canExecute = canExecute;
10    }
11    public bool CanExecute(object parameter)
12    {
13        bool result = _canExecute == null ? true : _canExecute(parameter);
14        return result;
15    }
16
17    public void Execute(object parameter)
18    {
19        _execute(parameter);
20    }
21
22    public event EventHandler CanExecuteChanged
23    {
24        add { CommandManager.RequerySuggested += value; }
25        remove { CommandManager.RequerySuggested -= value; }
26    }
27}
csharp

Now, let's stub out our ViewModel:

1public class MainWindowViewModel : INotifyPropertyChanged
2{
3    private string _title;
4    private double _currentTrackLenght;
5    private double _currentTrackPosition;
6    private string _playPauseImageSource;
7    private float _currentVolume;
8
9    private ObservableCollection<Track> _playlist;
10    private Track _currentlyPlayingTrack;
11    private Track _currentlySelectedTrack;
12    private AudioPlayer _audioPlayer;
13
14    public string Title
15    {
16        get { return _title; }
17        set
18        {
19            if (value == _title) return;
20            _title = value;
21            OnPropertyChanged(nameof(Title));
22        }
23    }
24
25    public string PlayPauseImageSource
26    {
27        get { return _playPauseImageSource; }
28        set
29        {
30            if (value == _playPauseImageSource) return;
31            _playPauseImageSource = value;
32            OnPropertyChanged(nameof(PlayPauseImageSource));
33        }
34    }
35
36    public float CurrentVolume
37    {
38        get { return _currentVolume; }
39        set
40        {
41
42            if (value.Equals(_currentVolume)) return;
43            _currentVolume = value;
44            OnPropertyChanged(nameof(CurrentVolume));
45        }
46    }
47
48    public double CurrentTrackLenght
49    {
50        get { return _currentTrackLenght; }
51        set
52        {
53            if (value.Equals(_currentTrackLenght)) return;
54            _currentTrackLenght = value;
55            OnPropertyChanged(nameof(CurrentTrackLenght));
56        }
57    }
58
59    public double CurrentTrackPosition
60    {
61        get { return _currentTrackPosition; }
62        set
63        {
64            if (value.Equals(_currentTrackPosition)) return;
65            _currentTrackPosition = value;
66            OnPropertyChanged(nameof(CurrentTrackPosition));
67        }
68    }
69
70    public Track CurrentlySelectedTrack
71    {
72        get { return _currentlySelectedTrack; }
73        set
74        {
75            if (Equals(value, _currentlySelectedTrack)) return;
76            _currentlySelectedTrack = value;
77            OnPropertyChanged(nameof(CurrentlySelectedTrack));
78        }
79    }
80
81    public Track CurrentlyPlayingTrack
82    {
83        get { return _currentlyPlayingTrack; }
84        set
85        {
86            if (Equals(value, _currentlyPlayingTrack)) return;
87            _currentlyPlayingTrack = value;
88            OnPropertyChanged(nameof(CurrentlyPlayingTrack));
89        }
90    }
91
92    public ObservableCollection<Track> Playlist
93    {
94        get { return _playlist; }
95        set
96        {
97            if (Equals(value, _playlist)) return;
98            _playlist = value;
99            OnPropertyChanged(nameof(Playlist));
100        }
101    }
102
103    public ICommand ExitApplicationCommand { get; set; }
104    public ICommand AddFileToPlaylistCommand { get; set; }
105    public ICommand AddFolderToPlaylistCommand { get; set; }
106    public ICommand SavePlaylistCommand { get; set; }
107    public ICommand LoadPlaylistCommand { get; set; }
108
109    public ICommand RewindToStartCommand { get; set; }
110    public ICommand StartPlaybackCommand { get; set; }
111    public ICommand StopPlaybackCommand { get; set; }
112    public ICommand ForwardToEndCommand { get; set; }
113    public ICommand ShuffleCommand { get; set; }
114
115    public ICommand TrackControlMouseDownCommand { get; set; }
116    public ICommand TrackControlMouseUpCommand { get; set; }
117    public ICommand VolumeControlValueChangedCommand { get; set; }
118
119    public event PropertyChangedEventHandler PropertyChanged;
120
121    public MainWindowViewModel()
122    {
123        LoadCommands();
124
125        Playlist = new ObservableCollection<Track>();
126
127		Title = "NaudioPlayer";
128        PlayPauseImageSource = "../Images/play.png";
129    }
130
131    private void LoadCommands()
132    {
133        // Menu commands
134        ExitApplicationCommand = new RelayCommand(ExitApplication,CanExitApplication);
135        AddFileToPlaylistCommand = new RelayCommand(AddFileToPlaylist, CanAddFileToPlaylist);
136        AddFolderToPlaylistCommand = new RelayCommand(AddFolderToPlaylist, CanAddFolderToPlaylist);
137        SavePlaylistCommand = new RelayCommand(SavePlaylist, CanSavePlaylist);
138        LoadPlaylistCommand = new RelayCommand(LoadPlaylist, CanLoadPlaylist);
139
140        // Player commands
141        RewindToStartCommand = new RelayCommand(RewindToStart, CanRewindToStart);
142        StartPlaybackCommand = new RelayCommand(StartPlayback, CanStartPlayback);
143        StopPlaybackCommand = new RelayCommand(StopPlayback, CanStopPlayback);
144        ForwardToEndCommand = new RelayCommand(ForwardToEnd, CanForwardToEnd);
145        ShuffleCommand = new RelayCommand(Shuffle, CanShuffle);
146
147        // Event commands
148        TrackControlMouseDownCommand = new RelayCommand(TrackControlMouseDown, CanTrackControlMouseDown);
149        TrackControlMouseUpCommand = new RelayCommand(TrackControlMouseUp, CanTrackControlMouseUp);
150        VolumeControlValueChangedCommand = new RelayCommand(VolumeControlValueChanged, CanVolumeControlValueChanged);
151    }
152
153    // Menu commands
154    private void ExitApplication(object p)
155    {
156
157    }
158    private bool CanExitApplication(object p)
159    {
160        return true;
161    }
162
163    private void AddFileToPlaylist(object p)
164    {
165
166    }
167    private bool CanAddFileToPlaylist(object p)
168    {
169        return true;
170    }
171
172    private void AddFolderToPlaylist(object p)
173    {
174
175    }
176
177    private bool CanAddFolderToPlaylist(object p)
178    {
179        return true;
180    }
181
182    private void SavePlaylist(object p)
183    {
184
185    }
186
187    private bool CanSavePlaylist(object p)
188    {
189        return true;
190    }
191
192    private void LoadPlaylist(object p)
193    {
194
195    }
196
197    private bool CanLoadPlaylist(object p)
198    {
199        return true;
200    }
201
202    // Player commands
203    private void RewindToStart(object p)
204    {
205
206    }
207    private bool CanRewindToStart(object p)
208    {
209        return true;
210    }
211
212    private void StartPlayback(object p)
213    {
214
215    }
216
217    private bool CanStartPlayback(object p)
218    {
219        return true;
220    }
221
222    private void StopPlayback(object p)
223    {
224
225    }
226    private bool CanStopPlayback(object p)
227    {
228        return true;
229    }
230
231    private void ForwardToEnd(object p)
232    {
233
234    }
235    private bool CanForwardToEnd(object p)
236    {
237        return true;
238    }
239
240    private void Shuffle(object p)
241    {
242
243    }
244    private bool CanShuffle(object p)
245    {
246        return true;
247    }
248
249    // Events
250    private void TrackControlMouseDown(object p)
251    {
252
253    }
254
255    private void TrackControlMouseUp(object p)
256    {
257
258    }
259
260    private bool CanTrackControlMouseDown(object p)
261    {
262        return true;
263    }
264
265    private bool CanTrackControlMouseUp(object p)
266    {
267        return true;
268    }
269
270    private void VolumeControlValueChanged(object p)
271    {
272
273    }
274
275    private bool CanVolumeControlValueChanged(object p)
276    {
277        return true;
278    }
279
280    [NotifyPropertyChangedInvocator]
281    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
282    {
283        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
284    }
285}
csharp

Menu Commands

Exiting the Application

Let's start with an easy yet important feature: exiting our application. We must consider two ways to quit:

  • User clicks "Exit" in the menu
  • User clicks the red "X"

To implement the first way:

1private void ExitApplication(object p)
2{
3    if (_audioPlayer != null)
4    {
5        _audioPlayer.Dispose();
6    }
7
8    Application.Current.Shutdown();
9}
10private bool CanExitApplication(object p)
11{
12    return true;
13}
csharp

As you can see above, we must manage the memory in case the user played some audio clips and AudioPlayer is therefore not equal to null. Even if we close the application, the audio clip would remain in the memory as a stream, so we have to Dispose() it to release the memory.

For the red X button, we must add an event in the constructor to let ViewModel know that we are exiting.

1public MainWindowViewModel()
2{
3    Application.Current.MainWindow.Closing += MainWindow_Closing;
4
5    Title = "NaudioPlayer";
6
7    LoadCommands();
8
9    Playlist = new ObservableCollection<Track>();
10
11    PlayPauseImageSource = "../Images/play.png";
12    CurrentVolume = 1;
13}
csharp

Then add the method:

1private void MainWindow_Closing(object sender, CancelEventArgs e)
2{
3    if (_audioPlayer != null)
4    {
5        _audioPlayer.Dispose();
6    }
7}
csharp

Adding Files to Playlist

Now, let's create our playlist by adding a single file. However, we do not want to modify our playlist while a clip is running. So we need a flag to see if we are playing a clip while attempting to modify our playlist. We must add another field and modify the constructor to set up this flag:

1private enum PlaybackState
2{
3    Playing, Stopped, Paused
4}
5
6private PlaybackState _playbackState;
7
8public MainWindowViewModel()
9{
10    Application.Current.MainWindow.Closing += MainWindow_Closing;
11
12    Title = "NaudioPlayer";
13
14    LoadCommands();
15
16    Playlist = new ObservableCollection<Track>();
17
18    _playbackState = PlaybackState.Stopped;
19
20    PlayPauseImageSource = "../Images/play.png";
21    CurrentVolume = 1;
22}
csharp

And to add a file, we do it with an OpenFileDialog:

1private void AddFileToPlaylist(object p)
2{
3    var ofd = new OpenFileDialog();
4    ofd.Filter = "Audio files (*.wav, *.mp3, *.wma, *.ogg, *.flac) | *.wav; *.mp3; *.wma; *.ogg; *.flac";
5    var result = ofd.ShowDialog();
6    if (result == true)
7    {
8        var friendlyName = ofd.SafeFileName.Remove(ofd.SafeFileName.Length - 4);
9        var track = new Track(ofd.FileName, friendlyName);
10        Playlist.Add(track);
11    }
12}
13private bool CanAddFileToPlaylist(object p)
14{
15    if (_playbackState == PlaybackState.Stopped)
16    {
17        return true;
18    }
19    return false;
20}
csharp

We also need to implement add a folder of files. To do this first we need a way to select a folder like we did with the file with CommonOpenFileDialog(). However core libraries do not have this class so we need to add this class via NuGet. The name of the package is Microsoft.WindowsAPICodePack-Core. This code can only be used on machines with Windows Vista or above.

1private void AddFolderToPlaylist(object p)
2{
3    var cofd = new CommonOpenFileDialog();
4    cofd.IsFolderPicker = true;
5    var result = cofd.ShowDialog();
6    if (result == CommonFileDialogResult.Ok)
7    {
8        var folderName = cofd.FileName;
9        var audioFiles = Directory.EnumerateFiles(folderName, "*.*", SearchOption.AllDirectories)
10                                  .Where(f=>f.EndsWith(".wav") || f.EndsWith(".mp3") || f.EndsWith(".wma") || f.EndsWith(".ogg") || f.EndsWith(".flac"));
11        foreach (var audioFile in audioFiles)
12        {
13            var removePath = audioFile.RemovePath();
14            var friendlyName = removePath.Remove(removePath.Length - 4);
15            var track = new Track(audioFile, friendlyName);
16            Playlist.Add(track);
17        }
18        Playlist = new ObservableCollection<Track>(Playlist.OrderBy(z => z.FriendlyName).ToList());
19    }
20}
21
22private bool CanAddFolderToPlaylist(object p)
23{
24    if (_playbackState == PlaybackState.Stopped)
25    {
26        return true;
27    }
28    return false;
29}
csharp

Saving and Loading The Playlist

To add even more to our playlist's functionality, we must be able to save and load our playlists. Let's choose .playlist as our file extension.

To save a playlist:

1private void SavePlaylist(object p)
2{
3    var sfd = new SaveFileDialog();
4    sfd.CreatePrompt = false;
5    sfd.OverwritePrompt = true;
6    sfd.Filter = "PLAYLIST files (*.playlist) | *.playlist";
7    if (sfd.ShowDialog() == true)
8    {
9        var ps = new PlaylistSaver();
10        ps.Save(Playlist, sfd.FileName); // save the playlist
11    }
12}
13
14private bool CanSavePlaylist(object p)
15{
16    return true;
17}
csharp

To load a playlist:

1private void LoadPlaylist(object p)
2{
3    var ofd = new OpenFileDialog();
4    ofd.Filter = "PLAYLIST files (*.playlist) | *.playlist";
5    if (ofd.ShowDialog() == true)
6    {
7        Playlist = new PlaylistLoader().Load(ofd.FileName).ToObservableCollection(); // load the playlist
8    }
9}
10
11private bool CanLoadPlaylist(object p)
12{
13    return true;
14}
csharp

We are finally done with our menu commands. Let's move onto the player buttons that deal with the actual audio.

Player buttons

Skip to Beginning

To skip to the beginning, we need to set the current track position to zero while the audio is playing. The code for that is:

1private void RewindToStart(object p)
2{
3    _audioPlayer.SetPosition(0); // set position to zero
4}
5private bool CanRewindToStart(object p)
6{
7    if (_playbackState == PlaybackState.Playing)
8    {
9        return true;
10    }
11    return false;
12}
csharp

Play/Pause Toggling

In this part we need to decide if we are playing or pausing. If we are stopped, it is easy, we just need to instantiate another AudioPlayer and play the clip. However if we are playing or in pause mode, we need to check if the playing clip is the same as selected clip on the UI and then call TogglePlayPause() to resume or pause the clip.

1private void StartPlayback(object p)
2{
3    if (CurrentlySelectedTrack != null)
4    {
5        if (_playbackState == PlaybackState.Stopped)
6        {
7            _audioPlayer = new AudioPlayer(CurrentlySelectedTrack.Filepath, CurrentVolume);
8            _audioPlayer.PlaybackStopType = AudioPlayer.PlaybackStopTypes.PlaybackStoppedReachingEndOfFile;
9            _audioPlayer.PlaybackPaused += _audioPlayer_PlaybackPaused;
10            _audioPlayer.PlaybackResumed += _audioPlayer_PlaybackResumed;
11            _audioPlayer.PlaybackStopped += _audioPlayer_PlaybackStopped;
12            CurrentTrackLenght = _audioPlayer.GetLenghtInSeconds();
13            CurrentlyPlayingTrack = CurrentlySelectedTrack;
14        }
15        if (CurrentlySelectedTrack == CurrentlyPlayingTrack)
16        {
17            _audioPlayer.TogglePlayPause(CurrentVolume);
18        }
19    }
20}
21private bool CanStartPlayback(object p)
22{
23    if (CurrentlySelectedTrack != null)
24    {
25        return true;
26    }
27    return false;
28}
csharp

Events for the AudioPlayer

You might have noticed that, when we instantiated an AudioPlayer, we also subscribed to some events. These events are vital for the UI to know what "mode" we are in.

In all 3 methods, we first set the PlaybackState, then load the correct image for the "play" button. However in the PlaybackStopped event, we will have to also refresh UI using CommandManager.InvalidateRequerySuggested() and set the current track position to zero indicating we finished the playback.

If the playback is finished because we have reached the end of a clip, we need to start playing the next clip. To find the next item, we need to setup an extension method for it.

1public static T NextItem<T>(this ObservableCollection<T> collection, T currentItem)
2{
3    var currentIndex = collection.IndexOf(currentItem);
4    if (currentIndex < collection.Count - 1)
5    {
6        return collection[currentIndex + 1];
7    }
8    return collection[0];
9}
csharp
1private void _audioPlayer_PlaybackStopped()
2{
3    _playbackState = PlaybackState.Stopped;
4    PlayPauseImageSource = "../Images/play.png";
5    CommandManager.InvalidateRequerySuggested();
6    CurrentTrackPosition = 0;
7
8    if (_audioPlayer.PlaybackStopType == AudioPlayer.PlaybackStopTypes.PlaybackStoppedReachingEndOfFile)
9    {
10        CurrentlySelectedTrack = Playlist.NextItem(CurrentlyPlayingTrack);
11        StartPlayback(null);
12    }
13}
14
15private void _audioPlayer_PlaybackResumed()
16{
17    _playbackState = PlaybackState.Playing;
18    PlayPauseImageSource = "../Images/pause.png";
19}
20
21private void _audioPlayer_PlaybackPaused()
22{
23    _playbackState = PlaybackState.Paused;
24    PlayPauseImageSource = "../Images/play.png";
25}
csharp

Stop

For stopping first we need to indicate why we are stopping. We are stopping because user clicked stop or we are stopping because we reached the end of the current clip. Then stop the current clip and dispose it.

1private void StopPlayback(object p)
2{
3    if (_audioPlayer != null)
4    {
5        _audioPlayer.PlaybackStopType = AudioPlayer.PlaybackStopTypes.PlaybackStoppedByUser;
6        _audioPlayer.Stop();
7    }
8}
9private bool CanStopPlayback(object p)
10{
11    if (_playbackState == PlaybackState.Playing || _playbackState == PlaybackState.Paused)
12    {
13        return true;
14    }
15    return false;
16}
csharp

Skip to Next Track

When we reach to the end of the track, we must automatically move to the next track. However, with the "Skip to Next Track" button, we can skip to the end of the current track and play the next one. We do this by setting the position to the last second of the current track.

1private void ForwardToEnd(object p)
2{
3    if (_audioPlayer != null)
4    {
5        _audioPlayer.PlaybackStopType = AudioPlayer.PlaybackStopTypes.PlaybackStoppedReachingEndOfFile;
6        _audioPlayer.SetPosition(_audioPlayer.GetLenghtInSeconds());
7    }
8}
9private bool CanForwardToEnd(object p)
10{
11    if (_playbackState == PlaybackState.Playing)
12    {
13        return true;
14    }
15    return false;
16}
csharp

Shuffle

First, we need to make an extension method for ObservableCollection<T>. We will use a commonly-used shuffle method. See this StackOverflow post for more.

1public static ObservableCollection<T> Shuffle<T>(this ObservableCollection<T> input)
2{
3    var provider = new RNGCryptoServiceProvider();
4    var n = input.Count;
5    while (n > 1)
6    {
7        var box = new byte[1];
8        do provider.GetBytes(box);
9        while (!(box[0] < n * (Byte.MaxValue / n)));
10        var k = (box[0] % n);
11        n--;
12        var value = input[k];
13        input[k] = input[n];
14        input[n] = value;
15    }
16
17    return input;
18}
csharp

And for the command:

1private void Shuffle(object p)
2{
3    Playlist = Playlist.Shuffle();
4}
5private bool CanShuffle(object p)
6{
7    if (_playbackState == PlaybackState.Stopped)
8    {
9        return true;
10    }
11    return false;
12}
csharp

MVVM event commands

For the last part, we need to deal with our MVVM event commands.

PreviewMouseDown and PreviewMouseUp Events on Seekbar

Here, we need to think about what we are actually doing while we are scrubbing left and right in the seekbar.

First, we click and hold our mouse button. Then, while button is held, we move the mouse. Finally, we release the button. It turns out that we need two events; one is for MouseDown and the other is MouseUp. While we are holding the mouse key, we need to pause the clip, and when we release, we set the current seekbar value to the current track's position to move the clip there.

1private void TrackControlMouseDown(object p)
2{
3    if (_audioPlayer != null)
4    {
5        _audioPlayer.Pause();
6    }
7}
8
9private void TrackControlMouseUp(object p)
10{
11    if (_audioPlayer != null)
12    {
13        _audioPlayer.SetPosition(CurrentTrackPosition);
14        _audioPlayer.Play(NAudio.Wave.PlaybackState.Paused, CurrentVolume);
15    }
16}
17
18private bool CanTrackControlMouseDown(object p)
19{
20    if (_playbackState == PlaybackState.Playing)
21    {
22        return true;
23    }
24    return false;
25}
26
27private bool CanTrackControlMouseUp(object p)
28{
29    if (_playbackState == PlaybackState.Paused)
30    {
31        return true;
32    }
33    return false;
34}
csharp

Volume Control Event

Volume control code is very similar to the seekbar, but it is way easier to implement because we can change the volume without knowing where we are on the track. We just set the current value of the slider to the volume itself.

1private void VolumeControlValueChanged(object p)
2{
3    if (_audioPlayer != null)
4    {
5        _audioPlayer.SetVolume(CurrentVolume); // set value of the slider to current volume
6    }
7}
8
9private bool CanVolumeControlValueChanged(object p)
10{
11    return true;
12}
csharp

Finally, we've finished developing a fully functional media player!

Conclusion

In this tutorial, we built a media player from scratch using the NAudio library. You can test it by loading your favorite music folder and playing it on the player. I hope this guide will help you on future audio projects.

Have fun and happy coding!