NeuralAmpModeler-rs 3.0.0

An opinionated, high-performance Neural Amp Modeler (NAM) client and core implementation in Rust for Linux/PipeWire and CLAP plugins.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Fábio Henrique de Lima Silva (fhl.bsb@gmail.com) All rights reserved.

//! WaveNet A2 static model — processing methods.
//!
//! ## Block Size Contract
//!
//! Any input size ≤ `max_buffer_size` is safe: processing is internally chunked
//! into sub-blocks of ≤ `WAVENET_MAX_NUM_FRAMES` (64), matching the kernel scratch
//! buffer capacity. Exceeding `max_buffer_size` causes silent truncation: only the
//! first `max_buffer_size` frames are processed and the remaining are left as zeros.
//! This matches the CLAP/audio host contract which guarantees
//! `block_size <= max_block_size` negotiated at activation.
//!
//! The `dispatch_simd!` macro evaluates `M::ISA` (compile-time constant
//! through the `SimdMath` trait) to monomorphize the full forward pass,
//! eliminating per-frame `is_x86_feature_detected` branches and the dynamic
//! read of `SIMD_MATH` in the inner loop.

use crate::math::common::SimdMath;
use crate::models::wavenet::common::WAVENET_MAX_NUM_FRAMES;

use super::super::super::params::{A2_HEAD_KERNEL_SIZE, A2_NUM_LAYERS};
use super::WaveNetA2;

impl<const CH: usize> WaveNetA2<CH> {
    /// Full forward pass through the A2 model.
    ///
    /// Processes `input` samples and writes to `output`.
    /// Requires layers to be populated via `set_weights`.
    /// Outputs silence until weights are loaded.
    ///
    /// ## Ring buffer architecture
    ///
    /// Layer history uses `MirroredBuffer<f32>` with power-of-2 sizes.
    /// Writes go to unmasked positions in the 2× virtual mapping; reads are
    /// branchless because the mirror maps `[S, 2S)` → `[0, S)`. When
    /// `buffer_start` approaches the 2× boundary, it rewinds by subtracting
    /// `ring_size`. No `copy_within` / memmove on the hot path.
    ///
    /// Head accumulator uses a plain `AlignedVec` with pow2 mask (`& ring_mask`).
    /// A pre-write memmove preserves `K-1` tail samples when the ring is about
    /// to overflow, keeping the write-positions unmasked for vectorized stores.
    pub fn process(&mut self, input: &[f32], output: &mut [f32]) {
        unsafe {
            crate::math::common::dispatch_simd!(self, process_internal, input, output);
        }
    }

    /// Monomorphized inner loop — see [`process`](Self::process) for contract.
    #[inline(always)]
    unsafe fn process_internal<M: SimdMath>(&mut self, input: &[f32], output: &mut [f32]) {
        let total = input.len();
        if total == 0 {
            return;
        }

        output[..total].fill(0.0);

        if self.layers.is_empty() {
            self.head_write_pos = (self.head_write_pos + total) & self.head_ring_mask;
            return;
        }

        debug_assert!(
            total <= self.max_buffer_size,
            "process: input ({total}) > max_buffer_size ({}) — host violated block-size contract",
            self.max_buffer_size
        );
        let nf_total = total.min(self.max_buffer_size);

        let mut pos = 0;
        while pos < nf_total {
            let nf = (nf_total - pos).min(WAVENET_MAX_NUM_FRAMES);

            self.rechannel_prescale(input, pos, nf);
            let head_wp = self.advance_head_ring(nf);

            for li in 0..A2_NUM_LAYERS {
                unsafe {
                    self.layer_forward_dispatch::<M>(li, nf, input, pos, head_wp);
                }
            }

            self.head_finalize(head_wp, nf, &mut output[pos..pos + nf]);
            pos += nf;
        }
    }

