133 lines
3.7 KiB
Rust
133 lines
3.7 KiB
Rust
|
|
//! 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,
|
||
|
|
}
|