Skip to main content

nam_rs/models/
container.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//! `ContainerModel` — A bundle of pre-trained submodels selected by a quality threshold.
5//!
6//! Each submodel covers values up to (but not including) its `max_value`.
7//! The last submodel is the fallback for values at or above the last threshold.
8//!
9//! ## Architecture
10//!
11//! This implements the official NAM `SlimmableContainer` architecture
12//! (registered since file version 0.7.0). It is the format used by the
13//! mainstream A2 distribution (nano + standard bundle).
14//!
15//! ## Crossfade
16//!
17//! Submodel switching uses a linear crossfade (32 ms) to prevent audible
18//! clicks. During the transition, input is processed through both submodels
19//! and outputs are blended linearly. The scratch buffer is pre-allocated
20//! (set_max_buffer_size) for zero alloc on the hot path.
21
22use std::cmp::Ordering;
23
24use super::slimmable::SlimmableModel;
25use super::{NamModel, StaticModel};
26use crate::common::spsc::RT_STATUS_SLIMMABLE_RESET_FAILED;
27
28const CROSSFADE_DURATION_MS: f32 = 32.0;
29
30/// A bundle of pre-trained submodels selected by a quality threshold.
31///
32/// Each submodel covers values up to (but not including) its `max_value`.
33/// The last submodel is the fallback for values at or above the last threshold.
34///
35/// ## Architecture
36///
37/// This implements the official NAM `SlimmableContainer` architecture
38/// (registered since file version 0.7.0). It is the format used by the
39/// mainstream A2 distribution (nano + standard bundle).
40pub struct ContainerModel {
41    submodels: Vec<(f32, Box<StaticModel>)>,
42    active_index: usize,
43    pending_index: Option<usize>,
44    crossfade_elapsed: usize,
45    crossfade_duration: usize,
46    scratch_buffer: Vec<f32>,
47    sample_rate: u32,
48    max_buffer_size: usize,
49    prewarm_on_reset: bool,
50}
51
52impl ContainerModel {
53    /// Creates a new `ContainerModel`.
54    ///
55    /// # Requirements (enforced)
56    ///
57    /// - `submodels` must be non-empty.
58    /// - Sorted by `max_value` ascending.
59    /// - Last `max_value` must be `>= 1.0`.
60    pub fn new(
61        mut submodels: Vec<(f32, Box<StaticModel>)>,
62        sample_rate: u32,
63    ) -> anyhow::Result<Self> {
64        if submodels.is_empty() {
65            anyhow::bail!("ContainerModel: no submodels provided");
66        }
67
68        submodels.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal));
69
70        for (i, (max_value, _)) in submodels.iter().enumerate() {
71            if !max_value.is_finite() || *max_value < 0.0 {
72                anyhow::bail!(
73                    "ContainerModel: submodel[{}] has invalid max_value={} \
74                     (must be finite and >= 0.0)",
75                    i,
76                    max_value
77                );
78            }
79        }
80
81        for w in submodels.windows(2) {
82            if w[1].0 <= w[0].0 {
83                anyhow::bail!("ContainerModel: submodels must be sorted by ascending max_value");
84            }
85        }
86        let last = submodels
87            .last()
88            .ok_or_else(|| anyhow::anyhow!("ContainerModel: submodels list is empty"))?;
89        if last.0 < 1.0 {
90            anyhow::bail!("ContainerModel: last submodel max_value must be >= 1.0");
91        }
92
93        let active_index = submodels.len() - 1;
94        let crossfade_duration =
95            (CROSSFADE_DURATION_MS / 1000.0 * sample_rate as f32).round() as usize;
96        let default_buf = 4096usize;
97
98        let mut container = Self {
99            submodels,
100            active_index,
101            pending_index: None,
102            crossfade_elapsed: 0,
103            crossfade_duration: crossfade_duration.max(1),
104            scratch_buffer: vec![0.0f32; default_buf],
105            sample_rate,
106            max_buffer_size: default_buf,
107            prewarm_on_reset: true,
108        };
109
110        for (_, model) in &mut container.submodels {
111            model.set_max_buffer_size(default_buf)?;
112        }
113
114        container.prewarm(4096);
115
116        Ok(container)
117    }
118
119    /// Returns the list of `(max_value, StaticModel ref)` for diagnostics.
120    pub fn submodels(&self) -> &[(f32, Box<StaticModel>)] {
121        &self.submodels
122    }
123
124    /// Mutable access to submodels (for testing purposes only).
125    pub fn submodels_mut(&mut self) -> &mut [(f32, Box<StaticModel>)] {
126        &mut self.submodels
127    }
128
129    /// Returns the index of the currently active submodel.
130    pub fn active_index(&self) -> usize {
131        self.active_index
132    }
133
134    /// Sets the active submodel index directly, bypassing crossfade (testing only).
135    pub fn set_active_index(&mut self, idx: usize) {
136        assert!(idx < self.submodels.len());
137        self.active_index = idx;
138        self.pending_index = None;
139        self.crossfade_elapsed = 0;
140    }
141
142    /// Returns the index of the pending submodel during crossfade, if any.
143    pub fn pending_index(&self) -> Option<usize> {
144        self.pending_index
145    }
146
147    /// Returns `true` if a crossfade is in progress.
148    pub fn is_crossfading(&self) -> bool {
149        self.pending_index.is_some()
150    }
151
152    /// Returns the sample rate used to validate submodels.
153    pub fn sample_rate(&self) -> u32 {
154        self.sample_rate
155    }
156
157    pub(crate) fn active(&self) -> &StaticModel {
158        &self.submodels[self.active_index].1
159    }
160}
161
162impl NamModel for ContainerModel {
163    #[inline(always)]
164    fn process(&mut self, input: &[f32], output: &mut [f32]) {
165        let n = input.len();
166
167        if let Some(pending_idx) = self.pending_index {
168            let active_idx = self.active_index;
169
170            self.submodels[pending_idx]
171                .1
172                .process(input, &mut self.scratch_buffer[..n]);
173            self.submodels[active_idx].1.process(input, output);
174
175            let t = (self.crossfade_elapsed as f32 / self.crossfade_duration as f32).min(1.0);
176            // SAFETY: scratch_buffer has the same length as output (both sized to n).
177            unsafe {
178                crate::math::dsp::gain::crossfade_blend_mono(output, &self.scratch_buffer[..n], t);
179            }
180
181            self.crossfade_elapsed += n;
182            if self.crossfade_elapsed >= self.crossfade_duration {
183                self.active_index = pending_idx;
184                self.pending_index = None;
185                self.crossfade_elapsed = 0;
186            }
187        } else {
188            self.submodels[self.active_index].1.process(input, output);
189        }
190    }
191
192    fn prewarm(&mut self, num_samples: usize) {
193        for (_, model) in &mut self.submodels {
194            model.prewarm(num_samples);
195        }
196    }
197
198    fn reset(&mut self, sample_rate: u32, max_buffer_size: usize) -> anyhow::Result<()> {
199        self.sample_rate = sample_rate;
200        self.max_buffer_size = max_buffer_size;
201        self.scratch_buffer.resize(max_buffer_size, 0.0);
202        self.crossfade_duration =
203            (CROSSFADE_DURATION_MS / 1000.0 * sample_rate as f32).round() as usize;
204        for (_, model) in &mut self.submodels {
205            model.set_max_buffer_size(max_buffer_size)?;
206        }
207        self.submodels[self.active_index]
208            .1
209            .reset(sample_rate, max_buffer_size)
210    }
211
212    fn set_max_buffer_size(&mut self, max_buf: usize) -> anyhow::Result<()> {
213        self.max_buffer_size = max_buf;
214        self.scratch_buffer.resize(max_buf, 0.0);
215        for (_, model) in &mut self.submodels {
216            model.set_max_buffer_size(max_buf)?;
217        }
218        Ok(())
219    }
220
221    fn prewarm_samples(&self) -> usize {
222        self.active().prewarm_samples()
223    }
224
225    fn prewarm_on_reset(&self) -> bool {
226        self.prewarm_on_reset
227    }
228
229    fn set_prewarm_on_reset(&mut self, val: bool) {
230        self.prewarm_on_reset = val;
231        for (_, model) in &mut self.submodels {
232            model.set_prewarm_on_reset(val);
233        }
234    }
235}
236
237impl SlimmableModel for ContainerModel {
238    fn slimmable_breakpoints(&self) -> Vec<f64> {
239        if self.submodels.len() <= 1 {
240            return vec![];
241        }
242        self.submodels[..self.submodels.len() - 1]
243            .iter()
244            .map(|(max_value, _)| *max_value as f64)
245            .collect()
246    }
247
248    fn set_slimmable_size(
249        &mut self,
250        val: f32,
251        rt_status: Option<&crate::common::spsc::RtStatusFlags>,
252    ) {
253        let mut next = self.submodels.len() - 1;
254
255        for (i, (max_value, _)) in self.submodels.iter().enumerate() {
256            if val < *max_value {
257                next = i;
258                break;
259            }
260        }
261
262        if next == self.active_index && self.pending_index.is_none() {
263            return;
264        }
265
266        if self.pending_index == Some(next) {
267            return;
268        }
269
270        debug_assert!(
271            self.scratch_buffer.len() >= self.max_buffer_size,
272            "scratch_buffer capacity invariant violated: {} < {}",
273            self.scratch_buffer.len(),
274            self.max_buffer_size
275        );
276
277        if let Some(idx) = self.pending_index {
278            self.active_index = idx;
279        }
280
281        if let Err(_e) = self.submodels[next]
282            .1
283            .reset(self.sample_rate, self.max_buffer_size)
284        {
285            if let Some(s) = rt_status {
286                s.set_flag(RT_STATUS_SLIMMABLE_RESET_FAILED);
287            }
288            return;
289        }
290
291        self.pending_index = Some(next);
292        self.crossfade_elapsed = 0;
293    }
294}
295
296impl super::sealed::Sealed for ContainerModel {}
297
298#[cfg(test)]
299#[path = "container_test.rs"]
300mod tests;