use anyhow::{bail, Context, Result};
pub const G711_CLOCK_HZ: u32 = 8000;
pub const G711_SAMPLES_PER_FRAME: usize = 160;
pub const MAX_DURATION_MS: u64 = 30_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioCodec {
Pcmu,
Pcma,
}
impl AudioCodec {
pub fn parse(value: &str) -> Result<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"pcmu" | "mulaw" | "ulaw" | "g711u" | "0" => Ok(Self::Pcmu),
"pcma" | "alaw" | "g711a" | "8" => Ok(Self::Pcma),
other => bail!(
"unsupported payload_type {other:?}; this engine synthesizes only \"pcmu\" (PT 0) \
or \"pcma\" (PT 8)"
),
}
}
pub fn payload_type(self) -> u8 {
match self {
Self::Pcmu => 0,
Self::Pcma => 8,
}
}
pub fn rtpmap_name(self) -> &'static str {
match self {
Self::Pcmu => "PCMU",
Self::Pcma => "PCMA",
}
}
pub fn encode_sample(self, pcm: i16) -> u8 {
match self {
Self::Pcmu => linear_to_ulaw(pcm),
Self::Pcma => linear_to_alaw(pcm),
}
}
}
#[derive(Debug, Clone)]
pub enum AudioContent {
Tone { hz: f64 },
Dtmf { digits: String },
Silence,
Raw { encoded: Vec<u8> },
}
pub fn parse_audio_content(action: &serde_json::Value) -> Result<AudioContent> {
let kind = action
.get("content")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_ascii_lowercase());
match kind.as_deref() {
Some("tone") | None => {
let hz = action
.get("tone_hz")
.and_then(|v| v.as_f64())
.unwrap_or(440.0);
if !(20.0..=3800.0).contains(&hz) {
bail!("tone_hz {hz} is outside the 20-3800 Hz range representable at 8 kHz");
}
Ok(AudioContent::Tone { hz })
}
Some("dtmf") => {
let digits = action
.get("digits")
.and_then(|v| v.as_str())
.context("content \"dtmf\" requires a \"digits\" string")?
.to_string();
if digits.is_empty() {
bail!("\"digits\" must be non-empty for dtmf content");
}
Ok(AudioContent::Dtmf { digits })
}
Some("silence") => Ok(AudioContent::Silence),
Some("raw") => {
let encoding = action
.get("encoding")
.and_then(|v| v.as_str())
.unwrap_or("hex");
if encoding != "hex" {
bail!(
"raw content encoding must be \"hex\" (got {encoding:?}); raw bytes are only \
accepted hex-encoded and are decoded before framing"
);
}
let hexstr = action
.get("samples")
.and_then(|v| v.as_str())
.context("raw content requires a hex \"samples\" string")?;
let encoded = hex::decode(hexstr.trim())
.context("raw \"samples\" is not valid hex; the engine decodes it for real")?;
if encoded.is_empty() {
bail!("decoded raw \"samples\" is empty");
}
Ok(AudioContent::Raw { encoded })
}
Some(other) => {
bail!("unknown content {other:?}; valid: \"tone\", \"dtmf\", \"silence\", \"raw\"")
}
}
}
pub fn synthesize(codec: AudioCodec, content: &AudioContent, duration_ms: u64) -> Result<Vec<u8>> {
if duration_ms == 0 {
bail!("duration_ms must be greater than zero");
}
if duration_ms > MAX_DURATION_MS {
bail!("duration_ms {duration_ms} exceeds the {MAX_DURATION_MS} ms cap");
}
let total = (G711_CLOCK_HZ as u64 * duration_ms / 1000) as usize;
let pcm: Vec<i16> = match content {
AudioContent::Raw { encoded } => return Ok(encoded.clone()),
AudioContent::Silence => vec![0i16; total],
AudioContent::Tone { hz } => (0..total)
.map(|n| {
let t = n as f64 / G711_CLOCK_HZ as f64;
(8000.0 * (2.0 * std::f64::consts::PI * hz * t).sin()) as i16
})
.collect(),
AudioContent::Dtmf { digits } => synthesize_dtmf(digits),
};
let max_samples = (G711_CLOCK_HZ as u64 * MAX_DURATION_MS / 1000) as usize;
if pcm.len() > max_samples {
bail!(
"dtmf digits synthesize to {} ms, past the {} ms cap ({} digits at {} ms each is the \
most one action may stream)",
pcm.len() * 1000 / G711_CLOCK_HZ as usize,
MAX_DURATION_MS,
max_samples / ((G711_CLOCK_HZ as usize * DTMF_DIGIT_MS) / 1000),
DTMF_DIGIT_MS
);
}
Ok(pcm.into_iter().map(|s| codec.encode_sample(s)).collect())
}
pub const DTMF_DIGIT_MS: usize = 200;
fn synthesize_dtmf(digits: &str) -> Vec<i16> {
let mut out = Vec::new();
let on = (G711_CLOCK_HZ as usize * 150) / 1000;
let off = (G711_CLOCK_HZ as usize * 50) / 1000;
for ch in digits.chars() {
if let Some((low, high)) = dtmf_freqs(ch) {
for n in 0..on {
let t = n as f64 / G711_CLOCK_HZ as f64;
let s = 4000.0
* ((2.0 * std::f64::consts::PI * low * t).sin()
+ (2.0 * std::f64::consts::PI * high * t).sin());
out.push(s as i16);
}
out.extend(std::iter::repeat(0i16).take(off));
}
}
out
}
fn dtmf_freqs(ch: char) -> Option<(f64, f64)> {
let (row, col) = match ch.to_ascii_uppercase() {
'1' => (697.0, 1209.0),
'2' => (697.0, 1336.0),
'3' => (697.0, 1477.0),
'A' => (697.0, 1633.0),
'4' => (770.0, 1209.0),
'5' => (770.0, 1336.0),
'6' => (770.0, 1477.0),
'B' => (770.0, 1633.0),
'7' => (852.0, 1209.0),
'8' => (852.0, 1336.0),
'9' => (852.0, 1477.0),
'C' => (852.0, 1633.0),
'*' => (941.0, 1209.0),
'0' => (941.0, 1336.0),
'#' => (941.0, 1477.0),
'D' => (941.0, 1633.0),
_ => return None,
};
Some((row, col))
}
#[derive(Debug, Clone)]
pub struct RtpPacketizer {
ssrc: u32,
payload_type: u8,
sequence: u16,
timestamp: u32,
}
impl RtpPacketizer {
pub fn new(
ssrc: u32,
payload_type: u8,
initial_seq: Option<u16>,
initial_ts: Option<u32>,
) -> Self {
use rand::Rng;
let mut rng = rand::thread_rng();
Self {
ssrc,
payload_type,
sequence: initial_seq.unwrap_or_else(|| rng.gen()),
timestamp: initial_ts.unwrap_or_else(|| rng.gen()),
}
}
pub fn ssrc(&self) -> u32 {
self.ssrc
}
pub fn next_sequence(&self) -> u16 {
self.sequence
}
pub fn next_timestamp(&self) -> u32 {
self.timestamp
}
pub fn packetize(&mut self, payload: &[u8], samples_per_frame: usize) -> Vec<Vec<u8>> {
let mut packets = Vec::new();
let mut first = true;
for chunk in payload.chunks(samples_per_frame.max(1)) {
let pkt = build_rtp_packet(
self.payload_type,
first,
self.sequence,
self.timestamp,
self.ssrc,
chunk,
);
packets.push(pkt);
self.sequence = self.sequence.wrapping_add(1);
self.timestamp = self.timestamp.wrapping_add(chunk.len() as u32);
first = false;
}
packets
}
}
pub fn build_rtp_packet(
payload_type: u8,
marker: bool,
sequence: u16,
timestamp: u32,
ssrc: u32,
payload: &[u8],
) -> Vec<u8> {
let mut pkt = Vec::with_capacity(12 + payload.len());
pkt.push(0x80);
pkt.push(((marker as u8) << 7) | (payload_type & 0x7F));
pkt.extend_from_slice(&sequence.to_be_bytes());
pkt.extend_from_slice(×tamp.to_be_bytes());
pkt.extend_from_slice(&ssrc.to_be_bytes());
pkt.extend_from_slice(payload);
pkt
}
#[derive(Debug, Clone)]
pub struct ParsedRtp {
pub version: u8,
pub marker: bool,
pub payload_type: u8,
pub sequence: u16,
pub timestamp: u32,
pub ssrc: u32,
pub payload_len: usize,
}
pub fn parse_rtp(data: &[u8]) -> Option<ParsedRtp> {
if data.len() < 12 {
return None;
}
let version = data[0] >> 6;
if version != 2 {
return None;
}
let cc = (data[0] & 0x0F) as usize;
let header_len = 12 + cc * 4;
if data.len() < header_len {
return None;
}
Some(ParsedRtp {
version,
marker: data[1] & 0x80 != 0,
payload_type: data[1] & 0x7F,
sequence: u16::from_be_bytes([data[2], data[3]]),
timestamp: u32::from_be_bytes([data[4], data[5], data[6], data[7]]),
ssrc: u32::from_be_bytes([data[8], data[9], data[10], data[11]]),
payload_len: data.len() - header_len,
})
}
pub fn is_rtcp(data: &[u8]) -> bool {
data.len() >= 2 && data[0] >> 6 == 2 && (200..=204).contains(&data[1])
}
pub fn build_rtcp_sender_report(
ssrc: u32,
rtp_timestamp: u32,
packet_count: u32,
octet_count: u32,
) -> Vec<u8> {
let now = crate::utils::clock::SystemTime::now()
.duration_since(crate::utils::clock::UNIX_EPOCH)
.unwrap_or_default();
let ntp_secs = now.as_secs() + 2_208_988_800;
let ntp_frac = ((now.subsec_nanos() as u64) << 32) / 1_000_000_000;
let mut pkt = Vec::with_capacity(28);
pkt.push(0x80); pkt.push(200); pkt.extend_from_slice(&6u16.to_be_bytes()); pkt.extend_from_slice(&ssrc.to_be_bytes());
pkt.extend_from_slice(&(ntp_secs as u32).to_be_bytes());
pkt.extend_from_slice(&(ntp_frac as u32).to_be_bytes());
pkt.extend_from_slice(&rtp_timestamp.to_be_bytes());
pkt.extend_from_slice(&packet_count.to_be_bytes());
pkt.extend_from_slice(&octet_count.to_be_bytes());
pkt
}
pub fn linear_to_ulaw(sample: i16) -> u8 {
const BIAS: i32 = 0x84;
const CLIP: i32 = 32635;
let mut pcm = sample as i32;
let sign = if pcm < 0 {
pcm = -pcm;
0x80
} else {
0
};
if pcm > CLIP {
pcm = CLIP;
}
pcm += BIAS;
let mut exponent = 7;
let mut mask = 0x4000;
while exponent > 0 && (pcm & mask) == 0 {
exponent -= 1;
mask >>= 1;
}
let mantissa = (pcm >> (exponent + 3)) & 0x0F;
let ulaw = !(sign | (exponent << 4) | mantissa);
(ulaw & 0xFF) as u8
}
pub fn linear_to_alaw(sample: i16) -> u8 {
let mut pcm = sample as i32;
let sign = if pcm >= 0 { 0x80 } else { 0 };
if sign == 0 {
pcm = -pcm;
}
if pcm > 32635 {
pcm = 32635;
}
let (exponent, mantissa) = if pcm >= 256 {
let mut exponent = 7;
let mut mask = 0x4000;
while exponent > 1 && (pcm & mask) == 0 {
exponent -= 1;
mask >>= 1;
}
(exponent, (pcm >> (exponent + 3)) & 0x0F)
} else {
(0, (pcm >> 4) & 0x0F)
};
let alaw = (sign | (exponent << 4) | mantissa) as u8;
alaw ^ 0x55
}