/// Incremental WAV file writer for streaming audio to disk use std::fs::File; use std::io::{self, Seek, SeekFrom, Write}; use std::path::Path; /// WAV file writer that supports incremental writing pub struct WavWriter { file: File, sample_rate: u32, channels: u32, frames_written: usize, } impl WavWriter { /// Create a new WAV file and write initial header /// The header is written with placeholder sizes that will be updated on finalization pub fn create(path: impl AsRef, sample_rate: u32, channels: u32) -> io::Result { let mut file = File::create(path)?; // Write initial WAV header with placeholder sizes write_wav_header(&mut file, sample_rate, channels, 0)?; Ok(Self { file, sample_rate, channels, frames_written: 0, }) } /// Append audio samples to the file /// Expects interleaved f32 samples in range [-1.0, 1.0] pub fn write_samples(&mut self, samples: &[f32]) -> io::Result<()> { // Convert f32 samples to 16-bit PCM let pcm_data: Vec = samples .iter() .flat_map(|&sample| { let clamped = sample.clamp(-1.0, 1.0); let pcm_value = (clamped * 32767.0) as i16; pcm_value.to_le_bytes() }) .collect(); self.file.write_all(&pcm_data)?; self.frames_written += samples.len() / self.channels as usize; Ok(()) } /// Get the current number of frames written pub fn frames_written(&self) -> usize { self.frames_written } /// Get the current duration in seconds pub fn duration(&self) -> f64 { self.frames_written as f64 / self.sample_rate as f64 } /// Finalize the WAV file by updating the header with correct sizes pub fn finalize(mut self) -> io::Result<()> { // Flush any remaining data self.file.flush()?; // Calculate total data size let data_size = self.frames_written * self.channels as usize * 2; // 2 bytes per sample (16-bit) // WAV file structure: // RIFF header (12 bytes): "RIFF" + size + "WAVE" // fmt chunk (24 bytes): "fmt " + size + format data // data chunk header (8 bytes): "data" + size // Total header = 44 bytes // RIFF chunk size = everything after offset 8 = 4 (WAVE) + 24 (fmt) + 8 (data header) + data_size let riff_chunk_size = 36 + data_size; // 36 = size from "WAVE" to end of data chunk header // Seek to RIFF chunk size (offset 4) self.file.seek(SeekFrom::Start(4))?; self.file.write_all(&(riff_chunk_size as u32).to_le_bytes())?; // Seek to data chunk size (offset 40) self.file.seek(SeekFrom::Start(40))?; self.file.write_all(&(data_size as u32).to_le_bytes())?; // Flush and sync to ensure all data is written to disk before file is closed self.file.flush()?; self.file.sync_all()?; Ok(()) } } /// Write WAV header with specified parameters fn write_wav_header(file: &mut File, sample_rate: u32, channels: u32, frames: usize) -> io::Result<()> { let bytes_per_sample = 2u16; // 16-bit PCM let data_size = (frames * channels as usize * bytes_per_sample as usize) as u32; // RIFF chunk size = everything after offset 8 // = 4 (WAVE) + 24 (fmt chunk) + 8 (data chunk header) + data_size let riff_chunk_size = 36 + data_size; // RIFF header file.write_all(b"RIFF")?; file.write_all(&riff_chunk_size.to_le_bytes())?; file.write_all(b"WAVE")?; // fmt chunk file.write_all(b"fmt ")?; file.write_all(&16u32.to_le_bytes())?; // fmt chunk size file.write_all(&1u16.to_le_bytes())?; // PCM format file.write_all(&(channels as u16).to_le_bytes())?; file.write_all(&sample_rate.to_le_bytes())?; let byte_rate = sample_rate * channels * bytes_per_sample as u32; file.write_all(&byte_rate.to_le_bytes())?; let block_align = channels as u16 * bytes_per_sample; file.write_all(&block_align.to_le_bytes())?; file.write_all(&(bytes_per_sample * 8).to_le_bytes())?; // bits per sample // data chunk header file.write_all(b"data")?; file.write_all(&data_size.to_le_bytes())?; Ok(()) }