bbx_dsp/blocks/effectors/
ambisonic_decoder.rs

1//! Ambisonic decoder block for converting B-format to speaker layouts.
2
3use std::marker::PhantomData;
4
5use crate::{
6    block::Block,
7    channel::{ChannelConfig, ChannelLayout},
8    context::DspContext,
9    graph::{MAX_BLOCK_INPUTS, MAX_BLOCK_OUTPUTS},
10    parameter::ModulationOutput,
11    sample::Sample,
12};
13
14/// Decodes ambisonics B-format to a speaker layout.
15///
16/// Converts SN3D normalized, ACN ordered ambisonic signals to
17/// discrete speaker feeds using a mode-matching decoder.
18///
19/// # Supported Configurations
20/// - Input: 1st order (4 ch), 2nd order (9 ch), or 3rd order (16 ch)
21/// - Output: Stereo, 5.1, 7.1, or custom layouts
22///
23/// # Example
24/// Decode first-order ambisonics to stereo using virtual speakers
25/// at ±30 degrees azimuth.
26pub struct AmbisonicDecoderBlock<S: Sample> {
27    input_order: usize,
28    output_layout: ChannelLayout,
29    decoder_matrix: Box<[[f64; MAX_BLOCK_INPUTS]; MAX_BLOCK_OUTPUTS]>,
30    _phantom: PhantomData<S>,
31}
32
33impl<S: Sample> AmbisonicDecoderBlock<S> {
34    /// Create a new ambisonic decoder.
35    ///
36    /// # Arguments
37    /// * `order` - Ambisonic order (1, 2, or 3)
38    /// * `output_layout` - Target speaker layout
39    ///
40    /// # Panics
41    /// Panics if order is not 1, 2, or 3.
42    pub fn new(order: usize, output_layout: ChannelLayout) -> Self {
43        assert!((1..=3).contains(&order), "Ambisonic order must be 1, 2, or 3");
44
45        let mut decoder = Self {
46            input_order: order,
47            output_layout,
48            decoder_matrix: Box::new([[0.0; MAX_BLOCK_INPUTS]; MAX_BLOCK_OUTPUTS]),
49            _phantom: PhantomData,
50        };
51        decoder.compute_decoder_matrix();
52        decoder
53    }
54
55    /// Returns the ambisonic order.
56    pub fn order(&self) -> usize {
57        self.input_order
58    }
59
60    /// Returns the output layout.
61    pub fn output_layout(&self) -> ChannelLayout {
62        self.output_layout
63    }
64
65    fn compute_decoder_matrix(&mut self) {
66        let speaker_positions = self.get_speaker_positions();
67        let num_speakers = self.output_layout.channel_count();
68        let num_channels = self.input_channel_count();
69
70        for (spk, &(azimuth, elevation)) in speaker_positions.iter().enumerate().take(num_speakers) {
71            let coeffs = self.compute_sh_coefficients(azimuth, elevation);
72            self.decoder_matrix[spk][..num_channels].copy_from_slice(&coeffs[..num_channels]);
73        }
74
75        self.normalize_decoder_matrix();
76    }
77
78    fn get_speaker_positions(&self) -> [(f64, f64); MAX_BLOCK_OUTPUTS] {
79        let mut positions = [(0.0, 0.0); MAX_BLOCK_OUTPUTS];
80
81        match self.output_layout {
82            ChannelLayout::Mono => {
83                positions[0] = (0.0, 0.0);
84            }
85            ChannelLayout::Stereo => {
86                positions[0] = (30.0, 0.0); // Left
87                positions[1] = (-30.0, 0.0); // Right
88            }
89            ChannelLayout::Surround51 => {
90                positions[0] = (30.0, 0.0); // L
91                positions[1] = (-30.0, 0.0); // R
92                positions[2] = (0.0, 0.0); // C
93                positions[3] = (0.0, 0.0); // LFE (not directional)
94                positions[4] = (110.0, 0.0); // Ls
95                positions[5] = (-110.0, 0.0); // Rs
96            }
97            ChannelLayout::Surround71 => {
98                positions[0] = (30.0, 0.0); // L
99                positions[1] = (-30.0, 0.0); // R
100                positions[2] = (0.0, 0.0); // C
101                positions[3] = (0.0, 0.0); // LFE (not directional)
102                positions[4] = (90.0, 0.0); // Ls
103                positions[5] = (-90.0, 0.0); // Rs
104                positions[6] = (150.0, 0.0); // Lrs
105                positions[7] = (-150.0, 0.0); // Rrs
106            }
107            ChannelLayout::Custom(n) => {
108                let angle_step = 360.0 / n as f64;
109                for (i, pos) in positions.iter_mut().enumerate().take(n) {
110                    *pos = (i as f64 * angle_step - 90.0, 0.0);
111                }
112            }
113            _ => {}
114        }
115
116        positions
117    }
118
119    fn compute_sh_coefficients(&self, azimuth_deg: f64, elevation_deg: f64) -> [f64; MAX_BLOCK_INPUTS] {
120        let mut coeffs = [0.0; MAX_BLOCK_INPUTS];
121
122        let az = azimuth_deg.to_radians();
123        let el = elevation_deg.to_radians();
124
125        let cos_el = el.cos();
126        let sin_el = el.sin();
127        let cos_az = az.cos();
128        let sin_az = az.sin();
129
130        // Order 0 (W channel)
131        coeffs[0] = 1.0;
132
133        if self.input_order >= 1 {
134            // Order 1: Y, Z, X (ACN 1, 2, 3)
135            coeffs[1] = cos_el * sin_az; // Y
136            coeffs[2] = sin_el; // Z
137            coeffs[3] = cos_el * cos_az; // X
138        }
139
140        if self.input_order >= 2 {
141            // Order 2: ACN 4-8
142            let cos_2az = (2.0 * az).cos();
143            let sin_2az = (2.0 * az).sin();
144            let sin_2el = (2.0 * el).sin();
145            let cos_el_sq = cos_el * cos_el;
146
147            coeffs[4] = 0.8660254037844386 * cos_el_sq * sin_2az; // V
148            coeffs[5] = 0.8660254037844386 * sin_2el * sin_az; // T
149            coeffs[6] = 0.5 * (3.0 * sin_el * sin_el - 1.0); // R
150            coeffs[7] = 0.8660254037844386 * sin_2el * cos_az; // S
151            coeffs[8] = 0.8660254037844386 * cos_el_sq * cos_2az; // U
152        }
153
154        if self.input_order >= 3 {
155            // Order 3: ACN 9-15
156            let cos_3az = (3.0 * az).cos();
157            let sin_3az = (3.0 * az).sin();
158            let cos_el_sq = cos_el * cos_el;
159            let cos_el_cu = cos_el_sq * cos_el;
160            let sin_el_sq = sin_el * sin_el;
161
162            coeffs[9] = 0.7905694150420949 * cos_el_cu * sin_3az; // Q
163            coeffs[10] = 1.9364916731037085 * cos_el_sq * sin_el * (2.0 * az).sin(); // O
164            coeffs[11] = 0.6123724356957945 * cos_el * (5.0 * sin_el_sq - 1.0) * sin_az; // M
165            coeffs[12] = 0.5 * sin_el * (5.0 * sin_el_sq - 3.0); // K
166            coeffs[13] = 0.6123724356957945 * cos_el * (5.0 * sin_el_sq - 1.0) * cos_az; // L
167            coeffs[14] = 1.9364916731037085 * cos_el_sq * sin_el * (2.0 * az).cos(); // N
168            coeffs[15] = 0.7905694150420949 * cos_el_cu * cos_3az; // P
169        }
170
171        coeffs
172    }
173
174    fn normalize_decoder_matrix(&mut self) {
175        let num_speakers = self.output_layout.channel_count();
176        let num_channels = self.input_channel_count();
177
178        if num_speakers == 0 {
179            return;
180        }
181
182        let energy_scale = 1.0 / (num_speakers as f64).sqrt();
183
184        for spk in 0..num_speakers {
185            for ch in 0..num_channels {
186                self.decoder_matrix[spk][ch] *= energy_scale;
187            }
188        }
189    }
190
191    fn input_channel_count(&self) -> usize {
192        (self.input_order + 1) * (self.input_order + 1)
193    }
194}
195
196impl<S: Sample> Block<S> for AmbisonicDecoderBlock<S> {
197    fn process(&mut self, inputs: &[&[S]], outputs: &mut [&mut [S]], _modulation_values: &[S], _context: &DspContext) {
198        let num_inputs = self.input_channel_count().min(inputs.len());
199        let num_outputs = self.output_layout.channel_count().min(outputs.len());
200
201        if num_inputs == 0 || num_outputs == 0 || inputs[0].is_empty() {
202            return;
203        }
204
205        let num_samples = inputs[0].len().min(outputs[0].len());
206
207        for (out_ch, output) in outputs.iter_mut().enumerate().take(num_outputs) {
208            for i in 0..num_samples {
209                let mut sum = 0.0f64;
210                for (in_ch, input) in inputs.iter().enumerate().take(num_inputs) {
211                    sum += input[i].to_f64() * self.decoder_matrix[out_ch][in_ch];
212                }
213                output[i] = S::from_f64(sum);
214            }
215        }
216    }
217
218    #[inline]
219    fn input_count(&self) -> usize {
220        self.input_channel_count()
221    }
222
223    #[inline]
224    fn output_count(&self) -> usize {
225        self.output_layout.channel_count()
226    }
227
228    #[inline]
229    fn modulation_outputs(&self) -> &[ModulationOutput] {
230        &[]
231    }
232
233    #[inline]
234    fn channel_config(&self) -> ChannelConfig {
235        ChannelConfig::Explicit
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    fn test_context() -> DspContext {
244        DspContext {
245            sample_rate: 44100.0,
246            num_channels: 2,
247            buffer_size: 4,
248            current_sample: 0,
249            channel_layout: ChannelLayout::Stereo,
250        }
251    }
252
253    #[test]
254    fn test_decoder_foa_to_stereo_channel_counts() {
255        let decoder = AmbisonicDecoderBlock::<f32>::new(1, ChannelLayout::Stereo);
256        assert_eq!(decoder.input_count(), 4);
257        assert_eq!(decoder.output_count(), 2);
258        assert_eq!(decoder.channel_config(), ChannelConfig::Explicit);
259    }
260
261    #[test]
262    fn test_decoder_soa_to_51_channel_counts() {
263        let decoder = AmbisonicDecoderBlock::<f32>::new(2, ChannelLayout::Surround51);
264        assert_eq!(decoder.input_count(), 9);
265        assert_eq!(decoder.output_count(), 6);
266    }
267
268    #[test]
269    fn test_decoder_toa_to_71_channel_counts() {
270        let decoder = AmbisonicDecoderBlock::<f32>::new(3, ChannelLayout::Surround71);
271        assert_eq!(decoder.input_count(), 16);
272        assert_eq!(decoder.output_count(), 8);
273    }
274
275    #[test]
276    fn test_decoder_foa_front_signal() {
277        let mut decoder = AmbisonicDecoderBlock::<f32>::new(1, ChannelLayout::Stereo);
278        let context = test_context();
279
280        // Encode a front signal: W=1, Y=0, Z=0, X=1
281        let w = [1.0f32; 4];
282        let y = [0.0f32; 4];
283        let z = [0.0f32; 4];
284        let x = [1.0f32; 4];
285        let mut left_out = [0.0f32; 4];
286        let mut right_out = [0.0f32; 4];
287
288        let inputs: [&[f32]; 4] = [&w, &y, &z, &x];
289        let mut outputs: [&mut [f32]; 2] = [&mut left_out, &mut right_out];
290
291        decoder.process(&inputs, &mut outputs, &[], &context);
292
293        // Front signal should have similar levels in L and R
294        let diff = (left_out[0] - right_out[0]).abs();
295        assert!(diff < 0.1, "Front signal should be balanced, diff={}", diff);
296    }
297
298    #[test]
299    fn test_decoder_foa_left_signal() {
300        let mut decoder = AmbisonicDecoderBlock::<f32>::new(1, ChannelLayout::Stereo);
301        let context = test_context();
302
303        // Encode a left signal: W=1, Y=1, Z=0, X=0
304        let w = [1.0f32; 4];
305        let y = [1.0f32; 4]; // Left
306        let z = [0.0f32; 4];
307        let x = [0.0f32; 4];
308        let mut left_out = [0.0f32; 4];
309        let mut right_out = [0.0f32; 4];
310
311        let inputs: [&[f32]; 4] = [&w, &y, &z, &x];
312        let mut outputs: [&mut [f32]; 2] = [&mut left_out, &mut right_out];
313
314        decoder.process(&inputs, &mut outputs, &[], &context);
315
316        // Left signal should be louder in left channel
317        assert!(
318            left_out[0] > right_out[0],
319            "Left signal should be louder in left channel: L={}, R={}",
320            left_out[0],
321            right_out[0]
322        );
323    }
324
325    #[test]
326    #[should_panic]
327    fn test_decoder_invalid_order_panics() {
328        let _ = AmbisonicDecoderBlock::<f32>::new(0, ChannelLayout::Stereo);
329    }
330
331    #[test]
332    #[should_panic]
333    fn test_decoder_order_too_high_panics() {
334        let _ = AmbisonicDecoderBlock::<f32>::new(4, ChannelLayout::Stereo);
335    }
336}