OpenALSoundController slowly runs out of available sources

This issue keeps cropping up for me, where after a relatively short time playing the game on android it will crash throwing an InstancePlayLimitException. I tracked it it down to the OpenALSoundController class, line 417, where it runs out of audio sources. I’m a bit confused by this as the SoundInstancePool class shows the playingInstances list isn’t full, and goes down to zero in scene transitions where no sounds are playing. What else counts as a audio source? I thought the limit only applied to sound effect instances, perhaps does it include music as well?

I did a custom build of monogame so I could trace the error and if I keep replaying a level I can see the available sources count slowly drop over time, so after a while it runs down to zero and throws this error. I must be missing something here. I can share my audio manager class, its short and simple.

There are quite a lot of sounds in game, its a racing game with trails of pickups on the track, so if you boost through a line of them it does play quite a few sounds (a bit like sonic when you run through a load of rings), but it shouldn’t hit anywhere near the 32 limit on android.

Here’s the class. The one shot method deliberately uses an audio instance at the moment because I want to trigger this error for debugging.

using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Media;
using System.Collections.Generic;

namespace P3
{
    public static class Audio
    {
        private static readonly List<SoundEffectInstance> playingInstances = new List<SoundEffectInstance>();

        private const int MAX_INSTANCES = 32;

        public enum MuteType { All, Sound, Music, VO }

        private static int tick = 0;

        private struct MuteConfig
        {
            public bool all;
            public bool sound;
            public bool music;
            public bool vo;

            public MuteConfig(bool all = false, bool sound = false, bool music = false, bool vo = false)
            {
                this.all = all;
                this.sound = sound;
                this.music = music;
                this.vo = vo;
            }
        }

        private static SoundEffectInstance vo;
        private static MuteConfig mute = new MuteConfig();
        private static bool playingVO = false;

        public static Song PlayMusic(string name, bool loop = true)
        {
            var music = Game.Instance.Assets.GetMusic(name);
            Microsoft.Xna.Framework.Media.MediaPlayer.IsRepeating = loop;
            Microsoft.Xna.Framework.Media.MediaPlayer.Play(music);
            return music;
        }

        public static void Update()
        {
            /*
            //debug stuff
            var instances = SoundEffectInstancePool.playingInstances;

            if (instances.Count > 30)
            {
                System.Diagnostics.Debug.WriteLine("over 30 sound effect instances in pool");
            }

            if (tick++ % 10 == 0)
            {
                System.Diagnostics.Debug.WriteLine(instances.Count);
            }
            */

            for (int i = 0; i < playingInstances.Count; i++)
            {
                var instance = playingInstances[i];

                if (instance.IsDisposed || instance.State == SoundState.Stopped)
                {
                    if (instance == vo)
                        playingVO = false;

                    instance.Stop(true);
                    playingInstances.Remove(instance);

                    if (!instance.IsDisposed)
                        instance.Dispose();

                    instance = null;
                }
            }
        }

        public static void StopMusic()
        {
            Microsoft.Xna.Framework.Media.MediaPlayer.Stop();
        }

        public static SoundEffectInstance PlaySound(string name, bool loop = false)
        {
            //if we exceed max instances return the last sound played - is this wise? 
            if (playingInstances.Count >= MAX_INSTANCES - 1)
                return playingInstances[playingInstances.Count - 1];

            var sound = Game.Instance.Assets.GetSound(name);
            var instance = sound.CreateInstance();
            instance.IsLooped = loop;
            instance.Play();

            playingInstances.Add(instance);
            return instance;
        }

        public static SoundEffectInstance PlaySound(string[] names, bool loop = false)
        {
            //if we exceed max instances return the last sound played - is this wise? 
            if (playingInstances.Count >= MAX_INSTANCES - 1)
                return playingInstances[playingInstances.Count - 1];

            var sound = Game.Instance.Assets.GetSound(Utils.SelectRandom(names));
            var instance = sound.CreateInstance();
            instance.IsLooped = loop;
            instance.Play();

            playingInstances.Add(instance);
            return instance;
        }

        public static SoundEffectInstance PlayOneShot(string name)
        {
            //if we exceed max instances return the last sound played - is this wise? 
            if (playingInstances.Count >= MAX_INSTANCES)
                return playingInstances[playingInstances.Count - 1];

            var sound = Game.Instance.Assets.GetSound(name);
            var instance = sound.CreateInstance();
            instance.Play();

            playingInstances.Add(instance);
            return instance;
        }

        public static SoundEffectInstance PlayOneShot(string[] names)
        {
            var sound = Game.Instance.Assets.GetSound(Utils.SelectRandom(names));
            var instance = sound.CreateInstance();
            sound.Play();

            playingInstances.Add(instance);
            return instance;
        }

        public static SoundEffectInstance PlayVO(string name, float volume = 1f, bool force = false)
        {
            if ((playingVO && !force) || playingInstances.Count >= MAX_INSTANCES - 1)
                return vo;

            StopVO();

            var sound = Game.Instance.Assets.GetSound(name);

            vo = sound.CreateInstance();
            vo.Volume = volume;
            vo.Play();

            playingVO = true;
            playingInstances.Add(vo);

            return vo;
        }

        public static SoundEffectInstance PlayVO(string[] names, float volume = 1f, bool force = false)
        {
            if ((playingVO && !force) || playingInstances.Count >= MAX_INSTANCES - 1)
                return vo;

            StopVO();

            var sound = Game.Instance.Assets.GetSound(Utils.SelectRandom(names));

            vo = sound.CreateInstance();
            vo.Volume = volume;
            vo.Play();

            playingVO = true;
            playingInstances.Add(vo);

            return vo;
        }

