Skip to main content

nam_rs/
model_runtime.rs

1//! Architecture-agnostic runtime: [`Model`] dispatches over the `.nam`'s declared
2//! architecture so consumers run any supported model without branching.
3
4use crate::error::Error;
5use crate::lstm::Lstm;
6use crate::model::{ModelConfig, NamModel};
7use crate::wavenet::WaveNet;
8
9/// A runnable NAM model of any supported architecture.
10///
11/// Build with [`Model::from_nam`]; then call [`Model::process_buffer`] on the audio
12/// thread. `#[non_exhaustive]` so future architectures don't break downstream
13/// `match`es.
14#[non_exhaustive]
15#[derive(Debug)]
16pub enum Model {
17    /// A WaveNet model. Boxed so the enum's variants are similarly sized (a `WaveNet`
18    /// carries many pre-allocated scratch buffers); the indirection is one pointer
19    /// hop off the build path, not the per-sample hot loop.
20    WaveNet(Box<WaveNet>),
21    /// An LSTM model.
22    Lstm(Lstm),
23    /// A width-selectable container of submodels.
24    Slimmable(Slimmable),
25}
26
27/// A width-selectable set of pre-built submodels (NAM Core `SlimmableContainer`).
28///
29/// All submodels are built up front, so switching the active one is a single index
30/// write — real-time-safe, no allocation, no rebuild. Each submodel keeps its own
31/// streaming state, so switching mid-stream leaves a short warmup transient on the
32/// newly-selected submodel (NAM Core behaves the same; it does not cross-feed the
33/// inactive submodels). The container itself holds no weights and does no DSP.
34#[derive(Debug)]
35pub struct Slimmable {
36    submodels: Vec<Model>,
37    max_values: Vec<f32>,
38    active: usize,
39}
40
41impl Slimmable {
42    /// Number of submodels.
43    pub fn len(&self) -> usize {
44        self.submodels.len()
45    }
46
47    /// Always `false` (a built container has at least one submodel).
48    pub fn is_empty(&self) -> bool {
49        self.submodels.is_empty()
50    }
51
52    /// Index of the currently-active submodel.
53    pub fn active_index(&self) -> usize {
54        self.active
55    }
56
57    /// Select a submodel by index, clamping out-of-range to the last (full) submodel
58    /// — mirroring NAM Core's "else last" leniency. Real-time-safe.
59    pub fn select(&mut self, index: usize) {
60        self.active = index.min(self.submodels.len() - 1);
61    }
62
63    /// Set the width dial: activate the first submodel whose `max_value` exceeds
64    /// `value`, else the last (full) submodel. Matches NAM Core `SetSlimmableSize`.
65    /// Real-time-safe.
66    pub fn set_slim_size(&mut self, value: f32) {
67        self.active = self
68            .max_values
69            .iter()
70            .position(|&m| m > value)
71            .unwrap_or(self.submodels.len() - 1);
72    }
73}
74
75impl Model {
76    /// Build the runtime matching `model.architecture`. All allocation happens here.
77    pub fn from_nam(model: &NamModel) -> Result<Self, Error> {
78        match &model.config {
79            ModelConfig::WaveNet(_) => Ok(Model::WaveNet(Box::new(WaveNet::new(model)?))),
80            ModelConfig::Lstm(_) => Ok(Model::Lstm(Lstm::new(model)?)),
81            ModelConfig::Slimmable(cfg) => {
82                if cfg.submodels.is_empty() {
83                    return Err(Error::UnsupportedFeature("empty SlimmableContainer".into()));
84                }
85                let mut submodels = Vec::with_capacity(cfg.submodels.len());
86                let mut max_values = Vec::with_capacity(cfg.submodels.len());
87                for sm in &cfg.submodels {
88                    submodels.push(Model::from_nam(&sm.model)?);
89                    max_values.push(sm.max_value);
90                }
91                let active = submodels.len() - 1; // default = full
92                Ok(Model::Slimmable(Slimmable {
93                    submodels,
94                    max_values,
95                    active,
96                }))
97            }
98        }
99    }
100
101    /// Build a model for use as a nested `condition_dsp`, where a WaveNet may emit
102    /// more than one output channel (its N rows feed the parent arrays' conditioning).
103    /// Only WaveNet has a multi-channel output path; LSTM/Slimmable are always mono,
104    /// so they fall back to [`Model::from_nam`].
105    pub(crate) fn from_nam_conditioning(model: &NamModel) -> Result<Self, Error> {
106        match &model.config {
107            ModelConfig::WaveNet(_) => {
108                Ok(Model::WaveNet(Box::new(WaveNet::new_conditioning(model)?)))
109            }
110            _ => Model::from_nam(model),
111        }
112    }
113
114    /// Process a buffer of mono samples in place. Allocation-free.
115    pub fn process_buffer(&mut self, io: &mut [f32]) {
116        match self {
117            Model::WaveNet(w) => w.process_buffer(io),
118            Model::Lstm(l) => l.process_buffer(io),
119            Model::Slimmable(s) => s.submodels[s.active].process_buffer(io),
120        }
121    }
122
123    /// Process a single mono sample. Allocation-free.
124    pub fn process_sample(&mut self, x: f32) -> f32 {
125        match self {
126            Model::WaveNet(w) => w.process_sample(x),
127            Model::Lstm(l) => l.process_sample(x),
128            Model::Slimmable(s) => s.submodels[s.active].process_sample(x),
129        }
130    }
131
132    /// Reset all internal state to the model's initial conditions.
133    pub fn reset(&mut self) {
134        match self {
135            Model::WaveNet(w) => w.reset(),
136            Model::Lstm(l) => l.reset(),
137            // Reset EVERY submodel: `reset` is a full clean slate, and a later
138            // `select` must not surface stale state from a previously-active submodel.
139            // Iterating a `Vec` allocates nothing, so this stays real-time-safe.
140            Model::Slimmable(s) => s.submodels.iter_mut().for_each(Model::reset),
141        }
142    }
143
144    /// The model's processing latency in samples.
145    ///
146    /// For WaveNet this is the receptive field: the first this-many output samples
147    /// of a fresh (or freshly [`reset`](Self::reset)) model are a startup transient
148    /// computed against zero history. A host can report it as plugin latency and/or
149    /// discard that many leading samples. LSTM has no warmup, so this is `0`.
150    pub fn receptive_field(&self) -> usize {
151        match self {
152            Model::WaveNet(w) => w.receptive_field(),
153            Model::Lstm(_) => 0,
154            Model::Slimmable(s) => s.submodels[s.active].receptive_field(),
155        }
156    }
157
158    /// Number of output channels this model emits, matching NAM Core. WaveNet defers to
159    /// its post-stack head / last layer-array; LSTM is always mono. Used when this model
160    /// is a nested `condition_dsp` whose rows become the parent's N-wide conditioning.
161    pub(crate) fn num_output_channels(&self) -> usize {
162        match self {
163            Model::WaveNet(w) => w.num_output_channels(),
164            Model::Lstm(_) => 1,
165            Model::Slimmable(s) => s.submodels[s.active].num_output_channels(),
166        }
167    }
168
169    /// Run a mono `input[..n]` chunk, writing `num_output_channels() × n` planar
170    /// `[ch][t]` into `out`. Allocation-free; used to produce a nested `condition_dsp`'s
171    /// multi-channel conditioning for the parent WaveNet.
172    pub(crate) fn process_block_multi(&mut self, input: &[f32], out: &mut [f32], n: usize) {
173        match self {
174            Model::WaveNet(w) => w.process_block_multi(input, out, n),
175            Model::Lstm(l) => {
176                // LSTM is always mono-out: copy the input into `out` and run in place.
177                out[..n].copy_from_slice(&input[..n]);
178                l.process_buffer(&mut out[..n]);
179            }
180            Model::Slimmable(s) => s.submodels[s.active].process_block_multi(input, out, n),
181        }
182    }
183
184    /// The width-selectable container, if this model is one. Use it to drive the
185    /// slim dial ([`Slimmable::select`] / [`Slimmable::set_slim_size`]); plain
186    /// WaveNet/LSTM models return `None`.
187    pub fn as_slimmable(&self) -> Option<&Slimmable> {
188        match self {
189            Model::Slimmable(s) => Some(s),
190            _ => None,
191        }
192    }
193
194    /// Mutable variant of [`Model::as_slimmable`], for setting the active submodel.
195    pub fn as_slimmable_mut(&mut self) -> Option<&mut Slimmable> {
196        match self {
197            Model::Slimmable(s) => Some(s),
198            _ => None,
199        }
200    }
201}
202
203// Compile-time guarantee that the runtime types stay `Send + Sync`: a real-time
204// consumer builds the model off the audio thread and moves it onto the audio thread.
205// If a future field drops either auto-trait (e.g. an `Rc` or `Cell` creeps in), this
206// fails to compile instead of breaking downstream code.
207const _: () = {
208    fn assert_send_sync<T: Send + Sync>() {}
209    let _ = assert_send_sync::<Model>;
210    let _ = assert_send_sync::<WaveNet>;
211    let _ = assert_send_sync::<Lstm>;
212    let _ = assert_send_sync::<Slimmable>;
213};
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    const TINY_WAVENET: &str = r#"{
220        "version": "0.5.4", "architecture": "WaveNet",
221        "config": { "layers": [{
222            "input_size": 1, "condition_size": 1, "channels": 1, "head_size": 1,
223            "kernel_size": 1, "dilations": [1], "activation": "ReLU",
224            "gated": false, "head_bias": false
225        }], "head": null, "head_scale": 10.0 },
226        "weights": [1.0, 2.0, 0.5, 1.0, 3.0, 0.1, 0.5, 10.0]
227    }"#;
228
229    const TINY_LSTM: &str = r#"{
230        "version": "0.5.4", "architecture": "LSTM",
231        "config": { "input_size": 1, "hidden_size": 1, "num_layers": 1 },
232        "weights": [1.0,0.0, 0.0,0.0, 2.0,0.0, 0.0,0.0, 0.0,0.0,0.0,0.0, 0.0, 0.0, 3.0, 0.5]
233    }"#;
234
235    #[test]
236    fn from_nam_builds_wavenet() {
237        let m = NamModel::from_json_str(TINY_WAVENET).unwrap();
238        let mut model = Model::from_nam(&m).unwrap();
239        assert!(matches!(model, Model::WaveNet(_)));
240        let mut buf = [0.5_f32];
241        model.process_buffer(&mut buf);
242        assert!((buf[0] - 10.0).abs() < 1e-5, "got {}", buf[0]);
243    }
244
245    #[test]
246    fn receptive_field_zero_for_lstm_warmup_for_wavenet() {
247        // TINY_WAVENET: kernel 1, dilation 1 -> rf = 1. LSTM has no warmup -> 0.
248        let wn = Model::from_nam(&NamModel::from_json_str(TINY_WAVENET).unwrap()).unwrap();
249        assert_eq!(wn.receptive_field(), 1);
250        let lstm = Model::from_nam(&NamModel::from_json_str(TINY_LSTM).unwrap()).unwrap();
251        assert_eq!(lstm.receptive_field(), 0);
252    }
253
254    #[test]
255    fn from_nam_builds_lstm() {
256        let m = NamModel::from_json_str(TINY_LSTM).unwrap();
257        let mut model = Model::from_nam(&m).unwrap();
258        assert!(matches!(model, Model::Lstm(_)));
259        let mut buf = [0.5_f32];
260        model.process_buffer(&mut buf);
261        assert!((buf[0] - 1.1623).abs() < 1e-3, "got {}", buf[0]);
262    }
263
264    fn container() -> Model {
265        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
266            .join("tests/fixtures/slimmable_container.nam");
267        let json = std::fs::read_to_string(path).expect("read container");
268        let m = NamModel::from_json_str(&json).expect("parse container");
269        Model::from_nam(&m).expect("build container")
270    }
271
272    #[test]
273    fn from_nam_builds_slimmable_default_full() {
274        let mut model = container();
275        let s = model.as_slimmable_mut().expect("is slimmable");
276        assert_eq!(s.len(), 3);
277        assert_eq!(s.active_index(), 2, "default = last/full submodel");
278    }
279
280    #[test]
281    fn select_clamps_out_of_range() {
282        let mut model = container();
283        let s = model.as_slimmable_mut().unwrap();
284        s.select(0);
285        assert_eq!(s.active_index(), 0);
286        s.select(99);
287        assert_eq!(s.active_index(), 2, "clamped to last");
288    }
289
290    #[test]
291    fn set_slim_size_picks_first_threshold_above_value() {
292        let mut model = container();
293        let s = model.as_slimmable_mut().unwrap();
294        // max_values = [0.33, 0.66, 1.0]; first max_value > v, else last.
295        s.set_slim_size(0.0);
296        assert_eq!(s.active_index(), 0); // 0.33 > 0.0
297        s.set_slim_size(0.5);
298        assert_eq!(s.active_index(), 1); // 0.33 !> 0.5, 0.66 > 0.5
299        s.set_slim_size(0.99);
300        assert_eq!(s.active_index(), 2); // only 1.0 > 0.99
301        s.set_slim_size(5.0);
302        assert_eq!(s.active_index(), 2); // none > 5.0 -> last
303    }
304
305    #[test]
306    fn reset_clears_all_submodels_not_just_active() {
307        // reset() must restore EVERY submodel to initial conditions, not only the
308        // active one — Model::reset's contract is a full clean slate. The LSTM
309        // submodel (index 0, receptive field 1) is ideal: state shows up immediately.
310        let mut model = container();
311
312        // Probe value a fresh (never-processed) submodel-0 produces.
313        let mut fresh = container();
314        fresh.as_slimmable_mut().unwrap().select(0);
315        let mut probe_fresh = vec![0.3_f32; 8];
316        fresh.process_buffer(&mut probe_fresh);
317
318        // Dirty submodel 0, switch away, reset, switch back: it must be clean again.
319        model.as_slimmable_mut().unwrap().select(0);
320        let mut warm = vec![0.5_f32; 16];
321        model.process_buffer(&mut warm);
322        model.as_slimmable_mut().unwrap().select(2);
323        model.reset();
324        model.as_slimmable_mut().unwrap().select(0);
325        let mut probe = vec![0.3_f32; 8];
326        model.process_buffer(&mut probe);
327
328        for (i, (got, want)) in probe.iter().zip(&probe_fresh).enumerate() {
329            assert!(
330                (got - want).abs() < 1e-6,
331                "reset left submodel 0 dirty at sample {i}: {got} vs fresh {want}"
332            );
333        }
334    }
335
336    #[test]
337    fn slimmable_processes_through_active_submodel() {
338        let mut model = container();
339        model.as_slimmable_mut().unwrap().select(0); // LSTM submodel
340        let mut a = vec![0.1_f32; 32];
341        model.process_buffer(&mut a);
342        model.as_slimmable_mut().unwrap().select(2); // full WaveNet submodel
343        let mut b = vec![0.1_f32; 32];
344        model.process_buffer(&mut b);
345    }
346
347    #[test]
348    fn as_slimmable_none_for_plain_models() {
349        let mut wn = Model::from_nam(&NamModel::from_json_str(TINY_WAVENET).unwrap()).unwrap();
350        assert!(wn.as_slimmable_mut().is_none());
351    }
352}