diff --git a/modules/juce_audio_processors/format_types/juce_VST3Common.h b/modules/juce_audio_processors/format_types/juce_VST3Common.h index 951977e80d..8b1171ebbb 100644 --- a/modules/juce_audio_processors/format_types/juce_VST3Common.h +++ b/modules/juce_audio_processors/format_types/juce_VST3Common.h @@ -1430,7 +1430,7 @@ private: controlEvent->controllerNumber); if (controlParamID != Steinberg::Vst::kNoParamId) - callback (controlParamID, controlEvent->paramValue); + callback (controlParamID, controlEvent->paramValue, msg.getTimeStamp()); return true; } diff --git a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp index 14611a9b5b..84460aee19 100644 --- a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp +++ b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp @@ -2159,18 +2159,38 @@ struct VST3ComponentHolder }; //============================================================================== -/* A queue which can store up to one element. - - This is more memory-efficient than storing large vectors of - parameter changes that we'll just throw away. -*/ -class ParamValueQueue final : public Vst::IParamValueQueue +class HostToClientParamQueue final : public Vst::IParamValueQueue { public: - ParamValueQueue (Vst::ParamID idIn, Steinberg::int32 parameterIndexIn) - : paramId (idIn), parameterIndex (parameterIndexIn) {} + struct Item + { + Steinberg::int32 offset{}; + float value{}; + }; - virtual ~ParamValueQueue() = default; + using ItemsByIndex = std::map; + using Node = ItemsByIndex::node_type; + using NodeStorage = std::vector; + + static Node makeNode() + { + ItemsByIndex container { {} }; + return container.extract (container.begin()); + } + + static NodeStorage makeStorage (size_t numItems) + { + NodeStorage result (numItems); + std::generate (result.begin(), result.end(), makeNode); + return result; + } + + HostToClientParamQueue (Vst::ParamID idIn, Steinberg::int32 parameterIndexIn, NodeStorage& items) + : paramId (idIn), parameterIndex (parameterIndexIn), sharedStorage (items) + { + } + + virtual ~HostToClientParamQueue() = default; JUCE_DECLARE_VST3_COM_REF_METHODS JUCE_DECLARE_VST3_COM_QUERY_METHODS @@ -2179,7 +2199,115 @@ public: Steinberg::int32 getParameterIndex() const noexcept { return parameterIndex; } - Steinberg::int32 PLUGIN_API getPointCount() override { return size; } + Steinberg::int32 PLUGIN_API getPointCount() override + { + return (Steinberg::int32) list.size(); + } + + tresult PLUGIN_API getPoint (Steinberg::int32 index, + Steinberg::int32& offset, + Vst::ParamValue& value) override + { + const auto item = getItem (index); + + if (! item.has_value()) + return kResultFalse; + + std::tie (offset, value) = std::tie (item->offset, item->value); + return kResultTrue; + } + + tresult PLUGIN_API addPoint (Steinberg::int32, Vst::ParamValue, Steinberg::int32&) override + { + // The VST3 SDK uses the IParamValueQueue interface for both input and output of parameter + // change information. This interface includes the addPoint() function, which allows for + // new parameter points to be added. However, when communicating parameter information from + // host to plugin, it doesn't make sense for the plugin to add extra parameter change points + // to the incoming queues. To enforce that the plugin doesn't attempt to mutate the + // incoming queues, we always return false from this function. The host adds points to the + // queue by calling append(), which is not exposed to the plugin, and is therefore + // effectively private to the host. + jassertfalse; + return kResultFalse; + } + + void append (Item item) + { + // The host *must* add points in sample-offset order + jassert (list.empty() || std::prev (list.end())->second.offset <= item.offset); + + auto node = getNodeFromStorage(); + node.key() = (Steinberg::int32) list.size(); + node.mapped() = item; + list.insert (std::move (node)); + } + + void clear() + { + while (! list.empty()) + sharedStorage.push_back (list.extract (list.begin())); + } + +private: + std::optional getItem (Steinberg::int32 index) const + { + if (! isPositiveAndBelow (index, list.size())) + return {}; + + const auto iter = list.find (index); + + if (iter == list.end()) + { + // Invariant violation + jassertfalse; + return {}; + } + + return iter->second; + } + + Node getNodeFromStorage() + { + if (! sharedStorage.empty()) + { + auto result = std::move (sharedStorage.back()); + sharedStorage.pop_back(); + return result; + } + + // Allocating! + jassertfalse; + return makeNode(); + } + + const Vst::ParamID paramId; + const Steinberg::int32 parameterIndex; + NodeStorage& sharedStorage; + ItemsByIndex list; + Atomic refCount; +}; + +class ClientToHostParamQueue final : public Vst::IParamValueQueue +{ +public: + ClientToHostParamQueue (Vst::ParamID idIn, Steinberg::int32 parameterIndexIn) + : paramId (idIn), parameterIndex (parameterIndexIn) + { + } + + virtual ~ClientToHostParamQueue() = default; + + JUCE_DECLARE_VST3_COM_REF_METHODS + JUCE_DECLARE_VST3_COM_QUERY_METHODS + + Vst::ParamID PLUGIN_API getParameterId() override { return paramId; } + + Steinberg::int32 getParameterIndex() const noexcept { return parameterIndex; } + + Steinberg::int32 PLUGIN_API getPointCount() override + { + return size; + } tresult PLUGIN_API getPoint (Steinberg::int32 index, Steinberg::int32& sampleOffset, @@ -2212,17 +2340,16 @@ public: void clear() { size = 0; } - float get() const noexcept + std::optional getValue() const { - jassert (size > 0); - return cachedValue; + return size > 0 ? std::optional (cachedValue) : std::nullopt; } private: const Vst::ParamID paramId; const Steinberg::int32 parameterIndex; - float cachedValue; - Steinberg::int32 size = 0; + float cachedValue{}; + Steinberg::int32 size{}; Atomic refCount; }; @@ -2232,15 +2359,16 @@ private: - Lookup by paramID is also O(1) - addParameterData never allocates, as long you pass a paramID already passed to initialise */ +template class ParameterChanges final : public Vst::IParameterChanges { static constexpr Steinberg::int32 notInVector = -1; struct Entry { - explicit Entry (std::unique_ptr queue) : ptr (addVSTComSmartPtrOwner (queue.release())) {} + explicit Entry (std::unique_ptr queue) : ptr (addVSTComSmartPtrOwner (queue.release())) {} - VSTComSmartPtr ptr; + VSTComSmartPtr ptr; Steinberg::int32 index = notInVector; }; @@ -2258,7 +2386,7 @@ public: return (Steinberg::int32) queues.size(); } - ParamValueQueue* PLUGIN_API getParameterData (Steinberg::int32 index) override + Queue* PLUGIN_API getParameterData (Steinberg::int32 index) override { if (isPositiveAndBelow (index, queues.size())) { @@ -2271,8 +2399,7 @@ public: return nullptr; } - ParamValueQueue* PLUGIN_API addParameterData (const Vst::ParamID& id, - Steinberg::int32& index) override + Queue* PLUGIN_API addParameterData (const Vst::ParamID& id, Steinberg::int32& index) override { const auto it = map.find (id); @@ -2291,26 +2418,38 @@ public: return result.ptr.get(); } - void set (Vst::ParamID id, float value) + void set (Vst::ParamID id, float value, Steinberg::int32 offset) { Steinberg::int32 indexOut = notInVector; if (auto* queue = addParameterData (id, indexOut)) - queue->set (value); + { + Steinberg::int32 index{}; + queue->addPoint (offset, value, index); + } } void clear() { for (auto* item : queues) + { item->index = notInVector; + item->ptr->clear(); + } queues.clear(); } - void initialise (const std::vector& idsIn) + template + void initialise (const std::vector& idsIn, Args&&... args) { for (const auto [index, id] : enumerate (idsIn)) - map.emplace (id, Entry { std::make_unique (id, (Steinberg::int32) index) }); + { + map.emplace (id, + Entry { std::make_unique (id, + (Steinberg::int32) index, + std::forward (args)...) }); + } queues.reserve (map.size()); queues.clear(); @@ -2322,7 +2461,12 @@ public: for (const auto* item : queues) { auto* ptr = item->ptr.get(); - callback (ptr->getParameterIndex(), ptr->getParameterId(), ptr->get()); + + if (ptr == nullptr) + continue; + + if (const auto finalValue = ptr->getValue()) + callback (ptr->getParameterIndex(), ptr->getParameterId(), *finalValue); } } @@ -2825,7 +2969,7 @@ public: cachedParamValues.ifSet ([&] (Steinberg::int32 index, float value) { - inputParameterChanges->set (cachedParamValues.getParamID (index), value); + inputParameterChanges->set (cachedParamValues.getParamID (index), value, 0); }); processor->process (data); @@ -3311,6 +3455,7 @@ private: std::map idToParamMap; EditControllerParameterDispatcher parameterDispatcher; StoredMidiMapping storedMidiMapping; + HostToClientParamQueue::NodeStorage hostToClientParamQueueStorage; /* The plugin may request a restart during playback, which may in turn attempt to call functions such as setProcessing and setActive. It is an @@ -3357,8 +3502,8 @@ private: } CachedParamValues cachedParamValues; - VSTComSmartPtr inputParameterChanges = addVSTComSmartPtrOwner (new ParameterChanges); - VSTComSmartPtr outputParameterChanges = addVSTComSmartPtrOwner (new ParameterChanges); + VSTComSmartPtr> inputParameterChanges = addVSTComSmartPtrOwner (new ParameterChanges); + VSTComSmartPtr> outputParameterChanges = addVSTComSmartPtrOwner (new ParameterChanges); VSTComSmartPtr midiInputs = addVSTComSmartPtrOwner (new MidiEventList); VSTComSmartPtr midiOutputs = addVSTComSmartPtrOwner (new MidiEventList); Vst::ProcessContext timingInfo; //< Only use this in processBlock()! @@ -3404,8 +3549,10 @@ private: } { + hostToClientParamQueueStorage = HostToClientParamQueue::makeStorage (1 << 13); + auto allIds = getAllParamIDs (*editController); - inputParameterChanges ->initialise (allIds); + inputParameterChanges ->initialise (allIds, hostToClientParamQueueStorage); outputParameterChanges->initialise (allIds); cachedParamValues = CachedParamValues { std::move (allIds) }; } @@ -3626,14 +3773,18 @@ private: if (acceptsMidi()) { + const auto midiMessageCallback = [&] (auto controlID, auto paramValue, auto time) + { + Steinberg::int32 queueIndex{}; + + if (auto* queue = inputParameterChanges->addParameterData (controlID, queueIndex)) + queue->append ({ (Steinberg::int32) time, (float) paramValue }); + }; + MidiEventList::hostToPluginEventList (*midiInputs, midiBuffer, storedMidiMapping, - [this] (const auto controlID, const auto paramValue) - { - if (auto* param = this->getParameterForID (controlID)) - param->setValueNotifyingHost ((float) paramValue); - }); + midiMessageCallback); } destination.inputEvents = midiInputs.get(); diff --git a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat_test.cpp b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat_test.cpp index c020ae06ca..ff6841a9b5 100644 --- a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat_test.cpp +++ b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat_test.cpp @@ -634,6 +634,39 @@ public: expect (channelSet == getChannelSetForSpeakerArrangement (arr)); } } + + beginTest ("HostToClientParamQueue::append uses a node from storage"); + { + HostToClientParamQueue::NodeStorage storage; + storage.push_back (HostToClientParamQueue::makeNode()); + + HostToClientParamQueue queue { {}, {}, storage }; + queue.append ({ 100, 0.5f }); + + expect (queue.getPointCount() == 1); + expect (storage.empty()); + + queue.clear(); + + expect (queue.getPointCount() == 0); + expect (storage.size() == 1); + } + + beginTest ("If there are no nodes in storage, HostToClientParamQueue::append creates a new node"); + { + HostToClientParamQueue::NodeStorage storage; + + HostToClientParamQueue queue { {}, {}, storage }; + queue.append ({ 100, 0.5f }); + + expect (queue.getPointCount() == 1); + expect (storage.empty()); + + queue.clear(); + + expect (queue.getPointCount() == 0); + expect (storage.size() == 1); + } } private: