C API - sampleplayer->setSample causing memory leaks

I recently started developing for my Playdate using the C API on Windows, and I'm trying to play audio for the first time, and I'm having a strange memory leak issue with sample player.

Here's the entire code of a simple reproduction project I've created:

#define TARGET_EXTENSION 1

#include "pd_api.h"

int update(void* userdata) {
	return 0;
}

int eventHandler(PlaydateAPI* pd, PDSystemEvent event, uint32_t arg) {
	if (event == kEventInit) {
		pd->system->setUpdateCallback(update, NULL);

		SamplePlayer* sampleplayer = pd->sound->sampleplayer->newPlayer();
		AudioSample* sample = pd->sound->sample->load("audio/move_piece.wav");

        // this line causes the memory leak
		pd->sound->sampleplayer->setSample(sampleplayer, sample);
		
		pd->sound->sampleplayer->freePlayer(sampleplayer);
		pd->sound->sample->freeSample(sample);
	}

	return 0;
}

On the left is what the malloc log looks like with the setSample line commented out, and on the right is what the log looks like when setSample is executed:

In my actual other project, executing code multiple times that calls setSample causes an additional pair of these two allocations to occur and stick around permanently every time.

Is there something I'm missing here that I'm supposed to also be freeing in addition to the sample player and the sample? I couldn't find anything in the documentation referencing anything of the sort.

Edit: I tested on the device itself and it's leaking memory on there as well.

Edit 2: I tried this with FilePlayer as well, and it also leaks memory; however, if I actually call Play on the FilePlayer and free it after it's done, there's no leak. If I call Play on the SamplePlayer and free it after it's done, it still leaks those two allocs in the screenshots.

You got me curious. Before I would give you my ungrounded speculations, I thought it would be fair if I tried to reproduce your problem locally first. So I just had to teach myself for the first time how to build C for Playdate. Thank you (-: (Bonus: Along the way I have also found two bugs [crashes] in the Playdate SDK toolchain.)

Long story short: I can't reproduce your leak. I tried SDK versions 2.1.0 and 2.1.1. (What's your version?) I only get one active allocation, like in your 1st screenshot.