    /// Phase 0: input rechannel pre-scaling — `input × rechannel_w_f32 → layer_in`.
    #[inline(always)]
    fn rechannel_prescale(&mut self, input: &[f32], pos: usize, nf: usize) {
        if CH == 8 {
            use core::arch::x86_64::{
                _mm256_load_ps, _mm256_mul_ps, _mm256_set1_ps, _mm256_store_ps,
            };
            unsafe {
                let rw_vec = _mm256_load_ps(self.rechannel_w_f32.as_ptr());
                for (f, &x) in input[pos..pos + nf].iter().enumerate() {
                    let x_vec = _mm256_set1_ps(x);
                    let res = _mm256_mul_ps(rw_vec, x_vec);
                    _mm256_store_ps(self.layer_in.as_mut_ptr().add(f * 8), res);
                }
            }
        } else {
            for (f, &x) in input[pos..pos + nf].iter().enumerate() {
                let base = f * CH;
                for c in 0..CH {
                    self.layer_in[base + c] = self.rechannel_w_f32[c] * x;
                }
            }
        }
    }

    /// Advances the head accumulator ring buffer.
    ///
    /// When the write cursor plus `nf` would overflow the ring capacity,
    /// the tail `K-1` samples are memmove'd to the start and the write
    /// position wraps around. Returns the (possibly wrapped) write position
    /// for use by the layer loop.
    #[inline(always)]
    fn advance_head_ring(&mut self, nf: usize) -> usize {
        let head_keep = A2_HEAD_KERNEL_SIZE - 1;
        let head_cap = self.head_ring_mask + 1;
        if self.head_write_pos + nf > head_cap {
            let keep_start = self.head_write_pos - head_keep;
            let keep_bytes = head_keep * CH;
            let src = keep_start * CH;
            self.head_accum.copy_within(src..src + keep_bytes, 0);
            self.head_write_pos = head_keep;
        }
        self.head_write_pos
    }

