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;
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;