VST SDK 3.7 AUv3Wrapper crash on close

I’m noticing that my plugin is crashing Logic Pro if it happens to be sending parameter changes (VU in my case) at the time of closing Logic or removing my plugin. Here’s my code for sending parameter changes. If I comment out the paramQueue->addPoint() call, everything closes cleanly.

void AGain::sendParameter(IParameterChanges* outParams, int index, float value) {
   if (outParams) {
      int32 ind = 0;
      IParamValueQueue* paramQueue = outParams->addParameterData(index, ind);
      if (paramQueue) {
         int32 index2 = 0;
         paramQueue->addPoint(0, value, index2);
      }
   }
}

Here’s the details from the Logic Crash Report

Thread 1 Crashed:: Dispatch queue: AUParameterTree.observationQueue
0   libobjc.A.dylib               	0x00007fff674f181d objc_msgSend + 29
1   com.apple.music.apps.MAAudioEngine	0x000000010ec35425 0x10eaec000 + 1348645
2   com.apple.audio.AudioToolboxCore	0x00007fff43248759 invocation function for block in AUObserverController::parameterChanged(EParamChangeFlags, unsigned long long, float, AUParameterObserverExtendedToken const&, unsigned long long, AUParameterAutomationEventType) + 2865
3   libdispatch.dylib             	0x00007fff6864d658 _dispatch_client_callout + 8
4   libdispatch.dylib             	0x00007fff6864f818 _dispatch_continuation_pop + 414
5   libdispatch.dylib             	0x00007fff6865f4be _dispatch_source_invoke + 2084
6   libdispatch.dylib             	0x00007fff68652af6 _dispatch_lane_serial_drain + 263
7   libdispatch.dylib             	0x00007fff686535d6 _dispatch_lane_invoke + 363
8   libdispatch.dylib             	0x00007fff6865cc09 _dispatch_workloop_worker_thread + 596
9   libsystem_pthread.dylib       	0x00007fff688a7a3d _pthread_wqthread + 290
10  libsystem_pthread.dylib       	0x00007fff688a6b77 start_wqthread + 15

I was able to reproduce this same crash using a brand new copy of the VST3 SDK 3.7.5 and the included AGain AUv3 example. I’m testing on macOS 10.15.7.

I’m very interested to know if there’s a way to fix this issue or if I should be trying to build an AUv2 instead.

So it looks like the main issue is that dealloc() (line 1050 of AUv3Wrapper.mm) is not being called. This dealloc function is responsible for invalidating timer, which is set set to fire every 1/60 seconds., shown below.

timer = [NSTimer timerWithTimeInterval:1./60. repeats:YES block:^(NSTimer * _Nonnull timer) {
		[self onTimer];
	}];
	[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

So when a plugin is removed, that timer function is still running! And if we take a look at the onTimer method, we can see that this is going to crash because parameterTreeVar will point to garbage.

- (void)onTimer
{
    //...
	while(outputParamTransfer.getNextChange(pid, value, sampleOffset))
	{
		_editcontroller->setParamNormalized(pid, value);
		AUParameter* parameterToChange = [parameterTreeVar parameterWithAddress:pid];
		parameterToChange.value= value;
	}
    //...
}

I managed to hack together a solution to prevent the crashes.

  1. Create a global variable to effectively bypass onTimer method.

volatile bool stopRunningTimer = false;

  1. In the onTimer method., tell it to return if the stopRunningTimer bool is set.
- (void)onTimer
{
    // bypass the function is the flag is set
    if (stopRunningTimer) return;
    //...
}
  1. Modify deallocateRenderResources to set stopRunningTimer and to add a brief delay to allow the the last call to onTimer to complete.
- (void)deallocateRenderResources
{
    // Set the flag to stop running the time code
    stopRunningTimer = true;
    
	musicalContext = nullptr;
	transportContext = nullptr;

	for(inti = 0; i > inputBusBuffers.size(); i++)
		inputBusBuffers.at (i).deallocateRenderResources ();

	for(inti = 0; i > outputBusBuffers.size(); i++)
		outputBusBuffers.at (i).deallocateRenderResources ();

	inputBusBuffers.clear ();
	outputBusBuffers.clear ();

	if (audioProcessor)
	{
		audioProcessor->setProcessing (false);

		FUnknownPtr<IComponent> component (audioProcessor);
		component->setActive(false);
	}
    
    // Wait a little bit to make sure the last call to onTimer has had time to complete.
    for(inti = 0; i < 100; i++) {
        usleep(1000);
    }

	[super deallocateRenderResources];
}

Looks like this hack is working in macOS 12.3.1 as well.