Files
siprouter/rust/crates/proxy-engine/src/recorder.rs

133 lines
3.7 KiB
Rust
Raw Normal View History

//! 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<std::io::BufWriter<std::fs::File>>,
transcoder: TranscodeState,
source_pt: u8,
total_samples: u64,
sample_rate: u32,
max_samples: Option<u64>,
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<u64>,
) -> Result<Self, String> {
// 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,
}