Skip to main content

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}