Sample accurate automation: hand the plugin the offset of every automation point (VST3), not the block's last value

JUCE 9.0.1. A VST3 host delivers a parameter’s automation as a queue of (sample offset, value) points per block. JuceVST3Component::processParameterChanges takes getPointFromQueue (paramQueue, numPoints - 1) - the last point - and sets the parameter once for the block, so a plugin that could render the curve at sample resolution (the host delivered it) steps it once per block, and a ramp becomes a staircase at the block rate.

Proposal (patch attached):

  • AudioProcessorParameter::setValueAt (int offsetSamples, float value), virtual, default setValue (value), so nothing changes for a parameter that does not override it.
  • The VST3 wrapper walks the whole queue and calls setValueAt for every point, in order; the last point still ends in setValue as before, so the parameter’s value after the block is what it was.

The AU and other formats keep their behaviour (AU’s kAudioUnitProperty_ScheduleParameters / AUScheduledParameterEvent would be the counterpart - ScheduleParameter in the AU wrapper could feed setValueAt with the event’s eventValues.immediate.bufferOffset, which the patch does not cover).

The two hunks are small and backward compatible; the plugin side decides per parameter whether offsets mean anything to it.

The patch against 9.0.1:

diff --git a/modules/juce_audio_plugin_client/juce_audio_plugin_client_VST3.cpp b/modules/juce_audio_plugin_client/juce_audio_plugin_client_VST3.cpp
index f758dc36ed..fc3967afdf 100644
--- a/modules/juce_audio_plugin_client/juce_audio_plugin_client_VST3.cpp
+++ b/modules/juce_audio_plugin_client/juce_audio_plugin_client_VST3.cpp
@@ -3531,9 +3531,15 @@ public:
                 }
                 else
                #endif
-                if (const auto change = getPointFromQueue (paramQueue, numPoints - 1))
+                if (auto* param = comPluginInstance->getParamForVSTParamID (vstParamID))
                 {
-                    if (auto* param = comPluginInstance->getParamForVSTParamID (vstParamID))
+                    // the whole queue, not only where it ends: a parameter that
+                    // reads the offsets renders the curve within the block
+                    for (Steinberg::int32 point = 0; point < numPoints; ++point)
+                        if (const auto change = getPointFromQueue (paramQueue, point))
+                            param->setValueAt ((int) change->offsetSamples, (float) change->value);
+
+                    if (const auto change = getPointFromQueue (paramQueue, numPoints - 1))
                         setValueAndNotifyIfChanged (*param, (float) change->value);
                 }
             }
diff --git a/modules/juce_audio_processors_headless/processors/juce_AudioProcessorParameter.h b/modules/juce_audio_processors_headless/processors/juce_AudioProcessorParameter.h
index 3007ea7cd3..9df5a4d083 100644
--- a/modules/juce_audio_processors_headless/processors/juce_AudioProcessorParameter.h
+++ b/modules/juce_audio_processors_headless/processors/juce_AudioProcessorParameter.h
@@ -128,6 +128,16 @@ public:
     */
     virtual void setValue (float newValue) = 0;
 
+    /** Called once per automation point of the block about to be rendered, on the
+        audio thread and before processBlock, by a format and wrapper that carry the
+        offset of a change within the block (VST3 parameter queues, AU scheduled
+        events, AAX). A parameter that needs only one value per block ignores it and
+        reads setValue as before.
+
+        The value passed will be between 0 and 1.0.
+    */
+    virtual void setValueAt (int /*offsetSamples*/, float /*newValue*/) {}
+
     /** A processor should call this when it needs to change one of its parameters.
 
         This could happen when the editor or some other internal operation changes