From 378058584c4c3eb2a59676c61b22a9f4537e3606 Mon Sep 17 00:00:00 2001 From: Marius Metzger Date: Wed, 9 Sep 2026 00:19:20 +0200 Subject: [PATCH 1/6] AUv2: render generated audio with silent input and valid action flags Allow initialized rendering with nonzero host action flags. Give each input pull its own flags and clear OutputIsSilence after CLAP processing, so a silent track input cannot mark Swell's preview output as silent. --- src/detail/auv2/process.cpp | 7 ++++++- src/wrapasauv2.cpp | 3 +-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/detail/auv2/process.cpp b/src/detail/auv2/process.cpp index 50ed81c8..e74c56ca 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; @@ -353,6 +355,9 @@ void ProcessAdapter::process(ProcessData &data) _plugin->process(_plugin, &_processData); + // A CLAP plugin may generate audible output even when its inputs are silent. + data.flags &= ~kAudioUnitRenderAction_OutputIsSilence; + processOutputEvents(); // clean up and prepare the events for the next cycle diff --git a/src/wrapasauv2.cpp b/src/wrapasauv2.cpp index d065a7e2..d59d39a9 100644 --- a/src/wrapasauv2.cpp +++ b/src/wrapasauv2.cpp @@ -1382,9 +1382,8 @@ void WrapAsAUV2::releaseHostMIDIOutput() OSStatus WrapAsAUV2::Render(AudioUnitRenderActionFlags &inFlags, const AudioTimeStamp &inTimeStamp, UInt32 inFrames) { - assert(inFlags == 0); ClapWrapper::detail::shared::SpinLockGuard processGuard(_processLock); - if (_initialized && (inFlags == 0)) + if (_initialized) { // do the render dance Clap::AUv2::ProcessData data{inFlags, inTimeStamp, inFrames, this}; From 121fe78cbe2a116d214f2035dc7f0d7333160b3b Mon Sep 17 00:00:00 2001 From: Marius Metzger Date: Wed, 9 Sep 2026 00:19:45 +0200 Subject: [PATCH 2/6] AUv2: honor activation failures and balance completed lifecycle stages Check activate and start_processing before publishing render readiness. Return an initialization failure when either is rejected, release the adapter, and stop or deactivate only stages that actually succeeded. This prevents a failed processor from appearing initialized to Logic and allows a subsequent initialization attempt to start cleanly. --- src/detail/auv2/auv2_base_classes.h | 6 ++++- src/wrapasauv2.cpp | 40 ++++++++++++++++++++--------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/detail/auv2/auv2_base_classes.h b/src/detail/auv2/auv2_base_classes.h index 25d30ede..174921d4 100644 --- a/src/detail/auv2/auv2_base_classes.h +++ b/src/detail/auv2/auv2_base_classes.h @@ -815,7 +815,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(); @@ -846,6 +846,10 @@ class WrapAsAUV2 : public ausdk::AUBase, // exist. Lives across flushes so gestures pair up; see flushParameters(). std::unique_ptr _flushAdapter; std::atomic _initialized = false; + /// Whether CLAP activation succeeded and requires a matching deactivation. + bool _clapActivated = 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/wrapasauv2.cpp b/src/wrapasauv2.cpp index d59d39a9..e3475e92 100644 --- a/src/wrapasauv2.cpp +++ b/src/wrapasauv2.cpp @@ -233,9 +233,8 @@ OSStatus WrapAsAUV2::Initialize() auto guarantee_mainthread = _plugin->AlwaysMainThread(); if (!activateCLAP()) { - // The host settled on a main-bus format pair the plugin does not accept - // (see activateCLAP). Refuse the initialization rather than render with - // buffers sized differently from the plugin's ports. + // A rejected configuration or lifecycle call leaves the CLAP deactivated. + // Report the failure so the host does not render an unavailable processor. return kAudioUnitErr_FormatNotSupported; } @@ -1339,11 +1338,22 @@ bool WrapAsAUV2::activateCLAP() _flushAdapter.reset(); } - _plugin->activate(); - _plugin->start_processing(); + _clapActivated = _plugin->activate(); + if (!_clapActivated) + { + deactivateCLAP(); + return false; + } + _clapProcessing = _plugin->start_processing(); + if (!_clapProcessing) + { + deactivateCLAP(); + return false; + } _initialized = true; + return true; } - return true; + return false; } void WrapAsAUV2::deactivateCLAP() @@ -1357,8 +1367,16 @@ void WrapAsAUV2::deactivateCLAP() _initialized = false; _processAdapter.reset(); } - _plugin->stop_processing(); - _plugin->deactivate(); + if (_clapProcessing) + { + _plugin->stop_processing(); + _clapProcessing = false; + } + if (_clapActivated) + { + _plugin->deactivate(); + _clapActivated = false; + } } } @@ -1633,10 +1651,8 @@ void WrapAsAUV2::onIdle() if (wasInitialized) { deactivateCLAP(); - // Cannot fail for the format-pair reason Initialize guards against: - // the formats have not changed since the last successful activation. - // If it fails anyway, _initialized stays false and renders return - // silence, the same state as before Initialize. + // Even unchanged formats can encounter a rejected CLAP lifecycle call. + // A failed restart leaves processing unavailable for a later retry. if (!activateCLAP()) { LOGINFO("[clap-wrapper] restart: could not reactivate the plugin"); From 0aedc9c72e81e718fe8a44d87191813761a7eee9 Mon Sep 17 00:00:00 2001 From: Marius Metzger Date: Wed, 9 Sep 2026 00:20:29 +0200 Subject: [PATCH 3/6] AUv2: serialize host lifecycle, idle, and editor entry Logic may initialize on a worker while the idle timer and Cocoa editor run on the main thread. Share the AU SDK entry mutex with those paths so they cannot access plugin main-thread state concurrently. Idle skips a busy tick without consuming pending requests or blocking the UI thread. Start idle after port setup and keep the shared mutex alive through view teardown. Editor callbacks run under their caller's entry guard. --- src/detail/auv2/auv2_base_classes.h | 3 +++ src/detail/auv2/auv2_shared.h | 4 ++++ src/detail/auv2/wrappedview.asinclude.mm | 12 +++++++++++- src/wrapasauv2.cpp | 17 ++++++++++++++++- 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/detail/auv2/auv2_base_classes.h b/src/detail/auv2/auv2_base_classes.h index 174921d4..49c4f4cc 100644 --- a/src/detail/auv2/auv2_base_classes.h +++ b/src/detail/auv2/auv2_base_classes.h @@ -832,6 +832,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; diff --git a/src/detail/auv2/auv2_shared.h b/src/detail/auv2/auv2_shared.h index 04d87895..b0344767 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 @@ -28,6 +30,8 @@ typedef struct ui_connection { uint32_t identifier = kAudioUnitProperty_ClapWrapper_UIConnection_id; 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/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 e3475e92..b095cc64 100644 --- a/src/wrapasauv2.cpp +++ b/src/wrapasauv2.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include extern bool fillAudioUnitCocoaView(AudioUnitCocoaViewInfo *viewInfo, std::shared_ptr); @@ -157,6 +158,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) { @@ -173,7 +178,6 @@ WrapAsAUV2::WrapAsAUV2(const std::string &clapname, const std::string &clapid, i if (_plugin) { _plugin->initialize(); - _os_attached.on(); } else { @@ -189,6 +193,7 @@ WrapAsAUV2::WrapAsAUV2(const std::string &clapname, const std::string &clapid, i WrapAsAUV2::~WrapAsAUV2() { + const std::lock_guard mainThreadGuard(*_mainThreadMutex); #if AUSDK_MIDI2_AVAILABLE if (auto blk = _midioutput_hosteventlistblock.exchange(nullptr)) Block_release(blk); for (auto blk : _retiredEventListBlocks) Block_release(blk); @@ -214,6 +219,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 @@ -883,6 +890,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) { @@ -1607,6 +1615,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(); @@ -2262,6 +2274,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, From a1ac38bef9adbd543dea3dbd3080a766bb777313 Mon Sep 17 00:00:00 2001 From: Marius Metzger Date: Wed, 9 Sep 2026 00:20:56 +0200 Subject: [PATCH 4/6] AUv2: destroy the CLAP editor once during cleanup The UI connection's destroy callback already calls gui.destroy and clears the editor state. Remove the second gui.destroy call from Cleanup. --- src/wrapasauv2.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wrapasauv2.cpp b/src/wrapasauv2.cpp index b095cc64..e6368cb0 100644 --- a/src/wrapasauv2.cpp +++ b/src/wrapasauv2.cpp @@ -724,7 +724,6 @@ void WrapAsAUV2::Cleanup() if (_plugin->_plugin && _plugin->_ext._gui) { this->_uiconn._destroyWindow(); - this->_plugin->_ext._gui->destroy(_plugin->_plugin); } } } From e800bb89f162a78a3c645a759deba43dab36947d Mon Sep 17 00:00:00 2001 From: Marius Metzger Date: Thu, 10 Sep 2026 09:02:42 +0200 Subject: [PATCH 5/6] Run clang-format --- src/detail/auv2/auv2_shared.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/detail/auv2/auv2_shared.h b/src/detail/auv2/auv2_shared.h index b0344767..935ec44a 100644 --- a/src/detail/auv2/auv2_shared.h +++ b/src/detail/auv2/auv2_shared.h @@ -29,7 +29,7 @@ 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 From 3e16498261f893186cbb29aa17c4d0579cbcc9b4 Mon Sep 17 00:00:00 2001 From: Marius Metzger Date: Tue, 22 Sep 2026 18:18:03 +0200 Subject: [PATCH 6/6] Address review comments --- src/detail/auv2/process.cpp | 4 ---- src/wrapasauv2.cpp | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/detail/auv2/process.cpp b/src/detail/auv2/process.cpp index c773c1c7..48384f50 100644 --- a/src/detail/auv2/process.cpp +++ b/src/detail/auv2/process.cpp @@ -354,10 +354,6 @@ void ProcessAdapter::process(ProcessData &data) #endif _plugin->process(_plugin, &_processData); - - // A CLAP plugin may generate audible output even when its inputs are silent. - data.flags &= ~kAudioUnitRenderAction_OutputIsSilence; - processOutputEvents(); // clean up and prepare the events for the next cycle diff --git a/src/wrapasauv2.cpp b/src/wrapasauv2.cpp index 50082511..dda87b09 100644 --- a/src/wrapasauv2.cpp +++ b/src/wrapasauv2.cpp @@ -255,7 +255,7 @@ OSStatus WrapAsAUV2::Initialize() { // A rejected configuration or lifecycle call leaves the CLAP deactivated. // Report the failure so the host does not render an unavailable processor. - return kAudioUnitErr_FormatNotSupported; + return kAudioUnitErr_FailedInitialization; } #if 0