Click here to Skip to main content
15,885,914 members
Articles / Web Development / HTML

SoundPlayer Bug - Calling Unmanaged APIs

Rate me:
Please Sign up or sign in to vote.
4.88/5 (28 votes)
25 Apr 20065 min read 92.4K   2.1K   35   16
Calling unmanaged code from managed code is very simple, but there are some things to look out for, that even Microsoft ignores

Introduction

Recently, I was making a full-fledged blackjack card game, and wanted to play some sounds throughout the game. .NET 2.0 has made this extremely easy with the new SoundPlayer class. To my surprise though, it does not handle the case of playing an asynchronous sound from a stream correctly. Microsoft has identified this as "By Design" rather than a bug, as shown here at MSDN Feedback. The problem stems from the difficulties in calling unmanaged code asynchronously. This article will explain the problem in the SoundPlayer class, the reasons Microsoft may have classed this as "By Design" rather than a bug, and a way to play sounds asynchronously from a stream guaranteed.

The SoundPlayer Problem

The problem with the SoundPlayer class resides deep within the class itself and the call it makes to the unmanaged PlaySound. The PlaySound function takes a pointer to a byte array when playing the sound from memory. When a call to an unmanaged API is made, the garbage collector automatically pins the objects passed in, and unpins them when the call returns. However, when PlaySound is called with the SND_ASYNC flag, "The sound is played asynchronously, and PlaySound returns immediately after beginning the sound." Now the question arises, if the managed byte array has been passed into the unmanaged function PlaySound and PlaySound returns immediately, can the byte[] be moved or collected by the garbage collector while the sound is playing? The answer is yes, as outlined here in Object Lifetime and Pinning, and this is the problem I have had when playing asynchronous embedded resource sounds using the SoundPlayer.

Reproducing the Problem

Due to fact that the problem only appears when the Garbage Collector runs at the instant just after the call to SoundPlayer.Play() and the byte[] passed into PlaySound by the SoundPlayer class must be moved or collected as well, the problem can be very difficult to reproduce. However, I have produced a demo application, downloadable above, that reliably (at least on my computer) reproduces the problem. To reproduce the problem, run the demo application, and press the "Using SoundPlayer" play button. You should hear a sound that has been corrupted; if not, press the Play Sound button a couple of more times after allowing the sound to completely play, or switch between pressing the two Play buttons. Now, if you click the Switch Image button, and press the Play button again, the sound plays correctly (or at least it does on my computer). Why? Well, because the first image is much, much larger, and thus causes the Garbage Collector to be far more aggressive when collecting memory. There are ways to help the situation, like making the SoundPlayer a class variable instead of creating a new one each time Play is pressed. In this example application, this seems to fix the problem. However, in the BlackJack game I wrote, the problem still existed, just a lot less frequently.

The Code to Reproduce the Problem

Playing the Sound

C#
SoundPlayer soundPlayer = new SoundPlayer();
soundPlayer.Stop();
soundPlayer.Stream = Properties.Resources.Windows_XP_Startup;
soundPlayer.Play();

Causing the Garbage Collector to Run

C#
private void ProcessingThread()
{
  // Do some work just so memory
  // is being created and destroyed
  while (processThread)
  {
    Thread.Sleep(10);
    panel1.Invalidate();
  }
}

private void panel1_Paint(object sender, PaintEventArgs e)
{
  TimeSpan ts = DateTime.Now.Subtract(dateTime);
  int rotateAngle = (int)(ts.TotalMilliseconds / 100);
  Math.DivRem(rotateAngle, 360, out rotateAngle);

  Image image = null;
  if (largeImage)
  {
    image = Properties.Resources.Bliss;
  }
  else
  {
    image = Properties.Resources.logo;
  }

  Matrix transformMatrix = new Matrix();
  transformMatrix.RotateAt(rotateAngle, 
    new PointF(this.Width / 2, this.Height / 2));
  e.Graphics.MultiplyTransform(transformMatrix);
  e.Graphics.DrawImage(image, ClientRectangle);
  transformMatrix.Dispose();


  GC.Collect();
}

How to 100% Reliably Play Embedded Resource Sounds Asynchronously

The answer to this lies in the article here in Object Lifetime and Pinning. The byte[] must be pinned while the sound is playing, or depending on if the PlaySound function caches the sound, while it is cached. To illustrate this in action, press the "Using a Pinned byte[]" Play button. This sound should never play a corrupt sound.

C#
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

namespace SoundPlayerBug
{
  public static class SoundPlayerAsync
  {
    [DllImport("winmm.dll", SetLastError = true)]
    public static extern bool PlaySound(byte[] ptrToSound,
       System.UIntPtr hmod, uint fdwSound);

    [DllImport("winmm.dll", SetLastError = true)]
    public static extern bool PlaySound(IntPtr ptrToSound,
       System.UIntPtr hmod, uint fdwSound);

    static private GCHandle? gcHandle = null;
    private static byte[] bytesToPlay = null;
    private static byte[] BytesToPlay
    {
      get { return bytesToPlay; }
      set 
      {
        FreeHandle();
        bytesToPlay = value;
      }
    }

    public static void PlaySound(System.IO.Stream stream)
    {
      PlaySound(stream, SoundFlags.SND_MEMORY | 
                        SoundFlags.SND_ASYNC);
    }

