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, output_channels, prepare_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
71impl OversamplingWrapper<TanhClipper> {
72    /// Creates an oversampled tanh soft clipper with the given drive and factor.
73    pub fn soft_clipper(drive: f32, factor: u8) -> Self {
74        Self::new(TanhClipper::new(drive), factor)
75    }
76}
77
78impl<P: NonlinearSampleProcessor> Processor for OversamplingWrapper<P> {
79    fn prepare(&mut self, cfg: PrepareConfig) {
80        prepare_channels(
81            &mut self.processors,
82            cfg.out_channels as usize,
83            self.prototype.clone(),
84        );
85        prepare_channels(&mut self.previous_inputs, cfg.out_channels as usize, 0.0);
86    }
87
88    fn reset(&mut self) {
89        self.previous_inputs.fill(0.0);
90        for processor in &mut self.processors {
91            processor.reset();
92        }
93    }
94
95    fn process(&mut self, block: &mut ProcessBlock<'_>) {
96        let channels = output_channels(block);
97        if self.processors.len() < channels {
98            self.processors.resize(channels, self.prototype.clone());
99            self.previous_inputs.resize(channels, 0.0);
100        }
101        let frames = block.frames as usize;
102        for channel in 0..channels {
103            for frame in 0..frames {
104                let input = input_sample(block, channel, frame);
105                block.out_audio[channel][frame] = self.process_channel_sample(channel, input);
106            }
107        }
108    }
109}
110
111/// An oversampled tanh soft clipper, the default [`OversamplingWrapper`].
112pub type OversampledSoftClipper = OversamplingWrapper<TanhClipper>;