nam_rs/dsp/oversample.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//! Half-Band Oversampling Engine for the neural stage.
5//!
6//! Implements optional 2×/4× oversampling around the neural model to reduce
7//! aliasing from non-linear activations, following the half-band filter design
8//! principles of Kahles, Esqueda & Välimäki (JAES 2019).
9//!
10//! ## Architecture
11//!
12//! Each 2× stage uses a Kaiser-windowed half-band FIR filter (25 taps, β=12,
13//! \>100 dB stop-band). The half-band property h\[2n\]=0 (n≠D/2) halves the
14//! effective MAC count per sample.
15//!
16//! - **Upsampler**: inserts zeros → filters. Even outputs = x[n-D/2]*0.5;
17//! odd outputs = convolution with non-zero odd taps.
18//! - **Downsampler**: FIR at full rate → decimates by 2. Uses contiguous
19//! double-buffer delay line (same pattern as `NamResampler`).
20//!
21//! ## RT-Safety
22//!
23//! All allocation in `OversampleEngine::new()`. During `process()`,
24//! only pre-allocated buffers — zero alloc, zero heap-drop.
25//!
26//! Factor change requires rebuild (off-RT), same path as model hot-swap.
27
28use super::stage::X2Stage;
29use crate::common::diagnostics::NamErrorCode;
30use crate::math::common::AlignedVec;
31
32/// Half-band FIR filter length (≡ 1 mod 4 so D=HB_TAPS/2 is even).
33/// 25 taps, D=12. Kaiser β=12 → >100 dB stop-band rejection.
34pub(crate) const HB_TAPS: usize = 25;
35/// Filter delay (group delay = HB_TAPS/2 = 12 samples at native rate).
36pub(crate) const HB_DELAY: usize = HB_TAPS / 2;
37
38/// Oversampling factor.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
40pub enum OversampleFactor {
41 /// No oversampling — pass-through with zero overhead.
42 #[default]
43 Off,
44 /// 2× oversampling (one half-band stage).
45 X2,
46 /// 4× oversampling (two cascaded half-band stages).
47 X4,
48}
49
50impl OversampleFactor {
51 /// Creates an `OversampleFactor` from a CLAP parameter value (0.0, 1.0, 2.0).
52 pub fn from_f32(val: f32) -> Self {
53 match val.round() as i32 {
54 1 => Self::X2,
55 2 => Self::X4,
56 _ => Self::Off,
57 }
58 }
59
60 /// Converts to its CLAP parameter value (0.0, 1.0, 2.0).
61 pub fn to_f32(self) -> f32 {
62 match self {
63 Self::Off => 0.0,
64 Self::X2 => 1.0,
65 Self::X4 => 2.0,
66 }
67 }
68
69 /// Returns the sample count multiplier (1, 2, or 4).
70 #[inline]
71 pub const fn multiplier(self) -> usize {
72 match self {
73 Self::Off => 1,
74 Self::X2 => 2,
75 Self::X4 => 4,
76 }
77 }
78
79 /// Returns the number of cascaded 2× stages (0, 1, or 2).
80 #[inline]
81 pub const fn stage_count(self) -> usize {
82 match self {
83 Self::Off => 0,
84 Self::X2 => 1,
85 Self::X4 => 2,
86 }
87 }
88}
89
90/// Typed bundle of oversampling stages, eliminating `Option::unwrap`
91/// on the RT hot-path. Variant discriminant is a zero-cost compile-time
92/// guarantee that the stages (when present) are always valid.
93enum OsStages {
94 Off,
95 X2 {
96 stage1: X2Stage,
97 },
98 X4 {
99 stage1: X2Stage,
100 stage2: Box<X2Stage>,
101 },
102}
103
104/// RT-safe half-band oversampling engine.
105///
106/// Wraps 1–2 cascaded 2× stages. Off mode is zero-cost pass-through
107/// (no state, infallible `copy_nonoverlapping`).
108///
109/// ## Usage (per stereo channel)
110///
111/// ```ignore
112/// // 1. Upsample model-rate input to oversampled rate
113/// let n_os = engine.upsample(&input[..n_native], os_up_buf);
114/// // 2. Model processes at oversampled rate
115/// model.process(&os_up_buf[..n_os], &mut os_model_buf[..n_os]);
116/// // 3. Downsample back to native rate
117/// let n_out = engine.downsample(&os_model_buf[..n_os], output);
118/// ```
119pub struct OversampleEngine {
120 factor: OversampleFactor,
121 stages: OsStages,
122 /// Scratch for X4 cascaded inter-stage (2× intermediate).
123 /// Sized for max_input × 2 (the intermediate upsampled rate between stages).
124 inter_buf: AlignedVec<f32>,
125 max_samples: usize,
126}
127
128impl OversampleEngine {
129 /// Creates a new engine with pre-allocated buffers.
130 ///
131 /// `max_input_samples`: max block size at native model rate
132 /// (e.g., `MAX_RESAMP_BUF = 8192`).
133 pub fn new(factor: OversampleFactor, max_input_samples: usize) -> Result<Self, NamErrorCode> {
134 let inter_size = if factor.stage_count() >= 2 {
135 max_input_samples * 2
136 } else {
137 1
138 };
139
140 let stages = match factor {
141 OversampleFactor::Off => OsStages::Off,
142 OversampleFactor::X2 => OsStages::X2 {
143 stage1: X2Stage::new()?,
144 },
145 OversampleFactor::X4 => OsStages::X4 {
146 stage1: X2Stage::new()?,
147 stage2: Box::new(X2Stage::new()?),
148 },
149 };
150 Ok(Self {
151 factor,
152 stages,
153 inter_buf: AlignedVec::new(inter_size, 0.0f32)?,
154 max_samples: max_input_samples,
155 })
156 }
157
158 /// Returns the current oversampling factor.
159 #[inline]
160 pub fn factor(&self) -> OversampleFactor {
161 self.factor
162 }
163
164 /// Returns `true` when oversampling is bypassed (Off).
165 #[inline]
166 pub fn is_bypass(&self) -> bool {
167 matches!(self.stages, OsStages::Off)
168 }
169
170 /// Returns the group delay in samples at the native (model) rate.
171 ///
172 /// Each 2× half-band stage introduces HB_DELAY (= 12) samples.
173 /// Off → 0, X2 → 12, X4 → 24.
174 #[inline]
175 pub fn latency_samples(&self) -> usize {
176 match self.factor {
177 OversampleFactor::Off => 0,
178 OversampleFactor::X2 => HB_DELAY,
179 OversampleFactor::X4 => 2 * HB_DELAY,
180 }
181 }
182
183 /// Upsamples mono input from native rate to oversampled rate.
184 ///
185 /// `output` must have room for `input.len() * factor.multiplier()` samples.
186 /// Returns number of oversampled samples written.
187 pub fn upsample(&mut self, input: &[f32], output: &mut [f32]) -> usize {
188 let n_in = input.len().min(self.max_samples);
189 let input = &input[..n_in];
190 debug_assert!(input.len() <= self.max_samples);
191 debug_assert!(
192 output.len() >= input.len() * self.factor.multiplier(),
193 "oversample: output buffer too small for upsampling factor"
194 );
195
196 match &mut self.stages {
197 OsStages::Off => {
198 let n = input.len().min(output.len());
199 // SAFETY: both input and output are valid for `n` elements
200 // (`n` is the minimum of both lengths, computed from the
201 // caller-provided slices which are already in scope). The
202 // regions do not overlap because `output` is a distinct
203 // mutable buffer.
204 unsafe {
205 core::ptr::copy_nonoverlapping(input.as_ptr(), output.as_mut_ptr(), n);
206 }
207 n
208 }
209 OsStages::X2 { stage1 } => stage1.upsample(input, output),
210 OsStages::X4 { stage1, stage2 } => {
211 let n_x2 = stage1.upsample(input, &mut self.inter_buf[..input.len() * 2]);
212 stage2.upsample(&self.inter_buf[..n_x2], output)
213 }
214 }
215 }
216
217 /// Downsamples mono input from oversampled rate back to native rate.
218 ///
219 /// `output` must have room for `input.len() / factor.multiplier()` samples.
220 /// Returns number of native-rate samples written.
221 pub fn downsample(&mut self, input: &[f32], output: &mut [f32]) -> usize {
222 let max_os = self.max_samples * self.factor.multiplier();
223 let n_in = input.len().min(max_os);
224 let input = &input[..n_in];
225 debug_assert!(
226 output.len() >= input.len() / self.factor.multiplier(),
227 "oversample: output buffer too small for downsampling factor"
228 );
229 match &mut self.stages {
230 OsStages::Off => {
231 let n = input.len().min(output.len());
232 // SAFETY: both input and output are valid for `n` elements
233 // (minimum of both lengths). The regions do not overlap
234 // because `output` is a distinct mutable buffer.
235 unsafe {
236 core::ptr::copy_nonoverlapping(input.as_ptr(), output.as_mut_ptr(), n);
237 }
238 n
239 }
240 OsStages::X2 { stage1 } => stage1.downsample(input, output),
241 OsStages::X4 { stage1, stage2 } => {
242 let n_x2 = stage2.downsample(input, &mut self.inter_buf[..input.len() / 2]);
243 stage1.downsample(&self.inter_buf[..n_x2], output)
244 }
245 }
246 }
247}
248
249/// Atomic bundle of stereo oversampling engines delivered via SPSC.
250///
251/// L and R engines are built together on the main thread and consumed
252/// together on the RT thread, ensuring they always share the same factor.
253pub struct OsEnginePair {
254 /// Left-channel oversampling engine.
255 pub l: Box<OversampleEngine>,
256 /// Right-channel oversampling engine.
257 pub r: Box<OversampleEngine>,
258}
259
260#[cfg(test)]
261#[path = "oversample_test.rs"]
262mod oversample_test;