use super::wave::{MemoryWave, WaveGrid};
use std::sync::{Arc, RwLock};
const SPEED_OF_SOUND: f32 = 343.0 / 10.0;
const DEFAULT_EAR_SEPARATION: u8 = 6;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Position {
pub x: u8,
pub y: u8,
}
impl Position {
pub fn new(x: u8, y: u8) -> Self {
Self { x, y }
}
pub fn distance_to(&self, other: &Position) -> f32 {
let dx = self.x as f32 - other.x as f32;
let dy = self.y as f32 - other.y as f32;
(dx * dx + dy * dy).sqrt()
}
pub fn angle_to(&self, other: &Position) -> f32 {
let dx = other.x as f32 - self.x as f32;
let dy = other.y as f32 - self.y as f32;
dy.atan2(dx)
}
}
#[derive(Debug, Clone)]
pub struct SoundSource {
pub position: Position,
pub wave: MemoryWave,
pub decay: f32,
pub active: bool,
}
impl SoundSource {
pub fn new(x: u8, y: u8, frequency: f32, amplitude: f32) -> Self {
Self {
position: Position::new(x, y),
wave: MemoryWave::new(frequency, amplitude),
decay: 1.0, active: true,
}
}
pub fn sample_at(&self, listener: &Position, t: f32) -> f32 {
if !self.active {
return 0.0;
}
let distance = self.position.distance_to(listener);
let delay = distance / SPEED_OF_SOUND;
let amplitude_factor = 1.0 / (1.0 + self.decay * distance * 0.1);
self.wave.calculate(t - delay) * amplitude_factor
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct StereoSample {
pub left: f32,
pub right: f32,
}
impl StereoSample {
pub fn new(left: f32, right: f32) -> Self {
Self { left, right }
}
pub fn mix(&mut self, other: StereoSample) {
self.left += other.left;
self.right += other.right;
}
pub fn apply_gain(&mut self, gain: f32) {
self.left *= gain;
self.right *= gain;
}
pub fn clamp(&mut self) {
self.left = self.left.clamp(-1.0, 1.0);
self.right = self.right.clamp(-1.0, 1.0);
}
}
pub struct SpatialAudioField {
grid: Arc<RwLock<WaveGrid>>,
sources: Vec<SoundSource>,
left_ear: Position,
right_ear: Position,
head_center: Position,
current_time: f32,
sample_rate: f32,
}
impl SpatialAudioField {
pub fn new() -> Self {
let center_y = 128u8;
let center_x = 128u8;
let half_sep = DEFAULT_EAR_SEPARATION / 2;
Self {
grid: Arc::new(RwLock::new(WaveGrid::new())),
sources: Vec::new(),
left_ear: Position::new(center_x - half_sep, center_y),
right_ear: Position::new(center_x + half_sep, center_y),
head_center: Position::new(center_x, center_y),
current_time: 0.0,
sample_rate: 44100.0, }
}
pub fn with_ears(left: Position, right: Position) -> Self {
let center_x = (left.x as u16 + right.x as u16) / 2;
let center_y = (left.y as u16 + right.y as u16) / 2;
Self {
grid: Arc::new(RwLock::new(WaveGrid::new())),
sources: Vec::new(),
left_ear: left,
right_ear: right,
head_center: Position::new(center_x as u8, center_y as u8),
current_time: 0.0,
sample_rate: 44100.0,
}
}
pub fn set_sample_rate(&mut self, rate: f32) {
self.sample_rate = rate;
}
pub fn add_source(&mut self, source: SoundSource) -> usize {
let idx = self.sources.len();
if let Ok(mut grid) = self.grid.write() {
grid.store(
source.position.x,
source.position.y,
(source.wave.amplitude * 65535.0) as u16,
source.wave.clone(),
);
}
self.sources.push(source);
idx
}
pub fn add_tone(&mut self, x: u8, y: u8, frequency: f32, amplitude: f32) -> usize {
self.add_source(SoundSource::new(x, y, frequency, amplitude))
}
pub fn remove_source(&mut self, idx: usize) -> Option<SoundSource> {
if idx < self.sources.len() {
Some(self.sources.remove(idx))
} else {
None
}
}
pub fn set_source_active(&mut self, idx: usize, active: bool) {
if let Some(source) = self.sources.get_mut(idx) {
source.active = active;
}
}
pub fn move_source(&mut self, idx: usize, new_pos: Position) {
if let Some(source) = self.sources.get_mut(idx) {
source.position = new_pos;
}
}
pub fn sample(&mut self) -> StereoSample {
let mut output = StereoSample::default();
for source in &self.sources {
let left_sample = source.sample_at(&self.left_ear, self.current_time);
let right_sample = source.sample_at(&self.right_ear, self.current_time);
output.left += left_sample;
output.right += right_sample;
}
self.current_time += 1.0 / self.sample_rate;
output.clamp();
output
}
pub fn sample_frames(&mut self, num_frames: usize) -> Vec<f32> {
let mut buffer = Vec::with_capacity(num_frames * 2);
for _ in 0..num_frames {
let sample = self.sample();
buffer.push(sample.left);
buffer.push(sample.right);
}
buffer
}
pub fn direction_of(&self, pos: &Position) -> f32 {
let angle = self.head_center.angle_to(pos);
let degrees = angle.to_degrees();
degrees - 90.0
}
pub fn itd_for(&self, pos: &Position) -> f32 {
let dist_left = pos.distance_to(&self.left_ear);
let dist_right = pos.distance_to(&self.right_ear);
(dist_left - dist_right) / SPEED_OF_SOUND
}
pub fn ild_for(&self, pos: &Position) -> f32 {
let dist_left = pos.distance_to(&self.left_ear);
let dist_right = pos.distance_to(&self.right_ear);
if dist_left > 0.1 && dist_right > 0.1 {
dist_right / dist_left
} else {
1.0
}
}
pub fn time(&self) -> f32 {
self.current_time
}
pub fn reset_time(&mut self) {
self.current_time = 0.0;
}
pub fn source_count(&self) -> usize {
self.sources.iter().filter(|s| s.active).count()
}
}
impl Default for SpatialAudioField {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_position_distance() {
let p1 = Position::new(0, 0);
let p2 = Position::new(3, 4);
assert!((p1.distance_to(&p2) - 5.0).abs() < 0.001);
}
#[test]
fn test_stereo_separation() {
let mut field = SpatialAudioField::new();
field.add_tone(64, 128, 440.0, 0.5);
let mut left_power = 0.0f32;
let mut right_power = 0.0f32;
for _ in 0..1000 {
let sample = field.sample();
left_power += sample.left * sample.left;
right_power += sample.right * sample.right;
}
assert!(left_power > right_power,
"Left should be louder for left-positioned source (L:{:.4} R:{:.4})",
left_power.sqrt(), right_power.sqrt());
}
#[test]
fn test_itd_calculation() {
let field = SpatialAudioField::new();
let left_source = Position::new(64, 128);
let itd = field.itd_for(&left_source);
assert!(itd < 0.0, "ITD should be negative for left source");
}
#[test]
fn test_center_source_equal() {
let mut field = SpatialAudioField::new();
field.add_tone(128, 200, 440.0, 0.5);
for _ in 0..100 {
let sample = field.sample();
let diff = (sample.left - sample.right).abs();
assert!(diff < 0.1, "Center source should have similar L/R");
}
}
}