Click here to Skip to main content
Click here to Skip to main content
Go to top

SoundPlayer bug - Calling unmanaged APIs

, 25 Apr 2006
Rate this:
Please Sign up or sign in to vote.
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 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 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

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

Causing the garbage collector to run

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.

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:

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 can not move or collect it.

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

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

CodeProject 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

Share

About the Author

shteff
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

 
QuestionThanks!!! Pinmemberanth718-Sep-11 15:03 
QuestionA word of warning PinmemberMarkLTX22-Aug-11 10:25 
QuestionHow to make it work in a web application? PinmemberAdnan Heryani11-Jun-09 2:25 
Generallike a charm Pinmemberscotchfaster1-Jan-09 14:08 
GeneralJust what the doctor ordered! Thanks! PinmemberCarl Remmers24-Apr-08 3:20 
QuestionPausing auido??? Pinmembergoodoljosh198031-Mar-08 7:17 
GeneralThanks! Pinmembernonnb19-Nov-07 18:32 
GeneralGood job PinmemberPang Wu5-Nov-07 7:03 
Generalshortcut PinmemberStonkie6-Apr-07 10:52 
GeneralRe: shortcut Pinmemberbeketata5-Sep-07 10:19 
GeneralWorks flawlessly Pinmemberburstingfist29-Aug-06 6:11 
GeneralGreat article! Pinmemberqborg19-Aug-06 4:53 
JokeGreat job Pinmemberinfinite428126-May-06 8:57 
Generalthanks PinmemberGoAn25-Apr-06 12:44 
GeneralSame with the waveIn and waveOut API's PinmemberMike Sargent25-Apr-06 11:52 

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

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

| Advertise | Privacy | Mobile
Web04 | 2.8.140926.1 | Last Updated 25 Apr 2006
Article Copyright 2006 by shteff
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid