commit 1960e35b6af1c79027319619414e60998028f418
parent e7dd8b5f16394edba278f9c20aa233a312ec73c6
Author: Steven Atkinson <steven@atkinson.mn>
Date: Fri, 22 Nov 2024 22:58:21 -0800
Backward-compatible unserialization to v0.7.3 (#532)
* Start refactor of legacy unserialization.
* Fix bugs
* Fix bugs, v0.7.3 loads correctly.
* Fix more bugs, current version loads correctly.
* Update pull_request_template.md
* Improve documentation and trim out unneeded includes due to hacky include pattern
Diffstat:
4 files changed, 310 insertions(+), 103 deletions(-)
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
@@ -10,6 +10,7 @@ Include [Closing words](https://docs.github.com/en/issues/tracking-your-work-wit
- [ ] Does the VST3 plugin pass all of the unit tests in the [VST3PluginTestHost](https://steinbergmedia.github.io/vst3_dev_portal/pages/What+is+the+VST+3+SDK/Plug-in+Test+Host.html)? (Download it as part of the VST3 SDK [here](https://www.steinberg.net/developers/).)
- [ ] Windows
- [ ] macOS
-- [ ] Does your PR add, remove, or rename any plugin parameters?
- - [ ] If yes, then have you ensured that older versions of the plug-in load correctly? (Usually, this means writing a new legacy unserialization function like [`_UnserializeStateLegacy_0_7_9`](https://github.com/sdatkinson/NeuralAmpModelerPlugin/blob/f755918e3f325f28658700ca954f8a47ec58d021/NeuralAmpModeler/NeuralAmpModeler.cpp#L823).)
+- [ ] Does your PR add, remove, or rename any plugin parameters? If yes...
+ - [ ] Have you ensured that the plug-in unserializes correctly?
+ - [ ] Have you ensured that _older_ versions of the plug-in load correctly? (See [`Unserialization.cpp`](https://github.com/sdatkinson/NeuralAmpModelerPlugin/blob/main/NeuralAmpModeler/Unserialization.cpp).)
diff --git a/NeuralAmpModeler/NeuralAmpModeler.cpp b/NeuralAmpModeler/NeuralAmpModeler.cpp
@@ -69,6 +69,11 @@ EMsgBoxResult _ShowMessageBox(iplug::igraphics::IGraphics* pGraphics, const char
#endif
}
+const std::string kCalibrateInputParamName = "CalibrateInput";
+const bool kDefaultCalibrateInput = false;
+const std::string kInputCalibrationLevelParamName = "InputCalibrationLevel";
+const double kDefaultInputCalibrationLevel = 12.0;
+
NeuralAmpModeler::NeuralAmpModeler(const InstanceInfo& info)
: Plugin(info, MakeConfig(kNumParams, kNumPresets))
@@ -85,8 +90,9 @@ NeuralAmpModeler::NeuralAmpModeler(const InstanceInfo& info)
GetParam(kEQActive)->InitBool("ToneStack", true);
GetParam(kOutputMode)->InitEnum("OutputMode", 1, {"Raw", "Normalized", "Calibrated"}); // TODO DRY w/ control
GetParam(kIRToggle)->InitBool("IRToggle", true);
- GetParam(kCalibrateInput)->InitBool("CalibrateInput", false);
- GetParam(kInputCalibrationLevel)->InitDouble("InputCalibrationLevel", 12.5, -30.0, 30.0, 0.1, "dBu");
+ GetParam(kCalibrateInput)->InitBool(kCalibrateInputParamName.c_str(), kDefaultCalibrateInput);
+ GetParam(kInputCalibrationLevel)
+ ->InitDouble(kInputCalibrationLevelParamName.c_str(), kDefaultInputCalibrationLevel, -60.0, 60.0, 0.1, "dBu");
mNoiseGateTrigger.AddListener(&mNoiseGateGain);
@@ -413,29 +419,20 @@ bool NeuralAmpModeler::SerializeState(IByteChunk& chunk) const
int NeuralAmpModeler::UnserializeState(const IByteChunk& chunk, int startPos)
{
+ // Look for the expected header. If it's there, then we'll know what to do.
WDL_String header;
int pos = startPos;
pos = chunk.GetStr(header, pos);
- // Unseralization:
+
+ const char* kExpectedHeader = "###NeuralAmpModeler###";
+ if (strcmp(header.Get(), kExpectedHeader) == 0)
{
- // Handle legacy plugin serialized states:
- // In v0.7.9, this was the NAM filepath. So, if we dont' get the expected header, then we can attempt to unserialize
- // as v0.7.9:
- const char* kExpectedHeader = "###NeuralAmpModeler###";
- if (strcmp(header.Get(), kExpectedHeader) == 0)
- {
- pos = _UnserializeStateCurrent(chunk, pos);
- }
- else
- {
- pos = _UnserializeStateLegacy_0_7_9(chunk, startPos);
- }
+ return _UnserializeStateWithKnownVersion(chunk, pos);
+ }
+ else
+ {
+ return _UnserializeStateWithUnknownVersion(chunk, startPos);
}
- if (mNAMPath.GetLength())
- _StageModel(mNAMPath);
- if (mIRPath.GetLength())
- _StageIR(mIRPath);
- return pos;
}
void NeuralAmpModeler::OnUIOpen()
@@ -857,81 +854,6 @@ void NeuralAmpModeler::_ProcessOutput(iplug::sample** inputs, iplug::sample** ou
#endif
}
-int NeuralAmpModeler::_UnserializeStateCurrent(const IByteChunk& chunk, int pos)
-{
- WDL_String version;
- pos = chunk.GetStr(version, pos);
- // Post-v0.7.9 legacy loading here once needed:
- // ...
-
- // Current version loading:
- pos = chunk.GetStr(mNAMPath, pos);
- pos = chunk.GetStr(mIRPath, pos);
- pos = UnserializeParams(chunk, pos);
- return pos;
-}
-
-int NeuralAmpModeler::_UnserializeStateLegacy_0_7_9(const IByteChunk& chunk, int startPos)
-{
- WDL_String dir;
- int pos = startPos;
- pos = chunk.GetStr(mNAMPath, pos);
- pos = chunk.GetStr(mIRPath, pos);
- auto unserialize = [&](const IByteChunk& chunk, int startPos) {
- // cf IPluginBase::UnserializeParams(const IByteChunk& chunk, int startPos)
-
- // These are the parameter names, in the order that they were serialized in v0.7.9.
- std::vector<std::string> oldParamNames{
- "Input", "Gate", "Bass", "Middle", "Treble", "Output", "NoiseGateActive", "ToneStack", "OutNorm", "IRToggle"};
- // These are their current names.
- // IF YOU CHANGE THE NAMES OF THE PARAMETERS, THEN YOU NEED TO UPDATE THIS!
- std::unordered_map<std::string, std::string> newNames{{"Gate", "Threshold"}};
- auto getParamByOldName = [&, newNames](std::string& oldName) {
- std::string name = newNames.find(oldName) != newNames.end() ? newNames.at(oldName) : oldName;
- // Could use a map but eh
- for (int i = 0; i < kNumParams; i++)
- {
- IParam* param = GetParam(i);
- if (strcmp(param->GetName(), name.c_str()) == 0)
- {
- return param;
- }
- }
- return (IParam*)nullptr;
- };
- TRACE
- int pos = startPos;
- ENTER_PARAMS_MUTEX
- int i = 0;
- for (auto it = oldParamNames.begin(); it != oldParamNames.end(); ++it, i++)
- {
- // Here's the change: instead of assuming that we can iterate through the parameters, we look for the one that now
- // holds this info.
- // IParam* pParam = mParams.Get(i);
- IParam* pParam = getParamByOldName(*it);
-
- double v = 0.0;
- pos = chunk.Get(&v, pos);
- // It's possible that future versions will not have all of the params of previous versions. If that's the case,
- // then this is a null ptr and we skip it.
- if (pParam)
- {
- pParam->Set(v);
- Trace(TRACELOC, "%d %s %f", i, pParam->GetName(), pParam->Value());
- }
- else
- {
- Trace(TRACELOC, "%d NOT-FOUND", i);
- }
- }
- OnParamReset(kPresetRecall);
- LEAVE_PARAMS_MUTEX
- return pos;
- };
- pos = unserialize(chunk, pos);
- return pos;
-}
-
void NeuralAmpModeler::_UpdateControlsFromModel()
{
if (mModel == nullptr)
@@ -985,3 +907,6 @@ void NeuralAmpModeler::_UpdateMeters(sample** inputPointer, sample** outputPoint
mInputSender.ProcessBlock(inputPointer, (int)nFrames, kCtrlTagInputMeter, nChansHack);
mOutputSender.ProcessBlock(outputPointer, (int)nFrames, kCtrlTagOutputMeter, nChansHack);
}
+
+// HACK
+#include "Unserialization.cpp"
diff --git a/NeuralAmpModeler/NeuralAmpModeler.h b/NeuralAmpModeler/NeuralAmpModeler.h
@@ -7,6 +7,7 @@
#include "AudioDSPTools/dsp/wav.h"
#include "AudioDSPTools/dsp/ResamplingContainer/ResamplingContainer.h"
+#include "Colors.h"
#include "ToneStack.h"
#include "IPlug_include_in_plug_hdr.h"
@@ -150,7 +151,7 @@ public:
int GetLatency() const { return NeedToResample() ? mResampler.GetLatency() : 0; };
- void Reset(const double sampleRate, const int maxBlockSize)
+ void Reset(const double sampleRate, const int maxBlockSize) override
{
mExpectedSampleRate = sampleRate;
mMaxExternalBlockSize = maxBlockSize;
@@ -244,11 +245,12 @@ private:
void _SetInputGain();
void _SetOutputGain();
- // Unserialize current-version plug-in data:
- int _UnserializeStateCurrent(const iplug::IByteChunk& chunk, int startPos);
- // Unserialize v0.7.9 legacy data:
- int _UnserializeStateLegacy_0_7_9(const iplug::IByteChunk& chunk, int startPos);
- // And other legacy unsrializations if/as needed...
+ // See: Unserialization.cpp
+ void _UnserializeApplyConfig(nlohmann::json& config);
+ // 0.7.9 and later
+ int _UnserializeStateWithKnownVersion(const iplug::IByteChunk& chunk, int startPos);
+ // Hopefully 0.7.3-0.7.8, but no gurantees
+ int _UnserializeStateWithUnknownVersion(const iplug::IByteChunk& chunk, int startPos);
// Update all controls that depend on a model
void _UpdateControlsFromModel();
diff --git a/NeuralAmpModeler/Unserialization.cpp b/NeuralAmpModeler/Unserialization.cpp
@@ -0,0 +1,279 @@
+// Unserialization
+//
+// This plugin is used in important places, so we need to be considerate when
+// attempting to unserialize. If the project was last saved with a legacy
+// version, then we need it to "update" to the current version is as
+// reasonable a way as possible.
+//
+// In order to handle older versions, the pattern is:
+// 1. Implement unserialization for every version into a version-specific
+// struct (Let's use our friend nlohmann::json. Why not?)
+// 2. Implement an "update" from each struct to the next one.
+// 3. Implement assigning the data contained in the current struct to the
+// current plugin configuration.
+//
+// This way, a constant amount of effort is required every time the
+// serialization changes instead of having to implement a current
+// unserialization for each past version.
+
+// Add new unserialization versions to the top, then add logic to the class method at the bottom.
+
+// Boilerplate
+
+void NeuralAmpModeler::_UnserializeApplyConfig(nlohmann::json& config)
+{
+ auto getParamByName = [&](std::string& name) {
+ // Could use a map but eh
+ for (int i = 0; i < kNumParams; i++)
+ {
+ iplug::IParam* param = GetParam(i);
+ if (strcmp(param->GetName(), name.c_str()) == 0)
+ {
+ return param;
+ }
+ }
+ // else
+ return (iplug::IParam*)nullptr;
+ };
+ TRACE
+ ENTER_PARAMS_MUTEX
+ for (auto it = config.begin(); it != config.end(); ++it)
+ {
+ std::string name = it.key();
+ iplug::IParam* pParam = getParamByName(name);
+ if (pParam != nullptr)
+ {
+ pParam->Set(*it);
+ iplug::Trace(TRACELOC, "%s %f", pParam->GetName(), pParam->Value());
+ }
+ else
+ {
+ iplug::Trace(TRACELOC, "%s NOT-FOUND", name.c_str());
+ }
+ }
+ OnParamReset(iplug::EParamSource::kPresetRecall);
+ LEAVE_PARAMS_MUTEX
+
+ mNAMPath.Set(static_cast<std::string>(config["NAMPath"]).c_str());
+ mIRPath.Set(static_cast<std::string>(config["IRPath"]).c_str());
+
+ if (mNAMPath.GetLength())
+ {
+ _StageModel(mNAMPath);
+ }
+ if (mIRPath.GetLength())
+ {
+ _StageIR(mIRPath);
+ }
+}
+
+// Unserialize NAM Path, IR path, then named keys
+int _UnserializePathsAndExpectedKeys(const iplug::IByteChunk& chunk, int startPos, nlohmann::json& config,
+ std::vector<std::string>& paramNames)
+{
+ int pos = startPos;
+ WDL_String path;
+ pos = chunk.GetStr(path, pos);
+ config["NAMPath"] = std::string(path.Get());
+ pos = chunk.GetStr(path, pos);
+ config["IRPath"] = std::string(path.Get());
+
+ for (auto it = paramNames.begin(); it != paramNames.end(); ++it)
+ {
+ double v = 0.0;
+ pos = chunk.Get(&v, pos);
+ config[*it] = v;
+ }
+ return pos;
+}
+
+void _RenameKeys(nlohmann::json& j, std::unordered_map<std::string, std::string> newNames)
+{
+ // Assumes no aliasing!
+ for (auto it = newNames.begin(); it != newNames.end(); ++it)
+ {
+ j[it->second] = j[it->first];
+ j.erase(it->first);
+ }
+}
+
+// v0.7.12
+
+void _UpdateConfigFrom_0_7_12(nlohmann::json& config)
+{
+ // Fill me in once something changes!
+}
+
+int _GetConfigFrom_0_7_12(const iplug::IByteChunk& chunk, int startPos, nlohmann::json& config)
+{
+ int pos = startPos;
+ std::vector<std::string> paramNames{"Input",
+ "Threshold",
+ "Bass",
+ "Middle",
+ "Treble",
+ "Output",
+ "NoiseGateActive",
+ "ToneStack",
+ "IRToggle",
+ "CalibrateInput",
+ "InputCalibrationLevel",
+ "OutputMode"};
+
+ pos = _UnserializePathsAndExpectedKeys(chunk, pos, config, paramNames);
+ // Then update:
+ _UpdateConfigFrom_0_7_12(config);
+ return pos;
+}
+
+// 0.7.10
+
+void _UpdateConfigFrom_0_7_10(nlohmann::json& config)
+{
+ // Note: "OutNorm" is Bool-like in v0.7.10, but "OutputMode" is enum.
+ // This works because 0 is "Raw" (cf OutNorm false) and 1 is "Calibrated" (cf OutNorm true).
+ std::unordered_map<std::string, std::string> newNames{{"OutNorm", "OutputMode"}};
+ _RenameKeys(config, newNames);
+ // There are new parameters. If they're not included, then 0.7.12 is ok, but future ones might not be.
+ config[kCalibrateInputParamName] = (double)kDefaultCalibrateInput;
+ config[kInputCalibrationLevelParamName] = kDefaultInputCalibrationLevel;
+ _UpdateConfigFrom_0_7_12(config);
+}
+
+int _GetConfigFrom_0_7_10(const iplug::IByteChunk& chunk, int startPos, nlohmann::json& config)
+{
+ std::vector<std::string> paramNames{
+ "Input", "Threshold", "Bass", "Middle", "Treble", "Output", "NoiseGateActive", "ToneStack", "OutNorm", "IRToggle"};
+ int pos = _UnserializePathsAndExpectedKeys(chunk, pos, config, paramNames);
+ // Then update:
+ _UpdateConfigFrom_0_7_10(config);
+ return pos;
+}
+
+// Earlier than 0.7.10 (Assumed to be 0.7.3-0.7.9)
+
+void _UpdateConfigFrom_Earlier(nlohmann::json& config)
+{
+ std::unordered_map<std::string, std::string> newNames{{"Gate", "Threshold"}};
+ _RenameKeys(config, newNames);
+ _UpdateConfigFrom_0_7_10(config);
+}
+
+int _GetConfigFrom_Earlier(const iplug::IByteChunk& chunk, int startPos, nlohmann::json& config)
+{
+ std::vector<std::string> paramNames{
+ "Input", "Gate", "Bass", "Middle", "Treble", "Output", "NoiseGateActive", "ToneStack", "OutNorm", "IRToggle"};
+
+ int pos = _UnserializePathsAndExpectedKeys(chunk, startPos, config, paramNames);
+ // Then update:
+ _UpdateConfigFrom_Earlier(config);
+ return pos;
+}
+
+//==============================================================================
+
+class _Version
+{
+public:
+ _Version(const int major, const int minor, const int patch)
+ : mMajor(major)
+ , mMinor(minor)
+ , mPatch(patch) {};
+ _Version(const std::string& versionStr)
+ {
+ std::istringstream stream(versionStr);
+ std::string token;
+ std::vector<int> parts;
+
+ // Split the string by "."
+ while (std::getline(stream, token, '.'))
+ {
+ parts.push_back(std::stoi(token)); // Convert to int and store
+ }
+
+ // Check if we have exactly 3 parts
+ if (parts.size() != 3)
+ {
+ throw std::invalid_argument("Input string does not contain exactly 3 segments separated by '.'");
+ }
+
+ // Assign the parts to the provided int variables
+ mMajor = parts[0];
+ mMinor = parts[1];
+ mPatch = parts[2];
+ };
+
+ bool operator>=(const _Version& other) const
+ {
+ // Compare on major version:
+ if (GetMajor() > other.GetMajor())
+ {
+ return true;
+ }
+ if (GetMajor() < other.GetMajor())
+ {
+ return false;
+ }
+ // Compare on minor
+ if (GetMinor() > other.GetMinor())
+ {
+ return true;
+ }
+ if (GetMinor() < other.GetMinor())
+ {
+ return false;
+ }
+ // Compare on patch
+ return GetPatch() >= other.GetPatch();
+ };
+
+ int GetMajor() const { return mMajor; };
+ int GetMinor() const { return mMinor; };
+ int GetPatch() const { return mPatch; };
+
+private:
+ int mMajor;
+ int mMinor;
+ int mPatch;
+};
+
+int NeuralAmpModeler::_UnserializeStateWithKnownVersion(const iplug::IByteChunk& chunk, int startPos)
+{
+ // We already got through the header before calling this.
+ int pos = startPos;
+
+ // Get the version
+ WDL_String wVersion;
+ pos = chunk.GetStr(wVersion, pos);
+ std::string versionStr(wVersion.Get());
+ _Version version(versionStr);
+ // Act accordingly
+ nlohmann::json config;
+ if (version >= _Version(0, 7, 12))
+ {
+ pos = _GetConfigFrom_0_7_12(chunk, pos, config);
+ }
+ else if (version >= _Version(0, 7, 10))
+ {
+ pos = _GetConfigFrom_0_7_10(chunk, pos, config);
+ }
+ else if (version >= _Version(0, 7, 9))
+ {
+ pos = _GetConfigFrom_Earlier(chunk, pos, config);
+ }
+ else
+ {
+ // You shouldn't be here...
+ assert(false);
+ }
+ _UnserializeApplyConfig(config);
+ return pos;
+}
+
+int NeuralAmpModeler::_UnserializeStateWithUnknownVersion(const iplug::IByteChunk& chunk, int startPos)
+{
+ nlohmann::json config;
+ int pos = _GetConfigFrom_Earlier(chunk, startPos, config);
+ _UnserializeApplyConfig(config);
+ return pos;
+}