Skip to main content

ringkernel_wavesim3d/audio/
mod.rs

1//! Audio system for 3D acoustic simulation.
2//!
3//! Provides:
4//! - Audio sources with 3D positioning
5//! - Binaural microphone capture
6//! - Audio file I/O
7//! - Real-time audio streaming
8
9pub mod binaural;
10pub mod source;
11
12pub use binaural::{BinauralMicrophone, BinauralProcessor, DelayLine, VirtualHead};
13pub use source::{AudioSource, SourceManager, SourceType};
14
15use crate::simulation::physics::Position3D;
16use std::path::Path;
17
18/// Load audio samples from a WAV file.
19pub fn load_wav<P: AsRef<Path>>(path: P) -> Result<(Vec<f32>, u32), WavError> {
20    let reader = hound::WavReader::open(path).map_err(|e| WavError::ReadError(e.to_string()))?;
21
22    let spec = reader.spec();
23    let sample_rate = spec.sample_rate;
24
25    // Convert samples to f32 normalized to -1.0 to 1.0
26    let samples: Vec<f32> = match spec.sample_format {
27        hound::SampleFormat::Float => reader
28            .into_samples::<f32>()
29            .filter_map(Result::ok)
30            .collect(),
31        hound::SampleFormat::Int => {
32            let max_val = (1i32 << (spec.bits_per_sample - 1)) as f32;
33            reader
34                .into_samples::<i32>()
35                .filter_map(Result::ok)
36                .map(|s| s as f32 / max_val)
37                .collect()
38        }
39    };
40
41    // Convert stereo to mono if needed
42    let mono_samples = if spec.channels == 2 {
43        samples
44            .chunks(2)
45            .map(|chunk| (chunk[0] + chunk.get(1).copied().unwrap_or(0.0)) / 2.0)
46            .collect()
47    } else {
48        samples
49    };
50
51    Ok((mono_samples, sample_rate))
52}
53
54/// Save audio samples to a WAV file.
55pub fn save_wav<P: AsRef<Path>>(
56    path: P,
57    samples: &[f32],
58    sample_rate: u32,
59    channels: u16,
60) -> Result<(), WavError> {
61    let spec = hound::WavSpec {
62        channels,
63        sample_rate,
64        bits_per_sample: 32,
65        sample_format: hound::SampleFormat::Float,
66    };
67
68    let mut writer =
69        hound::WavWriter::create(path, spec).map_err(|e| WavError::WriteError(e.to_string()))?;
70
71    for &sample in samples {
72        writer
73            .write_sample(sample)
74            .map_err(|e| WavError::WriteError(e.to_string()))?;
75    }
76
77    writer
78        .finalize()
79        .map_err(|e| WavError::WriteError(e.to_string()))?;
80
81    Ok(())
82}
83
84/// Save stereo audio to a WAV file.
85pub fn save_stereo_wav<P: AsRef<Path>>(
86    path: P,
87    left: &[f32],
88    right: &[f32],
89    sample_rate: u32,
90) -> Result<(), WavError> {
91    let mut interleaved = Vec::with_capacity(left.len() * 2);
92    for (l, r) in left.iter().zip(right.iter()) {
93        interleaved.push(*l);
94        interleaved.push(*r);
95    }
96    save_wav(path, &interleaved, sample_rate, 2)
97}
98
99/// Error type for WAV operations.
100#[derive(Debug, Clone)]
101pub enum WavError {
102    ReadError(String),
103    WriteError(String),
104}
105
106impl std::fmt::Display for WavError {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        match self {
109            WavError::ReadError(msg) => write!(f, "WAV read error: {}", msg),
110            WavError::WriteError(msg) => write!(f, "WAV write error: {}", msg),
111        }
112    }
113}
114
115impl std::error::Error for WavError {}
116
117/// Audio configuration for the simulation.
118#[derive(Debug, Clone)]
119pub struct AudioConfig {
120    /// Output sample rate
121    pub sample_rate: u32,
122    /// Buffer size in samples
123    pub buffer_size: usize,
124    /// Enable real-time audio output
125    pub enable_output: bool,
126    /// Enable recording
127    pub enable_recording: bool,
128}
129
130impl Default for AudioConfig {
131    fn default() -> Self {
132        Self {
133            sample_rate: 44100,
134            buffer_size: 1024,
135            enable_output: true,
136            enable_recording: false,
137        }
138    }
139}
140
141/// Combined audio system manager.
142pub struct AudioSystem {
143    /// Audio configuration
144    pub config: AudioConfig,
145    /// Source manager
146    pub sources: SourceManager,
147    /// Binaural microphone
148    pub microphone: Option<BinauralMicrophone>,
149    /// Recording buffer (left channel)
150    recording_left: Vec<f32>,
151    /// Recording buffer (right channel)
152    recording_right: Vec<f32>,
153    /// Is recording active
154    is_recording: bool,
155}
156
157impl AudioSystem {
158    /// Create a new audio system.
159    pub fn new(config: AudioConfig) -> Self {
160        Self {
161            config,
162            sources: SourceManager::new(),
163            microphone: None,
164            recording_left: Vec::new(),
165            recording_right: Vec::new(),
166            is_recording: false,
167        }
168    }
169
170    /// Initialize the binaural microphone.
171    pub fn init_microphone(&mut self, head: VirtualHead, simulation_dt: f32) {
172        self.microphone = Some(BinauralMicrophone::new(
173            head,
174            self.config.sample_rate,
175            simulation_dt,
176        ));
177    }
178
179    /// Add an audio source.
180    pub fn add_source(&mut self, source: AudioSource) -> u32 {
181        self.sources.add(source)
182    }
183
184    /// Create and add an impulse source.
185    pub fn add_impulse(&mut self, position: Position3D, amplitude: f32) -> u32 {
186        self.sources
187            .add(AudioSource::impulse(0, position, amplitude))
188    }
189
190    /// Create and add a tone source.
191    pub fn add_tone(&mut self, position: Position3D, frequency: f32, amplitude: f32) -> u32 {
192        self.sources
193            .add(AudioSource::tone(0, position, frequency, amplitude))
194    }
195
196    /// Start recording.
197    pub fn start_recording(&mut self) {
198        self.recording_left.clear();
199        self.recording_right.clear();
200        self.is_recording = true;
201    }
202
203    /// Stop recording and return the recorded audio.
204    pub fn stop_recording(&mut self) -> (Vec<f32>, Vec<f32>) {
205        self.is_recording = false;
206        (
207            std::mem::take(&mut self.recording_left),
208            std::mem::take(&mut self.recording_right),
209        )
210    }
211
212    /// Process recording (call after microphone capture).
213    pub fn process_recording(&mut self) {
214        if self.is_recording {
215            if let Some(mic) = &mut self.microphone {
216                let (left, right) = mic.get_samples(self.config.buffer_size);
217                self.recording_left.extend(left);
218                self.recording_right.extend(right);
219            }
220        }
221    }
222
223    /// Get the recording sample rate.
224    pub fn sample_rate(&self) -> u32 {
225        self.config.sample_rate
226    }
227
228    /// Reset all audio state.
229    pub fn reset(&mut self) {
230        self.sources.reset_all();
231        if let Some(mic) = &mut self.microphone {
232            mic.clear();
233        }
234        self.recording_left.clear();
235        self.recording_right.clear();
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn test_audio_system_creation() {
245        let config = AudioConfig::default();
246        let system = AudioSystem::new(config);
247
248        assert!(system.sources.is_empty());
249        assert!(system.microphone.is_none());
250    }
251
252    #[test]
253    fn test_add_sources() {
254        let mut system = AudioSystem::new(AudioConfig::default());
255
256        let id1 = system.add_impulse(Position3D::origin(), 1.0);
257        let id2 = system.add_tone(Position3D::new(1.0, 0.0, 0.0), 440.0, 0.5);
258
259        assert_ne!(id1, id2);
260        assert_eq!(system.sources.len(), 2);
261    }
262
263    #[test]
264    fn test_recording() {
265        let mut system = AudioSystem::new(AudioConfig::default());
266
267        system.start_recording();
268        assert!(system.is_recording);
269
270        let (left, right) = system.stop_recording();
271        assert!(!system.is_recording);
272        assert!(left.is_empty());
273        assert!(right.is_empty());
274    }
275}