use crate::graph::MAX_BLOCK_INPUTS;
pub const MAX_HRIR_LENGTH: usize = 512;
pub const MAX_VIRTUAL_SPEAKERS: usize = 8;
#[derive(Debug, Clone)]
pub struct VirtualSpeaker {
pub sh_weights: [f64; MAX_BLOCK_INPUTS],
pub left_hrir: &'static [f32],
pub right_hrir: &'static [f32],
}
impl VirtualSpeaker {
pub fn new(
azimuth_deg: f64,
elevation_deg: f64,
ambisonic_order: usize,
left_hrir: &'static [f32],
right_hrir: &'static [f32],
) -> Self {
let sh_weights = compute_sh_coefficients_max_re(azimuth_deg, elevation_deg, ambisonic_order);
Self {
sh_weights,
left_hrir,
right_hrir,
}
}
}
fn compute_sh_coefficients(azimuth_deg: f64, elevation_deg: f64, order: usize) -> [f64; MAX_BLOCK_INPUTS] {
let mut coeffs = [0.0; MAX_BLOCK_INPUTS];
let az = azimuth_deg.to_radians();
let el = elevation_deg.to_radians();
let cos_el = el.cos();
let sin_el = el.sin();
let cos_az = az.cos();
let sin_az = az.sin();
coeffs[0] = 1.0;
if order >= 1 {
coeffs[1] = cos_el * sin_az; coeffs[2] = sin_el; coeffs[3] = cos_el * cos_az; }
if order >= 2 {
let cos_2az = (2.0 * az).cos();
let sin_2az = (2.0 * az).sin();
let sin_2el = (2.0 * el).sin();
let cos_el_sq = cos_el * cos_el;
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; }
if order >= 3 {
let cos_3az = (3.0 * az).cos();
let sin_3az = (3.0 * az).sin();
let cos_el_sq = cos_el * cos_el;
let cos_el_cu = cos_el_sq * cos_el;
let sin_el_sq = sin_el * sin_el;
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; }
coeffs
}
fn compute_max_re_weights(order: usize) -> [f64; 4] {
const ALPHA: f64 = 2.406184877014388;
let mut weights = [1.0; 4];
let divisor = 2.0 * (order + 1) as f64;
for (l, weight) in weights.iter_mut().enumerate().take(order.min(3) + 1).skip(1) {
*weight = (ALPHA / divisor).cos().powi(l as i32);
}
weights
}
fn compute_sh_coefficients_max_re(azimuth_deg: f64, elevation_deg: f64, order: usize) -> [f64; MAX_BLOCK_INPUTS] {
let mut coeffs = compute_sh_coefficients(azimuth_deg, elevation_deg, order);
let weights = compute_max_re_weights(order);
for (l, &weight) in weights.iter().enumerate().take(order.min(3) + 1) {
let start = l * l;
let end = (l + 1) * (l + 1);
for coeff in coeffs.iter_mut().take(end).skip(start) {
*coeff *= weight;
}
}
coeffs
}
pub mod layouts {
pub const FOA_POSITIONS: [(f64, f64); 8] = [
(0.0, 0.0), (45.0, 0.0), (90.0, 0.0), (135.0, 0.0), (180.0, 0.0), (-135.0, 0.0), (-90.0, 0.0), (-45.0, 0.0), ];
pub const SURROUND_51_POSITIONS: [(f64, f64); 6] = [
(30.0, 0.0), (-30.0, 0.0), (0.0, 0.0), (0.0, 0.0), (110.0, 0.0), (-110.0, 0.0), ];
pub const SURROUND_71_POSITIONS: [(f64, f64); 8] = [
(30.0, 0.0), (-30.0, 0.0), (0.0, 0.0), (0.0, 0.0), (90.0, 0.0), (-90.0, 0.0), (150.0, 0.0), (-150.0, 0.0), ];
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sh_coefficients_front() {
let coeffs = compute_sh_coefficients(0.0, 0.0, 1);
assert!((coeffs[0] - 1.0).abs() < 1e-10, "W should be 1.0");
assert!(coeffs[1].abs() < 1e-10, "Y should be 0 for front");
assert!(coeffs[2].abs() < 1e-10, "Z should be 0 for horizon");
assert!((coeffs[3] - 1.0).abs() < 1e-10, "X should be 1.0 for front");
}
#[test]
fn test_sh_coefficients_left() {
let coeffs = compute_sh_coefficients(90.0, 0.0, 1);
assert!((coeffs[0] - 1.0).abs() < 1e-10, "W should be 1.0");
assert!((coeffs[1] - 1.0).abs() < 1e-10, "Y should be 1.0 for left");
assert!(coeffs[2].abs() < 1e-10, "Z should be 0 for horizon");
assert!(coeffs[3].abs() < 1e-10, "X should be 0 for left");
}
#[test]
fn test_sh_coefficients_above() {
let coeffs = compute_sh_coefficients(0.0, 90.0, 1);
assert!((coeffs[0] - 1.0).abs() < 1e-10, "W should be 1.0");
assert!(coeffs[1].abs() < 1e-10, "Y should be 0 for above");
assert!((coeffs[2] - 1.0).abs() < 1e-10, "Z should be 1.0 for above");
assert!(coeffs[3].abs() < 1e-10, "X should be 0 for above");
}
#[test]
fn test_foa_layout_positions() {
assert_eq!(layouts::FOA_POSITIONS.len(), 8);
assert!((layouts::FOA_POSITIONS[0].0).abs() < 1e-10);
let azimuths: Vec<f64> = layouts::FOA_POSITIONS.iter().map(|(az, _)| *az).collect();
assert!(azimuths.contains(&0.0));
assert!(azimuths.contains(&90.0));
assert!(azimuths.contains(&180.0));
assert!(azimuths.contains(&-90.0));
}
#[test]
fn test_max_re_weights_foa() {
let weights = super::compute_max_re_weights(1);
assert!((weights[0] - 1.0).abs() < 1e-10);
assert!((weights[1] - 0.824).abs() < 0.01);
}
#[test]
fn test_sh_coefficients_max_re_reduces_order1() {
let basic = super::compute_sh_coefficients(45.0, 0.0, 1);
let max_re = super::compute_sh_coefficients_max_re(45.0, 0.0, 1);
assert!((basic[0] - max_re[0]).abs() < 1e-10);
for i in 1..4 {
if basic[i].abs() > 1e-10 {
assert!(max_re[i].abs() < basic[i].abs());
}
}
}
#[test]
fn test_surround_51_layout_positions() {
assert_eq!(layouts::SURROUND_51_POSITIONS.len(), 6);
}
#[test]
fn test_surround_71_layout_positions() {
assert_eq!(layouts::SURROUND_71_POSITIONS.len(), 8);
}
}