WPF Audio Player






4.77/5 (13 votes)
Playing audio files in .NET/WPF (or replacing SoundPlayer and MediaPlayer).
Introduction
When writing a desktop application, it sometimes becomes necessary to play some audio files. .NET/WPF comes with two classes trying to achieve
this goal: SoundPlayer
and
MediaPlayer
Unfortunately, both classes come with some (severe) limitations that make them hard to use under certain (not so uncommon) circumstances. This article will provide a replacement for both classes. It'll also provide some more details on the limitations and problems associated with these two classes.
Using the code
Before going into more detail, let's jump ahead and take a look at the final class. It's called AudioPlayer
. Here's how to use it:
AudioPlayer myAudioPlayer = new AudioPlayer(...);
myAudioPlayer.Play();
This simply plays the audio file. The audio file is specified as an argument to the constructor. You can either use an absolute or relative file path on the file system, or choose a .NET assembly resource. If you want to use a resource, you first need to set its "Build Action" to "Embedded Resource". To do this, right-click the audio file in Solution Explorer and choose "Properties". This will open the "Properties" pane where you can select the appropriate build action.
Then you can create a AudioPlayer
instance like this:
// NOTE: "Assembly.GetExecutingAssembly()"
// returns the assembly in which the code is contained in.
// So, this example only works, if the audio file is in the same assembly (read: project)
// as the file containing this code.
AudioPlayer myAudioPlayer = new AudioPlayer(Assembly.GetExecutingAssembly(),
"MyRootNamespace", "myfolder/myfile.mp3");
Besides Play()
, AudioPlayer
contains at lot of other useful stuff. Here is its outline:
/// <summary>
/// This class is a replacement for <c>SoundPlayer</c>
/// and <c>MediaPlayer</c> classes. It solves
/// their shortcomings.
/// </summary>
public class AudioPlayer {
/// <summary>
/// Indicates whether currently the sound is playing.
/// </summary>
public bool IsPlaying { get; }
/// <summary>
/// Specifies whether the sound is to be looped. Defaults to <c>false</c>.
/// </summary>
public bool IsLooped { get; set; }
/// <summary>
/// The volume with which to play the sound.
/// Ranges from 0 to 1 (with 1 being the loudest).
/// Defaults to 1.
/// </summary>
public double Volume { get; set; }
/// <summary>
/// The length (duration) of this audio file.
/// </summary>
public Duration Length { get; }
/// <summary>
/// The position where the audio file is currently playing.
/// </summary>
public TimeSpan Position { get; set; }
/// <summary>
/// This event is fire if either the playback was stopped
/// by using <see cref="Stop"/> or when a
/// non-looping audio file has reached its end.
/// </summary>
public event EventHandler<EventArgs> PlaybackEnded;
/// <summary>
/// Creates an audio player from a file relative to the executing application's path.
/// </summary>
/// <param name="fileName">the audio file to be played</param>
/// <param name="looping">shall the audio file be played in a loop</param>
public AudioPlayer(string fileName, bool looping = false);
/// <summary>
/// Creates an audio player from a .NET assembly's (usually DLL) resource file.
/// </summary>
/// <param name="assembly">the assembly that contains the audio file</param>
/// <param name="assemblyNamespace">the default
/// namespace of the assembly (as specified in the
/// assembly's project settings)</param>
/// <param name="mediaFile">the audio file's
/// path relative to the assembly's root. This file's
/// build action must be set to "Embedded Resource".</param>
/// <param name="looping">shall the audio file be played in a loop</param>
public AudioPlayer(Assembly assembly, string assemblyNamespace, string mediaFile,
bool looping = false);
/// <summary>
/// Starts playing the sound. If <see cref="IsLooped"/> is <c>true</c>, this sound will be
/// played until <see cref="Stop"/> is called. Otherwise the sound is played once. Calling this
/// method while the sound is already playing resets the play position to the beginning of the
/// sound file (ie. the sound is restarted).
/// </summary>
public void Play();
/// <summary>
/// Stops the current playback. Does nothing, if the sound isn't playing.
/// </summary>
public void Stop();
}
The meaning and usage of each method/property should be straightforward.
The demo project contains example code for playing audio files from the file system as well as playing files from .NET assembly resources. You can find the code in MainWindow.xaml.cs in the "AudioPlayerDemo" project.
Comparison of SoundPlayer and MediaPlayer
As mentioned earlier, the .NET classes SoundPlayer
and MediaPlayer
come with some limitations. These limitations are listed here for your interest.
Feature | SoundPlayer | MediaPlayer |
Play multiple sounds at the same time | All SoundPlayer instances share one single "audio channel", i.e., you can't play multiple sounds at the same time, even if you have
multiple SoundPlayer instances. This also results in a problem when you try to repeatedly play the same sound rapidly. (Think of the click sound
of the click wheel on your iPod, if you own one.) In this case, the playback "delays" for some time. |
Can play multiple sounds at the same time. |
---|---|---|
Supports looping | Yes | Only through MediaEnded event with explicitly stopping and playing the sound. |
Supports loading audio files from resources | Yes | No |
Can play formats other than .wav (e.g., MP3s) | No, .wav only. | Yes, including .mp3. |
Supports easy re-play | Yes, simply call Play() again to play the sound again. |
No, requires the user to reset the playing position with Stop() before being able to play the sound again. |
Behind the scenes
Now that we've established the limitations of both SoundPlayer
and MediaPlayer
, let's dive a little bit deeper into resolving these limitations.
This section explains the problems that were encountered during the implementation of AudioPlayer
. Reading this section isn't required for using AudioPlayer
,
so you can skip it if you're just interested in using AudioPlayer
.
Let's get started. The only severe limitation of MediaPlayer
, in my opinion, is that it can't load audio files from .NET assembly resources. (The help page
MediaPlayer
clearly states this - but unfortunately provides no alternative solution.) So, I implemented AudioPlayer
as a wrapper
around MediaPlayer
(and not around SoundPlayer
).
Supporting "easy re-play" and "looping" was implemented easily enough, so I won't go into the details for these features. See the attached demo project for details.
Exporting resources to use them with MediaPlayer
The bigger problem was playing audio files from .NET assembly resources. As a workaround, the basic idea was to export a resource into a temporary file. This can easily be achieved by a code similar to this:
// An assembly can be obtained by using "Assembly.GetExecutingAssembly()".
public void ExportResource(Assembly assembly, string assemblyNamespace, string mediaFile) {
string fullFileName = assemblyNamespace + "." + mediaFile;
// "m_resourceTempDir" is the directory where to store the temporary files.
string tmpFile = Path.Combine(this.m_resourceTempDir, fullFileName);
using (Stream input = assembly.GetManifestResourceStream(fullFileName)) {
using (Stream file = File.OpenWrite(tmpFile)) {
// Function to copy the input stream to the output stream.
CopyStream(input, file);
}
}
}
We can then use this temporary file for MediaPlayer
and we're done - are we not? Unfortunately, not.
Deleting temporary files
The temporary file needs to be deleted when the application closes at the latest - and that's not as easy to implement as it sounds.
The following list lists all approaches that I've tried and also describes if and why they don't work:
- Use
Path.GetTempFileName
: This doesn't solve the problem as the temporary file won't be deleted when the application closes. - Use
CreateFile
together withFILE_FLAG_DELETE_ON_CLOSE
: Doesn't work because every file handle to this file needs to be opened withFILE_FLAG_DELETE_ON_CLOSE
being set. However, we have no control over howMediaPlayer
opens its files. - Remember each temporary file that was created and delete it when it is no longer used: This approach works, but also not as easily as it sounds. More on that below.
So, remembering all created temporary files is the way to go. The problem now is: When do we delete these files? There are two possibilities:
- Remember each created file in its associated
AudioPlayer
instance and delete it from its destructor/finalizer. - Keep a list of all created files in a single place (i.e., a singleton class) and delete all files from within an "application closing event" handler.
Unfortunately, this approach doesn't work out-of-the-box because MediaPlayer
holds a file handle to the temporary file. And as long as it holds this handle,
we can't delete the file. There is, however, the method MediaPlayer.Close()
that closes this handle.
Now, MediaPlayer
inherits from DispatcherObject
and therefore only allows modifications from the thread that created the MediaPlayer
instance. This includes the method Close()
, which is unfortunate because every destructor/finalizer as well as every AppDomain.ProcessExit
handler runs
on a separate thread. So, Close()
can't be called from either of them.
To solve this problem, one usually uses the DispatcherObject
's Dispatcher
to invoke the method on the owning thread.
Unfortunately, using Dispatcher.Invoke()
from within a destructor or a AppDomain.ProcessExit
event handler doesn't work.
Nothing happens on these calls so they can't be used in this context.
Our own MediaPlayer thread
The only solution to this problem I could think of is: Create a separate thread that creates and closes MediaPlayer
instances.
The basic implementation of the thread's run method would look like this:
private void RunThread() {
CreateMyMediaPlayerInstances();
WaitForThreadShutdown();
foreach (MediaPlayer player in this.m_myMediaPlayerInstances) {
player.Close();
}
}
Now, the easiest way to do this is to use a Dispatcher
on the thread.
This Dispatcher
then would be used to create and manipulate the MediaPlayer
instances. With this, the implementation would look like this:
// Constructor
private PlayerThread() {
Thread playerThread = new Thread(RunThread);
playerThread.IsBackground = true;
playerThread.SetApartmentState(ApartmentState.STA);
playerThread.Start();
AppDomain.CurrentDomain.ProcessExit += (s, e) => {
// Shutdown dispatcher
Dispatcher.FromThread(playerThread).InvokeShutdown();
// Wait for thread to terminate.
playerThread.Join();
RemoveAllTemporaryFiles();
};
}
private void RunThread() {
Dispatcher.Run(); // will return when the dispatch has been shut down
foreach (MediaPlayer player in this.m_myMediaPlayerInstances) {
player.Close();
}
}
Unfortunately (yet again), Dispatcher.InvokeShutdown()
doesn't work from within a AppDomain.ProcessExit
event handler. Just nothing happens
and also Dispatcher.Run()
never returns. (I filed a bug report
with Microsoft on this issue but I fear they will say it's by design.)
I also tried various other solutions such as repeatedly calling Dispatcher.ExitAllFrames()
or using Dispatcher.PushFrame(new DispatcherFrame(true))
but without any luck.
So, the final solution was to write my own "Dispatcher" implementation (called EventQueue
).
Note: It seems that MediaPlayer
uses DispatcherTimer
for its events (at least for MediaPlayer.MediaEnded
). However, because we're not using a Dispatcher
on the player thread, these timers are never
evaluated/executed and thus the events are never fired. Therefore, these events need to be "simulated" with DispatcherTimer
s in AudioPlayer
(which doesn't run
on the player thread and therefore can use DispatcherTimer
s).
Source code repository
Besides the download provided here at CodeProject, you can find the Mercurial repository for this article here: https://bitbucket.org/skrysmanski/audioplayer.
History
- 2011-08-02:
- Original article.