Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to know when SoundPlayer has finished playing a sound

I am using the following code to dynamically create a frequency tone in memory and play the tone asynchronously:

public static void PlayTone(UInt16 frequency, int msDuration, UInt16 volume = 16383)
{
    using (var mStrm = new MemoryStream())
    {
        using (var writer = new BinaryWriter(mStrm))
        {
            const double tau = 2*Math.PI;
            const int formatChunkSize = 16;
            const int headerSize = 8;
            const short formatType = 1;
            const short tracks = 1;
            const int samplesPerSecond = 44100;
            const short bitsPerSample = 16;
            const short frameSize = (short) (tracks*((bitsPerSample + 7)/8));
            const int bytesPerSecond = samplesPerSecond*frameSize;
            const int waveSize = 4;
            var samples = (int) ((decimal) samplesPerSecond*msDuration/1000);
            int dataChunkSize = samples*frameSize;
            int fileSize = waveSize + headerSize + formatChunkSize + headerSize + dataChunkSize;

            writer.Write(0x46464952);
            writer.Write(fileSize);
            writer.Write(0x45564157);
            writer.Write(0x20746D66);
            writer.Write(formatChunkSize);
            writer.Write(formatType);
            writer.Write(tracks);
            writer.Write(samplesPerSecond);
            writer.Write(bytesPerSecond);
            writer.Write(frameSize);
            writer.Write(bitsPerSample);
            writer.Write(0x61746164);
            writer.Write(dataChunkSize);

            double theta = frequency*tau/samplesPerSecond;
            double amp = volume >> 2;
            for (int step = 0; step < samples; step++)
            {
                writer.Write((short) (amp*Math.Sin(theta*step)));
            }

            mStrm.Seek(0, SeekOrigin.Begin);
            using (var player = new System.Media.SoundPlayer(mStrm))
            {
                player.Play();
            }
        }
    }
}

The code is working fine. The only issue is, how do I know when the tone has stopped playing? There doesn't appear to be a Completed event on the SoundPlayer class that I can subscribe to.

like image 666
Icemanind Avatar asked Dec 10 '14 02:12

Icemanind


1 Answers

You know, if all you want to do is play a single tone, there is Console.Beep. Granted, it doesn't do it in the background, but the technique I describe below will work fine for Console.Beep, and it prevents you having to create a memory stream just to play a tone.

In any case, SoundPlayer doesn't have the functionality that you want, but you can simulate it.

First, create your event handler:

void SoundPlayed(object sender, EventArgs e)
{
    // do whatever here
}

Change your PlayTone method so that it takes a callback function parameter:

public static void PlayTone(UInt16 frequency, int msDuration, UInt16 volume = 16383, EventHandler doneCallback = null)

Then, change the end of the method so that it calls PlaySync rather than Play, and calls the doneCallback after it's done:

using (var player = new System.Media.SoundPlayer(mStrm))
{
    player.PlaySync();
}    
if (doneCallback != null)
{
    // the callback is executed on the thread.
    doneCallback(this, new EventArgs());
}

Then, execute it in a thread or a task. For example:

var t = Task.Factory.StartNew(() => {PlayTone(100, 1000, 16383, SoundPlayed));

The primary problem with this is that the event notification occurs on the background thread. Probably the best thing to do if you need to affect the UI is to have the event handler synchronize with the UI thread. So in the SoundPlayed method, you'd call Form.Invoke (for WinForms) or Dispatcher.Invoke (WPF) to execute any UI actions.

like image 63
Jim Mischel Avatar answered Nov 02 '22 00:11

Jim Mischel