Skip to main content

nam_rs/models/convnet/
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//! ConvNet feed-forward model — chains ConvNetBlock layers sequentially
5//! with an optional post-stack head.
6
7use crate::math::common::{AlignedVec, SimdMath};
8use crate::models::wavenet::PostStackHead;
9use crate::models::wavenet::common::WAVENET_MAX_NUM_FRAMES;
10
11use super::block::ConvNetBlock;
12
13/// ConvNet feed-forward model.
14///
15/// Composed of a sequence of [`ConvNetBlock`]s chained sequentially,
16/// followed by an optional [`PostStackHead`] and a final `head_scale` gain.
17///
18/// Unlike WaveNet, ConvNet has no gating, no rechannel projections,
19/// no condition_dsp, and no dual-array architecture. Each block's
20/// output is the input of the next block directly.
21#[repr(align(64))]
22pub struct ConvNetModel {
23    /// Sequential ConvNet blocks.
24    pub blocks: Vec<ConvNetBlock>,
25    /// Final voltage compensation scale (Target Output Scale).
26    pub head_scale: f32,
27    /// Total receptive field (sum of all block RFs + head RF contribution).
28    pub receptive_field_size: usize,
29    /// Optional post-stack head sub-object (Conv1D + activation).
30    pub post_stack_head: Option<PostStackHead>,
31    /// Scratch buffer for post-stack head output.
32    pub head_output_scratch: AlignedVec<f32>,
33    /// Ping-pong scratch buffers for block-to-block signal relay.
34    pub(crate) scratch_a: AlignedVec<f32>,
35    pub(crate) scratch_b: AlignedVec<f32>,
36    /// Whether to execute prewarm during `reset()`. Default: `true`.
37    pub prewarm_on_reset: bool,
38    /// Optional C++ flat-format linear head weights (no activation).
39    /// Used when `post_stack_head` is `None` but the model has a separate
40    /// linear projection from `in_ch → out_ch`.
41    pub linear_head: Option<LinearHead>,
42}
43
44/// Simple linear projection head for C++ flat ConvNet format.
45#[derive(Clone)]
46#[repr(align(64))]
47pub struct LinearHead {
48    /// Row-major weight matrix: out_ch × in_ch.
49    pub weight: AlignedVec<f32>,
50    /// Bias vector: out_ch.
51    pub bias: AlignedVec<f32>,
52    /// Number of input channels.
53    pub in_ch: usize,
54    /// Number of output channels.
55    pub out_ch: usize,
56}
57
58impl ConvNetModel {
59    /// Returns the number of input channels expected by the first block.
60    pub fn in_channels(&self) -> usize {
61        self.blocks.first().map(|b| b.conv.in_ch).unwrap_or(1)
62    }
63
64    /// Returns the number of output channels produced by the model.
65    pub fn out_channels(&self) -> usize {
66        if let Some(ref head) = self.post_stack_head {
67            head.out_channels()
68        } else if let Some(ref linear) = self.linear_head {
69            linear.out_ch
70        } else {
71            self.blocks.last().map(|b| b.conv.out_ch).unwrap_or(1)
72        }
73    }
74
75    /// Resolves the full forward pass and produces waveform samples in zero allocation (DSP).
76    pub fn process(&mut self, input: &[f32], output: &mut [f32]) {
77        unsafe { crate::math::common::dispatch_simd!(self, process_internal, input, output) };
78    }
79
80    /// SIMD-dispatched processing kernel.
81    ///
82    /// Chains blocks sequentially: block 0 receives raw input, each subsequent
83    /// block receives the output of the previous block. After all blocks,
84    /// the result is passed through the optional post-stack head and
85    /// scaled by `head_scale`.
86    #[inline(always)]
87    unsafe fn process_internal<M: SimdMath>(&mut self, input: &[f32], output: &mut [f32]) {
88        let total_frames = input.len();
89        if total_frames == 0 || self.blocks.is_empty() {
90            output[..total_frames].fill(0.0);
91            return;
92        }
93
94        let out_ch = self.out_channels();
95        let mut pos = 0;
96
97        while pos < total_frames {
98            let num_frames = (total_frames - pos).min(WAVENET_MAX_NUM_FRAMES);
99            let in_slice = &input[pos..pos + num_frames];
100
101            let num_blocks = self.blocks.len();
102            // SAFETY: &mut self borrows blocks exclusively; Vec won't reallocate
103            // while this mutable reference exists. Calling as_mut_ptr() is sound
104            // because the Vec is never resized, dropped, or aliased during the
105            // function body. All pointer arithmetic respects i ∈ 0..num_blocks,
106            // and blocks_ptr.add(i) points to a valid initialized ConvNetBlock.
107            let blocks_ptr = self.blocks.as_mut_ptr();
108
109            // Block 0 writes to scratch_a
110            // SAFETY: blocks_ptr is valid (see above). blocks is non-empty
111            // (guarded at L90). i=0 is within bounds.
112            let first_out_ch = unsafe { (*blocks_ptr).conv.out_ch };
113            let dst_a = &mut self.scratch_a[..num_frames * first_out_ch];
114            // SAFETY: blocks_ptr and dst_a are valid; M provides safe SIMD dispatch.
115            unsafe {
116                (*blocks_ptr).process_block_internal::<M>(in_slice, dst_a, num_frames);
117            }
118
119            let mut src_is_a = true;
120
121            for i in 1..num_blocks {
122                // SAFETY: i < num_blocks by loop bound. blocks_ptr.add(i) yields
123                // a valid pointer to an initialized ConvNetBlock in the Vec.
124                let curr = unsafe { &mut *blocks_ptr.add(i) };
125                let curr_out_ch = curr.conv.out_ch;
126
127                if src_is_a {
128                    let src = &self.scratch_a
129                        [..num_frames * unsafe { (*blocks_ptr.add(i - 1)).conv.out_ch }];
130                    let dst = &mut self.scratch_b[..num_frames * curr_out_ch];
131                    // SAFETY: curr is valid (see above); src/dst are valid
132                    // slices within scratch_a/scratch_b which are separate
133                    // allocations.
134                    unsafe {
135                        curr.process_block_internal::<M>(src, dst, num_frames);
136                    }
137                } else {
138                    let src = &self.scratch_b
139                        [..num_frames * unsafe { (*blocks_ptr.add(i - 1)).conv.out_ch }];
140                    let dst = &mut self.scratch_a[..num_frames * curr_out_ch];
141                    // SAFETY: Same invariants as the if branch; src and dst
142                    // are reversed between scratch_a/scratch_b.
143                    unsafe {
144                        curr.process_block_internal::<M>(src, dst, num_frames);
145                    }
146                }
147
148                src_is_a = !src_is_a;
149            }
150
151            let last_result_in_a = (num_blocks - 1).is_multiple_of(2);
152            // SAFETY: num_blocks > 0 (guarded at L90). num_blocks-1 < num_blocks,
153            // so blocks_ptr.add(num_blocks-1) is in bounds.
154            let last_out_ch = unsafe { (*blocks_ptr.add(num_blocks - 1)).conv.out_ch };
155            let last_slice = if last_result_in_a {
156                &self.scratch_a[..num_frames * last_out_ch]
157            } else {
158                &self.scratch_b[..num_frames * last_out_ch]
159            };
160
161            if let Some(ref mut head_proc) = self.post_stack_head {
162                let head_out_ch = head_proc.out_channels();
163                let head_scratch = &mut self.head_output_scratch[..num_frames * head_out_ch];
164                unsafe {
165                    head_proc.process_block(last_slice, head_scratch, num_frames);
166                }
167                let out_start = pos * out_ch;
168                let out_slice = &mut output[out_start..out_start + num_frames * out_ch];
169                out_slice.copy_from_slice(head_scratch);
170                unsafe {
171                    M::apply_gain(out_slice, self.head_scale);
172                }
173            } else if let Some(ref linear) = self.linear_head {
174                let lh_out_ch = linear.out_ch;
175                let out_start = pos * out_ch;
176                let out_slice = &mut output[out_start..out_start + num_frames * lh_out_ch];
177                out_slice.fill(linear.bias[0]);
178                for f in 0..num_frames {
179                    let src = &last_slice[f * linear.in_ch..(f + 1) * linear.in_ch];
180                    let dst = &mut out_slice[f * lh_out_ch..(f + 1) * lh_out_ch];
181                    for (o, dst_val) in dst.iter_mut().enumerate().take(lh_out_ch) {
182                        let mut acc = linear.bias[o];
183                        let row_start = o * linear.in_ch;
184                        for (i, &src_val) in src.iter().enumerate().take(linear.in_ch) {
185                            acc += src_val * linear.weight[row_start + i];
186                        }
187                        *dst_val = acc;
188                    }
189                }
190                unsafe {
191                    M::apply_gain(out_slice, self.head_scale);
192                }
193            } else {
194                let out_start = pos * out_ch;
195                let out_slice = &mut output[out_start..out_start + num_frames * out_ch];
196                out_slice.copy_from_slice(last_slice);
197                unsafe {
198                    M::apply_gain(out_slice, self.head_scale);
199                }
200            }
201
202            pos += num_frames;
203        }
204    }
205
206    /// Stabilizes the model by processing silence (Zero Input) for pre-warm.
207    #[cold]
208    pub fn prewarm(&mut self) {
209        unsafe {
210            crate::math::common::dispatch_simd!(self, prewarm_internal);
211        }
212    }
213
214    #[inline(always)]
215    #[cold]
216    unsafe fn prewarm_internal<M: SimdMath>(&mut self) {
217        let num_blocks = self.blocks.len();
218        if num_blocks == 0 {
219            return;
220        }
221
222        let blocks_ptr = self.blocks.as_mut_ptr();
223
224        for i in 0..num_blocks {
225            unsafe {
226                (*blocks_ptr.add(i)).prewarm_internal::<M>();
227            }
228        }
229
230        if let Some(ref mut head_proc) = self.post_stack_head {
231            head_proc.prewarm();
232        }
233        // Linear head has no state — no prewarm needed.
234    }
235}
236
237#[cfg(test)]
238#[path = "convnet_model_test.rs"]
239mod tests;