use super::{
hrir_data::{HRIR_LENGTH, get_hrir_for_azimuth},
virtual_speaker::{MAX_HRIR_LENGTH, MAX_VIRTUAL_SPEAKERS, VirtualSpeaker, layouts},
};
use crate::{graph::MAX_BLOCK_INPUTS, sample::Sample};
pub struct HrtfConvolver {
signal_buffers: [[f32; MAX_HRIR_LENGTH]; MAX_VIRTUAL_SPEAKERS],
buffer_pos: usize,
speakers: [Option<VirtualSpeakerConfig>; MAX_VIRTUAL_SPEAKERS],
num_speakers: usize,
hrir_length: usize,
}
#[derive(Clone)]
struct VirtualSpeakerConfig {
sh_weights: [f64; MAX_BLOCK_INPUTS],
left_hrir: &'static [f32],
right_hrir: &'static [f32],
}
impl HrtfConvolver {
pub fn new_ambisonic(ambisonic_order: usize) -> Self {
let positions = layouts::FOA_POSITIONS;
let num_speakers = positions.len();
let mut speakers: [Option<VirtualSpeakerConfig>; MAX_VIRTUAL_SPEAKERS] = [const { None }; MAX_VIRTUAL_SPEAKERS];
for (i, &(azimuth, elevation)) in positions.iter().enumerate() {
let hrir = get_hrir_for_azimuth(azimuth);
let speaker = VirtualSpeaker::new(azimuth, elevation, ambisonic_order, hrir.left, hrir.right);
speakers[i] = Some(VirtualSpeakerConfig {
sh_weights: speaker.sh_weights,
left_hrir: speaker.left_hrir,
right_hrir: speaker.right_hrir,
});
}
Self {
signal_buffers: [[0.0; MAX_HRIR_LENGTH]; MAX_VIRTUAL_SPEAKERS],
buffer_pos: 0,
speakers,
num_speakers,
hrir_length: HRIR_LENGTH,
}
}
pub fn new_surround(channel_count: usize) -> Self {
let positions = match channel_count {
6 => &layouts::SURROUND_51_POSITIONS[..],
8 => &layouts::SURROUND_71_POSITIONS[..],
_ => panic!("Unsupported surround channel count: {channel_count}"),
};
let mut speakers: [Option<VirtualSpeakerConfig>; MAX_VIRTUAL_SPEAKERS] = [const { None }; MAX_VIRTUAL_SPEAKERS];
for (i, &(azimuth, _elevation)) in positions.iter().enumerate() {
let hrir = get_hrir_for_azimuth(azimuth);
let mut sh_weights = [0.0; MAX_BLOCK_INPUTS];
sh_weights[i] = 1.0;
speakers[i] = Some(VirtualSpeakerConfig {
sh_weights,
left_hrir: hrir.left,
right_hrir: hrir.right,
});
}
Self {
signal_buffers: [[0.0; MAX_HRIR_LENGTH]; MAX_VIRTUAL_SPEAKERS],
buffer_pos: 0,
speakers,
num_speakers: channel_count,
hrir_length: HRIR_LENGTH,
}
}
pub fn reset(&mut self) {
for buffer in &mut self.signal_buffers {
buffer.fill(0.0);
}
self.buffer_pos = 0;
}
pub fn process<S: Sample>(
&mut self,
inputs: &[&[S]],
left_output: &mut [S],
right_output: &mut [S],
num_input_channels: usize,
) {
if inputs.is_empty() || left_output.is_empty() || right_output.is_empty() {
return;
}
let num_samples = inputs[0].len().min(left_output.len()).min(right_output.len());
for sample_idx in 0..num_samples {
let mut input_samples = [0.0f64; MAX_BLOCK_INPUTS];
for (ch, input) in inputs.iter().enumerate().take(num_input_channels) {
input_samples[ch] = input[sample_idx].to_f64();
}
let (left, right) = self.process_sample(&input_samples, num_input_channels);
left_output[sample_idx] = S::from_f64(left);
right_output[sample_idx] = S::from_f64(right);
}
}
#[inline]
fn process_sample(&mut self, inputs: &[f64; MAX_BLOCK_INPUTS], num_channels: usize) -> (f64, f64) {
let mut left_sum = 0.0f64;
let mut right_sum = 0.0f64;
for speaker_idx in 0..self.num_speakers {
if let Some(ref speaker) = self.speakers[speaker_idx] {
let signal = self.decode_to_speaker(inputs, num_channels, &speaker.sh_weights);
self.signal_buffers[speaker_idx][self.buffer_pos] = signal as f32;
left_sum += self.convolve(speaker_idx, speaker.left_hrir);
right_sum += self.convolve(speaker_idx, speaker.right_hrir);
}
}
self.buffer_pos = (self.buffer_pos + 1) % self.hrir_length;
let normalization = 1.0 / (self.num_speakers as f64).sqrt();
(left_sum * normalization, right_sum * normalization)
}
#[inline]
fn decode_to_speaker(
&self,
inputs: &[f64; MAX_BLOCK_INPUTS],
num_channels: usize,
sh_weights: &[f64; MAX_BLOCK_INPUTS],
) -> f64 {
let mut sum = 0.0;
for ch in 0..num_channels {
sum += inputs[ch] * sh_weights[ch];
}
sum
}
#[inline]
fn convolve(&self, speaker_idx: usize, hrir: &[f32]) -> f64 {
let buffer = &self.signal_buffers[speaker_idx];
let len = self.hrir_length.min(hrir.len());
let mut sum = 0.0f64;
for (k, &h) in hrir.iter().enumerate().take(len) {
let buf_idx = (self.buffer_pos + self.hrir_length - k) % self.hrir_length;
sum += buffer[buf_idx] as f64 * h as f64;
}
sum
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_ambisonic_foa() {
let convolver = HrtfConvolver::new_ambisonic(1);
assert_eq!(convolver.num_speakers, 8);
assert_eq!(convolver.hrir_length, HRIR_LENGTH);
}
#[test]
fn test_new_surround_51() {
let convolver = HrtfConvolver::new_surround(6);
assert_eq!(convolver.num_speakers, 6);
}
#[test]
fn test_new_surround_71() {
let convolver = HrtfConvolver::new_surround(8);
assert_eq!(convolver.num_speakers, 8);
}
#[test]
fn test_reset() {
let mut convolver = HrtfConvolver::new_ambisonic(1);
let input = [1.0f32; 16];
let inputs: [&[f32]; 4] = [&input, &input, &input, &input];
let mut left = [0.0f32; 16];
let mut right = [0.0f32; 16];
convolver.process(&inputs, &mut left, &mut right, 4);
convolver.reset();
for buffer in &convolver.signal_buffers {
for &sample in buffer.iter() {
assert_eq!(sample, 0.0);
}
}
assert_eq!(convolver.buffer_pos, 0);
}
#[test]
fn test_process_silence() {
let mut convolver = HrtfConvolver::new_ambisonic(1);
let input = [0.0f32; 16];
let inputs: [&[f32]; 4] = [&input, &input, &input, &input];
let mut left = [1.0f32; 16];
let mut right = [1.0f32; 16];
convolver.process(&inputs, &mut left, &mut right, 4);
for &sample in &left {
assert!(sample.abs() < 1e-6, "Left should be silence");
}
for &sample in &right {
assert!(sample.abs() < 1e-6, "Right should be silence");
}
}
#[test]
fn test_process_produces_output() {
let mut convolver = HrtfConvolver::new_ambisonic(1);
let w_input = [1.0f32; 16];
let zero_input = [0.0f32; 16];
let inputs: [&[f32]; 4] = [&w_input, &zero_input, &zero_input, &zero_input];
let mut left = [0.0f32; 16];
let mut right = [0.0f32; 16];
convolver.process(&inputs, &mut left, &mut right, 4);
let left_energy: f32 = left.iter().map(|x| x * x).sum();
let right_energy: f32 = right.iter().map(|x| x * x).sum();
assert!(left_energy > 0.0, "Left should have output");
assert!(right_energy > 0.0, "Right should have output");
}
#[test]
fn test_left_signal_louder_in_left() {
let mut convolver = HrtfConvolver::new_ambisonic(1);
convolver.reset();
const LEN: usize = 512;
let w_input = [0.5f32; LEN];
let y_input = [1.0f32; LEN]; let zero_input = [0.0f32; LEN];
let inputs: [&[f32]; 4] = [&w_input, &y_input, &zero_input, &zero_input];
let mut left = [0.0f32; LEN];
let mut right = [0.0f32; LEN];
convolver.process(&inputs, &mut left, &mut right, 4);
let left_energy: f32 = left[256..].iter().map(|x| x * x).sum();
let right_energy: f32 = right[256..].iter().map(|x| x * x).sum();
assert!(
left_energy > right_energy,
"Left signal should be louder in left ear: L={}, R={}",
left_energy,
right_energy
);
}
}