How to know when SoundPlayer has finished playing a sound
Asked Answered
F

1

7

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.

Folio answered 10/12, 2014 at 2:30 Comment(3)
You are right you do not have a completed event, if you need one you most likely will need to use the Windows Media Player ActiveX Control.Domino
@MarkHall -- Can the ActiveX control grab the source from a MemoryStream or does it have to come from a URL/File?Folio
I am not sure about whether it does or not, one of the reasons I posted it as a comment. It has been a few years since I have had to use it and I just used a file.Domino
N
6

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.

Nairn answered 10/12, 2014 at 3:20 Comment(5)
Does Console.Beep play the sound through the sound card or the PC Speaker?Folio
This worked! I'm using .NET 3.5, so I had to call it like this: var t = new Thread(() => PlayTone(1000, 1000, Done)); and then start it: t.Start();. But its doing what I need.Folio
@icemanind: Documentation says that it plays "through the console speaker." On my machine, it appears to play through the sound card. A little looking around seems to say that it'll play through the sound card if one is available. Otherwise it beeps the internal speaker. But I haven't tested it, so I can't say for sure.Nairn
@icemanind: Rather than starting a new thread, you might consider instead using ThreadPool.QueueUserWorkItem. That way you won't have a bunch of Thread instances lying around that you have to Join.Nairn
Regarding where the sound plays, most computers today don't actually have a PC Speaker any more, and in recognition of that, since Windows 7, Windows has come equipped with a standard system service called "Beep" that intercepts attempts to do a PC speaker beep and simulates them using waveform output to the soundsystem.Plasticizer

© 2022 - 2024 — McMap. All rights reserved.