Skip to main content

aether_nodes/
panner.rs

1//! Stereo Panner — constant-power panning.
2//!
3//! Param layout:
4//!   0 = pan  (-1.0 = left, 0.0 = center, 1.0 = right)
5//!   1 = width (0.0 = mono, 1.0 = full stereo, >1.0 = wide)
6
7use aether_core::{node::DspNode, param::ParamBlock, BUFFER_SIZE, MAX_INPUTS};
8
9pub struct Panner {
10    // No state needed for constant-power panning
11}
12
13impl Panner {
14    pub fn new() -> Self {
15        Self {}
16    }
17}
18
19impl Default for Panner {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl DspNode for Panner {
26    fn process(
27        &mut self,
28        inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
29        output: &mut [f32; BUFFER_SIZE],
30        params: &mut ParamBlock,
31        _sample_rate: f32,
32    ) {
33        let silence = [0.0f32; BUFFER_SIZE];
34        let input_l = inputs[0].unwrap_or(&silence);
35        let input_r = inputs[1].unwrap_or(&silence);
36
37        let pan = params.get(0).current.clamp(-1.0, 1.0);
38        let width = params.get(1).current.clamp(0.0, 2.0);
39
40        // Constant-power panning law (sin/cos for equal loudness)
41        let pan_angle = (pan + 1.0) * 0.5 * std::f32::consts::FRAC_PI_2; // Map [-1,1] to [0, π/2]
42        let left_gain = pan_angle.cos();
43        let right_gain = pan_angle.sin();
44
45        for i in 0..BUFFER_SIZE {
46            // Mix input channels
47            let mono = (input_l[i] + input_r[i]) * 0.5;
48            let side = (input_l[i] - input_r[i]) * 0.5;
49
50            // Apply width control
51            let mid = mono;
52            let side_scaled = side * width;
53
54            // Apply panning
55            let left = (mid + side_scaled) * left_gain;
56            let right = (mid - side_scaled) * right_gain;
57
58            // For mono output, mix to center
59            output[i] = (left + right) * 0.5;
60
61            params.tick_all();
62        }
63    }
64
65    fn type_name(&self) -> &'static str {
66        "Panner"
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn test_panner_center() {
76        let mut panner = Panner::new();
77        let mut params = ParamBlock::new();
78        // pan=0.0 (center), width=1.0 (full stereo)
79        for &v in &[0.0f32, 1.0] {
80            params.add(v);
81        }
82
83        let input_l = [0.5f32; BUFFER_SIZE];
84        let input_r = [0.5f32; BUFFER_SIZE];
85        let mut inputs = [None; MAX_INPUTS];
86        inputs[0] = Some(&input_l);
87        inputs[1] = Some(&input_r);
88        let mut output = [0.0f32; BUFFER_SIZE];
89
90        panner.process(&inputs, &mut output, &mut params, 48000.0);
91
92        // Center panning should preserve signal (approximately)
93        assert!(
94            output[0] > 0.2 && output[0] < 0.8,
95            "center pan should preserve level, got {}",
96            output[0]
97        );
98    }
99
100    #[test]
101    fn test_panner_hard_left() {
102        let mut panner = Panner::new();
103        let mut params = ParamBlock::new();
104        // pan=-1.0 (hard left), width=1.0
105        for &v in &[-1.0f32, 1.0] {
106            params.add(v);
107        }
108
109        let input_l = [1.0f32; BUFFER_SIZE];
110        let input_r = [0.0f32; BUFFER_SIZE];
111        let mut inputs = [None; MAX_INPUTS];
112        inputs[0] = Some(&input_l);
113        inputs[1] = Some(&input_r);
114        let mut output = [0.0f32; BUFFER_SIZE];
115
116        panner.process(&inputs, &mut output, &mut params, 48000.0);
117
118        // Hard left should favor left channel
119        assert!(output[0] > 0.3, "hard left should pass left channel");
120    }
121
122    #[test]
123    fn test_panner_mono_width() {
124        let mut panner = Panner::new();
125        let mut params = ParamBlock::new();
126        // pan=0.0 (center), width=0.0 (mono)
127        for &v in &[0.0f32, 0.0] {
128            params.add(v);
129        }
130
131        let input_l = [0.5f32; BUFFER_SIZE];
132        let input_r = [0.5f32; BUFFER_SIZE];
133        let mut inputs = [None; MAX_INPUTS];
134        inputs[0] = Some(&input_l);
135        inputs[1] = Some(&input_r);
136        let mut output = [0.0f32; BUFFER_SIZE];
137
138        panner.process(&inputs, &mut output, &mut params, 48000.0);
139
140        // Mono width should collapse to center (approximately)
141        assert!(
142            output[0] > 0.2 && output[0] < 0.8,
143            "mono width should collapse stereo, got {}",
144            output[0]
145        );
146    }
147}