Skip to main content

rustriff_lib/services/effects/delay/
delay.rs

1use crate::domain::audio_processor::AudioProcessor;
2use crate::domain::dto::effect::delay_dto::DelayDto;
3use crate::domain::dto::effect::effect_dto::EffectDto;
4use crate::domain::effect::Effect;
5use atomic_float::AtomicF32;
6use std::collections::HashMap;
7use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
8use std::sync::Arc;
9
10pub struct Delay {
11    id: u32,
12    name: String,
13    is_active: Arc<AtomicBool>,
14    color: String,
15    delay_time: Arc<AtomicU32>, //20ms - 800ms
16    level: Arc<AtomicF32>,      //0.0-0.95
17    delay_buffer: Vec<f32>,
18    write_pos: usize,
19    sample_rate: u32,
20    delay_in_samples: usize,
21    last_feedback_output: f32,
22}
23
24const MAX_DELAY_TIME_FLOAT: f32 = 800.0;
25
26impl Delay {
27    pub fn new(
28        id: u32,
29        name: String,
30        is_active: bool,
31        color: String,
32        sample_rate: u32,
33        delay_time: u32,
34        level: f32,
35    ) -> Self {
36        let level_arc = Arc::new(AtomicF32::new(level.clamp(0.0, 0.95)));
37        let delay_time_arc = Arc::new(AtomicU32::new(delay_time.clamp(20, 800)));
38
39        let max_samples = (MAX_DELAY_TIME_FLOAT * sample_rate as f32 / 1000.0) as usize;
40        let delay_buffer = vec![0.0; max_samples + 1];
41
42        let mut instance = Self {
43            id,
44            name,
45            is_active: Arc::new(AtomicBool::new(is_active)),
46            color,
47            delay_time: delay_time_arc,
48            level: level_arc,
49            delay_buffer,
50            write_pos: 0,
51            sample_rate,
52            delay_in_samples: 0,
53            last_feedback_output: 0.0,
54        };
55
56        instance.calc_delay_in_samples();
57        instance
58    }
59
60    ///Calculates delay in samples with the given delay time and sample rate.
61    ///
62    /// This also sets the delay in samples to the calculated value
63    fn calc_delay_in_samples(&mut self) {
64        self.delay_in_samples = (self.delay_time.load(Ordering::Relaxed) as f32
65            * self.sample_rate as f32
66            / 1000.0) as usize;
67    }
68
69    // GETTERS
70    pub fn delay_time(&self) -> &Arc<AtomicU32> {
71        &self.delay_time
72    }
73
74    pub fn level(&self) -> &Arc<AtomicF32> {
75        &self.level
76    }
77
78    pub fn delay_buffer(&self) -> &Vec<f32> {
79        &self.delay_buffer
80    }
81
82    pub fn write_pos(&self) -> usize {
83        self.write_pos
84    }
85
86    pub fn sample_rate(&self) -> u32 {
87        self.sample_rate
88    }
89
90    // SETTERS
91    /// Sets the delay time (ms) to the new value clamped between [20,800]
92    ///
93    /// Calls `Delay::calc_delay_in_samples` to recalculate the delay in samples
94    pub fn set_delay_time(&mut self, delay_time: u32) {
95        self.delay_time
96            .store(delay_time.clamp(20, 800), Ordering::Relaxed);
97        self.calc_delay_in_samples()
98    }
99
100    /// Sets the level to the new value clamped between [0.0,0.95]
101    pub fn set_level(&mut self, level: f32) {
102        self.level.store(level.clamp(0.0, 0.95), Ordering::Relaxed);
103    }
104
105    /// Sets the sample rate (Hz) to the new value
106    ///
107    /// Calls `Delay::calc_delay_in_samples` to recalculate the delay in samples and
108    /// resizes the delay_buffer to accommodate for the new sample rate
109    pub fn set_sample_rate(&mut self, sample_rate: u32) {
110        self.sample_rate = sample_rate;
111        let max_samples = (MAX_DELAY_TIME_FLOAT * sample_rate as f32 / 1000.0) as usize;
112        self.delay_buffer.resize(max_samples + 1, 0.0);
113        self.calc_delay_in_samples();
114    }
115}
116
117impl AudioProcessor for Delay {
118    /// Processes a single audio sample through a feedback delay line with linear interpolation.
119    ///
120    /// The signal flow follows these steps:
121    /// 1.  **Interpolated Read**: Calculates a fractional read position to prevent "stepping"
122    ///     artifacts when delay time changes.
123    /// 2.  **Damping**: Applies a simple One-Pole Low-Pass Filter to the delayed signal.
124    /// 3.  **Feedback**: Mixes the filtered delayed signal back into the write head.
125    /// 4.  **Output Mix**: Combines the original (dry) signal with the delayed (wet) signal.
126    fn process(&mut self, sample: f32) -> f32 {
127        if self.delay_buffer.is_empty() {
128            return sample;
129        }
130
131        let feedback_amount = self.level.load(Ordering::Relaxed);
132
133        let target_delay_samples = self.delay_in_samples as f32;
134        let buf_len = self.delay_buffer.len() as f32;
135        let read_pos = (self.write_pos as f32 - target_delay_samples + buf_len) % buf_len;
136
137        let i_part = read_pos.floor() as usize;
138        let f_part = read_pos - i_part as f32;
139        let next_i = (i_part + 1) % self.delay_buffer.len();
140
141        let delayed_sample =
142            self.delay_buffer[i_part] * (1.0 - f_part) + self.delay_buffer[next_i] * f_part;
143
144        let filtered_feedback = (delayed_sample * 0.5) + (self.last_feedback_output * 0.5);
145        self.last_feedback_output = filtered_feedback;
146
147        self.delay_buffer[self.write_pos] = sample + (filtered_feedback * feedback_amount);
148
149        self.write_pos = (self.write_pos + 1) % self.delay_buffer.len();
150
151        sample + (delayed_sample * feedback_amount)
152    }
153}
154
155impl Effect for Delay {
156    fn id(&self) -> u32 {
157        self.id
158    }
159
160    fn name(&self) -> &str {
161        self.name.as_str()
162    }
163
164    fn get_color(&self) -> String {
165        self.color.clone()
166    }
167
168    /// Converts this effect into its serialisable DTO representation.
169    ///
170    /// Called when sending effect state to the frontend or external clients.
171    ///
172    /// # Returns
173    ///
174    /// [`EffectDto::Delay`] with all current parameters
175    fn to_dto(&self) -> EffectDto {
176        EffectDto::Delay(DelayDto {
177            id: self.id(),
178            name: self.name.clone(),
179            is_active: self.is_active(),
180            color: self.color.clone(),
181            delay_time: self.delay_time.load(Ordering::Relaxed),
182            level: self.level.load(Ordering::Relaxed),
183        })
184    }
185
186    fn active_flag(&self) -> Arc<AtomicBool> {
187        self.is_active.clone()
188    }
189
190    fn f32_params(&self) -> HashMap<&'static str, Arc<AtomicF32>> {
191        let mut map = HashMap::new();
192        map.insert("level", Arc::clone(&self.level));
193        map
194    }
195
196    fn u32_params(&self) -> HashMap<&'static str, Arc<AtomicU32>> {
197        let mut map = HashMap::new();
198        map.insert("delay_time", Arc::clone(&self.delay_time));
199        map
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    mod success_path {
208        use super::*;
209
210        #[test]
211        fn test_initialization_and_buffer_size() {
212            let sample_rate = 44100;
213            let delay_time_ms = 100;
214            let delay = Delay::new(
215                1,
216                "Test".to_string(),
217                true,
218                "blue".to_string(),
219                sample_rate,
220                delay_time_ms,
221                0.5,
222            );
223
224            // Max samples for 800ms (as defined in new()) @ 44.1khz is 35280
225            assert!(delay.delay_buffer().len() >= 35280);
226            assert_eq!(delay.sample_rate(), sample_rate);
227        }
228
229        #[test]
230        fn test_signal_passthrough_on_first_sample() {
231            let mut delay = Delay::new(
232                1,
233                "Test".to_string(),
234                true,
235                "blue".to_string(),
236                44100,
237                100,
238                0.5,
239            );
240            let input = 0.8;
241            let output = delay.process(input);
242
243            // On the very first sample, the buffer is empty (0.0).
244            // Output = Dry (0.8) + (Wet (0.0) * 0.5) = 0.8
245            assert_eq!(output, input);
246        }
247
248        #[test]
249        fn test_delay_echo_occurs() {
250            let sample_rate = 1000; // Low SR for easier math
251            let delay_time_ms = 100; // 100ms = 100 samples at 1000Hz
252            let mut delay = Delay::new(
253                1,
254                "Test".to_string(),
255                true,
256                "blue".to_string(),
257                sample_rate,
258                delay_time_ms,
259                0.5,
260            );
261
262            // Input an impulse
263            delay.process(1.0);
264
265            // Process silence for 99 samples
266            for _ in 0..99 {
267                delay.process(0.0);
268            }
269
270            // The 101st sample should contain the delayed signal
271            let echo = delay.process(0.0);
272            assert!(echo > 0.0, "Echo should be audible after delay period");
273        }
274    }
275
276    mod failure_path {
277        use super::*;
278
279        #[test]
280        fn test_parameter_clamping() {
281            // Test level clamping (max 0.95)
282            let mut delay = Delay::new(
283                1,
284                "Test".to_string(),
285                true,
286                "blue".to_string(),
287                44100,
288                100,
289                2.0,
290            );
291            assert!(delay.level().load(Ordering::Relaxed) <= 0.95);
292
293            // Test delay time clamping via setter (20ms - 300ms)
294            delay.set_delay_time(1000);
295            assert_eq!(delay.delay_time().load(Ordering::Relaxed), 800);
296
297            delay.set_delay_time(5);
298            assert_eq!(delay.delay_time().load(Ordering::Relaxed), 20);
299        }
300
301        #[test]
302        fn test_empty_buffer_safety() {
303            let mut delay = Delay::new(
304                1,
305                "Test".to_string(),
306                true,
307                "blue".to_string(),
308                44100,
309                100,
310                0.5,
311            );
312            // Manually force an empty buffer (edge case)
313            delay.set_sample_rate(0);
314            // Should not crash and should return dry signal
315            let output = delay.process(0.5);
316            assert_eq!(output, 0.5);
317        }
318    }
319}