diff --git a/.gitignore b/.gitignore index 8604b38..b7ee58e 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ *.app .vscode/ + +docs/_build/ +*.DS_Store diff --git a/NAM/get_dsp.cpp b/NAM/get_dsp.cpp index 8163f3a..5505cdc 100644 --- a/NAM/get_dsp.cpp +++ b/NAM/get_dsp.cpp @@ -14,17 +14,8 @@ namespace nam { -struct Version -{ - int major; - int minor; - int patch; -}; - Version ParseVersion(const std::string& versionStr) { - Version version; - // Split the version string into major, minor, and patch components std::stringstream ss(versionStr); std::string majorStr, minorStr, patchStr; @@ -33,11 +24,14 @@ Version ParseVersion(const std::string& versionStr) std::getline(ss, patchStr); // Parse the components as integers and assign them to the version struct + int major; + int minor; + int patch; try { - version.major = std::stoi(majorStr); - version.minor = std::stoi(minorStr); - version.patch = std::stoi(patchStr); + major = std::stoi(majorStr); + minor = std::stoi(minorStr); + patch = std::stoi(patchStr); } catch (const std::invalid_argument&) { @@ -49,24 +43,41 @@ Version ParseVersion(const std::string& versionStr) } // Validate the semver components - if (version.major < 0 || version.minor < 0 || version.patch < 0) + if (major < 0 || minor < 0 || patch < 0) { throw std::invalid_argument("Negative version component: " + versionStr); } - return version; + return Version(major, minor, patch); } void verify_config_version(const std::string versionStr) { Version version = ParseVersion(versionStr); - if (version.major != 0 || version.minor != 5) + Version currentVersion = ParseVersion(LATEST_FULLY_SUPPORTED_NAM_FILE_VERSION); + Version earliestSupportedVersion = ParseVersion(EARLIEST_SUPPORTED_NAM_FILE_VERSION); + + if (version < earliestSupportedVersion) { std::stringstream ss; - ss << "Model config is an unsupported version " << versionStr + ss << "Model config is an unsupported version " << versionStr << ". The earliest supported version is " + << earliestSupportedVersion.toString() << ". Try either converting the model to a more recent version, or " "update your version of the NAM plugin."; throw std::runtime_error(ss.str()); } + if (version.major > currentVersion.major || version.minor > currentVersion.minor) + { + std::stringstream ss; + ss << "Model config is an unsupported version " << versionStr << ". The latest fully-supported version is " + << currentVersion.toString(); + throw std::runtime_error(ss.str()); + } + else if (version.major == 0 && version.minor == 6 && version.patch > 0) + { + std::cerr << "Model config is a partially-supported version " << versionStr + << ". The latest fully-supported version is " << currentVersion.toString() + << ". Continuing with partial support." << std::endl; + } } std::vector GetWeights(nlohmann::json const& j) diff --git a/NAM/get_dsp.h b/NAM/get_dsp.h index 6353053..e629b30 100644 --- a/NAM/get_dsp.h +++ b/NAM/get_dsp.h @@ -6,6 +6,45 @@ namespace nam { +class Version +{ +public: + Version(int major, int minor, int patch) + : major(major) + , minor(minor) + , patch(patch) + { + } + + std::string toString() const + { + return std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(patch); + } + + bool operator>(const Version& other) const + { + return major > other.major + || (major == other.major && (minor > other.minor || (minor == other.minor && patch > other.patch))); + } + + bool operator<(const Version& other) const + { + return major < other.major + || (major == other.major && (minor < other.minor || (minor == other.minor && patch < other.patch))); + } + + int major; + int minor; + int patch; +}; + +Version ParseVersion(const std::string& versionStr); + +void verify_config_version(const std::string versionStr); + +const std::string LATEST_FULLY_SUPPORTED_NAM_FILE_VERSION = "0.6.0"; +const std::string EARLIEST_SUPPORTED_NAM_FILE_VERSION = "0.5.0"; + /// \brief Get NAM from a .nam file at the provided location /// \param config_filename Path to the .nam model file /// \return Unique pointer to a DSP object diff --git a/docs/index.rst b/docs/index.rst index 7f9bdda..68c7795 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,6 +8,7 @@ Welcome to the NeuralAmpModelerCore documentation. This library provides a core :caption: Contents: wavenet_walkthrough + nam_file_version api/index Overview diff --git a/docs/nam_file_version.rst b/docs/nam_file_version.rst new file mode 100644 index 0000000..c102d08 --- /dev/null +++ b/docs/nam_file_version.rst @@ -0,0 +1,33 @@ +``.nam`` file versions +====================== + +As more features are added to NAM, the version of the ``.nam`` file format will be incremented. +The general rules try to follow semantic versioning. +That means that any changes to file contents where an older version of +NeuralAmpModelerCore is either not able to understand the contents or might +misunderstand them (e.g. new fields that old code is not looking for) will trigger a +version bump communicating a breaking change (e.g. minor version while pre-v1.0.0; +later, a major version bump). +Improvements where the model will be loaded correctly, but possibly with some incomplete +functionality will trigger a version bump communicating a non-breaking change +(e.g. minor version or patch pre-v1.0.0). + +Version history +--------------- + +The following table shows which versions of NeuralAmpModelerCore support which model file versions: + +.. list-table:: Core Version Support Matrix + :header-rows: 1 + :widths: 30 70 + + * - Core Version + - Latest fully-supported ``.nam`` file version + * - 0.0.0 + - 0.5.1 + * - 0.2.0 + - 0.5.2 + * - 0.3.0 + - 0.5.3 + * - 0.4.0 + - 0.6.0 diff --git a/example_models/wavenet_a2_max.nam b/example_models/wavenet_a2_max.nam index cea7420..1a48c95 100644 --- a/example_models/wavenet_a2_max.nam +++ b/example_models/wavenet_a2_max.nam @@ -3,7 +3,7 @@ "This model is meant as a 'test case' to contain all of the new features that are being considered for A2.", "It doesn't have slimmability." ], - "version": "0.5.4", + "version": "0.6.0", "metadata": { "date": { "year": 2026, diff --git a/example_models/wavenet_condition_dsp.nam b/example_models/wavenet_condition_dsp.nam index 81487f0..b9e3a9a 100644 --- a/example_models/wavenet_condition_dsp.nam +++ b/example_models/wavenet_condition_dsp.nam @@ -1,5 +1,5 @@ { - "version": "0.5.4", + "version": "0.6.0", "metadata": { "date": { "year": 2026, @@ -17,7 +17,7 @@ "architecture": "WaveNet", "config": { "condition_dsp": { - "version": "0.5.4", + "version": "0.6.0", "metadata": { "date": { "year": 2026, diff --git a/tools/create_wavenet.py b/tools/create_wavenet.py index 76cec7c..aaa3d83 100755 --- a/tools/create_wavenet.py +++ b/tools/create_wavenet.py @@ -154,7 +154,7 @@ def generate_weights(weight_count: int, seed: int = None) -> List[float]: def create_wavenet_nam(config: Dict[str, Any], output_path: Path, seed: int = None, sample_rate: int = 48000, - version: str = "0.5.4") -> None: + version: str = "0.6.0") -> None: """ Create a WaveNet .nam file with the given configuration and random weights. diff --git a/tools/run_tests.cpp b/tools/run_tests.cpp index c86acec..218881e 100644 --- a/tools/run_tests.cpp +++ b/tools/run_tests.cpp @@ -221,6 +221,9 @@ int main() test_get_dsp::test_gets_output_level(); test_get_dsp::test_null_input_level(); test_get_dsp::test_null_output_level(); + test_get_dsp::test_version_patch_one_beyond_supported(); + test_get_dsp::test_version_minor_one_beyond_supported(); + test_get_dsp::test_version_too_early(); // Finally, some end-to-end tests. test_get_dsp::test_load_and_process_nam_files(); diff --git a/tools/test/test_get_dsp.cpp b/tools/test/test_get_dsp.cpp index 7540b17..7d3be4a 100644 --- a/tools/test/test_get_dsp.cpp +++ b/tools/test/test_get_dsp.cpp @@ -1,7 +1,9 @@ #include #include #include +#include #include +#include #include #include @@ -158,4 +160,76 @@ void test_load_and_process_nam_files() process_buffers(dsp.get(), num_buffers, buffer_size); } } + +// Helper function to create a config string with a specific version +std::string createConfigWithVersion(const std::string& version) +{ + nlohmann::json j = nlohmann::json::parse(basicConfigStr); + j["version"] = version; + return j.dump(); +} + +void test_version_patch_one_beyond_supported() +{ + // Test that a .nam file with version one patch beyond the latest fully supported + // can still be loaded + nam::Version latestVersion = nam::ParseVersion(nam::LATEST_FULLY_SUPPORTED_NAM_FILE_VERSION); + latestVersion.patch++; + const std::string configStr = createConfigWithVersion(latestVersion.toString()); + nam::dspData config = _GetConfig(configStr); + + // Suppress the warning message that gets printed to std::cerr + std::streambuf* originalCerr = std::cerr.rdbuf(); + std::ostringstream nullStream; + std::cerr.rdbuf(nullStream.rdbuf()); + + // Should succeed (with a warning, but we suppress it) + std::unique_ptr dsp = get_dsp(config); + assert(dsp != nullptr); + + // Restore original cerr + std::cerr.rdbuf(originalCerr); +} + +void test_version_minor_one_beyond_supported() +{ + // Test that a .nam file with version one minor beyond the latest fully supported + // cannot be loaded + nam::Version latestVersion = nam::ParseVersion(nam::LATEST_FULLY_SUPPORTED_NAM_FILE_VERSION); + latestVersion.minor++; + latestVersion.patch = 0; // Reset patch when incrementing minor + const std::string configStr = createConfigWithVersion(latestVersion.toString()); + nam::dspData config = _GetConfig(configStr); + + bool threw = false; + try + { + std::unique_ptr dsp = get_dsp(config); + } + catch (const std::runtime_error&) + { + threw = true; + } + assert(threw); +} + +void test_version_too_early() +{ + // Test that a .nam file with version too early (before earliest supported) cannot be loaded + nam::Version earliestVersion = nam::ParseVersion(nam::EARLIEST_SUPPORTED_NAM_FILE_VERSION); + earliestVersion.minor--; // Decrement minor to get a version before earliest supported + const std::string configStr = createConfigWithVersion(earliestVersion.toString()); + nam::dspData config = _GetConfig(configStr); + + bool threw = false; + try + { + std::unique_ptr dsp = get_dsp(config); + } + catch (const std::runtime_error&) + { + threw = true; + } + assert(threw); +} }; // namespace test_get_dsp \ No newline at end of file