use hisab::Vec3;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BFormatIr {
pub w: Vec<f32>,
pub x: Vec<f32>,
pub y: Vec<f32>,
pub z: Vec<f32>,
pub sample_rate: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HoaIr {
pub channels: Vec<Vec<f32>>,
pub order: u32,
pub sample_rate: u32,
}
#[inline]
pub fn encode_bformat(amplitude: f32, direction: Vec3, delay_samples: usize, ir: &mut BFormatIr) {
if delay_samples >= ir.w.len() || amplitude.abs() < f32::EPSILON {
return;
}
ir.w[delay_samples] += amplitude;
ir.x[delay_samples] += amplitude * direction.x;
ir.y[delay_samples] += amplitude * direction.y;
ir.z[delay_samples] += amplitude * direction.z;
}
#[inline]
pub fn encode_hoa(amplitude: f32, direction: Vec3, delay_samples: usize, ir: &mut HoaIr) {
let num_channels = ((ir.order + 1) * (ir.order + 1)) as usize;
if delay_samples >= ir.channels.first().map_or(0, |c| c.len())
|| amplitude.abs() < f32::EPSILON
|| ir.channels.len() < num_channels
{
return;
}
let r = direction.length();
if r < f32::EPSILON {
return;
}
let theta = (direction.z / r).acos(); let phi = direction.y.atan2(direction.x);
let sh = spherical_harmonics(ir.order, theta, phi);
for (ch, &coeff) in sh.iter().enumerate().take(num_channels) {
if ch < ir.channels.len() {
ir.channels[ch][delay_samples] += amplitude * coeff;
}
}
}
const MAX_HOA_ORDER: u32 = 3;
const MAX_HOA_CHANNELS: usize = 16;
#[must_use]
fn spherical_harmonics(order: u32, theta: f32, phi: f32) -> [f32; MAX_HOA_CHANNELS] {
let mut sh = [0.0_f32; MAX_HOA_CHANNELS];
let order = order.min(MAX_HOA_ORDER);
let cos_t = theta.cos();
let sin_t = theta.sin();
sh[0] = 1.0;
if order >= 1 {
sh[1] = sin_t * phi.sin(); sh[2] = cos_t; sh[3] = sin_t * phi.cos(); }
if order >= 2 {
let sin2 = sin_t * sin_t;
let cos2 = cos_t * cos_t;
let n2_2 = 3.0_f32.sqrt() / 2.0; let n2_1 = 3.0_f32.sqrt(); sh[4] = n2_2 * sin2 * (2.0 * phi).sin(); sh[5] = n2_1 * sin_t * cos_t * phi.sin(); sh[6] = 1.5 * cos2 - 0.5; sh[7] = n2_1 * sin_t * cos_t * phi.cos(); sh[8] = n2_2 * sin2 * (2.0 * phi).cos(); }
if order >= 3 {
let sin2 = sin_t * sin_t;
let sin3 = sin2 * sin_t;
let cos2 = cos_t * cos_t;
let n3_3 = (5.0 / 8.0_f32).sqrt(); let n3_2 = 15.0_f32.sqrt() / 2.0; let n3_1 = (3.0 / 8.0_f32).sqrt(); sh[9] = n3_3 * sin3 * (3.0 * phi).sin(); sh[10] = n3_2 * sin2 * cos_t * (2.0 * phi).sin(); sh[11] = n3_1 * sin_t * (5.0 * cos2 - 1.0) * phi.sin(); sh[12] = 2.5 * cos2 * cos_t - 1.5 * cos_t; sh[13] = n3_1 * sin_t * (5.0 * cos2 - 1.0) * phi.cos(); sh[14] = n3_2 * sin2 * cos_t * (2.0 * phi).cos(); sh[15] = n3_3 * sin3 * (3.0 * phi).cos(); }
sh
}
#[must_use]
pub fn new_bformat_ir(num_samples: usize, sample_rate: u32) -> BFormatIr {
BFormatIr {
w: vec![0.0; num_samples],
x: vec![0.0; num_samples],
y: vec![0.0; num_samples],
z: vec![0.0; num_samples],
sample_rate,
}
}
#[must_use]
pub fn new_hoa_ir(order: u32, num_samples: usize, sample_rate: u32) -> HoaIr {
let num_channels = ((order + 1) * (order + 1)) as usize;
HoaIr {
channels: (0..num_channels).map(|_| vec![0.0; num_samples]).collect(),
order,
sample_rate,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bformat_front_direction() {
let mut ir = new_bformat_ir(100, 48000);
encode_bformat(1.0, Vec3::X, 0, &mut ir);
assert!(
(ir.w[0] - 1.0).abs() < 0.01,
"W should be 1.0, got {}",
ir.w[0]
);
assert!((ir.x[0] - 1.0).abs() < 0.01);
assert!(ir.y[0].abs() < 0.01);
assert!(ir.z[0].abs() < 0.01);
}
#[test]
fn bformat_up_direction() {
let mut ir = new_bformat_ir(100, 48000);
encode_bformat(1.0, Vec3::Y, 0, &mut ir);
assert!((ir.w[0] - 1.0).abs() < 0.01);
assert!(ir.x[0].abs() < 0.01);
assert!((ir.y[0] - 1.0).abs() < 0.01);
assert!(ir.z[0].abs() < 0.01);
}
#[test]
fn bformat_out_of_bounds_safe() {
let mut ir = new_bformat_ir(10, 48000);
encode_bformat(1.0, Vec3::X, 100, &mut ir); assert!(ir.w.iter().all(|&v| v == 0.0));
}
#[test]
fn hoa_order_1_has_4_channels() {
let ir = new_hoa_ir(1, 100, 48000);
assert_eq!(ir.channels.len(), 4);
}
#[test]
fn hoa_order_3_has_16_channels() {
let ir = new_hoa_ir(3, 100, 48000);
assert_eq!(ir.channels.len(), 16);
}
#[test]
fn hoa_encode_front() {
let mut ir = new_hoa_ir(1, 100, 48000);
encode_hoa(1.0, Vec3::Z, 0, &mut ir);
assert!(ir.channels[0][0].abs() > 0.0, "W should have content");
}
#[test]
fn spherical_harmonics_order_0() {
let sh = spherical_harmonics(0, 0.0, 0.0);
assert_eq!(sh.len(), MAX_HOA_CHANNELS);
assert!((sh[0] - 1.0).abs() < 0.01);
}
#[test]
fn spherical_harmonics_order_1_has_values() {
let sh = spherical_harmonics(1, 0.5, 0.3);
assert_eq!(sh.len(), MAX_HOA_CHANNELS);
assert!(sh[0].abs() > 0.0, "W should be nonzero");
}
#[test]
fn spherical_harmonics_order_3_all_populated() {
let sh = spherical_harmonics(3, 0.5, 0.3);
assert_eq!(sh.len(), 16);
}
#[test]
fn bformat_ir_serializes() {
let ir = new_bformat_ir(10, 48000);
let json = serde_json::to_string(&ir).unwrap();
let back: BFormatIr = serde_json::from_str(&json).unwrap();
assert_eq!(ir, back);
}
}