Skip to main content

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;