    /// Per-layer forward dispatch for a single layer index.
    ///
    /// # Safety
    ///
    /// Caller must ensure `li < A2_NUM_LAYERS` and that `nf` frames of valid
    /// data are available at `input[pos..pos+nf]`. Internal conv/film/head
    /// accesses assume caller-verified buffer capacities.
    #[inline(always)]
    unsafe fn layer_forward_dispatch<M: SimdMath>(
        &mut self,
        li: usize,
        nf: usize,
        input: &[f32],
        pos: usize,
        head_wp: usize,
    ) {
        let is_first = li == 0;
        let is_last = li == A2_NUM_LAYERS - 1;
        let ch = CH;
        let ring_size = self.layer_ring_sizes[li];
        let lookback = self.layer_lookbacks[li];
        let max_lookback_cols = lookback / ch;
        let bs = self.layer_buffer_starts[li];

        debug_assert!(bs >= lookback);
        debug_assert!(bs + nf * ch <= ring_size * 2);

        // Copy layer_in → history buffer, then apply conv_pre_film.
        {
            let buf = &mut self.layer_buffers[li];
            buf[bs..bs + nf * ch].copy_from_slice(&self.layer_in[..nf * ch]);
            for f in 0..nf {
                if let Some(ref mut film) = self.layers[li].conv_pre_film {
                    unsafe {
                        film.process(
                            &mut buf[bs + f * ch..bs + (f + 1) * ch],
                            &input[pos + f..pos + f + 1],
                        );
                    }
                }
            }
        }

        if bs + nf * ch + self.max_buffer_size * ch > ring_size * 2 {
            self.layer_buffer_starts[li] = bs + nf * ch - ring_size;
        } else {
            self.layer_buffer_starts[li] = bs + nf * ch;
        }

        {
            let history = &self.layer_buffers[li][bs - lookback..bs + nf * ch];
            let layer = &mut self.layers[li];

            let conv_ch = layer.conv_ch.as_ref();
            let mixin_w = &layer.mixin_w;
            let l1x1_w = &layer.l1x1_w;
            let l1x1_b = &layer.l1x1_b;

            let mut film_block = super::super::super::film::FilmBlock {
                conv_pre_film: layer.conv_pre_film.as_mut(),
                conv_post_film: layer.conv_post_film.as_mut(),
                input_mixin_pre_film: layer.input_mixin_pre_film.as_mut(),
                input_mixin_post_film: layer.input_mixin_post_film.as_mut(),
                activation_pre_film: layer.activation_pre_film.as_mut(),
                activation_post_film: layer.activation_post_film.as_mut(),
                layer1x1_post_film: layer.layer1x1_post_film.as_mut(),
                head1x1_post_film: layer.head1x1_post_film.as_mut(),
            };

            if let Some(conv_ch) = conv_ch {
                match conv_ch {
                    super::super::super::layer::A2ConvCh::Ch3(ch3_conv) => unsafe {
                        super::super::super::conv1d_ch3::layer_forward_ch3_block(
                            ch3_conv,
                            mixin_w,
                            l1x1_w,
                            l1x1_b,
                            &mut film_block,
                            false,
                            history,
                            max_lookback_cols,
                            nf,
                            &input[pos..pos + nf],
                            &mut self.head_accum,
                            head_wp,
                            &mut self.layer_in,
                            is_first,
                            is_last,
                        );
                    },
                    super::super::super::layer::A2ConvCh::Ch8(ch8_conv) => unsafe {
                        match M::ISA {
                            crate::math::common::InstructionSet::Avx512
                            | crate::math::common::InstructionSet::Avx512VnniBf16 => {
                                super::super::super::conv1d_ch8::layer_forward_ch8_block_simdmath::<
                                    M,
                                >(
                                    ch8_conv,
                                    mixin_w,
                                    l1x1_w,
                                    l1x1_b,
                                    &mut film_block,
                                    false,
                                    history,
                                    max_lookback_cols,
                                    nf,
                                    &input[pos..pos + nf],
                                    &mut self.head_accum,
                                    head_wp,
                                    &mut self.layer_in,
                                    is_first,
                                    is_last,
                                );
                            }
                            _ => {
                                super::super::super::conv1d_ch8::layer_forward_ch8_block(
                                    ch8_conv,
                                    mixin_w,
                                    l1x1_w,
                                    l1x1_b,
                                    &mut film_block,
                                    false,
                                    history,
                                    max_lookback_cols,
                                    nf,
                                    &input[pos..pos + nf],
                                    &mut self.head_accum,
                                    head_wp,
                                    &mut self.layer_in,
                                    is_first,
                                    is_last,
                                );
                            }
                        }
                    },
                }
                return;
            }
            // Fallback: per-frame path using the generic A2Conv1d enum.
            #[cfg(any(test, feature = "dynamic-engine"))]
            {
                use super::super::super::params::A2_LEAKY_SLOPE;

                debug_assert!(self.z_scratch.len() >= ch);
                for f in 0..nf {
                    let frame_idx = max_lookback_cols + f;

                    // 1. Dilated conv → z_buf.
                    unsafe {
                        layer.conv.process_single_frame::<M>(
                            history,
                            &mut self.z_scratch[..ch],
                            frame_idx,
                            None,
                        );
                    }

                    // 1b. FiLM post-conv.
                    let cond = &input[pos + f..pos + f + 1];
                    if let Some(ref mut film) = film_block.conv_post_film {
                        unsafe {
                            film.process(&mut self.z_scratch[..ch], cond);
                        }
                    }

                    // 2. Input mixin — input_mixin_pre_film applied to condition (self-modulation).
                    let cond_val = input[pos + f];
                    let cond_for_mixin = if let Some(ref mut film) = film_block.input_mixin_pre_film
                    {
                        let mut modulated = cond_val;
                        let orig = cond_val;
                        unsafe {
                            film.process(
                                core::slice::from_mut(&mut modulated),
                                core::slice::from_ref(&orig),
                            );
                        }
                        modulated
                    } else {
                        cond_val
                    };
                    for c in 0..ch {
                        self.z_scratch[c] += mixin_w[c] * cond_for_mixin;
                    }

                    // 2b. FiLM post-mixin.
                    if let Some(ref mut film) = film_block.input_mixin_post_film {
                        unsafe {
                            film.process(&mut self.z_scratch[..ch], cond);
                        }
                    }
                    if let Some(ref mut film) = film_block.activation_pre_film {
                        unsafe {
                            film.process(&mut self.z_scratch[..ch], cond);
                        }
                    }

                    // 3. LeakyReLU.
                    for z in self.z_scratch.iter_mut().take(ch) {
                        if *z < 0.0 {
                            *z *= A2_LEAKY_SLOPE;
                        }
                    }

                    // 3b. FiLM post-activation.
                    if let Some(ref mut film) = film_block.activation_post_film {
                        unsafe {
                            film.process(&mut self.z_scratch[..ch], cond);
                        }
                    }

                    // 4. Head accumulator.
                    let head_off = (head_wp + f) * ch;
                    if is_first {
                        self.head_accum[head_off..head_off + ch]
                            .copy_from_slice(&self.z_scratch[..ch]);
                    } else {
                        for (c, z) in self.z_scratch.iter().enumerate().take(ch) {
                            self.head_accum[head_off + c] += *z;
                        }
                    }

                    // 5. L1x1 residual.
                    if !is_last {
                        let base = f * ch;
                        for c in 0..ch {
                            let mut sum = l1x1_b[c];
                            for u in 0..ch {
                                sum += l1x1_w[u * ch + c] * self.z_scratch[u];
                            }
                            self.layer_in[base + c] += sum;
                        }
                        if let Some(ref mut film) = film_block.layer1x1_post_film {
                            unsafe {
                                film.process(&mut self.layer_in[base..base + ch], cond);
                            }
                        }
                    }
                }
            }
            #[cfg(not(any(test, feature = "dynamic-engine")))]
            {
                // RT-safe fallback: this branch is unreachable per set_weights
                // invariant (A2 layers always have CH=3 or CH=8 conv). Retained
                // as a panic-free guard for future model format changes — never
                // panics on the audio thread.
                if let Some(ref rt) = self.rt_status {
                    rt.set_flag(crate::common::spsc::RT_STATUS_A2_FALLBACK_TRIGGERED);
                }
                debug_assert!(
                    false,
                    "A2 layers always have ch3 or ch8 conv; \
                     scalar fallback triggered — silencing layer output"
                );
                self.z_scratch[..ch].fill(0.0);
                for f in 0..nf {
                    let head_off = (head_wp + f) * ch;
                    if is_first {
                        self.head_accum[head_off..head_off + ch]
                            .copy_from_slice(&self.z_scratch[..ch]);
                    }
                    if !is_last {
                        let base = f * ch;
                        for c in 0..ch {
                            let mut sum = l1x1_b[c];
                            for u in 0..ch {
                                sum += l1x1_w[u * ch + c] * self.z_scratch[u];
                            }
                            self.layer_in[base + c] += sum;
                        }
                        if let Some(ref mut film) = film_block.layer1x1_post_film {
                            unsafe {
                                film.process(
                                    &mut self.layer_in[base..base + ch],
                                    &input[pos + f..pos + f + 1],
                                );
                            }
                        }
                    }
                }
            }
        }
    }

    /// Finalizes the head convolution and advances the head write position.
    #[inline(always)]
    fn head_finalize(&mut self, head_wp: usize, nf: usize, output: &mut [f32]) {
        self.head_write_pos = (head_wp + nf) & self.head_ring_mask;

        if let Some(ref head) = self.head_conv {
            head.process(
                &self.head_accum,
                self.head_write_pos,
                self.head_ring_mask,
                nf,
                output,
            );
        }
    }
}