HTML5: Sounds not getting garbage collected

I’m working on a HTML5 music player where all the music is loaded from server with something like:

sound = new Sound();
sound.addEventListener(Event.COMPLETE, onSoundLoadComplete, false, 0, true);
sound.load(new URLRequest(songUrl));

After this sound is not needed anymore, i try to remove it from memory with:

sound.removeEventListener(Event.COMPLETE, onSoundLoadComplete);
sound.close();
sound = null;

(This is a bit simplified version of what I’m doing, but basicaly that’s it)

Problem is, memory does not get released and after enough songs are loaded (and later removed), the browser crashes with “out of memory” message. Is there something obvious I’m missing?
Is it normal that when I load MP3 song, around 100MB of memory gets allocated?

I’m using Window’s Resource Monitor to check what’s going on in memory. Are there any better tools?

Thanks.

P.S.
I’m using OpenFL 4.4.1

Sorry for bumping my own thread, but there is really something fishy going on. Here is a very simplified stand-alone version of what I’m doing in my player:

I have an array of sounds which I load and play one by one every 5 seconds.
The sounds are played on two sound channels which I toggle (first sound plays on first channel, then second sound on second channel, third sound again on first channel and so on… second channel, first channel, second, first…).
After I play 5 sounds, I remove all event listeners, close the sounds, null them, stop both channels, null them and empty the channel array.
But still - there is aprox. 1GB (!?!!) of memory used which doesn’t get garbage collected.
What am I doing wrong?

Important notices:

  • i’m exporting to HTML5 and using Chrome
  • You must use “Allow-Control-Allow-Origin” plugin for Chrome to avoid the “No ‘Access-Control-Allow-Origin’ header is present on the requested resource.” error. Or use some other MP3 urls. (Very interesting: if “No ‘Access-Control-Allow-Origin’ header is present on the requested resource.” error comes up, there are NO memory issues, but that’s not how things work in my real-life player).

You can see my test case below.

	var mp3Urls:Array<String> = new Array<String>();
	var sounds:Array<Sound> = new Array<Sound>();
	var soundsLoaded:Int = 0;
	var finished:Bool = false;
	var newTimer:Timer;
	var soundChannel:Array<SoundChannel> = new Array<SoundChannel>();
	var activeChannel:Int = 0;
	
	function loadNextSound() {
		activeChannel++;
		if (activeChannel > 1)
			activeChannel = 0;
		var newSound = new Sound();
		newSound.addEventListener(Event.COMPLETE, onSoundLoadComplete, false, 0, true);
		newSound.load(new URLRequest(mp3Urls[soundsLoaded]));
		sounds.push(newSound);
	}

	function onSoundLoadComplete(e:Event) {
		if (soundChannel[activeChannel] != null) {
			soundChannel[activeChannel].stop();
		}
		if (sounds.length > 0) {
			soundChannel[activeChannel] = sounds[sounds.length - 1].play();
		}
		
		trace("sounds loaded: " + sounds.length);
		
		newTimer = new Timer(5000);
		newTimer.addEventListener(TimerEvent.TIMER, playerLoop, false, 0, true);
		newTimer.start();
	}

	function onSoundPlayComplete(e:Event) {
	}

	function deleteSounds() {
		trace("stoping channels");
		for (i in 0...soundChannel.length) {
			if (soundChannel[i] != null) {
				soundChannel[i].stop();
				soundChannel[i] = null;
			}
		}
		trace("deleting sounds");
		for (i in 0...sounds.length) {
			sounds[i].removeEventListener(Event.COMPLETE, onSoundLoadComplete, false);
			sounds[i].close();
			sounds[i] = null;
			trace("deleted index: " + i);
		}
		while (sounds.length > 0)
			sounds.pop();
		trace("sounds remaining: " + sounds.length);
		trace("sounds deleted");
		while (soundChannel.length > 0)
			soundChannel.pop();
		soundChannel = null;
		trace("channels remmoved");
	}

	function playerLoop(e:Event) {
		if (newTimer != null)
			newTimer.removeEventListener(TimerEvent.TIMER, playerLoop);
		if (soundsLoaded < 5 && !finished) {
			soundsLoaded++;
			loadNextSound();
		}
		else {
			finished = true;
			deleteSounds();
		}
	}

public static function main() {
   mp3Urls[0] = "http://www.mfiles.co.uk/mp3-downloads/brahms-cello-sonata-Em-1.mp3";
   mp3Urls[1] = "http://www.mfiles.co.uk/mp3-downloads/frederic-chopin-piano-sonata-2-op35-3-funeral-march.mp3";
   mp3Urls[2] = "http://www.mfiles.co.uk/mp3-downloads/saint-saens-carnival-of-the-animals-the-swan.mp3";
   mp3Urls[3] = "http://www.mfiles.co.uk/mp3-downloads/frederic-chopin-nocturne-no20.mp3";
   mp3Urls[4] = "http://www.mfiles.co.uk/mp3-downloads/mozart-clarinet-concerto-2.mp3";
   mp3Urls[5] = "http://www.mfiles.co.uk/mp3-downloads/schubert-trout-quintet-4.mp3";
   mp3Urls[6] = "http://www.mfiles.co.uk/mp3-downloads/brahms-cello-sonata-Em-1.mp3";
   loadNextSound();
   return;
}

I think I’ve figured it out. There is an empty dispose method in HTML5AudioSource.hx. Is there a reason for that?
I’ve added the howlers unload() call to that method and now memory is being cleaned up just fine.

Can someone predict any side effects?

Here’s the mentioned method in HTML5AudioSource.hx:

	public function dispose ():Void {
		
		#if howlerjs
		parent.buffer.__srcHowl.unload();
		#end
		
	}
1 Like

Thank you,

This caused problems on PiratePig, since the AudioBuffer is reused later by other AudioSource instances, but I have added this to AudioBuffer:

audioBuffer.dispose ();

It’s too bad that we can’t do this automatically if the audioBuffer is garbage collected :frowning:

EDIT: On the bright side, this should work under openfl.media.Sound now:

sound.close ();

(calls buffer.dispose() internally)

1 Like

You are right, after some additional testing I also noticed some ugly side effects.
But I still think the howler’s unload() method should be called somewhere. If this method is not called memory quickly gets stuffed with howler’s cache… 1GB after streaming 10 songs(?!)
Maybe in sound.close() ?

I’m testing this right now and looks good so far.

	public function dispose ():Void {
		
		#if lime_console
		if (channels > 0) {
			
			src.release ();
			channels = 0;
			
		}
		#end
		
		#if howlerjs
		
		__srcHowl.unload();
		
		#end
		
	}

Yeah, I think that’s the best solution for now :slight_smile:

1 Like

Great! Thank you for excellent suggestion. This problem has been bugging me for days.
Keep up the good work.

1 Like