    public static void PlaySound(System.IO.Stream stream, 
                                 SoundFlags flags)
    {
      LoadStream(stream);
      flags |= SoundFlags.SND_ASYNC;
      flags |= SoundFlags.SND_MEMORY;

      if (BytesToPlay != null)
      {
        gcHandle = GCHandle.Alloc(BytesToPlay, 
                                 GCHandleType.Pinned);
        PlaySound(gcHandle.Value.AddrOfPinnedObject(), 
                             (UIntPtr)0, (uint)flags);
      }
      else
      {
        PlaySound((byte[])null, (UIntPtr)0, (uint)flags);
      }
    }

    private static void LoadStream(System.IO.Stream stream)
    {
      if (stream != null)
      {
        byte[] bytesToPlay = new byte[stream.Length];
        stream.Read(bytesToPlay, 0, (int)stream.Length);
        BytesToPlay = bytesToPlay;
      }
      else
      {
        BytesToPlay = null;
      }
    }

    private static void FreeHandle()
    {
      if (gcHandle != null)
      {
        PlaySound((byte[])null, (UIntPtr)0, (uint)0);
        gcHandle.Value.Free();
        gcHandle = null;
      }
    }
  }

  [Flags]
  public enum SoundFlags : int
  {
    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
  }
}

The important part of the above code is:

C#
gcHandle = GCHandle.Alloc(BytesToPlay, GCHandleType.Pinned);
PlaySound(gcHandle.Value.AddrOfPinnedObject(), (UIntPtr)0, (uint)flags);

The byte array that is about to be played is pinned so the garbage collector cannot move or collect it.

Unpinning the byte array is also very important, otherwise you will have a memory leak.

C#
if (gcHandle != null)
{
    PlaySound((byte[])null, (UIntPtr)0, (uint)0);
    gcHandle.Value.Free();
    gcHandle = null;
}

Why Would Microsoft Resolve this as "By Design"

I believe Microsoft has resolved my bug report as "By Design" as there are Garbage Collector performance implications when objects are manually pinned. This is of increased concern with the SoundPlayer class as, when to unpin the object is impossible to determine because there is no way of confirming when the sound has finished playing. I would be intrigued to know how C++ programmers deal with calling the PlaySound function. It would seem to me they would have a similar problem in identifying when to delete the byte* passed to PlaySound. The solution to the problem I have presented does not deal with unpinning the byte[] unless another sound is played. This could potentially leave a byte[] pinned for a lengthy period of time unnecessarily.

Ways to Avoid the Problem

  • Don't play asynchronous sounds from memory using the SoundPlayer class.
  • Instead of using embedded sounds, install any sounds to be played as a file. Sounds played from files don't have the same problem.
  • Avoid memory intensive operations while a memory sound is playing asynchronously.

Thank You

Code Project has helped me in many ways over the years, so I hope this article saves some people the headache I had in trying to locate and resolve the above problem. This is also my first attempt at an article, so any feedback would be greatly appreciated.

License

This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below. A list of licenses authors might use can be found here.


Written By
Software Developer (Senior)
Australia Australia
I am currently a Software Engineer working for an international company on a defence project. I graduated from university in 2001 with a Bacehlor of Engineering (Aerospace Avionics) First Class Honours. Currently in my spare time I am experimenting with the joys of shareware. I also enjoy most sports including, basketball, netball and rockclimbing.
www.s3ware.com
www.s3search.com.au
Civic Shower Screens

Comments and Discussions

 
Questionlicensing Pin
Member 106280088-Jun-15 7:31
Member 106280088-Jun-15 7:31 
QuestionThanks!!! Pin
anth718-Sep-11 15:03
anth718-Sep-11 15:03 
QuestionA word of warning Pin
MarkLTX22-Aug-11 10:25
MarkLTX22-Aug-11 10:25 
QuestionHow to make it work in a web application? Pin
Adnan Heryani11-Jun-09 2:25
Adnan Heryani11-Jun-09 2:25 
Generallike a charm Pin
scotchfaster1-Jan-09 14:08
scotchfaster1-Jan-09 14:08 
GeneralJust what the doctor ordered! Thanks! Pin
Carl Remmers24-Apr-08 3:20
Carl Remmers24-Apr-08 3:20 
QuestionPausing auido??? Pin
goodoljosh198031-Mar-08 7:17
goodoljosh198031-Mar-08 7:17 
GeneralThanks! Pin
nonnb19-Nov-07 18:32
nonnb19-Nov-07 18:32 
GeneralGood job Pin
Pang Wu5-Nov-07 7:03
Pang Wu5-Nov-07 7:03 
Generalshortcut Pin
Stonkie6-Apr-07 10:52
Stonkie6-Apr-07 10:52 
GeneralRe: shortcut Pin
beketata5-Sep-07 10:19
beketata5-Sep-07 10:19 
GeneralWorks flawlessly Pin
Mark R. Johnson29-Aug-06 6:11
Mark R. Johnson29-Aug-06 6:11 
GeneralGreat article! Pin
qborg19-Aug-06 4:53
qborg19-Aug-06 4:53 
JokeGreat job Pin
FatCatProgrammer26-May-06 8:57
FatCatProgrammer26-May-06 8:57 
Generalthanks Pin
Pedro M. C. Cardoso25-Apr-06 12:44
Pedro M. C. Cardoso25-Apr-06 12:44 
GeneralSame with the waveIn and waveOut API's Pin
Mike Sargent25-Apr-06 11:52
Mike Sargent25-Apr-06 11:52 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.