bbx_dsp/blocks/effectors/
matrix_mixer.rs

1//! Matrix mixer block for flexible NxM channel routing.
2
3use crate::{
4    block::Block,
5    channel::ChannelConfig,
6    context::DspContext,
7    graph::{MAX_BLOCK_INPUTS, MAX_BLOCK_OUTPUTS},
8    parameter::ModulationOutput,
9    sample::Sample,
10};
11
12/// An NxM mixing matrix for flexible channel routing.
13///
14/// Each output is a weighted sum of all inputs, allowing arbitrary
15/// channel mixing configurations. The gain matrix determines how
16/// much of each input contributes to each output.
17///
18/// # Example
19/// A 4x2 matrix mixer can down-mix 4 channels to stereo by setting
20/// appropriate gains for left/right output combinations.
21pub struct MatrixMixerBlock<S: Sample> {
22    num_inputs: usize,
23    num_outputs: usize,
24    gains: [[S; MAX_BLOCK_INPUTS]; MAX_BLOCK_OUTPUTS],
25}
26
27impl<S: Sample> MatrixMixerBlock<S> {
28    /// Create a new matrix mixer with the given input and output counts.
29    ///
30    /// All gains are initialized to zero.
31    ///
32    /// # Panics
33    /// Panics if `inputs` or `outputs` is 0 or greater than 16.
34    pub fn new(inputs: usize, outputs: usize) -> Self {
35        assert!(inputs > 0 && inputs <= MAX_BLOCK_INPUTS);
36        assert!(outputs > 0 && outputs <= MAX_BLOCK_OUTPUTS);
37        Self {
38            num_inputs: inputs,
39            num_outputs: outputs,
40            gains: [[S::ZERO; MAX_BLOCK_INPUTS]; MAX_BLOCK_OUTPUTS],
41        }
42    }
43
44    /// Create a matrix mixer initialized as an identity matrix (passthrough).
45    ///
46    /// Each input channel maps directly to the corresponding output channel
47    /// with unity gain. Requires `inputs == outputs`.
48    ///
49    /// # Panics
50    /// Panics if `channels` is 0 or greater than 16.
51    pub fn identity(channels: usize) -> Self {
52        let mut mixer = Self::new(channels, channels);
53        for ch in 0..channels {
54            mixer.gains[ch][ch] = S::ONE;
55        }
56        mixer
57    }
58
59    /// Set the gain for a specific input-to-output routing.
60    ///
61    /// # Arguments
62    /// * `input` - The input channel index (0-based)
63    /// * `output` - The output channel index (0-based)
64    /// * `gain` - The gain to apply (typically 0.0 to 1.0)
65    ///
66    /// # Panics
67    /// Panics if `input >= num_inputs` or `output >= num_outputs`.
68    pub fn set_gain(&mut self, input: usize, output: usize, gain: S) {
69        assert!(input < self.num_inputs);
70        assert!(output < self.num_outputs);
71        self.gains[output][input] = gain;
72    }
73
74    /// Get the gain for a specific input-to-output routing.
75    ///
76    /// # Panics
77    /// Panics if `input >= num_inputs` or `output >= num_outputs`.
78    pub fn get_gain(&self, input: usize, output: usize) -> S {
79        assert!(input < self.num_inputs);
80        assert!(output < self.num_outputs);
81        self.gains[output][input]
82    }
83
84    /// Returns the number of input channels.
85    pub fn num_inputs(&self) -> usize {
86        self.num_inputs
87    }
88
89    /// Returns the number of output channels.
90    pub fn num_outputs(&self) -> usize {
91        self.num_outputs
92    }
93}
94
95impl<S: Sample> Block<S> for MatrixMixerBlock<S> {
96    fn process(&mut self, inputs: &[&[S]], outputs: &mut [&mut [S]], _modulation_values: &[S], _context: &DspContext) {
97        let num_inputs = self.num_inputs.min(inputs.len());
98        let num_outputs = self.num_outputs.min(outputs.len());
99
100        if num_inputs == 0 || num_outputs == 0 {
101            return;
102        }
103
104        let num_samples = inputs[0].len().min(outputs[0].len());
105
106        for (out_ch, output) in outputs.iter_mut().enumerate().take(num_outputs) {
107            for i in 0..num_samples {
108                let mut sum = S::ZERO;
109                for (in_ch, input) in inputs.iter().enumerate().take(num_inputs) {
110                    sum += input[i] * self.gains[out_ch][in_ch];
111                }
112                output[i] = sum;
113            }
114        }
115    }
116
117    #[inline]
118    fn input_count(&self) -> usize {
119        self.num_inputs
120    }
121
122    #[inline]
123    fn output_count(&self) -> usize {
124        self.num_outputs
125    }
126
127    #[inline]
128    fn modulation_outputs(&self) -> &[ModulationOutput] {
129        &[]
130    }
131
132    #[inline]
133    fn channel_config(&self) -> ChannelConfig {
134        ChannelConfig::Explicit
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::channel::ChannelLayout;
142
143    fn test_context() -> DspContext {
144        DspContext {
145            sample_rate: 44100.0,
146            num_channels: 2,
147            buffer_size: 4,
148            current_sample: 0,
149            channel_layout: ChannelLayout::Stereo,
150        }
151    }
152
153    #[test]
154    fn test_matrix_mixer_identity() {
155        let mut mixer = MatrixMixerBlock::<f32>::identity(2);
156        let context = test_context();
157
158        let left_in = [1.0f32, 2.0, 3.0, 4.0];
159        let right_in = [5.0f32, 6.0, 7.0, 8.0];
160        let mut left_out = [0.0f32; 4];
161        let mut right_out = [0.0f32; 4];
162
163        let inputs: [&[f32]; 2] = [&left_in, &right_in];
164        let mut outputs: [&mut [f32]; 2] = [&mut left_out, &mut right_out];
165
166        mixer.process(&inputs, &mut outputs, &[], &context);
167
168        assert_eq!(left_out, left_in);
169        assert_eq!(right_out, right_in);
170    }
171
172    #[test]
173    fn test_matrix_mixer_mono_sum() {
174        let mut mixer = MatrixMixerBlock::<f32>::new(2, 1);
175        mixer.set_gain(0, 0, 0.5);
176        mixer.set_gain(1, 0, 0.5);
177        let context = test_context();
178
179        let left_in = [1.0f32, 2.0, 3.0, 4.0];
180        let right_in = [1.0f32, 2.0, 3.0, 4.0];
181        let mut mono_out = [0.0f32; 4];
182
183        let inputs: [&[f32]; 2] = [&left_in, &right_in];
184        let mut outputs: [&mut [f32]; 1] = [&mut mono_out];
185
186        mixer.process(&inputs, &mut outputs, &[], &context);
187
188        let expected = [1.0, 2.0, 3.0, 4.0];
189        for (actual, exp) in mono_out.iter().zip(expected.iter()) {
190            assert!((actual - exp).abs() < 1e-6, "Mono sum mismatch: {} vs {}", actual, exp);
191        }
192    }
193
194    #[test]
195    fn test_matrix_mixer_swap_channels() {
196        let mut mixer = MatrixMixerBlock::<f32>::new(2, 2);
197        mixer.set_gain(0, 1, 1.0); // left input -> right output
198        mixer.set_gain(1, 0, 1.0); // right input -> left output
199        let context = test_context();
200
201        let left_in = [1.0f32, 2.0, 3.0, 4.0];
202        let right_in = [5.0f32, 6.0, 7.0, 8.0];
203        let mut left_out = [0.0f32; 4];
204        let mut right_out = [0.0f32; 4];
205
206        let inputs: [&[f32]; 2] = [&left_in, &right_in];
207        let mut outputs: [&mut [f32]; 2] = [&mut left_out, &mut right_out];
208
209        mixer.process(&inputs, &mut outputs, &[], &context);
210
211        assert_eq!(left_out, right_in);
212        assert_eq!(right_out, left_in);
213    }
214
215    #[test]
216    fn test_matrix_mixer_counts() {
217        let mixer = MatrixMixerBlock::<f32>::new(4, 2);
218        assert_eq!(mixer.input_count(), 4);
219        assert_eq!(mixer.output_count(), 2);
220        assert_eq!(mixer.channel_config(), ChannelConfig::Explicit);
221    }
222
223    #[test]
224    #[should_panic]
225    fn test_matrix_mixer_zero_inputs_panics() {
226        let _ = MatrixMixerBlock::<f32>::new(0, 2);
227    }
228
229    #[test]
230    #[should_panic]
231    fn test_matrix_mixer_zero_outputs_panics() {
232        let _ = MatrixMixerBlock::<f32>::new(2, 0);
233    }
234}