diff --git a/NAM/dsp.h b/NAM/dsp.h index 62a91f40..2fad0a23 100644 --- a/NAM/dsp.h +++ b/NAM/dsp.h @@ -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 weights; ///< Model weights diff --git a/NAM/sequential.cpp b/NAM/sequential.cpp new file mode 100644 index 00000000..c97c1fec --- /dev/null +++ b/NAM/sequential.cpp @@ -0,0 +1,244 @@ +#include "sequential.h" + +#include +#include +#include +#include + +#include "get_dsp.h" + +namespace +{ + +void validate_models_present(const std::vector>& 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>& models) +{ + validate_models_present(models); + return models.front()->NumInputChannels(); +} + +int get_output_channels(const std::vector>& models) +{ + validate_models_present(models); + return models.back()->NumOutputChannels(); +} + +double resolve_expected_sample_rate(const std::vector>& 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>& 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> 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> models; + models.reserve(models_json.size()); + + for (const auto& model_json : models_json) + { + static const std::vector 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>& models, + const std::vector& 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> 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 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::max() - samples) + return std::numeric_limits::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 SequentialConfig::create(std::vector 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(std::move(models), sampleRate); +} + +std::unique_ptr create_config(const nlohmann::json& config, const double sampleRate) +{ + (void)sampleRate; + auto parsed = std::make_unique(); + parsed->raw_config = config; + return parsed; +} + +static ConfigParserHelper _register_Sequential("Sequential", create_config); + +} // namespace sequential +} // namespace nam diff --git a/NAM/sequential.h b/NAM/sequential.h new file mode 100644 index 00000000..ccbc0119 --- /dev/null +++ b/NAM/sequential.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + +#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> 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> _models; + std::vector>> _stage_buffers; + std::vector> _stage_buffer_ptrs; +}; + +struct SequentialConfig : public ModelConfig +{ + nlohmann::json raw_config; + + std::unique_ptr create(std::vector weights, double sampleRate) override; +}; + +std::unique_ptr create_config(const nlohmann::json& config, double sampleRate); + +} // namespace sequential +} // namespace nam diff --git a/docs/nam_file_version.rst b/docs/nam_file_version.rst index ee8f403a..99a37403 100644 --- a/docs/nam_file_version.rst +++ b/docs/nam_file_version.rst @@ -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. diff --git a/tools/run_tests.cpp b/tools/run_tests.cpp index 294cf53b..db82922b 100644 --- a/tools/run_tests.cpp +++ b/tools/run_tests.cpp @@ -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" @@ -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(); diff --git a/tools/test/test_sequential.cpp b/tools/test/test_sequential.cpp new file mode 100644 index 00000000..c3cdf2c5 --- /dev/null +++ b/tools/test/test_sequential.cpp @@ -0,0 +1,292 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "json.hpp" + +#include "NAM/get_dsp.h" +#include "NAM/sequential.h" +#include "allocation_tracking.h" + +namespace test_sequential +{ +namespace +{ + +class TemporaryNamFile +{ +public: + explicit TemporaryNamFile(const nlohmann::json& contents) + : path(std::filesystem::temp_directory_path() / "nam_core_sequential_test.nam") + { + std::ofstream output(path); + output << contents; + } + + ~TemporaryNamFile() { std::filesystem::remove(path); } + + const std::filesystem::path path; +}; + +nlohmann::json make_linear_model(const std::vector& weights, const int receptive_field, + const double sample_rate = 48000.0, const int in_channels = 1, + const int out_channels = 1) +{ + return {{"version", "0.7.0"}, + {"architecture", "Linear"}, + {"config", + {{"receptive_field", receptive_field}, + {"bias", false}, + {"implementation", "direct"}, + {"in_channels", in_channels}, + {"out_channels", out_channels}}}, + {"weights", weights}, + {"sample_rate", sample_rate}}; +} + +nlohmann::json make_sequential_model(const std::vector& models, const double sample_rate = 48000.0) +{ + return {{"version", "0.7.0"}, + {"architecture", "Sequential"}, + {"metadata", nlohmann::json::object()}, + {"config", {{"models", models}}}, + {"weights", nlohmann::json::array()}, + {"sample_rate", sample_rate}}; +} + +std::vector make_input(const int num_samples) +{ + std::vector input(num_samples); + for (int i = 0; i < num_samples; ++i) + input[i] = (NAM_SAMPLE)(0.2 * std::sin(0.037 * i) + 0.05 * std::cos(0.011 * i)); + return input; +} + +std::vector process_model(nam::DSP& dsp, const std::vector& input, + const std::vector& chunk_sizes) +{ + const int max_chunk = *std::max_element(chunk_sizes.begin(), chunk_sizes.end()); + dsp.Reset(48000.0, max_chunk); + + std::vector output(input.size(), (NAM_SAMPLE)0.0); + size_t offset = 0; + size_t chunk_index = 0; + while (offset < input.size()) + { + const int requested = chunk_sizes[chunk_index % chunk_sizes.size()]; + const int count = std::min(requested, (int)(input.size() - offset)); + NAM_SAMPLE* input_ptr = const_cast(&input[offset]); + NAM_SAMPLE* output_ptr = &output[offset]; + dsp.process(&input_ptr, &output_ptr, count); + offset += count; + chunk_index++; + } + + return output; +} + +std::vector process_models_in_series(nam::DSP& first, nam::DSP& second, + const std::vector& input, + const std::vector& chunk_sizes) +{ + const int max_chunk = *std::max_element(chunk_sizes.begin(), chunk_sizes.end()); + first.Reset(48000.0, max_chunk); + second.Reset(48000.0, max_chunk); + + std::vector intermediate(max_chunk, (NAM_SAMPLE)0.0); + std::vector output(input.size(), (NAM_SAMPLE)0.0); + size_t offset = 0; + size_t chunk_index = 0; + while (offset < input.size()) + { + const int requested = chunk_sizes[chunk_index % chunk_sizes.size()]; + const int count = std::min(requested, (int)(input.size() - offset)); + NAM_SAMPLE* first_input_ptr = const_cast(&input[offset]); + NAM_SAMPLE* first_output_ptr = intermediate.data(); + first.process(&first_input_ptr, &first_output_ptr, count); + + NAM_SAMPLE* second_input_ptr = intermediate.data(); + NAM_SAMPLE* second_output_ptr = &output[offset]; + second.process(&second_input_ptr, &second_output_ptr, count); + + offset += count; + chunk_index++; + } + + return output; +} + +bool throws_runtime_error_containing(const std::function& callback, const std::string& expected) +{ + try + { + callback(); + } + catch (const std::runtime_error& error) + { + return std::string(error.what()).find(expected) != std::string::npos; + } + return false; +} + +} // namespace + +void test_sequential_loads_canonical_container_envelope() +{ + const auto model = make_sequential_model({make_linear_model({0.5f}, 1), make_linear_model({-2.0f}, 1)}); + + assert(model.at("version") == "0.7.0"); + assert(model.at("architecture") == "Sequential"); + assert(model.at("weights").empty()); + assert(model.at("sample_rate") == 48000.0); + assert(model.at("config").at("models").at(0).contains("architecture")); + assert(model.at("config").at("models").at(0).contains("weights")); + + nam::dspData returned_config; + auto dsp = nam::get_dsp(model, returned_config); + + assert(dsp != nullptr); + assert(returned_config.weights.empty()); + assert(returned_config.expected_sample_rate == 48000.0); + assert(dsp->GetExpectedSampleRate() == 48000.0); +} + +void test_sequential_loads_from_file_path() +{ + const auto model = make_sequential_model({make_linear_model({0.5f}, 1), make_linear_model({-2.0f}, 1)}); + const TemporaryNamFile file(model); + + auto dsp = nam::get_dsp(file.path); + + assert(dsp != nullptr); + assert(dsp->GetExpectedSampleRate() == 48000.0); +} + +void test_sequential_process_matches_manual_series() +{ + const auto first_model = make_linear_model({0.25f, 0.5f}, 2); + const auto second_model = make_linear_model({-0.75f}, 1); + const auto sequential_model = make_sequential_model({first_model, second_model}); + + auto sequential = nam::get_dsp(sequential_model); + auto first = nam::get_dsp(first_model); + auto second = nam::get_dsp(second_model); + + const auto input = make_input(257); + const std::vector chunks{1, 7, 32, 5, 64}; + const auto actual = process_model(*sequential, input, chunks); + const auto expected = process_models_in_series(*first, *second, input, chunks); + + for (size_t i = 0; i < input.size(); ++i) + assert(std::abs(actual[i] - expected[i]) < 1.0e-7); +} + +void test_sequential_process_is_realtime_safe_after_warmup() +{ + auto dsp = nam::get_dsp(make_sequential_model({make_linear_model({0.5f}, 1), make_linear_model({-2.0f}, 1)})); + constexpr int num_frames = 64; + dsp->Reset(48000.0, num_frames); + + std::vector input(num_frames, (NAM_SAMPLE)0.25); + std::vector output(num_frames, (NAM_SAMPLE)0.0); + NAM_SAMPLE* input_ptr = input.data(); + NAM_SAMPLE* output_ptr = output.data(); + + // Linear initializes an output buffer on its first process call; match its + // existing real-time-safety test convention by warming that path first. + dsp->process(&input_ptr, &output_ptr, num_frames); + + allocation_tracking::run_allocation_test_no_allocations( + nullptr, [&]() { dsp->process(&input_ptr, &output_ptr, num_frames); }, nullptr, + "test_sequential_process_is_realtime_safe_after_warmup"); +} + +void test_sequential_rejects_blocks_larger_than_reset_maximum() +{ + auto dsp = nam::get_dsp(make_sequential_model({make_linear_model({0.5f}, 1), make_linear_model({-2.0f}, 1)})); + dsp->Reset(48000.0, 4); + + std::vector input(8, (NAM_SAMPLE)0.25); + std::vector output(8, (NAM_SAMPLE)0.0); + NAM_SAMPLE* input_ptr = input.data(); + NAM_SAMPLE* output_ptr = output.data(); + + assert(throws_runtime_error_containing([&]() { dsp->process(&input_ptr, &output_ptr, 8); }, "maximum buffer size")); +} + +void test_sequential_rejects_lowercase_architecture() +{ + auto model = make_sequential_model({make_linear_model({1.0f}, 1), make_linear_model({1.0f}, 1)}); + model["architecture"] = "sequential"; + + assert(throws_runtime_error_containing([&]() { nam::get_dsp(model); }, + "No config parser registered for architecture: sequential")); +} + +void test_sequential_accepts_nested_sequential_child() +{ + const auto inner = make_sequential_model({make_linear_model({1.0f}, 1), make_linear_model({1.0f}, 1)}); + const auto outer = make_sequential_model({inner, make_linear_model({1.0f}, 1)}); + + auto dsp = nam::get_dsp(outer); + + assert(dsp != nullptr); + assert(dsp->GetExpectedSampleRate() == 48000.0); +} + +void test_sequential_rejects_empty_models() +{ + const auto model = make_sequential_model({}); + + assert(throws_runtime_error_containing([&]() { auto dsp = nam::get_dsp(model); }, "non-empty")); +} + +void test_sequential_rejects_nonempty_top_level_weights() +{ + auto model = make_sequential_model({make_linear_model({1.0f}, 1), make_linear_model({1.0f}, 1)}); + model["weights"] = nlohmann::json::array({1.0f}); + + assert(throws_runtime_error_containing([&]() { auto dsp = nam::get_dsp(model); }, "top-level weights")); +} + +void test_sequential_rejects_legacy_bare_child_configs() +{ + auto model = make_sequential_model({make_linear_model({1.0f}, 1), make_linear_model({1.0f}, 1)}); + model["config"]["models"] = + nlohmann::json::array({{{"receptive_field", 1}, {"bias", false}}, {{"receptive_field", 1}, {"bias", false}}}); + + assert(throws_runtime_error_containing([&]() { auto dsp = nam::get_dsp(model); }, "complete NAM model")); +} + +void test_sequential_rejects_sample_rate_mismatch() +{ + const auto model = + make_sequential_model({make_linear_model({1.0f}, 1, 48000.0), make_linear_model({1.0f}, 1, 44100.0)}); + + assert(throws_runtime_error_containing([&]() { auto dsp = nam::get_dsp(model); }, "sample rate mismatch")); +} + +void test_sequential_rejects_top_level_sample_rate_mismatch() +{ + const auto model = + make_sequential_model({make_linear_model({1.0f}, 1, 48000.0), make_linear_model({1.0f}, 1, 48000.0)}, 44100.0); + + assert(throws_runtime_error_containing([&]() { auto dsp = nam::get_dsp(model); }, "sample rate mismatch")); +} + +void test_sequential_rejects_channel_mismatch() +{ + const auto model = + make_sequential_model({make_linear_model({1.0f}, 1, 48000.0, 1, 2), make_linear_model({1.0f}, 1, 48000.0, 1, 1)}); + + assert(throws_runtime_error_containing([&]() { auto dsp = nam::get_dsp(model); }, "channel mismatch")); +} + +} // namespace test_sequential diff --git a/tools/test/test_wavenet/test_layer1x1.cpp b/tools/test/test_wavenet/test_layer1x1.cpp index c25504d7..7d0ba028 100644 --- a/tools/test/test_wavenet/test_layer1x1.cpp +++ b/tools/test/test_wavenet/test_layer1x1.cpp @@ -20,16 +20,13 @@ static nam::wavenet::_FiLMParams make_default_film_params() } // Helper function to create a Layer with default FiLM parameters -static nam::wavenet::detail::Layer make_layer(const int condition_size, const int channels, const int bottleneck, - const int kernel_size, const int dilation, - const nam::activations::ActivationConfig& activation_config, - const nam::wavenet::GatingMode gating_mode, const int groups_input, - const int groups_input_mixin, - const nam::wavenet::Layer1x1Params& layer1x1_params, - const nam::wavenet::Head1x1Params& head1x1_params, - const nam::activations::ActivationConfig& secondary_activation_config, - const nam::wavenet::_FiLMParams& layer1x1_post_film_params = - make_default_film_params()) +static nam::wavenet::detail::Layer make_layer( + const int condition_size, const int channels, const int bottleneck, const int kernel_size, const int dilation, + const nam::activations::ActivationConfig& activation_config, const nam::wavenet::GatingMode gating_mode, + const int groups_input, const int groups_input_mixin, const nam::wavenet::Layer1x1Params& layer1x1_params, + const nam::wavenet::Head1x1Params& head1x1_params, + const nam::activations::ActivationConfig& secondary_activation_config, + const nam::wavenet::_FiLMParams& layer1x1_post_film_params = make_default_film_params()) { auto film_params = make_default_film_params(); nam::wavenet::LayerParams layer_params(condition_size, channels, bottleneck, kernel_size, dilation, activation_config, @@ -346,8 +343,8 @@ static Eigen::MatrixXf run_layer1x1_post_film(const nam::wavenet::GatingMode gat // which diverges from the Python model. Changing its scale must move the output in every mode. void test_layer1x1_post_film_is_applied_for_every_gating_mode() { - const nam::wavenet::GatingMode modes[] = {nam::wavenet::GatingMode::NONE, nam::wavenet::GatingMode::GATED, - nam::wavenet::GatingMode::BLENDED}; + const nam::wavenet::GatingMode modes[] = { + nam::wavenet::GatingMode::NONE, nam::wavenet::GatingMode::GATED, nam::wavenet::GatingMode::BLENDED}; for (const auto mode : modes) { const auto unit_scale = run_layer1x1_post_film(mode, 1.0f);