feat(proxy-engine): add Rust-based outbound calling, WebRTC bridging, and voicemail handling
This commit is contained in:
173
rust/crates/proxy-engine/src/audio_player.rs
Normal file
173
rust/crates/proxy-engine/src/audio_player.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
//! Audio player — reads a WAV file and streams it as RTP packets.
|
||||
|
||||
use crate::rtp::{build_rtp_header, rtp_clock_increment};
|
||||
use codec_lib::{codec_sample_rate, TranscodeState};
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::time::{self, Duration};
|
||||
|
||||
/// Play a WAV file as RTP to a destination.
|
||||
/// Returns when playback is complete.
|
||||
pub async fn play_wav_file(
|
||||
file_path: &str,
|
||||
socket: Arc<UdpSocket>,
|
||||
dest: SocketAddr,
|
||||
codec_pt: u8,
|
||||
ssrc: u32,
|
||||
) -> Result<u32, String> {
|
||||
let path = Path::new(file_path);
|
||||
if !path.exists() {
|
||||
return Err(format!("WAV file not found: {file_path}"));
|
||||
}
|
||||
|
||||
// Read WAV file.
|
||||
let mut reader =
|
||||
hound::WavReader::open(path).map_err(|e| format!("open WAV {file_path}: {e}"))?;
|
||||
let spec = reader.spec();
|
||||
let wav_rate = spec.sample_rate;
|
||||
|
||||
// Read all samples as i16.
|
||||
let samples: Vec<i16> = if spec.bits_per_sample == 16 {
|
||||
reader
|
||||
.samples::<i16>()
|
||||
.filter_map(|s| s.ok())
|
||||
.collect()
|
||||
} else if spec.bits_per_sample == 32 && spec.sample_format == hound::SampleFormat::Float {
|
||||
reader
|
||||
.samples::<f32>()
|
||||
.filter_map(|s| s.ok())
|
||||
.map(|s| (s * 32767.0).round().clamp(-32768.0, 32767.0) as i16)
|
||||
.collect()
|
||||
} else {
|
||||
return Err(format!(
|
||||
"unsupported WAV format: {}bit {:?}",
|
||||
spec.bits_per_sample, spec.sample_format
|
||||
));
|
||||
};
|
||||
|
||||
if samples.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
// Create codec state for encoding.
|
||||
let mut transcoder = TranscodeState::new().map_err(|e| format!("codec init: {e}"))?;
|
||||
|
||||
// Resample to target codec rate.
|
||||
let target_rate = codec_sample_rate(codec_pt);
|
||||
let resampled = if wav_rate != target_rate {
|
||||
transcoder
|
||||
.resample(&samples, wav_rate, target_rate)
|
||||
.map_err(|e| format!("resample: {e}"))?
|
||||
} else {
|
||||
samples
|
||||
};
|
||||
|
||||
// Calculate frame size (20ms of audio at target rate).
|
||||
let frame_samples = (target_rate as usize) / 50; // 20ms = 1/50 second
|
||||
|
||||
// Stream as RTP at 20ms intervals.
|
||||
let mut seq: u16 = 0;
|
||||
let mut ts: u32 = 0;
|
||||
let mut offset = 0;
|
||||
let mut interval = time::interval(Duration::from_millis(20));
|
||||
let mut frames_sent = 0u32;
|
||||
|
||||
while offset < resampled.len() {
|
||||
interval.tick().await;
|
||||
|
||||
let end = (offset + frame_samples).min(resampled.len());
|
||||
let frame = &resampled[offset..end];
|
||||
|
||||
// Pad short final frame with silence.
|
||||
let frame_data = if frame.len() < frame_samples {
|
||||
let mut padded = frame.to_vec();
|
||||
padded.resize(frame_samples, 0);
|
||||
padded
|
||||
} else {
|
||||
frame.to_vec()
|
||||
};
|
||||
|
||||
// Encode to target codec.
|
||||
let encoded = match transcoder.encode_from_pcm(&frame_data, codec_pt) {
|
||||
Ok(e) if !e.is_empty() => e,
|
||||
_ => {
|
||||
offset += frame_samples;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Build RTP packet.
|
||||
let header = build_rtp_header(codec_pt, seq, ts, ssrc);
|
||||
let mut packet = header.to_vec();
|
||||
packet.extend_from_slice(&encoded);
|
||||
|
||||
let _ = socket.send_to(&packet, dest).await;
|
||||
|
||||
seq = seq.wrapping_add(1);
|
||||
ts = ts.wrapping_add(rtp_clock_increment(codec_pt));
|
||||
offset += frame_samples;
|
||||
frames_sent += 1;
|
||||
}
|
||||
|
||||
Ok(frames_sent)
|
||||
}
|
||||
|
||||
/// Generate and play a beep tone (sine wave) as RTP.
|
||||
pub async fn play_beep(
|
||||
socket: Arc<UdpSocket>,
|
||||
dest: SocketAddr,
|
||||
codec_pt: u8,
|
||||
ssrc: u32,
|
||||
start_seq: u16,
|
||||
start_ts: u32,
|
||||
freq_hz: u32,
|
||||
duration_ms: u32,
|
||||
) -> Result<(u16, u32), String> {
|
||||
let mut transcoder = TranscodeState::new().map_err(|e| format!("codec init: {e}"))?;
|
||||
let target_rate = codec_sample_rate(codec_pt);
|
||||
let frame_samples = (target_rate as usize) / 50;
|
||||
let total_samples = (target_rate as usize * duration_ms as usize) / 1000;
|
||||
|
||||
// Generate sine wave.
|
||||
let amplitude = 16000i16;
|
||||
let sine: Vec<i16> = (0..total_samples)
|
||||
.map(|i| {
|
||||
let t = i as f64 / target_rate as f64;
|
||||
(amplitude as f64 * (2.0 * std::f64::consts::PI * freq_hz as f64 * t).sin()) as i16
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut seq = start_seq;
|
||||
let mut ts = start_ts;
|
||||
let mut offset = 0;
|
||||
let mut interval = time::interval(Duration::from_millis(20));
|
||||
|
||||
while offset < sine.len() {
|
||||
interval.tick().await;
|
||||
|
||||
let end = (offset + frame_samples).min(sine.len());
|
||||
let mut frame = sine[offset..end].to_vec();
|
||||
frame.resize(frame_samples, 0);
|
||||
|
||||
let encoded = match transcoder.encode_from_pcm(&frame, codec_pt) {
|
||||
Ok(e) if !e.is_empty() => e,
|
||||
_ => {
|
||||
offset += frame_samples;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let header = build_rtp_header(codec_pt, seq, ts, ssrc);
|
||||
let mut packet = header.to_vec();
|
||||
packet.extend_from_slice(&encoded);
|
||||
let _ = socket.send_to(&packet, dest).await;
|
||||
|
||||
seq = seq.wrapping_add(1);
|
||||
ts = ts.wrapping_add(rtp_clock_increment(codec_pt));
|
||||
offset += frame_samples;
|
||||
}
|
||||
|
||||
Ok((seq, ts))
|
||||
}
|
||||
Reference in New Issue
Block a user