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.
Whatever your reason is, you have challenges waiting for you down the road when it comes to 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!
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.
The main purpose of this tutorial is to build a simple media player which will be able to:
It should look like this at the end (with my Witcher 3 soundtrack playlist):
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:
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:
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.
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:
C:\Program Files (x86)\Microsoft SDKs\Expression\Blend\.NETFramework\v4.5\Libraries
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:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
to the Window
.Now everything is ready for our 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>
So by looking at our XAML code, in our MainWindowViewModel
viewmodel, we need to implement the following commands:
Playlist
into a text file, simply by writing the path of each Track
in the Playlist
as <filename>.playlist
..playlist
file of our choosing and generate a Playlist
for us.Playlist
.Playlist
.CurrentTrackPosition
to 0, effectively skipping to start.CurrentlySelectedTrack
. When pressed during playback, it will pause the playback.Playlist
randomly.and properties:
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}
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}
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.
Now that we have initialized everything, we can implement the rest of our methods.
We need methods for:
PlaybackStopped
eventDispose()
method to clear up the memory1public 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}
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}
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}
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}
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}
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}
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}
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}
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}
For the last part, we need to deal with our MVVM event commands.
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}
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}
Finally, we've finished developing a fully functional media player!
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!