Playdate.sound.fileplayer leaks file handles when using pause()

During extended gameplay sessions, I was running into a bug where assets were failing to load due to
"Too many open files."

Due to my game design this didn't make much sense, so I set out to watch the file handles in the simulator using sysinternals Process Explorer (Process Explorer - Sysinternals | Microsoft Learn).

When combat occurs, my game pause()s the BGM and loads up the combat music. That code looks like this:

		if (filePlayer ~= nil) then
			filePlayer:pause()  -- Pause the dungeon music, we'll RESUME it when combat is over
			filePlayer = nil
		end
       ...
		filePlayer = snd.fileplayer.new("sounds/Combat_Music")
		filePlayer:setVolume(0.5) -- 0 - 1.0
		filePlayer:play(0) --repeat forever

After combat, I call filePlayer:stop(), set filePlayer =nil, and re-load & play()the normal dungeon music.

I can see in Process Explorer that this sequence results in the dungeon music file being held open:
image

Additional evidence:

  • If I replace filePlayer:pause() with filePlayer:stop(), there is no leak.
  • If, after seeing several leaks in Process Explorer, I re-load my game into the sim (restart debugging), the file handles remain open.
  • I added debug code to examine the results of playdate.sound.playingSources() every time I change music, and playingSources() does NOT list the additional (leaked) instances of filePlayer; indicating it believes they are no longer in use, even though the file handle remains open.

I can imagine some workaround here, so it's not exactly dire, but this class of bug results in very tough to find and fix bugs where after 64 file handles are exhausted assets will just start to fail to load.

Thanks for looking!
Sam

2 Likes

In the Lua code there we have an internal table that tracks which players are active so that they don't scope out while in use. What's happening here is stop() clears the player from that table but pause() doesn't, so there's still a reference to the player and it doesn't get cleaned up. In 2.4 I added reference counting to the low-level sound objects to clear up ownership confusion and I think with that we don't need that tracking table any more--if the Lua wrapper scopes out and gets collected, it now releases the low-level player instead of freeing it outright.

For what you're doing I don't see any reason to use pause() instead of stop(), since you're not reusing the player. (The difference between the two is mainly that stop() moves the play position back to the start of the file for the next play() call and pause() doesn't.) Reusing the player would also work! :slight_smile:

I've filed this and hope to get a fix in soon.

2 Likes

Thanks Dave, I really appreciate the quick response!

I've altered the code to re-use the same fileplayer as I actually do want pause()/resume functionality, and it's working great now without leaking handles.

2 Likes