diff --git a/NAM/slimmable_wavenet.cpp b/NAM/slimmable_wavenet.cpp
index 99af681..2357032 100644
--- a/NAM/slimmable_wavenet.cpp
+++ b/NAM/slimmable_wavenet.cpp
@@ -143,12 +143,13 @@ std::vector<float> extract_slimmed_weights(const std::vector<wavenet::LayerArray
     // ---- Per layer ----
     for (int l = 0; l < num_layers; l++)
     {
+      const int kernel_size = p.kernel_sizes[l];
       const bool gated = p.gating_modes[l] != wavenet::GatingMode::NONE;
       const int full_bg = gated ? 2 * full_bn : full_bn;
       const int slim_bg = gated ? 2 * slim_bn : slim_bn;
 
       // conv: Conv1D(channels -> B_g, K, bias=true)
-      extract_conv1d(src, full_ch, full_bg, slim_ch, slim_bg, p.kernel_size, slim);
+      extract_conv1d(src, full_ch, full_bg, slim_ch, slim_bg, kernel_size, slim);
 
       // input_mixin: Conv1x1(condition_size -> B_g, no bias)
       extract_conv1x1(src, p.condition_size, full_bg, p.condition_size, slim_bg, false, slim);
@@ -257,7 +258,7 @@ std::vector<wavenet::LayerArrayParams> modify_params_for_channels(
     int new_head_size = (i < num_arrays - 1) ? new_channels_per_array[i + 1] : p.head_size;
 
     modified.push_back(wavenet::LayerArrayParams(
-      new_input_size, p.condition_size, new_head_size, new_ch, new_bottleneck, p.kernel_size,
+      new_input_size, p.condition_size, new_head_size, new_ch, new_bottleneck, std::vector<int>(p.kernel_sizes),
       std::vector<int>(p.dilations), std::vector<activations::ActivationConfig>(p.activation_configs),
       std::vector<wavenet::GatingMode>(p.gating_modes), p.head_bias, p.groups_input, p.groups_input_mixin,
       p.layer1x1_params, p.head1x1_params, std::vector<activations::ActivationConfig>(p.secondary_activation_configs),
diff --git a/NAM/wavenet.cpp b/NAM/wavenet.cpp
index 04faa45..8ddce1a 100644
--- a/NAM/wavenet.cpp
+++ b/NAM/wavenet.cpp
@@ -312,8 +312,9 @@ nam::wavenet::_LayerArray::_LayerArray(const LayerArrayParams& params)
   const size_t num_layers = params.dilations.size();
   for (size_t i = 0; i < num_layers; i++)
   {
+    const int kernel_size = params.kernel_sizes[i];
     LayerParams layer_params(
-      params.condition_size, params.channels, params.bottleneck, params.kernel_size, params.dilations[i],
+      params.condition_size, params.channels, params.bottleneck, kernel_size, params.dilations[i],
       params.activation_configs[i], params.gating_modes[i], params.groups_input, params.groups_input_mixin,
       params.layer1x1_params, params.head1x1_params, params.secondary_activation_configs[i],
       params.conv_pre_film_params, params.conv_post_film_params, params.input_mixin_pre_film_params,
@@ -729,10 +730,47 @@ nam::wavenet::WaveNetConfig nam::wavenet::parse_config_json(const nlohmann::json
     const int input_size = layer_config["input_size"];
     const int condition_size = layer_config["condition_size"];
     const int head_size = layer_config["head_size"];
-    const int kernel_size = layer_config["kernel_size"];
     const auto dilations = layer_config["dilations"];
     const size_t num_layers = dilations.size();
 
+    // Parse kernel sizes - support legacy single-value kernel_size or new per-layer kernel_sizes
+    const bool has_kernel_size = layer_config.find("kernel_size") != layer_config.end();
+    const bool has_kernel_sizes = layer_config.find("kernel_sizes") != layer_config.end();
+    std::vector<int> kernel_sizes;
+    if (has_kernel_size && has_kernel_sizes)
+    {
+      throw std::runtime_error("Layer array " + std::to_string(i)
+                               + ": only one of kernel_size (int) or kernel_sizes (array) may be provided");
+    }
+    else if (has_kernel_sizes)
+    {
+      const auto& kernel_sizes_json = layer_config["kernel_sizes"];
+      if (!kernel_sizes_json.is_array())
+      {
+        throw std::runtime_error("Layer array " + std::to_string(i) + ": kernel_sizes must be an array");
+      }
+      for (const auto& ks_json : kernel_sizes_json)
+      {
+        kernel_sizes.push_back(ks_json.get<int>());
+      }
+      if (kernel_sizes.size() != num_layers)
+      {
+        throw std::runtime_error("Layer array " + std::to_string(i) + ": kernel_sizes array size ("
+                                 + std::to_string(kernel_sizes.size()) + ") must match dilations size ("
+                                 + std::to_string(num_layers) + ")");
+      }
+    }
+    else if (has_kernel_size)
+    {
+      const int kernel_size = layer_config["kernel_size"].get<int>();
+      kernel_sizes.resize(num_layers, kernel_size);
+    }
+    else
+    {
+      throw std::runtime_error("Layer array " + std::to_string(i)
+                               + ": either kernel_size (int) or kernel_sizes (array) must be provided");
+    }
+
     // Parse activation config(s) - support both single config and array
     std::vector<activations::ActivationConfig> activation_configs;
     if (layer_config["activation"].is_array())
@@ -929,7 +967,7 @@ nam::wavenet::WaveNetConfig nam::wavenet::parse_config_json(const nlohmann::json
     }
 
     wc.layer_array_params.push_back(nam::wavenet::LayerArrayParams(
-      input_size, condition_size, head_size, channels, bottleneck, kernel_size, dilations,
+      input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes), dilations,
       std::move(activation_configs), std::move(gating_modes), head_bias, groups, groups_input_mixin, layer1x1_params,
       head1x1_params, std::move(secondary_activation_configs), conv_pre_film_params, conv_post_film_params,
       input_mixin_pre_film_params, input_mixin_post_film_params, activation_pre_film_params,
diff --git a/NAM/wavenet.h b/NAM/wavenet.h
index ebb551a..6cb43e5 100644
--- a/NAM/wavenet.h
+++ b/NAM/wavenet.h
@@ -431,7 +431,7 @@ class LayerArrayParams
   /// \param head_size_ Size of the head output (after head rechannel)
   /// \param channels_ Number of channels in each layer
   /// \param bottleneck_ Bottleneck size (internal channel count)
-  /// \param kernel_size_ Kernel size for dilated convolutions
+  /// \param kernel_sizes_ Per-layer kernel sizes, one per layer
   /// \param dilations_ Vector of dilation factors, one per layer
   /// \param activation_configs_ Vector of primary activation configurations, one per layer
   /// \param gating_modes_ Vector of gating modes, one per layer
@@ -452,7 +452,7 @@ class LayerArrayParams
   /// \throws std::invalid_argument If dilations, activation_configs, gating_modes, or secondary_activation_configs
   /// sizes don't match
   LayerArrayParams(const int input_size_, const int condition_size_, const int head_size_, const int channels_,
-                   const int bottleneck_, const int kernel_size_, const std::vector<int>&& dilations_,
+                   const int bottleneck_, const std::vector<int>&& kernel_sizes_, const std::vector<int>&& dilations_,
                    const std::vector<activations::ActivationConfig>&& activation_configs_,
                    const std::vector<GatingMode>&& gating_modes_, const bool head_bias_, const int groups_input,
                    const int groups_input_mixin_, const Layer1x1Params& layer1x1_params_,
@@ -467,7 +467,7 @@ class LayerArrayParams
   , head_size(head_size_)
   , channels(channels_)
   , bottleneck(bottleneck_)
-  , kernel_size(kernel_size_)
+  , kernel_sizes(std::move(kernel_sizes_))
   , dilations(std::move(dilations_))
   , activation_configs(std::move(activation_configs_))
   , gating_modes(std::move(gating_modes_))
@@ -487,6 +487,15 @@ class LayerArrayParams
   , head1x1_post_film_params(head1x1_post_film_params_)
   {
     const size_t num_layers = dilations.size();
+    if (kernel_sizes.empty())
+    {
+      throw std::invalid_argument("LayerArrayParams: kernel_sizes must not be empty");
+    }
+    if (kernel_sizes.size() != num_layers)
+    {
+      throw std::invalid_argument("LayerArrayParams: dilations size (" + std::to_string(num_layers)
+                                  + ") must match kernel_sizes size (" + std::to_string(kernel_sizes.size()) + ")");
+    }
     if (activation_configs.size() != num_layers)
     {
       throw std::invalid_argument("LayerArrayParams: dilations size (" + std::to_string(num_layers)
@@ -511,7 +520,7 @@ class LayerArrayParams
   const int head_size; ///< Size of head output (after rechannel)
   const int channels; ///< Number of channels in each layer
   const int bottleneck; ///< Bottleneck size (internal channel count)
-  const int kernel_size; ///< Kernel size for dilated convolutions
+  std::vector<int> kernel_sizes; ///< Per-layer kernel sizes, one per layer
   std::vector<int> dilations; ///< Dilation factors, one per layer
   std::vector<activations::ActivationConfig> activation_configs; ///< Primary activation configurations, one per layer
   std::vector<GatingMode> gating_modes; ///< Gating modes, one per layer
diff --git a/tools/test/test_wavenet/test_condition_processing.cpp b/tools/test/test_wavenet/test_condition_processing.cpp
index 455fd9d..5a7e0d0 100644
--- a/tools/test/test_wavenet/test_condition_processing.cpp
+++ b/tools/test/test_wavenet/test_condition_processing.cpp
@@ -22,10 +22,10 @@ static nam::wavenet::_FiLMParams make_default_film_params()
 // Helper function to create LayerArrayParams with default FiLM parameters
 static nam::wavenet::LayerArrayParams make_layer_array_params(
   const int input_size, const int condition_size, const int head_size, const int channels, const int bottleneck,
-  const int kernel_size, std::vector<int>&& dilations, const nam::activations::ActivationConfig& activation_config,
-  const nam::wavenet::GatingMode gating_mode, const bool head_bias, const int groups_input,
-  const int groups_input_mixin, const nam::wavenet::Layer1x1Params& layer1x1_params,
-  const nam::wavenet::Head1x1Params& head1x1_params,
+  std::vector<int>&& kernel_sizes, std::vector<int>&& dilations,
+  const nam::activations::ActivationConfig& activation_config, const nam::wavenet::GatingMode gating_mode,
+  const bool head_bias, 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)
 {
   auto film_params = make_default_film_params();
@@ -34,11 +34,11 @@ static nam::wavenet::LayerArrayParams make_layer_array_params(
   std::vector<nam::wavenet::GatingMode> gating_modes(dilations.size(), gating_mode);
   std::vector<nam::activations::ActivationConfig> secondary_activation_configs(
     dilations.size(), secondary_activation_config);
-  return nam::wavenet::LayerArrayParams(input_size, condition_size, head_size, channels, bottleneck, kernel_size,
-                                        std::move(dilations), std::move(activation_configs), std::move(gating_modes),
-                                        head_bias, groups_input, groups_input_mixin, layer1x1_params, head1x1_params,
-                                        std::move(secondary_activation_configs), film_params, film_params, film_params,
-                                        film_params, film_params, film_params, film_params, film_params);
+  return nam::wavenet::LayerArrayParams(
+    input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes), std::move(dilations),
+    std::move(activation_configs), std::move(gating_modes), head_bias, groups_input, groups_input_mixin,
+    layer1x1_params, head1x1_params, std::move(secondary_activation_configs), film_params, film_params, film_params,
+    film_params, film_params, film_params, film_params, film_params);
 }
 
 // Helper function to create a simple WaveNet with specified input and output channels
@@ -54,6 +54,7 @@ std::unique_ptr<nam::wavenet::WaveNet> create_simple_wavenet(
   const int bottleneck = channels;
   const int kernel_size = 1;
   std::vector<int> dilations{1};
+  std::vector<int> kernel_sizes(dilations.size(), kernel_size);
   const auto activation = nam::activations::ActivationConfig::simple(nam::activations::ActivationType::ReLU);
   const nam::wavenet::GatingMode gating_mode = nam::wavenet::GatingMode::NONE;
   const bool head_bias = false;
@@ -66,7 +67,7 @@ std::unique_ptr<nam::wavenet::WaveNet> create_simple_wavenet(
   nam::wavenet::Head1x1Params head1x1_params(head1x1_active, channels, head1x1_groups);
 
   nam::wavenet::LayerArrayParams params =
-    make_layer_array_params(input_size, condition_size, head_size, channels, bottleneck, kernel_size,
+    make_layer_array_params(input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes),
                             std::move(dilations), activation, gating_mode, head_bias, groups, groups_input_mixin,
                             layer1x1_params, head1x1_params, nam::activations::ActivationConfig{});
   std::vector<nam::wavenet::LayerArrayParams> layer_array_params;
diff --git a/tools/test/test_wavenet/test_full.cpp b/tools/test/test_wavenet/test_full.cpp
index e383cc7..7f18b68 100644
--- a/tools/test/test_wavenet/test_full.cpp
+++ b/tools/test/test_wavenet/test_full.cpp
@@ -21,10 +21,10 @@ static nam::wavenet::_FiLMParams make_default_film_params()
 // Helper function to create LayerArrayParams with default FiLM parameters
 static nam::wavenet::LayerArrayParams make_layer_array_params(
   const int input_size, const int condition_size, const int head_size, const int channels, const int bottleneck,
-  const int kernel_size, std::vector<int>&& dilations, const nam::activations::ActivationConfig& activation_config,
-  const nam::wavenet::GatingMode gating_mode, const bool head_bias, const int groups_input,
-  const int groups_input_mixin, const nam::wavenet::Layer1x1Params& layer1x1_params,
-  const nam::wavenet::Head1x1Params& head1x1_params,
+  std::vector<int>&& kernel_sizes, std::vector<int>&& dilations,
+  const nam::activations::ActivationConfig& activation_config, const nam::wavenet::GatingMode gating_mode,
+  const bool head_bias, 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)
 {
   auto film_params = make_default_film_params();
@@ -33,11 +33,11 @@ static nam::wavenet::LayerArrayParams make_layer_array_params(
   std::vector<nam::wavenet::GatingMode> gating_modes(dilations.size(), gating_mode);
   std::vector<nam::activations::ActivationConfig> secondary_activation_configs(
     dilations.size(), secondary_activation_config);
-  return nam::wavenet::LayerArrayParams(input_size, condition_size, head_size, channels, bottleneck, kernel_size,
-                                        std::move(dilations), std::move(activation_configs), std::move(gating_modes),
-                                        head_bias, groups_input, groups_input_mixin, layer1x1_params, head1x1_params,
-                                        std::move(secondary_activation_configs), film_params, film_params, film_params,
-                                        film_params, film_params, film_params, film_params, film_params);
+  return nam::wavenet::LayerArrayParams(
+    input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes), std::move(dilations),
+    std::move(activation_configs), std::move(gating_modes), head_bias, groups_input, groups_input_mixin,
+    layer1x1_params, head1x1_params, std::move(secondary_activation_configs), film_params, film_params, film_params,
+    film_params, film_params, film_params, film_params, film_params);
 }
 // Test full WaveNet model
 void test_wavenet_model()
@@ -49,6 +49,7 @@ void test_wavenet_model()
   const int bottleneck = channels;
   const int kernel_size = 1;
   std::vector<int> dilations{1};
+  std::vector<int> kernel_sizes(dilations.size(), kernel_size);
   const auto activation = nam::activations::ActivationConfig::simple(nam::activations::ActivationType::ReLU);
   const nam::wavenet::GatingMode gating_mode = nam::wavenet::GatingMode::NONE;
   const bool head_bias = false;
@@ -62,8 +63,8 @@ void test_wavenet_model()
   nam::wavenet::Head1x1Params head1x1_params(head1x1_active, channels, 1);
   nam::activations::ActivationConfig empty_config{};
   nam::wavenet::LayerArrayParams params = make_layer_array_params(
-    input_size, condition_size, head_size, channels, bottleneck, kernel_size, std::move(dilations), activation,
-    gating_mode, head_bias, groups, groups_input_mixin, layer1x1_params, head1x1_params, empty_config);
+    input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes), std::move(dilations),
+    activation, gating_mode, head_bias, groups, groups_input_mixin, layer1x1_params, head1x1_params, empty_config);
   std::vector<nam::wavenet::LayerArrayParams> layer_array_params;
   layer_array_params.push_back(std::move(params));
 
@@ -128,16 +129,18 @@ void test_wavenet_multiple_arrays()
   const bool head1x1_active = false;
 
   nam::wavenet::Head1x1Params head1x1_params(head1x1_active, channels, 1);
-  layer_array_params.push_back(make_layer_array_params(input_size, condition_size, head_size, channels, bottleneck,
-                                                       kernel_size, std::move(dilations1), activation, gating_mode,
-                                                       head_bias, groups, groups_input_mixin, layer1x1_params,
-                                                       head1x1_params, nam::activations::ActivationConfig{}));
+  std::vector<int> kernel_sizes1(dilations1.size(), kernel_size);
+  layer_array_params.push_back(
+    make_layer_array_params(input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes1),
+                            std::move(dilations1), activation, gating_mode, head_bias, groups, groups_input_mixin,
+                            layer1x1_params, head1x1_params, nam::activations::ActivationConfig{}));
   // Second array (head_size of first must match channels of second)
   std::vector<int> dilations2{1};
-  layer_array_params.push_back(make_layer_array_params(head_size, condition_size, head_size, channels, bottleneck,
-                                                       kernel_size, std::move(dilations2), activation, gating_mode,
-                                                       head_bias, groups, groups_input_mixin, layer1x1_params,
-                                                       head1x1_params, nam::activations::ActivationConfig{}));
+  std::vector<int> kernel_sizes2(dilations2.size(), kernel_size);
+  layer_array_params.push_back(
+    make_layer_array_params(head_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes2),
+                            std::move(dilations2), activation, gating_mode, head_bias, groups, groups_input_mixin,
+                            layer1x1_params, head1x1_params, nam::activations::ActivationConfig{}));
 
   std::vector<float> weights;
   // Array 0: rechannel, layer, head_rechannel
@@ -178,6 +181,7 @@ void test_wavenet_zero_input()
   const int bottleneck = channels;
   const int kernel_size = 1;
   std::vector<int> dilations{1};
+  std::vector<int> kernel_sizes(dilations.size(), kernel_size);
   const auto activation = nam::activations::ActivationConfig::simple(nam::activations::ActivationType::ReLU);
   const nam::wavenet::GatingMode gating_mode = nam::wavenet::GatingMode::NONE;
   const bool head_bias = false;
@@ -190,7 +194,7 @@ void test_wavenet_zero_input()
   nam::wavenet::Head1x1Params head1x1_params(head1x1_active, channels, 1);
 
   nam::wavenet::LayerArrayParams params =
-    make_layer_array_params(input_size, condition_size, head_size, channels, bottleneck, kernel_size,
+    make_layer_array_params(input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes),
                             std::move(dilations), activation, gating_mode, head_bias, groups, groups_input_mixin,
                             layer1x1_params, head1x1_params, nam::activations::ActivationConfig{});
   std::vector<nam::wavenet::LayerArrayParams> layer_array_params;
@@ -229,6 +233,7 @@ void test_wavenet_different_buffer_sizes()
   const int bottleneck = channels;
   const int kernel_size = 1;
   std::vector<int> dilations{1};
+  std::vector<int> kernel_sizes(dilations.size(), kernel_size);
   const auto activation = nam::activations::ActivationConfig::simple(nam::activations::ActivationType::ReLU);
   const nam::wavenet::GatingMode gating_mode = nam::wavenet::GatingMode::NONE;
   const bool head_bias = false;
@@ -241,7 +246,7 @@ void test_wavenet_different_buffer_sizes()
   nam::wavenet::Head1x1Params head1x1_params(head1x1_active, channels, 1);
 
   nam::wavenet::LayerArrayParams params =
-    make_layer_array_params(input_size, condition_size, head_size, channels, bottleneck, kernel_size,
+    make_layer_array_params(input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes),
                             std::move(dilations), activation, gating_mode, head_bias, groups, groups_input_mixin,
                             layer1x1_params, head1x1_params, nam::activations::ActivationConfig{});
   std::vector<nam::wavenet::LayerArrayParams> layer_array_params;
@@ -283,6 +288,7 @@ void test_wavenet_prewarm()
   const int bottleneck = channels;
   const int kernel_size = 3;
   std::vector<int> dilations{1, 2, 4};
+  std::vector<int> kernel_sizes(dilations.size(), kernel_size);
   const auto activation = nam::activations::ActivationConfig::simple(nam::activations::ActivationType::ReLU);
   const nam::wavenet::GatingMode gating_mode = nam::wavenet::GatingMode::NONE;
   const bool head_bias = false;
@@ -296,7 +302,7 @@ void test_wavenet_prewarm()
   nam::wavenet::Head1x1Params head1x1_params(head1x1_active, channels, 1);
 
   nam::wavenet::LayerArrayParams params =
-    make_layer_array_params(input_size, condition_size, head_size, channels, bottleneck, kernel_size,
+    make_layer_array_params(input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes),
                             std::move(dilations), activation, gating_mode, head_bias, groups, groups_input_mixin,
                             layer1x1_params, head1x1_params, nam::activations::ActivationConfig{});
   std::vector<nam::wavenet::LayerArrayParams> layer_array_params;
diff --git a/tools/test/test_wavenet/test_kernel_sizes.cpp b/tools/test/test_wavenet/test_kernel_sizes.cpp
new file mode 100644
index 0000000..e266981
--- /dev/null
+++ b/tools/test/test_wavenet/test_kernel_sizes.cpp
@@ -0,0 +1,119 @@
+// Tests for per-layer kernel sizes in WaveNet LayerArrayParams and config parsing
+
+#include <cassert>
+#include <vector>
+
+#include "json.hpp"
+
+#include "NAM/wavenet.h"
+
+namespace test_wavenet
+{
+namespace test_kernel_sizes
+{
+
+// Verify that an integer kernel_size in JSON is expanded to all layers
+void test_kernel_size_int_compat()
+{
+  const std::string configStr = R"({
+    "version": "0.5.4",
+    "metadata": {},
+    "architecture": "WaveNet",
+    "config": {
+      "layers": [{
+        "input_size": 1,
+        "condition_size": 1,
+        "head_size": 1,
+        "channels": 1,
+        "kernel_size": 3,
+        "dilations": [1, 2, 4],
+        "activation": "ReLU",
+        "gated": false,
+        "head_bias": false
+      }],
+      "head_scale": 1.0
+    },
+    "weights": [1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0],
+    "sample_rate": 48000
+  })";
+
+  const nlohmann::json j = nlohmann::json::parse(configStr);
+  const auto wc = nam::wavenet::parse_config_json(j["config"], 48000.0);
+
+  assert(wc.layer_array_params.size() == 1);
+  const nam::wavenet::LayerArrayParams& p = wc.layer_array_params[0];
+
+  const int expected_kernel_size = 3;
+  const std::vector<int> expected_dilations{1, 2, 4};
+
+  assert(p.kernel_size == expected_kernel_size);
+  assert(p.dilations == expected_dilations);
+  assert(p.kernel_sizes.size() == expected_dilations.size());
+  for (size_t i = 0; i < p.kernel_sizes.size(); ++i)
+  {
+    assert(p.kernel_sizes[i] == expected_kernel_size);
+  }
+
+  // Verify that receptive field computation uses the (uniform) kernel size
+  nam::wavenet::_LayerArray array(p);
+  const long receptive_field = array.get_receptive_field();
+
+  long expected_receptive_field = 0;
+  for (size_t i = 0; i < expected_dilations.size(); ++i)
+  {
+    expected_receptive_field += expected_dilations[i] * (expected_kernel_size - 1);
+  }
+  assert(receptive_field == expected_receptive_field);
+}
+
+// Verify that an array kernel_size in JSON is honored per layer
+void test_kernel_size_per_layer_array()
+{
+  const std::string configStr = R"({
+    "version": "0.5.4",
+    "metadata": {},
+    "architecture": "WaveNet",
+    "config": {
+      "layers": [{
+        "input_size": 1,
+        "condition_size": 1,
+        "head_size": 1,
+        "channels": 1,
+        "kernel_size": [2, 3, 5],
+        "dilations": [1, 2, 4],
+        "activation": "ReLU",
+        "gated": false,
+        "head_bias": false
+      }],
+      "head_scale": 1.0
+    },
+    "weights": [1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0],
+    "sample_rate": 48000
+  })";
+
+  const nlohmann::json j = nlohmann::json::parse(configStr);
+  const auto wc = nam::wavenet::parse_config_json(j["config"], 48000.0);
+
+  assert(wc.layer_array_params.size() == 1);
+  const nam::wavenet::LayerArrayParams& p = wc.layer_array_params[0];
+
+  const std::vector<int> expected_kernel_sizes{2, 3, 5};
+  const std::vector<int> expected_dilations{1, 2, 4};
+
+  assert(p.kernel_sizes == expected_kernel_sizes);
+  assert(p.dilations == expected_dilations);
+
+  // Verify that receptive field computation uses the per-layer kernel sizes
+  nam::wavenet::_LayerArray array(p);
+  const long receptive_field = array.get_receptive_field();
+
+  long expected_receptive_field = 0;
+  for (size_t i = 0; i < expected_dilations.size(); ++i)
+  {
+    expected_receptive_field += expected_dilations[i] * (expected_kernel_sizes[i] - 1);
+  }
+  assert(receptive_field == expected_receptive_field);
+}
+
+} // namespace test_kernel_sizes
+} // namespace test_wavenet
diff --git a/tools/test/test_wavenet/test_layer_array.cpp b/tools/test/test_wavenet/test_layer_array.cpp
index b8445c9..f91ff98 100644
--- a/tools/test/test_wavenet/test_layer_array.cpp
+++ b/tools/test/test_wavenet/test_layer_array.cpp
@@ -36,8 +36,9 @@ static nam::wavenet::_LayerArray make_layer_array(const int input_size, const in
   std::vector<nam::activations::ActivationConfig> secondary_activation_configs(
     dilations.size(), secondary_activation_config);
   std::vector<int> dilations_copy = dilations; // Make a copy since we need to move it
+  std::vector<int> kernel_sizes(dilations.size(), kernel_size);
   nam::wavenet::LayerArrayParams params(
-    input_size, condition_size, head_size, channels, bottleneck, kernel_size, std::move(dilations_copy),
+    input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes), std::move(dilations_copy),
     std::move(activation_configs), std::move(gating_modes), head_bias, groups_input, groups_input_mixin,
     layer1x1_params, head1x1_params, std::move(secondary_activation_configs), film_params, film_params, film_params,
     film_params, film_params, film_params, film_params, film_params);
@@ -220,11 +221,12 @@ void test_layer_array_different_activations()
   assert(secondary_activation_configs.size() == dilations.size());
 
   auto film_params = make_default_film_params();
-  nam::wavenet::LayerArrayParams params(input_size, condition_size, head_size, channels, bottleneck, kernel_size,
-                                        std::move(dilations), std::move(activation_configs), std::move(gating_modes),
-                                        head_bias, groups, groups_input_mixin, layer1x1_params, head1x1_params,
-                                        std::move(secondary_activation_configs), film_params, film_params, film_params,
-                                        film_params, film_params, film_params, film_params, film_params);
+  std::vector<int> kernel_sizes(dilations.size(), kernel_size);
+  nam::wavenet::LayerArrayParams params(
+    input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes), std::move(dilations),
+    std::move(activation_configs), std::move(gating_modes), head_bias, groups, groups_input_mixin, layer1x1_params,
+    head1x1_params, std::move(secondary_activation_configs), film_params, film_params, film_params, film_params,
+    film_params, film_params, film_params, film_params);
   nam::wavenet::_LayerArray layer_array(params);
 
   const int numFrames = 4;
@@ -302,11 +304,12 @@ void test_layer_array_different_activations()
     dilations_all_relu.size(), nam::wavenet::GatingMode::NONE);
   std::vector<nam::activations::ActivationConfig> all_empty_secondary_configs(
     dilations_all_relu.size(), nam::activations::ActivationConfig{});
+  std::vector<int> kernel_sizes_all_relu(dilations_all_relu.size(), kernel_size);
   nam::wavenet::LayerArrayParams params_all_relu(
-    input_size, condition_size, head_size, channels, bottleneck, kernel_size, std::move(dilations_all_relu),
-    std::move(all_relu_configs), std::move(all_none_gating_modes), head_bias, groups, groups_input_mixin,
-    layer1x1_params, head1x1_params, std::move(all_empty_secondary_configs), film_params, film_params, film_params,
-    film_params, film_params, film_params, film_params, film_params);
+    input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes_all_relu),
+    std::move(dilations_all_relu), std::move(all_relu_configs), std::move(all_none_gating_modes), head_bias, groups,
+    groups_input_mixin, layer1x1_params, head1x1_params, std::move(all_empty_secondary_configs), film_params,
+    film_params, film_params, film_params, film_params, film_params, film_params, film_params);
   nam::wavenet::_LayerArray layer_array_all_relu(params_all_relu);
   layer_array_all_relu.SetMaxBufferSize(numFrames);
 
diff --git a/tools/test/test_wavenet/test_real_time_safe.cpp b/tools/test/test_wavenet/test_real_time_safe.cpp
index 7fbd5a6..9bea8e9 100644
--- a/tools/test/test_wavenet/test_real_time_safe.cpp
+++ b/tools/test/test_wavenet/test_real_time_safe.cpp
@@ -58,8 +58,9 @@ static nam::wavenet::_LayerArray make_layer_array(const int input_size, const in
   std::vector<nam::activations::ActivationConfig> secondary_activation_configs(
     dilations.size(), secondary_activation_config);
   std::vector<int> dilations_copy = dilations; // Make a copy since we need to move it
+  std::vector<int> kernel_sizes(dilations.size(), kernel_size);
   nam::wavenet::LayerArrayParams params(
-    input_size, condition_size, head_size, channels, bottleneck, kernel_size, std::move(dilations_copy),
+    input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes), std::move(dilations_copy),
     std::move(activation_configs), std::move(gating_modes), head_bias, groups_input, groups_input_mixin,
     layer1x1_params, head1x1_params, std::move(secondary_activation_configs), film_params, film_params, film_params,
     film_params, film_params, film_params, film_params, film_params);
@@ -69,10 +70,10 @@ static nam::wavenet::_LayerArray make_layer_array(const int input_size, const in
 // Helper function to create LayerArrayParams with default FiLM parameters
 static nam::wavenet::LayerArrayParams make_layer_array_params(
   const int input_size, const int condition_size, const int head_size, const int channels, const int bottleneck,
-  const int kernel_size, std::vector<int>&& dilations, const nam::activations::ActivationConfig& activation_config,
-  const nam::wavenet::GatingMode gating_mode, const bool head_bias, const int groups_input,
-  const int groups_input_mixin, const nam::wavenet::Layer1x1Params& layer1x1_params,
-  const nam::wavenet::Head1x1Params& head1x1_params,
+  std::vector<int>&& kernel_sizes, std::vector<int>&& dilations,
+  const nam::activations::ActivationConfig& activation_config, const nam::wavenet::GatingMode gating_mode,
+  const bool head_bias, 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)
 {
   auto film_params = make_default_film_params();
@@ -81,11 +82,11 @@ static nam::wavenet::LayerArrayParams make_layer_array_params(
   std::vector<nam::wavenet::GatingMode> gating_modes(dilations.size(), gating_mode);
   std::vector<nam::activations::ActivationConfig> secondary_activation_configs(
     dilations.size(), secondary_activation_config);
-  return nam::wavenet::LayerArrayParams(input_size, condition_size, head_size, channels, bottleneck, kernel_size,
-                                        std::move(dilations), std::move(activation_configs), std::move(gating_modes),
-                                        head_bias, groups_input, groups_input_mixin, layer1x1_params, head1x1_params,
-                                        std::move(secondary_activation_configs), film_params, film_params, film_params,
-                                        film_params, film_params, film_params, film_params, film_params);
+  return nam::wavenet::LayerArrayParams(
+    input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes), std::move(dilations),
+    std::move(activation_configs), std::move(gating_modes), head_bias, groups_input, groups_input_mixin,
+    layer1x1_params, head1x1_params, std::move(secondary_activation_configs), film_params, film_params, film_params,
+    film_params, film_params, film_params, film_params, film_params);
 }
 
 // Helper function to create a Layer with all FiLMs active
@@ -1003,19 +1004,21 @@ void test_process_realtime_safe()
   std::vector<nam::wavenet::LayerArrayParams> layer_array_params;
   // First layer array
   std::vector<int> dilations1{1};
+  std::vector<int> kernel_sizes1(dilations1.size(), kernel_size);
   const int bottleneck = channels;
   nam::wavenet::Layer1x1Params layer1x1_params(true, 1);
   nam::wavenet::Head1x1Params head1x1_params(false, channels, 1);
-  layer_array_params.push_back(make_layer_array_params(input_size, condition_size, head_size, channels, bottleneck,
-                                                       kernel_size, std::move(dilations1), activation, gating_mode,
-                                                       head_bias, groups, groups_input_mixin, layer1x1_params,
-                                                       head1x1_params, nam::activations::ActivationConfig{}));
+  layer_array_params.push_back(
+    make_layer_array_params(input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes1),
+                            std::move(dilations1), activation, gating_mode, head_bias, groups, groups_input_mixin,
+                            layer1x1_params, head1x1_params, nam::activations::ActivationConfig{}));
   // Second layer array (head_size of first must match channels of second)
   std::vector<int> dilations2{1};
-  layer_array_params.push_back(make_layer_array_params(head_size, condition_size, head_size, channels, bottleneck,
-                                                       kernel_size, std::move(dilations2), activation, gating_mode,
-                                                       head_bias, groups, groups_input_mixin, layer1x1_params,
-                                                       head1x1_params, nam::activations::ActivationConfig{}));
+  std::vector<int> kernel_sizes2(dilations2.size(), kernel_size);
+  layer_array_params.push_back(
+    make_layer_array_params(head_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes2),
+                            std::move(dilations2), activation, gating_mode, head_bias, groups, groups_input_mixin,
+                            layer1x1_params, head1x1_params, nam::activations::ActivationConfig{}));
 
   // Weights: Array 0: rechannel(1), layer(conv:1+1, input_mixin:1, 1x1:1+1), head_rechannel(1)
   //          Array 1: same structure
@@ -1086,10 +1089,11 @@ void test_process_3in_2out_realtime_safe()
 
   std::vector<nam::wavenet::LayerArrayParams> layer_array_params;
   std::vector<int> dilations1{1};
-  layer_array_params.push_back(make_layer_array_params(input_size, condition_size, head_size, channels, bottleneck,
-                                                       kernel_size, std::move(dilations1), activation, gating_mode,
-                                                       head_bias, groups, groups_input_mixin, layer1x1_params,
-                                                       head1x1_params, nam::activations::ActivationConfig{}));
+  std::vector<int> kernel_sizes1(dilations1.size(), kernel_size);
+  layer_array_params.push_back(
+    make_layer_array_params(input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes1),
+                            std::move(dilations1), activation, gating_mode, head_bias, groups, groups_input_mixin,
+                            layer1x1_params, head1x1_params, nam::activations::ActivationConfig{}));
 
   // Calculate weights:
   // _rechannel: Conv1x1(3, 4, bias=false) = 3*4 = 12 weights
diff --git a/tools/test/test_wavenet_configurable_gating.cpp b/tools/test/test_wavenet_configurable_gating.cpp
index 77870d7..ee07d94 100644
--- a/tools/test/test_wavenet_configurable_gating.cpp
+++ b/tools/test/test_wavenet_configurable_gating.cpp
@@ -36,10 +36,10 @@ static nam::wavenet::_Layer make_layer(const int condition_size, const int chann
 // Helper function to create LayerArrayParams with default FiLM parameters
 static nam::wavenet::LayerArrayParams make_layer_array_params(
   const int input_size, const int condition_size, const int head_size, const int channels, const int bottleneck,
-  const int kernel_size, std::vector<int>&& dilations, const nam::activations::ActivationConfig& activation_config,
-  const nam::wavenet::GatingMode gating_mode, const bool head_bias, const int groups_input,
-  const int groups_input_mixin, const nam::wavenet::Layer1x1Params& layer1x1_params,
-  const nam::wavenet::Head1x1Params& head1x1_params,
+  std::vector<int>&& kernel_sizes, std::vector<int>&& dilations,
+  const nam::activations::ActivationConfig& activation_config, const nam::wavenet::GatingMode gating_mode,
+  const bool head_bias, 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)
 {
   auto film_params = make_default_film_params();
@@ -48,11 +48,11 @@ static nam::wavenet::LayerArrayParams make_layer_array_params(
   std::vector<nam::wavenet::GatingMode> gating_modes(dilations.size(), gating_mode);
   std::vector<nam::activations::ActivationConfig> secondary_activation_configs(
     dilations.size(), secondary_activation_config);
-  return nam::wavenet::LayerArrayParams(input_size, condition_size, head_size, channels, bottleneck, kernel_size,
-                                        std::move(dilations), std::move(activation_configs), std::move(gating_modes),
-                                        head_bias, groups_input, groups_input_mixin, layer1x1_params, head1x1_params,
-                                        std::move(secondary_activation_configs), film_params, film_params, film_params,
-                                        film_params, film_params, film_params, film_params, film_params);
+  return nam::wavenet::LayerArrayParams(
+    input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes), std::move(dilations),
+    std::move(activation_configs), std::move(gating_modes), head_bias, groups_input, groups_input_mixin,
+    layer1x1_params, head1x1_params, std::move(secondary_activation_configs), film_params, film_params, film_params,
+    film_params, film_params, film_params, film_params, film_params);
 }
 
 // Helper function to create a LayerArray with default FiLM parameters
@@ -73,8 +73,9 @@ static nam::wavenet::_LayerArray make_layer_array(const int input_size, const in
   std::vector<nam::activations::ActivationConfig> secondary_activation_configs(
     dilations.size(), secondary_activation_config);
   std::vector<int> dilations_copy = dilations; // Make a copy since we need to move it
+  std::vector<int> kernel_sizes(dilations.size(), kernel_size);
   nam::wavenet::LayerArrayParams params(
-    input_size, condition_size, head_size, channels, bottleneck, kernel_size, std::move(dilations_copy),
+    input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes), std::move(dilations_copy),
     std::move(activation_configs), std::move(gating_modes), head_bias, groups_input, groups_input_mixin,
     layer1x1_params, head1x1_params, std::move(secondary_activation_configs), film_params, film_params, film_params,
     film_params, film_params, film_params, film_params, film_params);
@@ -166,8 +167,9 @@ class TestConfigurableGating
 
     // Test with different gating activations
     auto tanh_config = nam::activations::ActivationConfig::simple(nam::activations::ActivationType::Tanh);
+    std::vector<int> kernel_sizes_2{kernel_size, kernel_size};
     auto params_gated =
-      make_layer_array_params(input_size, condition_size, head_size, channels, bottleneck, kernel_size,
+      make_layer_array_params(input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes_2),
                               std::vector<int>{1, 2}, activation, nam::wavenet::GatingMode::GATED, head_bias,
                               groups_input, groups_input_mixin, layer1x1_params, head1x1_params, tanh_config);
 
@@ -180,8 +182,9 @@ class TestConfigurableGating
 
     // Test with different blending activations
     auto relu_config = nam::activations::ActivationConfig::simple(nam::activations::ActivationType::ReLU);
+    std::vector<int> kernel_sizes_2b{kernel_size, kernel_size};
     auto params_blended =
-      make_layer_array_params(input_size, condition_size, head_size, channels, bottleneck, kernel_size,
+      make_layer_array_params(input_size, condition_size, head_size, channels, bottleneck, std::move(kernel_sizes_2b),
                               std::vector<int>{1, 2}, activation, nam::wavenet::GatingMode::BLENDED, head_bias,
                               groups_input, groups_input_mixin, layer1x1_params, head1x1_params, relu_config);
 
