use crate::error::{CodecError, Result};
use crate::types::{AudioCodec, AudioCodecExt, CodecConfig, CodecInfo, CodecType};
mod impls;
use self::impls as g729_impl;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum G729Profile {
AnnexA,
AnnexAB,
}
impl G729Profile {
const fn name(self) -> &'static str {
match self {
Self::AnnexA => "G729A",
Self::AnnexAB => "G729BA",
}
}
const fn annex_b(self) -> bool {
matches!(self, Self::AnnexAB)
}
}
pub struct G729Codec {
encoder: g729_impl::G729Encoder,
decoder: g729_impl::G729Decoder,
profile: G729Profile,
}
impl G729Codec {
pub const FRAME_SAMPLES: usize = g729_impl::FRAME_SAMPLES;
pub const MAX_ENCODED_BYTES: usize = g729_impl::SPEECH_FRAME_BYTES;
pub fn new(config: CodecConfig) -> Result<Self> {
config.validate()?;
validate_g729_format(&config)?;
let profile = profile_from_config(&config)?;
let g729_config = g729_impl::G729Config {
annex_b: profile.annex_b(),
};
Ok(Self {
encoder: g729_impl::G729Encoder::new(g729_config),
decoder: g729_impl::G729Decoder::new(g729_config),
profile,
})
}
pub fn annex_b_enabled(&self) -> bool {
self.profile.annex_b()
}
}
impl AudioCodec for G729Codec {
fn encode(&mut self, samples: &[i16]) -> Result<Vec<u8>> {
let mut output = [0u8; Self::MAX_ENCODED_BYTES];
let written = self.encode_to_buffer(samples, &mut output)?;
Ok(output[..written].to_vec())
}
fn decode(&mut self, data: &[u8]) -> Result<Vec<i16>> {
let mut output = [0i16; Self::FRAME_SAMPLES];
let written = self.decode_to_buffer(data, &mut output)?;
Ok(output[..written].to_vec())
}
fn info(&self) -> CodecInfo {
CodecInfo {
name: self.profile.name(),
sample_rate: 8000,
channels: 1,
bitrate: 8000,
frame_size: Self::FRAME_SAMPLES,
payload_type: Some(18),
}
}
fn reset(&mut self) -> Result<()> {
let g729_config = g729_impl::G729Config {
annex_b: self.profile.annex_b(),
};
self.encoder = g729_impl::G729Encoder::new(g729_config);
self.decoder = g729_impl::G729Decoder::new(g729_config);
Ok(())
}
fn frame_size(&self) -> usize {
Self::FRAME_SAMPLES
}
}
impl AudioCodecExt for G729Codec {
fn encode_to_buffer(&mut self, samples: &[i16], output: &mut [u8]) -> Result<usize> {
if samples.len() != Self::FRAME_SAMPLES {
return Err(CodecError::InvalidFrameSize {
expected: Self::FRAME_SAMPLES,
actual: samples.len(),
});
}
if output.len() < Self::MAX_ENCODED_BYTES {
return Err(CodecError::BufferTooSmall {
needed: Self::MAX_ENCODED_BYTES,
actual: output.len(),
});
}
let mut frame = [0i16; Self::FRAME_SAMPLES];
frame.copy_from_slice(samples);
let mut encoded = [0u8; Self::MAX_ENCODED_BYTES];
let frame_type = self.encoder.encode(&frame, &mut encoded);
let byte_len = frame_type.byte_len();
output[..byte_len].copy_from_slice(&encoded[..byte_len]);
Ok(byte_len)
}
fn decode_to_buffer(&mut self, data: &[u8], output: &mut [i16]) -> Result<usize> {
if output.len() < Self::FRAME_SAMPLES {
return Err(CodecError::BufferTooSmall {
needed: Self::FRAME_SAMPLES,
actual: output.len(),
});
}
let decoded = self.decoder.decode_frame(data).map_err(map_g729_error)?;
output[..Self::FRAME_SAMPLES].copy_from_slice(&decoded);
Ok(Self::FRAME_SAMPLES)
}
fn max_encoded_size(&self, input_samples: usize) -> usize {
if input_samples == 0 {
0
} else {
input_samples.div_ceil(Self::FRAME_SAMPLES) * Self::MAX_ENCODED_BYTES
}
}
fn max_decoded_size(&self, input_bytes: usize) -> usize {
if matches!(
input_bytes,
0 | g729_impl::SID_FRAME_BYTES | g729_impl::SPEECH_FRAME_BYTES
) {
Self::FRAME_SAMPLES
} else {
0
}
}
}
fn validate_g729_format(config: &CodecConfig) -> Result<()> {
if config.sample_rate.hz() != 8000 {
return Err(CodecError::InvalidSampleRate {
rate: config.sample_rate.hz(),
supported: vec![8000],
});
}
if config.channels != 1 {
return Err(CodecError::InvalidChannelCount {
channels: config.channels,
supported: vec![1],
});
}
if let Some(frame_size_ms) = config.frame_size_ms {
if !frame_size_ms.is_finite() || frame_size_ms <= 0.0 {
return Err(CodecError::invalid_config(
"G.729 frame size must be a positive finite duration",
));
}
let frame_samples = (8.0 * frame_size_ms).round() as usize;
if frame_samples != G729Codec::FRAME_SAMPLES {
return Err(CodecError::InvalidFrameSize {
expected: G729Codec::FRAME_SAMPLES,
actual: frame_samples,
});
}
}
Ok(())
}
fn profile_from_config(config: &CodecConfig) -> Result<G729Profile> {
let params = &config.parameters.g729;
#[allow(deprecated)]
let annex_a_enabled = params.annex_a && params.reduced_complexity;
if !annex_a_enabled {
return Err(CodecError::invalid_config(
"Full-complexity base G.729 is not implemented; use Annex A",
));
}
#[allow(deprecated)]
let annex_b_enabled = params.annex_b && params.vad_enabled && params.cng_enabled;
match config.codec_type {
CodecType::G729A => Ok(G729Profile::AnnexA),
CodecType::G729BA => {
if annex_b_enabled {
Ok(G729Profile::AnnexAB)
} else {
Err(CodecError::invalid_config(
"G729BA requires Annex B; use G729A for Annex A-only operation",
))
}
}
CodecType::G729 => Ok(if annex_b_enabled {
G729Profile::AnnexAB
} else {
G729Profile::AnnexA
}),
codec_type => Err(CodecError::unsupported_codec(codec_type.name())),
}
}
fn map_g729_error(error: g729_impl::CodecError) -> CodecError {
match error {
g729_impl::CodecError::InvalidPcmLength { expected, got } => CodecError::InvalidFrameSize {
expected,
actual: got,
},
g729_impl::CodecError::InvalidBitstreamLength { expected, got } => {
CodecError::InvalidPayload {
details: format!(
"Invalid G.729 bitstream length: expected {expected:?}, got {got}"
),
}
}
g729_impl::CodecError::InvalidParameterLength { expected, got } => {
CodecError::InvalidPayload {
details: format!("Invalid G.729 parameter length: expected {expected}, got {got}"),
}
}
g729_impl::CodecError::InvalidFrameType { got } => CodecError::InvalidPayload {
details: format!("Invalid G.729 frame type code: {got}"),
},
g729_impl::CodecError::IoUnavailable => {
CodecError::decoding_failed("G.729 I/O helpers unavailable")
}
g729_impl::CodecError::BackendFailure => {
CodecError::decoding_failed("G.729 backend failed")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codecs::CodecFactory;
use crate::types::{AudioCodec, SampleRate};
const SAMPLE_RATE_HZ: f64 = 8_000.0;
const TONE_HZ: f64 = 300.0;
fn speech_samples() -> Vec<i16> {
(0..G729Codec::FRAME_SAMPLES)
.map(|sample| (sample * 128) as i16)
.collect()
}
fn tone_samples(frame_count: usize) -> Vec<i16> {
(0..G729Codec::FRAME_SAMPLES * frame_count)
.map(|sample| {
let t = sample as f64 / SAMPLE_RATE_HZ;
(10_000.0 * (2.0 * std::f64::consts::PI * TONE_HZ * t).sin()).round() as i16
})
.collect()
}
fn round_trip_frames(codec: &mut G729Codec, samples: &[i16]) -> (Vec<i16>, Vec<usize>) {
let mut decoded = Vec::with_capacity(samples.len());
let mut encoded_lengths = Vec::with_capacity(samples.len() / G729Codec::FRAME_SAMPLES);
for frame in samples.chunks_exact(G729Codec::FRAME_SAMPLES) {
let encoded = codec.encode(frame).unwrap();
encoded_lengths.push(encoded.len());
decoded.extend(codec.decode(&encoded).unwrap());
}
(decoded, encoded_lengths)
}
fn correlation(a: &[i16], b: &[i16]) -> f64 {
assert_eq!(a.len(), b.len());
let mean_a = a.iter().map(|&sample| f64::from(sample)).sum::<f64>() / a.len() as f64;
let mean_b = b.iter().map(|&sample| f64::from(sample)).sum::<f64>() / b.len() as f64;
let mut covariance = 0.0;
let mut variance_a = 0.0;
let mut variance_b = 0.0;
for (&sample_a, &sample_b) in a.iter().zip(b) {
let centered_a = f64::from(sample_a) - mean_a;
let centered_b = f64::from(sample_b) - mean_b;
covariance += centered_a * centered_b;
variance_a += centered_a * centered_a;
variance_b += centered_b * centered_b;
}
covariance / (variance_a.sqrt() * variance_b.sqrt())
}
fn rms(samples: &[i16]) -> f64 {
let power = samples
.iter()
.map(|&sample| f64::from(sample).powi(2))
.sum::<f64>()
/ samples.len() as f64;
power.sqrt()
}
#[test]
fn g729a_encodes_fixed_speech_frame() {
let config = CodecConfig::new(CodecType::G729A)
.with_sample_rate(SampleRate::Rate8000)
.with_channels(1);
let mut codec = G729Codec::new(config).unwrap();
let encoded = codec.encode(&speech_samples()).unwrap();
assert_eq!(encoded.len(), g729_impl::SPEECH_FRAME_BYTES);
assert!(!codec.annex_b_enabled());
let decoded = codec.decode(&encoded).unwrap();
assert_eq!(decoded.len(), G729Codec::FRAME_SAMPLES);
}
#[test]
fn g729a_tone_round_trip_preserves_audible_signal() {
let config = CodecConfig::new(CodecType::G729A)
.with_sample_rate(SampleRate::Rate8000)
.with_channels(1);
let mut codec = G729Codec::new(config).unwrap();
let input = tone_samples(50);
let (decoded, encoded_lengths) = round_trip_frames(&mut codec, &input);
assert_eq!(decoded.len(), input.len());
assert!(encoded_lengths.iter().all(|&len| len == 10));
let warmup = G729Codec::FRAME_SAMPLES * 5;
let corr = correlation(&input[warmup..], &decoded[warmup..]);
let decoded_rms = rms(&decoded[warmup..]);
assert!(
corr.abs() > 0.80,
"G.729A 300 Hz tone correlation too low after round trip: {corr:.4}"
);
assert!(
decoded_rms > 500.0,
"G.729A 300 Hz tone degraded to near-silence: RMS {decoded_rms:.2}"
);
}
#[test]
fn g729ba_tone_round_trip_preserves_audible_signal() {
let config = CodecConfig::new(CodecType::G729BA)
.with_sample_rate(SampleRate::Rate8000)
.with_channels(1);
let mut codec = G729Codec::new(config).unwrap();
let input = tone_samples(50);
let (decoded, encoded_lengths) = round_trip_frames(&mut codec, &input);
assert_eq!(decoded.len(), input.len());
assert!(encoded_lengths.iter().any(|&len| len == 10));
let warmup = G729Codec::FRAME_SAMPLES * 5;
let corr = correlation(&input[warmup..], &decoded[warmup..]);
let decoded_rms = rms(&decoded[warmup..]);
assert!(
corr.abs() > 0.70,
"G.729BA 300 Hz tone correlation too low after round trip: {corr:.4}"
);
assert!(
decoded_rms > 500.0,
"G.729BA 300 Hz tone degraded to near-silence: RMS {decoded_rms:.2}"
);
}
#[test]
fn g729ba_accepts_sid_and_nodata_payloads() {
let config = CodecConfig::new(CodecType::G729BA)
.with_sample_rate(SampleRate::Rate8000)
.with_channels(1);
let mut codec = G729Codec::new(config).unwrap();
assert!(codec.annex_b_enabled());
let decoded_nodata = codec.decode(&[]).unwrap();
assert_eq!(decoded_nodata.len(), G729Codec::FRAME_SAMPLES);
let sid = [0u8; g729_impl::SID_FRAME_BYTES];
let decoded_sid = codec.decode(&sid).unwrap();
assert_eq!(decoded_sid.len(), G729Codec::FRAME_SAMPLES);
}
#[test]
fn g729_rejects_full_complexity_request() {
let mut config = CodecConfig::new(CodecType::G729);
config.parameters.g729.annex_a = false;
let error = match G729Codec::new(config) {
Ok(_) => panic!("full-complexity G.729 config should be rejected"),
Err(error) => error,
};
assert!(matches!(error, CodecError::InvalidConfig { .. }));
}
#[test]
#[allow(deprecated)]
fn g729_rejects_legacy_full_complexity_request() {
let mut config = CodecConfig::new(CodecType::G729);
config.parameters.g729.reduced_complexity = false;
let error = match G729Codec::new(config) {
Ok(_) => panic!("legacy full-complexity G.729 config should be rejected"),
Err(error) => error,
};
assert!(matches!(error, CodecError::InvalidConfig { .. }));
}
#[test]
fn g729_validates_frame_size() {
let config = CodecConfig::new(CodecType::G729A).with_frame_size_ms(20.0);
let error = match G729Codec::new(config) {
Ok(_) => panic!("20 ms G.729 frames should be rejected"),
Err(error) => error,
};
assert!(matches!(error, CodecError::InvalidFrameSize { .. }));
}
#[test]
fn factory_supports_g729_names_and_payload_type() {
assert!(CodecFactory::is_supported("G729"));
assert!(CodecFactory::is_supported("G.729"));
assert!(CodecFactory::is_supported("G729A"));
assert!(CodecFactory::is_supported("G729AB"));
assert!(CodecFactory::is_supported("G729BA"));
let codec =
CodecFactory::create_by_name("G729A", CodecConfig::new(CodecType::G729)).unwrap();
assert_eq!(codec.info().name, "G729A");
let codec =
CodecFactory::create_by_name("G729AB", CodecConfig::new(CodecType::G729)).unwrap();
assert_eq!(codec.info().name, "G729BA");
let codec =
CodecFactory::create_by_payload_type(18, CodecConfig::new(CodecType::G729)).unwrap();
assert_eq!(codec.info().payload_type, Some(18));
}
#[test]
fn reset_preserves_selected_profile() {
let config = CodecConfig::new(CodecType::G729A);
let mut codec = G729Codec::new(config).unwrap();
codec.reset().unwrap();
let encoded = codec.encode(&speech_samples()).unwrap();
assert_eq!(encoded.len(), g729_impl::SPEECH_FRAME_BYTES);
assert!(!codec.annex_b_enabled());
}
}