Skip to main content

sim_lib_audio_dsp/
oversampling.rs

1use sim_lib_audio_graph_core::{PrepareConfig, ProcessBlock, Processor};
2
3use crate::common::{input_sample, prepare_channels, prepared_output_channels};
4
5/// A per-sample nonlinearity that can be wrapped by [`OversamplingWrapper`].
6pub trait NonlinearSampleProcessor: Clone + Send {
7    /// Clears any internal state.
8    fn reset(&mut self);
9    /// Maps one input sample to one output sample.
10    fn process_sample(&mut self, input: f32) -> f32;
11}
12
13/// A tanh soft-clipping nonlinearity.
14#[derive(Clone, Debug, PartialEq)]
15pub struct TanhClipper {
16    drive: f32,
17}
18
19impl TanhClipper {
20    /// Creates a tanh clipper with the given drive (clamped to `>= 0`).
21    pub fn new(drive: f32) -> Self {
22        Self {
23            drive: drive.max(0.0),
24        }
25    }
26}
27
28impl NonlinearSampleProcessor for TanhClipper {
29    fn reset(&mut self) {}
30
31    fn process_sample(&mut self, input: f32) -> f32 {
32        (input * self.drive).tanh()
33    }
34}
35
36/// A [`Processor`] that runs a [`NonlinearSampleProcessor`] at an integer
37/// oversampling factor, interpolating each input across the oversampled steps.
38#[derive(Clone, Debug, PartialEq)]
39pub struct OversamplingWrapper<P: NonlinearSampleProcessor> {
40    prototype: P,
41    processors: Vec<P>,
42    previous_inputs: Vec<f32>,
43    factor: u8,
44}
45
46impl<P: NonlinearSampleProcessor> OversamplingWrapper<P> {
47    /// Wraps a nonlinearity at the given oversampling factor (clamped to
48    /// `1..=16`).
49    pub fn new(processor: P, factor: u8) -> Self {
50        Self {
51            prototype: processor,
52            processors: Vec::new(),
53            previous_inputs: Vec::new(),
54            factor: factor.clamp(1, 16),
55        }
56    }
57
58    fn process_channel_sample(&mut self, channel: usize, input: f32) -> f32 {
59        let previous = self.previous_inputs[channel];
60        let mut output = 0.0;
61        for step in 1..=self.factor {
62            let t = step as f32 / self.factor as f32;
63            let upsampled = previous + (input - previous) * t;
64            output = self.processors[channel].process_sample(upsampled);
65        }
66        self.previous_inputs[channel] = input;
67        output
68    }
69
70    #[cfg(all(test, not(debug_assertions)))]
71    pub(crate) fn realtime_state_snapshot(&self) -> Vec<usize> {
72        vec![self.processors.capacity(), self.previous_inputs.capacity()]
73    }
74}
75
76impl OversamplingWrapper<TanhClipper> {
77    /// Creates an oversampled tanh soft clipper with the given drive and factor.
78    pub fn soft_clipper(drive: f32, factor: u8) -> Self {
79        Self::new(TanhClipper::new(drive), factor)
80    }
81}
82
83impl<P: NonlinearSampleProcessor> Processor for OversamplingWrapper<P> {
84    fn prepare(&mut self, cfg: PrepareConfig) {
85        prepare_channels(
86            &mut self.processors,
87            cfg.out_channels as usize,
88            self.prototype.clone(),
89        );
90        prepare_channels(&mut self.previous_inputs, cfg.out_channels as usize, 0.0);
91    }
92
93    fn reset(&mut self) {
94        self.previous_inputs.fill(0.0);
95        for processor in &mut self.processors {
96            processor.reset();
97        }
98    }
99
100    fn process(&mut self, block: &mut ProcessBlock<'_>) {
101        let channels =
102            prepared_output_channels(block, self.processors.len(), "OversamplingWrapper");
103        let frames = block.frames as usize;
104        for channel in 0..channels {
105            for frame in 0..frames {
106                let input = input_sample(block, channel, frame);
107                block.out_audio[channel][frame] = self.process_channel_sample(channel, input);
108            }
109        }
110    }
111}
112
113/// An oversampled tanh soft clipper, the default [`OversamplingWrapper`].
114pub type OversampledSoftClipper = OversamplingWrapper<TanhClipper>;