I have an application written in C# which plays little .wav files. It uses the SoundPlayer
class in the System.Media namepace to play the sounds, using a thread th
Unfortunately this is a messy process, but this works:
class Program
{
static void Main(string[] args)
{
SoundPlayerEx player = new SoundPlayerEx(@"c:\temp\sorry_dave.wav");
player.SoundFinished += player_SoundFinished;
Console.WriteLine("Press any key to play the sound");
Console.ReadKey(true);
player.PlayAsync();
Console.WriteLine("Press a key to stop the sound.");
Console.ReadKey(true);
player.Stop();
Console.WriteLine("Press any key to continue");
}
static void player_SoundFinished(object sender, EventArgs e)
{
Console.WriteLine("The sound finished playing");
}
}
public static class SoundInfo
{
[DllImport("winmm.dll")]
private static extern uint mciSendString(
string command,
StringBuilder returnValue,
int returnLength,
IntPtr winHandle);
public static int GetSoundLength(string fileName)
{
StringBuilder lengthBuf = new StringBuilder(32);
mciSendString(string.Format("open \"{0}\" type waveaudio alias wave", fileName), null, 0, IntPtr.Zero);
mciSendString("status wave length", lengthBuf, lengthBuf.Capacity, IntPtr.Zero);
mciSendString("close wave", null, 0, IntPtr.Zero);
int length = 0;
int.TryParse(lengthBuf.ToString(), out length);
return length;
}
}
public class SoundPlayerEx : SoundPlayer
{
public bool Finished { get; private set; }
private Task _playTask;
private CancellationTokenSource _tokenSource = new CancellationTokenSource();
private CancellationToken _ct;
private string _fileName;
private bool _playingAsync = false;
public event EventHandler SoundFinished;
public SoundPlayerEx(string soundLocation)
: base(soundLocation)
{
_fileName = soundLocation;
_ct = _tokenSource.Token;
}
public void PlayAsync()
{
Finished = false;
_playingAsync = true;
Task.Run(() =>
{
try
{
double lenMs = SoundInfo.GetSoundLength(_fileName);
DateTime stopAt = DateTime.Now.AddMilliseconds(lenMs);
this.Play();
while (DateTime.Now < stopAt)
{
_ct.ThrowIfCancellationRequested();
//The delay helps reduce processor usage while "spinning"
Task.Delay(10).Wait();
}
}
catch (OperationCanceledException)
{
base.Stop();
}
finally
{
OnSoundFinished();
}
}, _ct);
}
public new void Stop()
{
if (_playingAsync)
_tokenSource.Cancel();
else
base.Stop(); //To stop the SoundPlayer Wave file
}
protected virtual void OnSoundFinished()
{
Finished = true;
_playingAsync = false;
EventHandler handler = SoundFinished;
if (handler != null)
handler(this, EventArgs.Empty);
}
}
So why doesn't it work "normally"? Its a well known problem. The SoundPlayer
is a "fire and forget" piece of code, and if you don't cancel it on the same thread that you started it on, it will not do anything. A lot of people complain about it and as I'm sure you've seen there are very few solutions out side of using raw wave_out
calls or moving to DirectX (or with WPF, using the MediaPlayer
control).
This SoundPlayerEx
class has a couple properties that let you know when the sound is finished or to cancel playing a sound that you started asynchronously. There is no need to create a new thread to work on, making it a lot easier to use.
Feel free to expand on the code, it was a quick and dirty solution to your problem. The two classes you need are the SoundInfo class and the SoundPlayerEx class, the rest of the code above is a demo (replace the wav file with one of your own).
Note this is not a universal solution as it relies on the winmm.dll, so this will not port over to Mono (not sure if Mono has a SoundPlayer
class or not). Also since its a dirty solution, you won't get the Finished
event or property if you don't use the PlayAsync
call.