use std::path::Path; use kon_core::error::{KonError, Result}; use kon_core::types::AudioSamples; /// Write f32 PCM samples to a 16-bit WAV file. pub fn write_wav(path: &Path, audio: &AudioSamples) -> Result<()> { let spec = hound::WavSpec { channels: audio.channels(), sample_rate: audio.sample_rate(), bits_per_sample: 16, sample_format: hound::SampleFormat::Int, }; let mut writer = hound::WavWriter::create(path, spec) .map_err(|e| KonError::Io(std::io::Error::other(format!("WAV create failed: {e}"))))?; for &sample in audio.samples() { let clamped = sample.clamp(-1.0, 1.0); let int_sample = (clamped * i16::MAX as f32) as i16; writer .write_sample(int_sample) .map_err(|e| KonError::Io(std::io::Error::other(format!("WAV write failed: {e}"))))?; } writer .finalize() .map_err(|e| KonError::Io(std::io::Error::other(format!("WAV finalize failed: {e}"))))?; Ok(()) } /// Read a WAV file to f32 PCM AudioSamples. pub fn read_wav(path: &Path) -> Result { let reader = hound::WavReader::open(path) .map_err(|e| KonError::AudioDecodeFailed(format!("WAV open failed: {e}")))?; let spec = reader.spec(); let sample_rate = spec.sample_rate; let channels = spec.channels; let samples: Vec = match spec.sample_format { hound::SampleFormat::Int => reader .into_samples::() .filter_map(|s| s.ok()) .map(|s| s as f32 / (1 << (spec.bits_per_sample - 1)) as f32) .collect(), hound::SampleFormat::Float => reader .into_samples::() .filter_map(|s| s.ok()) .collect(), }; Ok(AudioSamples::new(samples, sample_rate, channels)) } #[cfg(test)] mod tests { use super::*; #[test] fn wav_roundtrip() { let temp_dir = std::env::temp_dir(); let path = temp_dir.join("kon_test_roundtrip.wav"); let original = AudioSamples::mono_16khz(vec![0.0, 0.5, -0.5, 0.25, -0.25]); write_wav(&path, &original).unwrap(); let loaded = read_wav(&path).unwrap(); assert_eq!(loaded.sample_rate(), 16000); assert_eq!(loaded.samples().len(), 5); // 16-bit quantisation introduces small error for (a, b) in original.samples().iter().zip(loaded.samples().iter()) { assert!( (a - b).abs() < 0.001, "Sample mismatch: original={a}, loaded={b}" ); } std::fs::remove_file(&path).ok(); } }