freeEffect can cause audio thread to crash

, ,

I've tracked down a crash in a game I've been working on to some code which calls freeEffect (actually one of the specialized variants, but also happens with freeEffect). It appears that freeing an effect that is still attached to an active channel will sometimes (details below) cause the audio thread to crash.

For example, this replacing the meat of the init code in bach.mid with this immediately crashes on my (macOS 15.1.1) simulator, and on device. I'm using SDK + firmware 2.7.0-beta2 but reporting here because I'm pretty sure this is true on 2.6.2 as well.

bool useDefaultChannel = false;
SoundChannel *channel;
if (useDefaultChannel)
{
	channel = snd->getDefaultChannel();
}
else
{
	channel = snd->channel->newChannel();
}

Overdrive *od = snd->effect->overdrive->newOverdrive();
snd->effect->overdrive->setGain(od, 0.5);
snd->channel->addEffect(channel, (SoundEffect *)od);
snd->effect->freeEffect(od);

FilePlayer *player = snd->fileplayer->newPlayer();
snd->fileplayer->loadIntoPlayer(player, "piano");
snd->channel->addSource(channel, (SoundSource *)player);
snd->fileplayer->play(player, 0);

While debugging, I found that the following things all appear to prevent the crash:

  • setting useDefaultChannel to true
  • removing the effect before freeing it
  • having no audio sources on the channel (eg leaving out the file player above)
  • adding the player to the channel before the overdrive (!)

I'm not sure if this is an expected consequence of freeing an in-use effect (and maybe could use a line in the documentation if so?), or if this is a bug. Thanks for taking a look!

Oh, and I should say, this is the top of a typical crash stack trace on the simulator:

Thread 18 Crashed:: com.apple.audio.IOThread.client
0   Playdate Simulator            	       0x1025617ac SoundLib_render + 1004
1   Playdate Simulator            	       0x1025617c4 SoundLib_render + 1028
2   Playdate Simulator            	       0x1025959c0 -[PCGameView renderAudio:::] + 296
3   Playdate Simulator            	       0x102596490 __24-[PCGameView startAudio]_block_invoke_2 + 40
4   AVFAudio                      	       0x1f9a65f1c __51+[AVAudioSourceNode pullInputBlockFromRenderBlock:]_block_invoke + 52
5   AVFAudio                      	       0x1f9a5ccec invocation function for block in AUGraphSourceNodeV3::AllocateInputBlock() + 32
6   AudioToolboxCore              	       0x1a0138e58 AUAudioUnitV2Bridge_Renderer::renderInputProc(void*, unsigned int*, AudioTimeStamp const*, unsigned int, unsigned int, AudioBufferList*) + 76
7   CoreAudio                     	       0x11ecd56e8 0x11eccc000 + 38632
8   CoreAudio                     	       0x11edd7e7c 0x11eccc000 + 1097340
9   CoreAudio                     	       0x11edda1dc 0x11eccc000 + 1106396
10  AudioToolboxCore              	       0x1a01ef4d4 AudioUnitRender + 428

This seems like expected behavior to me? addEffect doesn't make a copy of the effect data because the lifetime of userdata or effectProc can't be guaranteed. So the lifetime of the effect is left to the user to control.
So if you free the effect while the audio process is still calling into it, a crash would be expected.

This seems like expected behavior to me? addEffect doesn't make a copy of the effect data because the lifetime of userdata or effectProc can't be guaranteed. So the lifetime of the effect is left to the user to control.

That seems plausible to me, but "the effect unregisters itself from any channels when you free it" or "you must keep the userdata and effectProc alive until the last reference to the effect is dropped (which might be from a channel referring to it)" also seem like a possible choices. If this is expected behavior, I think documenting the expectations would be helpful. (Unless I missed them, which is quite possible!)

On the C side I've been expecting you to be aware of object lifespans and make sure you don't free something that the system is using, but over time as things have gotten more complex I've added retain counts to various types so we can pass ownership around. Sound sources now do this, and automatically remove themselves from the sound engine when you call their "free" functions (which are now misnamed, technically, but whatevs). We should do the same with effects. Filing that now! And in the meantime I'll put a note in the docs that effects shouldn't be freed while in use.

Maybe I'll go ahead and add retain counts everywhere, like I should have from the beginning.. :thinking:

1 Like

wait, no.. on second thought, effects don't need multiple ownership, they're only used in one place. I'll just have it remove the effect from the sound engine before it's freed and that'll have the same, uh, effect.

1 Like

Thanks Dave! That sounds great and explains why I couldn't reproduce this same behavior with sources. :slight_smile: