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