bbx_dsp/blocks/effectors/
ambisonic_decoder.rs1use 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
14pub 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 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 pub fn order(&self) -> usize {
57 self.input_order
58 }
59
60 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); positions[1] = (-30.0, 0.0); }
89 ChannelLayout::Surround51 => {
90 positions[0] = (30.0, 0.0); positions[1] = (-30.0, 0.0); positions[2] = (0.0, 0.0); positions[3] = (0.0, 0.0); positions[4] = (110.0, 0.0); positions[5] = (-110.0, 0.0); }
97 ChannelLayout::Surround71 => {
98 positions[0] = (30.0, 0.0); positions[1] = (-30.0, 0.0); positions[2] = (0.0, 0.0); positions[3] = (0.0, 0.0); positions[4] = (90.0, 0.0); positions[5] = (-90.0, 0.0); positions[6] = (150.0, 0.0); positions[7] = (-150.0, 0.0); }
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 coeffs[0] = 1.0;
132
133 if self.input_order >= 1 {
134 coeffs[1] = cos_el * sin_az; coeffs[2] = sin_el; coeffs[3] = cos_el * cos_az; }
139
140 if self.input_order >= 2 {
141 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; coeffs[5] = 0.8660254037844386 * sin_2el * sin_az; coeffs[6] = 0.5 * (3.0 * sin_el * sin_el - 1.0); coeffs[7] = 0.8660254037844386 * sin_2el * cos_az; coeffs[8] = 0.8660254037844386 * cos_el_sq * cos_2az; }
153
154 if self.input_order >= 3 {
155 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; coeffs[10] = 1.9364916731037085 * cos_el_sq * sin_el * (2.0 * az).sin(); coeffs[11] = 0.6123724356957945 * cos_el * (5.0 * sin_el_sq - 1.0) * sin_az; coeffs[12] = 0.5 * sin_el * (5.0 * sin_el_sq - 3.0); coeffs[13] = 0.6123724356957945 * cos_el * (5.0 * sin_el_sq - 1.0) * cos_az; coeffs[14] = 1.9364916731037085 * cos_el_sq * sin_el * (2.0 * az).cos(); coeffs[15] = 0.7905694150420949 * cos_el_cu * cos_3az; }
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 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 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 let w = [1.0f32; 4];
305 let y = [1.0f32; 4]; 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 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}