BUG+FIX VST3 program depended latency not recognised

Program depended latency is not recognised for VST3 clients, when plugin is newly loaded in JUCE hosts-projects
(dynamically changed latency is never recognised in JUCE-hosts, but this is another story)

I guess this is a bug (introduced through latest changes)

FIx:

diff --git a/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp b/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp
index ad464e362..53b7f45af 100644
--- a/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp
+++ b/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp
@@ -1248,7 +1248,7 @@ public:

             auto latencySamples = pluginInstance->getLatencySamples();

-            if (details.latencyChanged && latencySamples != lastLatencySamples)
+            if (details.latencyChanged || latencySamples != lastLatencySamples)^M
             {
                 flags |= Vst::kLatencyChanged;
                 lastLatencySamples = latencySamples;

@reuk according to git blame you changed this line last

Can you explain the problem you’re seeing in a bit more detail, including how it is triggered? Where are you calling setLatencySamples()? You mention both JUCE hosts and clients, so I’d like to make sure that this is the right change. At the moment, I think this line is correct: setLatencySamples always ends up calling this function with details.latencyChanged == true and I don’t think we should send a latency update to the host unless the latency has actually changed. Maybe there’s a particular broken sequence of calls that I haven’t considered, though.

My Plugin has a default latency of 64 which is set in prepareToPlay which will be called when the host project gets loaded

Stacktrace:

MyPlugin.vst3!My::MyAudioProcessor::prepareToPlay(double newSampleRate, int estimatedSamplesPerBlock) Line 521	C++
MyPlugin.vst3!juce::JuceVST3Component::preparePlugin(double sampleRate, int bufferSize) Line 3241	C++
MyPlugin.vst3!juce::JuceVST3Component::initialize(Steinberg::FUnknown * hostContext) Line 2093	C++
AudioPluginHost.exe!juce::VST3ComponentHolder::initialise() Line 1786	C++
AudioPluginHost.exe!juce::VST3PluginFormat::createPluginInstance(const juce::PluginDescription & description, double __formal, int __formal, std::function<void __cdecl(std::unique_ptr<juce::AudioPluginInstance,std::default_delete<juce::AudioPluginInstance>>,juce::String const &)> callback) Line 3545	C++
AudioPluginHost.exe!juce::AudioPluginFormat::createInstanceFromDescription(const juce::PluginDescription & desc, double initialSampleRate, int initialBufferSize, juce::String & errorMessage) Line 67	C++
AudioPluginHost.exe!juce::AudioPluginFormatManager::createPluginInstance(const juce::PluginDescription & description, double rate, int blockSize, juce::String & errorMessage) Line 96	C++
AudioPluginHost.exe!PluginGraph::createNodeFromXml::__l2::std::unique_ptr<juce::AudioPluginInstance,std::default_delete<juce::AudioPluginInstance>> <lambda>(void)::__l2::<lambda>(const juce::PluginDescription & description) Line 391	C++

This will call setLatency with a value 64

After that the editor controller is attached

MyPlugin.vst3!juce::AudioProcessorParameter::addListener(juce::AudioProcessorParameter::Listener * newListener) Line 1577	C++
MyPlugin.vst3!juce::JuceVST3EditController::OwnedParameterListener::OwnedParameterListener(juce::JuceVST3EditController & editController, juce::AudioProcessorParameter & juceParameter, unsigned long paramID) Line 1306	C++
[External Code]	
MyPlugin.vst3!juce::JuceVST3EditController::setupParameters() Line 1346	C++
MyPlugin.vst3!juce::JuceVST3EditController::connect(Steinberg::Vst::IConnectionPoint * other) Line 976	C++
AudioPluginHost.exe!juce::VST3PluginInstance::interconnectComponentAndController() Line 2972	C++
AudioPluginHost.exe!juce::VST3PluginInstance::initialise() Line 2233	C++
AudioPluginHost.exe!juce::VST3PluginFormat::createPluginInstance(const juce::PluginDescription & description, double __formal, int __formal, std::function<void __cdecl(std::unique_ptr<juce::AudioPluginInstance,std::default_delete<juce::AudioPluginInstance>>,juce::String const &)> callback) Line 3549	C++
AudioPluginHost.exe!juce::AudioPluginFormat::createInstanceFromDescription(const juce::PluginDescription & desc, double initialSampleRate, int initialBufferSize, juce::String & errorMessage) Line 67	C++
AudioPluginHost.exe!juce::AudioPluginFormatManager::createPluginInstance(const juce::PluginDescription & description, double rate, int blockSize, juce::String & errorMessage) Line 96	C++
AudioPluginHost.exe!PluginGraph::createNodeFromXml::__l2::std::unique_ptr<juce::AudioPluginInstance,std::default_delete<juce::AudioPluginInstance>> <lambda>(void)::__l2::<lambda>(const juce::PluginDescription & description) Line 391	C++

Then audioProcessorChanged is called directly, but this time latencyChanged is false
But the (new) editor-controller itself has lastLatency 0, but the current Latency is 64
so Vst::kLatencyChanged; will be not set
and lastLatencySamples will NOT be updated to 64

MyPlugin.vst3!juce::JuceVST3EditController::audioProcessorChanged(juce::AudioProcessor *     __formal, const juce::AudioProcessorListener::ChangeDetails & details) Line 1252	C++
MyPlugin.vst3!juce::JuceVST3EditController::setupParameters() Line 1388	C++
MyPlugin.vst3!juce::JuceVST3EditController::connect(Steinberg::Vst::IConnectionPoint * other) Line 976	C++
AudioPluginHost.exe!juce::VST3PluginInstance::interconnectComponentAndController() Line 2972	C++
AudioPluginHost.exe!juce::VST3PluginInstance::initialise() Line 2233	C++
AudioPluginHost.exe!juce::VST3PluginFormat::createPluginInstance(const juce::PluginDescription & description, double __formal, int __formal, std::function<void __cdecl(std::unique_ptr<juce::AudioPluginInstance,std::default_delete<juce::AudioPluginInstance>>,juce::String const &)> callback) Line 3549	C++
AudioPluginHost.exe!juce::AudioPluginFormat::createInstanceFromDescription(const juce::PluginDescription & desc, double initialSampleRate, int initialBufferSize, juce::String & errorMessage) Line 67	C++
AudioPluginHost.exe!juce::AudioPluginFormatManager::createPluginInstance(const juce::PluginDescription & description, double rate, int blockSize, juce::String & errorMessage) Line 96	C++MyPlugin

after that prepareToPlay is called multiple times, but I omit that because the latency does not change

Then the current state will be set, which result in setLatency(0) because this current program does not use Latency

 MyPlugin.vst3!juce::JuceVST3Component::setStateInformation(const void * data, int sizeAsInt) Line 2285	C++
 MyPlugin.vst3!juce::JuceVST3Component::loadVST2CcnKBlock(const char * data, int size) Line 2310	C++
 MyPlugin.vst3!juce::JuceVST3Component::loadVST2VstWBlock(const char * data, int size) Line 2296	C++
 MyPlugin.vst3!juce::JuceVST3Component::loadVST2CompatibleState(const char * data, int size) Line 2359	C++
 MyPlugin.vst3!juce::JuceVST3Component::loadStateData(const void * data, int size) Line 2380	C++
 MyPlugin.vst3!juce::JuceVST3Component::readFromUnknownStream(Steinberg::IBStream * state) Line 2449	C++
 MyPlugin.vst3!juce::JuceVST3Component::setState(Steinberg::IBStream * state) Line 2464	C++
 AudioPluginHost.exe!juce::VST3PluginInstance::setStateInformation(const void * data, int sizeInBytes) Line 2785	C++
 AudioPluginHost.exe!PluginGraph::createNodeFromXml(const juce::XmlElement & xml) Line 436	C++

This will trigger audioProcessorChanged() in JuceVST3EditController::audioProcessorChanged
but the lastLatencySamples is still 0, latencySamples != lastLatencySamples is not true, while latency changed is true, so Vst::kLatencyChanged; will not be set

BTW: independent from this problem, it would be cool if the AudioPluginHost dynamically can detect the changed latency in hosted plugins, and rebuilds the rendering graph automatically

Thanks for the detailed write-up. It sounds like the problem is that during prepareToPlay on the hosting side, the latency is reported as 64 samples, but when the plugin changes the latency to 0 samples in setStateInformation, the host isn’t notified. Have I understood correctly?

I think I’ve reproduced the issue by modifying the CMake AudioPlugin example, by adding setLatencySamples (64) in prepareToPlay, and setLatencySamples (0) in setStateInformation. The root of the problem seems to be that lastLatencySamples doesn’t initially reflect the processor’s actual latency. Adding this statement at line 1334 of juce_VST3_Wrapper.cpp seems to fix the issue:

lastLatencySamples = pluginInstance->getLatencySamples();

Would you be able to try out this change? If it looks good on your side, I’ll merge this to develop.

You mean line 1343, or?

Yes, it does help. But I am not an expert in the field of VST3 how the initial latency is communicated.
It works right now. I guess every host default behaviour is to read out the latency after the initial call of prepareToPlay (JuceVST3Component::initialize), even when not explicitly informed.

Alternative change could be:

Change Line 1386 to:

audioProcessorChanged (pluginInstance, ChangeDetails().withParameterInfoChanged (true).withLatencyChanged(true));

But this will result in a (I guess) unnecessary restart of the component (I actually not fully understand if the VST3EditController is host or plugin side, so forgive me if this is utterly nonsense)

Sorry, that was a typo. I meant just after these lines:

    void setupParameters()
    {
        if (auto* pluginInstance = getPluginInstance())
        {

It sounds like this change works for you, so I’ll commit this once it’s been reviewed by the team.

Thanks!

Thanks for the report, this change is implemented here: