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, output_channels, prepare_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
71impl Processor for ModulatedDelayProcessor {
72    fn prepare(&mut self, cfg: PrepareConfig) {
73        self.sample_rate_hz = cfg.sample_rate_hz as f32;
74        let line = DelayLine::new(self.max_delay_samples());
75        prepare_channels(&mut self.lines, cfg.out_channels as usize, line);
76    }
77
78    fn reset(&mut self) {
79        self.phase = 0.0;
80        for line in &mut self.lines {
81            line.reset();
82        }
83    }
84
85    fn process(&mut self, block: &mut ProcessBlock<'_>) {
86        let channels = output_channels(block);
87        if self.lines.len() < channels {
88            let max_delay_samples = self.max_delay_samples();
89            self.lines
90                .resize_with(channels, || DelayLine::new(max_delay_samples));
91        }
92        let frames = block.frames as usize;
93        for frame in 0..frames {
94            let delay = self.current_delay_samples();
95            for channel in 0..channels {
96                let input = input_sample(block, channel, frame);
97                let line = &mut self.lines[channel];
98                let delayed = line.read(delay);
99                line.push(input + delayed * self.feedback);
100                block.out_audio[channel][frame] = input * self.dry + delayed * self.wet;
101            }
102            self.advance_phase();
103        }
104    }
105}
106
107/// A chorus [`Processor`] built on a modulated delay.
108#[derive(Clone, Debug, PartialEq)]
109pub struct Chorus {
110    inner: ModulatedDelayProcessor,
111}
112
113impl Chorus {
114    /// Creates a chorus with the given LFO rate (Hz) and depth (ms).
115    pub fn new(rate_hz: f32, depth_ms: f32) -> Self {
116        Self {
117            inner: ModulatedDelayProcessor::new(18.0, depth_ms, rate_hz).with_mix(0.65, 0.35),
118        }
119    }
120}
121
122impl Processor for Chorus {
123    fn prepare(&mut self, cfg: PrepareConfig) {
124        self.inner.prepare(cfg);
125    }
126
127    fn reset(&mut self) {
128        self.inner.reset();
129    }
130
131    fn process(&mut self, block: &mut ProcessBlock<'_>) {
132        self.inner.process(block);
133    }
134}
135
136/// A flanger [`Processor`] built on a feedback-modulated delay.
137#[derive(Clone, Debug, PartialEq)]
138pub struct Flanger {
139    inner: ModulatedDelayProcessor,
140}
141
142impl Flanger {
143    /// Creates a flanger with the given LFO rate (Hz), depth (ms), and feedback.
144    pub fn new(rate_hz: f32, depth_ms: f32, feedback: f32) -> Self {
145        Self {
146            inner: ModulatedDelayProcessor::new(2.5, depth_ms, rate_hz)
147                .with_feedback(feedback)
148                .with_mix(0.55, 0.45),
149        }
150    }
151}
152
153impl Processor for Flanger {
154    fn prepare(&mut self, cfg: PrepareConfig) {
155        self.inner.prepare(cfg);
156    }
157
158    fn reset(&mut self) {
159        self.inner.reset();
160    }
161
162    fn process(&mut self, block: &mut ProcessBlock<'_>) {
163        self.inner.process(block);
164    }
165}
166
167/// A vibrato [`Processor`] (fully wet modulated delay).
168#[derive(Clone, Debug, PartialEq)]
169pub struct Vibrato {
170    inner: ModulatedDelayProcessor,
171}
172
173impl Vibrato {
174    /// Creates a vibrato with the given LFO rate (Hz) and depth (ms).
175    pub fn new(rate_hz: f32, depth_ms: f32) -> Self {
176        Self {
177            inner: ModulatedDelayProcessor::new(depth_ms, depth_ms, rate_hz).with_mix(0.0, 1.0),
178        }
179    }
180}
181
182impl Processor for Vibrato {
183    fn prepare(&mut self, cfg: PrepareConfig) {
184        self.inner.prepare(cfg);
185    }
186
187    fn reset(&mut self) {
188        self.inner.reset();
189    }
190
191    fn process(&mut self, block: &mut ProcessBlock<'_>) {
192        self.inner.process(block);
193    }
194}