cute_dsp/
spacing.rs

1//! Spacing: Custom Room Reverb Effect
2//!
3//! This module provides a reverb-like effect with support for custom source and receiver positions.
4//! It uses a multi-tap delay network to simulate early reflections and room size.
5
6use crate::delay::{Delay, InterpolatorLinear};
7use num_traits::{Float, FromPrimitive};
8use crate::filters::Biquad;
9use crate::mix::Hadamard;
10
11/// 3D position for source/receiver
12#[derive(Clone, Copy, Debug)]
13pub struct Position<T: Float> {
14    pub x: T,
15    pub y: T,
16    pub z: T,
17}
18
19impl<T: Float> Position<T> {
20    pub fn distance(&self, other: &Position<T>) -> T {
21        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2) + (self.z - other.z).powi(2)).sqrt()
22    }
23}
24
25/// A single path from source to receiver (could be direct or a reflection)
26#[derive(Clone, Debug)]
27pub struct Path<T: Float> {
28    pub delay_samples: T,
29    pub gain: T,
30    // Future: add filter, wall absorption, etc.
31}
32
33/// Main Spacing effect struct
34pub struct Spacing<T: Float> {
35    pub sample_rate: T,
36    pub sources: Vec<Position<T>>,
37    pub receivers: Vec<Position<T>>,
38    pub paths: Vec<(usize, usize, Path<T>)>, // (source_idx, receiver_idx, Path)
39    pub delays: Vec<Delay<T, InterpolatorLinear<T>>>,
40    // Reverb-like parameters
41    pub room_size: T, // scales all distances
42    pub damping: T,   // 0 = no damping, 1 = max damping
43    // New parameters
44    pub diff: T,      // 0 = no diffusion, 1 = max
45    pub bass: T,      // 0 = flat, >0 = boost, <0 = cut
46    pub decay: T,     // 0 = no decay, 1 = max decay
47    pub cross: T,     // 0 = no cross-mix, 1 = max
48    // State for filters/mixers
49    pub bass_filters: Vec<Biquad<T>>,
50    pub cross_mixer: Option<Hadamard<T>>,
51}
52
53impl<T: Float + FromPrimitive> Spacing<T> {
54    /// Create a new Spacing effect with a given sample rate
55    pub fn new(sample_rate: T) -> Self {
56        Self {
57            sample_rate,
58            sources: Vec::new(),
59            receivers: Vec::new(),
60            paths: Vec::new(),
61            delays: Vec::new(),
62            room_size: T::one(),
63            damping: T::zero(),
64            diff: T::zero(),
65            bass: T::zero(),
66            decay: T::zero(),
67            cross: T::zero(),
68            bass_filters: Vec::new(),
69            cross_mixer: None,
70        }
71    }
72
73    /// Set room size (scales all distances)
74    pub fn set_room_size(&mut self, size: T) {
75        let min = T::from(0.01).unwrap();
76        self.room_size = if size < min { min } else { size };
77    }
78    /// Set damping (0 = no damping, 1 = max damping)
79    pub fn set_damping(&mut self, damping: T) {
80        self.damping = if damping < T::zero() {
81            T::zero()
82        } else if damping > T::one() {
83            T::one()
84        } else {
85            damping
86        };
87    }
88    /// Set diffusion amount
89    pub fn set_diff(&mut self, diff: T) {
90        self.diff = if diff < T::zero() { T::zero() } else if diff > T::one() { T::one() } else { diff };
91    }
92    /// Set bass boost/cut (dB)
93    pub fn set_bass(&mut self, bass: T) {
94        self.bass = bass;
95        // Reconfigure filters
96        self.bass_filters.clear();
97        for _ in 0..self.receivers.len() {
98            let mut biq = Biquad::new(false);
99            let freq = T::from_f32(200.0).unwrap() / self.sample_rate; // 200 Hz cutoff
100            biq.low_shelf(freq, bass);
101            self.bass_filters.push(biq);
102        }
103    }
104    /// Set decay (0 = no decay, 1 = max decay)
105    pub fn set_decay(&mut self, decay: T) {
106        self.decay = if decay < T::zero() { T::zero() } else if decay > T::one() { T::one() } else { decay };
107    }
108    /// Set cross-mix (0 = none, 1 = max)
109    pub fn set_cross(&mut self, cross: T) {
110        self.cross = if cross < T::zero() { T::zero() } else if cross > T::one() { T::one() } else { cross };
111        if self.cross > T::zero() {
112            self.cross_mixer = Some(Hadamard::new(self.receivers.len()));
113        } else {
114            self.cross_mixer = None;
115        }
116    }
117
118    /// Add a source position
119    pub fn add_source(&mut self, pos: Position<T>) -> usize {
120        self.sources.push(pos);
121        self.sources.len() - 1
122    }
123
124    /// Add a receiver position
125    pub fn add_receiver(&mut self, pos: Position<T>) -> usize {
126        self.receivers.push(pos);
127        // Add a bass filter for the new receiver
128        let mut biq = Biquad::new(false);
129        let freq = T::from_f32(200.0).unwrap() / self.sample_rate;
130        biq.low_shelf(freq, self.bass);
131        self.bass_filters.push(biq);
132        self.receivers.len() - 1
133    }
134
135    /// Add a path (direct or reflection) between a source and receiver
136    pub fn add_path(&mut self, source_idx: usize, receiver_idx: usize, gain: T, extra_distance: T) {
137        let src = self.sources[source_idx];
138        let recv = self.receivers[receiver_idx];
139        let distance = (src.distance(&recv) + extra_distance) * self.room_size;
140        let speed_of_sound = T::from(343.0).unwrap();
141        let delay_samples = distance / speed_of_sound * self.sample_rate;
142        self.paths.push((source_idx, receiver_idx, Path { delay_samples, gain }));
143        let delay_len = delay_samples.ceil().to_usize().unwrap_or(1) + 1;
144        self.delays.push(Delay::new(InterpolatorLinear::new(), delay_len));
145    }
146
147    /// Clear all paths and delays
148    pub fn clear_paths(&mut self) {
149        self.paths.clear();
150        self.delays.clear();
151    }
152
153    /// Process a buffer for all receivers (multi-source, multi-output)
154    pub fn process(&mut self, inputs: &[&[T]], outputs: &mut [Vec<T>]) {
155        let len = if let Some(input) = inputs.get(0) { input.len() } else { 0 };
156        for out in outputs.iter_mut() {
157            out.clear();
158            out.resize(len, T::zero());
159        }
160        // Optionally apply input diffusion (pre-mix)
161        let mut diff_inputs: Vec<Vec<T>> = vec![];
162        if self.diff > T::zero() && inputs.len() > 1 {
163            diff_inputs = inputs.iter().map(|x| x.to_vec()).collect();
164            let mut frame: Vec<T> = diff_inputs.iter().map(|v| v[0]).collect();
165            let hadamard = Hadamard::new(inputs.len());
166            for i in 0..len {
167                for (j, v) in diff_inputs.iter_mut().enumerate() {
168                    frame[j] = v[i];
169                }
170                hadamard.in_place(&mut frame);
171                for (j, v) in diff_inputs.iter_mut().enumerate() {
172                    v[i] = frame[j] * self.diff + v[i] * (T::one() - self.diff);
173                }
174            }
175        }
176        let use_inputs: Vec<&[T]> = if self.diff > T::zero() && inputs.len() > 1 {
177            diff_inputs.iter().map(|v| v.as_slice()).collect()
178        } else {
179            inputs.iter().map(|x| *x).collect()
180        };
181        for i in 0..len {
182            let mut wet_sum = vec![T::zero(); outputs.len()];
183            for ((path_idx, (source_idx, receiver_idx, path)), delay) in self.paths.iter().enumerate().zip(self.delays.iter_mut()) {
184                let sample = if let Some(input) = use_inputs.get(*source_idx) {
185                    input[i]
186                } else {
187                    T::zero()
188                };
189                delay.write(sample);
190                let mut delayed = delay.read(path.delay_samples) * path.gain;
191                // Apply decay
192                if self.decay > T::zero() {
193                    let decay_factor = T::one() - self.decay * T::from_f32(0.001).unwrap();
194                    delayed = delayed * decay_factor.powi(i as i32);
195                }
196                // Apply damping
197                if self.damping > T::zero() {
198                    delayed = delayed * (T::one() - self.damping).powi(i as i32);
199                }
200                wet_sum[*receiver_idx] = wet_sum[*receiver_idx] + delayed;
201            }
202            // Apply bass filter per receiver
203            for (r, wet) in wet_sum.iter_mut().enumerate() {
204                if let Some(filt) = self.bass_filters.get_mut(r) {
205                    *wet = filt.process(*wet);
206                }
207            }
208            // Optionally apply cross-mix (Hadamard)
209            if let Some(mixer) = &self.cross_mixer {
210                let mut frame = wet_sum.clone();
211                mixer.in_place(&mut frame);
212                for (r, out) in outputs.iter_mut().enumerate() {
213                    out[i] = frame[r] * self.cross + wet_sum[r] * (T::one() - self.cross);
214                }
215            } else {
216                for (r, out) in outputs.iter_mut().enumerate() {
217                    out[i] = wet_sum[r];
218                }
219            }
220        }
221    }
222
223    // Future: add methods to add reflections, set wall positions, multi-channel output, etc.
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    #[test]
230    fn test_direct_path() {
231        let sample_rate = 48000.0f32;
232        let mut spacing = Spacing::<f32>::new(sample_rate);
233        let src = spacing.add_source(Position { x: 0.0, y: 0.0, z: 0.0 });
234        let recv = spacing.add_receiver(Position { x: 3.43, y: 0.0, z: 0.0 }); // 3.43m = 480 samples
235        spacing.add_path(src, recv, 1.0, 0.0);
236        let mut input = vec![0.0; 500];
237        input[0] = 1.0;
238        let mut outputs = vec![vec![0.0; 500]];
239        spacing.process(&[&input], &mut outputs);
240        let found = outputs[0][478..=482].iter().cloned().fold(f32::MIN, f32::max);
241        assert!((found - 1.0).abs() < 1e-5, "max in window around 480: {}", found);
242    }
243    #[test]
244    fn test_multiple_receivers_and_reflection() {
245        let sample_rate = 48000.0f32;
246        let mut spacing = Spacing::<f32>::new(sample_rate);
247        let src = spacing.add_source(Position { x: 0.0, y: 0.0, z: 0.0 });
248        let recv1 = spacing.add_receiver(Position { x: 3.43, y: 0.0, z: 0.0 });
249        let recv2 = spacing.add_receiver(Position { x: 3.43, y: 3.0, z: 0.0 });
250        spacing.add_path(src, recv1, 1.0, 0.0);
251        spacing.add_path(src, recv2, 0.5, 3.0); // Reflection: extra 3m
252        let mut input = vec![0.0; 1200];
253        input[0] = 1.0;
254        let mut outputs = vec![vec![0.0; 1200]; 2];
255        spacing.process(&[&input], &mut outputs);
256        let found1 = outputs[0][478..=482].iter().cloned().fold(f32::MIN, f32::max);
257        assert!((found1 - 1.0).abs() < 1e-5, "max in window around 480: {}", found1);
258        let d = (3.43f32.powi(2) + 3.0f32.powi(2)).sqrt() + 3.0;
259        let delay = (d / 343.0 * sample_rate).round() as usize;
260        if delay < outputs[1].len() - 1 {
261            let window = &outputs[1][delay.saturating_sub(2)..(delay+3).min(outputs[1].len())];
262            let max_val = window.iter().cloned().fold(f32::MIN, f32::max);
263            assert!((max_val > 0.2 && max_val < 0.3), "max_val={} in window around delay={}", max_val, delay);
264        } else {
265            panic!("Reflection delay {} exceeds output buffer length {}", delay, outputs[1].len());
266        }
267    }
268    #[test]
269    fn test_damping_reduces_amplitude() {
270        let sample_rate = 48000.0f32;
271        let mut spacing = Spacing::<f32>::new(sample_rate);
272        let src = spacing.add_source(Position { x: 0.0, y: 0.0, z: 0.0 });
273        let recv = spacing.add_receiver(Position { x: 3.43, y: 0.0, z: 0.0 });
274        spacing.add_path(src, recv, 1.0, 0.0);
275        let mut input = vec![0.0; 500];
276        input[0] = 1.0;
277        let mut outputs = vec![vec![0.0; 500]];
278        spacing.set_damping(0.0);
279        spacing.process(&[&input], &mut outputs);
280        let found_no_damping = outputs[0][478..=482].iter().cloned().fold(f32::MIN, f32::max);
281        spacing.clear_paths(); spacing.delays.clear();
282        spacing.add_path(src, recv, 1.0, 0.0);
283        spacing.set_damping(0.5);
284        let mut outputs2 = vec![vec![0.0; 500]];
285        spacing.process(&[&input], &mut outputs2);
286        let found_damping = outputs2[0][478..=482].iter().cloned().fold(f32::MIN, f32::max);
287        assert!(found_damping < found_no_damping, "Damping should reduce amplitude: {} vs {}", found_damping, found_no_damping);
288    }
289    #[test]
290    fn test_room_size_affects_delay() {
291        let sample_rate = 48000.0f32;
292        let mut spacing = Spacing::<f32>::new(sample_rate);
293        let src = spacing.add_source(Position { x: 0.0, y: 0.0, z: 0.0 });
294        let recv = spacing.add_receiver(Position { x: 3.43, y: 0.0, z: 0.0 });
295        spacing.set_room_size(1.0);
296        spacing.add_path(src, recv, 1.0, 0.0);
297        let mut input = vec![0.0; 2500];
298        input[0] = 1.0;
299        let mut outputs = vec![vec![0.0; 2500]];
300        spacing.process(&[&input], &mut outputs);
301        let _found1 = outputs[0][478..=482].iter().cloned().fold(f32::MIN, f32::max);
302        spacing.clear_paths(); spacing.delays.clear();
303        spacing.set_room_size(2.0);
304        spacing.add_path(src, recv, 1.0, 0.0);
305        let mut input = vec![0.0; 2500];
306        input[0] = 1.0;
307        let mut outputs2 = vec![vec![0.0; 2500]];
308        spacing.process(&[&input], &mut outputs2);
309        let found2 = outputs2[0][958..=962].iter().cloned().fold(f32::MIN, f32::max);
310        assert!(found2 > 0.2, "Impulse at double room size should be present: {}", found2);
311        assert!(outputs2[0][478..=482].iter().all(|&v| v < 1e-3), "Impulse should not be at original delay");
312    }
313    #[test]
314    fn test_multiple_sources() {
315        let sample_rate = 48000.0f32;
316        let mut spacing = Spacing::<f32>::new(sample_rate);
317        let src1 = spacing.add_source(Position { x: 0.0, y: 0.0, z: 0.0 });
318        let src2 = spacing.add_source(Position { x: 1.0, y: 0.0, z: 0.0 });
319        let recv = spacing.add_receiver(Position { x: 3.43, y: 0.0, z: 0.0 });
320        spacing.add_path(src1, recv, 1.0, 0.0);
321        spacing.add_path(src2, recv, 0.5, 0.0);
322        let mut input1 = vec![0.0; 540];
323        input1[0] = 1.0;
324        let mut input2 = vec![0.0; 540];
325        input2[30] = 1.0;
326        let mut outputs = vec![vec![0.0; 540]];
327        spacing.process(&[&input1, &input2], &mut outputs);
328        let peak1 = outputs[0][478..=482].iter().cloned().fold(f32::MIN, f32::max);
329        let peak2 = outputs[0][368..=372].iter().cloned().fold(f32::MIN, f32::max);
330        assert!(peak1 > 0.4 && peak2 > 0.2, "Both impulses should be present: peak1={}, peak2={}", peak1, peak2);
331        assert!(peak1 + peak2 > 0.9, "Sum of peaks should be > 0.9: {}", peak1 + peak2);
332    }
333}