Skip to main content

quiver/modules/
stereo.rs

1//! Mid/side stereo utilities (Q150).
2//!
3//! [`MidSideEncode`] converts a left/right pair to mid/side; [`MidSideDecode`]
4//! converts mid/side back to left/right with an adjustable stereo width. Encoding
5//! then decoding at width 1.0 is an exact identity.
6//!
7//! Conventions: `M = (L + R) * 0.5`, `S = (L - R) * 0.5`; decode
8//! `L = M + S*width`, `R = M - S*width`. Width 0 collapses to mono
9//! (`L == R == M`); a mono source (`L == R`) encodes to `S == 0`.
10
11use crate::port::{GraphModule, PortDef, PortSpec, PortValues, SignalKind};
12use alloc::vec;
13
14/// Encode a left/right stereo pair to mid/side.
15pub struct MidSideEncode {
16    spec: PortSpec,
17}
18
19impl MidSideEncode {
20    pub fn new() -> Self {
21        Self {
22            spec: PortSpec {
23                inputs: vec![
24                    PortDef::new(0, "left", SignalKind::Audio),
25                    PortDef::new(1, "right", SignalKind::Audio),
26                ],
27                outputs: vec![
28                    PortDef::new(10, "mid", SignalKind::Audio),
29                    PortDef::new(11, "side", SignalKind::Audio),
30                ],
31            },
32        }
33    }
34}
35
36impl Default for MidSideEncode {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl GraphModule for MidSideEncode {
43    fn port_spec(&self) -> &PortSpec {
44        &self.spec
45    }
46
47    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
48        let left = inputs.get_or(0, 0.0);
49        let right = inputs.get_or(1, 0.0);
50        outputs.set(10, (left + right) * 0.5);
51        outputs.set(11, (left - right) * 0.5);
52    }
53
54    fn reset(&mut self) {}
55
56    fn set_sample_rate(&mut self, _: f64) {}
57
58    fn type_id(&self) -> &'static str {
59        "mid_side_encode"
60    }
61}
62
63/// Decode a mid/side pair back to left/right, scaling the side signal by `width`.
64///
65/// `width` is an input port (default 1.0, clamped to `0..2`): 0 = mono, 1 = the
66/// exact inverse of [`MidSideEncode`], 2 = doubled stereo width.
67pub struct MidSideDecode {
68    spec: PortSpec,
69}
70
71impl MidSideDecode {
72    pub fn new() -> Self {
73        Self {
74            spec: PortSpec {
75                inputs: vec![
76                    PortDef::new(0, "mid", SignalKind::Audio),
77                    PortDef::new(1, "side", SignalKind::Audio),
78                    PortDef::new(2, "width", SignalKind::CvUnipolar)
79                        .with_default(1.0)
80                        .with_attenuverter(),
81                ],
82                outputs: vec![
83                    PortDef::new(10, "left", SignalKind::Audio),
84                    PortDef::new(11, "right", SignalKind::Audio),
85                ],
86            },
87        }
88    }
89}
90
91impl Default for MidSideDecode {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97impl GraphModule for MidSideDecode {
98    fn port_spec(&self) -> &PortSpec {
99        &self.spec
100    }
101
102    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
103        let mid = inputs.get_or(0, 0.0);
104        let side = inputs.get_or(1, 0.0);
105        let width = inputs.get_or(2, 1.0).clamp(0.0, 2.0);
106        let scaled = side * width;
107        outputs.set(10, mid + scaled);
108        outputs.set(11, mid - scaled);
109    }
110
111    fn reset(&mut self) {}
112
113    fn set_sample_rate(&mut self, _: f64) {}
114
115    fn type_id(&self) -> &'static str {
116        "mid_side_decode"
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    fn encode(l: f64, r: f64) -> (f64, f64) {
125        let mut m = MidSideEncode::new();
126        let mut inputs = PortValues::new();
127        let mut outputs = PortValues::new();
128        inputs.set(0, l);
129        inputs.set(1, r);
130        m.tick(&inputs, &mut outputs);
131        (outputs.get(10).unwrap(), outputs.get(11).unwrap())
132    }
133
134    fn decode(mid: f64, side: f64, width: f64) -> (f64, f64) {
135        let mut d = MidSideDecode::new();
136        let mut inputs = PortValues::new();
137        let mut outputs = PortValues::new();
138        inputs.set(0, mid);
139        inputs.set(1, side);
140        inputs.set(2, width);
141        d.tick(&inputs, &mut outputs);
142        (outputs.get(10).unwrap(), outputs.get(11).unwrap())
143    }
144
145    #[test]
146    fn test_encode_decode_identity() {
147        for &(l, r) in &[(1.0, -1.0), (0.3, 0.7), (-2.5, 4.1), (0.0, 0.0)] {
148            let (mid, side) = encode(l, r);
149            let (dl, dr) = decode(mid, side, 1.0);
150            assert!((dl - l).abs() < 1e-12, "L identity failed: {dl} != {l}");
151            assert!((dr - r).abs() < 1e-12, "R identity failed: {dr} != {r}");
152        }
153    }
154
155    #[test]
156    fn test_width_zero_is_mono() {
157        let (mid, side) = encode(1.0, -1.0);
158        let (dl, dr) = decode(mid, side, 0.0);
159        assert!((dl - dr).abs() < 1e-12, "width 0 must be mono");
160        assert!((dl - mid).abs() < 1e-12, "mono value must equal mid");
161    }
162
163    #[test]
164    fn test_mono_input_has_zero_side() {
165        let (_mid, side) = encode(0.5, 0.5);
166        assert!(side.abs() < 1e-12, "mono input must have zero side");
167    }
168
169    #[test]
170    fn test_width_clamped_and_doubled() {
171        let (mid, side) = encode(1.0, 0.0); // mid 0.5, side 0.5
172        let (dl, dr) = decode(mid, side, 2.0);
173        // width 2 => L = mid + 2*side = 0.5 + 1.0 = 1.5, R = mid - 1.0 = -0.5
174        assert!((dl - 1.5).abs() < 1e-12);
175        assert!((dr + 0.5).abs() < 1e-12);
176        // Over-range width clamps to 2.0.
177        let (dl2, _dr2) = decode(mid, side, 5.0);
178        assert!((dl2 - 1.5).abs() < 1e-12);
179    }
180
181    #[test]
182    fn test_type_ids() {
183        assert_eq!(MidSideEncode::default().type_id(), "mid_side_encode");
184        assert_eq!(MidSideDecode::default().type_id(), "mid_side_decode");
185    }
186}