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
2 changes: 1 addition & 1 deletion NAM/dsp.h
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ class Conv1x1
struct dspData
{
std::string version; ///< Data version. Follows conventions established in trainer code.
std::string architecture; ///< High-level architecture. Supported: "ConvNet", "LSTM", "Linear", "WaveNet"
std::string architecture; ///< High-level architecture, e.g. "ConvNet", "LSTM", "Linear", "WaveNet", "Sequential"
nlohmann::json config; ///< Model configuration JSON
nlohmann::json metadata; ///< Model metadata JSON
std::vector<float> weights; ///< Model weights
Expand Down
244 changes: 244 additions & 0 deletions NAM/sequential.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
#include "sequential.h"

#include <algorithm>
#include <limits>
#include <sstream>
#include <stdexcept>

#include "get_dsp.h"

namespace
{

void validate_models_present(const std::vector<std::unique_ptr<nam::DSP>>& models)
{
if (models.empty())
throw std::runtime_error("Sequential: 'models' must be a non-empty array");
for (const auto& model : models)
{
if (model == nullptr)
throw std::runtime_error("SequentialModel: null model provided");
}
}

int get_input_channels(const std::vector<std::unique_ptr<nam::DSP>>& models)
{
validate_models_present(models);
return models.front()->NumInputChannels();
}

int get_output_channels(const std::vector<std::unique_ptr<nam::DSP>>& models)
{
validate_models_present(models);
return models.back()->NumOutputChannels();
}

double resolve_expected_sample_rate(const std::vector<std::unique_ptr<nam::DSP>>& models,
const double expected_sample_rate)
{
validate_models_present(models);
double resolved = expected_sample_rate;

for (const auto& model : models)
{
const double child_sample_rate = model->GetExpectedSampleRate();
if (child_sample_rate == NAM_UNKNOWN_EXPECTED_SAMPLE_RATE)
continue;
if (resolved == NAM_UNKNOWN_EXPECTED_SAMPLE_RATE)
{
resolved = child_sample_rate;
continue;
}
if (child_sample_rate != resolved)
{
std::stringstream message;
message << "SequentialModel: submodel sample rate mismatch (expected " << resolved << ", got "
<< child_sample_rate << ")";
throw std::runtime_error(message.str());
}
}

return resolved;
}

void validate_channel_links(const std::vector<std::unique_ptr<nam::DSP>>& models)
{
validate_models_present(models);
for (size_t i = 1; i < models.size(); ++i)
{
const int previous_output_channels = models[i - 1]->NumOutputChannels();
const int next_input_channels = models[i]->NumInputChannels();
if (previous_output_channels != next_input_channels)
{
std::stringstream message;
message << "SequentialModel: channel mismatch between submodels " << i - 1 << " and " << i << " ("
<< previous_output_channels << " output channels versus " << next_input_channels << " input channels)";
throw std::runtime_error(message.str());
}
}
}

std::vector<std::unique_ptr<nam::DSP>> build_models(const nlohmann::json& config)
{
if (!config.contains("models"))
throw std::runtime_error("Sequential: config must contain a 'models' array");

const auto& models_json = config.at("models");
if (!models_json.is_array() || models_json.empty())
throw std::runtime_error("Sequential: 'models' must be a non-empty array");

std::vector<std::unique_ptr<nam::DSP>> models;
models.reserve(models_json.size());

for (const auto& model_json : models_json)
{
static const std::vector<std::string> required_keys{"version", "architecture", "config", "weights"};
if (!model_json.is_object()
|| std::any_of(required_keys.begin(), required_keys.end(),
[&model_json](const std::string& key) { return !model_json.contains(key); }))
{
throw std::runtime_error(
"Sequential: each child must be a complete NAM model with version, architecture, config, and weights");
}
models.push_back(nam::get_dsp(model_json));
}

return models;
}

void restore_child_prewarm_states(const std::vector<std::unique_ptr<nam::DSP>>& models,
const std::vector<bool>& prewarm_states)
{
for (size_t i = 0; i < models.size(); ++i)
models[i]->SetPrewarmOnReset(prewarm_states[i]);
}

} // namespace

namespace nam
{
namespace sequential
{

SequentialModel::SequentialModel(std::vector<std::unique_ptr<DSP>> models, const double expected_sample_rate)
: DSP(
get_input_channels(models), get_output_channels(models), resolve_expected_sample_rate(models, expected_sample_rate))
, _models(std::move(models))
{
validate_channel_links(_models);
}

void SequentialModel::process(NAM_SAMPLE** input, NAM_SAMPLE** output, const int num_frames)
{
if (num_frames < 0)
throw std::runtime_error("SequentialModel: num_frames cannot be negative");
if (num_frames > GetMaxBufferSize())
throw std::runtime_error("SequentialModel: num_frames exceeds the maximum buffer size provided to Reset");

NAM_SAMPLE** stage_input = input;
for (size_t i = 0; i < _models.size(); ++i)
{
NAM_SAMPLE** stage_output = i + 1 == _models.size() ? output : _stage_buffer_ptrs[i].data();
_models[i]->process(stage_input, stage_output, num_frames);
stage_input = stage_output;
}
}

void SequentialModel::prewarm()
{
DSP::prewarm();
}

void SequentialModel::Reset(const double sampleRate, const int maxBufferSize)
{
mExternalSampleRate = sampleRate;
mHaveExternalSampleRate = true;
SetMaxBufferSize(maxBufferSize);

std::vector<bool> child_prewarm_states;
child_prewarm_states.reserve(_models.size());
for (auto& model : _models)
{
child_prewarm_states.push_back(model->GetPrewarmOnReset());
model->SetPrewarmOnReset(false);
}

try
{
for (auto& model : _models)
model->Reset(sampleRate, maxBufferSize);
}
catch (...)
{
restore_child_prewarm_states(_models, child_prewarm_states);
throw;
}
restore_child_prewarm_states(_models, child_prewarm_states);

if (GetPrewarmOnReset())
prewarm();
}

void SequentialModel::SetPrewarmOnReset(const bool prewarmOnReset)
{
DSP::SetPrewarmOnReset(prewarmOnReset);
for (auto& model : _models)
model->SetPrewarmOnReset(prewarmOnReset);
}

int SequentialModel::GetPrewarmSamples()
{
int samples = 0;
for (auto& model : _models)
{
const int child_samples = model->GetPrewarmSamples();
if (child_samples > std::numeric_limits<int>::max() - samples)
return std::numeric_limits<int>::max();
samples += child_samples;
}
return samples;
}

void SequentialModel::SetMaxBufferSize(const int maxBufferSize)
{
DSP::SetMaxBufferSize(maxBufferSize);

const size_t intermediate_stages = _models.empty() ? 0 : _models.size() - 1;
_stage_buffers.resize(intermediate_stages);
_stage_buffer_ptrs.resize(intermediate_stages);

const int buffer_size = std::max(maxBufferSize, 0);
for (size_t stage = 0; stage < intermediate_stages; ++stage)
{
const int channels = _models[stage]->NumOutputChannels();
_stage_buffers[stage].resize(channels);
_stage_buffer_ptrs[stage].resize(channels);
for (int channel = 0; channel < channels; ++channel)
{
_stage_buffers[stage][channel].resize(buffer_size);
_stage_buffer_ptrs[stage][channel] = _stage_buffers[stage][channel].data();
}
}
}

std::unique_ptr<DSP> SequentialConfig::create(std::vector<float> weights, const double sampleRate)
{
if (!weights.empty())
throw std::runtime_error("Sequential: top-level weights must be empty; weights belong to the child models");

auto models = build_models(raw_config);
return std::make_unique<SequentialModel>(std::move(models), sampleRate);
}

std::unique_ptr<ModelConfig> create_config(const nlohmann::json& config, const double sampleRate)
{
(void)sampleRate;
auto parsed = std::make_unique<SequentialConfig>();
parsed->raw_config = config;
return parsed;
}

static ConfigParserHelper _register_Sequential("Sequential", create_config);

} // namespace sequential
} // namespace nam
51 changes: 51 additions & 0 deletions NAM/sequential.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#pragma once

#include <memory>
#include <vector>

#include "dsp.h"
#include "model_config.h"

namespace nam
{
namespace sequential
{

/// \brief A serial composition of DSP models.
///
/// Each child model processes the output of the previous child. Intermediate
/// buffers are allocated when the maximum buffer size is set and reused by
/// process().
class SequentialModel : public DSP
{
public:
/// \param models Child DSP models in processing order
/// \param expected_sample_rate Expected sample rate in Hz, or -1.0 to derive from children
SequentialModel(std::vector<std::unique_ptr<DSP>> models, double expected_sample_rate);

void process(NAM_SAMPLE** input, NAM_SAMPLE** output, int num_frames) override;
void prewarm() override;
void Reset(double sampleRate, int maxBufferSize) override;
void SetPrewarmOnReset(bool prewarmOnReset) override;
int GetPrewarmSamples() override;

protected:
void SetMaxBufferSize(int maxBufferSize) override;

private:
std::vector<std::unique_ptr<DSP>> _models;
std::vector<std::vector<std::vector<NAM_SAMPLE>>> _stage_buffers;
std::vector<std::vector<NAM_SAMPLE*>> _stage_buffer_ptrs;
};

struct SequentialConfig : public ModelConfig
{
nlohmann::json raw_config;

std::unique_ptr<DSP> create(std::vector<float> weights, double sampleRate) override;
};

std::unique_ptr<ModelConfig> create_config(const nlohmann::json& config, double sampleRate);

} // namespace sequential
} // namespace nam
29 changes: 29 additions & 0 deletions docs/nam_file_version.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,32 @@ The following table shows which versions of NeuralAmpModelerCore support which m
- 0.6.0
* - 0.4.1
- 0.7.0

Sequential models
-----------------

``Sequential`` is an architecture-specific composition of complete child NAM
models. It uses the existing top-level file envelope and does not introduce a
new file version::

{
"version": "0.7.0",
"architecture": "Sequential",
"config": {
"models": [
{"version": "0.7.0", "architecture": "WaveNet", "config": {}, "weights": [], "sample_rate": 48000},
{"version": "0.7.0", "architecture": "Linear", "config": {}, "weights": [], "sample_rate": 48000}
]
},
"weights": [],
"sample_rate": 48000
}

The top-level ``weights`` array is empty because the wrapper has no parameters
of its own. Each entry in ``config.models`` is a complete NAM model carrying
its own architecture, configuration, and weights. The top-level and child
sample rates must be compatible.

Sequential files emitted by the trainer before Core support was completed used
bare child configs and concatenated top-level weights. Those files omitted each
child's architecture and are not supported by this canonical format.
16 changes: 16 additions & 0 deletions tools/run_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include "test/test_noncontiguous_blocks.cpp"
#include "test/test_extensible.cpp"
#include "test/test_container.cpp"
#include "test/test_sequential.cpp"
#include "test/test_render_slim.cpp"
#include "test/test_slimmable_wavenet.cpp"
#include "test/test_a2_fast.cpp"
Expand Down Expand Up @@ -343,6 +344,21 @@ int main()
test_container::test_container_reset_only_resets_active_submodel();
test_container::test_container_switch_resets_before_activation();

// Sequential tests
test_sequential::test_sequential_loads_canonical_container_envelope();
test_sequential::test_sequential_loads_from_file_path();
test_sequential::test_sequential_process_matches_manual_series();
test_sequential::test_sequential_process_is_realtime_safe_after_warmup();
test_sequential::test_sequential_rejects_blocks_larger_than_reset_maximum();
test_sequential::test_sequential_rejects_lowercase_architecture();
test_sequential::test_sequential_accepts_nested_sequential_child();
test_sequential::test_sequential_rejects_empty_models();
test_sequential::test_sequential_rejects_nonempty_top_level_weights();
test_sequential::test_sequential_rejects_legacy_bare_child_configs();
test_sequential::test_sequential_rejects_sample_rate_mismatch();
test_sequential::test_sequential_rejects_top_level_sample_rate_mismatch();
test_sequential::test_sequential_rejects_channel_mismatch();

// Render --slim tests
test_render_slim::test_slim_changes_output();
test_render_slim::test_slim_rejects_non_slimmable();
Expand Down
Loading
Loading