Skip to main content

st/mem8/
spatial_audio.rs

1//! Spatial Audio Processing using MEM8 Wave Grid
2//!
3//! The 256×256 grid becomes a spatial "room" where:
4//! - Sound sources are placed at (x, y) positions
5//! - Two "ears" sample interference patterns at fixed positions
6//! - Wave propagation creates natural stereo separation
7//!
8//! Grid interpretation:
9//! - X,Y: Spatial position (u8 × u8 = 256×256 room)
10//! - Z (u16): Intensity/amplitude at that position
11//! - Time: Observation rate (sampling frequency)
12
13use super::wave::{MemoryWave, WaveGrid};
14use std::sync::{Arc, RwLock};
15
16/// Speed of sound in grid units per second
17/// (tuned for the 256×256 space - about 34 units = 1 "meter")
18const SPEED_OF_SOUND: f32 = 343.0 / 10.0; // ~34 grid units/sec
19
20/// Default ear separation (~17cm = ~6 grid units at our scale)
21const DEFAULT_EAR_SEPARATION: u8 = 6;
22
23/// Position in the spatial grid
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub struct Position {
26    pub x: u8,
27    pub y: u8,
28}
29
30impl Position {
31    pub fn new(x: u8, y: u8) -> Self {
32        Self { x, y }
33    }
34
35    /// Distance to another position
36    pub fn distance_to(&self, other: &Position) -> f32 {
37        let dx = self.x as f32 - other.x as f32;
38        let dy = self.y as f32 - other.y as f32;
39        (dx * dx + dy * dy).sqrt()
40    }
41
42    /// Angle to another position (radians, 0 = right, PI/2 = up)
43    pub fn angle_to(&self, other: &Position) -> f32 {
44        let dx = other.x as f32 - self.x as f32;
45        let dy = other.y as f32 - self.y as f32;
46        dy.atan2(dx)
47    }
48}
49
50/// A sound source in the spatial field
51#[derive(Debug, Clone)]
52pub struct SoundSource {
53    /// Position in the grid
54    pub position: Position,
55    /// The wave definition (frequency, amplitude, phase)
56    pub wave: MemoryWave,
57    /// Decay factor (how much amplitude decreases with distance)
58    pub decay: f32,
59    /// Whether this source is currently active
60    pub active: bool,
61}
62
63impl SoundSource {
64    pub fn new(x: u8, y: u8, frequency: f32, amplitude: f32) -> Self {
65        Self {
66            position: Position::new(x, y),
67            wave: MemoryWave::new(frequency, amplitude),
68            decay: 1.0, // Linear decay by default
69            active: true,
70        }
71    }
72
73    /// Calculate the wave value at a listener position and time
74    pub fn sample_at(&self, listener: &Position, t: f32) -> f32 {
75        if !self.active {
76            return 0.0;
77        }
78
79        let distance = self.position.distance_to(listener);
80
81        // Time delay based on distance and speed of sound
82        let delay = distance / SPEED_OF_SOUND;
83
84        // Amplitude decay with distance (inverse square law approximation)
85        let amplitude_factor = 1.0 / (1.0 + self.decay * distance * 0.1);
86
87        // Calculate wave value at delayed time
88        self.wave.calculate(t - delay) * amplitude_factor
89    }
90}
91
92/// Stereo sample output
93#[derive(Debug, Clone, Copy, Default)]
94pub struct StereoSample {
95    pub left: f32,
96    pub right: f32,
97}
98
99impl StereoSample {
100    pub fn new(left: f32, right: f32) -> Self {
101        Self { left, right }
102    }
103
104    /// Mix with another sample
105    pub fn mix(&mut self, other: StereoSample) {
106        self.left += other.left;
107        self.right += other.right;
108    }
109
110    /// Apply gain
111    pub fn apply_gain(&mut self, gain: f32) {
112        self.left *= gain;
113        self.right *= gain;
114    }
115
116    /// Clamp to valid range
117    pub fn clamp(&mut self) {
118        self.left = self.left.clamp(-1.0, 1.0);
119        self.right = self.right.clamp(-1.0, 1.0);
120    }
121}
122
123/// Spatial audio processor using the MEM8 wave grid
124pub struct SpatialAudioField {
125    /// The underlying wave grid (for storing/retrieving wave definitions)
126    grid: Arc<RwLock<WaveGrid>>,
127
128    /// Active sound sources
129    sources: Vec<SoundSource>,
130
131    /// Left ear position
132    left_ear: Position,
133
134    /// Right ear position
135    right_ear: Position,
136
137    /// Head center (for calculating angles)
138    head_center: Position,
139
140    /// Current time (advances with each sample)
141    current_time: f32,
142
143    /// Sample rate (samples per second)
144    sample_rate: f32,
145}
146
147impl SpatialAudioField {
148    /// Create a new spatial audio field with default ear positions
149    /// Ears are placed at the center of the grid, separated horizontally
150    pub fn new() -> Self {
151        let center_y = 128u8;
152        let center_x = 128u8;
153        let half_sep = DEFAULT_EAR_SEPARATION / 2;
154
155        Self {
156            grid: Arc::new(RwLock::new(WaveGrid::new())),
157            sources: Vec::new(),
158            left_ear: Position::new(center_x - half_sep, center_y),
159            right_ear: Position::new(center_x + half_sep, center_y),
160            head_center: Position::new(center_x, center_y),
161            current_time: 0.0,
162            sample_rate: 44100.0, // CD quality default
163        }
164    }
165
166    /// Create with custom ear positions
167    pub fn with_ears(left: Position, right: Position) -> Self {
168        let center_x = (left.x as u16 + right.x as u16) / 2;
169        let center_y = (left.y as u16 + right.y as u16) / 2;
170
171        Self {
172            grid: Arc::new(RwLock::new(WaveGrid::new())),
173            sources: Vec::new(),
174            left_ear: left,
175            right_ear: right,
176            head_center: Position::new(center_x as u8, center_y as u8),
177            current_time: 0.0,
178            sample_rate: 44100.0,
179        }
180    }
181
182    /// Set sample rate
183    pub fn set_sample_rate(&mut self, rate: f32) {
184        self.sample_rate = rate;
185    }
186
187    /// Add a sound source to the field
188    pub fn add_source(&mut self, source: SoundSource) -> usize {
189        let idx = self.sources.len();
190
191        // Also store in grid at the source position
192        if let Ok(mut grid) = self.grid.write() {
193            grid.store(
194                source.position.x,
195                source.position.y,
196                (source.wave.amplitude * 65535.0) as u16,
197                source.wave.clone(),
198            );
199        }
200
201        self.sources.push(source);
202        idx
203    }
204
205    /// Add a simple tone at a position
206    pub fn add_tone(&mut self, x: u8, y: u8, frequency: f32, amplitude: f32) -> usize {
207        self.add_source(SoundSource::new(x, y, frequency, amplitude))
208    }
209
210    /// Remove a sound source
211    pub fn remove_source(&mut self, idx: usize) -> Option<SoundSource> {
212        if idx < self.sources.len() {
213            Some(self.sources.remove(idx))
214        } else {
215            None
216        }
217    }
218
219    /// Activate/deactivate a source
220    pub fn set_source_active(&mut self, idx: usize, active: bool) {
221        if let Some(source) = self.sources.get_mut(idx) {
222            source.active = active;
223        }
224    }
225
226    /// Move a source to a new position
227    pub fn move_source(&mut self, idx: usize, new_pos: Position) {
228        if let Some(source) = self.sources.get_mut(idx) {
229            source.position = new_pos;
230        }
231    }
232
233    /// Sample all sources at the current time, returning stereo output
234    pub fn sample(&mut self) -> StereoSample {
235        let mut output = StereoSample::default();
236
237        for source in &self.sources {
238            let left_sample = source.sample_at(&self.left_ear, self.current_time);
239            let right_sample = source.sample_at(&self.right_ear, self.current_time);
240
241            output.left += left_sample;
242            output.right += right_sample;
243        }
244
245        // Advance time
246        self.current_time += 1.0 / self.sample_rate;
247
248        output.clamp();
249        output
250    }
251
252    /// Sample N frames and return as interleaved stereo buffer
253    pub fn sample_frames(&mut self, num_frames: usize) -> Vec<f32> {
254        let mut buffer = Vec::with_capacity(num_frames * 2);
255
256        for _ in 0..num_frames {
257            let sample = self.sample();
258            buffer.push(sample.left);
259            buffer.push(sample.right);
260        }
261
262        buffer
263    }
264
265    /// Calculate the perceived direction of a position from the listener
266    /// Returns angle in degrees (-90 = full left, 0 = center, 90 = full right)
267    pub fn direction_of(&self, pos: &Position) -> f32 {
268        let angle = self.head_center.angle_to(pos);
269        // Convert to degrees and adjust so 0 = forward
270        let degrees = angle.to_degrees();
271        // Assuming "forward" is +Y direction
272        degrees - 90.0
273    }
274
275    /// Get Interaural Time Difference for a position (in seconds)
276    pub fn itd_for(&self, pos: &Position) -> f32 {
277        let dist_left = pos.distance_to(&self.left_ear);
278        let dist_right = pos.distance_to(&self.right_ear);
279        (dist_left - dist_right) / SPEED_OF_SOUND
280    }
281
282    /// Get Interaural Level Difference for a position (as ratio)
283    pub fn ild_for(&self, pos: &Position) -> f32 {
284        let dist_left = pos.distance_to(&self.left_ear);
285        let dist_right = pos.distance_to(&self.right_ear);
286
287        // Ratio of amplitudes (inverse of distance ratio, simplified)
288        if dist_left > 0.1 && dist_right > 0.1 {
289            dist_right / dist_left
290        } else {
291            1.0
292        }
293    }
294
295    /// Current time in seconds
296    pub fn time(&self) -> f32 {
297        self.current_time
298    }
299
300    /// Reset time to zero
301    pub fn reset_time(&mut self) {
302        self.current_time = 0.0;
303    }
304
305    /// Number of active sources
306    pub fn source_count(&self) -> usize {
307        self.sources.iter().filter(|s| s.active).count()
308    }
309}
310
311impl Default for SpatialAudioField {
312    fn default() -> Self {
313        Self::new()
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_position_distance() {
323        let p1 = Position::new(0, 0);
324        let p2 = Position::new(3, 4);
325        assert!((p1.distance_to(&p2) - 5.0).abs() < 0.001);
326    }
327
328    #[test]
329    fn test_stereo_separation() {
330        let mut field = SpatialAudioField::new();
331
332        // Add a source to the left of center
333        field.add_tone(64, 128, 440.0, 0.5); // Left side
334
335        // Sample multiple times to get past the initial zero-crossing
336        // and accumulate total power in each channel
337        let mut left_power = 0.0f32;
338        let mut right_power = 0.0f32;
339
340        for _ in 0..1000 {
341            let sample = field.sample();
342            left_power += sample.left * sample.left;
343            right_power += sample.right * sample.right;
344        }
345
346        // RMS power should be higher in left channel for left-positioned source
347        assert!(left_power > right_power,
348                "Left should be louder for left-positioned source (L:{:.4} R:{:.4})",
349                left_power.sqrt(), right_power.sqrt());
350    }
351
352    #[test]
353    fn test_itd_calculation() {
354        let field = SpatialAudioField::new();
355
356        // Source directly to the left
357        let left_source = Position::new(64, 128);
358        let itd = field.itd_for(&left_source);
359
360        // ITD should be negative (arrives at left ear first)
361        assert!(itd < 0.0, "ITD should be negative for left source");
362    }
363
364    #[test]
365    fn test_center_source_equal() {
366        let mut field = SpatialAudioField::new();
367
368        // Add a source at center (directly in front)
369        field.add_tone(128, 200, 440.0, 0.5); // Centered, in front
370
371        // Sample multiple times and check L/R are similar
372        for _ in 0..100 {
373            let sample = field.sample();
374            let diff = (sample.left - sample.right).abs();
375            assert!(diff < 0.1, "Center source should have similar L/R");
376        }
377    }
378}