1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

VST3 Host: Retain parameter timestamps for MIDI CC messages

This commit is contained in:
reuk 2024-10-14 19:21:59 +01:00
parent 67396435e5
commit 6c18a352f1
No known key found for this signature in database
GPG key ID: FCB43929F012EE5C
3 changed files with 219 additions and 35 deletions

View file

@ -1430,7 +1430,7 @@ private:
controlEvent->controllerNumber);
if (controlParamID != Steinberg::Vst::kNoParamId)
callback (controlParamID, controlEvent->paramValue);
callback (controlParamID, controlEvent->paramValue, msg.getTimeStamp());
return true;
}

View file

@ -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<Steinberg::int32, Item>;
using Node = ItemsByIndex::node_type;
using NodeStorage = std::vector<Node>;
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<Item> 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<int> 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<float> getValue() const
{
jassert (size > 0);
return cachedValue;
return size > 0 ? std::optional<float> (cachedValue) : std::nullopt;
}
private:
const Vst::ParamID paramId;
const Steinberg::int32 parameterIndex;
float cachedValue;
Steinberg::int32 size = 0;
float cachedValue{};
Steinberg::int32 size{};
Atomic<int> 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 <typename Queue>
class ParameterChanges final : public Vst::IParameterChanges
{
static constexpr Steinberg::int32 notInVector = -1;
struct Entry
{
explicit Entry (std::unique_ptr<ParamValueQueue> queue) : ptr (addVSTComSmartPtrOwner (queue.release())) {}
explicit Entry (std::unique_ptr<Queue> queue) : ptr (addVSTComSmartPtrOwner (queue.release())) {}
VSTComSmartPtr<ParamValueQueue> ptr;
VSTComSmartPtr<Queue> 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<Vst::ParamID>& idsIn)
template <typename... Args>
void initialise (const std::vector<Vst::ParamID>& idsIn, Args&&... args)
{
for (const auto [index, id] : enumerate (idsIn))
map.emplace (id, Entry { std::make_unique<ParamValueQueue> (id, (Steinberg::int32) index) });
{
map.emplace (id,
Entry { std::make_unique<Queue> (id,
(Steinberg::int32) index,
std::forward<Args> (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<Vst::ParamID, VST3Parameter*> 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<ParameterChanges> inputParameterChanges = addVSTComSmartPtrOwner (new ParameterChanges);
VSTComSmartPtr<ParameterChanges> outputParameterChanges = addVSTComSmartPtrOwner (new ParameterChanges);
VSTComSmartPtr<ParameterChanges<HostToClientParamQueue>> inputParameterChanges = addVSTComSmartPtrOwner (new ParameterChanges<HostToClientParamQueue>);
VSTComSmartPtr<ParameterChanges<ClientToHostParamQueue>> outputParameterChanges = addVSTComSmartPtrOwner (new ParameterChanges<ClientToHostParamQueue>);
VSTComSmartPtr<MidiEventList> midiInputs = addVSTComSmartPtrOwner (new MidiEventList);
VSTComSmartPtr<MidiEventList> 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();

View file

@ -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: