From 2a61b2e0dff952e590863aeabb5b3e07de7e6a4f Mon Sep 17 00:00:00 2001 From: Steven Atkinson Date: Wed, 10 Jun 2026 12:56:03 -0700 Subject: [PATCH 1/6] Derive project version from header --- CMakeLists.txt | 16 ++++++++++++++-- README.md | 2 +- docs/conf.py | 22 ++++++++++++++++++++-- docs/wavenet_walkthrough.rst | 4 ++-- 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e9fee88..d21f36a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,19 @@ cmake_minimum_required(VERSION 3.10) -# Make sure this matches ./NAM/version.h! -project(NAM VERSION 0.4.0) +file(READ "${CMAKE_CURRENT_LIST_DIR}/NAM/version.h" _nam_version_header) +foreach(_component MAJOR MINOR PATCH) + string(REGEX MATCH + "#define[ \t]+NEURAL_AMP_MODELER_DSP_VERSION_${_component}[ \t]+([0-9]+)" + _nam_version_match "${_nam_version_header}" + ) + if(NOT _nam_version_match) + message(FATAL_ERROR "Could not parse NEURAL_AMP_MODELER_DSP_VERSION_${_component} from NAM/version.h") + endif() + set(_nam_version_${_component} "${CMAKE_MATCH_1}") +endforeach() + +set(_nam_project_version "${_nam_version_MAJOR}.${_nam_version_MINOR}.${_nam_version_PATCH}") +project(NAM VERSION "${_nam_project_version}") set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") diff --git a/README.md b/README.md index f94ea3f5..57ff429d 100644 --- a/README.md +++ b/README.md @@ -24,5 +24,5 @@ This library uses [Eigen](http://eigen.tuxfamily.org) to do the linear algebra r Tone3000 logo -Development of version 0.4.0 of this library has been generously supported by [TONE3000](https://tone3000.com). +Early development of this library was generously supported by [TONE3000](https://tone3000.com). **Thank you!** diff --git a/docs/conf.py b/docs/conf.py index 305152c8..28638cda 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,6 +4,7 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html import os +import re import sys from pathlib import Path @@ -19,8 +20,25 @@ project = 'NeuralAmpModelerCore' copyright = '2023-present Steven Atkinson' author = 'Neural Amp Modeler Contributors' -release = '0.4.0' -version = '0.4.0' + + +def _get_project_version(): + version_header = Path(__file__).resolve().parent.parent / 'NAM' / 'version.h' + contents = version_header.read_text(encoding='utf-8') + version_parts = {} + for component in ('MAJOR', 'MINOR', 'PATCH'): + match = re.search( + rf'#define\s+NEURAL_AMP_MODELER_DSP_VERSION_{component}\s+(\d+)', + contents, + ) + if match is None: + raise RuntimeError(f'Could not parse {component.lower()} version from {version_header}') + version_parts[component.lower()] = match.group(1) + return f"{version_parts['major']}.{version_parts['minor']}.{version_parts['patch']}" + + +release = _get_project_version() +version = release # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/wavenet_walkthrough.rst b/docs/wavenet_walkthrough.rst index 9dd90e63..c3cee7ed 100644 --- a/docs/wavenet_walkthrough.rst +++ b/docs/wavenet_walkthrough.rst @@ -27,8 +27,8 @@ Here's a rundown of what's not exactly the same at an informal level: gated activation. Here, the gated activation is optional (and is frequently not used, like in the popular A1 standard/lite/feather/nano configurations). -* In v0.4.0, even more modifications have been added in--FiLMs, a bottlneck, and an - arbitrary "conditioning DSP" module that can be used to embed the input signal in a more +* NAM adds further modifications: FiLMs, a bottleneck, and an arbitrary + "conditioning DSP" module that can be used to embed the input signal in a more effective way to modulate the layers in the main model. It doesn't need to be a WaveNet, but if it were then this feels more like a "cascading (stacked) WaveNet". From f7bf9af3c4536bbf6858ae12f2c09e8be1a72f92 Mon Sep 17 00:00:00 2001 From: Steven Atkinson Date: Wed, 10 Jun 2026 12:59:28 -0700 Subject: [PATCH 2/6] Restore README version reference --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 57ff429d..8e575962 100644 --- a/README.md +++ b/README.md @@ -24,5 +24,5 @@ This library uses [Eigen](http://eigen.tuxfamily.org) to do the linear algebra r Tone3000 logo -Early development of this library was generously supported by [TONE3000](https://tone3000.com). +Development of version 0.4.0 of this library has been generously supported by [TONE3000](https://tone3000.com). **Thank you!** From 4f72acd53d2885d6274e9fa66a334cac01ff2a10 Mon Sep 17 00:00:00 2001 From: Steven Atkinson Date: Wed, 10 Jun 2026 13:00:16 -0700 Subject: [PATCH 3/6] Restore WaveNet walkthrough version reference --- docs/wavenet_walkthrough.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/wavenet_walkthrough.rst b/docs/wavenet_walkthrough.rst index c3cee7ed..9dd90e63 100644 --- a/docs/wavenet_walkthrough.rst +++ b/docs/wavenet_walkthrough.rst @@ -27,8 +27,8 @@ Here's a rundown of what's not exactly the same at an informal level: gated activation. Here, the gated activation is optional (and is frequently not used, like in the popular A1 standard/lite/feather/nano configurations). -* NAM adds further modifications: FiLMs, a bottleneck, and an arbitrary - "conditioning DSP" module that can be used to embed the input signal in a more +* In v0.4.0, even more modifications have been added in--FiLMs, a bottlneck, and an + arbitrary "conditioning DSP" module that can be used to embed the input signal in a more effective way to modulate the layers in the main model. It doesn't need to be a WaveNet, but if it were then this feels more like a "cascading (stacked) WaveNet". From c136943a123ebe599f1e724f860f8eb01f5e4ebf Mon Sep 17 00:00:00 2001 From: Steven Atkinson Date: Wed, 10 Jun 2026 13:02:11 -0700 Subject: [PATCH 4/6] Restore README to main --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e575962..f94ea3f5 100644 --- a/README.md +++ b/README.md @@ -24,5 +24,5 @@ This library uses [Eigen](http://eigen.tuxfamily.org) to do the linear algebra r Tone3000 logo -Development of version 0.4.0 of this library has been generously supported by [TONE3000](https://tone3000.com). +Development of version 0.4.0 of this library has been generously supported by [TONE3000](https://tone3000.com). **Thank you!** From b0191082e3dd6c1b9beb0c6c1e4998d60a4cc727 Mon Sep 17 00:00:00 2001 From: Steven Atkinson Date: Wed, 10 Jun 2026 14:49:50 -0700 Subject: [PATCH 5/6] Add load option to suppress prewarm --- NAM/container.cpp | 7 +++ NAM/container.h | 1 + NAM/dsp.cpp | 32 ++++++++++++- NAM/dsp.h | 40 ++++++++++++---- NAM/get_dsp.cpp | 70 +++++++++++++++++---------- NAM/get_dsp.h | 27 +++++++++-- NAM/model_config.h | 2 +- NAM/wavenet/model.cpp | 7 +++ NAM/wavenet/model.h | 2 + NAM/wavenet/slimmable.cpp | 15 +++++- NAM/wavenet/slimmable.h | 1 + tools/run_tests.cpp | 6 +++ tools/test/test_container.cpp | 4 +- tools/test/test_dsp.cpp | 54 +++++++++++++++++++++ tools/test/test_get_dsp.cpp | 90 ++++++++++++++++++++++++++++++++++- 15 files changed, 310 insertions(+), 48 deletions(-) diff --git a/NAM/container.cpp b/NAM/container.cpp index ee7d9f16..2d25f044 100644 --- a/NAM/container.cpp +++ b/NAM/container.cpp @@ -61,6 +61,13 @@ void ContainerModel::prewarm() _submodels[active_index].model->prewarm(); } +void ContainerModel::SetPrewarmOnReset(const bool prewarmOnReset) +{ + DSP::SetPrewarmOnReset(prewarmOnReset); + for (auto& submodel : _submodels) + submodel.model->SetPrewarmOnReset(prewarmOnReset); +} + void ContainerModel::Reset(const double sampleRate, const int maxBufferSize) { std::lock_guard lock(_slim_set_mutex); diff --git a/NAM/container.h b/NAM/container.h index dccc9149..36aee5dc 100644 --- a/NAM/container.h +++ b/NAM/container.h @@ -37,6 +37,7 @@ class ContainerModel : public DSP, public SlimmableModel void process(NAM_SAMPLE** input, NAM_SAMPLE** output, const int num_frames) override; void prewarm() override; void Reset(const double sampleRate, const int maxBufferSize) override; + void SetPrewarmOnReset(const bool prewarmOnReset) override; void SetSlimmableSize(const double val) override; protected: diff --git a/NAM/dsp.cpp b/NAM/dsp.cpp index a4040c34..57d370fc 100644 --- a/NAM/dsp.cpp +++ b/NAM/dsp.cpp @@ -14,8 +14,27 @@ constexpr const long _INPUT_BUFFER_SAFETY_FACTOR = 32; +namespace +{ + +thread_local bool gPrewarmOnResetDefault = true; + +} // namespace + +nam::ScopedPrewarmOnResetDefault::ScopedPrewarmOnResetDefault(const bool prewarmOnReset) +: mPreviousPrewarmOnReset(gPrewarmOnResetDefault) +{ + gPrewarmOnResetDefault = prewarmOnReset; +} + +nam::ScopedPrewarmOnResetDefault::~ScopedPrewarmOnResetDefault() +{ + gPrewarmOnResetDefault = mPreviousPrewarmOnReset; +} + nam::DSP::DSP(const int in_channels, const int out_channels, const double expected_sample_rate) : mExpectedSampleRate(expected_sample_rate) +, mPrewarmOnReset(gPrewarmOnResetDefault) , mInChannels(in_channels) , mOutChannels(out_channels) { @@ -96,7 +115,18 @@ void nam::DSP::Reset(const double sampleRate, const int maxBufferSize) mHaveExternalSampleRate = true; SetMaxBufferSize(maxBufferSize); - prewarm(); + if (GetPrewarmOnReset()) + prewarm(); +} + +void nam::DSP::SetPrewarmOnReset(const bool prewarmOnReset) +{ + mPrewarmOnReset.store(prewarmOnReset, std::memory_order_release); +} + +bool nam::DSP::GetPrewarmOnReset() const +{ + return mPrewarmOnReset.load(std::memory_order_acquire); } void nam::DSP::SetLoudness(const double loudness) diff --git a/NAM/dsp.h b/NAM/dsp.h index c714a197..16b6fb0b 100644 --- a/NAM/dsp.h +++ b/NAM/dsp.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -39,6 +40,25 @@ namespace wavenet class WaveNet; } // namespace wavenet +/// \brief Temporarily change the thread-local prewarm-on-reset default for newly constructed DSP objects +/// +/// Existing DSP objects are not affected. DSP instances constructed while this object is alive on the current thread +/// copy the scoped default into their instance-level prewarm-on-reset setting. +class ScopedPrewarmOnResetDefault +{ +public: + explicit ScopedPrewarmOnResetDefault(const bool prewarmOnReset); + ~ScopedPrewarmOnResetDefault(); + + ScopedPrewarmOnResetDefault(const ScopedPrewarmOnResetDefault&) = delete; + ScopedPrewarmOnResetDefault& operator=(const ScopedPrewarmOnResetDefault&) = delete; + + bool PreviousPrewarmOnReset() const { return mPreviousPrewarmOnReset; } + +private: + bool mPreviousPrewarmOnReset; +}; + /// \brief Base class for all DSP models /// /// DSP provides the common interface for all neural network-based audio processing models. @@ -133,20 +153,19 @@ class DSP /// \brief General function for resetting the DSP unit /// - /// This doesn't call prewarm(). If you want to do that, then you might want to use ResetAndPrewarm(). - /// See https://github.com/sdatkinson/NeuralAmpModelerCore/issues/96 for the reasoning. + /// By default, this calls prewarm() after updating the sample rate and buffer size. Use SetPrewarmOnReset() to + /// disable or re-enable that behavior for a DSP instance. /// \param sampleRate Current sample rate /// \param maxBufferSize Maximum buffer size to process virtual void Reset(const double sampleRate, const int maxBufferSize); - /// \brief Reset the DSP unit, then prewarm - /// \param sampleRate Current sample rate - /// \param maxBufferSize Maximum buffer size to process - void ResetAndPrewarm(const double sampleRate, const int maxBufferSize) - { - Reset(sampleRate, maxBufferSize); - prewarm(); - } + /// \brief Control whether Reset() calls prewarm() + /// \param prewarmOnReset true for Reset() to call prewarm(), false to skip prewarm() + virtual void SetPrewarmOnReset(const bool prewarmOnReset); + + /// \brief Check whether Reset() calls prewarm() + /// \return true if Reset() calls prewarm() + bool GetPrewarmOnReset() const; /// \brief Set the input level /// \param inputLevel Input level in dBu @@ -177,6 +196,7 @@ class DSP double mExternalSampleRate = -1.0; // The largest buffer I expect to be told to process: int mMaxBufferSize = 0; + std::atomic mPrewarmOnReset; /// \brief Get how many samples should be processed for the model to be considered "warmed up" /// diff --git a/NAM/get_dsp.cpp b/NAM/get_dsp.cpp index 3aa85924..462d34f8 100644 --- a/NAM/get_dsp.cpp +++ b/NAM/get_dsp.cpp @@ -139,26 +139,42 @@ std::vector GetWeights(nlohmann::json const& j) throw std::runtime_error("Corrupted model file is missing weights."); } -std::unique_ptr get_dsp(const std::filesystem::path config_filename) +void populate_dsp_data(const nlohmann::json& config, dspData& returnedConfig) +{ + verify_config_version(config["version"].get()); + + nlohmann::json config_json = config["config"]; + std::vector weights = GetWeights(config); + + returnedConfig.version = config["version"].get(); + returnedConfig.architecture = config["architecture"].get(); + returnedConfig.config = config_json; + returnedConfig.metadata = config.value("metadata", nlohmann::json()); + returnedConfig.weights = weights; + returnedConfig.expected_sample_rate = nam::get_sample_rate_from_nam_file(config); +} + +std::unique_ptr get_dsp(const std::filesystem::path config_filename, DspLoadOptions options) { dspData temp; - return get_dsp(config_filename, temp); + return get_dsp(config_filename, temp, options); } -std::unique_ptr get_dsp(const nlohmann::json& config) +std::unique_ptr get_dsp(const nlohmann::json& config, DspLoadOptions options) { dspData temp; - return get_dsp(config, temp); + return get_dsp(config, temp, options); } -std::unique_ptr get_dsp(const std::filesystem::path config_filename, dspData& returnedConfig) +std::unique_ptr get_dsp(const std::filesystem::path config_filename, dspData& returnedConfig, + DspLoadOptions options) { if (!std::filesystem::exists(config_filename)) throw std::runtime_error("Config file doesn't exist!\n"); std::ifstream i(config_filename); nlohmann::json j; i >> j; - get_dsp(j, returnedConfig); + populate_dsp_data(j, returnedConfig); /*Copy to a new dsp_config object for get_dsp below, since not sure if weights actually get modified as being non-const references on some @@ -166,24 +182,12 @@ std::unique_ptr get_dsp(const std::filesystem::path config_filename, dspDat We need to return unmodified version of dsp_config via returnedConfig.*/ dspData conf = returnedConfig; - return get_dsp(conf); + return get_dsp(conf, options); } -std::unique_ptr get_dsp(const nlohmann::json& config, dspData& returnedConfig) +std::unique_ptr get_dsp(const nlohmann::json& config, dspData& returnedConfig, DspLoadOptions options) { - verify_config_version(config["version"].get()); - - auto architecture = config["architecture"]; - nlohmann::json config_json = config["config"]; - std::vector weights = GetWeights(config); - - // Assign values to returnedConfig - returnedConfig.version = config["version"].get(); - returnedConfig.architecture = config["architecture"].get(); - returnedConfig.config = config_json; - returnedConfig.metadata = config.value("metadata", nlohmann::json()); - returnedConfig.weights = weights; - returnedConfig.expected_sample_rate = nam::get_sample_rate_from_nam_file(config); + populate_dsp_data(config, returnedConfig); /*Copy to a new dsp_config object for get_dsp below, since not sure if weights actually get modified as being non-const references on some @@ -191,7 +195,7 @@ std::unique_ptr get_dsp(const nlohmann::json& config, dspData& returnedConf We need to return unmodified version of dsp_config via returnedConfig.*/ dspData conf = returnedConfig; - return get_dsp(conf); + return get_dsp(conf, options); } // ============================================================================= @@ -224,9 +228,6 @@ std::unique_ptr create_dsp(std::unique_ptr config, std::vector { auto out = config->create(std::move(weights), metadata.sample_rate); apply_metadata(*out, metadata); - // "pre-warm" the model to settle initial conditions - // Can this be removed now that it's part of Reset()? - out->prewarm(); return out; } @@ -234,7 +235,10 @@ std::unique_ptr create_dsp(std::unique_ptr config, std::vector // get_dsp(dspData&) — now uses unified path // ============================================================================= -std::unique_ptr get_dsp(dspData& conf) +namespace +{ + +std::unique_ptr get_dsp_with_current_prewarm_default(dspData& conf) { verify_config_version(conf.version); @@ -259,6 +263,20 @@ std::unique_ptr get_dsp(dspData& conf) return create_dsp(std::move(model_config), std::move(conf.weights), metadata); } +} // anonymous namespace + +std::unique_ptr get_dsp(dspData& conf, DspLoadOptions options) +{ + if (options.prewarm) + return get_dsp_with_current_prewarm_default(conf); + + ScopedPrewarmOnResetDefault scoped_prewarm_default(false); + auto dsp = get_dsp_with_current_prewarm_default(conf); + if (dsp != nullptr) + dsp->SetPrewarmOnReset(scoped_prewarm_default.PreviousPrewarmOnReset()); + return dsp; +} + double get_sample_rate_from_nam_file(const nlohmann::json& j) { if (j.find("sample_rate") != j.end()) diff --git a/NAM/get_dsp.h b/NAM/get_dsp.h index da874fe9..90f01b0a 100644 --- a/NAM/get_dsp.h +++ b/NAM/get_dsp.h @@ -65,34 +65,51 @@ void verify_config_version(const std::string versionStr); const std::string LATEST_FULLY_SUPPORTED_NAM_FILE_VERSION = "0.7.0"; const std::string EARLIEST_SUPPORTED_NAM_FILE_VERSION = "0.5.0"; +/// \brief Options that control DSP loading behavior +struct DspLoadOptions +{ + /// \brief Whether Reset() calls made during loading may prewarm constructed models + /// + /// Set this to false to avoid expensive prewarm work during get_dsp(). The returned model is restored to the + /// caller's previous prewarm-on-reset default before get_dsp() returns. + bool prewarm = true; +}; + /// \brief Get NAM from a .nam file at the provided location /// \param config_filename Path to the .nam model file +/// \param options Loading options /// \return Unique pointer to a DSP object -std::unique_ptr get_dsp(const std::filesystem::path config_filename); +std::unique_ptr get_dsp(const std::filesystem::path config_filename, DspLoadOptions options = DspLoadOptions()); /// \brief Get NAM from a provided configuration struct /// \param conf DSP data structure containing model configuration and weights +/// \param options Loading options /// \return Unique pointer to a DSP object -std::unique_ptr get_dsp(dspData& conf); +std::unique_ptr get_dsp(dspData& conf, DspLoadOptions options = DspLoadOptions()); /// \brief Get NAM from a .nam file and store its configuration /// /// Creates an instance of DSP and also returns a dspData struct that holds the data of the model. /// \param config_filename Path to the .nam model file /// \param returnedConfig Output parameter that will be filled with the model data +/// \param options Loading options /// \return Unique pointer to a DSP object -std::unique_ptr get_dsp(const std::filesystem::path config_filename, dspData& returnedConfig); +std::unique_ptr get_dsp(const std::filesystem::path config_filename, dspData& returnedConfig, + DspLoadOptions options = DspLoadOptions()); /// \brief Get NAM from a provided configuration JSON object /// \param config JSON configuration object /// \param returnedConfig Output parameter that will be filled with the model data +/// \param options Loading options /// \return Unique pointer to a DSP object -std::unique_ptr get_dsp(const nlohmann::json& config, dspData& returnedConfig); +std::unique_ptr get_dsp(const nlohmann::json& config, dspData& returnedConfig, + DspLoadOptions options = DspLoadOptions()); /// \brief Get NAM from a provided configuration JSON object (convenience overload) /// \param config JSON configuration object +/// \param options Loading options /// \return Unique pointer to a DSP object -std::unique_ptr get_dsp(const nlohmann::json& config); +std::unique_ptr get_dsp(const nlohmann::json& config, DspLoadOptions options = DspLoadOptions()); /// \brief Get sample rate from a .nam file /// \param j JSON object from the .nam file diff --git a/NAM/model_config.h b/NAM/model_config.h index 32825a51..d9653da8 100644 --- a/NAM/model_config.h +++ b/NAM/model_config.h @@ -106,7 +106,7 @@ struct ConfigParserHelper /// \brief Construct a DSP object from a typed config, weights, and metadata /// /// This is the single construction path used by both JSON and binary loaders. -/// Handles construction, metadata application, and prewarm. +/// Handles construction and metadata application. /// \param config Architecture-specific configuration (abstract base) /// \param weights Model weights (taken by value to allow move for WaveNet) /// \param metadata Model metadata (version, sample rate, loudness, levels) diff --git a/NAM/wavenet/model.cpp b/NAM/wavenet/model.cpp index eaf74add..9437e1e5 100644 --- a/NAM/wavenet/model.cpp +++ b/NAM/wavenet/model.cpp @@ -689,6 +689,13 @@ void nam::wavenet::WaveNet::SetMaxBufferSize(const int maxBufferSize) } } +void nam::wavenet::WaveNet::SetPrewarmOnReset(const bool prewarmOnReset) +{ + DSP::SetPrewarmOnReset(prewarmOnReset); + if (this->_condition_dsp != nullptr) + this->_condition_dsp->SetPrewarmOnReset(prewarmOnReset); +} + void nam::wavenet::WaveNet::_process_condition(const int num_frames) { if (this->_condition_dsp == nullptr) diff --git a/NAM/wavenet/model.h b/NAM/wavenet/model.h index 968baf87..508f9096 100644 --- a/NAM/wavenet/model.h +++ b/NAM/wavenet/model.h @@ -58,6 +58,8 @@ class WaveNet : public DSP /// \param num_frames Number of frames to process void process(NAM_SAMPLE** input, NAM_SAMPLE** output, const int num_frames) override; + void SetPrewarmOnReset(const bool prewarmOnReset) override; + /// \brief Set model weights from a vector /// \param weights Vector containing all model weights void set_weights_(std::vector& weights); diff --git a/NAM/wavenet/slimmable.cpp b/NAM/wavenet/slimmable.cpp index 19b019e2..69f077bc 100644 --- a/NAM/wavenet/slimmable.cpp +++ b/NAM/wavenet/slimmable.cpp @@ -410,8 +410,10 @@ std::unique_ptr SlimmableWavenet::_create_wavenet_for_channels(const std::v condition_dsp = get_dsp(_condition_dsp_json); double sampleRate = _current_sample_rate > 0 ? _current_sample_rate : GetExpectedSampleRate(); - return std::make_unique(_in_channels, *params_ptr, _head_scale, _with_head, std::nullopt, - std::move(weights), std::move(condition_dsp), sampleRate); + auto model = std::make_unique(_in_channels, *params_ptr, _head_scale, _with_head, std::nullopt, + std::move(weights), std::move(condition_dsp), sampleRate); + model->SetPrewarmOnReset(GetPrewarmOnReset()); + return model; } void SlimmableWavenet::_rebuild_model(const std::vector& target_channels) @@ -481,6 +483,15 @@ void SlimmableWavenet::Reset(const double sampleRate, const int maxBufferSize) pending->model->Reset(sampleRate, maxBufferSize); } +void SlimmableWavenet::SetPrewarmOnReset(const bool prewarmOnReset) +{ + DSP::SetPrewarmOnReset(prewarmOnReset); + if (_active_model) + _active_model->SetPrewarmOnReset(prewarmOnReset); + if (auto pending = _pending_load_acquire()) + pending->model->SetPrewarmOnReset(prewarmOnReset); +} + void SlimmableWavenet::SetSlimmableSize(const double val) { const size_t num_arrays = _original_params.size(); diff --git a/NAM/wavenet/slimmable.h b/NAM/wavenet/slimmable.h index f0687c09..bc416ac5 100644 --- a/NAM/wavenet/slimmable.h +++ b/NAM/wavenet/slimmable.h @@ -48,6 +48,7 @@ class SlimmableWavenet : public DSP, public SlimmableModel void process(NAM_SAMPLE** input, NAM_SAMPLE** output, const int num_frames) override; void prewarm() override; void Reset(const double sampleRate, const int maxBufferSize) override; + void SetPrewarmOnReset(const bool prewarmOnReset) override; void SetSlimmableSize(const double val) override; protected: diff --git a/tools/run_tests.cpp b/tools/run_tests.cpp index 0f9d50a3..0f927450 100644 --- a/tools/run_tests.cpp +++ b/tools/run_tests.cpp @@ -84,6 +84,9 @@ int main() test_dsp::test_has_output_level(); test_dsp::test_set_input_level(); test_dsp::test_set_output_level(); + test_dsp::test_reset_prewarm_on_reset_default(); + test_dsp::test_set_prewarm_on_reset(); + test_dsp::test_scoped_prewarm_on_reset_default(); test_linear::test_direct_known_values(); test_linear::test_fft_matches_direct_irregular_chunks(); @@ -285,6 +288,9 @@ int main() test_get_dsp::test_version_too_early(); test_get_dsp::test_is_version_supported_core_behavior(); test_get_dsp::test_register_custom_version_support_checker(); + test_get_dsp::test_get_dsp_default_allows_constructor_reset_prewarm(); + test_get_dsp::test_get_dsp_prewarm_option_suppresses_constructor_reset_prewarm(); + test_get_dsp::test_get_dsp_with_returned_config_constructs_once(); // Finally, some end-to-end tests. test_get_dsp::test_load_and_process_nam_files(); diff --git a/tools/test/test_container.cpp b/tools/test/test_container.cpp index a89e8a5c..46b17527 100644 --- a/tools/test/test_container.cpp +++ b/tools/test/test_container.cpp @@ -380,7 +380,7 @@ void test_container_default_is_max_size() NAM_SAMPLE* out_ptr; // Ensure both predictions start from identical model state. - dsp->ResetAndPrewarm(sample_rate, buffer_size); + dsp->Reset(sample_rate, buffer_size); // Process with default (should be max size) out_ptr = out_default.data(); dsp->process(&in_ptr, &out_ptr, buffer_size); @@ -389,7 +389,7 @@ void test_container_default_is_max_size() auto* slimmable = dynamic_cast(dsp.get()); assert(slimmable != nullptr); slimmable->SetSlimmableSize(1.0); - dsp->ResetAndPrewarm(sample_rate, buffer_size); + dsp->Reset(sample_rate, buffer_size); out_ptr = out_max.data(); dsp->process(&in_ptr, &out_ptr, buffer_size); diff --git a/tools/test/test_dsp.cpp b/tools/test/test_dsp.cpp index d019a878..334461d5 100644 --- a/tools/test/test_dsp.cpp +++ b/tools/test/test_dsp.cpp @@ -1,10 +1,24 @@ // Tests for dsp #include "NAM/dsp.h" +#include #include namespace test_dsp { +class PrewarmCountingDSP : public nam::DSP +{ +public: + PrewarmCountingDSP() + : nam::DSP(1, 1, 48000.0) + { + } + + void prewarm() override { prewarm_count++; } + + int prewarm_count = 0; +}; + // Simplest test: can I construct something! void test_construct() { @@ -91,6 +105,46 @@ void test_set_output_level() myDsp.SetOutputLevel(19.0); } +void test_reset_prewarm_on_reset_default() +{ + PrewarmCountingDSP dsp; + assert(dsp.GetPrewarmOnReset()); + + dsp.Reset(48000.0, 64); + + assert(dsp.prewarm_count == 1); +} + +void test_set_prewarm_on_reset() +{ + PrewarmCountingDSP dsp; + + dsp.SetPrewarmOnReset(false); + dsp.Reset(48000.0, 64); + assert(dsp.prewarm_count == 0); + + dsp.SetPrewarmOnReset(true); + dsp.Reset(48000.0, 64); + assert(dsp.prewarm_count == 1); +} + +void test_scoped_prewarm_on_reset_default() +{ + PrewarmCountingDSP before_scope; + assert(before_scope.GetPrewarmOnReset()); + + { + nam::ScopedPrewarmOnResetDefault scoped_default(false); + PrewarmCountingDSP in_scope; + assert(!in_scope.GetPrewarmOnReset()); + in_scope.Reset(48000.0, 64); + assert(in_scope.prewarm_count == 0); + } + + PrewarmCountingDSP after_scope; + assert(after_scope.GetPrewarmOnReset()); +} + void test_process_multi_channel() { const int in_channels = 2; diff --git a/tools/test/test_get_dsp.cpp b/tools/test/test_get_dsp.cpp index 150f50fe..7f97c6a4 100644 --- a/tools/test/test_get_dsp.cpp +++ b/tools/test/test_get_dsp.cpp @@ -10,6 +10,7 @@ #include "json.hpp" #include "NAM/get_dsp.h" +#include "NAM/registry.h" namespace test_get_dsp { @@ -46,6 +47,50 @@ nam::dspData _GetConfig(const std::string& configStr = basicConfigStr) return returnedConfig; } +namespace +{ + +constexpr const char* kConstructorResetArchitecture = "ConstructorResetPrewarmPolicyTest"; +int gConstructorResetConstructCount = 0; + +class ConstructorResetDSP : public nam::DSP +{ +public: + explicit ConstructorResetDSP(const double expected_sample_rate) + : nam::DSP(1, 1, expected_sample_rate) + { + gConstructorResetConstructCount++; + Reset(expected_sample_rate, 16); + } + + void prewarm() override { prewarm_count++; } + + int prewarm_count = 0; +}; + +std::unique_ptr ConstructorResetFactory(const nlohmann::json& config, std::vector& weights, + const double expectedSampleRate) +{ + (void)config; + (void)weights; + return std::make_unique(expectedSampleRate); +} + +static nam::factory::Helper _register_ConstructorResetPrewarmPolicyTest(kConstructorResetArchitecture, + ConstructorResetFactory); + +nlohmann::json build_constructor_reset_config() +{ + return nlohmann::json{{"version", "0.7.0"}, + {"metadata", nlohmann::json::object()}, + {"architecture", kConstructorResetArchitecture}, + {"config", nlohmann::json::object()}, + {"weights", nlohmann::json::array()}, + {"sample_rate", 48000}}; +} + +} // namespace + void test_gets_input_level() { nam::dspData config = _GetConfig(); @@ -273,4 +318,47 @@ void test_register_custom_version_support_checker() assert(nam::is_version_supported("DEMO::1.0.3") == nam::Supported::PARTIAL); assert(nam::is_version_supported("DEMO::2.0.0") == nam::Supported::NO); } -}; // namespace test_get_dsp \ No newline at end of file + +void test_get_dsp_default_allows_constructor_reset_prewarm() +{ + gConstructorResetConstructCount = 0; + + auto dsp = nam::get_dsp(build_constructor_reset_config()); + auto* typed = dynamic_cast(dsp.get()); + + assert(typed != nullptr); + assert(gConstructorResetConstructCount == 1); + assert(typed->prewarm_count == 1); + assert(typed->GetPrewarmOnReset()); +} + +void test_get_dsp_prewarm_option_suppresses_constructor_reset_prewarm() +{ + gConstructorResetConstructCount = 0; + + nam::DspLoadOptions options; + options.prewarm = false; + auto dsp = nam::get_dsp(build_constructor_reset_config(), options); + auto* typed = dynamic_cast(dsp.get()); + + assert(typed != nullptr); + assert(gConstructorResetConstructCount == 1); + assert(typed->prewarm_count == 0); + assert(typed->GetPrewarmOnReset()); + + typed->Reset(48000.0, 16); + assert(typed->prewarm_count == 1); +} + +void test_get_dsp_with_returned_config_constructs_once() +{ + gConstructorResetConstructCount = 0; + + nam::dspData returned_config; + auto dsp = nam::get_dsp(build_constructor_reset_config(), returned_config); + + assert(dsp != nullptr); + assert(gConstructorResetConstructCount == 1); + assert(returned_config.architecture == kConstructorResetArchitecture); +} +}; // namespace test_get_dsp From c09d49d2759a80d13f1ebe0b70747a9f7f69b9e5 Mon Sep 17 00:00:00 2001 From: Steven Atkinson Date: Wed, 10 Jun 2026 15:23:16 -0700 Subject: [PATCH 6/6] Clarify prewarm load option semantics --- NAM/get_dsp.cpp | 4 ++-- NAM/get_dsp.h | 10 ++++++---- tools/run_tests.cpp | 2 ++ tools/test/test_get_dsp.cpp | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/NAM/get_dsp.cpp b/NAM/get_dsp.cpp index 462d34f8..64393646 100644 --- a/NAM/get_dsp.cpp +++ b/NAM/get_dsp.cpp @@ -267,10 +267,10 @@ std::unique_ptr get_dsp_with_current_prewarm_default(dspData& conf) std::unique_ptr get_dsp(dspData& conf, DspLoadOptions options) { - if (options.prewarm) + if (!options.prewarm.has_value()) return get_dsp_with_current_prewarm_default(conf); - ScopedPrewarmOnResetDefault scoped_prewarm_default(false); + ScopedPrewarmOnResetDefault scoped_prewarm_default(*options.prewarm); auto dsp = get_dsp_with_current_prewarm_default(conf); if (dsp != nullptr) dsp->SetPrewarmOnReset(scoped_prewarm_default.PreviousPrewarmOnReset()); diff --git a/NAM/get_dsp.h b/NAM/get_dsp.h index 90f01b0a..c9e7941b 100644 --- a/NAM/get_dsp.h +++ b/NAM/get_dsp.h @@ -2,6 +2,7 @@ #include #include +#include #include #include "dsp.h" @@ -68,11 +69,12 @@ const std::string EARLIEST_SUPPORTED_NAM_FILE_VERSION = "0.5.0"; /// \brief Options that control DSP loading behavior struct DspLoadOptions { - /// \brief Whether Reset() calls made during loading may prewarm constructed models + /// \brief Whether to override the current prewarm-on-reset context during loading /// - /// Set this to false to avoid expensive prewarm work during get_dsp(). The returned model is restored to the - /// caller's previous prewarm-on-reset default before get_dsp() returns. - bool prewarm = true; + /// std::nullopt leaves the current thread-local context unchanged. Set this to false to avoid expensive prewarm work + /// during get_dsp(), or true to force prewarm during get_dsp(). When an override is provided, the returned model is + /// restored to the caller's previous prewarm-on-reset default before get_dsp() returns. + std::optional prewarm = std::nullopt; }; /// \brief Get NAM from a .nam file at the provided location diff --git a/tools/run_tests.cpp b/tools/run_tests.cpp index 0f927450..e765ddff 100644 --- a/tools/run_tests.cpp +++ b/tools/run_tests.cpp @@ -289,7 +289,9 @@ int main() test_get_dsp::test_is_version_supported_core_behavior(); test_get_dsp::test_register_custom_version_support_checker(); test_get_dsp::test_get_dsp_default_allows_constructor_reset_prewarm(); + test_get_dsp::test_get_dsp_default_inherits_scoped_prewarm_default(); test_get_dsp::test_get_dsp_prewarm_option_suppresses_constructor_reset_prewarm(); + test_get_dsp::test_get_dsp_prewarm_option_forces_constructor_reset_prewarm(); test_get_dsp::test_get_dsp_with_returned_config_constructs_once(); // Finally, some end-to-end tests. diff --git a/tools/test/test_get_dsp.cpp b/tools/test/test_get_dsp.cpp index 7f97c6a4..3cfd3687 100644 --- a/tools/test/test_get_dsp.cpp +++ b/tools/test/test_get_dsp.cpp @@ -332,6 +332,20 @@ void test_get_dsp_default_allows_constructor_reset_prewarm() assert(typed->GetPrewarmOnReset()); } +void test_get_dsp_default_inherits_scoped_prewarm_default() +{ + gConstructorResetConstructCount = 0; + + nam::ScopedPrewarmOnResetDefault scoped_default(false); + auto dsp = nam::get_dsp(build_constructor_reset_config()); + auto* typed = dynamic_cast(dsp.get()); + + assert(typed != nullptr); + assert(gConstructorResetConstructCount == 1); + assert(typed->prewarm_count == 0); + assert(!typed->GetPrewarmOnReset()); +} + void test_get_dsp_prewarm_option_suppresses_constructor_reset_prewarm() { gConstructorResetConstructCount = 0; @@ -350,6 +364,25 @@ void test_get_dsp_prewarm_option_suppresses_constructor_reset_prewarm() assert(typed->prewarm_count == 1); } +void test_get_dsp_prewarm_option_forces_constructor_reset_prewarm() +{ + gConstructorResetConstructCount = 0; + + nam::ScopedPrewarmOnResetDefault scoped_default(false); + nam::DspLoadOptions options; + options.prewarm = true; + auto dsp = nam::get_dsp(build_constructor_reset_config(), options); + auto* typed = dynamic_cast(dsp.get()); + + assert(typed != nullptr); + assert(gConstructorResetConstructCount == 1); + assert(typed->prewarm_count == 1); + assert(!typed->GetPrewarmOnReset()); + + typed->Reset(48000.0, 16); + assert(typed->prewarm_count == 1); +} + void test_get_dsp_with_returned_config_constructs_once() { gConstructorResetConstructCount = 0;