From 548437c2b81f82a4933f45839eceabedc4a7d61e Mon Sep 17 00:00:00 2001 From: Jesus Avila Date: Tue, 28 Jul 2026 14:00:40 -0400 Subject: [PATCH 1/3] Add local preset system for EQ/volume/model/IR (issues #252, #428) Introduces PresetManager, a plugin-agnostic class that persists named presets (model path, IR path, EQ/volume params) as JSON under the plugin's app-support directory, with atomic writes and duplicate/corruption-safe error handling. Wires it into NeuralAmpModeler via a new "Presets" button: save the current state as a preset, load or delete an existing one from a popup menu. Saving under a name that already exists asks for confirmation before overwriting. Applying a preset that was saved without a model or IR unloads the current one, so loading a preset always reproduces the state it captured rather than leaving the previous file behind. A failed disk write rolls back the in-memory change so the preset list never diverges from what's actually persisted. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GUjbC9Vj8wD6UAkLW5QjG4 --- NeuralAmpModeler/NeuralAmpModeler.cpp | 199 ++++++++++++++++++++ NeuralAmpModeler/NeuralAmpModeler.h | 29 +++ NeuralAmpModeler/NeuralAmpModelerControls.h | 125 ++++++++++++ NeuralAmpModeler/PresetManager.cpp | 174 +++++++++++++++++ NeuralAmpModeler/PresetManager.h | 62 ++++++ 5 files changed, 589 insertions(+) create mode 100644 NeuralAmpModeler/PresetManager.cpp create mode 100644 NeuralAmpModeler/PresetManager.h diff --git a/NeuralAmpModeler/NeuralAmpModeler.cpp b/NeuralAmpModeler/NeuralAmpModeler.cpp index 4a08266ac..c3a7684ff 100644 --- a/NeuralAmpModeler/NeuralAmpModeler.cpp +++ b/NeuralAmpModeler/NeuralAmpModeler.cpp @@ -2,6 +2,8 @@ #include // pow #include #include +#include +#include #include #include "Colors.h" @@ -75,10 +77,24 @@ const std::string kInputCalibrationLevelParamName = "InputCalibrationLevel"; const double kDefaultInputCalibrationLevel = 12.0; +namespace +{ +std::string GetPresetsBasePath() +{ + WDL_String appSupportPath; + AppSupportPath(appSupportPath, false); + return std::string(appSupportPath.Get()) + "/NeuralAmpModeler"; +} +} // namespace + NeuralAmpModeler::NeuralAmpModeler(const InstanceInfo& info) : Plugin(info, MakeConfig(kNumParams, kNumPresets)) +, mPresetManager(GetPresetsBasePath()) { _InitToneStack(); + // Silently proceeds with an empty in-memory preset list on failure (e.g. corrupt + // presets.json); the user finds out when Save()/Add() fail on next use. + mPresetManager.Load(); nam::activations::Activation::enable_fast_tanh(); GetParam(kInputLevel)->InitGain("Input", 0.0, -20.0, 20.0, 0.1); GetParam(kToneBass)->InitDouble("Bass", 5.0, 0.0, 10.0, 0.1); @@ -180,6 +196,8 @@ NeuralAmpModeler::NeuralAmpModeler(const InstanceInfo& info) // Misc Areas const auto settingsButtonArea = CornerButtonArea(b); + // Preset menu button (issue #252), top-left corner. + const auto presetButtonArea = mainArea.GetFromTLHC(56.0f, 18.0f).GetVShifted(16.0f).GetHShifted(15.0f); // Model loader button auto loadModelCompletionHandler = [&](const WDL_String& fileName, const WDL_String& path) { @@ -291,6 +309,13 @@ NeuralAmpModeler::NeuralAmpModeler(const InstanceInfo& info) }, gearSVG)); + // Presets (issue #252): click builds the menu from the current preset names and shows it. + // Black fill, no vector frame (DrawFrame(false) skips the border draw regardless of kFR). + const auto presetButtonStyle = style.WithColor(EVColor::kFG, COLOR_BLACK).WithDrawFrame(false); + auto showPresetMenuFunc = [&](IControl* pCaller) { pCaller->As()->ShowMenu(GetPresetNames()); }; + pGraphics->AttachControl(new NAMPresetControl(presetButtonArea, presetButtonStyle), kCtrlTagPresetControl) + ->SetAnimationEndActionFunction(showPresetMenuFunc); + pGraphics ->AttachControl(new NAMSettingsPageControl(b, backgroundBitmap, inputLevelBackgroundBitmap, switchHandleBitmap, crossSVG, style, radioButtonStyle), @@ -504,6 +529,10 @@ void NeuralAmpModeler::OnUIOpen() { _UpdateControlsFromModel(); } + + if (mCurrentPresetName.GetLength()) + SendControlMsgFromDelegate( + kCtrlTagPresetControl, kMsgTagPresetNameChanged, mCurrentPresetName.GetLength(), mCurrentPresetName.Get()); } void NeuralAmpModeler::OnParamChange(int paramIdx) @@ -572,6 +601,75 @@ bool NeuralAmpModeler::OnMessage(int msgTag, int ctrlTag, int dataSize, const vo return true; } + case kMsgTagLoadPreset: + { + const std::string name(reinterpret_cast(pData)); + const std::optional preset = mPresetManager.Get(name); + if (preset) + { + _ApplyPreset(*preset); + _SetCurrentPresetName(name); + } + else + _ShowMessageBox(GetUI(), ("Preset \"" + name + "\" not found.").c_str(), "Failed to load preset!", kMB_OK); + return true; + } + case kMsgTagSavePreset: + { + const std::string name(reinterpret_cast(pData)); + const nam_presets::PresetData preset = _CollectCurrentState(name); + // Set only when overwriting: the preset being replaced, kept so a failed disk + // write can put it back (see the rollback below). + std::optional replaced; + + if (mPresetManager.Add(preset) == nam_presets::PresetError::kDuplicateName) + { + const EMsgBoxResult result = _ShowMessageBox( + GetUI(), ("Overwrite preset \"" + name + "\"?").c_str(), "Preset already exists", kMB_YESNO); + if (result != kYES) + return true; + + replaced = mPresetManager.Get(name); + mPresetManager.Remove(name); + mPresetManager.Add(preset); // Can't fail: the name was just removed. + } + + if (mPresetManager.Save() != nam_presets::PresetError::kNone) + { + // Disk write failed: put the store back exactly as it was, so the preset list on + // screen never diverges from what's actually persisted. When overwriting, that + // means restoring the replaced preset, not just dropping the new one. + mPresetManager.Remove(name); + if (replaced) + mPresetManager.Add(*replaced); + _ShowMessageBox(GetUI(), "Failed to write presets file to disk.", "Failed to save preset!", kMB_OK); + } + else + _SetCurrentPresetName(name); + return true; + } + case kMsgTagDeletePreset: + { + const std::string name(reinterpret_cast(pData)); + const EMsgBoxResult result = + _ShowMessageBox(GetUI(), ("Delete preset \"" + name + "\"?").c_str(), "Delete preset?", kMB_YESNO); + if (result == kYES) + { + const std::optional removed = mPresetManager.Get(name); + mPresetManager.Remove(name); + if (mPresetManager.Save() != nam_presets::PresetError::kNone) + { + // Disk write failed: roll back the in-memory removal so the preset list on + // screen never claims a preset is gone when it's still on disk. + if (removed) + mPresetManager.Add(*removed); + _ShowMessageBox(GetUI(), "Failed to write presets file to disk.", "Failed to delete preset!", kMB_OK); + } + else if (name == mCurrentPresetName.Get()) + _SetCurrentPresetName(""); + } + return true; + } default: return false; } } @@ -742,6 +840,107 @@ void NeuralAmpModeler::_ApplySlimParamToLoadedNAMs() apply(mStagedModel.get()); } +namespace +{ +// Maps the EQ/volume params exposed by presets (issue #252) to the string keys +// stored in PresetData::paramValues. Local to this translation unit: PresetManager +// itself doesn't know about EParams. +const std::map kPresetParamNames = { + {kInputLevel, "InputLevel"}, + {kOutputLevel, "OutputLevel"}, + {kNoiseGateThreshold, "NoiseGateThreshold"}, + {kNoiseGateActive, "NoiseGateActive"}, + {kToneBass, "ToneBass"}, + {kToneMid, "ToneMid"}, + {kToneTreble, "ToneTreble"}, + {kEQActive, "EQActive"}, +}; +} // namespace + +nam_presets::PresetData NeuralAmpModeler::_CollectCurrentState(const std::string& presetName) const +{ + nam_presets::PresetData preset; + preset.name = presetName; + preset.modelPath = mNAMPath.Get(); + preset.irPath = mIRPath.Get(); + for (const auto& [paramIdx, paramName] : kPresetParamNames) + preset.paramValues[paramName] = GetParam(paramIdx)->Value(); + return preset; +} + +void NeuralAmpModeler::_ApplyPreset(const nam_presets::PresetData& preset) +{ + for (const auto& [paramIdx, paramName] : kPresetParamNames) + { + const auto it = preset.paramValues.find(paramName); + if (it == preset.paramValues.end()) + continue; + SetParameterValue(paramIdx, GetParam(paramIdx)->ToNormalized(it->second)); + } + // SetParameterValue() updates the DSP-side value and informs the host, but doesn't push + // the new values to the knobs on screen (that's only needed when params change from code, + // as opposed to the user dragging a knob, which is why nothing else in this plugin needed it). + SendCurrentParamValuesFromDelegate(); + + if (preset.modelPath.size()) + { + const WDL_String modelPath(preset.modelPath.c_str()); + const std::string msg = _StageModel(modelPath); + if (msg.size()) + { + std::stringstream ss; + ss << "Failed to load NAM model from preset. Message:\n\n" << msg; + _ShowMessageBox(GetUI(), ss.str().c_str(), "Failed to load model!", kMB_OK); + } + else if (GetUI() != nullptr) + { + SendControlMsgFromDelegate(kCtrlTagModelFileBrowser, kMsgTagLoadedModel, mNAMPath.GetLength(), mNAMPath.Get()); + } + } + else + { + // The preset was saved without a model, so it means "no model": unload whatever is + // loaded rather than leaving it behind. _ApplyDSPStaging picks the flag up on the + // audio thread, same as the browser's clear button does. + mShouldRemoveModel = true; + if (GetUI() != nullptr) + SendControlMsgFromDelegate(kCtrlTagModelFileBrowser, kMsgTagClearedFile, 0, nullptr); + } + + if (preset.irPath.size()) + { + const WDL_String irPath(preset.irPath.c_str()); + const dsp::wav::LoadReturnCode retCode = _StageIR(irPath); + if (retCode != dsp::wav::LoadReturnCode::SUCCESS) + { + std::stringstream message; + message << "Failed to load IR file " << preset.irPath << " from preset:\n"; + message << dsp::wav::GetMsgForLoadReturnCode(retCode); + _ShowMessageBox(GetUI(), message.str().c_str(), "Failed to load IR!", kMB_OK); + } + else if (GetUI() != nullptr) + { + SendControlMsgFromDelegate(kCtrlTagIRFileBrowser, kMsgTagLoadedIR, mIRPath.GetLength(), mIRPath.Get()); + } + } + else + { + // Likewise for the IR: an empty path in the preset means "no IR", not "leave it alone". + mShouldRemoveIR = true; + if (GetUI() != nullptr) + SendControlMsgFromDelegate(kCtrlTagIRFileBrowser, kMsgTagClearedFile, 0, nullptr); + } +} + +void NeuralAmpModeler::_SetCurrentPresetName(const std::string& name) +{ + mCurrentPresetName.Set(name.c_str()); + + if (GetUI() != nullptr) + SendControlMsgFromDelegate( + kCtrlTagPresetControl, kMsgTagPresetNameChanged, mCurrentPresetName.GetLength(), mCurrentPresetName.Get()); +} + std::string NeuralAmpModeler::_StageModel(const WDL_String& modelPath) { WDL_String previousNAMPath = mNAMPath; diff --git a/NeuralAmpModeler/NeuralAmpModeler.h b/NeuralAmpModeler/NeuralAmpModeler.h index f5dae839d..f237556b4 100644 --- a/NeuralAmpModeler/NeuralAmpModeler.h +++ b/NeuralAmpModeler/NeuralAmpModeler.h @@ -9,6 +9,7 @@ #include "../NeuralAmpModelerCore/NAM/slimmable.h" #include "Colors.h" +#include "PresetManager.h" #include "ToneStack.h" #include "IPlug_include_in_plug_hdr.h" @@ -65,6 +66,7 @@ enum ECtrlTags kCtrlTagSlimmableIcon, kCtrlTagSlimOverlayBackdrop, kCtrlTagSlimKnob, + kCtrlTagPresetControl, kNumCtrlTags }; @@ -74,10 +76,20 @@ enum EMsgTags kMsgTagClearModel = 0, kMsgTagClearIR, kMsgTagHighlightColor, + // Presets (issue #252): pData is a null-terminated preset name string. + kMsgTagLoadPreset, + kMsgTagSavePreset, + kMsgTagDeletePreset, // The following tags are from DSP -> UI kMsgTagLoadFailed, kMsgTagLoadedModel, kMsgTagLoadedIR, + // Presets: pData is the current preset name, or dataSize == 0 if none (reset to default label). + kMsgTagPresetNameChanged, + // Presets: a preset was applied that has no model/IR, so the DSP unloaded the current one + // and the file browser must reset to its default label. No payload: the control tag it's + // sent to says which browser is meant. + kMsgTagClearedFile, kNumMsgTags }; @@ -212,6 +224,9 @@ class NeuralAmpModeler final : public iplug::Plugin void OnParamChangeUI(int paramIdx, iplug::EParamSource source) override; bool OnMessage(int msgTag, int ctrlTag, int dataSize, const void* pData) override; + // Read-only: names of presets currently in the local preset store, for the UI to build its menu. + std::vector GetPresetNames() const { return mPresetManager.ListNames(); } + private: // Allocates mInputPointers and mOutputPointers void _AllocateIOPointers(const size_t nChans); @@ -257,6 +272,14 @@ class NeuralAmpModeler final : public iplug::Plugin void _SetOutputGain(); void _ApplySlimParamToLoadedNAMs(); + // Presets: build a PresetData snapshot from the current model/IR/EQ/volume state, + // or push a PresetData's model/IR/EQ/volume state into the plugin. + nam_presets::PresetData _CollectCurrentState(const std::string& presetName) const; + void _ApplyPreset(const nam_presets::PresetData& preset); + // Updates mCurrentPresetName and, if the UI is open, pushes it to the preset button's + // label. Pass an empty name to reset the button back to its default label. + void _SetCurrentPresetName(const std::string& name); + // See: Unserialization.cpp void _UnserializeApplyConfig(nlohmann::json& config); // 0.7.9 and later @@ -319,6 +342,12 @@ class NeuralAmpModeler final : public iplug::Plugin // Path to IR (.wav file) WDL_String mIRPath; + // Local preset store (issue #252 / #428). Lives outside host/DAW state. + nam_presets::PresetManager mPresetManager; + // Name of the preset currently loaded/saved, for display only. Session-scoped: + // not part of host-serialized state (Unserialization.cpp is untouched by design). + WDL_String mCurrentPresetName; + WDL_String mHighLightColor{PluginColors::NAM_THEMECOLOR.ToColorCode()}; std::unordered_map mNAMParams = {{"Input", 0.0}, {"Output", 0.0}}; diff --git a/NeuralAmpModeler/NeuralAmpModelerControls.h b/NeuralAmpModeler/NeuralAmpModelerControls.h index cd6e5b07b..2a0f9afdf 100644 --- a/NeuralAmpModeler/NeuralAmpModelerControls.h +++ b/NeuralAmpModeler/NeuralAmpModelerControls.h @@ -2,8 +2,11 @@ #include // std::round #include // FILE, fclose +#include // strlen #include // std::stringstream +#include #include // std::unordered_map +#include #include "IControls.h" #include "IPlugPaths.h" @@ -261,6 +264,122 @@ class NAMGetButtonControl : public NAMSquareButtonControl } }; +// Presets button (issue #252): click opens a menu to load an existing preset, or +// save the current model/IR/EQ/volume state as a new one. Message tags/handling +// live in NeuralAmpModeler::OnMessage; this control only builds/reads the menu. +class NAMPresetControl : public IVButtonControl +{ +public: + NAMPresetControl(const IRECT& bounds, const IVStyle& style) + : IVButtonControl(bounds, DefaultClickActionFunc, kDefaultLabel, style) + { + } + + // Called by the click handler with the current preset names, right before showing the menu. + void ShowMenu(std::vector presetNames) + { + mPresetNames = std::move(presetNames); + + // mMenu.Clear() destroys its previous Items, and an Item that owns a submenu + // (via std::unique_ptr) deletes it on destruction. So the delete + // submenu must be heap-allocated with `new` here, freshly, every time -- it must + // never point at a member/stack IPopupMenu, or that delete is undefined behavior. + mMenu.Clear(); + mDeleteMenuPtr = nullptr; + + mMenu.AddItem(new IPopupMenu::Item("Save current as preset...", IPopupMenu::Item::kNoFlags, kSaveTag)); + + if (mPresetNames.size()) + { + mMenu.AddSeparator(); + for (int i = 0; i < (int) mPresetNames.size(); i++) + mMenu.AddItem(new IPopupMenu::Item(mPresetNames[i].c_str(), IPopupMenu::Item::kNoFlags, i)); + + auto* pDeleteMenu = new IPopupMenu(); + for (int i = 0; i < (int) mPresetNames.size(); i++) + pDeleteMenu->AddItem(new IPopupMenu::Item(mPresetNames[i].c_str(), IPopupMenu::Item::kNoFlags, i)); + mMenu.AddItem("Delete preset", -1, pDeleteMenu); // mMenu's Item now owns pDeleteMenu + mDeleteMenuPtr = pDeleteMenu; + } + + GetUI()->CreatePopupMenu(*this, mMenu, GetRECT()); + } + + void OnPopupMenuSelection(IPopupMenu* pSelectedMenu, int valIdx) override + { + if (pSelectedMenu == nullptr) + return; + + IPopupMenu::Item* pItem = pSelectedMenu->GetChosenItem(); + if (pItem == nullptr) + return; + + const int tag = pItem->GetTag(); + + if (pSelectedMenu == mDeleteMenuPtr) + { + if (tag >= 0 && static_cast(tag) < mPresetNames.size()) + { + const std::string& name = mPresetNames[tag]; + GetDelegate()->SendArbitraryMsgFromUI(kMsgTagDeletePreset, kNoTag, (int) name.size() + 1, name.c_str()); + } + return; + } + + if (tag == kSaveTag) + { + GetUI()->CreateTextEntry(*this, mText, GetRECT(), ""); + } + else if (tag >= 0 && static_cast(tag) < mPresetNames.size()) + { + const std::string& name = mPresetNames[tag]; + GetDelegate()->SendArbitraryMsgFromUI(kMsgTagLoadPreset, kNoTag, (int) name.size() + 1, name.c_str()); + } + } + + void OnTextEntryCompletion(const char* str, int valIdx) override + { + if (str && str[0]) + GetDelegate()->SendArbitraryMsgFromUI(kMsgTagSavePreset, kNoTag, (int) strlen(str) + 1, str); + } + + void OnMsgFromDelegate(int msgTag, int dataSize, const void* pData) override + { + if (msgTag != kMsgTagPresetNameChanged) + return; + + if (dataSize > 0) + { + const char* name = reinterpret_cast(pData); + SetLabelStr(TruncatedForLabel(name).c_str()); + SetTooltip(name); // full name available on hover + } + else + { + SetLabelStr(kDefaultLabel); + SetTooltip(""); + } + } + +private: + static std::string TruncatedForLabel(const std::string& name) + { + static constexpr size_t kMaxChars = 8; + if (name.size() <= kMaxChars) + return name; + return name.substr(0, kMaxChars) + "..."; + } + + static constexpr int kSaveTag = -1; + static constexpr const char* kDefaultLabel = "Presets"; + + IPopupMenu mMenu; + // Non-owning: owned by mMenu's "Delete preset" Item (its unique_ptr). + // Only used to identify, in OnPopupMenuSelection, which menu the click came from. + IPopupMenu* mDeleteMenuPtr = nullptr; + std::vector mPresetNames; +}; + class NAMFileBrowserControl : public IDirBrowseControlBase { public: @@ -451,6 +570,12 @@ class NAMFileBrowserControl : public IDirBrowseControlBase SetBrowserState(NAMBrowserState::Loaded); } break; + case kMsgTagClearedFile: + // Same reset the clear button does locally, for when the DSP unloads the file on + // its own (applying a preset that has no model/IR). + mFileNameControl->SetLabelAndTooltip(mDefaultLabelStr.Get()); + SetBrowserState(NAMBrowserState::Empty); + break; default: break; } } diff --git a/NeuralAmpModeler/PresetManager.cpp b/NeuralAmpModeler/PresetManager.cpp new file mode 100644 index 000000000..bafd13c19 --- /dev/null +++ b/NeuralAmpModeler/PresetManager.cpp @@ -0,0 +1,174 @@ +#include "PresetManager.h" + +#include +#include +#include +#include + +#include "json.hpp" + +namespace nam_presets +{ + +namespace +{ +namespace fs = std::filesystem; + +nlohmann::json PresetToJson(const PresetData& preset) +{ + nlohmann::json j; + j["modelPath"] = preset.modelPath; + j["irPath"] = preset.irPath; + j["params"] = preset.paramValues; + return j; +} + +// false if the node isn't shaped as expected (that preset is skipped on load, doesn't abort the whole file). +bool PresetFromJson(const std::string& name, const nlohmann::json& j, PresetData& outPreset) +{ + if (!j.is_object()) + return false; + + outPreset.name = name; + outPreset.modelPath = j.value("modelPath", ""); + outPreset.irPath = j.value("irPath", ""); + outPreset.paramValues.clear(); + + if (j.contains("params") && j.at("params").is_object()) + { + for (auto it = j.at("params").begin(); it != j.at("params").end(); ++it) + { + if (it.value().is_number()) + outPreset.paramValues[it.key()] = it.value().get(); + } + } + return true; +} +} // namespace + +PresetManager::PresetManager(std::string basePath) +: mBasePath(std::move(basePath)) +, mPresetsFilePath(mBasePath + "/presets.json") +{ +} + +PresetError PresetManager::Initialize() +{ + std::error_code ec; + fs::create_directories(mBasePath, ec); + if (ec) + return PresetError::kDirCreateFailed; + + if (!fs::exists(mPresetsFilePath, ec)) + { + std::ofstream out(mPresetsFilePath, std::ios::trunc); + if (!out.is_open()) + return PresetError::kWriteFailed; + out << "{}"; + if (!out.good()) + return PresetError::kWriteFailed; + } + return PresetError::kNone; +} + +PresetError PresetManager::Load() +{ + const PresetError initErr = Initialize(); + if (initErr != PresetError::kNone) + return initErr; + + std::ifstream in(mPresetsFilePath); + if (!in.is_open()) + return PresetError::kFileNotFound; + + std::stringstream buffer; + buffer << in.rdbuf(); + + nlohmann::json root; + try + { + root = nlohmann::json::parse(buffer.str()); + } + catch (const nlohmann::json::exception&) + { + return PresetError::kParseError; + } + + if (!root.is_object()) + return PresetError::kParseError; + + std::map loaded; + for (auto it = root.begin(); it != root.end(); ++it) + { + PresetData preset; + if (PresetFromJson(it.key(), it.value(), preset)) + loaded[it.key()] = std::move(preset); + } + + mPresets = std::move(loaded); + return PresetError::kNone; +} + +PresetError PresetManager::Save() const +{ + nlohmann::json root = nlohmann::json::object(); + for (const auto& [name, preset] : mPresets) + root[name] = PresetToJson(preset); + + // Atomic write: write to a tmp file and rename it over the real file, + // so a crash mid-write never leaves presets.json corrupted. + const std::string tmpPath = mPresetsFilePath + ".tmp"; + { + std::ofstream out(tmpPath, std::ios::trunc); + if (!out.is_open()) + return PresetError::kWriteFailed; + out << root.dump(2); + if (!out.good()) + return PresetError::kWriteFailed; + } + + std::error_code ec; + fs::rename(tmpPath, mPresetsFilePath, ec); + if (ec) + return PresetError::kWriteFailed; + + return PresetError::kNone; +} + +PresetError PresetManager::Add(const PresetData& preset) +{ + if (mPresets.find(preset.name) != mPresets.end()) + return PresetError::kDuplicateName; + + mPresets[preset.name] = preset; + return PresetError::kNone; +} + +PresetError PresetManager::Remove(const std::string& name) +{ + const auto it = mPresets.find(name); + if (it == mPresets.end()) + return PresetError::kNotFound; + + mPresets.erase(it); + return PresetError::kNone; +} + +std::optional PresetManager::Get(const std::string& name) const +{ + const auto it = mPresets.find(name); + if (it == mPresets.end()) + return std::nullopt; + return it->second; +} + +std::vector PresetManager::ListNames() const +{ + std::vector names; + names.reserve(mPresets.size()); + for (const auto& [name, preset] : mPresets) + names.push_back(name); + return names; +} + +} // namespace nam_presets diff --git a/NeuralAmpModeler/PresetManager.h b/NeuralAmpModeler/PresetManager.h new file mode 100644 index 000000000..cf217fbc8 --- /dev/null +++ b/NeuralAmpModeler/PresetManager.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include +#include + +namespace nam_presets +{ + +// Raw preset data. Agnostic of the plugin's param enum: +// the caller (NeuralAmpModeler.cpp) maps kX <-> "paramName" when building/reading this. +struct PresetData +{ + std::string name; + std::string modelPath; // .nam file, absolute + std::string irPath; // .wav file, absolute, may be empty (no IR) + std::map paramValues; // "InputLevel" -> 0.5, etc. +}; + +enum class PresetError +{ + kNone, + kFileNotFound, + kParseError, // corrupt/invalid JSON + kDirCreateFailed, + kWriteFailed, + kDuplicateName, + kNotFound // preset requested by name doesn't exist +}; + +class PresetManager +{ +public: + // basePath: ~/Library/Application Support/NeuralAmpModeler (via AppSupportPath). + // Injected, not hardcoded, so it can be tested with a temp dir. + explicit PresetManager(std::string basePath); + + // Creates base directory + presets.json if they don't exist. Idempotent. + PresetError Initialize(); + + // Reads the whole store into memory (lazy, first call triggers Initialize()). + PresetError Load(); + + // Writes current in-memory state to disk (atomic write: tmp file + rename). + PresetError Save() const; + + // In-memory CRUD. Doesn't touch disk until the next Save(). + // Rejects if preset.name already exists (no overwrite) -> kDuplicateName. + // To replace an existing preset: Remove(name) + Add(preset). + PresetError Add(const PresetData& preset); + PresetError Remove(const std::string& name); + std::optional Get(const std::string& name) const; + std::vector ListNames() const; + +private: + std::string mBasePath; + std::string mPresetsFilePath; // mBasePath + "/presets.json" + std::map mPresets; +}; + +} // namespace nam_presets From f7e6f93244b5edd440006d41548b3a920c785a05 Mon Sep 17 00:00:00 2001 From: Jesus Avila Date: Tue, 28 Jul 2026 14:00:40 -0400 Subject: [PATCH 2/3] Build PresetManager.cpp in the macOS and Windows projects Adds PresetManager.cpp/.h to every macOS target and to the Windows app and VST3 projects. The Windows AAX project is intentionally left out: that build targets Pro Tools only and is not produced by CI. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GUjbC9Vj8wD6UAkLW5QjG4 --- .../projects/NeuralAmpModeler-app.vcxproj | 2 ++ .../NeuralAmpModeler-app.vcxproj.filters | 2 ++ .../project.pbxproj | 26 +++++++++++++++++++ .../projects/NeuralAmpModeler-vst3.vcxproj | 2 ++ .../NeuralAmpModeler-vst3.vcxproj.filters | 2 ++ 5 files changed, 34 insertions(+) diff --git a/NeuralAmpModeler/projects/NeuralAmpModeler-app.vcxproj b/NeuralAmpModeler/projects/NeuralAmpModeler-app.vcxproj index e016cc972..8806c6ef0 100644 --- a/NeuralAmpModeler/projects/NeuralAmpModeler-app.vcxproj +++ b/NeuralAmpModeler/projects/NeuralAmpModeler-app.vcxproj @@ -334,6 +334,7 @@ + @@ -398,6 +399,7 @@ + diff --git a/NeuralAmpModeler/projects/NeuralAmpModeler-app.vcxproj.filters b/NeuralAmpModeler/projects/NeuralAmpModeler-app.vcxproj.filters index f55ded252..821bdd164 100644 --- a/NeuralAmpModeler/projects/NeuralAmpModeler-app.vcxproj.filters +++ b/NeuralAmpModeler/projects/NeuralAmpModeler-app.vcxproj.filters @@ -2,6 +2,7 @@ + IGraphics @@ -117,6 +118,7 @@ + resources diff --git a/NeuralAmpModeler/projects/NeuralAmpModeler-macOS.xcodeproj/project.pbxproj b/NeuralAmpModeler/projects/NeuralAmpModeler-macOS.xcodeproj/project.pbxproj index e4cff679f..d998b21e2 100644 --- a/NeuralAmpModeler/projects/NeuralAmpModeler-macOS.xcodeproj/project.pbxproj +++ b/NeuralAmpModeler/projects/NeuralAmpModeler-macOS.xcodeproj/project.pbxproj @@ -472,6 +472,17 @@ B885CBC52304AE7300D73128 /* IPlugProcessor.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4F8F61A8202807B9003F2573 /* IPlugProcessor.cpp */; }; B8E22A0C220268C4007CBF4C /* IPlugVST3_ProcessorBase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B8E22A0A220268C4007CBF4C /* IPlugVST3_ProcessorBase.cpp */; }; B8E22A0D220268C4007CBF4C /* IPlugVST3_ProcessorBase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B8E22A0A220268C4007CBF4C /* IPlugVST3_ProcessorBase.cpp */; }; + C26CF4CE2FFD902F0069318D /* PresetManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C26CF4CD2FFD902F0069318D /* PresetManager.cpp */; }; + C26CF4CF2FFD902F0069318D /* PresetManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C26CF4CD2FFD902F0069318D /* PresetManager.cpp */; }; + C26CF4D02FFD902F0069318D /* PresetManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C26CF4CD2FFD902F0069318D /* PresetManager.cpp */; }; + C26CF4D12FFD902F0069318D /* PresetManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C26CF4CD2FFD902F0069318D /* PresetManager.cpp */; }; + C26CF4D22FFD902F0069318D /* PresetManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C26CF4CC2FFD902F0069318D /* PresetManager.h */; }; + C26CF4D32FFD902F0069318D /* PresetManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C26CF4CD2FFD902F0069318D /* PresetManager.cpp */; }; + C26CF4D42FFD902F0069318D /* PresetManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C26CF4CD2FFD902F0069318D /* PresetManager.cpp */; }; + C26CF4D52FFD902F0069318D /* PresetManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C26CF4CC2FFD902F0069318D /* PresetManager.h */; }; + C26CF4D62FFD902F0069318D /* PresetManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C26CF4CD2FFD902F0069318D /* PresetManager.cpp */; }; + C26CF4D72FFD902F0069318D /* PresetManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C26CF4CD2FFD902F0069318D /* PresetManager.cpp */; }; + C26CF4D82FFD902F0069318D /* PresetManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C26CF4CD2FFD902F0069318D /* PresetManager.cpp */; }; C2A2FAE112D0000120027AB66 /* wavenet/a2_fast.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C2A2FAE012D0000110027AB66 /* wavenet/a2_fast.cpp */; }; C2A2FAE122D0000120027AB66 /* wavenet/a2_fast.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C2A2FAE012D0000110027AB66 /* wavenet/a2_fast.cpp */; }; C2A2FAE132D0000120027AB66 /* wavenet/a2_fast.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C2A2FAE012D0000110027AB66 /* wavenet/a2_fast.cpp */; }; @@ -1044,6 +1055,8 @@ B8E22A0A220268C4007CBF4C /* IPlugVST3_ProcessorBase.cpp */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.cpp.cpp; name = IPlugVST3_ProcessorBase.cpp; path = ../../iPlug2/IPlug/VST3/IPlugVST3_ProcessorBase.cpp; sourceTree = ""; tabWidth = 2; }; B8E22A0B220268C4007CBF4C /* IPlugVST3_ProcessorBase.h */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.c.h; name = IPlugVST3_ProcessorBase.h; path = ../../iPlug2/IPlug/VST3/IPlugVST3_ProcessorBase.h; sourceTree = ""; tabWidth = 2; }; B8EA6B932203868500D23A86 /* IPlugVST3_Common.h */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.c.h; name = IPlugVST3_Common.h; path = ../../iPlug2/IPlug/VST3/IPlugVST3_Common.h; sourceTree = ""; tabWidth = 2; }; + C26CF4CC2FFD902F0069318D /* PresetManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ../PresetManager.h; sourceTree = ""; }; + C26CF4CD2FFD902F0069318D /* PresetManager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ../PresetManager.cpp; sourceTree = ""; }; C2A2FAE012D0000110027AB66 /* wavenet/a2_fast.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = wavenet/a2_fast.cpp; sourceTree = ""; }; C2A2FAE022D0000110027AB66 /* wavenet/a2_fast.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = wavenet/a2_fast.h; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1114,6 +1127,8 @@ 089C166AFE841209C02AAC07 /* IPlugExample */ = { isa = PBXGroup; children = ( + C26CF4CC2FFD902F0069318D /* PresetManager.h */, + C26CF4CD2FFD902F0069318D /* PresetManager.cpp */, 4F2FB1392A0047420027AB66 /* dsp */, 4F2FB1452A0047420027AB66 /* NAM */, AA355E2C295B688F0061AA3D /* Colors.h */, @@ -2018,6 +2033,7 @@ AAB7BBB72CC4B8C6000B8B6E /* get_dsp.h in Headers */, 4F2FB1902A0047430027AB66 /* version.h in Headers */, AA7C86002B439AC000B5FB3A /* ptrlist.h in Headers */, + C26CF4D52FFD902F0069318D /* PresetManager.h in Headers */, 4F4856852773C3B5005BCF8E /* IPlugAUViewController.h in Headers */, AA7C85FC2B439AC000B5FB3A /* _LanczosResampler.h in Headers */, 4F2FB17C2A0047430027AB66 /* ImpulseResponse.h in Headers */, @@ -2048,6 +2064,7 @@ 4F03A5D420A4621100EBDFFB /* IGraphicsUtilities.h in Headers */, AA7C85FF2B439AC000B5FB3A /* ptrlist.h in Headers */, 4F03A5AA20A4621100EBDFFB /* IGraphicsLiveEdit.h in Headers */, + C26CF4D22FFD902F0069318D /* PresetManager.h in Headers */, 4F6369EA20A466470022C370 /* IControl.h in Headers */, 4F03A5D520A4621100EBDFFB /* IGraphicsConstants.h in Headers */, 4FF3205820B2BFAB00269268 /* IPlugPaths.h in Headers */, @@ -2572,6 +2589,7 @@ 4F2FB1842A0047430027AB66 /* wav.cpp in Sources */, 4F2FB1C12A0047430027AB66 /* wavenet/model.cpp in Sources */, B1D2F7072D0000020027AB66 /* wavenet/slimmable.cpp in Sources */, + C26CF4D72FFD902F0069318D /* PresetManager.cpp in Sources */, C2A2FAE112D0000120027AB66 /* wavenet/a2_fast.cpp in Sources */, AAB7E0122CC4B8C6000B8B6E /* container.cpp in Sources */, 4F2FB1CF2A0047430027AB66 /* conv1d.cpp in Sources */, @@ -2625,6 +2643,7 @@ 4FD52136202A5BD000A4D22A /* dfx-au-utilities.c in Sources */, 4F7C4960255DDFC500DF7588 /* ITextEntryControl.cpp in Sources */, 4F5F344420C0226200487201 /* IPlugPaths.mm in Sources */, + C26CF4D02FFD902F0069318D /* PresetManager.cpp in Sources */, 4F2FB1752A0047430027AB66 /* ImpulseResponse.cpp in Sources */, 4F7C495E255DDFC500DF7588 /* IControls.cpp in Sources */, 4F35DEB0207E5C5A00867D8F /* IPlugPluginBase.cpp in Sources */, @@ -2679,6 +2698,7 @@ 4F3EE1DB231438D000004786 /* IPlugAPP_main.cpp in Sources */, 4F2FB1682A0047430027AB66 /* dsp.cpp in Sources */, 4F3EE1DD231438D000004786 /* IGraphicsMac.mm in Sources */, + C26CF4CE2FFD902F0069318D /* PresetManager.cpp in Sources */, 4F3EE1DE231438D000004786 /* NeuralAmpModeler.cpp in Sources */, 4F2FB1A42A0047430027AB66 /* convnet.cpp in Sources */, 4F3EE1E0231438D000004786 /* IPlugAPIBase.cpp in Sources */, @@ -2722,6 +2742,7 @@ 4F2FB1982A0047430027AB66 /* dsp.cpp in Sources */, AA341E252B9E5A530069C260 /* ToneStack.cpp in Sources */, 4F78BE2522E7406D00AD537E /* IPlugPluginBase.cpp in Sources */, + C26CF4D62FFD902F0069318D /* PresetManager.cpp in Sources */, 4F2FB1C22A0047430027AB66 /* wavenet/model.cpp in Sources */, B1D2F7082D0000020027AB66 /* wavenet/slimmable.cpp in Sources */, C2A2FAE142D0000120027AB66 /* wavenet/a2_fast.cpp in Sources */, @@ -2744,6 +2765,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C26CF4D42FFD902F0069318D /* PresetManager.cpp in Sources */, 4F81597C205D50EB00393585 /* baseiids.cpp in Sources */, 4FDAC0EC207D76C600299363 /* IPlugTimer.cpp in Sources */, 4F7C495D255DDFC400DF7588 /* ITextEntryControl.cpp in Sources */, @@ -2842,6 +2864,7 @@ 4F2FB1952A0047430027AB66 /* dsp.cpp in Sources */, AA341E212B9E5A530069C260 /* ToneStack.cpp in Sources */, 4FB600231567CB0A0020189A /* IPlugParameter.cpp in Sources */, + C26CF4D12FFD902F0069318D /* PresetManager.cpp in Sources */, 4F2FB1BF2A0047430027AB66 /* wavenet/model.cpp in Sources */, B1D2F7052D0000020027AB66 /* wavenet/slimmable.cpp in Sources */, C2A2FAE162D0000120027AB66 /* wavenet/a2_fast.cpp in Sources */, @@ -2864,6 +2887,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C26CF4D32FFD902F0069318D /* PresetManager.cpp in Sources */, 4F7C4964255DDFC800DF7588 /* IControls.cpp in Sources */, 4F03A5B220A4621100EBDFFB /* IGraphics.cpp in Sources */, 4F7C4966255DDFC800DF7588 /* ITextEntryControl.cpp in Sources */, @@ -2927,6 +2951,7 @@ 4F690C9B203A345100A4A13E /* IPlugAPP_main.cpp in Sources */, 4F2FB1602A0047430027AB66 /* dsp.cpp in Sources */, 4FB1F58920E4B004004157C8 /* IGraphicsMac.mm in Sources */, + C26CF4D82FFD902F0069318D /* PresetManager.cpp in Sources */, 4F3862EF2014BBEC0009F402 /* NeuralAmpModeler.cpp in Sources */, 4F2FB19C2A0047430027AB66 /* convnet.cpp in Sources */, 4F78D90B13B63BA50032E0F3 /* IPlugAPIBase.cpp in Sources */, @@ -2956,6 +2981,7 @@ 4FFBB90820863B0E00DDD0E7 /* pluginfactory.cpp in Sources */, 4FFBB90920863B0E00DDD0E7 /* vstinitiids.cpp in Sources */, 4FFBB90A20863B0E00DDD0E7 /* fdebug.cpp in Sources */, + C26CF4CF2FFD902F0069318D /* PresetManager.cpp in Sources */, 4FFBB90C20863B0E00DDD0E7 /* IPlugAPIBase.cpp in Sources */, B8E22A0D220268C4007CBF4C /* IPlugVST3_ProcessorBase.cpp in Sources */, 4FFBB90D20863B0E00DDD0E7 /* fdynlib.cpp in Sources */, diff --git a/NeuralAmpModeler/projects/NeuralAmpModeler-vst3.vcxproj b/NeuralAmpModeler/projects/NeuralAmpModeler-vst3.vcxproj index ec59513a1..8009af551 100644 --- a/NeuralAmpModeler/projects/NeuralAmpModeler-vst3.vcxproj +++ b/NeuralAmpModeler/projects/NeuralAmpModeler-vst3.vcxproj @@ -343,6 +343,7 @@ + @@ -426,6 +427,7 @@ + diff --git a/NeuralAmpModeler/projects/NeuralAmpModeler-vst3.vcxproj.filters b/NeuralAmpModeler/projects/NeuralAmpModeler-vst3.vcxproj.filters index 4f7ecfbc6..d9ee9751b 100644 --- a/NeuralAmpModeler/projects/NeuralAmpModeler-vst3.vcxproj.filters +++ b/NeuralAmpModeler/projects/NeuralAmpModeler-vst3.vcxproj.filters @@ -2,6 +2,7 @@ + IGraphics @@ -175,6 +176,7 @@ + resources From c72d4fa689a1495f681053c4bd1eee4e55a06bc8 Mon Sep 17 00:00:00 2001 From: Jesus Avila Date: Sat, 8 Aug 2026 18:13:10 -0400 Subject: [PATCH 3/3] Apply clang-format to preset manager code Fixes formatting violations per CONTRIBUTING.md: C-style cast spacing, message box call wrapping, and preset param map layout. Co-Authored-By: Claude Sonnet 5 --- NeuralAmpModeler/NeuralAmpModeler.cpp | 15 +++++---------- NeuralAmpModeler/NeuralAmpModelerControls.h | 10 +++++----- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/NeuralAmpModeler/NeuralAmpModeler.cpp b/NeuralAmpModeler/NeuralAmpModeler.cpp index c3a7684ff..1ebc51c27 100644 --- a/NeuralAmpModeler/NeuralAmpModeler.cpp +++ b/NeuralAmpModeler/NeuralAmpModeler.cpp @@ -624,8 +624,8 @@ bool NeuralAmpModeler::OnMessage(int msgTag, int ctrlTag, int dataSize, const vo if (mPresetManager.Add(preset) == nam_presets::PresetError::kDuplicateName) { - const EMsgBoxResult result = _ShowMessageBox( - GetUI(), ("Overwrite preset \"" + name + "\"?").c_str(), "Preset already exists", kMB_YESNO); + const EMsgBoxResult result = + _ShowMessageBox(GetUI(), ("Overwrite preset \"" + name + "\"?").c_str(), "Preset already exists", kMB_YESNO); if (result != kYES) return true; @@ -846,14 +846,9 @@ namespace // stored in PresetData::paramValues. Local to this translation unit: PresetManager // itself doesn't know about EParams. const std::map kPresetParamNames = { - {kInputLevel, "InputLevel"}, - {kOutputLevel, "OutputLevel"}, - {kNoiseGateThreshold, "NoiseGateThreshold"}, - {kNoiseGateActive, "NoiseGateActive"}, - {kToneBass, "ToneBass"}, - {kToneMid, "ToneMid"}, - {kToneTreble, "ToneTreble"}, - {kEQActive, "EQActive"}, + {kInputLevel, "InputLevel"}, {kOutputLevel, "OutputLevel"}, {kNoiseGateThreshold, "NoiseGateThreshold"}, + {kNoiseGateActive, "NoiseGateActive"}, {kToneBass, "ToneBass"}, {kToneMid, "ToneMid"}, + {kToneTreble, "ToneTreble"}, {kEQActive, "EQActive"}, }; } // namespace diff --git a/NeuralAmpModeler/NeuralAmpModelerControls.h b/NeuralAmpModeler/NeuralAmpModelerControls.h index 2a0f9afdf..79b08f2f2 100644 --- a/NeuralAmpModeler/NeuralAmpModelerControls.h +++ b/NeuralAmpModeler/NeuralAmpModelerControls.h @@ -292,11 +292,11 @@ class NAMPresetControl : public IVButtonControl if (mPresetNames.size()) { mMenu.AddSeparator(); - for (int i = 0; i < (int) mPresetNames.size(); i++) + for (int i = 0; i < (int)mPresetNames.size(); i++) mMenu.AddItem(new IPopupMenu::Item(mPresetNames[i].c_str(), IPopupMenu::Item::kNoFlags, i)); auto* pDeleteMenu = new IPopupMenu(); - for (int i = 0; i < (int) mPresetNames.size(); i++) + for (int i = 0; i < (int)mPresetNames.size(); i++) pDeleteMenu->AddItem(new IPopupMenu::Item(mPresetNames[i].c_str(), IPopupMenu::Item::kNoFlags, i)); mMenu.AddItem("Delete preset", -1, pDeleteMenu); // mMenu's Item now owns pDeleteMenu mDeleteMenuPtr = pDeleteMenu; @@ -321,7 +321,7 @@ class NAMPresetControl : public IVButtonControl if (tag >= 0 && static_cast(tag) < mPresetNames.size()) { const std::string& name = mPresetNames[tag]; - GetDelegate()->SendArbitraryMsgFromUI(kMsgTagDeletePreset, kNoTag, (int) name.size() + 1, name.c_str()); + GetDelegate()->SendArbitraryMsgFromUI(kMsgTagDeletePreset, kNoTag, (int)name.size() + 1, name.c_str()); } return; } @@ -333,14 +333,14 @@ class NAMPresetControl : public IVButtonControl else if (tag >= 0 && static_cast(tag) < mPresetNames.size()) { const std::string& name = mPresetNames[tag]; - GetDelegate()->SendArbitraryMsgFromUI(kMsgTagLoadPreset, kNoTag, (int) name.size() + 1, name.c_str()); + GetDelegate()->SendArbitraryMsgFromUI(kMsgTagLoadPreset, kNoTag, (int)name.size() + 1, name.c_str()); } } void OnTextEntryCompletion(const char* str, int valIdx) override { if (str && str[0]) - GetDelegate()->SendArbitraryMsgFromUI(kMsgTagSavePreset, kNoTag, (int) strlen(str) + 1, str); + GetDelegate()->SendArbitraryMsgFromUI(kMsgTagSavePreset, kNoTag, (int)strlen(str) + 1, str); } void OnMsgFromDelegate(int msgTag, int dataSize, const void* pData) override