acoustic_ofdm/
wav_io.rs

1// Copyright (c) 2026 Elias S. G. Carotti
2
3use std::error::Error;
4use std::path::Path;
5
6/// Saves mono PCM samples to a 16-bit WAV file.
7///
8/// Parameters:
9/// - `path`: destination WAV path.
10/// - `samples`: normalized mono samples (typically in `[-1, 1]`).
11/// - `sample_rate`: sample rate in Hz.
12/// Returns:
13/// - `Result<(), Box<dyn Error>>`: `Ok(())` on success.
14pub fn save_wav_mono_i16(
15    path: &Path,
16    samples: &[f32],
17    sample_rate: u32,
18) -> Result<(), Box<dyn Error>> {
19    let spec = hound::WavSpec {
20        channels: 1,
21        sample_rate,
22        bits_per_sample: 16,
23        sample_format: hound::SampleFormat::Int,
24    };
25    let mut wr = hound::WavWriter::create(path, spec)?;
26    for &s in samples {
27        let v = (s.clamp(-1.0, 1.0) * (i16::MAX as f32)).round() as i16;
28        wr.write_sample(v)?;
29    }
30    wr.finalize()?;
31    Ok(())
32}
33
34/// Loads a mono WAV file as normalized `f32` samples.
35///
36/// Parameters:
37/// - `path`: source WAV path.
38/// Returns:
39/// - `Result<(Vec<f32>, u32), Box<dyn Error>>`: `(samples, sample_rate)` on success.
40pub fn load_wav_mono_f32(path: &Path) -> Result<(Vec<f32>, u32), Box<dyn Error>> {
41    let mut rd = hound::WavReader::open(path)?;
42    let spec = rd.spec();
43    if spec.channels != 1 {
44        return Err("only mono WAV is supported".into());
45    }
46    let sr = spec.sample_rate;
47    let samples = match (spec.sample_format, spec.bits_per_sample) {
48        (hound::SampleFormat::Int, 16) => rd
49            .samples::<i16>()
50            .map(|r| r.map(|v| v as f32 / i16::MAX as f32))
51            .collect::<Result<Vec<_>, _>>()?,
52        _ => return Err("unsupported WAV format (expected 16-bit PCM mono)".into()),
53    };
54    Ok((samples, sr))
55}
56
57#[cfg(test)]
58mod tests {
59    use super::{load_wav_mono_f32, save_wav_mono_i16};
60    use std::path::PathBuf;
61    use std::time::{SystemTime, UNIX_EPOCH};
62
63    /// Builds a temporary path for WAV tests.
64    ///
65    /// Parameters:
66    /// - none.
67    /// Returns:
68    /// - `PathBuf`: temporary WAV file path.
69    fn tmp_wav_path() -> PathBuf {
70        let ts = SystemTime::now()
71            .duration_since(UNIX_EPOCH)
72            .expect("clock drift")
73            .as_nanos();
74        std::env::temp_dir().join(format!("acoustic_ofdm_wav_test_{ts}.wav"))
75    }
76
77    #[test]
78    /// Verifies WAV write/read round-trip.
79    fn wav_roundtrip_mono_i16() {
80        let path = tmp_wav_path();
81        let src = vec![0.0f32, 0.25, -0.25, 0.9, -0.9, 0.1, -0.1];
82        save_wav_mono_i16(&path, &src, 48_000).expect("write wav");
83        let (dst, sr) = load_wav_mono_f32(&path).expect("read wav");
84        assert_eq!(sr, 48_000);
85        assert_eq!(dst.len(), src.len());
86        for (a, b) in src.iter().zip(dst.iter()) {
87            assert!((a - b).abs() < 2.0 / i16::MAX as f32);
88        }
89        let _ = std::fs::remove_file(path);
90    }
91}
92
93// vim: set ts=4 sw=4 et: