diff --git a/src/detail/auv2/process.cpp b/src/detail/auv2/process.cpp index 50ed81c8..ebe3c4a3 100644 --- a/src/detail/auv2/process.cpp +++ b/src/detail/auv2/process.cpp @@ -852,4 +852,55 @@ void ProcessAdapter::addParameterEvent(const clap_param_info_t &info, double val this->_eventindices.emplace_back(this->_events.size()); this->_events.emplace_back(n); } + +// Moves the queued parameter events onto another adapter. This exists for one +// moment: deactivateCLAP() drops the process adapter, and anything the host set +// since the last render or flush is still sitting in it. Those values are as +// real as any other -- a host that sets a parameter and then deactivates the +// unit (a restart, a format change, a project close-and-reopen) has been told +// nothing went wrong -- so they move to the adapter the deactivated state +// flushes from rather than dying with this one. +// +// Only parameter events move. Everything else queued here belongs to a render +// that is not going to happen now, and a flush may not carry it anyway. +// +// The offsets go with the block they were offsets into, and dropping them is +// not tidiness: sortEventIndices() sorts on time first, and only breaks ties on +// the insertion index. Left as they were, an event stamped into some earlier +// block would sort *after* a value the deactivated state queues later at time +// 0 -- the stale value applied last, which is the inversion this transfer +// exists to avoid. At 0 they all tie, and the tiebreak then orders them the way +// they arrived: these first, anything queued afterwards over the top. +// +// Order follows position in _events, because flush() rebuilds _eventindices +// over it before sorting. Keeping the index list in step here is for a +// process() that never comes on the adapter these are going to. +size_t ProcessAdapter::transferPendingParametersTo(ProcessAdapter &other) +{ + if (&other == this) return 0; + + size_t moved = 0; + size_t kept = 0; + for (size_t i = 0; i < _events.size(); ++i) + { + if (isParameterEvent(_events[i].header.type)) + { + clap_multi_event_t n = _events[i]; + n.header.time = 0; + other._eventindices.emplace_back(other._events.size()); + other._events.emplace_back(n); + ++moved; + } + else + { + _events[kept++] = _events[i]; + } + } + + _events.resize(kept); + _eventindices.clear(); + for (size_t i = 0; i < kept; ++i) _eventindices.emplace_back(i); + + return moved; +} } // namespace Clap::AUv2 diff --git a/src/detail/auv2/process.h b/src/detail/auv2/process.h index 30186b1e..004a7e11 100644 --- a/src/detail/auv2/process.h +++ b/src/detail/auv2/process.h @@ -113,6 +113,10 @@ class ProcessAdapter UInt32 inOffsetSampleFrame); void stopNote(int32_t note_id, int16_t channel, UInt32 inOffsetSampleFrame); void addParameterEvent(const clap_param_info_t &info, double value, uint32_t inOffsetSampleFrame); + // Hands the parameter events still queued here to another adapter, so they + // survive this one being destroyed. Returns how many moved. See + // WrapAsAUV2::deactivateCLAP(). + size_t transferPendingParametersTo(ProcessAdapter &other); // void startNote() ~ProcessAdapter(); diff --git a/src/wrapasauv2.cpp b/src/wrapasauv2.cpp index f94388e4..d0dac7a1 100644 --- a/src/wrapasauv2.cpp +++ b/src/wrapasauv2.cpp @@ -1570,7 +1570,6 @@ void WrapAsAUV2::deactivateCLAP() // pointer and then use it, and the idle tick may be inside a flush on it. ClapWrapper::detail::shared::SpinLockGuard processGuard(_processLock); _initialized = false; - _processAdapter.reset(); // Stand the deactivated-state adapter up here, on the main thread, rather // than leaving SetParameter to build it on demand. AUBase calls @@ -1580,7 +1579,31 @@ void WrapAsAUV2::deactivateCLAP() // rebuild its DSP. A render thread that had to allocate the replacement // would be allocating inside a real-time callback; finding one already // here makes it a queue push and nothing more. - ensureFlushAdapter(); + // + // It is also where the process adapter's own backlog goes. Whatever the + // host set since the last render or flush is still queued there, and + // dropping the adapter would drop it: the host was told the value took, + // and the plugin would never hear it. This is the same loss SetParameter + // avoids while the CLAP is deactivated, at the other end of the window -- + // the moment the adapter goes away rather than the span when there is + // none. Delivery is the deactivated path's job from here: the next idle + // tick, or activateCLAP() ahead of clap_plugin.activate(), whichever + // comes first, both on the main thread with the plugin inactive, which is + // where clap_plugin_params.flush() is legal. Nothing is handed to the + // plugin here -- it is still active until the deactivate() below, and a + // flush at this point would have to claim the audio thread during + // teardown. + if (auto *flushAdapter = ensureFlushAdapter()) + { + if (_processAdapter && _processAdapter->transferPendingParametersTo(*flushAdapter) > 0) + { + // Nothing else would ask: the transfer is not a SetParameter, and the + // idle tick only flushes when something has asked it to. + _requestedFlush = true; + } + } + + _processAdapter.reset(); } _plugin->stop_processing(); _plugin->deactivate();