Game Design, Programming and running a one-man games business…

multithreading sound engine bug…

I have a bug thats driving me nuts. I use some middleware as a sound engine. its the ONLY middleware I use, and its bugging me. theoretically its easy to use, but I have a situation that it seems incapable of coping with.

With this middleware, I can play a sound, and request a pointer to track it. I can use that pointer later to adjust volume, or stop the sound, or query if its finished. For various reasons, I need to keep my own list of what sounds are currently playing. Thus I have a list of ‘current playing sounds’.

The middleware gives me a callback which triggers when a sound ends. This is handy, as I can then loop through the current playing sounds and remove it, keeping that list up to date. The sound engine runs in its own thread, so that callback triggers in a different thread to the main game.

This is where it goes wrong (but only on fast speed). I decide from the main game, to stop a sound. I firstly check that the sound exists within the current playing sounds. It does, so I access the sound pointer and tell it to stop. But wait! in-between those two events, the sound has expired naturally (in another thread) and the pointer has become invalid. CRASH.

using critical sections just produces race conditions, because stopping the sound has to happen in the same thread as the callback, and there are likely several sounds generating callbacks in the same frame (on fast speed) as the one I’m trying to stop, and it reaches a deadlock. It’s a real pain.

One solution is to make all such sounds loop (and thus never expire naturally, and rely on me killing them, which should work ok) and I thus never hit this problem. Another is to just not stop them prematurely (looks weird). I have currently hacked it, but I suspect the 1.18 build still has this issue manifesting itself as a 4x speed lots of beam-lasers crash.

Another solution is to tell the sound engine to run single threaded but that seems horrendously hacky.

I may have to try the always-loop solution. One day I’ll write my own sound engine again.

 


10 thoughts on multithreading sound engine bug…

  1. To clarify: the problem is that there are two functions, which run on different threads, that might invalidate an individual sound’s pointer?

    Are both those functions your code (one “normal”, one a callback), or is one embedded in the middleware?

    If they’re both in your code, could you have an individual lock/mutex/wait-handle/whatever for each sound, and both the functions have to acquire that before they check the validity of the pointer and then (if it’s valid) invalidate it? That way if multiple sounds are dying at once they don’t block each other.

    So basically, both would have the same segment of code:

    if(!IsSoundPointerValid(ptr))
    return;

    LockSoundPtr(ptr);

    if(IsSoundPointerValid(ptr))
    InvalidateSoundPointer(ptr);

    UnlockSoundPtr(ptr);

    That means keeping metadata (which is not immediately invalidated with the pointer) on each pointer, include the lock and an “is valid” bool, but that sounds like it might be less onerous than the alternatives.

    Or I might be completely misunderstanding the problem :)

    1. Yeah the problem seems to be not the actual accessing of it, but some convoluted situation regarding me checking its valid, *then* deleting it/stopping it, which triggers the callback, which then trigger other main-thread code, and thus causes a race condition thing.
      Its probably more complex than it should be. I’m definitely considering the ‘make them loop’ solution, as its only an issue for beam lasers, and theoretically some might have beam durations longer than the SFX anyway, so it would be a nice fix for that too…

  2. when you check that the sound is playing, could you just ignore sounds that have < N ms remaining since they're about to end naturally? That's pretty hacky, but seems less hacky than looping everything.

  3. Not sure if you can do this with your engine – but instead of having the Sound Engine delete pointers on the callback – can you have it decrease a reference count? Then basically on your MAIN thread, at the end of FrameUpdate(), you do a single sweep through the list for 0-reference count pointers and THEN delete them in one go there?

  4. Off hand it seems like there are a few correct solutions, but it mostly is the result of the middleware being too helpful.

    You can:
    a) Loop the sound if it’s mean’t to be played constantly (eg engine noise, or background music) and then only end it manually.
    b) Let it end naturally if it’s meant to be played once, and then replay it from the code if it needs to play again (eg “is ship firing laser”)

    There should be no circumstances where you need to proactively end sound effects, a situation like this would be to tell the sounds to mute instantly, but let them just play out. Like in a game-over situation you just mute everything after the final big boom animation or whatever.

  5. You could post stop requests to the sound engine thread (via another thread-safe list) and have it stop them for you. It can ignore ones that aren’t in the list as they have already ended naturally. It seems though that your race condition implies NONE of the things you do via that pointer are thread-safe and should all be done indirectly.

    1. Actually, given the conclusion of my previous post, you shouldn’t even use the pointer to refer to a sound as if they are re-used, you could end up changing a sound that has expired and immediately re-used as a different sound. The only way round this is to use some sort of (incrementing) ID for each sound on the game side instead of pointer and keep the pointer knowledge (and ID mapping) on the sound engine thread.

  6. *MOD? Think of it from the pov of sound mixing. Replace “stopping” with “muting”. Let the engine play the muted sound to end. If the middleware is well written it doesn’t really matter if there’s 1000 sounds playing simultaneously (some muted) or 1 from CPU usage pov.

  7. For this situation, I definitely recommend using a thread safe command buffer. The middleware handling code receives the destroy callback, generates a command representing that and posts to the main game thread.

    From here you can ensure everything happens in a well defined order, with no hazard of early invalidations.

Comments are currently closed.