diff --git a/src/detail/auv3/auv3_audiounit.h b/src/detail/auv3/auv3_audiounit.h index 98212c4b..9a790d24 100644 --- a/src/detail/auv3/auv3_audiounit.h +++ b/src/detail/auv3/auv3_audiounit.h @@ -33,6 +33,10 @@ // Subclasses (generated by build-helper) override this to provide plugin-specific info - (AUAudioUnit *)createAudioUnitWithComponentDescription:(AudioComponentDescription)desc error:(NSError **)error; +// The generated override must build its audio unit through this: the host's +// factory call and -loadView's self-bootstrap race, and only one audio unit may +// ever exist per view controller. `make` runs only if there is none yet. +- (AUAudioUnit *)createAudioUnitOnce:(ClapAUv3AudioUnit * (^)(NSError **))make error:(NSError **)error; @end @interface ClapAUv3AudioUnit : AUAudioUnit diff --git a/src/detail/auv3/auv3_audiounit.mm b/src/detail/auv3/auv3_audiounit.mm index cd7c6183..715aee23 100644 --- a/src/detail/auv3/auv3_audiounit.mm +++ b/src/detail/auv3/auv3_audiounit.mm @@ -2077,7 +2077,13 @@ - (BOOL)setGUISize:(uint32_t)width height:(uint32_t)height // misbehaviour. auto *gui = _impl->_plugin->_ext._gui; if (gui->adjust_size) gui->adjust_size(_impl->_plugin->_plugin, &width, &height); - return gui->set_size(_impl->_plugin->_plugin, width, height) ? YES : NO; + if (!gui->set_size(_impl->_plugin->_plugin, width, height)) return NO; + + // Keep the cached size in step: prepareGUIAndReturnWidth:height: hands it + // back, and that is how the view controller learns the adjusted size. + _impl->_guiWidth = width; + _impl->_guiHeight = height; + return YES; } - (void)setViewController:(ClapAUv3ViewController *)vc @@ -2277,6 +2283,8 @@ @implementation ClapAUv3ViewController CGSize _pluginSize; // We already tried to create the audio unit ourselves (see _bootstrapAudioUnit). BOOL _bootstrapAttempted; + // Serializes the two creation paths (see -createAudioUnitOnce:error:). + std::mutex _audioUnitMutex; } - (void)loadView @@ -2309,6 +2317,10 @@ - (void)loadView - (void)setAudioUnit:(ClapAUv3AudioUnit *)audioUnit { + if (_audioUnit == audioUnit) return; + // Never drop a live audio unit by bare overwrite: releasing the last reference + // reaches plugin->terminate() with the GUI still created, which CLAP forbids. + if (_audioUnit) [self _destroyGUI]; _audioUnit = audioUnit; // Establish the back-reference so the AU can return us from // requestViewControllerWithCompletionHandler: @@ -2362,6 +2374,22 @@ - (BOOL)_ownComponentDescription:(AudioComponentDescription *)outDesc return NO; } +// Both creation paths meet here: the host's factory call, which arrives on an +// XPC worker thread, and -loadView's self-bootstrap on the appex main thread. +// Unserialized they both see a nil audioUnit and build an audio unit, and the +// loser is then thrown away with its GUI still alive. Nothing under this lock +// ever waits on the main queue, so the losing path only stalls. +- (AUAudioUnit *)createAudioUnitOnce:(ClapAUv3AudioUnit * (^)(NSError **))make error:(NSError **)error +{ + std::lock_guard lock(_audioUnitMutex); + if (_audioUnit) return _audioUnit; + + ClapAUv3AudioUnit *au = make(error); + if (!au) return nil; + self.audioUnit = au; + return au; +} + // Out-of-process AUv3: the viewbridge loads our view from inside // -beginRequestWithExtensionContext:, before the host has asked the factory for // an audio unit, and it captures preferredContentSize as the view is vended — @@ -2565,10 +2593,20 @@ - (void)viewDidLayoutSubviews [self.audioUnit setGUISize:(uint32_t)bounds.size.width height:(uint32_t)bounds.size.height]; } - // Ensure the CLAP plugin's subview fills the container + // adjust_size may have snapped the container bounds (fixed aspect ratio, + // step sizes), and the plugin laid out at the snapped size — so the subview + // has to get that size too, not the raw bounds. + CGRect childFrame = bounds; + uint32_t agreedWidth = 0, agreedHeight = 0; + if ([self.audioUnit prepareGUIAndReturnWidth:&agreedWidth height:&agreedHeight] && agreedWidth > 0 && + agreedHeight > 0) + { + childFrame = CGRectMake(0, 0, agreedWidth, agreedHeight); + } + for (CLAPWRAP_ViewClass *subview in self.view.subviews) { - subview.frame = bounds; + subview.frame = childFrame; } } } diff --git a/src/detail/auv3/build-helper/build-helper.cpp b/src/detail/auv3/build-helper/build-helper.cpp index 8b878a6f..7997cfd9 100644 --- a/src/detail/auv3/build-helper/build-helper.cpp +++ b/src/detail/auv3/build-helper/build-helper.cpp @@ -481,19 +481,18 @@ int main(int argc, char **argv) cppf << "- (AUAudioUnit *)createAudioUnitWithComponentDescription:(AudioComponentDescription)desc\n" << " error:(NSError **)error {\n" - << " // The view controller may have created it already, to answer a size\n" - << " // query before the host got here (see -_bootstrapAudioUnit).\n" - << " if (self.audioUnit) return self.audioUnit;\n" - << " ClapAUv3AudioUnit *au = [[ClapAUv3AudioUnit alloc] initWithComponentDescription:desc\n" + << " // -loadView may be bootstrapping one right now on the main thread, so the\n" + << " // check and the build have to happen under the base class's lock.\n" + << " return [self createAudioUnitOnce:^ClapAUv3AudioUnit *(NSError **err) {\n" + << " return [[ClapAUv3AudioUnit alloc] initWithComponentDescription:desc\n" << " options:0\n" - << " error:error\n" + << " error:err\n" << " clapName:@\"" << escapeObjCString(u.clapname) << "\"\n" << " clapId:@\"" << escapeObjCString(u.clapid) << "\"\n" << " clapIndex:" << idx << "];\n" - << " self.audioUnit = au;\n" - << " return au;\n" + << " } error:error];\n" << "}\n" << "@end\n\n"; diff --git a/src/detail/auv3/templates/generated_auv3_entrypoints.hxx.in b/src/detail/auv3/templates/generated_auv3_entrypoints.hxx.in index 5592236d..2c730533 100644 --- a/src/detail/auv3/templates/generated_auv3_entrypoints.hxx.in +++ b/src/detail/auv3/templates/generated_auv3_entrypoints.hxx.in @@ -13,16 +13,15 @@ @implementation @AUV3_FACTORY_CLASS_NAME@ - (AUAudioUnit *)createAudioUnitWithComponentDescription:(AudioComponentDescription)desc error:(NSError **)error { - // The view controller may have created it already, to answer a size - // query before the host got here (see -_bootstrapAudioUnit). - if (self.audioUnit) return self.audioUnit; - ClapAUv3AudioUnit *au = [[ClapAUv3AudioUnit alloc] initWithComponentDescription:desc + // -loadView may be bootstrapping one right now on the main thread, so the + // check and the build have to happen under the base class's lock. + return [self createAudioUnitOnce:^ClapAUv3AudioUnit *(NSError **err) { + return [[ClapAUv3AudioUnit alloc] initWithComponentDescription:desc options:0 - error:error + error:err clapName:@"@AUV3_CLAP_NAME@" clapId:@"@AUV3_CLAP_ID@" clapIndex:0]; - self.audioUnit = au; - return au; + } error:error]; } @end