SoundEffect changes in XNA Game Studio 3.1

One of the few breaking API changes between Game Studio 3.0 and 3.1 concerns the SoundEffect.Play method.

There are two different scenarios for playing sound effects:

  • Fire & forget is when you want to trigger a sound, then not have to worry about it any more, and have the system automatically clean up after it finishes playing.

  • Create & configure is when you want to get back some sort of object that lets you alter the sound (changing volume or pitch, applying 3D settings, pausing or resuming) while it plays. This is obviously more powerful, but also more work because you have to keep track of which sounds are playing and clean up the relevant objects when you are done with them.

We think it is important for the SoundEffect API to support both scenarios, and in Game Studio 3.0, we tried to make the SoundEffect.Play method handle them both. It returned a SoundEffectInstance object, which you could use for create & configure scenarios, but you could also just ignore this return value if you were doing fire & forget, in which case we did some magic to make sure the garbage collector would not try to reclaim the SoundEffectInstance while the sound was still playing.

Turns out, this didn't work as well as we expected. There were two problems:

  • Because the Play method did not know whether you were going to store the return value, it had to allocate a new SoundEffectInstance each time, so fire & forget sounds created garbage.

  • The only way we could tell the difference between fire & forget vs. create & configure usage was by noticing if the garbage collector tried to reclaim a SoundEffectInstance that was still playing. But .NET garbage collection is not deterministic! This worked ok as long as GC occurred regularly, but the better optimized your game was, the less frequently GC would run, in which case fire & forget sounds might not be reclaimed often enough, so the system would run out of voices.

In Game Studio 3.1, we changed the API to make these two usage scenarios explicit:

  • SoundEffect.Play is now only for fire & forget, and it no longer returns a SoundEffectInstance. Instead, you get back a bool indicating whether the call succeeded. This no longer generates any garbage, and no longer supports looping (because if you started a looping sound this way, it would be impossible to ever stop it, which doesn't seem particularly useful 🙂

  • For create & configure scenarios, we added a new SoundEffect.CreateInstance method. This returns a paused SoundEffectInstance, so you can set properties such as Volume, Pitch, and Pan, then call SoundEffectInstance.Play when you are ready to trigger it.

We removed the SoundEffect.Play3D method. To play a 3D sound, call SoundEffect.CreateInstance, SoundEffectInstance.Apply3D, and then SoundEffectInstance.Play. You cannot use fire & forget with 3D sounds.

We also changed what happens if you try to play too many sounds at the same time. In GS 3.0, the Play call would throw InstancePlayLimitException. In 3.1, CreateInstance still throws this exception if it runs out of voices, but Play just returns false. This is convenient for the common case where you are playing fire & forget sounds and want to silently ignore voice overflows, as you can simply ignore the Play return value and no longer have to write special error handling code.

Comments (5)

  1. Kevin Gadd says:

    Great decisions, I think. The designer of FMod had to make similar ones when working on FMod Ex, and in my experience they all turned out to be the right ones – in this case, convenience is the enemy of both correctness and good performance.

    Correctness is a little obscure there, I suppose: Since Play would return an already-playing instance, it’s possible that a context switch would prevent you from adjusting properties/pausing the instance before samples are sent to the player’s speakers. Disconcerting when it happens.

  2. Danny Tuppeny says:

    This is a great change. I spent ages trying to reproduce random crashes in my game with the debugger attached. Eventually I discovered it was too many sounds being played (I didn’t actually notice a SoundEffectInstance was returned and was calling Play() on the SoundEffect each time!).

    I had to create a nasty wrapper class that called Play() on the SoundEffect the first time, then on the SoundEffectInstance each time after. This new change means I don’t have to worry 😀

  3. Stainless says:

    Thanks Shawn, I just ripped a couple of teddy bears apart after I upgraded to 3.1 and found all my sound management was broken.

    At least I can fix it now, but why isn’t this information on the creators club website, in big bold letters, with a sign saying "beware of the leopard".

    Breaking changes should be highlighted and solutions published with a release.

  4. Hi Shawn, I have a question regarding SoundEffect.Play() in XNA 3.1: Above, you write that using SoundEffect.Play() in the basic sense (not create and configure, just fire and forget) "no longer generates any garbage." Using a fairly basic setup in my code, I have tested playing the same sound effect several times per second for minutes at a time (for the sake of profiling).

    When I profile this with CLRProfiler, I see allocations for System.WeakReference objects equal to the amount of times I called SoundEffect.Play(). Pushing further into CLRProfiler, the allocations are all coming from SoundEffect::Play.  Am I doing something wrong or am I misunderstanding something fundamental here? I want to believe that there is a no-garbage audio solution in XNA. Any advice or guidance you could give would be much appreciated.

    Thanks so much for this and all your previous blogs. You have been a huge help to me in my dealings with XNA. Keep up the great work!

  5. For anybody who is interested, I’ve continued this discussion (see my previous comment above) on the XNA forums where I have also posted source code for reproducing my problem(error?):

Skip to main content