nam_rs/models/linear/process.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//! Hot-path audio processing methods for the Linear model.
5//!
6//! Separated from the model definition to keep the core struct and
7//! constructors in `linear.rs` while isolating the RT-critical
8//! process/prewarm/reset logic.
9
10use super::LinearMode;
11
12impl super::LinearModel {
13 /// Processes a single audio sample using the Linear model.
14 ///
15 /// 1. Writes the sample into the ring buffer (`history`).
16 /// 2. Advances the write pointer in the mirrored area.
17 /// 3. Dispatches according to the active `mode`:
18 /// - **Direct**: dot product over the full receptive field + bias.
19 /// - **FFT**: dot product over the head (`P` taps) + bias + tail sample
20 /// from the pre-computed `tail_output_buf`. Every `P` samples, a new
21 /// tail block is computed via `LinearFftState::process_tail_block`.
22 ///
23 /// # Safety
24 /// `self.weights` must be 64-byte aligned (guaranteed by `AlignedVec`).
25 #[inline(always)]
26 pub(crate) unsafe fn process_sample(&mut self, input: f32) -> f32 {
27 self.history[self.write_pos] = input;
28
29 self.write_pos += 1;
30 if self.write_pos >= self.double_limit {
31 self.write_pos -= self.history.size();
32 }
33
34 match &mut self.mode {
35 LinearMode::Direct => {
36 let start = self.write_pos - self.receptive_field;
37 let window = &self.history[start..self.write_pos];
38 let dot = unsafe {
39 crate::math::dsp::stereo::convolve_mono(
40 self.weights.as_ptr(),
41 window.as_ptr(),
42 self.receptive_field,
43 )
44 };
45 self.bias + dot
46 }
47 LinearMode::Fft(state) => {
48 let p = state.p;
49
50 let head_weights_ptr =
51 unsafe { self.weights.as_ptr().add(self.receptive_field - p) };
52 let head_start = self.write_pos - p;
53 let head_window = &self.history[head_start..self.write_pos];
54 let head_dot = unsafe {
55 crate::math::dsp::stereo::convolve_mono(
56 head_weights_ptr,
57 head_window.as_ptr(),
58 p,
59 )
60 };
61
62 let y_tail = state.tail_output_buf[state.sample_counter];
63 state.sample_counter += 1;
64
65 if state.sample_counter >= p {
66 let block_start = self.write_pos - 2 * p;
67 let block_window = &self.history[block_start..self.write_pos];
68 state.process_tail_block(block_window);
69 state.sample_counter = 0;
70 }
71
72 self.bias + head_dot + y_tail
73 }
74 }
75 }
76
77 /// Processes a block of audio samples.
78 ///
79 /// # Safety
80 /// `self.weights` must be 64-byte aligned.
81 #[inline(always)]
82 pub unsafe fn process(&mut self, input: &[f32], output: &mut [f32]) {
83 let n = core::cmp::min(input.len(), output.len());
84 for i in 0..n {
85 unsafe {
86 output[i] = self.process_sample(input[i]);
87 }
88 }
89 }
90
91 /// Fills the history buffer with zeros, resets the write pointer, and
92 /// reinitializes the FFT state (if active).
93 #[cold]
94 pub fn prewarm(&mut self, _num_samples: usize) {
95 let size = self.history.size();
96 for i in 0..(size * 2) {
97 self.history[i] = 0.0;
98 }
99 self.write_pos = size;
100 if let LinearMode::Fft(ref mut state) = self.mode {
101 state.reset();
102 }
103 }
104
105 /// Resets internal state: zeroes the history buffer, write pointer,
106 /// and FFT state (if active).
107 #[cold]
108 pub fn reset(&mut self, _sample_rate: u32, _max_buffer_size: usize) {
109 let size = self.history.size();
110 for i in 0..(size * 2) {
111 self.history[i] = 0.0;
112 }
113 self.write_pos = size;
114 if let LinearMode::Fft(ref mut state) = self.mode {
115 state.reset();
116 }
117 }
118}