bbx_dsp/blocks/effectors/
mixer.rs

1//! Channel-wise audio mixer block.
2
3use std::marker::PhantomData;
4
5use crate::{
6    block::Block, channel::ChannelConfig, context::DspContext, graph::MAX_BLOCK_INPUTS, parameter::ModulationOutput,
7    sample::Sample,
8};
9
10/// Normalization strategy for summed signals.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum NormalizationStrategy {
13    /// Divide by number of sources (average).
14    Average,
15    /// Divide by sqrt(N) for constant power summing.
16    #[default]
17    ConstantPower,
18}
19
20/// A channel-wise audio mixer that sums multiple sources per output channel.
21///
22/// Unlike [`MatrixMixerBlock`](super::matrix_mixer::MatrixMixerBlock) which requires explicit
23/// gain setup, `MixerBlock` automatically groups inputs by channel and sums them. This is
24/// useful for combining multiple audio sources into a single stereo (or N-channel) output.
25///
26/// # Input Organization
27///
28/// Inputs are organized in groups, where each group represents one source's contribution to
29/// all output channels. For stereo output with 3 sources:
30/// - Inputs 0, 1: Source A (L, R)
31/// - Inputs 2, 3: Source B (L, R)
32/// - Inputs 4, 5: Source C (L, R)
33///
34/// The mixer sums: Output L = A.L + B.L + C.L, Output R = A.R + B.R + C.R
35pub struct MixerBlock<S: Sample> {
36    num_sources: usize,
37    num_channels: usize,
38    normalization: NormalizationStrategy,
39    _phantom: PhantomData<S>,
40}
41
42impl<S: Sample> MixerBlock<S> {
43    /// Create a new mixer for the given number of sources and output channels.
44    ///
45    /// # Arguments
46    /// * `num_sources` - Number of sources to mix (each provides all channels)
47    /// * `num_channels` - Number of output channels (e.g., 2 for stereo)
48    ///
49    /// # Panics
50    /// Panics if the total input count exceeds MAX_BLOCK_INPUTS (16).
51    pub fn new(num_sources: usize, num_channels: usize) -> Self {
52        assert!(num_sources > 0, "Must have at least one source");
53        assert!(num_channels > 0, "Must have at least one channel");
54        assert!(
55            num_sources * num_channels <= MAX_BLOCK_INPUTS,
56            "Total inputs {} exceeds MAX_BLOCK_INPUTS {}",
57            num_sources * num_channels,
58            MAX_BLOCK_INPUTS
59        );
60        Self {
61            num_sources,
62            num_channels,
63            normalization: NormalizationStrategy::ConstantPower,
64            _phantom: PhantomData,
65        }
66    }
67
68    /// Create a stereo mixer for the given number of sources.
69    pub fn stereo(num_sources: usize) -> Self {
70        Self::new(num_sources, 2)
71    }
72
73    /// Set the normalization strategy.
74    pub fn with_normalization(mut self, normalization: NormalizationStrategy) -> Self {
75        self.normalization = normalization;
76        self
77    }
78
79    /// Returns the number of sources being mixed.
80    pub fn num_sources(&self) -> usize {
81        self.num_sources
82    }
83
84    /// Returns the number of output channels.
85    pub fn num_channels(&self) -> usize {
86        self.num_channels
87    }
88}
89
90impl<S: Sample> Block<S> for MixerBlock<S> {
91    fn process(&mut self, inputs: &[&[S]], outputs: &mut [&mut [S]], _modulation_values: &[S], _context: &DspContext) {
92        let num_channels = self.num_channels.min(outputs.len());
93        if num_channels == 0 || inputs.is_empty() {
94            return;
95        }
96
97        let num_samples = outputs[0].len();
98        let normalization_factor = match self.normalization {
99            NormalizationStrategy::Average => S::from_f64(1.0 / self.num_sources as f64),
100            NormalizationStrategy::ConstantPower => S::from_f64(1.0 / (self.num_sources as f64).sqrt()),
101        };
102
103        for (ch, output) in outputs.iter_mut().enumerate().take(num_channels) {
104            for sample in output.iter_mut().take(num_samples) {
105                *sample = S::ZERO;
106            }
107
108            for source_idx in 0..self.num_sources {
109                let input_idx = source_idx * self.num_channels + ch;
110                if let Some(input) = inputs.get(input_idx) {
111                    let len = num_samples.min(input.len());
112                    for i in 0..len {
113                        output[i] += input[i];
114                    }
115                }
116            }
117
118            for sample in output.iter_mut().take(num_samples) {
119                *sample *= normalization_factor;
120            }
121        }
122    }
123
124    #[inline]
125    fn input_count(&self) -> usize {
126        self.num_sources * self.num_channels
127    }
128
129    #[inline]
130    fn output_count(&self) -> usize {
131        self.num_channels
132    }
133
134    #[inline]
135    fn modulation_outputs(&self) -> &[ModulationOutput] {
136        &[]
137    }
138
139    #[inline]
140    fn channel_config(&self) -> ChannelConfig {
141        ChannelConfig::Explicit
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use crate::channel::ChannelLayout;
149
150    fn test_context() -> DspContext {
151        DspContext {
152            sample_rate: 44100.0,
153            num_channels: 2,
154            buffer_size: 4,
155            current_sample: 0,
156            channel_layout: ChannelLayout::Stereo,
157        }
158    }
159
160    #[test]
161    fn test_mixer_two_stereo_sources() {
162        let mut mixer = MixerBlock::<f32>::stereo(2);
163        let context = test_context();
164
165        // Source 0: L=[1,1,1,1], R=[2,2,2,2]
166        // Source 1: L=[3,3,3,3], R=[4,4,4,4]
167        let src0_l = [1.0f32; 4];
168        let src0_r = [2.0f32; 4];
169        let src1_l = [3.0f32; 4];
170        let src1_r = [4.0f32; 4];
171        let mut out_l = [0.0f32; 4];
172        let mut out_r = [0.0f32; 4];
173
174        let inputs: [&[f32]; 4] = [&src0_l, &src0_r, &src1_l, &src1_r];
175        let mut outputs: [&mut [f32]; 2] = [&mut out_l, &mut out_r];
176
177        mixer.process(&inputs, &mut outputs, &[], &context);
178
179        // Default is ConstantPower: L = (1+3)/sqrt(2), R = (2+4)/sqrt(2)
180        let sqrt2 = 2.0_f32.sqrt();
181        for &sample in &out_l {
182            assert!((sample - 4.0 / sqrt2).abs() < 1e-6);
183        }
184        for &sample in &out_r {
185            assert!((sample - 6.0 / sqrt2).abs() < 1e-6);
186        }
187    }
188
189    #[test]
190    fn test_mixer_average_normalization() {
191        let mut mixer = MixerBlock::<f32>::stereo(2).with_normalization(NormalizationStrategy::Average);
192        let context = test_context();
193
194        let src0_l = [2.0f32; 4];
195        let src0_r = [4.0f32; 4];
196        let src1_l = [2.0f32; 4];
197        let src1_r = [4.0f32; 4];
198        let mut out_l = [0.0f32; 4];
199        let mut out_r = [0.0f32; 4];
200
201        let inputs: [&[f32]; 4] = [&src0_l, &src0_r, &src1_l, &src1_r];
202        let mut outputs: [&mut [f32]; 2] = [&mut out_l, &mut out_r];
203
204        mixer.process(&inputs, &mut outputs, &[], &context);
205
206        // Expected: L = (2+2)/2 = 2, R = (4+4)/2 = 4
207        for &sample in &out_l {
208            assert!((sample - 2.0).abs() < 1e-6);
209        }
210        for &sample in &out_r {
211            assert!((sample - 4.0).abs() < 1e-6);
212        }
213    }
214
215    #[test]
216    fn test_mixer_constant_power_normalization() {
217        let mut mixer = MixerBlock::<f32>::stereo(4).with_normalization(NormalizationStrategy::ConstantPower);
218        let context = test_context();
219
220        // 4 sources, all with value 1.0
221        let inputs: Vec<[f32; 4]> = vec![[1.0f32; 4]; 8]; // 4 sources * 2 channels
222        let input_refs: Vec<&[f32]> = inputs.iter().map(|a| a.as_slice()).collect();
223        let mut out_l = [0.0f32; 4];
224        let mut out_r = [0.0f32; 4];
225        let mut outputs: [&mut [f32]; 2] = [&mut out_l, &mut out_r];
226
227        mixer.process(&input_refs, &mut outputs, &[], &context);
228
229        // Expected: (1+1+1+1) / sqrt(4) = 4/2 = 2
230        for &sample in &out_l {
231            assert!((sample - 2.0).abs() < 1e-6);
232        }
233    }
234
235    #[test]
236    fn test_mixer_mono_three_sources() {
237        let mut mixer = MixerBlock::<f32>::new(3, 1);
238        let context = test_context();
239
240        let src0 = [1.0f32; 4];
241        let src1 = [2.0f32; 4];
242        let src2 = [3.0f32; 4];
243        let mut output = [0.0f32; 4];
244
245        let inputs: [&[f32]; 3] = [&src0, &src1, &src2];
246        let mut outputs: [&mut [f32]; 1] = [&mut output];
247
248        mixer.process(&inputs, &mut outputs, &[], &context);
249
250        // Default is ConstantPower: (1+2+3) / sqrt(3)
251        let expected = 6.0 / 3.0_f32.sqrt();
252        for &sample in &output {
253            assert!((sample - expected).abs() < 1e-6);
254        }
255    }
256
257    #[test]
258    fn test_mixer_input_output_counts() {
259        let mixer = MixerBlock::<f32>::new(3, 2);
260        assert_eq!(mixer.input_count(), 6); // 3 sources * 2 channels
261        assert_eq!(mixer.output_count(), 2);
262        assert_eq!(mixer.channel_config(), ChannelConfig::Explicit);
263    }
264
265    #[test]
266    fn test_mixer_f64() {
267        let mut mixer = MixerBlock::<f64>::stereo(2);
268        let context = test_context();
269
270        let src0_l = [0.5f64; 4];
271        let src0_r = [0.25f64; 4];
272        let src1_l = [0.5f64; 4];
273        let src1_r = [0.25f64; 4];
274        let mut out_l = [0.0f64; 4];
275        let mut out_r = [0.0f64; 4];
276
277        let inputs: [&[f64]; 4] = [&src0_l, &src0_r, &src1_l, &src1_r];
278        let mut outputs: [&mut [f64]; 2] = [&mut out_l, &mut out_r];
279
280        mixer.process(&inputs, &mut outputs, &[], &context);
281
282        // Default is ConstantPower: (0.5+0.5)/sqrt(2) and (0.25+0.25)/sqrt(2)
283        let sqrt2 = 2.0_f64.sqrt();
284        for &sample in &out_l {
285            assert!((sample - 1.0 / sqrt2).abs() < 1e-12);
286        }
287        for &sample in &out_r {
288            assert!((sample - 0.5 / sqrt2).abs() < 1e-12);
289        }
290    }
291
292    #[test]
293    #[should_panic(expected = "Must have at least one source")]
294    fn test_mixer_zero_sources_panics() {
295        let _ = MixerBlock::<f32>::new(0, 2);
296    }
297
298    #[test]
299    #[should_panic(expected = "Must have at least one channel")]
300    fn test_mixer_zero_channels_panics() {
301        let _ = MixerBlock::<f32>::new(2, 0);
302    }
303
304    #[test]
305    #[should_panic(expected = "exceeds MAX_BLOCK_INPUTS")]
306    fn test_mixer_exceeds_max_inputs_panics() {
307        let _ = MixerBlock::<f32>::new(9, 2); // 9*2 = 18 > 16
308    }
309}