Wednesday, January 21, 2009

How to play a sound

http://www.bobpowell.net/playsnd.htm

Windows Forms classes are bereft of a simple way to play sounds. This glaring oversight is however easily rectified with a little bit of platform-invoke interop.

There are a couple of ways to do this. The first method requires the multimedia API sndPlaySnd and a few constants defined but once this is done you can add sounds to your Windows Forms applications easily. This API is a subset of the PlaySound API which can also be accessed through interop.

First you need to import the sndPlaySound or PlaySound API from the multimedia DLL. Interop enables us to import these signatures in a number of ways. For example, in some configurations the sndPlaySound method can be passed the address of a .WAV sound stored in memory but C# and VB are a bit finicky about casting strings to pointers so we can overload the p/invoke import statement to provide a version of the method signature that accepts an IntPtr instead of a string.

[DllImport("Winmm.dll")]

static extern int sndPlaySound(string lpszSound, int fuSound);

[DllImport("Winmm.dll")]

static extern int sndPlaySound(IntPtr ptr, int fuSound);

_

Shared Function sndPlaySound(lpszSound As String, fuSound As Integer) As Integer

_

Shared Function sndPlaySound(ptr As IntPtr, fuSound As Integer) As Integer

The constants required for the multimedia functions can be converted from the old C++ header files.

enum soundConstants

{

SND_SYNC = 0x0000, /* play synchronously (default) */

SND_ASYNC = 0x0001, /* play asynchronously */

SND_NODEFAULT = 0x0002, /* silence (!default) if sound not found */

SND_MEMORY = 0x0004, /* pszSound points to a memory file */

SND_LOOP = 0x0008, /* loop the sound until next sndPlaySound */

SND_NOSTOP = 0x0010, /* don't stop any currently playing sound */

SND_NOWAIT = 0x00002000, /* don't wait if the driver is busy */

SND_ALIAS = 0x00010000, /* name is a registry alias */

SND_ALIAS_ID = 0x00110000, /* alias is a predefined ID */

SND_FILENAME = 0x00020000, /* name is file name */

SND_RESOURCE = 0x00040004, /* name is resource name or atom */

SND_PURGE = 0x0040, /* purge non-static events for task */

SND_APPLICATION = 0x0080, /* look for application specific association */

}

Enum soundConstants

SND_SYNC = &H0 ' play synchronously (default)

SND_ASYNC = &H1 ' play asynchronously

SND_NODEFAULT = &H2 ' silence (!default) if sound not found

SND_MEMORY = &H4 ' pszSound points to a memory file

SND_LOOP = &H8 ' loop the sound until next sndPlaySound

SND_NOSTOP = &H10 ' don't stop any currently playing sound

SND_NOWAIT = &H2000 ' don't wait if the driver is busy

SND_ALIAS = &H10000 ' name is a registry alias

SND_ALIAS_ID = &H110000 ' alias is a predefined ID

SND_FILENAME = &H20000 ' name is file name

SND_RESOURCE = &H40004 ' name is resource name or atom

SND_PURGE = &H40 ' purge non-static events for task

SND_APPLICATION = &H80 ' look for application specific association

End Enum 'soundConstants

Then, to play a sound simply call the method and provide the sound file.

sndPlaySound(@"C:\Sounds\Explosion1.wav", (int)soundConstants.SND_ASYNC);

sndPlaySound("C:\Sounds\Explosion1.wav", CInt(soundConstants.SND_ASYNC))

You can embed a .wav file into your application by adding it to the application and then setting the build action to "embedded resource" in the solution item properties. This enables you to ship an application with the sounds built into the resources and so there are no extra files to distribute.

Getting at the file is a little more complex than simply nominating the filename an must be done using a Garbage Collector handle (GCHandle) and a pinned array. The following listing assumes a file has been embedded in the resources and is accessible as a stream.

Stream s=this.GetType().Assembly.GetManifestResourceStream("PlaySound.win_1.wav");

byte[] buffer=new byte[s.Length];

s.Read(buffer,0,(int)s.Length);

GCHandle h=GCHandle.Alloc(buffer);

IntPtr ptr=Marshal.UnsafeAddrOfPinnedArrayElement(buffer,0);

sndPlaySound(ptr,(int)soundConstants.SND_MEMORY | (int)soundConstants.SND_ASYNC);

h.Free();

Dim s As Stream = Me.GetType().Assembly.GetManifestResourceStream("PlaySoundVB.win_1.wav")

Dim buffer(s.Length) As Byte

s.Read(buffer, 0, CInt(s.Length))

Dim h As GCHandle = GCHandle.Alloc(buffer)

Dim ptr As IntPtr = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0)

sndPlaySound(ptr, CInt(soundConstants.SND_MEMORY) Or CInt(soundConstants.SND_ASYNC))

-----------------------------------------------------------------------------------------

  1. // Include file
  2. #include "mmsystem.h"
  3. // Link to this library
  4. #pragma comment( lib, "winmm.lib" )
  5. int main( int argc, char **argv )
  6. {
  7. // Will block till the whole file is played, use SND_ASYNC to play asynchronously
  8. sndPlaySound( "c://windows//media//ding.wav", SND_SYNC );
  9. // Play for ever, should use SND_ASYNC
  10. sndPlaySound( "c://windows//media//ding.wav", SND_ASYNC|SND_LOOP );
  11. // Sleep for 5 seconds
  12. Sleep( 5000 );
  13. // Enough is enough stop making that stupid noise
  14. sndPlaySound( NULL, SND_SYNC );
  15. return 0;
  16. }// End main

No comments: