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.
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.