use serde::{Deserialize, Serialize};
use tracing::debug;
use goonj::binaural::{HrtfDataset, HrtfPair, generate_binaural_ir};
use goonj::impulse::IrConfig;
use goonj::material::AcousticMaterial;
use goonj::room::AcousticRoom;
use hisab::Vec3;
use crate::error::{NaadError, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BinauralProcessor {
#[serde(skip)]
left_ir: Vec<f32>,
#[serde(skip)]
right_ir: Vec<f32>,
#[serde(skip)]
left_buffer: Vec<f32>,
#[serde(skip)]
right_buffer: Vec<f32>,
#[serde(skip)]
position: usize,
pub sample_rate: u32,
pub azimuth: f32,
pub elevation: f32,
}
impl BinauralProcessor {
pub fn new(azimuth: f32, elevation: f32, sample_rate: u32) -> Result<Self> {
if sample_rate == 0 {
return Err(NaadError::ComputationError {
message: "sample rate must be > 0".into(),
});
}
let room = AcousticRoom::shoebox(6.0, 4.0, 3.0, AcousticMaterial::concrete());
let source = Vec3::new(2.0, 1.5, 2.0);
let listener = Vec3::new(4.0, 1.5, 2.0);
let hrtf = build_minimal_hrtf(azimuth, elevation, sample_rate);
let ir_config = IrConfig {
sample_rate,
max_order: 2,
num_diffuse_rays: 500,
max_bounces: 20,
max_time_seconds: 0.3,
seed: 42,
};
let binaural = generate_binaural_ir(source, listener, &room, &hrtf, &ir_config);
let left_ir = binaural.left;
let right_ir = binaural.right;
let ir_len = left_ir.len().max(right_ir.len()).max(1);
debug!(
azimuth,
elevation, ir_len, sample_rate, "binaural processor created"
);
Ok(Self {
left_ir,
right_ir,
left_buffer: vec![0.0; ir_len],
right_buffer: vec![0.0; ir_len],
position: 0,
sample_rate,
azimuth,
elevation,
})
}
#[inline]
#[must_use]
pub fn process_sample(&mut self, input: f32) -> (f32, f32) {
let left_len = self.left_ir.len();
let right_len = self.right_ir.len();
if left_len == 0 && right_len == 0 {
return (input, input);
}
let buf_len = self.left_buffer.len();
if buf_len == 0 {
return (input, input);
}
self.left_buffer[self.position] = input;
self.right_buffer[self.position] = input;
let mut left_out = 0.0_f32;
for (k, &h) in self.left_ir.iter().enumerate() {
let idx = (self.position + buf_len - k) % buf_len;
left_out += self.left_buffer[idx] * h;
}
let mut right_out = 0.0_f32;
for (k, &h) in self.right_ir.iter().enumerate() {
let idx = (self.position + buf_len - k) % buf_len;
right_out += self.right_buffer[idx] * h;
}
self.position = (self.position + 1) % buf_len;
(left_out, right_out)
}
pub fn rebuild(&mut self) -> Result<()> {
let new = Self::new(self.azimuth, self.elevation, self.sample_rate)?;
self.left_ir = new.left_ir;
self.right_ir = new.right_ir;
self.left_buffer = new.left_buffer;
self.right_buffer = new.right_buffer;
self.position = 0;
Ok(())
}
#[must_use]
pub fn is_loaded(&self) -> bool {
!self.left_ir.is_empty()
}
}
fn build_minimal_hrtf(azimuth: f32, elevation: f32, sample_rate: u32) -> HrtfDataset {
let hrir_len = 32;
let make_pair = |az: f32, el: f32| -> HrtfPair {
let mut left = vec![0.0_f32; hrir_len];
let mut right = vec![0.0_f32; hrir_len];
let left_gain = (1.0 - az.sin() * 0.5).max(0.1);
let right_gain = (1.0 + az.sin() * 0.5).max(0.1);
for i in 0..hrir_len {
let decay = (-3.0 * i as f32 / hrir_len as f32).exp();
left[i] = left_gain * decay;
right[i] = right_gain * decay;
}
HrtfPair {
azimuth: az,
elevation: el,
left,
right,
}
};
let pairs = vec![
make_pair(0.0, 0.0), make_pair(std::f32::consts::FRAC_PI_2, 0.0), make_pair(-std::f32::consts::FRAC_PI_2, 0.0), make_pair(std::f32::consts::PI, 0.0), make_pair(azimuth, elevation), ];
HrtfDataset::from_pairs(pairs, sample_rate)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_binaural_creates() {
let proc = BinauralProcessor::new(0.5, 0.0, 48000);
assert!(proc.is_ok(), "should create binaural processor");
}
#[test]
fn test_binaural_produces_stereo() {
let mut proc = BinauralProcessor::new(0.5, 0.0, 48000).unwrap();
let (l, r) = proc.process_sample(1.0);
assert!(l.is_finite());
assert!(r.is_finite());
}
#[test]
fn test_binaural_stereo_differs_at_side() {
let mut proc = BinauralProcessor::new(std::f32::consts::FRAC_PI_2, 0.0, 48000).unwrap();
let mut diff_found = false;
for i in 0..1000 {
let input = if i == 0 { 1.0 } else { 0.0 };
let (l, r) = proc.process_sample(input);
if (l - r).abs() > 1e-6 {
diff_found = true;
break;
}
}
assert!(diff_found, "stereo output should differ for side source");
}
#[test]
fn test_zero_sample_rate_errors() {
let result = BinauralProcessor::new(0.0, 0.0, 0);
assert!(result.is_err());
}
#[test]
fn test_serde_roundtrip() {
let proc = BinauralProcessor::new(0.3, 0.1, 48000).unwrap();
let json = serde_json::to_string(&proc).unwrap();
let back: BinauralProcessor = serde_json::from_str(&json).unwrap();
assert!((proc.azimuth - back.azimuth).abs() < f32::EPSILON);
assert_eq!(proc.sample_rate, back.sample_rate);
assert!(back.left_ir.is_empty());
}
}