//! Audio recorder — receives RTP packets and writes a WAV file. use codec_lib::TranscodeState; use std::path::Path; /// Active recording session. pub struct Recorder { writer: hound::WavWriter>, transcoder: TranscodeState, source_pt: u8, total_samples: u64, sample_rate: u32, max_samples: Option, file_path: String, } impl Recorder { /// Create a new recorder that writes to a WAV file. /// `source_pt` is the RTP payload type of the incoming audio. /// `max_duration_ms` optionally limits the recording length. pub fn new( file_path: &str, source_pt: u8, max_duration_ms: Option, ) -> Result { // Ensure parent directory exists. if let Some(parent) = Path::new(file_path).parent() { std::fs::create_dir_all(parent) .map_err(|e| format!("create dir: {e}"))?; } let sample_rate = 8000u32; // Record at 8kHz (standard telephony) let spec = hound::WavSpec { channels: 1, sample_rate, bits_per_sample: 16, sample_format: hound::SampleFormat::Int, }; let writer = hound::WavWriter::create(file_path, spec) .map_err(|e| format!("create WAV {file_path}: {e}"))?; let transcoder = TranscodeState::new().map_err(|e| format!("codec init: {e}"))?; let max_samples = max_duration_ms.map(|ms| (sample_rate as u64 * ms) / 1000); Ok(Self { writer, transcoder, source_pt, total_samples: 0, sample_rate, max_samples, file_path: file_path.to_string(), }) } /// Process an incoming RTP packet (full packet with header). /// Returns true if recording should continue, false if max duration reached. pub fn process_rtp(&mut self, data: &[u8]) -> bool { if data.len() <= 12 { return true; // Too short, skip. } let pt = data[1] & 0x7F; // Skip telephone-event (DTMF) packets. if pt == 101 { return true; } let payload = &data[12..]; if payload.is_empty() { return true; } // Decode to PCM. let (pcm, rate) = match self.transcoder.decode_to_pcm(payload, self.source_pt) { Ok(result) => result, Err(_) => return true, // Decode failed, skip packet. }; // Resample to 8kHz if needed. let pcm_8k = if rate != self.sample_rate { match self.transcoder.resample(&pcm, rate, self.sample_rate) { Ok(r) => r, Err(_) => return true, } } else { pcm }; // Write samples. for &sample in &pcm_8k { if let Err(_) = self.writer.write_sample(sample) { return false; } self.total_samples += 1; if let Some(max) = self.max_samples { if self.total_samples >= max { return false; // Max duration reached. } } } true } /// Stop recording and finalize the WAV file. pub fn stop(self) -> RecordingResult { let duration_ms = if self.sample_rate > 0 { (self.total_samples * 1000) / self.sample_rate as u64 } else { 0 }; // Writer is finalized on drop (writes RIFF header sizes). drop(self.writer); RecordingResult { file_path: self.file_path, duration_ms, total_samples: self.total_samples, } } } pub struct RecordingResult { pub file_path: String, pub duration_ms: u64, pub total_samples: u64, }