diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 2db3d266..922ed239 100644 --- 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 index 32889f48..490951cb 100644 --- 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 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 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 index 90a884b2..e83fa5a9 100644 --- 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 @@ class ResamplingNAM : public nam::DSP 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 @@ class NeuralAmpModeler final : public iplug::Plugin 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 new file mode 100644 index 00000000..2c9cf6b9 --- /dev/null +++ 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(config["NAMPath"]).c_str()); + mIRPath.Set(static_cast(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& 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 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 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 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 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 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 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 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; +}