ringkernel_wavesim3d/audio/
source.rs

1//! Audio sources for 3D acoustic simulation.
2//!
3//! Supports various audio source types:
4//! - Impulse (single spike)
5//! - Continuous tone (sinusoidal)
6//! - Audio file playback
7//! - Real-time microphone input
8
9use crate::simulation::physics::Position3D;
10use std::sync::Arc;
11
12/// Type of audio source signal.
13#[derive(Debug, Clone)]
14pub enum SourceType {
15    /// Single impulse (delta function)
16    Impulse { amplitude: f32, fired: bool },
17    /// Continuous sinusoidal tone
18    Tone {
19        frequency_hz: f32,
20        amplitude: f32,
21        phase: f32,
22    },
23    /// Noise (white or pink)
24    Noise {
25        amplitude: f32,
26        pink: bool,
27        state: [f32; 7], // Pink noise filter state
28    },
29    /// Audio file playback
30    AudioFile {
31        samples: Arc<Vec<f32>>,
32        sample_rate: u32,
33        position: usize,
34        looping: bool,
35    },
36    /// Real-time input buffer
37    LiveInput {
38        buffer: Arc<std::sync::Mutex<Vec<f32>>>,
39        read_pos: usize,
40    },
41    /// Chirp (frequency sweep)
42    Chirp {
43        start_freq: f32,
44        end_freq: f32,
45        duration_s: f32,
46        amplitude: f32,
47        elapsed: f32,
48    },
49    /// Gaussian pulse (smooth impulse)
50    GaussianPulse {
51        center_time: f32,
52        sigma: f32,
53        amplitude: f32,
54        elapsed: f32,
55    },
56}
57
58impl Default for SourceType {
59    fn default() -> Self {
60        SourceType::Impulse {
61            amplitude: 1.0,
62            fired: false,
63        }
64    }
65}
66
67/// A 3D audio source in the simulation.
68#[derive(Debug, Clone)]
69pub struct AudioSource {
70    /// Unique identifier
71    pub id: u32,
72    /// Position in 3D space (meters)
73    pub position: Position3D,
74    /// Source signal type
75    pub source_type: SourceType,
76    /// Whether the source is active
77    pub active: bool,
78    /// Directivity pattern (1.0 = omnidirectional)
79    pub directivity: f32,
80    /// Source radius for injection (cells)
81    pub injection_radius: f32,
82}
83
84impl AudioSource {
85    /// Create a new impulse source.
86    pub fn impulse(id: u32, position: Position3D, amplitude: f32) -> Self {
87        Self {
88            id,
89            position,
90            source_type: SourceType::Impulse {
91                amplitude,
92                fired: false,
93            },
94            active: true,
95            directivity: 1.0,
96            injection_radius: 1.0,
97        }
98    }
99
100    /// Create a new tone source.
101    pub fn tone(id: u32, position: Position3D, frequency_hz: f32, amplitude: f32) -> Self {
102        Self {
103            id,
104            position,
105            source_type: SourceType::Tone {
106                frequency_hz,
107                amplitude,
108                phase: 0.0,
109            },
110            active: true,
111            directivity: 1.0,
112            injection_radius: 1.0,
113        }
114    }
115
116    /// Create a noise source.
117    pub fn noise(id: u32, position: Position3D, amplitude: f32, pink: bool) -> Self {
118        Self {
119            id,
120            position,
121            source_type: SourceType::Noise {
122                amplitude,
123                pink,
124                state: [0.0; 7],
125            },
126            active: true,
127            directivity: 1.0,
128            injection_radius: 1.0,
129        }
130    }
131
132    /// Create a chirp (frequency sweep) source.
133    pub fn chirp(
134        id: u32,
135        position: Position3D,
136        start_freq: f32,
137        end_freq: f32,
138        duration_s: f32,
139        amplitude: f32,
140    ) -> Self {
141        Self {
142            id,
143            position,
144            source_type: SourceType::Chirp {
145                start_freq,
146                end_freq,
147                duration_s,
148                amplitude,
149                elapsed: 0.0,
150            },
151            active: true,
152            directivity: 1.0,
153            injection_radius: 1.0,
154        }
155    }
156
157    /// Create a Gaussian pulse source.
158    pub fn gaussian_pulse(
159        id: u32,
160        position: Position3D,
161        center_time: f32,
162        sigma: f32,
163        amplitude: f32,
164    ) -> Self {
165        Self {
166            id,
167            position,
168            source_type: SourceType::GaussianPulse {
169                center_time,
170                sigma,
171                amplitude,
172                elapsed: 0.0,
173            },
174            active: true,
175            directivity: 1.0,
176            injection_radius: 1.0,
177        }
178    }
179
180    /// Create an audio file source.
181    pub fn from_samples(
182        id: u32,
183        position: Position3D,
184        samples: Vec<f32>,
185        sample_rate: u32,
186        looping: bool,
187    ) -> Self {
188        Self {
189            id,
190            position,
191            source_type: SourceType::AudioFile {
192                samples: Arc::new(samples),
193                sample_rate,
194                position: 0,
195                looping,
196            },
197            active: true,
198            directivity: 1.0,
199            injection_radius: 1.0,
200        }
201    }
202
203    /// Set the injection radius (in grid cells).
204    pub fn with_radius(mut self, radius: f32) -> Self {
205        self.injection_radius = radius.max(0.5);
206        self
207    }
208
209    /// Set directivity (1.0 = omnidirectional).
210    pub fn with_directivity(mut self, directivity: f32) -> Self {
211        self.directivity = directivity.clamp(0.0, 1.0);
212        self
213    }
214
215    /// Move the source to a new position.
216    pub fn set_position(&mut self, pos: Position3D) {
217        self.position = pos;
218    }
219
220    /// Get the next sample value and advance the source state.
221    ///
222    /// Returns the pressure value to inject at this time step.
223    pub fn next_sample(&mut self, time_step: f32) -> f32 {
224        if !self.active {
225            return 0.0;
226        }
227
228        match &mut self.source_type {
229            SourceType::Impulse { amplitude, fired } => {
230                if !*fired {
231                    *fired = true;
232                    *amplitude
233                } else {
234                    0.0
235                }
236            }
237            SourceType::Tone {
238                frequency_hz,
239                amplitude,
240                phase,
241            } => {
242                let sample = *amplitude * (*phase * 2.0 * std::f32::consts::PI).sin();
243                *phase += *frequency_hz * time_step;
244                if *phase > 1.0 {
245                    *phase -= 1.0;
246                }
247                sample
248            }
249            SourceType::Noise {
250                amplitude,
251                pink,
252                state,
253            } => {
254                // Generate white noise
255                let white = (rand::random::<f32>() * 2.0 - 1.0) * *amplitude;
256
257                if *pink {
258                    // Paul Kellet's pink noise filter
259                    state[0] = 0.99886 * state[0] + white * 0.0555179;
260                    state[1] = 0.99332 * state[1] + white * 0.0750759;
261                    state[2] = 0.96900 * state[2] + white * 0.153_852;
262                    state[3] = 0.86650 * state[3] + white * 0.3104856;
263                    state[4] = 0.55000 * state[4] + white * 0.5329522;
264                    state[5] = -0.7616 * state[5] - white * 0.0168980;
265                    let pink = state[0]
266                        + state[1]
267                        + state[2]
268                        + state[3]
269                        + state[4]
270                        + state[5]
271                        + state[6]
272                        + white * 0.5362;
273                    state[6] = white * 0.115926;
274                    pink * 0.11
275                } else {
276                    white
277                }
278            }
279            SourceType::AudioFile {
280                samples,
281                sample_rate,
282                position,
283                looping,
284            } => {
285                if *position >= samples.len() {
286                    if *looping {
287                        *position = 0;
288                    } else {
289                        return 0.0;
290                    }
291                }
292
293                // Simple nearest-neighbor resampling
294                // TODO: Implement proper resampling
295                let sample = samples[*position];
296                let samples_per_step = (*sample_rate as f32 * time_step) as usize;
297                *position += samples_per_step.max(1);
298
299                sample
300            }
301            SourceType::LiveInput { buffer, read_pos } => {
302                if let Ok(buf) = buffer.lock() {
303                    if *read_pos < buf.len() {
304                        let sample = buf[*read_pos];
305                        *read_pos += 1;
306                        return sample;
307                    }
308                }
309                0.0
310            }
311            SourceType::Chirp {
312                start_freq,
313                end_freq,
314                duration_s,
315                amplitude,
316                elapsed,
317            } => {
318                if *elapsed >= *duration_s {
319                    return 0.0;
320                }
321
322                let t = *elapsed / *duration_s;
323                // Logarithmic frequency sweep
324                let freq = *start_freq * (*end_freq / *start_freq).powf(t);
325                let phase = 2.0 * std::f32::consts::PI * freq * *elapsed;
326                let sample = *amplitude * phase.sin();
327
328                *elapsed += time_step;
329                sample
330            }
331            SourceType::GaussianPulse {
332                center_time,
333                sigma,
334                amplitude,
335                elapsed,
336            } => {
337                let t = *elapsed - *center_time;
338                let gaussian = (-t * t / (2.0 * *sigma * *sigma)).exp();
339                let sample = *amplitude * gaussian;
340
341                *elapsed += time_step;
342                sample
343            }
344        }
345    }
346
347    /// Reset the source to its initial state.
348    pub fn reset(&mut self) {
349        match &mut self.source_type {
350            SourceType::Impulse { fired, .. } => *fired = false,
351            SourceType::Tone { phase, .. } => *phase = 0.0,
352            SourceType::Noise { state, .. } => *state = [0.0; 7],
353            SourceType::AudioFile { position, .. } => *position = 0,
354            SourceType::LiveInput { read_pos, .. } => *read_pos = 0,
355            SourceType::Chirp { elapsed, .. } => *elapsed = 0.0,
356            SourceType::GaussianPulse { elapsed, .. } => *elapsed = 0.0,
357        }
358    }
359
360    /// Check if the source has finished (for one-shot sources).
361    pub fn is_finished(&self) -> bool {
362        match &self.source_type {
363            SourceType::Impulse { fired, .. } => *fired,
364            SourceType::AudioFile {
365                samples,
366                position,
367                looping,
368                ..
369            } => !*looping && *position >= samples.len(),
370            SourceType::Chirp {
371                duration_s,
372                elapsed,
373                ..
374            } => *elapsed >= *duration_s,
375            SourceType::GaussianPulse { elapsed, sigma, .. } => *elapsed > sigma * 6.0,
376            _ => false, // Continuous sources never finish
377        }
378    }
379}
380
381/// Manager for multiple audio sources.
382#[derive(Default)]
383pub struct SourceManager {
384    sources: Vec<AudioSource>,
385    next_id: u32,
386}
387
388impl SourceManager {
389    pub fn new() -> Self {
390        Self::default()
391    }
392
393    /// Add a source and return its ID.
394    pub fn add(&mut self, mut source: AudioSource) -> u32 {
395        let id = self.next_id;
396        source.id = id;
397        self.next_id += 1;
398        self.sources.push(source);
399        id
400    }
401
402    /// Remove a source by ID.
403    pub fn remove(&mut self, id: u32) -> bool {
404        if let Some(pos) = self.sources.iter().position(|s| s.id == id) {
405            self.sources.remove(pos);
406            true
407        } else {
408            false
409        }
410    }
411
412    /// Get a reference to a source by ID.
413    pub fn get(&self, id: u32) -> Option<&AudioSource> {
414        self.sources.iter().find(|s| s.id == id)
415    }
416
417    /// Get a mutable reference to a source by ID.
418    pub fn get_mut(&mut self, id: u32) -> Option<&mut AudioSource> {
419        self.sources.iter_mut().find(|s| s.id == id)
420    }
421
422    /// Iterate over all sources.
423    pub fn iter(&self) -> impl Iterator<Item = &AudioSource> {
424        self.sources.iter()
425    }
426
427    /// Iterate mutably over all sources.
428    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut AudioSource> {
429        self.sources.iter_mut()
430    }
431
432    /// Get the number of sources.
433    pub fn len(&self) -> usize {
434        self.sources.len()
435    }
436
437    /// Check if there are no sources.
438    pub fn is_empty(&self) -> bool {
439        self.sources.is_empty()
440    }
441
442    /// Reset all sources.
443    pub fn reset_all(&mut self) {
444        for source in &mut self.sources {
445            source.reset();
446        }
447    }
448
449    /// Remove finished one-shot sources.
450    pub fn cleanup_finished(&mut self) {
451        self.sources.retain(|s| !s.is_finished() || s.active);
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458
459    #[test]
460    fn test_impulse_source() {
461        let mut source = AudioSource::impulse(0, Position3D::origin(), 1.0);
462        let dt = 0.001;
463
464        // First sample should be the impulse
465        assert_eq!(source.next_sample(dt), 1.0);
466
467        // Subsequent samples should be zero
468        assert_eq!(source.next_sample(dt), 0.0);
469        assert_eq!(source.next_sample(dt), 0.0);
470
471        // After reset, should fire again
472        source.reset();
473        assert_eq!(source.next_sample(dt), 1.0);
474    }
475
476    #[test]
477    fn test_tone_source() {
478        let mut source = AudioSource::tone(0, Position3D::origin(), 440.0, 1.0);
479        let dt = 0.001;
480
481        // Generate some samples
482        let mut samples = Vec::new();
483        for _ in 0..100 {
484            samples.push(source.next_sample(dt));
485        }
486
487        // Should have oscillating values
488        let max = samples.iter().fold(0.0_f32, |a, &b| a.max(b));
489        let min = samples.iter().fold(0.0_f32, |a, &b| a.min(b));
490
491        assert!(max > 0.5, "Max should be positive: {}", max);
492        assert!(min < -0.5, "Min should be negative: {}", min);
493    }
494
495    #[test]
496    fn test_chirp_source() {
497        let mut source = AudioSource::chirp(0, Position3D::origin(), 100.0, 1000.0, 0.01, 1.0);
498        let dt = 0.0001;
499
500        let mut count = 0;
501        while source.next_sample(dt) != 0.0 || count < 10 {
502            count += 1;
503            if count > 1000 {
504                break;
505            }
506        }
507
508        // Should finish after duration
509        assert!(source.is_finished());
510    }
511
512    #[test]
513    fn test_source_manager() {
514        let mut manager = SourceManager::new();
515
516        let id1 = manager.add(AudioSource::impulse(0, Position3D::origin(), 1.0));
517        let id2 = manager.add(AudioSource::tone(
518            0,
519            Position3D::new(1.0, 0.0, 0.0),
520            440.0,
521            0.5,
522        ));
523
524        assert_eq!(manager.len(), 2);
525
526        // IDs should be unique
527        assert_ne!(id1, id2);
528
529        // Should be able to get sources
530        assert!(manager.get(id1).is_some());
531        assert!(manager.get(id2).is_some());
532
533        // Remove one
534        assert!(manager.remove(id1));
535        assert_eq!(manager.len(), 1);
536        assert!(manager.get(id1).is_none());
537    }
538
539    #[test]
540    fn test_gaussian_pulse() {
541        let mut source = AudioSource::gaussian_pulse(0, Position3D::origin(), 0.005, 0.001, 1.0);
542        let dt = 0.0001;
543
544        let mut max_val = 0.0_f32;
545        for _ in 0..200 {
546            let sample = source.next_sample(dt);
547            max_val = max_val.max(sample.abs());
548        }
549
550        // Should have a significant pulse
551        assert!(max_val > 0.5, "Peak should be significant: {}", max_val);
552    }
553}