cute_dsp/
mix.rs

1//! Multichannel mixing utilities
2//!
3//! This module provides utilities for stereo/multichannel mixing operations,
4//! including orthogonal matrices and stereo-to-multi channel conversion.
5
6#![allow(unused_imports)]
7
8#[cfg(feature = "std")]
9use std::f32::consts::PI;
10
11#[cfg(not(feature = "std"))]
12use core::f32::consts::PI;
13
14use num_traits::{Float, FromPrimitive};
15
16/// Hadamard matrix: high mixing levels, N log(N) operations
17pub struct Hadamard<T: Float> {
18    size: usize,
19    _marker: core::marker::PhantomData<T>,
20}
21
22impl<T: Float + FromPrimitive> Hadamard<T> {
23    /// Create a new Hadamard matrix with the specified size
24    pub fn new(size: usize) -> Self {
25        Self {
26            size,
27            _marker: core::marker::PhantomData,
28        }
29    }
30    
31    /// Apply the matrix in-place, scaled so it's orthogonal
32    pub fn in_place(&self, data: &mut [T]) {
33        self.unscaled_in_place(data);
34        
35        let factor = self.scaling_factor();
36        for c in 0..self.size {
37            data[c] = data[c] * factor;
38        }
39    }
40    
41    /// Scaling factor applied to make it orthogonal
42    pub fn scaling_factor(&self) -> T {
43        if self.size == 0 {
44            T::one()
45        } else {
46            T::from_f32(1.0 / (self.size as f32).sqrt()).unwrap()
47        }
48    }
49    
50    pub fn unscaled_in_place(&self, data: &mut [T]) {
51        if self.size <= 1 {
52            return;
53        }
54        
55        let mut h_size = 1;
56        while h_size < self.size {
57            for start_index in (0..self.size).step_by(h_size * 2) {
58                for i in start_index..(start_index + h_size).min(self.size) {
59                    if i + h_size < self.size {
60                        let a = data[i];
61                        let b = data[i + h_size];
62                        data[i] = a + b;
63                        data[i + h_size] = a - b;
64                    }
65                }
66            }
67            h_size *= 2;
68        }
69    }
70}
71
72/// Householder matrix: moderate mixing, 2N operations
73pub struct Householder<T: Float> {
74    size: usize,
75    _marker: core::marker::PhantomData<T>,
76}
77
78impl<T: Float + FromPrimitive> Householder<T> {
79    /// Create a new Householder matrix with the specified size
80    pub fn new(size: usize) -> Self {
81        Self {
82            size,
83            _marker: core::marker::PhantomData,
84        }
85    }
86    
87    /// Apply the matrix in-place
88    pub fn in_place(&self, data: &mut [T]) {
89        if self.size < 1 {
90            return;
91        }
92        
93        let factor = T::from_f32(-2.0 / self.size as f32).unwrap();
94        
95        let mut sum = data[0];
96        for i in 1..self.size {
97            sum = sum + data[i];
98        }
99        sum = sum * factor;
100        
101        for i in 0..self.size {
102            data[i] = data[i] + sum;
103        }
104    }
105    
106    /// The matrix is already orthogonal, but this is here for compatibility with Hadamard
107    pub fn scaling_factor(&self) -> T {
108        T::one()
109    }
110}
111
112/// Upmix/downmix a stereo signal to an (even) multi-channel signal
113///
114/// When spreading out, it rotates the input by various amounts (e.g. a four-channel signal
115/// would produce `(left, right, mid, side)`), such that energy is preserved for each pair.
116///
117/// When mixing together, it uses the opposite rotations, such that upmix → downmix
118/// produces the same stereo signal (when scaled by `.scaling_factor1()`).
119pub struct StereoMultiMixer<T: Float> {
120    channels: usize,
121    coeffs: Vec<T>,
122}
123
124impl<T: Float + FromPrimitive> StereoMultiMixer<T> {
125    /// Create a new mixer with the specified number of channels (must be even)
126    pub fn new(channels: usize) -> Self {
127        assert!(channels > 0, "StereoMultiMixer must have a positive number of channels");
128        assert_eq!(channels % 2, 0, "StereoMultiMixer must have an even number of channels");
129        
130        let h_channels = channels / 2;
131        let mut coeffs = vec![T::zero(); channels];
132        
133        coeffs[0] = T::one();
134        coeffs[1] = T::zero();
135        
136        for i in 1..h_channels {
137            let phase = PI * i as f32 / channels as f32;
138            coeffs[2 * i] = T::from_f32(phase.cos()).unwrap();
139            coeffs[2 * i + 1] = T::from_f32(phase.sin()).unwrap();
140        }
141        
142        Self { channels, coeffs }
143    }
144    
145    /// Convert a stereo signal to a multi-channel signal
146    pub fn stereo_to_multi(&self, input: &[T; 2], output: &mut [T]) {
147        let scale = T::from_f32((2.0 / self.channels as f32).sqrt()).unwrap();
148        output[0] = input[0] * scale;
149        output[1] = input[1] * scale;
150        
151        for i in (2..self.channels).step_by(2) {
152            output[i] = (input[0] * self.coeffs[i] + input[1] * self.coeffs[i + 1]) * scale;
153            output[i + 1] = (input[1] * self.coeffs[i] - input[0] * self.coeffs[i + 1]) * scale;
154        }
155    }
156    
157    /// Convert a multi-channel signal back to stereo
158    pub fn multi_to_stereo(&self, input: &[T], output: &mut [T; 2]) {
159        output[0] = input[0];
160        output[1] = input[1];
161        
162        for i in (2..self.channels).step_by(2) {
163            output[0] = output[0] + input[i] * self.coeffs[i] - input[i + 1] * self.coeffs[i + 1];
164            output[1] = output[1] + input[i + 1] * self.coeffs[i] + input[i] * self.coeffs[i + 1];
165        }
166    }
167    
168    /// Scaling factor for the downmix, if channels are phase-aligned
169    pub fn scaling_factor1(&self) -> T {
170        T::from_f32(2.0 / self.channels as f32).unwrap()
171    }
172    
173    /// Scaling factor for the downmix, if channels are independent
174    pub fn scaling_factor2(&self) -> T {
175        T::from_f32((2.0 / self.channels as f32).sqrt()).unwrap()
176    }
177}
178
179/// A cheap (polynomial) almost-energy-preserving crossfade
180///
181/// Maximum energy error: 1.06%, average 0.64%, curves overshoot by 0.3%
182pub fn cheap_energy_crossfade<T: Float + FromPrimitive>(x: T, to_coeff: &mut T, from_coeff: &mut T) {
183    *to_coeff = x;
184    *from_coeff = T::one() - x;
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn test_householder() {
193        let householder = Householder::<f32>::new(4);
194        let mut data = vec![1.0, 2.0, 3.0, 4.0];
195        
196        householder.in_place(&mut data);
197        
198        // Expected result: original - 2*mean*[1,1,1,1]
199        let mean = (1.0 + 2.0 + 3.0 + 4.0) / 4.0;
200        let expected = vec![
201            1.0 - 2.0 * mean,
202            2.0 - 2.0 * mean,
203            3.0 - 2.0 * mean,
204            4.0 - 2.0 * mean,
205        ];
206        
207        for i in 0..4 {
208            assert!((data[i] - expected[i]).abs() < 1e-10);
209        }
210    }
211
212    #[test]
213    fn test_hadamard() {
214        let hadamard = Hadamard::<f32>::new(4);
215        let mut data = vec![1.0, 2.0, 3.0, 4.0];
216
217        hadamard.in_place(&mut data);
218
219        // Correct expected values for 4-point Hadamard transform
220        let expected = vec![5.0, -1.0, -2.0, 0.0];
221        for i in 0..4 {
222            assert!((data[i] - expected[i]).abs() < 1e-6);
223        }
224    }
225
226    #[test]
227    fn test_stereo_multi_mixer() {
228        let mixer = StereoMultiMixer::<f32>::new(4);
229        let input = [1.0, 2.0];
230        let mut output = vec![0.0; 4];
231
232        mixer.stereo_to_multi(&input, &mut output);
233
234        // Check energy preservation
235        let input_energy = input[0] * input[0] + input[1] * input[1];
236        let output_energy = output.iter().map(|&x| x * x).sum::<f32>();
237        assert!((input_energy - output_energy).abs() < 1e-6);
238
239        // Test round-trip with correct scaling factor
240        let mut round_trip = [0.0, 0.0];
241        mixer.multi_to_stereo(&output, &mut round_trip);
242
243        // Use scaling factor for independent channels
244        let scale = mixer.scaling_factor2();
245        assert!((round_trip[0] * scale - input[0]).abs() < 1e-6);
246        assert!((round_trip[1] * scale - input[1]).abs() < 1e-6);
247    }
248
249    #[test]
250    fn test_cheap_energy_crossfade() {
251        let mut to_coeff = 0.0;
252        let mut from_coeff = 0.0;
253
254        cheap_energy_crossfade(0.5, &mut to_coeff, &mut from_coeff);
255
256        // At x=0.5, coefficients should be equal
257        assert!((to_coeff - from_coeff).abs() < 1e-6);
258
259        // Sum should be close to 1.0 (within the error margin)
260        assert!((to_coeff + from_coeff - 1.0).abs() < 0.03);
261
262        // Test at extremes
263        cheap_energy_crossfade(0.0, &mut to_coeff, &mut from_coeff);
264        assert!(to_coeff < 0.01);
265        assert!((from_coeff - 1.0).abs() < 0.01);
266
267        cheap_energy_crossfade(1.0, &mut to_coeff, &mut from_coeff);
268        assert!((to_coeff - 1.0).abs() < 0.01);
269        assert!(from_coeff < 0.01);
270    }
271}