Skip to main content

sim_lib_audio_dsp/
modulation.rs

1use std::f32::consts::TAU;
2
3use sim_lib_audio_graph_core::{PrepareConfig, ProcessBlock, Processor};
4
5use crate::{
6    common::{input_sample, prepare_channels, prepared_output_channels},
7    delay::DelayLine,
8};
9
10/// An LFO-modulated delay [`Processor`] underpinning chorus, flanger, and
11/// vibrato effects.
12#[derive(Clone, Debug, PartialEq)]
13pub struct ModulatedDelayProcessor {
14    base_delay_seconds: f32,
15    depth_seconds: f32,
16    rate_hz: f32,
17    feedback: f32,
18    wet: f32,
19    dry: f32,
20    sample_rate_hz: f32,
21    phase: f32,
22    lines: Vec<DelayLine>,
23}
24
25impl ModulatedDelayProcessor {
26    /// Creates a modulated delay from base delay, modulation depth (both in
27    /// milliseconds), and LFO rate in hertz.
28    pub fn new(base_delay_ms: f32, depth_ms: f32, rate_hz: f32) -> Self {
29        Self {
30            base_delay_seconds: (base_delay_ms / 1000.0).max(0.0),
31            depth_seconds: (depth_ms / 1000.0).max(0.0),
32            rate_hz: rate_hz.max(0.0),
33            feedback: 0.0,
34            wet: 0.5,
35            dry: 0.5,
36            sample_rate_hz: 48_000.0,
37            phase: 0.0,
38            lines: Vec::new(),
39        }
40    }
41
42    /// Returns the processor with feedback set, clamped to `-0.99..=0.99`.
43    pub fn with_feedback(mut self, feedback: f32) -> Self {
44        self.feedback = feedback.clamp(-0.99, 0.99);
45        self
46    }
47
48    /// Returns the processor with explicit dry and wet mix levels.
49    pub fn with_mix(mut self, dry: f32, wet: f32) -> Self {
50        self.dry = dry;
51        self.wet = wet;
52        self
53    }
54
55    fn max_delay_samples(&self) -> usize {
56        ((self.base_delay_seconds + self.depth_seconds) * self.sample_rate_hz).ceil() as usize + 2
57    }
58
59    fn current_delay_samples(&self) -> f32 {
60        let lfo = self.phase.sin() * 0.5 + 0.5;
61        (self.base_delay_seconds + self.depth_seconds * lfo) * self.sample_rate_hz
62    }
63
64    fn advance_phase(&mut self) {
65        if self.sample_rate_hz > 0.0 {
66            self.phase = (self.phase + TAU * self.rate_hz / self.sample_rate_hz).rem_euclid(TAU);
67        }
68    }
69
70    #[cfg(all(test, not(debug_assertions)))]
71    pub(crate) fn realtime_state_snapshot(&self) -> Vec<usize> {
72        let mut snapshot = Vec::with_capacity(self.lines.len() + 1);
73        snapshot.push(self.lines.capacity());
74        snapshot.extend(self.lines.iter().map(DelayLine::allocated_capacity));
75        snapshot
76    }
77}
78
79impl Processor for ModulatedDelayProcessor {
80    fn prepare(&mut self, cfg: PrepareConfig) {
81        self.sample_rate_hz = cfg.sample_rate_hz as f32;
82        let line = DelayLine::new(self.max_delay_samples());
83        prepare_channels(&mut self.lines, cfg.out_channels as usize, line);
84    }
85
86    fn reset(&mut self) {
87        self.phase = 0.0;
88        for line in &mut self.lines {
89            line.reset();
90        }
91    }
92
93    fn process(&mut self, block: &mut ProcessBlock<'_>) {
94        let channels = prepared_output_channels(block, self.lines.len(), "ModulatedDelayProcessor");
95        let frames = block.frames as usize;
96        for frame in 0..frames {
97            let delay = self.current_delay_samples();
98            for channel in 0..channels {
99                let input = input_sample(block, channel, frame);
100                let line = &mut self.lines[channel];
101                let delayed = line.read(delay);
102                line.push(input + delayed * self.feedback);
103                block.out_audio[channel][frame] = input * self.dry + delayed * self.wet;
104            }
105            self.advance_phase();
106        }
107    }
108}
109
110/// A chorus [`Processor`] built on a modulated delay.
111#[derive(Clone, Debug, PartialEq)]
112pub struct Chorus {
113    inner: ModulatedDelayProcessor,
114}
115
116impl Chorus {
117    /// Creates a chorus with the given LFO rate (Hz) and depth (ms).
118    pub fn new(rate_hz: f32, depth_ms: f32) -> Self {
119        Self {
120            inner: ModulatedDelayProcessor::new(18.0, depth_ms, rate_hz).with_mix(0.65, 0.35),
121        }
122    }
123
124    #[cfg(all(test, not(debug_assertions)))]
125    pub(crate) fn realtime_state_snapshot(&self) -> Vec<usize> {
126        self.inner.realtime_state_snapshot()
127    }
128}
129
130impl Processor for Chorus {
131    fn prepare(&mut self, cfg: PrepareConfig) {
132        self.inner.prepare(cfg);
133    }
134
135    fn reset(&mut self) {
136        self.inner.reset();
137    }
138
139    fn process(&mut self, block: &mut ProcessBlock<'_>) {
140        self.inner.process(block);
141    }
142}
143
144/// A flanger [`Processor`] built on a feedback-modulated delay.
145#[derive(Clone, Debug, PartialEq)]
146pub struct Flanger {
147    inner: ModulatedDelayProcessor,
148}
149
150impl Flanger {
151    /// Creates a flanger with the given LFO rate (Hz), depth (ms), and feedback.
152    pub fn new(rate_hz: f32, depth_ms: f32, feedback: f32) -> Self {
153        Self {
154            inner: ModulatedDelayProcessor::new(2.5, depth_ms, rate_hz)
155                .with_feedback(feedback)
156                .with_mix(0.55, 0.45),
157        }
158    }
159
160    #[cfg(all(test, not(debug_assertions)))]
161    pub(crate) fn realtime_state_snapshot(&self) -> Vec<usize> {
162        self.inner.realtime_state_snapshot()
163    }
164}
165
166impl Processor for Flanger {
167    fn prepare(&mut self, cfg: PrepareConfig) {
168        self.inner.prepare(cfg);
169    }
170
171    fn reset(&mut self) {
172        self.inner.reset();
173    }
174
175    fn process(&mut self, block: &mut ProcessBlock<'_>) {
176        self.inner.process(block);
177    }
178}
179
180/// A vibrato [`Processor`] (fully wet modulated delay).
181#[derive(Clone, Debug, PartialEq)]
182pub struct Vibrato {
183    inner: ModulatedDelayProcessor,
184}
185
186impl Vibrato {
187    /// Creates a vibrato with the given LFO rate (Hz) and depth (ms).
188    pub fn new(rate_hz: f32, depth_ms: f32) -> Self {
189        Self {
190            inner: ModulatedDelayProcessor::new(depth_ms, depth_ms, rate_hz).with_mix(0.0, 1.0),
191        }
192    }
193
194    #[cfg(all(test, not(debug_assertions)))]
195    pub(crate) fn realtime_state_snapshot(&self) -> Vec<usize> {
196        self.inner.realtime_state_snapshot()
197    }
198}
199
200impl Processor for Vibrato {
201    fn prepare(&mut self, cfg: PrepareConfig) {
202        self.inner.prepare(cfg);
203    }
204
205    fn reset(&mut self) {
206        self.inner.reset();
207    }
208
209    fn process(&mut self, block: &mut ProcessBlock<'_>) {
210        self.inner.process(block);
211    }
212}