diff --git a/src/detail/auv2/auv2_base_classes.h b/src/detail/auv2/auv2_base_classes.h index 8b748ed3..fe9904a8 100644 --- a/src/detail/auv2/auv2_base_classes.h +++ b/src/detail/auv2/auv2_base_classes.h @@ -839,7 +839,7 @@ class WrapAsAUV2 : public ausdk::AUBase, // buffer sizes). bool applyConfigurationFromBusFormats(); - // Returns false when the plugin rejects the host-chosen bus formats; the + // Returns false when the plugin rejects the bus formats or fails to start; // caller must treat that as a failed initialization. bool activateCLAP(); void deactivateCLAP(); @@ -860,6 +860,9 @@ class WrapAsAUV2 : public ausdk::AUBase, // --------------- internals + /// Shares the SDK's non-realtime entry lock with idle callbacks and the Cocoa editor. + std::shared_ptr _mainThreadMutex = std::make_shared(); + // the wrapped CLAP: std::string _clapname; std::string _clapid; @@ -882,6 +885,8 @@ class WrapAsAUV2 : public ausdk::AUBase, // false while the AU keeps running but nothing may enter the plugin. Main // thread only (activateCLAP()/deactivateCLAP()). bool _clapActive = false; + /// Whether CLAP processing started and requires a matching stop notification. + bool _clapProcessing = false; // some info about the wrapped clap // audio-port layout captured at PostConstructor. Scanning the CLAP diff --git a/src/detail/auv2/auv2_shared.h b/src/detail/auv2/auv2_shared.h index 04d87895..935ec44a 100644 --- a/src/detail/auv2/auv2_shared.h +++ b/src/detail/auv2/auv2_shared.h @@ -14,6 +14,8 @@ #include #include +#include +#include #include "clap_proxy.h" #include @@ -27,7 +29,9 @@ namespace free_audio::auv2_wrapper typedef struct ui_connection { uint32_t identifier = kAudioUnitProperty_ClapWrapper_UIConnection_id; - Clap::Plugin *_plugin = nullptr; // points to the plugin instance + Clap::Plugin *_plugin = nullptr; // points to the plugin instance + /// Serializes host lifecycle calls with editor calls and outlives either connection endpoint. + std::shared_ptr _mainThreadMutex; clap_window_t *_window = nullptr; // points to a window handle, actually ptr to wrapping NSView class uint32_t *_canary = nullptr; // a canary in the Windows class std::function _registerWindow = nullptr; diff --git a/src/detail/auv2/process.cpp b/src/detail/auv2/process.cpp index ebe3c4a3..48384f50 100644 --- a/src/detail/auv2/process.cpp +++ b/src/detail/auv2/process.cpp @@ -292,7 +292,9 @@ void ProcessAdapter::process(ProcessData &data) for (uint32_t i = 0; i < _numInputs; ++i) { auto &m = static_cast(*_audioInputScope->SafeGetElement(i)); - if (m.PullInput(data.flags, data.timestamp, i, data.numSamples) == noErr) + // Silence reported by one input must not describe another input or the plugin output. + auto inputFlags = data.flags & ~kAudioUnitRenderAction_OutputIsSilence; + if (m.PullInput(inputFlags, data.timestamp, i, data.numSamples) == noErr) { AudioBufferList &myInBuffers = m.GetBufferList(); auto num = myInBuffers.mNumberBuffers; @@ -352,7 +354,6 @@ void ProcessAdapter::process(ProcessData &data) #endif _plugin->process(_plugin, &_processData); - processOutputEvents(); // clean up and prepare the events for the next cycle diff --git a/src/detail/auv2/wrappedview.asinclude.mm b/src/detail/auv2/wrappedview.asinclude.mm index 67341133..e05e59b6 100644 --- a/src/detail/auv2/wrappedview.asinclude.mm +++ b/src/detail/auv2/wrappedview.asinclude.mm @@ -46,7 +46,7 @@ @implementation CLAP_WRAPPER_COCOA_CLASS - (NSView *)uiViewForAudioUnit:(AudioUnit)inAudioUnit withSize:(NSSize)inPreferredSize { - static free_audio::auv2_wrapper::ui_connection uiconn; + free_audio::auv2_wrapper::ui_connection uiconn; // free_audio::auv2_wrapper::ui_connection connection; // Remember we end up being called here because that's what AUCocoaUIView does in the initiation @@ -89,6 +89,9 @@ - (id)initWithAUv2:(free_audio::auv2_wrapper::ui_connection *)cont preferredSize LOGINFO("[clap-wrapper] creating NSView"); ui = *cont; + // The property lookup has returned, so editor calls need their own SDK entry guard. + const auto mainThreadMutex = ui._mainThreadMutex; + const ausdk::AUEntryGuard mainThreadGuard(mainThreadMutex.get()); canary = 0xbeebbeeb; if (ui._registerWindow) @@ -151,6 +154,8 @@ - (void)doIdle } - (void)viewDidMoveToWindow { + const auto mainThreadMutex = ui._mainThreadMutex; + const ausdk::AUEntryGuard mainThreadGuard(mainThreadMutex.get()); if ([self window] == nil) { LOGINFO("[clap-wrapper] - view removed from a window"); @@ -171,6 +176,9 @@ - (void)viewDidMoveToWindow - (void)dealloc { + // Super deallocation releases the C++ ivars before this scope unlocks the mutex. + const auto mainThreadMutex = ui._mainThreadMutex; + const ausdk::AUEntryGuard mainThreadGuard(mainThreadMutex.get()); LOGINFO("[clap-wrapper] NS View dealloc"); if (idleTimer) { @@ -186,6 +194,8 @@ - (void)dealloc - (void)setFrame:(NSRect)newSize { [super setFrame:newSize]; + const auto mainThreadMutex = ui._mainThreadMutex; + const ausdk::AUEntryGuard mainThreadGuard(mainThreadMutex.get()); if (canary) { auto gui = ui._plugin->_ext._gui; diff --git a/src/wrapasauv2.cpp b/src/wrapasauv2.cpp index 7963b8d4..dda87b09 100644 --- a/src/wrapasauv2.cpp +++ b/src/wrapasauv2.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include extern bool fillAudioUnitCocoaView(AudioUnitCocoaViewInfo *viewInfo, std::shared_ptr); @@ -158,6 +159,10 @@ WrapAsAUV2::WrapAsAUV2(const std::string &clapname, const std::string &clapid, i , _idx{idx} , _os_attached([this] { os::attach(this); }, [this] { os::detach(this); }) { + // Logic may initialize an AU on a worker while its editor and idle timer run. + // The SDK holds this lock across its complete non-realtime dispatch, including + // changes to AUBase::IsInitialized() before and after the virtual callbacks. + SetMutex(_mainThreadMutex.get()); _uiIsOpened = false; if (!_desc) { @@ -175,7 +180,6 @@ WrapAsAUV2::WrapAsAUV2(const std::string &clapname, const std::string &clapid, i if (_plugin) { _plugin->initialize(); - _os_attached.on(); } else { @@ -191,6 +195,7 @@ WrapAsAUV2::WrapAsAUV2(const std::string &clapname, const std::string &clapid, i WrapAsAUV2::~WrapAsAUV2() { + const std::lock_guard mainThreadGuard(*_mainThreadMutex); // The index outlives this instance (it is cached per module) and its crawl // thread holds our callback, so it has to be dropped before anything else. if (_presetIndex && _presetIndexToken) @@ -227,6 +232,8 @@ WrapAsAUV2::~WrapAsAUV2() { CFRelease(_current_program_name); } + // AUBase must not retain a pointer to a member-owned lock during base destruction. + SetMutex(nullptr); } // the very very reduced state machine @@ -246,10 +253,9 @@ OSStatus WrapAsAUV2::Initialize() auto guarantee_mainthread = _plugin->AlwaysMainThread(); if (!activateCLAP()) { - // The plugin refused the host-chosen main-bus format pair, or refused to - // activate at all (see activateCLAP). Refuse the initialization rather - // than render with buffers sized differently from the plugin's ports. - return kAudioUnitErr_FormatNotSupported; + // A rejected configuration or lifecycle call leaves the CLAP deactivated. + // Report the failure so the host does not render an unavailable processor. + return kAudioUnitErr_FailedInitialization; } #if 0 @@ -890,7 +896,6 @@ void WrapAsAUV2::Cleanup() if (_plugin->_plugin && _plugin->_ext._gui) { this->_uiconn._destroyWindow(); - this->_plugin->_ext._gui->destroy(_plugin->_plugin); } } } @@ -1056,6 +1061,7 @@ OSStatus WrapAsAUV2::GetProperty(AudioUnitPropertyID inID, AudioUnitScope inScop // return noErr; case kAudioUnitProperty_ClapWrapper_UIConnection_id: _uiconn._plugin = _plugin.get(); + _uiconn._mainThreadMutex = _mainThreadMutex; _uiconn._window = nullptr; _uiconn._registerWindow = [this](auto *x, auto *y) { @@ -1568,10 +1574,16 @@ bool WrapAsAUV2::activateCLAP() } _clapActive = true; - _plugin->start_processing(); + _clapProcessing = _plugin->start_processing(); + if (!_clapProcessing) + { + deactivateCLAP(); + return false; + } _initialized = true; + return true; } - return true; + return false; } void WrapAsAUV2::deactivateCLAP() @@ -1618,14 +1630,15 @@ void WrapAsAUV2::deactivateCLAP() _processAdapter.reset(); } - - // CLAP forbids either call on a plugin that is not active, and a - // reactivation that failed leaves it exactly that way. - if (_clapActive) + if (_clapProcessing) { - _clapActive = false; _plugin->stop_processing(); + _clapProcessing = false; + } + if (_clapActive) + { _plugin->deactivate(); + _clapActive = false; } } } @@ -1898,6 +1911,10 @@ void WrapAsAUV2::pushQueuedEventsToHost() void WrapAsAUV2::onIdle() { + // Preserve pending work when a host lifecycle call owns the CLAP main-thread state. + // Waiting here could block the real UI thread while host initialization waits for it. + const std::unique_lock mainThreadGuard(*_mainThreadMutex, std::try_to_lock); + if (!mainThreadGuard.owns_lock()) return; if (!_plugin) return; pushQueuedEventsToHost(); @@ -2617,6 +2634,9 @@ void WrapAsAUV2::PostConstructor() Inputs().GetElement(0)->SetName(CFSTR("Input")); LOGINFO("[clap-wrapper] PostConstructor: added placeholder silent input bus"); } + + // The timer must not enter the CLAP while its initial port configuration is being queried. + _os_attached.on(); } UInt32 WrapAsAUV2::GetAudioChannelLayout(AudioUnitScope scope, AudioUnitElement element,