        public static void StopVO()
        {
            if (vo != null && !vo.IsDisposed)
            {
                vo.Stop(true);
                vo.Dispose();
                vo = null;
            }
        }

        public static void Mute(MuteType type = MuteType.All)
        {
            switch (type)
            {
                case MuteType.All:
                    mute.all = true;
                    break;
                case MuteType.Sound:
                    mute.sound = true;
                    break;
                case MuteType.Music:
                    mute.music = true;
                    break;
                case MuteType.VO:
                    mute.vo = true;
                    break;
            }

            if (type == MuteType.All)
            {
                SoundEffect.MasterVolume = 0f;
                Microsoft.Xna.Framework.Media.MediaPlayer.IsMuted = true;
            }
            else if (type == MuteType.Sound)
            {
                SoundEffect.MasterVolume = 0f;
            }
            else if (type == MuteType.Music)
            {
                Microsoft.Xna.Framework.Media.MediaPlayer.IsMuted = true;
            }
        }

        public static void UnMute(MuteType type = MuteType.All)
        {
            switch (type)
            {
                case MuteType.All:
                    mute.all = false;
                    break;
                case MuteType.Sound:
                    mute.sound = false;
                    break;
                case MuteType.Music:
                    mute.music = false;
                    break;
                case MuteType.VO:
                    mute.vo = false;
                    break;
            }

            if (type == MuteType.All)
            {
                SoundEffect.MasterVolume = 1f;
                Microsoft.Xna.Framework.Media.MediaPlayer.IsMuted = false;
            }
            else if (type == MuteType.Sound)
            {
                SoundEffect.MasterVolume = 1f;
            }
            else if (type == MuteType.Music)
            {
                Microsoft.Xna.Framework.Media.MediaPlayer.IsMuted = false;
            }
        }

        public static bool IsMute(MuteType type = MuteType.All)
        {
            switch (type)
            {
                case MuteType.All: return mute.all;
                case MuteType.Sound: return mute.sound;
                case MuteType.Music: return mute.music;
                case MuteType.VO: return mute.vo;
            }
            return false;
        }
    }
}

While not Android, I did notice that MonoGame’s does not properly delete OpenAL buffers in DesktopGL. Here is an issue I filed. Debug builds of MonoGame will throw errors saying it failed to delete the buffers.

I looked into it for some time and could not find the issue, as it looks like everything is being disposed properly. There are likely more details about OpenAL that needs to be accounted for in the code, but I’m not very familiar with it. Chances are we might have improvements if MonoGame upgrades its version of OpenAL, as new versions have several fixes.

1 Like

The issue also occurs on Windows GL builds it just doesn’t cause a crash (or it will but will take a lot longer).

I’m currently running a debug build to try and find the issue and I see the same error you mention every time I exit the app so it might be related.

Thing is if it was all sounds I would hit the 32 max instances limit very quickly, where this seems to grow slowly. This makes me suspect it might be looping sounds that are the issue as there are only one or two per level. There are no sounds playing, I make sure to stop them, but something is hanging around in the audio sources list that I can’t get access to.

I suspect you are right and the two issues are related. I added a hack to bypass the issue for the time being so we can test and see if the crashing we were getting is solved, and it no longer throws this error when I exit the app on WindowsGL build.

It might be worth you logging availableSourcesCollection.Count in OpenALSoundController to see if anything is hanging around in there for you too.

Out of curiosity, what is the hack you implemented?

After adding in the logs I couldn’t replicate it for some reason. However, I did notice that the PlatformDispose method of SoundEffectInstance.OpenAL does not actually do anything, and I think I found the issue.

The sources get recycled back to the availableSourcesCollection only when Stop is called, but the SoundEffectInstancePool manually calls Stop in its update loop if the sound is disposed or stopped. Here’s the snippet:

if (inst.IsDisposed || inst.State == SoundState.Stopped || (inst._effect == null && !inst._isDynamic))
{
#if OPENAL
    if (!inst.IsDisposed)
    inst.Stop(true); // force stopping it to free its AL source
#endif
    Add(inst);
    continue;
}

It looks like it does this here so the SoundEffectInstance pool can know when to add it back, but I see an issue with this. If Stop is not called on the SoundEffectInstance before SoundEffectPool.Update runs and the OALSoundBuffer is disposed, it will throw the exception since the instance is still in use. Important to note is there is a lock on the availableSourcesCollection, which may play a factor in delayed cleanup. I’ll post this in the issue and see how we can resolve this.

I think I was mistaken, switching stuff about so much I think the DesktopGL build was using the release dll again so wasn’t showing this error. Though they may still be related issues.

However I have managed to create a reproducible test case for my problem. Basically when the player starts the game we had a line of code like this, calling the audio manager class I posted above.

engineSFX = Audio.PlaySound("sfx_rover_loop_00", true);
engineSFX.Pause();

And then in some begin method called when the race began, we did engineSFX.Play(), then when the race ended, we called engineSFX.Stop(true). For some reason this pause, followed by play, then calling stop, causes some kind of leak where it doesn’t get cleared out from the availableSourcesCollection in OpenALSoundController. After a while it’d run out of available sources and throw the InstancePlayLimitException. I’d say this is a bug as it essentially renders pausing the sounds unusable, so I will open up a separate issue on GitHub.