简体   繁体   中英

SoundPlayer.Stop does not stop sound playback

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?

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM