From 03c47e15d3563db633079f60ee99a0f9a215558b Mon Sep 17 00:00:00 2001 From: Ricard Wanderlof Date: Mon, 12 Oct 2026 21:46:46 +0200 Subject: [PATCH] Set unset parameters to default values (VST3, AU, CLAP) (#553) When DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE is set, when loading a preset that does not set all the possible parameters in the plugin, set the unset parameters to their default values rather than leaving them at whatever value they happen to be. This makes it possible to upgrade a preset to a version which introduces new parameters, while still making it possible to consistently load preset files that have been saved from the older version of the plugin. Otherwise, the resulting set of parameter values would depend on what the new parameters were set to previously. This affects the loading of VST3, AU and CLAP presets. For LV2, the loading is normally executed directly by the host, so the corresponding change needs to be done to the host instead (for instance, the Zynthian fork of jalv does this when supplied with the appropriate command line argument). --- distrho/DistrhoPlugin.hpp | 32 +++++++++++++ distrho/src/DistrhoPlugin.cpp | 4 ++ distrho/src/DistrhoPluginAU.cpp | 46 +++++++++++++++++++ distrho/src/DistrhoPluginCLAP.cpp | 50 ++++++++++++++++++++ distrho/src/DistrhoPluginChecks.h | 4 ++ distrho/src/DistrhoPluginInternal.hpp | 10 +++- distrho/src/DistrhoPluginVST3.cpp | 66 +++++++++++++++++++++++++++ 7 files changed, 211 insertions(+), 1 deletion(-) diff --git a/distrho/DistrhoPlugin.hpp b/distrho/DistrhoPlugin.hpp index 8570f9e97..c42e0acab 100644 --- a/distrho/DistrhoPlugin.hpp +++ b/distrho/DistrhoPlugin.hpp @@ -51,6 +51,20 @@ START_NAMESPACE_DISTRHO DISTRHO_PLUGIN_WANT_PROGRAMS activates program related features.@n When enabled you need to implement initProgramName() and loadProgram(). + DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE makes full-state restore (as + used by e.g. a saved VST3/CLAP preset file) deterministic regardless of + the plugin's prior runtime state: any parameter not covered by the + incoming state is reset to its coded default, rather than being left at + whatever value the plugin instance happened to already have. This holds + whether or not DISTRHO_PLUGIN_WANT_PROGRAMS is also enabled. When it is, + this flag additionally changes loadProgram()'s signature to accept an + optional bool* parametersSet out-parameter (see loadProgram()'s own + documentation for its contract), so that a parameter already covered by a + restored program is not needlessly reset a second time, nor incorrectly + overwritten if the program left it at a value other than its coded + default. Existing loadProgram() overrides need updating to populate + parametersSet when this flag is turned on. + DISTRHO_PLUGIN_WANT_STATE activates internal state features.@n When enabled you need to implement initState() and setState(). @@ -335,12 +349,30 @@ class Plugin virtual void setParameterValue(uint32_t index, float value); #if DISTRHO_PLUGIN_WANT_PROGRAMS + #if DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE + /** + Load a program.@n + The host may call this function from any context, including realtime processing.@n + Must be implemented by your plugin class only if DISTRHO_PLUGIN_WANT_PROGRAMS is enabled. + @param parametersSet Optional pointer to a caller-allocated, zero-initialized + bool array of size getParameterCount(). If non-null, implementations + must set parametersSet[i] = true for every parameter index i whose + value this call assigns. Callers that don't need this information + (e.g. a plain host-triggered program change) may omit it. + @note This extra parameter only exists because + DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE is enabled; see its + description above for why, and update any existing loadProgram() + override to match when turning this flag on. + */ + virtual void loadProgram(uint32_t index, bool* parametersSet = nullptr); + #else /** Load a program.@n The host may call this function from any context, including realtime processing.@n Must be implemented by your plugin class only if DISTRHO_PLUGIN_WANT_PROGRAMS is enabled. */ virtual void loadProgram(uint32_t index); + #endif #endif #if DISTRHO_PLUGIN_WANT_FULL_STATE diff --git a/distrho/src/DistrhoPlugin.cpp b/distrho/src/DistrhoPlugin.cpp index e2ffadb28..e87c481f2 100644 --- a/distrho/src/DistrhoPlugin.cpp +++ b/distrho/src/DistrhoPlugin.cpp @@ -230,7 +230,11 @@ float Plugin::getParameterValue(uint32_t) const { return 0.0f; } void Plugin::setParameterValue(uint32_t, float) {} #if DISTRHO_PLUGIN_WANT_PROGRAMS + #if DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE +void Plugin::loadProgram(uint32_t, bool*) {} + #else void Plugin::loadProgram(uint32_t) {} + #endif #endif #if DISTRHO_PLUGIN_WANT_FULL_STATE diff --git a/distrho/src/DistrhoPluginAU.cpp b/distrho/src/DistrhoPluginAU.cpp index 919faa719..ce78e0da3 100644 --- a/distrho/src/DistrhoPluginAU.cpp +++ b/distrho/src/DistrhoPluginAU.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #ifndef DISTRHO_PLUGIN_BRAND_ID @@ -2649,6 +2650,18 @@ class PluginAU reinterpret_cast(&data)),); DISTRHO_SAFE_ASSERT_RETURN(CFGetTypeID(data) == CFDictionaryGetTypeID(),); + #if DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE + // Tracks which parameters this call actually assigned a value to, + // whether via a loaded program or an explicit "params" entry, so that + // anything left untouched can be backfilled with its coded default + // below instead of silently keeping whatever value the plugin + // instance happened to have before this call. + // std::vector is NOT usable here since its bit-packed + // specialization can't hand back a real bool* for loadProgram()'s + // out-parameter. + const std::unique_ptr paramsSet(new bool[fParameterCount]()); + #endif + #if DISTRHO_PLUGIN_WANT_PROGRAMS CFNumberRef programRef = nullptr; if (CFDictionaryGetValueIfPresent(data, CFSTR("program"), reinterpret_cast(&programRef)) @@ -2662,7 +2675,11 @@ class PluginAU if (program >= 0) { fLastFactoryProgram = program; + #if DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE + fPlugin.loadProgram(fLastFactoryProgram, paramsSet.get()); + #else fPlugin.loadProgram(fLastFactoryProgram); + #endif notifyPropertyListeners('DPFo', kAudioUnitScope_Global, 0); } @@ -2778,6 +2795,9 @@ class PluginAU fLastParameterValues[j] = value; fPlugin.setParameterValue(j, value); + #if DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE + paramsSet[j] = true; + #endif notifyPropertyListeners('DPFp', kAudioUnitScope_Global, j); if (fBypassParameterIndex == j) @@ -2789,6 +2809,32 @@ class PluginAU std::free(symbol); } + + #if DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE + // Anything not explicitly touched above -- whether because it was + // never mentioned by the incoming preset at all, because a "params" + // entry was malformed or named an unknown symbol, or (when + // DISTRHO_PLUGIN_WANT_PROGRAMS is also enabled) because the requested + // program did not assign it -- is reset to its coded default here, + // rather than being left at whatever value this plugin instance had + // before this call. Each parameter is still only ever assigned once + // per call: this loop is disjoint from both the program-load and the + // explicit "params" assignments above. + for (uint32_t j=0; j +#include #include #include "clap/entry.h" @@ -1567,6 +1568,20 @@ class PluginCLAP : ClapEventQueue bool fillingKey = true; // if filling key or value char queryingType = 'i'; // can be 'n', 's' or 'p' (none, states, parameters) + #if DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE + // Tracks which parameters this call actually assigned a value to, + // whether via a loaded program or an explicit "__dpf_parameters_begin__" + // entry, so that anything left untouched can be backfilled with its + // coded default below instead of silently keeping whatever value the + // plugin instance happened to have before this call. + // A plain new[]/delete[] pair would leak on the early-return paths + // below (several of which are inside the read loop), so this needs + // to be RAII; std::vector is NOT usable here since its + // bit-packed specialization can't hand back a real bool* for + // loadProgram()'s out-parameter. + const std::unique_ptr paramsSet(new bool[fCachedParameters.numParams]()); + #endif + char buffer[512], orig; buffer[sizeof(buffer)-1] = '\xff'; @@ -1673,7 +1688,11 @@ class PluginCLAP : ClapEventQueue DISTRHO_SAFE_ASSERT_CONTINUE(program >= 0); fCurrentProgram = static_cast(program); + #if DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE + fPlugin.loadProgram(fCurrentProgram, paramsSet.get()); + #else fPlugin.loadProgram(fCurrentProgram); + #endif #if DISTRHO_PLUGIN_HAS_UI if (ui != nullptr) @@ -1730,6 +1749,9 @@ class PluginCLAP : ClapEventQueue } #endif fPlugin.setParameterValue(j, fvalue); + #if DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE + paramsSet[j] = true; + #endif break; } } @@ -1741,6 +1763,34 @@ class PluginCLAP : ClapEventQueue } } + #if DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE + // Anything not explicitly touched above -- whether because it was + // never mentioned by the incoming state at all, or (when + // DISTRHO_PLUGIN_WANT_PROGRAMS is also enabled) because a requested + // program index turned out to be invalid and fPlugin.loadProgram() + // silently no-op'd -- is reset to its coded default here, rather + // than being left at whatever value this plugin instance happened + // to have before this call. This is what makes a (possibly + // partial) state restore deterministic regardless of the plugin's + // prior runtime state. Each parameter is still only ever assigned + // once per call: this loop is disjoint from both the program-load + // and explicit-parameter-list assignments above. + for (uint32_t j = 0; j < fCachedParameters.numParams; ++j) + { + if (paramsSet[j] || fPlugin.isParameterOutputOrTrigger(j)) + continue; + + const float fvalue = fPlugin.getParameterRanges(j).def; + + fCachedParameters.values[j] = fvalue; + #if DISTRHO_PLUGIN_HAS_UI + if (ui != nullptr) + fCachedParameters.changed[j] = true; + #endif + fPlugin.setParameterValue(j, fvalue); + } + #endif // DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE + if (fHostExtensions.params != nullptr) fHostExtensions.params->rescan(fHost, CLAP_PARAM_RESCAN_VALUES|CLAP_PARAM_RESCAN_TEXT); diff --git a/distrho/src/DistrhoPluginChecks.h b/distrho/src/DistrhoPluginChecks.h index 52dc9d765..70a250a66 100644 --- a/distrho/src/DistrhoPluginChecks.h +++ b/distrho/src/DistrhoPluginChecks.h @@ -81,6 +81,10 @@ # define DISTRHO_PLUGIN_WANT_PROGRAMS 0 #endif +#ifndef DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE +# define DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE 0 +#endif + #ifndef DISTRHO_PLUGIN_WANT_STATE # define DISTRHO_PLUGIN_WANT_STATE 0 #endif diff --git a/distrho/src/DistrhoPluginInternal.hpp b/distrho/src/DistrhoPluginInternal.hpp index d0bef80a4..212e5cb9a 100644 --- a/distrho/src/DistrhoPluginInternal.hpp +++ b/distrho/src/DistrhoPluginInternal.hpp @@ -841,12 +841,20 @@ class PluginExporter return fData->programNames[index]; } - void loadProgram(const uint32_t index) + void loadProgram(const uint32_t index + #if DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE + , bool* const parametersSet = nullptr + #endif + ) { DISTRHO_SAFE_ASSERT_RETURN(fPlugin != nullptr,); DISTRHO_SAFE_ASSERT_RETURN(fData != nullptr && index < fData->programCount,); + #if DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE + fPlugin->loadProgram(index, parametersSet); + #else fPlugin->loadProgram(index); + #endif } #endif diff --git a/distrho/src/DistrhoPluginVST3.cpp b/distrho/src/DistrhoPluginVST3.cpp index 7295c8923..c3b205943 100644 --- a/distrho/src/DistrhoPluginVST3.cpp +++ b/distrho/src/DistrhoPluginVST3.cpp @@ -55,6 +55,7 @@ #include "travesty/host.h" #include +#include #include #include @@ -954,6 +955,20 @@ class PluginVst3 bool fillingKey = true; // if filling key or value char queryingType = 'i'; // can be 'n', 's' or 'p' (none, states, parameters) + #if DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE + // Tracks which parameters this call actually assigned a value to, + // whether via a loaded program or an explicit "__dpf_parameters_begin__" + // entry, so that anything left untouched can be backfilled with its + // coded default below instead of silently keeping whatever value the + // plugin instance happened to have before this call. + // A plain new[]/delete[] pair would leak on the early-return paths + // below (several of which are inside the read loop), so this needs + // to be RAII; std::vector is NOT usable here since its + // bit-packed specialization can't hand back a real bool* for + // loadProgram()'s out-parameter. + const std::unique_ptr paramsSet(new bool[fParameterCount]()); + #endif + char buffer[512], orig; buffer[sizeof(buffer)-1] = '\xff'; v3_result res; @@ -1063,7 +1078,11 @@ class PluginVst3 DISTRHO_SAFE_ASSERT_CONTINUE(program >= 0); fCurrentProgram = static_cast(program); + #if DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE + fPlugin.loadProgram(fCurrentProgram, paramsSet.get()); + #else fPlugin.loadProgram(fCurrentProgram); + #endif #if DISTRHO_PLUGIN_HAS_UI if (connectedToUI) @@ -1135,6 +1154,9 @@ class PluginVst3 } #endif fPlugin.setParameterValue(j, fvalue); + #if DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE + paramsSet[j] = true; + #endif break; } } @@ -1146,6 +1168,50 @@ class PluginVst3 } } + #if DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE + // Anything not explicitly touched above -- whether because it was + // never mentioned by the incoming state at all, or (when + // DISTRHO_PLUGIN_WANT_PROGRAMS is also enabled) because a requested + // program index turned out to be invalid and fPlugin.loadProgram() + // silently no-op'd -- is reset to its coded default here, rather + // than being left at whatever value this plugin instance happened + // to have before this call. This is what makes a (possibly + // partial) state restore deterministic regardless of the plugin's + // prior runtime state. Each parameter is still only ever assigned + // once per call: this loop is disjoint from both the program-load + // and explicit-parameter-list assignments above. + for (uint32_t j = 0; j < fParameterCount; ++j) + { + if (paramsSet[j] || fPlugin.isParameterOutputOrTrigger(j)) + continue; + + const float fvalue = fPlugin.getParameterRanges(j).def; + + fCachedParameterValues[kVst3InternalParameterBaseCount + j] = fvalue; + + #if DPF_VST3_USES_SEPARATE_CONTROLLER + // If this is the component make sure the controller also knows about the state change + if (fIsComponent) + { + componentValuesChanged = true; + fParameterValuesChangedDuringProcessing[kVst3InternalParameterBaseCount + j] = true; + } + #else + componentValuesChanged = true; + #endif + + #if DISTRHO_PLUGIN_HAS_UI + if (connectedToUI) + { + // UI parameter updates are handled outside the read loop (after host param restart) + fParameterValueChangesForUI[kVst3InternalParameterBaseCount + j] = true; + } + #endif + + fPlugin.setParameterValue(j, fvalue); + } + #endif // DISTRHO_PLUGIN_WANT_STRICT_PARAMETER_RESTORE + if (fComponentHandler != nullptr && componentValuesChanged) v3_cpp_obj(fComponentHandler)->restart_component(fComponentHandler, V3_RESTART_PARAM_VALUES_CHANGED);