Reputation: 8594
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 that calls the SoundPlayer.PlaySync
method to play the .wav file. It's all wrapped up in a class that looks like this:
public class SoundController {
private object soundLocker = new object();
protected Thread SoundThread { get; set; }
protected string NextSound { get; set; }
protected AutoResetEvent PlayASoundPlag { get; set; }
protected Dictionary<string, SoundPlayer> Sounds { get; set; }
protected bool Stopping { get; set; }
public string SoundPlaying { get; private set; }
public SoundController() {
PendingCount = 0;
PlayASoundFlag = new AutoResetEvent( false );
Sounds = new Dictionary<string, SoundPlayer>();
soundLocker = new object();
Stopping = false;
SoundThread = new Thread( new ThreadStart( SoundPlayer ) ) { Name = "SoundThread", IsBackground = true };
SoundThread.Start();
}
private void SoundPlayer() {
do {
PlayASoundFlag.WaitOne();
bool soundWasPlayed = false;
while ( !Stopping && NextSound != null ) {
lock ( soundLocker ) {
SoundPlaying = NextSound;
NextSound = null;
}
Sounds[ SoundPlaying ].PlaySync();
lock ( soundLocker ) {
SoundPlaying = null;
soundWasPlayed = true;
}
}
} while ( !Stopping );
}
public bool HasSound( string key ) {
return Sounds.ContainsKey( key );
}
public void PlayAlarmSound( string key, bool stopCurrentSound ) {
if ( !Sounds.ContainsKey( key ) )
throw new ArgumentException( "Sound unknown", "key" );
lock ( soundLocker ) {
NextSound = key;
if ( SoundPlaying != null && stopCurrentSound )
Sounds[ SoundPlaying ].Stop();
PlayASoundFlag.Set();
}
}
}
When my program calls the PlaySound
method, and a sound is currently playing, the Stop
method is called, but the sound that's playing doesn't actually stop. I've placed trace points on the call to Stop
and a line I added after it just so I could see when the call was made and when it returned, while listening with headphones. It's obvious that the sound plays all the way through to the end.
How do I get the sounds to stop playing reliably?
Upvotes: 1
Views: 2733
Reputation: 11273
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.
Upvotes: 3