Skip to main content

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