Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions NAM/get_dsp.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#include <fstream>
#include <iostream>
#include <mutex>
#include <regex>
Expand Down Expand Up @@ -169,11 +168,7 @@ std::unique_ptr<DSP> get_dsp(const nlohmann::json& config, DspLoadOptions option
std::unique_ptr<DSP> 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;
const auto j = validate_nam_file(config_filename);
populate_dsp_data(j, returnedConfig);

/*Copy to a new dsp_config object for get_dsp below,
Expand Down
4 changes: 3 additions & 1 deletion NAM/get_dsp.h
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#pragma once

#include <fstream>
#include <memory>
#include <optional>
#include <vector>

#include "dsp.h"
#include "nam_file.h"

namespace nam
{
Expand Down Expand Up @@ -81,6 +81,7 @@ struct DspLoadOptions
/// \param config_filename Path to the .nam model file
/// \param options Loading options
/// \return Unique pointer to a DSP object
/// \throws NamFileValidationError If the file cannot be read or does not contain a minimally valid .nam configuration
std::unique_ptr<DSP> get_dsp(const std::filesystem::path config_filename, DspLoadOptions options = DspLoadOptions());

/// \brief Get NAM from a provided configuration struct
Expand All @@ -96,6 +97,7 @@ std::unique_ptr<DSP> get_dsp(dspData& conf, DspLoadOptions options = DspLoadOpti
/// \param returnedConfig Output parameter that will be filled with the model data
/// \param options Loading options
/// \return Unique pointer to a DSP object
/// \throws NamFileValidationError If the file cannot be read or does not contain a minimally valid .nam configuration
std::unique_ptr<DSP> get_dsp(const std::filesystem::path config_filename, dspData& returnedConfig,
DspLoadOptions options = DspLoadOptions());

Expand Down
41 changes: 41 additions & 0 deletions NAM/nam_file.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#include "nam_file.h"

#include <array>
#include <fstream>
#include <string>

namespace nam
{
nlohmann::json validate_nam_file(const std::filesystem::path& filename)
{
if (!std::filesystem::exists(filename))
throw NamFileValidationError("Could not validate .nam file [" + filename.string() + "]: file does not exist.");

std::ifstream input(filename);
if (!input.is_open())
throw NamFileValidationError("Could not validate .nam file [" + filename.string() + "]: file could not be read.");

nlohmann::json config;
try
{
input >> config;
}
catch (const nlohmann::json::parse_error& error)
{
throw NamFileValidationError("Could not parse .nam file [" + filename.string() + "]: " + error.what());
}

if (!config.is_object())
throw NamFileValidationError("Invalid .nam file [" + filename.string() + "]: root JSON value must be an object.");

constexpr std::array<const char*, 4> required_keys{"version", "architecture", "config", "weights"};
for (const auto* key : required_keys)
{
if (config.find(key) == config.end())
throw NamFileValidationError("Invalid .nam file [" + filename.string() + "]: missing required key \"" + key
+ "\".");
}

return config;
}
} // namespace nam
22 changes: 22 additions & 0 deletions NAM/nam_file.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

#include <filesystem>
#include <stdexcept>

#include "json.hpp"

namespace nam
{
/// \brief Indicates that a .nam file failed validation while loading
class NamFileValidationError : public std::runtime_error
{
public:
using std::runtime_error::runtime_error;
};

/// \brief Parse and validate a .nam file
/// \param filename Path to the .nam file
/// \return The parsed model configuration
/// \throws NamFileValidationError If the file cannot be read, parsed, or does not contain the minimum required fields
nlohmann::json validate_nam_file(const std::filesystem::path& filename);
} // namespace nam
7 changes: 7 additions & 0 deletions tools/run_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include "test/test_linear.cpp"
#include "test/test_lstm.cpp"
#include "test/test_lstm_realtime_safe.cpp"
#include "test/test_nam_file.cpp"
#include "test/test_wavenet_configurable_gating.cpp"
#include "test/test_noncontiguous_blocks.cpp"
#include "test/test_extensible.cpp"
Expand Down Expand Up @@ -297,6 +298,8 @@ int main()

test_get_dsp::test_gets_input_level();
test_get_dsp::test_gets_output_level();
test_get_dsp::test_empty_nam_file_throws_validation_error();
test_get_dsp::test_malformed_nam_file_throws_validation_error();
test_get_dsp::test_null_input_level();
test_get_dsp::test_null_output_level();
test_get_dsp::test_version_patch_one_beyond_supported();
Expand All @@ -310,6 +313,10 @@ int main()
test_get_dsp::test_get_dsp_prewarm_option_forces_constructor_reset_prewarm();
test_get_dsp::test_get_dsp_with_returned_config_constructs_once();

test_nam_file::test_accepts_minimum_valid_file();
test_nam_file::test_rejects_non_object_json();
test_nam_file::test_rejects_missing_required_keys();

// Finally, some end-to-end tests.
test_get_dsp::test_load_and_process_nam_files();

Expand Down
54 changes: 54 additions & 0 deletions tools/test/test_get_dsp.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#include <cassert>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <vector>

#include "json.hpp"
Expand All @@ -14,6 +17,8 @@

namespace test_get_dsp
{
static_assert(std::is_base_of_v<std::runtime_error, nam::NamFileValidationError>);

// Config
const std::string basicConfigStr =
R"({"version": "0.5.4", "metadata": {"date": {"year": 2024, "month": 10, "day": 9, "hour": 18, "minute": 44, "second": 41}, "loudness": -37.8406867980957, "gain": 0.13508800804658277, "name": "Test LSTM", "modeled_by": "Steve", "gear_type": "amp", "gear_make": "Darkglass Electronics", "gear_model": "Microtubes 900 v2", "tone_type": "clean", "input_level_dbu": 18.3, "output_level_dbu": 12.3, "training": {"settings": {"ignore_checks": false}, "data": {"latency": {"manual": null, "calibration": {"algorithm_version": 1, "delays": [-16], "safety_factor": 1, "recommended": -17, "warnings": {"matches_lookahead": false, "disagreement_too_high": false}}}, "checks": {"version": 3, "passed": true}}, "validation_esr": null}}, "architecture": "LSTM", "config": {"input_size": 1, "hidden_size": 3, "num_layers": 1}, "weights": [-0.21677088737487793, -0.6683622002601624, -0.2560940980911255, -0.3588429093360901, 0.17952610552310944, 0.19445613026618958, -0.01662646047770977, 0.5353694558143616, -0.2536540627479553, -0.5132213234901428, -0.020476307719945908, 0.08592455089092255, -0.6891753673553467, 0.3627359867095947, 0.008421811275184155, 0.3113192617893219, 0.14251480996608734, 0.07989779114723206, -0.18211324512958527, 0.7118963003158569, 0.41084015369415283, -0.6571938395500183, -0.13214066624641418, -0.2698603868484497, 0.49387243390083313, -0.3491725027561188, 0.6353667974472046, -0.5005152225494385, 0.2052856683731079, -0.4301638901233673, -0.15770092606544495, -0.7181791067123413, 0.056290093809366226, -0.49049463868141174, 0.6623441576957703, 0.09029324352741241, 0.34005245566368103, 0.16416560113430023, 0.15520110726356506, -0.4155678153038025, -0.36928507685661316, 0.3211132884025574, -0.6769840121269226, -0.1575538069009781, 0.05268515646457672, -0.4191459119319916, 0.599330484867096, 0.21518059074878693, -4.246325492858887, -3.315647840499878, -4.328850746154785, 4.496089458465576, 5.015639305114746, 3.6492037773132324, 0.14431169629096985, -0.6633821725845337, 0.11673200130462646, -0.1418764889240265, -0.4897872805595398, -0.8689419031143188, -0.06714004278182983, -0.4450395107269287, -0.02142983116209507, -0.15136894583702087, -2.775207996368408, -0.08681213855743408, 0.05702732503414154, 0.670292317867279, 0.31442636251449585, 0.30793967843055725], "sample_rate": 48000})";
Expand Down Expand Up @@ -50,6 +55,21 @@ nam::dspData _GetConfig(const std::string& configStr = basicConfigStr)
namespace
{

class TemporaryNamFile
{
public:
TemporaryNamFile(const std::string& filename, const std::string& contents)
: path(std::filesystem::temp_directory_path() / filename)
{
std::ofstream output(path);
output << contents;
}

~TemporaryNamFile() { std::filesystem::remove(path); }

const std::filesystem::path path;
};

constexpr const char* kConstructorResetArchitecture = "ConstructorResetPrewarmPolicyTest";
int gConstructorResetConstructCount = 0;

Expand Down Expand Up @@ -91,6 +111,40 @@ nlohmann::json build_constructor_reset_config()

} // namespace

void test_empty_nam_file_throws_validation_error()
{
const TemporaryNamFile file("nam_core_empty_file_test.nam", "");

try
{
nam::get_dsp(file.path);
assert(false);
}
catch (const nam::NamFileValidationError& error)
{
const std::string message = error.what();
assert(message.find(file.path.string()) != std::string::npos);
assert(message.find("parse") != std::string::npos);
}
}

void test_malformed_nam_file_throws_validation_error()
{
const TemporaryNamFile file("nam_core_malformed_file_test.nam", R"({"version":)");

try
{
nam::get_dsp(file.path);
assert(false);
}
catch (const nam::NamFileValidationError& error)
{
const std::string message = error.what();
assert(message.find(file.path.string()) != std::string::npos);
assert(message.find("parse") != std::string::npos);
}
}

void test_gets_input_level()
{
nam::dspData config = _GetConfig();
Expand Down
82 changes: 82 additions & 0 deletions tools/test/test_nam_file.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#include <cassert>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>

#include "json.hpp"

#include "NAM/nam_file.h"

namespace test_nam_file
{
namespace
{

class TemporaryNamFile
{
public:
TemporaryNamFile(const std::string& filename, const std::string& contents)
: path(std::filesystem::temp_directory_path() / filename)
{
std::ofstream output(path);
output << contents;
}

~TemporaryNamFile() { std::filesystem::remove(path); }

const std::filesystem::path path;
};

nlohmann::json minimum_valid_nam_file()
{
return {{"version", "0.7.0"},
{"architecture", "Test"},
{"config", nlohmann::json::object()},
{"weights", nlohmann::json::array()}};
}

void assert_validation_error(const TemporaryNamFile& file, const std::string& expected_detail)
{
try
{
nam::validate_nam_file(file.path);
assert(false);
}
catch (const nam::NamFileValidationError& error)
{
const std::string message = error.what();
assert(message.find(file.path.string()) != std::string::npos);
assert(message.find(expected_detail) != std::string::npos);
}
}

} // namespace

void test_accepts_minimum_valid_file()
{
const TemporaryNamFile file("nam_core_minimum_valid_file_test.nam", minimum_valid_nam_file().dump());
const auto config = nam::validate_nam_file(file.path);

assert(config.is_object());
}

void test_rejects_non_object_json()
{
const TemporaryNamFile file("nam_core_non_object_file_test.nam", "[]");
assert_validation_error(file, "object");
}

void test_rejects_missing_required_keys()
{
const std::vector<std::string> required_keys{"version", "architecture", "config", "weights"};
for (const auto& key : required_keys)
{
auto config = minimum_valid_nam_file();
config.erase(key);
const TemporaryNamFile file("nam_core_missing_" + key + "_test.nam", config.dump());
assert_validation_error(file, key);
}
}

} // namespace test_nam_file
Loading