Summary
In the LV2 plugin host (juce_LV2PluginFormatImpl.h), parameter changes made via AudioProcessorParameter::setValue() before the first prepareToPlay() (or between two prepareToPlay() calls without an intervening processBlock()) are silently discarded. Afterwards the plugin runs with its old values. getValue() and getText() report the old values too, even though the host had just set new ones.
Cause
LV2Parameter::setValue()only writes intoParameterValuesAndFlags(the cache) and marks the parameter as changed. The value reaches the control port in the nextprocessBlock()(preparePortsForRun()→postChangedParametersToProcessor()).prepareToPlay()deactivates the instance and saves the state withgetStateInformation(), which reads the control ports, so the pending values are not in the saved state. It then creates a new instance and restores that state.applyStateWithAppropriateLocking()finally callsparameterValues.updateFromControlPorts(), which overwrites the cache with the restored (old) port values.
Reproduction
- Create an LV2 effect instance, e.g. x42-eq (
http://gareus.org/oss/lv2/fil4#stereo). - Call
setValue()on its “Gain” parameter (−18 dB) without processing any audio. - Call
prepareToPlay (44100, 512)and process a test signal. - The output level is unchanged, and the parameter reads back its default.
Hosts that create plugins and set parameters before audio starts all hit this. Two examples: an offline render path, and a host whose playback graph only prepares plugins on first play. In Tracktion Engine, the first render after adding an LV2 plugin used the default values.
Fix
Before saving the state in prepareToPlay(), take the pending (not yet processed) values from the cache. After restoring the state into the new instance, write them back with Update::yes, so the next processBlock() delivers them as usual. This covers port-backed and patch-backed parameters the same way, and changes nothing when no values are pending.
diff --git a/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatImpl.h b/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatImpl.h
index e74c93c..ec4ca94 100644
--- a/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatImpl.h
+++ b/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatImpl.h
@@ -3126,6 +3126,23 @@ public:
});
}
+ /* Values set by the host that have not been passed to the processor yet.
+ Clears their "needs processor update" flags; hand them back with
+ restorePendingProcessorValues().
+ */
+ std::vector<std::pair<size_t, float>> takePendingProcessorValues()
+ {
+ std::vector<std::pair<size_t, float>> result;
+ cache.ifProcessorValuesChanged ([&] (size_t index, float value) { result.emplace_back (index, value); });
+ return result;
+ }
+
+ void restorePendingProcessorValues (const std::vector<std::pair<size_t, float>>& pending)
+ {
+ for (const auto& [index, value] : pending)
+ cache.set (index, value, Update::yes);
+ }
+
void postChangedParametersToUi (UiEventListener* target,
const ParameterWriterUrids helperUrids,
MessageBufferInterface<UiMessageHeader>& uiMessages)
@@ -3461,6 +3478,13 @@ public:
deactivate();
+ // Parameter changes made since the last processBlock() only live in the
+ // parameter cache, not in the control ports that the saved state is read
+ // from, and restoring the state below overwrites the cache. Keep them and
+ // re-apply them to the new instance, otherwise a value set before the
+ // first prepareToPlay() (or between two prepareToPlay() calls) is lost.
+ const auto pending = parameterValues.takePendingProcessorValues();
+
MemoryBlock mb;
getStateInformation (mb);
@@ -3473,6 +3497,7 @@ public:
// prepareToPlay is *guaranteed* not to be called concurrently with processBlock
setStateInformationImpl (mb.getData(), (int) mb.getSize(), ConcurrentWithAudioCallback::no);
+ parameterValues.restorePendingProcessorValues (pending);
jassert (numSamples == instance->features.getMaxBlockSize());
activate();
Tested on Linux with LSP Parametric Equalizer (filter type/frequency) and x42-eq (gain/low-pass): values set before the first prepareToPlay() now take effect, and behaviour with values set during playback is unchanged.