nam_rs/models/a2/model/static/mod.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//! WaveNet A2 static model (`WaveNetA2<const CH: usize>`).
5//!
6//! Single layer-array of 23 dilated causal layers with skip-connection accumulator
7//! and head rechannel convolution, matching the fast-path from `a2_fast.cpp`.
8//!
9//! ## Architecture
10//!
11//! 1. Input rechannel: `Conv1x1(1 → CH)` (bias, no activation)
12//! 2. 23 layers: dilated conv → input-mixin → LeakyReLU → head_accum += out → layer1x1 → residual
13//! 3. Head conv: `Conv1D(CH → 1, K=16, bias)` over head_accum ring → × head_scale
14//!
15//! Processing is chunked by `WAVENET_MAX_NUM_FRAMES` (64) with zero allocation on the hot-path.
16//!
17//! ## Ring buffer architecture
18//!
19//! Each layer's history is a power-of-2 `MirroredBuffer<f32>` that provides
20//! branchless reads via virtual-memory mirroring. The `buffer_start` pointer
21//! advances through the 2× virtual mapping; when it approaches the 2× boundary,
22//! it rewinds by subtracting `ring_size`. Reads at `buffer_start - offset` are
23//! always valid because the mirrored mapping maps `[S, 2S)` → `[0, S)`.
24//!
25//! The head accumulator uses a plain `AlignedVec` with pow2 mask (`& ring_mask`)
26//! for branchless ring access — no MirroredBuffer needed since the head reads
27//! are already mask-based.
28//!
29//! ## Source of truth
30//! - `a2_fast.cpp`: class `A2FastModel` (members, process, prewarm, reset)
31//! - `detail.h`: `LayerArray::Process` (per-layer sequence)
32
33use super::super::head::A2HeadConv;
34use super::super::layer::A2Layer;
35use super::super::params::{A2_DILATIONS, A2_KERNEL_SIZES, A2_NUM_LAYERS};
36use super::a2_receptive_field;
37use crate::dsp::mirror_buf::MirroredBuffer;
38use crate::math::common::AlignedVec;
39use crate::models::wavenet::common::WAVENET_MAX_NUM_FRAMES;
40use serde_json::Value;
41use std::sync::Arc;
42
43pub mod prewarm;
44pub mod process;
45
46/// Complete WaveNet A2 Model.
47///
48/// `CH` = channel count (3 for Lite/Nano, 8 for Full/Standard).
49pub struct WaveNetA2<const CH: usize> {
50 /// 23 A2 layers (one per layer index). Populated by `set_weights`.
51 pub layers: Vec<A2Layer>,
52
53 /// Input rechannel weights: `Conv1x1(1 → CH)` (no bias).
54 pub rechannel_w_f32: AlignedVec<f32>,
55
56 /// Head convolution (K=16 over skip-connection accumulator, bias, head_scale).
57 pub head_conv: Option<A2HeadConv>,
58
59 /// Head accumulator ring buffer (skip-connection sum, column-major, pow2 size).
60 pub head_accum: AlignedVec<f32>,
61
62 /// Write position in `head_accum` (in columns, wraps via `head_ring_mask`).
63 pub head_write_pos: usize,
64
65 /// Ring mask for `head_accum` (pow2 ring, mask = capacity - 1).
66 pub head_ring_mask: usize,
67
68 /// Per-layer history buffers: one MirroredBuffer per layer (23 total).
69 /// Each buffer provides 2× virtual mapping for branchless ring access.
70 pub layer_buffers: Vec<MirroredBuffer<f32>>,
71
72 /// Per-layer ring sizes in elements (pow2 page-aligned). For rewind: `start -= ring_size`.
73 pub layer_ring_sizes: Vec<usize>,
74
75 /// Per-layer maximum dilation lookback = (kernel-1) * dilation.
76 pub layer_lookbacks: Vec<usize>,
77
78 /// Per-layer buffer starts (advanced with each written frame, rewound near 2× boundary).
79 pub layer_buffer_starts: Vec<usize>,
80
81 /// Inter-layer data buffer: `CH × max_buffer_size` f32, reused across layers.
82 /// Each layer reads from it, then writes its l1x1 residual back (in-place update).
83 pub layer_in: AlignedVec<f32>,
84
85 /// Total receptive field: sum of `(kernel-1)*dilation` + head kernel - 1.
86 pub receptive_field_size: usize,
87
88 /// Maximum frames per processing block (= `WAVENET_MAX_NUM_FRAMES`).
89 pub max_buffer_size: usize,
90
91 /// Raw JSON for the single layer array, preserved for FiLM config parsing.
92 /// `None` when the model is constructed directly (not via JSON deserialization).
93 pub layer_raw: Option<Value>,
94
95 /// Per-frame scratch buffer for the fallback conv path (CH f32).
96 /// Allocated once at model creation to avoid heap ops on the hot path.
97 pub z_scratch: AlignedVec<f32>,
98
99 /// `RtStatusFlags` for silent RT→Main telemetry (RF8).
100 pub rt_status: Option<Arc<crate::common::spsc::RtStatusFlags>>,
101 /// Whether to execute prewarm during `reset()`. Default: `true`.
102 pub prewarm_on_reset: bool,
103}
104
105impl<const CH: usize> WaveNetA2<CH> {
106 /// Creates a new uninitialized WaveNet A2 model.
107 ///
108 /// Allocates ring buffers (MirroredBuffer per layer, pow2 head accumulator)
109 /// sized for the architecture and computes the receptive field.
110 /// Weight-bearing fields start empty and are populated by the weight loader.
111 pub fn new() -> anyhow::Result<Self> {
112 let rf = a2_receptive_field();
113 let max_buf = WAVENET_MAX_NUM_FRAMES;
114
115 let head_ring_size = (rf + max_buf + 1).next_power_of_two();
116 let head_ring_mask = head_ring_size - 1;
117
118 let mut layer_buffers = Vec::with_capacity(A2_NUM_LAYERS);
119 let mut layer_ring_sizes = Vec::with_capacity(A2_NUM_LAYERS);
120 let mut layer_lookbacks = Vec::with_capacity(A2_NUM_LAYERS);
121 let mut layer_buffer_starts = Vec::with_capacity(A2_NUM_LAYERS);
122
123 for i in 0..A2_NUM_LAYERS {
124 let max_lookback = (A2_KERNEL_SIZES[i] - 1) * A2_DILATIONS[i];
125 let cap = max_lookback + max_buf + 1;
126 let mb = MirroredBuffer::<f32>::new(cap * CH)?;
127 let ring_size = mb.size();
128 layer_buffers.push(mb);
129 layer_ring_sizes.push(ring_size);
130 layer_lookbacks.push(max_lookback * CH);
131 layer_buffer_starts.push(ring_size);
132 }
133
134 Ok(Self {
135 layers: Vec::with_capacity(A2_NUM_LAYERS),
136 rechannel_w_f32: AlignedVec::new(CH, 0.0f32)
137 .expect("allocation should succeed for test-sized buffers"),
138 head_conv: None,
139 head_accum: AlignedVec::new(head_ring_size * CH, 0.0f32)
140 .expect("allocation should succeed for test-sized buffers"),
141 head_write_pos: rf,
142 head_ring_mask,
143 layer_buffers,
144 layer_ring_sizes,
145 layer_lookbacks,
146 layer_buffer_starts,
147 layer_in: AlignedVec::new(CH * max_buf, 0.0f32)
148 .expect("allocation should succeed for test-sized buffers"),
149 receptive_field_size: rf,
150 max_buffer_size: max_buf,
151 layer_raw: None,
152 z_scratch: AlignedVec::new(CH, 0.0f32)
153 .expect("allocation should succeed for test-sized buffers"),
154 rt_status: None,
155 prewarm_on_reset: true,
156 })
157 }
158
159 /// Returns the channel count.
160 #[inline(always)]
161 pub fn channels(&self) -> usize {
162 CH
163 }
164
165 /// Injects `RtStatusFlags` for RT→Main telemetry (RF8).
166 pub fn inject_rt_status(&mut self, rt_status: Arc<crate::common::spsc::RtStatusFlags>) {
167 self.rt_status = Some(rt_status);
168 }
169
170 /// Returns the total receptive field size.
171 #[inline(always)]
172 pub fn receptive_field(&self) -> usize {
173 self.receptive_field_size
174 }
175
176 /// Reallocates internal buffers to support the given maximum block size.
177 ///
178 /// Any block size is accepted: processing is internally chunked into
179 /// sub-blocks of ≤ `WAVENET_MAX_NUM_FRAMES` (64) by [`process`], matching
180 /// the kernel scratch buffer capacity. Call this to inform the model
181 /// of the negotiated CLAP/audio host block size so that internal ring
182 /// buffers are sized correctly.
183 ///
184 /// If `max_buf` is smaller than the current capacity, this is a no-op.
185 /// If `max_buf` equals the current capacity, state variables are reset
186 /// and all buffers are zero-filled in-place — avoiding heap allocation
187 /// on the RT thread (F9 / RT-Safety).
188 pub fn set_max_buffer_size(&mut self, max_buf: usize) -> anyhow::Result<()> {
189 if max_buf < self.max_buffer_size {
190 return Ok(());
191 }
192 if max_buf == self.max_buffer_size {
193 let rf = self.receptive_field_size;
194 let ha_len = self.head_accum.len();
195 self.head_accum[..ha_len].fill(0.0);
196 self.head_write_pos = rf;
197 for buf in self.layer_buffers.iter_mut() {
198 let len = buf.size();
199 buf[..len].fill(0.0);
200 }
201 self.layer_buffer_starts
202 .copy_from_slice(&self.layer_ring_sizes);
203 let li_len = self.layer_in.len();
204 self.layer_in[..li_len].fill(0.0);
205 return Ok(());
206 }
207 self.max_buffer_size = max_buf;
208 let rf = self.receptive_field_size;
209
210 self.layer_buffers.clear();
211 self.layer_ring_sizes.clear();
212 self.layer_lookbacks.clear();
213 self.layer_buffer_starts.clear();
214
215 for i in 0..A2_NUM_LAYERS {
216 let max_lookback = (A2_KERNEL_SIZES[i] - 1) * A2_DILATIONS[i];
217 let cap = max_lookback + max_buf + 1;
218 let mb = MirroredBuffer::<f32>::new(cap * CH)?;
219 let ring_size = mb.size();
220 self.layer_buffers.push(mb);
221 self.layer_ring_sizes.push(ring_size);
222 self.layer_lookbacks.push(max_lookback * CH);
223 self.layer_buffer_starts.push(ring_size);
224 }
225
226 self.layer_in = AlignedVec::new(CH * max_buf, 0.0f32)
227 .expect("allocation should succeed for test-sized buffers");
228
229 let head_ring_size = (rf + max_buf + 1).next_power_of_two();
230 self.head_ring_mask = head_ring_size - 1;
231 self.head_accum = AlignedVec::new(head_ring_size * CH, 0.0f32)
232 .expect("allocation should succeed for test-sized buffers");
233 self.head_write_pos = rf;
234
235 Ok(())
236 }
237
238 /// Resets internal state for a new sample rate and max buffer size.
239 pub fn reset(&mut self, _sample_rate: u32, max_buffer_size: usize) -> anyhow::Result<()> {
240 self.set_max_buffer_size(max_buffer_size)?;
241 if self.prewarm_on_reset {
242 self.prewarm();
243 }
244 Ok(())
245 }
246
247 /// Returns whether weights have been loaded via `set_weights`.
248 #[inline(always)]
249 pub fn has_weights(&self) -> bool {
250 !self.layers.is_empty()
251 }
252
253 /// Stores the raw layer JSON for FiLM config parsing during weight loading.
254 pub fn set_layer_raw(&mut self, raw: Option<Value>) {
255 self.layer_raw = raw;
256 }
257}