To make sure I don't miss anything else, I have also tried:

  • Actually playing a sound by adding a play() call. It worked fine.
    pd->sound->sampleplayer->play(sampleplayer, 1, 1.0);
  • Provoking memory leak by removing two last calls, freeSample() and freePlayer(). I got two additional allocation, like in your 2nd screenshot.
  • Smashed buttons and toggles in Simulator to make sure Malloc Log displays current information (-:

NaΓ―ve but obligatory questions:

  • You sure you have a clean build, you build correct source files, and you upload correct build to device/simulator?
  • What's your SDK version, again?

Speculations:

  • Maybe some subtle build options and flags? I just used your source example, a makefile from $PLAYDATE_SDK_PATH/C_API/Examples/Hello\ World/Makefile, and a main_theme.wav file from Example/Level\ 1-1.
  • Maybe something fishy with your wav file that triggers a leak inside Playdate? Try another one, who knows!
  • Rather a general nit: I think you might want to free player and sample in reverse order.

That's all I can think of right now. Discuss! (-:

P.S. I got a work around for those two toolchain crashes. I know devs are pretty busy anyway, so I don't know whether I should bother to file reports?

1 Like

Hey, thanks so much for trying it on your end!

Believe it or not, it actually was the wav file that was causing it; I tried a different random one from my computer and the leak doesn't happen with it.

The leak happens with all four wav files in my game, and I saved all of them with the IMA ADPCM encoding that the documentation recommends, whereas the other random file I tried that doesn't leak just has the default 16-bit PCM encoding, so maybe there's some issue with that encoding they recommended that's causing it.

Either way, thanks for recommending I try that! I guess I'll see if I can fiddle with any settings in Audacity to fix it, or if not I can probably just use the default encoding instead.

Edit: I tried fiddling with some settings in Audacity and also tried instead using adpcm-xq to encode the files per a recommendation I saw in another thread, and the memory leak still happens with anything that uses the ADPCM encoding. I'm not sure if I'm doing something wrong or there's some bug, but I guess I can just use the standard 16-bit encoding and have larger filesizes for now.

Nice (-:

If you are certain that certain audio binary cause memory leaks on the SDK side, you should report it as a bug. However, after a night of sleep I am not convinced it's a memory leak just yet.

  1. I got these two samples in different encodings:

    πŸš€ exiftool -s1 Source/audio/*_01.* -FileType -Encoding -NumChannels -SampleRate -BitsPerSample
    ======== Source/audio/ima_adpcm_01.wav
    FileType                        : WAV
    Encoding                        : Intel IMA/DVI-ADPCM
    NumChannels                     : 2
    SampleRate                      : 44100
    BitsPerSample                   : 4
    ======== Source/audio/ms_pcm_01.wav
    FileType                        : WAV
    Encoding                        : Microsoft PCM
    NumChannels                     : 2
    SampleRate                      : 44100
    BitsPerSample                   : 16
        2 image files read
    
  2. My wav files get converted to pda files. They seem to retain internal encoding though:

    πŸš€ file HelloWorld.pdx/audio/*_01.pda
    HelloWorld.pdx/audio/ima_adpcm_01.pda: Playdate audio file 44100 Hz, 4-bit ADPCM, 2 channel
    HelloWorld.pdx/audio/ms_pcm_01.pda:    Playdate audio file 44100 Hz, signed, 16-bit little-endian PCM, 1 channel
    
  3. I suspect it may be not a leak, but some residue memory used by the SDK. It can be a cache to track what files have been loaded to avoid re-loading, etc. In other words, it may be necessary bookkeeping on part of the runtime. You may check that by doing some blackbox poking, e.g. create a bunch of sample from the same file or a bunch of samples from different samples. Then see if memory usage increases proportionally too.

  4. I made one example variation of that idea and I still can't see a memory leak :woman_shrugging: I free resources on device lock event (I couldn't get kEventKeyPressed/kEventKeyReleased working! Bug?) to give have gradual control and watch malloc log in steps.

    demo

    setsamplemalloc

    modified code
    #define TARGET_EXTENSION 1
    
    #include "pd_api.h"
    
    int update(void* userdata) {
    	return 0;
    }
    
    static SamplePlayer* player;
    static AudioSample* sample_pcm[5];
    static AudioSample* sample_adpcm[5];
    
    int eventHandler(PlaydateAPI* pd, PDSystemEvent event, uint32_t arg) {
    	if (event == kEventInit) {
    		pd->system->setUpdateCallback(update, NULL);
    
    		player = pd->sound->sampleplayer->newPlayer();
    
    		sample_adpcm[0] = pd->sound->sample->load("audio/ima_adpcm_01.wav");
    		sample_adpcm[1] = pd->sound->sample->load("audio/ima_adpcm_02.wav");
    		sample_adpcm[2] = pd->sound->sample->load("audio/ima_adpcm_03.wav");
    		sample_adpcm[3] = pd->sound->sample->load("audio/ima_adpcm_04.wav");
    		sample_adpcm[4] = pd->sound->sample->load("audio/ima_adpcm_05.wav");
    		sample_pcm[0] = pd->sound->sample->load("audio/ms_pcm_01.wav");
    		sample_pcm[1] = pd->sound->sample->load("audio/ms_pcm_02.wav");
    		sample_pcm[2] = pd->sound->sample->load("audio/ms_pcm_03.wav");
    		sample_pcm[3] = pd->sound->sample->load("audio/ms_pcm_04.wav");
    		sample_pcm[4] = pd->sound->sample->load("audio/ms_pcm_05.wav");
    
    		// these lines cause the memory leak?
    		pd->sound->sampleplayer->setSample(player, sample_adpcm[0]);
    		pd->sound->sampleplayer->setSample(player, sample_adpcm[1]);
    		pd->sound->sampleplayer->setSample(player, sample_adpcm[2]);
    		pd->sound->sampleplayer->setSample(player, sample_adpcm[3]);
    		pd->sound->sampleplayer->setSample(player, sample_adpcm[4]);
    		pd->sound->sampleplayer->setSample(player, sample_pcm[0]);
    		pd->sound->sampleplayer->setSample(player, sample_pcm[1]);
    		pd->sound->sampleplayer->setSample(player, sample_pcm[2]);
    		pd->sound->sampleplayer->setSample(player, sample_pcm[3]);
    		pd->sound->sampleplayer->setSample(player, sample_pcm[4]);
    	}
    
    	else if (event == kEventUnlock) {
    		pd->sound->sample->freeSample(sample_adpcm[0]);
    		pd->sound->sample->freeSample(sample_adpcm[1]);
    		pd->sound->sample->freeSample(sample_adpcm[2]);
    		pd->sound->sample->freeSample(sample_adpcm[3]);
    		pd->sound->sample->freeSample(sample_adpcm[4]);
    		pd->sound->sample->freeSample(sample_pcm[0]);
    		pd->sound->sample->freeSample(sample_pcm[1]);
    		pd->sound->sample->freeSample(sample_pcm[2]);
    		pd->sound->sample->freeSample(sample_pcm[3]);
    		pd->sound->sample->freeSample(sample_pcm[4]);
    
    		pd->sound->sampleplayer->freePlayer(player);
    	}
    
    	return 0;
    }
    
    wavs i used

    wavs.zip (77.5 KB)

I'm pleasantly surprised that file reports Playdate format files correctly. I'm wondering how?

I know right? (-: It was a serendipitous discovery for me as well! I don't remember how exactly I had it. Maybe just a habit of feeding everything to file and see what comes out (-:

Anyway, on my Fedora Linux system file comes from Ian Darwin's Fine Free File Command. Going down the rabbit hole, some good soul has added it a year ago. That's the "how?".

For another instance, I checked OpenBSD's file and it does not seem to have it (I glossed over the source code only, since I don't have an access to a live system atm.)

1 Like

Magic! Love it. I'll have to check macOS, though I'm not hopeful. Maybe I can add it myself.

https://opensource.apple.com/ β†’ https://github.com/apple-oss-distributions/file/tree/file-96.0.2/file/magic/Magdir β†’ no Playdate. But maybe they accept patches? (-;

I've discovered what's happening on my end: If I call setSample with an ADPCM file and then call setSample on the same sample player with a non-ADPCM file, the extra allocated memory goes away. So it does look like it's some residual cached memory, as you suggested.

Unfortunately if you don't call setSample with a non-ADPCM file on the same sample player, the extra memory still doesn't get deallocated even when you free both the sample player and the sample, causing a leak. I feel like that has to be a bug, so I might report it.

I guess I could work around it by having some empty sample that the player gets assigned before it's freed, or something.

Over time of using your app, does memory usage grow proportionally? Does allocated memory constantly grow in spite of you diligent requests to free it?

In other words, does it drip and leak? Or is there just a fixed chunk that appears active although you called all your free() routines?

But maybe it's time someone of the devs chime in and comment. You can move this discussion to the SDK Bug Reports - Playdate Developer Forum section too.

Yes, memory usage grows over time. If in the game update function I create a sample player, assign the same ADPCM sample to it, and then free the sample player, two more of those allocations pile up every single frame.

I'll try to post about it in the bug report forum later today.

1 Like

We can turn this thread into a bug report if you'd like?

Oh, if you're able to do that that would be great!

1 Like

It looks like ADPCM wave files are causing the SamplePlayer to leak some data. We'll get this sorted out. Thanks for the report!

3 Likes

I believe we fixed this in the 2.2 update. Please let us know if you're still seeing a leak here!