Click here to Skip to main content
15,868,164 members
Articles / Desktop Programming / WPF

WPF Audio Player

Rate me:
Please Sign up or sign in to vote.
4.77/5 (13 votes)
2 Aug 2011Apache6 min read 81.6K   5.8K   31   8
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:

C#
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.

Selecting the correct build action for .NET assembly resources.

Then you can create a AudioPlayer instance like this:

C#
// 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:

C#
/// <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.

FeatureSoundPlayerMediaPlayer
Play multiple sounds at the same timeAll 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 loopingYesOnly through MediaEnded event with explicitly stopping and playing the sound.
Supports loading audio files from resourcesYesNo
Can play formats other than .wav (e.g., MP3s)No, .wav only.Yes, including .mp3.
Supports easy re-playYes, 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:

C#
// 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 with FILE_FLAG_DELETE_ON_CLOSE: Doesn't work because every file handle to this file needs to be opened with FILE_FLAG_DELETE_ON_CLOSE being set. However, we have no control over how MediaPlayer 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.

Exception thrown when trying to close a MediaPlayer from a destructor/finalizer.

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:

C#
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:

C#
// 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 DispatcherTimers in AudioPlayer (which doesn't run on the player thread and therefore can use DispatcherTimers).

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.

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0


Written By
Software Developer University of Stuttgart
Germany Germany
I have studied Software Engineering and am currently working at the University of Stuttgart, Germany.

I have been programming for many years and have a background in C++, C#, Java, Python and web languages (HTML, CSS, JavaScript).

Comments and Discussions

 
Questionstreaming to upnp devices Pin
Member 1125180920-Nov-14 18:21
Member 1125180920-Nov-14 18:21 
Questionexception in windows XP Pin
YevgeniyZ24-Sep-12 2:46
YevgeniyZ24-Sep-12 2:46 
QuestionMust be Open Source? Pin
Juanfra32-May-12 8:40
Juanfra32-May-12 8:40 
AnswerRe: Must be Open Source? Pin
Sebastian Krysmanski6-May-12 20:27
Sebastian Krysmanski6-May-12 20:27 
GeneralRe: Must be Open Source? Pin
Juanfra37-May-12 9:37
Juanfra37-May-12 9:37 
GeneralRe: Must be Open Source? Pin
Sebastian Krysmanski9-May-12 4:23
Sebastian Krysmanski9-May-12 4:23 
QuestionMake Our Day... Pin
Clinton Gallagher8-Aug-11 15:35
professionalClinton Gallagher8-Aug-11 15:35 
Audiophiles need a solution that will allow them to use Windows Task Scheduler to schedule an app that can record talk shows played back using WinAmp. The pervasive Streamripper will record but I've never been able to get it to respond to Task Scheduler.
clintonG

QuestionGood work! Pin
lapadets2-Aug-11 4:48
lapadets2-Aug-11 4:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.