nam_rs/loader/nam_json/model.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 Fábio Henrique de Lima Silva (fhl.bsb@gmail.com) All rights reserved.
3
4//! Data structures for the `.nam` format (JSON).
5//!
6//! Contains the structs that model the neural model file.
7
8use serde::{Deserialize, Serialize};
9
10use super::validation::{
11 deserialize_sample_rate, deserialize_submodels, deserialize_training, deserialize_weights,
12};
13
14/// Structure representing a date and time associated with the model's metadata.
15#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Default)]
16pub struct NamDate {
17 /// Year.
18 pub year: Option<i32>,
19 /// Month.
20 pub month: Option<i32>,
21 /// Day.
22 pub day: Option<i32>,
23 /// Hour.
24 pub hour: Option<i32>,
25 /// Minute.
26 pub minute: Option<i32>,
27 /// Second.
28 pub second: Option<i32>,
29}
30
31/// Optional metadata contained at the end of the `.nam` format.
32#[derive(Deserialize, Serialize, Debug, Clone, Default)]
33pub struct NamMetadata {
34 /// Model authorship or export date.
35 pub date: Option<NamDate>,
36 /// The model name.
37 pub name: Option<String>,
38 /// Who made/trained the model.
39 pub modeled_by: Option<String>,
40 /// Manufacturer of the original equipment (e.g. Fender).
41 pub gear_make: Option<String>,
42 /// The model of the original equipment (e.g. Deluxe Reverb).
43 pub gear_model: Option<String>,
44 /// What type of equipment is this? Options: "amp", "pedal", "pedal_amp", "amp_cab", "amp_pedal_cab", "preamp", and "studio".
45 pub gear_type: Option<String>,
46 /// What style of equipment? Options: "clean", "overdrive", "crunch", "hi_gain", and "fuzz".
47 pub tone_type: Option<String>,
48 /// Optional documentation about Pydantic training configuration.
49 #[serde(default, deserialize_with = "deserialize_training")]
50 pub training: Option<serde_json::Value>,
51 /// Expected input level for the model (dBu). Used in input gain staging.
52 pub input_level_dbu: Option<f32>,
53 /// Expected output level for the model (dBu). Used in output gain staging.
54 pub output_level_dbu: Option<f32>,
55 /// Overall recorded loudness.
56 pub loudness: Option<f32>,
57}
58
59/// Helper struct for deserializing `NamLayerConfig` — mirrors the JSON shape
60/// and captures all typed fields while also storing the raw JSON value for
61/// complex nested checks (activation arrays, FiLM keys, etc.).
62#[derive(Deserialize)]
63#[serde(rename_all = "snake_case")]
64struct NamLayerConfigHelper {
65 input_size: Option<usize>,
66 condition_size: Option<usize>,
67 head_size: Option<usize>,
68 channels: Option<usize>,
69 kernel_size: Option<usize>,
70 kernel_sizes: Option<Vec<usize>>,
71 dilations: Option<Vec<usize>>,
72 activation: Option<serde_json::Value>,
73 gated: Option<bool>,
74 head_bias: Option<bool>,
75 bottleneck: Option<usize>,
76}
77
78/// The structural configuration of a single layer of the network (whether WaveNet or LSTM).
79#[derive(Serialize, Debug, Clone, Default)]
80pub struct NamLayerConfig {
81 /// Optional: Input tensor size.
82 pub input_size: Option<usize>,
83 /// Optional: Conditioning tensor size (e.g. external parameters).
84 pub condition_size: Option<usize>,
85 /// Optional: Output tensor size (head size).
86 pub head_size: Option<usize>,
87 /// Optional: Number of internal channels (e.g. 16 or 24).
88 pub channels: Option<usize>,
89 /// Optional: Convolutional kernel size.
90 pub kernel_size: Option<usize>,
91 /// Optional: Per-layer kernel sizes (A2 architecture — 23 elements).
92 pub kernel_sizes: Option<Vec<usize>>,
93 /// Optional: Array of dilation factors.
94 pub dilations: Option<Vec<usize>>,
95 /// Optional: Activation function (e.g. "Tanh" for A1, array of objects for A2 LeakyReLU).
96 pub activation: Option<String>,
97 /// Optional: Whether the architecture uses gating.
98 pub gated: Option<bool>,
99 /// Optional: Whether the processing head has bias.
100 pub head_bias: Option<bool>,
101 /// Optional: Bottleneck size (internal channel count for A2).
102 pub bottleneck: Option<usize>,
103 /// Raw JSON value for this layer, preserved for complex shape
104 /// checks (activation arrays, FiLM keys, condition_dsp, etc.).
105 /// `None` when the struct is constructed directly (not via JSON deserialization).
106 #[serde(skip)]
107 pub layer_raw: Option<serde_json::Value>,
108}
109
110impl NamLayerConfig {
111 /// Parses the activation configuration from the preserved raw JSON.
112 ///
113 /// Extracts per-layer `activation`, `gating_mode`, and `secondary_activation`
114 /// arrays for `num_layers` expected entries (23 for standard A2).
115 /// Returns `None` when `layer_raw` is absent or the `activation` array
116 /// is missing/invalid.
117 ///
118 /// This is the primary entry point for the dynamic A2 engine
119 /// to obtain the heterogeneous activation configuration at model build time.
120 pub fn parse_activation_config(
121 &self,
122 num_layers: usize,
123 ) -> Option<super::activation_parser::LayerActivationConfig> {
124 let raw = self.layer_raw.as_ref()?;
125 super::activation_parser::parse_layer_activations(raw, num_layers)
126 }
127}
128
129impl<'de> Deserialize<'de> for NamLayerConfig {
130 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
131 where
132 D: serde::Deserializer<'de>,
133 {
134 let raw_value = serde_json::Value::deserialize(deserializer)?;
135 let helper: NamLayerConfigHelper =
136 serde_json::from_value(raw_value.clone()).map_err(serde::de::Error::custom)?;
137
138 let activation = match helper.activation {
139 Some(serde_json::Value::String(s)) => Some(s),
140 Some(serde_json::Value::Array(_)) => None,
141 Some(serde_json::Value::Object(_)) => None,
142 Some(serde_json::Value::Null) | None => None,
143 _ => {
144 return Err(serde::de::Error::custom(
145 "unsupported activation format: expected a string, array, object, or null",
146 ));
147 }
148 };
149
150 Ok(NamLayerConfig {
151 input_size: helper.input_size,
152 condition_size: helper.condition_size,
153 head_size: helper.head_size,
154 channels: helper.channels,
155 kernel_size: helper.kernel_size,
156 kernel_sizes: helper.kernel_sizes,
157 dilations: helper.dilations,
158 activation,
159 gated: helper.gated,
160 head_bias: helper.head_bias,
161 bottleneck: helper.bottleneck,
162 layer_raw: Some(raw_value),
163 })
164 }
165}
166
167/// Implementation mode for Linear architecture convolution.
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
169pub enum LinearImplementation {
170 /// Auto-select based on receptive field size.
171 #[default]
172 Auto,
173 /// Direct time-domain convolution (full receptive field dot product).
174 Direct,
175 /// FFT partitioned convolution (zero-latency hybrid: direct head + FFT tail).
176 Fft,
177}
178
179impl LinearImplementation {}
180
181impl std::str::FromStr for LinearImplementation {
182 type Err = ();
183
184 fn from_str(s: &str) -> Result<Self, Self::Err> {
185 let lower = s.to_lowercase();
186 match lower.as_str() {
187 "auto" => Ok(Self::Auto),
188 "direct" => Ok(Self::Direct),
189 "fft" | "partitioned_fft" | "partitioned-fft" => Ok(Self::Fft),
190 "legacy" | "old" => Ok(Self::Auto),
191 _ => Err(()),
192 }
193 }
194}
195
196#[cfg(test)]
197#[path = "model_test.rs"]
198mod tests;
199
200/// Weight layout options supported in the `.namb` format.
201#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
202#[repr(u8)]
203pub enum WeightsLayout {
204 /// Original layout (standard NAM): `[Gate][H][IH]` for LSTM, `[OUT][IN][K]` for Conv1D.
205 #[default]
206 Original = 0,
207 /// Layout optimized for LSTM: `[Gate][IH][H]`.
208 GateMajorLstm = 1,
209 /// Layout optimized for WaveNet: Interleaved 4-Wide (`[OUT/4][K][IN][4]`).
210 Interleaved4WaveNet = 2,
211}
212
213/// Configuration for the post-stack head sub-object (WaveNet / ConvNet).
214///
215/// Mirrors the `_Head` structure in NAMCore's `convnet.h:108-118`.
216/// Contains a Conv1D + activation that processes the signal after the
217/// stack of blocks, before the output.
218#[derive(Serialize, Debug, Clone, PartialEq, Default)]
219pub struct HeadConfig {
220 /// Number of internal channels for the head Conv1D.
221 pub channels: Option<usize>,
222 /// Whether the head Conv1D has a bias term.
223 pub bias: Option<bool>,
224 /// Number of output channels (typically 1 for mono).
225 pub out_channels: Option<usize>,
226 /// Activation function name (e.g. "Tanh", "ReLU").
227 pub activation: Option<String>,
228 /// Kernel size for the head Conv1D.
229 pub kernel_size: Option<usize>,
230}
231
232/// The internal configuration of the architecture node in the JSON.
233#[derive(Deserialize, Serialize, Debug, Clone, Default)]
234pub struct NamConfig {
235 /// List of stacked layer configurations (present in WaveNet, ConvNet; absent in LSTM).
236 #[serde(default)]
237 pub layers: Vec<NamLayerConfig>,
238 /// Number of input channels (WaveNet A2). Defaults to 1 when absent.
239 #[serde(default)]
240 pub in_channels: Option<usize>,
241 /// Number of output channels. Defaults to 1 when absent (nam-rs only supports mono).
242 #[serde(default)]
243 pub out_channels: Option<usize>,
244 /// Post-stack head sub-object (WaveNet with head / ConvNet).
245 /// `null` in JSON becomes `Some(Value::Null)` (head present but empty).
246 /// Absent in JSON becomes `None`.
247 /// Non-null values contain the head configuration object.
248 pub head: Option<serde_json::Value>,
249 /// Fine scale over the network summation (WaveNet / ConvNet).
250 pub head_scale: Option<f32>,
251 /// Number of layers (for LSTMs in C++ it is the layer count, or explicit)
252 pub num_layers: Option<usize>,
253 /// Hidden size of the LSTM cell
254 pub hidden_size: Option<usize>,
255 /// Receptive field size (Linear architecture only).
256 pub receptive_field: Option<usize>,
257 /// Whether a bias scalar is included (Linear architecture only).
258 pub bias: Option<bool>,
259 /// Submodel entries for SlimmableContainer architecture.
260 /// Each entry is `{"max_value": f32, "model": { ... full inner .nam JSON ... }}`.
261 #[serde(
262 default,
263 skip_serializing_if = "Option::is_none",
264 deserialize_with = "deserialize_submodels"
265 )]
266 pub submodels: Option<Vec<serde_json::Value>>,
267 /// Nested condition DSP sub-model (raw JSON).
268 ///
269 /// Self-contained `.nam` model with its own `version`, `architecture`,
270 /// `config`, `weights`, and `sample_rate`. Parsed and built independently
271 /// before the main model's layer arrays are processed. The sub-model's
272 /// output channels serve as the condition input for the main model's layers
273 /// (C++ `_process_condition()` mirror, wavenet/model.cpp:692-722).
274 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub condition_dsp: Option<serde_json::Value>,
276 /// Linear convolution implementation mode ("Auto", "Direct", "Fft").
277 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub implementation: Option<String>,
279 /// ConvNet C++ flat format: scalar channel count (config root level).
280 #[serde(default, rename = "channels")]
281 pub conv_channels: Option<usize>,
282 /// ConvNet C++ flat format: global dilations array (one per block).
283 #[serde(default, rename = "dilations")]
284 pub conv_dilations: Option<Vec<usize>>,
285 /// ConvNet C++ flat format: global batchnorm flag.
286 #[serde(default, rename = "batchnorm")]
287 pub conv_batchnorm: Option<bool>,
288}
289
290impl NamConfig {
291 /// Extracts a typed `HeadConfig` from the raw `head` JSON value.
292 ///
293 /// Returns `None` if the head field is absent, `null`, or not an object.
294 pub fn parse_head(&self) -> Option<HeadConfig> {
295 let val = self.head.as_ref()?;
296 if val.is_null() || !val.is_object() {
297 return None;
298 }
299 let channels = val
300 .get("channels")
301 .and_then(|v| v.as_u64())
302 .and_then(|v| usize::try_from(v).ok());
303 let bias = val.get("bias").and_then(|v| v.as_bool());
304 let out_channels = val
305 .get("out_channels")
306 .and_then(|v| v.as_u64())
307 .and_then(|v| usize::try_from(v).ok());
308 let activation = val
309 .get("activation")
310 .and_then(|v| v.as_str())
311 .map(String::from);
312 let kernel_size = val
313 .get("kernel_size")
314 .and_then(|v| v.as_u64())
315 .and_then(|v| usize::try_from(v).ok());
316 Some(HeadConfig {
317 channels,
318 bias,
319 out_channels,
320 activation,
321 kernel_size,
322 })
323 }
324}
325
326/// Root mapping structure for `.nam` files.
327#[derive(Deserialize, Serialize, Debug, Clone)]
328pub struct NamModelData {
329 /// Version in the JSON header (e.g. "0.5.4")
330 pub version: Option<String>,
331 /// Declared architecture type ("WaveNet", "LSTM", "ConvNet", "Linear", "SlimmableContainer")
332 pub architecture: String,
333 /// Structural configuration of hyperparameters
334 pub config: NamConfig,
335 /// The huge Float32 tensors flattened in SoA format.
336 #[serde(deserialize_with = "deserialize_weights")]
337 pub weights: Vec<f32>,
338 /// Original sample rate projected by the modeling (always ideal reference 48 kHz).
339 #[serde(default, deserialize_with = "deserialize_sample_rate")]
340 pub sample_rate: Option<f32>,
341 /// Extra physical-acoustic properties associated.
342 pub metadata: Option<NamMetadata>,
343 /// Weight layout (used only in the .namb v2+ binary format).
344 #[serde(skip)]
345 pub weights_layout: WeightsLayout,
346}